@vpalmisano/webrtcperf 4.4.14 → 4.5.1

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/build/src/plot.js CHANGED
@@ -6,9 +6,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.plotConfig = plotConfig;
7
7
  exports.plot = plot;
8
8
  exports.plotHtml = plotHtml;
9
+ exports.plotDetailedStatsDashboard = plotDetailedStatsDashboard;
9
10
  const fs_1 = __importDefault(require("fs"));
10
11
  const utils_1 = require("./utils");
11
12
  const json5_1 = __importDefault(require("json5"));
13
+ const scenarios_1 = require("./scenarios");
14
+ const path_1 = __importDefault(require("path"));
12
15
  const log = (0, utils_1.logger)('webrtcperf:plot');
13
16
  const SERIES_COLORS = [
14
17
  'rgba(33, 150, 243, 1)',
@@ -121,4 +124,391 @@ async function plotHtml(options, series) {
121
124
  </html>`;
122
125
  await fs_1.default.promises.writeFile(options.filePath || 'plot.html', data);
123
126
  }
127
+ function groupByParticipant(rows) {
128
+ const m = new Map();
129
+ for (const r of rows) {
130
+ if (!r.participantName)
131
+ continue;
132
+ const p = r.participantName;
133
+ if (!m.has(p))
134
+ m.set(p, []);
135
+ m.get(p).push(r);
136
+ }
137
+ return m;
138
+ }
139
+ async function plotDetailedStatsDashboard(statsFile, outFile = 'plot.html') {
140
+ const rows = await (0, scenarios_1.parseStatsFile)(statsFile);
141
+ if (rows.length === 0) {
142
+ log.warn('No stats found');
143
+ return;
144
+ }
145
+ const byParticipant = groupByParticipant(rows);
146
+ const participants = Array.from(byParticipant.keys()).sort();
147
+ const charts = [];
148
+ const build = (id, title, yLabel, processValue, width) => {
149
+ const series = [];
150
+ if (!id.startsWith('_')) {
151
+ for (const [participant, rows] of byParticipant.entries()) {
152
+ const dataPerTrack = new Map();
153
+ for (const r of rows) {
154
+ const trackId = r.trackId || '';
155
+ if (!dataPerTrack.has(trackId))
156
+ dataPerTrack.set(trackId, []);
157
+ const data = dataPerTrack.get(trackId);
158
+ const v = r[id];
159
+ if (v !== undefined)
160
+ data.push({ x: r.datetime, y: processValue ? processValue(v) : v });
161
+ }
162
+ dataPerTrack.forEach((data, trackId) => {
163
+ if (data.length)
164
+ series.push({ label: `${participant}${trackId ? ` (${trackId})` : ''}`, data: data });
165
+ });
166
+ }
167
+ }
168
+ charts.push({ id, title, yLabel, datasets: series, width });
169
+ };
170
+ const kbps = (v) => v / 1000;
171
+ const percent = (v) => v * 100;
172
+ const ms = (v) => v * 1000;
173
+ [
174
+ { id: '_throttle', title: 'Throttle settings' },
175
+ { id: 'throttleUpRate', title: 'Throttle up rate', yLabel: 'Kbps', processValue: kbps, width: 2 },
176
+ { id: 'throttleUpDelay', title: 'Throttle up delay', yLabel: 'ms', width: 2 },
177
+ { id: 'throttleUpLoss', title: 'Throttle up loss', yLabel: '%', width: 2 },
178
+ { id: 'throttleDownRate', title: 'Throttle down rate', yLabel: 'Kbps', processValue: kbps, width: 2 },
179
+ { id: 'throttleDownDelay', title: 'Throttle down delay', yLabel: 'ms', width: 2 },
180
+ { id: 'throttleDownLoss', title: 'Throttle down loss', yLabel: '%', width: 2 },
181
+ // Performance / Connectivity
182
+ { id: '_performance', title: 'Performance / Connectivity' },
183
+ { id: 'pageCpu', title: 'Page CPU', yLabel: '%' },
184
+ { id: 'pageMemory', title: 'Page memory', yLabel: 'MB' },
185
+ { id: 'peerConnectionConnectionTime', title: 'Peer connection connection time', yLabel: 's' },
186
+ { id: 'peerConnectionDisconnectionTime', title: 'Peer connection disconnection time', yLabel: 's' },
187
+ // Sent audio
188
+ { id: '_sentAudio', title: 'Sent audio' },
189
+ { id: 'audioSentBitrates', title: 'Sent audio bitrate', yLabel: 'Kbps', processValue: kbps },
190
+ { id: 'audioSentPacketsLossRate', title: 'Send audio loss', yLabel: '%', processValue: percent },
191
+ { id: 'audioSentRoundTripTime', title: 'Send audio RTT', yLabel: 'ms', processValue: ms },
192
+ { id: 'audioSentJitter', title: 'Send audio jitter', yLabel: 'ms', processValue: ms },
193
+ // Sent video
194
+ { id: '_sentVideo', title: 'Sent video' },
195
+ { id: 'videoSentBitrates', title: 'Sent video bitrate', yLabel: 'Kbps', processValue: kbps },
196
+ { id: 'videoSentPacketsLossRate', title: 'Send video loss', yLabel: '%', processValue: percent },
197
+ { id: 'videoSentRoundTripTime', title: 'Send video RTT', yLabel: 'ms', processValue: ms },
198
+ { id: 'videoSentJitter', title: 'Send video jitter', yLabel: 'ms', processValue: ms },
199
+ { id: 'videoSentWidth', title: 'Send video width', yLabel: 'px' },
200
+ { id: 'videoSentHeight', title: 'Send video height', yLabel: 'px' },
201
+ { id: 'videoSentFps', title: 'Send video framerate', yLabel: 'fps' },
202
+ {
203
+ id: 'transportSentAvailableOutgoingBitrate',
204
+ title: 'Send available bitrate',
205
+ yLabel: 'Kbps',
206
+ processValue: kbps,
207
+ },
208
+ { id: 'videoQualityLimitationCpu', title: 'Send video CPU limitation', yLabel: '%' },
209
+ { id: 'videoQualityLimitationBandwidth', title: 'Send video bandwidth limitation', yLabel: '%' },
210
+ { id: 'videoFirCountReceived', title: 'Send video FIR count', yLabel: 'count' },
211
+ { id: 'videoPliCountReceived', title: 'Send video PLI count', yLabel: 'count' },
212
+ // Sent screen
213
+ { id: '_sentScreen', title: 'Sent screen' },
214
+ { id: 'screenSentBitrates', title: 'Sent screen bitrate', yLabel: 'Kbps', processValue: kbps },
215
+ { id: 'screenSentPacketsLossRate', title: 'Send screen loss', yLabel: '%', processValue: percent },
216
+ { id: 'screenSentRoundTripTime', title: 'Send screen RTT', yLabel: 'ms', processValue: ms },
217
+ { id: 'screenSentJitter', title: 'Send screen jitter', yLabel: 'ms', processValue: ms },
218
+ { id: 'screenSentWidth', title: 'Send screen width', yLabel: 'px' },
219
+ { id: 'screenSentHeight', title: 'Send screen height', yLabel: 'px' },
220
+ { id: 'screenSentFps', title: 'Send screen framerate', yLabel: 'fps' },
221
+ { id: '' },
222
+ { id: 'screenQualityLimitationCpu', title: 'Send screen CPU limitation', yLabel: '%' },
223
+ { id: 'screenQualityLimitationBandwidth', title: 'Send screen bandwidth limitation', yLabel: '%' },
224
+ { id: 'screenFirCountReceived', title: 'Send screen FIR count', yLabel: 'count' },
225
+ { id: 'screenPliCountReceived', title: 'Send screen PLI count', yLabel: 'count' },
226
+ // Recv audio
227
+ { id: '_recvAudio', title: 'Recv audio' },
228
+ { id: 'audioRecvBitrates', title: 'Recv audio bitrate', yLabel: 'Kbps', processValue: kbps },
229
+ { id: 'audioRecvPacketsLossRate', title: 'Recv audio loss', yLabel: '%', processValue: percent },
230
+ { id: 'audioRecvJitter', title: 'Recv audio jitter', yLabel: 'ms', processValue: ms },
231
+ { id: 'audioRecvAvgJitterBufferDelay', title: 'Recv audio jitter buffer', yLabel: 'ms', processValue: ms },
232
+ { id: 'audioRecvLevel', title: 'Recv audio level', yLabel: 'db' },
233
+ { id: 'audioRecvConcealmentEvents', title: 'Recv audio concealment events', yLabel: 'count' },
234
+ {
235
+ id: 'audioRecvInsertedSamplesForDeceleration',
236
+ title: 'Recv audio inserted samples',
237
+ yLabel: 'count',
238
+ },
239
+ {
240
+ id: 'audioRecvRemovedSamplesForAcceleration',
241
+ title: 'Recv audio removed samples',
242
+ yLabel: 'count',
243
+ },
244
+ { id: 'audioRecvEndToEndDelay', title: 'Recv audio end to end delay', yLabel: 'ms', processValue: ms },
245
+ // Recv video
246
+ { id: '_recvVideo', title: 'Recv video' },
247
+ { id: 'videoRecvBitrates', title: 'Recv video bitrate', yLabel: 'Kbps', processValue: kbps },
248
+ { id: 'videoRecvPacketsLossRate', title: 'Recv video loss', yLabel: '%', processValue: percent },
249
+ { id: 'videoRecvJitter', title: 'Recv video jitter', yLabel: 'ms', processValue: ms },
250
+ { id: 'videoRecvAvgJitterBufferDelay', title: 'Recv video jitter buffer', yLabel: 'ms', processValue: ms },
251
+ { id: 'videoRecvWidth', title: 'Recv video width', yLabel: 'px' },
252
+ { id: 'videoRecvHeight', title: 'Recv video height', yLabel: 'px' },
253
+ { id: 'videoRecvFps', title: 'Recv video framerate', yLabel: 'fps' },
254
+ { id: 'videoTotalFreezesDuration', title: 'Recv video freezes', yLabel: 'count' },
255
+ { id: 'videoFirCountSent', title: 'Recv video FIR sent', yLabel: 'count' },
256
+ { id: 'videoPliCountSent', title: 'Recv video PLI sent', yLabel: 'count' },
257
+ { id: 'videoRecvEndToEndDelay', title: 'Recv video end to end delay', yLabel: 'ms', processValue: ms },
258
+ { id: '' },
259
+ // Recv screen
260
+ { id: '_recvScreen', title: 'Recv screen' },
261
+ { id: 'screenRecvBitrates', title: 'Recv screen bitrate', yLabel: 'Kbps', processValue: kbps },
262
+ { id: 'screenRecvPacketsLossRate', title: 'Recv screen loss', yLabel: '%', processValue: percent },
263
+ { id: 'screenRecvJitter', title: 'Recv screen jitter', yLabel: 'ms', processValue: ms },
264
+ { id: 'screenRecvAvgJitterBufferDelay', title: 'Recv screen jitter buffer', yLabel: 'ms', processValue: ms },
265
+ { id: 'screenRecvWidth', title: 'Recv screen width', yLabel: 'px' },
266
+ { id: 'screenRecvHeight', title: 'Recv screen height', yLabel: 'px' },
267
+ { id: 'screenRecvFps', title: 'Recv screen framerate', yLabel: 'fps' },
268
+ { id: 'screenTotalFreezesDuration', title: 'Recv screen freezes', yLabel: 'count' },
269
+ { id: 'screenFirCountSent', title: 'Recv screen FIR sent', yLabel: 'count' },
270
+ { id: 'screenPliCountSent', title: 'Recv screen PLI sent', yLabel: 'count' },
271
+ { id: 'screenRecvEndToEndDelay', title: 'Recv screen end to end delay', yLabel: 'ms', processValue: ms },
272
+ { id: '' },
273
+ ].forEach(graph => {
274
+ build(graph.id, graph.title, graph.yLabel, graph.processValue, graph.width);
275
+ });
276
+ const [_, id, scenario] = path_1.default.basename(path_1.default.dirname(statsFile)).split('_');
277
+ const description = (0, scenarios_1.formatThrottleRule)((0, scenarios_1.parseThrottleRule)(scenario), true, false);
278
+ const data = `\
279
+ <!DOCTYPE html>
280
+ <html lang="en">
281
+ <head>
282
+ <meta charset="UTF-8">
283
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
284
+ <title>${id} (${description})</title>
285
+ <link rel="icon" href="https://raw.githubusercontent.com/vpalmisano/webrtcperf/devel/media/logo.svg">
286
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
287
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-chart-error-bars"></script>
288
+ <script src="https://cdn.jsdelivr.net/npm/hammerjs"></script>
289
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom"></script>
290
+ <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
291
+ <link href="https://cdn.jsdelivr.net/npm/@mdi/font@7.x/css/materialdesignicons.min.css" rel="stylesheet">
292
+ <link href="https://cdn.jsdelivr.net/npm/vuetify@3.7.2/dist/vuetify.min.css" rel="stylesheet">
293
+ <script src="https://cdn.jsdelivr.net/npm/vuetify@3.7.2/dist/vuetify.min.js"></script>
294
+ </head>
295
+ <body>
296
+ <div id="app">
297
+ <v-app>
298
+ <v-main>
299
+ <v-app-bar color="primary" density="compact">
300
+ <v-app-bar-title><b>${id}</b> (${description})</v-app-bar-title>
301
+ <template v-slot:append>
302
+ <v-select color="primary" :items="participants" v-model="selected" density="compact" variant="solo" hide-details="auto"></v-select>
303
+ </template>
304
+ </v-app-bar>
305
+ <v-container fluid>
306
+ <v-row dense>
307
+ <v-col v-for="c in charts" :key="c.id" cols="12" :md="isExpanded(c.id) ? 12 : c.width || 3">
308
+ <template v-if="c.id">
309
+ <v-card color="primary" :variant="c.id.startsWith('_') ? 'tonal' : 'text'">
310
+ <v-card-title class="text-subtitle-1 d-flex align-center flex-nowrap" @click="toggleExpanded(c)" style="cursor: pointer;">
311
+ <span class="text-truncate">{{ c.title }}</span>
312
+ </v-card-title>
313
+ <v-card-text v-if="!c.id.startsWith('_')" style="min-height: 250px;">
314
+ <canvas :id="c.id"></canvas>
315
+ </v-card-text>
316
+ </v-card>
317
+ </template>
318
+ <template v-else>
319
+ <div class="empty-slot"></div>
320
+ </template>
321
+ </v-col>
322
+ </v-row>
323
+ </v-container>
324
+ <v-footer color="primary" density="compact">
325
+ <v-btn class="text-none" variant="text" density="compact" size="small" href="https://github.com/vpalmisano/webrtcperf" target="_blank">Generated with webrtcperf</v-btn>
326
+ </v-footer>
327
+ </v-main>
328
+ </v-app>
329
+ </div>
330
+
331
+ <script>
332
+ const { createApp, onMounted, nextTick, watch, ref } = Vue;
333
+ const vuetify = Vuetify.createVuetify();
334
+ const PARTICIPANTS = ${json5_1.default.stringify(['All', ...participants])};
335
+ const CHARTS = ${json5_1.default.stringify(charts)};
336
+ const SERIES_COLORS = ${json5_1.default.stringify(SERIES_COLORS)};
337
+
338
+ function fmtTime(v) {
339
+ const d = new Date(Number(v));
340
+ if (!isFinite(d.getTime())) return v;
341
+ const pad = n => String(n).padStart(2, '0');
342
+ return pad(d.getHours()) + ':' + pad(d.getMinutes())
343
+ }
344
+
345
+ function buildDatasets(datasets, selected) {
346
+ const filtered = selected === 'All' ? datasets : datasets.filter(d => d.label.startsWith(selected));
347
+ return filtered.map((s, i) => ({
348
+ label: s.label,
349
+ data: s.data,
350
+ fill: false,
351
+ backgroundColor: SERIES_COLORS[i % SERIES_COLORS.length],
352
+ borderColor: SERIES_COLORS[i % SERIES_COLORS.length],
353
+ borderWidth: 1,
354
+ pointRadius: 0,
355
+ }));
356
+ }
357
+
358
+ createApp({
359
+ setup() {
360
+ const participants = ref(PARTICIPANTS);
361
+ const selected = ref('All');
362
+ const charts = ref(CHARTS);
363
+ const chartInstances = new Map();
364
+ const expanded = ref(new Set());
365
+
366
+ function onPanZoom({ chart }) {
367
+ const x = chart.scales.x;
368
+ if (!x) return;
369
+ // Sync zoom level across all charts
370
+ for (const { chart: otherChart } of chartInstances.values()) {
371
+ if (otherChart !== chart) {
372
+ const otherX = otherChart.scales.x;
373
+ if (otherX) {
374
+ otherX.options.min = x.min;
375
+ otherX.options.max = x.max;
376
+ otherChart.update('none');
377
+ }
378
+ }
379
+ }
380
+ }
381
+
382
+ function createPanel(chartSpec) {
383
+ const canvas = document.getElementById(chartSpec.id);
384
+ if (!canvas) return;
385
+ const ctx = canvas.getContext('2d');
386
+ const chart = new Chart(ctx, {
387
+ type: 'line',
388
+ options: {
389
+ maintainAspectRatio: false,
390
+ animation: false,
391
+ layout: {
392
+ padding: 0,
393
+ },
394
+ interaction: {
395
+ intersect: false,
396
+ mode: 'x',
397
+ },
398
+ plugins: {
399
+ tooltip: {
400
+ callbacks: {
401
+ title: (context) => {
402
+ if (context[0].parsed.x !== null) {
403
+ return fmtTime(context[0].parsed.x);
404
+ }
405
+ return '';
406
+ },
407
+ label: (context) => {
408
+ return context.parsed.y;
409
+ }
410
+ },
411
+ },
412
+ legend: {
413
+ display: true,
414
+ position: 'bottom',
415
+ align: 'start',
416
+ maxHeight: 50,
417
+ labels: {
418
+ boxWidth: 8,
419
+ boxHeight: 8,
420
+ font: {
421
+ size: 8,
422
+ lineHeight: 1,
423
+ },
424
+ },
425
+ },
426
+ zoom: {
427
+ pan: {
428
+ enabled: true,
429
+ mode: 'x',
430
+ modifierKey: 'ctrl',
431
+ onPan: onPanZoom,
432
+ },
433
+ zoom: {
434
+ drag: { enabled: true },
435
+ mode: 'x',
436
+ onZoom: onPanZoom,
437
+ },
438
+ },
439
+ },
440
+ scales: {
441
+ x: {
442
+ type: 'linear',
443
+ title: { display: false, text: 'Time' },
444
+ ticks: { display: true, callback: (value) => fmtTime(value) },
445
+ },
446
+ y: {
447
+ type: 'linear',
448
+ title: { display: true, text: chartSpec.yLabel },
449
+ min: 0,
450
+ },
451
+ },
452
+ },
453
+ data: { datasets: buildDatasets(chartSpec.datasets, selected.value) },
454
+ });
455
+ chartInstances.set(chartSpec.id, { chart, spec: chartSpec });
456
+
457
+ canvas.addEventListener('contextmenu', e => {
458
+ e.preventDefault();
459
+ resetZoom();
460
+ });
461
+ }
462
+
463
+ function rebuildCharts() {
464
+ for (const { chart } of chartInstances.values()) chart.destroy();
465
+ chartInstances.clear();
466
+ nextTick(() => charts.value.forEach(createPanel));
467
+ }
468
+
469
+ function applyFilter() {
470
+ for (const { chart, spec } of chartInstances.values()) {
471
+ chart.data.datasets = buildDatasets(spec.datasets, selected.value);
472
+ chart.update();
473
+ }
474
+ }
475
+
476
+ function resetZoom() {
477
+ for (const { chart } of chartInstances.values()) {
478
+ chart.resetZoom('none');
479
+ if (chart.scales.x) {
480
+ chart.scales.x.options.min = undefined;
481
+ chart.scales.x.options.max = undefined;
482
+ }
483
+ chart.update('none');
484
+ }
485
+ }
486
+
487
+ function toggleExpanded(c) {
488
+ const id = c.id;
489
+ const s = new Set(expanded.value);
490
+ if (s.has(id)) s.delete(id); else s.add(id);
491
+ expanded.value = s;
492
+ nextTick(() => { const entry = chartInstances.get(id); entry?.chart?.resize(); });
493
+ }
494
+
495
+ function isExpanded(id) {
496
+ return id.startsWith('_') || expanded.value.has(id);
497
+ }
498
+
499
+ onMounted(() => {
500
+ nextTick(() => charts.value.forEach(createPanel));
501
+ window.addEventListener('resize', () => { for (const { chart } of chartInstances.values()) chart.resize(); });
502
+ });
503
+
504
+ watch(selected, () => applyFilter());
505
+
506
+ return { participants, selected, charts, resetZoom, toggleExpanded, isExpanded };
507
+ }
508
+ }).use(vuetify).mount('#app');
509
+ </script>
510
+ </body>
511
+ </html>`;
512
+ await fs_1.default.promises.writeFile(outFile, data);
513
+ }
124
514
  //# sourceMappingURL=plot.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"plot.js","sourceRoot":"","sources":["../../src/plot.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs'\n\nimport { logger } from './utils'\nimport json5 from 'json5'\n\nconst log = logger('webrtcperf:plot')\n\nexport type PlotOptions = {\n type?: string\n title?: string\n xLabel?: string\n yLabel?: string\n yMin?: number\n yMax?: number\n labels?: (string | number)[]\n filePath?: string\n}\n\nexport type PlotData = {\n label: string\n data: { x: number | string; y: number; yMin?: number; yMax?: number }[]\n}\n\nconst SERIES_COLORS = [\n 'rgba(33, 150, 243, 1)',\n 'rgba(244, 67, 54, 1)',\n 'rgba(76, 175, 80, 1)',\n 'rgba(255, 193, 7, 1)',\n 'rgba(156, 39, 176, 1)',\n]\n\nexport function plotConfig(options: PlotOptions, series: PlotData[]) {\n log.debug('plotConfig')\n\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n type: (options.type || 'line') as any,\n options: {\n plugins: {\n title: options.title\n ? {\n display: true,\n text: options.title,\n }\n : undefined,\n zoom: {\n pan: {\n enabled: true,\n mode: 'x',\n modifierKey: 'ctrl',\n },\n zoom: {\n drag: {\n enabled: true,\n },\n mode: 'x',\n },\n },\n },\n scales: {\n x: {\n type: 'category',\n title: options.xLabel\n ? {\n display: true,\n text: options.xLabel,\n }\n : undefined,\n },\n y: {\n type: 'linear',\n title: options.yLabel\n ? {\n display: true,\n text: options.yLabel,\n }\n : undefined,\n min: options.yMin,\n max: options.yMax,\n },\n },\n },\n data: {\n labels: options.labels,\n datasets: series.map((s, i) => ({\n fill: false,\n backgroundColor: SERIES_COLORS[i % SERIES_COLORS.length],\n borderColor: SERIES_COLORS[i % SERIES_COLORS.length],\n borderWidth: 1,\n pointRadius: 0,\n ...s,\n })),\n },\n }\n}\n\nexport async function plot(options: PlotOptions, series: PlotData[]) {\n const {\n CategoryScale,\n Chart,\n LinearScale,\n LineController,\n BarController,\n LineElement,\n BarElement,\n PointElement,\n Legend,\n Title,\n } = await import('chart.js')\n const { BarWithErrorBar, BarWithErrorBarsController } = await import('chartjs-chart-error-bars')\n Chart.register(\n CategoryScale,\n LineController,\n LineElement,\n BarController,\n LinearScale,\n BarElement,\n PointElement,\n Legend,\n Title,\n BarWithErrorBar,\n BarWithErrorBarsController,\n )\n const { Canvas } = await import('skia-canvas')\n\n log.debug('plot')\n const config = plotConfig(options, series)\n const canvas = new Canvas(1280, 720)\n const chart = new Chart(canvas as unknown as HTMLCanvasElement, config)\n await canvas.toFile(options.filePath || 'plot.png', { format: 'png', matte: 'white' })\n chart.destroy()\n}\n\nexport async function plotHtml(options: PlotOptions, series: PlotData[]) {\n log.debug('plotHtml')\n const config = plotConfig(options, series)\n\n const data = `\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${options.title || 'Plot'}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/chartjs-chart-error-bars\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/hammerjs\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom\"></script>\n</head>\n<body>\n <div>\n <canvas id=\"chart\"></canvas>\n </div>\n <script>\n const ctx = document.getElementById('chart');\n const chart = new Chart(ctx, ${json5.stringify(config)});\n chart.options.onClick = e => e.chart.resetZoom();\n addEventListener('resize', () => chart.resize());\n </script>\n</body>\n</html>`\n await fs.promises.writeFile(options.filePath || 'plot.html', data)\n}\n"]}
1
+ {"version":3,"file":"plot.js","sourceRoot":"","sources":["../../src/plot.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs'\n\nimport { logger } from './utils'\nimport json5 from 'json5'\nimport { formatThrottleRule, parseStatsFile, parseThrottleRule, StatsRow } from './scenarios'\nimport path from 'path'\n\nconst log = logger('webrtcperf:plot')\n\nexport type PlotOptions = {\n type?: string\n title?: string\n xLabel?: string\n yLabel?: string\n yMin?: number\n yMax?: number\n labels?: (string | number)[]\n filePath?: string\n}\n\nexport type PlotData = {\n label: string\n data: { x: number | string; y: number; yMin?: number; yMax?: number }[]\n}\n\nconst SERIES_COLORS = [\n 'rgba(33, 150, 243, 1)',\n 'rgba(244, 67, 54, 1)',\n 'rgba(76, 175, 80, 1)',\n 'rgba(255, 193, 7, 1)',\n 'rgba(156, 39, 176, 1)',\n]\n\nexport function plotConfig(options: PlotOptions, series: PlotData[]) {\n log.debug('plotConfig')\n\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n type: (options.type || 'line') as any,\n options: {\n plugins: {\n title: options.title\n ? {\n display: true,\n text: options.title,\n }\n : undefined,\n zoom: {\n pan: {\n enabled: true,\n mode: 'x',\n modifierKey: 'ctrl',\n },\n zoom: {\n drag: {\n enabled: true,\n },\n mode: 'x',\n },\n },\n },\n scales: {\n x: {\n type: 'category',\n title: options.xLabel\n ? {\n display: true,\n text: options.xLabel,\n }\n : undefined,\n },\n y: {\n type: 'linear',\n title: options.yLabel\n ? {\n display: true,\n text: options.yLabel,\n }\n : undefined,\n min: options.yMin,\n max: options.yMax,\n },\n },\n },\n data: {\n labels: options.labels,\n datasets: series.map((s, i) => ({\n fill: false,\n backgroundColor: SERIES_COLORS[i % SERIES_COLORS.length],\n borderColor: SERIES_COLORS[i % SERIES_COLORS.length],\n borderWidth: 1,\n pointRadius: 0,\n ...s,\n })),\n },\n }\n}\n\nexport async function plot(options: PlotOptions, series: PlotData[]) {\n const {\n CategoryScale,\n Chart,\n LinearScale,\n LineController,\n BarController,\n LineElement,\n BarElement,\n PointElement,\n Legend,\n Title,\n } = await import('chart.js')\n const { BarWithErrorBar, BarWithErrorBarsController } = await import('chartjs-chart-error-bars')\n Chart.register(\n CategoryScale,\n LineController,\n LineElement,\n BarController,\n LinearScale,\n BarElement,\n PointElement,\n Legend,\n Title,\n BarWithErrorBar,\n BarWithErrorBarsController,\n )\n const { Canvas } = await import('skia-canvas')\n\n log.debug('plot')\n const config = plotConfig(options, series)\n const canvas = new Canvas(1280, 720)\n const chart = new Chart(canvas as unknown as HTMLCanvasElement, config)\n await canvas.toFile(options.filePath || 'plot.png', { format: 'png', matte: 'white' })\n chart.destroy()\n}\n\nexport async function plotHtml(options: PlotOptions, series: PlotData[]) {\n log.debug('plotHtml')\n const config = plotConfig(options, series)\n\n const data = `\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${options.title || 'Plot'}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/chartjs-chart-error-bars\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/hammerjs\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom\"></script>\n</head>\n<body>\n <div>\n <canvas id=\"chart\"></canvas>\n </div>\n <script>\n const ctx = document.getElementById('chart');\n const chart = new Chart(ctx, ${json5.stringify(config)});\n chart.options.onClick = e => e.chart.resetZoom();\n addEventListener('resize', () => chart.resize());\n </script>\n</body>\n</html>`\n await fs.promises.writeFile(options.filePath || 'plot.html', data)\n}\n\nfunction groupByParticipant(rows: StatsRow[]) {\n const m = new Map<string, StatsRow[]>()\n for (const r of rows) {\n if (!r.participantName) continue\n const p = r.participantName as string\n if (!m.has(p)) m.set(p, [])\n m.get(p)!.push(r)\n }\n return m\n}\n\nexport async function plotDetailedStatsDashboard(statsFile: string, outFile = 'plot.html') {\n const rows = await parseStatsFile(statsFile)\n if (rows.length === 0) {\n log.warn('No stats found')\n return\n }\n\n const byParticipant = groupByParticipant(rows)\n const participants = Array.from(byParticipant.keys()).sort()\n\n type ChartSpec = { id: string; title?: string; yLabel?: string; datasets?: PlotData[]; width?: number }\n const charts: ChartSpec[] = []\n\n const build = (\n id: string,\n title?: string,\n yLabel?: string,\n processValue?: (value: number) => number,\n width?: number,\n ) => {\n const series: PlotData[] = []\n if (!id.startsWith('_')) {\n for (const [participant, rows] of byParticipant.entries()) {\n const dataPerTrack = new Map<string, { x: number; y: number }[]>()\n for (const r of rows) {\n const trackId = (r.trackId as string) || ''\n if (!dataPerTrack.has(trackId)) dataPerTrack.set(trackId, [])\n const data = dataPerTrack.get(trackId)!\n const v = r[id] as number\n if (v !== undefined) data.push({ x: r.datetime as number, y: processValue ? processValue(v) : v })\n }\n dataPerTrack.forEach((data, trackId) => {\n if (data.length) series.push({ label: `${participant}${trackId ? ` (${trackId})` : ''}`, data: data })\n })\n }\n }\n charts.push({ id, title, yLabel, datasets: series, width })\n }\n\n const kbps = (v: number) => v / 1000\n const percent = (v: number) => v * 100\n const ms = (v: number) => v * 1000\n\n ;[\n { id: '_throttle', title: 'Throttle settings' },\n { id: 'throttleUpRate', title: 'Throttle up rate', yLabel: 'Kbps', processValue: kbps, width: 2 },\n { id: 'throttleUpDelay', title: 'Throttle up delay', yLabel: 'ms', width: 2 },\n { id: 'throttleUpLoss', title: 'Throttle up loss', yLabel: '%', width: 2 },\n { id: 'throttleDownRate', title: 'Throttle down rate', yLabel: 'Kbps', processValue: kbps, width: 2 },\n { id: 'throttleDownDelay', title: 'Throttle down delay', yLabel: 'ms', width: 2 },\n { id: 'throttleDownLoss', title: 'Throttle down loss', yLabel: '%', width: 2 },\n // Performance / Connectivity\n { id: '_performance', title: 'Performance / Connectivity' },\n { id: 'pageCpu', title: 'Page CPU', yLabel: '%' },\n { id: 'pageMemory', title: 'Page memory', yLabel: 'MB' },\n { id: 'peerConnectionConnectionTime', title: 'Peer connection connection time', yLabel: 's' },\n { id: 'peerConnectionDisconnectionTime', title: 'Peer connection disconnection time', yLabel: 's' },\n // Sent audio\n { id: '_sentAudio', title: 'Sent audio' },\n { id: 'audioSentBitrates', title: 'Sent audio bitrate', yLabel: 'Kbps', processValue: kbps },\n { id: 'audioSentPacketsLossRate', title: 'Send audio loss', yLabel: '%', processValue: percent },\n { id: 'audioSentRoundTripTime', title: 'Send audio RTT', yLabel: 'ms', processValue: ms },\n { id: 'audioSentJitter', title: 'Send audio jitter', yLabel: 'ms', processValue: ms },\n // Sent video\n { id: '_sentVideo', title: 'Sent video' },\n { id: 'videoSentBitrates', title: 'Sent video bitrate', yLabel: 'Kbps', processValue: kbps },\n { id: 'videoSentPacketsLossRate', title: 'Send video loss', yLabel: '%', processValue: percent },\n { id: 'videoSentRoundTripTime', title: 'Send video RTT', yLabel: 'ms', processValue: ms },\n { id: 'videoSentJitter', title: 'Send video jitter', yLabel: 'ms', processValue: ms },\n { id: 'videoSentWidth', title: 'Send video width', yLabel: 'px' },\n { id: 'videoSentHeight', title: 'Send video height', yLabel: 'px' },\n { id: 'videoSentFps', title: 'Send video framerate', yLabel: 'fps' },\n {\n id: 'transportSentAvailableOutgoingBitrate',\n title: 'Send available bitrate',\n yLabel: 'Kbps',\n processValue: kbps,\n },\n { id: 'videoQualityLimitationCpu', title: 'Send video CPU limitation', yLabel: '%' },\n { id: 'videoQualityLimitationBandwidth', title: 'Send video bandwidth limitation', yLabel: '%' },\n { id: 'videoFirCountReceived', title: 'Send video FIR count', yLabel: 'count' },\n { id: 'videoPliCountReceived', title: 'Send video PLI count', yLabel: 'count' },\n // Sent screen\n { id: '_sentScreen', title: 'Sent screen' },\n { id: 'screenSentBitrates', title: 'Sent screen bitrate', yLabel: 'Kbps', processValue: kbps },\n { id: 'screenSentPacketsLossRate', title: 'Send screen loss', yLabel: '%', processValue: percent },\n { id: 'screenSentRoundTripTime', title: 'Send screen RTT', yLabel: 'ms', processValue: ms },\n { id: 'screenSentJitter', title: 'Send screen jitter', yLabel: 'ms', processValue: ms },\n { id: 'screenSentWidth', title: 'Send screen width', yLabel: 'px' },\n { id: 'screenSentHeight', title: 'Send screen height', yLabel: 'px' },\n { id: 'screenSentFps', title: 'Send screen framerate', yLabel: 'fps' },\n { id: '' },\n { id: 'screenQualityLimitationCpu', title: 'Send screen CPU limitation', yLabel: '%' },\n { id: 'screenQualityLimitationBandwidth', title: 'Send screen bandwidth limitation', yLabel: '%' },\n { id: 'screenFirCountReceived', title: 'Send screen FIR count', yLabel: 'count' },\n { id: 'screenPliCountReceived', title: 'Send screen PLI count', yLabel: 'count' },\n // Recv audio\n { id: '_recvAudio', title: 'Recv audio' },\n { id: 'audioRecvBitrates', title: 'Recv audio bitrate', yLabel: 'Kbps', processValue: kbps },\n { id: 'audioRecvPacketsLossRate', title: 'Recv audio loss', yLabel: '%', processValue: percent },\n { id: 'audioRecvJitter', title: 'Recv audio jitter', yLabel: 'ms', processValue: ms },\n { id: 'audioRecvAvgJitterBufferDelay', title: 'Recv audio jitter buffer', yLabel: 'ms', processValue: ms },\n { id: 'audioRecvLevel', title: 'Recv audio level', yLabel: 'db' },\n { id: 'audioRecvConcealmentEvents', title: 'Recv audio concealment events', yLabel: 'count' },\n {\n id: 'audioRecvInsertedSamplesForDeceleration',\n title: 'Recv audio inserted samples',\n yLabel: 'count',\n },\n {\n id: 'audioRecvRemovedSamplesForAcceleration',\n title: 'Recv audio removed samples',\n yLabel: 'count',\n },\n { id: 'audioRecvEndToEndDelay', title: 'Recv audio end to end delay', yLabel: 'ms', processValue: ms },\n // Recv video\n { id: '_recvVideo', title: 'Recv video' },\n { id: 'videoRecvBitrates', title: 'Recv video bitrate', yLabel: 'Kbps', processValue: kbps },\n { id: 'videoRecvPacketsLossRate', title: 'Recv video loss', yLabel: '%', processValue: percent },\n { id: 'videoRecvJitter', title: 'Recv video jitter', yLabel: 'ms', processValue: ms },\n { id: 'videoRecvAvgJitterBufferDelay', title: 'Recv video jitter buffer', yLabel: 'ms', processValue: ms },\n { id: 'videoRecvWidth', title: 'Recv video width', yLabel: 'px' },\n { id: 'videoRecvHeight', title: 'Recv video height', yLabel: 'px' },\n { id: 'videoRecvFps', title: 'Recv video framerate', yLabel: 'fps' },\n { id: 'videoTotalFreezesDuration', title: 'Recv video freezes', yLabel: 'count' },\n { id: 'videoFirCountSent', title: 'Recv video FIR sent', yLabel: 'count' },\n { id: 'videoPliCountSent', title: 'Recv video PLI sent', yLabel: 'count' },\n { id: 'videoRecvEndToEndDelay', title: 'Recv video end to end delay', yLabel: 'ms', processValue: ms },\n { id: '' },\n // Recv screen\n { id: '_recvScreen', title: 'Recv screen' },\n { id: 'screenRecvBitrates', title: 'Recv screen bitrate', yLabel: 'Kbps', processValue: kbps },\n { id: 'screenRecvPacketsLossRate', title: 'Recv screen loss', yLabel: '%', processValue: percent },\n { id: 'screenRecvJitter', title: 'Recv screen jitter', yLabel: 'ms', processValue: ms },\n { id: 'screenRecvAvgJitterBufferDelay', title: 'Recv screen jitter buffer', yLabel: 'ms', processValue: ms },\n { id: 'screenRecvWidth', title: 'Recv screen width', yLabel: 'px' },\n { id: 'screenRecvHeight', title: 'Recv screen height', yLabel: 'px' },\n { id: 'screenRecvFps', title: 'Recv screen framerate', yLabel: 'fps' },\n { id: 'screenTotalFreezesDuration', title: 'Recv screen freezes', yLabel: 'count' },\n { id: 'screenFirCountSent', title: 'Recv screen FIR sent', yLabel: 'count' },\n { id: 'screenPliCountSent', title: 'Recv screen PLI sent', yLabel: 'count' },\n { id: 'screenRecvEndToEndDelay', title: 'Recv screen end to end delay', yLabel: 'ms', processValue: ms },\n { id: '' },\n ].forEach(graph => {\n build(graph.id, graph.title, graph.yLabel, graph.processValue, graph.width)\n })\n\n const [_, id, scenario] = path.basename(path.dirname(statsFile)).split('_')\n const description = formatThrottleRule(parseThrottleRule(scenario), true, false)\n\n const data = `\\\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${id} (${description})</title>\n <link rel=\"icon\" href=\"https://raw.githubusercontent.com/vpalmisano/webrtcperf/devel/media/logo.svg\">\n <script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/chartjs-chart-error-bars\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/hammerjs\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom\"></script>\n <script src=\"https://unpkg.com/vue@3/dist/vue.global.prod.js\"></script>\n <link href=\"https://cdn.jsdelivr.net/npm/@mdi/font@7.x/css/materialdesignicons.min.css\" rel=\"stylesheet\">\n <link href=\"https://cdn.jsdelivr.net/npm/vuetify@3.7.2/dist/vuetify.min.css\" rel=\"stylesheet\">\n <script src=\"https://cdn.jsdelivr.net/npm/vuetify@3.7.2/dist/vuetify.min.js\"></script>\n</head>\n<body>\n <div id=\"app\">\n <v-app>\n <v-main>\n <v-app-bar color=\"primary\" density=\"compact\">\n <v-app-bar-title><b>${id}</b> (${description})</v-app-bar-title>\n <template v-slot:append>\n <v-select color=\"primary\" :items=\"participants\" v-model=\"selected\" density=\"compact\" variant=\"solo\" hide-details=\"auto\"></v-select>\n </template>\n </v-app-bar>\n <v-container fluid>\n <v-row dense>\n <v-col v-for=\"c in charts\" :key=\"c.id\" cols=\"12\" :md=\"isExpanded(c.id) ? 12 : c.width || 3\">\n <template v-if=\"c.id\">\n <v-card color=\"primary\" :variant=\"c.id.startsWith('_') ? 'tonal' : 'text'\">\n <v-card-title class=\"text-subtitle-1 d-flex align-center flex-nowrap\" @click=\"toggleExpanded(c)\" style=\"cursor: pointer;\">\n <span class=\"text-truncate\">{{ c.title }}</span>\n </v-card-title>\n <v-card-text v-if=\"!c.id.startsWith('_')\" style=\"min-height: 250px;\">\n <canvas :id=\"c.id\"></canvas>\n </v-card-text>\n </v-card>\n </template>\n <template v-else>\n <div class=\"empty-slot\"></div>\n </template>\n </v-col>\n </v-row>\n </v-container>\n <v-footer color=\"primary\" density=\"compact\">\n <v-btn class=\"text-none\" variant=\"text\" density=\"compact\" size=\"small\" href=\"https://github.com/vpalmisano/webrtcperf\" target=\"_blank\">Generated with webrtcperf</v-btn>\n </v-footer>\n </v-main>\n </v-app>\n </div>\n\n <script>\n const { createApp, onMounted, nextTick, watch, ref } = Vue;\n const vuetify = Vuetify.createVuetify();\n const PARTICIPANTS = ${json5.stringify(['All', ...participants])};\n const CHARTS = ${json5.stringify(charts)};\n const SERIES_COLORS = ${json5.stringify(SERIES_COLORS)};\n\n function fmtTime(v) {\n const d = new Date(Number(v));\n if (!isFinite(d.getTime())) return v;\n const pad = n => String(n).padStart(2, '0');\n return pad(d.getHours()) + ':' + pad(d.getMinutes())\n }\n\n function buildDatasets(datasets, selected) {\n const filtered = selected === 'All' ? datasets : datasets.filter(d => d.label.startsWith(selected));\n return filtered.map((s, i) => ({\n label: s.label,\n data: s.data,\n fill: false,\n backgroundColor: SERIES_COLORS[i % SERIES_COLORS.length],\n borderColor: SERIES_COLORS[i % SERIES_COLORS.length],\n borderWidth: 1,\n pointRadius: 0,\n }));\n }\n\n createApp({\n setup() {\n const participants = ref(PARTICIPANTS);\n const selected = ref('All');\n const charts = ref(CHARTS);\n const chartInstances = new Map();\n const expanded = ref(new Set());\n\n function onPanZoom({ chart }) {\n const x = chart.scales.x;\n if (!x) return;\n // Sync zoom level across all charts\n for (const { chart: otherChart } of chartInstances.values()) {\n if (otherChart !== chart) {\n const otherX = otherChart.scales.x;\n if (otherX) {\n otherX.options.min = x.min;\n otherX.options.max = x.max;\n otherChart.update('none');\n }\n }\n }\n }\n\n function createPanel(chartSpec) {\n const canvas = document.getElementById(chartSpec.id);\n if (!canvas) return;\n const ctx = canvas.getContext('2d');\n const chart = new Chart(ctx, {\n type: 'line',\n options: {\n maintainAspectRatio: false,\n animation: false,\n layout: {\n padding: 0,\n },\n interaction: {\n intersect: false,\n mode: 'x',\n },\n plugins: {\n tooltip: {\n callbacks: {\n title: (context) => {\n if (context[0].parsed.x !== null) {\n return fmtTime(context[0].parsed.x);\n }\n return '';\n },\n label: (context) => {\n return context.parsed.y;\n }\n },\n },\n legend: { \n display: true,\n position: 'bottom',\n align: 'start',\n maxHeight: 50,\n labels: {\n boxWidth: 8,\n boxHeight: 8,\n font: {\n size: 8,\n lineHeight: 1,\n },\n },\n },\n zoom: {\n pan: {\n enabled: true,\n mode: 'x',\n modifierKey: 'ctrl',\n onPan: onPanZoom,\n },\n zoom: {\n drag: { enabled: true },\n mode: 'x',\n onZoom: onPanZoom,\n },\n },\n },\n scales: {\n x: { \n type: 'linear',\n title: { display: false, text: 'Time' },\n ticks: { display: true, callback: (value) => fmtTime(value) },\n },\n y: {\n type: 'linear',\n title: { display: true, text: chartSpec.yLabel },\n min: 0,\n },\n },\n },\n data: { datasets: buildDatasets(chartSpec.datasets, selected.value) },\n });\n chartInstances.set(chartSpec.id, { chart, spec: chartSpec });\n\n canvas.addEventListener('contextmenu', e => {\n e.preventDefault();\n resetZoom();\n });\n }\n\n function rebuildCharts() {\n for (const { chart } of chartInstances.values()) chart.destroy();\n chartInstances.clear();\n nextTick(() => charts.value.forEach(createPanel));\n }\n\n function applyFilter() {\n for (const { chart, spec } of chartInstances.values()) {\n chart.data.datasets = buildDatasets(spec.datasets, selected.value);\n chart.update();\n }\n }\n\n function resetZoom() {\n for (const { chart } of chartInstances.values()) {\n chart.resetZoom('none');\n if (chart.scales.x) {\n chart.scales.x.options.min = undefined;\n chart.scales.x.options.max = undefined;\n }\n chart.update('none');\n }\n }\n\n function toggleExpanded(c) {\n const id = c.id;\n const s = new Set(expanded.value);\n if (s.has(id)) s.delete(id); else s.add(id);\n expanded.value = s;\n nextTick(() => { const entry = chartInstances.get(id); entry?.chart?.resize(); });\n }\n\n function isExpanded(id) {\n return id.startsWith('_') || expanded.value.has(id);\n }\n\n onMounted(() => {\n nextTick(() => charts.value.forEach(createPanel));\n window.addEventListener('resize', () => { for (const { chart } of chartInstances.values()) chart.resize(); });\n });\n\n watch(selected, () => applyFilter());\n\n return { participants, selected, charts, resetZoom, toggleExpanded, isExpanded };\n }\n }).use(vuetify).mount('#app');\n </script>\n</body>\n</html>`\n\n await fs.promises.writeFile(outFile, data)\n}\n"]}
@@ -1,11 +1,12 @@
1
1
  import { FastStats } from './stats';
2
2
  import { ThrottleRule } from '@vpalmisano/throttler';
3
+ export type StatsRow = Record<string, string | number>;
3
4
  /**
4
5
  * It parses a CSV stats file and returns an array of objects representing each row.
5
6
  * @param filePath The path to the CSV stats file.
6
7
  * @returns An array of objects where each object represents a row in the CSV file with keys as column headers.
7
8
  */
8
- export declare function parseStatsFile(filePath: string): Promise<Record<string, string | number>[]>;
9
+ export declare function parseStatsFile(filePath: string): Promise<StatsRow[]>;
9
10
  export type StatsSummary = {
10
11
  timestamp: number;
11
12
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"scenarios.js","sourceRoot":"","sources":["../../src/scenarios.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs'\nimport path from 'path'\nimport { FastStats } from './stats'\nimport { ThrottleConfig, ThrottleRule } from '@vpalmisano/throttler'\nimport { Config } from './config'\nimport { Auth, google } from 'googleapis'\nimport { logger } from './utils'\nimport { sprintf } from 'sprintf-js'\nimport { PlotData, plotHtml } from './plot'\n\nconst log = logger('webrtcperf:scenarios')\n\n/**\n * It parses a CSV stats file and returns an array of objects representing each row.\n * @param filePath The path to the CSV stats file.\n * @returns An array of objects where each object represents a row in the CSV file with keys as column headers.\n */\nexport async function parseStatsFile(filePath: string) {\n log.debug(`parseStatsFile: ${filePath}`)\n const fileData = await fs.promises.readFile(filePath, 'utf-8')\n const lines = fileData.split('\\n')\n const headers = lines[0].split(',')\n const data = lines.slice(1).map(line =>\n line.split(',').reduce(\n (acc, value, index) => {\n if (value !== '') {\n acc[headers[index]] = isNaN(Number(value)) ? value : Number(value)\n }\n return acc\n },\n {} as Record<string, string | number>,\n ),\n )\n return data\n}\n\nexport type StatsSummary = {\n timestamp: number\n id: string\n scenario: string\n videoRecvBitratePerPixel: FastStats\n videoRecvFps: FastStats\n videoSentFps: FastStats\n}\n\n/**\n * It aggregates the stats summary from multiple test runs in a directory.\n * @param options.dirPath Directory path containing test run subdirectories. Default is 'logs'.\n * @param options.senderParticipantName Participant name of the sender. Default is 'Participant-000001'.\n * @param options.receiverParticipantName Participant name of the receiver. Default is 'Participant-000000'.\n * @param options.nameParser Function to parse test directory names. Default splits by '_' and extracts id and scenario.\n * @returns Array of aggregated stats including timestamp, id, scenario, videoRecvBitratePerPixel, videoRecvFps, and videoSentFps.\n */\nexport async function aggregateStatsSummary({\n dirPath = 'logs',\n senderParticipantName = 'Participant-000001',\n receiverParticipantName = 'Participant-000000',\n nameParser = (name: string) => {\n const [_, id, scenario] = name.split('_')\n return { id, scenario }\n },\n}) {\n log.debug(`aggregateStatsSummary: ${dirPath}`)\n const stats: StatsSummary[] = []\n const results = await fs.promises.readdir(dirPath)\n for (const test of results) {\n const filePath = path.join(dirPath, test, 'detailed-stats-summary.csv')\n if (!fs.existsSync(filePath)) continue\n const timestamp = fs.statSync(path.join(dirPath, test)).ctime.getTime()\n const data = await parseStatsFile(filePath)\n const { id, scenario } = nameParser(test)\n\n const aggregated = {\n timestamp,\n id,\n scenario,\n videoRecvBitratePerPixel: new FastStats(),\n videoRecvFps: new FastStats(),\n videoSentFps: new FastStats(),\n }\n data.forEach(v => {\n const { participantName, trackId } = v as { participantName: string; trackId: string }\n const metrics = v as Record<string, number>\n if (participantName === receiverParticipantName) {\n if (trackId?.endsWith('-v') && metrics.videoRecvFrames > 0) {\n const videoRecvBitratePerPixel =\n metrics.videoRecvBitrates / (metrics.videoRecvWidth * metrics.videoRecvHeight)\n if (!isNaN(videoRecvBitratePerPixel)) aggregated.videoRecvBitratePerPixel.push(videoRecvBitratePerPixel)\n if (!isNaN(metrics.videoRecvFps)) aggregated.videoRecvFps.push(metrics.videoRecvFps)\n }\n } else if (participantName === senderParticipantName) {\n if (trackId?.endsWith('-v') && metrics.videoSentFrames > 0) {\n if (!isNaN(metrics.videoSentFps)) aggregated.videoSentFps.push(metrics.videoSentFps)\n }\n }\n })\n stats.push(aggregated)\n }\n return stats.sort((a, b) => a.timestamp - b.timestamp)\n}\n\n/**\n * It uploads the aggregated stats to a Google Sheet.\n * A valid Google service account credentials file must be specified\n * in the `GOOGLE_CREDENTIALS_PATH` environment variable.\n * @param stats The aggregated stats to upload.\n * @param spreadsheetId The ID of the Google Spreadsheet.\n * @param table The name of the table (sheet) within the spreadsheet. Default is 'data'.\n */\nexport async function uploadStatsToGoogleSheet(stats: StatsSummary[], spreadsheetId: string, table = 'data') {\n log.debug(`uploadResultsToGoogleSheet spreadsheetId: ${spreadsheetId} table: ${table}`)\n if (!process.env.GOOGLE_CREDENTIALS_PATH) throw new Error('GOOGLE_CREDENTIALS_PATH environment variable is not set')\n if (!fs.existsSync(process.env.GOOGLE_CREDENTIALS_PATH))\n throw new Error(`Google credentials file not found: ${process.env.GOOGLE_CREDENTIALS_PATH}`)\n if (!stats.length) return\n const auth = new Auth.GoogleAuth({\n keyFile: process.env.GOOGLE_CREDENTIALS_PATH,\n scopes: ['https://www.googleapis.com/auth/spreadsheets'],\n })\n const sheets = google.sheets({ version: 'v4', auth })\n // Update headers.\n const headers = ['datetime', 'id', 'scenario', 'videoRecvBitratePerPixel', 'videoRecvFps']\n await sheets.spreadsheets.values.update({\n spreadsheetId,\n range: `${table}!A1:E1`,\n valueInputOption: 'USER_ENTERED',\n requestBody: { majorDimension: 'ROWS', values: [headers] },\n })\n // Append values.\n const values = [] as string[][]\n stats.forEach(s => {\n const { timestamp, id, scenario, videoRecvBitratePerPixel, videoRecvFps } = s\n if (!videoRecvBitratePerPixel.length) return\n const datetime = new Date(timestamp).toLocaleString('en-US', {\n timeZone: 'UTC',\n hourCycle: 'h23',\n })\n values.push([\n datetime,\n id,\n formatThrottleRule(parseThrottleRule(scenario), true),\n videoRecvBitratePerPixel.percentile(95).toFixed(3),\n videoRecvFps.percentile(95).toFixed(3),\n ])\n })\n if (values.length) {\n await sheets.spreadsheets.values.append({\n spreadsheetId,\n range: `${table}!A:E`,\n valueInputOption: 'USER_ENTERED',\n insertDataOption: 'INSERT_ROWS',\n requestBody: { majorDimension: 'ROWS', values },\n })\n }\n}\n\nexport type ThrottleDirection = 'up' | 'down' | 'bidi'\n\nexport function formatBitrate(bitrate: number | undefined, prefix = ' ', pad = true) {\n if (bitrate === undefined) return ''\n let suffix = 'Kbps'\n if (bitrate >= 1000) {\n bitrate /= 1000\n suffix = 'Mbps'\n }\n return `${prefix}${sprintf(`%${pad ? '5' : ''}.4g`, bitrate)}${suffix}`\n}\n\nexport function formatLoss(loss: number | undefined, prefix = ' ', pad = true) {\n return loss !== undefined ? `${prefix}${loss.toFixed(0).padStart(pad ? 2 : 0, ' ')}%` : ''\n}\n\nexport function formatDelay(delay: number | undefined, prefix = ' ', pad = true) {\n return delay !== undefined ? `${prefix}${delay.toFixed(0).padStart(pad ? 3 : 0, ' ')}ms` : ''\n}\n\nexport function formatThrottleRule(\n throttleRule: ThrottleRule & { direction: ThrottleDirection },\n human = false,\n pad = true,\n) {\n const { rate, loss, delay, direction } = throttleRule\n return human\n ? `${direction.padEnd(pad ? 4 : 0, ' ')}${formatBitrate(rate, ' ', pad)}${formatLoss(loss, ' ', pad)}${formatDelay(delay, ' ', pad)}`\n : `${direction}-r${rate}-l${loss}-d${delay}`\n}\n\nexport function parseThrottleRule(throttleDesc: string) {\n const match = throttleDesc.match(/(up|down|bidi)-r(\\d+)-l([\\d.]+)-d(\\d+)/)\n if (!match) throw new Error(`Invalid throttle description: ${throttleDesc}`)\n const direction = match[1] as ThrottleDirection\n const rate = parseInt(match[2])\n const loss = parseInt(match[3])\n const delay = parseInt(match[4])\n return { direction, rate, loss, delay }\n}\n\nexport async function plotStatsSummary(stats: StatsSummary[]) {\n const labels = new Set<string>()\n const data = {} as Record<string, Record<string, FastStats>>\n stats.forEach(s => {\n const { id, scenario, videoRecvBitratePerPixel } = s\n if (!videoRecvBitratePerPixel.length) return null\n if (!data[id]) {\n data[id] = {}\n }\n const scenarioFormatted = formatThrottleRule(parseThrottleRule(scenario), true, true)\n if (!data[id][scenarioFormatted]) {\n data[id][scenarioFormatted] = new FastStats()\n }\n labels.add(scenarioFormatted)\n data[id][scenarioFormatted].push(videoRecvBitratePerPixel.percentile(95))\n })\n const series: PlotData[] = []\n Object.entries(data)\n .sort((a, b) => a[0].localeCompare(b[0]))\n .forEach(([id, data]) => {\n const plotData: PlotData = { label: id, data: [] }\n Object.entries(data)\n .sort((a, b) => a[0].localeCompare(b[0]))\n .forEach(([scenario, stats]) => {\n plotData.data.push({\n x: scenario,\n y: stats.percentile(50),\n yMin: stats.percentile(5),\n yMax: stats.percentile(95),\n })\n })\n series.push(plotData)\n })\n await plotHtml(\n {\n type: 'barWithErrorBars',\n xLabel: 'Scenario',\n yLabel: 'Video Receive Bitrate per Pixel',\n labels: Array.from(labels).sort((a, b) => a.localeCompare(b)),\n },\n series,\n )\n}\n\n/**\n * It generates a test configuration with a scenario including 2 participants.\n * The first participant sends video and the second receives it.\n * Both participants send and receive audio.\n * The network conditions are applied according to the specified direction to the sender (`up`),\n * the receiver (`down`) or both (`bidi`).\n * The test is repeated the specified number of times.\n * The output is an array of partial configuration objects that can be used to run the tests\n * with the main application, after merging it with a configuration that includes\n * the destination url (mandatory) and other optional parameters.\n * @param id The unique identifier for the test scenario.\n * @param options.rate The target bandwidth in kbps.\n * @param options.loss The packet loss percentage.\n * @param options.delay The network delay in milliseconds.\n * @param options.direction The direction of the network throttling: 'up', 'down', or 'bidi'.\n * @param repeat The number of times to repeat the test scenario. Default is 1.\n * @returns An array of partial configuration objects for each test scenario.\n */\nexport async function twoParticipantsWithRateLossDelay(\n id: string,\n { rate, loss, delay, direction }: { rate: number; loss: number; delay: number; direction: ThrottleDirection },\n repeat: 1,\n) {\n const throttle: ThrottleConfig = {}\n const queue = 25\n if (direction === 'down' || direction === 'bidi') {\n throttle.down = [\n { rate: 20000, loss: 0, delay: 0, queue },\n { rate, loss, delay, queue, at: 30 },\n ]\n }\n if (direction === 'up' || direction === 'bidi') {\n throttle.up = [\n { rate: 20000, loss: 0, delay, queue },\n { rate, loss, delay, queue, at: 30 },\n ]\n }\n const throttleDesc = formatThrottleRule({ rate, loss, delay, direction })\n const now = Date.now()\n const ret: Partial<Config>[] = []\n for (let i = 0; i < repeat; i++) {\n const basePath = `logs/${now}-${i + 1}_${id}_${throttleDesc}`\n const sessions = direction === 'bidi' ? '0-1' : direction === 'down' ? '0' : '1'\n ret.push({\n sessions: 2,\n runDuration: 60 * 3,\n debuggingPort: 9000,\n prometheusPushgateway: 'http://localhost:9091',\n prometheusPushgatewayJobName: id,\n statsPath: `${basePath}/stats.csv`,\n detailedStatsPath: `${basePath}/detailed-stats.csv`,\n showPageLog: false,\n showStats: false,\n statsInterval: 5,\n scriptParams: JSON.stringify({\n enableMic: '0-1',\n enableCam: '1',\n }),\n throttleConfig: JSON.stringify([\n {\n sessions,\n protocol: 'udp',\n skipSourcePorts: '53,80,443',\n skipDestinationPorts: '53,80,443',\n ...throttle,\n },\n ]),\n })\n }\n return ret\n}\n"]}
1
+ {"version":3,"file":"scenarios.js","sourceRoot":"","sources":["../../src/scenarios.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs'\nimport path from 'path'\nimport { FastStats } from './stats'\nimport { ThrottleConfig, ThrottleRule } from '@vpalmisano/throttler'\nimport { Config } from './config'\nimport { Auth, google } from 'googleapis'\nimport { logger } from './utils'\nimport { sprintf } from 'sprintf-js'\nimport { PlotData, plotHtml } from './plot'\n\nconst log = logger('webrtcperf:scenarios')\n\nexport type StatsRow = Record<string, string | number>\n\n/**\n * It parses a CSV stats file and returns an array of objects representing each row.\n * @param filePath The path to the CSV stats file.\n * @returns An array of objects where each object represents a row in the CSV file with keys as column headers.\n */\nexport async function parseStatsFile(filePath: string) {\n log.debug(`parseStatsFile: ${filePath}`)\n const fileData = await fs.promises.readFile(filePath, 'utf-8')\n const lines = fileData.split('\\n')\n const headers = lines[0].split(',')\n const data = lines.slice(1).map(line =>\n line.split(',').reduce((acc, value, index) => {\n if (value !== '') {\n acc[headers[index]] = isNaN(Number(value)) ? value : Number(value)\n }\n return acc\n }, {} as StatsRow),\n )\n return data\n}\n\nexport type StatsSummary = {\n timestamp: number\n id: string\n scenario: string\n videoRecvBitratePerPixel: FastStats\n videoRecvFps: FastStats\n videoSentFps: FastStats\n}\n\n/**\n * It aggregates the stats summary from multiple test runs in a directory.\n * @param options.dirPath Directory path containing test run subdirectories. Default is 'logs'.\n * @param options.senderParticipantName Participant name of the sender. Default is 'Participant-000001'.\n * @param options.receiverParticipantName Participant name of the receiver. Default is 'Participant-000000'.\n * @param options.nameParser Function to parse test directory names. Default splits by '_' and extracts id and scenario.\n * @returns Array of aggregated stats including timestamp, id, scenario, videoRecvBitratePerPixel, videoRecvFps, and videoSentFps.\n */\nexport async function aggregateStatsSummary({\n dirPath = 'logs',\n senderParticipantName = 'Participant-000001',\n receiverParticipantName = 'Participant-000000',\n nameParser = (name: string) => {\n const [_, id, scenario] = name.split('_')\n return { id, scenario }\n },\n}) {\n log.debug(`aggregateStatsSummary: ${dirPath}`)\n const stats: StatsSummary[] = []\n const results = await fs.promises.readdir(dirPath)\n for (const test of results) {\n const filePath = path.join(dirPath, test, 'detailed-stats-summary.csv')\n if (!fs.existsSync(filePath)) continue\n const timestamp = fs.statSync(path.join(dirPath, test)).ctime.getTime()\n const data = await parseStatsFile(filePath)\n const { id, scenario } = nameParser(test)\n\n const aggregated = {\n timestamp,\n id,\n scenario,\n videoRecvBitratePerPixel: new FastStats(),\n videoRecvFps: new FastStats(),\n videoSentFps: new FastStats(),\n }\n data.forEach(v => {\n const { participantName, trackId } = v as { participantName: string; trackId: string }\n const metrics = v as Record<string, number>\n if (participantName === receiverParticipantName) {\n if (trackId?.endsWith('-v') && metrics.videoRecvFrames > 0) {\n const videoRecvBitratePerPixel =\n metrics.videoRecvBitrates / (metrics.videoRecvWidth * metrics.videoRecvHeight)\n if (!isNaN(videoRecvBitratePerPixel)) aggregated.videoRecvBitratePerPixel.push(videoRecvBitratePerPixel)\n if (!isNaN(metrics.videoRecvFps)) aggregated.videoRecvFps.push(metrics.videoRecvFps)\n }\n } else if (participantName === senderParticipantName) {\n if (trackId?.endsWith('-v') && metrics.videoSentFrames > 0) {\n if (!isNaN(metrics.videoSentFps)) aggregated.videoSentFps.push(metrics.videoSentFps)\n }\n }\n })\n stats.push(aggregated)\n }\n return stats.sort((a, b) => a.timestamp - b.timestamp)\n}\n\n/**\n * It uploads the aggregated stats to a Google Sheet.\n * A valid Google service account credentials file must be specified\n * in the `GOOGLE_CREDENTIALS_PATH` environment variable.\n * @param stats The aggregated stats to upload.\n * @param spreadsheetId The ID of the Google Spreadsheet.\n * @param table The name of the table (sheet) within the spreadsheet. Default is 'data'.\n */\nexport async function uploadStatsToGoogleSheet(stats: StatsSummary[], spreadsheetId: string, table = 'data') {\n log.debug(`uploadResultsToGoogleSheet spreadsheetId: ${spreadsheetId} table: ${table}`)\n if (!process.env.GOOGLE_CREDENTIALS_PATH) throw new Error('GOOGLE_CREDENTIALS_PATH environment variable is not set')\n if (!fs.existsSync(process.env.GOOGLE_CREDENTIALS_PATH))\n throw new Error(`Google credentials file not found: ${process.env.GOOGLE_CREDENTIALS_PATH}`)\n if (!stats.length) return\n const auth = new Auth.GoogleAuth({\n keyFile: process.env.GOOGLE_CREDENTIALS_PATH,\n scopes: ['https://www.googleapis.com/auth/spreadsheets'],\n })\n const sheets = google.sheets({ version: 'v4', auth })\n // Update headers.\n const headers = ['datetime', 'id', 'scenario', 'videoRecvBitratePerPixel', 'videoRecvFps']\n await sheets.spreadsheets.values.update({\n spreadsheetId,\n range: `${table}!A1:E1`,\n valueInputOption: 'USER_ENTERED',\n requestBody: { majorDimension: 'ROWS', values: [headers] },\n })\n // Append values.\n const values = [] as string[][]\n stats.forEach(s => {\n const { timestamp, id, scenario, videoRecvBitratePerPixel, videoRecvFps } = s\n if (!videoRecvBitratePerPixel.length) return\n const datetime = new Date(timestamp).toLocaleString('en-US', {\n timeZone: 'UTC',\n hourCycle: 'h23',\n })\n values.push([\n datetime,\n id,\n formatThrottleRule(parseThrottleRule(scenario), true),\n videoRecvBitratePerPixel.percentile(95).toFixed(3),\n videoRecvFps.percentile(95).toFixed(3),\n ])\n })\n if (values.length) {\n await sheets.spreadsheets.values.append({\n spreadsheetId,\n range: `${table}!A:E`,\n valueInputOption: 'USER_ENTERED',\n insertDataOption: 'INSERT_ROWS',\n requestBody: { majorDimension: 'ROWS', values },\n })\n }\n}\n\nexport type ThrottleDirection = 'up' | 'down' | 'bidi'\n\nexport function formatBitrate(bitrate: number | undefined, prefix = ' ', pad = true) {\n if (bitrate === undefined) return ''\n let suffix = 'Kbps'\n if (bitrate >= 1000) {\n bitrate /= 1000\n suffix = 'Mbps'\n }\n return `${prefix}${sprintf(`%${pad ? '5' : ''}.4g`, bitrate)}${suffix}`\n}\n\nexport function formatLoss(loss: number | undefined, prefix = ' ', pad = true) {\n return loss !== undefined ? `${prefix}${loss.toFixed(0).padStart(pad ? 2 : 0, ' ')}%` : ''\n}\n\nexport function formatDelay(delay: number | undefined, prefix = ' ', pad = true) {\n return delay !== undefined ? `${prefix}${delay.toFixed(0).padStart(pad ? 3 : 0, ' ')}ms` : ''\n}\n\nexport function formatThrottleRule(\n throttleRule: ThrottleRule & { direction: ThrottleDirection },\n human = false,\n pad = true,\n) {\n const { rate, loss, delay, direction } = throttleRule\n return human\n ? `${direction.padEnd(pad ? 4 : 0, ' ')}${formatBitrate(rate, ' ', pad)}${formatLoss(loss, ' ', pad)}${formatDelay(delay, ' ', pad)}`\n : `${direction}-r${rate}-l${loss}-d${delay}`\n}\n\nexport function parseThrottleRule(throttleDesc: string) {\n const match = throttleDesc.match(/(up|down|bidi)-r(\\d+)-l([\\d.]+)-d(\\d+)/)\n if (!match) throw new Error(`Invalid throttle description: ${throttleDesc}`)\n const direction = match[1] as ThrottleDirection\n const rate = parseInt(match[2])\n const loss = parseInt(match[3])\n const delay = parseInt(match[4])\n return { direction, rate, loss, delay }\n}\n\nexport async function plotStatsSummary(stats: StatsSummary[]) {\n const labels = new Set<string>()\n const data = {} as Record<string, Record<string, FastStats>>\n stats.forEach(s => {\n const { id, scenario, videoRecvBitratePerPixel } = s\n if (!videoRecvBitratePerPixel.length) return null\n if (!data[id]) {\n data[id] = {}\n }\n const scenarioFormatted = formatThrottleRule(parseThrottleRule(scenario), true, true)\n if (!data[id][scenarioFormatted]) {\n data[id][scenarioFormatted] = new FastStats()\n }\n labels.add(scenarioFormatted)\n data[id][scenarioFormatted].push(videoRecvBitratePerPixel.percentile(95))\n })\n const series: PlotData[] = []\n Object.entries(data)\n .sort((a, b) => a[0].localeCompare(b[0]))\n .forEach(([id, data]) => {\n const plotData: PlotData = { label: id, data: [] }\n Object.entries(data)\n .sort((a, b) => a[0].localeCompare(b[0]))\n .forEach(([scenario, stats]) => {\n plotData.data.push({\n x: scenario,\n y: stats.percentile(50),\n yMin: stats.percentile(5),\n yMax: stats.percentile(95),\n })\n })\n series.push(plotData)\n })\n await plotHtml(\n {\n type: 'barWithErrorBars',\n xLabel: 'Scenario',\n yLabel: 'Video Receive Bitrate per Pixel',\n labels: Array.from(labels).sort((a, b) => a.localeCompare(b)),\n },\n series,\n )\n}\n\n/**\n * It generates a test configuration with a scenario including 2 participants.\n * The first participant sends video and the second receives it.\n * Both participants send and receive audio.\n * The network conditions are applied according to the specified direction to the sender (`up`),\n * the receiver (`down`) or both (`bidi`).\n * The test is repeated the specified number of times.\n * The output is an array of partial configuration objects that can be used to run the tests\n * with the main application, after merging it with a configuration that includes\n * the destination url (mandatory) and other optional parameters.\n * @param id The unique identifier for the test scenario.\n * @param options.rate The target bandwidth in kbps.\n * @param options.loss The packet loss percentage.\n * @param options.delay The network delay in milliseconds.\n * @param options.direction The direction of the network throttling: 'up', 'down', or 'bidi'.\n * @param repeat The number of times to repeat the test scenario. Default is 1.\n * @returns An array of partial configuration objects for each test scenario.\n */\nexport async function twoParticipantsWithRateLossDelay(\n id: string,\n { rate, loss, delay, direction }: { rate: number; loss: number; delay: number; direction: ThrottleDirection },\n repeat: 1,\n) {\n const throttle: ThrottleConfig = {}\n const queue = 25\n if (direction === 'down' || direction === 'bidi') {\n throttle.down = [\n { rate: 20000, loss: 0, delay: 0, queue },\n { rate, loss, delay, queue, at: 30 },\n ]\n }\n if (direction === 'up' || direction === 'bidi') {\n throttle.up = [\n { rate: 20000, loss: 0, delay, queue },\n { rate, loss, delay, queue, at: 30 },\n ]\n }\n const throttleDesc = formatThrottleRule({ rate, loss, delay, direction })\n const now = Date.now()\n const ret: Partial<Config>[] = []\n for (let i = 0; i < repeat; i++) {\n const basePath = `logs/${now}-${i + 1}_${id}_${throttleDesc}`\n const sessions = direction === 'bidi' ? '0-1' : direction === 'down' ? '0' : '1'\n ret.push({\n sessions: 2,\n runDuration: 60 * 3,\n debuggingPort: 9000,\n prometheusPushgateway: 'http://localhost:9091',\n prometheusPushgatewayJobName: id,\n statsPath: `${basePath}/stats.csv`,\n detailedStatsPath: `${basePath}/detailed-stats.csv`,\n showPageLog: false,\n showStats: false,\n statsInterval: 5,\n scriptParams: JSON.stringify({\n enableMic: '0-1',\n enableCam: '1',\n }),\n throttleConfig: JSON.stringify([\n {\n sessions,\n protocol: 'udp',\n skipSourcePorts: '53,80,443',\n skipDestinationPorts: '53,80,443',\n ...throttle,\n },\n ]),\n })\n }\n return ret\n}\n"]}