@nestia/benchmark 14.0.1 → 14.0.2
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/lib/DynamicBenchmarker.d.ts +235 -6
- package/lib/DynamicBenchmarker.js +212 -61
- package/lib/DynamicBenchmarker.js.map +1 -1
- package/lib/IBenchmarkEvent.d.ts +19 -0
- package/lib/internal/DynamicBenchmarkReporter.d.ts +50 -1
- package/lib/internal/DynamicBenchmarkReporter.js +30 -11
- package/lib/internal/DynamicBenchmarkReporter.js.map +1 -1
- package/lib/internal/DynamicBenchmarkStatistics.d.ts +25 -0
- package/lib/internal/DynamicBenchmarkStatistics.js +51 -0
- package/lib/internal/DynamicBenchmarkStatistics.js.map +1 -0
- package/lib/internal/IBenchmarkMaster.d.ts +26 -0
- package/lib/internal/IBenchmarkServant.d.ts +19 -0
- package/package.json +4 -3
- package/src/DynamicBenchmarker.ts +376 -95
- package/src/IBenchmarkEvent.ts +24 -0
- package/src/internal/DynamicBenchmarkReporter.ts +59 -8
- package/src/internal/DynamicBenchmarkStatistics.ts +62 -0
- package/src/internal/IBenchmarkMaster.ts +27 -0
- package/src/internal/IBenchmarkServant.ts +19 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { IConnection } from "@nestia/fetcher";
|
|
2
2
|
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import NodePath from "path";
|
|
3
5
|
import { Driver, WorkerConnector, WorkerServer } from "tgrid";
|
|
4
|
-
import { HashMap, hash
|
|
6
|
+
import { HashMap, hash } from "tstl";
|
|
5
7
|
|
|
6
8
|
import { IBenchmarkEvent } from "./IBenchmarkEvent";
|
|
7
9
|
import { DynamicBenchmarkReporter } from "./internal/DynamicBenchmarkReporter";
|
|
10
|
+
import { DynamicBenchmarkStatistics } from "./internal/DynamicBenchmarkStatistics";
|
|
8
11
|
import { IBenchmarkMaster } from "./internal/IBenchmarkMaster";
|
|
9
12
|
import { IBenchmarkServant } from "./internal/IBenchmarkServant";
|
|
10
13
|
|
|
@@ -33,17 +36,65 @@ import { IBenchmarkServant } from "./internal/IBenchmarkServant";
|
|
|
33
36
|
* Additionally, if you hope to see some utilization cases, see the below
|
|
34
37
|
* example tagged links.
|
|
35
38
|
*
|
|
39
|
+
* Processing cost: Totals are split across T servants once; E events are
|
|
40
|
+
* aggregated with hash-based endpoint grouping. Progress notifications update
|
|
41
|
+
* the aggregate from one servant counter delta in constant time. Event
|
|
42
|
+
* statistics and rendering traverse their input populations; discovery visits
|
|
43
|
+
* each real directory once.
|
|
44
|
+
*
|
|
45
|
+
* Computation reuse: A master distributes distinct request budgets rather than
|
|
46
|
+
* replaying one effectful request. Endpoint grouping shares one event
|
|
47
|
+
* population across whole-run and endpoint statistics. Results are not cached
|
|
48
|
+
* across benchmark runs because server state and timing change.
|
|
49
|
+
*
|
|
50
|
+
* Resource ownership: Each master owns at most threads connectors and
|
|
51
|
+
* simultaneous function workers; connector closure is attempted on
|
|
52
|
+
* connection/execution success or failure; a rejected close is suppressed and
|
|
53
|
+
* may leave an unreleased child process. Events retained by servants/master
|
|
54
|
+
* scale with logged events, and memory samples with run duration. The
|
|
55
|
+
* independent sampler detaches its report target on stop or rejection and
|
|
56
|
+
* clears/wakes its owned timer. A pending caller memory getter has no
|
|
57
|
+
* cancellation hook, but its task retains only the sampler state/getter and
|
|
58
|
+
* cannot keep the master report arrays, connectors or event collections through
|
|
59
|
+
* that state. Its eventual result is discarded after deactivation. Returned
|
|
60
|
+
* reports transfer their arrays to callers.
|
|
61
|
+
*
|
|
62
|
+
* Platform boundary: WorkerConnector process mode owns Node child-process
|
|
63
|
+
* launch/IPC and its explicit stdio modes. Feature discovery resolves the
|
|
64
|
+
* caller location with native path and fs realpath/stat, follows directory
|
|
65
|
+
* links with realpath cycle detection and imports discovered native absolute
|
|
66
|
+
* paths. It does not infer filesystem case policy from OS names.
|
|
67
|
+
*
|
|
36
68
|
* @author Jeongho Nam - https://github.com/samchon
|
|
37
69
|
* @example
|
|
38
70
|
* https://github.com/samchon/nestia-start/blob/master/test/benchmaark/index.ts
|
|
39
71
|
*
|
|
40
72
|
* @example
|
|
41
73
|
* https://github.com/samchon/backend/blob/master/test/benchmark/index.ts
|
|
74
|
+
*
|
|
75
|
+
* @evidence contracts/common.md#principled-implementation The master spawns one child process per thread through tgrid's process-mode `WorkerConnector`, drives each servant over RPC, and merges the returned events; totals are split by floor plus remainder so the shares sum to the requested count and simultaneous budget.
|
|
76
|
+
* @evidence contracts/common.md#clear-and-simple-design One namespace is the public surface (master, servant, markdown, props, report); execution, discovery, and cleanup helpers stay private, and statistics and rendering live in their own internal namespaces.
|
|
77
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Nothing names a server, feature, or file: the feature extension, prefix, and filter are caller inputs, and the servant path is a property rather than an assumed location.
|
|
78
|
+
* @evidence contracts/common.md#meaningful-documentation The namespace prose explains the two-program model, what each program does, and how to obtain markdown, with links to the member documentation and to example repositories.
|
|
42
79
|
*/
|
|
43
80
|
export namespace DynamicBenchmarker {
|
|
44
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Properties of the master program.
|
|
83
|
+
*
|
|
84
|
+
* Platform boundary: The servant path uses native filesystem spelling, and
|
|
85
|
+
* stdio exposes Node child-process modes without imposing separator or case
|
|
86
|
+
* policy.
|
|
87
|
+
*
|
|
88
|
+
* @evidence contracts/common.md#principled-implementation The fields are exactly what the master needs to plan a run: totals, parallelism, how to reach the servant program, and optional hooks; the count, threads, and simultaneous constraints are enforced by `master`, which the field prose states.
|
|
89
|
+
* @evidence contracts/common.md#clear-and-simple-design A flat property record; optional members carry the only extension points (filter, progress, memory, stdio).
|
|
90
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Every value is caller-supplied; no default names a target application or fixture.
|
|
91
|
+
* @evidence contracts/common.md#meaningful-documentation Each property documents its meaning and constraints, including how simultaneous is distributed among threads and that the filter receives a file basename.
|
|
92
|
+
*/
|
|
45
93
|
export interface IMasterProps {
|
|
46
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Total number of benchmark function invocations, each of which may log
|
|
96
|
+
* several HTTP requests.
|
|
97
|
+
*/
|
|
47
98
|
count: number;
|
|
48
99
|
|
|
49
100
|
/**
|
|
@@ -82,13 +133,24 @@ export namespace DynamicBenchmarker {
|
|
|
82
133
|
*
|
|
83
134
|
* @param file File name (basename) of the benchmark functions
|
|
84
135
|
* @returns Whether to execute the function or not.
|
|
136
|
+
* @evidence contracts/common.md#principled-implementation The predicate is asked for each candidate file name through the master driver before the servant imports it, so a rejected file's functions can never execute.
|
|
137
|
+
* @evidence contracts/common.md#clear-and-simple-design A single predicate on the file basename, with a default that accepts everything.
|
|
138
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Selection is by the caller's predicate, not by known file names.
|
|
139
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the argument (basename, not function name) and that a `false` result prevents the import.
|
|
85
140
|
*/
|
|
86
141
|
filter?: (file: string) => boolean;
|
|
87
142
|
|
|
88
143
|
/**
|
|
89
144
|
* Progress callback function.
|
|
90
145
|
*
|
|
91
|
-
*
|
|
146
|
+
* Progress counts completed benchmark function invocations, regardless of
|
|
147
|
+
* how many HTTP events each invocation logs.
|
|
148
|
+
*
|
|
149
|
+
* @param complete The number of completed benchmark function invocations.
|
|
150
|
+
* @evidence contracts/common.md#principled-implementation The callback receives the sum of completed invocation counts from every servant and is called once more with the requested count at the end, including when no matching functions were discovered.
|
|
151
|
+
* @evidence contracts/common.md#clear-and-simple-design One optional notification with a single number argument.
|
|
152
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The callback does not configure request counts or concurrency; a throw from the final callback propagates after servants have been closed rather than being hidden.
|
|
153
|
+
* @evidence contracts/common.md#meaningful-documentation The comment distinguishes completed benchmark function invocations from logged HTTP events.
|
|
92
154
|
*/
|
|
93
155
|
progress?: (complete: number) => void;
|
|
94
156
|
|
|
@@ -100,6 +162,16 @@ export namespace DynamicBenchmarker {
|
|
|
100
162
|
* Specify this property only when your backend server is running on a
|
|
101
163
|
* different process, so that need to measure the memory usage of the
|
|
102
164
|
* backend server from other process.
|
|
165
|
+
*
|
|
166
|
+
* The function is called once a second while the benchmark runs. If it
|
|
167
|
+
* rejects, the sampling stops and the report keeps the samples taken so
|
|
168
|
+
* far. A result still pending when execution ends is discarded when it
|
|
169
|
+
* settles.
|
|
170
|
+
*
|
|
171
|
+
* @evidence contracts/common.md#principled-implementation When the server under test runs in another process, the master's own `process.memoryUsage()` measures the wrong process, so the caller supplies a getter for the right one; it is sampled once a second while the run lasts.
|
|
172
|
+
* @evidence contracts/common.md#clear-and-simple-design One optional async getter that replaces the default sampler and nothing else.
|
|
173
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The default measures the master process itself; a caller getter is used as given, and a getter that rejects ends sampling rather than being retried or replaced.
|
|
174
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states when to set it, the sampling cadence, and that a rejection stops the sampling and keeps earlier samples.
|
|
103
175
|
*/
|
|
104
176
|
memory?: () => Promise<NodeJS.MemoryUsage>;
|
|
105
177
|
|
|
@@ -111,7 +183,18 @@ export namespace DynamicBenchmarker {
|
|
|
111
183
|
stdio?: undefined | "overlapped" | "pipe" | "ignore" | "inherit";
|
|
112
184
|
}
|
|
113
185
|
|
|
114
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Properties of the servant program.
|
|
188
|
+
*
|
|
189
|
+
* Platform boundary: The feature location and extension describe native files
|
|
190
|
+
* separately from the connection's protocol address; relative paths use the
|
|
191
|
+
* servant's working directory.
|
|
192
|
+
*
|
|
193
|
+
* @evidence contracts/common.md#principled-implementation The fields define how a servant finds benchmark functions (location, extension, prefix) and how it builds their arguments (connection and parameters), which is everything discovery and invocation need.
|
|
194
|
+
* @evidence contracts/common.md#clear-and-simple-design A generic property record parameterized by the tuple of function arguments, with the optional extension defaulting to the built JavaScript.
|
|
195
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The extension is declared rather than inferred from the servant's own module, which once loaded no feature at all when the library was built and the features ran from TypeScript source.
|
|
196
|
+
* @evidence contracts/common.md#meaningful-documentation Each property documents its role, the default extension, the file-name and function-name prefix rules, and what happens when nothing matches.
|
|
197
|
+
*/
|
|
115
198
|
export interface IServantProps<Parameters extends any[]> {
|
|
116
199
|
/**
|
|
117
200
|
* Default connection.
|
|
@@ -120,7 +203,12 @@ export namespace DynamicBenchmarker {
|
|
|
120
203
|
*/
|
|
121
204
|
connection: IConnection;
|
|
122
205
|
|
|
123
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Location of the benchmark functions.
|
|
208
|
+
*
|
|
209
|
+
* A relative path is resolved against the current working directory of the
|
|
210
|
+
* servant, the same base that lists the directory.
|
|
211
|
+
*/
|
|
124
212
|
location: string;
|
|
125
213
|
|
|
126
214
|
/**
|
|
@@ -145,6 +233,9 @@ export namespace DynamicBenchmarker {
|
|
|
145
233
|
* would never be executed. Also, when a file name (basename) does not start
|
|
146
234
|
* with the prefix, the file would never be imported either, so that every
|
|
147
235
|
* function defined in the file would never be executed.
|
|
236
|
+
*
|
|
237
|
+
* When no function matches, no request is executed and the report carries
|
|
238
|
+
* no events.
|
|
148
239
|
*/
|
|
149
240
|
prefix: string;
|
|
150
241
|
|
|
@@ -157,36 +248,117 @@ export namespace DynamicBenchmarker {
|
|
|
157
248
|
*
|
|
158
249
|
* @param connection Default connection instance
|
|
159
250
|
* @param name Function name
|
|
251
|
+
* @evidence contracts/common.md#principled-implementation A function from the per-request connection and the function name to the argument tuple lets each execution receive a connection whose logger records its events.
|
|
252
|
+
* @evidence contracts/common.md#clear-and-simple-design One callback, called per execution, with the two inputs a caller needs to build arguments.
|
|
253
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Arguments come only from the caller's function; the connection it receives is the servant's logging copy.
|
|
254
|
+
* @evidence contracts/common.md#meaningful-documentation The comment warns that the logger of the received connection must be carried into the returned parameters, the fact that decides whether events are recorded.
|
|
160
255
|
*/
|
|
161
256
|
parameters: (connection: IConnection, name: string) => Parameters;
|
|
162
257
|
}
|
|
163
258
|
|
|
164
|
-
/**
|
|
259
|
+
/**
|
|
260
|
+
* Benchmark report.
|
|
261
|
+
*
|
|
262
|
+
* Times are ISO 8601 strings, durations are milliseconds, and the
|
|
263
|
+
* {@link statistics} and every entry of {@link endpoints} describe the same
|
|
264
|
+
* collected events, grouped by method and path template.
|
|
265
|
+
*
|
|
266
|
+
* @evidence contracts/common.md#principled-implementation The report is the master's merged observation: the run parameters, whole-run statistics, per-endpoint statistics, and memory samples, all derived from one event collection.
|
|
267
|
+
* @evidence contracts/common.md#clear-and-simple-design A flat record whose nested types (`IEndpoint`, `IStatistics`, `IMemory`) name the parts, with no behavior.
|
|
268
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Every field is measured or copied from the request; none is a fixture value.
|
|
269
|
+
* @evidence contracts/common.md#meaningful-documentation The prose states the time format, the duration unit, and how statistics and endpoints relate.
|
|
270
|
+
*/
|
|
165
271
|
export interface IReport {
|
|
272
|
+
/** Requested number of benchmark function invocations. */
|
|
166
273
|
count: number;
|
|
274
|
+
|
|
275
|
+
/** Number of servant processes requested. */
|
|
167
276
|
threads: number;
|
|
277
|
+
|
|
278
|
+
/** Aggregate limit on concurrent benchmark function invocations. */
|
|
168
279
|
simultaneous: number;
|
|
280
|
+
|
|
281
|
+
/** ISO 8601 time when connected servants started executing. */
|
|
169
282
|
started_at: string;
|
|
283
|
+
|
|
284
|
+
/** ISO 8601 time when every servant finished executing. */
|
|
170
285
|
completed_at: string;
|
|
286
|
+
|
|
287
|
+
/** Statistics over all HTTP events logged by the benchmark functions. */
|
|
171
288
|
statistics: IReport.IStatistics;
|
|
289
|
+
|
|
290
|
+
/** The same events grouped by HTTP method and route path template. */
|
|
172
291
|
endpoints: Array<IReport.IEndpoint & IReport.IStatistics>;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Memory samples obtained during execution; failed or pending readings are
|
|
295
|
+
* omitted.
|
|
296
|
+
*/
|
|
173
297
|
memories: IReport.IMemory[];
|
|
174
298
|
}
|
|
175
299
|
export namespace IReport {
|
|
300
|
+
/**
|
|
301
|
+
* Identity of an endpoint in the report: the HTTP method and the route
|
|
302
|
+
* path, using the path template when the route declares one.
|
|
303
|
+
*
|
|
304
|
+
* @evidence contracts/common.md#principled-implementation An endpoint is identified by method and path, the pair the master groups events by, with the template preferred so parameterized paths aggregate.
|
|
305
|
+
* @evidence contracts/common.md#clear-and-simple-design Two string fields with no behavior.
|
|
306
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Grouping uses the route metadata carried by each event, without a list of known endpoints.
|
|
307
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states which path form is used for grouping.
|
|
308
|
+
*/
|
|
176
309
|
export interface IEndpoint {
|
|
310
|
+
/** HTTP method of the grouped requests. */
|
|
177
311
|
method: string;
|
|
312
|
+
|
|
313
|
+
/** Route template, falling back to the concrete route path. */
|
|
178
314
|
path: string;
|
|
179
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Statistics of a set of benchmarked requests.
|
|
318
|
+
*
|
|
319
|
+
* Durations are milliseconds from the request start to its completion.
|
|
320
|
+
* `mean`, `stdev`, `minimum`, and `maximum` are `null` when there are no
|
|
321
|
+
* events, and `stdev` is the population standard deviation. `success`
|
|
322
|
+
* counts the events whose executed function did not throw.
|
|
323
|
+
*
|
|
324
|
+
* @evidence contracts/common.md#principled-implementation Counts are exact, the mean and population standard deviation are computed over the same set, and `null` marks the empty set where no value is defined.
|
|
325
|
+
* @evidence contracts/common.md#clear-and-simple-design A flat record of six measures reused for the whole run and for each endpoint.
|
|
326
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The values are computed from the events; no measure is defaulted to a plausible number.
|
|
327
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the unit, the empty-set convention, the deviation kind, and what success counts.
|
|
328
|
+
*/
|
|
180
329
|
export interface IStatistics {
|
|
330
|
+
/** Number of logged HTTP events. */
|
|
181
331
|
count: number;
|
|
332
|
+
|
|
333
|
+
/** Events whose enclosing benchmark function completed without throwing. */
|
|
182
334
|
success: number;
|
|
335
|
+
|
|
336
|
+
/** Mean duration in milliseconds, or null for no events. */
|
|
183
337
|
mean: number | null;
|
|
338
|
+
|
|
339
|
+
/** Population standard deviation in milliseconds, or null for no events. */
|
|
184
340
|
stdev: number | null;
|
|
341
|
+
|
|
342
|
+
/** Shortest duration in milliseconds, or null for no events. */
|
|
185
343
|
minimum: number | null;
|
|
344
|
+
|
|
345
|
+
/** Longest duration in milliseconds, or null for no events. */
|
|
186
346
|
maximum: number | null;
|
|
187
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* One memory sample taken while the benchmark ran: an ISO 8601 time and the
|
|
350
|
+
* process memory usage read by the sampler.
|
|
351
|
+
*
|
|
352
|
+
* @evidence contracts/common.md#principled-implementation A sample pairs the instant with the usage object exactly as the getter returned it, so the report keeps Node's own fields.
|
|
353
|
+
* @evidence contracts/common.md#clear-and-simple-design Two fields with no behavior.
|
|
354
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The usage is the getter's return value, not a synthesized reading.
|
|
355
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the time format and the source of the usage.
|
|
356
|
+
*/
|
|
188
357
|
export interface IMemory {
|
|
358
|
+
/** ISO 8601 time when the memory getter completed. */
|
|
189
359
|
time: string;
|
|
360
|
+
|
|
361
|
+
/** Node memory usage fields in bytes, supplied by the selected getter. */
|
|
190
362
|
usage: NodeJS.MemoryUsage;
|
|
191
363
|
}
|
|
192
364
|
}
|
|
@@ -199,8 +371,45 @@ export namespace DynamicBenchmarker {
|
|
|
199
371
|
* Note that, {@link IMasterProps.servant} property must be the path of the
|
|
200
372
|
* servant program executing the {@link servant} function.
|
|
201
373
|
*
|
|
374
|
+
* The servants are child processes. When a servant fails to connect or to
|
|
375
|
+
* execute, every servant receives a close attempt before the returned promise
|
|
376
|
+
* rejects.
|
|
377
|
+
*
|
|
378
|
+
* Processing cost: Totals are split across T servants once; E events are
|
|
379
|
+
* aggregated with hash-based endpoint grouping. Progress notifications update
|
|
380
|
+
* the aggregate from one servant counter delta in constant time. Event
|
|
381
|
+
* statistics and rendering traverse their input populations; discovery visits
|
|
382
|
+
* each real directory once.
|
|
383
|
+
*
|
|
384
|
+
* Computation reuse: A master distributes distinct request budgets rather
|
|
385
|
+
* than replaying one effectful request. Endpoint grouping shares one event
|
|
386
|
+
* population across whole-run and endpoint statistics. Results are not cached
|
|
387
|
+
* across benchmark runs because server state and timing change.
|
|
388
|
+
*
|
|
389
|
+
* Resource ownership: Each master owns at most threads connectors and
|
|
390
|
+
* simultaneous function workers; connector closure is attempted on
|
|
391
|
+
* connection/execution success or failure; a rejected close is suppressed and
|
|
392
|
+
* may leave an unreleased child process. Events retained by servants/master
|
|
393
|
+
* scale with logged events, and memory samples with run duration. The
|
|
394
|
+
* independent sampler detaches its report target on stop or rejection and
|
|
395
|
+
* clears/wakes its owned timer. A pending caller memory getter has no
|
|
396
|
+
* cancellation hook, but its task retains only the sampler state/getter and
|
|
397
|
+
* cannot keep the master report arrays, connectors or event collections
|
|
398
|
+
* through that state. Its eventual result is discarded after deactivation.
|
|
399
|
+
* Returned reports transfer their arrays to callers.
|
|
400
|
+
*
|
|
401
|
+
* Platform boundary: WorkerConnector process mode owns Node child-process
|
|
402
|
+
* launch/IPC and its explicit stdio modes. Feature discovery resolves the
|
|
403
|
+
* caller location with native path and fs realpath/stat, follows directory
|
|
404
|
+
* links with realpath cycle detection and imports discovered native absolute
|
|
405
|
+
* paths. It does not infer filesystem case policy from OS names.
|
|
406
|
+
*
|
|
202
407
|
* @param props Properties of the master program
|
|
203
408
|
* @returns Benchmark report
|
|
409
|
+
* @evidence contracts/common.md#principled-implementation Inputs are validated as safe integers before any process starts; totals are distributed by floor plus remainder; servants execute concurrently and every event is grouped by method and path template in a hash map; closing is attempted for every spawned servant on success and on failure, and the memory sampler is deactivated when the run ends, detaching its report array and clearing/waking its owned timer immediately; a pending caller getter may settle later but cannot append.
|
|
410
|
+
* @evidence contracts/common.md#clear-and-simple-design One function owns the run and keeps the sequence readable: validate, spawn, sample, execute, close, aggregate; statistics come from `DynamicBenchmarkStatistics` and the closing helper is shared by the failure and success paths.
|
|
411
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts A servant that fails to close cannot alter a finished report and is not allowed to hide the run's own error, so closing failures are dropped on purpose; no other error is swallowed except a rejecting memory getter, which ends sampling as documented.
|
|
412
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states what the master does, that the servant property must name the servant program, that servants are child processes, and that every servant receives a close attempt before a failure rejects.
|
|
204
413
|
*/
|
|
205
414
|
export const master = async (props: IMasterProps): Promise<IReport> => {
|
|
206
415
|
if (!Number.isSafeInteger(props.count) || props.count < 0)
|
|
@@ -220,6 +429,7 @@ export namespace DynamicBenchmarker {
|
|
|
220
429
|
`DynamicBenchmarker.master(): simultaneous (${props.simultaneous}) must not be less than threads (${props.threads}), as each servant runs at least one request at a time.`,
|
|
221
430
|
);
|
|
222
431
|
const completes: number[] = new Array(props.threads).fill(0);
|
|
432
|
+
let completed: number = 0;
|
|
223
433
|
// the first `total % threads` servants take one more than the rest, so the
|
|
224
434
|
// shares sum to the total
|
|
225
435
|
const distribute = (total: number): number[] =>
|
|
@@ -232,61 +442,63 @@ export namespace DynamicBenchmarker {
|
|
|
232
442
|
);
|
|
233
443
|
const counts: number[] = distribute(props.count);
|
|
234
444
|
const budgets: number[] = distribute(props.simultaneous);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
>[] = await Promise.
|
|
445
|
+
// every servant is a child process, so each connector created must be
|
|
446
|
+
// closed on every path out of this function, including a failed connection
|
|
447
|
+
// of a sibling
|
|
448
|
+
const spawned: Servant[] = [];
|
|
449
|
+
const settled: PromiseSettledResult<void>[] = await Promise.allSettled(
|
|
240
450
|
new Array(props.threads).fill(null).map(async (_, i) => {
|
|
241
|
-
const connector: WorkerConnector
|
|
242
|
-
null,
|
|
243
|
-
IBenchmarkMaster,
|
|
244
|
-
IBenchmarkServant
|
|
245
|
-
> = new WorkerConnector(
|
|
451
|
+
const connector: Servant = new WorkerConnector(
|
|
246
452
|
null,
|
|
247
453
|
{
|
|
248
454
|
filter: props.filter ?? (() => true),
|
|
249
455
|
progress: (current) => {
|
|
456
|
+
completed += current - completes[i]!;
|
|
250
457
|
completes[i] = current;
|
|
251
|
-
if (props.progress)
|
|
252
|
-
props.progress(completes.reduce((a, b) => a + b, 0));
|
|
458
|
+
if (props.progress) props.progress(completed);
|
|
253
459
|
},
|
|
254
460
|
},
|
|
255
461
|
"process",
|
|
256
462
|
);
|
|
463
|
+
spawned.push(connector);
|
|
257
464
|
await connector.connect(props.servant, { stdio: props.stdio });
|
|
258
|
-
return connector;
|
|
259
465
|
}),
|
|
260
466
|
);
|
|
467
|
+
const refused: PromiseRejectedResult | undefined = settled.find(
|
|
468
|
+
(result): result is PromiseRejectedResult => result.status === "rejected",
|
|
469
|
+
);
|
|
470
|
+
if (refused !== undefined) {
|
|
471
|
+
await closeAll(spawned);
|
|
472
|
+
throw refused.reason;
|
|
473
|
+
}
|
|
474
|
+
const servants: Servant[] = spawned;
|
|
261
475
|
|
|
262
476
|
const started_at: Date = new Date();
|
|
263
477
|
const memories: IReport.IMemory[] = [];
|
|
264
|
-
let completed_at: Date
|
|
265
|
-
|
|
266
|
-
(async () => {
|
|
267
|
-
const getter = props.memory ?? (async () => process.memoryUsage());
|
|
268
|
-
while (completed_at === null) {
|
|
269
|
-
await sleep_for(1_000);
|
|
270
|
-
memories.push({
|
|
271
|
-
usage: await getter(),
|
|
272
|
-
time: new Date().toISOString(),
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
})().catch(() => {});
|
|
478
|
+
let completed_at: Date;
|
|
479
|
+
let events: IBenchmarkEvent[];
|
|
276
480
|
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
count: counts[index]!,
|
|
282
|
-
simultaneous: budgets[index]!,
|
|
283
|
-
}),
|
|
284
|
-
),
|
|
285
|
-
)
|
|
286
|
-
).flat();
|
|
481
|
+
const stopSampling = startMemorySampler(
|
|
482
|
+
props.memory ?? (async () => process.memoryUsage()),
|
|
483
|
+
memories,
|
|
484
|
+
);
|
|
287
485
|
|
|
288
|
-
|
|
289
|
-
|
|
486
|
+
try {
|
|
487
|
+
events = (
|
|
488
|
+
await Promise.all(
|
|
489
|
+
servants.map((connector, index) =>
|
|
490
|
+
connector.getDriver().execute({
|
|
491
|
+
count: counts[index]!,
|
|
492
|
+
simultaneous: budgets[index]!,
|
|
493
|
+
}),
|
|
494
|
+
),
|
|
495
|
+
)
|
|
496
|
+
).flat();
|
|
497
|
+
completed_at = new Date();
|
|
498
|
+
} finally {
|
|
499
|
+
stopSampling();
|
|
500
|
+
await closeAll(servants);
|
|
501
|
+
}
|
|
290
502
|
if (props.progress) props.progress(props.count);
|
|
291
503
|
|
|
292
504
|
const endpoints: HashMap<IReport.IEndpoint, IBenchmarkEvent[]> =
|
|
@@ -308,9 +520,9 @@ export namespace DynamicBenchmarker {
|
|
|
308
520
|
count: props.count,
|
|
309
521
|
threads: props.threads,
|
|
310
522
|
simultaneous: props.simultaneous,
|
|
311
|
-
statistics:
|
|
523
|
+
statistics: DynamicBenchmarkStatistics.of(events),
|
|
312
524
|
endpoints: [...endpoints].map((it) => ({
|
|
313
|
-
...
|
|
525
|
+
...DynamicBenchmarkStatistics.of(it.second),
|
|
314
526
|
...it.first,
|
|
315
527
|
})),
|
|
316
528
|
started_at: started_at.toISOString(),
|
|
@@ -324,8 +536,33 @@ export namespace DynamicBenchmarker {
|
|
|
324
536
|
*
|
|
325
537
|
* Creates a servant program executing the prefixed functions in parallel.
|
|
326
538
|
*
|
|
539
|
+
* Processing cost: Each execute request discovers matching modules once, then
|
|
540
|
+
* runs at most simultaneous loop workers and count invocations. Event storage
|
|
541
|
+
* grows with actual logger calls rather than an assumed one event per
|
|
542
|
+
* invocation.
|
|
543
|
+
*
|
|
544
|
+
* Computation reuse: Discovery is refreshed for each execute request because
|
|
545
|
+
* the supplied filter and filesystem can change; requests themselves are
|
|
546
|
+
* effectful and cannot reuse earlier observations.
|
|
547
|
+
*
|
|
548
|
+
* Resource ownership: The returned WorkerServer transfers connection
|
|
549
|
+
* lifecycle to its caller. Each execute owns its discovery collections,
|
|
550
|
+
* workers and event arrays until it completes; the master closes its servant
|
|
551
|
+
* process. No discovered-module cache is retained by this namespace beyond
|
|
552
|
+
* Node own loader.
|
|
553
|
+
*
|
|
554
|
+
* Platform boundary: WorkerConnector process mode owns Node child-process
|
|
555
|
+
* launch/IPC and its explicit stdio modes. Feature discovery resolves the
|
|
556
|
+
* caller location with native path and fs realpath/stat, follows directory
|
|
557
|
+
* links with realpath cycle detection and imports discovered native absolute
|
|
558
|
+
* paths. It does not infer filesystem case policy from OS names.
|
|
559
|
+
*
|
|
327
560
|
* @param props Properties of the servant program
|
|
328
561
|
* @returns Servant program as a worker server
|
|
562
|
+
* @evidence contracts/common.md#principled-implementation A servant opens a `WorkerServer` that exposes one `execute` operation; the master decides counts and budgets, so the servant only needs the location and naming rules to find functions.
|
|
563
|
+
* @evidence contracts/common.md#clear-and-simple-design A thin adapter over the worker server whose behavior lives in the private `execute` and `iterate` helpers.
|
|
564
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Discovery is driven by the caller's location, extension, and prefix, and by the master's filter; no file or function name is assumed.
|
|
565
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states that it creates the servant program and returns it as a worker server.
|
|
329
566
|
*/
|
|
330
567
|
export const servant = async <Parameters extends any[]>(
|
|
331
568
|
props: IServantProps<Parameters>,
|
|
@@ -344,11 +581,27 @@ export namespace DynamicBenchmarker {
|
|
|
344
581
|
/**
|
|
345
582
|
* Convert the benchmark report to markdown content.
|
|
346
583
|
*
|
|
584
|
+
* Processing cost: Rendering delegates to the reporter and scales with report
|
|
585
|
+
* endpoints, samples and output bytes; host metadata is obtained through Node
|
|
586
|
+
* os APIs.
|
|
587
|
+
*
|
|
588
|
+
* Platform boundary: The wrapper obtains host information through Node os
|
|
589
|
+
* APIs and passes those facts to the pure renderer without constructing
|
|
590
|
+
* native file paths or shell commands.
|
|
591
|
+
*
|
|
347
592
|
* @param report Benchmark report
|
|
348
593
|
* @returns Markdown content
|
|
594
|
+
* @evidence contracts/common.md#principled-implementation Rendering is a pure function of the report and a few host facts, delegated to the reporter so the report type and its presentation stay separate.
|
|
595
|
+
* @evidence contracts/common.md#clear-and-simple-design The wrapper captures three native host facts and delegates rendering, giving users one entry point without exposing the internal reporter.
|
|
596
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The function observes native host facts and formats the report without altering its measurements.
|
|
597
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states input and output: a report in, markdown content out.
|
|
349
598
|
*/
|
|
350
599
|
export const markdown = (report: DynamicBenchmarker.IReport): string =>
|
|
351
|
-
DynamicBenchmarkReporter.markdown(report
|
|
600
|
+
DynamicBenchmarkReporter.markdown(report, {
|
|
601
|
+
cpu: os.cpus()[0]?.model,
|
|
602
|
+
memory: os.totalmem(),
|
|
603
|
+
node: process.version,
|
|
604
|
+
});
|
|
352
605
|
|
|
353
606
|
const execute =
|
|
354
607
|
<Parameters extends any[]>(ctx: {
|
|
@@ -364,10 +617,11 @@ export namespace DynamicBenchmarker {
|
|
|
364
617
|
collection: functions,
|
|
365
618
|
driver: ctx.driver,
|
|
366
619
|
props: ctx.props,
|
|
367
|
-
})(ctx.props.location);
|
|
620
|
+
})(NodePath.resolve(ctx.props.location));
|
|
368
621
|
|
|
369
622
|
const entireEvents: IBenchmarkEvent[] = [];
|
|
370
623
|
let scheduled: number = 0;
|
|
624
|
+
let completed: number = 0;
|
|
371
625
|
await Promise.all(
|
|
372
626
|
new Array(mass.simultaneous)
|
|
373
627
|
.fill(null)
|
|
@@ -398,16 +652,81 @@ export namespace DynamicBenchmarker {
|
|
|
398
652
|
} catch (exp) {
|
|
399
653
|
for (const e of localEvents) e.success = false;
|
|
400
654
|
}
|
|
401
|
-
|
|
402
|
-
ctx.driver.progress(entireEvents.length).catch(() => {});
|
|
655
|
+
ctx.driver.progress(++completed).catch(() => {});
|
|
403
656
|
}
|
|
404
657
|
}),
|
|
405
658
|
);
|
|
406
|
-
await ctx.driver.progress(
|
|
659
|
+
await ctx.driver.progress(completed);
|
|
407
660
|
return entireEvents;
|
|
408
661
|
};
|
|
409
662
|
}
|
|
410
663
|
|
|
664
|
+
type Servant = WorkerConnector<null, IBenchmarkMaster, IBenchmarkServant>;
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Keeps the sampler's pending caller task separate from the master's report,
|
|
668
|
+
* connectors and events. Stopping detaches the report array immediately;
|
|
669
|
+
* cancellation of a caller-owned getter remains its caller's responsibility.
|
|
670
|
+
*/
|
|
671
|
+
const startMemorySampler = (
|
|
672
|
+
getter: () => Promise<NodeJS.MemoryUsage>,
|
|
673
|
+
memories: DynamicBenchmarker.IReport.IMemory[],
|
|
674
|
+
): (() => void) => {
|
|
675
|
+
const state: MemorySamplerState = {
|
|
676
|
+
active: true,
|
|
677
|
+
target: memories,
|
|
678
|
+
};
|
|
679
|
+
sampleMemory(getter, state).catch(() => {});
|
|
680
|
+
return () => stopMemorySampler(state);
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
interface MemorySamplerState {
|
|
684
|
+
active: boolean;
|
|
685
|
+
target: DynamicBenchmarker.IReport.IMemory[] | null;
|
|
686
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
687
|
+
wake?: () => void;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const stopMemorySampler = (state: MemorySamplerState): void => {
|
|
691
|
+
state.active = false;
|
|
692
|
+
state.target = null;
|
|
693
|
+
if (state.timer !== undefined) clearTimeout(state.timer);
|
|
694
|
+
state.timer = undefined;
|
|
695
|
+
const wake = state.wake;
|
|
696
|
+
state.wake = undefined;
|
|
697
|
+
wake?.();
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const sampleMemory = async (
|
|
701
|
+
getter: () => Promise<NodeJS.MemoryUsage>,
|
|
702
|
+
state: MemorySamplerState,
|
|
703
|
+
): Promise<void> => {
|
|
704
|
+
try {
|
|
705
|
+
while (state.active) {
|
|
706
|
+
await new Promise<void>((resolve) => {
|
|
707
|
+
state.wake = resolve;
|
|
708
|
+
state.timer = setTimeout(() => {
|
|
709
|
+
state.timer = undefined;
|
|
710
|
+
state.wake = undefined;
|
|
711
|
+
resolve();
|
|
712
|
+
}, 1_000);
|
|
713
|
+
});
|
|
714
|
+
if (!state.active) return;
|
|
715
|
+
const usage = await getter();
|
|
716
|
+
if (!state.active) return;
|
|
717
|
+
state.target!.push({ usage, time: new Date().toISOString() });
|
|
718
|
+
}
|
|
719
|
+
} finally {
|
|
720
|
+
stopMemorySampler(state);
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// A servant that fails to close cannot change a finished report, and must not
|
|
725
|
+
// hide the error that ended the run, so a failed close is dropped.
|
|
726
|
+
const closeAll = async (servants: Servant[]): Promise<void> => {
|
|
727
|
+
await Promise.allSettled(servants.map((connector) => connector.close()));
|
|
728
|
+
};
|
|
729
|
+
|
|
411
730
|
interface IFunction<Parameters extends any[]> {
|
|
412
731
|
key: string;
|
|
413
732
|
value: (...args: Parameters) => Promise<void>;
|
|
@@ -419,12 +738,15 @@ const iterate =
|
|
|
419
738
|
driver: Driver<IBenchmarkMaster>;
|
|
420
739
|
props: DynamicBenchmarker.IServantProps<Parameters>;
|
|
421
740
|
}) =>
|
|
422
|
-
async (path: string): Promise<void> => {
|
|
741
|
+
async (path: string, visited: Set<string> = new Set()): Promise<void> => {
|
|
742
|
+
const identity: string = await fs.promises.realpath(path);
|
|
743
|
+
if (visited.has(identity)) return;
|
|
744
|
+
visited.add(identity);
|
|
423
745
|
const directory: string[] = await fs.promises.readdir(path);
|
|
424
746
|
for (const file of directory) {
|
|
425
747
|
const location: string = `${path}/${file}`;
|
|
426
748
|
const stat: fs.Stats = await fs.promises.stat(location);
|
|
427
|
-
if (stat.isDirectory() === true) await iterate(ctx)(location);
|
|
749
|
+
if (stat.isDirectory() === true) await iterate(ctx)(location, visited);
|
|
428
750
|
// Load feature files whose extension matches `props.extension`. This is
|
|
429
751
|
// configurable instead of inferred from the servant's own module
|
|
430
752
|
// extension, because the benchmark library may be built (`.js`) while the
|
|
@@ -432,6 +754,7 @@ const iterate =
|
|
|
432
754
|
// inferring would look for `.js` features that do not exist, load
|
|
433
755
|
// nothing, and spin forever.
|
|
434
756
|
else if (
|
|
757
|
+
stat.isFile() &&
|
|
435
758
|
file.endsWith(`.${ctx.props.extension ?? "js"}`) &&
|
|
436
759
|
file.endsWith(".d.ts") === false
|
|
437
760
|
) {
|
|
@@ -451,45 +774,3 @@ const iterate =
|
|
|
451
774
|
}
|
|
452
775
|
}
|
|
453
776
|
};
|
|
454
|
-
|
|
455
|
-
const statistics = (
|
|
456
|
-
events: IBenchmarkEvent[],
|
|
457
|
-
): DynamicBenchmarker.IReport.IStatistics => {
|
|
458
|
-
const successes: IBenchmarkEvent[] = events.filter((event) => event.success);
|
|
459
|
-
return {
|
|
460
|
-
count: events.length,
|
|
461
|
-
success: successes.length,
|
|
462
|
-
...average(events),
|
|
463
|
-
};
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
const average = (
|
|
467
|
-
events: IBenchmarkEvent[],
|
|
468
|
-
): Pick<
|
|
469
|
-
DynamicBenchmarker.IReport.IStatistics,
|
|
470
|
-
"mean" | "stdev" | "minimum" | "maximum"
|
|
471
|
-
> => {
|
|
472
|
-
if (events.length === 0)
|
|
473
|
-
return {
|
|
474
|
-
mean: null,
|
|
475
|
-
stdev: null,
|
|
476
|
-
minimum: null,
|
|
477
|
-
maximum: null,
|
|
478
|
-
};
|
|
479
|
-
let mean: number = 0;
|
|
480
|
-
let stdev: number = 0;
|
|
481
|
-
let minimum: number = Number.MAX_SAFE_INTEGER;
|
|
482
|
-
let maximum: number = Number.MIN_SAFE_INTEGER;
|
|
483
|
-
for (const event of events) {
|
|
484
|
-
const elapsed: number =
|
|
485
|
-
new Date(event.completed_at).getTime() -
|
|
486
|
-
new Date(event.started_at).getTime();
|
|
487
|
-
mean += elapsed;
|
|
488
|
-
stdev += elapsed * elapsed;
|
|
489
|
-
minimum = Math.min(minimum, elapsed);
|
|
490
|
-
maximum = Math.max(maximum, elapsed);
|
|
491
|
-
}
|
|
492
|
-
mean /= events.length;
|
|
493
|
-
stdev = Math.sqrt(stdev / events.length - mean * mean);
|
|
494
|
-
return { mean, stdev, minimum, maximum };
|
|
495
|
-
};
|