lag-monitor
Version:
A lightweight utility for monitoring JavaScript event loop lag in real-time
227 lines (224 loc) • 5.98 kB
JavaScript
// src/CircularBuffer.ts
var CircularBuffer = class {
constructor(capacity) {
this.capacity = capacity;
this.index = 0;
this.filled = 0;
this.buffer = new Array(capacity);
}
push(item) {
this.buffer[this.index] = item;
this.index = (this.index + 1) % this.capacity;
this.filled = Math.min(this.filled + 1, this.capacity);
}
get latest() {
if (this.filled === 0) return void 0;
const lastIndex = (this.index - 1 + this.capacity) % this.capacity;
return this.buffer[lastIndex];
}
toArray() {
if (this.filled < this.capacity) {
return this.buffer.slice(0, this.filled);
}
return [
...this.buffer.slice(this.index),
...this.buffer.slice(0, this.index)
];
}
get length() {
return this.filled;
}
clear() {
this.index = 0;
this.filled = 0;
}
};
var CircularBuffer_default = CircularBuffer;
// src/LagMonitor.ts
var DEFAULT_OPTIONS = {
lagType: "setTimeout",
sampleRate: 100,
sampleCount: 10,
callback: void 0,
autoStart: true
};
var LagMonitor = class {
constructor(options = DEFAULT_OPTIONS) {
this.isActive = false;
this.timerId = null;
this.lastTimestamp = void 0;
this.options = Object.assign({}, DEFAULT_OPTIONS, options);
this.userMetrics = options.userMetrics || {};
this.lagSamples = new CircularBuffer_default(
this.options.sampleCount || DEFAULT_OPTIONS.sampleCount
);
if (this.options.autoStart) {
this.start();
}
}
// Built-in getters
get latest() {
return this.lagSamples.latest || 0;
}
get average() {
return this.lagSamples.toArray().reduce((sum, val) => sum + val, 0) / this.lagSamples.length;
}
get min() {
return Math.min(...this.lagSamples.toArray());
}
get max() {
return Math.max(...this.lagSamples.toArray());
}
scheduleNext(fn, delay) {
this.clearTimer();
if (this.options.lagType === "requestAnimationFrame") {
if (typeof requestAnimationFrame !== "undefined") {
return requestAnimationFrame(fn);
}
throw new Error(
"requestAnimationFrame is not supported in this environment. Use setTimeout or setInterval instead."
);
} else if (this.options.lagType === "setInterval") {
return setInterval(fn, delay);
} else {
return setTimeout(fn, delay);
}
}
tick() {
if (!this.isActive) return;
const now = performance.now();
const lag = this.lastTimestamp ? Math.max(
0,
now - this.lastTimestamp - (this.options.sampleRate || DEFAULT_OPTIONS.sampleRate)
) : 0;
this.lastTimestamp = now;
this.lagSamples.push(lag);
if (this.options.callback) {
try {
this.options.callback(lag, this.snapshot());
} catch (error) {
console.warn("Lag monitor callback error:", error);
}
}
this.clearTimer();
this.timerId = this.scheduleNext(
() => this.tick(),
// Anon fxn in order to preserve `this` context
this.options.sampleRate || DEFAULT_OPTIONS.sampleRate
);
}
clearTimer() {
if (this.timerId) {
if (this.options.lagType === "requestAnimationFrame") {
cancelAnimationFrame(this.timerId);
} else if (this.options.lagType === "setInterval") {
clearInterval(this.timerId);
} else {
clearTimeout(this.timerId);
}
this.timerId = null;
}
}
/**
* Get a metric value by name - handles both built-in and user-defined metrics
*/
getMetricValue(metric) {
const userMetric = this.userMetrics[metric];
if (userMetric) {
try {
return userMetric(this.lagSamples.toArray());
} catch (error) {
console.warn(`Error computing user metric '${metric}':`, error);
return void 0;
}
}
switch (metric) {
case "latest":
return this.latest;
case "average":
return this.average;
case "min":
return this.min;
case "max":
return this.max;
case "samples":
return this.lagSamples.length;
}
return void 0;
}
/**
* Get all available metric functions (built-in + user-defined)
*/
getAvailableMetrics() {
const builtInMetrics = ["latest", "average", "min", "max", "samples"];
const userMetricNames = Object.keys(this.userMetrics);
return [...builtInMetrics, ...userMetricNames];
}
/**
* Compute a specific metric or all metrics
*/
snapshot(metric) {
if (metric) {
const value = this.getMetricValue(metric);
return { [metric]: value };
}
const result = {};
this.getAvailableMetrics().forEach((metricName) => {
result[metricName] = this.getMetricValue(metricName);
});
return result;
}
start() {
if (this.isActive) {
console.warn("Lag monitor is already running");
return;
}
this.isActive = true;
this.lastTimestamp = performance.now();
this.timerId = this.scheduleNext(
() => this.tick(),
this.options.sampleRate || DEFAULT_OPTIONS.sampleRate
);
}
stop() {
if (!this.isActive) {
console.warn("Lag monitor is not running");
return;
}
this.isActive = false;
this.clearTimer();
this.lastTimestamp = void 0;
}
reset() {
this.lagSamples.clear();
this.lastTimestamp = performance.now();
this.isActive = false;
this.clearTimer();
this.timerId = null;
}
restart() {
this.reset();
this.start();
}
isRunning() {
return this.isActive;
}
};
var LagMonitor_default = LagMonitor;
// src/index.ts
var index_default = LagMonitor_default;
export {
CircularBuffer_default as CircularBuffer,
LagMonitor_default as LagMonitor,
index_default as default
};
/**
* Event Loop Lag Monitor
*
* A lightweight utility for monitoring JavaScript event loop lag in real-time.
* Useful for performance monitoring, debugging, and alerting on event loop blocking.
*
* @author Your Name
* @version 1.0.0
* @license MIT
*/