@vpalmisano/webrtcperf 4.4.12 → 4.4.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vpalmisano/webrtcperf",
3
- "version": "4.4.12",
3
+ "version": "4.4.14",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/vpalmisano/webrtcperf.git"
@@ -90,7 +90,7 @@
90
90
  "yaml": "^2.8.1"
91
91
  },
92
92
  "devDependencies": {
93
- "@eslint/js": "^9.35.0",
93
+ "@eslint/js": "^9.36.0",
94
94
  "@types/basic-auth": "^1.1.3",
95
95
  "@types/compression": "^1.8.1",
96
96
  "@types/convict": "^6.1.6",
@@ -109,7 +109,7 @@
109
109
  "@typescript-eslint/parser": "^8.44.0",
110
110
  "@vpalmisano/typedoc-cookie-consent": "^0.0.4",
111
111
  "@vpalmisano/typedoc-plugin-ga": "^1.0.6",
112
- "eslint": "^9.35.0",
112
+ "eslint": "^9.36.0",
113
113
  "eslint-config-prettier": "^10.1.8",
114
114
  "eslint-import-resolver-typescript": "^4.4.4",
115
115
  "eslint-plugin-import": "^2.32.0",
package/src/plot.ts CHANGED
@@ -1,22 +1,100 @@
1
+ import fs from 'fs'
2
+
1
3
  import { logger } from './utils'
4
+ import json5 from 'json5'
2
5
 
3
6
  const log = logger('webrtcperf:plot')
4
7
 
5
- export type PlotData = {
6
- x: (string | number)[]
7
- y: number[]
8
- label: string
9
- }
10
-
11
8
  export type PlotOptions = {
12
9
  type?: string
13
10
  title?: string
11
+ xLabel?: string
12
+ yLabel?: string
13
+ yMin?: number
14
+ yMax?: number
15
+ labels?: (string | number)[]
14
16
  filePath?: string
15
- min?: number
16
- max?: number
17
17
  }
18
18
 
19
- export async function plot(data: PlotData, options: PlotOptions) {
19
+ export type PlotData = {
20
+ label: string
21
+ data: { x: number | string; y: number; yMin?: number; yMax?: number }[]
22
+ }
23
+
24
+ const SERIES_COLORS = [
25
+ 'rgba(33, 150, 243, 1)',
26
+ 'rgba(244, 67, 54, 1)',
27
+ 'rgba(76, 175, 80, 1)',
28
+ 'rgba(255, 193, 7, 1)',
29
+ 'rgba(156, 39, 176, 1)',
30
+ ]
31
+
32
+ export function plotConfig(options: PlotOptions, series: PlotData[]) {
33
+ log.debug('plotConfig')
34
+
35
+ return {
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ type: (options.type || 'line') as any,
38
+ options: {
39
+ plugins: {
40
+ title: options.title
41
+ ? {
42
+ display: true,
43
+ text: options.title,
44
+ }
45
+ : undefined,
46
+ zoom: {
47
+ pan: {
48
+ enabled: true,
49
+ mode: 'x',
50
+ modifierKey: 'ctrl',
51
+ },
52
+ zoom: {
53
+ drag: {
54
+ enabled: true,
55
+ },
56
+ mode: 'x',
57
+ },
58
+ },
59
+ },
60
+ scales: {
61
+ x: {
62
+ type: 'category',
63
+ title: options.xLabel
64
+ ? {
65
+ display: true,
66
+ text: options.xLabel,
67
+ }
68
+ : undefined,
69
+ },
70
+ y: {
71
+ type: 'linear',
72
+ title: options.yLabel
73
+ ? {
74
+ display: true,
75
+ text: options.yLabel,
76
+ }
77
+ : undefined,
78
+ min: options.yMin,
79
+ max: options.yMax,
80
+ },
81
+ },
82
+ },
83
+ data: {
84
+ labels: options.labels,
85
+ datasets: series.map((s, i) => ({
86
+ fill: false,
87
+ backgroundColor: SERIES_COLORS[i % SERIES_COLORS.length],
88
+ borderColor: SERIES_COLORS[i % SERIES_COLORS.length],
89
+ borderWidth: 1,
90
+ pointRadius: 0,
91
+ ...s,
92
+ })),
93
+ },
94
+ }
95
+ }
96
+
97
+ export async function plot(options: PlotOptions, series: PlotData[]) {
20
98
  const {
21
99
  CategoryScale,
22
100
  Chart,
@@ -28,10 +106,8 @@ export async function plot(data: PlotData, options: PlotOptions) {
28
106
  PointElement,
29
107
  Legend,
30
108
  Title,
31
- // eslint-disable-next-line @typescript-eslint/no-require-imports
32
- } = require('chart.js')
33
- // eslint-disable-next-line @typescript-eslint/no-require-imports
34
- const ErrorBarsPlugin = require('chartjs-chart-error-bars')
109
+ } = await import('chart.js')
110
+ const { BarWithErrorBar, BarWithErrorBarsController } = await import('chartjs-chart-error-bars')
35
111
  Chart.register(
36
112
  CategoryScale,
37
113
  LineController,
@@ -42,44 +118,46 @@ export async function plot(data: PlotData, options: PlotOptions) {
42
118
  PointElement,
43
119
  Legend,
44
120
  Title,
45
- ErrorBarsPlugin,
121
+ BarWithErrorBar,
122
+ BarWithErrorBarsController,
46
123
  )
47
- // eslint-disable-next-line @typescript-eslint/no-require-imports
48
- const { Canvas } = require('skia-canvas')
124
+ const { Canvas } = await import('skia-canvas')
49
125
 
50
126
  log.debug('plot')
51
-
127
+ const config = plotConfig(options, series)
52
128
  const canvas = new Canvas(1280, 720)
53
- const chart = new Chart(canvas, {
54
- type: options.type || 'line',
55
- data: {
56
- labels: data.x,
57
- datasets: [
58
- {
59
- label: data.label,
60
- data: data.y,
61
- fill: false,
62
- borderColor: 'rgb(0, 0, 0)',
63
- borderWidth: 1,
64
- pointRadius: 0,
65
- },
66
- ],
67
- },
68
- options: {
69
- plugins: {
70
- title: {
71
- display: !!options.title,
72
- text: options.title || '',
73
- },
74
- },
75
- scales: {
76
- y: {
77
- min: options.min,
78
- max: options.max,
79
- },
80
- },
81
- },
82
- })
129
+ const chart = new Chart(canvas as unknown as HTMLCanvasElement, config)
83
130
  await canvas.toFile(options.filePath || 'plot.png', { format: 'png', matte: 'white' })
84
131
  chart.destroy()
85
132
  }
133
+
134
+ export async function plotHtml(options: PlotOptions, series: PlotData[]) {
135
+ log.debug('plotHtml')
136
+ const config = plotConfig(options, series)
137
+
138
+ const data = `
139
+ <!DOCTYPE html>
140
+ <html lang="en">
141
+ <head>
142
+ <meta charset="UTF-8">
143
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
144
+ <title>${options.title || 'Plot'}</title>
145
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
146
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-chart-error-bars"></script>
147
+ <script src="https://cdn.jsdelivr.net/npm/hammerjs"></script>
148
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom"></script>
149
+ </head>
150
+ <body>
151
+ <div>
152
+ <canvas id="chart"></canvas>
153
+ </div>
154
+ <script>
155
+ const ctx = document.getElementById('chart');
156
+ const chart = new Chart(ctx, ${json5.stringify(config)});
157
+ chart.options.onClick = e => e.chart.resetZoom();
158
+ addEventListener('resize', () => chart.resize());
159
+ </script>
160
+ </body>
161
+ </html>`
162
+ await fs.promises.writeFile(options.filePath || 'plot.html', data)
163
+ }
package/src/scenarios.ts CHANGED
@@ -6,6 +6,7 @@ import { Config } from './config'
6
6
  import { Auth, google } from 'googleapis'
7
7
  import { logger } from './utils'
8
8
  import { sprintf } from 'sprintf-js'
9
+ import { PlotData, plotHtml } from './plot'
9
10
 
10
11
  const log = logger('webrtcperf:scenarios')
11
12
 
@@ -155,28 +156,32 @@ export async function uploadStatsToGoogleSheet(stats: StatsSummary[], spreadshee
155
156
 
156
157
  export type ThrottleDirection = 'up' | 'down' | 'bidi'
157
158
 
158
- export function formatBitrate(bitrate: number | undefined, prefix = ' ') {
159
+ export function formatBitrate(bitrate: number | undefined, prefix = ' ', pad = true) {
159
160
  if (bitrate === undefined) return ''
160
161
  let suffix = 'Kbps'
161
162
  if (bitrate >= 1000) {
162
163
  bitrate /= 1000
163
164
  suffix = 'Mbps'
164
165
  }
165
- return `${prefix}${sprintf('%5.4g', bitrate)}${suffix}`
166
+ return `${prefix}${sprintf(`%${pad ? '5' : ''}.4g`, bitrate)}${suffix}`
166
167
  }
167
168
 
168
- export function formatLoss(loss: number | undefined, prefix = ' ') {
169
- return loss !== undefined ? `${prefix}${loss.toFixed(0).padStart(2, ' ')}%` : ''
169
+ export function formatLoss(loss: number | undefined, prefix = ' ', pad = true) {
170
+ return loss !== undefined ? `${prefix}${loss.toFixed(0).padStart(pad ? 2 : 0, ' ')}%` : ''
170
171
  }
171
172
 
172
- export function formatDelay(delay: number | undefined, prefix = ' ') {
173
- return delay !== undefined ? `${prefix}${delay.toFixed(0).padStart(3, ' ')}ms` : ''
173
+ export function formatDelay(delay: number | undefined, prefix = ' ', pad = true) {
174
+ return delay !== undefined ? `${prefix}${delay.toFixed(0).padStart(pad ? 3 : 0, ' ')}ms` : ''
174
175
  }
175
176
 
176
- export function formatThrottleRule(throttleRule: ThrottleRule & { direction: ThrottleDirection }, human = false) {
177
+ export function formatThrottleRule(
178
+ throttleRule: ThrottleRule & { direction: ThrottleDirection },
179
+ human = false,
180
+ pad = true,
181
+ ) {
177
182
  const { rate, loss, delay, direction } = throttleRule
178
183
  return human
179
- ? `${direction.padEnd(4, ' ')}${formatBitrate(rate)}${formatLoss(loss)}${formatDelay(delay)}`
184
+ ? `${direction.padEnd(pad ? 4 : 0, ' ')}${formatBitrate(rate, ' ', pad)}${formatLoss(loss, ' ', pad)}${formatDelay(delay, ' ', pad)}`
180
185
  : `${direction}-r${rate}-l${loss}-d${delay}`
181
186
  }
182
187
 
@@ -190,6 +195,50 @@ export function parseThrottleRule(throttleDesc: string) {
190
195
  return { direction, rate, loss, delay }
191
196
  }
192
197
 
198
+ export async function plotStatsSummary(stats: StatsSummary[]) {
199
+ const labels = new Set<string>()
200
+ const data = {} as Record<string, Record<string, FastStats>>
201
+ stats.forEach(s => {
202
+ const { id, scenario, videoRecvBitratePerPixel } = s
203
+ if (!videoRecvBitratePerPixel.length) return null
204
+ if (!data[id]) {
205
+ data[id] = {}
206
+ }
207
+ const scenarioFormatted = formatThrottleRule(parseThrottleRule(scenario), true, true)
208
+ if (!data[id][scenarioFormatted]) {
209
+ data[id][scenarioFormatted] = new FastStats()
210
+ }
211
+ labels.add(scenarioFormatted)
212
+ data[id][scenarioFormatted].push(videoRecvBitratePerPixel.percentile(95))
213
+ })
214
+ const series: PlotData[] = []
215
+ Object.entries(data)
216
+ .sort((a, b) => a[0].localeCompare(b[0]))
217
+ .forEach(([id, data]) => {
218
+ const plotData: PlotData = { label: id, data: [] }
219
+ Object.entries(data)
220
+ .sort((a, b) => a[0].localeCompare(b[0]))
221
+ .forEach(([scenario, stats]) => {
222
+ plotData.data.push({
223
+ x: scenario,
224
+ y: stats.percentile(50),
225
+ yMin: stats.percentile(5),
226
+ yMax: stats.percentile(95),
227
+ })
228
+ })
229
+ series.push(plotData)
230
+ })
231
+ await plotHtml(
232
+ {
233
+ type: 'barWithErrorBars',
234
+ xLabel: 'Scenario',
235
+ yLabel: 'Video Receive Bitrate per Pixel',
236
+ labels: Array.from(labels).sort((a, b) => a.localeCompare(b)),
237
+ },
238
+ series,
239
+ )
240
+ }
241
+
193
242
  /**
194
243
  * It generates a test configuration with a scenario including 2 participants.
195
244
  * The first participant sends video and the second receives it.