@vpalmisano/webrtcperf 4.3.2 → 4.4.2

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 ADDED
@@ -0,0 +1,131 @@
1
+ import path from 'path'
2
+ import Docker from 'dockerode'
3
+ import { logger, resolvePackagePath } from './utils'
4
+ import { loadConfig } from './config'
5
+ import fs from 'fs'
6
+
7
+ const log = logger('webrtcperf:docker')
8
+
9
+ export async function runWithDocker(argv: string[]) {
10
+ const docker = new Docker()
11
+ const configPath = argv.filter(s => s !== '--docker')[0]
12
+ if (!configPath) throw new Error('No configuration file specified')
13
+ const configName = path.basename(configPath)
14
+ const config = (await loadConfig(configPath))[0]
15
+
16
+ const startTimestamp = Date.now()
17
+ const dataDir = path.resolve(path.dirname(configPath), 'logs', `${startTimestamp}`)
18
+ await fs.promises.mkdir(dataDir, { recursive: true })
19
+
20
+ const binds: string[] = [
21
+ `${path.resolve(configPath)}:/config/${configName}:ro`,
22
+ '/dev/shm:/dev/shm',
23
+ `${dataDir}:/data`,
24
+ '/tmp/webrtcperf-cache:/root/.webrtcperf',
25
+ ]
26
+
27
+ if (config.scriptPath) {
28
+ const scriptName = path.basename(config.scriptPath)
29
+ binds.push(`${path.resolve(config.scriptPath)}:/scripts/${scriptName}:ro`)
30
+ }
31
+
32
+ if (process.env.DEBUG_SRC) {
33
+ binds.push(`${resolvePackagePath('app.min.js')}:/app/app.min.js:ro`)
34
+ }
35
+
36
+ const portBindings: Docker.PortMap = {}
37
+ 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}` }]
42
+ exposedPorts[port] = {}
43
+ }
44
+ }
45
+
46
+ const env = [
47
+ `DEBUG_LEVEL=${process.env.DEBUG_LEVEL || 'info'}`,
48
+ 'SHOW_PAGE_LOG=false',
49
+ 'SHOW_STATS=false',
50
+ 'SERVER_PORT=5000',
51
+ 'SERVER_USE_HTTPS=true',
52
+ 'SERVER_DATA=/data',
53
+ `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
+ ]
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')) {
69
+ env.push('PROMETHEUS_PUSHGATEWAY=http://pushgateway:9091')
70
+ }
71
+
72
+ const containerConfig: Docker.ContainerCreateOptions = {
73
+ Image: 'ghcr.io/vpalmisano/webrtcperf:devel',
74
+ name: 'webrtcperf',
75
+ Cmd: [`/config/${configName}`],
76
+ HostConfig: {
77
+ Binds: binds,
78
+ PortBindings: portBindings,
79
+ CapAdd: ['NET_ADMIN'],
80
+ NetworkMode: config.prometheusPushgateway.startsWith('http://localhost') ? 'prometheus-stack_default' : 'bridge',
81
+ },
82
+ Env: env,
83
+ AttachStdin: true,
84
+ AttachStdout: true,
85
+ AttachStderr: true,
86
+ Tty: true,
87
+ OpenStdin: true,
88
+ StdinOnce: true,
89
+ ExposedPorts: exposedPorts,
90
+ }
91
+
92
+ try {
93
+ if (!process.env.DEBUG_SRC) {
94
+ log.info('Pulling latest webrtcperf image...')
95
+ await docker.pull('ghcr.io/vpalmisano/webrtcperf:devel')
96
+ }
97
+
98
+ try {
99
+ const existingContainer = await docker.getContainer('webrtcperf')
100
+ await existingContainer.remove({ force: true })
101
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
102
+ } catch (err: unknown) {
103
+ // Container doesn't exist, continue
104
+ }
105
+
106
+ const container = await docker.createContainer(containerConfig)
107
+ await container.start()
108
+
109
+ const stream = await container.attach({
110
+ stream: true,
111
+ stdin: true,
112
+ stdout: true,
113
+ stderr: true,
114
+ })
115
+
116
+ process.stdin.pipe(stream)
117
+ stream.pipe(process.stdout)
118
+
119
+ await new Promise(resolve => {
120
+ container.wait((err: Error, data: { StatusCode: number }) => {
121
+ if (err) log.error('Error waiting for container:', data, err.stack)
122
+ resolve(data)
123
+ })
124
+ })
125
+
126
+ await container.remove()
127
+ } catch (error) {
128
+ log.error('Docker operation failed:', error)
129
+ throw error
130
+ }
131
+ }
package/src/session.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { getSessionThrottleValues, throttleLauncher } from '@vpalmisano/throttler'
1
+ import { getSessionThrottleValues, throttleLauncher, throttleNotifier } from '@vpalmisano/throttler'
2
2
  import assert from 'assert'
3
3
  import axios from 'axios'
4
4
  import EventEmitter from 'events'
@@ -30,8 +30,8 @@ import { gunzipSync } from 'zlib'
30
30
  import { RtcStats, rtcStatKey, updateRtcStats } from './rtcstats'
31
31
  import { FastStats } from './stats'
32
32
  import {
33
- PeerConnectionExternal,
34
- PeerConnectionExternalMethod,
33
+ /* PeerConnectionExternal,
34
+ PeerConnectionExternalMethod, */
35
35
  checkChromeExecutable,
36
36
  downloadUrl,
37
37
  enabledForSession,
@@ -163,6 +163,7 @@ export interface SessionParams {
163
163
  userAgent: string
164
164
  id: number
165
165
  throttleIndex: number
166
+ useBrowserThrottling: boolean
166
167
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
167
168
  evaluateAfter?: any[]
168
169
  exposedFunctions?: string
@@ -279,6 +280,8 @@ export class Session extends EventEmitter {
279
280
  readonly id: number
280
281
  /** The throttle configuration index assigned to the session. */
281
282
  readonly throttleIndex: number
283
+ /** If true, the network will be throttled using the browser internal throttling mechanism. */
284
+ readonly useBrowserThrottling: boolean
282
285
  /** The test page url. */
283
286
  readonly url: string
284
287
  /** The url query. */
@@ -378,6 +381,7 @@ export class Session extends EventEmitter {
378
381
  userAgent,
379
382
  id,
380
383
  throttleIndex,
384
+ useBrowserThrottling,
381
385
  evaluateAfter,
382
386
  exposedFunctions,
383
387
  scriptParams,
@@ -475,6 +479,7 @@ export class Session extends EventEmitter {
475
479
  this.emulateCpuThrottling = emulateCpuThrottling
476
480
 
477
481
  this.throttleIndex = throttleIndex
482
+ this.useBrowserThrottling = useBrowserThrottling
478
483
  this.evaluateAfter = evaluateAfter || []
479
484
  this.exposedFunctions = exposedFunctions || {}
480
485
  if (scriptParams) {
@@ -676,7 +681,7 @@ export class Session extends EventEmitter {
676
681
  deviceScaleFactor: this.deviceScaleFactor,
677
682
  isMobile: false,
678
683
  hasTouch: false,
679
- isLandscape: false,
684
+ isLandscape: true,
680
685
  },
681
686
  })
682
687
  } catch (err) {
@@ -803,7 +808,7 @@ try {
803
808
  } catch (err) {
804
809
  console.error('[webrtcperf] Error parsing scriptParams:', err);
805
810
  webrtcperf.params = {};
806
- }
811
+ };
807
812
  `
808
813
 
809
814
  if (this.serverPort) {
@@ -892,6 +897,9 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
892
897
 
893
898
  const page = await this.getNewPage(tabIndex)
894
899
 
900
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
901
+ const pageCDPSession = (page as any)._client() as CDPSession
902
+
895
903
  await page.setBypassCSP(true)
896
904
 
897
905
  if (this.userAgent) {
@@ -910,23 +918,29 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
910
918
  if (this.localStorage) {
911
919
  log.debug('Using localStorage:', this.localStorage)
912
920
  Object.entries(this.localStorage).map(([key, value]) => {
913
- cmd += `localStorage.setItem('${key}', '${JSON.stringify(value)}');\n`
921
+ cmd += `window.localStorage.setItem('${key}', ${JSON.stringify(value)});\n`
914
922
  })
915
923
  }
916
924
  if (this.sessionStorage) {
917
925
  log.debug('Using sessionStorage:', this.sessionStorage)
918
926
  Object.entries(this.sessionStorage).map(([key, value]) => {
919
- cmd += `sessionStorage.setItem('${key}', '${JSON.stringify(value)}');\n`
927
+ cmd += `window.sessionStorage.setItem('${key}', ${JSON.stringify(value)});\n`
920
928
  })
921
929
  }
930
+ cmd += `
931
+ Object.defineProperty(window.screen, 'width', { value: ${this.windowWidth}, writable: false });
932
+ Object.defineProperty(window.screen, 'height', { value: ${this.windowHeight}, writable: false });
933
+ Object.defineProperty(window.screen, 'availWidth', { value: ${this.windowWidth}, writable: false });
934
+ Object.defineProperty(window.screen, 'availHeight', { value: ${this.windowHeight}, writable: false });
935
+ Object.defineProperty(window.screen.orientation, 'type', { value: 'landscape-primary', writable: false });
936
+ `
922
937
  log.debug('init command:', cmd)
923
938
  await page.evaluateOnNewDocument(cmd)
924
939
 
925
940
  // Clear cookies.
926
941
  if (this.clearCookies) {
927
942
  try {
928
- const client = await page.target().createCDPSession()
929
- await client.send('Network.clearBrowserCookies')
943
+ await pageCDPSession.send('Network.clearBrowserCookies')
930
944
  } catch (err) {
931
945
  log.error(`clearCookies error: ${(err as Error).stack}`)
932
946
  }
@@ -970,8 +984,6 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
970
984
  // Enable request interception.
971
985
  let setRequestInterceptionState = true
972
986
 
973
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
974
- const pageCDPSession = (page as any)._client() as CDPSession
975
987
  await pageCDPSession.send('Network.setBypassServiceWorker', {
976
988
  bypass: true,
977
989
  })
@@ -1174,7 +1186,7 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
1174
1186
  }
1175
1187
 
1176
1188
  // PeerConnectionExternal
1177
- await page.exposeFunction(
1189
+ /* await page.exposeFunction(
1178
1190
  'createPeerConnectionExternal',
1179
1191
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1180
1192
  async (options: any) => {
@@ -1192,7 +1204,7 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
1192
1204
  return pc[name](arg)
1193
1205
  }
1194
1206
  },
1195
- )
1207
+ )*/
1196
1208
 
1197
1209
  // Simulate keypress
1198
1210
  await page.exposeFunction('keypressText', async (selector: string, text: string, delay = 20) => {
@@ -1305,7 +1317,7 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
1305
1317
  if (this.showPageLog || saveFile) {
1306
1318
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1307
1319
  page.on('pageerror', async (error: any) => {
1308
- const text = `pageerror: ${error.message?.message || error.message} - ${error.message?.stack || error.stack}`
1320
+ const text = `pageerror: ${error?.message?.message || error?.message || error} - ${error?.message?.stack || error?.stack}`
1309
1321
  await this.onPageMessage(index, 'error', text, saveFile)
1310
1322
  })
1311
1323
 
@@ -1420,12 +1432,25 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
1420
1432
  resourcesStats.wsRecvBytes += event.response.payloadData.length
1421
1433
  })
1422
1434
 
1423
- // hardware concurrency
1435
+ // Hardware concurrency.
1424
1436
  if (this.hardwareConcurrency) {
1425
1437
  const plugin = NavigatorHardwareConcurrency({ hardwareConcurrency: this.hardwareConcurrency })
1426
1438
  await plugin.onPageCreated(page)
1427
1439
  }
1428
1440
 
1441
+ // Network throttling.
1442
+ if (this.throttleIndex > -1 && (process.platform !== 'linux' || this.useBrowserThrottling)) {
1443
+ log.debug(`Using internal network throttling`)
1444
+ await pageCDPSession.send('Network.emulateNetworkConditions', {
1445
+ offline: false,
1446
+ uploadThroughput: 100000000 / 8,
1447
+ downloadThroughput: 100000000 / 8,
1448
+ latency: 0,
1449
+ packetLoss: 0,
1450
+ packetQueueLength: 0,
1451
+ })
1452
+ }
1453
+
1429
1454
  // Load page script.
1430
1455
  {
1431
1456
  const filePath = resolvePackagePath('node_modules/@vpalmisano/webrtcperf-js/dist/webrtcperf.js')
@@ -1485,6 +1510,13 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
1485
1510
  // add to pages map
1486
1511
  this.pages.set(index, page)
1487
1512
 
1513
+ if (this.throttleIndex > -1 && (process.platform !== 'linux' || this.useBrowserThrottling)) {
1514
+ await this.applyNetworkThrottling(pageCDPSession)
1515
+ throttleNotifier.on('change', async () => {
1516
+ await this.applyNetworkThrottling(pageCDPSession)
1517
+ })
1518
+ }
1519
+
1488
1520
  log.debug(`Page ${index + 1} "${url}" loaded in ${(Date.now() - pageLoadTime) / 1000}s`)
1489
1521
 
1490
1522
  for (let i = 0; i < this.evaluateAfter.length; i++) {
@@ -1496,6 +1528,25 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
1496
1528
  }
1497
1529
  }
1498
1530
 
1531
+ private async applyNetworkThrottling(pageCDPSession: CDPSession) {
1532
+ const throttleUpValues = getSessionThrottleValues(this.throttleIndex, 'up')
1533
+ const throttleDownValues = getSessionThrottleValues(this.throttleIndex, 'down')
1534
+ const params = {
1535
+ offline: false,
1536
+ uploadThroughput: throttleUpValues.rate || -1,
1537
+ downloadThroughput: throttleDownValues.rate || -1,
1538
+ latency: Math.max(throttleUpValues.delay || 0, throttleDownValues.delay || 0),
1539
+ packetLoss: Math.max(throttleUpValues.loss || 0, throttleDownValues.loss || 0),
1540
+ packetQueueLength: Math.max(throttleUpValues.queue || 0, throttleDownValues.queue || 0),
1541
+ }
1542
+ log.debug(`Apply internal network throttling: ${JSON.stringify(params)}`)
1543
+ await pageCDPSession.send('Network.emulateNetworkConditions', {
1544
+ ...params,
1545
+ uploadThroughput: params.uploadThroughput !== -1 ? params.uploadThroughput / 8 : -1,
1546
+ downloadThroughput: params.downloadThroughput !== -1 ? params.downloadThroughput / 8 : -1,
1547
+ })
1548
+ }
1549
+
1499
1550
  private async getNewPage(tabIndex: number): Promise<Page> {
1500
1551
  log.debug(`getNewPage ${tabIndex}`)
1501
1552
  assert(this.context, 'NoBrowserContextCreated')
package/src/utils.ts CHANGED
@@ -651,28 +651,37 @@ export async function runShellCommand(
651
651
  cmd: string,
652
652
  verbose = false,
653
653
  maxBuffer = 1024 * 1024,
654
+ { provideStdin = false, returnStdout = true, returnStderr = true } = {},
654
655
  ): Promise<{ stdout: string; stderr: string }> {
655
656
  if (verbose) log.debug(`runShellCommand cmd: ${cmd}`)
656
657
  return new Promise((resolve, reject) => {
657
658
  const p = spawn(cmd, {
658
659
  shell: true,
659
- stdio: ['ignore', 'pipe', 'pipe'],
660
+ stdio: [
661
+ provideStdin ? 'inherit' : 'ignore',
662
+ returnStdout ? 'pipe' : 'inherit',
663
+ returnStderr ? 'pipe' : 'inherit',
664
+ ],
660
665
  detached: true,
661
666
  })
662
667
  let stdout = ''
663
668
  let stderr = ''
664
- p.stdout.on('data', data => {
665
- if (maxBuffer && stdout.length > maxBuffer) {
666
- stdout = stdout.slice(data.length)
667
- }
668
- stdout += data
669
- })
670
- p.stderr.on('data', data => {
671
- if (maxBuffer && stderr.length > maxBuffer) {
672
- stderr = stderr.slice(data.length)
673
- }
674
- stderr += data
675
- })
669
+ if (returnStdout) {
670
+ p.stdout?.on('data', data => {
671
+ if (maxBuffer && stdout.length > maxBuffer) {
672
+ stdout = stdout.slice(data.length)
673
+ }
674
+ stdout += data
675
+ })
676
+ }
677
+ if (returnStderr) {
678
+ p.stderr?.on('data', data => {
679
+ if (maxBuffer && stderr.length > maxBuffer) {
680
+ stderr = stderr.slice(data.length)
681
+ }
682
+ stderr += data
683
+ })
684
+ }
676
685
  p.once('error', err => reject(err))
677
686
  p.once('close', code => {
678
687
  if (code !== 0) {