Skip to content

perf(benchmark): gate on Bazel analysis phase and report per-function Starlark cost#1316

Draft
xangcastle wants to merge 12 commits into
mainfrom
xangcastle/performance-profiling
Draft

perf(benchmark): gate on Bazel analysis phase and report per-function Starlark cost#1316
xangcastle wants to merge 12 commits into
mainfrom
xangcastle/performance-profiling

Conversation

@xangcastle

Copy link
Copy Markdown
Member

Rewrite the benchmark to gate on Bazel's analysis phase (runAnalysisPhase extracted from --profile) instead of process wall time, excluding JVM/IO noise. The two separate workspaces are merged into a single parameterized one where --packages drive both the analysis load and a fan-in py_binary whose venv scales with it, and the PR comment now surfaces a per-function Starlark CPU breakdown (via --starlark_cpu_profile) so that when something regresses it tells you which functions got slower not just that it did.


Changes are visible to end-users: no

Test plan

  • Covered by existing test cases

@xangcastle
xangcastle requested a review from jbedard July 16, 2026 18:18
@aspect-workflows

aspect-workflows Bot commented Jul 16, 2026

Copy link
Copy Markdown

✨ Aspect Workflows Tasks

📅 Fri Jul 17 18:25:30 UTC 2026

✅ 40 successful tasks

  • ✅ buildifier · ⏱ 16.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ gazelle · ⏱ 16.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ test-e2e-bazel-8 [test] · ⏱ 2m 21s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (197/197 passed)
  • ✅ test-e2e-bazel-9 [test] · ⏱ 2m 57s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (191/191 passed)
  • ✅ test-e2e-interpreter-build-config-bazel-8 [test] · ⏱ 20.6s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-build-config-bazel-9 [test] · ⏱ 1m 2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-input-validation-bazel-8 [test] · ⏱ 18.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-input-validation-bazel-9 [test] · ⏱ 50.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-runtime-metadata-bazel-8 [test] · ⏱ 19.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-e2e-interpreter-runtime-metadata-bazel-9 [test] · ⏱ 1m 3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-e2e-interpreter-toolchain-settings-bazel-8 [test] · ⏱ 15.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-toolchain-settings-bazel-9 [test] · ⏱ 1m 11s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-rules-proto-grpc-python-bazel-8 [test] · ⏱ 2m 11s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-rules-proto-grpc-python-bazel-9 [test] · ⏱ 2m 21s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-rules-python-interop-bazel-8 [test] · ⏱ 31.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (6/6 passed)
  • ✅ test-e2e-rules-python-interop-bazel-9 [test] · ⏱ 45.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (6/6 passed)
  • ✅ test-examples-debugger-bazel-8 [test] · ⏱ 25.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-debugger-bazel-9 [test] · ⏱ 1m 19s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-dev_deps-bazel-8 [test] · ⏱ 32.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-dev_deps-bazel-9 [test] · ⏱ 42.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-django-bazel-8 [test] · ⏱ 23.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-django-bazel-9 [test] · ⏱ 43.4s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-multi_version-bazel-8 [test] · ⏱ 29.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-examples-multi_version-bazel-9 [test] · ⏱ 1m 7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-examples-protobuf-bazel-8 [test] · ⏱ 1m 30s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-protobuf-bazel-9 [test] · ⏱ 2m 2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-py_binary-bazel-8 [test] · ⏱ 24.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_binary-bazel-9 [test] · ⏱ 38.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-py_pex_binary-bazel-8 [test] · ⏱ 21.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_pex_binary-bazel-9 [test] · ⏱ 39.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-py_venv-bazel-8 [test] · ⏱ 21.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-examples-py_venv-bazel-9 [test] · ⏱ 50.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-examples-pytest-bazel-8 [test] · ⏱ 58.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (9/9 passed)
  • ✅ test-examples-pytest-bazel-9 [test] · ⏱ 1m 11s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (9/9 passed)
  • ✅ test-examples-uv_pip_compile-bazel-8 [test] · ⏱ 28.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-uv_pip_compile-bazel-9 [test] · ⏱ 38.4s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-virtual_deps-bazel-8 [test] · ⏱ 24.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-virtual_deps-bazel-9 [test] · ⏱ 59.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-root-bazel-8 [test] · ⏱ 2m 46s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (250/250 passed)
  • ✅ test-root-bazel-9 [test] · ⏱ 3m 39s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (249/249 passed)

⏱ Last updated Fri Jul 17 18:29:41 UTC 2026 · 📊 GitHub API quota 1,245/15,000 (8% used, resets in 11m)
🚀 Powered by Aspect CLI (v2026.28.2)  |  Aspect Build · X · LinkedIn · YouTube

@xangcastle
xangcastle force-pushed the xangcastle/performance-profiling branch from 271921a to 6db41ca Compare July 16, 2026 18:24
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Bazel analysis benchmark

Version Analysis (ms) Median (ms) ± stddev Wall (ms) vs BCR vs main Packages Targets
BCR 2.0.0-alpha.4 (baseline) 6546.677 6581.486 ±186.154 11700.535 101 304
HEAD main
This PR

Analysis phase (runAnalysisPhase) extracted from --profile on Linux
Gate: analysis_ms, PR vs HEAD main (threshold: 10%). Wall time is informational (JVM/IO overhead). BCR is a historical baseline only.

@xangcastle
xangcastle force-pushed the xangcastle/performance-profiling branch from 6db41ca to 38e531d Compare July 16, 2026 18:37
@xangcastle
xangcastle marked this pull request as ready for review July 16, 2026 18:47
@xangcastle
xangcastle force-pushed the xangcastle/performance-profiling branch from 38e531d to 94513f0 Compare July 16, 2026 22:15

@tamird tamird left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The analysis-phase gate is useful; I found one issue that makes the per-function comparison report false new regressions.

Comment thread benchmark/profile_benchmark.py Outdated
… Starlark cost

Rewrite the benchmark to gate on Bazel's analysis phase (runAnalysisPhase extracted from --profile) instead of process wall time, excluding JVM/IO noise. The two separate workspaces are merged into a single parameterized one where --packages drive both the analysis load and a fan-in py_binary whose venv scales with it, and the PR comment now surfaces a per-function Starlark CPU breakdown (via --starlark_cpu_profile) so that when something regresses it tells you which functions got slower not just that it did.
@xangcastle
xangcastle force-pushed the xangcastle/performance-profiling branch from 94513f0 to 1fcb6bc Compare July 16, 2026 22:25
@xangcastle
xangcastle marked this pull request as draft July 16, 2026 22:42

@tamird tamird left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refreshed aggregation fixes the false-new report, but the new significance filter can now hide real per-function regressions and still has no cutoff regression test. I also found unrelated root lockfile churn introduced by the latest commit.

Comment thread benchmark/compare.py Outdated
Comment thread MODULE.bazel.lock Outdated
@xangcastle
xangcastle force-pushed the xangcastle/performance-profiling branch from 0808d64 to 750747b Compare July 16, 2026 23:09

@tamird tamird left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest profile-path change reintroduces sampling bias for intermittently observed functions. The earlier significance and remaining root-lockfile findings still apply, and the requested aggregation/comparator regressions are still absent.

Comment thread benchmark/profile_benchmark.py Outdated

@tamird tamird left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One additional significance issue: main and PR profiles can contain different numbers of usable Starlark runs, so their standard errors cannot share one sample count.

Comment thread benchmark/compare.py Outdated
se>0 treating deterministic regressions as no-signal
Conditional mean / dropped runs
Single n for both sides

@tamird tamird left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new revision fixes zero-filled sparse functions, per-function significance, deterministic movers, and unequal sample counts. Three attribution/aggregation gaps remain, and none of the requested statistical regressions were added. Please add focused cases for sparse and fully empty runs, cancelling speedups, zero variance, unequal counts, duplicate function names across files, and builtin caller attribution so later changes cannot silently reintroduce these diagnostics errors.

Comment thread benchmark/pprof_decode.py
values = _parse_packed_or_singles(chunk, 2)
if not loc_ids or val_idx >= len(values):
continue
leaf = loc_ids[0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This attributes every sample to the leaf, including builtins, while the rendered report says builtins are attributed to the caller. In real profiles alias/config_setting/glob therefore dominate the movers table instead of identifying actionable Starlark code. Walk outward through loc_ids to the first non-builtin frame, and cover a builtin-leaf/caller sample.

Comment thread benchmark/pprof_decode.py
if fid is None:
continue
name = s(fn_name.get(fid))
totals[name] += values[val_idx] / divisor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keying totals/files only by the short function name merges unrelated functions such as _impl from different .bzl files and keeps whichever filename was seen first. That can create false movers and point reviewers at the wrong source. Preserve normalized (file, function) identity through aggregation/comparison and add a same-name/different-file regression.

star_path if star_enabled else None, cwd=run_cwd)
analysis_us.append(a)
wall_ms.append(w)
if star:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A valid decoded profile with zero samples is {}, so this drops the entire measured run. The sparse-function zero-fill above still computes a conditional mean, understates variance, and reports the wrong run count whenever one run is fully empty. Append the empty observation whenever Starlark profiling is enabled (and distinguish an actual decode failure), with a fully empty-run regression. The nearby star_runs/run_once annotations also still say dict[str, float], but the decoder returns dict[str, tuple[float, str]].

Comment thread benchmark/compare.py
pr_total_ms = pr_total.get("mean_ms", 0.0)
delta_total = pr_total_ms - main_total_ms
pct_total = pct(main_total_ms, pr_total_ms) if main_total_ms else 0.0
total_se = _combined_se(main_total.get("stddev_ms", 0.0), main_n,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

total_se is now calculated but never used after removing the total gate. Please delete the dead calculation; the per-function standard errors below are the values that matter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants