@artilleryio/int-core 2.27.0 → 2.28.0-670d2fa
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
|
@@ -0,0 +1,213 @@
|
|
|
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 { randomUUID } from 'node:crypto';
|
|
5
|
+
import arrivals from 'arrivals';
|
|
6
|
+
import async from 'async';
|
|
7
|
+
import createDebug from 'debug';
|
|
8
|
+
import driftless from 'driftless';
|
|
9
|
+
import { EventEmitter } from 'eventemitter3';
|
|
10
|
+
import _ from 'lodash';
|
|
11
|
+
import ms from 'ms';
|
|
12
|
+
const debug = createDebug('phases');
|
|
13
|
+
const isUndefined = _.isUndefined;
|
|
14
|
+
export default phaser;
|
|
15
|
+
async function sleep(ms) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
setTimeout(resolve, ms);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function phaser(phaseSpecs) {
|
|
21
|
+
const ee = new EventEmitter();
|
|
22
|
+
const tasks = _.map(phaseSpecs, (spec, i) => {
|
|
23
|
+
[
|
|
24
|
+
'arrivalRate',
|
|
25
|
+
'arrivalCount',
|
|
26
|
+
'pause',
|
|
27
|
+
'rampTo',
|
|
28
|
+
'duration',
|
|
29
|
+
'maxVusers'
|
|
30
|
+
].forEach((k) => {
|
|
31
|
+
if (isUndefined(spec[k]) || spec[k] === 'number') {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (k === 'duration' || k === 'pause') {
|
|
35
|
+
//if it's already a number in string format, don't apply ms, as it's the default behaviour, so we don't want to do ms calculations
|
|
36
|
+
//otherwise, ms returns the value in milliseconds, so we need to convert to seconds
|
|
37
|
+
const convertedDuration = Number.isInteger(_.toNumber(spec[k]))
|
|
38
|
+
? spec[k]
|
|
39
|
+
: ms(spec[k]) / 1000;
|
|
40
|
+
//throw error if invalid time format to prevent test from running infinitely
|
|
41
|
+
if (!convertedDuration) {
|
|
42
|
+
throw new Error(`Invalid ${k} for phase: ${spec[k]}`);
|
|
43
|
+
}
|
|
44
|
+
spec[k] = convertedDuration;
|
|
45
|
+
}
|
|
46
|
+
// Cast defined but non-number (eg: from ENV) values
|
|
47
|
+
spec[k] = _.toNumber(spec[k]);
|
|
48
|
+
});
|
|
49
|
+
if (isUndefined(spec.index)) {
|
|
50
|
+
spec.index = i;
|
|
51
|
+
}
|
|
52
|
+
if (!isUndefined(spec.arrivalRate) && isUndefined(spec.rampTo)) {
|
|
53
|
+
spec.mode = spec.mode || 'uniform';
|
|
54
|
+
}
|
|
55
|
+
if (!isUndefined(spec.pause)) {
|
|
56
|
+
return createPause(spec, ee);
|
|
57
|
+
}
|
|
58
|
+
if (!isUndefined(spec.arrivalCount)) {
|
|
59
|
+
return createArrivalCount(spec, ee);
|
|
60
|
+
}
|
|
61
|
+
if (!isUndefined(spec.arrivalRate)) {
|
|
62
|
+
// If arrivalRate is zero and it's not a ramp, it's the same as a pause:
|
|
63
|
+
if (spec.arrivalRate === 0 && isUndefined(spec.rampTo)) {
|
|
64
|
+
return createPause(Object.assign(spec, { pause: spec.duration }), ee);
|
|
65
|
+
}
|
|
66
|
+
// If it's a ramp, create that:
|
|
67
|
+
if (!isUndefined(spec.rampTo)) {
|
|
68
|
+
return createRamp(spec, ee);
|
|
69
|
+
}
|
|
70
|
+
// Otherwise it's a plain arrival phase:
|
|
71
|
+
return createArrivalRate(spec, ee);
|
|
72
|
+
}
|
|
73
|
+
console.log('Unknown phase spec\n%j\nThis should not happen', spec);
|
|
74
|
+
});
|
|
75
|
+
ee.run = () => {
|
|
76
|
+
async.series(tasks, (err) => {
|
|
77
|
+
if (err) {
|
|
78
|
+
debug(err);
|
|
79
|
+
}
|
|
80
|
+
ee.emit('done');
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
return ee;
|
|
84
|
+
}
|
|
85
|
+
function createPause(spec, ee) {
|
|
86
|
+
const duration = spec.pause * 1000;
|
|
87
|
+
const task = (callback) => {
|
|
88
|
+
spec.startTime = Date.now();
|
|
89
|
+
spec.id = randomUUID();
|
|
90
|
+
ee.emit('phaseStarted', spec);
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
spec.endTime = Date.now();
|
|
93
|
+
ee.emit('phaseCompleted', spec);
|
|
94
|
+
return callback(null);
|
|
95
|
+
}, duration);
|
|
96
|
+
};
|
|
97
|
+
return task;
|
|
98
|
+
}
|
|
99
|
+
function createRamp(spec, ee) {
|
|
100
|
+
const duration = spec.duration || 1;
|
|
101
|
+
const arrivalRate = spec.arrivalRate;
|
|
102
|
+
const rampTo = spec.rampTo;
|
|
103
|
+
const worker = spec.worker;
|
|
104
|
+
const totalWorkers = spec.totalWorkers;
|
|
105
|
+
const difference = rampTo - arrivalRate;
|
|
106
|
+
const periods = duration;
|
|
107
|
+
debug(`worker ${worker} totalWorkers ${totalWorkers} arrivalRate ${arrivalRate} rampTo ${rampTo} difference ${difference} periods ${periods}`);
|
|
108
|
+
const periodArrivals = [];
|
|
109
|
+
const periodTick = [];
|
|
110
|
+
// if there is only one peridod we generate mean arrivals
|
|
111
|
+
if (periods === 1) {
|
|
112
|
+
const rawPeriodArrivals = (rampTo + arrivalRate) / 2;
|
|
113
|
+
periodArrivals[0] = adjustArrivalsByWorker(rawPeriodArrivals, totalWorkers, worker);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// for each period we calculate the corresponding arrivals:
|
|
117
|
+
// knowing that arrivals(0) = arrivalRate and arrivals(duration -1) = rampTo
|
|
118
|
+
// then: arrivals(t) = difference / (duration-1) * t + arrivalRate
|
|
119
|
+
for (let i = 0; i < periods; i++) {
|
|
120
|
+
const rawPeriodArrivals = (difference / (duration - 1)) * i + arrivalRate;
|
|
121
|
+
// take into account added decimals and bump worker arrivals if needed
|
|
122
|
+
periodArrivals[i] = adjustArrivalsByWorker(rawPeriodArrivals, totalWorkers, worker);
|
|
123
|
+
// Needed ticks to get to periodArrivals in 1000ms
|
|
124
|
+
periodTick[i] = Math.min(Math.floor(1000 / periodArrivals[i]), 1000);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
debug(`periodArrivals ${periodArrivals}`);
|
|
128
|
+
debug(`periodTick ${periodTick}`);
|
|
129
|
+
return async function rampTask(_callback) {
|
|
130
|
+
spec.startTime = Date.now();
|
|
131
|
+
spec.id = randomUUID();
|
|
132
|
+
ee.emit('phaseStarted', spec);
|
|
133
|
+
for (let period = 0; period < periods; period++) {
|
|
134
|
+
ticker(period);
|
|
135
|
+
await sleep(1000);
|
|
136
|
+
}
|
|
137
|
+
spec.endTime = Date.now();
|
|
138
|
+
ee.emit('phaseCompleted', spec);
|
|
139
|
+
};
|
|
140
|
+
function adjustArrivalsByWorker(rawPeriodArrivals, totalWorkers, worker) {
|
|
141
|
+
// We use the floor of the expected arrivals, then we add up all decimal digits
|
|
142
|
+
// and evaluate if one or more workers should bump their arrivalRate.
|
|
143
|
+
let arrivals = Math.floor(rawPeriodArrivals);
|
|
144
|
+
// Think of fractionalPart * workers as the amount of arrivals that could not be
|
|
145
|
+
// shared evenly across all workers.
|
|
146
|
+
if (Math.round((rawPeriodArrivals % 1) * totalWorkers) >= worker) {
|
|
147
|
+
arrivals = arrivals + 1;
|
|
148
|
+
}
|
|
149
|
+
return arrivals;
|
|
150
|
+
}
|
|
151
|
+
function ticker(currentPeriod) {
|
|
152
|
+
// ensure we don't go past 1s
|
|
153
|
+
const delay = Math.min(periodTick[currentPeriod], 1000);
|
|
154
|
+
let currentArrivals = 0;
|
|
155
|
+
const arrivalTimer = driftless.setDriftlessInterval(function arrivals() {
|
|
156
|
+
if (currentArrivals < periodArrivals[currentPeriod]) {
|
|
157
|
+
ee.emit('arrival', spec);
|
|
158
|
+
currentArrivals++;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
currentPeriod++;
|
|
162
|
+
driftless.clearDriftless(arrivalTimer);
|
|
163
|
+
}
|
|
164
|
+
}, delay);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function createArrivalCount(spec, ee) {
|
|
169
|
+
const task = (callback) => {
|
|
170
|
+
spec.startTime = Date.now();
|
|
171
|
+
spec.id = randomUUID();
|
|
172
|
+
ee.emit('phaseStarted', spec);
|
|
173
|
+
const duration = spec.duration * 1000;
|
|
174
|
+
if (spec.arrivalCount > 0) {
|
|
175
|
+
const interval = duration / spec.arrivalCount;
|
|
176
|
+
const p = arrivals.uniform.process(interval, duration);
|
|
177
|
+
p.on('arrival', () => {
|
|
178
|
+
ee.emit('arrival', spec);
|
|
179
|
+
});
|
|
180
|
+
p.on('finished', () => {
|
|
181
|
+
spec.endTime = Date.now();
|
|
182
|
+
ee.emit('phaseCompleted', spec);
|
|
183
|
+
return callback(null);
|
|
184
|
+
});
|
|
185
|
+
p.start();
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
return callback(null);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
return task;
|
|
192
|
+
}
|
|
193
|
+
function createArrivalRate(spec, ee) {
|
|
194
|
+
const task = (callback) => {
|
|
195
|
+
spec.startTime = Date.now();
|
|
196
|
+
spec.id = randomUUID();
|
|
197
|
+
ee.emit('phaseStarted', spec);
|
|
198
|
+
const ar = 1000 / spec.arrivalRate;
|
|
199
|
+
const duration = spec.duration * 1000;
|
|
200
|
+
debug('creating a %s process for arrivalRate', spec.mode);
|
|
201
|
+
const p = arrivals[spec.mode].process(ar, duration);
|
|
202
|
+
p.on('arrival', () => {
|
|
203
|
+
ee.emit('arrival', spec);
|
|
204
|
+
});
|
|
205
|
+
p.on('finished', () => {
|
|
206
|
+
spec.endTime = Date.now();
|
|
207
|
+
ee.emit('phaseCompleted', spec);
|
|
208
|
+
return callback(null);
|
|
209
|
+
});
|
|
210
|
+
p.start();
|
|
211
|
+
};
|
|
212
|
+
return task;
|
|
213
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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 _ from 'lodash';
|
|
5
|
+
export default function createReader(order, spec) {
|
|
6
|
+
if (order === 'sequence') {
|
|
7
|
+
return createSequencedReader();
|
|
8
|
+
}
|
|
9
|
+
else if (typeof order === 'undefined' &&
|
|
10
|
+
typeof spec?.name !== 'undefined' &&
|
|
11
|
+
spec?.loadAll === true) {
|
|
12
|
+
return createEverythingReader(spec);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
// random
|
|
16
|
+
return createRandomReader();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function createSequencedReader() {
|
|
20
|
+
let i = 0;
|
|
21
|
+
return (data) => {
|
|
22
|
+
const result = data[i];
|
|
23
|
+
if (i < data.length - 1) {
|
|
24
|
+
i++;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
i = 0;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function createEverythingReader(spec) {
|
|
33
|
+
let parsedData;
|
|
34
|
+
return (data) => {
|
|
35
|
+
if (!parsedData) {
|
|
36
|
+
parsedData = [];
|
|
37
|
+
// Parse the row into an object based on the fields spec
|
|
38
|
+
if (spec.fields?.length > 0) {
|
|
39
|
+
for (const row of data) {
|
|
40
|
+
const o = {};
|
|
41
|
+
for (let i = 0; i < spec.fields.length; i++) {
|
|
42
|
+
const fieldName = spec.fields[i];
|
|
43
|
+
o[fieldName] = row[i];
|
|
44
|
+
}
|
|
45
|
+
parsedData.push(o);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Otherwise just return the array of rows
|
|
50
|
+
parsedData = data;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return parsedData;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function createRandomReader() {
|
|
57
|
+
return (data) => data[Math.max(0, _.random(0, data.length - 1))];
|
|
58
|
+
}
|