@jsenv/test 3.7.38 → 3.7.39
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/dist/jsenv_test.js +150 -39
- package/package.json +3 -3
- package/src/execution/execute_test_plan.js +127 -19
- package/src/execution/execution_timings.js +23 -20
package/dist/jsenv_test.js
CHANGED
|
@@ -5708,11 +5708,11 @@ const isV8Coverage = (coverage) => Boolean(coverage.result);
|
|
|
5708
5708
|
* Remembers, from one run to the next, how long each execution took and how much
|
|
5709
5709
|
* time it asked for (see requestAllocatedMs).
|
|
5710
5710
|
*
|
|
5711
|
-
* What it is for: with a fixed number of parallel slots,
|
|
5712
|
-
*
|
|
5713
|
-
* slot busy while every other one is idle
|
|
5714
|
-
*
|
|
5715
|
-
*
|
|
5711
|
+
* What it is for: with a fixed number of parallel slots, an execution still
|
|
5712
|
+
* running when everything after it is done decides alone when the run ends.
|
|
5713
|
+
* Started last it keeps one slot busy while every other one is idle. Knowing
|
|
5714
|
+
* the durations of the previous run is what allows to recognize that execution
|
|
5715
|
+
* and start it ahead of its turn (see "parallel.maxAhead").
|
|
5716
5716
|
*
|
|
5717
5717
|
* Only the start order is affected: executions keep the index they got from the
|
|
5718
5718
|
* filesystem, so they are still reported in that order.
|
|
@@ -5735,33 +5735,36 @@ const createExecutionTimings = ({ fileUrl }) => {
|
|
|
5735
5735
|
}
|
|
5736
5736
|
return previousEntry.allocatedMsRequested;
|
|
5737
5737
|
},
|
|
5738
|
-
|
|
5738
|
+
// how long each execution is expected to take on this run
|
|
5739
|
+
estimateDurations: (executionArray) => {
|
|
5739
5740
|
const durationMsMap = new Map();
|
|
5740
5741
|
const durationMsArray = [];
|
|
5741
5742
|
for (const execution of executionArray) {
|
|
5743
|
+
if (execution.skipped) {
|
|
5744
|
+
// a skipped execution costs nothing; it must not weigh on what is
|
|
5745
|
+
// expected from the others
|
|
5746
|
+
durationMsMap.set(execution, 0);
|
|
5747
|
+
continue;
|
|
5748
|
+
}
|
|
5742
5749
|
const previousEntry = previousEntryMap.get(execution.name);
|
|
5743
5750
|
if (previousEntry) {
|
|
5744
5751
|
durationMsMap.set(execution, previousEntry.durationMs);
|
|
5745
5752
|
durationMsArray.push(previousEntry.durationMs);
|
|
5746
5753
|
}
|
|
5747
5754
|
}
|
|
5748
|
-
if (durationMsArray.length === 0) {
|
|
5749
|
-
return executionArray;
|
|
5750
|
-
}
|
|
5751
5755
|
// an execution never seen before is assumed to last as long as the median:
|
|
5752
|
-
// being new is not a reason to be
|
|
5756
|
+
// being new is not a reason to be considered short
|
|
5753
5757
|
durationMsArray.sort((a, b) => a - b);
|
|
5754
5758
|
const medianDurationMs =
|
|
5755
|
-
durationMsArray
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
durationMsMap.
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
});
|
|
5759
|
+
durationMsArray.length === 0
|
|
5760
|
+
? 0
|
|
5761
|
+
: durationMsArray[Math.floor(durationMsArray.length / 2)];
|
|
5762
|
+
for (const execution of executionArray) {
|
|
5763
|
+
if (!durationMsMap.has(execution)) {
|
|
5764
|
+
durationMsMap.set(execution, medianDurationMs);
|
|
5765
|
+
}
|
|
5766
|
+
}
|
|
5767
|
+
return durationMsMap;
|
|
5765
5768
|
},
|
|
5766
5769
|
record: (execution) => {
|
|
5767
5770
|
const { status, timings } = execution.result;
|
|
@@ -7848,7 +7851,7 @@ const ensureWebServerIsStarted = async (
|
|
|
7848
7851
|
* @param {Object} [testPlanParameters.webServer] Web server info; required when executing test on browsers
|
|
7849
7852
|
* @param {Object} testPlanParameters.testPlan Object associating files with runtimes where they will be executed
|
|
7850
7853
|
* @param {Object|false} [testPlanParameters.parallel] Maximum amount of execution running at the same time
|
|
7851
|
-
* @param {Object|false} [testPlanParameters.executionTimings=false] Remembers how long executions took to start the longest ones
|
|
7854
|
+
* @param {Object|false} [testPlanParameters.executionTimings=false] Remembers how long executions took; on the next run "parallel.maxAhead" uses it to start the longest ones ahead of their turn
|
|
7852
7855
|
* @param {number} [testPlanParameters.defaultMsAllocatedPerExecution=30000] Milliseconds after which execution is aborted and considered as failed by timeout
|
|
7853
7856
|
* @param {boolean} [testPlanParameters.failFast=false] Fails immediatly when a test execution fails
|
|
7854
7857
|
* @param {Object|false} [testPlanParameters.coverage=false] Controls if coverage is collected during files executions
|
|
@@ -7910,6 +7913,9 @@ const parallelDefault = {
|
|
|
7910
7913
|
// percentage resolved against parallel.max; an execution is heavy when it is
|
|
7911
7914
|
// allocated more time than the others (see defaultMsAllocatedPerExecution)
|
|
7912
7915
|
maxHeavy: "75%",
|
|
7916
|
+
// how many slots may be given to an execution started ahead of its turn;
|
|
7917
|
+
// 0 keeps the order given by the filesystem (see "executionTimings")
|
|
7918
|
+
maxAhead: 0,
|
|
7913
7919
|
};
|
|
7914
7920
|
const executionTimingsDefault = {
|
|
7915
7921
|
fileUrl: undefined,
|
|
@@ -8246,6 +8252,20 @@ const executeTestPlan = async ({
|
|
|
8246
8252
|
`parallel.maxHeavy must be a number or a percentage, got ${maxHeavy}`,
|
|
8247
8253
|
);
|
|
8248
8254
|
}
|
|
8255
|
+
|
|
8256
|
+
const maxAhead = parallel.maxAhead;
|
|
8257
|
+
if (typeof maxAhead === "string") {
|
|
8258
|
+
const maxAheadAsRatio = assertPercentageAndConvertToRatio(maxAhead);
|
|
8259
|
+
parallel.maxAhead = Math.round(maxAheadAsRatio * parallel.max);
|
|
8260
|
+
} else if (typeof maxAhead === "number") {
|
|
8261
|
+
if (maxAhead < 0) {
|
|
8262
|
+
parallel.maxAhead = 0;
|
|
8263
|
+
}
|
|
8264
|
+
} else {
|
|
8265
|
+
throw new TypeError(
|
|
8266
|
+
`parallel.maxAhead must be a number or a percentage, got ${maxAhead}`,
|
|
8267
|
+
);
|
|
8268
|
+
}
|
|
8249
8269
|
}
|
|
8250
8270
|
// executionTimings
|
|
8251
8271
|
{
|
|
@@ -8825,7 +8845,7 @@ To fix this warning:
|
|
|
8825
8845
|
const callWhenPreviousExecutionAreDone = createCallOrderer();
|
|
8826
8846
|
|
|
8827
8847
|
// an execution allowed more time than the others is a heavy one: it is
|
|
8828
|
-
//
|
|
8848
|
+
// worth not having too many of at once
|
|
8829
8849
|
const heavyExecutionSet = new Set();
|
|
8830
8850
|
for (const execution of executionPlanifiedArray) {
|
|
8831
8851
|
if (execution.skipped) {
|
|
@@ -8844,13 +8864,91 @@ To fix this warning:
|
|
|
8844
8864
|
}
|
|
8845
8865
|
}
|
|
8846
8866
|
}
|
|
8847
|
-
const
|
|
8848
|
-
? timingsMemory.sortByLongestFirst(executionPlanifiedArray)
|
|
8849
|
-
: executionPlanifiedArray;
|
|
8850
|
-
|
|
8851
|
-
const executionRemainingSet = new Set(executionStartOrderArray);
|
|
8867
|
+
const executionRemainingSet = new Set(executionPlanifiedArray);
|
|
8852
8868
|
const executionExecutingSet = new Set();
|
|
8869
|
+
// the executions currently running ahead of their turn
|
|
8870
|
+
const executionAheadSet = new Set();
|
|
8853
8871
|
const lockedResourceSet = new Set();
|
|
8872
|
+
const findResourceLockedByAnother = (execution) => {
|
|
8873
|
+
if (!execution.params.locks) {
|
|
8874
|
+
return null;
|
|
8875
|
+
}
|
|
8876
|
+
return (
|
|
8877
|
+
execution.params.locks.find((resourceToLock) =>
|
|
8878
|
+
lockedResourceSet.has(resourceToLock),
|
|
8879
|
+
) || null
|
|
8880
|
+
);
|
|
8881
|
+
};
|
|
8882
|
+
const durationMsMap =
|
|
8883
|
+
timingsMemory && parallel.maxAhead > 0
|
|
8884
|
+
? timingsMemory.estimateDurations(executionPlanifiedArray)
|
|
8885
|
+
: null;
|
|
8886
|
+
/*
|
|
8887
|
+
* Executions start in the order the filesystem gives them, which is also
|
|
8888
|
+
* the order they are reported in: the run then progresses where it is
|
|
8889
|
+
* read. One departure from that order pays for itself: an execution still
|
|
8890
|
+
* running once everything after it is done decides alone when the run
|
|
8891
|
+
* ends, so the time it spends waiting for its turn is time the whole run
|
|
8892
|
+
* waits at the end. "parallel.maxAhead" says how many slots can be spent
|
|
8893
|
+
* on such an execution; the other slots keep the natural order, because
|
|
8894
|
+
* starting the heaviest executions of the plan all at once makes them
|
|
8895
|
+
* fight for cpu and memory, which pauses everything (see maxCpu/maxMemory).
|
|
8896
|
+
*/
|
|
8897
|
+
const pickExecutionToStartAhead = (executionPickedSet) => {
|
|
8898
|
+
if (!durationMsMap) {
|
|
8899
|
+
// nothing remembers which execution is the long one
|
|
8900
|
+
return null;
|
|
8901
|
+
}
|
|
8902
|
+
if (
|
|
8903
|
+
executionAheadSet.size + executionPickedSet.size >=
|
|
8904
|
+
parallel.maxAhead
|
|
8905
|
+
) {
|
|
8906
|
+
return null;
|
|
8907
|
+
}
|
|
8908
|
+
const executionRemainingArray = [...executionRemainingSet];
|
|
8909
|
+
// what a remaining execution costs on average; the last one to start is
|
|
8910
|
+
// always the one still running at the end, so without this reference the
|
|
8911
|
+
// slot would go to whichever execution happens to be last rather than to
|
|
8912
|
+
// a long one
|
|
8913
|
+
let msRemainingTotal = 0;
|
|
8914
|
+
let executionRemainingCount = 0;
|
|
8915
|
+
for (const execution of executionRemainingArray) {
|
|
8916
|
+
const durationMs = durationMsMap.get(execution);
|
|
8917
|
+
if (durationMs > 0) {
|
|
8918
|
+
msRemainingTotal += durationMs;
|
|
8919
|
+
executionRemainingCount++;
|
|
8920
|
+
}
|
|
8921
|
+
}
|
|
8922
|
+
if (executionRemainingCount === 0) {
|
|
8923
|
+
return null;
|
|
8924
|
+
}
|
|
8925
|
+
const msAverage = msRemainingTotal / executionRemainingCount;
|
|
8926
|
+
let executionAhead = null;
|
|
8927
|
+
let executionAheadMs = 0;
|
|
8928
|
+
// the executions are visited backwards so that, for each one, the time
|
|
8929
|
+
// still to be spent after it is already known
|
|
8930
|
+
let msAfter = 0;
|
|
8931
|
+
let index = executionRemainingArray.length;
|
|
8932
|
+
while (index--) {
|
|
8933
|
+
const execution = executionRemainingArray[index];
|
|
8934
|
+
const durationMs = durationMsMap.get(execution);
|
|
8935
|
+
if (
|
|
8936
|
+
// the ones about to start anyway have nothing to gain
|
|
8937
|
+
index >= parallel.max &&
|
|
8938
|
+
durationMs > msAverage &&
|
|
8939
|
+
durationMs > executionAheadMs &&
|
|
8940
|
+
// it would still be running when everything after it is done
|
|
8941
|
+
durationMs > msAfter / parallel.max &&
|
|
8942
|
+
!executionPickedSet.has(execution) &&
|
|
8943
|
+
!findResourceLockedByAnother(execution)
|
|
8944
|
+
) {
|
|
8945
|
+
executionAhead = execution;
|
|
8946
|
+
executionAheadMs = durationMs;
|
|
8947
|
+
}
|
|
8948
|
+
msAfter += durationMs;
|
|
8949
|
+
}
|
|
8950
|
+
return executionAhead;
|
|
8951
|
+
};
|
|
8854
8952
|
const start = async (execution) => {
|
|
8855
8953
|
execution.fileExecutionCount = Object.keys(
|
|
8856
8954
|
testPlanResult.results[execution.fileRelativeUrl],
|
|
@@ -8901,6 +8999,7 @@ To fix this warning:
|
|
|
8901
8999
|
}
|
|
8902
9000
|
mutateCountersAfterExecutionEnds(counters, execution);
|
|
8903
9001
|
executionExecutingSet.delete(execution);
|
|
9002
|
+
executionAheadSet.delete(execution);
|
|
8904
9003
|
for (const afterEachCallback of afterEachCallbackSet) {
|
|
8905
9004
|
afterEachCallback(execution, testPlanResult, testPlanHelpers);
|
|
8906
9005
|
}
|
|
@@ -8928,8 +9027,21 @@ To fix this warning:
|
|
|
8928
9027
|
heavyExecutingCount++;
|
|
8929
9028
|
}
|
|
8930
9029
|
}
|
|
9030
|
+
// the executions allowed to run ahead of their turn are considered
|
|
9031
|
+
// first: the slot they take is the whole point
|
|
9032
|
+
const executionPickedSet = new Set();
|
|
9033
|
+
while (true) {
|
|
9034
|
+
const executionAhead = pickExecutionToStartAhead(executionPickedSet);
|
|
9035
|
+
if (!executionAhead) {
|
|
9036
|
+
break;
|
|
9037
|
+
}
|
|
9038
|
+
executionPickedSet.add(executionAhead);
|
|
9039
|
+
}
|
|
8931
9040
|
const promises = [];
|
|
8932
|
-
for (const executionCandidate of
|
|
9041
|
+
for (const executionCandidate of new Set([
|
|
9042
|
+
...executionPickedSet,
|
|
9043
|
+
...executionRemainingSet,
|
|
9044
|
+
])) {
|
|
8933
9045
|
if (executionExecutingSet.size >= parallel.max) {
|
|
8934
9046
|
break;
|
|
8935
9047
|
}
|
|
@@ -8962,22 +9074,21 @@ To fix this warning:
|
|
|
8962
9074
|
continue;
|
|
8963
9075
|
}
|
|
8964
9076
|
}
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
`"${resourceLockedByAnother}" is locked, ${executionCandidate.name} will wait until it is released by a previous execution`,
|
|
8973
|
-
);
|
|
8974
|
-
continue;
|
|
8975
|
-
}
|
|
9077
|
+
const resourceLockedByAnother =
|
|
9078
|
+
findResourceLockedByAnother(executionCandidate);
|
|
9079
|
+
if (resourceLockedByAnother) {
|
|
9080
|
+
logger.debug(
|
|
9081
|
+
`"${resourceLockedByAnother}" is locked, ${executionCandidate.name} will wait until it is released by a previous execution`,
|
|
9082
|
+
);
|
|
9083
|
+
continue;
|
|
8976
9084
|
}
|
|
8977
9085
|
}
|
|
8978
9086
|
if (heavyExecutionSet.has(executionCandidate)) {
|
|
8979
9087
|
heavyExecutingCount++;
|
|
8980
9088
|
}
|
|
9089
|
+
if (executionPickedSet.has(executionCandidate)) {
|
|
9090
|
+
executionAheadSet.add(executionCandidate);
|
|
9091
|
+
}
|
|
8981
9092
|
const promise = (async () => {
|
|
8982
9093
|
await start(executionCandidate);
|
|
8983
9094
|
await startAsMuchAsPossible();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/test",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@c88/v8-coverage": "0.1.1",
|
|
34
|
-
"@jsenv/ast": "6.
|
|
35
|
-
"@jsenv/plugin-supervisor": "1.8.
|
|
34
|
+
"@jsenv/ast": "6.10.0",
|
|
35
|
+
"@jsenv/plugin-supervisor": "1.8.16",
|
|
36
36
|
"@jsenv/sourcemap": "1.4.3",
|
|
37
37
|
"he": "1.2.0",
|
|
38
38
|
"istanbul-lib-coverage": "3.2.2",
|
|
@@ -47,7 +47,7 @@ import { assertAndNormalizeWebServer } from "./web_server_param.js";
|
|
|
47
47
|
* @param {Object} [testPlanParameters.webServer] Web server info; required when executing test on browsers
|
|
48
48
|
* @param {Object} testPlanParameters.testPlan Object associating files with runtimes where they will be executed
|
|
49
49
|
* @param {Object|false} [testPlanParameters.parallel] Maximum amount of execution running at the same time
|
|
50
|
-
* @param {Object|false} [testPlanParameters.executionTimings=false] Remembers how long executions took to start the longest ones
|
|
50
|
+
* @param {Object|false} [testPlanParameters.executionTimings=false] Remembers how long executions took; on the next run "parallel.maxAhead" uses it to start the longest ones ahead of their turn
|
|
51
51
|
* @param {number} [testPlanParameters.defaultMsAllocatedPerExecution=30000] Milliseconds after which execution is aborted and considered as failed by timeout
|
|
52
52
|
* @param {boolean} [testPlanParameters.failFast=false] Fails immediatly when a test execution fails
|
|
53
53
|
* @param {Object|false} [testPlanParameters.coverage=false] Controls if coverage is collected during files executions
|
|
@@ -109,6 +109,9 @@ const parallelDefault = {
|
|
|
109
109
|
// percentage resolved against parallel.max; an execution is heavy when it is
|
|
110
110
|
// allocated more time than the others (see defaultMsAllocatedPerExecution)
|
|
111
111
|
maxHeavy: "75%",
|
|
112
|
+
// how many slots may be given to an execution started ahead of its turn;
|
|
113
|
+
// 0 keeps the order given by the filesystem (see "executionTimings")
|
|
114
|
+
maxAhead: 0,
|
|
112
115
|
};
|
|
113
116
|
const executionTimingsDefault = {
|
|
114
117
|
fileUrl: undefined,
|
|
@@ -445,6 +448,20 @@ export const executeTestPlan = async ({
|
|
|
445
448
|
`parallel.maxHeavy must be a number or a percentage, got ${maxHeavy}`,
|
|
446
449
|
);
|
|
447
450
|
}
|
|
451
|
+
|
|
452
|
+
const maxAhead = parallel.maxAhead;
|
|
453
|
+
if (typeof maxAhead === "string") {
|
|
454
|
+
const maxAheadAsRatio = assertPercentageAndConvertToRatio(maxAhead);
|
|
455
|
+
parallel.maxAhead = Math.round(maxAheadAsRatio * parallel.max);
|
|
456
|
+
} else if (typeof maxAhead === "number") {
|
|
457
|
+
if (maxAhead < 0) {
|
|
458
|
+
parallel.maxAhead = 0;
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
throw new TypeError(
|
|
462
|
+
`parallel.maxAhead must be a number or a percentage, got ${maxAhead}`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
448
465
|
}
|
|
449
466
|
// executionTimings
|
|
450
467
|
{
|
|
@@ -1024,7 +1041,7 @@ To fix this warning:
|
|
|
1024
1041
|
const callWhenPreviousExecutionAreDone = createCallOrderer();
|
|
1025
1042
|
|
|
1026
1043
|
// an execution allowed more time than the others is a heavy one: it is
|
|
1027
|
-
//
|
|
1044
|
+
// worth not having too many of at once
|
|
1028
1045
|
const heavyExecutionSet = new Set();
|
|
1029
1046
|
for (const execution of executionPlanifiedArray) {
|
|
1030
1047
|
if (execution.skipped) {
|
|
@@ -1043,13 +1060,91 @@ To fix this warning:
|
|
|
1043
1060
|
}
|
|
1044
1061
|
}
|
|
1045
1062
|
}
|
|
1046
|
-
const
|
|
1047
|
-
? timingsMemory.sortByLongestFirst(executionPlanifiedArray)
|
|
1048
|
-
: executionPlanifiedArray;
|
|
1049
|
-
|
|
1050
|
-
const executionRemainingSet = new Set(executionStartOrderArray);
|
|
1063
|
+
const executionRemainingSet = new Set(executionPlanifiedArray);
|
|
1051
1064
|
const executionExecutingSet = new Set();
|
|
1065
|
+
// the executions currently running ahead of their turn
|
|
1066
|
+
const executionAheadSet = new Set();
|
|
1052
1067
|
const lockedResourceSet = new Set();
|
|
1068
|
+
const findResourceLockedByAnother = (execution) => {
|
|
1069
|
+
if (!execution.params.locks) {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
return (
|
|
1073
|
+
execution.params.locks.find((resourceToLock) =>
|
|
1074
|
+
lockedResourceSet.has(resourceToLock),
|
|
1075
|
+
) || null
|
|
1076
|
+
);
|
|
1077
|
+
};
|
|
1078
|
+
const durationMsMap =
|
|
1079
|
+
timingsMemory && parallel.maxAhead > 0
|
|
1080
|
+
? timingsMemory.estimateDurations(executionPlanifiedArray)
|
|
1081
|
+
: null;
|
|
1082
|
+
/*
|
|
1083
|
+
* Executions start in the order the filesystem gives them, which is also
|
|
1084
|
+
* the order they are reported in: the run then progresses where it is
|
|
1085
|
+
* read. One departure from that order pays for itself: an execution still
|
|
1086
|
+
* running once everything after it is done decides alone when the run
|
|
1087
|
+
* ends, so the time it spends waiting for its turn is time the whole run
|
|
1088
|
+
* waits at the end. "parallel.maxAhead" says how many slots can be spent
|
|
1089
|
+
* on such an execution; the other slots keep the natural order, because
|
|
1090
|
+
* starting the heaviest executions of the plan all at once makes them
|
|
1091
|
+
* fight for cpu and memory, which pauses everything (see maxCpu/maxMemory).
|
|
1092
|
+
*/
|
|
1093
|
+
const pickExecutionToStartAhead = (executionPickedSet) => {
|
|
1094
|
+
if (!durationMsMap) {
|
|
1095
|
+
// nothing remembers which execution is the long one
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
if (
|
|
1099
|
+
executionAheadSet.size + executionPickedSet.size >=
|
|
1100
|
+
parallel.maxAhead
|
|
1101
|
+
) {
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
const executionRemainingArray = [...executionRemainingSet];
|
|
1105
|
+
// what a remaining execution costs on average; the last one to start is
|
|
1106
|
+
// always the one still running at the end, so without this reference the
|
|
1107
|
+
// slot would go to whichever execution happens to be last rather than to
|
|
1108
|
+
// a long one
|
|
1109
|
+
let msRemainingTotal = 0;
|
|
1110
|
+
let executionRemainingCount = 0;
|
|
1111
|
+
for (const execution of executionRemainingArray) {
|
|
1112
|
+
const durationMs = durationMsMap.get(execution);
|
|
1113
|
+
if (durationMs > 0) {
|
|
1114
|
+
msRemainingTotal += durationMs;
|
|
1115
|
+
executionRemainingCount++;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (executionRemainingCount === 0) {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
const msAverage = msRemainingTotal / executionRemainingCount;
|
|
1122
|
+
let executionAhead = null;
|
|
1123
|
+
let executionAheadMs = 0;
|
|
1124
|
+
// the executions are visited backwards so that, for each one, the time
|
|
1125
|
+
// still to be spent after it is already known
|
|
1126
|
+
let msAfter = 0;
|
|
1127
|
+
let index = executionRemainingArray.length;
|
|
1128
|
+
while (index--) {
|
|
1129
|
+
const execution = executionRemainingArray[index];
|
|
1130
|
+
const durationMs = durationMsMap.get(execution);
|
|
1131
|
+
if (
|
|
1132
|
+
// the ones about to start anyway have nothing to gain
|
|
1133
|
+
index >= parallel.max &&
|
|
1134
|
+
durationMs > msAverage &&
|
|
1135
|
+
durationMs > executionAheadMs &&
|
|
1136
|
+
// it would still be running when everything after it is done
|
|
1137
|
+
durationMs > msAfter / parallel.max &&
|
|
1138
|
+
!executionPickedSet.has(execution) &&
|
|
1139
|
+
!findResourceLockedByAnother(execution)
|
|
1140
|
+
) {
|
|
1141
|
+
executionAhead = execution;
|
|
1142
|
+
executionAheadMs = durationMs;
|
|
1143
|
+
}
|
|
1144
|
+
msAfter += durationMs;
|
|
1145
|
+
}
|
|
1146
|
+
return executionAhead;
|
|
1147
|
+
};
|
|
1053
1148
|
const start = async (execution) => {
|
|
1054
1149
|
execution.fileExecutionCount = Object.keys(
|
|
1055
1150
|
testPlanResult.results[execution.fileRelativeUrl],
|
|
@@ -1100,6 +1195,7 @@ To fix this warning:
|
|
|
1100
1195
|
}
|
|
1101
1196
|
mutateCountersAfterExecutionEnds(counters, execution);
|
|
1102
1197
|
executionExecutingSet.delete(execution);
|
|
1198
|
+
executionAheadSet.delete(execution);
|
|
1103
1199
|
for (const afterEachCallback of afterEachCallbackSet) {
|
|
1104
1200
|
afterEachCallback(execution, testPlanResult, testPlanHelpers);
|
|
1105
1201
|
}
|
|
@@ -1127,8 +1223,21 @@ To fix this warning:
|
|
|
1127
1223
|
heavyExecutingCount++;
|
|
1128
1224
|
}
|
|
1129
1225
|
}
|
|
1226
|
+
// the executions allowed to run ahead of their turn are considered
|
|
1227
|
+
// first: the slot they take is the whole point
|
|
1228
|
+
const executionPickedSet = new Set();
|
|
1229
|
+
while (true) {
|
|
1230
|
+
const executionAhead = pickExecutionToStartAhead(executionPickedSet);
|
|
1231
|
+
if (!executionAhead) {
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
executionPickedSet.add(executionAhead);
|
|
1235
|
+
}
|
|
1130
1236
|
const promises = [];
|
|
1131
|
-
for (const executionCandidate of
|
|
1237
|
+
for (const executionCandidate of new Set([
|
|
1238
|
+
...executionPickedSet,
|
|
1239
|
+
...executionRemainingSet,
|
|
1240
|
+
])) {
|
|
1132
1241
|
if (executionExecutingSet.size >= parallel.max) {
|
|
1133
1242
|
break;
|
|
1134
1243
|
}
|
|
@@ -1161,22 +1270,21 @@ To fix this warning:
|
|
|
1161
1270
|
continue;
|
|
1162
1271
|
}
|
|
1163
1272
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
`"${resourceLockedByAnother}" is locked, ${executionCandidate.name} will wait until it is released by a previous execution`,
|
|
1172
|
-
);
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1273
|
+
const resourceLockedByAnother =
|
|
1274
|
+
findResourceLockedByAnother(executionCandidate);
|
|
1275
|
+
if (resourceLockedByAnother) {
|
|
1276
|
+
logger.debug(
|
|
1277
|
+
`"${resourceLockedByAnother}" is locked, ${executionCandidate.name} will wait until it is released by a previous execution`,
|
|
1278
|
+
);
|
|
1279
|
+
continue;
|
|
1175
1280
|
}
|
|
1176
1281
|
}
|
|
1177
1282
|
if (heavyExecutionSet.has(executionCandidate)) {
|
|
1178
1283
|
heavyExecutingCount++;
|
|
1179
1284
|
}
|
|
1285
|
+
if (executionPickedSet.has(executionCandidate)) {
|
|
1286
|
+
executionAheadSet.add(executionCandidate);
|
|
1287
|
+
}
|
|
1180
1288
|
const promise = (async () => {
|
|
1181
1289
|
await start(executionCandidate);
|
|
1182
1290
|
await startAsMuchAsPossible();
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Remembers, from one run to the next, how long each execution took and how much
|
|
3
3
|
* time it asked for (see requestAllocatedMs).
|
|
4
4
|
*
|
|
5
|
-
* What it is for: with a fixed number of parallel slots,
|
|
6
|
-
*
|
|
7
|
-
* slot busy while every other one is idle
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* What it is for: with a fixed number of parallel slots, an execution still
|
|
6
|
+
* running when everything after it is done decides alone when the run ends.
|
|
7
|
+
* Started last it keeps one slot busy while every other one is idle. Knowing
|
|
8
|
+
* the durations of the previous run is what allows to recognize that execution
|
|
9
|
+
* and start it ahead of its turn (see "parallel.maxAhead").
|
|
10
10
|
*
|
|
11
11
|
* Only the start order is affected: executions keep the index they got from the
|
|
12
12
|
* filesystem, so they are still reported in that order.
|
|
@@ -31,33 +31,36 @@ export const createExecutionTimings = ({ fileUrl }) => {
|
|
|
31
31
|
}
|
|
32
32
|
return previousEntry.allocatedMsRequested;
|
|
33
33
|
},
|
|
34
|
-
|
|
34
|
+
// how long each execution is expected to take on this run
|
|
35
|
+
estimateDurations: (executionArray) => {
|
|
35
36
|
const durationMsMap = new Map();
|
|
36
37
|
const durationMsArray = [];
|
|
37
38
|
for (const execution of executionArray) {
|
|
39
|
+
if (execution.skipped) {
|
|
40
|
+
// a skipped execution costs nothing; it must not weigh on what is
|
|
41
|
+
// expected from the others
|
|
42
|
+
durationMsMap.set(execution, 0);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
38
45
|
const previousEntry = previousEntryMap.get(execution.name);
|
|
39
46
|
if (previousEntry) {
|
|
40
47
|
durationMsMap.set(execution, previousEntry.durationMs);
|
|
41
48
|
durationMsArray.push(previousEntry.durationMs);
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
|
-
if (durationMsArray.length === 0) {
|
|
45
|
-
return executionArray;
|
|
46
|
-
}
|
|
47
51
|
// an execution never seen before is assumed to last as long as the median:
|
|
48
|
-
// being new is not a reason to be
|
|
52
|
+
// being new is not a reason to be considered short
|
|
49
53
|
durationMsArray.sort((a, b) => a - b);
|
|
50
54
|
const medianDurationMs =
|
|
51
|
-
durationMsArray
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
durationMsMap.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
55
|
+
durationMsArray.length === 0
|
|
56
|
+
? 0
|
|
57
|
+
: durationMsArray[Math.floor(durationMsArray.length / 2)];
|
|
58
|
+
for (const execution of executionArray) {
|
|
59
|
+
if (!durationMsMap.has(execution)) {
|
|
60
|
+
durationMsMap.set(execution, medianDurationMs);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return durationMsMap;
|
|
61
64
|
},
|
|
62
65
|
record: (execution) => {
|
|
63
66
|
const { status, timings } = execution.result;
|