@vpalmisano/webrtcperf 4.4.10 → 4.4.12

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.
@@ -9,3 +9,4 @@ export * from './stats';
9
9
  export * from './utils';
10
10
  export * from './vmaf';
11
11
  export * from './scenarios';
12
+ export * from './plot';
@@ -25,4 +25,5 @@ __exportStar(require("./stats"), exports);
25
25
  __exportStar(require("./utils"), exports);
26
26
  __exportStar(require("./vmaf"), exports);
27
27
  __exportStar(require("./scenarios"), exports);
28
+ __exportStar(require("./plot"), exports);
28
29
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,wCAAqB;AACrB,2CAAwB;AACxB,2CAAwB;AACxB,0CAAuB;AACvB,6CAA0B;AAC1B,2CAAwB;AACxB,4CAAyB;AACzB,0CAAuB;AACvB,0CAAuB;AACvB,yCAAsB;AACtB,8CAA2B","sourcesContent":["export * from './app'\nexport * from './config'\nexport * from './docker'\nexport * from './media'\nexport * from './rtcstats'\nexport * from './server'\nexport * from './session'\nexport * from './stats'\nexport * from './utils'\nexport * from './vmaf'\nexport * from './scenarios'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,wCAAqB;AACrB,2CAAwB;AACxB,2CAAwB;AACxB,0CAAuB;AACvB,6CAA0B;AAC1B,2CAAwB;AACxB,4CAAyB;AACzB,0CAAuB;AACvB,0CAAuB;AACvB,yCAAsB;AACtB,8CAA2B;AAC3B,yCAAsB","sourcesContent":["export * from './app'\nexport * from './config'\nexport * from './docker'\nexport * from './media'\nexport * from './rtcstats'\nexport * from './server'\nexport * from './session'\nexport * from './stats'\nexport * from './utils'\nexport * from './vmaf'\nexport * from './scenarios'\nexport * from './plot'\n"]}
@@ -0,0 +1,13 @@
1
+ export type PlotData = {
2
+ x: (string | number)[];
3
+ y: number[];
4
+ label: string;
5
+ };
6
+ export type PlotOptions = {
7
+ type?: string;
8
+ title?: string;
9
+ filePath?: string;
10
+ min?: number;
11
+ max?: number;
12
+ };
13
+ export declare function plot(data: PlotData, options: PlotOptions): Promise<void>;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.plot = plot;
4
+ const utils_1 = require("./utils");
5
+ const log = (0, utils_1.logger)('webrtcperf:plot');
6
+ async function plot(data, options) {
7
+ const { CategoryScale, Chart, LinearScale, LineController, BarController, LineElement, BarElement, PointElement, Legend, Title,
8
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9
+ } = require('chart.js');
10
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
11
+ const ErrorBarsPlugin = require('chartjs-chart-error-bars');
12
+ Chart.register(CategoryScale, LineController, LineElement, BarController, LinearScale, BarElement, PointElement, Legend, Title, ErrorBarsPlugin);
13
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
14
+ const { Canvas } = require('skia-canvas');
15
+ log.debug('plot');
16
+ const canvas = new Canvas(1280, 720);
17
+ const chart = new Chart(canvas, {
18
+ type: options.type || 'line',
19
+ data: {
20
+ labels: data.x,
21
+ datasets: [
22
+ {
23
+ label: data.label,
24
+ data: data.y,
25
+ fill: false,
26
+ borderColor: 'rgb(0, 0, 0)',
27
+ borderWidth: 1,
28
+ pointRadius: 0,
29
+ },
30
+ ],
31
+ },
32
+ options: {
33
+ plugins: {
34
+ title: {
35
+ display: !!options.title,
36
+ text: options.title || '',
37
+ },
38
+ },
39
+ scales: {
40
+ y: {
41
+ min: options.min,
42
+ max: options.max,
43
+ },
44
+ },
45
+ },
46
+ });
47
+ await canvas.toFile(options.filePath || 'plot.png', { format: 'png', matte: 'white' });
48
+ chart.destroy();
49
+ }
50
+ //# sourceMappingURL=plot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plot.js","sourceRoot":"","sources":["../../src/plot.ts"],"names":[],"mappings":";;AAkBA,oBAkEC;AApFD,mCAAgC;AAEhC,MAAM,GAAG,GAAG,IAAA,cAAM,EAAC,iBAAiB,CAAC,CAAA;AAgB9B,KAAK,UAAU,IAAI,CAAC,IAAc,EAAE,OAAoB;IAC7D,MAAM,EACJ,aAAa,EACb,KAAK,EACL,WAAW,EACX,cAAc,EACd,aAAa,EACb,WAAW,EACX,UAAU,EACV,YAAY,EACZ,MAAM,EACN,KAAK;IACL,iEAAiE;MAClE,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IACvB,iEAAiE;IACjE,MAAM,eAAe,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAA;IAC3D,KAAK,CAAC,QAAQ,CACZ,aAAa,EACb,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,UAAU,EACV,YAAY,EACZ,MAAM,EACN,KAAK,EACL,eAAe,CAChB,CAAA;IACD,iEAAiE;IACjE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;IAEzC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IAEjB,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IACpC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,EAAE;QAC9B,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,MAAM;QAC5B,IAAI,EAAE;YACJ,MAAM,EAAE,IAAI,CAAC,CAAC;YACd,QAAQ,EAAE;gBACR;oBACE,KAAK,EAAE,IAAI,CAAC,KAAK;oBACjB,IAAI,EAAE,IAAI,CAAC,CAAC;oBACZ,IAAI,EAAE,KAAK;oBACX,WAAW,EAAE,cAAc;oBAC3B,WAAW,EAAE,CAAC;oBACd,WAAW,EAAE,CAAC;iBACf;aACF;SACF;QACD,OAAO,EAAE;YACP,OAAO,EAAE;gBACP,KAAK,EAAE;oBACL,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK;oBACxB,IAAI,EAAE,OAAO,CAAC,KAAK,IAAI,EAAE;iBAC1B;aACF;YACD,MAAM,EAAE;gBACN,CAAC,EAAE;oBACD,GAAG,EAAE,OAAO,CAAC,GAAG;oBAChB,GAAG,EAAE,OAAO,CAAC,GAAG;iBACjB;aACF;SACF;KACF,CAAC,CAAA;IACF,MAAM,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,IAAI,UAAU,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAA;IACtF,KAAK,CAAC,OAAO,EAAE,CAAA;AACjB,CAAC","sourcesContent":["import { logger } from './utils'\n\nconst log = logger('webrtcperf:plot')\n\nexport type PlotData = {\n x: (string | number)[]\n y: number[]\n label: string\n}\n\nexport type PlotOptions = {\n type?: string\n title?: string\n filePath?: string\n min?: number\n max?: number\n}\n\nexport async function plot(data: PlotData, options: PlotOptions) {\n const {\n CategoryScale,\n Chart,\n LinearScale,\n LineController,\n BarController,\n LineElement,\n BarElement,\n PointElement,\n Legend,\n Title,\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n } = require('chart.js')\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const ErrorBarsPlugin = require('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 ErrorBarsPlugin,\n )\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { Canvas } = require('skia-canvas')\n\n log.debug('plot')\n\n const canvas = new Canvas(1280, 720)\n const chart = new Chart(canvas, {\n type: options.type || 'line',\n data: {\n labels: data.x,\n datasets: [\n {\n label: data.label,\n data: data.y,\n fill: false,\n borderColor: 'rgb(0, 0, 0)',\n borderWidth: 1,\n pointRadius: 0,\n },\n ],\n },\n options: {\n plugins: {\n title: {\n display: !!options.title,\n text: options.title || '',\n },\n },\n scales: {\n y: {\n min: options.min,\n max: options.max,\n },\n },\n },\n })\n await canvas.toFile(options.filePath || 'plot.png', { format: 'png', matte: 'white' })\n chart.destroy()\n}\n"]}
@@ -1,6 +1,27 @@
1
1
  import { FastStats } from './stats';
2
2
  import { ThrottleRule } from '@vpalmisano/throttler';
3
+ /**
4
+ * It parses a CSV stats file and returns an array of objects representing each row.
5
+ * @param filePath The path to the CSV stats file.
6
+ * @returns An array of objects where each object represents a row in the CSV file with keys as column headers.
7
+ */
3
8
  export declare function parseStatsFile(filePath: string): Promise<Record<string, string | number>[]>;
9
+ export type StatsSummary = {
10
+ timestamp: number;
11
+ id: string;
12
+ scenario: string;
13
+ videoRecvBitratePerPixel: FastStats;
14
+ videoRecvFps: FastStats;
15
+ videoSentFps: FastStats;
16
+ };
17
+ /**
18
+ * It aggregates the stats summary from multiple test runs in a directory.
19
+ * @param options.dirPath Directory path containing test run subdirectories. Default is 'logs'.
20
+ * @param options.senderParticipantName Participant name of the sender. Default is 'Participant-000001'.
21
+ * @param options.receiverParticipantName Participant name of the receiver. Default is 'Participant-000000'.
22
+ * @param options.nameParser Function to parse test directory names. Default splits by '_' and extracts id and scenario.
23
+ * @returns Array of aggregated stats including timestamp, id, scenario, videoRecvBitratePerPixel, videoRecvFps, and videoSentFps.
24
+ */
4
25
  export declare function aggregateStatsSummary({ dirPath, senderParticipantName, receiverParticipantName, nameParser, }: {
5
26
  dirPath?: string | undefined;
6
27
  senderParticipantName?: string | undefined;
@@ -9,23 +30,48 @@ export declare function aggregateStatsSummary({ dirPath, senderParticipantName,
9
30
  id: string;
10
31
  scenario: string;
11
32
  }) | undefined;
12
- }): Promise<{
13
- timestamp: number;
14
- id: string;
15
- scenario: string;
16
- videoRecvBitratePerPixel: FastStats;
17
- videoRecvFps: FastStats;
18
- videoSentFps: FastStats;
19
- }[]>;
33
+ }): Promise<StatsSummary[]>;
34
+ /**
35
+ * It uploads the aggregated stats to a Google Sheet.
36
+ * A valid Google service account credentials file must be specified
37
+ * in the `GOOGLE_CREDENTIALS_PATH` environment variable.
38
+ * @param stats The aggregated stats to upload.
39
+ * @param spreadsheetId The ID of the Google Spreadsheet.
40
+ * @param table The name of the table (sheet) within the spreadsheet. Default is 'data'.
41
+ */
42
+ export declare function uploadStatsToGoogleSheet(stats: StatsSummary[], spreadsheetId: string, table?: string): Promise<void>;
20
43
  export type ThrottleDirection = 'up' | 'down' | 'bidi';
21
- export declare function formatThrottleRule(throttleRule: ThrottleRule, direction: ThrottleDirection): string;
44
+ export declare function formatBitrate(bitrate: number | undefined, prefix?: string): string;
45
+ export declare function formatLoss(loss: number | undefined, prefix?: string): string;
46
+ export declare function formatDelay(delay: number | undefined, prefix?: string): string;
47
+ export declare function formatThrottleRule(throttleRule: ThrottleRule & {
48
+ direction: ThrottleDirection;
49
+ }, human?: boolean): string;
22
50
  export declare function parseThrottleRule(throttleDesc: string): {
23
51
  direction: ThrottleDirection;
24
52
  rate: number;
25
53
  loss: number;
26
54
  delay: number;
27
55
  };
28
- export declare function simpleTestWithRateLossDelay(id: string, { rate, loss, delay, direction }: {
56
+ /**
57
+ * It generates a test configuration with a scenario including 2 participants.
58
+ * The first participant sends video and the second receives it.
59
+ * Both participants send and receive audio.
60
+ * The network conditions are applied according to the specified direction to the sender (`up`),
61
+ * the receiver (`down`) or both (`bidi`).
62
+ * The test is repeated the specified number of times.
63
+ * The output is an array of partial configuration objects that can be used to run the tests
64
+ * with the main application, after merging it with a configuration that includes
65
+ * the destination url (mandatory) and other optional parameters.
66
+ * @param id The unique identifier for the test scenario.
67
+ * @param options.rate The target bandwidth in kbps.
68
+ * @param options.loss The packet loss percentage.
69
+ * @param options.delay The network delay in milliseconds.
70
+ * @param options.direction The direction of the network throttling: 'up', 'down', or 'bidi'.
71
+ * @param repeat The number of times to repeat the test scenario. Default is 1.
72
+ * @returns An array of partial configuration objects for each test scenario.
73
+ */
74
+ export declare function twoParticipantsWithRateLossDelay(id: string, { rate, loss, delay, direction }: {
29
75
  rate: number;
30
76
  loss: number;
31
77
  delay: number;
@@ -5,13 +5,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.parseStatsFile = parseStatsFile;
7
7
  exports.aggregateStatsSummary = aggregateStatsSummary;
8
+ exports.uploadStatsToGoogleSheet = uploadStatsToGoogleSheet;
9
+ exports.formatBitrate = formatBitrate;
10
+ exports.formatLoss = formatLoss;
11
+ exports.formatDelay = formatDelay;
8
12
  exports.formatThrottleRule = formatThrottleRule;
9
13
  exports.parseThrottleRule = parseThrottleRule;
10
- exports.simpleTestWithRateLossDelay = simpleTestWithRateLossDelay;
14
+ exports.twoParticipantsWithRateLossDelay = twoParticipantsWithRateLossDelay;
11
15
  const fs_1 = __importDefault(require("fs"));
12
16
  const path_1 = __importDefault(require("path"));
13
17
  const stats_1 = require("./stats");
18
+ const googleapis_1 = require("googleapis");
19
+ const utils_1 = require("./utils");
20
+ const sprintf_js_1 = require("sprintf-js");
21
+ const log = (0, utils_1.logger)('webrtcperf:scenarios');
22
+ /**
23
+ * It parses a CSV stats file and returns an array of objects representing each row.
24
+ * @param filePath The path to the CSV stats file.
25
+ * @returns An array of objects where each object represents a row in the CSV file with keys as column headers.
26
+ */
14
27
  async function parseStatsFile(filePath) {
28
+ log.debug(`parseStatsFile: ${filePath}`);
15
29
  const fileData = await fs_1.default.promises.readFile(filePath, 'utf-8');
16
30
  const lines = fileData.split('\n');
17
31
  const headers = lines[0].split(',');
@@ -23,10 +37,19 @@ async function parseStatsFile(filePath) {
23
37
  }, {}));
24
38
  return data;
25
39
  }
40
+ /**
41
+ * It aggregates the stats summary from multiple test runs in a directory.
42
+ * @param options.dirPath Directory path containing test run subdirectories. Default is 'logs'.
43
+ * @param options.senderParticipantName Participant name of the sender. Default is 'Participant-000001'.
44
+ * @param options.receiverParticipantName Participant name of the receiver. Default is 'Participant-000000'.
45
+ * @param options.nameParser Function to parse test directory names. Default splits by '_' and extracts id and scenario.
46
+ * @returns Array of aggregated stats including timestamp, id, scenario, videoRecvBitratePerPixel, videoRecvFps, and videoSentFps.
47
+ */
26
48
  async function aggregateStatsSummary({ dirPath = 'logs', senderParticipantName = 'Participant-000001', receiverParticipantName = 'Participant-000000', nameParser = (name) => {
27
49
  const [_, id, scenario] = name.split('_');
28
50
  return { id, scenario };
29
51
  }, }) {
52
+ log.debug(`aggregateStatsSummary: ${dirPath}`);
30
53
  const stats = [];
31
54
  const results = await fs_1.default.promises.readdir(dirPath);
32
55
  for (const test of results) {
@@ -67,9 +90,84 @@ async function aggregateStatsSummary({ dirPath = 'logs', senderParticipantName =
67
90
  }
68
91
  return stats.sort((a, b) => a.timestamp - b.timestamp);
69
92
  }
70
- function formatThrottleRule(throttleRule, direction) {
71
- const { rate, loss, delay } = throttleRule;
72
- return `${direction}-r${rate}-l${loss}-d${delay}`;
93
+ /**
94
+ * It uploads the aggregated stats to a Google Sheet.
95
+ * A valid Google service account credentials file must be specified
96
+ * in the `GOOGLE_CREDENTIALS_PATH` environment variable.
97
+ * @param stats The aggregated stats to upload.
98
+ * @param spreadsheetId The ID of the Google Spreadsheet.
99
+ * @param table The name of the table (sheet) within the spreadsheet. Default is 'data'.
100
+ */
101
+ async function uploadStatsToGoogleSheet(stats, spreadsheetId, table = 'data') {
102
+ log.debug(`uploadResultsToGoogleSheet spreadsheetId: ${spreadsheetId} table: ${table}`);
103
+ if (!process.env.GOOGLE_CREDENTIALS_PATH)
104
+ throw new Error('GOOGLE_CREDENTIALS_PATH environment variable is not set');
105
+ if (!fs_1.default.existsSync(process.env.GOOGLE_CREDENTIALS_PATH))
106
+ throw new Error(`Google credentials file not found: ${process.env.GOOGLE_CREDENTIALS_PATH}`);
107
+ if (!stats.length)
108
+ return;
109
+ const auth = new googleapis_1.Auth.GoogleAuth({
110
+ keyFile: process.env.GOOGLE_CREDENTIALS_PATH,
111
+ scopes: ['https://www.googleapis.com/auth/spreadsheets'],
112
+ });
113
+ const sheets = googleapis_1.google.sheets({ version: 'v4', auth });
114
+ // Update headers.
115
+ const headers = ['datetime', 'id', 'scenario', 'videoRecvBitratePerPixel', 'videoRecvFps'];
116
+ await sheets.spreadsheets.values.update({
117
+ spreadsheetId,
118
+ range: `${table}!A1:E1`,
119
+ valueInputOption: 'USER_ENTERED',
120
+ requestBody: { majorDimension: 'ROWS', values: [headers] },
121
+ });
122
+ // Append values.
123
+ const values = [];
124
+ stats.forEach(s => {
125
+ const { timestamp, id, scenario, videoRecvBitratePerPixel, videoRecvFps } = s;
126
+ if (!videoRecvBitratePerPixel.length)
127
+ return;
128
+ const datetime = new Date(timestamp).toLocaleString('en-US', {
129
+ timeZone: 'UTC',
130
+ hourCycle: 'h23',
131
+ });
132
+ values.push([
133
+ datetime,
134
+ id,
135
+ formatThrottleRule(parseThrottleRule(scenario), true),
136
+ videoRecvBitratePerPixel.percentile(95).toFixed(3),
137
+ videoRecvFps.percentile(95).toFixed(3),
138
+ ]);
139
+ });
140
+ if (values.length) {
141
+ await sheets.spreadsheets.values.append({
142
+ spreadsheetId,
143
+ range: `${table}!A:E`,
144
+ valueInputOption: 'USER_ENTERED',
145
+ insertDataOption: 'INSERT_ROWS',
146
+ requestBody: { majorDimension: 'ROWS', values },
147
+ });
148
+ }
149
+ }
150
+ function formatBitrate(bitrate, prefix = ' ') {
151
+ if (bitrate === undefined)
152
+ return '';
153
+ let suffix = 'Kbps';
154
+ if (bitrate >= 1000) {
155
+ bitrate /= 1000;
156
+ suffix = 'Mbps';
157
+ }
158
+ return `${prefix}${(0, sprintf_js_1.sprintf)('%5.4g', bitrate)}${suffix}`;
159
+ }
160
+ function formatLoss(loss, prefix = ' ') {
161
+ return loss !== undefined ? `${prefix}${loss.toFixed(0).padStart(2, ' ')}%` : '';
162
+ }
163
+ function formatDelay(delay, prefix = ' ') {
164
+ return delay !== undefined ? `${prefix}${delay.toFixed(0).padStart(3, ' ')}ms` : '';
165
+ }
166
+ function formatThrottleRule(throttleRule, human = false) {
167
+ const { rate, loss, delay, direction } = throttleRule;
168
+ return human
169
+ ? `${direction.padEnd(4, ' ')}${formatBitrate(rate)}${formatLoss(loss)}${formatDelay(delay)}`
170
+ : `${direction}-r${rate}-l${loss}-d${delay}`;
73
171
  }
74
172
  function parseThrottleRule(throttleDesc) {
75
173
  const match = throttleDesc.match(/(up|down|bidi)-r(\d+)-l([\d.]+)-d(\d+)/);
@@ -81,7 +179,25 @@ function parseThrottleRule(throttleDesc) {
81
179
  const delay = parseInt(match[4]);
82
180
  return { direction, rate, loss, delay };
83
181
  }
84
- async function simpleTestWithRateLossDelay(id, { rate, loss, delay, direction }, repeat) {
182
+ /**
183
+ * It generates a test configuration with a scenario including 2 participants.
184
+ * The first participant sends video and the second receives it.
185
+ * Both participants send and receive audio.
186
+ * The network conditions are applied according to the specified direction to the sender (`up`),
187
+ * the receiver (`down`) or both (`bidi`).
188
+ * The test is repeated the specified number of times.
189
+ * The output is an array of partial configuration objects that can be used to run the tests
190
+ * with the main application, after merging it with a configuration that includes
191
+ * the destination url (mandatory) and other optional parameters.
192
+ * @param id The unique identifier for the test scenario.
193
+ * @param options.rate The target bandwidth in kbps.
194
+ * @param options.loss The packet loss percentage.
195
+ * @param options.delay The network delay in milliseconds.
196
+ * @param options.direction The direction of the network throttling: 'up', 'down', or 'bidi'.
197
+ * @param repeat The number of times to repeat the test scenario. Default is 1.
198
+ * @returns An array of partial configuration objects for each test scenario.
199
+ */
200
+ async function twoParticipantsWithRateLossDelay(id, { rate, loss, delay, direction }, repeat) {
85
201
  const throttle = {};
86
202
  const queue = 25;
87
203
  if (direction === 'down' || direction === 'bidi') {
@@ -96,7 +212,7 @@ async function simpleTestWithRateLossDelay(id, { rate, loss, delay, direction },
96
212
  { rate, loss, delay, queue, at: 30 },
97
213
  ];
98
214
  }
99
- const throttleDesc = formatThrottleRule({ rate, loss, delay }, direction);
215
+ const throttleDesc = formatThrottleRule({ rate, loss, delay, direction });
100
216
  const now = Date.now();
101
217
  const ret = [];
102
218
  for (let i = 0; i < repeat; i++) {
@@ -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'\n\nexport async function parseStatsFile(filePath: string) {\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 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 const stats = [] as {\n timestamp: number\n id: string\n scenario: string\n videoRecvBitratePerPixel: FastStats\n videoRecvFps: FastStats\n videoSentFps: FastStats\n }[]\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\nexport type ThrottleDirection = 'up' | 'down' | 'bidi'\n\nexport function formatThrottleRule(throttleRule: ThrottleRule, direction: ThrottleDirection) {\n const { rate, loss, delay } = throttleRule\n return `${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 simpleTestWithRateLossDelay(\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'\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 = ' ') {\n if (bitrate === undefined) return ''\n let suffix = 'Kbps'\n if (bitrate >= 1000) {\n bitrate /= 1000\n suffix = 'Mbps'\n }\n return `${prefix}${sprintf('%5.4g', bitrate)}${suffix}`\n}\n\nexport function formatLoss(loss: number | undefined, prefix = ' ') {\n return loss !== undefined ? `${prefix}${loss.toFixed(0).padStart(2, ' ')}%` : ''\n}\n\nexport function formatDelay(delay: number | undefined, prefix = ' ') {\n return delay !== undefined ? `${prefix}${delay.toFixed(0).padStart(3, ' ')}ms` : ''\n}\n\nexport function formatThrottleRule(throttleRule: ThrottleRule & { direction: ThrottleDirection }, human = false) {\n const { rate, loss, delay, direction } = throttleRule\n return human\n ? `${direction.padEnd(4, ' ')}${formatBitrate(rate)}${formatLoss(loss)}${formatDelay(delay)}`\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\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"]}