You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
764 lines
29 KiB
764 lines
29 KiB
# Trace Processor
|
|
|
|
_The Trace Processor is a C++ library
|
|
([/src/trace_processor](/src/trace_processor)) that ingests traces encoded in a
|
|
wide variety of formats and exposes an SQL interface for querying trace events
|
|
contained in a consistent set of tables. It also has other features including
|
|
computation of summary metrics, annotating the trace with user-friendly
|
|
descriptions and deriving new events from the contents of the trace._
|
|
|
|
![Trace processor block diagram](/docs/images/trace-processor.png)
|
|
|
|
## Quickstart
|
|
|
|
The [quickstart](/docs/quickstart/trace-analysis.md) provides a quick overview
|
|
on how to run SQL queries against traces using trace processor.
|
|
|
|
## Introduction
|
|
|
|
Events in a trace are optimized for fast, low-overhead recording. Therefore
|
|
traces need significant data processing to extract meaningful information from
|
|
them. This is compounded by the number of legacy formats which are still in use and
|
|
need to be supported in trace analysis tools.
|
|
|
|
The trace processor abstracts this complexity by parsing traces, extracting the
|
|
data inside, and exposing it in a set of database tables which can be queried
|
|
with SQL.
|
|
|
|
Features of the trace processor include:
|
|
|
|
* Execution of SQL queries on a custom, in-memory, columnar database backed by
|
|
the SQLite query engine.
|
|
* Metrics subsystem which allows computation of summarized view of the trace
|
|
(e.g. CPU or memory usage of a process, time taken for app startup etc.).
|
|
* Annotating events in the trace with user-friendly descriptions, providing
|
|
context and explanation of events to newer users.
|
|
* Creation of new events derived from the contents of the trace.
|
|
|
|
The formats supported by trace processor include:
|
|
|
|
* Perfetto native protobuf format
|
|
* Linux ftrace
|
|
* Android systrace
|
|
* Chrome JSON (including JSON embedding Android systrace text)
|
|
* Fuchsia binary format
|
|
* [Ninja](https://ninja-build.org/) logs (the build system)
|
|
|
|
The trace processor is embedded in a wide variety of trace analysis tools, including:
|
|
|
|
* [trace_processor](/docs/analysis/trace-processor.md), a standalone binary
|
|
providing a shell interface (and the reference embedder).
|
|
* [Perfetto UI](https://ui.perfetto.dev), in the form of a WebAssembly module.
|
|
* [Android Graphics Inspector](https://gpuinspector.dev/).
|
|
* [Android Studio](https://developer.android.com/studio/).
|
|
|
|
## Concepts
|
|
|
|
The trace processor has some foundational terminology and concepts which are
|
|
used in the rest of documentation.
|
|
|
|
### Events
|
|
|
|
In the most general sense, a trace is simply a collection of timestamped
|
|
"events". Events can have associated metadata and context which allows them to
|
|
be interpreted and analyzed.
|
|
|
|
Events form the foundation of trace processor and are one of two types: slices
|
|
and counters.
|
|
|
|
#### Slices
|
|
|
|
![Examples of slices](/docs/images/slices.png)
|
|
|
|
A slice refers to an interval of time with some data describing what was
|
|
happening in that interval. Some example of slices include:
|
|
|
|
* Scheduling slices for each CPU
|
|
* Atrace slices on Android
|
|
* Userspace slices from Chrome
|
|
|
|
#### Counters
|
|
|
|
![Examples of counters](/docs/images/counters.png)
|
|
|
|
A counter is a continuous value which varies over time. Some examples of
|
|
counters include:
|
|
|
|
* CPU frequency for each CPU core
|
|
* RSS memory events - both from the kernel and polled from /proc/stats
|
|
* atrace counter events from Android
|
|
* Chrome counter events
|
|
|
|
### Tracks
|
|
|
|
A track is a named partition of events of the same type and the same associated
|
|
context. For example:
|
|
|
|
* Scheduling slices have one track for each CPU
|
|
* Sync userspace slice have one track for each thread which emitted an event
|
|
* Async userspace slices have one track for each “cookie” linking a set of async
|
|
events
|
|
|
|
The most intuitive way to think of a track is to imagine how they would be drawn
|
|
in a UI; if all the events are in a single row, they belong to the same track.
|
|
For example, all the scheduling events for CPU 5 are on the same track:
|
|
|
|
![CPU slices track](/docs/images/cpu-slice-track.png)
|
|
|
|
Tracks can be split into various types based on the type of event they contain
|
|
and the context they are associated with. Examples include:
|
|
|
|
* Global tracks are not associated to any context and contain slices
|
|
* Thread tracks are associated to a single thread and contain slices
|
|
* Counter tracks are not associated to any context and contain counters
|
|
* CPU counter tracks are associated to a single CPU and contain counters
|
|
|
|
### Thread and process identifiers
|
|
|
|
The handling of threads and processes needs special care when considered in the
|
|
context of tracing; identifiers for threads and processes (e.g. `pid`/`tgid` and
|
|
`tid` in Android/macOS/Linux) can be reused by the operating system over the
|
|
course of a trace. This means they cannot be relied upon as a unique identifier
|
|
when querying tables in trace processor.
|
|
|
|
To solve this problem, the trace processor uses `utid` (_unique_ tid) for
|
|
threads and `upid` (_unique_ pid) for processes. All references to threads and
|
|
processes (e.g. in CPU scheduling data, thread tracks) uses `utid` and `upid`
|
|
instead of the system identifiers.
|
|
|
|
## Object-oriented tables
|
|
|
|
Modeling an object with many types is a common problem in trace processor. For
|
|
example, tracks can come in many varieties (thread tracks, process tracks,
|
|
counter tracks etc). Each type has a piece of data associated to it unique to
|
|
that type; for example, thread tracks have a `utid` of the thread, counter
|
|
tracks have the `unit` of the counter.
|
|
|
|
To solve this problem in object-oriented languages, a `Track` class could be
|
|
created and inheritance used for all subclasses (e.g. `ThreadTrack` and
|
|
`CounterTrack` being subclasses of `Track`, `ProcessCounterTrack` being a
|
|
subclass of `CounterTrack` etc).
|
|
|
|
![Object-oriented table diagram](/docs/images/oop-table-inheritance.png)
|
|
|
|
In trace processor, this "object-oriented" approach is replicated by having
|
|
different tables for each type of object. For example, we have a `track` table
|
|
as the "root" of the hierarchy with the `thread_track` and `counter_track`
|
|
tables "inheriting from" the `track` table.
|
|
|
|
NOTE: [The appendix below](#appendix-table-inheritance) gives the exact rules
|
|
for inheritance between tables for interested readers.
|
|
|
|
Inheritance between the tables works in the natural way (i.e. how it works in
|
|
OO languages) and is best summarized by a diagram.
|
|
|
|
![SQL table inheritance diagram](/docs/images/tp-table-inheritance.png)
|
|
|
|
NOTE: For an up-to-date of how tables currently inherit from each other as well
|
|
as a comprehensive reference of all the column and how they are inherited see
|
|
the [SQL tables](/docs/analysis/sql-tables.autogen) reference page.
|
|
|
|
## Writing Queries
|
|
|
|
### Context using tracks
|
|
|
|
A common question when querying tables in trace processor is: "how do I obtain
|
|
the process or thread for a slice?". Phrased more generally, the question is
|
|
"how do I get the context for an event?".
|
|
|
|
In trace processor, any context associated with all events on a track is found
|
|
on the associated `track` tables.
|
|
|
|
For example, to obtain the `utid` of any thread which emitted a `measure` slice
|
|
|
|
```sql
|
|
SELECT utid
|
|
FROM slice
|
|
JOIN thread_track ON thread_track.id = slice.track_id
|
|
WHERE slice.name = 'measure'
|
|
```
|
|
|
|
Similarly, to obtain the `upid`s of any process which has a `mem.swap` counter
|
|
greater than 1000
|
|
|
|
```sql
|
|
SELECT upid
|
|
FROM counter
|
|
JOIN process_counter_track ON process_counter_track.id = slice.track_id
|
|
WHERE process_counter_track.name = 'mem.swap' AND value > 1000
|
|
```
|
|
|
|
If the source and type of the event is known beforehand (which is generally the
|
|
case), the following can be used to find the `track` table to join with
|
|
|
|
| Event type | Associated with | Track table | Constraint in WHERE clause |
|
|
| :--------- | ------------------ | --------------------- | -------------------------- |
|
|
| slice | N/A (global scope) | track | `type = 'track'` |
|
|
| slice | thread | thread_track | N/A |
|
|
| slice | process | process_track | N/A |
|
|
| counter | N/A (global scope) | counter_track | `type = 'counter_track'` |
|
|
| counter | thread | thread_counter_track | N/A |
|
|
| counter | process | process_counter_track | N/A |
|
|
| counter | cpu | cpu_counter_track | N/A |
|
|
|
|
On the other hand, sometimes the source is not known. In this case, joining with
|
|
the `track `table and looking up the `type` column will give the exact track
|
|
table to join with.
|
|
|
|
For example, to find the type of track for `measure` events, the following query
|
|
could be used.
|
|
|
|
```sql
|
|
SELECT type
|
|
FROM slice
|
|
JOIN track ON track.id = slice.track_id
|
|
WHERE slice.name = 'measure'
|
|
```
|
|
|
|
### Thread and process tables
|
|
|
|
While obtaining `utid`s and `upid`s are a step in the right direction, generally
|
|
users want the original `tid`, `pid`, and process/thread names.
|
|
|
|
The `thread` and `process` tables map `utid`s and `upid`s to threads and
|
|
processes respectively. For example, to lookup the thread with `utid` 10
|
|
|
|
```sql
|
|
SELECT tid, name
|
|
FROM thread
|
|
WHERE utid = 10
|
|
```
|
|
|
|
The `thread` and `process` tables can also be joined with the associated track
|
|
tables directly to jump directly from the slice or counter to the information
|
|
about processes and threads.
|
|
|
|
For example, to get a list of all the threads which emitted a `measure` slice
|
|
|
|
```sql
|
|
SELECT thread.name AS thread_name
|
|
FROM slice
|
|
JOIN thread_track ON slice.track_id = thread_track.id
|
|
JOIN thread USING(utid)
|
|
WHERE slice.name = 'measure'
|
|
GROUP BY thread_name
|
|
```
|
|
|
|
## Operator tables
|
|
SQL queries are usually sufficient to retrieve data from trace processor.
|
|
Sometimes though, certain constructs can be difficult to express pure SQL.
|
|
|
|
In these situations, trace processor has special "operator tables" which solve
|
|
a particular problem in C++ but expose an SQL interface for queries to take
|
|
advantage of.
|
|
|
|
### Span join
|
|
Span join is a custom operator table which computes the intersection of
|
|
spans of time from two tables or views. A column (called the *partition*)
|
|
can optionally be specified which divides the rows from each table into
|
|
partitions before computing the intersection.
|
|
|
|
![Span join block diagram](/docs/images/span-join.png)
|
|
|
|
```sql
|
|
-- Get all the scheduling slices
|
|
CREATE VIEW sp_sched AS
|
|
SELECT ts, dur, cpu, utid
|
|
FROM sched
|
|
|
|
-- Get all the cpu frequency slices
|
|
CREATE VIEW sp_frequency AS
|
|
SELECT
|
|
ts,
|
|
lead(ts) OVER (PARTITION BY cpu ORDER BY ts) - ts as dur,
|
|
cpu,
|
|
value as freq
|
|
FROM counter
|
|
|
|
-- Create the span joined table which combines cpu frequency with
|
|
-- scheduling slices.
|
|
CREATE VIRTUAL TABLE sched_with_frequency
|
|
USING SPAN_JOIN(sp_sched PARTITIONED cpu, sp_frequency PARTITIONED cpu)
|
|
|
|
-- This span joined table can be queried as normal and has the columns from both
|
|
-- tables.
|
|
SELECT ts, dur, cpu, utid, freq
|
|
FROM sched_with_frequency
|
|
```
|
|
|
|
NOTE: A partition can be specified on neither, either or both tables. If
|
|
specified on both, the same column name has to be specified on each table.
|
|
|
|
WARNING: An important restriction on span joined tables is that spans from
|
|
the same table in the same partition *cannot* overlap. For performance
|
|
reasons, span join does not attempt to detect and error out in this situation;
|
|
instead, incorrect rows will silently be produced.
|
|
|
|
### Ancestor slice
|
|
ancestor_slice is a custom operator table that takes a
|
|
[slice table's id column](/docs/analysis/sql-tables.autogen#slice) and computes
|
|
all slices on the same track that are direct parents above that id (i.e. given
|
|
a slice id it will return as rows all slices that can be found by following
|
|
the parent_id column to the top slice (depth = 0)).
|
|
|
|
The returned format is the same as the
|
|
[slice table](/docs/analysis/sql-tables.autogen#slice)
|
|
|
|
For example, the following finds the top level slice given a bunch of slices of
|
|
interest.
|
|
|
|
```sql
|
|
CREATE VIEW interesting_slices AS
|
|
SELECT id, ts, dur, track_id
|
|
FROM slice WHERE name LIKE "%interesting slice name%";
|
|
|
|
SELECT
|
|
*
|
|
FROM
|
|
interesting_slices LEFT JOIN
|
|
ancestor_slice(interesting_slices.id) AS ancestor ON ancestor.depth = 0
|
|
```
|
|
|
|
### Descendant slice
|
|
descendant_slice is a custom operator table that takes a
|
|
[slice table's id column](/docs/analysis/sql-tables.autogen#slice) and
|
|
computes all slices on the same track that are nested under that id (i.e.
|
|
all slices that are on the same track at the same time frame with a depth
|
|
greater than the given slice's depth.
|
|
|
|
The returned format is the same as the
|
|
[slice table](/docs/analysis/sql-tables.autogen#slice)
|
|
|
|
For example, the following finds the number of slices under each slice of
|
|
interest.
|
|
|
|
```sql
|
|
CREATE VIEW interesting_slices AS
|
|
SELECT id, ts, dur, track_id
|
|
FROM slice WHERE name LIKE "%interesting slice name%";
|
|
|
|
SELECT
|
|
*
|
|
(
|
|
SELECT
|
|
COUNT(*) AS total_descendants
|
|
FROM descendant_slice(interesting_slice.id)
|
|
)
|
|
FROM interesting_slices
|
|
```
|
|
|
|
### Connected/Following/Preceding flows
|
|
|
|
DIRECTLY_CONNECTED_FLOW, FOLLOWING_FLOW and PRECEDING_FLOW are custom operator
|
|
tables that take a
|
|
[slice table's id column](/docs/analysis/sql-tables.autogen#slice) and collect
|
|
all entries of [flow table](/docs/analysis/sql-tables.autogen#flow), that are
|
|
directly or indirectly connected to the given starting slice.
|
|
|
|
`DIRECTLY_CONNECTED_FLOW(start_slice_id)` - contains all entries of
|
|
[flow table](/docs/analysis/sql-tables.autogen#flow) that are present in any
|
|
chain of kind: `flow[0] -> flow[1] -> ... -> flow[n]`, where
|
|
`flow[i].slice_out = flow[i+1].slice_in` and `flow[0].slice_out = start_slice_id
|
|
OR start_slice_id = flow[n].slice_in`.
|
|
|
|
NOTE: Unlike the following/preceding flow functions, this function will not
|
|
include flows connected to ancestors or descendants while searching for flows
|
|
from a slice. It only includes the slices in the directly connected chain.
|
|
|
|
`FOLLOWING_FLOW(start_slice_id)` - contains all flows which can be reached from
|
|
a given slice via recursively following from flow's outgoing slice to its
|
|
incoming one and from a reached slice to its child. The return table contains
|
|
all entries of [flow table](/docs/analysis/sql-tables.autogen#flow) that are
|
|
present in any chain of kind: `flow[0] -> flow[1] -> ... -> flow[n]`, where
|
|
`flow[i+1].slice_out IN DESCENDANT_SLICE(flow[i].slice_in) OR
|
|
flow[i+1].slice_out = flow[i].slice_in` and `flow[0].slice_out IN
|
|
DESCENDANT_SLICE(start_slice_id) OR flow[0].slice_out = start_slice_id`.
|
|
|
|
`PRECEDING_FLOW(start_slice_id)` - contains all flows which can be reached from
|
|
a given slice via recursively following from flow's incoming slice to its
|
|
outgoing one and from a reached slice to its parent. The return table contains
|
|
all entries of [flow table](/docs/analysis/sql-tables.autogen#flow) that are
|
|
present in any chain of kind: `flow[n] -> flow[n-1] -> ... -> flow[0]`, where
|
|
`flow[i].slice_in IN ANCESTOR_SLICE(flow[i+1].slice_out) OR flow[i].slice_in =
|
|
flow[i+1].slice_out` and `flow[0].slice_in IN ANCESTOR_SLICE(start_slice_id) OR
|
|
flow[0].slice_in = start_slice_id`.
|
|
|
|
```sql
|
|
--number of following flows for each slice
|
|
SELECT (SELECT COUNT(*) FROM FOLLOWING_FLOW(slice_id)) as following FROM slice;
|
|
```
|
|
|
|
## Metrics
|
|
|
|
TIP: To see how to add to add a new metric to trace processor, see the checklist
|
|
[here](/docs/contributing/common-tasks.md#new-metric).
|
|
|
|
The metrics subsystem is a significant part of trace processor and thus is
|
|
documented on its own [page](/docs/analysis/metrics.md).
|
|
|
|
## Annotations
|
|
|
|
TIP: To see how to add to add a new annotation to trace processor, see the
|
|
checklist [here](/docs/contributing/common-tasks.md#new-annotation).
|
|
|
|
Annotations attach a human-readable description to a slice in the trace. This
|
|
can include information like the source of a slice, why a slice is important and
|
|
links to documentation where the viewer can learn more about the slice.
|
|
In essence, descriptions act as if an expert was telling the user what the slice
|
|
means.
|
|
|
|
For example, consider the `inflate` slice which occurs during view inflation in
|
|
Android. We can add the following description and link:
|
|
|
|
**Description**: Constructing a View hierarchy from pre-processed XML via
|
|
LayoutInflater#layout. This includes constructing all of the View objects in the
|
|
hierarchy, and applying styled attributes.
|
|
|
|
## Creating derived events
|
|
|
|
TIP: To see how to add to add a new annotation to trace processor, see the
|
|
checklist [here](/docs/contributing/common-tasks.md#new-annotation).
|
|
|
|
This feature allows creation of new events (slices and counters) from the data
|
|
in the trace. These events can then be displayed in the UI tracks as if they
|
|
were part of the trace itself.
|
|
|
|
This is useful as often the data in the trace is very low-level. While low
|
|
level information is important for experts to perform deep debugging, often
|
|
users are just looking for a high level overview without needing to consider
|
|
events from multiple locations.
|
|
|
|
For example, an app startup in Android spans multiple components including
|
|
`ActivityManager`, `system_server`, and the newly created app process derived
|
|
from `zygote`. Most users do not need this level of detail; they are only
|
|
interested in a single slice spanning the entire startup.
|
|
|
|
Creating derived events is tied very closely to
|
|
[metrics subsystem](/docs/analysis/metrics.md); often SQL-based metrics need to
|
|
create higher-level abstractions from raw events as intermediate artifacts.
|
|
|
|
From previous example, the
|
|
[startup metric](/src/trace_processor/metrics/android/android_startup.sql)
|
|
creates the exact `launching` slice we want to display in the UI.
|
|
|
|
The other benefit of aligning the two is that changes in metrics are
|
|
automatically kept in sync with what the user sees in the UI.
|
|
|
|
## Alerts
|
|
|
|
Alerts are used to draw the attention of the user to interesting parts of the
|
|
trace; this are usually warnings or errors about anomalies which occurred in the
|
|
trace.
|
|
|
|
Currently, alerts are not implemented in the trace processor but the API to
|
|
create derived events was designed with them in mind. We plan on adding another
|
|
column `alert_type` (name to be finalized) to the annotations table which can
|
|
have the value `warning`, `error` or `null`. Depending on this value, the
|
|
Perfetto UI will flag these events to the user.
|
|
|
|
NOTE: we do not plan on supporting case where alerts need to be added to
|
|
existing events. Instead, new events should be created using annotations
|
|
and alerts added on these instead; this is because the trace processor
|
|
storage is monotonic-append-only.
|
|
|
|
## Python API
|
|
|
|
The trace processor Python API is built on the existing HTTP interface of `trace processor`
|
|
and is available as part of the standalone build. The API allows you to load in traces and
|
|
query tables and run metrics without requiring the `trace_processor` binary to be
|
|
downloaded or installed.
|
|
|
|
### Setup
|
|
```
|
|
pip install perfetto
|
|
```
|
|
NOTE: The API is only compatible with Python3.
|
|
|
|
```python
|
|
from perfetto.trace_processor import TraceProcessor
|
|
# Initialise TraceProcessor with a trace file
|
|
tp = TraceProcessor(file_path='trace.perfetto-trace')
|
|
```
|
|
|
|
NOTE: The TraceProcessor can be initialized in a combination of ways including:
|
|
<br> - An address at which there exists a running instance of `trace_processor` with a
|
|
loaded trace (e.g. `TraceProcessor(addr='localhost:9001')`)
|
|
<br> - An address at which there exists a running instance of `trace_processor` and
|
|
needs a trace to be loaded in
|
|
(e.g. `TraceProcessor(addr='localhost:9001', file_path='trace.perfetto-trace')`)
|
|
<br> - A path to a `trace_processor` binary and the trace to be loaded in
|
|
(e.g. `TraceProcessor(bin_path='./trace_processor', file_path='trace.perfetto-trace')`)
|
|
|
|
|
|
### API
|
|
|
|
The `trace_processor.api` module contains the `TraceProcessor` class which provides various
|
|
functions that can be called on the loaded trace. For more information on how to use
|
|
these functions, see this [`example`](/src/trace_processor/python/example.py).
|
|
|
|
#### Query
|
|
The query() function takes an SQL query as input and returns an iterator through the rows
|
|
of the result.
|
|
|
|
```python
|
|
from perfetto.trace_processor import TraceProcessor
|
|
tp = TraceProcessor(file_path='trace.perfetto-trace')
|
|
|
|
qr_it = tp.query('SELECT ts, dur, name FROM slice')
|
|
for row in qr_it:
|
|
print(row.ts, row.dur, row.name)
|
|
```
|
|
**Output**
|
|
```
|
|
261187017446933 358594 eglSwapBuffersWithDamageKHR
|
|
261187017518340 357 onMessageReceived
|
|
261187020825163 9948 queueBuffer
|
|
261187021345235 642 bufferLoad
|
|
261187121345235 153 query
|
|
...
|
|
```
|
|
The QueryResultIterator can also be converted to a Pandas DataFrame, although this
|
|
requires you to have both the `NumPy` and `Pandas` modules installed.
|
|
```python
|
|
from perfetto.trace_processor import TraceProcessor
|
|
tp = TraceProcessor(file_path='trace.perfetto-trace')
|
|
|
|
qr_it = tp.query('SELECT ts, dur, name FROM slice')
|
|
qr_df = qr_it.as_pandas_dataframe()
|
|
print(qr_df.to_string())
|
|
```
|
|
**Output**
|
|
```
|
|
ts dur name
|
|
-------------------- -------------------- ---------------------------
|
|
261187017446933 358594 eglSwapBuffersWithDamageKHR
|
|
261187017518340 357 onMessageReceived
|
|
261187020825163 9948 queueBuffer
|
|
261187021345235 642 bufferLoad
|
|
261187121345235 153 query
|
|
...
|
|
```
|
|
Furthermore, you can use the query result in a Pandas DataFrame format to easily
|
|
make visualisations from the trace data.
|
|
```python
|
|
from perfetto.trace_processor import TraceProcessor
|
|
tp = TraceProcessor(file_path='trace.perfetto-trace')
|
|
|
|
qr_it = tp.query('SELECT ts, value FROM counter WHERE track_id=50')
|
|
qr_df = qr_it.as_pandas_dataframe()
|
|
qr_df = qr_df.replace(np.nan,0)
|
|
qr_df = qr_df.set_index('ts')['value'].plot()
|
|
```
|
|
**Output**
|
|
|
|
![Graph made frpm the query results](/docs/images/example_pd_graph.png)
|
|
|
|
|
|
#### Metric
|
|
The metric() function takes in a list of trace metrics and returns the results as a Protobuf.
|
|
|
|
```python
|
|
from perfetto.trace_processor import TraceProcessor
|
|
tp = TraceProcessor(file_path='trace.perfetto-trace')
|
|
|
|
ad_cpu_metrics = tp.metric(['android_cpu'])
|
|
print(ad_cpu_metrics)
|
|
```
|
|
**Output**
|
|
```
|
|
metrics {
|
|
android_cpu {
|
|
process_info {
|
|
name: "/system/bin/init"
|
|
threads {
|
|
name: "init"
|
|
core {
|
|
id: 1
|
|
metrics {
|
|
mcycles: 1
|
|
runtime_ns: 570365
|
|
min_freq_khz: 1900800
|
|
max_freq_khz: 1900800
|
|
avg_freq_khz: 1902017
|
|
}
|
|
}
|
|
core {
|
|
id: 3
|
|
metrics {
|
|
mcycles: 0
|
|
runtime_ns: 366406
|
|
min_freq_khz: 1900800
|
|
max_freq_khz: 1900800
|
|
avg_freq_khz: 1902908
|
|
}
|
|
}
|
|
...
|
|
}
|
|
...
|
|
}
|
|
process_info {
|
|
name: "/system/bin/logd"
|
|
threads {
|
|
name: "logd.writer"
|
|
core {
|
|
id: 0
|
|
metrics {
|
|
mcycles: 8
|
|
runtime_ns: 33842357
|
|
min_freq_khz: 595200
|
|
max_freq_khz: 1900800
|
|
avg_freq_khz: 1891825
|
|
}
|
|
}
|
|
core {
|
|
id: 1
|
|
metrics {
|
|
mcycles: 9
|
|
runtime_ns: 36019300
|
|
min_freq_khz: 1171200
|
|
max_freq_khz: 1900800
|
|
avg_freq_khz: 1887969
|
|
}
|
|
}
|
|
...
|
|
}
|
|
...
|
|
}
|
|
...
|
|
}
|
|
}
|
|
```
|
|
|
|
### HTTP
|
|
The `trace_processor.http` module contains the `TraceProcessorHttp` class which
|
|
provides methods to make HTTP requests to an address at which there already
|
|
exists a running instance of `trace_processor` with a trace loaded in. All
|
|
results are returned in Protobuf format
|
|
(see [`trace_processor_proto`](/protos/perfetto/trace_processor/trace_processor.proto)).
|
|
Some functions include:
|
|
* `execute_query()` - Takes in an SQL query and returns a `QueryResult` Protobuf
|
|
message
|
|
* `compute_metric()` - Takes in a list of trace metrics and returns a
|
|
`ComputeMetricResult` Protobuf message
|
|
* `status()` - Returns a `StatusResult` Protobuf message
|
|
|
|
|
|
## Testing
|
|
|
|
Trace processor is mainly tested in two ways:
|
|
1. Unit tests of low-level building blocks
|
|
2. "Diff" tests which parse traces and check the output of queries
|
|
|
|
### Unit tests
|
|
Unit testing trace processor is the same as in other parts of Perfetto and
|
|
other C++ projects. However, unlike the rest of Perfetto, unit testing is
|
|
relatively light in trace processor.
|
|
|
|
We have discovered over time that unit tests are generally too brittle
|
|
when dealing with code which parses traces leading to painful, mechanical
|
|
changes being needed when refactorings happen.
|
|
|
|
Because of this, we choose to focus on diff tests for most areas (e.g.
|
|
parsing events, testing schema of tables, testing metrics etc.) and only
|
|
use unit testing for the low-level building blocks on which the rest of
|
|
trace processor is built.
|
|
|
|
### Diff tests
|
|
Diff tests are essentially integration tests for trace processor and the
|
|
main way trace processor is tested.
|
|
|
|
Each diff test takes as input a) a trace file b) a query file *or* a metric
|
|
name. It runs `trace_processor_shell` to parse the trace and then executes
|
|
the query/metric. The result is then compared to a 'golden' file and any
|
|
difference is highlighted.
|
|
|
|
All diff tests are organized under [test/trace_processor](/test/trace_processor)
|
|
and are run by the script
|
|
[`tools/diff_test_trace_processor.py`](/tools/diff_test_trace_processor.py).
|
|
New tests can be added with the helper script
|
|
[`tools/add_tp_diff_test.py`](/tools/add_tp_diff_test.py).
|
|
|
|
NOTE: `trace_processor_shell` and associated proto descriptors needs to be
|
|
built before running `tools/diff_test_trace_processor.py`. The easiest way
|
|
to do this is to run `tools/ninja -C <out directory>` both initially and on
|
|
every change to trace processor code or builtin metrics.
|
|
|
|
#### Choosing where to add diff tests
|
|
When adding a new test with `tools/add_tp_diff_test.py`, the user is
|
|
prompted for a folder to add the new test to. Often this can be confusing
|
|
as a test can fall into more than one category. This section is a guide
|
|
to decide which folder to choose.
|
|
|
|
Broadly, there are two categories which all folders fall into:
|
|
1. __"Area" folders__ which encompass a "vertical" area of interest
|
|
e.g. startup/ contains Android app startup related tests or chrome/
|
|
contains all Chrome related tests.
|
|
2. __"Feature" folders__ which encompass a particular feature of
|
|
trace processor e.g. process_tracking/ tests the lifetime tracking of
|
|
processes, span_join/ tests the span join operator.
|
|
|
|
"Area" folders should be preferred for adding tests unless the test is
|
|
applicable to more than one "area"; in this case, one of "feature" folders
|
|
can be used instead.
|
|
|
|
Here are some common scenarios in which new tests may be added and
|
|
answers on where to add the test:
|
|
|
|
__Scenario__: A new event is being parsed, the focus of the test is to ensure
|
|
the event is being parsed correctly and the event is focused on a single
|
|
vertical "Area".
|
|
|
|
_Answer_: Add the test in one of the "Area" folders.
|
|
|
|
__Scenario__: A new event is being parsed and the focus of the test is to ensure
|
|
the event is being parsed correctly and the event is applicable to more than one
|
|
vertical "Area".
|
|
|
|
_Answer_: Add the test to the parsing/ folder.
|
|
|
|
__Scenario__: A new metric is being added and the focus of the test is to
|
|
ensure the metric is being correctly computed.
|
|
|
|
_Answer_: Add the test in one of the "Area" folders.
|
|
|
|
__Scenario__: A new dynamic table is being added and the focus of the test is to
|
|
ensure the dynamic table is being correctly computed...
|
|
|
|
_Answer_: Add the test to the dynamic/ folder
|
|
|
|
__Scenario__: The interals of trace processor are being modified and the test
|
|
is to ensure the trace processor is correctly filtering/sorting important
|
|
built-in tables.
|
|
|
|
_Answer_: Add the test to the tables/ folder.
|
|
|
|
|
|
## Appendix: table inheritance
|
|
|
|
Concretely, the rules for inheritance between tables works are as follows:
|
|
|
|
* Every row in a table has an `id` which is unique for a hierarchy of tables.
|
|
* For example, every `track` will have an `id` which is unique among all
|
|
tracks (regardless of the type of track)
|
|
* If a table C inherits from P, each row in C will also be in P _with the same
|
|
id_
|
|
* This allows for ids to act as "pointers" to rows; lookups by id can be
|
|
performed on any table which has that row
|
|
* For example, every `process_counter_track` row will have a matching row in
|
|
`counter_track` which will itself have matching rows in `track`
|
|
* If a table C with columns `A` and `B` inherits from P with column `A`, `A`
|
|
will have the same data in both C and P
|
|
* For example, suppose
|
|
* `process_counter_track` has columns `name`, `unit` and `upid`
|
|
* `counter_track` has `name` and `unit`
|
|
* `track` has `name`
|
|
* Every row in `process_counter_track` will have the same `name` for the row
|
|
with the same id in `track` and `counter_track`
|
|
* Similarly, every row in `process_counter_track` will have both the same
|
|
`name ` and `unit` for the row with the same id in `counter_track`
|
|
* Every row in a table has a `type` column. This specifies the _most specific_
|
|
table this row belongs to.
|
|
* This allows _dynamic casting_ of a row to its most specific type
|
|
* For example, for if a row in the `track` is actually a
|
|
`process_counter_track`, it's type column will be `process_counter_track`.
|