Skip to content

refactor: adopt timeMedian in remaining benchmark timing loops#1905

Closed
carlos-alm wants to merge 1 commit into
fix/issue-1773-destructured-binding-kindfrom
fix/issue-1774-adopt-timemedian-sync-sites
Closed

refactor: adopt timeMedian in remaining benchmark timing loops#1905
carlos-alm wants to merge 1 commit into
fix/issue-1773-destructured-binding-kindfrom
fix/issue-1774-adopt-timemedian-sync-sites

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

  • Group A (5 sync sites — query-benchmark.ts's benchDepths/benchDiffImpact, incremental-benchmark.ts's parent-process nativeBatchMs/jsFallbackMs, benchmark.ts's benchQuery): converted to async, adopted timeMedian directly. Handled an await foo().bar precedence gotcha (parses as await (foo().bar), not (await foo()).bar) by adding explicit parens at all affected call sites.
  • Group B (2 side-data sites needing {ms, phases}, not just a numeric median): added timeMedianWithValue<T>(fn, runs, beforeEach?) to bench-timing.ts. The beforeEach hook preserves a real pre-existing methodology detail — the probe-file write happens before timing starts in the original code; folding it into the timed closure would have silently counted disk-write time as rebuild latency.

Closes #1774

Test plan

  • npm test — full suite green (3564 passed, 0 failed)
  • npm run lint — clean (scripts/ confirmed outside Biome's scope)
  • node --experimental-strip-types --check on all 4 files (scripts/ is excluded from tsconfig, so this is the actual syntax-validation mechanism these scripts rely on)
  • Methodology regression measured, not assumed: ran affected scripts' workers before/after (3x each) — latencies statistically indistinguishable; isolated microbenchmark of await fn() overhead found ~0.7μs/call, ~4 orders of magnitude below the smallest real latency in scope

Filed #1904 for an out-of-scope finding: query-benchmark.ts's hub-selection queries can non-deterministically resolve to any of 7 different nodes named buildGraph, undermining cross-run comparability.

Stacked on #1902 (base branch fix/issue-1773-destructured-binding-kind) — only the scripts/ diff is this issue's change.

Deduplicates the "loop N times, time each run, compute median" pattern
across the 7 remaining call sites that predated scripts/lib/bench-timing.ts:

- query-benchmark.ts's benchDepths()/benchDiffImpact() and
  incremental-benchmark.ts's parent-process nativeBatchMs/jsFallbackMs
  computations and benchmark.ts's benchQuery() now adopt timeMedian()
  directly (converted to async; propagation stops at each call site
  since all callers are already at ESM top-level/top-level-await scope).

- incremental-benchmark.ts's and benchmark.ts's "1-file rebuild"
  measurements need the phases of the median-timed run, not just the
  numeric median, so bench-timing.ts gains timeMedianWithValue(fn, runs,
  beforeEach?) — returns the {ms, value} pair for the median-duration
  run, with an optional untimed beforeEach(i) hook for per-iteration
  setup (writing the probe file) that must stay outside the timed
  window.

Verified no methodology regression: ran query-benchmark.ts and
incremental-benchmark.ts before/after on this repo (3 runs and 1 run
respectively) with consistent latencies, plus an isolated microbenchmark
quantifying the await-on-sync-value overhead at ~1 microsecond/call —
negligible against the millisecond-to-hundred-millisecond latencies
these scripts measure.

Fixes #1774

Impact: 4 functions changed, 4 affected
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR refactors four benchmark scripts to eliminate duplicated timing loops by adopting the shared timeMedian helper and adding a new timeMedianWithValue<T> variant for sites that need both a median latency and the associated side data from the winning run.

  • bench-timing.ts: Adds timeMedianWithValue<T> — runs fn N times, collects { ms, value } per run, sorts by duration, and returns the median-index pair. An optional beforeEach(i) hook runs outside the timed window for per-iteration setup.
  • benchmark.ts / incremental-benchmark.ts: 1-file rebuild loops switch to timeMedianWithValue (probe-file writes in beforeEach); scalar loops switch to timeMedian. Warmup/cleanup methodology preserved exactly.
  • query-benchmark.ts: benchDepths and benchDiffImpact made async, converted to timeMedian; property-access call sites correctly parenthesised for await precedence.

Confidence Score: 4/5

The refactor is a clean mechanical substitution — all timing methodology is preserved and the new timeMedianWithValue helper is straightforward. The two observations are minor and non-blocking.

All four files convert correctly from manual timing loops to the shared helpers. The beforeEach hook correctly excludes probe-file I/O from measured time. The await precedence issue with property access is handled correctly at every call site. The two notes flagged are both non-blocking: timeMedianWithValue returning undefined for runs=0 is not reachable at current call sites, and the lastResult mismatch in benchDiffImpact is a pre-existing semantic quirk that deterministic query results make harmless.

scripts/lib/bench-timing.ts (missing empty-samples guard in timeMedianWithValue) and scripts/query-benchmark.ts (benchDiffImpact lastResult closure).

Important Files Changed

Filename Overview
scripts/lib/bench-timing.ts Adds timeMedianWithValue — collects { ms, value } pairs, sorts by duration, returns the median-index pair. Minor robustness gap: no guard when runs=0, unlike median()'s arr.length===0 check.
scripts/query-benchmark.ts Converts benchDepths/benchDiffImpact to async; adopts timeMedian; correctly adds parens for await precedence. benchDiffImpact still captures side data via mutable closure — lastResult is always the last sequential run, not the median-duration run.
scripts/benchmark.ts Converts benchQuery to async, uses timeMedianWithValue for 1-file rebuild timing with probe-file write in beforeEach, relies on function-declaration hoisting for the forward reference at line 137 — all correct.
scripts/incremental-benchmark.ts Parent-process import-resolution loops converted from manual loops to timeMedian; worker-side 1-file rebuild adopts timeMedianWithValue with beforeEach for probe-file writes. Both conversions are methodologically equivalent to the original code.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["timeMedian(fn, runs)"] --> B["Loop runs times\nawait fn\ncollect ms[]"]
    B --> C["median(ms[])"]
    C --> D["return number"]
    E["timeMedianWithValue(fn, runs, beforeEach?)"] --> F["Loop runs times\nbeforeEach(i) outside timer\nawait fn(i) inside timer\ncollect {ms, value}[]"]
    F --> G["sort by ms"]
    G --> H["samples[floor(N/2)]"]
    H --> I["return {ms, value}"]
    J["benchmark.ts 1-file rebuild"] -->|needs phases| E
    K["incremental-benchmark.ts 1-file rebuild"] -->|needs phases| E
    L["benchmark.ts benchQuery"] -->|scalar only| A
    M["incremental-benchmark.ts nativeBatchMs/jsFallbackMs"] -->|scalar only| A
    N["query-benchmark.ts benchDepths/benchDiffImpact"] -->|scalar only| A
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["timeMedian(fn, runs)"] --> B["Loop runs times\nawait fn\ncollect ms[]"]
    B --> C["median(ms[])"]
    C --> D["return number"]
    E["timeMedianWithValue(fn, runs, beforeEach?)"] --> F["Loop runs times\nbeforeEach(i) outside timer\nawait fn(i) inside timer\ncollect {ms, value}[]"]
    F --> G["sort by ms"]
    G --> H["samples[floor(N/2)]"]
    H --> I["return {ms, value}"]
    J["benchmark.ts 1-file rebuild"] -->|needs phases| E
    K["incremental-benchmark.ts 1-file rebuild"] -->|needs phases| E
    L["benchmark.ts benchQuery"] -->|scalar only| A
    M["incremental-benchmark.ts nativeBatchMs/jsFallbackMs"] -->|scalar only| A
    N["query-benchmark.ts benchDepths/benchDiffImpact"] -->|scalar only| A
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "refactor: adopt timeMedian in remaining ..." | Re-trigger Greptile

Comment on lines +83 to +86
}
samples.sort((a, b) => a.ms - b.ms);
return samples[Math.floor(samples.length / 2)];
}

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.

P2 Missing empty-samples guard

If runs is ever 0, samples is empty and samples[Math.floor(0/2)] returns undefined. Any caller that then accesses .ms or .value will throw a TypeError. The sibling median() handles this with an early if (arr.length === 0) return 0 guard — timeMedianWithValue has no equivalent, making it inconsistent with the rest of the module's defensive pattern.

Fix in Claude Code

Comment on lines 223 to 233
let lastResult = null;
for (let i = 0; i < RUNS; i++) {
const start = performance.now();
lastResult = diffImpactData(dbPath, { staged: true, depth: 3, noTests: true });
timings.push(performance.now() - start);
}
const latencyMs = round1(
await timeMedian(() => {
lastResult = diffImpactData(dbPath, { staged: true, depth: 3, noTests: true });
}, RUNS),
);

return {
latencyMs: round1(median(timings)),
latencyMs,
affectedFunctions: lastResult?.affectedFunctions?.length || 0,
affectedFiles: lastResult?.affectedFiles?.length || 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.

P2 lastResult reflects the last chronological run, not the median-duration run

timeMedian runs iterations sequentially from i=0 to i=RUNS-1, so after the loop lastResult always holds the result of the final iteration — not the run whose timing was selected as the median. This creates a conceptual mismatch: latencyMs comes from the median-duration run, while affectedFunctions/affectedFiles come from whichever run happened to be last. timeMedianWithValue was purpose-built for exactly this shape (scalar timing + associated side data per run), and would keep the reported count attributable to the same run whose latency is reported.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

4 functions changed4 callers affected across 3 files

  • benchQuery in scripts/benchmark.ts:197 (2 transitive callers)
  • timeMedianWithValue in scripts/lib/bench-timing.ts:72 (2 transitive callers)
  • benchDepths in scripts/query-benchmark.ts:172 (1 transitive callers)
  • benchDiffImpact in scripts/query-benchmark.ts:200 (1 transitive callers)

@carlos-alm carlos-alm deleted the branch fix/issue-1773-destructured-binding-kind July 8, 2026 08:06
@carlos-alm carlos-alm closed this Jul 8, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant