Skip to content

Commit

Permalink
feat: use histogram and summary for elu calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-tymoshenko committed Jan 10, 2024
1 parent b619eff commit f4fac17
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 45 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ available on Linux.
- `register` to which registry the metrics should be registered. Default: the global default registry.
- `gcDurationBuckets` with custom buckets for GC duration histogram. Default buckets of GC duration histogram are `[0.001, 0.01, 0.1, 1, 2, 5]` (in seconds).
- `eventLoopMonitoringPrecision` with sampling rate in milliseconds. Must be greater than zero. Default: 10.
- `eventLoopUtilizationTimeout` measurement duration in milliseconds. Must be greater than zero. Default: 100.
- `eventLoopUtilizationTimeout` interval in milliseconds to calculate event loop utilization. Must be greater than zero. Default: 100.
- `eventLoopUtilizationBuckets` with custom buckets for event loop utilization histogram. Default buckets of event loop utilization histogram are `[0.01, 0.05, 0.1, 0.25, 0.5, 0.6, 0.7, 0.75, 0.8, 0.9, 0.95, 0.99, 1]` (in seconds).
- `eventLoopUtilizationPercentiles` with custom percentiles for event loop utilization summary. Default percentiles of event loop utilization summary are `[0.01, 0.05, 0.5, 0.9, 0.95, 0.99, 0.999]`.
- `eventLoopUtilizationMaxAgeSeconds` summary sliding window time in seconds. Must be greater than zero. Default: 60.
- `eventLoopUtilizationAgeBuckets` summary sliding window buckets. Must be greater than zero. Default: 5.

To register metrics to another registry, pass it in as `register`:

Expand Down
4 changes: 4 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,10 @@ export interface DefaultMetricsCollectorConfiguration<
gcDurationBuckets?: number[];
eventLoopMonitoringPrecision?: number;
eventLoopUtilizationTimeout?: number;
eventLoopUtilizationBuckets?: number[];
eventLoopUtilizationPercentiles?: number[];
eventLoopUtilizationAgeBuckets: number;
eventLoopUtilizationMaxAgeSeconds: number;
labels?: Object;
}

Expand Down
86 changes: 70 additions & 16 deletions lib/metrics/eventLoopUtilization.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const Gauge = require('../gauge');
const Summary = require('../summary');
const Histogram = require('../histogram');

// Check if perf_hooks module is available
let perf_hooks;
Expand All @@ -12,7 +13,19 @@ try {
}

// Reported always.
const NODEJS_EVENTLOOP_UTILIZATION = 'nodejs_eventloop_utilization';
const NODEJS_EVENTLOOP_UTILIZATION_SUMMARY =
'nodejs_eventloop_utilization_summary';

const NODEJS_EVENTLOOP_UTILIZATION_HISTOGRAM =
'nodejs_eventloop_utilization_histogram';

const DEFAULT_ELU_HISTOGRAM_BUCKETS = [
0.01, 0.05, 0.1, 0.25, 0.5, 0.6, 0.7, 0.75, 0.8, 0.9, 0.95, 0.99, 1,
];

const DEFAULT_ELU_SUMMARY_PERCENTILES = [
0.01, 0.05, 0.5, 0.9, 0.95, 0.99, 0.999,
];

module.exports = (registry, config = {}) => {
if (
Expand All @@ -30,23 +43,64 @@ module.exports = (registry, config = {}) => {
const labelNames = Object.keys(labels);
const registers = registry ? [registry] : undefined;

new Gauge({
name: namePrefix + NODEJS_EVENTLOOP_UTILIZATION,
const ageBuckets = config.eventLoopUtilizationAgeBuckets
? config.eventLoopUtilizationAgeBuckets
: 5;

const maxAgeSeconds = config.eventLoopUtilizationMaxAgeSeconds
? config.eventLoopUtilizationMaxAgeSeconds
: 60;

const percentiles = config.eventLoopUtilizationSummaryPercentiles
? config.eventLoopUtilizationSummaryPercentiles
: DEFAULT_ELU_SUMMARY_PERCENTILES;

const summary = new Summary({
name: namePrefix + NODEJS_EVENTLOOP_UTILIZATION_SUMMARY,
help: 'Ratio of time the event loop is not idling in the event provider to the total time the event loop is running.',
maxAgeSeconds,
ageBuckets,
percentiles,
registers,
labelNames,
});

const buckets = config.eventLoopUtilizationBuckets
? config.eventLoopUtilizationBuckets
: DEFAULT_ELU_HISTOGRAM_BUCKETS;

const histogram = new Histogram({
name: namePrefix + NODEJS_EVENTLOOP_UTILIZATION_HISTOGRAM,
help: 'Ratio of time the event loop is not idling in the event provider to the total time the event loop is running.',
buckets,
registers,
labelNames,
async collect() {
const start = eventLoopUtilization();

return new Promise(resolve => {
setTimeout(() => {
const end = eventLoopUtilization();
this.set(labels, eventLoopUtilization(end, start).utilization);
resolve();
}, config.eventLoopUtilizationTimeout || 100);
});
},
});

const intervalTimeout = config.eventLoopUtilizationTimeout || 100;

let elu1 = eventLoopUtilization();
let start = process.hrtime();

setInterval(() => {
const elu2 = eventLoopUtilization();
const end = process.hrtime();

const timeMs = (end[0] - start[0]) * 1000 + (end[1] - start[1]) / 1e6;
const value = eventLoopUtilization(elu2, elu1).utilization;

const blockedIntervalsNumber = Math.round(timeMs / intervalTimeout);
for (let i = 0; i < blockedIntervalsNumber; i++) {
summary.observe(value);
histogram.observe(value);
}

elu1 = elu2;
start = end;
}, intervalTimeout).unref();
};

module.exports.metricNames = [NODEJS_EVENTLOOP_UTILIZATION];
module.exports.metricNames = [
NODEJS_EVENTLOOP_UTILIZATION_SUMMARY,
NODEJS_EVENTLOOP_UTILIZATION_HISTOGRAM,
];
109 changes: 81 additions & 28 deletions test/metrics/eventLoopUtilizationTest.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use strict';

describe('eventLoopUtilization', () => {
const register = require('../../index').register;
const elu = require('../../lib/metrics/eventLoopUtilization');
const { eventLoopUtilization } = require('perf_hooks').performance;
const { setTimeout: sleep } = require('timers/promises');
const register = require('../../index').register;
const elu = require('../../lib/metrics/eventLoopUtilization');
const { eventLoopUtilization } = require('perf_hooks').performance;

describe('eventLoopUtilization', () => {
beforeAll(() => {
register.clear();
});
Expand All @@ -14,42 +15,94 @@ describe('eventLoopUtilization', () => {
});

it('should add metric to the registry', async () => {
if (!eventLoopUtilization) return;

expect(await register.getMetricsAsJSON()).toHaveLength(0);

if (!eventLoopUtilization) return;
elu(register, { eventLoopUtilizationTimeout: 50 });

const expectedELU = Math.random();
await blockEventLoop(expectedELU, 3000);

elu(register, { eventLoopUtilizationTimeout: 1000 });
const metrics = await register.getMetricsAsJSON();
expect(metrics).toHaveLength(2);

const eluStart = eventLoopUtilization();
const metricsPromise = register.getMetricsAsJSON();
{
const percentilesCount = 7;

setImmediate(() => blockEventLoop(500));
const eluSummaryMetric = metrics[0];
expect(eluSummaryMetric.type).toEqual('summary');
expect(eluSummaryMetric.name).toEqual(
'nodejs_eventloop_utilization_summary',
);
expect(eluSummaryMetric.help).toEqual(
'Ratio of time the event loop is not idling in the event provider to the total time the event loop is running.',
);
expect(eluSummaryMetric.values).toHaveLength(percentilesCount + 2);

const metrics = await metricsPromise;
const eluEnd = eventLoopUtilization();
const sum = eluSummaryMetric.values[percentilesCount];
const count = eluSummaryMetric.values[percentilesCount + 1];

expect(metrics).toHaveLength(1);
expect(sum.metricName).toEqual(
'nodejs_eventloop_utilization_summary_sum',
);
expect(count.metricName).toEqual(
'nodejs_eventloop_utilization_summary_count',
);
const calculatedELU = sum.value / count.value;
const delta = Math.abs(calculatedELU - expectedELU);
expect(delta).toBeLessThanOrEqual(0.05);
}

const eluMetric = metrics[0];
expect(eluMetric.help).toEqual(
'Ratio of time the event loop is not idling in the event provider to the total time the event loop is running.',
);
expect(eluMetric.type).toEqual('gauge');
expect(eluMetric.name).toEqual('nodejs_eventloop_utilization');
expect(eluMetric.values).toHaveLength(1);
{
const bucketsCount = 14;

const eluValue = eluMetric.values[0].value;
expect(eluValue).toBeGreaterThanOrEqual(0);
expect(eluValue).toBeLessThanOrEqual(1);
const eluHistogramMetric = metrics[1];
expect(eluHistogramMetric.type).toEqual('histogram');
expect(eluHistogramMetric.name).toEqual(
'nodejs_eventloop_utilization_histogram',
);
expect(eluHistogramMetric.help).toEqual(
'Ratio of time the event loop is not idling in the event provider to the total time the event loop is running.',
);
expect(eluHistogramMetric.values).toHaveLength(bucketsCount + 2);

const expectedELU = eventLoopUtilization(eluEnd, eluStart).utilization;
expect(eluValue).toBeCloseTo(expectedELU, 2);
const sum = eluHistogramMetric.values[bucketsCount];
const count = eluHistogramMetric.values[bucketsCount + 1];

expect(sum.metricName).toEqual(
'nodejs_eventloop_utilization_histogram_sum',
);
expect(count.metricName).toEqual(
'nodejs_eventloop_utilization_histogram_count',
);
const calculatedELU = sum.value / count.value;
const delta = Math.abs(calculatedELU - expectedELU);
expect(delta).toBeLessThanOrEqual(0.05);

const infBucket = eluHistogramMetric.values[bucketsCount - 1];
expect(infBucket.labels.le).toEqual('+Inf');
expect(infBucket.value).toEqual(count.value);

const le1Bucket = eluHistogramMetric.values[bucketsCount - 2];
expect(le1Bucket.labels.le).toEqual(1);
expect(le1Bucket.value).toEqual(count.value);
}
});
});

function blockEventLoop(ms) {
const start = Date.now();
while (Date.now() - start < ms) {
// heavy operations
async function blockEventLoop(ratio, ms) {
const frameMs = 1000;
const framesNumber = Math.round(ms / frameMs);

const blockedFrameTime = ratio * frameMs;
const freeFrameTime = frameMs - blockedFrameTime;

for (let i = 0; i < framesNumber; i++) {
const endBlockedTime = Date.now() + blockedFrameTime;
while (Date.now() < endBlockedTime) {
// heavy operations
}
await sleep(freeFrameTime);
}
}

0 comments on commit f4fac17

Please sign in to comment.