diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cce5de9..127771a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed - remove unnecessary loop from `osMemoryHeapLinux` +- Improve performance of `hashObject` by using pre-sorted array of label names ### Added diff --git a/lib/counter.js b/lib/counter.js index 4204bf5b..22a440ec 100644 --- a/lib/counter.js +++ b/lib/counter.js @@ -40,7 +40,7 @@ class Counter extends Metric { incWithoutExemplar(labels, value) { let hash = ''; if (isObject(labels)) { - hash = hashObject(labels); + hash = hashObject(labels, this.sortedLabelNames); validateLabel(this.labelNames, labels); } else { value = labels; @@ -130,7 +130,7 @@ class Counter extends Metric { remove(...args) { const labels = getLabels(this.labelNames, args) || {}; validateLabel(this.labelNames, labels); - return removeLabels.call(this, this.hashMap, labels); + return removeLabels.call(this, this.hashMap, labels, this.sortedLabelNames); } } diff --git a/lib/gauge.js b/lib/gauge.js index 77327b6b..3725bf01 100644 --- a/lib/gauge.js +++ b/lib/gauge.js @@ -120,7 +120,7 @@ class Gauge extends Metric { } _getValue(labels) { - const hash = hashObject(labels || {}); + const hash = hashObject(labels || {}, this.sortedLabelNames); return this.hashMap[hash] ? this.hashMap[hash].value : 0; } @@ -139,7 +139,7 @@ class Gauge extends Metric { remove(...args) { const labels = getLabels(this.labelNames, args); validateLabel(this.labelNames, labels); - removeLabels.call(this, this.hashMap, labels); + removeLabels.call(this, this.hashMap, labels, this.sortedLabelNames); } } @@ -158,7 +158,7 @@ function setDelta(gauge, labels, delta) { } validateLabel(gauge.labelNames, labels); - const hash = hashObject(labels); + const hash = hashObject(labels, gauge.sortedLabelNames); setValueDelta(gauge.hashMap, delta, labels, hash); } diff --git a/lib/histogram.js b/lib/histogram.js index e603da03..18c99268 100644 --- a/lib/histogram.js +++ b/lib/histogram.js @@ -83,7 +83,7 @@ class Histogram extends Metric { } updateExemplar(labels, value, exemplarLabels) { - const hash = hashObject(labels); + const hash = hashObject(labels, this.sortedLabelNames); const bound = findBound(this.upperBounds, value); const { bucketExemplars } = this.hashMap[hash]; let exemplar = bucketExemplars[bound]; @@ -132,7 +132,7 @@ class Histogram extends Metric { * @returns {void} */ zero(labels) { - const hash = hashObject(labels); + const hash = hashObject(labels, this.sortedLabelNames); this.hashMap[hash] = createBaseValues( labels, this.bucketValues, @@ -167,7 +167,7 @@ class Histogram extends Metric { remove(...args) { const labels = getLabels(this.labelNames, args); validateLabel(this.labelNames, labels); - removeLabels.call(this, this.hashMap, labels); + removeLabels.call(this, this.hashMap, labels, this.sortedLabelNames); } } @@ -214,7 +214,7 @@ function observe(labels) { ); } - const hash = hashObject(labelValuePair.labels); + const hash = hashObject(labelValuePair.labels, this.sortedLabelNames); let valueFromMap = this.hashMap[hash]; if (!valueFromMap) { valueFromMap = createBaseValues( diff --git a/lib/metric.js b/lib/metric.js index 2e0f4191..a2bdf304 100644 --- a/lib/metric.js +++ b/lib/metric.js @@ -39,10 +39,17 @@ class Metric { if (!validateLabelName(this.labelNames)) { throw new Error('Invalid label name'); } + if (this.collect && typeof this.collect !== 'function') { throw new Error('Optional "collect" parameter must be a function'); } + if (this.labelNames) { + this.sortedLabelNames = [...this.labelNames].sort(); + } else { + this.sortedLabelNames = []; + } + this.reset(); for (const register of this.registers) { diff --git a/lib/summary.js b/lib/summary.js index a60f24df..28fdb07a 100644 --- a/lib/summary.js +++ b/lib/summary.js @@ -115,7 +115,7 @@ class Summary extends Metric { remove(...args) { const labels = getLabels(this.labelNames, args); validateLabel(this.labelNames, labels); - removeLabels.call(this, this.hashMap, labels); + removeLabels.call(this, this.hashMap, labels, this.sortedLabelNames); } } @@ -170,7 +170,7 @@ function observe(labels) { ); } - const hash = hashObject(labelValuePair.labels); + const hash = hashObject(labelValuePair.labels, this.sortedLabelNames); let summaryOfLabel = this.hashMap[hash]; if (!summaryOfLabel) { summaryOfLabel = { diff --git a/lib/util.js b/lib/util.js index c9e3cb3b..8054c1bb 100644 --- a/lib/util.js +++ b/lib/util.js @@ -14,8 +14,12 @@ exports.getValueAsString = function getValueString(value) { } }; -exports.removeLabels = function removeLabels(hashMap, labels) { - const hash = hashObject(labels); +exports.removeLabels = function removeLabels( + hashMap, + labels, + sortedLabelNames, +) { + const hash = hashObject(labels, sortedLabelNames); delete hashMap[hash]; }; @@ -59,32 +63,44 @@ exports.getLabels = function (labelNames, args) { return acc; }; -function hashObject(labels) { - // We don't actually need a hash here. We just need a string that - // is unique for each possible labels object and consistent across - // calls with equivalent labels objects. - let keys = Object.keys(labels); +function fastHashObject(keys, labels) { if (keys.length === 0) { return ''; } - // else - if (keys.length > 1) { - keys = keys.sort(); // need consistency across calls - } let hash = ''; - let i = 0; - const size = keys.length; - for (; i < size - 1; i++) { - hash += `${keys[i]}:${labels[keys[i]]},`; + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = labels[key]; + if (value === undefined) continue; + + hash += `${key}:${value},`; } - hash += `${keys[i]}:${labels[keys[i]]}`; + return hash; } + +function hashObject(labels, labelNames) { + // We don't actually need a hash here. We just need a string that + // is unique for each possible labels object and consistent across + // calls with equivalent labels objects. + + if (labelNames) { + return fastHashObject(labelNames, labels); + } + + const keys = Object.keys(labels); + if (keys.length > 1) { + keys.sort(); // need consistency across calls + } + + return fastHashObject(keys, labels); +} exports.hashObject = hashObject; exports.isObject = function isObject(obj) { - return obj === Object(obj); + return obj !== null && typeof obj === 'object'; }; exports.nowTimestamp = function nowTimestamp() {