Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ project adheres to [Semantic Versioning](http://semver.org/).

### Added

- Default metric `nodejs_heap_size_limit_bytes` (V8 heap size limit from `getHeapStatistics()`).
- Expanded benchmarking code
- new WorkerRegistry to provide equivalent support to AggregatorRegistry

Expand Down
4 changes: 4 additions & 0 deletions example/default-metrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ nodejs_heap_size_total_bytes 8425472
# TYPE nodejs_heap_size_used_bytes gauge
nodejs_heap_size_used_bytes 6379336
# HELP nodejs_heap_size_limit_bytes V8 maximum JavaScript heap size limit in bytes.
# TYPE nodejs_heap_size_limit_bytes gauge
nodejs_heap_size_limit_bytes 2147483648
# HELP nodejs_external_memory_bytes Node.js external memory size in bytes.
# TYPE nodejs_external_memory_bytes gauge
nodejs_external_memory_bytes 746074
Expand Down
38 changes: 38 additions & 0 deletions lib/metrics/heapSizeAndUsed.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
'use strict';

const Gauge = require('../gauge');
const v8 = require('v8');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't going to work with Bun. Probably better to move this into isHeapStatisticsSupported()

const safeMemoryUsage = require('./helpers/safeMemoryUsage');

const NODEJS_HEAP_SIZE_TOTAL = 'nodejs_heap_size_total_bytes';
const NODEJS_HEAP_SIZE_USED = 'nodejs_heap_size_used_bytes';
const NODEJS_HEAP_SIZE_LIMIT = 'nodejs_heap_size_limit_bytes';
const NODEJS_EXTERNAL_MEMORY = 'nodejs_external_memory_bytes';

function isHeapStatisticsSupported() {
if (typeof v8.getHeapStatistics !== 'function') {
return false;
}
try {
v8.getHeapStatistics();
return true;
} catch (e) {
if (e.code === 'ERR_NOT_IMPLEMENTED') {
return false;
}
throw e;
}
}

const heapStatisticsSupported = isHeapStatisticsSupported();

module.exports = (registry, config = {}) => {
if (typeof process.memoryUsage !== 'function') {
return;
}

const labels = config.labels ? config.labels : {};
const labelNames = Object.keys(labels);

const registers = registry ? [registry] : undefined;
const namePrefix = config.prefix ? config.prefix : '';
let heapSizeLimit;
const collect = () => {
const memUsage = safeMemoryUsage();
if (memUsage) {
Expand All @@ -25,6 +46,13 @@ module.exports = (registry, config = {}) => {
externalMemUsed.set(labels, memUsage.external);
}
}
if (heapSizeLimit) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not running the check here?

try {
heapSizeLimit.set(labels, v8.getHeapStatistics().heap_size_limit);
} catch {
// noop
}
}
};

const heapSizeTotal = new Gauge({
Expand All @@ -47,10 +75,20 @@ module.exports = (registry, config = {}) => {
registers,
labelNames,
});

if (heapStatisticsSupported) {
heapSizeLimit = new Gauge({
name: namePrefix + NODEJS_HEAP_SIZE_LIMIT,
help: 'V8 maximum JavaScript heap size limit in bytes.',
registers,
labelNames,
});
}
};

module.exports.metricNames = [
NODEJS_HEAP_SIZE_TOTAL,
NODEJS_HEAP_SIZE_USED,
...(heapStatisticsSupported ? [NODEJS_HEAP_SIZE_LIMIT] : []),
NODEJS_EXTERNAL_MEMORY,
];
145 changes: 115 additions & 30 deletions test/metrics/heapSizeAndUsedTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,126 @@ describe.each([
['Prometheus', Registry.PROMETHEUS_CONTENT_TYPE],
['OpenMetrics', Registry.OPENMETRICS_CONTENT_TYPE],
])('heapSizeAndUsed with %s registry', (tag, regType) => {
const heapSizeAndUsed = require('../../lib/metrics/heapSizeAndUsed');
const globalRegistry = require('../../lib/registry').globalRegistry;
const memoryUsedFn = process.memoryUsage;

beforeEach(() => {
globalRegistry.setContentType(regType);
});

afterEach(() => {
process.memoryUsage = memoryUsedFn;
globalRegistry.clear();
});

it('should set gauge values from memoryUsage', async () => {
process.memoryUsage = function () {
return { heapTotal: 1000, heapUsed: 500, external: 100 };
};

heapSizeAndUsed();
// Note: these three gauges' values are set by the _total gauge's
// "collect" function.

const totalGauge = globalRegistry.getSingleMetric(
'nodejs_heap_size_total_bytes',
);
expect((await totalGauge.get()).values[0].value).toEqual(1000);

const usedGauge = globalRegistry.getSingleMetric(
'nodejs_heap_size_used_bytes',
);
expect((await usedGauge.get()).values[0].value).toEqual(500);

const externalGauge = globalRegistry.getSingleMetric(
'nodejs_external_memory_bytes',
);
expect((await externalGauge.get()).values[0].value).toEqual(100);
it('should set gauge values from memoryUsage and heap statistics', async () => {
await jest.isolateModulesAsync(async () => {
jest.doMock('v8', () => {
return {
getHeapStatistics() {
return { heap_size_limit: 2147483648 };
},
};
});

const heapSizeAndUsed = require('../../lib/metrics/heapSizeAndUsed');
const globalRegistry = require('../../lib/registry').globalRegistry;

globalRegistry.setContentType(regType);
process.memoryUsage = function () {
return { heapTotal: 1000, heapUsed: 500, external: 100 };
};

try {
heapSizeAndUsed();
// Note: these gauges' values are set by the _total gauge's
// "collect" function.

const totalGauge = globalRegistry.getSingleMetric(
'nodejs_heap_size_total_bytes',
);
expect((await totalGauge.get()).values[0].value).toEqual(1000);

const usedGauge = globalRegistry.getSingleMetric(
'nodejs_heap_size_used_bytes',
);
expect((await usedGauge.get()).values[0].value).toEqual(500);

const externalGauge = globalRegistry.getSingleMetric(
'nodejs_external_memory_bytes',
);
expect((await externalGauge.get()).values[0].value).toEqual(100);

const limitGauge = globalRegistry.getSingleMetric(
'nodejs_heap_size_limit_bytes',
);
expect((await limitGauge.get()).values[0].value).toEqual(2147483648);
} finally {
globalRegistry.clear();
}
});
});
});

describe('heapSizeAndUsed isolated v8 behaviour', () => {
it('should read heap_size_limit from getHeapStatistics on each collect', async () => {
await jest.isolateModulesAsync(async () => {
let n = 0;
jest.doMock('v8', () => {
return {
getHeapStatistics() {
n++;
return { heap_size_limit: n * 1000 };
},
};
});

const { Registry } = require('../../index');
const heapSizeAndUsed = require('../../lib/metrics/heapSizeAndUsed');
const reg = new Registry();
const savedMem = process.memoryUsage;
process.memoryUsage = () => {
return {
heapTotal: 1,
heapUsed: 1,
external: 0,
};
};
try {
heapSizeAndUsed(reg);
const totalGauge = reg.getSingleMetric('nodejs_heap_size_total_bytes');
await totalGauge.get();
const limitGauge = reg.getSingleMetric('nodejs_heap_size_limit_bytes');
const data = await limitGauge.get();
// 1st call: isHeapStatisticsSupported(); 2nd: collect()
expect(n).toBe(2);
expect(data.values[0].value).toBe(2000);
} finally {
process.memoryUsage = savedMem;
}
});
});

it('should omit limit metric when getHeapStatistics is unsupported', async () => {
await jest.isolateModulesAsync(async () => {
jest.doMock('v8', () => {
return {
getHeapStatistics() {
const err = new Error('not implemented');
err.code = 'ERR_NOT_IMPLEMENTED';
throw err;
},
};
});

const { Registry } = require('../../index');
const heapSizeAndUsed = require('../../lib/metrics/heapSizeAndUsed');

expect(heapSizeAndUsed.metricNames).toEqual([
'nodejs_heap_size_total_bytes',
'nodejs_heap_size_used_bytes',
'nodejs_external_memory_bytes',
]);

const reg = new Registry();
heapSizeAndUsed(reg);
expect(
reg.getSingleMetric('nodejs_heap_size_limit_bytes'),
).toBeUndefined();
});
});
});