-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplanning.js
More file actions
600 lines (590 loc) · 27.8 KB
/
planning.js
File metadata and controls
600 lines (590 loc) · 27.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
/* eslint-disable no-underscore-dangle */
const modes = require('./modes.js')
const planning = {
// ###############
// ## DURATIONS ##
// ###############
/**
* Obtain the duration of a phase in seconds.
* @param phase The phase to obtain duration from.
* @returns {number} The duration of the given phase in seconds. If a duration cannot be obtained -1 is returned.
*/
phaseDurationInSeconds: (phase) => {
if ('duration' in phase) {
return phase.duration
} else if ('pause' in phase) {
return phase.pause
} else {
return -1
}
},
/**
* Calculate the total duration of the Artillery script in seconds. If a phase does not have a valid duration,
* the index of that phase, multiplied by -1, will be returned. This way a result less than zero result can
* easily be differentiated from a valid duration and the offending phase can be identified.
* @param script The script to identify a total duration for.
* @returns {number} The total duration in seconds for the given script. If any phases do not contain a valid duration,
* the index of the first phase without a valid duration will be returned, multiplied by -1.
*/
scriptDurationInSeconds: (script) => {
let ret = 0
let i
let phaseDurationInSeconds
for (i = 0; i < script.config.phases.length; i++) {
phaseDurationInSeconds = planning.phaseDurationInSeconds(script.config.phases[i])
if (phaseDurationInSeconds < 0) {
ret = -1 * i
break
} else {
ret += phaseDurationInSeconds
}
}
return ret
},
/**
* Split the given phase along the time dimension so that the resulting chunk is no longer than the given chunkSize
* @param phase The phase to split so that the produced chunk is no more than chunkSize seconds long
* @param chunkSize The duration, in seconds, that the chunk removed from the phase should be no longer than
* @returns {{chunk, remainder: *}} The chunk that is chunkSize duration and the remainder of the phase.
*/
splitPhaseByDurationInSeconds: (phase, chunkSize) => {
const ret = {
chunk: JSON.parse(JSON.stringify(phase)),
remainder: phase,
}
let diff
let ratio
if ('duration' in phase) {
// split the ramp - the rampTo for the chunk and the arrival rate for the remainder are changed.
if ('rampTo' in phase && 'arrivalRate' in phase) {
// if arrivalRate < rampTo (ramping down) this will be negative, that's okay
diff = phase.rampTo - phase.arrivalRate
ratio = chunkSize / phase.duration
// TODO should we round? Potentially, this could lead to non-smooth ramps
ret.chunk.rampTo = Math.round(ret.chunk.arrivalRate + (diff * ratio))
ret.remainder.arrivalRate = ret.chunk.rampTo
} else if ('arrivalCount' in phase) { // split the arrival count proportionally
ratio = chunkSize / phase.duration
// TODO should we round? Potentially, this could lead to non-constant arrivals
ret.chunk.arrivalCount = Math.round(ret.chunk.arrivalCount * ratio)
ret.remainder.arrivalCount -= ret.chunk.arrivalCount
}
// nothing to do in the 'arrivalRate' ONLY case, since arrivalRate doesn't change based on time reduction
ret.chunk.duration = chunkSize
ret.remainder.duration -= chunkSize
} else if ('pause' in phase) {
ret.chunk.pause = chunkSize
ret.remainder.pause -= chunkSize
}
return ret
},
/**
* Split the given script along the time dimension so that the resulting chunk is no longer than the given chunkSize
* @param script The script to split so that the produced chunk is no more than chunkSize seconds long
* @param chunkSize The duration, in seconds, that the chunk removed from the script should be no longer than
* @returns {{chunk, remainder: *}} The chunk that is chunkSize duration and the remainder of the script.
*/
splitScriptByDurationInSeconds: (script, chunkSize) => {
const ret = {
chunk: JSON.parse(JSON.stringify(script)),
remainder: script,
}
let remainingDurationInSeconds = chunkSize
let phase
let phaseDurationInSeconds
let phaseParts
ret.chunk.config.phases = []
if (ret.remainder._start) {
delete ret.remainder._start
}
while (remainingDurationInSeconds > 0 && ret.remainder.config.phases.length) {
phase = ret.remainder.config.phases.shift()
phaseDurationInSeconds = planning.phaseDurationInSeconds(phase)
if (phaseDurationInSeconds > remainingDurationInSeconds) { // split phase
phaseParts = planning.splitPhaseByDurationInSeconds(phase, remainingDurationInSeconds)
ret.chunk.config.phases.push(phaseParts.chunk)
ret.remainder.config.phases.unshift(phaseParts.remainder)
remainingDurationInSeconds = 0
} else {
ret.chunk.config.phases.push(phase)
remainingDurationInSeconds -= phaseDurationInSeconds
}
}
return ret
},
// ##############
// ## REQUESTS ##
// ##############
/**
* Obtain the specified requests per second of a phase.
* @param phase The phase to obtain specified requests per second from.
* @returns The specified requests per second of a phase. If a valid specification is not available, -1 is returned.
*/
phaseRequestsPerSecond: (phase) => {
if ('rampTo' in phase && 'arrivalRate' in phase) {
return Math.max(phase.arrivalRate, phase.rampTo)
} else if ('arrivalRate' in phase) {
return phase.arrivalRate
} else if ('arrivalCount' in phase && 'duration' in phase) {
return phase.arrivalCount / phase.duration
} else if ('pause' in phase) {
return 0
} else {
return -1
}
},
/**
* Calculate the maximum requests per second specified by a script. If a phase does not have a valid requests per second,
* the index of that phase, multiplied by -1, will be returned. This way a result less than zero result can easily be
* differentiated from a valid requests per second and the offending phase can be identified.
*
* @param script The script to identify a maximum requests per second for.
* @returns {number} The requests per second specified by the script or -i if an invalid phase is encountered,
* where i is the zero based index of the phase containing an invalid requests per second value.
*/
scriptRequestsPerSecond: (script) => {
/*
* See https://artillery.io/docs/script_reference.html#phases for phase types.
*
* The following was obtained 07/26/2016:
* arrivalRate - specify the arrival rate of virtual users for a duration of time. - A linear “ramp” in arrival
* can be also be created with the rampTo option. // max(arrivalRate, rampTo) RPS
* arrivalCount - specify the number of users to create over a period of time. // arrivalCount/duration RPS
* pause - pause and do nothing for a duration of time. // zero RPS
*/
let ret = 0
let i
let phaseRps
for (i = 0; i < script.config.phases.length; i++) {
phaseRps = planning.phaseRequestsPerSecond(script.config.phases[i])
if (phaseRps < 0) {
ret = -1 * i
break
} else {
ret = Math.max(ret, phaseRps)
}
}
return ret
},
// Given that we have a flat line, abc and intersection should be reducible to fewer instructions.
// sigh... time constraints.
/**
* Determine the Ax +By = C specification of the strait line that intersects the two given points
* See https://www.topcoder.com/community/data-science/data-science-tutorials/geometry-concepts-line-intersection-and-its-applications/
* @param p1 The first point of the line segment to identify. E.g. { x: 0, y: 0 }
* @param p2 The second point of the line segment to identify. E.g. { x: 2, y: 2 }
* @returns
* {
* {
* A: number,
* B: number,
* C: number
* }
* }
* The line specification (Ax +By = C) running through the two given points. E.g. { A: 2, B: -2, C: 0 }
*/
abc: (p1, p2) => {
const ret = {
A: p2.y - p1.y,
B: p1.x - p2.x,
}
ret.C = (ret.A * p1.x) + (ret.B * p1.y)
return ret
},
/**
* Determine the intersection point of two lines specified by an A, B, C trio
* See https://www.topcoder.com/community/data-science/data-science-tutorials/geometry-concepts-line-intersection-and-its-applications/
* @param l1 The first line to determine the intersection point for. E.g. { A: 2, B: -2, C: 0 }
* @param l2 The second line to determine the intersection point for. E.g. { A: 0, B: -2, C: -2 }
* @returns {{x: number, y: number}} The point of intersection between the two given lines. E.g. {x: 1, y: 1}
*/
intersect: (l1, l2) => {
const ret = {}
const det = (l1.A * l2.B) - (l2.A * l1.B)
if (det === 0) {
throw new Error('Parallel lines never intersect, detect and avoid this case')
} else {
ret.x = Math.round(((l2.B * l1.C) - (l1.B * l2.C)) / det)
ret.y = Math.round(((l1.A * l2.C) - (l2.A * l1.C)) / det)
}
return ret
},
/**
* Determine the intersection of the phase's ramp specification with the chunkSize limit
* @param phase The phase to intersect with the given chunkSize limit
* @param chunkSize The limit to RPS for the given phase
* @returns {{x: number, y: number}} The intersection point of the phase's ramp with the chunkSize limit
*/
intersection: (phase, chunkSize) => {
const ramp = planning.abc({ x: 0, y: phase.arrivalRate }, { x: phase.duration, y: phase.rampTo })
const limit = planning.abc({ x: 0, y: chunkSize }, { x: phase.duration, y: chunkSize })
return planning.intersect(ramp, limit)
},
/**
* Overwrite the given field with the given value in the given phase. If the value is null, then if the attribute
* is defined in the given phase, delete the attribute from the phase.
* @param phase The phase to alter
* @param field The field in the phase to set or delete
* @param value The value to set the field of the phase to have or, if null, to delete
*/
overWrite: (phase, field, value) => {
if (value !== null) {
phase[field] = value // eslint-disable-line no-param-reassign
} else if (field in phase) {
delete phase[field] // eslint-disable-line no-param-reassign
}
},
/**
* Copy the given phase, overwriting it's values with the given values, and then push the result to the given array
* @param arr The array to put the resulting phase copy into
* @param phase The phase to copy
* @param arrivalCount The arrivalCount value to set (see artillery.io)
* @param arrivalRate The arrivalRate value to set (see artillery.io)
* @param rampTo The rampTo value to set (see artillery.io)
* @param duration The duration value to set (see artillery.io)
* @param pause The pause value to set (see artillery.io)
*/
copyOverwritePush: (arr, phase, arrivalCount, arrivalRate, rampTo, duration, pause) => {
const newPhase = JSON.parse(JSON.stringify(phase))
planning.overWrite(newPhase, 'arrivalCount', arrivalCount)
planning.overWrite(newPhase, 'arrivalRate', arrivalRate)
planning.overWrite(newPhase, 'rampTo', rampTo)
planning.overWrite(newPhase, 'duration', duration)
planning.overWrite(newPhase, 'pause', pause)
arr.push(newPhase)
},
/**
* Add an arrivalCount phase that is an altered copy of the given phase to the given phase array
* @param arr The array to add the specified phase to
* @param phase The phase to copy and alter
* @param arrivalCount The arrivalCount of the new phase (see artillery.io)
* @param duration The duration of the new phase (see artillery.io)
*/
addArrivalCount: (arr, phase, arrivalCount, duration) => {
planning.copyOverwritePush(arr, phase, arrivalCount, null, null, duration, null)
},
/**
* Add an arrivalRate phase that is an altered copy of the given phase to the given phase array
* @param arr The array to add the specified phase to
* @param phase The phase to copy and alter
* @param arrivalRate The arrivalRate of the new phase (see artillery.io)
* @param duration The duration of the new phase (see artillery.io)
*/
addArrivalRate: (arr, phase, arrivalRate, duration) => {
planning.copyOverwritePush(arr, phase, null, arrivalRate, null, duration, null)
},
/**
* Add an arrivalRate phase that is an altered copy of the given phase to the given phase array
* @param arr The array to add the specified phase to
* @param phase The phase to copy and alter
* @param arrivalRate The arrivalRate of the new phase (see artillery.io)
* @param rampTo The rampTo of the new phase (see artillery.io)
* @param duration The duration of the new phase (see artillery.io)
*/
addRamp: (arr, phase, arrivalRate, rampTo, duration) => {
planning.copyOverwritePush(arr, phase, null, arrivalRate > 0 ? arrivalRate : 1, rampTo, duration, null)
},
/**
* Add an arrivalRate phase that is an altered copy of the given phase to the given phase array
* @param arr The array to add the specified phase to
* @param phase The phase to copy and alter
* @param pause The pause of the new phase (see artillery.io)
*/
addPause: (arr, phase, pause) => {
planning.copyOverwritePush(arr, phase, null, null, null, null, pause)
},
/**
* Split the requests per second of a phase to be no more than the given chunkSize.
* @param phase The phase to split
* @param chunkSize The maximum number of requests per second to allow in the chunked off phase
* @returns {*} {{chunk, remainder: *}} The chunk that is at most chunkSize requests per second and the
* remainder of the pahse.
*/
splitPhaseByRequestsPerSecond: (phase, chunkSize) => {
const ret = {
chunk: [],
remainder: [],
}
let max
let min
let intersection
let rps
let arrivalCount
if ('rampTo' in phase && 'arrivalRate' in phase && phase.rampTo === phase.arrivalRate) {
// no actual ramp... :P Still, be nice and tolerate this for users
delete phase.rampTo // eslint-disable-line no-param-reassign
}
if ('rampTo' in phase && 'arrivalRate' in phase) { // ramp phase
max = Math.max(phase.arrivalRate, phase.rampTo)
min = Math.min(phase.arrivalRate, phase.rampTo)
if (max <= chunkSize) {
// the highest portion of the ramp does not exceed the chunkSize, consume the phase and create a pause remainder
planning.addRamp(ret.chunk, phase, phase.arrivalRate, phase.rampTo, phase.duration)
planning.addPause(ret.remainder, phase, phase.duration)
} else if (min >= chunkSize) {
// the least portion of the ramp exceeds chunkSize, produce a constant arrival and reduce the ramp by chunkSize
planning.addArrivalRate(ret.chunk, phase, chunkSize, phase.duration)
planning.addRamp(ret.remainder, phase, phase.arrivalRate - chunkSize, phase.rampTo - chunkSize, phase.duration)
} else {
// otherwise, the chunkSize intersects the phase's request per second trajectory, differentially split across
// the intersection
// Case 1 Case 2 Where
// y2 | | * y1 | * | cs = chunkSize
// | p r | r p d = duration
// cs |- - - *- - - cs |- - - *- - - x = intersection
// | r | a | a | r y1 = arrivalRate
// y1 | * y2 | * y2 = rampTo
// | | | | r = a ramp phase
// 0 _|____________ 0 _|____________ a = an constant arrival phase
// | | p = a pause phase
// 0 x d 0 x d * = a starting, ending, or intermediate RPS
intersection = planning.intersection(phase, chunkSize)
if (phase.arrivalRate < phase.rampTo) {
planning.addRamp(ret.chunk, phase, phase.arrivalRate, chunkSize, intersection.x)
planning.addArrivalRate(ret.chunk, phase, chunkSize, phase.duration - intersection.x)
planning.addPause(ret.remainder, phase, intersection.x)
planning.addRamp(ret.remainder, phase, 1, phase.rampTo - chunkSize, phase.duration - intersection.x)
} else {
planning.addArrivalRate(ret.chunk, phase, chunkSize, intersection.x)
planning.addRamp(ret.chunk, phase, chunkSize, phase.rampTo, phase.duration - intersection.x)
planning.addRamp(ret.remainder, phase, phase.arrivalRate - chunkSize, 1, intersection.x)
planning.addPause(ret.remainder, phase, phase.duration - intersection.x)
}
}
} else if ('arrivalRate' in phase) { // constant rate phase
if (phase.arrivalRate > chunkSize) { // subtract the chunkSize if greater than that
planning.addArrivalRate(ret.chunk, phase, chunkSize, phase.duration)
planning.addArrivalRate(ret.remainder, phase, phase.arrivalRate - chunkSize, phase.duration)
} else { // Otherwise, include the entire arrival and create a pause for the remainder
planning.addArrivalRate(ret.chunk, phase, phase.arrivalRate, phase.duration)
planning.addPause(ret.remainder, phase, phase.duration)
}
} else if ('arrivalCount' in phase && 'duration' in phase) {
// constant rate stated as total scenarios delivered over a duration
rps = phase.arrivalCount / phase.duration
if (rps >= chunkSize) {
arrivalCount = Math.floor(chunkSize * phase.duration)
planning.addArrivalCount(ret.chunk, phase, arrivalCount, phase.duration)
planning.addArrivalCount(ret.remainder, phase, phase.arrivalCount - arrivalCount, phase.duration)
} else {
planning.addArrivalCount(ret.chunk, phase, phase.arrivalCount, phase.duration)
planning.addPause(ret.remainder, phase, phase.duration)
}
} else if ('pause' in phase) {
planning.addPause(ret.chunk, phase, phase.pause)
planning.addPause(ret.remainder, phase, phase.pause)
}
return ret
},
/**
* Split the given script in to a chunk of the given maximum size in requests per second and a remainder. This is
* usually done because the script specifies too much load to produce from a single function. Do this by chunking
* off the maximum RPS a single function can handle.
* @param script The script to split off a chunk with the given maximum requests per second
* @param chunkSize The maximum requests per second of any phase
* @returns {{chunk, remainder}} The Lambda-sized chunk that was removed from the script and the remaining
* script to execute
*/
splitScriptByRequestsPerSecond: (script, chunkSize) => {
const ret = {
chunk: JSON.parse(JSON.stringify(script)),
remainder: JSON.parse(JSON.stringify(script)),
}
let phaseParts
let i
let j
ret.chunk.config.phases = []
ret.remainder.config.phases = []
for (i = 0; i < script.config.phases.length; i++) {
phaseParts = planning.splitPhaseByRequestsPerSecond(script.config.phases[i], chunkSize)
for (j = 0; j < phaseParts.chunk.length; j++) {
ret.chunk.config.phases.push(phaseParts.chunk[j])
}
for (j = 0; j < phaseParts.remainder.length; j++) {
ret.remainder.config.phases.push(phaseParts.remainder[j])
}
}
return ret
},
/**
* Split the given event by duration using the given settings
* @param timeNow The time identity to use in logging activity
* @param event The script to split and schedule the chunks of
* @param settings The settings to use for splitting the script
* @returns {*|{chunk, remainder: *}}
*/
splitScriptByDurationInSecondsAndSchedule: (timeNow, event, settings) => {
const script = event
// SPLIT
if (script._trace) {
console.log(`splitting script by duration from ${script._genesis} in ${timeNow} @ ${Date.now()}`)
}
const parts = planning.splitScriptByDurationInSeconds(script, settings.maxChunkDurationInSeconds)
// SCHEDULE
if (!parts.chunk._start) {
parts.chunk._start = timeNow + settings.timeBufferInMilliseconds
}
if (script._trace) {
console.log(`scheduling immediate chunk start from ${script._genesis} in ${timeNow} for execution @ ${parts.chunk._start}`)
}
// kick the reminder off timeBufferInMilliseconds ms before the end of the chunk's completion
parts.remainder._start = parts.chunk._start + (settings.maxChunkDurationInSeconds * 1000)
if (script._trace) {
console.log(`scheduling future chunk start from ${script._genesis} in ${timeNow} for execution @ ${parts.remainder._start}`)
}
return parts
},
/**
* Split the given event by requests per second using the given settings
* @param timeNow The time identity to use in logging activity
* @param event The script to split and schedule the chunks of
* @param settings The settings to use for splitting the script
* @returns {ScriptChunk[]} The scripts into which the given event was split
*/
splitScriptByRequestsPerSecondAndSchedule: (timeNow, event, settings) => {
const plan = []
let script = event
let scriptRequestsPerSecond
const initialLength = plan.length
if (script._trace) {
console.log(`splitting immediate chunk by requests per second from ${script._genesis} in ${timeNow} @ ${Date.now()}`)
}
if (!script._start) {
script._start = timeNow + settings.timeBufferInMilliseconds // eslint-disable-line no-param-reassign
}
do {
const parts = planning.splitScriptByRequestsPerSecond(script, settings.maxChunkRequestsPerSecond)
plan.push(parts.chunk)
script = parts.remainder
scriptRequestsPerSecond = planning.scriptRequestsPerSecond(script) // determine whether we need to continue chunking
} while (scriptRequestsPerSecond > 0)
if (script._trace) {
console.log(`immediate chunk split in to ${plan.length - initialLength} chunks, with ${initialLength} future chunk(s) from ${script._genesis} in ${timeNow} @ ${Date.now()}`)
}
return plan
},
// ######################
// ## SERVICE SAMPLING ##
// ######################
/**
* Split the given script into an array of scripts, one for each flow in the given script, each specifying the
* execution of the single contained flow exactly once.
* @param script The script to split. Note that the
* @param settings The settings to use in generating phases for each script
* @returns {Array} An array of scripts that each contain a single flow from the original script and specify its
* execution exactly once.
*/
splitScriptByFlow: (script) => {
let i
let last = 0
const scripts = []
let newScript
const oldScript = JSON.parse(JSON.stringify(script))
oldScript.mode = modes.PERF
delete oldScript.config.phases
for (i = 0; i < oldScript.scenarios.length; i++) { // break each flow into a new script
// there is a non-standard specification in artillery where you can specify a flow as a series of array entries
// that will be composed for you. Something like:
// [
// name: 'foo',
// weight: 1,
// flow: { ... },
// name: 'bar',
// weight: 2,
// flow: { ... }
// ]
// is interpreted as:
// [
// { name: 'foo', weight: 1, flow: { ... } },
// { name: 'bar', weight: 2, flow: { ... } }
// ]
// for completeness, this logic accounts for that valid (though inadvisable) script format
if (oldScript.scenarios[i].flow) {
newScript = JSON.parse(JSON.stringify(oldScript))
newScript.config.phases = planning.generateSamplingPhases(script.sampling) // do this for every script so that they don't act in sync and create harmonic effects
newScript.scenarios = oldScript.scenarios.slice(last, i + 1)
last = i + 1
scripts.push(newScript)
}
}
return scripts
},
/**
* Generate a series of sampling phases with pauses between them according to the given settings
* @param script The script for which sampling phases are to be generated
* @param sampling The settings to use in generating phases for the given script
* @returns {Array} The generated sample phases for the given script
*/
generateSamplingPhases: (sampling) => {
const phases = []
// Note: rather than generating these times ourselves, we could use the poisson distribution feature but we want an exact number of executions
// in order to facilitate simple thresholding on the number of successes in a manner that is predictable and legible to users
for (let i = 0; i < sampling.size; i++) {
// Add a pause (even the first time) so as to avoid walls of requests
// Math.random => [0, 1]
// [0, 1] * 2 => [0, 2]
// [0, 2] * pauseVariance => [0, 2 * pauseVariance]
let pause = (Math.random() * 2 * sampling.pauseVariance)
// [0, 2 * pauseVariance] - pauseVariance => [-pauseVariance, +pauseVariance]
pause -= sampling.pauseVariance
// [-pauseVariance, +pauseVariance] + averagePause => [averagePause - pauseVariance, averagePause + pauseVariance]
pause += sampling.averagePause
phases.push({ pause })
phases.push({ duration: 1, arrivalRate: 1 }) // exactly once (settings.task.sampling.size times)
}
return phases
},
// ##############
// ## PLANNING ##
// ##############
/**
* Create an execution plan for a performance mode script
* @param timeNow The time identity to use in logging activity
* @param script The script to split and schedule the chunks of
* @param settings The settings to use for splitting the script
* @returns {ScriptChunk[]} The script chunks obtains from splitting the script (if appropriate) by duration and requests per second
*/
planPerformance: (timeNow, script, settings) => {
const plan = []
let updatedScript = planning.ensureScriptGenesis(script, timeNow)
// ## Duration ##
const scriptDurationInSeconds = planning.scriptDurationInSeconds(updatedScript)
// if there is more script to execute than we're able to complete in a single execution, chomp off the initial executable
// duration of that script as the current executable of our plan.
if (scriptDurationInSeconds > settings.maxChunkDurationInSeconds) {
const parts = planning.splitScriptByDurationInSecondsAndSchedule(timeNow, updatedScript, settings)
updatedScript = parts.chunk
plan.push(parts.remainder)
}
// ## Requests Per Second ##
const scriptRequestsPerSecond = planning.scriptRequestsPerSecond(updatedScript)
// if the current script can be executed in a single run add it to the plan, otherwise, remove chunks that can be,
// adding them to the plan until no more script remains
if (scriptRequestsPerSecond <= settings.maxChunkRequestsPerSecond) {
plan.push(updatedScript)
} else {
const parts = planning.splitScriptByRequestsPerSecondAndSchedule(timeNow, updatedScript, settings)
plan.push.apply(plan, parts) // eslint-disable-line prefer-spread
}
return plan
},
/**
* Plan for executing a set of samples for each unique flow in the given script where each sample
* executes its unique flow once
* @param timeNow The time at which the event was received for this execution
* @param script The Artillery (http://artillery.io) script to split into acceptance tests
* @param settings Execution environment and script constraints
* @returns {*|Array} The scripts to execute as part of this sample plan
*/
planSamples: (timeNow, script, settings) => {
const updatedScript = planning.ensureScriptGenesis(script, timeNow)
updatedScript._start = timeNow // immediate execution
updatedScript._invokeType = 'RequestResponse' // we care about the results (and will record/analyze them)
return planning.splitScriptByFlow(updatedScript, settings)
},
ensureScriptGenesis: (script, timeNow) =>
Object.assign(script, {
_genesis: script._genesis === undefined ? timeNow : script._genesis,
}),
}
module.exports = planning