@nestia/benchmark 7.0.0-dev.20250607 → 7.0.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/LICENSE +21 -21
- package/README.md +92 -92
- package/package.json +5 -5
- package/src/DynamicBenchmarker.ts +442 -442
- package/src/IBenchmarkEvent.ts +10 -10
- package/src/index.ts +2 -2
- package/src/internal/DynamicBenchmarkReporter.ts +104 -104
- package/src/internal/IBenchmarkMaster.ts +4 -4
- package/src/internal/IBenchmarkServant.ts +8 -8
|
@@ -1,442 +1,442 @@
|
|
|
1
|
-
import { IConnection } from "@nestia/fetcher";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import { Driver, WorkerConnector, WorkerServer } from "tgrid";
|
|
4
|
-
import { HashMap, hash, sleep_for } from "tstl";
|
|
5
|
-
|
|
6
|
-
import { IBenchmarkEvent } from "./IBenchmarkEvent";
|
|
7
|
-
import { DynamicBenchmarkReporter } from "./internal/DynamicBenchmarkReporter";
|
|
8
|
-
import { IBenchmarkMaster } from "./internal/IBenchmarkMaster";
|
|
9
|
-
import { IBenchmarkServant } from "./internal/IBenchmarkServant";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Dynamic benchmark executor running prefixed functions.
|
|
13
|
-
*
|
|
14
|
-
* `DynamicBenchmarker` is composed with two programs,
|
|
15
|
-
* {@link DynamicBenchmarker.master} and
|
|
16
|
-
* {@link DynamicBenchmarker.servant servants}. The master program creates
|
|
17
|
-
* multiple servant programs, and the servant programs execute the prefixed
|
|
18
|
-
* functions in parallel. When the pre-congirued count of requests are all
|
|
19
|
-
* completed, the master program collects the results and returns them.
|
|
20
|
-
*
|
|
21
|
-
* Therefore, when you want to benchmark the performance of a backend server,
|
|
22
|
-
* you have to make two programs; one for calling the
|
|
23
|
-
* {@link DynamicBenchmarker.master} function, and the other for calling the
|
|
24
|
-
* {@link DynamicBenchmarker.servant} function. Also, never forget to write
|
|
25
|
-
* the path of the servant program to the
|
|
26
|
-
* {@link DynamicBenchmarker.IMasterProps.servant} property.
|
|
27
|
-
*
|
|
28
|
-
* Also, you when you complete the benchmark execution through the
|
|
29
|
-
* {@link DynamicBenchmarker.master} and {@link DynamicBenchmarker.servant}
|
|
30
|
-
* functions, you can convert the result to markdown content by using the
|
|
31
|
-
* {@link DynamicBenchmarker.markdown} function.
|
|
32
|
-
*
|
|
33
|
-
* Additionally, if you hope to see some utilization cases,
|
|
34
|
-
* see the below example tagged links.
|
|
35
|
-
*
|
|
36
|
-
* @example https://github.com/samchon/nestia-start/blob/master/test/benchmaark/index.ts
|
|
37
|
-
* @example https://github.com/samchon/backend/blob/master/test/benchmark/index.ts
|
|
38
|
-
* @author Jeongho Nam - https://github.com/samchon
|
|
39
|
-
*/
|
|
40
|
-
export namespace DynamicBenchmarker {
|
|
41
|
-
/**
|
|
42
|
-
* Properties of the master program.
|
|
43
|
-
*/
|
|
44
|
-
export interface IMasterProps {
|
|
45
|
-
/**
|
|
46
|
-
* Total count of the requests.
|
|
47
|
-
*/
|
|
48
|
-
count: number;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Number of threads.
|
|
52
|
-
*
|
|
53
|
-
* The number of threads to be executed as parallel servant.
|
|
54
|
-
*/
|
|
55
|
-
threads: number;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Number of simultaneous requests.
|
|
59
|
-
*
|
|
60
|
-
* The number of requests to be executed simultaneously.
|
|
61
|
-
*
|
|
62
|
-
* This property value would be divided by the {@link threads} in the servants.
|
|
63
|
-
*/
|
|
64
|
-
simultaneous: number;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Path of the servant program.
|
|
68
|
-
*
|
|
69
|
-
* The path of the servant program executing the
|
|
70
|
-
* {@link DynamicBenchmarker.servant} function.
|
|
71
|
-
*/
|
|
72
|
-
servant: string;
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Filter function.
|
|
76
|
-
*
|
|
77
|
-
* The filter function to determine whether to execute the function in
|
|
78
|
-
* the servant or not.
|
|
79
|
-
*
|
|
80
|
-
* @param name Function name
|
|
81
|
-
* @returns Whether to execute the function or not.
|
|
82
|
-
*/
|
|
83
|
-
filter?: (name: string) => boolean;
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Progress callback function.
|
|
87
|
-
*
|
|
88
|
-
* @param complete The number of completed requests.
|
|
89
|
-
*/
|
|
90
|
-
progress?: (complete: number) => void;
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Get memory usage.
|
|
94
|
-
*
|
|
95
|
-
* Get the memory usage of the master program.
|
|
96
|
-
*
|
|
97
|
-
* Specify this property only when your backend server is running on
|
|
98
|
-
* a different process, so that need to measure the memory usage of
|
|
99
|
-
* the backend server from other process.
|
|
100
|
-
*/
|
|
101
|
-
memory?: () => Promise<NodeJS.MemoryUsage>;
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Standard I/O option.
|
|
105
|
-
*
|
|
106
|
-
* The standard I/O option for the servant programs.
|
|
107
|
-
*/
|
|
108
|
-
stdio?: undefined | "overlapped" | "pipe" | "ignore" | "inherit";
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Properties of the servant program.
|
|
113
|
-
*/
|
|
114
|
-
export interface IServantProps<Parameters extends any[]> {
|
|
115
|
-
/**
|
|
116
|
-
* Default connection.
|
|
117
|
-
*
|
|
118
|
-
* Default connection to be used in the servant.
|
|
119
|
-
*/
|
|
120
|
-
connection: IConnection;
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Location of the benchmark functions.
|
|
124
|
-
*/
|
|
125
|
-
location: string;
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Prefix of the benchmark functions.
|
|
129
|
-
*
|
|
130
|
-
* Every prefixed function will be executed in the servant.
|
|
131
|
-
*
|
|
132
|
-
* In other words, if a function name doesn't start with the prefix,
|
|
133
|
-
* then it would never be executed.
|
|
134
|
-
*/
|
|
135
|
-
prefix: string;
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Get parameters of a function.
|
|
139
|
-
*
|
|
140
|
-
* When composing the parameters, never forget to copy the
|
|
141
|
-
* {@link IConnection.logger} property of default connection to the
|
|
142
|
-
* returning parameters.
|
|
143
|
-
*
|
|
144
|
-
* @param connection Default connection instance
|
|
145
|
-
* @param name Function name
|
|
146
|
-
*/
|
|
147
|
-
parameters: (connection: IConnection, name: string) => Parameters;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Benchmark report.
|
|
152
|
-
*/
|
|
153
|
-
export interface IReport {
|
|
154
|
-
count: number;
|
|
155
|
-
threads: number;
|
|
156
|
-
simultaneous: number;
|
|
157
|
-
started_at: string;
|
|
158
|
-
completed_at: string;
|
|
159
|
-
statistics: IReport.IStatistics;
|
|
160
|
-
endpoints: Array<IReport.IEndpoint & IReport.IStatistics>;
|
|
161
|
-
memories: IReport.IMemory[];
|
|
162
|
-
}
|
|
163
|
-
export namespace IReport {
|
|
164
|
-
export interface IEndpoint {
|
|
165
|
-
method: string;
|
|
166
|
-
path: string;
|
|
167
|
-
}
|
|
168
|
-
export interface IStatistics {
|
|
169
|
-
count: number;
|
|
170
|
-
success: number;
|
|
171
|
-
mean: number | null;
|
|
172
|
-
stdev: number | null;
|
|
173
|
-
minimum: number | null;
|
|
174
|
-
maximum: number | null;
|
|
175
|
-
}
|
|
176
|
-
export interface IMemory {
|
|
177
|
-
time: string;
|
|
178
|
-
usage: NodeJS.MemoryUsage;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Master program.
|
|
184
|
-
*
|
|
185
|
-
* Creates a master program that executing the servant programs in parallel.
|
|
186
|
-
*
|
|
187
|
-
* Note that, {@link IMasterProps.servant} property must be the path of
|
|
188
|
-
* the servant program executing the {@link servant} function.
|
|
189
|
-
*
|
|
190
|
-
* @param props Properties of the master program
|
|
191
|
-
* @returns Benchmark report
|
|
192
|
-
*/
|
|
193
|
-
export const master = async (props: IMasterProps): Promise<IReport> => {
|
|
194
|
-
const completes: number[] = new Array(props.threads).fill(0);
|
|
195
|
-
const servants: WorkerConnector<
|
|
196
|
-
null,
|
|
197
|
-
IBenchmarkMaster,
|
|
198
|
-
IBenchmarkServant
|
|
199
|
-
>[] = await Promise.all(
|
|
200
|
-
new Array(props.threads).fill(null).map(async (_, i) => {
|
|
201
|
-
const connector: WorkerConnector<
|
|
202
|
-
null,
|
|
203
|
-
IBenchmarkMaster,
|
|
204
|
-
IBenchmarkServant
|
|
205
|
-
> = new WorkerConnector(
|
|
206
|
-
null,
|
|
207
|
-
{
|
|
208
|
-
filter: props.filter ?? (() => true),
|
|
209
|
-
progress: (current) => {
|
|
210
|
-
completes[i] = current;
|
|
211
|
-
if (props.progress)
|
|
212
|
-
props.progress(completes.reduce((a, b) => a + b, 0));
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
"process",
|
|
216
|
-
);
|
|
217
|
-
await connector.connect(props.servant, { stdio: props.stdio });
|
|
218
|
-
return connector;
|
|
219
|
-
}),
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
const started_at: Date = new Date();
|
|
223
|
-
const memories: IReport.IMemory[] = [];
|
|
224
|
-
let completed_at: Date | null = null;
|
|
225
|
-
|
|
226
|
-
(async () => {
|
|
227
|
-
const getter = props.memory ?? (async () => process.memoryUsage());
|
|
228
|
-
while (completed_at === null) {
|
|
229
|
-
await sleep_for(1_000);
|
|
230
|
-
memories.push({
|
|
231
|
-
usage: await getter(),
|
|
232
|
-
time: new Date().toISOString(),
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
})().catch(() => {});
|
|
236
|
-
|
|
237
|
-
const events: IBenchmarkEvent[] = (
|
|
238
|
-
await Promise.all(
|
|
239
|
-
servants.map((connector) =>
|
|
240
|
-
connector.getDriver().execute({
|
|
241
|
-
count: Math.ceil(props.count / props.threads),
|
|
242
|
-
simultaneous: Math.ceil(props.simultaneous / props.threads),
|
|
243
|
-
}),
|
|
244
|
-
),
|
|
245
|
-
)
|
|
246
|
-
).flat();
|
|
247
|
-
|
|
248
|
-
completed_at = new Date();
|
|
249
|
-
await Promise.all(servants.map((connector) => connector.close()));
|
|
250
|
-
if (props.progress) props.progress(props.count);
|
|
251
|
-
|
|
252
|
-
const endpoints: HashMap<IReport.IEndpoint, IBenchmarkEvent[]> =
|
|
253
|
-
new HashMap(
|
|
254
|
-
(key) => hash(key.method, key.path),
|
|
255
|
-
(x, y) => x.method === y.method && x.path === y.path,
|
|
256
|
-
);
|
|
257
|
-
for (const e of events)
|
|
258
|
-
endpoints
|
|
259
|
-
.take(
|
|
260
|
-
{
|
|
261
|
-
method: e.metadata.method,
|
|
262
|
-
path: e.metadata.template ?? e.metadata.path,
|
|
263
|
-
},
|
|
264
|
-
() => [],
|
|
265
|
-
)
|
|
266
|
-
.push(e);
|
|
267
|
-
return {
|
|
268
|
-
count: props.count,
|
|
269
|
-
threads: props.threads,
|
|
270
|
-
simultaneous: props.simultaneous,
|
|
271
|
-
statistics: statistics(events),
|
|
272
|
-
endpoints: [...endpoints].map((it) => ({
|
|
273
|
-
...statistics(it.second),
|
|
274
|
-
...it.first,
|
|
275
|
-
})),
|
|
276
|
-
started_at: started_at.toISOString(),
|
|
277
|
-
completed_at: completed_at.toISOString(),
|
|
278
|
-
memories,
|
|
279
|
-
};
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Create a servant program.
|
|
284
|
-
*
|
|
285
|
-
* Creates a servant program executing the prefixed functions in parallel.
|
|
286
|
-
*
|
|
287
|
-
* @param props Properties of the servant program
|
|
288
|
-
* @returns Servant program as a worker server
|
|
289
|
-
*/
|
|
290
|
-
export const servant = async <Parameters extends any[]>(
|
|
291
|
-
props: IServantProps<Parameters>,
|
|
292
|
-
): Promise<WorkerServer<null, IBenchmarkServant, IBenchmarkMaster>> => {
|
|
293
|
-
const server: WorkerServer<null, IBenchmarkServant, IBenchmarkMaster> =
|
|
294
|
-
new WorkerServer();
|
|
295
|
-
await server.open({
|
|
296
|
-
execute: execute({
|
|
297
|
-
driver: server.getDriver(),
|
|
298
|
-
props,
|
|
299
|
-
}),
|
|
300
|
-
});
|
|
301
|
-
return server;
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Convert the benchmark report to markdown content.
|
|
306
|
-
*
|
|
307
|
-
* @param report Benchmark report
|
|
308
|
-
* @returns Markdown content
|
|
309
|
-
*/
|
|
310
|
-
export const markdown = (report: DynamicBenchmarker.IReport): string =>
|
|
311
|
-
DynamicBenchmarkReporter.markdown(report);
|
|
312
|
-
|
|
313
|
-
const execute =
|
|
314
|
-
<Parameters extends any[]>(ctx: {
|
|
315
|
-
driver: Driver<IBenchmarkMaster>;
|
|
316
|
-
props: IServantProps<Parameters>;
|
|
317
|
-
}) =>
|
|
318
|
-
async (mass: {
|
|
319
|
-
count: number;
|
|
320
|
-
simultaneous: number;
|
|
321
|
-
}): Promise<IBenchmarkEvent[]> => {
|
|
322
|
-
const functions: IFunction<Parameters>[] = [];
|
|
323
|
-
await iterate({
|
|
324
|
-
collection: functions,
|
|
325
|
-
driver: ctx.driver,
|
|
326
|
-
props: ctx.props,
|
|
327
|
-
})(ctx.props.location);
|
|
328
|
-
|
|
329
|
-
const entireEvents: IBenchmarkEvent[] = [];
|
|
330
|
-
await Promise.all(
|
|
331
|
-
new Array(mass.simultaneous)
|
|
332
|
-
.fill(null)
|
|
333
|
-
.map(() => 1)
|
|
334
|
-
.map(async () => {
|
|
335
|
-
while (entireEvents.length < mass.count) {
|
|
336
|
-
const localEvents: IBenchmarkEvent[] = [];
|
|
337
|
-
const func: IFunction<Parameters> =
|
|
338
|
-
functions[Math.floor(Math.random() * functions.length)];
|
|
339
|
-
const connection: IConnection = {
|
|
340
|
-
...ctx.props.connection,
|
|
341
|
-
logger: async (fe): Promise<void> => {
|
|
342
|
-
const be: IBenchmarkEvent = {
|
|
343
|
-
metadata: fe.route,
|
|
344
|
-
status: fe.status,
|
|
345
|
-
started_at: fe.started_at.toISOString(),
|
|
346
|
-
respond_at: fe.respond_at?.toISOString() ?? null,
|
|
347
|
-
completed_at: fe.completed_at.toISOString(),
|
|
348
|
-
success: true,
|
|
349
|
-
};
|
|
350
|
-
localEvents.push(be);
|
|
351
|
-
entireEvents.push(be);
|
|
352
|
-
},
|
|
353
|
-
};
|
|
354
|
-
try {
|
|
355
|
-
await func.value(...ctx.props.parameters(connection, func.key));
|
|
356
|
-
} catch (exp) {
|
|
357
|
-
for (const e of localEvents)
|
|
358
|
-
e.success = e.status === 200 || e.status === 201;
|
|
359
|
-
}
|
|
360
|
-
if (localEvents.length !== 0)
|
|
361
|
-
ctx.driver.progress(entireEvents.length).catch(() => {});
|
|
362
|
-
}
|
|
363
|
-
}),
|
|
364
|
-
);
|
|
365
|
-
await ctx.driver.progress(entireEvents.length);
|
|
366
|
-
return entireEvents;
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
interface IFunction<Parameters extends any[]> {
|
|
371
|
-
key: string;
|
|
372
|
-
value: (...args: Parameters) => Promise<void>;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const iterate =
|
|
376
|
-
<Parameters extends any[]>(ctx: {
|
|
377
|
-
collection: IFunction<Parameters>[];
|
|
378
|
-
driver: Driver<IBenchmarkMaster>;
|
|
379
|
-
props: DynamicBenchmarker.IServantProps<Parameters>;
|
|
380
|
-
}) =>
|
|
381
|
-
async (path: string): Promise<void> => {
|
|
382
|
-
const directory: string[] = await fs.promises.readdir(path);
|
|
383
|
-
for (const file of directory) {
|
|
384
|
-
const location: string = `${path}/${file}`;
|
|
385
|
-
const stat: fs.Stats = await fs.promises.stat(location);
|
|
386
|
-
if (stat.isDirectory() === true) await iterate(ctx)(location);
|
|
387
|
-
else if (file.endsWith(".js") === true) {
|
|
388
|
-
const modulo = await import(location);
|
|
389
|
-
for (const [key, value] of Object.entries(modulo)) {
|
|
390
|
-
if (typeof value !== "function") continue;
|
|
391
|
-
else if (key.startsWith(ctx.props.prefix) === false) continue;
|
|
392
|
-
else if ((await ctx.driver.filter(key)) === false) continue;
|
|
393
|
-
ctx.collection.push({
|
|
394
|
-
key,
|
|
395
|
-
value: value as (...args: Parameters) => Promise<any>,
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
const statistics = (
|
|
403
|
-
events: IBenchmarkEvent[],
|
|
404
|
-
): DynamicBenchmarker.IReport.IStatistics => {
|
|
405
|
-
const successes: IBenchmarkEvent[] = events.filter((event) => event.success);
|
|
406
|
-
return {
|
|
407
|
-
count: events.length,
|
|
408
|
-
success: successes.length,
|
|
409
|
-
...average(events),
|
|
410
|
-
};
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
const average = (
|
|
414
|
-
events: IBenchmarkEvent[],
|
|
415
|
-
): Pick<
|
|
416
|
-
DynamicBenchmarker.IReport.IStatistics,
|
|
417
|
-
"mean" | "stdev" | "minimum" | "maximum"
|
|
418
|
-
> => {
|
|
419
|
-
if (events.length === 0)
|
|
420
|
-
return {
|
|
421
|
-
mean: null,
|
|
422
|
-
stdev: null,
|
|
423
|
-
minimum: null,
|
|
424
|
-
maximum: null,
|
|
425
|
-
};
|
|
426
|
-
let mean: number = 0;
|
|
427
|
-
let stdev: number = 0;
|
|
428
|
-
let minimum: number = Number.MAX_SAFE_INTEGER;
|
|
429
|
-
let maximum: number = Number.MIN_SAFE_INTEGER;
|
|
430
|
-
for (const event of events) {
|
|
431
|
-
const elapsed: number =
|
|
432
|
-
new Date(event.completed_at).getTime() -
|
|
433
|
-
new Date(event.started_at).getTime();
|
|
434
|
-
mean += elapsed;
|
|
435
|
-
stdev += elapsed * elapsed;
|
|
436
|
-
minimum = Math.min(minimum, elapsed);
|
|
437
|
-
maximum = Math.max(maximum, elapsed);
|
|
438
|
-
}
|
|
439
|
-
mean /= events.length;
|
|
440
|
-
stdev = Math.sqrt(stdev / events.length - mean * mean);
|
|
441
|
-
return { mean, stdev, minimum, maximum };
|
|
442
|
-
};
|
|
1
|
+
import { IConnection } from "@nestia/fetcher";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { Driver, WorkerConnector, WorkerServer } from "tgrid";
|
|
4
|
+
import { HashMap, hash, sleep_for } from "tstl";
|
|
5
|
+
|
|
6
|
+
import { IBenchmarkEvent } from "./IBenchmarkEvent";
|
|
7
|
+
import { DynamicBenchmarkReporter } from "./internal/DynamicBenchmarkReporter";
|
|
8
|
+
import { IBenchmarkMaster } from "./internal/IBenchmarkMaster";
|
|
9
|
+
import { IBenchmarkServant } from "./internal/IBenchmarkServant";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Dynamic benchmark executor running prefixed functions.
|
|
13
|
+
*
|
|
14
|
+
* `DynamicBenchmarker` is composed with two programs,
|
|
15
|
+
* {@link DynamicBenchmarker.master} and
|
|
16
|
+
* {@link DynamicBenchmarker.servant servants}. The master program creates
|
|
17
|
+
* multiple servant programs, and the servant programs execute the prefixed
|
|
18
|
+
* functions in parallel. When the pre-congirued count of requests are all
|
|
19
|
+
* completed, the master program collects the results and returns them.
|
|
20
|
+
*
|
|
21
|
+
* Therefore, when you want to benchmark the performance of a backend server,
|
|
22
|
+
* you have to make two programs; one for calling the
|
|
23
|
+
* {@link DynamicBenchmarker.master} function, and the other for calling the
|
|
24
|
+
* {@link DynamicBenchmarker.servant} function. Also, never forget to write
|
|
25
|
+
* the path of the servant program to the
|
|
26
|
+
* {@link DynamicBenchmarker.IMasterProps.servant} property.
|
|
27
|
+
*
|
|
28
|
+
* Also, you when you complete the benchmark execution through the
|
|
29
|
+
* {@link DynamicBenchmarker.master} and {@link DynamicBenchmarker.servant}
|
|
30
|
+
* functions, you can convert the result to markdown content by using the
|
|
31
|
+
* {@link DynamicBenchmarker.markdown} function.
|
|
32
|
+
*
|
|
33
|
+
* Additionally, if you hope to see some utilization cases,
|
|
34
|
+
* see the below example tagged links.
|
|
35
|
+
*
|
|
36
|
+
* @example https://github.com/samchon/nestia-start/blob/master/test/benchmaark/index.ts
|
|
37
|
+
* @example https://github.com/samchon/backend/blob/master/test/benchmark/index.ts
|
|
38
|
+
* @author Jeongho Nam - https://github.com/samchon
|
|
39
|
+
*/
|
|
40
|
+
export namespace DynamicBenchmarker {
|
|
41
|
+
/**
|
|
42
|
+
* Properties of the master program.
|
|
43
|
+
*/
|
|
44
|
+
export interface IMasterProps {
|
|
45
|
+
/**
|
|
46
|
+
* Total count of the requests.
|
|
47
|
+
*/
|
|
48
|
+
count: number;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Number of threads.
|
|
52
|
+
*
|
|
53
|
+
* The number of threads to be executed as parallel servant.
|
|
54
|
+
*/
|
|
55
|
+
threads: number;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Number of simultaneous requests.
|
|
59
|
+
*
|
|
60
|
+
* The number of requests to be executed simultaneously.
|
|
61
|
+
*
|
|
62
|
+
* This property value would be divided by the {@link threads} in the servants.
|
|
63
|
+
*/
|
|
64
|
+
simultaneous: number;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Path of the servant program.
|
|
68
|
+
*
|
|
69
|
+
* The path of the servant program executing the
|
|
70
|
+
* {@link DynamicBenchmarker.servant} function.
|
|
71
|
+
*/
|
|
72
|
+
servant: string;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Filter function.
|
|
76
|
+
*
|
|
77
|
+
* The filter function to determine whether to execute the function in
|
|
78
|
+
* the servant or not.
|
|
79
|
+
*
|
|
80
|
+
* @param name Function name
|
|
81
|
+
* @returns Whether to execute the function or not.
|
|
82
|
+
*/
|
|
83
|
+
filter?: (name: string) => boolean;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Progress callback function.
|
|
87
|
+
*
|
|
88
|
+
* @param complete The number of completed requests.
|
|
89
|
+
*/
|
|
90
|
+
progress?: (complete: number) => void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get memory usage.
|
|
94
|
+
*
|
|
95
|
+
* Get the memory usage of the master program.
|
|
96
|
+
*
|
|
97
|
+
* Specify this property only when your backend server is running on
|
|
98
|
+
* a different process, so that need to measure the memory usage of
|
|
99
|
+
* the backend server from other process.
|
|
100
|
+
*/
|
|
101
|
+
memory?: () => Promise<NodeJS.MemoryUsage>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Standard I/O option.
|
|
105
|
+
*
|
|
106
|
+
* The standard I/O option for the servant programs.
|
|
107
|
+
*/
|
|
108
|
+
stdio?: undefined | "overlapped" | "pipe" | "ignore" | "inherit";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Properties of the servant program.
|
|
113
|
+
*/
|
|
114
|
+
export interface IServantProps<Parameters extends any[]> {
|
|
115
|
+
/**
|
|
116
|
+
* Default connection.
|
|
117
|
+
*
|
|
118
|
+
* Default connection to be used in the servant.
|
|
119
|
+
*/
|
|
120
|
+
connection: IConnection;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Location of the benchmark functions.
|
|
124
|
+
*/
|
|
125
|
+
location: string;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Prefix of the benchmark functions.
|
|
129
|
+
*
|
|
130
|
+
* Every prefixed function will be executed in the servant.
|
|
131
|
+
*
|
|
132
|
+
* In other words, if a function name doesn't start with the prefix,
|
|
133
|
+
* then it would never be executed.
|
|
134
|
+
*/
|
|
135
|
+
prefix: string;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get parameters of a function.
|
|
139
|
+
*
|
|
140
|
+
* When composing the parameters, never forget to copy the
|
|
141
|
+
* {@link IConnection.logger} property of default connection to the
|
|
142
|
+
* returning parameters.
|
|
143
|
+
*
|
|
144
|
+
* @param connection Default connection instance
|
|
145
|
+
* @param name Function name
|
|
146
|
+
*/
|
|
147
|
+
parameters: (connection: IConnection, name: string) => Parameters;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Benchmark report.
|
|
152
|
+
*/
|
|
153
|
+
export interface IReport {
|
|
154
|
+
count: number;
|
|
155
|
+
threads: number;
|
|
156
|
+
simultaneous: number;
|
|
157
|
+
started_at: string;
|
|
158
|
+
completed_at: string;
|
|
159
|
+
statistics: IReport.IStatistics;
|
|
160
|
+
endpoints: Array<IReport.IEndpoint & IReport.IStatistics>;
|
|
161
|
+
memories: IReport.IMemory[];
|
|
162
|
+
}
|
|
163
|
+
export namespace IReport {
|
|
164
|
+
export interface IEndpoint {
|
|
165
|
+
method: string;
|
|
166
|
+
path: string;
|
|
167
|
+
}
|
|
168
|
+
export interface IStatistics {
|
|
169
|
+
count: number;
|
|
170
|
+
success: number;
|
|
171
|
+
mean: number | null;
|
|
172
|
+
stdev: number | null;
|
|
173
|
+
minimum: number | null;
|
|
174
|
+
maximum: number | null;
|
|
175
|
+
}
|
|
176
|
+
export interface IMemory {
|
|
177
|
+
time: string;
|
|
178
|
+
usage: NodeJS.MemoryUsage;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Master program.
|
|
184
|
+
*
|
|
185
|
+
* Creates a master program that executing the servant programs in parallel.
|
|
186
|
+
*
|
|
187
|
+
* Note that, {@link IMasterProps.servant} property must be the path of
|
|
188
|
+
* the servant program executing the {@link servant} function.
|
|
189
|
+
*
|
|
190
|
+
* @param props Properties of the master program
|
|
191
|
+
* @returns Benchmark report
|
|
192
|
+
*/
|
|
193
|
+
export const master = async (props: IMasterProps): Promise<IReport> => {
|
|
194
|
+
const completes: number[] = new Array(props.threads).fill(0);
|
|
195
|
+
const servants: WorkerConnector<
|
|
196
|
+
null,
|
|
197
|
+
IBenchmarkMaster,
|
|
198
|
+
IBenchmarkServant
|
|
199
|
+
>[] = await Promise.all(
|
|
200
|
+
new Array(props.threads).fill(null).map(async (_, i) => {
|
|
201
|
+
const connector: WorkerConnector<
|
|
202
|
+
null,
|
|
203
|
+
IBenchmarkMaster,
|
|
204
|
+
IBenchmarkServant
|
|
205
|
+
> = new WorkerConnector(
|
|
206
|
+
null,
|
|
207
|
+
{
|
|
208
|
+
filter: props.filter ?? (() => true),
|
|
209
|
+
progress: (current) => {
|
|
210
|
+
completes[i] = current;
|
|
211
|
+
if (props.progress)
|
|
212
|
+
props.progress(completes.reduce((a, b) => a + b, 0));
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
"process",
|
|
216
|
+
);
|
|
217
|
+
await connector.connect(props.servant, { stdio: props.stdio });
|
|
218
|
+
return connector;
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const started_at: Date = new Date();
|
|
223
|
+
const memories: IReport.IMemory[] = [];
|
|
224
|
+
let completed_at: Date | null = null;
|
|
225
|
+
|
|
226
|
+
(async () => {
|
|
227
|
+
const getter = props.memory ?? (async () => process.memoryUsage());
|
|
228
|
+
while (completed_at === null) {
|
|
229
|
+
await sleep_for(1_000);
|
|
230
|
+
memories.push({
|
|
231
|
+
usage: await getter(),
|
|
232
|
+
time: new Date().toISOString(),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
})().catch(() => {});
|
|
236
|
+
|
|
237
|
+
const events: IBenchmarkEvent[] = (
|
|
238
|
+
await Promise.all(
|
|
239
|
+
servants.map((connector) =>
|
|
240
|
+
connector.getDriver().execute({
|
|
241
|
+
count: Math.ceil(props.count / props.threads),
|
|
242
|
+
simultaneous: Math.ceil(props.simultaneous / props.threads),
|
|
243
|
+
}),
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
).flat();
|
|
247
|
+
|
|
248
|
+
completed_at = new Date();
|
|
249
|
+
await Promise.all(servants.map((connector) => connector.close()));
|
|
250
|
+
if (props.progress) props.progress(props.count);
|
|
251
|
+
|
|
252
|
+
const endpoints: HashMap<IReport.IEndpoint, IBenchmarkEvent[]> =
|
|
253
|
+
new HashMap(
|
|
254
|
+
(key) => hash(key.method, key.path),
|
|
255
|
+
(x, y) => x.method === y.method && x.path === y.path,
|
|
256
|
+
);
|
|
257
|
+
for (const e of events)
|
|
258
|
+
endpoints
|
|
259
|
+
.take(
|
|
260
|
+
{
|
|
261
|
+
method: e.metadata.method,
|
|
262
|
+
path: e.metadata.template ?? e.metadata.path,
|
|
263
|
+
},
|
|
264
|
+
() => [],
|
|
265
|
+
)
|
|
266
|
+
.push(e);
|
|
267
|
+
return {
|
|
268
|
+
count: props.count,
|
|
269
|
+
threads: props.threads,
|
|
270
|
+
simultaneous: props.simultaneous,
|
|
271
|
+
statistics: statistics(events),
|
|
272
|
+
endpoints: [...endpoints].map((it) => ({
|
|
273
|
+
...statistics(it.second),
|
|
274
|
+
...it.first,
|
|
275
|
+
})),
|
|
276
|
+
started_at: started_at.toISOString(),
|
|
277
|
+
completed_at: completed_at.toISOString(),
|
|
278
|
+
memories,
|
|
279
|
+
};
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create a servant program.
|
|
284
|
+
*
|
|
285
|
+
* Creates a servant program executing the prefixed functions in parallel.
|
|
286
|
+
*
|
|
287
|
+
* @param props Properties of the servant program
|
|
288
|
+
* @returns Servant program as a worker server
|
|
289
|
+
*/
|
|
290
|
+
export const servant = async <Parameters extends any[]>(
|
|
291
|
+
props: IServantProps<Parameters>,
|
|
292
|
+
): Promise<WorkerServer<null, IBenchmarkServant, IBenchmarkMaster>> => {
|
|
293
|
+
const server: WorkerServer<null, IBenchmarkServant, IBenchmarkMaster> =
|
|
294
|
+
new WorkerServer();
|
|
295
|
+
await server.open({
|
|
296
|
+
execute: execute({
|
|
297
|
+
driver: server.getDriver(),
|
|
298
|
+
props,
|
|
299
|
+
}),
|
|
300
|
+
});
|
|
301
|
+
return server;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Convert the benchmark report to markdown content.
|
|
306
|
+
*
|
|
307
|
+
* @param report Benchmark report
|
|
308
|
+
* @returns Markdown content
|
|
309
|
+
*/
|
|
310
|
+
export const markdown = (report: DynamicBenchmarker.IReport): string =>
|
|
311
|
+
DynamicBenchmarkReporter.markdown(report);
|
|
312
|
+
|
|
313
|
+
const execute =
|
|
314
|
+
<Parameters extends any[]>(ctx: {
|
|
315
|
+
driver: Driver<IBenchmarkMaster>;
|
|
316
|
+
props: IServantProps<Parameters>;
|
|
317
|
+
}) =>
|
|
318
|
+
async (mass: {
|
|
319
|
+
count: number;
|
|
320
|
+
simultaneous: number;
|
|
321
|
+
}): Promise<IBenchmarkEvent[]> => {
|
|
322
|
+
const functions: IFunction<Parameters>[] = [];
|
|
323
|
+
await iterate({
|
|
324
|
+
collection: functions,
|
|
325
|
+
driver: ctx.driver,
|
|
326
|
+
props: ctx.props,
|
|
327
|
+
})(ctx.props.location);
|
|
328
|
+
|
|
329
|
+
const entireEvents: IBenchmarkEvent[] = [];
|
|
330
|
+
await Promise.all(
|
|
331
|
+
new Array(mass.simultaneous)
|
|
332
|
+
.fill(null)
|
|
333
|
+
.map(() => 1)
|
|
334
|
+
.map(async () => {
|
|
335
|
+
while (entireEvents.length < mass.count) {
|
|
336
|
+
const localEvents: IBenchmarkEvent[] = [];
|
|
337
|
+
const func: IFunction<Parameters> =
|
|
338
|
+
functions[Math.floor(Math.random() * functions.length)];
|
|
339
|
+
const connection: IConnection = {
|
|
340
|
+
...ctx.props.connection,
|
|
341
|
+
logger: async (fe): Promise<void> => {
|
|
342
|
+
const be: IBenchmarkEvent = {
|
|
343
|
+
metadata: fe.route,
|
|
344
|
+
status: fe.status,
|
|
345
|
+
started_at: fe.started_at.toISOString(),
|
|
346
|
+
respond_at: fe.respond_at?.toISOString() ?? null,
|
|
347
|
+
completed_at: fe.completed_at.toISOString(),
|
|
348
|
+
success: true,
|
|
349
|
+
};
|
|
350
|
+
localEvents.push(be);
|
|
351
|
+
entireEvents.push(be);
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
try {
|
|
355
|
+
await func.value(...ctx.props.parameters(connection, func.key));
|
|
356
|
+
} catch (exp) {
|
|
357
|
+
for (const e of localEvents)
|
|
358
|
+
e.success = e.status === 200 || e.status === 201;
|
|
359
|
+
}
|
|
360
|
+
if (localEvents.length !== 0)
|
|
361
|
+
ctx.driver.progress(entireEvents.length).catch(() => {});
|
|
362
|
+
}
|
|
363
|
+
}),
|
|
364
|
+
);
|
|
365
|
+
await ctx.driver.progress(entireEvents.length);
|
|
366
|
+
return entireEvents;
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
interface IFunction<Parameters extends any[]> {
|
|
371
|
+
key: string;
|
|
372
|
+
value: (...args: Parameters) => Promise<void>;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const iterate =
|
|
376
|
+
<Parameters extends any[]>(ctx: {
|
|
377
|
+
collection: IFunction<Parameters>[];
|
|
378
|
+
driver: Driver<IBenchmarkMaster>;
|
|
379
|
+
props: DynamicBenchmarker.IServantProps<Parameters>;
|
|
380
|
+
}) =>
|
|
381
|
+
async (path: string): Promise<void> => {
|
|
382
|
+
const directory: string[] = await fs.promises.readdir(path);
|
|
383
|
+
for (const file of directory) {
|
|
384
|
+
const location: string = `${path}/${file}`;
|
|
385
|
+
const stat: fs.Stats = await fs.promises.stat(location);
|
|
386
|
+
if (stat.isDirectory() === true) await iterate(ctx)(location);
|
|
387
|
+
else if (file.endsWith(".js") === true) {
|
|
388
|
+
const modulo = await import(location);
|
|
389
|
+
for (const [key, value] of Object.entries(modulo)) {
|
|
390
|
+
if (typeof value !== "function") continue;
|
|
391
|
+
else if (key.startsWith(ctx.props.prefix) === false) continue;
|
|
392
|
+
else if ((await ctx.driver.filter(key)) === false) continue;
|
|
393
|
+
ctx.collection.push({
|
|
394
|
+
key,
|
|
395
|
+
value: value as (...args: Parameters) => Promise<any>,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const statistics = (
|
|
403
|
+
events: IBenchmarkEvent[],
|
|
404
|
+
): DynamicBenchmarker.IReport.IStatistics => {
|
|
405
|
+
const successes: IBenchmarkEvent[] = events.filter((event) => event.success);
|
|
406
|
+
return {
|
|
407
|
+
count: events.length,
|
|
408
|
+
success: successes.length,
|
|
409
|
+
...average(events),
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const average = (
|
|
414
|
+
events: IBenchmarkEvent[],
|
|
415
|
+
): Pick<
|
|
416
|
+
DynamicBenchmarker.IReport.IStatistics,
|
|
417
|
+
"mean" | "stdev" | "minimum" | "maximum"
|
|
418
|
+
> => {
|
|
419
|
+
if (events.length === 0)
|
|
420
|
+
return {
|
|
421
|
+
mean: null,
|
|
422
|
+
stdev: null,
|
|
423
|
+
minimum: null,
|
|
424
|
+
maximum: null,
|
|
425
|
+
};
|
|
426
|
+
let mean: number = 0;
|
|
427
|
+
let stdev: number = 0;
|
|
428
|
+
let minimum: number = Number.MAX_SAFE_INTEGER;
|
|
429
|
+
let maximum: number = Number.MIN_SAFE_INTEGER;
|
|
430
|
+
for (const event of events) {
|
|
431
|
+
const elapsed: number =
|
|
432
|
+
new Date(event.completed_at).getTime() -
|
|
433
|
+
new Date(event.started_at).getTime();
|
|
434
|
+
mean += elapsed;
|
|
435
|
+
stdev += elapsed * elapsed;
|
|
436
|
+
minimum = Math.min(minimum, elapsed);
|
|
437
|
+
maximum = Math.max(maximum, elapsed);
|
|
438
|
+
}
|
|
439
|
+
mean /= events.length;
|
|
440
|
+
stdev = Math.sqrt(stdev / events.length - mean * mean);
|
|
441
|
+
return { mean, stdev, minimum, maximum };
|
|
442
|
+
};
|