From ea7c08dc9869d25e4329128d783ed8032dc053cb Mon Sep 17 00:00:00 2001 From: Yosi Attias Date: Mon, 25 Sep 2023 14:25:30 +0300 Subject: [PATCH 1/4] perf: hashObject: use pre-calculated `sortedLabelNames` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since label names are predefined ahead-of-time and constant during runtime, we can sort them before-hand and use them when running `hashObject. node v18.18.2 ``` http fetch GET 200 https://registry.npmjs.org/prom-client 83ms http fetch GET 200 https://registry.npmjs.org/prom-client/-/prom-client-15.0.0.tgz 42ms + prom-client@15.0.0 added 4 packages from 4 contributors and audited 4 packages in 0.382s found 0 vulnerabilities - histogram ➭ observe#1 with 64 ➭ current x 104,381 ops/sec ±0.42% (99 runs sampled) - histogram ➭ observe#1 with 64 ➭ prom-client@latest x 103,709 ops/sec ±0.72% (96 runs sampled) - histogram ➭ observe#2 with 8 ➭ current x 68,178 ops/sec ±0.20% (98 runs sampled) - histogram ➭ observe#2 with 8 ➭ prom-client@latest x 53,510 ops/sec ±0.18% (97 runs sampled) - histogram ➭ observe#2 with 4 and 2 with 2 ➭ current x 34,169 ops/sec ±0.22% (97 runs sampled) - histogram ➭ observe#2 with 4 and 2 with 2 ➭ prom-client@latest x 28,170 ops/sec ±0.32% (97 runs sampled) - histogram ➭ observe#2 with 2 and 2 with 4 ➭ current x 36,057 ops/sec ±0.36% (97 runs sampled) - histogram ➭ observe#2 with 2 and 2 with 4 ➭ prom-client@latest x 28,804 ops/sec ±0.20% (98 runs sampled) - histogram ➭ observe#6 with 2 ➭ current x 23,360 ops/sec ±0.14% (97 runs sampled) - histogram ➭ observe#6 with 2 ➭ prom-client@latest x 19,420 ops/sec ±0.36% (96 runs sampled) - counter ➭ inc ➭ current x 59,643,643 ops/sec ±0.13% (101 runs sampled) - counter ➭ inc ➭ prom-client@latest x 37,023,723 ops/sec ±0.46% (95 runs sampled) - counter ➭ inc with labels ➭ current x 84,762 ops/sec ±0.25% (97 runs sampled) - counter ➭ inc with labels ➭ prom-client@latest x 67,225 ops/sec ±0.62% (95 runs sampled) - gauge ➭ inc ➭ current x 61,652,788 ops/sec ±0.41% (96 runs sampled) - gauge ➭ inc ➭ prom-client@latest x 31,247,588 ops/sec ±0.28% (98 runs sampled) - gauge ➭ inc with labels ➭ current x 86,552 ops/sec ±0.54% (97 runs sampled) - gauge ➭ inc with labels ➭ prom-client@latest x 64,590 ops/sec ±0.47% (95 runs sampled) - summary ➭ observe#1 with 64 ➭ current x 87,909 ops/sec ±0.18% (98 runs sampled) - summary ➭ observe#1 with 64 ➭ prom-client@latest x 94,806 ops/sec ±0.87% (89 runs sampled) - summary ➭ observe#2 with 8 ➭ current x 61,909 ops/sec ±0.15% (99 runs sampled) - summary ➭ observe#2 with 8 ➭ prom-client@latest x 49,337 ops/sec ±0.35% (100 runs sampled) - summary ➭ observe#2 with 4 and 2 with 2 ➭ current x 32,320 ops/sec ±0.20% (100 runs sampled) - summary ➭ observe#2 with 4 and 2 with 2 ➭ prom-client@latest x 27,597 ops/sec ±0.79% (95 runs sampled) - summary ➭ observe#2 with 2 and 2 with 4 ➭ current x 32,187 ops/sec ±0.21% (100 runs sampled) - summary ➭ observe#2 with 2 and 2 with 4 ➭ prom-client@latest x 27,513 ops/sec ±0.37% (93 runs sampled) - summary ➭ observe#6 with 2 ➭ current x 22,375 ops/sec ±0.47% (94 runs sampled) - summary ➭ observe#6 with 2 ➭ prom-client@latest x 19,161 ops/sec ±0.14% (100 runs sampled) ┌───────────────────────────────┬────────────────────┬────────────────────┐ │ histogram │ current │ prom-client@latest │ ├───────────────────────────────┼────────────────────┼────────────────────┤ │ observe#1 with 64 │ 104381.0327549696 │ 103709.41386205897 │ ├───────────────────────────────┼────────────────────┼────────────────────┤ │ observe#2 with 8 │ 68178.1543735591 │ 53509.94862568404 │ ├───────────────────────────────┼────────────────────┼────────────────────┤ │ observe#2 with 4 and 2 with 2 │ 34169.376174278514 │ 28170.10920967872 │ ├───────────────────────────────┼────────────────────┼────────────────────┤ │ observe#2 with 2 and 2 with 4 │ 36056.509189653516 │ 28804.04102513799 │ ├───────────────────────────────┼────────────────────┼────────────────────┤ │ observe#6 with 2 │ 23359.579524196415 │ 19419.640968121184 │ └───────────────────────────────┴────────────────────┴────────────────────┘ ┌─────────────────┬───────────────────┬────────────────────┐ │ counter │ current │ prom-client@latest │ ├─────────────────┼───────────────────┼────────────────────┤ │ inc │ 59643642.79575977 │ 37023723.28415886 │ ├─────────────────┼───────────────────┼────────────────────┤ │ inc with labels │ 84761.87024308582 │ 67224.57758629428 │ └─────────────────┴───────────────────┴────────────────────┘ ┌─────────────────┬───────────────────┬────────────────────┐ │ gauge │ current │ prom-client@latest │ ├─────────────────┼───────────────────┼────────────────────┤ │ inc │ 61652788.35302279 │ 31247587.722701166 │ ├─────────────────┼───────────────────┼────────────────────┤ │ inc with labels │ 86551.57212390135 │ 64589.60038388497 │ └─────────────────┴───────────────────┴────────────────────┘ ┌───────────────────────────────┬────────────────────┬────────────────────┐ │ summary │ current │ prom-client@latest │ ├───────────────────────────────┼────────────────────┼────────────────────┤ │ observe#1 with 64 │ 87908.71972414361 │ 94806.18075509806 │ ├───────────────────────────────┼────────────────────┼────────────────────┤ │ observe#2 with 8 │ 61909.18982275139 │ 49336.623224028765 │ ├───────────────────────────────┼────────────────────┼────────────────────┤ │ observe#2 with 4 and 2 with 2 │ 32319.8211562837 │ 27596.72566017053 │ ├───────────────────────────────┼────────────────────┼────────────────────┤ │ observe#2 with 2 and 2 with 4 │ 32187.468085238048 │ 27513.002743185254 │ ├───────────────────────────────┼────────────────────┼────────────────────┤ │ observe#6 with 2 │ 22375.38970678346 │ 19160.867264237862 │ └───────────────────────────────┴────────────────────┴────────────────────┘ ✓ histogram ➭ observe#1 with 64 is 0.6476% faster. ✓ histogram ➭ observe#2 with 8 is 27.41% faster. ✓ histogram ➭ observe#2 with 4 and 2 with 2 is 21.30% faster. ✓ histogram ➭ observe#2 with 2 and 2 with 4 is 25.18% faster. ✓ histogram ➭ observe#6 with 2 is 20.29% faster. ✓ counter ➭ inc is 61.10% faster. ✓ counter ➭ inc with labels is 26.09% faster. ✓ gauge ➭ inc is 97.30% faster. ✓ gauge ➭ inc with labels is 34.00% faster. ⚠ summary ➭ observe#1 with 64 is 7.846% acceptably slower. ✓ summary ➭ observe#2 with 8 is 25.48% faster. ✓ summary ➭ observe#2 with 4 and 2 with 2 is 17.11% faster. ✓ summary ➭ observe#2 with 2 and 2 with 4 is 16.99% faster. ✓ summary ➭ observe#6 with 2 is 16.78% faster. ``` --- lib/counter.js | 4 ++-- lib/gauge.js | 6 +++--- lib/histogram.js | 8 ++++---- lib/metric.js | 7 +++++++ lib/summary.js | 4 ++-- lib/util.js | 48 ++++++++++++++++++++++++++++++++---------------- 6 files changed, 50 insertions(+), 27 deletions(-) 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..28631e0b 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) { +function fastHashObject(keys, labels) { + if (keys.length === 0) { + return ''; + } + + let hash = ''; + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = labels[key]; + if (typeof value === 'undefined') continue; + + hash += `${key}:${value},`; + } + + 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. - let keys = Object.keys(labels); - if (keys.length === 0) { - return ''; + + if (labelNames) { + return fastHashObject(labelNames, labels); } - // else + + let keys = Object.keys(labels); 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]]},`; - } - hash += `${keys[i]}:${labels[keys[i]]}`; - return hash; + 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() { From b919fd3d03a62709a626f335ad91aa53189e5166 Mon Sep 17 00:00:00 2001 From: Yosi Attias Date: Thu, 26 Oct 2023 20:31:34 +0300 Subject: [PATCH 2/4] Add CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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 From 3776b1d8315e08591d3e74737ae6f61059e19559 Mon Sep 17 00:00:00 2001 From: Yosi Attias Date: Fri, 27 Oct 2023 09:13:56 +0300 Subject: [PATCH 3/4] hashObject: remove useless `keys` assignment --- lib/util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/util.js b/lib/util.js index 28631e0b..e6ba5bda 100644 --- a/lib/util.js +++ b/lib/util.js @@ -90,9 +90,9 @@ function hashObject(labels, labelNames) { return fastHashObject(labelNames, labels); } - let keys = Object.keys(labels); + const keys = Object.keys(labels); if (keys.length > 1) { - keys = keys.sort(); // need consistency across calls + keys.sort(); // need consistency across calls } return fastHashObject(keys, labels); From aa7ec2353df9b34438a674074df5c029106ab283 Mon Sep 17 00:00:00 2001 From: Yosi Attias Date: Fri, 27 Oct 2023 16:30:27 +0300 Subject: [PATCH 4/4] fastHashObject: change undefined check --- lib/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.js b/lib/util.js index e6ba5bda..8054c1bb 100644 --- a/lib/util.js +++ b/lib/util.js @@ -73,7 +73,7 @@ function fastHashObject(keys, labels) { for (let i = 0; i < keys.length; i++) { const key = keys[i]; const value = labels[key]; - if (typeof value === 'undefined') continue; + if (value === undefined) continue; hash += `${key}:${value},`; }