blob: 1b3199a4c110739928cd4b76dbbcabde3c2b825a [file] [log] [blame] [view]
# Batch Trace Processor
This document describes the overall design of Batch Trace Processor and
aids in integrating it into other systems.
![BTP Overview](/docs/images/perfetto-btp-overview.svg)
## Motivation
The Perfetto trace processor is the de-facto way to perform analysis on a
single trace. Using the
[trace processor Python API](/docs/analysis/trace-processor#python-api),
traces can be queried interactively, plots made from those results etc.
While queries on a single trace are useful when debugging a specific problem
in that trace or in the very early stages of understanding a domain, it soon
becomes limiting. One trace is unlikely to be representative
of the entire population and it's easy to overfit queries i.e. spend a
lot of effort on breaking down a problem in that trace while neglecting
other, more common issues in the population.
Because of this, what we actually want is to be able to query many traces
(usually on the order of 250-10000+) and identify the patterns which show
up in a significant fraction of them. This ensures that time is being spent
on issues which are affecting user experience instead of just a random
problem which happened to show up in the trace.
One low-effort option for solving this problem is simply to ask people to use
utilities like [Executors](https://docs.python.org/3/library/concurrent.futures.html#executor-objects)
with the Python API to load multiple traces and query them in parallel.
Unfortunately, there are several downsides to this approach:
* Every user has to reinvent the wheel every time they want to query multiple
traces. Over time, there would likely be a proliferation of slightly modified
code which is copied from each place.
* While the basics of parallelising queries on multiple traces on a single
machine is straightforward, one day, we may want to shard trace processing
across multiple machines. Once this happens, the complexity of the code would
rise significantly to the point where a central implementation becomes a
necessity. Because of this, it's better to have the API first before engineers
start building their own custom solutions.
* A big aim for the Perfetto team these days is to make trace analysis more
accessible to reduce the number of places where we need to be in the loop.
Having a well supported API for an important usecase like bulk trace analysis
directly helps with this.
While we've discussed querying traces so far, the experience for loading traces
from different traces should be just as good. This has historically been a big
reason why the Python API has not gained as much adoption as we would have
liked.
Especially internally in Google, we should not be relying on engineers
knowing where traces live on the network filesystem and the directory layout.
Instead, they should be able to simply be able to specify the data source (i.e.
lab, testing population) and some parameters (e.g. build id, date, kernel
version) that traces should match should match and traces meeting these criteria
should found and loaded.
Putting all this together, we want to build a library which can:
* Interactively query ~1000+ traces in O(s) (for simple queries)
* Expose full SQL expressiveness from trace processor
* Load traces from many sources with minimal ceremony. This should include
Google-internal sources: e.g. lab runs and internal testing populations
* Integrate with data analysis libraries for easy charting and visulazation
## Design Highlights
In this section, we briefly discuss some of the most impactful design decisions
taken when building batch trace processor and the reasons behind them.
### Language
The choice of langugage is pretty straightforward. Python is already the go-to
langugage for data analysis in a wide variety of domains and our problem
is not unique enough to warrant making a different decision. Moreover, another
point in favour is the existence of the Python API for trace processor. This
further eases the implementation as we do not have to start from scratch.
The main downside of choosing Python is performance but given that that all
the data crunching happens in C++ inside TP, this is not a big factor.
### Trace URIs and Resolvers
[Trace URIs](/docs/analysis/batch-trace-processor#trace-uris)
are an elegant solution to the problem of loading traces from a diverse range
of public and internal sources. As with web URIs, the idea with trace URIs is
to describe both the protocol (i.e. the source) from which traces should be
fetched and the arguments (i.e. query parameters) which the traces should match.
Batch trace processor should integrate tightly with trace URIs and their
resolvers. Users should be able to pass either just the URI (whcih is really
just a string for maximum flexibility) or a resolver object which can yield a
list of trace file paths.
To handle URI strings, there should be some mecahinsm of "registering" resolvers
to make them eligible to resolve a certain "protocol". By default, we should
provide a resolver to handle filesystem. We should ensure that the resolver
design is such that resolvers can be closed soruce while the rest of batch trace
processor is open.
Along with the job of yielding a list of traces, resolvers should also be
responsible for creating metadata for each trace these are different pieces
of information about the trace that the user might be interested in e.g. OS
version, device name, collected date etc. The metadata can then be used when
"flattening" results across many traces as discussed below.
### Persisting loaded traces
Optimizing the loading of traces is critical for the O(s) query performance
we want out of batch trace processor. Traces are often accessed
over the network meaning fetching their contents has a high latency.
Traces also take at least a few seconds to parse, eating up the budget for
O(s) before even getting the running time of queries.
To address this issue, we take the decision to keep all traces fully loaded in
memory in trace processor instances. That way, instead of loading them on every
query/set of queries, we can issue queries directly.
For the moment, we restrict the loading and querying of traces to a
single machine. While querying n traces is "embarassngly parallel" and shards
perfectly across multiple machines, introducing distributed systems to any
solution simply makes everything more complicated. The move to multiple
machines is explored further in the "Future plans" section.
### Flattening query results
The naive way to return the result of querying n traces is a list
of n elements, with each element being result for a single trace. However,
after performing several case-study performance investigations using BTP, it
became obvious that this obvious answer was not the most convienent for the end
user.
Instead, a pattern which proved very useful was to "flatten" the results into
a single table, containing the results from all the traces. However,
simply flattening causes us to lose the information about which trace a row
originated from. We can deal with this by allowing resolvers to silently add
columns with the metadata for each trace.
So suppose we query three traces with:
```SELECT ts, dur FROM slice```
Then in the flattening operation might do something like this behind the scenes:
![BTP Flattening](/docs/images/perfetto-btp-flattening.svg)
## Integration points
Batch trace processor needs to be both open source yet allow deep integration
with Google internal tooling. Because of this, there are various integration
points built design to allow closed compoentns to be slotted in place of the
default, open source ones.
The first point is the formalization of the idea "platform" code. Even since the
begining of the Python API, there was always a need for code internally to be
run slightly different to open source code. For example, Google internal Python
distrubution does not use Pip, instead packaging dependencies into a single
binary. The notion of a "platform" loosely existed to abstract this sort of
differences but this was very ad-hoc. As part of batch trace processor
implementation, this has been retroactively formalized.
Resolvers are another big point of pluggability. By allowing registration of
a "protocol" for each internal trace source (e.g. lab, testing population), we
allow for trace loading to be neatly abstracted.
Finally, for batch trace processor specifically, we abstract the creation of
thread pools for loading traces and running queries. The parallelism and memory
available to programs internally is often does not 1:1 correspond with the
available CPUs/memory on the system: internal APIs need to be accessed to find
out this information.
## Future plans
One common problem when running batch trace processor is that we are
constrained by a single machine and so can only load O(1000) traces.
For rare problems, there might only be a handful of traces matching a given
pattern even in such a large sample.
A way around this would be to build a "no trace limit" mode. The idea here
is that you would develop queries like usual with batch trace processor
operating on a O(1000) traces with O(s) performance. Once the queries are
relatively finalized, we could then "switch" the mode of batch trace processor
to opeate closer to a "MapReduce" style pipeline which operates over O(10000)+
traces loading O(n cpus) traces at any one time.
This allows us to retain both the quick iteration speed while developing queries
while also allowing for large scale analysis without needing to move code
to a pipeline model. However, this approach does not really resolve the root
cause of the problem which is that we are restricted to a single machine.
The "ideal" solution here is to, as mentioned above, shard batch trace processor
across >1 machine. When querying traces, each trace is entirely independent of
any other so paralleising across multiple machines yields very close to perfect
gains in performance at little cost.
This is would be however quite a complex undertaking. We would need to design
the API in such a way that allows for pluggable integration with various compute
platforms (e.g. GCP, Google internal, your custom infra). Even restricting to
just Google infra and leaving others as open for contribution, internal infra's
ideal workload does not match the approach of "have a bunch of machines tied to
one user waiting for their input". There would need to be significiant research
and design work before going here but it would likely be wortwhile.