@jsenv/test 3.7.37 → 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.
@@ -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, the moment a long
5712
- * execution starts decides when the whole run ends. Started last it keeps one
5713
- * slot busy while every other one is idle; started first it runs while the short
5714
- * ones fill the slots around it. Knowing the durations of the previous run is
5715
- * what allows to start the long ones first.
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
- sortByLongestFirst: (executionArray) => {
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 pushed at the end of the run
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[Math.floor(durationMsArray.length / 2)];
5756
- // sort is stable: executions with the same estimation stay in the order
5757
- // they were found on the filesystem
5758
- return [...executionArray].sort((leftExecution, rightExecution) => {
5759
- const leftDurationMs =
5760
- durationMsMap.get(leftExecution) ?? medianDurationMs;
5761
- const rightDurationMs =
5762
- durationMsMap.get(rightExecution) ?? medianDurationMs;
5763
- return rightDurationMs - leftDurationMs;
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 first on the next run
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
- // both worth starting early and worth not having too many of at once
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 executionStartOrderArray = timingsMemory
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 executionRemainingSet) {
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
- if (executionCandidate.params.locks) {
8966
- const resourceLockedByAnother =
8967
- executionCandidate.params.locks.find((resourceToLock) =>
8968
- lockedResourceSet.has(resourceToLock),
8969
- );
8970
- if (resourceLockedByAnother) {
8971
- logger.debug(
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.37",
3
+ "version": "3.7.39",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,9 +31,9 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@c88/v8-coverage": "0.1.1",
34
- "@jsenv/ast": "6.9.3",
35
- "@jsenv/plugin-supervisor": "1.8.14",
36
- "@jsenv/sourcemap": "1.4.2",
34
+ "@jsenv/ast": "6.10.0",
35
+ "@jsenv/plugin-supervisor": "1.8.16",
36
+ "@jsenv/sourcemap": "1.4.3",
37
37
  "he": "1.2.0",
38
38
  "istanbul-lib-coverage": "3.2.2",
39
39
  "istanbul-lib-instrument": "6.0.3",
@@ -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 first on the next run
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
- // both worth starting early and worth not having too many of at once
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 executionStartOrderArray = timingsMemory
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 executionRemainingSet) {
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
- if (executionCandidate.params.locks) {
1165
- const resourceLockedByAnother =
1166
- executionCandidate.params.locks.find((resourceToLock) =>
1167
- lockedResourceSet.has(resourceToLock),
1168
- );
1169
- if (resourceLockedByAnother) {
1170
- logger.debug(
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, the moment a long
6
- * execution starts decides when the whole run ends. Started last it keeps one
7
- * slot busy while every other one is idle; started first it runs while the short
8
- * ones fill the slots around it. Knowing the durations of the previous run is
9
- * what allows to start the long ones first.
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
- sortByLongestFirst: (executionArray) => {
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 pushed at the end of the run
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[Math.floor(durationMsArray.length / 2)];
52
- // sort is stable: executions with the same estimation stay in the order
53
- // they were found on the filesystem
54
- return [...executionArray].sort((leftExecution, rightExecution) => {
55
- const leftDurationMs =
56
- durationMsMap.get(leftExecution) ?? medianDurationMs;
57
- const rightDurationMs =
58
- durationMsMap.get(rightExecution) ?? medianDurationMs;
59
- return rightDurationMs - leftDurationMs;
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;