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_blamein isolation with warmup discarded. It deliberately excludes the rest of the request path (Gitlab::Blame, GraphQL serialization,Userresolution via theAuthorfragment, 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 blamewalks the same history regardless of-Lrange, 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.
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:
- Backend: lift the 100-line cap in
Resolvers::BlameResolver. One-line change. The benchmark data above is the justification. - Frontend scope is contained:
blameDatais already keyed bylinenoandBlameInfopositions 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 inrequestBlameInfoinsource_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/MAINTAINERSDefaults run 4 file positions × 4 range sizes × 4 runs = 64 blame calls. Customize via ?runs=2&sizes=70,1000&start_lines=1,14000.
