1# Trace Processor (C++) 2 3_The Trace Processor is a C++ library 4([/src/trace_processor](/src/trace_processor)) that ingests traces encoded in a 5wide variety of formats and exposes an SQL interface for querying trace events 6contained in a consistent set of tables. It also has other features including 7computation of summary metrics, annotating the trace with user-friendly 8descriptions and deriving new events from the contents of the trace._ 9 10 11 12## Quickstart 13 14The [quickstart](/docs/quickstart/trace-analysis.md) provides a quick overview 15on how to run SQL queries against traces using trace processor. 16 17## Introduction 18 19Events in a trace are optimized for fast, low-overhead recording. Therefore 20traces need significant data processing to extract meaningful information from 21them. This is compounded by the number of legacy formats which are still in use and 22need to be supported in trace analysis tools. 23 24The trace processor abstracts this complexity by parsing traces, extracting the 25data inside, and exposing it in a set of database tables which can be queried 26with SQL. 27 28Features of the trace processor include: 29 30* Execution of SQL queries on a custom, in-memory, columnar database backed by 31 the SQLite query engine. 32* Metrics subsystem which allows computation of summarized view of the trace 33 (e.g. CPU or memory usage of a process, time taken for app startup etc.). 34* Annotating events in the trace with user-friendly descriptions, providing 35 context and explanation of events to newer users. 36* Creation of new events derived from the contents of the trace. 37 38The formats supported by trace processor include: 39 40* Perfetto native protobuf format 41* Linux ftrace 42* Android systrace 43* Chrome JSON (including JSON embedding Android systrace text) 44* Fuchsia binary format 45* [Ninja](https://ninja-build.org/) logs (the build system) 46 47The trace processor is embedded in a wide variety of trace analysis tools, including: 48 49* [trace_processor](/docs/analysis/trace-processor.md), a standalone binary 50 providing a shell interface (and the reference embedder). 51* [Perfetto UI](https://ui.perfetto.dev), in the form of a WebAssembly module. 52* [Android GPU Inspector](https://gpuinspector.dev/). 53* [Android Studio](https://developer.android.com/studio/). 54 55## Concepts 56 57The trace processor has some foundational terminology and concepts which are 58used in the rest of documentation. 59 60### Events 61 62In the most general sense, a trace is simply a collection of timestamped 63"events". Events can have associated metadata and context which allows them to 64be interpreted and analyzed. 65 66Events form the foundation of trace processor and are one of two types: slices 67and counters. 68 69#### Slices 70 71 72 73A slice refers to an interval of time with some data describing what was 74happening in that interval. Some example of slices include: 75 76* Scheduling slices for each CPU 77* Atrace slices on Android 78* Userspace slices from Chrome 79 80#### Counters 81 82 83 84A counter is a continuous value which varies over time. Some examples of 85counters include: 86 87* CPU frequency for each CPU core 88* RSS memory events - both from the kernel and polled from /proc/stats 89* atrace counter events from Android 90* Chrome counter events 91 92### Tracks 93 94A track is a named partition of events of the same type and the same associated 95context. For example: 96 97* Scheduling slices have one track for each CPU 98* Sync userspace slice have one track for each thread which emitted an event 99* Async userspace slices have one track for each “cookie” linking a set of async 100 events 101 102The most intuitive way to think of a track is to imagine how they would be drawn 103in a UI; if all the events are in a single row, they belong to the same track. 104For example, all the scheduling events for CPU 5 are on the same track: 105 106 107 108Tracks can be split into various types based on the type of event they contain 109and the context they are associated with. Examples include: 110 111* Global tracks are not associated to any context and contain slices 112* Thread tracks are associated to a single thread and contain slices 113* Counter tracks are not associated to any context and contain counters 114* CPU counter tracks are associated to a single CPU and contain counters 115 116### Thread and process identifiers 117 118The handling of threads and processes needs special care when considered in the 119context of tracing; identifiers for threads and processes (e.g. `pid`/`tgid` and 120`tid` in Android/macOS/Linux) can be reused by the operating system over the 121course of a trace. This means they cannot be relied upon as a unique identifier 122when querying tables in trace processor. 123 124To solve this problem, the trace processor uses `utid` (_unique_ tid) for 125threads and `upid` (_unique_ pid) for processes. All references to threads and 126processes (e.g. in CPU scheduling data, thread tracks) uses `utid` and `upid` 127instead of the system identifiers. 128 129## Object-oriented tables 130 131Modeling an object with many types is a common problem in trace processor. For 132example, tracks can come in many varieties (thread tracks, process tracks, 133counter tracks etc). Each type has a piece of data associated to it unique to 134that type; for example, thread tracks have a `utid` of the thread, counter 135tracks have the `unit` of the counter. 136 137To solve this problem in object-oriented languages, a `Track` class could be 138created and inheritance used for all subclasses (e.g. `ThreadTrack` and 139`CounterTrack` being subclasses of `Track`, `ProcessCounterTrack` being a 140subclass of `CounterTrack` etc). 141 142 143 144In trace processor, this "object-oriented" approach is replicated by having 145different tables for each type of object. For example, we have a `track` table 146as the "root" of the hierarchy with the `thread_track` and `counter_track` 147tables "inheriting from" the `track` table. 148 149NOTE: [The appendix below](#appendix-table-inheritance) gives the exact rules 150for inheritance between tables for interested readers. 151 152Inheritance between the tables works in the natural way (i.e. how it works in 153OO languages) and is best summarized by a diagram. 154 155 156 157NOTE: For an up-to-date of how tables currently inherit from each other as well 158as a comprehensive reference of all the column and how they are inherited see 159the [SQL tables](/docs/analysis/sql-tables.autogen) reference page. 160 161## Writing Queries 162 163### Context using tracks 164 165A common question when querying tables in trace processor is: "how do I obtain 166the process or thread for a slice?". Phrased more generally, the question is 167"how do I get the context for an event?". 168 169In trace processor, any context associated with all events on a track is found 170on the associated `track` tables. 171 172For example, to obtain the `utid` of any thread which emitted a `measure` slice 173 174```sql 175SELECT utid 176FROM slice 177JOIN thread_track ON thread_track.id = slice.track_id 178WHERE slice.name = 'measure' 179``` 180 181Similarly, to obtain the `upid`s of any process which has a `mem.swap` counter 182greater than 1000 183 184```sql 185SELECT upid 186FROM counter 187JOIN process_counter_track ON process_counter_track.id = counter.track_id 188WHERE process_counter_track.name = 'mem.swap' AND value > 1000 189``` 190 191If the source and type of the event is known beforehand (which is generally the 192case), the following can be used to find the `track` table to join with 193 194| Event type | Associated with | Track table | Constraint in WHERE clause | 195| :--------- | ------------------ | --------------------- | -------------------------- | 196| slice | N/A (global scope) | track | `type = 'track'` | 197| slice | thread | thread_track | N/A | 198| slice | process | process_track | N/A | 199| counter | N/A (global scope) | counter_track | `type = 'counter_track'` | 200| counter | thread | thread_counter_track | N/A | 201| counter | process | process_counter_track | N/A | 202| counter | cpu | cpu_counter_track | N/A | 203 204On the other hand, sometimes the source is not known. In this case, joining with 205the `track `table and looking up the `type` column will give the exact track 206table to join with. 207 208For example, to find the type of track for `measure` events, the following query 209could be used. 210 211```sql 212SELECT track.type 213FROM slice 214JOIN track ON track.id = slice.track_id 215WHERE slice.name = 'measure' 216``` 217 218### Thread and process tables 219 220While obtaining `utid`s and `upid`s are a step in the right direction, generally 221users want the original `tid`, `pid`, and process/thread names. 222 223The `thread` and `process` tables map `utid`s and `upid`s to threads and 224processes respectively. For example, to lookup the thread with `utid` 10 225 226```sql 227SELECT tid, name 228FROM thread 229WHERE utid = 10 230``` 231 232The `thread` and `process` tables can also be joined with the associated track 233tables directly to jump directly from the slice or counter to the information 234about processes and threads. 235 236For example, to get a list of all the threads which emitted a `measure` slice 237 238```sql 239SELECT thread.name AS thread_name 240FROM slice 241JOIN thread_track ON slice.track_id = thread_track.id 242JOIN thread USING(utid) 243WHERE slice.name = 'measure' 244GROUP BY thread_name 245``` 246 247## Helper functions 248Helper functions are functions built into C++ which reduce the amount of 249boilerplate which needs to be written in SQL. 250 251### Extract args 252`EXTRACT_ARG` is a helper function which retreives a property of an 253event (e.g. slice or counter) from the `args` table. 254 255It takes an `arg_set_id` and `key` as input and returns the value looked 256up in the `args` table. 257 258For example, to retrieve the `prev_comm` field for `sched_switch` events in 259the `ftrace_event` table. 260```sql 261SELECT EXTRACT_ARG(arg_set_id, 'prev_comm') 262FROM ftrace_event 263WHERE name = 'sched_switch' 264``` 265 266Behind the scenes, the above query would desugar to the following: 267```sql 268SELECT 269 ( 270 SELECT string_value 271 FROM args 272 WHERE key = 'prev_comm' AND args.arg_set_id = raw.arg_set_id 273 ) 274FROM ftrace_event 275WHERE name = 'sched_switch' 276``` 277 278NOTE: while convinient, `EXTRACT_ARG` can inefficient compared to a `JOIN` 279when working with very large tables; a function call is required for every 280row which will be slower than the batch filters/sorts used by `JOIN`. 281 282## Operator tables 283SQL queries are usually sufficient to retrieve data from trace processor. 284Sometimes though, certain constructs can be difficult to express pure SQL. 285 286In these situations, trace processor has special "operator tables" which solve 287a particular problem in C++ but expose an SQL interface for queries to take 288advantage of. 289 290### Span join 291Span join is a custom operator table which computes the intersection of 292spans of time from two tables or views. A span in this concept is a row in a 293table/view which contains a "ts" (timestamp) and "dur" (duration) columns. 294 295A column (called the *partition*) can optionally be specified which divides the 296rows from each table into partitions before computing the intersection. 297 298 299 300```sql 301-- Get all the scheduling slices 302CREATE VIEW sp_sched AS 303SELECT ts, dur, cpu, utid 304FROM sched; 305 306-- Get all the cpu frequency slices 307CREATE VIEW sp_frequency AS 308SELECT 309 ts, 310 lead(ts) OVER (PARTITION BY track_id ORDER BY ts) - ts as dur, 311 cpu, 312 value as freq 313FROM counter 314JOIN cpu_counter_track ON counter.track_id = cpu_counter_track.id 315WHERE cpu_counter_track.name = 'cpufreq'; 316 317-- Create the span joined table which combines cpu frequency with 318-- scheduling slices. 319CREATE VIRTUAL TABLE sched_with_frequency 320USING SPAN_JOIN(sp_sched PARTITIONED cpu, sp_frequency PARTITIONED cpu); 321 322-- This span joined table can be queried as normal and has the columns from both 323-- tables. 324SELECT ts, dur, cpu, utid, freq 325FROM sched_with_frequency; 326``` 327 328NOTE: A partition can be specified on neither, either or both tables. If 329specified on both, the same column name has to be specified on each table. 330 331WARNING: An important restriction on span joined tables is that spans from 332the same table in the same partition *cannot* overlap. For performance 333reasons, span join does not attempt to detect and error out in this situation; 334instead, incorrect rows will silently be produced. 335 336WARNING: Partitions mush be integers. Importantly, string partitions are *not* 337supported; note that strings *can* be converted to integers by 338applying the `HASH` function to the string column. 339 340Left and outer span joins are also supported; both function analogously to 341the left and outer joins from SQL. 342```sql 343-- Left table partitioned + right table unpartitioned. 344CREATE VIRTUAL TABLE left_join 345USING SPAN_LEFT_JOIN(table_a PARTITIONED a, table_b); 346 347-- Both tables unpartitioned. 348CREATE VIRTUAL TABLE outer_join 349USING SPAN_OUTER_JOIN(table_x, table_y); 350``` 351 352NOTE: there is a subtlety if the partitioned table is empty and is 353either a) part of an outer join b) on the right side of a left join. 354In this case, *no* slices will be emitted even if the other table is 355non-empty. This approach was decided as being the most natural 356after considering how span joins are used in practice. 357 358### Ancestor slice 359ancestor_slice is a custom operator table that takes a 360[slice table's id column](/docs/analysis/sql-tables.autogen#slice) and computes 361all slices on the same track that are direct parents above that id (i.e. given 362a slice id it will return as rows all slices that can be found by following 363the parent_id column to the top slice (depth = 0)). 364 365The returned format is the same as the 366[slice table](/docs/analysis/sql-tables.autogen#slice) 367 368For example, the following finds the top level slice given a bunch of slices of 369interest. 370 371```sql 372CREATE VIEW interesting_slices AS 373SELECT id, ts, dur, track_id 374FROM slice WHERE name LIKE "%interesting slice name%"; 375 376SELECT 377 * 378FROM 379 interesting_slices LEFT JOIN 380 ancestor_slice(interesting_slices.id) AS ancestor ON ancestor.depth = 0 381``` 382 383### Ancestor slice by stack 384ancestor_slice_by_stack is a custom operator table that takes a 385[slice table's stack_id column](/docs/analysis/sql-tables.autogen#slice) and 386finds all slice ids with that stack_id, then, for each id it computes 387all the ancestor slices similarly to 388[ancestor_slice](/docs/analysis/trace-processor#ancestor-slice). 389 390The returned format is the same as the 391[slice table](/docs/analysis/sql-tables.autogen#slice) 392 393For example, the following finds the top level slice of all slices with the 394given name. 395 396```sql 397CREATE VIEW interesting_stack_ids AS 398SELECT stack_id 399FROM slice WHERE name LIKE "%interesting slice name%"; 400 401SELECT 402 * 403FROM 404 interesting_stack_ids LEFT JOIN 405 ancestor_slice_by_stack(interesting_stack_ids.stack_id) AS ancestor 406 ON ancestor.depth = 0 407``` 408 409### Descendant slice 410descendant_slice is a custom operator table that takes a 411[slice table's id column](/docs/analysis/sql-tables.autogen#slice) and 412computes all slices on the same track that are nested under that id (i.e. 413all slices that are on the same track at the same time frame with a depth 414greater than the given slice's depth. 415 416The returned format is the same as the 417[slice table](/docs/analysis/sql-tables.autogen#slice) 418 419For example, the following finds the number of slices under each slice of 420interest. 421 422```sql 423CREATE VIEW interesting_slices AS 424SELECT id, ts, dur, track_id 425FROM slice WHERE name LIKE "%interesting slice name%"; 426 427SELECT 428 * 429 ( 430 SELECT 431 COUNT(*) AS total_descendants 432 FROM descendant_slice(interesting_slice.id) 433 ) 434FROM interesting_slices 435``` 436 437### Descendant slice by stack 438descendant_slice_by_stack is a custom operator table that takes a 439[slice table's stack_id column](/docs/analysis/sql-tables.autogen#slice) and 440finds all slice ids with that stack_id, then, for each id it computes 441all the descendant slices similarly to 442[descendant_slice](/docs/analysis/trace-processor#descendant-slice). 443 444The returned format is the same as the 445[slice table](/docs/analysis/sql-tables.autogen#slice) 446 447For example, the following finds the next level descendant of all slices with 448the given name. 449 450```sql 451CREATE VIEW interesting_stacks AS 452SELECT stack_id, depth 453FROM slice WHERE name LIKE "%interesting slice name%"; 454 455SELECT 456 * 457FROM 458 interesting_stacks LEFT JOIN 459 descendant_slice_by_stack(interesting_stacks.stack_id) AS descendant 460 ON descendant.depth = interesting_stacks.depth + 1 461``` 462 463### Connected/Following/Preceding flows 464 465DIRECTLY_CONNECTED_FLOW, FOLLOWING_FLOW and PRECEDING_FLOW are custom operator 466tables that take a 467[slice table's id column](/docs/analysis/sql-tables.autogen#slice) and collect 468all entries of [flow table](/docs/analysis/sql-tables.autogen#flow), that are 469directly or indirectly connected to the given starting slice. 470 471`DIRECTLY_CONNECTED_FLOW(start_slice_id)` - contains all entries of 472[flow table](/docs/analysis/sql-tables.autogen#flow) that are present in any 473chain of kind: `flow[0] -> flow[1] -> ... -> flow[n]`, where 474`flow[i].slice_out = flow[i+1].slice_in` and `flow[0].slice_out = start_slice_id 475OR start_slice_id = flow[n].slice_in`. 476 477NOTE: Unlike the following/preceding flow functions, this function will not 478include flows connected to ancestors or descendants while searching for flows 479from a slice. It only includes the slices in the directly connected chain. 480 481`FOLLOWING_FLOW(start_slice_id)` - contains all flows which can be reached from 482a given slice via recursively following from flow's outgoing slice to its 483incoming one and from a reached slice to its child. The return table contains 484all entries of [flow table](/docs/analysis/sql-tables.autogen#flow) that are 485present in any chain of kind: `flow[0] -> flow[1] -> ... -> flow[n]`, where 486`flow[i+1].slice_out IN DESCENDANT_SLICE(flow[i].slice_in) OR 487flow[i+1].slice_out = flow[i].slice_in` and `flow[0].slice_out IN 488DESCENDANT_SLICE(start_slice_id) OR flow[0].slice_out = start_slice_id`. 489 490`PRECEDING_FLOW(start_slice_id)` - contains all flows which can be reached from 491a given slice via recursively following from flow's incoming slice to its 492outgoing one and from a reached slice to its parent. The return table contains 493all entries of [flow table](/docs/analysis/sql-tables.autogen#flow) that are 494present in any chain of kind: `flow[n] -> flow[n-1] -> ... -> flow[0]`, where 495`flow[i].slice_in IN ANCESTOR_SLICE(flow[i+1].slice_out) OR flow[i].slice_in = 496flow[i+1].slice_out` and `flow[0].slice_in IN ANCESTOR_SLICE(start_slice_id) OR 497flow[0].slice_in = start_slice_id`. 498 499```sql 500--number of following flows for each slice 501SELECT (SELECT COUNT(*) FROM FOLLOWING_FLOW(slice_id)) as following FROM slice; 502``` 503 504## Metrics 505 506TIP: To see how to add to add a new metric to trace processor, see the checklist 507[here](/docs/contributing/common-tasks.md#new-metric). 508 509The metrics subsystem is a significant part of trace processor and thus is 510documented on its own [page](/docs/analysis/metrics.md). 511 512## Creating derived events 513 514TIP: To see how to add to add a new annotation to trace processor, see the 515 checklist [here](/docs/contributing/common-tasks.md#new-annotation). 516 517This feature allows creation of new events (slices and counters) from the data 518in the trace. These events can then be displayed in the UI tracks as if they 519were part of the trace itself. 520 521This is useful as often the data in the trace is very low-level. While low 522level information is important for experts to perform deep debugging, often 523users are just looking for a high level overview without needing to consider 524events from multiple locations. 525 526For example, an app startup in Android spans multiple components including 527`ActivityManager`, `system_server`, and the newly created app process derived 528from `zygote`. Most users do not need this level of detail; they are only 529interested in a single slice spanning the entire startup. 530 531Creating derived events is tied very closely to 532[metrics subsystem](/docs/analysis/metrics.md); often SQL-based metrics need to 533create higher-level abstractions from raw events as intermediate artifacts. 534 535From previous example, the 536[startup metric](/src/trace_processor/metrics/sql/android/android_startup.sql) 537creates the exact `launching` slice we want to display in the UI. 538 539The other benefit of aligning the two is that changes in metrics are 540automatically kept in sync with what the user sees in the UI. 541 542## Python API 543The trace processor's C++ library is also exposed through Python. This 544is documented on a [separate page](/docs/analysis/trace-processor-python.md). 545 546## Testing 547 548Trace processor is mainly tested in two ways: 5491. Unit tests of low-level building blocks 5502. "Diff" tests which parse traces and check the output of queries 551 552### Unit tests 553Unit testing trace processor is the same as in other parts of Perfetto and 554other C++ projects. However, unlike the rest of Perfetto, unit testing is 555relatively light in trace processor. 556 557We have discovered over time that unit tests are generally too brittle 558when dealing with code which parses traces leading to painful, mechanical 559changes being needed when refactorings happen. 560 561Because of this, we choose to focus on diff tests for most areas (e.g. 562parsing events, testing schema of tables, testing metrics etc.) and only 563use unit testing for the low-level building blocks on which the rest of 564trace processor is built. 565 566### Diff tests 567Diff tests are essentially integration tests for trace processor and the 568main way trace processor is tested. 569 570Each diff test takes as input a) a trace file b) a query file *or* a metric 571name. It runs `trace_processor_shell` to parse the trace and then executes 572the query/metric. The result is then compared to a 'golden' file and any 573difference is highlighted. 574 575All diff tests are organized under [test/trace_processor](/test/trace_processor) 576in `tests{_category name}.py` files as methods of a class in each file 577and are run by the script 578[`tools/diff_test_trace_processor.py`](/tools/diff_test_trace_processor.py). 579To add a new test its enough to add a new method starting with `test_` in suitable 580python tests file. 581 582Methods can't take arguments and have to return `DiffTestBlueprint`: 583```python 584class DiffTestBlueprint: 585 trace: Union[Path, Json, Systrace, TextProto] 586 query: Union[str, Path, Metric] 587 out: Union[Path, Json, Csv, TextProto] 588``` 589*Trace* and *Out*: For every type apart from `Path`, contents of the object will be treated as 590file contents so it has to follow the same rules. 591 592*Query*: For metric tests it is enough to provide the metric name. For query tests there 593can be a raw SQL statement, for example `"SELECT * FROM SLICE"` or path to an `.sql` file. 594 595 596 597NOTE: `trace_processor_shell` and associated proto descriptors needs to be 598built before running `tools/diff_test_trace_processor.py`. The easiest way 599to do this is to run `tools/ninja -C <out directory>` both initially and on 600every change to trace processor code or builtin metrics. 601 602#### Choosing where to add diff tests 603`diff_tests/` folder contains four directories corresponding to different 604areas of trace processor. 6051. __stdlib__: Tests focusing on testing Perfetto Standard Library, both 606 prelude and the regular modules. The subdirectories in this folder 607 should generally correspond to directories in `perfetto_sql/stdlib`. 6082. __parser__: Tests focusing on ensuring that different trace files are 609 parsed correctly and the corresponding built-in tables are populated. 6103. __metrics__: Tests focusing on testing metrics located in 611 `trace_processor/metrics/sql`. This organisation is mostly historical 612 and code (and corresponding tests) is expected to move to `stdlib` over time. 6134. __syntax__: Tests focusing on testing the core syntax of PerfettoSQL 614 (i.e. `CREATE PERFETTO TABLE` or `CREATE PERFETTO FUNCTION`). 615 616__Scenario__: A new stdlib module `foo/bar.sql` is being added. 617 618_Answer_: Add the test to the `stdlib/foo/bar_tests.py` file. 619 620__Scenario__: A new event is being parsed, the focus of the test is to ensure 621the event is being parsed correctly. 622 623_Answer_: Add the test in one of the `parser` subdirectories. Prefer adding a 624test to an existing related directory (i.e. `sched`, `power`) if one exists. 625 626__Scenario__: A new metric is being added and the focus of the test is to 627ensure the metric is being correctly computed. 628 629_Answer_: Add the test in one of the `metrics` subdirectories. Prefer adding a 630test to an existing related directory if one exists. Also consider adding the 631code in question to stdlib. 632 633__Scenario__: A new dynamic table is being added and the focus of the test is to 634ensure the dynamic table is being correctly computed... 635 636_Answer_: Add the test to the `stdlib/dynamic_tables` folder 637 638__Scenario__: The interals of trace processor are being modified and the test 639is to ensure the trace processor is correctly filtering/sorting important 640built-in tables. 641 642_Answer_: Add the test to the `parser/core_tables` folder. 643 644## Appendix: table inheritance 645 646Concretely, the rules for inheritance between tables works are as follows: 647 648* Every row in a table has an `id` which is unique for a hierarchy of tables. 649 * For example, every `track` will have an `id` which is unique among all 650 tracks (regardless of the type of track) 651* If a table C inherits from P, each row in C will also be in P _with the same 652 id_ 653 * This allows for ids to act as "pointers" to rows; lookups by id can be 654 performed on any table which has that row 655 * For example, every `process_counter_track` row will have a matching row in 656 `counter_track` which will itself have matching rows in `track` 657* If a table C with columns `A` and `B` inherits from P with column `A`, `A` 658 will have the same data in both C and P 659 * For example, suppose 660 * `process_counter_track` has columns `name`, `unit` and `upid` 661 * `counter_track` has `name` and `unit` 662 * `track` has `name` 663 * Every row in `process_counter_track` will have the same `name` for the row 664 with the same id in `track` and `counter_track` 665 * Similarly, every row in `process_counter_track` will have both the same 666 `name ` and `unit` for the row with the same id in `counter_track` 667* Every row in a table has a `type` column. This specifies the _most specific_ 668 table this row belongs to. 669 * This allows _dynamic casting_ of a row to its most specific type 670 * For example, for if a row in the `track` is actually a 671 `process_counter_track`, it's type column will be `process_counter_track`. 672