@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/build/src/plot.d.ts +75 -8
- package/build/src/plot.js +106 -32
- package/build/src/plot.js.map +1 -1
- package/build/src/scenarios.d.ts +5 -4
- package/build/src/scenarios.js +51 -8
- package/build/src/scenarios.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/plot.ts +125 -47
- package/src/scenarios.ts +57 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vpalmisano/webrtcperf",
|
|
3
|
-
"version": "4.4.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
32
|
-
} =
|
|
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
|
-
|
|
121
|
+
BarWithErrorBar,
|
|
122
|
+
BarWithErrorBarsController,
|
|
46
123
|
)
|
|
47
|
-
|
|
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('
|
|
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(
|
|
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.
|