@studnicky/timing
High-resolution timing tracker for collecting operation metrics.
Install
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:
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:
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.
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().
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:
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
| Hook | When it fires | Args |
|---|---|---|
onInitialize | After the instance is fully initialized | startTime: bigint |
onEvent | After an event is added to the cache | data: TimingEventDataEntity.Type, timestamp: bigint |
onEvict | Before an event is evicted from the cache | name: string |
onClear | Before the cache is cleared | (none) |
onGetEvents | At the start of each getEvents() call | eventCount: number |
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.
Entities
@studnicky/timing/entities exports every schema namespace in src/entities.
import { TimingEventDataEntity } from '@studnicky/timing/entities';Interfaces
@studnicky/timing/interfaces exports every TypeScript interface in src/interfaces, including configuration and state contracts.
import type { TimingInterface } from '@studnicky/timing/interfaces';Exports
| Symbol | Purpose | Import path |
|---|---|---|
TIMING_STATUS | Provides supported timing status values. | @studnicky/timing |
NoOpTiming | Provides no op timing functionality. | @studnicky/timing |
Timing | Provides timing functionality. | @studnicky/timing |
TimingBuildError | Represents timing build failures. | @studnicky/timing |
TimingEvent | Provides timing event functionality. | @studnicky/timing |
BrowserTiming | Provides timing through the browser Performance API. | @studnicky/timing/browser |