Skip to content

Commit fbcb3dc

Browse files
UzlopakCopilot
andauthored
feat: implement timestamp provider (#440)
* make timestamp configurable * simplify measure methods * rename to timestamp provider * add readme * export types * rename getTimestamp to getTimestampProvider * fix * add information about used timestamp provider * Update src/utils.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/types.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * cosmetic changes * more precise wording --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7df7e41 commit fbcb3dc

File tree

10 files changed

+495
-78
lines changed

10 files changed

+495
-78
lines changed

README.md

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,57 @@ task.addEventListener('cycle', (evt) => {
100100

101101
### [`BenchEvent`](https://tinylibs.github.io/tinybench/types/BenchEvent.html)
102102

103-
## `process.hrtime`
103+
## Timestamp Providers
104104

105-
if you want more accurate results for nodejs with `process.hrtime`, then import
106-
the `hrtimeNow` function from the library and pass it to the `Bench` options.
105+
Tinybench can utilize different timestamp providers for measuring time intervals.
106+
By default it uses `performance.now()`.
107+
108+
The `timestampProvider` option can be set when creating a `Bench` instance. It
109+
accepts either a `TimestampProvider` object or shorthands for the common
110+
providers `hrtimeNow` and `performanceNow`.
111+
112+
If you use `bun` runtime, you can also use `bunNanoseconds` shorthand.
113+
114+
You can set the `timestampProvider` to `auto` to let Tinybench choose the most
115+
precise available timestamp provider based on the runtime.
107116

108117
```ts
109-
import { hrtimeNow } from 'tinybench'
118+
import { Bench } from 'tinybench'
119+
120+
const bench = new Bench({
121+
timestampProvider: 'hrtimeNow' // or 'performanceNow', 'bunNanoseconds', 'auto'
122+
})
110123
```
111124

112-
It may make your benchmarks slower.
125+
If you want to provide a custom timestamp provider, you can create an object that implements
126+
the `TimestampProvider` interface:
127+
128+
```ts
129+
import { Bench, TimestampProvider } from 'tinybench'
130+
131+
// Custom timestamp provider using Date.now()
132+
const dateNowTimestampProvider: TimestampProvider = {
133+
name: 'dateNow', // name of the provider
134+
fn: Date.now, // function that returns the current timestamp
135+
toMs: ts => ts, // convert the timestamp to milliseconds
136+
fromMs: ts => ts // convert milliseconds to the format used by fn()
137+
}
138+
139+
const bench = new Bench({
140+
timestampProvider: dateNowTimestampProvider
141+
})
142+
```
143+
144+
You can also set the `now` option to a function that returns the current timestamp.
145+
It will be converted to a `TimestampProvider` internally.
146+
147+
```ts
148+
import { Bench } from 'tinybench'
149+
150+
const bench = new Bench({
151+
now: Date.now
152+
})
153+
```
113154

114155
## Async Detection
115156

src/bench.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
JSRuntime,
1111
RemoveEventListenerOptionsArgument,
1212
TaskResult,
13+
TimestampProvider,
1314
} from './types'
1415

1516
import {
@@ -24,7 +25,7 @@ import { Task } from './task'
2425
import {
2526
assert,
2627
defaultConvertTaskResultForConsoleTable,
27-
performanceNow,
28+
getTimestampProvider,
2829
runtime,
2930
runtimeVersion,
3031
} from './utils'
@@ -119,6 +120,11 @@ export class Bench extends EventTarget implements BenchLike {
119120
*/
120121
readonly time: number
121122

123+
/**
124+
* A timestamp provider and its related functions.
125+
*/
126+
readonly timestampProvider: TimestampProvider
127+
122128
/**
123129
* Whether to warmup the tasks before running them
124130
*/
@@ -166,7 +172,15 @@ export class Bench extends EventTarget implements BenchLike {
166172

167173
this.time = restOptions.time ?? defaultTime
168174
this.iterations = restOptions.iterations ?? defaultIterations
169-
this.now = restOptions.now ?? performanceNow
175+
176+
assert(
177+
!(restOptions.now !== undefined && restOptions.timestampProvider !== undefined),
178+
'Cannot set both `now` and `timestampProvider` options'
179+
)
180+
this.timestampProvider = getTimestampProvider(restOptions.now ?? restOptions.timestampProvider)
181+
182+
this.now = () => this.timestampProvider.toMs(this.timestampProvider.fn())
183+
170184
this.warmup = restOptions.warmup ?? true
171185
this.warmupIterations =
172186
restOptions.warmupIterations ?? defaultWarmupIterations

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export type {
3030
TaskResultNotStarted,
3131
TaskResultRuntimeInfo,
3232
TaskResultStarted,
33+
TaskResultTimestampProviderInfo,
3334
TaskResultWithStatistics,
35+
TimestampFn,
36+
TimestampFns,
37+
TimestampProvider,
38+
TimestampValue,
3439
} from './types'
3540
export { hrtimeNow, performanceNow as now, nToMs } from './utils'

src/task.ts

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import type {
1010
TaskEvents,
1111
TaskResult,
1212
TaskResultRuntimeInfo,
13+
TaskResultTimestampProviderInfo,
14+
TimestampFn,
15+
TimestampProvider,
16+
TimestampValue,
1317
} from './types'
1418

1519
import { BenchEvent } from './event'
@@ -78,11 +82,12 @@ export class Task extends EventTarget {
7882
* The result of the task.
7983
* @returns The task result including state, statistics, and runtime information
8084
*/
81-
get result (): TaskResult & TaskResultRuntimeInfo {
85+
get result (): TaskResult & TaskResultRuntimeInfo & TaskResultTimestampProviderInfo {
8286
return {
8387
...this.#result,
8488
runtime: this.#bench.runtime,
8589
runtimeVersion: this.#bench.runtimeVersion,
90+
timestampProviderName: this.#bench.timestampProvider.name,
8691
}
8792
}
8893

@@ -144,6 +149,21 @@ export class Task extends EventTarget {
144149
*/
145150
readonly #signal: AbortSignal | undefined
146151

152+
/**
153+
* The timestamp function
154+
*/
155+
readonly #timestampFn: TimestampFn
156+
157+
/**
158+
* The timestamp provider
159+
*/
160+
readonly #timestampProvider: TimestampProvider
161+
162+
/**
163+
* The timestamp to milliseconds conversion function
164+
*/
165+
readonly #timestampToMs: (value: TimestampValue) => number
166+
147167
constructor (bench: BenchLike, name: string, fn: Fn, fnOpts: FnOptions = {}) {
148168
super()
149169
this.#bench = bench
@@ -153,6 +173,9 @@ export class Task extends EventTarget {
153173
this.#async = fnOpts.async ?? isFnAsyncResource(fn)
154174
this.#signal = fnOpts.signal
155175
this.#retainSamples = fnOpts.retainSamples ?? bench.retainSamples
176+
this.#timestampProvider = bench.timestampProvider
177+
this.#timestampFn = bench.timestampProvider.fn
178+
this.#timestampToMs = bench.timestampProvider.toMs
156179

157180
for (const hookName of hookNames) {
158181
if (this.#fnOpts[hookName] != null) {
@@ -336,12 +359,9 @@ export class Task extends EventTarget {
336359
await this.#fnOpts.beforeEach.call(this, mode)
337360
}
338361

339-
let taskTime: number
340-
if (this.#async) {
341-
({ taskTime } = await this.#measureOnce())
342-
} else {
343-
({ taskTime } = this.#measureOnceSync())
344-
}
362+
const taskTime = this.#async
363+
? await this.#measure()
364+
: this.#measureSync()
345365

346366
samples.push(taskTime)
347367
totalTime += taskTime
@@ -351,15 +371,16 @@ export class Task extends EventTarget {
351371
}
352372
}
353373
}
374+
354375
if (this.#bench.concurrency === 'task') {
355376
try {
356377
await withConcurrency({
357378
fn: benchmarkTask,
358379
iterations,
359380
limit: Math.max(1, Math.floor(this.#bench.threshold)),
360-
now: this.#bench.now,
361381
signal: this.#signal ?? this.#bench.signal,
362382
time,
383+
timestampProvider: this.#timestampProvider,
363384
})
364385
} catch (error) {
365386
return { error: toError(error) }
@@ -429,7 +450,7 @@ export class Task extends EventTarget {
429450
)
430451
}
431452

432-
const { taskTime } = this.#measureOnceSync()
453+
const taskTime = this.#measureSync()
433454

434455
samples.push(taskTime)
435456
totalTime += taskTime
@@ -472,43 +493,40 @@ export class Task extends EventTarget {
472493

473494
/**
474495
* Measures a single execution of the task function asynchronously.
475-
* @returns An object containing the function result and the measured execution time
496+
* @returns The measured execution time
476497
*/
477-
async #measureOnce (): Promise<{
478-
fnResult: ReturnType<Fn>
479-
taskTime: number
480-
}> {
481-
const taskStart = this.#bench.now()
498+
async #measure (): Promise<number> {
499+
const taskStart = this.#timestampFn() as unknown as number
482500
// eslint-disable-next-line no-useless-call
483501
const fnResult = await this.#fn.call(this)
484-
let taskTime = this.#bench.now() - taskStart
502+
const taskTime = this.#timestampToMs((this.#timestampFn() as unknown as number) - taskStart)
485503

486504
const overriddenDuration = getOverriddenDurationFromFnResult(fnResult)
487505
if (overriddenDuration !== undefined) {
488-
taskTime = overriddenDuration
506+
return overriddenDuration
489507
}
490-
return { fnResult, taskTime }
508+
return taskTime
491509
}
492510

493511
/**
494512
* Measures a single execution of the task function synchronously.
495-
* @returns An object containing the function result and the measured execution time
513+
* @returns The measured execution time
496514
*/
497-
#measureOnceSync (): { fnResult: ReturnType<Fn>; taskTime: number } {
498-
const taskStart = this.#bench.now()
515+
#measureSync (): number {
516+
const taskStart = this.#timestampFn() as unknown as number
499517
// eslint-disable-next-line no-useless-call
500518
const fnResult = this.#fn.call(this)
501-
let taskTime = this.#bench.now() - taskStart
519+
const taskTime = this.#timestampToMs(this.#timestampFn() as unknown as number - taskStart)
502520

503521
assert(
504522
!isPromiseLike(fnResult),
505523
'task function must be sync when using `runSync()`'
506524
)
507525
const overriddenDuration = getOverriddenDurationFromFnResult(fnResult)
508526
if (overriddenDuration !== undefined) {
509-
taskTime = overriddenDuration
527+
return overriddenDuration
510528
}
511-
return { fnResult, taskTime }
529+
return taskTime
512530
}
513531

514532
/**

src/types.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export interface BenchLike extends EventTarget {
7272
* A function to get a timestamp.
7373
*/
7474
now: NowFn
75+
7576
/**
7677
* Removes a previously registered event listener.
7778
*/
@@ -119,6 +120,10 @@ export interface BenchLike extends EventTarget {
119120
* The amount of time to run each task.
120121
*/
121122
time: number
123+
/**
124+
* The timestamp provider used by the benchmark.
125+
*/
126+
timestampProvider: TimestampProvider
122127
/**
123128
* Whether to warmup the tasks before running them
124129
*/
@@ -201,6 +206,12 @@ export interface BenchOptions {
201206
*/
202207
time?: number
203208

209+
/**
210+
* The timestamp provider used by the benchmark. By default 'performance.now'
211+
* will be used.
212+
*/
213+
timestampProvider?: TimestampFns | TimestampProvider
214+
204215
/**
205216
* Warmup benchmark.
206217
* @default true
@@ -374,6 +385,7 @@ export type JSRuntime =
374385
| 'v8'
375386
| 'workerd'
376387

388+
/**
377389
/**
378390
* A function that returns the current timestamp.
379391
*/
@@ -614,6 +626,7 @@ export interface TaskResultRuntimeInfo {
614626
*/
615627
runtimeVersion: string
616628
}
629+
617630
/**
618631
* The task result for started tasks
619632
*/
@@ -624,6 +637,16 @@ export interface TaskResultStarted {
624637
state: 'started'
625638
}
626639

640+
/**
641+
* The timestamp provider information for task results
642+
*/
643+
export interface TaskResultTimestampProviderInfo {
644+
/**
645+
* the name of the timestamp provider used during the benchmark
646+
*/
647+
timestampProviderName: (string & {}) | TimestampFns
648+
}
649+
627650
/**
628651
* The statistical data for task results
629652
*/
@@ -648,3 +671,53 @@ export interface TaskResultWithStatistics {
648671
*/
649672
totalTime: number
650673
}
674+
675+
/**
676+
* A timestamp function that returns either a number or bigint.
677+
*/
678+
export type TimestampFn = () => TimestampValue
679+
680+
/**
681+
* Possible timestamp provider names.
682+
* 'custom' is used when a custom timestamp function is provided.
683+
*/
684+
export type TimestampFns =
685+
| 'auto'
686+
| 'bunNanoseconds'
687+
| 'custom'
688+
| 'hrtimeNow'
689+
| 'performanceNow'
690+
691+
/**
692+
* A timestamp provider and its related functions.
693+
*/
694+
export interface TimestampProvider {
695+
/**
696+
* The actual function of the timestamp provider.
697+
* @returns the timestamp value
698+
*/
699+
fn: TimestampFn
700+
/**
701+
* Converts milliseconds to the timestamp value.
702+
* @param value - the milliseconds value
703+
* @returns the timestamp value
704+
*/
705+
fromMs: (value: number) => TimestampValue
706+
/**
707+
* The name of the timestamp provider.
708+
*/
709+
name: (string & {}) | TimestampFns
710+
/**
711+
* Converts the timestamp value to milliseconds.
712+
* @param value - the timestamp value
713+
* @returns the milliseconds
714+
*/
715+
toMs: (value: TimestampValue) => number
716+
}
717+
718+
/**
719+
* A timestamp value, either number or bigint. Internally timestamps can use
720+
* either representation depending on the environment and the chosen timestamp
721+
* function.
722+
*/
723+
export type TimestampValue = bigint | number

0 commit comments

Comments
 (0)