@vpalmisano/webrtcperf 4.4.8 → 4.4.10

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/src/docker.ts CHANGED
@@ -1,48 +1,51 @@
1
- import path from 'path'
2
1
  import Docker from 'dockerode'
3
2
  import { logger, resolvePackagePath } from './utils'
4
3
  import { loadConfig } from './config'
4
+ import { runShellCommand } from '@vpalmisano/throttler'
5
+ import os from 'os'
5
6
  import fs from 'fs'
6
7
 
7
8
  const log = logger('webrtcperf:docker')
8
9
 
9
10
  export async function runWithDocker(argv: string[]) {
10
11
  const docker = new Docker()
11
- const configPath = argv.filter(s => s !== '--docker')[0]
12
+ const configPath = argv[0]
12
13
  if (!configPath) throw new Error('No configuration file specified')
13
- const configName = path.basename(configPath)
14
- const config = (await loadConfig(configPath))[0]
14
+ const configs = await loadConfig(configPath)
15
+ if (!configs.length) throw new Error('Failed to load configuration file')
15
16
 
16
17
  const startTimestamp = Date.now()
17
- const dataDir = path.resolve(path.dirname(configPath), 'logs', `${startTimestamp}`)
18
- await fs.promises.mkdir(dataDir, { recursive: true })
18
+ const dataDir = process.cwd()
19
+ const tmpDir = os.tmpdir()
20
+
21
+ const jsonConfigPath = `${tmpDir}/webrtcperf-config-${startTimestamp}.json`
22
+ await fs.promises.writeFile(jsonConfigPath, JSON.stringify(configs), 'utf-8')
19
23
 
20
24
  const binds: string[] = [
21
- `${path.resolve(configPath)}:/config/${configName}:ro`,
22
25
  '/dev/shm:/dev/shm',
23
26
  `${dataDir}:/data`,
24
- '/tmp/webrtcperf-cache:/root/.webrtcperf',
27
+ `${tmpDir}/webrtcperf-cache:/root/.webrtcperf`,
28
+ `${jsonConfigPath}:/tmp/config.json:ro`,
25
29
  ]
26
30
 
27
- if (config.scriptPath) {
28
- const scriptName = path.basename(config.scriptPath)
29
- binds.push(`${path.resolve(config.scriptPath)}:/scripts/${scriptName}:ro`)
30
- }
31
-
32
31
  if (process.env.DEBUG_SRC) {
33
32
  binds.push(`${resolvePackagePath('app.min.js')}:/app/app.min.js:ro`)
34
33
  }
35
34
 
36
35
  const portBindings: Docker.PortMap = {}
37
36
  const exposedPorts: { [portAndProtocol: string]: object } = {}
38
- if (config.debuggingPort) {
39
- for (let i = 0; i < config.sessions; i++) {
40
- const port = `${config.debuggingPort + i}/tcp`
41
- portBindings[port] = [{ HostPort: `${config.debuggingPort + i}` }]
37
+ if (configs[0].debuggingPort) {
38
+ for (let i = 0; i < configs[0].sessions; i++) {
39
+ const port = `${configs[0].debuggingPort + i}/tcp`
40
+ portBindings[port] = [{ HostPort: `${configs[0].debuggingPort + i}` }]
42
41
  exposedPorts[port] = {}
43
42
  }
44
43
  }
45
44
 
45
+ if (configs[0].throttleConfig && os.platform() === 'linux') {
46
+ await runShellCommand('sudo modprobe ifb numifbs=1')
47
+ }
48
+
46
49
  const env = [
47
50
  `DEBUG_LEVEL=${process.env.DEBUG_LEVEL || 'info'}`,
48
51
  'SHOW_PAGE_LOG=false',
@@ -51,33 +54,25 @@ export async function runWithDocker(argv: string[]) {
51
54
  'SERVER_USE_HTTPS=true',
52
55
  'SERVER_DATA=/data',
53
56
  `START_TIMESTAMP=${startTimestamp}`,
54
- `STATS_PATH=/data/stats.csv`,
55
- `PAGE_LOG_PATH=/data/page.log`,
56
- `DETAILED_STATS_PATH=/data/detailed-stats.csv`,
57
57
  ]
58
58
 
59
- if (config.scriptPath) {
60
- const scriptName = path.basename(config.scriptPath)
61
- env.push(`SCRIPT_PATH=/scripts/${scriptName}`)
62
- }
63
-
64
- if (config.debuggingPort) {
65
- env.push(`DEBUGGING_PORT=${config.debuggingPort}`)
66
- }
67
-
68
- if (config.prometheusPushgateway.startsWith('http://localhost')) {
59
+ if (configs[0].prometheusPushgateway.startsWith('http://localhost')) {
69
60
  env.push('PROMETHEUS_PUSHGATEWAY=http://pushgateway:9091')
70
61
  }
71
62
 
72
63
  const containerConfig: Docker.ContainerCreateOptions = {
73
64
  Image: 'ghcr.io/vpalmisano/webrtcperf:devel',
74
65
  name: 'webrtcperf',
75
- Cmd: [`/config/${configName}`],
66
+ WorkingDir: '/data',
67
+ Cmd: ['/tmp/config.json'],
76
68
  HostConfig: {
77
69
  Binds: binds,
78
70
  PortBindings: portBindings,
79
- CapAdd: ['NET_ADMIN'],
80
- NetworkMode: config.prometheusPushgateway.startsWith('http://localhost') ? 'prometheus-stack_default' : 'bridge',
71
+ CapAdd: configs[0].throttleConfig && os.platform() === 'linux' ? ['NET_ADMIN'] : [],
72
+ NetworkMode: configs[0].prometheusPushgateway.startsWith('http://localhost')
73
+ ? 'prometheus-stack_default'
74
+ : 'bridge',
75
+ ExtraHosts: process.env.EXTRA_HOSTS ? process.env.EXTRA_HOSTS.split(',').map(h => h.trim()) : [],
81
76
  },
82
77
  Env: env,
83
78
  AttachStdin: true,
@@ -128,4 +123,6 @@ export async function runWithDocker(argv: string[]) {
128
123
  log.error('Docker operation failed:', error)
129
124
  throw error
130
125
  }
126
+
127
+ await fs.promises.unlink(jsonConfigPath)
131
128
  }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './app'
2
2
  export * from './config'
3
+ export * from './docker'
3
4
  export * from './media'
4
5
  export * from './rtcstats'
5
6
  export * from './server'
@@ -7,3 +8,4 @@ export * from './session'
7
8
  export * from './stats'
8
9
  export * from './utils'
9
10
  export * from './vmaf'
11
+ export * from './scenarios'
@@ -0,0 +1,148 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { FastStats } from './stats'
4
+ import { ThrottleConfig, ThrottleRule } from '@vpalmisano/throttler'
5
+ import { Config } from './config'
6
+
7
+ export async function parseStatsFile(filePath: string) {
8
+ const fileData = await fs.promises.readFile(filePath, 'utf-8')
9
+ const lines = fileData.split('\n')
10
+ const headers = lines[0].split(',')
11
+ const data = lines.slice(1).map(line =>
12
+ line.split(',').reduce(
13
+ (acc, value, index) => {
14
+ if (value !== '') {
15
+ acc[headers[index]] = isNaN(Number(value)) ? value : Number(value)
16
+ }
17
+ return acc
18
+ },
19
+ {} as Record<string, string | number>,
20
+ ),
21
+ )
22
+ return data
23
+ }
24
+
25
+ export async function aggregateStatsSummary({
26
+ dirPath = 'logs',
27
+ senderParticipantName = 'Participant-000001',
28
+ receiverParticipantName = 'Participant-000000',
29
+ nameParser = (name: string) => {
30
+ const [_, id, scenario] = name.split('_')
31
+ return { id, scenario }
32
+ },
33
+ }) {
34
+ const stats = [] as {
35
+ timestamp: number
36
+ id: string
37
+ scenario: string
38
+ videoRecvBitratePerPixel: FastStats
39
+ videoRecvFps: FastStats
40
+ videoSentFps: FastStats
41
+ }[]
42
+ const results = await fs.promises.readdir(dirPath)
43
+ for (const test of results) {
44
+ const filePath = path.join(dirPath, test, 'detailed-stats-summary.csv')
45
+ if (!fs.existsSync(filePath)) continue
46
+ const timestamp = fs.statSync(path.join(dirPath, test)).ctime.getTime()
47
+ const data = await parseStatsFile(filePath)
48
+ const { id, scenario } = nameParser(test)
49
+
50
+ const aggregated = {
51
+ timestamp,
52
+ id,
53
+ scenario,
54
+ videoRecvBitratePerPixel: new FastStats(),
55
+ videoRecvFps: new FastStats(),
56
+ videoSentFps: new FastStats(),
57
+ }
58
+ data.forEach(v => {
59
+ const { participantName, trackId } = v as { participantName: string; trackId: string }
60
+ const metrics = v as Record<string, number>
61
+ if (participantName === receiverParticipantName) {
62
+ if (trackId?.endsWith('-v') && metrics.videoRecvFrames > 0) {
63
+ const videoRecvBitratePerPixel =
64
+ metrics.videoRecvBitrates / (metrics.videoRecvWidth * metrics.videoRecvHeight)
65
+ if (!isNaN(videoRecvBitratePerPixel)) aggregated.videoRecvBitratePerPixel.push(videoRecvBitratePerPixel)
66
+ if (!isNaN(metrics.videoRecvFps)) aggregated.videoRecvFps.push(metrics.videoRecvFps)
67
+ }
68
+ } else if (participantName === senderParticipantName) {
69
+ if (trackId?.endsWith('-v') && metrics.videoSentFrames > 0) {
70
+ if (!isNaN(metrics.videoSentFps)) aggregated.videoSentFps.push(metrics.videoSentFps)
71
+ }
72
+ }
73
+ })
74
+ stats.push(aggregated)
75
+ }
76
+ return stats.sort((a, b) => a.timestamp - b.timestamp)
77
+ }
78
+
79
+ export type ThrottleDirection = 'up' | 'down' | 'bidi'
80
+
81
+ export function formatThrottleRule(throttleRule: ThrottleRule, direction: ThrottleDirection) {
82
+ const { rate, loss, delay } = throttleRule
83
+ return `${direction}-r${rate}-l${loss}-d${delay}`
84
+ }
85
+
86
+ export function parseThrottleRule(throttleDesc: string) {
87
+ const match = throttleDesc.match(/(up|down|bidi)-r(\d+)-l([\d.]+)-d(\d+)/)
88
+ if (!match) throw new Error(`Invalid throttle description: ${throttleDesc}`)
89
+ const direction = match[1] as ThrottleDirection
90
+ const rate = parseInt(match[2])
91
+ const loss = parseInt(match[3])
92
+ const delay = parseInt(match[4])
93
+ return { direction, rate, loss, delay }
94
+ }
95
+
96
+ export async function simpleTestWithRateLossDelay(
97
+ id: string,
98
+ { rate, loss, delay, direction }: { rate: number; loss: number; delay: number; direction: ThrottleDirection },
99
+ repeat: 1,
100
+ ) {
101
+ const throttle: ThrottleConfig = {}
102
+ const queue = 25
103
+ if (direction === 'down' || direction === 'bidi') {
104
+ throttle.down = [
105
+ { rate: 20000, loss: 0, delay: 0, queue },
106
+ { rate, loss, delay, queue, at: 30 },
107
+ ]
108
+ }
109
+ if (direction === 'up' || direction === 'bidi') {
110
+ throttle.up = [
111
+ { rate: 20000, loss: 0, delay, queue },
112
+ { rate, loss, delay, queue, at: 30 },
113
+ ]
114
+ }
115
+ const throttleDesc = formatThrottleRule({ rate, loss, delay }, direction)
116
+ const now = Date.now()
117
+ const ret: Partial<Config>[] = []
118
+ for (let i = 0; i < repeat; i++) {
119
+ const basePath = `logs/${now}-${i + 1}_${id}_${throttleDesc}`
120
+ const sessions = direction === 'bidi' ? '0-1' : direction === 'down' ? '0' : '1'
121
+ ret.push({
122
+ sessions: 2,
123
+ runDuration: 60 * 3,
124
+ debuggingPort: 9000,
125
+ prometheusPushgateway: 'http://localhost:9091',
126
+ prometheusPushgatewayJobName: id,
127
+ statsPath: `${basePath}/stats.csv`,
128
+ detailedStatsPath: `${basePath}/detailed-stats.csv`,
129
+ showPageLog: false,
130
+ showStats: false,
131
+ statsInterval: 5,
132
+ scriptParams: JSON.stringify({
133
+ enableMic: '0-1',
134
+ enableCam: '1',
135
+ }),
136
+ throttleConfig: JSON.stringify([
137
+ {
138
+ sessions,
139
+ protocol: 'udp',
140
+ skipSourcePorts: '53,80,443',
141
+ skipDestinationPorts: '53,80,443',
142
+ ...throttle,
143
+ },
144
+ ]),
145
+ })
146
+ }
147
+ return ret
148
+ }
package/src/utils.ts CHANGED
@@ -23,7 +23,6 @@ import pidusage from 'pidusage'
23
23
  import puppeteer, { ImageFormat, Page } from 'puppeteer-core'
24
24
 
25
25
  import { Session } from './session'
26
- import { FastStats } from './stats'
27
26
 
28
27
  // eslint-disable-next-line
29
28
  const ps = require('pidusage/lib/ps')
@@ -1256,75 +1255,3 @@ export async function getDockerLogsPath(): Promise<string> {
1256
1255
  }
1257
1256
  return logPath
1258
1257
  }
1259
-
1260
- export async function parseStatsFile(filePath: string) {
1261
- const fileData = await fs.promises.readFile(filePath, 'utf-8')
1262
- const lines = fileData.split('\n')
1263
- const headers = lines[0].split(',')
1264
- const data = lines.slice(1).map(line =>
1265
- line.split(',').reduce(
1266
- (acc, value, index) => {
1267
- if (value !== '') {
1268
- acc[headers[index]] = isNaN(Number(value)) ? value : Number(value)
1269
- }
1270
- return acc
1271
- },
1272
- {} as Record<string, string | number>,
1273
- ),
1274
- )
1275
- return data
1276
- }
1277
-
1278
- export async function aggregateStatsSummary({
1279
- dirPath = 'logs',
1280
- senderParticipantName = 'Participant-000001',
1281
- receiverParticipantName = 'Participant-000000',
1282
- nameParser = (name: string) => {
1283
- const [destination, scenario] = name.split('_')
1284
- return { destination, scenario }
1285
- },
1286
- }) {
1287
- const stats = [] as {
1288
- timestamp: number
1289
- destination: string
1290
- scenario: string
1291
- videoRecvBitratePerPixel: FastStats
1292
- videoRecvFps: FastStats
1293
- videoSentFps: FastStats
1294
- }[]
1295
- const results = await fs.promises.readdir(dirPath)
1296
- for (const test of results) {
1297
- const filePath = path.join(dirPath, test, 'detailed-stats-summary.csv')
1298
- if (!fs.existsSync(filePath)) continue
1299
- const timestamp = fs.statSync(path.join(dirPath, test)).ctime.getTime()
1300
- const data = await parseStatsFile(filePath)
1301
- const { destination, scenario } = nameParser(test)
1302
-
1303
- const aggregated = {
1304
- timestamp,
1305
- destination,
1306
- scenario,
1307
- videoRecvBitratePerPixel: new FastStats(),
1308
- videoRecvFps: new FastStats(),
1309
- videoSentFps: new FastStats(),
1310
- }
1311
- data.forEach(v => {
1312
- const { participantName, trackId } = v as { participantName: string; trackId: string }
1313
- const metrics = v as Record<string, number>
1314
- if (participantName === receiverParticipantName) {
1315
- if (trackId?.endsWith('-v') && metrics.videoRecvFrames > 0) {
1316
- const videoRecvBitratePerPixel =
1317
- metrics.videoRecvBitrates / (metrics.videoRecvWidth * metrics.videoRecvHeight)
1318
- if (!isNaN(videoRecvBitratePerPixel)) aggregated.videoRecvBitratePerPixel.push(videoRecvBitratePerPixel)
1319
- if (!isNaN(metrics.videoRecvFps)) aggregated.videoRecvFps.push(metrics.videoRecvFps)
1320
- }
1321
- } else if (participantName === senderParticipantName) {
1322
- if (trackId?.endsWith('-v') && metrics.videoSentFrames > 0) {
1323
- if (!isNaN(metrics.videoSentFps)) aggregated.videoSentFps.push(metrics.videoSentFps)
1324
- }
1325
- }
1326
- })
1327
- stats.push(aggregated)
1328
- }
1329
- return stats.sort((a, b) => a.timestamp - b.timestamp)
1330
- }