@sdeverywhere/check-core 0.1.5 → 0.1.6

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/index.js CHANGED
@@ -38,33 +38,178 @@ var __async = (__this, __arguments, generator) => {
38
38
  });
39
39
  };
40
40
 
41
+ // src/bundle/impl-vars-codec.ts
42
+ function encodeImplVars(input) {
43
+ const subscripts = [];
44
+ const variables = [];
45
+ const varTypes = [];
46
+ const varInstances = {};
47
+ const subscriptMap = /* @__PURE__ */ new Map();
48
+ const variableMap = /* @__PURE__ */ new Map();
49
+ const varTypeMap = /* @__PURE__ */ new Map();
50
+ for (const [groupKey, implVars] of Object.entries(input)) {
51
+ const instances = [];
52
+ for (const implVar of implVars) {
53
+ const varIdInfo = parseSubscripts(implVar.varId);
54
+ const varNameInfo = parseSubscripts(implVar.varName);
55
+ let varTypeIndex = varTypeMap.get(implVar.varType);
56
+ if (varTypeIndex === void 0) {
57
+ varTypeIndex = varTypes.length;
58
+ varTypes.push(implVar.varType);
59
+ varTypeMap.set(implVar.varType, varTypeIndex);
60
+ }
61
+ let variableIndex = variableMap.get(implVar.varIndex);
62
+ if (variableIndex === void 0) {
63
+ variableIndex = variables.length;
64
+ variables.push({
65
+ n: varNameInfo.base,
66
+ i: varIdInfo.base,
67
+ x: implVar.varIndex
68
+ });
69
+ variableMap.set(implVar.varIndex, variableIndex);
70
+ }
71
+ let subscriptIndices;
72
+ let subscriptValues;
73
+ if (varIdInfo.subscripts.length > 0) {
74
+ subscriptIndices = [];
75
+ subscriptValues = implVar.subscriptIndices || [];
76
+ for (let i = 0; i < varIdInfo.subscripts.length; i++) {
77
+ const subscriptId = varIdInfo.subscripts[i];
78
+ const subscriptName = varNameInfo.subscripts[i];
79
+ let subscriptIndex = subscriptMap.get(subscriptId);
80
+ if (subscriptIndex === void 0) {
81
+ subscriptIndex = subscripts.length;
82
+ subscripts.push({
83
+ n: subscriptName,
84
+ i: subscriptId
85
+ });
86
+ subscriptMap.set(subscriptId, subscriptIndex);
87
+ }
88
+ subscriptIndices.push(subscriptIndex);
89
+ }
90
+ }
91
+ const instance = [varTypeIndex, variableIndex];
92
+ if (subscriptIndices) {
93
+ instance.push(...subscriptIndices, ...subscriptValues);
94
+ }
95
+ instances.push(instance);
96
+ }
97
+ varInstances[groupKey] = instances;
98
+ }
99
+ return {
100
+ subscripts,
101
+ variables,
102
+ varTypes,
103
+ varInstances
104
+ };
105
+ }
106
+ function decodeImplVars(encoded) {
107
+ const result = {};
108
+ for (const [groupKey, instances] of Object.entries(encoded.varInstances)) {
109
+ const implVars = [];
110
+ for (const instance of instances) {
111
+ const varType = encoded.varTypes[instance[0]];
112
+ const variable = encoded.variables[instance[1]];
113
+ let varId = variable.i;
114
+ let varName = variable.n;
115
+ if (instance.length > 2) {
116
+ const subscriptIds = [];
117
+ const subscriptNames = [];
118
+ const subscriptCount = (instance.length - 2) / 2;
119
+ const subscriptIndices = instance.slice(2, 2 + subscriptCount);
120
+ for (const subscriptIndex of subscriptIndices) {
121
+ const subscript = encoded.subscripts[subscriptIndex];
122
+ subscriptIds.push(subscript.i);
123
+ subscriptNames.push(subscript.n);
124
+ }
125
+ varId += `[${subscriptIds.join(",")}]`;
126
+ varName += `[${subscriptNames.join(",")}]`;
127
+ }
128
+ const implVar = {
129
+ varId,
130
+ varName,
131
+ varType,
132
+ varIndex: variable.x,
133
+ subscriptIndices: instance.length > 2 ? instance.slice(2 + (instance.length - 2) / 2) : void 0
134
+ };
135
+ implVars.push(implVar);
136
+ }
137
+ result[groupKey] = implVars;
138
+ }
139
+ return result;
140
+ }
141
+ function parseSubscripts(text) {
142
+ const bracketIndex = text.indexOf("[");
143
+ if (bracketIndex === -1) {
144
+ return { base: text, subscripts: [] };
145
+ }
146
+ const base = text.substring(0, bracketIndex);
147
+ const subscriptText = text.substring(bracketIndex + 1, text.lastIndexOf("]"));
148
+ const subscripts = subscriptText.split(",").map((s) => s.trim());
149
+ return { base, subscripts };
150
+ }
151
+
41
152
  // src/_shared/task-queue.ts
42
- var TaskQueue = class {
43
- constructor(processor) {
44
- this.processor = processor;
153
+ var TaskQueue = class _TaskQueue {
154
+ /**
155
+ * @param executors The map of available task executors.
156
+ */
157
+ constructor(executors) {
158
+ this.executors = executors;
45
159
  /** The queue of task keys, most recent at front. */
46
160
  this.taskKeyQueue = [];
47
161
  /** The map of tasks. */
48
162
  this.taskMap = /* @__PURE__ */ new Map();
163
+ // /** The set of keys for the executors that are currently processing tasks. */
164
+ // private readonly activeExecutorKeys: Set<TaskExecutorKey> = new Set()
165
+ /** The idle event listeners. */
166
+ this.idleListeners = [];
49
167
  /** Whether tasks are being processed. */
50
168
  this.processing = false;
51
169
  /** Whether `shutdown` has been called. */
52
170
  this.stopped = false;
53
171
  }
54
- addTask(key, input, onComplete) {
172
+ /**
173
+ * Initialize the shared `TaskQueue` instance.
174
+ *
175
+ * @param executors The map of available task executors.
176
+ */
177
+ static initialize(executors) {
178
+ if (executors.size === 0) {
179
+ throw new Error("Must provide at least one executor");
180
+ }
181
+ this.instance = new _TaskQueue(executors);
182
+ }
183
+ /**
184
+ * Get the shared `TaskQueue` instance.
185
+ */
186
+ static getInstance() {
187
+ if (!this.instance) {
188
+ throw new Error("TaskQueue not initialized; must call `initialize` first");
189
+ }
190
+ return this.instance;
191
+ }
192
+ /**
193
+ * Add a task to the queue.
194
+ *
195
+ * @param task The task to add.
196
+ */
197
+ addTask(task) {
55
198
  if (this.stopped) {
56
199
  return;
57
200
  }
58
- if (this.taskMap.has(key)) {
59
- throw new Error(`Task already added for key ${key}`);
201
+ if (this.taskMap.has(task.key)) {
202
+ throw new Error(`Task already added for key ${task.key}`);
60
203
  }
61
- this.taskKeyQueue.push(key);
62
- this.taskMap.set(key, {
63
- input,
64
- onComplete
65
- });
204
+ this.taskKeyQueue.push(task.key);
205
+ this.taskMap.set(task.key, task);
66
206
  this.processTasksIfNeeded();
67
207
  }
208
+ /**
209
+ * Cancel a task.
210
+ *
211
+ * @param taskKey The key of the task to cancel.
212
+ */
68
213
  cancelTask(taskKey) {
69
214
  const index = this.taskKeyQueue.indexOf(taskKey);
70
215
  if (index >= 0) {
@@ -72,9 +217,37 @@ var TaskQueue = class {
72
217
  }
73
218
  this.taskMap.delete(taskKey);
74
219
  }
220
+ /**
221
+ * Add an idle listener.
222
+ *
223
+ * @param listener The listener to add.
224
+ */
225
+ onIdle(listener) {
226
+ this.idleListeners.push(listener);
227
+ }
228
+ /**
229
+ * Remove an idle listener.
230
+ *
231
+ * @param listener The listener to remove.
232
+ */
233
+ removeIdleListener(listener) {
234
+ this.idleListeners.splice(this.idleListeners.indexOf(listener), 1);
235
+ }
236
+ /**
237
+ * Notify the idle listeners.
238
+ *
239
+ * @param error The error to notify the listeners with.
240
+ */
241
+ notifyIdle(error) {
242
+ for (const listener of this.idleListeners) {
243
+ listener(error);
244
+ }
245
+ }
246
+ /**
247
+ * Shutdown the task queue, cancelling all pending tasks.
248
+ */
75
249
  shutdown() {
76
250
  this.stopped = true;
77
- this.processing = false;
78
251
  this.taskKeyQueue.length = 0;
79
252
  this.taskMap.clear();
80
253
  }
@@ -82,75 +255,98 @@ var TaskQueue = class {
82
255
  if (!this.stopped && !this.processing) {
83
256
  this.processing = true;
84
257
  setTimeout(() => {
85
- this.processNextTask();
258
+ this.processNextTasks();
86
259
  });
87
260
  }
88
261
  }
89
- processNextTask() {
262
+ processNextTasks() {
90
263
  return __async(this, null, function* () {
91
- var _a, _b;
92
- const taskKey = this.taskKeyQueue.shift();
93
- if (!taskKey) {
264
+ const taskKeys = this.taskKeyQueue.splice(0, this.executors.size);
265
+ if (taskKeys.length === 0) {
266
+ this.processing = false;
267
+ if (!this.stopped) {
268
+ this.notifyIdle();
269
+ }
94
270
  return;
95
271
  }
96
- const task = this.taskMap.get(taskKey);
97
- if (task) {
272
+ const executeCalls = [];
273
+ const availableExecutorKeys = Array.from(this.executors.keys());
274
+ for (const taskKey of taskKeys) {
275
+ const task = this.taskMap.get(taskKey);
276
+ if (!task) {
277
+ continue;
278
+ }
98
279
  this.taskMap.delete(taskKey);
99
- } else {
100
- return;
280
+ const executorKey = availableExecutorKeys.shift();
281
+ const executor = this.executors.get(executorKey);
282
+ if (!executor) {
283
+ throw new Error(`No executor found for key ${executorKey}`);
284
+ }
285
+ executeCalls.push(executor.execute(task));
101
286
  }
102
- let output;
103
287
  try {
104
- output = yield this.processor.process(task.input);
288
+ yield Promise.all(executeCalls);
105
289
  } catch (e) {
106
290
  if (!this.stopped) {
107
291
  this.shutdown();
108
- (_a = this.onIdle) == null ? void 0 : _a.call(this, e);
292
+ this.notifyIdle(e);
109
293
  }
110
294
  return;
111
295
  }
112
- task.onComplete(output);
113
296
  if (this.taskKeyQueue.length > 0) {
114
297
  setTimeout(() => {
115
- this.processNextTask();
298
+ this.processNextTasks();
116
299
  });
117
300
  } else {
118
301
  this.processing = false;
119
302
  if (!this.stopped) {
120
- (_b = this.onIdle) == null ? void 0 : _b.call(this);
303
+ this.notifyIdle();
121
304
  }
122
305
  }
123
306
  });
124
307
  }
125
308
  };
309
+ function createExecutor(bundleModelL, bundleModelR) {
310
+ return {
311
+ execute: (task) => __async(this, null, function* () {
312
+ return task.process({
313
+ L: bundleModelL,
314
+ R: bundleModelR
315
+ });
316
+ })
317
+ };
318
+ }
126
319
 
127
320
  // src/check/check-data-coordinator.ts
128
321
  var CheckDataCoordinator = class {
129
- constructor(bundleModel) {
130
- this.bundleModel = bundleModel;
131
- this.taskQueue = new TaskQueue({
132
- process: (request) => __async(this, null, function* () {
133
- const result = yield this.bundleModel.getDatasetsForScenario(request.scenarioSpec, [request.datasetKey]);
134
- const dataset = result.datasetMap.get(request.datasetKey);
135
- return {
136
- dataset
137
- };
138
- })
139
- });
322
+ constructor(taskQueue) {
323
+ this.taskQueue = taskQueue;
140
324
  }
141
325
  requestDataset(requestKey, scenarioSpec, datasetKey, onResponse) {
142
- const request = {
143
- scenarioSpec,
144
- datasetKey
326
+ const task = {
327
+ key: requestKey,
328
+ kind: "check-data-coordinator",
329
+ process: (bundleModels) => __async(this, null, function* () {
330
+ const bundleModelR = bundleModels.R;
331
+ const result = yield bundleModelR.getDatasetsForScenario(scenarioSpec, [datasetKey]);
332
+ const dataset = result.datasetMap.get(datasetKey);
333
+ onResponse(dataset);
334
+ })
145
335
  };
146
- this.taskQueue.addTask(requestKey, request, (response) => {
147
- onResponse(response.dataset);
148
- });
336
+ this.taskQueue.addTask(task);
149
337
  }
150
338
  cancelRequest(key) {
151
339
  this.taskQueue.cancelTask(key);
152
340
  }
153
341
  };
342
+ function createCheckDataCoordinator() {
343
+ return new CheckDataCoordinator(TaskQueue.getInstance());
344
+ }
345
+ function createCheckDataCoordinatorForTests(bundleModel) {
346
+ const executor = createExecutor(void 0, bundleModel);
347
+ const taskQueue = new TaskQueue(/* @__PURE__ */ new Map([["test-executor-0", executor]]));
348
+ return new CheckDataCoordinator(taskQueue);
349
+ }
154
350
 
155
351
  // src/check/check-report.ts
156
352
  import assertNever2 from "assert-never";
@@ -212,6 +408,10 @@ function buildCheckReport(checkPlan, checkResults) {
212
408
  testStatus = "failed";
213
409
  scenarioStatus = "failed";
214
410
  datasetStatus = "failed";
411
+ } else if (checkResult.status === "skipped" && testStatus === "passed") {
412
+ testStatus = "skipped";
413
+ scenarioStatus = "skipped";
414
+ datasetStatus = "skipped";
215
415
  }
216
416
  }
217
417
  predicateReports.push(predicateReport(predicatePlan, checkKey, checkResult));
@@ -1147,16 +1347,6 @@ function actionForPredicate(predicateSpec) {
1147
1347
  };
1148
1348
  }
1149
1349
 
1150
- // src/_shared/combo.ts
1151
- function cartesianProductOf(arr) {
1152
- return arr.reduce(
1153
- (a, b) => {
1154
- return a.map((x) => b.map((y) => x.concat([y]))).reduce((v, w) => v.concat(w), []);
1155
- },
1156
- [[]]
1157
- );
1158
- }
1159
-
1160
1350
  // src/check/check-dataset.ts
1161
1351
  function expandDatasets(modelSpec, datasetSpec) {
1162
1352
  var _a;
@@ -1185,25 +1375,10 @@ function expandDatasets(modelSpec, datasetSpec) {
1185
1375
  name: match.outputVar.varName
1186
1376
  });
1187
1377
  } else if (match.implVar) {
1188
- const implVar = match.implVar;
1189
- if (implVar.dimensions.length > 0) {
1190
- const baseDatasetKey = match.datasetKey;
1191
- const subscripts = [...implVar.dimensions.map((dim) => dim.subscripts)];
1192
- const subscriptCombos = cartesianProductOf(subscripts);
1193
- for (const subscriptCombo of subscriptCombos) {
1194
- const subIdParts = subscriptCombo.map((sub) => `[${sub.id}]`).join("");
1195
- const subNameParts = subscriptCombo.map((sub) => sub.name).join(",");
1196
- checkDatasets.push({
1197
- datasetKey: `${baseDatasetKey}${subIdParts}`,
1198
- name: `${implVar.varName}[${subNameParts}]`
1199
- });
1200
- }
1201
- } else {
1202
- checkDatasets.push({
1203
- datasetKey: match.datasetKey,
1204
- name: implVar.varName
1205
- });
1206
- }
1378
+ checkDatasets.push({
1379
+ datasetKey: match.datasetKey,
1380
+ name: match.implVar.varName
1381
+ });
1207
1382
  }
1208
1383
  }
1209
1384
  return checkDatasets;
@@ -1611,12 +1786,18 @@ var CheckPlanner = class {
1611
1786
  this.dataRefs = /* @__PURE__ */ new Map();
1612
1787
  this.checkKey = 1;
1613
1788
  }
1614
- addAllChecks(checkSpec, simplifyScenarios) {
1789
+ addAllChecks(checkSpec, skipChecks) {
1790
+ function skipCheckKey(groupName, testName) {
1791
+ return `${groupName.toLowerCase()} :: ${testName.toLowerCase()}`;
1792
+ }
1793
+ const skipChecksSet = new Set(skipChecks.map((check) => skipCheckKey(check.groupName, check.testName)));
1615
1794
  for (const groupSpec of checkSpec.groups) {
1616
1795
  const groupName = groupSpec.describe;
1617
1796
  const planTests = [];
1618
1797
  for (const testSpec of groupSpec.tests) {
1619
1798
  const testName = testSpec.it;
1799
+ const shouldSkip = skipChecksSet.has(skipCheckKey(groupName, testName));
1800
+ const simplifyScenarios = false;
1620
1801
  const checkScenarios = expandScenarios(this.modelSpec, testSpec.scenarios || [], simplifyScenarios);
1621
1802
  const checkDatasets = [];
1622
1803
  for (const datasetSpec of testSpec.datasets) {
@@ -1657,7 +1838,8 @@ var CheckPlanner = class {
1657
1838
  scenario: checkScenario,
1658
1839
  dataset: checkDataset,
1659
1840
  action: checkAction,
1660
- dataRefs
1841
+ dataRefs,
1842
+ skip: shouldSkip
1661
1843
  });
1662
1844
  }
1663
1845
  planDatasets.push({
@@ -1794,6 +1976,7 @@ function checkSummaryFromReport(checkReport) {
1794
1976
  break;
1795
1977
  case "failed":
1796
1978
  case "error":
1979
+ case "skipped":
1797
1980
  predicateSummaries.push({
1798
1981
  checkKey: predicate.checkKey,
1799
1982
  result: predicate.result
@@ -1811,14 +1994,14 @@ function checkSummaryFromReport(checkReport) {
1811
1994
  predicateSummaries
1812
1995
  };
1813
1996
  }
1814
- function checkReportFromSummary(checkConfig, checkSummary) {
1997
+ function checkReportFromSummary(checkConfig, checkSummary, skipChecks = []) {
1815
1998
  const checkSpecResult = parseTestYaml(checkConfig.tests);
1816
1999
  if (checkSpecResult.isErr()) {
1817
2000
  return void 0;
1818
2001
  }
1819
2002
  const checkSpec = checkSpecResult.value;
1820
- const checkPlanner = new CheckPlanner(checkConfig.bundle.model.modelSpec);
1821
- checkPlanner.addAllChecks(checkSpec, false);
2003
+ const checkPlanner = new CheckPlanner(checkConfig.bundle.modelSpec);
2004
+ checkPlanner.addAllChecks(checkSpec, skipChecks);
1822
2005
  const checkPlan = checkPlanner.buildPlan();
1823
2006
  const checkResults = /* @__PURE__ */ new Map();
1824
2007
  for (const predicateSummary of checkSummary.predicateSummaries) {
@@ -1828,98 +2011,115 @@ function checkReportFromSummary(checkConfig, checkSummary) {
1828
2011
  }
1829
2012
 
1830
2013
  // src/comparison/run/comparison-data-coordinator.ts
1831
- import { assertNever as assertNever7 } from "assert-never";
1832
2014
  var ComparisonDataCoordinator = class {
1833
- constructor(bundleModelL, bundleModelR) {
1834
- this.bundleModelL = bundleModelL;
1835
- this.bundleModelR = bundleModelR;
1836
- this.taskQueue = new TaskQueue({
1837
- process: (request) => __async(this, null, function* () {
1838
- switch (request.kind) {
1839
- case "dataset":
1840
- return this.processDatasetRequest(request);
1841
- case "graph-data":
1842
- return this.processGraphDataRequest(request);
1843
- default:
1844
- assertNever7(request);
2015
+ constructor(taskQueue) {
2016
+ this.taskQueue = taskQueue;
2017
+ }
2018
+ /**
2019
+ * Request datasets from the two models.
2020
+ *
2021
+ * @param requestKey The unique key for the request.
2022
+ * @param sourceL The source of the first ("left") dataset. If "left", the datasets will
2023
+ * be fetched from the "left" bundle's model, otherwise they will be fetched from the
2024
+ * "right" bundle's model.
2025
+ * @param scenarioSpecL The scenario used for the first ("left") model of the comparison.
2026
+ * @param sourceR The source of the second ("right") dataset. If "left", the datasets
2027
+ * will be fetched from the "left" bundle's model, otherwise they will be fetched from
2028
+ * the "right" bundle's model.
2029
+ * @param scenarioSpecR The scenario used for the second ("right") model of the comparison.
2030
+ * @param graphId The keys of the datasets to be fetched.
2031
+ * @param onResponse The callback that will be called with the dataset maps.
2032
+ */
2033
+ requestDatasetMaps(requestKey, sourceL, scenarioSpecL, sourceR, scenarioSpecR, datasetKeys, onResponse) {
2034
+ function fetchDatasets(bundleModel, scenarioSpec) {
2035
+ return __async(this, null, function* () {
2036
+ if (scenarioSpec) {
2037
+ return bundleModel.getDatasetsForScenario(scenarioSpec, datasetKeys);
2038
+ } else {
2039
+ return void 0;
2040
+ }
2041
+ });
2042
+ }
2043
+ const task = {
2044
+ key: requestKey,
2045
+ kind: "comparison-data-coordinator",
2046
+ process: (bundleModels) => __async(this, null, function* () {
2047
+ const modelL = sourceL === "left" ? bundleModels.L : bundleModels.R;
2048
+ const modelR = sourceR === "left" ? bundleModels.L : bundleModels.R;
2049
+ let resultL;
2050
+ let resultR;
2051
+ if (modelL === modelR) {
2052
+ resultL = yield fetchDatasets(modelL, scenarioSpecL);
2053
+ resultR = yield fetchDatasets(modelR, scenarioSpecR);
2054
+ } else {
2055
+ const results = yield Promise.all([
2056
+ fetchDatasets(modelL, scenarioSpecL),
2057
+ fetchDatasets(modelR, scenarioSpecR)
2058
+ ]);
2059
+ resultL = results[0];
2060
+ resultR = results[1];
1845
2061
  }
2062
+ onResponse(resultL == null ? void 0 : resultL.datasetMap, resultR == null ? void 0 : resultR.datasetMap);
1846
2063
  })
1847
- });
1848
- }
1849
- processDatasetRequest(request) {
1850
- return __async(this, null, function* () {
1851
- function fetchDatasets(bundleModel, scenarioSpec) {
1852
- return __async(this, null, function* () {
1853
- if (scenarioSpec) {
1854
- return bundleModel.getDatasetsForScenario(scenarioSpec, request.datasetKeys);
1855
- } else {
1856
- return void 0;
1857
- }
1858
- });
1859
- }
1860
- const [resultL, resultR] = yield Promise.all([
1861
- fetchDatasets(this.bundleModelL, request.scenarioSpecL),
1862
- fetchDatasets(this.bundleModelR, request.scenarioSpecR)
1863
- ]);
1864
- return {
1865
- kind: "dataset",
1866
- datasetMapL: resultL == null ? void 0 : resultL.datasetMap,
1867
- datasetMapR: resultR == null ? void 0 : resultR.datasetMap
1868
- };
1869
- });
1870
- }
1871
- processGraphDataRequest(request) {
1872
- return __async(this, null, function* () {
1873
- function fetchGraphData(bundleModel, scenarioSpec) {
1874
- return __async(this, null, function* () {
1875
- if (scenarioSpec) {
1876
- return bundleModel.getGraphDataForScenario(scenarioSpec, request.graphId);
1877
- } else {
1878
- return void 0;
1879
- }
1880
- });
1881
- }
1882
- const [graphDataL, graphDataR] = yield Promise.all([
1883
- fetchGraphData(this.bundleModelL, request.scenarioSpecL),
1884
- fetchGraphData(this.bundleModelR, request.scenarioSpecR)
1885
- ]);
1886
- return {
1887
- kind: "graph-data",
1888
- graphDataL,
1889
- graphDataR
1890
- };
1891
- });
1892
- }
1893
- requestDatasetMaps(requestKey, scenarioSpecL, scenarioSpecR, datasetKeys, onResponse) {
1894
- const request = {
1895
- kind: "dataset",
1896
- scenarioSpecL,
1897
- scenarioSpecR,
1898
- datasetKeys
1899
2064
  };
1900
- this.taskQueue.addTask(requestKey, request, (response) => {
1901
- if (response.kind === "dataset") {
1902
- onResponse(response.datasetMapL, response.datasetMapR);
1903
- }
1904
- });
2065
+ this.taskQueue.addTask(task);
1905
2066
  }
1906
- requestGraphData(requestKey, scenarioSpecL, scenarioSpecR, graphId, onResponse) {
1907
- const request = {
1908
- kind: "graph-data",
1909
- scenarioSpecL,
1910
- scenarioSpecR,
1911
- graphId
2067
+ /**
2068
+ * Request graph data from the two models.
2069
+ *
2070
+ * @param requestKey The unique key for the request.
2071
+ * @param sourceL The source of the first ("left") dataset. If "left", the datasets will
2072
+ * be fetched from the "left" bundle's model, otherwise they will be fetched from the
2073
+ * "right" bundle's model.
2074
+ * @param scenarioSpecL The scenario used for the first ("left") model of the comparison.
2075
+ * @param sourceR The source of the second ("right") dataset. If "left", the datasets
2076
+ * will be fetched from the "left" bundle's model, otherwise they will be fetched from
2077
+ * the "right" bundle's model.
2078
+ * @param scenarioSpecR The scenario used for the second ("right") model of the comparison.
2079
+ * @param graphId The ID of the graph for which data will be fetched.
2080
+ * @param onResponse The callback that will be called with the graph data.
2081
+ */
2082
+ requestGraphData(requestKey, sourceL, scenarioSpecL, sourceR, scenarioSpecR, graphId, onResponse) {
2083
+ function fetchGraphData(bundleModel, scenarioSpec) {
2084
+ return __async(this, null, function* () {
2085
+ if (scenarioSpec) {
2086
+ return bundleModel.getGraphDataForScenario(scenarioSpec, graphId);
2087
+ } else {
2088
+ return void 0;
2089
+ }
2090
+ });
2091
+ }
2092
+ const task = {
2093
+ key: requestKey,
2094
+ kind: "comparison-data-coordinator",
2095
+ process: (bundleModels) => __async(this, null, function* () {
2096
+ const modelL = sourceL === "left" ? bundleModels.L : bundleModels.R;
2097
+ const modelR = sourceR === "left" ? bundleModels.L : bundleModels.R;
2098
+ let graphDataL;
2099
+ let graphDataR;
2100
+ if (modelL === modelR) {
2101
+ graphDataL = yield fetchGraphData(modelL, scenarioSpecL);
2102
+ graphDataR = yield fetchGraphData(modelR, scenarioSpecR);
2103
+ } else {
2104
+ const results = yield Promise.all([
2105
+ fetchGraphData(modelL, scenarioSpecL),
2106
+ fetchGraphData(modelR, scenarioSpecR)
2107
+ ]);
2108
+ graphDataL = results[0];
2109
+ graphDataR = results[1];
2110
+ }
2111
+ onResponse(graphDataL, graphDataR);
2112
+ })
1912
2113
  };
1913
- this.taskQueue.addTask(requestKey, request, (response) => {
1914
- if (response.kind === "graph-data") {
1915
- onResponse(response.graphDataL, response.graphDataR);
1916
- }
1917
- });
2114
+ this.taskQueue.addTask(task);
1918
2115
  }
1919
2116
  cancelRequest(key) {
1920
2117
  this.taskQueue.cancelTask(key);
1921
2118
  }
1922
2119
  };
2120
+ function createComparisonDataCoordinator() {
2121
+ return new ComparisonDataCoordinator(TaskQueue.getInstance());
2122
+ }
1923
2123
 
1924
2124
  // src/comparison/diff-datasets/diff-datasets.ts
1925
2125
  function diffDatasets(datasetL, datasetR) {
@@ -2069,14 +2269,14 @@ function diffGraphs(graphL, graphR, scenarioKey, testSummaries) {
2069
2269
 
2070
2270
  // src/comparison/report/comparison-reporting.ts
2071
2271
  function comparisonSummaryFromReport(comparisonReport) {
2272
+ var _a, _b;
2072
2273
  const terseSummaries = [];
2073
2274
  for (const r of comparisonReport.testReports) {
2074
- if (r.diffReport.validity === "both" && r.diffReport.maxDiff > 0) {
2075
- terseSummaries.push({
2076
- s: r.scenarioKey,
2077
- d: r.datasetKey,
2078
- md: r.diffReport.maxDiff
2079
- });
2275
+ const baselineMaxDiff = (_a = r.baselineDiffReport) == null ? void 0 : _a.maxDiff;
2276
+ const baselineAvgDiff = (_b = r.baselineDiffReport) == null ? void 0 : _b.avgDiff;
2277
+ const summary = testSummaryFromReport(r, baselineMaxDiff, baselineAvgDiff);
2278
+ if (summary) {
2279
+ terseSummaries.push(summary);
2080
2280
  }
2081
2281
  }
2082
2282
  return {
@@ -2085,6 +2285,43 @@ function comparisonSummaryFromReport(comparisonReport) {
2085
2285
  perfReportR: comparisonReport.perfReportR
2086
2286
  };
2087
2287
  }
2288
+ function testSummaryFromReport(r, baselineMaxDiff, baselineAvgDiff) {
2289
+ var _a;
2290
+ function baselineRelativeDiff(diffValue, baselineDiffValue) {
2291
+ if (baselineDiffValue !== void 0) {
2292
+ const epsilon = 1e-6;
2293
+ if (baselineDiffValue === 0) {
2294
+ baselineDiffValue = epsilon;
2295
+ }
2296
+ return diffValue / baselineDiffValue;
2297
+ } else {
2298
+ if (diffValue === 0) {
2299
+ return 0;
2300
+ } else {
2301
+ return 1;
2302
+ }
2303
+ }
2304
+ }
2305
+ if (r.diffReport === void 0) {
2306
+ return {
2307
+ s: r.scenarioKey,
2308
+ d: r.datasetKey
2309
+ };
2310
+ } else if (((_a = r.diffReport) == null ? void 0 : _a.validity) === "both" && r.diffReport.maxDiff > 0) {
2311
+ const maxDiffRelativeToBaseline = baselineRelativeDiff(r.diffReport.maxDiff, baselineMaxDiff);
2312
+ const avgDiffRelativeToBaseline = baselineRelativeDiff(r.diffReport.avgDiff, baselineAvgDiff);
2313
+ return {
2314
+ s: r.scenarioKey,
2315
+ d: r.datasetKey,
2316
+ md: r.diffReport.maxDiff,
2317
+ ad: r.diffReport.avgDiff,
2318
+ mdb: maxDiffRelativeToBaseline,
2319
+ adb: avgDiffRelativeToBaseline
2320
+ };
2321
+ } else {
2322
+ return void 0;
2323
+ }
2324
+ }
2088
2325
  function restoreFromTerseSummaries(comparisonConfig, terseSummaries) {
2089
2326
  const existingSummaries = /* @__PURE__ */ new Map();
2090
2327
  for (const summary of terseSummaries) {
@@ -2097,24 +2334,33 @@ function restoreFromTerseSummaries(comparisonConfig, terseSummaries) {
2097
2334
  for (const datasetKey of datasetKeys) {
2098
2335
  const key = `${scenario.key}::${datasetKey}`;
2099
2336
  const existingSummary = existingSummaries.get(key);
2100
- const maxDiff = (existingSummary == null ? void 0 : existingSummary.md) || 0;
2101
- allTestSummaries.push({
2102
- s: scenario.key,
2103
- d: datasetKey,
2104
- md: maxDiff
2105
- });
2337
+ if (existingSummary) {
2338
+ allTestSummaries.push(existingSummary);
2339
+ } else {
2340
+ allTestSummaries.push({
2341
+ s: scenario.key,
2342
+ d: datasetKey,
2343
+ md: 0,
2344
+ ad: 0,
2345
+ mdb: 0,
2346
+ adb: 0
2347
+ });
2348
+ }
2106
2349
  }
2107
2350
  }
2108
2351
  return allTestSummaries;
2109
2352
  }
2110
2353
 
2111
2354
  // src/comparison/report/buckets.ts
2112
- function getBucketIndex(diffPct, thresholds) {
2113
- if (diffPct === 0) {
2355
+ function getBucketIndex(diff, thresholds) {
2356
+ if (diff === void 0) {
2357
+ return thresholds.length + 2;
2358
+ }
2359
+ if (diff === 0) {
2114
2360
  return 0;
2115
2361
  }
2116
2362
  for (let i = 0; i < thresholds.length; i++) {
2117
- if (diffPct < thresholds[i]) {
2363
+ if (diff < thresholds[i]) {
2118
2364
  return i + 1;
2119
2365
  }
2120
2366
  }
@@ -2122,14 +2368,32 @@ function getBucketIndex(diffPct, thresholds) {
2122
2368
  }
2123
2369
 
2124
2370
  // src/comparison/report/comparison-group-scores.ts
2125
- function getScoresForTestSummaries(testSummaries, thresholds) {
2126
- const diffCountByBucket = Array(thresholds.length + 2).fill(0);
2127
- const totalMaxDiffByBucket = Array(thresholds.length + 2).fill(0);
2371
+ function getScoresForTestSummaries(testSummaries, thresholds, sortMode) {
2372
+ const diffCountByBucket = Array(thresholds.length + 3).fill(0);
2373
+ const totalDiffByBucket = Array(thresholds.length + 3).fill(0);
2128
2374
  let totalDiffCount = 0;
2375
+ let valueKey;
2376
+ switch (sortMode) {
2377
+ case "max-diff":
2378
+ valueKey = "md";
2379
+ break;
2380
+ case "avg-diff":
2381
+ valueKey = "ad";
2382
+ break;
2383
+ case "max-diff-relative":
2384
+ valueKey = "mdb";
2385
+ break;
2386
+ case "avg-diff-relative":
2387
+ valueKey = "adb";
2388
+ break;
2389
+ }
2129
2390
  for (const testSummary of testSummaries) {
2130
- const bucketIndex = getBucketIndex(testSummary.md, thresholds);
2391
+ const value = testSummary[valueKey];
2392
+ const bucketIndex = getBucketIndex(value, thresholds);
2131
2393
  diffCountByBucket[bucketIndex]++;
2132
- totalMaxDiffByBucket[bucketIndex] += testSummary.md;
2394
+ if (value !== void 0) {
2395
+ totalDiffByBucket[bucketIndex] += value;
2396
+ }
2133
2397
  totalDiffCount++;
2134
2398
  }
2135
2399
  let diffPercentByBucket;
@@ -2140,20 +2404,20 @@ function getScoresForTestSummaries(testSummaries, thresholds) {
2140
2404
  }
2141
2405
  return {
2142
2406
  totalDiffCount,
2143
- totalMaxDiffByBucket,
2407
+ totalDiffByBucket,
2144
2408
  diffCountByBucket,
2145
2409
  diffPercentByBucket
2146
2410
  };
2147
2411
  }
2148
2412
 
2149
2413
  // src/comparison/report/comparison-grouping.ts
2150
- import { assertNever as assertNever8 } from "assert-never";
2151
- function categorizeComparisonTestSummaries(comparisonConfig, terseSummaries) {
2414
+ import { assertNever as assertNever7 } from "assert-never";
2415
+ function categorizeComparisonTestSummaries(comparisonConfig, terseSummaries, sortMode) {
2152
2416
  const allTestSummaries = restoreFromTerseSummaries(comparisonConfig, terseSummaries);
2153
2417
  const groupsByScenario = groupComparisonTestSummaries(allTestSummaries, "by-scenario");
2154
- const byScenario = categorizeComparisonGroups(comparisonConfig, [...groupsByScenario.values()]);
2418
+ const byScenario = categorizeComparisonGroups(comparisonConfig, [...groupsByScenario.values()], sortMode);
2155
2419
  const groupsByDataset = groupComparisonTestSummaries(allTestSummaries, "by-dataset");
2156
- const byDataset = categorizeComparisonGroups(comparisonConfig, [...groupsByDataset.values()]);
2420
+ const byDataset = categorizeComparisonGroups(comparisonConfig, [...groupsByDataset.values()], sortMode);
2157
2421
  return {
2158
2422
  allTestSummaries,
2159
2423
  byScenario,
@@ -2172,7 +2436,7 @@ function groupComparisonTestSummaries(testSummaries, groupKind) {
2172
2436
  groupKey = testSummary.s;
2173
2437
  break;
2174
2438
  default:
2175
- assertNever8(groupKind);
2439
+ assertNever7(groupKind);
2176
2440
  }
2177
2441
  const group = groups.get(groupKey);
2178
2442
  if (group) {
@@ -2187,7 +2451,7 @@ function groupComparisonTestSummaries(testSummaries, groupKind) {
2187
2451
  }
2188
2452
  return groups;
2189
2453
  }
2190
- function categorizeComparisonGroups(comparisonConfig, allGroups) {
2454
+ function categorizeComparisonGroups(comparisonConfig, allGroups, sortMode) {
2191
2455
  const allGroupSummaries = /* @__PURE__ */ new Map();
2192
2456
  const withErrors = [];
2193
2457
  const onlyInLeft = [];
@@ -2197,7 +2461,9 @@ function categorizeComparisonGroups(comparisonConfig, allGroups) {
2197
2461
  function addSummaryForGroup(group, root, validInL, validInR) {
2198
2462
  let scores;
2199
2463
  if (validInL && validInR) {
2200
- scores = getScoresForTestSummaries(group.testSummaries, comparisonConfig.thresholds);
2464
+ const isRelativeMode = sortMode === "max-diff-relative" || sortMode === "avg-diff-relative";
2465
+ const thresholds = isRelativeMode ? comparisonConfig.ratioThresholds : comparisonConfig.thresholds;
2466
+ scores = getScoresForTestSummaries(group.testSummaries, thresholds, sortMode);
2201
2467
  }
2202
2468
  const groupSummary = {
2203
2469
  root,
@@ -2206,7 +2472,9 @@ function categorizeComparisonGroups(comparisonConfig, allGroups) {
2206
2472
  };
2207
2473
  allGroupSummaries.set(group.key, groupSummary);
2208
2474
  if (validInL && validInR) {
2209
- if (scores.totalDiffCount !== scores.diffCountByBucket[0]) {
2475
+ const noDiffCount = scores.diffCountByBucket[0];
2476
+ const skippedCount = scores.diffCountByBucket[5];
2477
+ if (scores.totalDiffCount !== noDiffCount + skippedCount) {
2210
2478
  withDiffs.push(groupSummary);
2211
2479
  } else {
2212
2480
  withoutDiffs.push(groupSummary);
@@ -2236,7 +2504,7 @@ function categorizeComparisonGroups(comparisonConfig, allGroups) {
2236
2504
  break;
2237
2505
  }
2238
2506
  default:
2239
- assertNever8(group.kind);
2507
+ assertNever7(group.kind);
2240
2508
  }
2241
2509
  }
2242
2510
  if (withDiffs.length > 1) {
@@ -2298,13 +2566,13 @@ function sortScenarioGroupSummaries(summaries) {
2298
2566
  });
2299
2567
  }
2300
2568
  function compareScores(a, b) {
2301
- if (a.totalMaxDiffByBucket.length !== b.totalMaxDiffByBucket.length) {
2569
+ if (a.totalDiffByBucket.length !== b.totalDiffByBucket.length) {
2302
2570
  return 0;
2303
2571
  }
2304
- const len = a.totalMaxDiffByBucket.length;
2572
+ const len = a.totalDiffByBucket.length;
2305
2573
  for (let i = len - 1; i >= 0; i--) {
2306
- const aTotal = a.totalMaxDiffByBucket[i];
2307
- const bTotal = b.totalMaxDiffByBucket[i];
2574
+ const aTotal = a.totalDiffByBucket[i];
2575
+ const bTotal = b.totalDiffByBucket[i];
2308
2576
  if (aTotal > bTotal) {
2309
2577
  return 1;
2310
2578
  } else if (aTotal < bTotal) {
@@ -2316,7 +2584,7 @@ function compareScores(a, b) {
2316
2584
 
2317
2585
  // src/comparison/config/parse/comparison-parser.ts
2318
2586
  import Ajv2 from "ajv";
2319
- import assertNever9 from "assert-never";
2587
+ import assertNever8 from "assert-never";
2320
2588
  import { err as err2, ok as ok2 } from "neverthrow";
2321
2589
  import yaml2 from "yaml";
2322
2590
 
@@ -2852,7 +3120,7 @@ function parseComparisonSpecs(specSource) {
2852
3120
  parsed = yaml2.parse(specSource.content);
2853
3121
  break;
2854
3122
  default:
2855
- assertNever9(specSource.kind);
3123
+ assertNever8(specSource.kind);
2856
3124
  }
2857
3125
  if (validate(parsed)) {
2858
3126
  for (const specItem of parsed) {
@@ -3079,7 +3347,7 @@ function viewGroupSpecFromParsed(parsedViewGroup) {
3079
3347
  }
3080
3348
 
3081
3349
  // src/comparison/config/resolve/comparison-resolver.ts
3082
- import { assertNever as assertNever11 } from "assert-never";
3350
+ import { assertNever as assertNever10 } from "assert-never";
3083
3351
 
3084
3352
  // src/bundle/model-inputs.ts
3085
3353
  var ModelInputs = class {
@@ -3142,7 +3410,7 @@ var ModelInputs = class {
3142
3410
  };
3143
3411
 
3144
3412
  // src/comparison/config/resolve/comparison-scenario-specs.ts
3145
- import { assertNever as assertNever10 } from "assert-never";
3413
+ import { assertNever as assertNever9 } from "assert-never";
3146
3414
  function scenarioSpecsFromSettings(settings) {
3147
3415
  switch (settings.kind) {
3148
3416
  case "all-inputs-settings": {
@@ -3155,7 +3423,7 @@ function scenarioSpecsFromSettings(settings) {
3155
3423
  return [specL, specR];
3156
3424
  }
3157
3425
  default:
3158
- assertNever10(settings);
3426
+ assertNever9(settings);
3159
3427
  }
3160
3428
  }
3161
3429
  function scenarioSpecFromInputs(inputs, side) {
@@ -3374,7 +3642,7 @@ function resolveScenariosFromSpec(modelInputsL, modelInputsR, scenarioSpec, genK
3374
3642
  ];
3375
3643
  }
3376
3644
  default:
3377
- assertNever11(scenarioSpec);
3645
+ assertNever10(scenarioSpec);
3378
3646
  }
3379
3647
  }
3380
3648
  function resolveScenarioMatrix(modelInputsL, modelInputsR, genKey) {
@@ -3430,7 +3698,7 @@ function resolveScenarioForInputSpecs(modelInputsL, modelInputsR, key, id, title
3430
3698
  case "input-at-value":
3431
3699
  return resolveInputForName(modelInputsL, modelInputsR, inputSpec.inputName, inputSpec.value);
3432
3700
  default:
3433
- assertNever11(inputSpec);
3701
+ assertNever10(inputSpec);
3434
3702
  }
3435
3703
  });
3436
3704
  const settings = {
@@ -3463,7 +3731,7 @@ function resolveScenarioForDistinctInputSpecs(modelInputsL, modelInputsR, key, i
3463
3731
  inputState = resolveInputForNameInModel(modelInputs, inputSpec.inputName, inputSpec.value);
3464
3732
  break;
3465
3733
  default:
3466
- assertNever11(inputSpec);
3734
+ assertNever10(inputSpec);
3467
3735
  }
3468
3736
  if (inputState.error !== void 0) {
3469
3737
  inputsWithErrors.push({
@@ -3679,7 +3947,7 @@ function inputValueAtPosition2(inputVar, position) {
3679
3947
  case "at-maximum":
3680
3948
  return inputVar.maxValue;
3681
3949
  default:
3682
- assertNever11(position);
3950
+ assertNever10(position);
3683
3951
  }
3684
3952
  }
3685
3953
  var ResolvedScenarioGroups = class {
@@ -3738,7 +4006,7 @@ function resolveGraphsFromSpec(modelSpecL, modelSpecR, resolvedGraphGroups, grap
3738
4006
  return [...graphIds];
3739
4007
  }
3740
4008
  default:
3741
- assertNever11(graphsSpec.preset);
4009
+ assertNever10(graphsSpec.preset);
3742
4010
  }
3743
4011
  }
3744
4012
  // eslint-disable-next-line no-fallthrough
@@ -3752,7 +4020,7 @@ function resolveGraphsFromSpec(modelSpecL, modelSpecR, resolvedGraphGroups, grap
3752
4020
  return groupSpec.graphIds;
3753
4021
  }
3754
4022
  default:
3755
- assertNever11(graphsSpec);
4023
+ assertNever10(graphsSpec);
3756
4024
  }
3757
4025
  }
3758
4026
  function resolveViewForScenarioId(resolvedScenarios, viewTitle, viewSubtitle, scenarioId, graphIds, graphOrder) {
@@ -3926,7 +4194,7 @@ function resolveViewGroupFromSpec(modelSpecL, modelSpecR, modelOutputs, resolved
3926
4194
  views.push(resolveViewForScenario(void 0, void 0, scenario, graphIds, graphOrder));
3927
4195
  break;
3928
4196
  default:
3929
- assertNever11(scenario);
4197
+ assertNever10(scenario);
3930
4198
  }
3931
4199
  }
3932
4200
  } else {
@@ -3935,13 +4203,13 @@ function resolveViewGroupFromSpec(modelSpecL, modelSpecR, modelOutputs, resolved
3935
4203
  break;
3936
4204
  }
3937
4205
  default:
3938
- assertNever11(refSpec);
4206
+ assertNever10(refSpec);
3939
4207
  }
3940
4208
  }
3941
4209
  break;
3942
4210
  }
3943
4211
  default:
3944
- assertNever11(viewGroupSpec);
4212
+ assertNever10(viewGroupSpec);
3945
4213
  }
3946
4214
  return {
3947
4215
  kind: "view-group",
@@ -4004,9 +4272,9 @@ var ComparisonDatasetsImpl = class {
4004
4272
  }
4005
4273
  const allOutputVarKeysSet = /* @__PURE__ */ new Set();
4006
4274
  const modelOutputVarKeysSet = /* @__PURE__ */ new Set();
4007
- function addOutputVars(outputVars, handleRenames) {
4275
+ function addOutputVars(outputVars, handleRenames2) {
4008
4276
  outputVars.forEach((outputVar, key) => {
4009
- const remappedKey = handleRenames ? leftKeyForRightKey(key) : key;
4277
+ const remappedKey = handleRenames2 ? leftKeyForRightKey(key) : key;
4010
4278
  allOutputVarKeysSet.add(remappedKey);
4011
4279
  if (outputVar.sourceName === void 0) {
4012
4280
  modelOutputVarKeysSet.add(remappedKey);
@@ -4120,109 +4388,41 @@ var ComparisonScenariosImpl = class {
4120
4388
  }
4121
4389
  };
4122
4390
 
4123
- // src/config/synchronized-model.ts
4124
- function synchronizedBundleModel(sourceModel) {
4125
- const promiseQueue = new PromiseQueue();
4126
- return {
4127
- modelSpec: sourceModel.modelSpec,
4128
- getDatasetsForScenario: (scenarioSpec, datasetKeys) => {
4129
- return promiseQueue.add(() => sourceModel.getDatasetsForScenario(scenarioSpec, datasetKeys));
4130
- },
4131
- getGraphDataForScenario: (scenarioSpec, graphId) => {
4132
- return promiseQueue.add(() => sourceModel.getGraphDataForScenario(scenarioSpec, graphId));
4133
- },
4134
- getGraphLinksForScenario: sourceModel.getGraphLinksForScenario.bind(sourceModel)
4135
- };
4136
- }
4137
- var PromiseQueue = class {
4138
- constructor() {
4139
- this.tasks = [];
4140
- this.runningCount = 0;
4141
- }
4142
- add(f) {
4143
- return new Promise((resolve, reject) => {
4144
- const run = () => __async(this, null, function* () {
4145
- this.runningCount++;
4146
- const promise = f();
4147
- try {
4148
- const result = yield promise;
4149
- resolve(result);
4150
- } catch (e) {
4151
- reject(e);
4152
- } finally {
4153
- this.runningCount--;
4154
- this.runNext();
4155
- }
4156
- });
4157
- if (this.runningCount < 1) {
4158
- run();
4159
- } else {
4160
- this.tasks.push(run);
4161
- }
4162
- });
4163
- }
4164
- runNext() {
4165
- if (this.tasks.length > 0) {
4166
- const task = this.tasks.shift();
4167
- if (task) {
4168
- task();
4169
- }
4170
- }
4171
- }
4172
- };
4173
-
4174
4391
  // src/config/config.ts
4175
4392
  function createConfig(options) {
4176
4393
  return __async(this, null, function* () {
4177
- var _a;
4178
- const origCurrentBundle = yield loadSynchronized(options.current);
4394
+ var _a, _b, _c, _d, _e;
4395
+ let concurrentModels;
4396
+ if (options.concurrency === void 0) {
4397
+ concurrentModels = 1;
4398
+ } else if (options.concurrency === 0) {
4399
+ let coreCount;
4400
+ if (typeof navigator !== "undefined") {
4401
+ coreCount = navigator.hardwareConcurrency;
4402
+ }
4403
+ if (coreCount === void 0 || coreCount < 1) {
4404
+ coreCount = 1;
4405
+ }
4406
+ concurrentModels = Math.max(1, Math.floor(coreCount / 2));
4407
+ } else {
4408
+ concurrentModels = Math.max(1, options.concurrency);
4409
+ }
4410
+ const origCurrentBundle = yield loadBundle(options.current, concurrentModels);
4179
4411
  let currentBundle;
4180
4412
  let comparisonConfig;
4181
4413
  if (options.comparison === void 0) {
4182
4414
  currentBundle = origCurrentBundle;
4183
4415
  } else {
4184
- const baselineBundle = yield loadSynchronized(options.comparison.baseline);
4185
- const renamedDatasetKeys = (_a = options.comparison.datasets) == null ? void 0 : _a.renamedDatasetKeys;
4186
- const invertedRenamedKeys = /* @__PURE__ */ new Map();
4187
- renamedDatasetKeys == null ? void 0 : renamedDatasetKeys.forEach((newKey, oldKey) => {
4188
- invertedRenamedKeys.set(newKey, oldKey);
4189
- });
4190
- const rightKeyForLeftKey = (leftKey) => {
4191
- return (renamedDatasetKeys == null ? void 0 : renamedDatasetKeys.get(leftKey)) || leftKey;
4192
- };
4193
- const leftKeyForRightKey = (rightKey) => {
4194
- return invertedRenamedKeys.get(rightKey) || rightKey;
4195
- };
4196
- const origBundleModelR = origCurrentBundle.model;
4197
- const adjBundleModelR = {
4198
- modelSpec: origBundleModelR.modelSpec,
4199
- getDatasetsForScenario: (scenarioSpec, datasetKeys) => __async(this, null, function* () {
4200
- const rightKeys = datasetKeys.map(rightKeyForLeftKey);
4201
- const result = yield origBundleModelR.getDatasetsForScenario(scenarioSpec, rightKeys);
4202
- const mapWithRightKeys = result.datasetMap;
4203
- const mapWithLeftKeys = /* @__PURE__ */ new Map();
4204
- for (const [rightKey, dataset] of mapWithRightKeys.entries()) {
4205
- const leftKey = leftKeyForRightKey(rightKey);
4206
- mapWithLeftKeys.set(leftKey, dataset);
4207
- }
4208
- return {
4209
- datasetMap: mapWithLeftKeys,
4210
- modelRunTime: result.modelRunTime
4211
- };
4212
- }),
4213
- getGraphDataForScenario: origBundleModelR.getGraphDataForScenario.bind(origBundleModelR),
4214
- getGraphLinksForScenario: origBundleModelR.getGraphLinksForScenario.bind(origBundleModelR)
4215
- };
4216
- currentBundle = __spreadProps(__spreadValues({}, origCurrentBundle), {
4217
- model: adjBundleModelR
4218
- });
4219
- const modelSpecL = baselineBundle.model.modelSpec;
4220
- const modelSpecR = currentBundle.model.modelSpec;
4416
+ const baselineBundle = yield loadBundle(options.comparison.baseline, concurrentModels);
4417
+ currentBundle = handleRenames(origCurrentBundle, (_a = options.comparison.datasets) == null ? void 0 : _a.renamedDatasetKeys);
4418
+ const modelSpecL = baselineBundle.modelSpec;
4419
+ const modelSpecR = currentBundle.modelSpec;
4221
4420
  const comparisonDefs = resolveComparisonSpecsFromSources(modelSpecL, modelSpecR, options.comparison.specs);
4222
4421
  comparisonConfig = {
4223
4422
  bundleL: baselineBundle,
4224
4423
  bundleR: currentBundle,
4225
- thresholds: options.comparison.thresholds,
4424
+ thresholds: (_b = options.comparison.thresholds) != null ? _b : [1, 5, 10],
4425
+ ratioThresholds: (_c = options.comparison.ratioThresholds) != null ? _c : [1, 2, 3],
4226
4426
  scenarios: getComparisonScenarios(comparisonDefs.scenarios),
4227
4427
  datasets: getComparisonDatasets(modelSpecL, modelSpecR, options.comparison.datasets),
4228
4428
  viewGroups: comparisonDefs.viewGroups,
@@ -4233,26 +4433,75 @@ function createConfig(options) {
4233
4433
  bundle: currentBundle,
4234
4434
  tests: options.check.tests
4235
4435
  };
4436
+ const executors = /* @__PURE__ */ new Map();
4437
+ for (let i = 0; i < checkConfig.bundle.models.length; i++) {
4438
+ const bundleModelL = (_d = comparisonConfig == null ? void 0 : comparisonConfig.bundleL.models) == null ? void 0 : _d[i];
4439
+ const bundleModelR = ((_e = comparisonConfig == null ? void 0 : comparisonConfig.bundleR.models) == null ? void 0 : _e[i]) || checkConfig.bundle.models[i];
4440
+ const executor = createExecutor(bundleModelL, bundleModelR);
4441
+ executors.set(`executor-${i}`, executor);
4442
+ }
4443
+ TaskQueue.initialize(executors);
4236
4444
  return {
4237
4445
  check: checkConfig,
4238
4446
  comparison: comparisonConfig
4239
4447
  };
4240
4448
  });
4241
4449
  }
4242
- function loadSynchronized(sourceBundle) {
4450
+ function loadBundle(bundle, concurrentModels) {
4243
4451
  return __async(this, null, function* () {
4244
- const sourceModel = yield sourceBundle.bundle.initModel();
4245
- const synchronizedModel = synchronizedBundleModel(sourceModel);
4452
+ const initCalls = Array.from({ length: concurrentModels }, () => bundle.bundle.initModel());
4453
+ const models = yield Promise.all(initCalls);
4246
4454
  return {
4247
- name: sourceBundle.name,
4248
- version: sourceBundle.bundle.version,
4249
- model: synchronizedModel
4455
+ name: bundle.name,
4456
+ version: bundle.bundle.version,
4457
+ modelSpec: bundle.bundle.modelSpec,
4458
+ models
4459
+ };
4460
+ });
4461
+ }
4462
+ function handleRenames(origCurrentBundle, renamedDatasetKeys) {
4463
+ if (renamedDatasetKeys === void 0 || renamedDatasetKeys.size === 0) {
4464
+ return origCurrentBundle;
4465
+ }
4466
+ const invertedRenamedKeys = /* @__PURE__ */ new Map();
4467
+ renamedDatasetKeys.forEach((newKey, oldKey) => {
4468
+ invertedRenamedKeys.set(newKey, oldKey);
4469
+ });
4470
+ const rightKeyForLeftKey = (leftKey) => {
4471
+ return (renamedDatasetKeys == null ? void 0 : renamedDatasetKeys.get(leftKey)) || leftKey;
4472
+ };
4473
+ const leftKeyForRightKey = (rightKey) => {
4474
+ return invertedRenamedKeys.get(rightKey) || rightKey;
4475
+ };
4476
+ function wrapModel(origBundleModelR) {
4477
+ return {
4478
+ modelSpec: origBundleModelR.modelSpec,
4479
+ getDatasetsForScenario: (scenarioSpec, datasetKeys) => __async(this, null, function* () {
4480
+ const rightKeys = datasetKeys.map(rightKeyForLeftKey);
4481
+ const result = yield origBundleModelR.getDatasetsForScenario(scenarioSpec, rightKeys);
4482
+ const mapWithRightKeys = result.datasetMap;
4483
+ const mapWithLeftKeys = /* @__PURE__ */ new Map();
4484
+ for (const [rightKey, dataset] of mapWithRightKeys.entries()) {
4485
+ const leftKey = leftKeyForRightKey(rightKey);
4486
+ mapWithLeftKeys.set(leftKey, dataset);
4487
+ }
4488
+ return {
4489
+ datasetMap: mapWithLeftKeys,
4490
+ modelRunTime: result.modelRunTime
4491
+ };
4492
+ }),
4493
+ getGraphDataForScenario: origBundleModelR.getGraphDataForScenario.bind(origBundleModelR),
4494
+ getGraphLinksForScenario: origBundleModelR.getGraphLinksForScenario.bind(origBundleModelR)
4250
4495
  };
4496
+ }
4497
+ const wrappedModels = origCurrentBundle.models.map(wrapModel);
4498
+ return __spreadProps(__spreadValues({}, origCurrentBundle), {
4499
+ models: wrappedModels
4251
4500
  });
4252
4501
  }
4253
4502
 
4254
4503
  // src/perf/perf-runner.ts
4255
- import { assertNever as assertNever12 } from "assert-never";
4504
+ import { assertNever as assertNever11 } from "assert-never";
4256
4505
 
4257
4506
  // src/perf/perf-stats.ts
4258
4507
  var PerfStats = class {
@@ -4289,80 +4538,110 @@ var PerfStats = class {
4289
4538
  };
4290
4539
 
4291
4540
  // src/perf/perf-runner.ts
4292
- var warmupCount = 5;
4293
- var runCount = 100;
4541
+ function runPerfWithTaskQueue(taskQueue, callbacks, options) {
4542
+ const perfRunner = new PerfRunner(taskQueue, callbacks, options);
4543
+ perfRunner.start();
4544
+ return () => {
4545
+ perfRunner.cancel();
4546
+ };
4547
+ }
4548
+ function runPerf(callbacks, options) {
4549
+ const taskQueue = TaskQueue.getInstance();
4550
+ return runPerfWithTaskQueue(taskQueue, callbacks, options);
4551
+ }
4294
4552
  var PerfRunner = class {
4295
- constructor(bundleModelL, bundleModelR, mode = "serial") {
4296
- this.bundleModelL = bundleModelL;
4297
- this.bundleModelR = bundleModelR;
4298
- this.mode = mode;
4299
- const scenarioSpec = allInputsAtPositionSpec("at-default");
4300
- this.taskQueue = new TaskQueue({
4301
- process: (request) => __async(this, null, function* () {
4302
- switch (request.kind) {
4303
- case "left": {
4304
- const result = yield bundleModelL.getDatasetsForScenario(scenarioSpec, []);
4305
- return {
4306
- runTimeL: result.modelRunTime
4307
- };
4308
- }
4309
- case "right": {
4310
- const result = yield bundleModelR.getDatasetsForScenario(scenarioSpec, []);
4311
- return {
4312
- runTimeR: result.modelRunTime
4313
- };
4314
- }
4315
- case "both": {
4316
- const [resultL, resultR] = yield Promise.all([
4317
- bundleModelL.getDatasetsForScenario(scenarioSpec, []),
4318
- bundleModelR.getDatasetsForScenario(scenarioSpec, [])
4319
- ]);
4320
- return {
4321
- runTimeL: resultL.modelRunTime,
4322
- runTimeR: resultR.modelRunTime
4323
- };
4324
- }
4325
- default:
4326
- assertNever12(request.kind);
4327
- }
4328
- })
4329
- });
4553
+ constructor(taskQueue, callbacks, options) {
4554
+ this.taskQueue = taskQueue;
4555
+ this.callbacks = callbacks;
4556
+ this.options = options;
4557
+ this.pendingTaskKeys = /* @__PURE__ */ new Set();
4558
+ this.stopped = false;
4559
+ }
4560
+ cancel() {
4561
+ if (!this.stopped) {
4562
+ for (const taskKey of this.pendingTaskKeys) {
4563
+ this.taskQueue.cancelTask(taskKey);
4564
+ }
4565
+ this.stopped = true;
4566
+ }
4330
4567
  }
4331
4568
  start() {
4569
+ var _a, _b, _c, _d, _e, _f;
4332
4570
  const statsL = new PerfStats();
4333
4571
  const statsR = new PerfStats();
4334
- this.taskQueue.onIdle = (error) => {
4335
- var _a;
4336
- if (error) {
4337
- this.onError(error);
4338
- } else {
4339
- (_a = this.onComplete) == null ? void 0 : _a.call(this, statsL.toReport(), statsR.toReport());
4340
- }
4341
- };
4342
- const taskQueue = this.taskQueue;
4343
- function addTask(index, warmup, kind) {
4344
- const key = `${warmup ? "warmup-" : ""}${kind}-${index}`;
4345
- const request = {
4346
- kind
4347
- };
4348
- taskQueue.addTask(key, request, (response) => {
4349
- if (!warmup && response.runTimeL !== void 0) {
4350
- statsL.addRun(response.runTimeL);
4351
- }
4352
- if (!warmup && response.runTimeR !== void 0) {
4353
- statsR.addRun(response.runTimeR);
4354
- }
4355
- });
4572
+ const scenarioSpec = allInputsAtPositionSpec("at-default");
4573
+ const warmupCount = (_b = (_a = this.options) == null ? void 0 : _a.warmupCount) != null ? _b : 5;
4574
+ const runCount = (_d = (_c = this.options) == null ? void 0 : _c.runCount) != null ? _d : 100;
4575
+ let totalTasks = 0;
4576
+ if (((_e = this.options) == null ? void 0 : _e.mode) === "parallel") {
4577
+ totalTasks = warmupCount + runCount;
4578
+ } else {
4579
+ totalTasks = (warmupCount + runCount) * 2;
4356
4580
  }
4581
+ let tasksCompleted = 0;
4582
+ let perfTaskId = 1;
4583
+ const addTask = (warmup, kind) => {
4584
+ const task = {
4585
+ key: `perf-runner-${perfTaskId++}`,
4586
+ kind: "perf-runner",
4587
+ process: (bundleModels) => __async(this, null, function* () {
4588
+ var _a2, _b2, _c2, _d2;
4589
+ this.pendingTaskKeys.delete(task.key);
4590
+ try {
4591
+ let runTimeL;
4592
+ let runTimeR;
4593
+ switch (kind) {
4594
+ case "left": {
4595
+ const result = yield bundleModels.L.getDatasetsForScenario(scenarioSpec, []);
4596
+ runTimeL = result.modelRunTime;
4597
+ break;
4598
+ }
4599
+ case "right": {
4600
+ const result = yield bundleModels.R.getDatasetsForScenario(scenarioSpec, []);
4601
+ runTimeR = result.modelRunTime;
4602
+ break;
4603
+ }
4604
+ case "both": {
4605
+ const [resultL, resultR] = yield Promise.all([
4606
+ bundleModels.L.getDatasetsForScenario(scenarioSpec, []),
4607
+ bundleModels.R.getDatasetsForScenario(scenarioSpec, [])
4608
+ ]);
4609
+ runTimeL = resultL.modelRunTime;
4610
+ runTimeR = resultR.modelRunTime;
4611
+ break;
4612
+ }
4613
+ default:
4614
+ assertNever11(kind);
4615
+ }
4616
+ if (!warmup) {
4617
+ if (runTimeL !== void 0) {
4618
+ statsL.addRun(runTimeL);
4619
+ }
4620
+ if (runTimeR !== void 0) {
4621
+ statsR.addRun(runTimeR);
4622
+ }
4623
+ }
4624
+ tasksCompleted++;
4625
+ if (tasksCompleted === totalTasks) {
4626
+ (_b2 = (_a2 = this.callbacks).onComplete) == null ? void 0 : _b2.call(_a2, statsL.toReport(), statsR.toReport());
4627
+ }
4628
+ } catch (error) {
4629
+ (_d2 = (_c2 = this.callbacks).onError) == null ? void 0 : _d2.call(_c2, error);
4630
+ }
4631
+ })
4632
+ };
4633
+ this.taskQueue.addTask(task);
4634
+ this.pendingTaskKeys.add(task.key);
4635
+ };
4357
4636
  function addTasks(kind) {
4358
4637
  for (let i = 0; i < warmupCount; i++) {
4359
- addTask(i, true, kind);
4638
+ addTask(true, kind);
4360
4639
  }
4361
4640
  for (let i = 0; i < runCount; i++) {
4362
- addTask(i, false, kind);
4641
+ addTask(false, kind);
4363
4642
  }
4364
4643
  }
4365
- if (this.mode === "parallel") {
4644
+ if (((_f = this.options) == null ? void 0 : _f.mode) === "parallel") {
4366
4645
  addTasks("both");
4367
4646
  } else {
4368
4647
  addTasks("left");
@@ -4371,6 +4650,303 @@ var PerfRunner = class {
4371
4650
  }
4372
4651
  };
4373
4652
 
4653
+ // src/trace/trace-runner.ts
4654
+ import { assertNever as assertNever12 } from "assert-never";
4655
+ function runTraceWithTaskQueue(modelSpec, taskQueue, callbacks, options) {
4656
+ const traceRunner = new TraceRunner(taskQueue, callbacks);
4657
+ traceRunner.start(modelSpec, options);
4658
+ return () => {
4659
+ traceRunner.cancel();
4660
+ };
4661
+ }
4662
+ function runTrace(modelSpec, callbacks, options) {
4663
+ const taskQueue = TaskQueue.getInstance();
4664
+ return runTraceWithTaskQueue(modelSpec, taskQueue, callbacks, options);
4665
+ }
4666
+ var TraceRunner = class {
4667
+ constructor(taskQueue, callbacks) {
4668
+ this.taskQueue = taskQueue;
4669
+ this.callbacks = callbacks;
4670
+ this.pendingTaskKeys = /* @__PURE__ */ new Set();
4671
+ this.stopped = false;
4672
+ }
4673
+ cancel() {
4674
+ if (!this.stopped) {
4675
+ for (const taskKey of this.pendingTaskKeys) {
4676
+ this.taskQueue.cancelTask(taskKey);
4677
+ }
4678
+ this.stopped = true;
4679
+ }
4680
+ }
4681
+ start(modelSpec, options) {
4682
+ const allDatasetKeys = [...modelSpec.implVars.keys()];
4683
+ const traceRequests = [];
4684
+ const batchSize = 2e3;
4685
+ for (let i = 0; i < allDatasetKeys.length; i += batchSize) {
4686
+ const datasetKeysForBatch = allDatasetKeys.slice(i, i + batchSize);
4687
+ switch (options.kind) {
4688
+ case "compare-to-bundle":
4689
+ traceRequests.push({
4690
+ kind: "compare-to-bundle",
4691
+ datasetKeys: datasetKeysForBatch,
4692
+ bundleSide0: options.bundleSide0,
4693
+ scenarioSpec0: options.scenarioSpec0,
4694
+ bundleSide1: options.bundleSide1,
4695
+ scenarioSpec1: options.scenarioSpec1
4696
+ });
4697
+ break;
4698
+ case "compare-to-ext-data":
4699
+ traceRequests.push({
4700
+ kind: "compare-to-ext-data",
4701
+ datasetKeys: datasetKeysForBatch,
4702
+ extData: options.extData,
4703
+ bundleSide: options.bundleSide,
4704
+ scenarioSpec: options.scenarioSpec
4705
+ });
4706
+ break;
4707
+ default:
4708
+ assertNever12(options);
4709
+ }
4710
+ }
4711
+ const allDatasetReports = /* @__PURE__ */ new Map();
4712
+ const taskCount = traceRequests.length;
4713
+ let tasksCompleted = 0;
4714
+ let traceTaskId = 1;
4715
+ for (const traceRequest of traceRequests) {
4716
+ const task = {
4717
+ key: `trace-runner-${traceTaskId++}`,
4718
+ kind: "trace-runner",
4719
+ process: (bundleModels) => __async(this, null, function* () {
4720
+ var _a, _b;
4721
+ this.pendingTaskKeys.delete(task.key);
4722
+ let datasetReports;
4723
+ switch (traceRequest.kind) {
4724
+ case "compare-to-bundle":
4725
+ datasetReports = yield processCompareToBundleRequest(traceRequest, bundleModels);
4726
+ break;
4727
+ case "compare-to-ext-data":
4728
+ datasetReports = yield processCompareToExtDataRequest(traceRequest, bundleModels);
4729
+ break;
4730
+ default:
4731
+ assertNever12(traceRequest);
4732
+ }
4733
+ for (const datasetReport of datasetReports) {
4734
+ allDatasetReports.set(datasetReport.datasetKey, datasetReport);
4735
+ }
4736
+ tasksCompleted++;
4737
+ if (tasksCompleted === taskCount) {
4738
+ const traceReport = {
4739
+ datasetReports: allDatasetReports
4740
+ };
4741
+ (_b = (_a = this.callbacks).onComplete) == null ? void 0 : _b.call(_a, traceReport);
4742
+ }
4743
+ })
4744
+ };
4745
+ this.taskQueue.addTask(task);
4746
+ this.pendingTaskKeys.add(task.key);
4747
+ }
4748
+ }
4749
+ };
4750
+ function processCompareToBundleRequest(request, bundleModels) {
4751
+ return __async(this, null, function* () {
4752
+ const bundleModel0 = request.bundleSide0 === "left" ? bundleModels.L : bundleModels.R;
4753
+ const bundleModel1 = request.bundleSide1 === "left" ? bundleModels.L : bundleModels.R;
4754
+ let result0;
4755
+ let result1;
4756
+ if (bundleModel1 === bundleModel0) {
4757
+ result0 = yield bundleModel0.getDatasetsForScenario(request.scenarioSpec0, request.datasetKeys);
4758
+ result1 = yield bundleModel1.getDatasetsForScenario(request.scenarioSpec1, request.datasetKeys);
4759
+ } else {
4760
+ ;
4761
+ [result0, result1] = yield Promise.all([
4762
+ bundleModel0.getDatasetsForScenario(request.scenarioSpec0, request.datasetKeys),
4763
+ bundleModel1.getDatasetsForScenario(request.scenarioSpec1, request.datasetKeys)
4764
+ ]);
4765
+ }
4766
+ const datasetReports = [];
4767
+ for (const datasetKey of request.datasetKeys) {
4768
+ const dataset0 = result0.datasetMap.get(datasetKey);
4769
+ const dataset1 = result1.datasetMap.get(datasetKey);
4770
+ const datasetReport = diffDatasets2(
4771
+ datasetKey,
4772
+ dataset0,
4773
+ dataset1,
4774
+ /*matchPrecisionOfLeft=*/
4775
+ false
4776
+ );
4777
+ datasetReports.push(datasetReport);
4778
+ }
4779
+ return datasetReports;
4780
+ });
4781
+ }
4782
+ function processCompareToExtDataRequest(request, bundleModels) {
4783
+ return __async(this, null, function* () {
4784
+ const bundleModel = request.bundleSide === "left" ? bundleModels.L : bundleModels.R;
4785
+ const resultR = yield bundleModel.getDatasetsForScenario(request.scenarioSpec, request.datasetKeys);
4786
+ const datasetReports = [];
4787
+ for (const datasetKey of request.datasetKeys) {
4788
+ let datasetL = request.extData.get(datasetKey);
4789
+ if (datasetL === void 0) {
4790
+ const datasetKeyParts = datasetKey.split("[");
4791
+ if (datasetKeyParts.length === 2) {
4792
+ const baseKey = datasetKeyParts[0];
4793
+ const keySubParts = datasetKeyParts[1].replace("]", "");
4794
+ const keySubIds = keySubParts.split(",");
4795
+ const subIdPermutations = permutationsOf(keySubIds);
4796
+ for (const subIds of subIdPermutations) {
4797
+ const datDatasetKey = `${baseKey}[${subIds.join(",")}]`;
4798
+ datasetL = request.extData.get(datDatasetKey);
4799
+ if (datasetL !== void 0) {
4800
+ break;
4801
+ }
4802
+ }
4803
+ }
4804
+ if (datasetL === void 0) {
4805
+ console.warn(`WARNING: Failed to find data in dat file for key=${datasetKey}`);
4806
+ }
4807
+ }
4808
+ const datasetR = resultR.datasetMap.get(datasetKey);
4809
+ const datasetReport = diffDatasets2(
4810
+ datasetKey,
4811
+ datasetL,
4812
+ datasetR,
4813
+ /*matchPrecisionOfLeft=*/
4814
+ true
4815
+ );
4816
+ datasetReports.push(datasetReport);
4817
+ }
4818
+ return datasetReports;
4819
+ });
4820
+ }
4821
+ function diffDatasets2(datasetKey, datasetL, datasetR, matchPrecisionOfLeft) {
4822
+ const points = /* @__PURE__ */ new Map();
4823
+ let minValueL = Number.MAX_VALUE;
4824
+ let maxValueL = Number.MIN_VALUE;
4825
+ let minValueR = Number.MAX_VALUE;
4826
+ let maxValueR = Number.MIN_VALUE;
4827
+ let minValue = Number.MAX_VALUE;
4828
+ let maxValue = Number.MIN_VALUE;
4829
+ let minRawDiff = Number.MAX_VALUE;
4830
+ let maxRawDiff = -1;
4831
+ let maxDiffPoint;
4832
+ let diffCount = 0;
4833
+ let totalRawDiff = 0;
4834
+ if (datasetL && datasetR) {
4835
+ const times = /* @__PURE__ */ new Set([...datasetL.keys(), ...datasetR.keys()]);
4836
+ for (const t of times) {
4837
+ const valueL = datasetL.get(t);
4838
+ if (valueL !== void 0) {
4839
+ if (valueL < minValueL) minValueL = valueL;
4840
+ if (valueL > maxValueL) maxValueL = valueL;
4841
+ if (valueL < minValue) minValue = valueL;
4842
+ if (valueL > maxValue) maxValue = valueL;
4843
+ }
4844
+ let valueR;
4845
+ const rawValueR = datasetR.get(t);
4846
+ if (rawValueR !== void 0) {
4847
+ if (matchPrecisionOfLeft && valueL !== void 0) {
4848
+ valueR = matchPrecision(rawValueR, valueL);
4849
+ } else {
4850
+ valueR = rawValueR;
4851
+ }
4852
+ if (valueR < minValueR) minValueR = valueR;
4853
+ if (valueR > maxValueR) maxValueR = valueR;
4854
+ if (valueR < minValue) minValue = valueR;
4855
+ if (valueR > maxValue) maxValue = valueR;
4856
+ }
4857
+ if (valueL === void 0 || valueR === void 0) {
4858
+ continue;
4859
+ }
4860
+ const point = {
4861
+ time: t,
4862
+ valueL,
4863
+ valueR
4864
+ };
4865
+ points.set(t, point);
4866
+ const rawDiff = Math.abs(valueR - valueL);
4867
+ if (rawDiff < minRawDiff) {
4868
+ minRawDiff = rawDiff;
4869
+ }
4870
+ if (rawDiff > maxRawDiff) {
4871
+ maxRawDiff = rawDiff;
4872
+ maxDiffPoint = point;
4873
+ }
4874
+ diffCount++;
4875
+ totalRawDiff += rawDiff;
4876
+ }
4877
+ }
4878
+ function pct(x) {
4879
+ return x * 100;
4880
+ }
4881
+ let minDiff;
4882
+ let maxDiff;
4883
+ let avgDiff;
4884
+ if (minValueL === maxValueL && minValueR === maxValueR) {
4885
+ const diff = pct(maxValueL !== 0 ? Math.abs((maxValueR - maxValueL) / maxValueL) : 1);
4886
+ minDiff = diff;
4887
+ maxDiff = diff;
4888
+ avgDiff = diff;
4889
+ } else {
4890
+ const spread = maxValue - minValue;
4891
+ minDiff = pct(spread > 0 ? minRawDiff / spread : 0);
4892
+ maxDiff = pct(spread > 0 ? maxRawDiff / spread : 0);
4893
+ const avgRawDiff = totalRawDiff / diffCount;
4894
+ avgDiff = pct(spread > 0 ? avgRawDiff / spread : 0);
4895
+ }
4896
+ let validity;
4897
+ if (datasetL && datasetR) {
4898
+ validity = "both";
4899
+ } else if (datasetL) {
4900
+ validity = "left-only";
4901
+ } else if (datasetR) {
4902
+ validity = "right-only";
4903
+ } else {
4904
+ validity = "neither";
4905
+ }
4906
+ return {
4907
+ datasetKey,
4908
+ validity,
4909
+ points,
4910
+ minValue,
4911
+ maxValue,
4912
+ avgDiff,
4913
+ minDiff,
4914
+ maxDiff,
4915
+ maxDiffPoint
4916
+ };
4917
+ }
4918
+ function matchPrecision(x, baseline) {
4919
+ const s = baseline.toString();
4920
+ if (s.includes("e")) {
4921
+ return x;
4922
+ }
4923
+ const parts = s.split(".");
4924
+ if (parts.length < 2) {
4925
+ return x;
4926
+ }
4927
+ const sigDigits = parts[1].replace(/0+$/, "").length;
4928
+ if (sigDigits > 21) {
4929
+ return x;
4930
+ }
4931
+ return parseFloat(x.toFixed(sigDigits));
4932
+ }
4933
+ function permutationsOf(inputArr) {
4934
+ const result = [];
4935
+ const permute = (arr, m = []) => {
4936
+ if (arr.length === 0) {
4937
+ result.push(m);
4938
+ } else {
4939
+ for (let i = 0; i < arr.length; i++) {
4940
+ const curr = arr.slice();
4941
+ const next = curr.splice(i, 1);
4942
+ permute(curr.slice(), m.concat(next));
4943
+ }
4944
+ }
4945
+ };
4946
+ permute(inputArr);
4947
+ return result;
4948
+ }
4949
+
4374
4950
  // src/data/data-planner.ts
4375
4951
  var DataPlanner = class {
4376
4952
  /**
@@ -4557,10 +5133,10 @@ function scenarioPairUid(scenarioSpecL, scenarioSpecR) {
4557
5133
  }
4558
5134
 
4559
5135
  // src/check/check-runner.ts
4560
- function runChecks(checkConfig, checkSpec, dataPlanner, refDataPlanner, simplifyScenarios) {
4561
- const modelSpec = checkConfig.bundle.model.modelSpec;
5136
+ function runChecks(checkConfig, checkSpec, dataPlanner, refDataPlanner, skipChecks) {
5137
+ const modelSpec = checkConfig.bundle.modelSpec;
4562
5138
  const checkPlanner = new CheckPlanner(modelSpec);
4563
- checkPlanner.addAllChecks(checkSpec, simplifyScenarios);
5139
+ checkPlanner.addAllChecks(checkSpec, skipChecks);
4564
5140
  const checkPlan = checkPlanner.buildPlan();
4565
5141
  const refDatasets = /* @__PURE__ */ new Map();
4566
5142
  for (const [dataRefKey, dataRef] of checkPlan.dataRefs.entries()) {
@@ -4573,11 +5149,15 @@ function runChecks(checkConfig, checkSpec, dataPlanner, refDataPlanner, simplify
4573
5149
  }
4574
5150
  const checkResults = /* @__PURE__ */ new Map();
4575
5151
  for (const [checkKey, checkTask] of checkPlan.tasks.entries()) {
4576
- dataPlanner.addRequest(void 0, checkTask.scenario.spec, checkTask.dataset.datasetKey, (datasets) => {
4577
- const dataset = datasets.datasetR;
4578
- const checkResult = runCheck(checkTask, dataset, refDatasets);
4579
- checkResults.set(checkKey, checkResult);
4580
- });
5152
+ if (checkTask.skip === true) {
5153
+ checkResults.set(checkKey, { status: "skipped" });
5154
+ } else {
5155
+ dataPlanner.addRequest(void 0, checkTask.scenario.spec, checkTask.dataset.datasetKey, (datasets) => {
5156
+ const dataset = datasets.datasetR;
5157
+ const checkResult = runCheck(checkTask, dataset, refDatasets);
5158
+ checkResults.set(checkKey, checkResult);
5159
+ });
5160
+ }
4581
5161
  }
4582
5162
  return () => {
4583
5163
  return buildCheckReport(checkPlan, checkResults);
@@ -4642,21 +5222,73 @@ function runCheck(checkTask, dataset, refDatasets) {
4642
5222
  }
4643
5223
 
4644
5224
  // src/comparison/run/comparison-runner.ts
4645
- function runComparisons(comparisonConfig, dataPlanner) {
5225
+ function runComparisons(comparisonConfig, dataPlanner, skipScenarios) {
5226
+ function skipScenarioKey(title, subtitle) {
5227
+ let key = title.toLowerCase();
5228
+ if (subtitle) {
5229
+ key += ` :: ${subtitle.toLowerCase()}`;
5230
+ }
5231
+ return key;
5232
+ }
5233
+ const skipScenariosSet = new Set(skipScenarios.map((scenario) => skipScenarioKey(scenario.title, scenario.subtitle)));
5234
+ const allScenarios = [...comparisonConfig.scenarios.getAllScenarios()];
5235
+ let baselineScenario;
5236
+ const baselineScenarioIndex = allScenarios.findIndex((scenario) => {
5237
+ const settings = scenario.settings;
5238
+ return settings.kind === "all-inputs-settings" && settings.position === "at-default";
5239
+ });
5240
+ if (baselineScenarioIndex !== -1) {
5241
+ baselineScenario = allScenarios.splice(baselineScenarioIndex, 1)[0];
5242
+ }
4646
5243
  const testReports = [];
4647
- for (const scenario of comparisonConfig.scenarios.getAllScenarios()) {
5244
+ const baselineDiffReports = /* @__PURE__ */ new Map();
5245
+ function runComparisonsForScenario(scenario, isBaseline) {
4648
5246
  const datasetKeys = comparisonConfig.datasets.getDatasetKeysForScenario(scenario);
4649
- for (const datasetKey of datasetKeys) {
4650
- dataPlanner.addRequest(scenario.specL, scenario.specR, datasetKey, (datasets) => {
4651
- const diffReport = diffDatasets(datasets.datasetL, datasets.datasetR);
5247
+ const shouldSkip = skipScenariosSet.has(skipScenarioKey(scenario.title, scenario.subtitle));
5248
+ if (shouldSkip) {
5249
+ for (const datasetKey of datasetKeys) {
4652
5250
  testReports.push({
4653
5251
  scenarioKey: scenario.key,
4654
5252
  datasetKey,
4655
- diffReport
5253
+ diffReport: void 0
4656
5254
  });
5255
+ }
5256
+ return;
5257
+ }
5258
+ for (const datasetKey of datasetKeys) {
5259
+ dataPlanner.addRequest(scenario.specL, scenario.specR, datasetKey, (datasets) => {
5260
+ const diffReport = diffDatasets(datasets.datasetL, datasets.datasetR);
5261
+ if (isBaseline) {
5262
+ baselineDiffReports.set(datasetKey, diffReport);
5263
+ testReports.push({
5264
+ scenarioKey: scenario.key,
5265
+ datasetKey,
5266
+ diffReport
5267
+ });
5268
+ } else {
5269
+ const baselineDiffReport = baselineDiffReports.get(datasetKey);
5270
+ testReports.push({
5271
+ scenarioKey: scenario.key,
5272
+ datasetKey,
5273
+ diffReport,
5274
+ baselineDiffReport
5275
+ });
5276
+ }
4657
5277
  });
4658
5278
  }
4659
5279
  }
5280
+ runComparisonsForScenario(
5281
+ baselineScenario,
5282
+ /*isBaseline=*/
5283
+ true
5284
+ );
5285
+ for (const scenario of allScenarios) {
5286
+ runComparisonsForScenario(
5287
+ scenario,
5288
+ /*isBaseline=*/
5289
+ false
5290
+ );
5291
+ }
4660
5292
  return () => {
4661
5293
  return testReports;
4662
5294
  };
@@ -4664,74 +5296,52 @@ function runComparisons(comparisonConfig, dataPlanner) {
4664
5296
 
4665
5297
  // src/suite/suite-runner.ts
4666
5298
  var SuiteRunner = class {
4667
- constructor(config, callbacks) {
5299
+ constructor(config, taskQueue, callbacks) {
4668
5300
  this.config = config;
5301
+ this.taskQueue = taskQueue;
4669
5302
  this.callbacks = callbacks;
4670
5303
  this.perfStatsL = new PerfStats();
4671
5304
  this.perfStatsR = new PerfStats();
5305
+ this.pendingTaskKeys = /* @__PURE__ */ new Set();
4672
5306
  this.stopped = false;
4673
- this.taskQueue = new TaskQueue({
4674
- process: (request) => {
4675
- return this.processRequest(request);
4676
- }
4677
- });
4678
5307
  }
4679
5308
  cancel() {
4680
5309
  if (!this.stopped) {
5310
+ for (const taskKey of this.pendingTaskKeys) {
5311
+ this.taskQueue.cancelTask(taskKey);
5312
+ }
4681
5313
  this.stopped = true;
4682
- this.taskQueue.shutdown();
4683
5314
  }
4684
5315
  }
4685
5316
  start(options) {
4686
5317
  var _a, _b, _c, _d, _e, _f, _g, _h;
4687
5318
  (_b = (_a = this.callbacks).onProgress) == null ? void 0 : _b.call(_a, 0);
4688
- const modelSpec = this.config.check.bundle.model.modelSpec;
4689
- const dataPlanner = new DataPlanner(modelSpec.outputVars.size);
4690
- const refDataPlanner = new DataPlanner(modelSpec.outputVars.size);
5319
+ const modelSpecR = this.config.check.bundle.modelSpec;
5320
+ const dataPlanner = new DataPlanner(modelSpecR.outputVars.size);
5321
+ const refDataPlanner = new DataPlanner(modelSpecR.outputVars.size);
4691
5322
  const checkSpecResult = parseTestYaml(this.config.check.tests);
4692
5323
  if (checkSpecResult.isErr()) {
4693
5324
  (_d = (_c = this.callbacks).onError) == null ? void 0 : _d.call(_c, checkSpecResult.error);
4694
5325
  return;
4695
5326
  }
4696
5327
  const checkSpec = checkSpecResult.value;
4697
- const simplifyScenarios = (options == null ? void 0 : options.simplifyScenarios) === true;
4698
- const buildCheckReport2 = runChecks(this.config.check, checkSpec, dataPlanner, refDataPlanner, simplifyScenarios);
5328
+ const skipChecks = (options == null ? void 0 : options.skipChecks) || [];
5329
+ const buildCheckReport2 = runChecks(this.config.check, checkSpec, dataPlanner, refDataPlanner, skipChecks);
4699
5330
  let buildComparisonTestReports;
4700
5331
  if (this.config.comparison) {
4701
- buildComparisonTestReports = runComparisons(this.config.comparison, dataPlanner);
5332
+ const skipScenarios = (options == null ? void 0 : options.skipComparisonScenarios) || [];
5333
+ buildComparisonTestReports = runComparisons(this.config.comparison, dataPlanner, skipScenarios);
4702
5334
  }
4703
- this.taskQueue.onIdle = (error) => {
4704
- var _a2, _b2, _c2, _d2;
4705
- if (this.stopped) {
4706
- return;
4707
- }
4708
- if (error) {
4709
- (_b2 = (_a2 = this.callbacks).onError) == null ? void 0 : _b2.call(_a2, error);
4710
- } else {
4711
- const checkReport = buildCheckReport2();
4712
- let comparisonReport;
4713
- if (this.config.comparison) {
4714
- comparisonReport = {
4715
- testReports: buildComparisonTestReports(),
4716
- perfReportL: this.perfStatsL.toReport(),
4717
- perfReportR: this.perfStatsR.toReport()
4718
- };
4719
- }
4720
- (_d2 = (_c2 = this.callbacks).onComplete) == null ? void 0 : _d2.call(_c2, {
4721
- checkReport,
4722
- comparisonReport
4723
- });
4724
- }
4725
- };
4726
5335
  const refDataPlan = refDataPlanner.buildPlan();
4727
5336
  const dataPlan = dataPlanner.buildPlan();
4728
5337
  const dataRequests = [...refDataPlan.requests, ...dataPlan.requests];
4729
5338
  const taskCount = dataRequests.length;
4730
5339
  if (taskCount === 0) {
5340
+ const checkReport = buildCheckReport2();
4731
5341
  let comparisonReport;
4732
5342
  if (this.config.comparison) {
4733
5343
  comparisonReport = {
4734
- testReports: [],
5344
+ testReports: buildComparisonTestReports(),
4735
5345
  perfReportL: this.perfStatsL.toReport(),
4736
5346
  perfReportR: this.perfStatsR.toReport()
4737
5347
  };
@@ -4739,26 +5349,54 @@ var SuiteRunner = class {
4739
5349
  this.cancel();
4740
5350
  (_f = (_e = this.callbacks).onProgress) == null ? void 0 : _f.call(_e, 1);
4741
5351
  (_h = (_g = this.callbacks).onComplete) == null ? void 0 : _h.call(_g, {
4742
- checkReport: {
4743
- groups: []
4744
- },
5352
+ checkReport,
4745
5353
  comparisonReport
4746
5354
  });
4747
5355
  return;
4748
5356
  }
5357
+ const buildReport = (error) => {
5358
+ var _a2, _b2, _c2, _d2;
5359
+ if (error) {
5360
+ (_b2 = (_a2 = this.callbacks).onError) == null ? void 0 : _b2.call(_a2, error);
5361
+ } else {
5362
+ const checkReport = buildCheckReport2();
5363
+ let comparisonReport;
5364
+ if (this.config.comparison) {
5365
+ comparisonReport = {
5366
+ testReports: buildComparisonTestReports(),
5367
+ perfReportL: this.perfStatsL.toReport(),
5368
+ perfReportR: this.perfStatsR.toReport()
5369
+ };
5370
+ }
5371
+ (_d2 = (_c2 = this.callbacks).onComplete) == null ? void 0 : _d2.call(_c2, {
5372
+ checkReport,
5373
+ comparisonReport
5374
+ });
5375
+ }
5376
+ };
4749
5377
  let tasksCompleted = 0;
4750
5378
  let dataTaskId = 1;
4751
5379
  for (const dataRequest of dataRequests) {
4752
- this.taskQueue.addTask(`data${dataTaskId++}`, dataRequest, () => {
4753
- var _a2, _b2;
4754
- tasksCompleted++;
4755
- (_b2 = (_a2 = this.callbacks).onProgress) == null ? void 0 : _b2.call(_a2, tasksCompleted / taskCount);
4756
- });
5380
+ const task = {
5381
+ key: `suite-runner-${dataTaskId++}`,
5382
+ kind: "suite-runner",
5383
+ process: (bundleModels) => __async(this, null, function* () {
5384
+ var _a2, _b2;
5385
+ this.pendingTaskKeys.delete(task.key);
5386
+ yield this.processRequest(dataRequest, bundleModels);
5387
+ tasksCompleted++;
5388
+ (_b2 = (_a2 = this.callbacks).onProgress) == null ? void 0 : _b2.call(_a2, tasksCompleted / taskCount);
5389
+ if (tasksCompleted === taskCount) {
5390
+ buildReport();
5391
+ }
5392
+ })
5393
+ };
5394
+ this.taskQueue.addTask(task);
5395
+ this.pendingTaskKeys.add(task.key);
4757
5396
  }
4758
5397
  }
4759
- processRequest(request) {
5398
+ processRequest(request, bundleModels) {
4760
5399
  return __async(this, null, function* () {
4761
- var _a, _b;
4762
5400
  const datasetKeySet = /* @__PURE__ */ new Set();
4763
5401
  for (const dataTask of request.dataTasks) {
4764
5402
  datasetKeySet.add(dataTask.datasetKey);
@@ -4773,8 +5411,8 @@ var SuiteRunner = class {
4773
5411
  }
4774
5412
  });
4775
5413
  }
4776
- const bundleModelL = (_a = this.config.comparison) == null ? void 0 : _a.bundleL.model;
4777
- const bundleModelR = ((_b = this.config.comparison) == null ? void 0 : _b.bundleR.model) || this.config.check.bundle.model;
5414
+ const bundleModelL = bundleModels.L;
5415
+ const bundleModelR = bundleModels.R;
4778
5416
  const [datasetsResultL, datasetsResultR] = yield Promise.all([
4779
5417
  getDatasets(bundleModelL, request.scenarioSpecL),
4780
5418
  getDatasets(bundleModelR, request.scenarioSpecR)
@@ -4798,22 +5436,29 @@ var SuiteRunner = class {
4798
5436
  });
4799
5437
  }
4800
5438
  };
4801
- function runSuite(config, callbacks, options) {
4802
- const suiteRunner = new SuiteRunner(config, callbacks);
5439
+ function runSuiteWithTaskQueue(config, taskQueue, callbacks, options) {
5440
+ const suiteRunner = new SuiteRunner(config, taskQueue, callbacks);
4803
5441
  suiteRunner.start(options);
4804
5442
  return () => {
4805
5443
  suiteRunner.cancel();
4806
5444
  };
4807
5445
  }
5446
+ function runSuite(config, callbacks, options) {
5447
+ const taskQueue = TaskQueue.getInstance();
5448
+ return runSuiteWithTaskQueue(config, taskQueue, callbacks, options);
5449
+ }
4808
5450
 
4809
5451
  // src/suite/suite-reporting.ts
4810
- function suiteSummaryFromReport(suiteReport) {
5452
+ function suiteSummaryFromReport(suiteReport, elapsedMillis) {
4811
5453
  const checkSummary = checkSummaryFromReport(suiteReport.checkReport);
4812
5454
  let comparisonSummary;
4813
5455
  if (suiteReport.comparisonReport) {
4814
5456
  comparisonSummary = comparisonSummaryFromReport(suiteReport.comparisonReport);
4815
5457
  }
5458
+ const date = (/* @__PURE__ */ new Date()).toISOString();
4816
5459
  return {
5460
+ date,
5461
+ elapsed: elapsedMillis,
4817
5462
  checkSummary,
4818
5463
  comparisonSummary
4819
5464
  };
@@ -4821,20 +5466,27 @@ function suiteSummaryFromReport(suiteReport) {
4821
5466
  export {
4822
5467
  CheckDataCoordinator,
4823
5468
  ComparisonDataCoordinator,
4824
- PerfRunner,
4825
5469
  PerfStats,
4826
5470
  categorizeComparisonTestSummaries,
4827
5471
  checkReportFromSummary,
4828
5472
  checkSummaryFromReport,
4829
5473
  comparisonSummaryFromReport,
5474
+ createCheckDataCoordinator,
5475
+ createCheckDataCoordinatorForTests,
5476
+ createComparisonDataCoordinator,
4830
5477
  createConfig,
4831
5478
  datasetMessage,
5479
+ decodeImplVars,
4832
5480
  diffDatasets,
4833
5481
  diffGraphs,
5482
+ encodeImplVars,
4834
5483
  getScoresForTestSummaries,
4835
5484
  predicateMessage,
5485
+ runPerf,
4836
5486
  runSuite,
5487
+ runTrace,
4837
5488
  scenarioMessage,
4838
- suiteSummaryFromReport
5489
+ suiteSummaryFromReport,
5490
+ testSummaryFromReport
4839
5491
  };
4840
5492
  //# sourceMappingURL=index.js.map