Skip to content

Commit b8355f8

Browse files
authored
Extensible eBPF timeline attributes (#1162)
The purpose of this PR includes: 1. Providing a systematic way to add additional attributes to events on the timeline generated from eBPF traces. 2. Adding additional USDT probes in mmtk-core and display more useful information on the timeline. 3. Allowing users, especially VM binding developers, to extend the tracing scripts to visualize additional USDT probes in the binding. In previous PRs #1154 and #1158, we use "meta" events to add attributes to work packets, including the number of slots processed by ProcessEdgesWork and the number of allocated blocks visited by SweepChunk. This PR refactors the scripts to standardize the use of "meta" events. Specifically - The Rust program executes USDT trace points to expose additional details (such as the number of slots processed) during the execution of a work packet - `capture.bt` emits "meta" events for such USDTs, and - `visualize.py` adds attributes to the current unfinished GC event and/or work packet event of the current thread. In this way, the information about a GC or a work packet can be gradually added by multiple USDT trace points, providing more flexibility than having to provide all information at the beginning or the end of the work packet. Using this mechanism, we added a few more USDT trace points in mmtk-core, including: - Generation plans trigger a USDT trace point when they have decided if the current GC is a full-heap GC. The information is displayed on the big "GC" bar (in "Thread 0") for each GC on the timeline. - Immix-based plans trigger a USDT trace point when they have decided if the current GC is a defrag GC. Displayed on the "GC" bar, too. - Root-scanning packets now have "roots" attributes showing how many root slots or root nodes are reported. - ScanObjects work packets (as well as ProcessEdgesWork packets that scan objects immediately) now display the number of objects scanned. We also introduce extension mechanisms to `capture.py` and `visualize.py`. Both of them now accept the `-x` or `--extra` command line argument. Specifically, - `capture.py` uses `-x` to specify an additional script to be appended after the `capture.bt` script. - `visualize.py` uses `-x` to specify a Python script to handle unknown event names. The extension mechanism allows VM binding developers to insert custom USDT trace points in the VM binding, and extend the timeline tools to visualize those custom events.
1 parent 55e4017 commit b8355f8

File tree

13 files changed

+563
-69
lines changed

13 files changed

+563
-69
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ Cargo.lock
3030

3131
# Generated for testing tutorial code build
3232
/src/plan/mygc
33+
34+
# Python script cache
35+
__pycache__

src/plan/generational/immix/global.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,13 @@ impl<VM: VMBinding> Plan for GenImmix<VM> {
106106
#[allow(clippy::branches_sharing_code)]
107107
fn schedule_collection(&'static self, scheduler: &GCWorkScheduler<Self::VM>) {
108108
let is_full_heap = self.requires_full_heap_collection();
109+
probe!(mmtk, gen_full_heap, is_full_heap);
110+
109111
if !is_full_heap {
110-
debug!("Nursery GC");
112+
info!("Nursery GC");
111113
scheduler.schedule_common_work::<GenImmixNurseryGCWorkContext<VM>>(self);
112114
} else {
115+
info!("Full heap GC");
113116
crate::plan::immix::Immix::schedule_immix_full_heap_collection::<
114117
GenImmix<VM>,
115118
GenImmixMatureGCWorkContext<VM, TRACE_KIND_FAST>,

src/plan/sticky/immix/global.rs

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ impl<VM: VMBinding> Plan for StickyImmix<VM> {
8787
fn schedule_collection(&'static self, scheduler: &crate::scheduler::GCWorkScheduler<Self::VM>) {
8888
let is_full_heap = self.requires_full_heap_collection();
8989
self.gc_full_heap.store(is_full_heap, Ordering::SeqCst);
90+
probe!(mmtk, gen_full_heap, is_full_heap);
9091

9192
if !is_full_heap {
9293
info!("Nursery GC");

src/policy/immix/defrag.rs

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ impl Defrag {
7979
|| super::STRESS_DEFRAG
8080
|| (collect_whole_heap && user_triggered && full_heap_system_gc));
8181
info!("Defrag: {}", in_defrag);
82+
probe!(mmtk, immix_defrag, in_defrag);
8283
self.in_defrag_collection
8384
.store(in_defrag, Ordering::Release)
8485
}

src/scheduler/gc_work.rs

+24
Original file line numberDiff line numberDiff line change
@@ -717,10 +717,28 @@ impl<VM: VMBinding, DPE: ProcessEdgesWork<VM = VM>, PPE: ProcessEdgesWork<VM = V
717717
}
718718
}
719719

720+
/// For USDT tracepoints for roots.
721+
/// Keep in sync with `tools/tracing/timeline/visualize.py`.
722+
#[repr(usize)]
723+
enum RootsKind {
724+
NORMAL = 0,
725+
PINNING = 1,
726+
TPINNING = 2,
727+
}
728+
720729
impl<VM: VMBinding, DPE: ProcessEdgesWork<VM = VM>, PPE: ProcessEdgesWork<VM = VM>>
721730
RootsWorkFactory<VM::VMSlot> for ProcessEdgesWorkRootsWorkFactory<VM, DPE, PPE>
722731
{
723732
fn create_process_roots_work(&mut self, slots: Vec<VM::VMSlot>) {
733+
// Note: We should use the same USDT name "mmtk:roots" for all the three kinds of roots. A
734+
// VM binding may not call all of the three methods in this impl. For example, the OpenJDK
735+
// binding only calls `create_process_roots_work`, and the Ruby binding only calls
736+
// `create_process_pinning_roots_work`. Because `ProcessEdgesWorkRootsWorkFactory<VM, DPE,
737+
// PPE>` is a generic type, the Rust compiler emits the function bodies on demand, so the
738+
// resulting machine code may not contain all three USDT trace points. If they have
739+
// different names, and our `capture.bt` mentions all of them, `bpftrace` may complain that
740+
// it cannot find one or more of those USDT trace points in the binary.
741+
probe!(mmtk, roots, RootsKind::NORMAL, slots.len());
724742
crate::memory_manager::add_work_packet(
725743
self.mmtk,
726744
WorkBucketStage::Closure,
@@ -729,6 +747,7 @@ impl<VM: VMBinding, DPE: ProcessEdgesWork<VM = VM>, PPE: ProcessEdgesWork<VM = V
729747
}
730748

731749
fn create_process_pinning_roots_work(&mut self, nodes: Vec<ObjectReference>) {
750+
probe!(mmtk, roots, RootsKind::PINNING, nodes.len());
732751
// Will process roots within the PinningRootsTrace bucket
733752
// And put work in the Closure bucket
734753
crate::memory_manager::add_work_packet(
@@ -739,6 +758,7 @@ impl<VM: VMBinding, DPE: ProcessEdgesWork<VM = VM>, PPE: ProcessEdgesWork<VM = V
739758
}
740759

741760
fn create_process_tpinning_roots_work(&mut self, nodes: Vec<ObjectReference>) {
761+
probe!(mmtk, roots, RootsKind::TPINNING, nodes.len());
742762
crate::memory_manager::add_work_packet(
743763
self.mmtk,
744764
WorkBucketStage::TPinningClosure,
@@ -823,6 +843,10 @@ pub trait ScanObjectsWork<VM: VMBinding>: GCWork<VM> + Sized {
823843
}
824844
}
825845

846+
let total_objects = objects_to_scan.len();
847+
let scan_and_trace = scan_later.len();
848+
probe!(mmtk, scan_objects, total_objects, scan_and_trace);
849+
826850
// If any object does not support slot-enqueuing, we process them now.
827851
if !scan_later.is_empty() {
828852
let object_tracer_context = ProcessEdgesWorkTracerContext::<Self::E> {

tools/tracing/README.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ shipped with the MMTk release you use.
1717
Currently, the core provides the following tracepoints.
1818

1919
- `mmtk:collection_initialized()`: All GC worker threads are spawn
20-
- `mmtk:prepare_fork()`: The VM requests MMTk core to prepare for calling `fork()`.
20+
- `mmtk:prepare_to_fork()`: The VM requests MMTk core to prepare for calling `fork()`.
2121
- `mmtk:after_fork()`: The VM notifies MMTk core it has finished calling `fork()`.
2222
- `mmtk:goal_set(goal: int)`: GC workers have started working on a goal.
2323
- `mmtk:goal_complete(goal: int)`: GC workers have fihisned working on a goal.
@@ -27,9 +27,22 @@ Currently, the core provides the following tracepoints.
2727
- `mmtk:gcworker_exit()`: a GC worker thread exits its work loop
2828
- `mmtk:gc_start()`: a collection epoch starts
2929
- `mmtk:gc_end()`: a collection epoch ends
30+
- `mmtk:gen_full_heap(is_full_heap: bool)`: the generational plan has determined whether the current
31+
GC is a full heap GC. Only executed if the plan is generational.
32+
- `mmtk:immix_defrag(is_defrag_gc: bool)`: the Immix-based plan has determined whether the current
33+
GC is a defrag GC. Only executed if the plan is Immix-based (i.e. Immix, GenImmix and
34+
StickyImmix). Will not be executed during nursery GCs (for GenImmix and StickyImmix).
35+
- `mmtk:roots(kind: int, len: int)`: reporing roots to mmtk-core during root scanning. `kind` can
36+
be 0, 1 or 2 for normal roots, pinning roots and transitively pinning roots, respectively.
37+
`len` is the number of slots or nodes reported.
3038
- `mmtk:process_slots(num_slots: int, is_roots: bool)`: an invocation of the `process_slots`
3139
method. The first argument is the number of slots to be processed, and the second argument is
3240
whether these slots are root slots.
41+
- `mmtk:scan_objects(total_objects: int, scan_and_trace: int)`: an invocation of the
42+
`ScanObjectsWork::do_work_common` method. `total_objects` is the total number of objects in the
43+
work packet, and `scan_and_trace` is the number of objects scanned using the
44+
`Scanning::scan_object_and_trace_edges` method. Other objects are scanned using
45+
`Scanning::scan_object`.
3346
- `mmtk:sweep_chunk(allocated_blocks: int)`: an execution of the `SweepChunk` work packet (for
3447
both `MarkSweepSpace` and `ImmixSpace`). `allocated_blocks` is the number of allocated blocks
3548
in the chunk processed by the work packet.

tools/tracing/timeline/EXTENSION.md

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Extending the timeline tool
2+
3+
This document is mainly for VM binding developers who want to extend this timeline tool to trace and
4+
visualize VM-specific events on the timeline.
5+
6+
## Custom work packets in VM bindings
7+
8+
mmtk-core contains trace points that captures the beginning and end of *all* work packets. If a VM
9+
bindings defines its own work packets and execute them, they will automatically appear on the
10+
timeline, without needing to modify or extend any scripts.
11+
12+
But if you wish to add additional attributes to work packets or events and browse them in Perfetto
13+
UI, please read on.
14+
15+
## The output format of `capture.bt`
16+
17+
The capturing script `capture.bt` prints events in a text format. Each line contains
18+
comma-separated values:
19+
20+
```
21+
name,ph,tid,timestamp,arg0,arg1,arg2,...
22+
```
23+
24+
The `visualize.py` script will transform those lines into the [Trace Event Format], a JSON-based
25+
format suitable for Perfetto UI, like this: `{"name": name, "ph": ph, "tid": tid, "ts": ts}`.
26+
Possible values of the event type (or "phase", "ph") are defined by the [Trace Event Format]. For
27+
example, "B" and "E" represent the beginning and the end of a duration event, while "i" represents
28+
an instant event. Additional arguments (arg0, arg1, ...) are processed by `visualize.py` in ways
29+
specific to each event. Some arguments are added to the resulting JSON object, for example
30+
`{"name": name, ..., "args": {"is_roots": 1, "num_slots": 2}}` The data in "args" are
31+
human-readable, and can be displayed on Perfetto UI.
32+
33+
[Trace Event Format]: https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU
34+
35+
## Extending the capturing script
36+
37+
VM binding developers may insert custom USDT trace points into the VM binding. They need to be
38+
captured by bpftrace to be displayed on the timeline.
39+
40+
The `capture.py` can use the `-x` command line option to append a script after `capture.bt` which is
41+
the base bpftrace script used by `capture.py`. Because the extended script is simply appended to
42+
`capture.bt`, it has access to all "map" variables (`@`) defined in `capture.bt`, such as
43+
`@harness`, `@enable_print`, etc. The Python script `capture.py` also uses [template strings] to
44+
replace things starting with `$`. Specifically, `$MMTK` will be replaced with the path to the MMTk
45+
binary. It will affect the extended script, too.
46+
47+
[template strings]: https://docs.python.org/3/library/string.html#template-strings
48+
49+
For example, you can hack the [mmtk-openjdk](https://github.com/mmtk/mmtk-openjdk) binding and add a
50+
dependency to the `probe` crate in `Cargo.toml` if it doesn't already have one.
51+
52+
```toml
53+
probe = "0.5"
54+
```
55+
56+
Then add the following `probe!` macro in `stop_all_mutators` in `collection.rs`:
57+
58+
```rust
59+
probe!(mmtk_openjdk, hello);
60+
```
61+
62+
and create a bpftrace script `capture_openjdk_example.bt`:
63+
64+
```c
65+
usdt:$MMTK:mmtk_openjdk:hello {
66+
if (@enable_print) {
67+
printf("hello,i,%d,%lu\n", tid, nsecs);
68+
}
69+
}
70+
```
71+
72+
and use the `-x` command line option while invoking `capture.py`:
73+
74+
```shell
75+
./capture.py -x capture_openjdk_example.bt -m /path/to/libmmtk_openjdk.so ... > output.log
76+
```
77+
78+
and run a benchmark with OpenJDK (such as `lusearch`). Use the unmodified `visualize.py` to process
79+
the log, and you will see many arrows representing the "hello" events on the timeline. They should
80+
be quite obvious because each one will be a lone instant event right below a `StopMutators` work
81+
packet.
82+
83+
## Extending the visualization script
84+
85+
The `visualize.py` script also allows extension using the `-x` command line option. The `-x` option
86+
points to a Python script that implements two functions: `enrich_event_extra` and
87+
`enrich_meta_extra` (you may omit either one if you don't need). `enrich_event_extra` is used to
88+
process events that the `visualize.py` script doesn't recognize. We'll cover `enrich_meta_extra`
89+
later.
90+
91+
For example, modify the `probe!` macro and add an argument:
92+
93+
```rust
94+
probe!(mmtk_openjdk, hello, 42);
95+
```
96+
97+
and modify `capture_openjdk_example.bt` to print `arg0` in the CSV:
98+
99+
```c
100+
printf("hello,i,%d,%lu,%lu\n", tid, nsecs, arg0);
101+
```
102+
103+
and create a Python script `visualize_openjdk_example.py`:
104+
105+
```python
106+
def enrich_event_extra(log_processor, name, ph, tid, ts, result, rest):
107+
match name:
108+
case "hello":
109+
result["args"] |= {
110+
"the_number": int(rest[0]),
111+
}
112+
```
113+
114+
Process the log with `visualize.py`, adding a `-x` option:
115+
116+
```shell
117+
./visualize.py -x visualize_openjdk_example.py output.log
118+
```
119+
120+
Load the output into Perfetto UI and select the hello event, and you shall see the "the_number"
121+
argument in the "Arguments" block on the right side of the "Current Selection" panel.
122+
123+
## Meta events
124+
125+
The `capture.bt` script and its extensions can print events with type "meta" instead of the usual
126+
"B", "E", "i", etc. "meta" is not a valid event type defined by the [Trace Event Format]. While
127+
going through the log, the `visualize.py` script remembers the current GC and the current work
128+
packet each thread is executing. When `visualize.py` sees a "meta" event, it will find the
129+
previously created JSON objects for the beginning of the current GC and the beginning of the current
130+
work packet of the current thread, and modify them, usually by adding more arguments to the event
131+
using information (arguments) provided by the "meta" event. For example, the `gen_full_heap` "meta"
132+
event adds an argument to the "GC" bar (on the timeline of "Thread 0") to display whether the
133+
current GC is a full-heap GC (as opposed to nursery GC), and the `process_slots` "meta" event
134+
patches the work packet event with additional arguments to display the number of slots processed and
135+
whether the slots are roots.
136+
137+
Users can extend `visualize.py` and define the `enrich_meta_extra` function to handle "meta" events
138+
the `visualize.py` script doesn't recognize.
139+
140+
For example, hack the mmtk-openjdk binding again, and add the following `probe!` macro into
141+
`scan_vm_specific_roots` in `scanning.rs`:
142+
143+
```rust
144+
probe!(mmtk_openjdk, hello2, 43);
145+
```
146+
147+
and add the following `probe!` macro into `scan_roots_in_mutator_thread` in `scanning.rs`:
148+
149+
```rust
150+
probe!(mmtk_openjdk, hello3, 44);
151+
```
152+
153+
Capture the event in `capture_openjdk_example.bt`:
154+
155+
```c
156+
usdt:$MMTK:mmtk_openjdk:hello2 {
157+
printf("hello2,meta,%d,%lu,%lu\n", tid, nsecs, arg0);
158+
}
159+
160+
usdt:$MMTK:mmtk_openjdk:hello3 {
161+
if (@enable_print) {
162+
printf("hello3,meta,%d,%lu,%lu\n", tid, nsecs, arg0);
163+
}
164+
}
165+
```
166+
167+
Process the meta event in `visualize_openjdk_example.py`:
168+
169+
```python
170+
def enrich_meta_extra(log_processor, name, tid, ts, gc, wp, rest):
171+
if gc is not None:
172+
match name:
173+
case "hello2":
174+
gc["args"] |= {
175+
"the_number": int(rest[0]),
176+
}
177+
178+
if wp is not None:
179+
match name:
180+
case "hello3":
181+
wp["args"] |= {
182+
"the_number": int(rest[0]),
183+
}
184+
```
185+
186+
Run a benchmark, capture a log (with `-x capture_openjdk_example.bt`) and visualize it (with `-x
187+
visualize_openjdk_example.py`). Load it into Perfetto UI. Select a `GC` bar and you should see the
188+
`the_number` argument being 43; select a `ScanMutatorRoots` work packet, and you will see the
189+
`the_number` argument being 44. If you use `-e` to capture logs for every few GCs, you will find
190+
that the `the_number` argument also exists on GCs that don't record work packets. That's because we
191+
don't have `if (@enable_print)` for "hello2" in `capture_openjdk_example.bt`.
192+
193+
## Notes on dropped events
194+
195+
bpftrace may drop events, so it may fail to record the beginning of some work packets. This affects
196+
work packets defined in both mmtk-core and the VM binding. If this happens, `visualize.py` may see
197+
some "meta" events on threads which are apparently not executing any work packets. Such "meta"
198+
events are silently ignored.
199+
200+
## Advanced usage
201+
202+
The `enrich_event_extra` and `enrich_meta_extra` have the `LogProcessor` instance as their first
203+
parameters. This allows the functions to use the `LogProcessor` instance to store states that
204+
persist across multiple invocations of `enrich_event_extra` and `enrich_meta_extra`. For example,
205+
the VM binding can define its own duration events (those with "B" and "E" events and shown as a bar
206+
on the timeline). The extension script can exploit the dynamic nature of Python, and use instance
207+
variables of the `LogProcessor` instance to record the "current" event, and let subsequent "meta"
208+
events patch those "current" events.
209+
210+
The `LogProcessor` parameter also allows the extension script to hack into the internals of the log
211+
processor, just in case the extension mechanism is not flexible enough. This is not elegant in
212+
terms of encapsulation and API design, but the tools are designed to be practical and not getting in
213+
the user's ways. Since MMTk is a free open-source software, feel free to hack those tools to suit
214+
your specific needs, and discuss with MMTk developers in our official [Zulip channel].
215+
216+
[Zulip channel]: https://mmtk.zulipchat.com/
217+
218+
<!--
219+
vim: ts=4 sw=4 sts=4 et tw=100
220+
-->

tools/tracing/timeline/README.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# MMTk GC visualization
22

3-
This directory contains tools for visualizing the execution time of each work packet on a timeline.
3+
This directory contains tools for visualizing the execution time of each work packet on a timeline.
44

55
## Before Running
66

7-
Before running, you should make sure the [bpftrace] command line utility is installed.
7+
Before running, you should make sure the [bpftrace] command line utility is installed. You also
8+
need Python 3.10 or later.
89

910
[bpftrace]: https://github.com/iovisor/bpftrace
1011

@@ -129,6 +130,12 @@ like this:
129130

130131
![Perfetto UI timeline](./perfetto-example.png)
131132

133+
## Extending the timeline tool
134+
135+
VM binding developers can insert USDT trace points, too, and our scripts `capture.py` and
136+
`visualize.py` provides mechanisms for extension. Read [EXTENSION.md](EXTENSION.md) for more
137+
details.
138+
132139
## Known issues
133140

134141
### "(unknonwn:xxxx)" work packet names

0 commit comments

Comments
 (0)