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.
- package/.cspell.json +7 -1
- package/CHANGELOG.md +3 -3
- package/README.md +111 -86
- package/dist/cli/perf.d.ts +57 -18
- package/dist/cli.mjs +1371 -342
- package/dist/cli.mjs.map +1 -1
- package/dist/parser.cjs.js +57 -4
- package/dist/parser.cjs.js.map +1 -1
- package/dist/parser.esm.js +57 -4
- package/dist/parser.esm.js.map +1 -1
- package/dist/router.cjs.js +1 -1
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +5 -5
- package/dist/router.esm.js +1 -1
- package/dist/router.esm.js.map +1 -1
- package/dist/router.umd.js +1 -1
- package/dist/router.umd.js.map +1 -1
- package/dist/routing/__tests__/access.test.d.ts +1 -0
- package/dist/routing/__tests__/plainRouter.test.d.ts +1 -0
- package/dist/routing/__tests__/rangeResult.test.d.ts +1 -0
- package/dist/routing/__tests__/rangeRouter.test.d.ts +1 -0
- package/dist/routing/__tests__/rangeState.test.d.ts +1 -0
- package/dist/routing/__tests__/raptor.test.d.ts +1 -0
- package/dist/routing/__tests__/state.test.d.ts +1 -0
- package/dist/routing/access.d.ts +55 -0
- package/dist/routing/plainRouter.d.ts +21 -0
- package/dist/routing/plotter.d.ts +9 -0
- package/dist/routing/query.d.ts +132 -13
- package/dist/routing/rangeResult.d.ts +155 -0
- package/dist/routing/rangeRouter.d.ts +24 -0
- package/dist/routing/rangeState.d.ts +83 -0
- package/dist/routing/raptor.d.ts +96 -0
- package/dist/routing/result.d.ts +27 -7
- package/dist/routing/route.d.ts +5 -21
- package/dist/routing/router.d.ts +20 -91
- package/dist/routing/state.d.ts +92 -17
- package/dist/timetable/route.d.ts +8 -0
- package/dist/timetable/timetable.d.ts +17 -1
- package/package.json +1 -1
- package/src/__e2e__/benchmark.json +18 -0
- package/src/__e2e__/router.test.ts +461 -127
- package/src/cli/minotor.ts +39 -3
- package/src/cli/perf.ts +324 -60
- package/src/cli/repl.ts +96 -41
- package/src/router.ts +11 -3
- package/src/routing/__tests__/access.test.ts +294 -0
- package/src/routing/__tests__/plainRouter.test.ts +1633 -0
- package/src/routing/__tests__/plotter.test.ts +8 -8
- package/src/routing/__tests__/rangeResult.test.ts +273 -0
- package/src/routing/__tests__/rangeRouter.test.ts +472 -0
- package/src/routing/__tests__/rangeState.test.ts +246 -0
- package/src/routing/__tests__/raptor.test.ts +366 -0
- package/src/routing/__tests__/result.test.ts +27 -27
- package/src/routing/__tests__/route.test.ts +28 -0
- package/src/routing/__tests__/router.test.ts +75 -1587
- package/src/routing/__tests__/state.test.ts +78 -0
- package/src/routing/access.ts +144 -0
- package/src/routing/plainRouter.ts +60 -0
- package/src/routing/plotter.ts +53 -6
- package/src/routing/query.ts +116 -13
- package/src/routing/rangeResult.ts +292 -0
- package/src/routing/rangeRouter.ts +167 -0
- package/src/routing/rangeState.ts +150 -0
- package/src/routing/raptor.ts +416 -0
- package/src/routing/result.ts +68 -26
- package/src/routing/route.ts +15 -53
- package/src/routing/router.ts +40 -480
- package/src/routing/state.ts +191 -32
- package/src/timetable/__tests__/timetable.test.ts +373 -0
- package/src/timetable/route.ts +16 -4
- package/src/timetable/timetable.ts +54 -1
package/src/cli/minotor.ts
CHANGED
|
@@ -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(
|
|
164
|
-
|
|
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
|
-
'
|
|
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 {
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
25
|
-
*
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
stopsIndex.findStopBySourceStopId(
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
306
|
+
* Benchmarks {@link Router.rangeRoute} across a set of range queries.
|
|
181
307
|
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
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
|
|
188
|
-
*
|
|
189
|
-
* @param
|
|
190
|
-
*
|
|
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('
|
|
433
|
+
console.log(' (no results)');
|
|
198
434
|
return;
|
|
199
435
|
}
|
|
200
436
|
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
results.reduce((
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
results.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
};
|