diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb6d232..d6312502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ project adheres to [Semantic Versioning](http://semver.org/). - AggregatorRegistry renamed to ClusterRegistry, old name deprecated - chore: update faceoff to 1.1 - perf: Stat aggregation uses similar strategy to collection. 60% faster aggregation +- perf: Improve the memory usage of histograms by delaying allocation of bucket data +- fix: Avoid updating exemplar values during subsequent metric changes (Fixes [#616](https://github.com/prometheus/client_js/issues/616)) ### Added diff --git a/lib/histogram.js b/lib/histogram.js index 6655ad54..35da9f0d 100644 --- a/lib/histogram.js +++ b/lib/histogram.js @@ -23,7 +23,7 @@ class Histogram extends Metric { this.type = 'histogram'; this.defaultLabels = {}; this.defaultExemplarLabelSet = {}; - this.enableExemplars = false; + this.enableExemplars = config.enableExemplars; for (const label of this.labelNames) { if (label === 'le') { @@ -37,26 +37,17 @@ class Histogram extends Metric { return acc; }, {}); - if (config.enableExemplars) { - this.enableExemplars = true; - this.bucketExemplars = this.upperBounds.reduce((acc, upperBound) => { - acc[upperBound] = null; - return acc; - }, {}); - Object.freeze(this.bucketExemplars); - this.observe = this.observeWithExemplar; - } else { - this.observe = this.observeWithoutExemplar; - } + this.observe = this.enableExemplars + ? this.observeWithExemplar + : this.observeWithoutExemplar; - Object.freeze(this.bucketValues); Object.freeze(this.upperBounds); if (this.labelNames.length === 0) { this.store = new LabelMap(this.labelNames); this.store.merge( {}, - createBaseValues({}, this.bucketValues, this.bucketExemplars), + createBaseValues({}, this.bucketValues, this.enableExemplars), ); } } @@ -76,14 +67,13 @@ class Histogram extends Metric { value, exemplarLabels = this.defaultExemplarLabelSet, } = {}) { - observe(this, labels === 0 ? 0 : labels || {}, value); - this.updateExemplar(labels, value, exemplarLabels); + const valueFromMap = observe(this, labels === 0 ? 0 : labels || {}, value); + this.updateExemplar(valueFromMap, value, exemplarLabels); } - updateExemplar(labels, value, exemplarLabels) { + updateExemplar({ bucketExemplars }, value, exemplarLabels) { if (isEmpty(exemplarLabels)) return; const bound = findBound(this.upperBounds, value); - const { bucketExemplars } = this.store.entry(labels); let exemplar = bucketExemplars[bound]; if (!isObject(exemplar)) { exemplar = new Exemplar(); @@ -133,7 +123,7 @@ class Histogram extends Metric { this.store.validate(labels); this.store.merge( labels, - createBaseValues(labels, this.bucketValues, this.bucketExemplars), + createBaseValues(labels, this.bucketValues, this.enableExemplars), ); } @@ -159,7 +149,9 @@ class Histogram extends Metric { const labels = getLabels(this.labelNames, args); this.store.validate(labels); return { - observe: value => observe(this, labels, value), + observe: value => { + observe(this, labels, value); + }, startTimer: () => startTimer(this, labels), }; } @@ -219,6 +211,7 @@ function findBound(upperBounds, value) { * @param {Histogram} histogram * @param {object} labels - Object with labels where key is the label key and value is label value. Can only be one level deep * @param {number} value - Value to observe in the histogram + * @returns {object} bucket data */ function observe(histogram, labels, value) { const labelValuePair = convertLabelsAndValues(labels, value); @@ -237,7 +230,7 @@ function observe(histogram, labels, value) { createBaseValues( labelValuePair.labels, histogram.bucketValues, - histogram.bucketExemplars, + histogram.enableExemplars, ), ); } @@ -247,20 +240,22 @@ function observe(histogram, labels, value) { entry.sum += labelValuePair.value; entry.count += 1; - if (Object.hasOwn(entry.bucketValues, b)) { + if (typeof entry.bucketValues[b] === 'number') { entry.bucketValues[b] += 1; } + + return entry; } -function createBaseValues(labels, bucketValues, bucketExemplars) { +function createBaseValues(labels, bucketValues, enableExemplars) { const result = { labels, - bucketValues: { ...bucketValues }, + bucketValues: Object.create(bucketValues), sum: 0, count: 0, }; - if (bucketExemplars) { - result.bucketExemplars = { ...bucketExemplars }; + if (enableExemplars) { + result.bucketExemplars = {}; } return result; } @@ -281,16 +276,15 @@ function extractBucketValuesForExport(histogram) { const name = `${histogram.name}_bucket`; return bucketData => { let acc = 0; + const { labels, bucketValues, bucketExemplars } = bucketData; const buckets = histogram.upperBounds.map(upperBound => { - acc += bucketData.bucketValues[upperBound]; + acc += bucketValues[upperBound]; return setValuePair( { le: upperBound }, acc, name, - bucketData.bucketExemplars - ? bucketData.bucketExemplars[upperBound] - : null, - bucketData.labels, + bucketExemplars?.[upperBound] ?? null, + labels, ); }); return { buckets, data: bucketData };