Draft: [POC] Blame range benchmark for #586593

Purpose

DEV-ONLY benchmark tooling that directly tests the cost-vs-range-size claim in #586593 note_3040921734:

the cost on the backend/git is analogous regardless of chunk size

If true, the slow-start strategy works (replace many small Gitaly calls with fewer larger ones at near-equivalent cost). If false, slow-start fails.

This MR exists as a permalink to the POC branch and to host the findings comment. It is not intended to be merged. See BLAME_BENCHMARK_POC.md for setup and usage.

Caveats

  • Scope: Gitaly RPC only. This benchmark measures gitaly_commit_client.raw_blame in isolation with warmup discarded. It deliberately excludes the rest of the request path (Gitlab::Blame, GraphQL serialization, User resolution via the Author fragment, Apollo, network).
  • All measurements were taken locally on a single GDK instance. Production behavior may differ. The ratio between range sizes is what validates the proposal — git blame walks the same history regardless of -L range, so the ratio is the load-bearing measurement, not the absolute timings.
  • The absolute timings here (~3s baseline for 70 lines) are lower than the ~8s figure cited in the original issue from staging. The discrepancy is expected for a local-vs-staging comparison and does not affect the proposal's premise. The slow-start win in production will be proportionally similar.
  • Single file tested. The internal cross-position consistency (1.21× variance) suggests the result generalizes, but worth re-validating on a second pathological file before final implementation.

Findings

Tested locally on GDK against a Linux kernel mirror, file MAINTAINERS (27,910 lines, deep history). The benchmark calls gitaly_commit_client.raw_blame directly at multiple range sizes from multiple file positions, in shuffled order, with warmup runs discarded and a fresh per-call request deadline budget.

Results: Gitaly median time per call

Position 70 lines 250 lines 600 lines 1000 lines Ratio (1000 vs 70)
Line 1 2980 ms 3150 ms 3426 ms 3644 ms 1.22×
Line 6977 (~25%) 2922 ms 3211 ms 3615 ms 3854 ms 1.32×
Line 13955 (~50%) 2553 ms 3125 ms 3324 ms 3777 ms 1.48×
Line 20932 (~75%) 3013 ms 3179 ms 3543 ms 3960 ms 1.31×

Each cell is the median of 3 runs (1 warmup discarded). Per-cell min/max stayed within ~3% of median across all measurements.

image

Verdict: Valid

A 14× larger range costs only 22-48% more time, depending on file position. Cross-position ratio variance is 1.21× — the conclusion is consistent across the file.

The proposal's premise holds: Gitaly's cost is dominated by history walking, which happens regardless of the requested range size.

Implications

For a user scrolling through 1000 lines of MAINTAINERS:

  • Current implementation (~14 sequential 70-line requests): ~14 × ~3.0s ≈ ~42s of cumulative Gitaly work
  • Slow-start (70 + 100 + 250 + 600 = 1020 lines in 4 requests): ~3.0 + ~3.1 + ~3.4 + ~3.5 ≈ ~13s of Gitaly work
  • Steady-state (5th trigger and beyond): one ~3.7s request per 1000 lines

The user-visible win is larger than the Gitaly-time win because each window's blame data covers all lines in the window, so subsequent scrolls into already-loaded territory render instantly from client cache.

Recommendation

Proceed with implementation. The proposal needs:

  1. Backend: lift the 100-line cap in Resolvers::BlameResolver. One-line change. The benchmark data above is the justification.
  2. Frontend scope is contained: blameData is already keyed by lineno and BlameInfo positions markers via DOM offset lookups, so blame fetching is logically independent from chunk rendering today. The only coupling is the 1:1 chunk→fetch mapping in requestBlameInfo in source_viewer.vue, which can be replaced with a range-tracking fetch coordinator (~150-250 LOC incl. tests) — a contained change, not a viewer refactor.

A no-backend-changes alternative (parallel small requests within the existing 100-line cap) is also viable and worth comparing during the frontend PoC.

What this branch adds

File Purpose
app/services/blame_benchmark/runner.rb Benchmark harness: shuffled runs, warmup discard, isolated per-call timing
app/services/blame_benchmark/verdict.rb Translates results into pass/fail with documented thresholds
app/views/projects/blame/benchmark.html.haml Single results view with per-position cards + overall verdict
app/controllers/projects/blame_controller.rb New benchmark action gated to Rails.env.development? or test?
config/routes/repository.rb New /-/blame_benchmark/<ref>/<path> route
BLAME_BENCHMARK_POC.md Setup + usage docs

How to reproduce

See BLAME_BENCHMARK_POC.md. Short version:

git checkout 586593-benchmark-poc
gdk restart rails
# Visit:
# http://gdk.test:3000/<namespace>/<project>/-/blame_benchmark/master/MAINTAINERS

Defaults run 4 file positions × 4 range sizes × 4 runs = 64 blame calls. Customize via ?runs=2&sizes=70,1000&start_lines=1,14000.

Edited by Jacques Erasmus

Merge request reports

Loading