minotor 11.1.2 → 11.2.0

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.
Files changed (71) hide show
  1. package/.cspell.json +7 -1
  2. package/CHANGELOG.md +3 -3
  3. package/README.md +111 -86
  4. package/dist/cli/perf.d.ts +57 -18
  5. package/dist/cli.mjs +1371 -342
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/parser.cjs.js +57 -4
  8. package/dist/parser.cjs.js.map +1 -1
  9. package/dist/parser.esm.js +57 -4
  10. package/dist/parser.esm.js.map +1 -1
  11. package/dist/router.cjs.js +1 -1
  12. package/dist/router.cjs.js.map +1 -1
  13. package/dist/router.d.ts +5 -5
  14. package/dist/router.esm.js +1 -1
  15. package/dist/router.esm.js.map +1 -1
  16. package/dist/router.umd.js +1 -1
  17. package/dist/router.umd.js.map +1 -1
  18. package/dist/routing/__tests__/access.test.d.ts +1 -0
  19. package/dist/routing/__tests__/plainRouter.test.d.ts +1 -0
  20. package/dist/routing/__tests__/rangeResult.test.d.ts +1 -0
  21. package/dist/routing/__tests__/rangeRouter.test.d.ts +1 -0
  22. package/dist/routing/__tests__/rangeState.test.d.ts +1 -0
  23. package/dist/routing/__tests__/raptor.test.d.ts +1 -0
  24. package/dist/routing/__tests__/state.test.d.ts +1 -0
  25. package/dist/routing/access.d.ts +55 -0
  26. package/dist/routing/plainRouter.d.ts +21 -0
  27. package/dist/routing/plotter.d.ts +9 -0
  28. package/dist/routing/query.d.ts +132 -13
  29. package/dist/routing/rangeResult.d.ts +155 -0
  30. package/dist/routing/rangeRouter.d.ts +24 -0
  31. package/dist/routing/rangeState.d.ts +83 -0
  32. package/dist/routing/raptor.d.ts +96 -0
  33. package/dist/routing/result.d.ts +27 -7
  34. package/dist/routing/route.d.ts +5 -21
  35. package/dist/routing/router.d.ts +20 -91
  36. package/dist/routing/state.d.ts +92 -17
  37. package/dist/timetable/route.d.ts +8 -0
  38. package/dist/timetable/timetable.d.ts +17 -1
  39. package/package.json +1 -1
  40. package/src/__e2e__/benchmark.json +18 -0
  41. package/src/__e2e__/router.test.ts +461 -127
  42. package/src/cli/minotor.ts +39 -3
  43. package/src/cli/perf.ts +324 -60
  44. package/src/cli/repl.ts +96 -41
  45. package/src/router.ts +11 -3
  46. package/src/routing/__tests__/access.test.ts +294 -0
  47. package/src/routing/__tests__/plainRouter.test.ts +1633 -0
  48. package/src/routing/__tests__/plotter.test.ts +8 -8
  49. package/src/routing/__tests__/rangeResult.test.ts +273 -0
  50. package/src/routing/__tests__/rangeRouter.test.ts +472 -0
  51. package/src/routing/__tests__/rangeState.test.ts +246 -0
  52. package/src/routing/__tests__/raptor.test.ts +366 -0
  53. package/src/routing/__tests__/result.test.ts +27 -27
  54. package/src/routing/__tests__/route.test.ts +28 -0
  55. package/src/routing/__tests__/router.test.ts +75 -1587
  56. package/src/routing/__tests__/state.test.ts +78 -0
  57. package/src/routing/access.ts +144 -0
  58. package/src/routing/plainRouter.ts +60 -0
  59. package/src/routing/plotter.ts +53 -6
  60. package/src/routing/query.ts +116 -13
  61. package/src/routing/rangeResult.ts +292 -0
  62. package/src/routing/rangeRouter.ts +167 -0
  63. package/src/routing/rangeState.ts +150 -0
  64. package/src/routing/raptor.ts +416 -0
  65. package/src/routing/result.ts +68 -26
  66. package/src/routing/route.ts +15 -53
  67. package/src/routing/router.ts +40 -480
  68. package/src/routing/state.ts +191 -32
  69. package/src/timetable/__tests__/timetable.test.ts +373 -0
  70. package/src/timetable/route.ts +16 -4
  71. package/src/timetable/timetable.ts +54 -1
@@ -12,8 +12,11 @@ import {
12
12
  import { Router, StopsIndex, Timetable } from '../router.js';
13
13
  import {
14
14
  loadQueriesFromJson,
15
+ loadRangeQueriesFromJson,
15
16
  prettyPrintPerformanceResults,
16
17
  testBestRoutePerformance,
18
+ testRangeResultPerformance,
19
+ testRangeRouterPerformance,
17
20
  testRouterPerformance,
18
21
  } from './perf.js';
19
22
  import { startRepl } from './repl.js';
@@ -160,17 +163,50 @@ program
160
163
  const queries = loadQueriesFromJson(routesPath, stopsIndex);
161
164
  const iterations = parseInt(options.iterations, 10);
162
165
 
163
- const routerResults = testRouterPerformance(router, queries, iterations);
164
- prettyPrintPerformanceResults(routerResults, 'Router Performance');
166
+ const routerResults = testRouterPerformance(
167
+ router,
168
+ queries,
169
+ iterations,
170
+ stopsIndex,
171
+ );
172
+ prettyPrintPerformanceResults(
173
+ routerResults,
174
+ 'Point queries — router.route()',
175
+ );
165
176
 
166
177
  const bestRouteResults = testBestRoutePerformance(
167
178
  router,
168
179
  queries,
169
180
  iterations,
181
+ stopsIndex,
170
182
  );
171
183
  prettyPrintPerformanceResults(
172
184
  bestRouteResults,
173
- 'bestRoute Performance (reconstruction only)',
185
+ 'Point queries — result.bestRoute() (reconstruction only)',
186
+ );
187
+
188
+ const rangeQueries = loadRangeQueriesFromJson(routesPath, stopsIndex);
189
+
190
+ const rangeRouterResults = testRangeRouterPerformance(
191
+ router,
192
+ rangeQueries,
193
+ iterations,
194
+ stopsIndex,
195
+ );
196
+ prettyPrintPerformanceResults(
197
+ rangeRouterResults,
198
+ 'Range queries — router.rangeRoute()',
199
+ );
200
+
201
+ const rangeResultResults = testRangeResultPerformance(
202
+ router,
203
+ rangeQueries,
204
+ iterations,
205
+ stopsIndex,
206
+ );
207
+ prettyPrintPerformanceResults(
208
+ rangeResultResults,
209
+ 'Range queries — rangeResult.getRoutes() (reconstruction only)',
174
210
  );
175
211
  },
176
212
  );
package/src/cli/perf.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import fs from 'fs';
2
2
  import { performance } from 'perf_hooks';
3
3
 
4
- import { Query, Router, StopsIndex } from '../router.js';
5
- import { timeFromString } from '../timetable/time.js';
4
+ import {
5
+ Query,
6
+ RangeQuery,
7
+ RangeResult,
8
+ Router,
9
+ StopsIndex,
10
+ } from '../router.js';
11
+ import { timeFromString, timeToString } from '../timetable/time.js';
6
12
 
7
13
  type PerformanceResult = {
8
- task: Query;
14
+ label: string;
9
15
  meanTimeUs: number;
10
16
  meanMemoryMb: number;
11
17
  };
@@ -14,18 +20,78 @@ type SerializedQuery = {
14
20
  from: string;
15
21
  to: string[];
16
22
  departureTime: string;
23
+ lastDepartureTime?: string;
17
24
  maxTransfers?: number;
18
25
  };
19
26
 
27
+ // ─── Table renderer ───────────────────────────────────────────────────────────
28
+
29
+ type Column = {
30
+ header: string;
31
+ width: number;
32
+ align: 'left' | 'right';
33
+ };
34
+
35
+ const renderTable = (
36
+ columns: Column[],
37
+ rows: string[][],
38
+ footerRow: string[],
39
+ ): string => {
40
+ const bar = (l: string, m: string, r: string) =>
41
+ l + columns.map((c) => '─'.repeat(c.width + 2)).join(m) + r;
42
+
43
+ const renderRow = (cells: string[]) =>
44
+ '│' +
45
+ cells
46
+ .map((cell, i) => {
47
+ const width = columns[i]?.width ?? 0;
48
+ const align = columns[i]?.align ?? 'left';
49
+ const padded =
50
+ align === 'right' ? cell.padStart(width) : cell.padEnd(width);
51
+ return ` ${padded} `;
52
+ })
53
+ .join('│') +
54
+ '│';
55
+
56
+ return [
57
+ bar('┌', '┬', '┐'),
58
+ renderRow(columns.map((c) => c.header)),
59
+ bar('├', '┼', '┤'),
60
+ ...rows.map(renderRow),
61
+ bar('├', '┼', '┤'),
62
+ renderRow(footerRow),
63
+ bar('└', '┴', '┘'),
64
+ ].join('\n');
65
+ };
66
+
67
+ // ─── Query label ──────────────────────────────────────────────────────────────
68
+
69
+ const buildQueryLabel = (query: Query, stopsIndex: StopsIndex): string => {
70
+ const fromName =
71
+ stopsIndex.findStopById(query.from)?.name ?? String(query.from);
72
+
73
+ const toNames = [...query.to]
74
+ .map((id) => stopsIndex.findStopById(id)?.name ?? String(id))
75
+ .join(' / ');
76
+
77
+ const dep = timeToString(query.departureTime);
78
+
79
+ if (query instanceof RangeQuery) {
80
+ const lastDep = timeToString(query.lastDepartureTime);
81
+ return `${fromName} → ${toNames} ${dep}–${lastDep}`;
82
+ }
83
+
84
+ return `${fromName} → ${toNames} ${dep}`;
85
+ };
86
+
87
+ // ─── Query loaders ────────────────────────────────────────────────────────────
88
+
20
89
  /**
21
90
  * Loads a list of routing queries from a JSON file and resolves the
22
91
  * human-readable stop IDs to the internal numeric IDs used by the router.
23
92
  *
24
- * The file must contain a JSON array whose elements each have the shape:
25
- * ```json
26
- * { "from": "STOP_A", "to": ["STOP_B", "STOP_C"], "departureTime": "08:30:00" }
27
- * ```
28
- * An optional `maxTransfers` integer field is also supported.
93
+ * Only entries that do **not** carry a `lastDepartureTime` field are loaded
94
+ * range-query entries are silently skipped.
29
95
  *
30
96
  * @param filePath - Path to the JSON file containing the serialized queries.
31
97
  * @param stopsIndex - The stops index used to resolve source stop IDs to the
@@ -44,31 +110,88 @@ export const loadQueriesFromJson = (
44
110
  fileContent,
45
111
  ) as SerializedQuery[];
46
112
 
47
- return serializedQueries.map((serializedQuery) => {
48
- const fromStop = stopsIndex.findStopBySourceStopId(serializedQuery.from);
49
- const toStops = Array.from(serializedQuery.to).map((stopId) =>
50
- stopsIndex.findStopBySourceStopId(stopId),
51
- );
113
+ return serializedQueries
114
+ .filter((q) => q.lastDepartureTime === undefined)
115
+ .map((serializedQuery) => {
116
+ const fromStop = stopsIndex.findStopBySourceStopId(serializedQuery.from);
117
+ const toStops = Array.from(serializedQuery.to).map((stopId) =>
118
+ stopsIndex.findStopBySourceStopId(stopId),
119
+ );
120
+
121
+ if (!fromStop || toStops.some((toStop) => !toStop)) {
122
+ throw new Error(
123
+ `Invalid task: Start or end station not found for task ${JSON.stringify(serializedQuery)}`,
124
+ );
125
+ }
126
+ const queryBuilder = new Query.Builder()
127
+ .from(fromStop.id)
128
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
129
+ .to(new Set(toStops.map((stop) => stop!.id)))
130
+ .departureTime(timeFromString(serializedQuery.departureTime));
131
+
132
+ if (serializedQuery.maxTransfers !== undefined) {
133
+ queryBuilder.maxTransfers(serializedQuery.maxTransfers);
134
+ }
135
+
136
+ return queryBuilder.build();
137
+ });
138
+ };
139
+
140
+ /**
141
+ * Loads a list of range routing queries from a JSON file and resolves the
142
+ * human-readable stop IDs to the internal numeric IDs used by the router.
143
+ *
144
+ * Only entries that carry a `lastDepartureTime` field are loaded — plain
145
+ * point-query entries are silently skipped.
146
+ *
147
+ * @param filePath - Path to the JSON file containing the serialized queries.
148
+ * @param stopsIndex - The stops index used to resolve source stop IDs to the
149
+ * internal numeric IDs expected by the router.
150
+ * @returns An array of fully constructed {@link RangeQuery} objects ready to
151
+ * be passed to {@link Router.rangeRoute}.
152
+ * @throws If the file cannot be read, the JSON is malformed, or any stop ID
153
+ * referenced in the file cannot be found in the stops index.
154
+ */
155
+ export const loadRangeQueriesFromJson = (
156
+ filePath: string,
157
+ stopsIndex: StopsIndex,
158
+ ): RangeQuery[] => {
159
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
160
+ const serializedQueries: SerializedQuery[] = JSON.parse(
161
+ fileContent,
162
+ ) as SerializedQuery[];
52
163
 
53
- if (!fromStop || toStops.some((toStop) => !toStop)) {
54
- throw new Error(
55
- `Invalid task: Start or end station not found for task ${JSON.stringify(serializedQuery)}`,
164
+ return serializedQueries
165
+ .filter((q) => q.lastDepartureTime !== undefined)
166
+ .map((serializedQuery) => {
167
+ const fromStop = stopsIndex.findStopBySourceStopId(serializedQuery.from);
168
+ const toStops = Array.from(serializedQuery.to).map((stopId) =>
169
+ stopsIndex.findStopBySourceStopId(stopId),
56
170
  );
57
- }
58
- const queryBuilder = new Query.Builder()
59
- .from(fromStop.id)
60
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
61
- .to(new Set(toStops.map((stop) => stop!.id)))
62
- .departureTime(timeFromString(serializedQuery.departureTime));
63
-
64
- if (serializedQuery.maxTransfers !== undefined) {
65
- queryBuilder.maxTransfers(serializedQuery.maxTransfers);
66
- }
67
171
 
68
- return queryBuilder.build();
69
- });
172
+ if (!fromStop || toStops.some((toStop) => !toStop)) {
173
+ throw new Error(
174
+ `Invalid task: Start or end station not found for task ${JSON.stringify(serializedQuery)}`,
175
+ );
176
+ }
177
+ const queryBuilder = new RangeQuery.Builder()
178
+ .from(fromStop.id)
179
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
180
+ .to(new Set(toStops.map((stop) => stop!.id)))
181
+ .departureTime(timeFromString(serializedQuery.departureTime))
182
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
183
+ .lastDepartureTime(timeFromString(serializedQuery.lastDepartureTime!));
184
+
185
+ if (serializedQuery.maxTransfers !== undefined) {
186
+ queryBuilder.maxTransfers(serializedQuery.maxTransfers);
187
+ }
188
+
189
+ return queryBuilder.build();
190
+ });
70
191
  };
71
192
 
193
+ // ─── Benchmark runners ────────────────────────────────────────────────────────
194
+
72
195
  /**
73
196
  * Benchmarks {@link Router.route} across a set of queries.
74
197
  *
@@ -77,6 +200,7 @@ export const loadQueriesFromJson = (
77
200
  * produced per query.
78
201
  * @param iterations - Number of times each query is repeated. Higher values
79
202
  * yield a more stable mean at the cost of longer wall-clock time.
203
+ * @param stopsIndex - Used to resolve stop names for result labels.
80
204
  * @returns An array of {@link PerformanceResult} objects, one per query, each
81
205
  * containing the mean wall-clock time (µs) and mean heap delta (MB).
82
206
  */
@@ -84,6 +208,7 @@ export const testRouterPerformance = (
84
208
  router: Router,
85
209
  tasks: Query[],
86
210
  iterations: number,
211
+ stopsIndex: StopsIndex,
87
212
  ): PerformanceResult[] => {
88
213
  const results: PerformanceResult[] = [];
89
214
 
@@ -111,7 +236,7 @@ export const testRouterPerformance = (
111
236
  }
112
237
 
113
238
  results.push({
114
- task,
239
+ label: buildQueryLabel(task, stopsIndex),
115
240
  meanTimeUs: totalTime / iterations,
116
241
  meanMemoryMb: totalMemory / iterations / (1024 * 1024),
117
242
  });
@@ -129,6 +254,7 @@ export const testRouterPerformance = (
129
254
  * @param tasks - The list of queries to benchmark. One {@link PerformanceResult}
130
255
  * is produced per query.
131
256
  * @param iterations - Number of times `bestRoute` is called per query.
257
+ * @param stopsIndex - Used to resolve stop names for result labels.
132
258
  * @returns An array of {@link PerformanceResult} objects, one per query, each
133
259
  * containing the mean wall-clock time (µs) and mean heap delta (MB) for the
134
260
  * `bestRoute` call alone.
@@ -137,11 +263,11 @@ export const testBestRoutePerformance = (
137
263
  router: Router,
138
264
  tasks: Query[],
139
265
  iterations: number,
266
+ stopsIndex: StopsIndex,
140
267
  ): PerformanceResult[] => {
141
268
  const results: PerformanceResult[] = [];
142
269
 
143
270
  for (const task of tasks) {
144
- // Compute the routing result once — this is not part of the benchmark.
145
271
  const result = router.route(task);
146
272
 
147
273
  let totalTime = 0;
@@ -167,7 +293,7 @@ export const testBestRoutePerformance = (
167
293
  }
168
294
 
169
295
  results.push({
170
- task,
296
+ label: buildQueryLabel(task, stopsIndex),
171
297
  meanTimeUs: totalTime / iterations,
172
298
  meanMemoryMb: totalMemory / iterations / (1024 * 1024),
173
299
  });
@@ -177,44 +303,182 @@ export const testBestRoutePerformance = (
177
303
  };
178
304
 
179
305
  /**
180
- * Prints a human-readable summary of performance results to stdout.
306
+ * Benchmarks {@link Router.rangeRoute} across a set of range queries.
181
307
  *
182
- * Displays an overall mean across all tasks followed by a per-task breakdown.
183
- * An optional {@link label} is printed as a section header so that results
184
- * from different benchmark phases (e.g. routing vs. reconstruction) can be
185
- * told apart when several calls appear in the same run.
308
+ * @param router - The router instance to benchmark.
309
+ * @param tasks - The list of range queries to run. One {@link PerformanceResult}
310
+ * is produced per query.
311
+ * @param iterations - Number of times each query is repeated.
312
+ * @param stopsIndex - Used to resolve stop names for result labels.
313
+ * @returns An array of {@link PerformanceResult} objects, one per query, each
314
+ * containing the mean wall-clock time (µs) and mean heap delta (MB).
315
+ */
316
+ export const testRangeRouterPerformance = (
317
+ router: Router,
318
+ tasks: RangeQuery[],
319
+ iterations: number,
320
+ stopsIndex: StopsIndex,
321
+ ): PerformanceResult[] => {
322
+ const results: PerformanceResult[] = [];
323
+
324
+ for (const task of tasks) {
325
+ let totalTime = 0;
326
+ let totalMemory = 0;
327
+
328
+ for (let i = 0; i < iterations; i++) {
329
+ if (global.gc) {
330
+ global.gc();
331
+ }
332
+
333
+ const startMemory = process.memoryUsage().heapUsed;
334
+ const startTime = performance.now();
335
+
336
+ router.rangeRoute(task);
337
+
338
+ const endTime = performance.now();
339
+ const endMemory = process.memoryUsage().heapUsed;
340
+
341
+ totalTime += (endTime - startTime) * 1_000;
342
+ if (endMemory >= startMemory) {
343
+ totalMemory += endMemory - startMemory;
344
+ }
345
+ }
346
+
347
+ results.push({
348
+ label: buildQueryLabel(task, stopsIndex),
349
+ meanTimeUs: totalTime / iterations,
350
+ meanMemoryMb: totalMemory / iterations / (1024 * 1024),
351
+ });
352
+ }
353
+
354
+ return results;
355
+ };
356
+
357
+ /**
358
+ * Benchmarks {@link RangeResult.getRoutes} — the full Pareto-frontier
359
+ * reconstruction phase — independently of the range routing phase.
186
360
  *
187
- * @param results - The performance results to display, as returned by
188
- * {@link testRouterPerformance} or {@link testBestRoutePerformance}.
189
- * @param label - Optional heading printed above the results block.
190
- * Defaults to `'Performance Results'`.
361
+ * @param router - The router instance used to produce the range results that
362
+ * are then fed into `getRoutes`.
363
+ * @param tasks - The list of range queries to benchmark. One
364
+ * {@link PerformanceResult} is produced per query.
365
+ * @param iterations - Number of times `getRoutes` is called per query.
366
+ * @param stopsIndex - Used to resolve stop names for result labels.
367
+ * @returns An array of {@link PerformanceResult} objects, one per query, each
368
+ * containing the mean wall-clock time (µs) and mean heap delta (MB) for the
369
+ * `getRoutes` call alone.
370
+ */
371
+ export const testRangeResultPerformance = (
372
+ router: Router,
373
+ tasks: RangeQuery[],
374
+ iterations: number,
375
+ stopsIndex: StopsIndex,
376
+ ): PerformanceResult[] => {
377
+ const results: PerformanceResult[] = [];
378
+
379
+ for (const task of tasks) {
380
+ const rangeResult: RangeResult = router.rangeRoute(task);
381
+
382
+ let totalTime = 0;
383
+ let totalMemory = 0;
384
+
385
+ for (let i = 0; i < iterations; i++) {
386
+ if (global.gc) {
387
+ global.gc();
388
+ }
389
+
390
+ const startMemory = process.memoryUsage().heapUsed;
391
+ const startTime = performance.now();
392
+
393
+ rangeResult.getRoutes();
394
+
395
+ const endTime = performance.now();
396
+ const endMemory = process.memoryUsage().heapUsed;
397
+
398
+ totalTime += (endTime - startTime) * 1_000;
399
+ if (endMemory >= startMemory) {
400
+ totalMemory += endMemory - startMemory;
401
+ }
402
+ }
403
+
404
+ results.push({
405
+ label: buildQueryLabel(task, stopsIndex),
406
+ meanTimeUs: totalTime / iterations,
407
+ meanMemoryMb: totalMemory / iterations / (1024 * 1024),
408
+ });
409
+ }
410
+
411
+ return results;
412
+ };
413
+
414
+ // ─── Output ───────────────────────────────────────────────────────────────────
415
+
416
+ /**
417
+ * Prints a table summary of performance results to stdout.
418
+ *
419
+ * Each row corresponds to one task, identified by a human-readable query label
420
+ * (origin → destination + departure time). A footer row shows the mean across
421
+ * all tasks. An optional `label` is printed as a section header above the table.
422
+ *
423
+ * @param results - The performance results to display.
424
+ * @param label - Heading printed above the table. Defaults to `'Performance Results'`.
191
425
  */
192
426
  export const prettyPrintPerformanceResults = (
193
427
  results: PerformanceResult[],
194
428
  label = 'Performance Results',
195
429
  ): void => {
430
+ console.log(`\n${label}`);
431
+
196
432
  if (results.length === 0) {
197
- console.log('No performance results to display.');
433
+ console.log(' (no results)');
198
434
  return;
199
435
  }
200
436
 
201
- const overallMeanTimeNs =
202
- results.reduce((sum, result) => sum + result.meanTimeUs, 0) /
203
- results.length;
204
- const overallMeanMemoryMb =
205
- results.reduce((sum, result) => sum + result.meanMemoryMb, 0) /
206
- results.length;
207
-
208
- console.log(`${label}:`);
209
- console.log(` Mean Time (µs): ${overallMeanTimeNs.toFixed(0)}`);
210
- console.log(` Mean Memory (MB): ${overallMeanMemoryMb.toFixed(2)}`);
211
- console.log('');
212
-
213
- console.log('Individual Task Results:');
214
- results.forEach((result, index) => {
215
- console.log(`Task ${index + 1}:`);
216
- console.log(` Mean Time (µs): ${result.meanTimeUs.toFixed(0)}`);
217
- console.log(` Mean Memory (MB): ${result.meanMemoryMb.toFixed(2)}`);
218
- console.log('');
219
- });
437
+ const fmtTime = (n: number) => Math.round(n).toLocaleString('en-US');
438
+ const fmtMem = (n: number) => n.toFixed(2);
439
+
440
+ const meanTime =
441
+ results.reduce((s, r) => s + r.meanTimeUs, 0) / results.length;
442
+ const meanMem =
443
+ results.reduce((s, r) => s + r.meanMemoryMb, 0) / results.length;
444
+
445
+ const queryHeader = 'Query';
446
+ const timeHeader = 'Time (µs)';
447
+ const memHeader = 'Mem (MB)';
448
+
449
+ const timeVals = results.map((r) => fmtTime(r.meanTimeUs));
450
+ const memVals = results.map((r) => fmtMem(r.meanMemoryMb));
451
+ const meanTimeStr = fmtTime(meanTime);
452
+ const meanMemStr = fmtMem(meanMem);
453
+
454
+ const queryWidth = Math.max(
455
+ queryHeader.length,
456
+ 'mean'.length,
457
+ ...results.map((r) => r.label.length),
458
+ );
459
+ const timeWidth = Math.max(
460
+ timeHeader.length,
461
+ meanTimeStr.length,
462
+ ...timeVals.map((v) => v.length),
463
+ );
464
+ const memWidth = Math.max(
465
+ memHeader.length,
466
+ meanMemStr.length,
467
+ ...memVals.map((v) => v.length),
468
+ );
469
+
470
+ const columns: Column[] = [
471
+ { header: queryHeader, width: queryWidth, align: 'left' },
472
+ { header: timeHeader, width: timeWidth, align: 'right' },
473
+ { header: memHeader, width: memWidth, align: 'right' },
474
+ ];
475
+
476
+ const rows = results.map((r, i) => [
477
+ r.label,
478
+ timeVals[i] ?? '',
479
+ memVals[i] ?? '',
480
+ ]);
481
+ const footer = ['mean', meanTimeStr, meanMemStr];
482
+
483
+ console.log(renderTable(columns, rows, footer));
220
484
  };