@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.
@@ -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';