@kree4js/commons-performance
v1.0.1
Published
Commons Performance, used inside kree4js
Maintainers
Readme
@kree4js/commons-performance
A high-performance, tag-based metrics and timing framework for Node.js and browsers. Provides synchronous low-overhead measurement (Timeline) and asynchronous periodic statistical analysis (Analyzer) for performance monitoring, SLA tracking, and business metrics collection.
Table of Contents
Installation
npm install @kree4js/commons-performanceimport { Analyzer, Timeline } from '@kree4js/commons-performance'Quick Start
import { Analyzer } from '@kree4js/commons-performance'
const analyzer = Analyzer.from('api-server')
.addSpan('Request.Start', 'Request.End', 'route:users').as('usersLatency')
.addCounter('Request.End', 'route:users').as('usersCount')
.addReportPeriod('minute')
.addReportPeriod('hour')
.start()
analyzer.mark('Request.Start', 'route:users')
await handleRequest()
analyzer.mark('Request.End', 'route:users')
analyzer.on('report', (report) => {
console.log(report.type, report.tags, report.metrics)
})
// Example report output:
// {
// type: "Analyzer-minute",
// tags: "route:users",
// metrics: {
// usersLatency: { count: 150, max: 45, min: 12, mean: 28.5, sum: 4275, lastTs: 1715423400000, lastValue: 31 },
// usersCount: 150
// }
// }Core Concepts
| Concept | Description |
|---------|-------------|
| Marker | A timestamped event point. Has a name, optional value, and optional tag. |
| Span | Calculates the time delta between two Markers. Produces count/max/min/mean/sum/lastTs/lastValue. |
| Counter | Counts how many times a specific Marker name occurred. Produces a single number. |
| Scalar | Tracks raw numeric values and computes statistics. Produces count/max/min/mean/sum. |
| Rate | Computes the ratio of two Counters or Scalars. Produces a single number. |
| Tag | A filter string in key:value format. Enables multi-dimensional metric breakdowns. |
| Timeline | Synchronous measurement engine. Records Markers and computes Spans on demand, zero async overhead. |
| Analyzer | Asynchronous periodic analysis engine. Buffers Markers, processes via Calculators, and emits report events. |
| snapshot() | Returns a three-level keyed object: __main__ (L2), tag names (L1), full tag strings (L0). |
Metric Types
Span (Time Interval)
Calculates the time difference between a startMarker and an endMarker. Each Span instance tracks one specific metric (determined by Marker names and Tags).
measure() output fields:
| Field | Type | Description |
|-------|------|-------------|
| count | number | Number of measurements |
| max | number | Maximum time delta (milliseconds) |
| min | number | Minimum time delta (milliseconds) |
| mean | number | Arithmetic mean (sum / count) |
| sum | number | Cumulative time delta (milliseconds) |
| lastTs | number\|undefined | Timestamp of the last measurement |
| lastValue | number\|undefined | Time delta of the last measurement (milliseconds) |
const analyzer = Analyzer.from('example')
.addSpan('Start', 'End').as('latency')
.addSpan('DB.Start', 'DB.End', 'db:mysql').as('dbLatency')
.start()
analyzer.mark('Start')
analyzer.mark('DB.Start', 'db:mysql')
// ... database query ...
analyzer.mark('DB.End', 'db:mysql')
analyzer.mark('End')
const r = analyzer.snapshot()
// r.__main__.latency → { count: 1, max: N, min: N, mean: N, sum: N, lastTs: N, lastValue: N }
// r['db:mysql'].dbLatency → { count: 1, max: N, min: N, mean: N, sum: N, lastTs: N, lastValue: N }Note: The endMarker must be marked after the corresponding startMarker. If an endMarker appears before the startMarker, that measurement is ignored.
Counter
Counts how many times a marker name occurs. Each mark(name) call increments the counter by 1.
const analyzer = Analyzer.from('traffic')
.addCounter('Request.End', 'route:users').as('usersCount')
.addCounter('Error.Occurred', 'route:users').as('usersErrors')
.addReportPeriod('minute')
.start()
analyzer.mark('Request.End', 'route:users')
analyzer.mark('Request.End', 'route:users')
analyzer.mark('Error.Occurred', 'route:users')
const r = analyzer.snapshot()
// r['route:users'].usersCount → 2
// r['route:users'].usersErrors → 1Counter.measure() returns a single number (the count), not an object.
Scalar
Tracks a sequence of raw numeric values and computes statistics. Unlike Span, Scalar does not compute time deltas — it tracks arbitrary values you pass in.
const analyzer = Analyzer.from('resources')
.addScalar('Queue.Depth', 'queue:order').as('orderQueueDepth')
.addScalar('Memory.Used', 'server:prod').as('memoryUsage')
.addReportPeriod('minute')
.start()
// The second argument is the numeric value
analyzer.mark('Queue.Depth', 10, 'queue:order')
analyzer.mark('Queue.Depth', 25, 'queue:order')
analyzer.mark('Queue.Depth', 15, 'queue:order')
const r = analyzer.snapshot()
// r['queue:order'].orderQueueDepth → { count: 3, max: 25, min: 10, mean: 16.67, sum: 50 }Rate
Computes a ratio by referencing existing Counters or Scalars. Rate does not create new Marker listeners — it reads data from the referenced metrics.
addRate() has four call forms:
// 1. addRate(numeratorName, denominatorName)
// Both numerator and denominator are un-tagged Counters/Scalars
analyzer.addRate('success', 'total')
// 2. addRate(numeratorName, denominatorName, rateTag)
// Both numerator and denominator must have Counters/Scalars with rateTag
analyzer.addRate('success', 'total', 'route:users')
// 3. addRate(numeratorName, denominatorName, rateTag, metricTag)
// Both numerator and denominator must have metricTag, Rate itself gets rateTag
analyzer.addRate('success', 'total', 'good', 'math:a')
// 4. addRate(numeratorName, denominatorName, rateTag, numeratorTag, denominatorTag)
// Numerator has numeratorTag, denominator has denominatorTag, Rate gets rateTag
analyzer.addRate('success', 'total', 'good', 'math:a', 'special:b')Important: The referenced Counter/Scalar must be added via addCounter() / addScalar() before calling addRate().
const analyzer = Analyzer.from('reliability')
.addCounter('Request.Success', 'route:users')
.addCounter('Request.End', 'route:users')
.addRate('Request.Success', 'Request.End', 'route:users').as('successRate')
.start()
analyzer.mark('Request.Success', 'route:users')
analyzer.mark('Request.Success', 'route:users')
analyzer.mark('Request.End', 'route:users')
analyzer.mark('Request.End', 'route:users')
analyzer.mark('Request.End', 'route:users')
const r = analyzer.snapshot()
// r['route:users'].successRate → 0.666...Tag System
Syntax
Tags use key:value string format. key is the tag name, value is the tag value.
"route:users" → name="route", value="users"
"env:prod" → name="env", value="prod"
"math" → name="math", value=undefined (name-only tag)Compound Tags: Use ; as an AND separator to combine multiple sub-tags. Each sub-tag is separated by semicolon and processed together.
"call:123;dst:nodeB" → [{ name: "call", value: "123" }, { name: "dst", value: "nodeB" }]Tags serve two purposes:
- Match: Determines which Markers are processed by which Spans/Counters/Scalars
- Group: Determines how metric data aggregates into different levels
Three-Level Aggregation (L0 / L1 / L2)
snapshot() returns a keyed object with three aggregation levels:
| Level | Key Example | Meaning |
|-------|------------|---------|
| L0 | "route:users", "env:prod" | Metrics for the exact tag string |
| L1 | "route", "env" | All L0 entries merged by tag name |
| L2 | "__main__" | All data (including un-tagged) fully aggregated |
Example:
const analyzer = Analyzer.from('example')
.addSpan('A', 'B')
.addSpan('A', 'B', 'route:users')
.addSpan('A', 'B', 'route:orders')
.addCounter('req.count')
analyzer.mark('A')
analyzer.mark('B')
analyzer.mark('A', 'route:users')
analyzer.mark('B', 'route:users')
analyzer.mark('A', 'route:orders')
analyzer.mark('B', 'route:orders')
analyzer.mark('req.count')
analyzer.mark('req.count')
const r = analyzer.snapshot()
// r.__main__ → { "A-B": spanStats, "req.count": 2 } // L2: full aggregation
// r.route → { "A-B": mergedSpanStats } // L1: merged by tag name
// r["route:users"] → { "A-B": spanStats } // L0: exact tag
// r["route:orders"] → { "A-B": spanStats } // L0: exact tag"main" is not a Tag — it is a reserved key representing the L2 full aggregation: including un-tagged data plus all L0/L1 data. Using __main__ as a tag name is prohibited and will throw an error.
Compound Tags and L1 Aggregation: When using compound tags, data is aggregated into multiple L1 groups simultaneously — one for each sub-tag name.
const analyzer = Analyzer.from('example')
.addSpan('A', 'B', 'call:123;dst:nodeB').as('latency')
.start()
analyzer.mark('A', 'call:123;dst:nodeB')
analyzer.mark('B', 'call:123;dst:nodeB')
const r = analyzer.snapshot()
// r["call:123;dst:nodeB"].latency → { count: 1, ... } // L0: exact compound tag
// r.call.latency → { count: 1, ... } // L1: aggregated by "call"
// r.dst.latency → { count: 1, ... } // L1: aggregated by "dst"
// r.__main__.latency → { count: 1, ... } // L2: full aggregationTag Canonicalization: Compound tags are automatically canonicalized — sub-tags are sorted by name. This ensures call:123;dst:nodeB and dst:nodeB;call:123 are treated as identical.
analyzer.mark('A', 'dst:nodeB;call:123') // Order doesn't matter
analyzer.mark('B', 'call:123;dst:nodeB') // Same as above, canonicalized to same keyTag Matching Rules
The tag specified when defining a Span determines which Markers trigger that Span:
// Only matches Markers of A and B that carry the "route:users" tag
analyzer.addSpan('A', 'B', 'route:users')
// Works: Markers with "route:users" tag
analyzer.mark('A', 'route:users')
analyzer.mark('B', 'route:users')
// Does NOT work: this Marker lacks "route:users" tag, will not be processed
analyzer.mark('A', 'route:orders')Span tag source precedence:
- If
spanTagis defined, usespanTag - If
startTag === endTag, usestartTag - If
startTagandendTagdiffer, join as"startTag-endTag" - If no tags at all, returns
undefined
API Reference
Analyzer
Asynchronous periodic statistical analysis engine. Extends EventEmitter.
import { Analyzer } from '@kree4js/commons-performance'
const analyzer = Analyzer.from('app-name', { slideWindowSize: 1024 })Constructor options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| slideWindowSize | number | 1024 | Marker buffer capacity; old data is automatically discarded when full |
Fluent API:
analyzer
.addSpan(startMarker, endMarker, [startTag], [endTag]) // Define time delta measurement
.as('name') // Rename the last added metric
.addCounter(marker, [tag]) // Define a counter
.addScalar(marker, [tag]) // Define a scalar
.addRate(num, denom, [rateTag], [numTag], [denomTag]) // Define a rate
.addReportPeriod('minute') // Enable minute reports
.addReportPeriod('hour') // Enable hour reports
.addReportPeriod('day') // Enable day reports
.start() // Start all Calculatorsmark(name, valueOrTag, ...tags):
mark() is the core method with multiple overload forms:
// 1. Marker name only (for Span and Counter)
analyzer.mark('Request.Start')
// 2. Marker + single tag
analyzer.mark('Request.Start', 'route:users')
// 3. Marker + multiple tags (marks multiple tags simultaneously)
analyzer.mark('Request.Start', 'route:users', 'env:prod')
// 4. Marker + numeric value (for Scalar)
analyzer.mark('Queue.Depth', 42)
// 5. Marker + value + single tag
analyzer.mark('Queue.Depth', 42, 'queue:order')
// 6. Marker + value + multiple tags
analyzer.mark('Queue.Depth', 42, 'queue:order', 'env:prod')Multi-tag semantics: When multiple tags are passed to mark(), one Marker is created for each tag. For example, mark('A', 'x:1', 'y:2') creates two Markers: one tagged "x:1" and another tagged "y:2". This means a single mark call can feed multiple dimensional Spans/Counters simultaneously.
const analyzer = Analyzer.from('example')
.addSpan('A', 'B', 'x:1')
.addSpan('A', 'B', 'y:2')
.start()
// One mark call satisfies both x:1 and y:2 Spans
analyzer.mark('A', 'x:1', 'y:2')
analyzer.mark('B', 'x:1', 'y:2')
const r = analyzer.snapshot()
// Both r['x:1']['A-B'] and r['y:2']['A-B'] have datasnapshot(deadline?):
Returns a three-level keyed object snapshot of all current metrics. Automatically flushes all unprocessed Markers.
const r = analyzer.snapshot()
// r = {
// "route:users": { "A-B": spanStats, "req.count": 5 },
// "route:orders": { "A-B": spanStats, "req.count": 3 },
// route: { "A-B": mergedSpanStats, "req.count": 8 },
// __main__: { "A-B": mergedSpanStats, "req.count": 8 }
// }An optional deadline parameter specifies the timestamp cutoff (defaults to Date.now() + 1). Only Markers before the deadline are flushed.
const r = analyzer.snapshot(Date.now() + 60_000) // Only flush data older than 60 secondsstart() / stop() / reset():
analyzer.start() // Start all Calculators, begin emitting report events
analyzer.stop() // Stop all Calculators
analyzer.reset() // Reset all metrics (Span/Counter/Scalar), does not clear the Marker queueEvents:
analyzer.on('report', (report) => {
// Example report output:
// {
// type: "Analyzer-minute",
// tags: "route:users",
// metrics: {
// usersLatency: { count: 150, max: 45, min: 12, mean: 28.5, sum: 4275, lastTs: 1715423400000, lastValue: 31 },
// usersCount: 150
// }
// }
})Timeline
Synchronous low-overhead measurement engine. Records Markers and computes Spans on demand with zero async overhead, suitable for hot paths.
import { Timeline } from '@kree4js/commons-performance'
const timeline = Timeline.from('http-request')
.addSpan('Request.Start', 'Request.End').as('totalLatency')
.addSpan('DB.Start', 'DB.End').as('dbLatency')
timeline.mark('Request.Start')
// ... process ...
timeline.mark('DB.Start')
// ... database query ...
timeline.mark('DB.End')
timeline.mark('Request.End')
const total = timeline.measureNamed('totalLatency')
// { count: 1, max: N, min: N, mean: N, sum: N, lastTs: N, lastValue: N }
const db = timeline.measureNamed('dbLatency')
// { count: 1, max: N, min: N, mean: N, sum: N, lastTs: N, lastValue: N }
timeline.reset() // Reset all Spans for the next measurement cycleTimeline with Tags:
const timeline = Timeline.from('multi-route')
.addSpan('Request.Start', 'Request.End', 'route:users').as('usersLatency')
.addSpan('Request.Start', 'Request.End', 'route:orders').as('ordersLatency')
timeline.mark('Request.Start', 'route:users')
timeline.mark('Request.End', 'route:users')
timeline.mark('Request.Start', 'route:orders')
timeline.mark('Request.End', 'route:orders')
timeline.measureNamed('usersLatency', 'route:users') // { ... }
timeline.measureNamed('ordersLatency', 'route:orders') // { ... }Measure by Marker names (no alias needed):
timeline.measureMarker('Request.Start', 'Request.End', 'route:users')
// Equivalent to measureNamed('usersLatency', 'route:users') (when .as() is used).as(name, tag?):
timeline
.addSpan('A', 'B')
.as('mySpan', 'custom:tag') // Rename + optional Span tagSpan
A metric type that calculates the time difference between two Markers.
import { Measure } from '@kree4js/commons-performance'
// Measure is the public export alias for Span
// Internal usage:
span.mark(marker) // Process a single Marker
span.mark([marker1, marker2]) // Process Markers in batch
span.measure() // Get SpanMeasureResult
span.reset() // Reset the SpanCounter
A metric type that counts Marker occurrences.
import { Counter } from '@kree4js/commons-performance'
const counter = new Counter('Request.End', 'myName', 'route:users')
counter.update(3) // +3
counter.update() // +1
counter.measure() // → 4 (single number)
counter.reset() // → 0Scalar
A metric type that tracks a sequence of raw numeric values.
import { Scalar } from '@kree4js/commons-performance'
const scalar = new Scalar('Queue.Depth', 'queue:order', 'myScalar')
scalar.update(10)
scalar.update(20)
scalar.update(30)
scalar.measure() // → { count: 3, max: 30, min: 10, mean: 20, sum: 60 }
scalar.reset()Rate
A metric type that computes the ratio of two metrics.
import { Rate, Counter } from '@kree4js/commons-performance'
const success = new Counter('success')
const total = new Counter('total')
const rate = new Rate(success, total, 'route:users')
rate.measure() // → 0 (total is 0)
success.update(3)
total.update(5)
rate.measure() // → 0.6
rate.reset() // Resets both numerator and denominator