Skip to content

Commit

Permalink
Improve performance of hashObject (#594)
Browse files Browse the repository at this point in the history
* perf: hashObject: use pre-calculated `sortedLabelNames`

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.
```

* Add CHANGELOG entry

* hashObject: remove useless `keys` assignment

* fastHashObject: change undefined check
  • Loading branch information
yosiat committed Nov 25, 2023
1 parent 798b9c8 commit efdb283
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 28 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions lib/counter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down
6 changes: 3 additions & 3 deletions lib/gauge.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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);
}
}

Expand All @@ -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);
}

Expand Down
8 changes: 4 additions & 4 deletions lib/histogram.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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(
Expand Down
7 changes: 7 additions & 0 deletions lib/metric.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions lib/summary.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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 = {
Expand Down
50 changes: 33 additions & 17 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
};

Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit efdb283

Please sign in to comment.