CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
605 lines
22 KiB
Python
605 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# MIT License
|
|
#
|
|
# Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in all
|
|
# copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
"""
|
|
Export SQTT auto-instrumentation traces to Perfetto/Chrome JSON Trace Event
|
|
Format.
|
|
|
|
One JSON file per ui_* trace directory (each is its own SQTT time domain;
|
|
no cross-collection time alignment is attempted).
|
|
|
|
Open the resulting files at https://ui.perfetto.dev (drag & drop).
|
|
|
|
Usage:
|
|
# Auto-discover ui_*/ dirs and code objects under base_dir
|
|
python3 sqtt_perfetto.py build/test/ --demangle
|
|
|
|
# Write all JSONs into one directory
|
|
python3 sqtt_perfetto.py build/test/ -o /tmp/perfetto_out
|
|
|
|
# Convert cycles to nanoseconds using a known GPU clock rate
|
|
python3 sqtt_perfetto.py build/test/ --clock-rate-ghz 2.1
|
|
|
|
# Filter to one wave
|
|
python3 sqtt_perfetto.py build/test/ --cu 0 --simd 0 --wave 0
|
|
|
|
# Open the first generated trace in ui.perfetto.dev
|
|
python3 sqtt_perfetto.py build/test/ --show
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import webbrowser
|
|
from collections import defaultdict
|
|
from typing import Optional
|
|
|
|
from sqtt_data import (
|
|
FuncMap,
|
|
ShaderRecord,
|
|
WaveSpan,
|
|
build_kernel_to_co,
|
|
decode_marker,
|
|
demangle_name,
|
|
discover_base_dir,
|
|
find_wave_span_at,
|
|
load_funcmaps,
|
|
load_occupancy,
|
|
load_shaderdata,
|
|
make_funcmap_resolver,
|
|
merge_funcmaps,
|
|
preprocess_records,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Wave grouping helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
WaveKey = tuple[int, int, int, int, int] # (se, cu, simd, wave_id, instance)
|
|
|
|
|
|
def _build_slot_launches(
|
|
wave_spans: dict[tuple, list[WaveSpan]],
|
|
) -> dict[tuple, list[int]]:
|
|
"""Sorted launch times per (SE, CU, SIMD, wave_slot). Used to assign instance
|
|
indices to records that share a physical wave slot across relaunches."""
|
|
slot_launches: dict[tuple, list[int]] = defaultdict(list)
|
|
for wk, spans in wave_spans.items():
|
|
for span in spans:
|
|
slot_launches[wk].append(span.launch_time)
|
|
for wk in slot_launches:
|
|
slot_launches[wk].sort()
|
|
return slot_launches
|
|
|
|
|
|
def _wave_instance(slot_launches: dict[tuple, list[int]],
|
|
shader_engine: int, cu: int, simd: int, wave_id: int, time: int) -> int:
|
|
"""Index of the most recent launch at or before `time` for this slot."""
|
|
launches = slot_launches.get((shader_engine, cu, simd, wave_id), [])
|
|
if not launches:
|
|
return 0
|
|
lo, hi = 0, len(launches) - 1
|
|
result = 0
|
|
while lo <= hi:
|
|
mid = (lo + hi) // 2
|
|
if launches[mid] <= time:
|
|
result = mid
|
|
lo = mid + 1
|
|
else:
|
|
hi = mid - 1
|
|
return result
|
|
|
|
|
|
def group_by_wave_instance(
|
|
records: list[ShaderRecord],
|
|
wave_spans: dict[tuple, list[WaveSpan]],
|
|
filter_cu: Optional[int] = None,
|
|
filter_simd: Optional[int] = None,
|
|
filter_wave: Optional[int] = None,
|
|
) -> dict[WaveKey, list[ShaderRecord]]:
|
|
"""Group records by (SE, CU, SIMD, wave_id, instance), preserving time order."""
|
|
slot_launches = _build_slot_launches(wave_spans)
|
|
grouped: dict[WaveKey, list[ShaderRecord]] = defaultdict(list)
|
|
for rec in records:
|
|
if rec.flags & 2:
|
|
continue
|
|
if filter_cu is not None and rec.cu != filter_cu:
|
|
continue
|
|
if filter_simd is not None and rec.simd != filter_simd:
|
|
continue
|
|
if filter_wave is not None and rec.wave_id != filter_wave:
|
|
continue
|
|
inst = _wave_instance(slot_launches, rec.shader_engine, rec.cu, rec.simd,
|
|
rec.wave_id, rec.time)
|
|
grouped[(rec.shader_engine, rec.cu, rec.simd, rec.wave_id, inst)].append(rec)
|
|
for k in grouped:
|
|
grouped[k].sort(key=lambda r: r.time)
|
|
return grouped
|
|
|
|
|
|
def pack_tid(shader_engine: int, cu: int, simd: int, wave_id: int, instance: int) -> int:
|
|
"""Pack a shader-engine-aware wave key into a 64-bit thread id.
|
|
|
|
The lower 32 bits retain the old CU/SIMD/wave/instance layout; shader
|
|
engine occupies the high bits so equal wave slots on two engines do not
|
|
share a Perfetto track.
|
|
"""
|
|
return (shader_engine << 32) | (cu << 20) | (simd << 16) | (wave_id << 8) | instance
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Per-wave event emission
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Synthetic pid for records that fall outside any known wave span.
|
|
ORPHAN_PID = 0
|
|
ORPHAN_NAME = "unknown dispatch"
|
|
|
|
|
|
def _funcmap_for_dispatch(
|
|
dispatch_id: Optional[int],
|
|
dispatches: dict[str, str],
|
|
kernel_to_co: dict[str, int],
|
|
per_co: dict[int, FuncMap],
|
|
merged_fm: FuncMap,
|
|
) -> FuncMap:
|
|
"""Pick the funcmap to use for a given dispatch (per code object,
|
|
falling back to the merged map)."""
|
|
if dispatch_id is None:
|
|
return merged_fm
|
|
raw_name = dispatches.get(str(dispatch_id), "")
|
|
if not raw_name:
|
|
return merged_fm
|
|
co_id = kernel_to_co.get(raw_name)
|
|
if co_id is None:
|
|
return merged_fm
|
|
return per_co.get(co_id, merged_fm)
|
|
|
|
|
|
def emit_wave_events(
|
|
wave_records: list[ShaderRecord],
|
|
wave_key: WaveKey,
|
|
dispatches: dict[str, str],
|
|
kernel_to_co: dict[str, int],
|
|
per_co: dict[int, FuncMap],
|
|
merged_fm: FuncMap,
|
|
wave_spans: dict[tuple, list[WaveSpan]],
|
|
ts_scale: float,
|
|
) -> tuple[list[dict], int]:
|
|
"""Walk one wave's records and emit Perfetto B/E/i events.
|
|
|
|
Returns (events, unmatched_exits). Mirrors the control flow of
|
|
build_stacks in sqtt_flamegraph.py (dispatch-boundary close, exit_prev
|
|
pop / enter push, funcmap resolution) but emits trace events instead
|
|
of accumulating cycle counts.
|
|
"""
|
|
events: list[dict] = []
|
|
unmatched = 0
|
|
shader_engine, cu, simd, wave_id, instance = wave_key
|
|
tid = pack_tid(shader_engine, cu, simd, wave_id, instance)
|
|
base_wk = wave_key[:4]
|
|
spans = wave_spans.get(base_wk, [])
|
|
|
|
stack: list[tuple[str, str]] = [] # (name, mtype) pairs we've pushed
|
|
cur_dispatch: Optional[int] = None
|
|
cur_funcmap: FuncMap = merged_fm
|
|
|
|
def _ts(cycles: int) -> float:
|
|
return cycles * ts_scale
|
|
|
|
for rec in wave_records:
|
|
# Update dispatch context. When the dispatch changes mid-stream
|
|
# we synthesize E events for anything still on the stack -- a
|
|
# function entered under dispatch A doesn't outlive A.
|
|
span = find_wave_span_at(spans, rec.time)
|
|
dispatch_id = span.dispatch_id if span else ORPHAN_PID
|
|
|
|
if dispatch_id != cur_dispatch:
|
|
close_pid = cur_dispatch if cur_dispatch is not None else ORPHAN_PID
|
|
for _ in stack:
|
|
events.append({"ph": "E", "ts": _ts(rec.time),
|
|
"pid": close_pid, "tid": tid})
|
|
stack.clear()
|
|
cur_dispatch = dispatch_id
|
|
cur_funcmap = _funcmap_for_dispatch(
|
|
dispatch_id, dispatches, kernel_to_co, per_co, merged_fm)
|
|
|
|
marker_id, enter, exit_prev = decode_marker(rec.value, cur_funcmap)
|
|
if marker_id == 0 and not exit_prev:
|
|
continue # noop / reserved value
|
|
|
|
name, mtype = cur_funcmap.resolve(marker_id)
|
|
|
|
# Point markers (barriers, memory ops, user points) are instantaneous
|
|
# -- emit as Perfetto instant events with thread scope so they appear
|
|
# inline in the wave's track. They don't mutate the call stack.
|
|
if mtype == "point":
|
|
events.append({"ph": "i", "ts": _ts(rec.time),
|
|
"pid": dispatch_id, "tid": tid,
|
|
"name": name, "cat": "sqtt.point", "s": "t"})
|
|
continue
|
|
|
|
# exit_prev pops first, then enter pushes -- matches build_stacks
|
|
# ordering. Fused exit+enter (combined marker) does both.
|
|
if exit_prev:
|
|
if stack:
|
|
events.append({"ph": "E", "ts": _ts(rec.time),
|
|
"pid": dispatch_id, "tid": tid})
|
|
stack.pop()
|
|
else:
|
|
unmatched += 1
|
|
|
|
if enter:
|
|
events.append({"ph": "B", "ts": _ts(rec.time),
|
|
"pid": dispatch_id, "tid": tid,
|
|
"name": name, "cat": f"sqtt.{mtype}"})
|
|
stack.append((name, mtype))
|
|
|
|
# Close anything still open at end of wave. Use last_record.time + 1
|
|
# so the synthetic E sorts strictly after any real event at the same
|
|
# cycle.
|
|
if stack:
|
|
end_ts = _ts(wave_records[-1].time + 1)
|
|
close_pid = cur_dispatch if cur_dispatch is not None else ORPHAN_PID
|
|
for _ in stack:
|
|
events.append({"ph": "E", "ts": end_ts,
|
|
"pid": close_pid, "tid": tid})
|
|
|
|
return events, unmatched
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Metadata events (process / thread names)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def build_metadata_events(
|
|
pids_seen: set[int],
|
|
tids_seen: set[tuple],
|
|
dispatches: dict[str, str],
|
|
do_demangle: bool,
|
|
) -> list[dict]:
|
|
"""Emit Perfetto metadata events naming each process (dispatch) and
|
|
each thread (wave instance).
|
|
|
|
tids_seen is a set of (pid, tid, wave_key) triples so we can recover
|
|
the (SE, CU, SIMD, wave_id, instance) tuple for the thread name.
|
|
"""
|
|
metadata: list[dict] = []
|
|
|
|
for pid in sorted(pids_seen):
|
|
if pid == ORPHAN_PID:
|
|
name = ORPHAN_NAME
|
|
else:
|
|
raw = dispatches.get(str(pid), "")
|
|
display = demangle_name(raw) if (raw and do_demangle) else raw
|
|
name = f"dispatch {pid}: {display}" if display else f"dispatch {pid}"
|
|
metadata.append({
|
|
"ph": "M", "name": "process_name", "pid": pid,
|
|
"args": {"name": name},
|
|
})
|
|
|
|
for pid, tid, wave_key in sorted(tids_seen):
|
|
shader_engine, cu, simd, wave_id, instance = wave_key
|
|
thread_name = f"SE{shader_engine}/CU{cu}/SIMD{simd}/W{wave_id}#{instance}"
|
|
metadata.append({
|
|
"ph": "M", "name": "thread_name", "pid": pid, "tid": tid,
|
|
"args": {"name": thread_name},
|
|
})
|
|
|
|
return metadata
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Per-trace-dir export
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def export_trace_dir(
|
|
trace_dir: str,
|
|
per_co: dict[int, FuncMap],
|
|
kernel_to_co: dict[str, int],
|
|
merged_fm: FuncMap,
|
|
do_demangle: bool,
|
|
clock_rate_ghz: Optional[float],
|
|
filter_cu: Optional[int],
|
|
filter_simd: Optional[int],
|
|
filter_wave: Optional[int],
|
|
) -> Optional[tuple[list[dict], int, int]]:
|
|
"""Process one ui_* directory. Returns (json_events, n_events,
|
|
unmatched_exits) or None if the directory has no usable data.
|
|
|
|
json_events is the full list of trace events (data events + metadata)
|
|
ready to wrap in `{"displayTimeUnit": "ns", "traceEvents": ...}`.
|
|
"""
|
|
records = load_shaderdata(trace_dir)
|
|
if not records:
|
|
return None
|
|
dispatches, wave_spans = load_occupancy(trace_dir)
|
|
resolver = make_funcmap_resolver(per_co, dispatches, kernel_to_co, wave_spans, merged_fm)
|
|
records, _addr = preprocess_records(records, merged_fm, resolver)
|
|
if not records:
|
|
return None
|
|
|
|
grouped = group_by_wave_instance(
|
|
records, wave_spans,
|
|
filter_cu=filter_cu, filter_simd=filter_simd, filter_wave=filter_wave)
|
|
if not grouped:
|
|
return None
|
|
|
|
# ts_scale: cycles -> displayed time unit. Default leaves cycles as-is
|
|
# and labels them ns (proportions correct, absolute values nominal).
|
|
# With --clock-rate-ghz we convert to real ns.
|
|
ts_scale = (1.0 / clock_rate_ghz) if clock_rate_ghz else 1.0
|
|
|
|
all_events: list[dict] = []
|
|
pids_seen: set[int] = set()
|
|
tids_seen: set[tuple] = set()
|
|
total_unmatched = 0
|
|
|
|
for wave_key, recs in grouped.items():
|
|
evs, unmatched = emit_wave_events(
|
|
recs, wave_key, dispatches, kernel_to_co, per_co, merged_fm,
|
|
wave_spans, ts_scale)
|
|
total_unmatched += unmatched
|
|
all_events.extend(evs)
|
|
shader_engine, cu, simd, wave_id, instance = wave_key
|
|
tid = pack_tid(shader_engine, cu, simd, wave_id, instance)
|
|
for ev in evs:
|
|
pids_seen.add(ev["pid"])
|
|
tids_seen.add((ev["pid"], tid, wave_key))
|
|
|
|
if not all_events:
|
|
return None
|
|
|
|
metadata = build_metadata_events(pids_seen, tids_seen, dispatches,
|
|
do_demangle)
|
|
# Metadata first so Perfetto picks up names before rendering tracks.
|
|
return (metadata + all_events, len(all_events), total_unmatched)
|
|
|
|
|
|
def write_perfetto_json(
|
|
events: list[dict],
|
|
output_path: str,
|
|
) -> int:
|
|
"""Write the trace event list as a Chrome JSON Trace. Returns file size."""
|
|
payload = {
|
|
"displayTimeUnit": "ns",
|
|
"traceEvents": events,
|
|
}
|
|
# Compact separators — these files can get large.
|
|
with open(output_path, "w") as f:
|
|
json.dump(payload, f, separators=(",", ":"))
|
|
return os.path.getsize(output_path)
|
|
|
|
|
|
def output_path_for(trace_dir: str, out_dir: Optional[str]) -> str:
|
|
"""Resolve the output JSON path. With out_dir, files live in out_dir/.
|
|
Without, files are siblings of the ui_* directory.
|
|
"""
|
|
name = os.path.basename(os.path.normpath(trace_dir)) + ".perfetto.json"
|
|
if out_dir:
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
return os.path.join(out_dir, name)
|
|
return os.path.join(os.path.dirname(os.path.abspath(trace_dir)), name)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Self-test (no rocprofv3 needed)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _self_test() -> int:
|
|
"""Minimal in-memory smoke test of the event-emission pipeline.
|
|
|
|
Builds synthetic ShaderRecord/WaveSpan/FuncMap inputs, runs
|
|
export_trace_dir's core logic via emit_wave_events, and asserts the
|
|
resulting event list contains the expected B/E/i/M shape. Returns 0
|
|
on success, nonzero on failure.
|
|
"""
|
|
# Funcmap: id 1 = "compute" (function), id 2 = "barrier" (point).
|
|
fm = FuncMap()
|
|
fm.markers[1] = ("compute", "function")
|
|
fm.markers[2] = ("barrier", "point")
|
|
|
|
# Encode marker values: (id << 2) | enter | exit_prev
|
|
enter_compute = (1 << 2) | 0x2
|
|
exit_prev = 0x1
|
|
point_barrier = (2 << 2)
|
|
|
|
# One wave (SE=0, cu=0, simd=1, wave_id=3) running dispatch 7.
|
|
recs = [
|
|
ShaderRecord(time=100, value=enter_compute, cu=0, simd=1, wave_id=3, flags=0),
|
|
ShaderRecord(time=200, value=point_barrier, cu=0, simd=1, wave_id=3, flags=0),
|
|
ShaderRecord(time=300, value=exit_prev, cu=0, simd=1, wave_id=3, flags=0),
|
|
]
|
|
wave_spans = {(0, 0, 1, 3): [WaveSpan(launch_time=0, retire_time=1000, dispatch_id=7)]}
|
|
dispatches = {"7": "compute_kernel<int>"}
|
|
|
|
grouped = group_by_wave_instance(recs, wave_spans)
|
|
assert len(grouped) == 1, f"expected 1 wave group, got {len(grouped)}"
|
|
wave_key = next(iter(grouped))
|
|
assert wave_key == (0, 0, 1, 3, 0), wave_key
|
|
|
|
events, unmatched = emit_wave_events(
|
|
grouped[wave_key], wave_key, dispatches, kernel_to_co={},
|
|
per_co={}, merged_fm=fm, wave_spans=wave_spans, ts_scale=1.0)
|
|
assert unmatched == 0, f"unmatched exits: {unmatched}"
|
|
|
|
# Expect: B(compute), i(barrier), E(compute)
|
|
phs = [e["ph"] for e in events]
|
|
assert phs == ["B", "i", "E"], phs
|
|
assert events[0]["name"] == "compute" and events[0]["cat"] == "sqtt.function"
|
|
assert events[1]["name"] == "barrier" and events[1]["cat"] == "sqtt.point"
|
|
assert events[1]["s"] == "t"
|
|
assert all(e["pid"] == 7 for e in events)
|
|
expected_tid = pack_tid(0, 0, 1, 3, 0)
|
|
assert all(e["tid"] == expected_tid for e in events)
|
|
|
|
# B/E pairing (per (pid, tid))
|
|
depth = 0
|
|
for e in events:
|
|
if e["ph"] == "B":
|
|
depth += 1
|
|
elif e["ph"] == "E":
|
|
depth -= 1
|
|
assert depth >= 0, "negative stack depth"
|
|
assert depth == 0, f"unbalanced B/E (final depth {depth})"
|
|
|
|
# Metadata events
|
|
pids_seen = {e["pid"] for e in events}
|
|
tids_seen = {(e["pid"], expected_tid, wave_key) for e in events}
|
|
md = build_metadata_events(pids_seen, tids_seen, dispatches, do_demangle=False)
|
|
md_phs = {e["name"] for e in md if e["ph"] == "M"}
|
|
assert "process_name" in md_phs and "thread_name" in md_phs, md_phs
|
|
|
|
# Round-trip the full payload through json to be sure it serializes
|
|
payload = {"displayTimeUnit": "ns", "traceEvents": md + events}
|
|
encoded = json.dumps(payload)
|
|
reparsed = json.loads(encoded)
|
|
assert reparsed["traceEvents"][-1]["ph"] == "E"
|
|
|
|
print("self-test OK", file=sys.stderr)
|
|
return 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Export SQTT auto-instrumentation traces to Perfetto JSON")
|
|
parser.add_argument("base_dir", nargs="?",
|
|
help="Base directory containing trace output. Auto-discovers "
|
|
"ui_*/shaderdata_*.json and *code_object_id*.out files "
|
|
"recursively.")
|
|
parser.add_argument("--code-objects", "-c", nargs="+",
|
|
help="Code object files (.out) with .sqtt_funcmap sections. "
|
|
"If omitted, auto-detected from base_dir.")
|
|
parser.add_argument("--output-dir", "-o", default=None,
|
|
help="Directory to write *.perfetto.json files into. "
|
|
"Default: write next to each ui_* directory.")
|
|
parser.add_argument("--demangle", "-d", action="store_true",
|
|
help="Demangle C++ function and dispatch names")
|
|
parser.add_argument("--clock-rate-ghz", type=float, default=None,
|
|
help="Convert SQTT cycles to ns using this GPU clock rate. "
|
|
"Default: emit cycles as-is and label them ns.")
|
|
parser.add_argument("--cu", type=int, default=None,
|
|
help="Filter to specific CU/WGP ID")
|
|
parser.add_argument("--simd", type=int, default=None,
|
|
help="Filter to specific SIMD ID")
|
|
parser.add_argument("--wave", type=int, default=None,
|
|
help="Filter to specific wave ID")
|
|
parser.add_argument("--show", "-s", action="store_true",
|
|
help="Open ui.perfetto.dev in a browser after export. "
|
|
"(The user must drag-and-drop the generated JSON file.)")
|
|
parser.add_argument("--self-test", action="store_true",
|
|
help="Run an internal smoke test on synthetic records and exit.")
|
|
args = parser.parse_args()
|
|
|
|
if args.self_test:
|
|
return _self_test()
|
|
|
|
if not args.base_dir:
|
|
parser.error("base_dir is required (or use --self-test)")
|
|
|
|
base_dir = args.base_dir
|
|
if not os.path.isdir(base_dir):
|
|
print(f"Error: {base_dir} is not a directory", file=sys.stderr)
|
|
return 1
|
|
|
|
trace_dirs, discovered_cos = discover_base_dir(base_dir)
|
|
code_objects = args.code_objects or discovered_cos
|
|
|
|
if not trace_dirs:
|
|
print(f"Error: no shaderdata_*.json files found under {base_dir}",
|
|
file=sys.stderr)
|
|
return 1
|
|
if not code_objects:
|
|
print("Error: no code objects found. Use --code-objects.",
|
|
file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"Found {len(trace_dirs)} trace dir(s), "
|
|
f"{len(code_objects)} code object(s)", file=sys.stderr)
|
|
|
|
# Funcmaps: load unmangled to build the kernel->code_object map (which
|
|
# keys on mangled names from occupancy.json), then optionally a
|
|
# demangled copy for display.
|
|
per_co_raw = load_funcmaps(code_objects, do_demangle=False)
|
|
kernel_to_co = build_kernel_to_co(per_co_raw)
|
|
per_co = (load_funcmaps(code_objects, do_demangle=True)
|
|
if args.demangle else per_co_raw)
|
|
|
|
merged_fm = merge_funcmaps(per_co.values())
|
|
|
|
written: list[tuple[str, int, int]] = []
|
|
skipped: list[str] = []
|
|
total_unmatched = 0
|
|
|
|
for td in trace_dirs:
|
|
result = export_trace_dir(
|
|
td, per_co, kernel_to_co, merged_fm,
|
|
do_demangle=args.demangle,
|
|
clock_rate_ghz=args.clock_rate_ghz,
|
|
filter_cu=args.cu, filter_simd=args.simd, filter_wave=args.wave)
|
|
if result is None:
|
|
skipped.append(td)
|
|
continue
|
|
events, n_events, unmatched = result
|
|
total_unmatched += unmatched
|
|
out_path = output_path_for(td, args.output_dir)
|
|
size = write_perfetto_json(events, out_path)
|
|
written.append((out_path, size, n_events))
|
|
|
|
if not written:
|
|
print("Error: no perfetto traces produced", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"Wrote {len(written)} perfetto trace(s):", file=sys.stderr)
|
|
for path, size, n_events in written:
|
|
size_mb = size / (1024 * 1024)
|
|
print(f" {path} ({size_mb:.1f} MB, {n_events} events)",
|
|
file=sys.stderr)
|
|
if skipped:
|
|
print(f"Skipped {len(skipped)} empty trace dir(s)", file=sys.stderr)
|
|
if total_unmatched:
|
|
print(f"Note: {total_unmatched} unmatched exit marker(s) "
|
|
"(stack was empty when an exit was encountered)",
|
|
file=sys.stderr)
|
|
if not args.clock_rate_ghz:
|
|
print("Note: timestamps are SQTT cycles displayed as ns. "
|
|
"Pass --clock-rate-ghz to convert to real time.",
|
|
file=sys.stderr)
|
|
print("Open at https://ui.perfetto.dev (drag & drop)", file=sys.stderr)
|
|
|
|
if args.show:
|
|
webbrowser.open("https://ui.perfetto.dev")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|