@artilleryio/int-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +46 -0
- package/LICENSE.txt +353 -0
- package/README.md +1 -0
- package/index.js +75 -0
- package/lib/engine_http.js +886 -0
- package/lib/engine_socketio.js +376 -0
- package/lib/engine_ws.js +366 -0
- package/lib/is-idle-phase.js +14 -0
- package/lib/phases.js +184 -0
- package/lib/readers.js +65 -0
- package/lib/runner.js +454 -0
- package/lib/ssms.js +715 -0
- package/lib/weighted-pick.js +88 -0
- package/package.json +49 -0
package/lib/ssms.js
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
// Super simple metric store
|
|
6
|
+
|
|
7
|
+
// Use our own fork of DDSketch until this PR is merged into main:
|
|
8
|
+
// https://github.com/DataDog/sketches-js/pull/13
|
|
9
|
+
const { DDSketch } = require('@artilleryio/sketches-js');
|
|
10
|
+
// const {DDSketch} = require('@datadog/sketches-js');
|
|
11
|
+
const EventEmitter = require('events');
|
|
12
|
+
const { setDriftlessInterval, clearDriftless } = require('driftless');
|
|
13
|
+
const debug = require('debug')('ssms');
|
|
14
|
+
|
|
15
|
+
class SSMS extends EventEmitter {
|
|
16
|
+
constructor(_options) {
|
|
17
|
+
super();
|
|
18
|
+
|
|
19
|
+
this.opts = _options || {};
|
|
20
|
+
|
|
21
|
+
this._counterEarliestMeasurementByPeriod = {};
|
|
22
|
+
this._counterLastMeasurementByPeriod = {};
|
|
23
|
+
this._histogramEarliestMeasurementByPeriod = {};
|
|
24
|
+
this._histogramLastMeasurementByPeriod = {};
|
|
25
|
+
|
|
26
|
+
this._aggregationIntervalSec = 5;
|
|
27
|
+
this._aggregateInterval = setDriftlessInterval(
|
|
28
|
+
this.aggregate.bind(this),
|
|
29
|
+
this._aggregationIntervalSec * 1000
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
this._lastPeriod = null;
|
|
33
|
+
|
|
34
|
+
this.isPullOnly = this.opts.pullOnly;
|
|
35
|
+
|
|
36
|
+
if (!this.isPullOnly) {
|
|
37
|
+
this._emitInterval = setDriftlessInterval(
|
|
38
|
+
this._maybeEmitMostRecentPeriod.bind(this),
|
|
39
|
+
Math.max((this._aggregationIntervalSec * 1000) / 2, 1000)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this._counters = [];
|
|
44
|
+
this._histograms = [];
|
|
45
|
+
this._rates = [];
|
|
46
|
+
|
|
47
|
+
this._active = true;
|
|
48
|
+
|
|
49
|
+
this._aggregatedCounters = {};
|
|
50
|
+
this._aggregatedHistograms = {};
|
|
51
|
+
this._aggregatedRates = {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
stop() {
|
|
55
|
+
this._active = false;
|
|
56
|
+
clearDriftless(this._aggregateInterval);
|
|
57
|
+
|
|
58
|
+
if (!this.isPullOnly) {
|
|
59
|
+
clearDriftless(this._emitInterval);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static report(pds) {
|
|
66
|
+
return pds;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// TODO: first/last metric timestamps should not = period
|
|
70
|
+
static empty(ts) {
|
|
71
|
+
const period = normalizeTs(ts || Date.now());
|
|
72
|
+
return {
|
|
73
|
+
counters: {},
|
|
74
|
+
histograms: {},
|
|
75
|
+
rates: {},
|
|
76
|
+
firstCounterAt: 0,
|
|
77
|
+
firstHistogramAt: 0,
|
|
78
|
+
lastCounterAt: 0,
|
|
79
|
+
lastHistogramAt: 0,
|
|
80
|
+
firstMetricAt: 0,
|
|
81
|
+
lastMetricAt: 0,
|
|
82
|
+
period
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static summarizeHistogram(h) {
|
|
87
|
+
return summarizeHistogram(h);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Take metric data for a period and return a summary object with 1.6.x-compatible format
|
|
91
|
+
static legacyReport(pd) {
|
|
92
|
+
const result = {
|
|
93
|
+
// Custom field compatibility not supported:
|
|
94
|
+
customStats: {},
|
|
95
|
+
counters: {},
|
|
96
|
+
|
|
97
|
+
scenariosAvoided: pd.counters['vusers.skipped'] || 0,
|
|
98
|
+
timestamp: new Date(pd.period),
|
|
99
|
+
scenariosCreated: pd.counters['vusers.created'] || 0,
|
|
100
|
+
scenariosCompleted: pd.counters['vusers.completed'] || 0,
|
|
101
|
+
|
|
102
|
+
requestsCompleted:
|
|
103
|
+
pd.counters['http.responses'] ||
|
|
104
|
+
pd.counters['socketio.emit'] ||
|
|
105
|
+
pd.counters['websocket.messages_sent'] ||
|
|
106
|
+
0,
|
|
107
|
+
latency: {},
|
|
108
|
+
rps: {
|
|
109
|
+
mean: pd.rates
|
|
110
|
+
? pd.rates['http.response_rate'] ||
|
|
111
|
+
pd.rates['socketio.emit_rate'] ||
|
|
112
|
+
0
|
|
113
|
+
: 0,
|
|
114
|
+
count:
|
|
115
|
+
pd.counters['http.responses'] || pd.counters['socketio.emit'] || 0
|
|
116
|
+
},
|
|
117
|
+
scenarioDuration: {},
|
|
118
|
+
scenarioCounts: {},
|
|
119
|
+
|
|
120
|
+
errors: {},
|
|
121
|
+
codes: {}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
pd.histograms &&
|
|
126
|
+
typeof pd.histograms['vusers.session_length'] !== 'undefined'
|
|
127
|
+
) {
|
|
128
|
+
result.scenarioDuration = summarizeHistogram(
|
|
129
|
+
pd.histograms['vusers.session_length']
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// scenarioCounts
|
|
134
|
+
const names = Object.keys(pd.counters).filter((k) =>
|
|
135
|
+
k.startsWith('vusers.created_by_name.')
|
|
136
|
+
);
|
|
137
|
+
for (const n of names) {
|
|
138
|
+
result.scenarioCounts[n.split('vusers.created_by_name.')[1]] =
|
|
139
|
+
pd.counters[n];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// latency
|
|
143
|
+
const latencyh = pd.histograms
|
|
144
|
+
? pd.histograms['http.response_time'] ||
|
|
145
|
+
pd.histograms['socketio.response_time']
|
|
146
|
+
: null;
|
|
147
|
+
if (latencyh) {
|
|
148
|
+
result.latency = summarizeHistogram(latencyh);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// HTTP codes
|
|
152
|
+
const codeNames = Object.keys(pd.counters).filter((k) =>
|
|
153
|
+
k.match(/^(http|socketio)\.codes.*/)
|
|
154
|
+
);
|
|
155
|
+
for (const n of codeNames) {
|
|
156
|
+
const code = parseInt(n.split('.codes.')[1]);
|
|
157
|
+
result.codes[code] = pd.counters[n];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// errors
|
|
161
|
+
const errNames = Object.keys(pd.counters).filter((k) =>
|
|
162
|
+
k.startsWith('errors.')
|
|
163
|
+
);
|
|
164
|
+
for (const n of errNames) {
|
|
165
|
+
const errName = n.split('errors.')[1];
|
|
166
|
+
result.errors[errName] = pd.counters[n];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
report: function () {
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Return object indexed by period (as string):
|
|
177
|
+
static mergeBuckets(periodData) {
|
|
178
|
+
debug(`mergeBuckets // timeslices: ${periodData.map((pd) => pd.period)}`);
|
|
179
|
+
|
|
180
|
+
// Returns result[timestamp] = {histograms:{},counters:{},rates:{}}
|
|
181
|
+
// ie. the result is indexed by timeslice
|
|
182
|
+
const result = {};
|
|
183
|
+
|
|
184
|
+
for (const pd of periodData) {
|
|
185
|
+
const ts = pd.period;
|
|
186
|
+
|
|
187
|
+
if (!result[ts]) {
|
|
188
|
+
result[ts] = {
|
|
189
|
+
counters: {},
|
|
190
|
+
histograms: {},
|
|
191
|
+
rates: {}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
pd.counters = pd.counters || {};
|
|
196
|
+
pd.histograms = pd.histograms || {};
|
|
197
|
+
pd.rates = pd.rates || {};
|
|
198
|
+
|
|
199
|
+
//
|
|
200
|
+
// counters
|
|
201
|
+
//
|
|
202
|
+
for (const [name, value] of Object.entries(pd.counters)) {
|
|
203
|
+
if (!result[ts].counters[name]) {
|
|
204
|
+
result[ts].counters[name] = 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
result[ts].counters[name] += value;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
//
|
|
211
|
+
// histograms
|
|
212
|
+
//
|
|
213
|
+
for (const [name, origValue] of Object.entries(pd.histograms)) {
|
|
214
|
+
const value = SSMS.cloneHistogram(origValue);
|
|
215
|
+
if (typeof result[ts].histograms[name] === 'undefined') {
|
|
216
|
+
result[ts].histograms[name] = value;
|
|
217
|
+
} else {
|
|
218
|
+
// NOTE: this will throw if gamma (accuracy) parameters are different
|
|
219
|
+
// in those two sketches
|
|
220
|
+
result[ts].histograms[name].merge(value);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
//
|
|
225
|
+
// rates
|
|
226
|
+
//
|
|
227
|
+
for (const [name, value] of Object.entries(pd.rates)) {
|
|
228
|
+
if (typeof result[ts].rates[name] === 'undefined') {
|
|
229
|
+
result[ts].rates[name] = 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
result[ts].rates[name] += value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const name of Object.keys(pd.rates)) {
|
|
236
|
+
result[ts][name] = round(result[ts][name] / periodData.length, 1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
result[ts].firstCounterAt = min([
|
|
240
|
+
result[ts].firstCounterAt,
|
|
241
|
+
pd.firstCounterAt
|
|
242
|
+
]);
|
|
243
|
+
result[ts].firstHistogramAt = min([
|
|
244
|
+
result[ts].firstHistogramAt,
|
|
245
|
+
pd.firstHistogramAt
|
|
246
|
+
]);
|
|
247
|
+
result[ts].lastCounterAt = max([
|
|
248
|
+
result[ts].lastCounterAt,
|
|
249
|
+
pd.lastCounterAt
|
|
250
|
+
]);
|
|
251
|
+
result[ts].lastHistogramAt = max([
|
|
252
|
+
result[ts].lastHistogramAt,
|
|
253
|
+
pd.lastHistogramAt
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
result[ts].firstMetricAt = min([
|
|
257
|
+
result[ts].firstHistogramAt,
|
|
258
|
+
result[ts].firstCounterAt
|
|
259
|
+
]);
|
|
260
|
+
result[ts].lastMetricAt = max([
|
|
261
|
+
result[ts].lastHistogramAt,
|
|
262
|
+
result[ts].lastCounterAt
|
|
263
|
+
]);
|
|
264
|
+
result[ts].period = ts;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Aggregate at lower resolution, i.e. combine three distinct periods of 10s into one of 30s
|
|
271
|
+
// Note: does not check that periods are contiguous, everything is simply merged together
|
|
272
|
+
static pack(periods) {
|
|
273
|
+
const result = {
|
|
274
|
+
counters: {},
|
|
275
|
+
histograms: {},
|
|
276
|
+
rates: {}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
for (const pd of periods) {
|
|
280
|
+
pd.counters = Object.assign({}, pd.counters || {});
|
|
281
|
+
pd.histograms = Object.assign({}, pd.histograms || {});
|
|
282
|
+
pd.rates = Object.assign({}, pd.rates || {});
|
|
283
|
+
|
|
284
|
+
for (const [name, value] of Object.entries(pd.counters)) {
|
|
285
|
+
if (!result.counters[name]) {
|
|
286
|
+
result.counters[name] = 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
result.counters[name] += value;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for (const [name, origValue] of Object.entries(pd.histograms)) {
|
|
293
|
+
const value = SSMS.cloneHistogram(origValue);
|
|
294
|
+
if (typeof result.histograms[name] === 'undefined') {
|
|
295
|
+
result.histograms[name] = value;
|
|
296
|
+
} else {
|
|
297
|
+
// NOTE: this will throw if gamma (accuracy) parameters are different
|
|
298
|
+
// in those two sketches
|
|
299
|
+
result.histograms[name].merge(value);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
for (const [name, value] of Object.entries(pd.rates)) {
|
|
304
|
+
if (!result.rates[name]) {
|
|
305
|
+
result.rates[name] = 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// TODO: retain first/last so that we have the duration
|
|
309
|
+
// or retain the duration of the window in which rate events
|
|
310
|
+
// were recorded alongside the average value
|
|
311
|
+
result.rates[name] += value;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const [name, _value] of Object.entries(result.rates)) {
|
|
316
|
+
result.rates[name] = round(result.rates[name] / periods.length, 0);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
result.firstCounterAt = min(periods.map((p) => p.firstCounterAt));
|
|
320
|
+
result.firstHistogramAt = min(periods.map((p) => p.firstHistogramAt));
|
|
321
|
+
result.lastCounterAt = max(periods.map((p) => p.lastCounterAt));
|
|
322
|
+
result.lastHistogramAt = max(periods.map((p) => p.lastHistogramAt));
|
|
323
|
+
|
|
324
|
+
result.firstMetricAt = min([
|
|
325
|
+
result.firstHistogramAt,
|
|
326
|
+
result.firstCounterAt
|
|
327
|
+
]);
|
|
328
|
+
result.lastMetricAt = max([result.lastHistogramAt, result.lastCounterAt]);
|
|
329
|
+
|
|
330
|
+
result.period = max(periods.map((p) => p.period));
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
static cloneHistogram(h) {
|
|
335
|
+
return DDSketch.fromProto(h.toProto());
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
static serializeMetrics(pd) {
|
|
339
|
+
// TODO: Add ability to include arbitrary metadata e.g. worker IDs
|
|
340
|
+
const serializedHistograms = {};
|
|
341
|
+
const ph = pd.histograms;
|
|
342
|
+
if (ph) {
|
|
343
|
+
for (const n of Object.keys(ph)) {
|
|
344
|
+
const h = ph[n];
|
|
345
|
+
const buf = h.toProto();
|
|
346
|
+
serializedHistograms[n] = buf;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// TODO: Mark as serialized, otherwise to check whether we have a serialized object or not
|
|
351
|
+
// is to check if .histograms is a Buffer
|
|
352
|
+
const result = Object.assign({}, pd, { histograms: serializedHistograms });
|
|
353
|
+
return stringify(result);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
static deserializeMetrics(pd) {
|
|
357
|
+
const object = parse(pd);
|
|
358
|
+
for (const [name, buf] of Object.entries(object.histograms)) {
|
|
359
|
+
const h = DDSketch.fromProto(buf);
|
|
360
|
+
object.histograms[name] = h;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
object.period = object.period;
|
|
364
|
+
|
|
365
|
+
return object;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
getBucketIds() {
|
|
369
|
+
return [
|
|
370
|
+
...new Set(
|
|
371
|
+
Object.keys(this._aggregatedCounters)
|
|
372
|
+
.concat(Object.keys(this._aggregatedHistograms))
|
|
373
|
+
.sort()
|
|
374
|
+
)
|
|
375
|
+
].reverse();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// TODO: Deprecate
|
|
379
|
+
counter(name, value) {
|
|
380
|
+
this.incr(name, value);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
incr(name, value, t) {
|
|
384
|
+
this._counters.push(t || Date.now(), name, value);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// TODO: Deprecate
|
|
388
|
+
summary(name, value) {
|
|
389
|
+
this.histogram(name, value);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
histogram(name, value, t) {
|
|
393
|
+
this._histograms.push(t || Date.now(), name, value);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
rate(name, t) {
|
|
397
|
+
this._rates.push(t || Date.now(), name);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
getMetrics(period) {
|
|
401
|
+
const result = {};
|
|
402
|
+
|
|
403
|
+
const counters = this._aggregatedCounters[period];
|
|
404
|
+
const histograms = this._aggregatedHistograms[period];
|
|
405
|
+
const rates = this._aggregatedRates[period];
|
|
406
|
+
|
|
407
|
+
if (counters) {
|
|
408
|
+
result.counters = counters;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (histograms) {
|
|
412
|
+
result.histograms = histograms;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (rates) {
|
|
416
|
+
result.rates = rates;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
result.period = period;
|
|
420
|
+
result.firstCounterAt = this._counterEarliestMeasurementByPeriod[period];
|
|
421
|
+
result.firstHistogramAt =
|
|
422
|
+
this._histogramEarliestMeasurementByPeriod[period];
|
|
423
|
+
|
|
424
|
+
result.lastCounterAt = this._counterLastMeasurementByPeriod[period];
|
|
425
|
+
result.lastHistogramAt = this._histogramLastMeasurementByPeriod[period];
|
|
426
|
+
|
|
427
|
+
result.firstMetricAt = min([
|
|
428
|
+
result.firstHistogramAt,
|
|
429
|
+
result.firstCounterAt
|
|
430
|
+
]);
|
|
431
|
+
result.lastMetricAt = max([result.lastHistogramAt, result.lastCounterAt]);
|
|
432
|
+
|
|
433
|
+
// TODO: Include size of the window, for cases when it's not 10s
|
|
434
|
+
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
_aggregateHistograms(upToTimeslice) {
|
|
439
|
+
for (let i = 0; i < this._histograms.length; i += 3) {
|
|
440
|
+
const ts = this._histograms[i];
|
|
441
|
+
const timeslice = normalizeTs(ts);
|
|
442
|
+
|
|
443
|
+
if (timeslice >= upToTimeslice) {
|
|
444
|
+
this._histograms.splice(0, i);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const name = this._histograms[i + 1];
|
|
449
|
+
const value = this._histograms[i + 2];
|
|
450
|
+
|
|
451
|
+
if (!this._aggregatedHistograms[timeslice]) {
|
|
452
|
+
this._aggregatedHistograms[timeslice] = {};
|
|
453
|
+
this._histogramEarliestMeasurementByPeriod[timeslice] = ts;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!this._aggregatedHistograms[timeslice][name]) {
|
|
457
|
+
this._aggregatedHistograms[timeslice][name] = new DDSketch({
|
|
458
|
+
relativeAccuracy: 0.01
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// TODO: Benchmark
|
|
463
|
+
this._histogramLastMeasurementByPeriod[timeslice] = ts;
|
|
464
|
+
|
|
465
|
+
this._aggregatedHistograms[timeslice][name].accept(value);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
this._histograms.splice(0, this._histograms.length);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
_aggregateCounters(upToTimeslice) {
|
|
472
|
+
// Consider memory-CPU tradeoff. Depending on the length of the buffer, we may want to
|
|
473
|
+
// not exceed N total entries we're processing if we can delay reporting by one or more
|
|
474
|
+
// reporting periods
|
|
475
|
+
|
|
476
|
+
for (let i = 0; i < this._counters.length; i += 3) {
|
|
477
|
+
const ts = this._counters[i];
|
|
478
|
+
const timeslice = normalizeTs(ts);
|
|
479
|
+
|
|
480
|
+
if (timeslice >= upToTimeslice) {
|
|
481
|
+
this._counters.splice(0, i);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const name = this._counters[i + 1];
|
|
486
|
+
const value = this._counters[i + 2];
|
|
487
|
+
|
|
488
|
+
if (!this._aggregatedCounters[timeslice]) {
|
|
489
|
+
this._aggregatedCounters[timeslice] = {};
|
|
490
|
+
this._counterEarliestMeasurementByPeriod[timeslice] = ts;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (typeof this._aggregatedCounters[timeslice][name] === 'undefined') {
|
|
494
|
+
this._aggregatedCounters[timeslice][name] = value;
|
|
495
|
+
} else {
|
|
496
|
+
this._aggregatedCounters[timeslice][name] += value;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
this._counterLastMeasurementByPeriod[timeslice] = ts;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
this._counters.splice(0, this._counters.length);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
_aggregateRates(upToTimeslice) {
|
|
506
|
+
debug('_aggregateRates to', upToTimeslice, new Date(upToTimeslice));
|
|
507
|
+
|
|
508
|
+
const a = {};
|
|
509
|
+
let spliceTo = this._rates.length;
|
|
510
|
+
|
|
511
|
+
for (let i = 0; i < this._rates.length; i += 2) {
|
|
512
|
+
const ts = this._rates[i];
|
|
513
|
+
const timeslice = normalizeTs(ts);
|
|
514
|
+
|
|
515
|
+
if (timeslice >= upToTimeslice) {
|
|
516
|
+
debug(
|
|
517
|
+
'_aggregateRates early return // i=',
|
|
518
|
+
i,
|
|
519
|
+
'timeslice=',
|
|
520
|
+
timeslice,
|
|
521
|
+
new Date(timeslice)
|
|
522
|
+
);
|
|
523
|
+
spliceTo = i;
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const name = this._rates[i + 1];
|
|
528
|
+
if (!a[timeslice]) {
|
|
529
|
+
a[timeslice] = {};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (!a[timeslice][name]) {
|
|
533
|
+
a[timeslice][name] = {
|
|
534
|
+
first: Number.POSITIVE_INFINITY,
|
|
535
|
+
last: 0,
|
|
536
|
+
count: 0
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
a[timeslice][name].first = Math.min(a[timeslice][name].first, ts);
|
|
541
|
+
a[timeslice][name].last = Math.max(a[timeslice][name].last, ts);
|
|
542
|
+
a[timeslice][name].count++;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
for (const [ts, rs] of Object.entries(a)) {
|
|
546
|
+
for (const [name, _] of Object.entries(rs)) {
|
|
547
|
+
const { first, last, count } = a[ts][name];
|
|
548
|
+
if (!this._aggregatedRates[ts]) {
|
|
549
|
+
this._aggregatedRates[ts] = {};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
this._aggregatedRates[ts][name] = round(
|
|
553
|
+
count / (Math.max(last - first, 1000) / 1000),
|
|
554
|
+
0
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
this._rates.splice(0, spliceTo);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
aggregate(forceAll) {
|
|
563
|
+
const currentTimeslice =
|
|
564
|
+
normalizeTs(Date.now()) + (forceAll ? 30 * 1000 : 0);
|
|
565
|
+
|
|
566
|
+
this._aggregateCounters(currentTimeslice);
|
|
567
|
+
this._aggregateHistograms(currentTimeslice);
|
|
568
|
+
this._aggregateRates(currentTimeslice);
|
|
569
|
+
|
|
570
|
+
if (forceAll) {
|
|
571
|
+
this._emitPeriods();
|
|
572
|
+
} else {
|
|
573
|
+
this._maybeEmitMostRecentPeriod();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
_emitPeriods() {
|
|
578
|
+
const bucketIds = this.getBucketIds();
|
|
579
|
+
const lastPeriod = parseInt(this._lastPeriod, 10);
|
|
580
|
+
|
|
581
|
+
for (let i = 0; i < bucketIds.length; i++) {
|
|
582
|
+
const period = bucketIds[i];
|
|
583
|
+
|
|
584
|
+
if (!this._lastPeriod || parseInt(period, 10) > lastPeriod) {
|
|
585
|
+
this.emit('metricData', period, this.getMetrics(period));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
_maybeEmitMostRecentPeriod() {
|
|
591
|
+
const p = this.getBucketIds()[0];
|
|
592
|
+
|
|
593
|
+
if (p && p !== this._lastPeriod) {
|
|
594
|
+
this.emit('metricData', p, this.getMetrics(p)); // Measurements in period p have been aggregated
|
|
595
|
+
this._lastPeriod = p;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function normalizeTs(epochMs, windowSize = 10) {
|
|
601
|
+
// Reset down to minute
|
|
602
|
+
const m = Math.floor((epochMs - (epochMs % 1000)) / 1000 / 60) * 60 * 1000;
|
|
603
|
+
// Number of seconds past the minute
|
|
604
|
+
const s = ((epochMs - (epochMs % 1000)) / 1000) % 60;
|
|
605
|
+
// Number of seconds to take off
|
|
606
|
+
const d = s % windowSize;
|
|
607
|
+
return m + (s - d) * 1000;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Function hms(epochMs) {
|
|
611
|
+
// return [
|
|
612
|
+
// Math.round((epochMs / 1000 / 60 / 60) % 24),
|
|
613
|
+
// Math.round((epochMs / 1000 / 60) % 60),
|
|
614
|
+
// Math.round(epochMs / 1000) % 60
|
|
615
|
+
// ];
|
|
616
|
+
// }
|
|
617
|
+
|
|
618
|
+
function round(number, decimals) {
|
|
619
|
+
const m = 10 ** decimals;
|
|
620
|
+
return Math.round(number * m) / m;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// h is an instance of DDSketch
|
|
624
|
+
function summarizeHistogram(h) {
|
|
625
|
+
return {
|
|
626
|
+
min: round(h.min, 1),
|
|
627
|
+
max: round(h.max, 1),
|
|
628
|
+
count: h.count,
|
|
629
|
+
p50: round(h.getValueAtQuantile(0.5), 1),
|
|
630
|
+
median: round(h.getValueAtQuantile(0.5), 1), // Here for compatibility
|
|
631
|
+
p75: round(h.getValueAtQuantile(0.75), 1),
|
|
632
|
+
p90: round(h.getValueAtQuantile(0.9), 1),
|
|
633
|
+
p95: round(h.getValueAtQuantile(0.95), 1),
|
|
634
|
+
p99: round(h.getValueAtQuantile(0.99), 1),
|
|
635
|
+
p999: round(h.getValueAtQuantile(0.999), 1)
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/// ///////////////////////////////////////////
|
|
640
|
+
function stringify(value, space) {
|
|
641
|
+
return JSON.stringify(value, replacer, space);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function parse(text) {
|
|
645
|
+
return JSON.parse(text, reviver);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function replacer(key, value) {
|
|
649
|
+
if (isBufferLike(value) && isArray(value.data)) {
|
|
650
|
+
if (value.data.length > 0) {
|
|
651
|
+
value.data = 'base64:' + Buffer.from(value.data).toString('base64');
|
|
652
|
+
} else {
|
|
653
|
+
value.data = '';
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return value;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function reviver(key, value) {
|
|
661
|
+
if (isBufferLike(value)) {
|
|
662
|
+
if (isArray(value.data)) {
|
|
663
|
+
return Buffer.from(value.data);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (isString(value.data)) {
|
|
667
|
+
if (value.data.startsWith('base64:')) {
|
|
668
|
+
return Buffer.from(value.data.slice('base64:'.length), 'base64');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Assume that the string is UTF-8 encoded (or empty).
|
|
672
|
+
return Buffer.from(value.data);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return value;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function isBufferLike(x) {
|
|
680
|
+
return (
|
|
681
|
+
isObject(x) && x.type === 'Buffer' && (isArray(x.data) || isString(x.data))
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function isArray(x) {
|
|
686
|
+
return Array.isArray(x);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function isString(x) {
|
|
690
|
+
return typeof x === 'string';
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function isObject(x) {
|
|
694
|
+
return typeof x === 'object' && x !== null;
|
|
695
|
+
}
|
|
696
|
+
/// /////////////////
|
|
697
|
+
|
|
698
|
+
// Like Math.min and Math.max but take a list of values, and ignore
|
|
699
|
+
// undefined's rather than returning NaN when a value is undefined.
|
|
700
|
+
// Returns undefined if all arguments are undefined.
|
|
701
|
+
function min(values) {
|
|
702
|
+
const m = Math.min(...values.filter((x) => x));
|
|
703
|
+
return m === Number.POSITIVE_INFINITY ? undefined : m;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function max(values) {
|
|
707
|
+
const m = Math.max(...values.filter((x) => x));
|
|
708
|
+
return m === Number.NEGATIVE_INFINITY ? undefined : m;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
module.exports = {
|
|
712
|
+
SSMS,
|
|
713
|
+
summarizeHistogram,
|
|
714
|
+
normalizeTs
|
|
715
|
+
};
|