@vpalmisano/webrtcperf 4.2.2 → 4.4.1

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/server.ts CHANGED
@@ -11,10 +11,12 @@ import { WebSocketServer } from 'ws'
11
11
  import zlib from 'zlib'
12
12
  import auth from 'basic-auth'
13
13
 
14
- import { loadConfig } from './config'
14
+ import { Config, loadConfig } from './config'
15
15
  import { Session, SessionParams } from './session'
16
16
  import { Stats } from './stats'
17
17
  import { logger, runShellCommand, getDockerLogsPath } from './utils'
18
+ import { getSessionThrottleIndex } from '@vpalmisano/throttler'
19
+ import { MediaPath, prepareFakeMedia } from './media'
18
20
 
19
21
  const log = logger('webrtcperf:server')
20
22
 
@@ -272,7 +274,8 @@ export class Server {
272
274
  private async putSession(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
273
275
  log.debug(`PUT /session`, req.body)
274
276
  try {
275
- const id = this.stats.consumeSessionId()
277
+ const config = req.body as Config
278
+ const id = this.stats.consumeSessionId(config.tabsPerSession)
276
279
  await this.startLocalSession(id, req.body)
277
280
  res.json({
278
281
  message: `Session created`,
@@ -293,10 +296,10 @@ export class Server {
293
296
  private async putSessions(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
294
297
  log.debug(`PUT /sessions`, req.body)
295
298
  try {
296
- const { sessions } = req.body
299
+ const { sessions, tabsPerSession } = req.body as Config
297
300
  const sessionsIds = []
298
301
  for (let i = 0; i < sessions; i++) {
299
- const id = this.stats.sessions.size
302
+ const id = this.stats.consumeSessionId(tabsPerSession)
300
303
  await this.startLocalSession(id, req.body)
301
304
  sessionsIds.push(id)
302
305
  }
@@ -471,12 +474,25 @@ export class Server {
471
474
  * @param config The session configuration.
472
475
  */
473
476
  private async startLocalSession(id: number, config: SessionParams): Promise<Session> {
474
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
475
- const sessionConfig = loadConfig(undefined, config) as any
476
- const session = new Session({ ...sessionConfig, id })
477
+ const configs = await loadConfig(undefined, config)
478
+ const sessionConfig = configs[0]
479
+ const throttleIndex = getSessionThrottleIndex(id)
480
+ const spawnPeriod = 1000 / sessionConfig.spawnRate
481
+
482
+ // Prepare fake video and audio.
483
+ const mediaPaths: MediaPath[] = []
484
+ if (sessionConfig.videoPath) {
485
+ for (const videoPath of sessionConfig.videoPath.split(',')) {
486
+ const ret = await prepareFakeMedia({ ...sessionConfig, videoPath })
487
+ mediaPaths.push(ret)
488
+ }
489
+ }
490
+ const mediaPath = mediaPaths.length ? mediaPaths[id % mediaPaths.length] : undefined
491
+
492
+ const session = new Session({ ...sessionConfig, throttleIndex, spawnPeriod, mediaPath, id })
477
493
  session.once('stop', () => {
478
494
  console.warn(`Session ${id} stopped, reloading...`)
479
- setTimeout(this.startLocalSession.bind(this), sessionConfig.spawnPeriod, id, config)
495
+ setTimeout(this.startLocalSession.bind(this), spawnPeriod, id, config)
480
496
  })
481
497
  this.stats.addSession(session)
482
498
  try {
@@ -510,13 +526,17 @@ export class Server {
510
526
  log.debug('start')
511
527
  if (this.serverUseHttps) {
512
528
  const destDir = path.join(os.homedir(), '.webrtcperf/ssl')
513
- await runShellCommand(
514
- `mkdir -p ${destDir} && openssl req -newkey rsa:2048 -nodes -keyout ${destDir}/domain.key -x509 -days 365 -out ${destDir}/domain.crt -subj "/C=EU/ST=London/L=London/O=Global Security/OU=IT Department/CN=example.com"`,
515
- )
529
+ const keyPath = path.join(destDir, 'domain.key')
530
+ const crtPath = path.join(destDir, 'domain.crt')
531
+ if (!fs.existsSync(keyPath) || !fs.existsSync(crtPath)) {
532
+ await runShellCommand(
533
+ `mkdir -p ${destDir} && openssl req -newkey rsa:2048 -nodes -keyout ${keyPath} -x509 -days 365 -out ${crtPath} -subj "/C=EU/ST=London/L=London/O=Global Security/OU=IT Department/CN=example.com"`,
534
+ )
535
+ }
516
536
  this.server = _createServer(
517
537
  {
518
- key: fs.readFileSync(`${destDir}/domain.key`),
519
- cert: fs.readFileSync(`${destDir}/domain.crt`),
538
+ key: fs.readFileSync(keyPath),
539
+ cert: fs.readFileSync(crtPath),
520
540
  },
521
541
  this.app,
522
542
  )
package/src/session.ts CHANGED
@@ -676,7 +676,7 @@ export class Session extends EventEmitter {
676
676
  deviceScaleFactor: this.deviceScaleFactor,
677
677
  isMobile: false,
678
678
  hasTouch: false,
679
- isLandscape: false,
679
+ isLandscape: true,
680
680
  },
681
681
  })
682
682
  } catch (err) {
@@ -803,7 +803,7 @@ try {
803
803
  } catch (err) {
804
804
  console.error('[webrtcperf] Error parsing scriptParams:', err);
805
805
  webrtcperf.params = {};
806
- }
806
+ };
807
807
  `
808
808
 
809
809
  if (this.serverPort) {
@@ -910,15 +910,22 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
910
910
  if (this.localStorage) {
911
911
  log.debug('Using localStorage:', this.localStorage)
912
912
  Object.entries(this.localStorage).map(([key, value]) => {
913
- cmd += `localStorage.setItem('${key}', '${JSON.stringify(value)}');\n`
913
+ cmd += `window.localStorage.setItem('${key}', ${JSON.stringify(value)});\n`
914
914
  })
915
915
  }
916
916
  if (this.sessionStorage) {
917
917
  log.debug('Using sessionStorage:', this.sessionStorage)
918
918
  Object.entries(this.sessionStorage).map(([key, value]) => {
919
- cmd += `sessionStorage.setItem('${key}', '${JSON.stringify(value)}');\n`
919
+ cmd += `window.sessionStorage.setItem('${key}', ${JSON.stringify(value)});\n`
920
920
  })
921
921
  }
922
+ cmd += `
923
+ Object.defineProperty(window.screen, 'width', { value: ${this.windowWidth}, writable: false });
924
+ Object.defineProperty(window.screen, 'height', { value: ${this.windowHeight}, writable: false });
925
+ Object.defineProperty(window.screen, 'availWidth', { value: ${this.windowWidth}, writable: false });
926
+ Object.defineProperty(window.screen, 'availHeight', { value: ${this.windowHeight}, writable: false });
927
+ Object.defineProperty(window.screen.orientation, 'type', { value: 'landscape-primary', writable: false });
928
+ `
922
929
  log.debug('init command:', cmd)
923
930
  await page.evaluateOnNewDocument(cmd)
924
931
 
@@ -1305,7 +1312,7 @@ webrtcperf.config.AUDIO_URL = "http${this.serverUseHttps ? 's' : ''}://localhost
1305
1312
  if (this.showPageLog || saveFile) {
1306
1313
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1307
1314
  page.on('pageerror', async (error: any) => {
1308
- const text = `pageerror: ${error.message?.message || error.message} - ${error.message?.stack || error.stack}`
1315
+ const text = `pageerror: ${error?.message?.message || error?.message || error} - ${error?.message?.stack || error?.stack}`
1309
1316
  await this.onPageMessage(index, 'error', text, saveFile)
1310
1317
  })
1311
1318
 
package/src/stats.ts CHANGED
@@ -32,23 +32,19 @@ function calculateFailAmountPercentile(stat: FastStats, percentile = 95): number
32
32
  class StatsWriter {
33
33
  fname: string
34
34
  columns: string[]
35
- private _header_written = false
35
+ private headerWritten = false
36
36
 
37
37
  constructor(fname = 'stats.log', columns: string[]) {
38
38
  this.fname = fname
39
39
  this.columns = columns
40
40
  }
41
41
 
42
- /**
43
- * push
44
- * @param dataColumns
45
- */
46
- async push(dataColumns: string[]): Promise<void> {
47
- if (!this._header_written) {
42
+ async push(dataColumns: string[], append = true): Promise<void> {
43
+ if (!this.headerWritten || !append) {
48
44
  const data = ['datetime', ...this.columns].join(',') + '\n'
49
45
  await fs.promises.mkdir(path.dirname(this.fname), { recursive: true })
50
46
  await fs.promises.writeFile(this.fname, data)
51
- this._header_written = true
47
+ this.headerWritten = true
52
48
  }
53
49
  //
54
50
  const data = [Date.now(), ...dataColumns].join(',') + '\n'
@@ -330,6 +326,7 @@ export class Stats extends events.EventEmitter {
330
326
  nextSessionId: number
331
327
  statsWriter: StatsWriter | null
332
328
  detailedStatsWriter: StatsWriter | null
329
+ detailedStatsSummaryWriter: StatsWriter | null
333
330
  private scheduler?: Scheduler
334
331
 
335
332
  private alertRules: Record<string, AlertRule> | null = null
@@ -386,18 +383,18 @@ export class Stats extends events.EventEmitter {
386
383
 
387
384
  collectedStats: Record<string, CollectedStats>
388
385
 
389
- collectedStatsConfig = {
386
+ private collectedStatsConfig = {
390
387
  url: '',
391
388
  pages: 0,
392
389
  startTime: 0,
393
390
  }
394
- externalCollectedStats = new Map<
391
+ private externalCollectedStats = new Map<
395
392
  string,
396
393
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
397
394
  { addedTime: number; externalStats: any; config: any }
398
395
  >()
399
- pushStatsInstance: axios.AxiosInstance | null = null
400
-
396
+ private pushStatsInstance: axios.AxiosInstance | null = null
397
+ private detailedStatsSummary: Record<string, Record<string, FastStats>> = {}
401
398
  private running = false
402
399
 
403
400
  /**
@@ -485,6 +482,7 @@ export class Stats extends events.EventEmitter {
485
482
 
486
483
  this.statsWriter = null
487
484
  this.detailedStatsWriter = null
485
+ this.detailedStatsSummaryWriter = null
488
486
  if (alertRules.trim()) {
489
487
  this.alertRules = json5.parse(alertRules)
490
488
  log.debug(`using alertRules: ${JSON.stringify(this.alertRules, undefined, 2)}`)
@@ -615,6 +613,11 @@ export class Stats extends events.EventEmitter {
615
613
  'trackId',
616
614
  ...this.statsNames,
617
615
  ])
616
+ this.detailedStatsSummaryWriter = new StatsWriter(this.detailedStatsPath.replace(/\.(.+)$/, '-summary.$1'), [
617
+ 'participantName',
618
+ 'trackId',
619
+ ...this.statsNames,
620
+ ])
618
621
  }
619
622
 
620
623
  if (this.prometheusPushgateway) {
@@ -821,8 +824,7 @@ export class Stats extends events.EventEmitter {
821
824
  collectedStats.all.push(obj)
822
825
  } else {
823
826
  for (const [key, value] of Object.entries(obj)) {
824
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
825
- if (typeof value === 'number' && isFinite(value as any)) {
827
+ if (typeof value === 'number' && isFinite(value)) {
826
828
  collectedStats.all.push(value)
827
829
  // Push host label.
828
830
  const { trackId, hostName, participantName } = parseRtStatKey(key)
@@ -955,7 +957,7 @@ export class Stats extends events.EventEmitter {
955
957
 
956
958
  async writeDetailedStats() {
957
959
  if (!this.detailedStatsWriter) return
958
- const participantTrackStats = new Map<string, Record<string, string>>()
960
+ const participantTrackStats = new Map<string, Record<string, number>>()
959
961
  Object.entries(this.collectedStats).forEach(([name, stats]) => {
960
962
  Object.entries(stats.byParticipantAndTrack).forEach(([label, value]) => {
961
963
  let stats = participantTrackStats.get(label)
@@ -963,19 +965,47 @@ export class Stats extends events.EventEmitter {
963
965
  stats = {}
964
966
  participantTrackStats.set(label, stats)
965
967
  }
966
- stats[name] = toPrecision(value, 6)
968
+ stats[name] = value
967
969
  })
968
970
  })
969
971
  for (const [label, trackStats] of participantTrackStats.entries()) {
970
972
  const [participantName, trackId] = label.split(':', 2)
973
+ if (!this.detailedStatsSummary[label]) {
974
+ this.detailedStatsSummary[label] = {}
975
+ }
976
+ const summary = this.detailedStatsSummary[label]
971
977
  const values = [participantName, trackId]
972
978
  for (const name of this.statsNames) {
973
- values.push(trackStats[name] ?? '')
979
+ values.push(trackStats[name] !== undefined ? toPrecision(trackStats[name], 6) : '')
980
+ // Update the summary stats.
981
+ if (!summary[name]) {
982
+ summary[name] = new FastStats({ store_data: false })
983
+ }
984
+ const stat = summary[name]
985
+ if (trackStats[name] !== undefined) {
986
+ stat.push(trackStats[name])
987
+ }
974
988
  }
975
989
  await this.detailedStatsWriter.push(values)
976
990
  }
977
991
  }
978
992
 
993
+ async writeDetailedStatsSummary() {
994
+ if (!this.detailedStatsSummaryWriter) return
995
+ let append = false
996
+ for (const label of Object.keys(this.detailedStatsSummary)) {
997
+ const summary = this.detailedStatsSummary[label]
998
+ const [participantName, trackId] = label.split(':', 2)
999
+ const values = [participantName, trackId]
1000
+ for (const name of this.statsNames) {
1001
+ const stat = summary[name]
1002
+ values.push(stat?.length > 0 ? toPrecision(stat.amean(), 6) : '')
1003
+ }
1004
+ await this.detailedStatsSummaryWriter.push(values, append)
1005
+ append = true
1006
+ }
1007
+ }
1008
+
979
1009
  /**
980
1010
  * addCollectedStats
981
1011
  * @param id
@@ -1652,17 +1682,13 @@ export class Stats extends events.EventEmitter {
1652
1682
  }
1653
1683
  }
1654
1684
 
1655
- /**
1656
- * Stop the stats collector and the added Sessions.
1657
- */
1658
- async stop(): Promise<void> {
1659
- if (!this.running) {
1660
- return
1661
- }
1685
+ async stop() {
1686
+ if (!this.running) return
1662
1687
  this.running = false
1663
1688
  log.debug('stop')
1689
+
1664
1690
  if (this.scheduler) {
1665
- this.scheduler.stop()
1691
+ await this.scheduler.stop()
1666
1692
  this.scheduler = undefined
1667
1693
  }
1668
1694
 
@@ -1676,7 +1702,12 @@ export class Stats extends events.EventEmitter {
1676
1702
  }
1677
1703
  this.sessions.clear()
1678
1704
 
1705
+ await this.writeDetailedStatsSummary()
1706
+
1679
1707
  this.statsWriter = null
1708
+ this.detailedStatsWriter = null
1709
+ this.detailedStatsSummaryWriter = null
1710
+ this.detailedStatsSummary = {}
1680
1711
 
1681
1712
  // delete metrics
1682
1713
  if (this.gateway) {
package/src/utils.ts CHANGED
@@ -291,7 +291,6 @@ export async function randomActivateAudio(
291
291
  pages = pages.concat(sessionPages)
292
292
  }
293
293
  }
294
- // Remove pages with no audio tracks.
295
294
  for (const [i, page] of pages.entries()) {
296
295
  if (!page) {
297
296
  continue
@@ -307,15 +306,8 @@ export async function randomActivateAudio(
307
306
  }
308
307
  }
309
308
  const pagesWithAudio: Page[] = pages.filter(p => !!p)
310
- //
311
309
  const index = Math.floor(Math.random() * pagesWithAudio.length)
312
310
  const enable = Math.round(100 * Math.random()) <= randomAudioProbability
313
- log.debug('randomActivateAudio %j', {
314
- pages: pagesWithAudio.length,
315
- randomAudioProbability,
316
- index,
317
- enable,
318
- })
319
311
  for (const [i, page] of pagesWithAudio.entries()) {
320
312
  try {
321
313
  if (i === index) {
@@ -613,7 +605,8 @@ SIGNALS.forEach(event =>
613
605
  export async function checkChromeExecutable(): Promise<string> {
614
606
  // eslint-disable-next-line @typescript-eslint/no-require-imports
615
607
  const { loadConfig } = require('./config')
616
- const config = await loadConfig()
608
+ const configs = await loadConfig()
609
+ const config = configs[0]
617
610
  const cacheDir = path.join(os.homedir(), '.webrtcperf/chrome')
618
611
 
619
612
  const fixSemVer = (v: string) => v.split('.').slice(0, 3).join('.')
@@ -658,28 +651,37 @@ export async function runShellCommand(
658
651
  cmd: string,
659
652
  verbose = false,
660
653
  maxBuffer = 1024 * 1024,
654
+ { provideStdin = false, returnStdout = true, returnStderr = true } = {},
661
655
  ): Promise<{ stdout: string; stderr: string }> {
662
656
  if (verbose) log.debug(`runShellCommand cmd: ${cmd}`)
663
657
  return new Promise((resolve, reject) => {
664
658
  const p = spawn(cmd, {
665
659
  shell: true,
666
- stdio: ['ignore', 'pipe', 'pipe'],
660
+ stdio: [
661
+ provideStdin ? 'inherit' : 'ignore',
662
+ returnStdout ? 'pipe' : 'inherit',
663
+ returnStderr ? 'pipe' : 'inherit',
664
+ ],
667
665
  detached: true,
668
666
  })
669
667
  let stdout = ''
670
668
  let stderr = ''
671
- p.stdout.on('data', data => {
672
- if (maxBuffer && stdout.length > maxBuffer) {
673
- stdout = stdout.slice(data.length)
674
- }
675
- stdout += data
676
- })
677
- p.stderr.on('data', data => {
678
- if (maxBuffer && stderr.length > maxBuffer) {
679
- stderr = stderr.slice(data.length)
680
- }
681
- stderr += data
682
- })
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
+ }
683
685
  p.once('error', err => reject(err))
684
686
  p.once('close', code => {
685
687
  if (code !== 0) {
@@ -806,21 +808,26 @@ export class Scheduler {
806
808
  log.debug(`[${this.name}-scheduler] constructor interval=${this.interval}ms`)
807
809
  }
808
810
 
809
- start(): void {
811
+ start() {
810
812
  log.debug(`[${this.name}-scheduler] start`)
811
813
  this.running = true
812
814
  this.scheduleNext()
813
815
  }
814
816
 
815
- stop(): void {
817
+ async stop() {
816
818
  log.debug(`[${this.name}-scheduler] stop`)
817
819
  this.running = false
818
820
  if (this.statsTimeoutId) {
819
821
  clearTimeout(this.statsTimeoutId)
820
822
  }
823
+ try {
824
+ await this.callback(Date.now())
825
+ } catch (err) {
826
+ log.error(`[${this.name}-scheduler] stop callback error: ${(err as Error).stack}`, err)
827
+ }
821
828
  }
822
829
 
823
- private scheduleNext(): void {
830
+ private scheduleNext() {
824
831
  if (!this.running) {
825
832
  return
826
833
  }