Skip to content

Commit 571f8e1

Browse files
committed
Use a similar strategy to LabelMap for stats coalescing for cluster.js
This has to wrestle with the problem of lack of fore-knowledge of the canonical set of labels. We achieve this by assuming 6 labels to begin with, first filling in the empties as we discover new ones, and tnen expanding the columns by ⌈25%⌉ if they don't, and re-keying the entire table (by concatenating extra ||'s onto the end).. Before and after: ✓ cluster ➭ aggregate() is 6.088% faster. ✓ cluster ➭ aggregate() is 149.4% faster.
1 parent a411425 commit 571f8e1

5 files changed

Lines changed: 366 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ project adheres to [Semantic Versioning](http://semver.org/).
2424
- perf: Remove truthy conditionals in hot code paths
2525
- Show the invalid name in the validation errors
2626
- perf: Improve performance of registry defaultLabels during metric processing
27-
- perf: New, more space-efficient storage engine, 20-45% faster stats recording
27+
- perf: New, more space-efficient storage engine, 20-45% faster stats recording, 148% faster aggregation
2828

2929
### Added
3030

benchmarks/util.js

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ function setupUtilSuite(suite) {
1414
foo: 'longish',
1515
user_agent: 'Chrome',
1616
gateway: 'lb04',
17+
method: 'get',
18+
status_code: 200,
19+
phase: 'load',
1720
});
1821
},
1922
{ setup: findUtil },
@@ -27,9 +30,13 @@ function setupUtilSuite(suite) {
2730
}
2831

2932
labelMap.validate({
30-
foo: 'longish:tag:goes:here',
33+
foo: 'longish',
3134
user_agent: 'Chrome',
32-
status_code: 503,
35+
gateway: 'lb04',
36+
method: 'get',
37+
status_code: 200,
38+
phase: 'load',
39+
label1: 4,
3340
});
3441
},
3542
{ setup },
@@ -45,11 +52,41 @@ function setupUtilSuite(suite) {
4552
labelMap.keyFrom({
4653
foo: 'longish',
4754
user_agent: 'Chrome',
48-
status_code: 503,
55+
gateway: 'lb04',
56+
method: 'get',
57+
status_code: 301,
58+
phase: 'load',
59+
label1: 4,
4960
});
5061
},
5162
{ setup },
5263
);
64+
65+
suite.add(
66+
'LabelGrouper.keyFrom()',
67+
(client, labelGrouper) => {
68+
if (labelGrouper === undefined) {
69+
return;
70+
}
71+
72+
labelGrouper.keyFrom({
73+
foo: 'longish',
74+
user_agent: 'Chrome',
75+
gateway: 'lb04',
76+
method: 'get',
77+
status_code: 503,
78+
phase: 'load',
79+
label1: 4,
80+
});
81+
},
82+
{
83+
setup: client => {
84+
const Util = findUtil(client);
85+
86+
return Util && new Util.LabelGrouper();
87+
},
88+
},
89+
);
5390
}
5491

5592
function setup(client) {
@@ -62,6 +99,8 @@ function setup(client) {
6299
'gateway',
63100
'method',
64101
'status_code',
102+
'phase',
103+
'label1',
65104
]);
66105
}
67106
}

lib/metricAggregators.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
const { Grouper, hashObject } = require('./util');
3+
const { LabelGrouper } = require('./util');
44

55
/**
66
* Returns a new function that applies the `aggregatorFn` to the values.
@@ -24,10 +24,10 @@ function AggregatorFactory(aggregatorFn) {
2424
const name = value.metricName ?? '';
2525
let group = byNames.get(name);
2626
if (group === undefined) {
27-
group = new Grouper();
27+
group = new LabelGrouper();
2828
byNames.set(name, group);
2929
}
30-
group.add(hashObject(value.labels), value);
30+
group.add(value);
3131
});
3232
});
3333
// Apply aggregator function to gathered metrics.

lib/util.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,3 +352,177 @@ class Grouper extends Map {
352352
}
353353

354354
exports.Grouper = Grouper;
355+
356+
const GROWTH_RATE = 1.25;
357+
358+
/**
359+
* For grouping metrics by labels for reporting purposes.
360+
*/
361+
class LabelGrouper {
362+
/** @type {string[]} */
363+
#labelNames = new Array(6).fill('');
364+
365+
/** @type {Map<string, Array<any>>} */
366+
#map = new Map();
367+
368+
/**
369+
* Current number of columns
370+
* @type {number}
371+
*/
372+
#count = 0;
373+
374+
/**
375+
* Create a grouper.
376+
*/
377+
constructor() {}
378+
379+
/**
380+
* Adds the `value` to the `key`'s array of values.
381+
* @param {StatsEntry} value Value to add to `key`'s array.
382+
* @returns {LabelGrouper} undefined.
383+
*/
384+
add(value) {
385+
const labels = value.labels;
386+
const key = this.keyFrom(labels);
387+
388+
const entry = this.#map.get(key);
389+
if (entry !== undefined) {
390+
entry.push(value);
391+
} else {
392+
this.#map.set(key, [value]);
393+
}
394+
395+
return this;
396+
}
397+
398+
/**
399+
* Look up an entry by labels.
400+
* Note: This can end up modifying the store if labels are missing.
401+
* @param {object} labels Key to retrieve.
402+
* @returns {Array<StatsEntry>} undefined.
403+
*/
404+
get(labels) {
405+
return this.#map.get(this.keyFrom(labels));
406+
}
407+
408+
/**
409+
* Return all of the entries in this collection.
410+
* @returns {Iterator<StatsEntry>}
411+
*/
412+
values() {
413+
return this.#map.values().filter(entry => entry.length > 0);
414+
}
415+
416+
/**
417+
* Loop over the entries.
418+
* @param fn {Function}
419+
*/
420+
forEach(fn) {
421+
return this.#map.forEach(fn);
422+
}
423+
424+
/**
425+
* Remove all values from the Grouper.
426+
* Leaves the entries, but zeroes out the value arrays.
427+
* @returns {LabelGrouper}
428+
*/
429+
clear() {
430+
for (const entry of this.#map.values()) {
431+
entry.length = 0;
432+
}
433+
434+
return this;
435+
}
436+
437+
/**
438+
* Create a key for the given labels.
439+
* Note: This can end up modifying the store if labels are missing.
440+
* @param labels
441+
* @returns {string}
442+
*/
443+
keyFrom(labels = {}) {
444+
const keys = Object.keys(labels);
445+
446+
if (keys.length === 0) {
447+
return '';
448+
}
449+
450+
const arr = new Array(this.#labelNames.length);
451+
452+
let count = 0;
453+
for (let i = 0; i < this.#count; i++) {
454+
const name = this.#labelNames[i];
455+
const value = labels[name];
456+
457+
if (value !== undefined) {
458+
arr[i] = value;
459+
count++;
460+
}
461+
}
462+
463+
if (count < keys.length) {
464+
let pos = this.#count;
465+
const missing = this.#expandLabels(labels);
466+
for (const name of missing) {
467+
arr[pos++] = labels[name];
468+
}
469+
}
470+
471+
return arr.join('|');
472+
}
473+
474+
/**
475+
* Size of the collection.
476+
* @returns {number}
477+
*/
478+
get size() {
479+
return this.#map.size;
480+
}
481+
482+
/**
483+
* Search the labels for missing values and expand the lookup table to handle them.
484+
* @param labels
485+
* @returns {*[]}
486+
*/
487+
#expandLabels(labels) {
488+
const missing = [];
489+
490+
for (const name of Object.keys(labels)) {
491+
if (this.#labelNames.indexOf(name) === -1) {
492+
missing.push(name);
493+
}
494+
}
495+
496+
const target = missing.length + this.#count;
497+
const current = this.#labelNames.length;
498+
let width = current;
499+
500+
if (target > width) {
501+
while (target > width) {
502+
width = Math.ceil(width * GROWTH_RATE);
503+
}
504+
505+
this.#labelNames.length = width;
506+
this.#labelNames.fill('', current, width);
507+
508+
const extension = '|'.repeat(width - current);
509+
const newMap = new Map();
510+
511+
for (const [key, value] of this.#map.entries()) {
512+
newMap.set(`${key}${extension}`, value);
513+
}
514+
515+
this.#map = newMap;
516+
}
517+
518+
for (let i = this.#count, j = 0; j < missing.length; i++, j++) {
519+
this.#labelNames[i] = missing[j];
520+
}
521+
522+
this.#count = target;
523+
524+
return missing;
525+
}
526+
}
527+
528+
module.exports.LabelGrouper = LabelGrouper;

0 commit comments

Comments
 (0)