diff --git a/CHANGELOG.md b/CHANGELOG.md index 057e81fb..8de5dab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/example/default-metrics.js b/example/default-metrics.js index 7d2b9494..a3168a7b 100644 --- a/example/default-metrics.js +++ b/example/default-metrics.js @@ -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 diff --git a/lib/metrics/heapSizeAndUsed.js b/lib/metrics/heapSizeAndUsed.js index ae8a145d..88ea5cf7 100644 --- a/lib/metrics/heapSizeAndUsed.js +++ b/lib/metrics/heapSizeAndUsed.js @@ -1,21 +1,42 @@ 'use strict'; const Gauge = require('../gauge'); +const v8 = require('v8'); 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) { @@ -25,6 +46,13 @@ module.exports = (registry, config = {}) => { externalMemUsed.set(labels, memUsage.external); } } + if (heapSizeLimit) { + try { + heapSizeLimit.set(labels, v8.getHeapStatistics().heap_size_limit); + } catch { + // noop + } + } }; const heapSizeTotal = new Gauge({ @@ -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, ]; diff --git a/test/metrics/heapSizeAndUsedTest.js b/test/metrics/heapSizeAndUsedTest.js index 3c917ed6..30cbb4de 100644 --- a/test/metrics/heapSizeAndUsedTest.js +++ b/test/metrics/heapSizeAndUsedTest.js @@ -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(); + }); }); });