Skip to content

Commit 0647d27

Browse files
feat: add timer-overhead correction, saturation warning, and resolution diagnostic (#571)
* feat: add timer-overhead correction, saturation warning, and resolution diagnostic Three cooperating diagnostics for sub-microsecond benchmarking, integrated in a single coherent code path so that each one can rely on the others: - BenchOptions.subtractTimerOverhead (default false): when enabled, the cost of one timestamp provider call is calibrated once at construction time via the new exported calibrateTimerOverhead helper, then subtracted from each raw latency sample (clamped to zero) before statistics are computed. Samples returned by the task function via overriddenDuration are intentional user values and are skipped by the correction. - 'warning' event on BenchEvents and TaskEvents, dispatched on both the Bench and the Task instances when the latency samples of a task are dominated by the timer resolution. Detection uses three OR'd criteria computed by detectTimerSaturation: more than half zero samples, fewer than max(3, min(10, n/1000)) distinct values, or zero MAD with n > 100. An n < 10 guard prevents false positives on unit-style benchmarks. - Task.detectedResolution getter, populated after each run with the smallest strictly-positive sample value that appears at least twice (smallest reproducibly observed increment). Falls back to the strict minimum when no positive value repeats. A new estimateResolution helper is exported. calibrateTimerOverhead (utils.ts): - Subtracts in the provider's native type before converting to milliseconds (toMs(b - a) rather than toMs(b) - toMs(a)), preserving bigint precision on long-uptime hosts. - Discards a configurable warmup phase (default 64 pairs) so the JIT reaches its steady-state tier before measurements begin. - Returns 0 when fewer than half the back-to-back pairs produce a positive delta — in that regime the timer resolution exceeds the call cost and the positive deltas measure a tick boundary, not the call cost. - Configurable estimator: 'median' (default), 'min', or 'p05'. Task#processRunResult orders the diagnostics so they always reflect the raw, uncorrected measurements: 1. sortSamples on raw latencies 2. estimateResolution on raw sorted samples 3. when overhead correction is active, compute raw statistics, evaluate detectTimerSaturation against the raw distribution, then apply the correction in-place (skipping overridden samples) and re-sort only when overridden samples were skipped 4. compute the final (possibly corrected) statistics 5. when no correction was applied, evaluate detectTimerSaturation against the final samples (raw == final in this path) This consolidates three previously separate proposals (PRs #568/#569/#570) into a single coherent change: composing them naively would have caused the diagnostics to operate on the corrected-and-clamped sample set, producing artificially small detected-resolution values and false-positive saturation warnings on benchmarks that activate overhead subtraction. * fix: align overridden samples and harden subtractTimerOverhead Apply audit-driven fixes to PR #571: * fix(task): correct overhead before sort to keep latencySamples aligned with isOverridden (collection order). Previous logic indexed isOverridden after sortSamples, corrupting both overriddenDuration preservation and measured-sample skip in mixed-mode tasks. * fix(task): run timer-saturation detection on a measured-only subset so constant overriddenDuration values cannot trigger a spurious low-distinct-count warning. * fix(bench): assert subtractTimerOverhead is incompatible with concurrency: 'task' (sequential calibration would not reflect per-iteration cost under concurrency). * fix(bench): normalize subtractTimerOverhead with ?? false instead of === true to accept any falsy default consistently. * fix(index): export detectTimerSaturation alongside the other timer diagnostics helpers. * docs(types): rewrite subtractTimerOverhead JSDoc with an honest treatment of the max(0, x) clamp and the two caveats (concurrency, overriddenDuration). Remove an orphan /** block. * test: rewrite the overriddenDuration warning test to assert warningCount === 0, matching the measured-only saturation behavior. * refactor(utils): expose saturation classifier and tighten timer typing * Add `classifyTimerSaturation` returning a `TimerSaturationReason` (`'zero-dominated' | 'low-distinct' | 'zero-mad'`) and re-implement `detectTimerSaturation` as a boolean wrapper. * Tighten `detectTimerSaturation`/`classifyTimerSaturation` parameter type from `Samples` to `SortedSamples`. * Short-circuit the distinct-value loop once the threshold is reached. * Add `medianAbsoluteDeviation(SortedSamples)` helper. * Rename `TimerOverheadEstimator` to `TimerOverheadEstimatorKind`. * Replace `as unknown as number` with `as bigint` in `calibrateTimerOverhead`; document operator polymorphism. * Re-export `classifyTimerSaturation`, `medianAbsoluteDeviation`, `TimerSaturationReason`, `TimerOverheadEstimatorKind` from the package entry point. * feat(event): carry timer saturation reason on warning events Extend `BenchEvent` with an optional `reason` payload symmetrical to `error`. The `reason` getter is typed as `TimerSaturationReason | undefined` for `'warning'` events and `undefined` for every other event type. * Move `TimerSaturationReason` from `utils.ts` to `types.ts` to align with the `Statistics`/`Samples` convention (types in `types.ts`, helpers in `utils.ts`). * Add a `'warning'` constructor overload accepting an optional reason. * Re-export `TimerSaturationReason` from the `./types` block in the package entry point. * fix(task): align resolution and saturation diagnostics with measured-only samples * Compute `detectedResolution` from the measured-only subset (excluding `overriddenDuration` samples). A constant override value is no longer reported as the timer grain. * Allocate `isOverridden` unconditionally so the measured-only filter is also active when `subtractTimerOverhead` is disabled. * Replace Phase 6 `computeStatistics` recomputation with the dedicated `medianAbsoluteDeviation` helper. * Use `classifyTimerSaturation` and propagate the `TimerSaturationReason` onto the `'warning'` event payload. * Update `Task.detectedResolution` JSDoc to reflect the measured-only semantics; update the `#processRunResult` ordering description. * fix(bench): enforce subtractTimerOverhead invariant at run() and tighten options coercion * Coerce `subtractTimerOverhead` with `=== true`, matching the sibling `retainSamples` form. Truthy non-boolean values from JS callers are now rejected. * Re-state the constructor assert message in remediation form (action the user can take, not the internal cause). * Add the same assert at the start of `run()`. `concurrency` is documented as a post-construction-mutable field, so the constructor check alone leaves the mutation path uncovered. * Note the constraint and the dual enforcement in the `subtractTimerOverhead` field JSDoc. * fix(types): make BenchLike.timerOverhead optional and readonly Third-party `BenchLike` implementers can omit the field (semantically equivalent to the existing `undefined` sentinel that `Task` already handles). The `readonly` modifier matches the concrete `Bench.timerOverhead` declaration and forbids mutation through the interface, which `Task` reads on every cycle. * docs(types): document subtractTimerOverhead clamp consequences honestly Rewrite the `subtractTimerOverhead` JSDoc with a mathematically grounded treatment: * Statistics list refers to all fields of `Statistics`; previously enumerated only seven of eighteen fields. * The `rme` inflation factor `M / (M − Ĉ)` is stated deterministically in the clean-shift regime, not hedged with 'potentially'. * The collapse of `p50`, `mad`, and `aad` to zero in the sub-overhead regime is named explicitly with the threshold. * Three observable consequences of the `max(0, …)` clamp are listed (`latency.min` may be 0; throughput substitutes the mean for clamped samples; criterion `'zero-dominated'` cannot distinguish clamped samples from genuine zeros). * test: cover alignment, p05 estimator, run() invariant, and saturation classifier * New `test/subtract-timer-overhead-alignment.test.ts` — exercises the Phase 1/2 alignment invariant on a heterogeneous run (alternating overridden + measured iterations) using a deterministic timestamp provider. Pins exact multiset counts so an off-by-one in the `isOverridden`/`latencySamples` index alignment fails the test. * `test/calibrate-timer-overhead.test.ts`: - Replace the loose `min ≤ median * 2` assertion with a deterministic estimator-ordering test using a scripted ascending-pair provider. - Add a deterministic `'p05'` test pinning the `max(0, ⌈n·0.05⌉ − 1)` index math at three sample sizes. - Add tests for the `subtractTimerOverhead` + `concurrency: 'task'` constructor assert and the equivalent `run()` runtime check. * `test/detected-resolution.test.ts`: replace the conditional `if (resolution !== undefined)` block with unconditional assertions. * `test/utils-detect-timer-saturation.test.ts`: add `classifyTimerSaturation` parallel coverage for each criterion (returning the precise reason string) plus the n<10 and healthy-spread negative cases. * New `test/warning-event-reason.test.ts` — verifies `BenchEvent.reason` carries the saturation reason for `'warning'` events and is `undefined` for other event types. * docs(readme): document timer overhead correction, per-sample override, and timer diagnostics * New 'Timer Overhead Correction' section covers `subtractTimerOverhead`, the calibration helper, and the `concurrency: 'task'` and sub-overhead caveats. * New 'Per-Sample Override' section documents `overriddenDuration` (previously absent from the README despite being supported in code). * New 'Timer Diagnostics' section covers `Task.detectedResolution` and the `'warning'` event with its `TimerSaturationReason` payload, plus pointers to the standalone helpers. * Extend the `BenchEvents` listener example with a `'warning'` listener that reads `evt.reason`. * fix(utils): use backticked refs for non-exported symbols in JSDoc `computeStatistics` and `absoluteDeviationMedian` are not re-exported from the package entry point, so `{@link …}` references to them trigger `typedoc --treatWarningsAsErrors`. Switch them to plain backticked code references; `{@link}` is preserved only for symbols listed in the public exports. * fix(index): export hrtimeNow and performanceNow timestamp providers The README timer-overhead example and `calibrateTimerOverhead` require a `TimestampProvider` object, but none was exported from the package entry. * perf(task): derive detectedResolution from sorted samples `estimateResolution` scans the sorted sample array for the first repeated positive value instead of building a value-keyed Map over every sample, removing an O(distinct) allocation on the default post-processing path. Its signature is tightened to `SortedSamples` (matching its `classifyTimerSaturation` / `detectTimerSaturation` siblings) and resolution is computed after the working-array sort. Also corrects the `detectedResolution` getter doc (strict-min fallback; corrected-samples note under `subtractTimerOverhead`) and the `#processRunResult` phase-ordering doc (`'warning'` is dispatched before `'cycle'`/`'complete'`). * test(utils): lock timer-saturation classifier thresholds Cover the `zero-mad` n=100/101 boundary and the distinct-count ceiling of 10 at n=10000. * docs(readme): clarify the coarse-timer no-op condition Calibration returns 0 when fewer than half the pairs yield a positive delta (C < R/2), not at a fixed 1 ms resolution. * refactor(bench): hoist duplicated subtractTimerOverhead/concurrency assert message The identical guard message in the constructor and `run()` is now a single module-level constant. * refactor(task): use hasAnyOverridden instead of reference-identity check Branch on the named `hasAnyOverridden` boolean rather than the `measuredOnly === latencySamples` array identity; behaviour-identical, self-documenting. * refactor(utils): rename calibrate options to pairs/warmupPairs The `CalibrateTimerOverheadOptions` fields count back-to-back call pairs, not latency samples; rename disambiguates from the codebase-wide `Samples` meaning (unreleased option, no published break). Also document the deliberate p05 nearest-rank choice vs `quantileSorted`. * docs(readme): configure concurrency and threshold via constructor options `concurrency` and `threshold` are `readonly`; the example now sets them at construction instead of mutating them after the fact. * docs(bench): clarify the run() re-assert guards JS-side mutation The `run()` re-check exists to catch untyped mutation of the `readonly` `concurrency` field after construction, not a supported reconfiguration path. * refactor(task): unify isOverridden guard and correct its param JSDoc Use optional chaining at both guard sites (dropping a non-null-assertion eslint-disable), and fix the #processRunResult param doc: isOverridden is undefined on the error / no-valid-samples path, not when overhead correction is disabled. * refactor(utils): guard pairs===0 explicitly in calibrateTimerOverhead Return early on the degenerate zero-pairs input so the coarse-timer guard (deltas.length * 2 < pairs) covers every remaining case, removing the redundant empty-deltas check. * fix(utils): guard non-positive pairs in calibrateTimerOverhead `pairs < 0` fell through the coarse-timer guard and produced `NaN` (median) or `undefined` (min/p05); `pairs <= 0` now returns 0, restoring the graceful behaviour of the removed empty-deltas check. Covered by a regression test. * docs(readme): clarify overriddenDuration does not bypass the timer The timestamp provider is still invoked around the task function; only the measured value is discarded and overhead correction skipped. * fix(utils): reject non-finite/non-integer pair counts in calibrateTimerOverhead `{ pairs: Infinity }` / `{ warmupPairs: Infinity }` (and NaN/non-integer) hung or produced NaN; `Number.isInteger` guards coerce them to the existing no-op (return 0 / skip warmup). Covered by regression tests. * perf(task): track overridden samples by index Set, not a parallel boolean[] Replace the always-allocated `isOverridden: boolean[]` (one slot per sample, even with no override) with a `Set<number>` of overridden collection-order indices, populated only on override. Eliminates an O(n) parallel array on the default path; alignment invariant and concurrency safety preserved (index is taken synchronously right after the sample push). * refactor(task): extract BenchmarkResult union for #benchmark/#benchmarkSync The identical error-XOR-samples return union was duplicated across both methods; hoist it to a single module-local `BenchmarkResult` type (mirrors the local-type convention in utils.ts). Type-only; no behavior change. * docs(task): finish isOverridden -> overriddenIndices rename in #processRunResult The 'Ordering' JSDoc block still referenced the removed `isOverridden` array (and array-indexed it, which a Set is not); align the wording with the `overriddenIndices` Set introduced in 15f30d9. * test(task): rename stale isOverridden test title to overridden samples The tracked field was renamed to `overriddenIndices` (a Set) in 15f30d9; this aligns the last remaining `isOverridden` reference (a test title).
1 parent aeb7d30 commit 0647d27

13 files changed

Lines changed: 1262 additions & 38 deletions

README.md

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ Both the `Task` and `Bench` classes extend the `EventTarget` object. So you can
8787
bench.addEventListener('cycle', (evt) => {
8888
const task = evt.task!;
8989
});
90+
91+
// runs when timer saturation is detected for a task's measured samples
92+
bench.addEventListener('warning', (evt) => {
93+
const task = evt.task!;
94+
const reason = evt.reason; // 'zero-dominated' | 'low-distinct' | 'zero-mad'
95+
});
9096
```
9197

9298
#### [`TaskEvents`](https://tinylibs.github.io/tinybench/types/TaskEvents.html)
@@ -147,8 +153,10 @@ await bench.run()
147153
- When `mode` is set to 'bench', different tasks within the bench run concurrently. Concurrent cycles.
148154

149155
```ts
150-
bench.threshold = 10 // The maximum number of concurrent tasks to run. Defaults to Number.POSITIVE_INFINITY.
151-
bench.concurrency = 'task' // The concurrency mode to determine how tasks are run.
156+
const bench = new Bench({
157+
concurrency: 'task', // The concurrency mode to determine how tasks are run.
158+
threshold: 10, // The maximum number of concurrent tasks to run. Defaults to Number.POSITIVE_INFINITY.
159+
})
152160
await bench.run()
153161
```
154162

@@ -286,6 +294,99 @@ const bench = new Bench({
286294
})
287295
```
288296

297+
## Timer Overhead Correction
298+
299+
Each timer call (`performance.now()`, `process.hrtime.bigint()`, …) has a
300+
non-zero call cost `C`. For a task whose true duration `X` is comparable
301+
to `C`, the raw measured sample `X + C` is dominated by the timer rather
302+
than the task.
303+
304+
When `subtractTimerOverhead: true` is set, an estimate `Ĉ` is computed
305+
once at construction time via [`calibrateTimerOverhead`](https://tinylibs.github.io/tinybench/functions/calibrateTimerOverhead.html),
306+
and `Math.max(0, raw_sample - Ĉ)` is used as each non-overridden sample
307+
before statistics are computed.
308+
309+
```ts
310+
const bench = new Bench({ subtractTimerOverhead: true })
311+
console.log(bench.timerOverhead) // calibrated Ĉ in ms (or undefined)
312+
```
313+
314+
The calibration helper is also exported for direct use, with a
315+
configurable estimator strategy (`'median'` default, or `'min'` / `'p05'`):
316+
317+
```ts
318+
import { calibrateTimerOverhead, hrtimeNowTimestampProvider } from 'tinybench'
319+
320+
const overhead = calibrateTimerOverhead(hrtimeNowTimestampProvider, {
321+
estimator: 'p05',
322+
pairs: 1024,
323+
warmupPairs: 64,
324+
})
325+
```
326+
327+
**Caveats.**
328+
329+
- Incompatible with `concurrency: 'task'` — overhead is calibrated
330+
sequentially and does not reflect concurrent execution cost.
331+
Construction (and `run()`) throws if both are set.
332+
- For sub-overhead measurements (`X ≈ Ĉ`) the `max(0, …)` clamp
333+
truncates the lower tail and biases statistics; prefer
334+
`overriddenDuration` (see below).
335+
- When the timer is too coarse to resolve the call cost — fewer than half
336+
of the calibration pairs produce a positive delta (call cost `C < R / 2`,
337+
e.g. a `Date.now`-class timer with `>= 1 ms` resolution) — the calibration
338+
returns `0` and the option becomes a no-op.
339+
340+
## Per-Sample Override (`overriddenDuration`)
341+
342+
A task function may return an object containing `overriddenDuration`
343+
(in ms). That value is recorded in place of the timer-measured sample:
344+
the timer still brackets the task function, but its measurement is
345+
discarded and overhead correction is not applied to the substituted
346+
value. Useful for externally-timed work or sub-overhead measurements
347+
that the timer cannot resolve.
348+
349+
```ts
350+
bench.add('externally-timed', () => {
351+
const start = process.hrtime.bigint()
352+
doWork()
353+
const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6
354+
return { overriddenDuration: elapsedMs }
355+
})
356+
```
357+
358+
Overridden samples are excluded from `Task.detectedResolution` and
359+
from timer-saturation detection.
360+
361+
## Timer Diagnostics
362+
363+
After `bench.run()` (or `runSync()`), each task exposes
364+
`detectedResolution` — the smallest reproducibly observed positive
365+
sample (in ms) among the timer-measured samples, or `undefined` when no
366+
positive timer measurement was seen (e.g. every sample was overridden).
367+
368+
```ts
369+
const task = bench.getTask('foo')
370+
console.log(task?.detectedResolution) // e.g. 0.000041 (≈ 41 ns)
371+
```
372+
373+
When the timer's resolution dominates a task's measured distribution
374+
(more than half zero samples, fewer than `max(3, min(10, ⌊n / 1000⌋))`
375+
distinct values, or zero MAD with `n > 100`), tinybench dispatches a
376+
`'warning'` event on both the task and the bench, carrying the matching
377+
[`TimerSaturationReason`](https://tinylibs.github.io/tinybench/types/TimerSaturationReason.html):
378+
379+
```ts
380+
bench.addEventListener('warning', evt => {
381+
console.warn(`timer-saturated: ${evt.task?.name} — ${evt.reason}`)
382+
})
383+
```
384+
385+
The same heuristic and estimator are exposed as standalone helpers for
386+
custom analysis: [`detectTimerSaturation`](https://tinylibs.github.io/tinybench/functions/detectTimerSaturation.html),
387+
[`classifyTimerSaturation`](https://tinylibs.github.io/tinybench/functions/classifyTimerSaturation.html),
388+
and [`estimateResolution`](https://tinylibs.github.io/tinybench/functions/estimateResolution.html).
389+
289390
## Aborting Benchmarks
290391

291392
Tinybench supports aborting benchmarks using `AbortSignal` at both the bench and task levels:

src/bench.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ import { BenchEvent } from './event'
2424
import { Task } from './task'
2525
import {
2626
assert,
27+
calibrateTimerOverhead,
2728
defaultConvertTaskResultForConsoleTable,
2829
getTimestampProvider,
2930
runtime,
3031
runtimeVersion,
3132
} from './utils'
3233

34+
const subtractTimerOverheadConcurrencyError =
35+
'`subtractTimerOverhead` cannot be used with `concurrency: "task"` — set `concurrency` to `null` or `"bench"`, or disable `subtractTimerOverhead`'
36+
3337
/**
3438
* The Bench class keeps track of the benchmark tasks and controls them.
3539
*/
@@ -95,6 +99,18 @@ export class Bench extends EventTarget implements BenchLike {
9599
*/
96100
readonly signal?: AbortSignal
97101

102+
/**
103+
* Whether to subtract an estimated timestamp provider call overhead from
104+
* each raw latency sample.
105+
*
106+
* Incompatible with `concurrency: 'task'`. Enforced at construction and
107+
* re-checked at the start of {@link Bench.run} to guard against untyped
108+
* (JS-side) mutation of the `readonly` `concurrency` field after
109+
* construction.
110+
* @default false
111+
*/
112+
readonly subtractTimerOverhead: boolean
113+
98114
/**
99115
* A teardown function that runs after each task execution.
100116
*/
@@ -120,6 +136,15 @@ export class Bench extends EventTarget implements BenchLike {
120136
*/
121137
readonly time: number
122138

139+
/**
140+
* The estimated cost of one timestamp provider call in milliseconds.
141+
*
142+
* `undefined` when {@link subtractTimerOverhead} is `false`.
143+
* Otherwise calibrated once at construction time via
144+
* {@link calibrateTimerOverhead}.
145+
*/
146+
readonly timerOverhead: number | undefined
147+
123148
/**
124149
* A timestamp provider and its related functions.
125150
*/
@@ -195,6 +220,14 @@ export class Bench extends EventTarget implements BenchLike {
195220
this.throws = restOptions.throws ?? false
196221
this.signal = restOptions.signal
197222
this.retainSamples = restOptions.retainSamples === true
223+
this.subtractTimerOverhead = restOptions.subtractTimerOverhead === true
224+
assert(
225+
!(this.subtractTimerOverhead && this.concurrency === 'task'),
226+
subtractTimerOverheadConcurrencyError
227+
)
228+
this.timerOverhead = this.subtractTimerOverhead
229+
? calibrateTimerOverhead(this.timestampProvider)
230+
: undefined
198231

199232
if (this.signal) {
200233
this.signal.addEventListener(
@@ -264,6 +297,10 @@ export class Bench extends EventTarget implements BenchLike {
264297
* @returns the tasks array
265298
*/
266299
async run (): Promise<Task[]> {
300+
assert(
301+
!(this.subtractTimerOverhead && this.concurrency === 'task'),
302+
subtractTimerOverheadConcurrencyError
303+
)
267304
if (this.warmup) {
268305
await this.#warmupTasks()
269306
}

src/event.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
BenchEventsOptionalTask,
55
BenchEventsWithError,
66
BenchEventsWithTask,
7+
TimerSaturationReason,
78
} from './types'
89

910
/**
@@ -24,6 +25,20 @@ class BenchEvent<
2425
return this.#error as K extends BenchEventsWithError ? Error : undefined
2526
}
2627

28+
/**
29+
* The reason a `'warning'` event was dispatched.
30+
* @returns The {@link TimerSaturationReason} for `'warning'` events;
31+
* `undefined` for every other event type and for `'warning'` events
32+
* dispatched without a reason
33+
*/
34+
get reason (): K extends 'warning'
35+
? TimerSaturationReason | undefined
36+
: undefined {
37+
return this.#reason as K extends 'warning'
38+
? TimerSaturationReason | undefined
39+
: undefined
40+
}
41+
2742
/**
2843
* The task associated with the event.
2944
* @returns The task if the event type is one that includes a task; otherwise, undefined
@@ -41,15 +56,25 @@ class BenchEvent<
4156
}
4257

4358
#error?: Error
59+
#reason?: TimerSaturationReason
4460
#task?: Task
4561

62+
constructor (type: 'warning', task: Task, reason?: TimerSaturationReason)
4663
constructor (type: BenchEventsWithError, task: Task, error: Error)
4764
constructor (type: BenchEventsWithTask, task: Task)
4865
constructor (type: BenchEventsOptionalTask, task?: Task)
49-
constructor (type: BenchEvents, task?: Task, error?: Error) {
66+
constructor (
67+
type: BenchEvents,
68+
task?: Task,
69+
errorOrReason?: Error | TimerSaturationReason
70+
) {
5071
super(type)
5172
this.#task = task
52-
this.#error = error
73+
if (typeof errorOrReason === 'string') {
74+
this.#reason = errorOrReason
75+
} else {
76+
this.#error = errorOrReason
77+
}
5378
}
5479
}
5580

src/index.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,27 @@ export type {
3232
TaskResultStarted,
3333
TaskResultTimestampProviderInfo,
3434
TaskResultWithStatistics,
35+
TimerSaturationReason,
3536
TimestampFn,
3637
TimestampFns,
3738
TimestampProvider,
3839
TimestampValue,
3940
} from './types'
40-
export { formatNumber, hrtimeNow, mToNs, performanceNow as now, nToMs } from './utils'
41+
export type {
42+
CalibrateTimerOverheadOptions,
43+
TimerOverheadEstimatorKind,
44+
} from './utils'
45+
export {
46+
calibrateTimerOverhead,
47+
classifyTimerSaturation,
48+
detectTimerSaturation,
49+
estimateResolution,
50+
formatNumber,
51+
hrtimeNow,
52+
hrtimeNowTimestampProvider,
53+
medianAbsoluteDeviation,
54+
mToNs,
55+
performanceNow as now,
56+
nToMs,
57+
performanceNowTimestampProvider,
58+
} from './utils'

0 commit comments

Comments
 (0)