summaryrefslogtreecommitdiff
path: root/libguile/whippet/ctf_to_json.py
diff options
context:
space:
mode:
authorAndy Wingo <wingo@pobox.com>2025-04-11 14:10:41 +0200
committerAndy Wingo <wingo@pobox.com>2025-04-11 14:10:41 +0200
commitdb181e67ff482ab08cbdb0c1a88997b3a886f900 (patch)
tree6d16fbca515ae3cf506a9d16dbc89c472446e5d9 /libguile/whippet/ctf_to_json.py
parentaf96820e072d18c49ac03e80c6f3466d568dc77d (diff)
parentf909438596f63ecd44c5b9a28385260714857361 (diff)
downloadguile-db181e67ff482ab08cbdb0c1a88997b3a886f900.tar.gz
Merged Whippet into libguile/whippet
Diffstat (limited to 'libguile/whippet/ctf_to_json.py')
-rwxr-xr-xlibguile/whippet/ctf_to_json.py160
1 files changed, 160 insertions, 0 deletions
diff --git a/libguile/whippet/ctf_to_json.py b/libguile/whippet/ctf_to_json.py
new file mode 100755
index 000000000..f6b7f429a
--- /dev/null
+++ b/libguile/whippet/ctf_to_json.py
@@ -0,0 +1,160 @@
+#!/usr/bin/env python3
+# Any copyright is dedicated to the Public Domain.
+# https://creativecommons.org/publicdomain/zero/1.0/
+#
+# Originally written by Andy Wingo <wingo@igalia.com>.
+
+import bt2 # From the babeltrace2 package.
+import sys
+import json
+from enum import Enum
+
+# Usage: ./ctf_to_json.py ~/lttng-traces/name-of-your-trace > foo.json
+#
+# Convert a Common Trace Format (CTF) trace, for example as produced by
+# LTTng, to the JSON-based Trace Event Format (TEF), for example as
+# consumed by `chrome://tracing`, `https://ui.perfetto.dev/`, or
+# `https://profiler.firefox.com`.
+
+# The Trace Event Format is documented here:
+#
+# https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview?tab=t.0
+
+# By default, events are emitted as EventPhase.INSTANT. We also support
+# rewriting the event stream so as to generate EventPhase.BEGIN /
+# EventPhase.END events for specific named events.
+
+synthetic_events = {
+ 'gc': ['whippet:mutator_cause_gc',
+ 'whippet:restarting_mutators'],
+ 'stop-the-world': ['whippet:requesting_stop',
+ 'whippet:mutators_stopped'],
+ 'trace': ['whippet:prepare_gc',
+ 'whippet:restarting_mutators'],
+ 'mutator-stopped': ['whippet:mutator_stopping',
+ 'whippet:mutator_restarted'],
+ 'trace-roots': ['whippet:trace_roots_begin',
+ 'whippet:trace_roots_end'],
+ 'trace-check-termination': ['whippet:trace_check_termination_begin',
+ 'whippet:trace_check_termination_end'],
+ 'trace-objects': ['whippet:trace_objects_begin',
+ 'whippet:trace_objects_end'],
+ 'trace-worker': ['whippet:trace_worker_begin',
+ 'whippet:trace_worker_end']
+}
+
+class EventPhase(Enum):
+ BEGIN = 'B'
+ END = 'E'
+ COMPLETE = 'X'
+ INSTANT = 'i'
+ COUNTER = 'C'
+ NESTABLE_START = 'b'
+ NESTABLE_INSTANT = 'n'
+ NESTABLE_END = 'e'
+ FLOW_START = 's'
+ FLOW_STEP = 't'
+ FLOW_END = 'f'
+ SAMPLE = 'P'
+ OBJECT_CREATED = 'N'
+ OBJECT_SNAPSHOT = 'O'
+ OBJECT_DESTROYED = 'D'
+ METADATA = 'M'
+ MEMORY_DUMP_GLOBAL = 'V'
+ MEMORY_DUMP_PROCESS = 'V'
+ MARK = 'R'
+ CLOCK_SYNC = 'c'
+ CONTEXT_BEGIN = '('
+ CONTEXT_END = ')'
+
+base_time = None
+def event_us(msg):
+ assert(msg.default_clock_snapshot.clock_class.name == 'monotonic')
+ assert(msg.default_clock_snapshot.clock_class.frequency == 1e9)
+ global base_time
+ ns = msg.default_clock_snapshot.value
+ if base_time is None:
+ base_time = ns
+ return (ns - base_time) * 1e-3
+
+def lower(x):
+ if isinstance(x, str) or isinstance(x, int) or isinstance(x, float):
+ return x
+ if isinstance(x, dict) or isinstance(x, bt2._StructureFieldConst):
+ return {lower(k):lower(v) for k, v in x.items()}
+ if isinstance(x, bt2._BoolValueConst) or isinstance(x, bt2._BoolFieldConst):
+ return bool(x)
+ if isinstance(x, bt2._EnumerationFieldConst):
+ return repr(x)
+ if isinstance(x, bt2._IntegerValueConst) or isinstance(x, bt2._IntegerFieldConst):
+ return int(x)
+ if isinstance(x, bt2._RealValueConst) or isinstance(x, bt2._RealFieldConst):
+ return float(x)
+ if isinstance(x, bt2._StringValueConst) or isinstance(x, bt2._StringFieldConst):
+ return str(x)
+ raise ValueError("Unexpected value from trace", x)
+
+# Specific Whippet events.
+synthetic_begin = {}
+synthetic_end = {}
+for synthetic, [begin, end] in synthetic_events.items():
+ synthetic_begin[begin] = []
+ synthetic_end[end] = []
+for synthetic, [begin, end] in synthetic_events.items():
+ synthetic_begin[begin].append(synthetic)
+ synthetic_end[end].append(synthetic)
+
+def put(str):
+ sys.stdout.write(str)
+
+need_comma = False
+def print_event(ev):
+ global need_comma
+ if need_comma:
+ sys.stdout.write(',\n ')
+ else:
+ need_comma = True
+ # It appears to be faster to make a string, then print the string,
+ # than to call json.dump with a file object.
+ # json.dump(ev, sys.stdout, ensure_ascii=False, check_circular=False)
+ put(json.dumps(ev, ensure_ascii=False, check_circular=False))
+
+def emit_event(msg, name, phase):
+ ev = {'name': name,
+ 'cat': 'whippet',
+ 'ph': phase.value,
+ 'ts': event_us(msg),
+ 'pid': lower(msg.event.common_context_field['vpid']),
+ 'tid': lower(msg.event.common_context_field['vtid']),
+ 'args': lower(msg.event.payload_field)}
+ print_event(ev)
+def emit_begin_event(msg, name):
+ emit_event(msg, name, EventPhase.BEGIN)
+def emit_end_event(msg, name):
+ emit_event(msg, name, EventPhase.END)
+
+def emit_events(msg):
+ emit_event(msg, msg.event.name, EventPhase.INSTANT)
+ for begin in synthetic_begin.get(msg.event.name, []):
+ emit_begin_event(msg, begin)
+ for end in synthetic_end.get(msg.event.name, []):
+ emit_end_event(msg, end)
+
+def ctf_to_json(path):
+ msg_it = bt2.TraceCollectionMessageIterator(path)
+ put('{\n')
+ put(' "traceEvents": [\n ')
+ for msg in msg_it:
+ if hasattr(msg, 'event'):
+ emit_events(msg)
+ put('\n')
+ put('\n ],\n')
+ put(' "displayTimeUnit": "ns"\n')
+ put('}\n')
+
+if len(sys.argv) != 2:
+ sys.stderr.write(
+ 'usage: ' + sys.argv[0] + ' ~/lttng-traces/name-of-your-trace\n')
+ sys.exit(1)
+else:
+ ctf_to_json(sys.argv[1])