Skip to content

Commit

Permalink
feat: implement GC metrics collection without native(C++) modules
Browse files Browse the repository at this point in the history
  • Loading branch information
Yuriy Vasiyarov committed Sep 2, 2019
1 parent f326857 commit 99f65d3
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ project adheres to [Semantic Versioning](http://semver.org/).
### Changed

### Added
- `nodejs_gc_runs` metric to the `collectDefaultMetrics()`. It counts number of GC runs with split by GC type.
- `nodejs_gc_duration_summary` metric to the `collectDefaultMetrics()`. It counts 0.5, 0.75, 0.9, 0.99 percentiles of GC duration (in seconds).

## [11.5.3] - 2019-06-27

Expand Down
13 changes: 13 additions & 0 deletions example/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ if (cluster.isWorker) {
}, 2000);
}

// Generate some garbage
const t = [];
setInterval(() => {
for (let i = 0; i < 100; i++) {
t.push(new Date());
}
}, 10);
setInterval(() => {
while (t.length > 0) {
t.pop();
}
});

server.get('/metrics', (req, res) => {
res.set('Content-Type', register.contentType);
res.end(register.metrics());
Expand Down
4 changes: 3 additions & 1 deletion lib/defaultMetrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const processRequests = require('./metrics/processRequests');
const heapSizeAndUsed = require('./metrics/heapSizeAndUsed');
const heapSpacesSizeAndUsed = require('./metrics/heapSpacesSizeAndUsed');
const version = require('./metrics/version');
const gc = require('./metrics/gc');
const { globalRegistry } = require('./registry');
const { printDeprecationCollectDefaultMetricsNumber } = require('./util');

Expand All @@ -25,7 +26,8 @@ const metrics = {
processRequests,
heapSizeAndUsed,
heapSpacesSizeAndUsed,
version
version,
gc
};
const metricsList = Object.keys(metrics);

Expand Down
78 changes: 78 additions & 0 deletions lib/metrics/gc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use strict';
const Counter = require('../counter');
const Summary = require('../summary');

let perf_hooks;

try {
// eslint-disable-next-line
perf_hooks = require('perf_hooks');
} catch (e) {
// node version is too old
}

const NODEJS_GC_RUNS = 'nodejs_gc_runs';
const NODEJS_GC_DURATION_SUMMARY = 'nodejs_gc_duration_summary';

function gcKindToString(gcKind) {
let gcKindName = '';
switch (gcKind) {
case perf_hooks.constants.NODE_PERFORMANCE_GC_MAJOR:
gcKindName = 'major';
break;
case perf_hooks.constants.NODE_PERFORMANCE_GC_MINOR:
gcKindName = 'minor';
break;
case perf_hooks.constants.NODE_PERFORMANCE_GC_INCREMENTAL:
gcKindName = 'incremental';
break;
case perf_hooks.constants.NODE_PERFORMANCE_GC_WEAKCB:
gcKindName = 'weakcb';
break;
default:
gcKindName = 'unknown';
break;
}
return gcKindName;
}

module.exports = (registry, config = {}) => {
if (!perf_hooks) {
return () => {};
}

const namePrefix = config.prefix ? config.prefix : '';
const gcCount = new Counter({
name: namePrefix + NODEJS_GC_RUNS,
help:
'Count of garbage collections. gc_type label is one of major, minor, incremental or weakcb.',
labelNames: ['gc_type'],
registers: registry ? [registry] : undefined
});
const gcSummary = new Summary({
name: namePrefix + NODEJS_GC_DURATION_SUMMARY,
help:
'Summary of garbage collections. gc_type label is one of major, minor, incremental or weakcb.',
labelNames: ['gc_type'],
maxAgeSeconds: 600,
ageBuckets: 5,
percentiles: [0.5, 0.75, 0.9, 0.99],
registers: registry ? [registry] : undefined
});

const obs = new perf_hooks.PerformanceObserver(list => {
const entry = list.getEntries()[0];
const labels = { gc_type: gcKindToString(entry.kind) };

gcCount.inc(labels, 1);
// Convert duration from milliseconds to seconds
gcSummary.observe(labels, entry.duration / 1000);
});

// We do not expect too many gc events per second, so we do not use buffering
obs.observe({ entryTypes: ['gc'], buffered: false });

return () => {};
};

module.exports.metricNames = [NODEJS_GC_RUNS, NODEJS_GC_DURATION_SUMMARY];
49 changes: 49 additions & 0 deletions test/metrics/gcTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';

describe('gc', () => {
const register = require('../../index').register;
const processHandles = require('../../lib/metrics/gc');

beforeAll(() => {
register.clear();
});

afterEach(() => {
register.clear();
});

it('should add metric to the registry', () => {
expect(register.getMetricsAsJSON()).toHaveLength(0);

processHandles()();

const metrics = register.getMetricsAsJSON();

// Check if perf_hooks module is available
let perf_hooks;
try {
// eslint-disable-next-line
perf_hooks = require('perf_hooks');
} catch (e) {
// node version is too old
}

if (perf_hooks) {
expect(metrics).toHaveLength(2);

expect(metrics[0].help).toEqual(
'Count of garbage collections. gc_type label is one of major, minor, incremental or weakcb.'
);
expect(metrics[0].type).toEqual('counter');
expect(metrics[0].name).toEqual('nodejs_gc_runs');

expect(metrics[1].help).toEqual(
'Summary of garbage collections. gc_type label is one of major, minor, incremental or weakcb.'
);
expect(metrics[1].type).toEqual('summary');
expect(metrics[1].name).toEqual('nodejs_gc_duration_summary');
} else {
expect(metrics).toHaveLength(0);
}
});
});

0 comments on commit 99f65d3

Please sign in to comment.