node-av 5.1.0 → 5.2.0-beta.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/dist/api/rtp-stream.js +0 -6
- package/dist/api/rtp-stream.js.map +1 -1
- package/package.json +26 -11
- package/benchmarks/cases/latency.ts +0 -361
- package/benchmarks/cases/memory.ts +0 -260
- package/benchmarks/cases/transcode.ts +0 -271
- package/benchmarks/index.ts +0 -264
- package/benchmarks/regen-report.ts +0 -22
- package/benchmarks/results/.gitkeep +0 -2
- package/benchmarks/runner.ts +0 -247
- package/benchmarks/utils/ffmpeg-cli.ts +0 -363
- package/benchmarks/utils/measure.ts +0 -275
- package/benchmarks/utils/report.ts +0 -405
- package/binding.gyp +0 -166
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FFmpeg CLI Wrapper
|
|
3
|
-
*
|
|
4
|
-
* Provides utilities for running FFmpeg CLI commands and measuring
|
|
5
|
-
* their performance for comparison with node-av.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { spawn } from 'node:child_process';
|
|
9
|
-
import { existsSync } from 'node:fs';
|
|
10
|
-
import { platform } from 'node:os';
|
|
11
|
-
|
|
12
|
-
import { ffmpegPath, isFfmpegAvailable } from '../../src/ffmpeg/index.js';
|
|
13
|
-
|
|
14
|
-
import type { MeasureResult } from './measure.js';
|
|
15
|
-
|
|
16
|
-
export interface FFmpegRunOptions {
|
|
17
|
-
/** Input file path */
|
|
18
|
-
input: string;
|
|
19
|
-
/** Output file path */
|
|
20
|
-
output: string;
|
|
21
|
-
/** Additional FFmpeg arguments */
|
|
22
|
-
args?: string[];
|
|
23
|
-
/** Timeout in milliseconds */
|
|
24
|
-
timeout?: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface FFmpegRunResult {
|
|
28
|
-
/** Exit code */
|
|
29
|
-
exitCode: number;
|
|
30
|
-
/** Standard output */
|
|
31
|
-
stdout: string;
|
|
32
|
-
/** Standard error (contains progress) */
|
|
33
|
-
stderr: string;
|
|
34
|
-
/** Duration in milliseconds */
|
|
35
|
-
durationMs: number;
|
|
36
|
-
/** Parsed frame count if available */
|
|
37
|
-
framesProcessed?: number;
|
|
38
|
-
/** Parsed FPS if available */
|
|
39
|
-
fps?: number;
|
|
40
|
-
/** Peak memory (estimated from /usr/bin/time if available) */
|
|
41
|
-
peakMemoryBytes?: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Get the FFmpeg binary path, checking if it exists
|
|
46
|
-
*/
|
|
47
|
-
export function getFFmpegPath(): string {
|
|
48
|
-
if (isFfmpegAvailable()) {
|
|
49
|
-
return ffmpegPath();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Try system ffmpeg
|
|
53
|
-
const systemPaths = ['/usr/bin/ffmpeg', '/usr/local/bin/ffmpeg', '/opt/homebrew/bin/ffmpeg'];
|
|
54
|
-
|
|
55
|
-
for (const path of systemPaths) {
|
|
56
|
-
if (existsSync(path)) {
|
|
57
|
-
return path;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
throw new Error('FFmpeg binary not found. Please install FFmpeg or run npm install to download the bundled binary.');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Run FFmpeg CLI with the given arguments
|
|
66
|
-
*/
|
|
67
|
-
export async function runFFmpegCLI(options: FFmpegRunOptions): Promise<FFmpegRunResult> {
|
|
68
|
-
const { input, output, args = [], timeout = 300000 } = options;
|
|
69
|
-
|
|
70
|
-
const ffmpeg = getFFmpegPath();
|
|
71
|
-
|
|
72
|
-
const ffmpegArgs = ['-hide_banner', '-y', '-i', input, ...args, output];
|
|
73
|
-
|
|
74
|
-
// Use /usr/bin/time to measure memory usage
|
|
75
|
-
// macOS: /usr/bin/time -l outputs "maximum resident set size" in bytes
|
|
76
|
-
// Linux: /usr/bin/time -v outputs "Maximum resident set size (kbytes)"
|
|
77
|
-
const os = platform();
|
|
78
|
-
const useTime = os === 'darwin' || os === 'linux';
|
|
79
|
-
const timePath = '/usr/bin/time';
|
|
80
|
-
const timeExists = useTime && existsSync(timePath);
|
|
81
|
-
|
|
82
|
-
let command: string;
|
|
83
|
-
let fullArgs: string[];
|
|
84
|
-
|
|
85
|
-
if (timeExists) {
|
|
86
|
-
command = timePath;
|
|
87
|
-
if (os === 'darwin') {
|
|
88
|
-
// macOS: -l for resource usage
|
|
89
|
-
fullArgs = ['-l', ffmpeg, ...ffmpegArgs];
|
|
90
|
-
} else {
|
|
91
|
-
// Linux: -v for verbose (includes memory)
|
|
92
|
-
fullArgs = ['-v', ffmpeg, ...ffmpegArgs];
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
command = ffmpeg;
|
|
96
|
-
fullArgs = ffmpegArgs;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return new Promise((resolve, reject) => {
|
|
100
|
-
const startTime = process.hrtime.bigint();
|
|
101
|
-
|
|
102
|
-
const proc = spawn(command, fullArgs, {
|
|
103
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
let stdout = '';
|
|
107
|
-
let stderr = '';
|
|
108
|
-
|
|
109
|
-
proc.stdout.on('data', (data) => {
|
|
110
|
-
stdout += data.toString();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
proc.stderr.on('data', (data) => {
|
|
114
|
-
stderr += data.toString();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const timeoutId = setTimeout(() => {
|
|
118
|
-
proc.kill('SIGKILL');
|
|
119
|
-
reject(new Error(`FFmpeg timed out after ${timeout}ms`));
|
|
120
|
-
}, timeout);
|
|
121
|
-
|
|
122
|
-
proc.on('close', (code) => {
|
|
123
|
-
clearTimeout(timeoutId);
|
|
124
|
-
const endTime = process.hrtime.bigint();
|
|
125
|
-
const durationMs = Number(endTime - startTime) / 1_000_000;
|
|
126
|
-
|
|
127
|
-
// Parse frame count from stderr (look for the last occurrence)
|
|
128
|
-
const frameMatches = [...stderr.matchAll(/frame=\s*(\d+)/g)];
|
|
129
|
-
const frameCount = frameMatches.length > 0 ? parseInt(frameMatches[frameMatches.length - 1][1], 10) : undefined;
|
|
130
|
-
|
|
131
|
-
// Parse FPS - try multiple methods:
|
|
132
|
-
// 1. Direct fps= value (if > 0)
|
|
133
|
-
// 2. Calculate from speed= multiplier and video duration
|
|
134
|
-
// 3. Calculate from frame count and our measured duration
|
|
135
|
-
let fps: number | undefined;
|
|
136
|
-
|
|
137
|
-
const fpsMatch = /fps=\s*([\d.]+)/.exec(stderr);
|
|
138
|
-
if (fpsMatch && parseFloat(fpsMatch[1]) > 0) {
|
|
139
|
-
fps = parseFloat(fpsMatch[1]);
|
|
140
|
-
} else if (frameCount && durationMs > 0) {
|
|
141
|
-
// Calculate FPS from frames processed and actual duration
|
|
142
|
-
fps = (frameCount / durationMs) * 1000;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Parse peak memory from /usr/bin/time output
|
|
146
|
-
let peakMemoryBytes: number | undefined;
|
|
147
|
-
if (timeExists) {
|
|
148
|
-
if (os === 'darwin') {
|
|
149
|
-
// macOS: "maximum resident set size" is in bytes
|
|
150
|
-
// Format: " 123456789 maximum resident set size"
|
|
151
|
-
const memMatch = /(\d+)\s+maximum resident set size/.exec(stderr);
|
|
152
|
-
if (memMatch) {
|
|
153
|
-
peakMemoryBytes = parseInt(memMatch[1], 10);
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
// Linux: "Maximum resident set size (kbytes): 123456"
|
|
157
|
-
const memMatch = /Maximum resident set size.*?:\s*(\d+)/.exec(stderr);
|
|
158
|
-
if (memMatch) {
|
|
159
|
-
peakMemoryBytes = parseInt(memMatch[1], 10) * 1024; // Convert KB to bytes
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
resolve({
|
|
165
|
-
exitCode: code ?? 0,
|
|
166
|
-
stdout,
|
|
167
|
-
stderr,
|
|
168
|
-
durationMs,
|
|
169
|
-
framesProcessed: frameCount,
|
|
170
|
-
fps,
|
|
171
|
-
peakMemoryBytes,
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
proc.on('error', (err) => {
|
|
176
|
-
clearTimeout(timeoutId);
|
|
177
|
-
reject(err);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Run FFmpeg CLI and return a MeasureResult for comparison
|
|
184
|
-
*/
|
|
185
|
-
export async function measureFFmpegCLI(options: FFmpegRunOptions): Promise<MeasureResult> {
|
|
186
|
-
const result = await runFFmpegCLI(options);
|
|
187
|
-
|
|
188
|
-
if (result.exitCode !== 0) {
|
|
189
|
-
throw new Error(`FFmpeg failed with exit code ${result.exitCode}: ${result.stderr}`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Calculate FPS from frames and duration if not available from output
|
|
193
|
-
const fps = result.fps ?? (result.framesProcessed ? (result.framesProcessed / result.durationMs) * 1000 : undefined);
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
durationMs: result.durationMs,
|
|
197
|
-
peakMemoryBytes: result.peakMemoryBytes ?? 0,
|
|
198
|
-
memorySamples: [],
|
|
199
|
-
framesProcessed: result.framesProcessed,
|
|
200
|
-
fps,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Get FFmpeg version string
|
|
206
|
-
*/
|
|
207
|
-
export async function getFFmpegVersion(): Promise<string> {
|
|
208
|
-
const ffmpeg = getFFmpegPath();
|
|
209
|
-
|
|
210
|
-
return new Promise((resolve, reject) => {
|
|
211
|
-
const proc = spawn(ffmpeg, ['-version'], {
|
|
212
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
let stdout = '';
|
|
216
|
-
|
|
217
|
-
proc.stdout.on('data', (data) => {
|
|
218
|
-
stdout += data.toString();
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
proc.on('close', (code) => {
|
|
222
|
-
if (code !== 0) {
|
|
223
|
-
reject(new Error('Failed to get FFmpeg version'));
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Parse version from first line
|
|
228
|
-
const match = /ffmpeg version (\S+)/.exec(stdout);
|
|
229
|
-
resolve(match ? match[1] : 'unknown');
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
proc.on('error', reject);
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Probe a media file using ffprobe
|
|
238
|
-
*/
|
|
239
|
-
export async function probeMediaFile(
|
|
240
|
-
filePath: string,
|
|
241
|
-
): Promise<{ duration: number; videoCodec?: string; audioCodec?: string; width?: number; height?: number; fps?: number }> {
|
|
242
|
-
const ffmpeg = getFFmpegPath();
|
|
243
|
-
const ffprobe = ffmpeg.replace('ffmpeg', 'ffprobe');
|
|
244
|
-
|
|
245
|
-
// Check if ffprobe exists, otherwise use ffmpeg -i
|
|
246
|
-
const probeExists = existsSync(ffprobe);
|
|
247
|
-
|
|
248
|
-
return new Promise((resolve, reject) => {
|
|
249
|
-
if (probeExists) {
|
|
250
|
-
const proc = spawn(ffprobe, ['-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', filePath], {
|
|
251
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
let stdout = '';
|
|
255
|
-
|
|
256
|
-
proc.stdout.on('data', (data) => {
|
|
257
|
-
stdout += data.toString();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
proc.on('close', (code) => {
|
|
261
|
-
if (code !== 0) {
|
|
262
|
-
reject(new Error('Failed to probe media file'));
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
const info = JSON.parse(stdout);
|
|
268
|
-
const videoStream = info.streams?.find((s: { codec_type: string }) => s.codec_type === 'video');
|
|
269
|
-
const audioStream = info.streams?.find((s: { codec_type: string }) => s.codec_type === 'audio');
|
|
270
|
-
|
|
271
|
-
let fps: number | undefined;
|
|
272
|
-
if (videoStream?.r_frame_rate) {
|
|
273
|
-
const [num, den] = videoStream.r_frame_rate.split('/').map(Number);
|
|
274
|
-
fps = num / den;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
resolve({
|
|
278
|
-
duration: parseFloat(info.format?.duration ?? '0'),
|
|
279
|
-
videoCodec: videoStream?.codec_name,
|
|
280
|
-
audioCodec: audioStream?.codec_name,
|
|
281
|
-
width: videoStream?.width,
|
|
282
|
-
height: videoStream?.height,
|
|
283
|
-
fps,
|
|
284
|
-
});
|
|
285
|
-
} catch (e) {
|
|
286
|
-
reject(e);
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
proc.on('error', reject);
|
|
291
|
-
} else {
|
|
292
|
-
// Fallback: use ffmpeg -i and parse stderr
|
|
293
|
-
const proc = spawn(ffmpeg, ['-i', filePath], {
|
|
294
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
let stderr = '';
|
|
298
|
-
|
|
299
|
-
proc.stderr.on('data', (data) => {
|
|
300
|
-
stderr += data.toString();
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
proc.on('close', () => {
|
|
304
|
-
// Parse duration
|
|
305
|
-
const durationMatch = /Duration: (\d+):(\d+):(\d+\.\d+)/.exec(stderr);
|
|
306
|
-
let duration = 0;
|
|
307
|
-
if (durationMatch) {
|
|
308
|
-
duration = parseInt(durationMatch[1]) * 3600 + parseInt(durationMatch[2]) * 60 + parseFloat(durationMatch[3]);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Parse video stream info
|
|
312
|
-
const videoMatch = /Stream #\d+:\d+.*Video: (\w+).*, (\d+)x(\d+).*, ([\d.]+) fps/.exec(stderr);
|
|
313
|
-
|
|
314
|
-
// Parse audio stream info
|
|
315
|
-
const audioMatch = /Stream #\d+:\d+.*Audio: (\w+)/.exec(stderr);
|
|
316
|
-
|
|
317
|
-
resolve({
|
|
318
|
-
duration,
|
|
319
|
-
videoCodec: videoMatch?.[1],
|
|
320
|
-
width: videoMatch ? parseInt(videoMatch[2]) : undefined,
|
|
321
|
-
height: videoMatch ? parseInt(videoMatch[3]) : undefined,
|
|
322
|
-
fps: videoMatch ? parseFloat(videoMatch[4]) : undefined,
|
|
323
|
-
audioCodec: audioMatch?.[1],
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
proc.on('error', reject);
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Build common FFmpeg arguments for different test cases
|
|
334
|
-
* Note: -threads 0 enables auto-detection (same as node-av default)
|
|
335
|
-
*/
|
|
336
|
-
export const FFmpegArgs = {
|
|
337
|
-
/** Software H.264 encoding */
|
|
338
|
-
swH264: (crf = 23): string[] => ['-threads', '0', '-c:v', 'libx264', '-preset', 'medium', '-crf', crf.toString(), '-c:a', 'copy'],
|
|
339
|
-
|
|
340
|
-
/** Software H.265/HEVC encoding */
|
|
341
|
-
swH265: (crf = 28): string[] => ['-threads', '0', '-c:v', 'libx265', '-preset', 'medium', '-crf', crf.toString(), '-c:a', 'copy'],
|
|
342
|
-
|
|
343
|
-
/** Hardware H.264 encoding (VideoToolbox) */
|
|
344
|
-
hwH264VideoToolbox: (): string[] => ['-threads', '0', '-c:v', 'h264_videotoolbox', '-b:v', '2M', '-c:a', 'copy'],
|
|
345
|
-
|
|
346
|
-
/** Hardware H.264 encoding (NVENC) */
|
|
347
|
-
hwH264Nvenc: (): string[] => ['-threads', '0', '-c:v', 'h264_nvenc', '-preset', 'p4', '-b:v', '2M', '-c:a', 'copy'],
|
|
348
|
-
|
|
349
|
-
/** Hardware H.264 encoding (VAAPI) */
|
|
350
|
-
hwH264Vaapi: (): string[] => ['-threads', '0', '-vaapi_device', '/dev/dri/renderD128', '-c:v', 'h264_vaapi', '-b:v', '2M', '-c:a', 'copy'],
|
|
351
|
-
|
|
352
|
-
/** Hardware H.264 encoding (QSV) */
|
|
353
|
-
hwH264Qsv: (): string[] => ['-threads', '0', '-c:v', 'h264_qsv', '-preset', 'medium', '-b:v', '2M', '-c:a', 'copy'],
|
|
354
|
-
|
|
355
|
-
/** Stream copy (no re-encoding) */
|
|
356
|
-
streamCopy: (): string[] => ['-c', 'copy'],
|
|
357
|
-
|
|
358
|
-
/** Video only (no audio) */
|
|
359
|
-
videoOnly: (videoArgs: string[]): string[] => [...videoArgs, '-an'],
|
|
360
|
-
|
|
361
|
-
/** First N seconds only */
|
|
362
|
-
duration: (seconds: number, args: string[]): string[] => ['-t', seconds.toString(), ...args],
|
|
363
|
-
};
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Time and Memory Measurement Utilities
|
|
3
|
-
*
|
|
4
|
-
* Provides utilities for measuring execution time, memory usage,
|
|
5
|
-
* and computing statistics for benchmark results.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Result of a single benchmark run
|
|
10
|
-
*/
|
|
11
|
-
export interface MeasureResult {
|
|
12
|
-
/** Duration in milliseconds */
|
|
13
|
-
durationMs: number;
|
|
14
|
-
/** Peak memory usage in bytes */
|
|
15
|
-
peakMemoryBytes: number;
|
|
16
|
-
/** Memory samples taken during execution (bytes) */
|
|
17
|
-
memorySamples: number[];
|
|
18
|
-
/** Frames processed (if applicable) */
|
|
19
|
-
framesProcessed?: number;
|
|
20
|
-
/** FPS calculated from frames and duration */
|
|
21
|
-
fps?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Statistics computed from multiple runs
|
|
26
|
-
*/
|
|
27
|
-
export interface Stats {
|
|
28
|
-
mean: number;
|
|
29
|
-
min: number;
|
|
30
|
-
max: number;
|
|
31
|
-
stdDev: number;
|
|
32
|
-
median: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Aggregated benchmark result from multiple iterations
|
|
37
|
-
*/
|
|
38
|
-
export interface AggregatedResult {
|
|
39
|
-
iterations: number;
|
|
40
|
-
durationMs: Stats;
|
|
41
|
-
peakMemoryBytes: Stats;
|
|
42
|
-
fps?: Stats;
|
|
43
|
-
framesProcessed?: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Memory sampler that records memory usage at regular intervals
|
|
48
|
-
* Uses delta from baseline to measure actual memory growth during operation
|
|
49
|
-
*/
|
|
50
|
-
export class MemorySampler {
|
|
51
|
-
private samples: number[] = [];
|
|
52
|
-
private interval: ReturnType<typeof setInterval> | null = null;
|
|
53
|
-
private peakMemory = 0;
|
|
54
|
-
private baselineMemory = 0;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Start sampling memory at the given interval
|
|
58
|
-
* Records baseline before starting to measure delta
|
|
59
|
-
*/
|
|
60
|
-
start(intervalMs = 100): void {
|
|
61
|
-
this.samples = [];
|
|
62
|
-
this.peakMemory = 0;
|
|
63
|
-
|
|
64
|
-
// Force GC before taking baseline for more accurate measurement
|
|
65
|
-
if (global.gc) {
|
|
66
|
-
global.gc();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Record baseline memory
|
|
70
|
-
this.baselineMemory = process.memoryUsage().rss;
|
|
71
|
-
|
|
72
|
-
this.interval = setInterval(() => {
|
|
73
|
-
const memUsage = process.memoryUsage();
|
|
74
|
-
const rss = memUsage.rss;
|
|
75
|
-
this.samples.push(rss);
|
|
76
|
-
if (rss > this.peakMemory) {
|
|
77
|
-
this.peakMemory = rss;
|
|
78
|
-
}
|
|
79
|
-
}, intervalMs);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Stop sampling and return results
|
|
84
|
-
* Returns peak memory as delta from baseline
|
|
85
|
-
*/
|
|
86
|
-
stop(): { samples: number[]; peakMemory: number; baselineMemory: number } {
|
|
87
|
-
if (this.interval) {
|
|
88
|
-
clearInterval(this.interval);
|
|
89
|
-
this.interval = null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Take one final sample
|
|
93
|
-
const finalRss = process.memoryUsage().rss;
|
|
94
|
-
this.samples.push(finalRss);
|
|
95
|
-
if (finalRss > this.peakMemory) {
|
|
96
|
-
this.peakMemory = finalRss;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
samples: this.samples,
|
|
101
|
-
peakMemory: this.peakMemory,
|
|
102
|
-
baselineMemory: this.baselineMemory,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* High-resolution timer for measuring execution time
|
|
109
|
-
*/
|
|
110
|
-
export class Timer {
|
|
111
|
-
private startTime = 0n;
|
|
112
|
-
private endTime = 0n;
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Start the timer
|
|
116
|
-
*/
|
|
117
|
-
start(): void {
|
|
118
|
-
this.startTime = process.hrtime.bigint();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Stop the timer
|
|
123
|
-
*/
|
|
124
|
-
stop(): void {
|
|
125
|
-
this.endTime = process.hrtime.bigint();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Get elapsed time in milliseconds
|
|
130
|
-
*/
|
|
131
|
-
getElapsedMs(): number {
|
|
132
|
-
return Number(this.endTime - this.startTime) / 1_000_000;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Get elapsed time in seconds
|
|
137
|
-
*/
|
|
138
|
-
getElapsedSeconds(): number {
|
|
139
|
-
return this.getElapsedMs() / 1000;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Measure execution time and memory usage of an async function
|
|
145
|
-
* Memory is measured as delta from baseline for accurate comparison
|
|
146
|
-
*/
|
|
147
|
-
export async function measure(fn: () => Promise<{ framesProcessed?: number }>, options: { memorySampleIntervalMs?: number } = {}): Promise<MeasureResult> {
|
|
148
|
-
const { memorySampleIntervalMs = 100 } = options;
|
|
149
|
-
|
|
150
|
-
// Force garbage collection if available to get clean baseline
|
|
151
|
-
if (global.gc) {
|
|
152
|
-
global.gc();
|
|
153
|
-
// Wait a bit for GC to complete
|
|
154
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
155
|
-
global.gc();
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const timer = new Timer();
|
|
159
|
-
const memorySampler = new MemorySampler();
|
|
160
|
-
|
|
161
|
-
memorySampler.start(memorySampleIntervalMs);
|
|
162
|
-
timer.start();
|
|
163
|
-
|
|
164
|
-
const result = await fn();
|
|
165
|
-
|
|
166
|
-
timer.stop();
|
|
167
|
-
const memoryResult = memorySampler.stop();
|
|
168
|
-
|
|
169
|
-
const durationMs = timer.getElapsedMs();
|
|
170
|
-
const framesProcessed = result.framesProcessed;
|
|
171
|
-
|
|
172
|
-
// Calculate peak memory as delta from baseline
|
|
173
|
-
// This gives us the actual memory growth during the operation
|
|
174
|
-
const peakMemoryDelta = memoryResult.peakMemory - memoryResult.baselineMemory;
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
durationMs,
|
|
178
|
-
peakMemoryBytes: Math.max(0, peakMemoryDelta), // Ensure non-negative
|
|
179
|
-
memorySamples: memoryResult.samples,
|
|
180
|
-
framesProcessed,
|
|
181
|
-
fps: framesProcessed ? (framesProcessed / durationMs) * 1000 : undefined,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Compute statistics from an array of numbers
|
|
187
|
-
*/
|
|
188
|
-
export function computeStats(values: number[]): Stats {
|
|
189
|
-
if (values.length === 0) {
|
|
190
|
-
return { mean: 0, min: 0, max: 0, stdDev: 0, median: 0 };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
194
|
-
const sum = values.reduce((a, b) => a + b, 0);
|
|
195
|
-
const mean = sum / values.length;
|
|
196
|
-
const min = sorted[0];
|
|
197
|
-
const max = sorted[sorted.length - 1];
|
|
198
|
-
|
|
199
|
-
// Standard deviation
|
|
200
|
-
const squaredDiffs = values.map((v) => Math.pow(v - mean, 2));
|
|
201
|
-
const avgSquaredDiff = squaredDiffs.reduce((a, b) => a + b, 0) / values.length;
|
|
202
|
-
const stdDev = Math.sqrt(avgSquaredDiff);
|
|
203
|
-
|
|
204
|
-
// Median
|
|
205
|
-
const mid = Math.floor(sorted.length / 2);
|
|
206
|
-
const median = sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
207
|
-
|
|
208
|
-
return { mean, min, max, stdDev, median };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Aggregate multiple measurement results into statistics
|
|
213
|
-
*/
|
|
214
|
-
export function aggregateResults(results: MeasureResult[]): AggregatedResult {
|
|
215
|
-
const durations = results.map((r) => r.durationMs);
|
|
216
|
-
const peakMemories = results.map((r) => r.peakMemoryBytes);
|
|
217
|
-
const fpsValues = results.filter((r) => r.fps !== undefined).map((r) => r.fps!);
|
|
218
|
-
|
|
219
|
-
const framesProcessed = results.find((r) => r.framesProcessed)?.framesProcessed;
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
iterations: results.length,
|
|
223
|
-
durationMs: computeStats(durations),
|
|
224
|
-
peakMemoryBytes: computeStats(peakMemories),
|
|
225
|
-
fps: fpsValues.length > 0 ? computeStats(fpsValues) : undefined,
|
|
226
|
-
framesProcessed,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Format bytes to human-readable string
|
|
232
|
-
*/
|
|
233
|
-
export function formatBytes(bytes: number): string {
|
|
234
|
-
const units = ['B', 'KB', 'MB', 'GB'];
|
|
235
|
-
let value = bytes;
|
|
236
|
-
let unitIndex = 0;
|
|
237
|
-
|
|
238
|
-
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
239
|
-
value /= 1024;
|
|
240
|
-
unitIndex++;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return `${value.toFixed(1)} ${units[unitIndex]}`;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Format milliseconds to human-readable string
|
|
248
|
-
*/
|
|
249
|
-
export function formatDuration(ms: number): string {
|
|
250
|
-
if (ms < 1000) {
|
|
251
|
-
return `${ms.toFixed(1)}ms`;
|
|
252
|
-
} else if (ms < 60000) {
|
|
253
|
-
return `${(ms / 1000).toFixed(2)}s`;
|
|
254
|
-
} else {
|
|
255
|
-
const minutes = Math.floor(ms / 60000);
|
|
256
|
-
const seconds = ((ms % 60000) / 1000).toFixed(1);
|
|
257
|
-
return `${minutes}m ${seconds}s`;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Calculate percentage difference between two values
|
|
263
|
-
*/
|
|
264
|
-
export function percentDiff(baseline: number, comparison: number): number {
|
|
265
|
-
if (baseline === 0) return 0;
|
|
266
|
-
return ((comparison - baseline) / baseline) * 100;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Format percentage difference with sign
|
|
271
|
-
*/
|
|
272
|
-
export function formatPercentDiff(diff: number): string {
|
|
273
|
-
const sign = diff > 0 ? '+' : '';
|
|
274
|
-
return `${sign}${diff.toFixed(1)}%`;
|
|
275
|
-
}
|