Skip to content

Commit 0808d64

Browse files
committed
getting relative paths of the files
1 parent 5debb8f commit 0808d64

3 files changed

Lines changed: 65 additions & 23 deletions

File tree

benchmark/compare.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,38 @@ def _short(name: str, limit: int = 48) -> str:
9494
return name
9595

9696

97+
# Top-level dirs in the rules_py repo; used to reduce noisy pprof file paths
98+
# (external repo or absolute checkout paths) to a repo-relative, clickable path.
99+
_RULES_PY_ROOTS = ("py/", "uv/", "docs/", "e2e/", "examples/", "tools/")
100+
101+
102+
def _relpath(file: str | None, limit: int = 56) -> str:
103+
"""Reduce a pprof source file to something locatable.
104+
105+
<builtin> -> "builtin"; a bzlmod MODULE.bazel URL -> "bzlmod"; otherwise try
106+
to find a rules_py top-level dir and return from there (works for both the
107+
bzlmod external path and an absolute local-checkout path).
108+
"""
109+
if not file or file in ("<unknown>", ""):
110+
return ""
111+
if file == "<builtin>":
112+
return "builtin"
113+
if file.startswith("http"):
114+
return "bzlmod"
115+
for root in _RULES_PY_ROOTS:
116+
idx = file.find("/" + root)
117+
if idx != -1:
118+
rel = file[idx + 1:]
119+
return rel if len(rel) <= limit else rel[: limit - 1] + "\u2026"
120+
if "/external/" in file:
121+
after = file.split("/external/", 1)[1]
122+
parts = after.split("/", 1)
123+
rel = parts[1] if len(parts) > 1 else after
124+
return rel if len(rel) <= limit else rel[: limit - 1] + "\u2026"
125+
base = os.path.basename(file)
126+
return base if len(base) <= limit else base[: limit - 1] + "\u2026"
127+
128+
97129
def _starlark_section(main_result: dict[str, Any], pr_result: dict[str, Any]) -> str:
98130
"""Diagnostic Starlark CPU diff (PR vs main). Informational, not a gate.
99131
@@ -159,24 +191,27 @@ def combined_se(m_std: float, p_std: float) -> float:
159191
movers.sort(key=lambda x: x[3], reverse=True)
160192

161193
out += "**Top movers (candidates, \u0394 > 3\u03c3 marked):**\n\n"
162-
out += "| Function | main ms | PR ms | \u0394 ms | \u00b1 stderr | \u0394 % |\n|---|---|---|---|---|---|\n"
194+
out += "| Function | File | main ms | PR ms | \u0394 ms | \u00b1 stderr | \u0394 % |\n|---|---|---|---|---|---|---|\n"
163195
for name, m_ms, pr_ms, d, se in movers[:10]:
164196
if m_ms > 0:
165197
dpct = f"+{d / m_ms * 100:.0f}%"
166198
else:
167199
dpct = "new"
168200
flag = " \u26a0\ufe0f" if (se > 0 and d > FN_SIGMA * se) else ""
169201
out += (
170-
f"| `{_short(name)}` | {m_ms:.1f} | {pr_ms:.1f} | "
171-
f"+{d:.1f} | \u00b1{se:.1f} | {dpct}{flag} |\n"
202+
f"| `{_short(name)}` | `{_relpath(pr_fns[name].get('file'))}` | {m_ms:.1f} | "
203+
f"{pr_ms:.1f} | +{d:.1f} | \u00b1{se:.1f} | {dpct}{flag} |\n"
172204
)
173205

174206
hot = [r for r in pr_fns.values() if r.get("pct", 0.0) >= HOTSPOT_MIN_PCT][:10]
175207
if hot:
176208
out += "\n**Top by absolute time (PR):**\n\n"
177-
out += "| Function | PR ms | % of total |\n|---|---|---|\n"
209+
out += "| Function | File | PR ms | % of total |\n|---|---|---|---|\n"
178210
for r in hot:
179-
out += f"| `{_short(r['name'])}` | {r['mean_ms']:.1f} | {r.get('pct', 0.0):.1f}% |\n"
211+
out += (
212+
f"| `{_short(r['name'])}` | `{_relpath(r.get('file'))}` | "
213+
f"{r['mean_ms']:.1f} | {r.get('pct', 0.0):.1f}% |\n"
214+
)
180215

181216
out += (
182217
"\n> Sample-based attribution (Starlark CPU across the whole `--nobuild` run: "

benchmark/pprof_decode.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,12 @@ def _read_payload(path: str) -> bytes:
8787
return open(path, "rb").read()
8888

8989

90-
def decode_starlark_pprof(path: str) -> dict[str, float]:
91-
"""Return {function_name: cpu_ms} aggregated by leaf Starlark function.
90+
def decode_starlark_pprof(path: str) -> dict[str, tuple[float, str]]:
91+
"""Return {function_name: (cpu_ms, source_file)} aggregated by leaf function.
9292
93-
Returns {} if the profile is missing or unparseable (fail-open).
93+
source_file is the pprof Function.filename (e.g. an external repo path, or
94+
``<builtin>`` for native rules/methods). Returns {} on missing/unparseable
95+
profile (fail-open).
9496
"""
9597
data = _read_payload(path)
9698
top = _fields(data)
@@ -120,13 +122,14 @@ def s(idx: object | int) -> str:
120122
elif unit == "nanoseconds":
121123
val_idx, divisor = i, 1000000.0
122124

123-
# function: id -> name index
125+
# function: id -> (name index, filename index)
124126
fn_name: dict[int, int] = {}
127+
fn_file: dict[int, int] = {}
125128
for chunk in top.get(5, []):
126129
f = _fields(chunk)
127130
fid = f.get(1, [0])[0]
128-
name_idx = f.get(2, [0])[0]
129-
fn_name[fid] = name_idx
131+
fn_name[fid] = f.get(2, [0])[0]
132+
fn_file[fid] = f.get(4, [0])[0]
130133

131134
# location: id -> leaf function id (first line)
132135
loc_to_fn: dict[int, int] = {}
@@ -139,6 +142,7 @@ def s(idx: object | int) -> str:
139142
loc_to_fn[lid] = line.get(1, [0])[0]
140143

141144
totals: dict[str, float] = defaultdict(float)
145+
files: dict[str, str] = {}
142146
for chunk in top.get(2, []):
143147
loc_ids = _parse_packed_or_singles(chunk, 1)
144148
values = _parse_packed_or_singles(chunk, 2)
@@ -148,14 +152,15 @@ def s(idx: object | int) -> str:
148152
fid = loc_to_fn.get(leaf)
149153
if fid is None:
150154
continue
151-
name_idx = fn_name.get(fid)
152-
totals[s(name_idx)] += values[val_idx] / divisor
155+
name = s(fn_name.get(fid))
156+
totals[name] += values[val_idx] / divisor
157+
files.setdefault(name, s(fn_file.get(fid)))
153158

154-
return dict(totals)
159+
return {name: (ms, files.get(name, "<unknown>")) for name, ms in totals.items()}
155160

156161

157162
if __name__ == "__main__":
158163
import sys
159-
rows = sorted(decode_starlark_pprof(sys.argv[1]).items(), key=lambda kv: -kv[1])
160-
for name, ms in rows[:25]:
161-
print(f"{ms:9.2f} ms {name}")
164+
rows = sorted(decode_starlark_pprof(sys.argv[1]).items(), key=lambda kv: -kv[1][0])
165+
for name, (ms, _file) in rows[:25]:
166+
print(f"{ms:9.2f} ms {name} [{_file}]")

benchmark/profile_benchmark.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,27 +71,29 @@ def stats_ms(values_ms: list[float]) -> dict[str, float]:
7171
}
7272

7373

74-
def aggregate_starlark(star_runs: list[dict[str, float]]) -> dict[str, Any]:
75-
"""Aggregate per-run {fn: ms} dicts into per-function stats + a run-level total.
74+
def aggregate_starlark(star_runs: list[dict[str, tuple[float, str]]]) -> dict[str, Any]:
75+
"""Aggregate per-run {fn: (ms, file)} dicts into per-function stats + total.
7676
7777
Returns {'total': {mean_ms, stddev_ms, runs}, 'functions': [...]}. ALL
7878
functions are kept (truncation is render-time in compare.py) and each carries
79-
its stddev across runs so the comparator can flag only statistically
80-
significant deltas instead of run-to-run sampling noise.
79+
its stddev across runs and source file so the comparator can flag only
80+
statistically significant deltas and point at where to read the code.
8181
"""
8282
runs = len(star_runs)
8383
names: set[str] = set()
8484
for d in star_runs:
8585
names.update(d.keys())
8686
functions: list[dict[str, Any]] = []
8787
for name in names:
88-
series = [d.get(name, 0.0) for d in star_runs]
88+
series = [d[name][0] for d in star_runs if name in d]
89+
file = next((d[name][1] for d in star_runs if name in d and d[name][1]), "<unknown>")
8990
functions.append({
9091
"name": name,
92+
"file": file,
9193
"mean_ms": statistics.mean(series),
9294
"stddev_ms": statistics.stdev(series) if len(series) > 1 else 0.0,
9395
})
94-
totals = [sum(d.values()) for d in star_runs]
96+
totals = [sum(ms for (ms, _f) in d.values()) for d in star_runs]
9597
total_mean = statistics.mean(totals)
9698
total_std = statistics.stdev(totals) if len(totals) > 1 else 0.0
9799
for r in functions:

0 commit comments

Comments
 (0)