Skip to content

@studnicky/timing

High-resolution timing tracker for collecting operation metrics.

Install

bash
pnpm add @studnicky/timing

@studnicky/timing declares a root usage API and explicit public subpaths.

Usage

Create a Timing instance with Timing.create() or a trusted TimingOptionsEntity.create(...) value, then record frozen component.operation[.status] data with TimingEvent.create({ component, operation, status? }). Elapsed milliseconds are collected in a flat map keyed by event name:

ts
import { TimingOptionsEntity } from '../src/entities/index.js';
import { Timing, TIMING_STATUS, TimingEvent } from '../src/index.js';

const timing = Timing.create(TimingOptionsEntity.create({ 'maximumEvents': 50 }));

// Record a plain component.operation event
timing.event(
  TimingEvent.create({ 'component': 'GraphAdapter', 'operation': 'query' })
);

// Record component.operation.status events
timing.event(
  TimingEvent.create({ 'component': 'CacheService', 'operation': 'get', 'status': TIMING_STATUS.START })
);

timing.event(
  TimingEvent.create({ 'component': 'CacheService', 'operation': 'get', 'status': TIMING_STATUS.COMPLETE })
);

timing.event(
  TimingEvent.create({ 'component': 'CacheService', 'operation': 'get', 'status': TIMING_STATUS.HIT })
);

const events = timing.getEvents();

console.log('events:', events);

No-op for production disabling

NoOpTiming implements the same interface with zero overhead; all calls are accepted and discarded:

ts
import { NoOpTiming, TimingEvent } from '../src/index.js';

const timing = NoOpTiming.create();

// Recording events is accepted without error but produces no stored data
timing.event(TimingEvent.create({ 'component': 'Cache', 'operation': 'get' }));
timing.event(TimingEvent.create({ 'component': 'Cache', 'operation': 'set' }));

const events = timing.getEvents();

// clear() returns the same instance for method chaining
const returned = timing.clear();

console.log('NoOpTiming.getEvents():', events);
console.log('clear() returns self:', returned === timing);

Try it

Direct factory

Timing.create(TimingOptionsEntity.create({ maximumEvents: 50 })) constructs the tracker. The example records a GraphAdapter.query event plus three CacheService.get events with start, complete, and hit statuses. The output map shows each event key with its elapsed-milliseconds value relative to instance creation.

Loading example…

Lifecycle hooks

ObservedTiming subclasses Timing and overrides five hooks: onInitialize, onEvent, onEvict, onClear, and onGetEvents. With maxEvents=3 the cache holds three entries; the fourth event triggers onEvict for the oldest. Watch the hook trace print for every operation, including the two getEvents() calls and the single clear().

Loading example…

Public API

The root exports Timing, TimingEvent, NoOpTiming, TIMING_STATUS, and TimingBuildError. @studnicky/timing/browser exports BrowserTiming, which uses the native Performance API. Schema-backed timing entities use @studnicky/timing/entities; TimingInterface uses @studnicky/timing/interfaces.

Extending

Timing exposes five protected observability hooks. Subclass and override them to intercept events for metrics export, structured logging, or test assertions:

ts
import { TimingOptionsEntity } from '../src/entities/index.js';
import { Timing, TimingEvent } from '../src/index.js';

class InstrumentedTiming extends Timing {
  readonly fired: string[] = [];

  protected override onEvent(data: { 'event': string }): void {
    this.fired.push(data.event);
  }

  static of(): InstrumentedTiming {
    return new InstrumentedTiming(TimingOptionsEntity.create());
  }
}

const timing = InstrumentedTiming.of();

timing.event(
  TimingEvent.create({ 'component': 'GraphAdapter', 'operation': 'query' })
);

timing.event(
  TimingEvent.create({ 'component': 'AuthService', 'operation': 'verify' })
);

console.log('fired:', timing.fired);

Observability hooks

HookWhen it firesArgs
onInitializeAfter the instance is fully initializedstartTime: bigint
onEventAfter an event is added to the cachedata: TimingEventDataEntity.Type, timestamp: bigint
onEvictBefore an event is evicted from the cachename: string
onClearBefore the cache is cleared(none)
onGetEventsAt the start of each getEvents() calleventCount: number
ts
import type { TimingEventDataEntity } from '../src/entities/index.js';

import { TimingOptionsEntity } from '../src/entities/index.js';
import { Timing, TimingEvent } from '../src/index.js';

class ObservedTiming extends Timing {
  // onInitialize fires inside super() before class field initializers run.
  // Use `declare` so TypeScript knows the type but emits no own-property
  // initializer that would reset the value after super() returns.
  declare initEvents: { 'startTime': bigint }[];
  recordedEvents: { 'data': TimingEventDataEntity.Type; 'timestamp': bigint }[] = [];
  evictedNames: string[] = [];
  clearCount = 0;
  getEventsCalls: { 'eventCount': number }[] = [];

  public constructor(options: TimingOptionsEntity.Type = TimingOptionsEntity.create()) {
    super(options);
  }

  protected override onInitialize(startTime: bigint): void {
    console.log(`[timing] initialize startTime=${startTime}`);
    // Bootstrap the array here because this fires before the field initializer.
    this.initEvents ??= [];
    this.initEvents.push({ 'startTime': startTime });
  }

  protected override onEvent(data: TimingEventDataEntity.Type, timestamp: bigint): void {
    console.log(`[timing] event name=${data.event} timestamp=${timestamp}`);
    this.recordedEvents.push({ 'data': data, 'timestamp': timestamp });
  }

  protected override onEvict(name: string): void {
    console.log(`[timing] evict name=${name}`);
    this.evictedNames.push(name);
  }

  protected override onClear(): void {
    console.log('[timing] clear');
    this.clearCount++;
  }

  protected override onGetEvents(eventCount: number): void {
    console.log(`[timing] getEvents eventCount=${eventCount}`);
    this.getEventsCalls.push({ 'eventCount': eventCount });
  }
}

// Create an ObservedTiming with a small maximumEvents to trigger eviction
const timing = new ObservedTiming(TimingOptionsEntity.create({ 'maximumEvents': 3 }));

// Record two events (cache: initialize + DbAdapter.query + CacheService.get = 3, at capacity)
timing.event(
  TimingEvent.create({ 'component': 'DbAdapter', 'operation': 'query' })
);

timing.event(
  TimingEvent.create({ 'component': 'CacheService', 'operation': 'get' })
);

// Call getEvents to trigger onGetEvents (3 entries in cache)
const snapshot = timing.getEvents();
console.log('snapshot keys:', [...snapshot.keys()]);

// Clear to trigger onClear
timing.clear();

// Fill cache to capacity (maximumEvents: 3) then overflow to trigger eviction
timing.event(
  TimingEvent.create({ 'component': 'DbAdapter', 'operation': 'insert' })
);

timing.event(
  TimingEvent.create({ 'component': 'CacheService', 'operation': 'set' })
);

timing.event(
  TimingEvent.create({ 'component': 'MetricsService', 'operation': 'flush' })
);

// This 4th event overflows the cache — evicts DbAdapter.insert
timing.event(
  TimingEvent.create({ 'component': 'MetricsService', 'operation': 'emit' })
);

// Final getEvents call
const final = timing.getEvents();
console.log('final keys:', [...final.keys()]);

The base class never calls any logger or metrics library. All hooks are no-ops by default.

Source on GitHub

Entities

@studnicky/timing/entities exports every schema namespace in src/entities.

typescript
import { TimingEventDataEntity } from '@studnicky/timing/entities';

Interfaces

@studnicky/timing/interfaces exports every TypeScript interface in src/interfaces, including configuration and state contracts.

typescript
import type { TimingInterface } from '@studnicky/timing/interfaces';

Exports

SymbolPurposeImport path
TIMING_STATUSProvides supported timing status values.@studnicky/timing
NoOpTimingProvides no op timing functionality.@studnicky/timing
TimingProvides timing functionality.@studnicky/timing
TimingBuildErrorRepresents timing build failures.@studnicky/timing
TimingEventProvides timing event functionality.@studnicky/timing
BrowserTimingProvides timing through the browser Performance API.@studnicky/timing/browser