Skip to content

feat: add opt-in subtractTimerOverhead option#568

Closed
jerome-benoit wants to merge 1 commit into
tinylibs:mainfrom
jerome-benoit:feat/subtract-timer-overhead
Closed

feat: add opt-in subtractTimerOverhead option#568
jerome-benoit wants to merge 1 commit into
tinylibs:mainfrom
jerome-benoit:feat/subtract-timer-overhead

Conversation

@jerome-benoit

Copy link
Copy Markdown
Collaborator

Summary

Add a new BenchOptions.subtractTimerOverhead boolean (default false). When enabled, the cost of one timestamp provider call is calibrated once at construction time via the new exported calibrateTimerOverhead helper, and subtracted from each raw latency sample (clamped to zero) before statistics are computed.

The estimate is exposed as bench.timerOverhead (number | undefined).

Implementation

  • calibrateTimerOverhead(provider, samples = 1024) returns the median of the strictly positive deltas of back-to-back now() calls, or 0 on coarse-timer runtimes.
  • The correction is applied post-loop, in Task#processRunResult, just before sortSamples. The sampling loop's exit condition keeps using raw timings and converges naturally even when the function under test runs at the timer grain — applying the correction inside #measure/#measureSync would zero-out samples on sub-µs functions and cause the OR-loop's totalTime < time condition to never be met.
  • overriddenDuration samples are still collected via the same path and corrected uniformly; the correction is opt-in so a benchmark relying on overridden durations should leave it disabled.

Caveats (documented in JSDoc)

Only location statistics (mean, percentiles) are corrected. Variance, sd, sem, moe and rme are unchanged because subtracting a constant does not reduce dispersion (Var(X − C) = Var(X) for a constant C). bench.timerOverhead is undefined when the option is off, 0 on runtimes whose timer resolution exceeds its own call cost.

Risk

None for existing benchmarks — option defaults to false, behavior strictly unchanged when omitted. No new runtime dependency. Bundle size impact is minimal (calibration helper + 5 LOC in the hot path).

Add a new BenchOptions.subtractTimerOverhead boolean (default false). When
enabled, the cost of one timestamp provider call is calibrated once at
construction time via the new exported calibrateTimerOverhead helper, and
subtracted from each raw latency sample (clamped to zero) before the
statistics are computed.

The correction is applied post-loop, in Task#processRunResult, so the
sampling loop's exit condition keeps using raw timings and converges
naturally even when the function under test runs at the timer grain.

Only location statistics (mean, percentiles) are corrected; variance and
relative margin of error are unchanged since subtracting a constant does
not reduce dispersion. The estimate is exposed as bench.timerOverhead
(undefined when the option is off, 0 on coarse-timer runtimes).
@jerome-benoit jerome-benoit requested a review from Uzlopak as a code owner May 30, 2026 00:34
@pkg-pr-new

pkg-pr-new Bot commented May 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/tinylibs/tinybench@568

commit: 8ab8fc7

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8ab8fc7a18

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/utils.ts
Comment on lines +358 to +359
if (delta > 0) {
deltas.push(delta)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid treating coarse clock ticks as call overhead

For quantized providers such as the documented Date.now() custom provider, most back-to-back calls return the same value, but any pair that straddles a clock tick yields a positive delta, typically 1 ms. Because zero deltas are discarded here, one tick during calibration makes the median positive delta 1 ms instead of the intended coarse-timer no-op, so subtractTimerOverhead can subtract a full millisecond from every sample and clamp sub-ms tasks to zero. The calibration should account for the zero samples / clock resolution rather than using only positive deltas.

Useful? React with 👍 / 👎.

Comment thread src/task.ts
if (overhead !== undefined && overhead > 0) {
for (let i = 0; i < latencySamples.length; i++) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
latencySamples[i] = Math.max(0, latencySamples[i]! - overhead)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve totalTime as the measured cycle duration

When subtractTimerOverhead is enabled, mutating latencySamples here also changes the samples later summed into TaskResult.totalTime and period, even though the run loop stopped using the uncorrected timings. For fast tasks where raw samples are close to the calibrated overhead, the benchmark can run until the raw total reaches bench.time while the reported totalTime is far below that limit (or even 0), which breaks the existing semantics that totalTime is the actual benchmark cycle time rather than just a corrected statistic.

Useful? React with 👍 / 👎.

@jerome-benoit

Copy link
Copy Markdown
Collaborator Author

Closed in favor of #571 which consolidates this feature with #569 and #570 into a single coherent change. The 8-agent cross-validated audit identified that naively composing the three PRs would have caused the diagnostics in #569 and #570 to operate on the corrected-and-clamped sample set, producing artificially small detected-resolution values and false-positive saturation warnings whenever overhead subtraction was active. The consolidated PR also folds in the fixes for the issues surfaced by the same audit: bigint-precision-preserving subtraction in calibration (toMs(b - a)), JIT warmup before sample collection, coarse-timer fail-mode guard (returns 0 when C < R/2), 'min' | 'p05' estimator alternatives, and overriddenDuration-aware correction skip.

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.

1 participant