Skip to content

Repository files navigation

perfy

build coverage mutation score version downloads zero dependencies ESM TS license

This module is ESM 🔆. Please read this.

A tiny, zero-dependency utility for measuring code execution time in high-resolution real time — named timers, one-shot exec() wrappers, and a rich elapsed-time result. Runs in Node.js, browsers, Deno and Bun.

import { perfy } from 'perfy';

perfy.start('loop');
// ...heavy work...
console.log(perfy.end('loop').time); // -> 1.235 (sec.)

Note

Perfy picks the most precise clock available: process.hrtime.bigint() on Node.js (exact integer nanoseconds), falling back to performance.now() elsewhere. The elapsed time is computed with integer-nanosecond math, so it never accumulates floating-point drift. Wall-clock startTime / endTime stamps come from Date.now().

Installation

npm install perfy

Usage

Call perfy.start('name') to create a timer and mark its start, then perfy.end('name') to get the elapsed-time result. By default the instance is destroyed once ended.

import { perfy } from 'perfy';

perfy.start('loop-stuff');
// ...some heavy stuff here...
const result = perfy.end('loop-stuff');
console.log(result.time); // -> 1.459 (sec.)

...or wrap the work in exec() and let Perfy time it for you:

perfy.exec('async-stuff', (done) => {
  // ...some heavy async stuff here...
  const result = done();
  console.log(result.time); // -> 1.459 (sec.)
});

The exported perfy is a shared singleton — the simplest way to use the library. When you want an isolated registry (or a custom clock, e.g. in tests), construct your own:

import { Perfy } from 'perfy';

const perfy = new Perfy();

Exports

Everything is a named export (there is no default export):

import {
  perfy,              // shared Perfy singleton — the simplest entry point
  Perfy,              // class — construct an isolated registry: new Perfy(clock?)
  PerfyItem,          // a single timing instance (advanced / typing)
  PerfyError,         // Error subclass thrown on failure, with a `.code`
  createNanoClock,    // build a NanoClock from given host objects
  defaultNanoClock    // the NanoClock selected for this environment
} from 'perfy';

import type {
  IPerfyResult,       // the elapsed-time result object
  IPerfyMeasurement,  // the disposable handle returned by measure()
  NanoClock,          // () => bigint monotonic nanosecond clock
  PerfyErrorCode,     // 'NAME_REQUIRED' | 'NO_INSTANCE' | 'NOT_STARTED' | 'INVALID_CALLBACK' | 'NO_CLOCK'
  DoneFn,             // the `done` callback passed to a callback-style exec task
  SyncTask,
  AsyncTask,
  PromiseTask,
  PerfyTask           // SyncTask | AsyncTask | PromiseTask
} from 'perfy';

API

Every method that takes a name throws a PerfyError with code NAME_REQUIRED when it is empty.

Method Returns Description
start(name, autoDestroy?) Perfy Creates a new instance under name and marks its start time. Reusing a name overwrites it. autoDestroy (default true) drops the instance when end() is called. Chainable.
end(name) IPerfyResult Ends the instance and returns the elapsed-time result. If autoDestroy was left on, the instance is removed right after. Calling end() again on a kept instance returns the same cached result. Throws NO_INSTANCE if no such instance exists.
lap(name) IPerfyResult Records a split — the elapsed time since the previous lap (or since start for the first) — then advances the lap marker. The instance keeps running; end() still reports the total from start. Throws NO_INSTANCE if no such instance exists.
measure(name, onEnd?) IPerfyMeasurement Starts a kept instance and returns a disposable whose [Symbol.dispose] ends it — so a using declaration times its enclosing scope. The result stays retrievable via result(name); pass onEnd to receive it immediately. Disposing twice is a no-op.
exec([name,] fn) IPerfyResult | Promise<IPerfyResult> | Perfy Times the execution of fn, picking the mode from the task itself. Synchronous (fn returns a non-thenable) → ended automatically, result returned. Promise-returning (fn returns a promise) → awaited, resolves to the result (a rejection is propagated). Callback-style (fn(done) declares a done argument) → must call done() to end; returns the Perfy instance immediately. Pass a name to keep the instance. Throws INVALID_CALLBACK if fn is not a function.
result(name) IPerfyResult | null The stored result of a kept, ended instance — or null if it does not exist or has not ended yet.
exists(name) boolean Whether an instance currently exists under name. false once an auto-destroyed instance has ended.
names() string[] Names of all existing instances.
count() number Number of existing instances.
destroy(name) Perfy Destroys the instance under name, if any. Chainable.
destroyAll() Perfy Destroys all existing instances. Chainable.

The Result Object

end() (and exec() / result()) return an IPerfyResult — every field is a plain number/string, so the object is safe to JSON.stringify.

Property Type Description
name string Name of the instance ('' for an unnamed exec()).
time number Full elapsed time in seconds (float, 3 decimals). e.g. 1.235
milliseconds number Full elapsed time in milliseconds (float). e.g. 1235.125
nanoseconds number Full elapsed time in nanoseconds. e.g. 1235125283
summary string Human-readable shorthand. e.g. 'loop: 1.235 sec.'
startTime number UTC wall-clock time (ms) at start, via Date.now(). e.g. 1533302465251
endTime number UTC wall-clock time (ms) at end, via Date.now(). e.g. 1533302466486

Examples

Reading the elapsed time in different units:

perfy.start('metric');
// ...
const r = perfy.end('metric');
console.log(`${r.time} sec.`);          // -> 1.234 sec.
console.log(`${r.milliseconds} ms.`);   // -> 1234.567 ms.
console.log(r.summary);                 // -> metric: 1.234 sec.

Auto-destroy (default):

perfy.start('metric').count();  // -> 1
perfy.end('metric');
perfy.count();                  // -> 0 (destroyed on end)

Keep the instance (disable autoDestroy):

perfy.start('metric', false);
perfy.end('metric').time;   // -> 0.123
perfy.exists('metric');     // -> true (kept)
perfy.result('metric');     // -> the same result object

Timing a synchronous functionexec() returns the result directly:

const result = perfy.exec(() => {
  // sync work
});
console.log(result.time);

Timing a promise / async functionexec awaits it and resolves to the result:

const result = await perfy.exec('fetch', async () => {
  await fetch('https://example.com');
});
console.log(result.time);

Timing a callback-style async function — call done() when finished:

perfy.exec((done) => {
  setTimeout(() => {
    const result = done();
    console.log(result.time);
  }, 1000);
});

Laps — record splits within one running timer:

perfy.start('pipeline');
loadData();
console.log('load:', perfy.lap('pipeline').time);
transform();
console.log('transform:', perfy.lap('pipeline').time);
console.log('total:', perfy.end('pipeline').time);

Scope timing with using — the timer ends automatically when the block exits:

{
  using _ = perfy.measure('block', (r) => console.log(r.time));
  // ...work...
} // ended here

Named exec() keeps the instance for later retrieval:

perfy.exec('async-op', (done) => {
  done();
});
perfy.exists('async-op');  // -> true
perfy.result('async-op');  // -> the result object

Destroy everything:

perfy.destroyAll().count(); // -> 0

Errors

Every failure throws a PerfyError — an Error subclass carrying a stable, machine-readable code (NAME_REQUIRED, NO_INSTANCE, NOT_STARTED, INVALID_CALLBACK, NO_CLOCK):

import { perfy, PerfyError } from 'perfy';

try {
  perfy.end('never-started');
} catch (err) {
  if (err instanceof PerfyError && err.code === 'NO_INSTANCE') {
    // handle it
  }
}

Tests & Quality

100% test coverage (statements, branches, functions, lines) and a 100% Stryker mutation score, run across Node.js 22 & 24 in CI.

Changelog

See CHANGELOG.md. v2 is a breaking release (ESM-only, universal clock, streamlined result object) — the migration notes live there.

Related

  • tasktimer — An accurate timer utility for running periodic tasks on the given interval ticks or dates.

License

© 2026, Onur Yıldırım. MIT License.

About

A tiny, zero-dependency utility for measuring code execution time in high-resolution real time. Works in Node.js, browsers, Deno and Bun.

Topics

Resources

Stars

55 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages