Files
dolphin/Tools/PerformanceTrace/summarize_cpu_threads.py
T
Yaya48 c230971f07 IOS LLE: checkpoint NAND conformance and diagnostic tooling
Preserve formatted NAND boot and Wii Remote pairing, add DMA/SRAM conformance tests and a Wii comparison probe. Keep P1 profiling tools and native update A/B evidence; reject the unhelpful targeted SRAM shortcut.
2026-09-23 13:30:17 +02:00

101 lines
4.5 KiB
Python

"""Summarize CPU leaf samples per thread from an already-aligned P1 capture.
These are sample counts, not exact CPU times or a critical-path attribution.
"""
import argparse
from collections import Counter, defaultdict
import json
from pathlib import Path
from analyze_p1 import percentile
def summarize(path, pid):
by_thread = defaultdict(Counter)
processors = defaultdict(Counter)
sample_begin = None
sample_end = None
with path.open(encoding="utf-8") as source:
for line in source:
event = json.loads(line)
if event["native"] or event["pid"] != pid or event["event"] != "SampledProfile":
continue
data = event["data"]
tid = int(data["ThreadId"])
weight = int(data.get("Count", 1))
by_thread[tid][data.get("Symbol", "") or "<unresolved>"] += weight
processors[tid][int(data["Processor"])] += weight
sample_begin = event["qpc"] if sample_begin is None else min(sample_begin, event["qpc"])
sample_end = event["qpc"] if sample_end is None else max(sample_end, event["qpc"])
if sample_begin is None:
raise ValueError("No target CPU samples in the selected window")
frequency = None
previous_frame = {}
frame_deltas = defaultdict(list)
snapshots = {}
with path.open(encoding="utf-8") as source:
for line in source:
event = json.loads(line)
if not event["native"] or event["pid"] != pid:
continue
qpc = event["qpc"]
if not sample_begin <= qpc <= sample_end:
continue
data = event["data"]
if event["event"] == "Marker" and data["Name"] == "PerformanceMetricsReset":
previous_frame.clear()
elif event["event"] == "Frame":
if frequency is None:
frequency = int(data["QPCFrequency"])
elif frequency != int(data["QPCFrequency"]):
raise ValueError("Inconsistent frame QPC frequency")
kind = data["Kind"]
if kind in previous_frame:
frame_deltas[kind].append(1000 * (qpc - previous_frame[kind]) / frequency)
previous_frame[kind] = qpc
elif event["event"] == "Snapshot":
core = int(data["CoreId"])
if core not in snapshots:
snapshots[core] = [data, data]
else:
snapshots[core][1] = data
frames = {kind: {"intervals": len(deltas), "p50_ms": percentile(deltas, .5),
"p95_ms": percentile(deltas, .95), "p99_ms": percentile(deltas, .99)}
for kind, deltas in frame_deltas.items()}
return {
"pid": pid,
"sample_count": sum(sum(symbols.values()) for symbols in by_thread.values()),
"sample_window_qpc": {"begin": sample_begin, "end": sample_end,
"duration_seconds": (sample_end - sample_begin) / frequency
if frequency else None},
"frame_intervals_in_sample_window": frames,
"snapshot_deltas_in_sample_window": {
str(core): {field: last[field] - first[field]
for field in ("Instructions", "NativeInstructions", "SlowReads",
"SlowWrites", "Compiles", "Fallbacks")}
for core, (first, last) in snapshots.items()
},
"note": "Leaf sample counts and host frame intervals within the same selected window; no exact CPU duration or critical-path claim.",
"threads": [
{"tid": tid, "samples": sum(symbols.values()),
"processors": dict(sorted(processors[tid].items())),
"top_symbols": [{"symbol": symbol, "samples": count}
for symbol, count in symbols.most_common(12)]}
for tid, symbols in sorted(by_thread.items(),
key=lambda item: -sum(item[1].values()))
],
}
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("events", type=Path)
parser.add_argument("--pid", required=True, type=int)
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
result = summarize(args.events, args.pid)
with args.output.open("x", encoding="utf-8") as target:
json.dump(result, target, indent=2)
print(f"{result['sample_count']} CPU samples from {len(result['threads'])} threads")