@artilleryio/int-core 2.27.0 → 2.28.0-2c19512
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/.turbo/turbo-build.log +4 -0
- package/dist/index.js +13 -0
- package/dist/lib/engine_http.js +817 -0
- package/dist/lib/engine_socketio.js +347 -0
- package/dist/lib/engine_ws.js +278 -0
- package/dist/lib/is-idle-phase.js +9 -0
- package/dist/lib/phases.js +213 -0
- package/dist/lib/readers.js +58 -0
- package/dist/lib/runner.js +463 -0
- package/dist/lib/ssms.js +555 -0
- package/dist/lib/update-global-object.js +75 -0
- package/dist/lib/weighted-pick.js +54 -0
- package/index.ts +14 -0
- package/lib/{engine_http.js → engine_http.ts} +28 -28
- package/lib/{engine_socketio.js → engine_socketio.ts} +13 -11
- package/lib/{engine_ws.js → engine_ws.ts} +16 -10
- package/lib/{is-idle-phase.js → is-idle-phase.ts} +1 -3
- package/lib/{phases.js → phases.ts} +12 -10
- package/lib/{readers.js → readers.ts} +2 -4
- package/lib/{runner.js → runner.ts} +84 -69
- package/lib/{ssms.js → ssms.ts} +18 -18
- package/{index.js → lib/update-global-object.ts} +9 -14
- package/lib/{weighted-pick.js → weighted-pick.ts} +7 -3
- package/package.json +10 -17
- package/tsconfig.build.json +9 -0
- package/types.d.ts +12 -0
package/dist/lib/ssms.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
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
|
+
// Super simple metric store
|
|
5
|
+
// Use our own fork of DDSketch until this PR is merged into main:
|
|
6
|
+
// https://github.com/DataDog/sketches-js/pull/13
|
|
7
|
+
import { EventEmitter } from 'node:events';
|
|
8
|
+
import { DDSketch } from '@artilleryio/sketches-js';
|
|
9
|
+
import createDebug from 'debug';
|
|
10
|
+
import { clearDriftless, setDriftlessInterval } from 'driftless';
|
|
11
|
+
const debug = createDebug('ssms');
|
|
12
|
+
const MAX_METRIC_NAME_LENGTH = 1024;
|
|
13
|
+
class SSMS extends EventEmitter {
|
|
14
|
+
constructor(_options) {
|
|
15
|
+
super();
|
|
16
|
+
this.opts = _options || {};
|
|
17
|
+
this._counterEarliestMeasurementByPeriod = {};
|
|
18
|
+
this._counterLastMeasurementByPeriod = {};
|
|
19
|
+
this._histogramEarliestMeasurementByPeriod = {};
|
|
20
|
+
this._histogramLastMeasurementByPeriod = {};
|
|
21
|
+
this._aggregationIntervalSec = 5;
|
|
22
|
+
this._aggregateInterval = setDriftlessInterval(this.aggregate.bind(this), this._aggregationIntervalSec * 1000);
|
|
23
|
+
this._lastPeriod = null;
|
|
24
|
+
this.isPullOnly = this.opts.pullOnly;
|
|
25
|
+
if (!this.isPullOnly) {
|
|
26
|
+
this._emitInterval = setDriftlessInterval(this._maybeEmitMostRecentPeriod.bind(this), Math.max((this._aggregationIntervalSec * 1000) / 2, 1000));
|
|
27
|
+
}
|
|
28
|
+
this._counters = [];
|
|
29
|
+
this._histograms = [];
|
|
30
|
+
this._rates = [];
|
|
31
|
+
this._active = true;
|
|
32
|
+
this._aggregatedCounters = {};
|
|
33
|
+
this._aggregatedHistograms = {};
|
|
34
|
+
this._aggregatedRates = {};
|
|
35
|
+
}
|
|
36
|
+
stop() {
|
|
37
|
+
this._active = false;
|
|
38
|
+
clearDriftless(this._aggregateInterval);
|
|
39
|
+
if (!this.isPullOnly) {
|
|
40
|
+
clearDriftless(this._emitInterval);
|
|
41
|
+
}
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
static report(pds) {
|
|
45
|
+
return pds;
|
|
46
|
+
}
|
|
47
|
+
// TODO: first/last metric timestamps should not = period
|
|
48
|
+
static empty(ts) {
|
|
49
|
+
const period = normalizeTs(ts || Date.now());
|
|
50
|
+
return {
|
|
51
|
+
counters: {},
|
|
52
|
+
histograms: {},
|
|
53
|
+
rates: {},
|
|
54
|
+
firstCounterAt: 0,
|
|
55
|
+
firstHistogramAt: 0,
|
|
56
|
+
lastCounterAt: 0,
|
|
57
|
+
lastHistogramAt: 0,
|
|
58
|
+
firstMetricAt: 0,
|
|
59
|
+
lastMetricAt: 0,
|
|
60
|
+
period
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
static summarizeHistogram(h) {
|
|
64
|
+
return summarizeHistogram(h);
|
|
65
|
+
}
|
|
66
|
+
// Take metric data for a period and return a summary object with 1.6.x-compatible format
|
|
67
|
+
static legacyReport(pd) {
|
|
68
|
+
const result = {
|
|
69
|
+
// Custom field compatibility not supported:
|
|
70
|
+
customStats: {},
|
|
71
|
+
counters: {},
|
|
72
|
+
scenariosAvoided: pd.counters['vusers.skipped'] || 0,
|
|
73
|
+
timestamp: new Date(pd.period),
|
|
74
|
+
scenariosCreated: pd.counters['vusers.created'] || 0,
|
|
75
|
+
scenariosCompleted: pd.counters['vusers.completed'] || 0,
|
|
76
|
+
requestsCompleted: pd.counters['http.responses'] ||
|
|
77
|
+
pd.counters['socketio.emit'] ||
|
|
78
|
+
pd.counters['websocket.messages_sent'] ||
|
|
79
|
+
0,
|
|
80
|
+
latency: {},
|
|
81
|
+
rps: {
|
|
82
|
+
mean: pd.rates
|
|
83
|
+
? pd.rates['http.response_rate'] ||
|
|
84
|
+
pd.rates['socketio.emit_rate'] ||
|
|
85
|
+
0
|
|
86
|
+
: 0,
|
|
87
|
+
count: pd.counters['http.responses'] || pd.counters['socketio.emit'] || 0
|
|
88
|
+
},
|
|
89
|
+
scenarioDuration: {},
|
|
90
|
+
scenarioCounts: {},
|
|
91
|
+
errors: {},
|
|
92
|
+
codes: {}
|
|
93
|
+
};
|
|
94
|
+
if (pd.histograms &&
|
|
95
|
+
typeof pd.histograms['vusers.session_length'] !== 'undefined') {
|
|
96
|
+
result.scenarioDuration = summarizeHistogram(pd.histograms['vusers.session_length']);
|
|
97
|
+
}
|
|
98
|
+
// scenarioCounts
|
|
99
|
+
const names = Object.keys(pd.counters).filter((k) => k.startsWith('vusers.created_by_name.'));
|
|
100
|
+
for (const n of names) {
|
|
101
|
+
result.scenarioCounts[n.split('vusers.created_by_name.')[1]] =
|
|
102
|
+
pd.counters[n];
|
|
103
|
+
}
|
|
104
|
+
// latency
|
|
105
|
+
const latencyh = pd.histograms
|
|
106
|
+
? pd.histograms['http.response_time'] ||
|
|
107
|
+
pd.histograms['socketio.response_time']
|
|
108
|
+
: null;
|
|
109
|
+
if (latencyh) {
|
|
110
|
+
result.latency = summarizeHistogram(latencyh);
|
|
111
|
+
}
|
|
112
|
+
// HTTP codes
|
|
113
|
+
const codeNames = Object.keys(pd.counters).filter((k) => k.match(/^(http|socketio)\.codes.*/));
|
|
114
|
+
for (const n of codeNames) {
|
|
115
|
+
const code = parseInt(n.split('.codes.')[1], 10);
|
|
116
|
+
result.codes[code] = pd.counters[n];
|
|
117
|
+
}
|
|
118
|
+
// errors
|
|
119
|
+
const errNames = Object.keys(pd.counters).filter((k) => k.startsWith('errors.'));
|
|
120
|
+
for (const n of errNames) {
|
|
121
|
+
const errName = n.split('errors.')[1];
|
|
122
|
+
result.errors[errName] = pd.counters[n];
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
report: () => result
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Return object indexed by period (as string):
|
|
129
|
+
static mergeBuckets(periodData) {
|
|
130
|
+
debug(`mergeBuckets // timeslices: ${periodData.map((pd) => pd.period)}`);
|
|
131
|
+
// Returns result[timestamp] = {histograms:{},counters:{},rates:{}}
|
|
132
|
+
// ie. the result is indexed by timeslice
|
|
133
|
+
const result = {};
|
|
134
|
+
for (const pd of periodData) {
|
|
135
|
+
const ts = pd.period;
|
|
136
|
+
if (!result[ts]) {
|
|
137
|
+
result[ts] = {
|
|
138
|
+
counters: {},
|
|
139
|
+
histograms: {},
|
|
140
|
+
rates: {}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
pd.counters = pd.counters || {};
|
|
144
|
+
pd.histograms = pd.histograms || {};
|
|
145
|
+
pd.rates = pd.rates || {};
|
|
146
|
+
//
|
|
147
|
+
// counters
|
|
148
|
+
//
|
|
149
|
+
for (const [name, value] of Object.entries(pd.counters)) {
|
|
150
|
+
if (!result[ts].counters[name]) {
|
|
151
|
+
result[ts].counters[name] = 0;
|
|
152
|
+
}
|
|
153
|
+
result[ts].counters[name] += value;
|
|
154
|
+
}
|
|
155
|
+
//
|
|
156
|
+
// histograms
|
|
157
|
+
//
|
|
158
|
+
for (const [name, origValue] of Object.entries(pd.histograms)) {
|
|
159
|
+
const value = SSMS.cloneHistogram(origValue);
|
|
160
|
+
if (typeof result[ts].histograms[name] === 'undefined') {
|
|
161
|
+
result[ts].histograms[name] = value;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// NOTE: this will throw if gamma (accuracy) parameters are different
|
|
165
|
+
// in those two sketches
|
|
166
|
+
result[ts].histograms[name].merge(value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//
|
|
170
|
+
// rates
|
|
171
|
+
//
|
|
172
|
+
for (const [name, value] of Object.entries(pd.rates)) {
|
|
173
|
+
if (typeof result[ts].rates[name] === 'undefined') {
|
|
174
|
+
result[ts].rates[name] = 0;
|
|
175
|
+
}
|
|
176
|
+
result[ts].rates[name] += value;
|
|
177
|
+
}
|
|
178
|
+
for (const name of Object.keys(pd.rates)) {
|
|
179
|
+
result[ts][name] = round(result[ts][name] / periodData.length, 1);
|
|
180
|
+
}
|
|
181
|
+
result[ts].firstCounterAt = min([
|
|
182
|
+
result[ts].firstCounterAt,
|
|
183
|
+
pd.firstCounterAt
|
|
184
|
+
]);
|
|
185
|
+
result[ts].firstHistogramAt = min([
|
|
186
|
+
result[ts].firstHistogramAt,
|
|
187
|
+
pd.firstHistogramAt
|
|
188
|
+
]);
|
|
189
|
+
result[ts].lastCounterAt = max([
|
|
190
|
+
result[ts].lastCounterAt,
|
|
191
|
+
pd.lastCounterAt
|
|
192
|
+
]);
|
|
193
|
+
result[ts].lastHistogramAt = max([
|
|
194
|
+
result[ts].lastHistogramAt,
|
|
195
|
+
pd.lastHistogramAt
|
|
196
|
+
]);
|
|
197
|
+
result[ts].firstMetricAt = min([
|
|
198
|
+
result[ts].firstHistogramAt,
|
|
199
|
+
result[ts].firstCounterAt
|
|
200
|
+
]);
|
|
201
|
+
result[ts].lastMetricAt = max([
|
|
202
|
+
result[ts].lastHistogramAt,
|
|
203
|
+
result[ts].lastCounterAt
|
|
204
|
+
]);
|
|
205
|
+
result[ts].period = ts;
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
// Aggregate at lower resolution, i.e. combine three distinct periods of 10s into one of 30s
|
|
210
|
+
// Note: does not check that periods are contiguous, everything is simply merged together
|
|
211
|
+
static pack(periods) {
|
|
212
|
+
const result = {
|
|
213
|
+
counters: {},
|
|
214
|
+
histograms: {},
|
|
215
|
+
rates: {}
|
|
216
|
+
};
|
|
217
|
+
for (const pd of periods) {
|
|
218
|
+
pd.counters = Object.assign({}, pd.counters || {});
|
|
219
|
+
pd.histograms = Object.assign({}, pd.histograms || {});
|
|
220
|
+
pd.rates = Object.assign({}, pd.rates || {});
|
|
221
|
+
for (const [name, value] of Object.entries(pd.counters)) {
|
|
222
|
+
if (!result.counters[name]) {
|
|
223
|
+
result.counters[name] = 0;
|
|
224
|
+
}
|
|
225
|
+
result.counters[name] += value;
|
|
226
|
+
}
|
|
227
|
+
for (const [name, origValue] of Object.entries(pd.histograms)) {
|
|
228
|
+
const value = SSMS.cloneHistogram(origValue);
|
|
229
|
+
if (typeof result.histograms[name] === 'undefined') {
|
|
230
|
+
result.histograms[name] = value;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
// NOTE: this will throw if gamma (accuracy) parameters are different
|
|
234
|
+
// in those two sketches
|
|
235
|
+
result.histograms[name].merge(value);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const [name, value] of Object.entries(pd.rates)) {
|
|
239
|
+
if (!result.rates[name]) {
|
|
240
|
+
result.rates[name] = 0;
|
|
241
|
+
}
|
|
242
|
+
// TODO: retain first/last so that we have the duration
|
|
243
|
+
// or retain the duration of the window in which rate events
|
|
244
|
+
// were recorded alongside the average value
|
|
245
|
+
result.rates[name] += value;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const [name, _value] of Object.entries(result.rates)) {
|
|
249
|
+
result.rates[name] = round(result.rates[name] / periods.length, 0);
|
|
250
|
+
}
|
|
251
|
+
result.firstCounterAt = min(periods.map((p) => p.firstCounterAt));
|
|
252
|
+
result.firstHistogramAt = min(periods.map((p) => p.firstHistogramAt));
|
|
253
|
+
result.lastCounterAt = max(periods.map((p) => p.lastCounterAt));
|
|
254
|
+
result.lastHistogramAt = max(periods.map((p) => p.lastHistogramAt));
|
|
255
|
+
result.firstMetricAt = min([
|
|
256
|
+
result.firstHistogramAt,
|
|
257
|
+
result.firstCounterAt
|
|
258
|
+
]);
|
|
259
|
+
result.lastMetricAt = max([result.lastHistogramAt, result.lastCounterAt]);
|
|
260
|
+
result.period = max(periods.map((p) => p.period));
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
static cloneHistogram(h) {
|
|
264
|
+
return DDSketch.fromProto(h.toProto());
|
|
265
|
+
}
|
|
266
|
+
static serializeMetrics(pd) {
|
|
267
|
+
// TODO: Add ability to include arbitrary metadata e.g. worker IDs
|
|
268
|
+
const serializedHistograms = {};
|
|
269
|
+
const ph = pd.histograms;
|
|
270
|
+
if (ph) {
|
|
271
|
+
for (const n of Object.keys(ph)) {
|
|
272
|
+
const h = ph[n];
|
|
273
|
+
const buf = h.toProto();
|
|
274
|
+
serializedHistograms[n] = buf;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// TODO: Mark as serialized, otherwise to check whether we have a serialized object or not
|
|
278
|
+
// is to check if .histograms is a Buffer
|
|
279
|
+
const result = Object.assign({}, pd, { histograms: serializedHistograms });
|
|
280
|
+
return stringify(result);
|
|
281
|
+
}
|
|
282
|
+
static deserializeMetrics(pd) {
|
|
283
|
+
const object = parse(pd);
|
|
284
|
+
for (const [name, buf] of Object.entries(object.histograms)) {
|
|
285
|
+
const h = DDSketch.fromProto(buf);
|
|
286
|
+
object.histograms[name] = h;
|
|
287
|
+
}
|
|
288
|
+
return object;
|
|
289
|
+
}
|
|
290
|
+
getBucketIds() {
|
|
291
|
+
return [
|
|
292
|
+
...new Set(Object.keys(this._aggregatedCounters)
|
|
293
|
+
.concat(Object.keys(this._aggregatedHistograms))
|
|
294
|
+
.sort())
|
|
295
|
+
].reverse();
|
|
296
|
+
}
|
|
297
|
+
// TODO: Deprecate
|
|
298
|
+
counter(name, value) {
|
|
299
|
+
this.incr(name.slice(0, MAX_METRIC_NAME_LENGTH), value);
|
|
300
|
+
}
|
|
301
|
+
incr(name, value, t) {
|
|
302
|
+
this._counters.push(t || Date.now(), name.slice(0, MAX_METRIC_NAME_LENGTH), value);
|
|
303
|
+
}
|
|
304
|
+
// TODO: Deprecate
|
|
305
|
+
summary(name, value) {
|
|
306
|
+
this.histogram(name.slice(0, MAX_METRIC_NAME_LENGTH), value);
|
|
307
|
+
}
|
|
308
|
+
histogram(name, value, t) {
|
|
309
|
+
this._histograms.push(t || Date.now(), name.slice(0, MAX_METRIC_NAME_LENGTH), value);
|
|
310
|
+
}
|
|
311
|
+
rate(name, t) {
|
|
312
|
+
this._rates.push(t || Date.now(), name.slice(0, MAX_METRIC_NAME_LENGTH));
|
|
313
|
+
}
|
|
314
|
+
getMetrics(period) {
|
|
315
|
+
const result = {};
|
|
316
|
+
const counters = this._aggregatedCounters[period];
|
|
317
|
+
const histograms = this._aggregatedHistograms[period];
|
|
318
|
+
const rates = this._aggregatedRates[period];
|
|
319
|
+
if (counters) {
|
|
320
|
+
result.counters = counters;
|
|
321
|
+
}
|
|
322
|
+
if (histograms) {
|
|
323
|
+
result.histograms = histograms;
|
|
324
|
+
}
|
|
325
|
+
if (rates) {
|
|
326
|
+
result.rates = rates;
|
|
327
|
+
}
|
|
328
|
+
result.period = period;
|
|
329
|
+
result.firstCounterAt = this._counterEarliestMeasurementByPeriod[period];
|
|
330
|
+
result.firstHistogramAt =
|
|
331
|
+
this._histogramEarliestMeasurementByPeriod[period];
|
|
332
|
+
result.lastCounterAt = this._counterLastMeasurementByPeriod[period];
|
|
333
|
+
result.lastHistogramAt = this._histogramLastMeasurementByPeriod[period];
|
|
334
|
+
result.firstMetricAt = min([
|
|
335
|
+
result.firstHistogramAt,
|
|
336
|
+
result.firstCounterAt
|
|
337
|
+
]);
|
|
338
|
+
result.lastMetricAt = max([result.lastHistogramAt, result.lastCounterAt]);
|
|
339
|
+
// TODO: Include size of the window, for cases when it's not 10s
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
_aggregateHistograms(upToTimeslice) {
|
|
343
|
+
for (let i = 0; i < this._histograms.length; i += 3) {
|
|
344
|
+
const ts = this._histograms[i];
|
|
345
|
+
const timeslice = normalizeTs(ts);
|
|
346
|
+
if (timeslice >= upToTimeslice) {
|
|
347
|
+
this._histograms.splice(0, i);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const name = this._histograms[i + 1];
|
|
351
|
+
const value = this._histograms[i + 2];
|
|
352
|
+
if (!this._aggregatedHistograms[timeslice]) {
|
|
353
|
+
this._aggregatedHistograms[timeslice] = {};
|
|
354
|
+
this._histogramEarliestMeasurementByPeriod[timeslice] = ts;
|
|
355
|
+
}
|
|
356
|
+
if (!this._aggregatedHistograms[timeslice][name]) {
|
|
357
|
+
this._aggregatedHistograms[timeslice][name] = new DDSketch({
|
|
358
|
+
relativeAccuracy: 0.01
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
// TODO: Benchmark
|
|
362
|
+
this._histogramLastMeasurementByPeriod[timeslice] = ts;
|
|
363
|
+
this._aggregatedHistograms[timeslice][name].accept(value);
|
|
364
|
+
}
|
|
365
|
+
this._histograms.splice(0, this._histograms.length);
|
|
366
|
+
}
|
|
367
|
+
_aggregateCounters(upToTimeslice) {
|
|
368
|
+
// Consider memory-CPU tradeoff. Depending on the length of the buffer, we may want to
|
|
369
|
+
// not exceed N total entries we're processing if we can delay reporting by one or more
|
|
370
|
+
// reporting periods
|
|
371
|
+
for (let i = 0; i < this._counters.length; i += 3) {
|
|
372
|
+
const ts = this._counters[i];
|
|
373
|
+
const timeslice = normalizeTs(ts);
|
|
374
|
+
if (timeslice >= upToTimeslice) {
|
|
375
|
+
this._counters.splice(0, i);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const name = this._counters[i + 1];
|
|
379
|
+
const value = this._counters[i + 2];
|
|
380
|
+
if (!this._aggregatedCounters[timeslice]) {
|
|
381
|
+
this._aggregatedCounters[timeslice] = {};
|
|
382
|
+
this._counterEarliestMeasurementByPeriod[timeslice] = ts;
|
|
383
|
+
}
|
|
384
|
+
if (typeof this._aggregatedCounters[timeslice][name] === 'undefined') {
|
|
385
|
+
this._aggregatedCounters[timeslice][name] = value;
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
this._aggregatedCounters[timeslice][name] += value;
|
|
389
|
+
}
|
|
390
|
+
this._counterLastMeasurementByPeriod[timeslice] = ts;
|
|
391
|
+
}
|
|
392
|
+
this._counters.splice(0, this._counters.length);
|
|
393
|
+
}
|
|
394
|
+
_aggregateRates(upToTimeslice) {
|
|
395
|
+
debug('_aggregateRates to', upToTimeslice, new Date(upToTimeslice));
|
|
396
|
+
const a = {};
|
|
397
|
+
let spliceTo = this._rates.length;
|
|
398
|
+
for (let i = 0; i < this._rates.length; i += 2) {
|
|
399
|
+
const ts = this._rates[i];
|
|
400
|
+
const timeslice = normalizeTs(ts);
|
|
401
|
+
if (timeslice >= upToTimeslice) {
|
|
402
|
+
debug('_aggregateRates early return // i=', i, 'timeslice=', timeslice, new Date(timeslice));
|
|
403
|
+
spliceTo = i;
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
const name = this._rates[i + 1];
|
|
407
|
+
if (!a[timeslice]) {
|
|
408
|
+
a[timeslice] = {};
|
|
409
|
+
}
|
|
410
|
+
if (!a[timeslice][name]) {
|
|
411
|
+
a[timeslice][name] = {
|
|
412
|
+
first: Number.POSITIVE_INFINITY,
|
|
413
|
+
last: 0,
|
|
414
|
+
count: 0
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
a[timeslice][name].first = Math.min(a[timeslice][name].first, ts);
|
|
418
|
+
a[timeslice][name].last = Math.max(a[timeslice][name].last, ts);
|
|
419
|
+
a[timeslice][name].count++;
|
|
420
|
+
}
|
|
421
|
+
for (const [ts, rs] of Object.entries(a)) {
|
|
422
|
+
for (const [name, _] of Object.entries(rs)) {
|
|
423
|
+
const { first, last, count } = a[ts][name];
|
|
424
|
+
if (!this._aggregatedRates[ts]) {
|
|
425
|
+
this._aggregatedRates[ts] = {};
|
|
426
|
+
}
|
|
427
|
+
this._aggregatedRates[ts][name] = round(count / (Math.max(last - first, 1000) / 1000), 0);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
this._rates.splice(0, spliceTo);
|
|
431
|
+
}
|
|
432
|
+
aggregate(forceAll) {
|
|
433
|
+
const currentTimeslice = normalizeTs(Date.now()) + (forceAll ? 30 * 1000 : 0);
|
|
434
|
+
this._aggregateCounters(currentTimeslice);
|
|
435
|
+
this._aggregateHistograms(currentTimeslice);
|
|
436
|
+
this._aggregateRates(currentTimeslice);
|
|
437
|
+
if (forceAll) {
|
|
438
|
+
this._emitPeriods();
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
this._maybeEmitMostRecentPeriod();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
_emitPeriods() {
|
|
445
|
+
const bucketIds = this.getBucketIds();
|
|
446
|
+
const lastPeriod = parseInt(this._lastPeriod, 10);
|
|
447
|
+
for (let i = 0; i < bucketIds.length; i++) {
|
|
448
|
+
const period = bucketIds[i];
|
|
449
|
+
if (!this._lastPeriod || parseInt(period, 10) > lastPeriod) {
|
|
450
|
+
this.emit('metricData', period, this.getMetrics(period));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
_maybeEmitMostRecentPeriod() {
|
|
455
|
+
const p = this.getBucketIds()[0];
|
|
456
|
+
if (p && p !== this._lastPeriod) {
|
|
457
|
+
this.emit('metricData', p, this.getMetrics(p)); // Measurements in period p have been aggregated
|
|
458
|
+
this._lastPeriod = p;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function normalizeTs(epochMs, windowSize = 10) {
|
|
463
|
+
// Reset down to minute
|
|
464
|
+
const m = Math.floor((epochMs - (epochMs % 1000)) / 1000 / 60) * 60 * 1000;
|
|
465
|
+
// Number of seconds past the minute
|
|
466
|
+
const s = ((epochMs - (epochMs % 1000)) / 1000) % 60;
|
|
467
|
+
// Number of seconds to take off
|
|
468
|
+
const d = s % windowSize;
|
|
469
|
+
return m + (s - d) * 1000;
|
|
470
|
+
}
|
|
471
|
+
// Function hms(epochMs) {
|
|
472
|
+
// return [
|
|
473
|
+
// Math.round((epochMs / 1000 / 60 / 60) % 24),
|
|
474
|
+
// Math.round((epochMs / 1000 / 60) % 60),
|
|
475
|
+
// Math.round(epochMs / 1000) % 60
|
|
476
|
+
// ];
|
|
477
|
+
// }
|
|
478
|
+
function round(number, decimals) {
|
|
479
|
+
const m = 10 ** decimals;
|
|
480
|
+
return Math.round(number * m) / m;
|
|
481
|
+
}
|
|
482
|
+
// h is an instance of DDSketch
|
|
483
|
+
function summarizeHistogram(h) {
|
|
484
|
+
return {
|
|
485
|
+
min: round(h.min, 1),
|
|
486
|
+
max: round(h.max, 1),
|
|
487
|
+
count: h.count,
|
|
488
|
+
mean: round(h.sum / h.count, 1),
|
|
489
|
+
p50: round(h.getValueAtQuantile(0.5), 1),
|
|
490
|
+
median: round(h.getValueAtQuantile(0.5), 1), // Here for compatibility
|
|
491
|
+
p75: round(h.getValueAtQuantile(0.75), 1),
|
|
492
|
+
p90: round(h.getValueAtQuantile(0.9), 1),
|
|
493
|
+
p95: round(h.getValueAtQuantile(0.95), 1),
|
|
494
|
+
p99: round(h.getValueAtQuantile(0.99), 1),
|
|
495
|
+
p999: round(h.getValueAtQuantile(0.999), 1)
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
/// ///////////////////////////////////////////
|
|
499
|
+
function stringify(value, space) {
|
|
500
|
+
return JSON.stringify(value, replacer, space);
|
|
501
|
+
}
|
|
502
|
+
function parse(text) {
|
|
503
|
+
return JSON.parse(text, reviver);
|
|
504
|
+
}
|
|
505
|
+
function replacer(_key, value) {
|
|
506
|
+
if (isBufferLike(value) && isArray(value.data)) {
|
|
507
|
+
if (value.data.length > 0) {
|
|
508
|
+
value.data = `base64:${Buffer.from(value.data).toString('base64')}`;
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
value.data = '';
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return value;
|
|
515
|
+
}
|
|
516
|
+
function reviver(_key, value) {
|
|
517
|
+
if (isBufferLike(value)) {
|
|
518
|
+
if (isArray(value.data)) {
|
|
519
|
+
return Buffer.from(value.data);
|
|
520
|
+
}
|
|
521
|
+
if (isString(value.data)) {
|
|
522
|
+
if (value.data.startsWith('base64:')) {
|
|
523
|
+
return Buffer.from(value.data.slice('base64:'.length), 'base64');
|
|
524
|
+
}
|
|
525
|
+
// Assume that the string is UTF-8 encoded (or empty).
|
|
526
|
+
return Buffer.from(value.data);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return value;
|
|
530
|
+
}
|
|
531
|
+
function isBufferLike(x) {
|
|
532
|
+
return (isObject(x) && x.type === 'Buffer' && (isArray(x.data) || isString(x.data)));
|
|
533
|
+
}
|
|
534
|
+
function isArray(x) {
|
|
535
|
+
return Array.isArray(x);
|
|
536
|
+
}
|
|
537
|
+
function isString(x) {
|
|
538
|
+
return typeof x === 'string';
|
|
539
|
+
}
|
|
540
|
+
function isObject(x) {
|
|
541
|
+
return typeof x === 'object' && x !== null;
|
|
542
|
+
}
|
|
543
|
+
/// /////////////////
|
|
544
|
+
// Like Math.min and Math.max but take a list of values, and ignore
|
|
545
|
+
// undefined's rather than returning NaN when a value is undefined.
|
|
546
|
+
// Returns undefined if all arguments are undefined.
|
|
547
|
+
function min(values) {
|
|
548
|
+
const m = Math.min(...values.filter((x) => x));
|
|
549
|
+
return m === Number.POSITIVE_INFINITY ? undefined : m;
|
|
550
|
+
}
|
|
551
|
+
function max(values) {
|
|
552
|
+
const m = Math.max(...values.filter((x) => x));
|
|
553
|
+
return m === Number.NEGATIVE_INFINITY ? undefined : m;
|
|
554
|
+
}
|
|
555
|
+
export { SSMS, summarizeHistogram, normalizeTs };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { engine_util } from '@artilleryio/int-commons';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { SSMS } from "./ssms.js";
|
|
5
|
+
// NOTE: This may be called more than once, and so should be non-destructive
|
|
6
|
+
async function updateGlobalObject(opts = {}) {
|
|
7
|
+
global.artillery = global.artillery || {};
|
|
8
|
+
global.artillery.runtimeOptions = global.artillery.runtimeOptions || {};
|
|
9
|
+
global.artillery.runtimeOptions.extendedHTTPMetrics =
|
|
10
|
+
typeof process.env.ARTILLERY_EXTENDED_HTTP_METRICS !== 'undefined';
|
|
11
|
+
global.artillery.metrics = global.artillery.metrics || {};
|
|
12
|
+
global.artillery.metrics.event = async (msg, opts) => {
|
|
13
|
+
if (opts.level === 'error') {
|
|
14
|
+
console.log(chalk.red(msg));
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.log(msg);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
global.artillery.util = global.artillery.util || {};
|
|
21
|
+
global.artillery.util.template = engine_util.template;
|
|
22
|
+
global.artillery.plugins = global.artillery.plugins || [];
|
|
23
|
+
global.artillery.extensionEvents = global.artillery.extensionEvents || [];
|
|
24
|
+
global.artillery.ext =
|
|
25
|
+
global.artillery.ext ||
|
|
26
|
+
async function (event) {
|
|
27
|
+
// TODO: Validate events object
|
|
28
|
+
this.extensionEvents.push(event);
|
|
29
|
+
};
|
|
30
|
+
if (!Object.hasOwn(global.artillery, 'globalEvents')) {
|
|
31
|
+
Object.defineProperty(global.artillery, 'globalEvents', {
|
|
32
|
+
value: new EventEmitter()
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
global.artillery.__SSMS = SSMS;
|
|
36
|
+
if (!Object.hasOwn(global.artillery, 'suggestedExitCode')) {
|
|
37
|
+
Object.defineProperty(global.artillery, 'suggestedExitCode', {
|
|
38
|
+
get() {
|
|
39
|
+
return global.artillery._exitCode;
|
|
40
|
+
},
|
|
41
|
+
set(code) {
|
|
42
|
+
global.artillery._exitCode = code;
|
|
43
|
+
if (typeof global.artillery._workerThreadSend === 'function') {
|
|
44
|
+
global.artillery._workerThreadSend({
|
|
45
|
+
event: 'setSuggestedExitCode',
|
|
46
|
+
code: code
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
global.artillery.logger =
|
|
53
|
+
global.artillery.logger ||
|
|
54
|
+
((opts) => ({
|
|
55
|
+
log: (...args) => {
|
|
56
|
+
global.artillery.globalEvents.emit('log', opts, ...args);
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
global.artillery.log =
|
|
60
|
+
global.artillery.log ||
|
|
61
|
+
((...args) => {
|
|
62
|
+
global.artillery.globalEvents.emit('log', {}, ...args);
|
|
63
|
+
});
|
|
64
|
+
if (opts.version) {
|
|
65
|
+
global.artillery.version = opts.version;
|
|
66
|
+
}
|
|
67
|
+
if (opts.telemetry) {
|
|
68
|
+
global.artillery.telemetry = opts.telemetry;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function main() {
|
|
72
|
+
await updateGlobalObject();
|
|
73
|
+
}
|
|
74
|
+
main();
|
|
75
|
+
export { updateGlobalObject };
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import l from 'lodash';
|
|
6
|
+
export default create;
|
|
7
|
+
// naive implementation of selection with replacement
|
|
8
|
+
function create(list) {
|
|
9
|
+
const dist = l.reduce(list, (acc, el, i) => {
|
|
10
|
+
for (let j = 0; j < el.weight * 100; j++) {
|
|
11
|
+
acc.push(i);
|
|
12
|
+
}
|
|
13
|
+
return acc;
|
|
14
|
+
}, []);
|
|
15
|
+
return () => {
|
|
16
|
+
const i = dist[l.random(0, dist.length - 1)];
|
|
17
|
+
return [i, list[i]];
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function bench() {
|
|
21
|
+
const items = [
|
|
22
|
+
{ weight: 0, value: 'zero' },
|
|
23
|
+
{ weight: 1, value: 'a' },
|
|
24
|
+
{ weight: 2, value: 'b' },
|
|
25
|
+
{ weight: 3, value: 'c' },
|
|
26
|
+
{ weight: 4, value: 'd' },
|
|
27
|
+
{ weight: 5, value: 'e' },
|
|
28
|
+
{ weight: 1, value: 'f' },
|
|
29
|
+
{ weight: 2, value: 'g' },
|
|
30
|
+
{ weight: 3, value: 'h' },
|
|
31
|
+
{ weight: 4, value: 'i' },
|
|
32
|
+
{ weight: 5, value: 'j' },
|
|
33
|
+
{ weight: 10, value: 'k' }
|
|
34
|
+
];
|
|
35
|
+
const picker = create(items);
|
|
36
|
+
const ITERS = 10 ** 6;
|
|
37
|
+
const startedAt = Date.now();
|
|
38
|
+
const picks = l.map(l.range(ITERS), () => {
|
|
39
|
+
const x = picker()[1];
|
|
40
|
+
return x;
|
|
41
|
+
});
|
|
42
|
+
const delta = Date.now() - startedAt;
|
|
43
|
+
console.log('ITERS = %s\ndelta = %s\nITERS/delta = %s', ITERS, delta, Math.round(ITERS / delta));
|
|
44
|
+
const sumWeights = l.reduce(items, (acc, item) => acc + item.weight, 0);
|
|
45
|
+
console.log('sumWeights = %s', sumWeights);
|
|
46
|
+
l.each(items, (p) => {
|
|
47
|
+
const count = l.filter(picks, { value: p.value }).length;
|
|
48
|
+
console.log('Count of %s = %s (should be: %s%, is: %s%)', p.value, count, Math.round((p.weight / sumWeights) * 1000) / 1000, Math.round((count / ITERS) * 1000) / 1000);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (process.argv[1] &&
|
|
52
|
+
import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
53
|
+
bench();
|
|
54
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// IMPORTANT: this file must stay free of TypeScript-only syntax
|
|
2
|
+
// (annotations, casts). It is the package entry that CommonJS
|
|
3
|
+
// consumers reach via require(); require-extension hooks (e.g.
|
|
4
|
+
// pirates via @tapjs/processinfo) bypass Node's native type
|
|
5
|
+
// stripping for directly-required files. Erasure-identical source
|
|
6
|
+
// keeps the entry loadable everywhere; everything imported from it
|
|
7
|
+
// loads through the ESM loader, where stripping always applies.
|
|
8
|
+
|
|
9
|
+
export { default as engine_http } from './lib/engine_http.ts';
|
|
10
|
+
export { default as isIdlePhase } from './lib/is-idle-phase.ts';
|
|
11
|
+
export * as runner from './lib/runner.ts';
|
|
12
|
+
export * as ssms from './lib/ssms.ts';
|
|
13
|
+
// Side effect: sets up the global artillery object on load
|
|
14
|
+
export { updateGlobalObject } from './lib/update-global-object.ts';
|