# Batch Trace Processor

_The Batch Trace Processor is a Python library wrapping the
[Trace Processor](/docs/analysis/trace-processor.md): it allows fast (<1s)
interactive queries on large sets (up to ~1000) of traces._

## Installation

Batch Trace Processor is part of the `perfetto` Python library and can be
installed by running:

```shell
pip3 install pandas       # prerequisite for Batch Trace Processor
pip3 install perfetto
```

## Loading traces
NOTE: if you are a Googler, have a look at
[go/perfetto-btp-load-internal](http://goto.corp.google.com/perfetto-btp-load-internal) for how to load traces from Google-internal sources.

The simplest way to load traces in is by passing a list of file paths to load:
```python
from perfetto.batch_trace_processor.api import BatchTraceProcessor

files = [
  'traces/slow-start.pftrace',
  'traces/oom.pftrace',
  'traces/high-battery-drain.pftrace',
]
with BatchTraceProcessor(files) as btp:
  btp.query('...')
```

[glob](https://docs.python.org/3/library/glob.html) can be used to load
all traces in a directory:
```python
from perfetto.batch_trace_processor.api import BatchTraceProcessor

files = glob.glob('traces/*.pftrace')
with BatchTraceProcessor(files) as btp:
  btp.query('...')
```

NOTE: loading too many traces can cause out-of-memory issues: see
[this](/docs/analysis/batch-trace-processor#memory-usage) section for details.

A common requirement is to load traces located in the cloud or by sending
a request to a server. To support this usecase, traces can also be loaded
using [trace URIs](/docs/analysis/batch-trace-processor#trace-uris):
```python
from perfetto.batch_trace_processor.api import BatchTraceProcessor
from perfetto.batch_trace_processor.api import BatchTraceProcessorConfig
from perfetto.trace_processor.api import TraceProcessorConfig
from perfetto.trace_uri_resolver.registry import ResolverRegistry
from perfetto.trace_uri_resolver.resolver import TraceUriResolver

class FooResolver(TraceUriResolver):
  # See "Trace URIs" section below for how to implement a URI resolver.

config = BatchTraceProcessorConfig(
  # See "Trace URIs" below
)
with BatchTraceProcessor('foo:bar=1,baz=abc', config=config) as btp:
  btp.query('...')
```

## Writing queries
Writing queries with batch trace processor works very similarly to the
[Python API](/docs/analysis/trace-processor-python.md).

For example, to get a count of the number of userspace slices:
```python
>>> btp.query('select count(1) from slice')
[  count(1)
0  2092592,   count(1)
0   156071,   count(1)
0   121431]
```
The return value of `query` is a list of [Pandas](https://pandas.pydata.org/)
dataframes, one for each trace loaded.

A common requirement is for all of the traces to be flattened into a
single dataframe instead of getting one dataframe per-trace. To support this,
the `query_and_flatten` function can be used:
```python
>>> btp.query_and_flatten('select count(1) from slice')
  count(1)
0  2092592
1   156071
2   121431
```

`query_and_flatten` also implicitly adds columns indicating the originating
trace. The exact columns added depend on the resolver being used: consult your
resolver's documentation for more information.

[Polars](https://pola.rs/) DataFrames are also supported as an alternative to
Pandas. `query_polars` mirrors `query` and returns a list of Polars DataFrames
(one per trace); `query_and_flatten_polars` mirrors `query_and_flatten` and
concatenates them into a single DataFrame. Polars support requires an optional
dependency:

```shell
pip3 install perfetto[polars]
```

```python
>>> btp.query_polars('select count(1) from slice')
[shape: (1, 1)
┌──────────┐
│ count(1) │
│ ---      │
│ i64      │
╞══════════╡
│  2092592 │
└──────────┘, shape: (1, 1)
┌──────────┐
│ count(1) │
│ ---      │
│ i64      │
╞══════════╡
│   156071 │
└──────────┘, ...]

>>> btp.query_and_flatten_polars('select count(1) from slice')
shape: (3, 1)
┌──────────┐
│ count(1) │
│ ---      │
│ i64      │
╞══════════╡
│  2092592 │
│   156071 │
│   121431 │
└──────────┘
```

## Trace URIs
Trace URIs are a powerful feature of the batch trace processor. URIs decouple
the notion of "paths" to traces from the filesystem. Instead, the URI
describes *how* a trace should be fetched (i.e. by sending a HTTP request
to a server, from cloud storage etc).

The syntax of trace URIs are similar to web
[URLs](https://en.wikipedia.org/wiki/URL). Formally a trace URI has the
structure:
```
Trace URI = protocol:key1=val1(;keyn=valn)*
```

As an example:
```
gcs:bucket=foo;path=bar
```
would indicate that traces should be fetched using the protocol `gcs`
([Google Cloud Storage](https://cloud.google.com/storage)) with traces
located at bucket `foo` and path `bar` in the bucket.

NOTE: the `gcs` resolver is *not* actually included: it's simply given as its
an easy to understand example.

URIs are only a part of the puzzle: ultimately batch trace processor still needs
the bytes of the traces to be able to parse and query them. The job of
converting URIs to trace bytes is left to *resolvers* - Python
classes associated to each *protocol* and use the key-value pairs in the URI
to lookup the traces to be parsed.

By default, batch trace processor only ships with a single resolver which knows
how to lookup filesystem paths: however, custom resolvers can be easily
created and registered. See the documentation on the
[TraceUriResolver class](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/python/perfetto/trace_uri_resolver/resolver.py;l=56?q=resolver.py)
for information on how to do this.

## Memory usage
Memory usage is a very important thing to pay attention to working with batch
trace processor. Every trace loaded lives fully in memory: this is magic behind
making queries fast (<1s) even on hundreds of traces.

This also means that the number of traces you can load is heavily limited by
the amount of memory available available. As a rule of thumb, if your
average trace size is S and you are trying to load N traces, you will have
2 * S * N memory usage. Note that this can vary significantly based on the
exact contents and sizes of your trace.

## Advanced features
### Sharing computations between TP and BTP
Sometimes it can be useful to parameterise code to work with either trace
processor or batch trace processor. `execute` or `execute_and_flatten`
can be used for this purpose:
```python
def some_complex_calculation(tp):
  res = tp.query('...').as_pandas_dataframe()
  # ... do some calculations with res
  return res

# |some_complex_calculation| can be called with a [TraceProcessor] object:
tp = TraceProcessor('/foo/bar.pftrace')
some_complex_calculation(tp)

# |some_complex_calculation| can also be passed to |execute| or
# |execute_and_flatten|
btp = BatchTraceProcessor(['...', '...', '...'])

# Like |query|, |execute| returns one result per trace. Note that the returned
# value *does not* have to be a Pandas dataframe.
[a, b, c] = btp.execute(some_complex_calculation)

# Like |query_and_flatten|, |execute_and_flatten| merges the Pandas dataframes
# returned per trace into a single dataframe, adding any columns requested by
# the resolver.
flattened_res = btp.execute_and_flatten(some_complex_calculation)
```
