@vpalmisano/webrtcperf 4.4.11 → 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/index.d.ts +1 -0
- package/build/src/index.js +1 -0
- package/build/src/index.js.map +1 -1
- package/build/src/plot.d.ts +80 -0
- package/build/src/plot.js +124 -0
- package/build/src/plot.js.map +1 -0
- package/build/src/scenarios.d.ts +5 -1
- package/build/src/scenarios.js +56 -9
- package/build/src/scenarios.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/src/index.ts +1 -0
- package/src/plot.ts +163 -0
- package/src/scenarios.ts +59 -9
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",
|
|
@@ -130,7 +130,8 @@
|
|
|
130
130
|
},
|
|
131
131
|
"optionalDependencies": {
|
|
132
132
|
"chart.js": "^4.5.0",
|
|
133
|
-
"
|
|
133
|
+
"chartjs-chart-error-bars": "^4.4.4",
|
|
134
|
+
"skia-canvas": "^3.0.7",
|
|
134
135
|
"zeromq": "^6.5.0"
|
|
135
136
|
}
|
|
136
137
|
}
|
package/src/index.ts
CHANGED
package/src/plot.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
|
|
3
|
+
import { logger } from './utils'
|
|
4
|
+
import json5 from 'json5'
|
|
5
|
+
|
|
6
|
+
const log = logger('webrtcperf:plot')
|
|
7
|
+
|
|
8
|
+
export type PlotOptions = {
|
|
9
|
+
type?: string
|
|
10
|
+
title?: string
|
|
11
|
+
xLabel?: string
|
|
12
|
+
yLabel?: string
|
|
13
|
+
yMin?: number
|
|
14
|
+
yMax?: number
|
|
15
|
+
labels?: (string | number)[]
|
|
16
|
+
filePath?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
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[]) {
|
|
98
|
+
const {
|
|
99
|
+
CategoryScale,
|
|
100
|
+
Chart,
|
|
101
|
+
LinearScale,
|
|
102
|
+
LineController,
|
|
103
|
+
BarController,
|
|
104
|
+
LineElement,
|
|
105
|
+
BarElement,
|
|
106
|
+
PointElement,
|
|
107
|
+
Legend,
|
|
108
|
+
Title,
|
|
109
|
+
} = await import('chart.js')
|
|
110
|
+
const { BarWithErrorBar, BarWithErrorBarsController } = await import('chartjs-chart-error-bars')
|
|
111
|
+
Chart.register(
|
|
112
|
+
CategoryScale,
|
|
113
|
+
LineController,
|
|
114
|
+
LineElement,
|
|
115
|
+
BarController,
|
|
116
|
+
LinearScale,
|
|
117
|
+
BarElement,
|
|
118
|
+
PointElement,
|
|
119
|
+
Legend,
|
|
120
|
+
Title,
|
|
121
|
+
BarWithErrorBar,
|
|
122
|
+
BarWithErrorBarsController,
|
|
123
|
+
)
|
|
124
|
+
const { Canvas } = await import('skia-canvas')
|
|
125
|
+
|
|
126
|
+
log.debug('plot')
|
|
127
|
+
const config = plotConfig(options, series)
|
|
128
|
+
const canvas = new Canvas(1280, 720)
|
|
129
|
+
const chart = new Chart(canvas as unknown as HTMLCanvasElement, config)
|
|
130
|
+
await canvas.toFile(options.filePath || 'plot.png', { format: 'png', matte: 'white' })
|
|
131
|
+
chart.destroy()
|
|
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
|
@@ -5,6 +5,8 @@ import { ThrottleConfig, ThrottleRule } from '@vpalmisano/throttler'
|
|
|
5
5
|
import { Config } from './config'
|
|
6
6
|
import { Auth, google } from 'googleapis'
|
|
7
7
|
import { logger } from './utils'
|
|
8
|
+
import { sprintf } from 'sprintf-js'
|
|
9
|
+
import { PlotData, plotHtml } from './plot'
|
|
8
10
|
|
|
9
11
|
const log = logger('webrtcperf:scenarios')
|
|
10
12
|
|
|
@@ -154,28 +156,32 @@ export async function uploadStatsToGoogleSheet(stats: StatsSummary[], spreadshee
|
|
|
154
156
|
|
|
155
157
|
export type ThrottleDirection = 'up' | 'down' | 'bidi'
|
|
156
158
|
|
|
157
|
-
function formatBitrate(bitrate: number | undefined, prefix = ' ') {
|
|
159
|
+
export function formatBitrate(bitrate: number | undefined, prefix = ' ', pad = true) {
|
|
158
160
|
if (bitrate === undefined) return ''
|
|
159
161
|
let suffix = 'Kbps'
|
|
160
|
-
if (bitrate >=
|
|
162
|
+
if (bitrate >= 1000) {
|
|
161
163
|
bitrate /= 1000
|
|
162
164
|
suffix = 'Mbps'
|
|
163
165
|
}
|
|
164
|
-
return `${prefix}${bitrate
|
|
166
|
+
return `${prefix}${sprintf(`%${pad ? '5' : ''}.4g`, bitrate)}${suffix}`
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
function formatLoss(loss: number | undefined, prefix = ' ') {
|
|
168
|
-
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, ' ')}%` : ''
|
|
169
171
|
}
|
|
170
172
|
|
|
171
|
-
function formatDelay(delay: number | undefined, prefix = ' ') {
|
|
172
|
-
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` : ''
|
|
173
175
|
}
|
|
174
176
|
|
|
175
|
-
export function formatThrottleRule(
|
|
177
|
+
export function formatThrottleRule(
|
|
178
|
+
throttleRule: ThrottleRule & { direction: ThrottleDirection },
|
|
179
|
+
human = false,
|
|
180
|
+
pad = true,
|
|
181
|
+
) {
|
|
176
182
|
const { rate, loss, delay, direction } = throttleRule
|
|
177
183
|
return human
|
|
178
|
-
? `${direction.padEnd(4, ' ')}${formatBitrate(rate)}${formatLoss(loss)}${formatDelay(delay)}`
|
|
184
|
+
? `${direction.padEnd(pad ? 4 : 0, ' ')}${formatBitrate(rate, ' ', pad)}${formatLoss(loss, ' ', pad)}${formatDelay(delay, ' ', pad)}`
|
|
179
185
|
: `${direction}-r${rate}-l${loss}-d${delay}`
|
|
180
186
|
}
|
|
181
187
|
|
|
@@ -189,6 +195,50 @@ export function parseThrottleRule(throttleDesc: string) {
|
|
|
189
195
|
return { direction, rate, loss, delay }
|
|
190
196
|
}
|
|
191
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
|
+
|
|
192
242
|
/**
|
|
193
243
|
* It generates a test configuration with a scenario including 2 participants.
|
|
194
244
|
* The first participant sends video and the second receives it.
|