@vpalmisano/webrtcperf 4.2.0 → 4.3.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/README.md +16 -1
- package/app.min.js +1 -1
- package/build/src/app.d.ts +13 -4
- package/build/src/app.js +170 -124
- package/build/src/app.js.map +1 -1
- package/build/src/config.d.ts +2 -91
- package/build/src/config.js +37 -29
- package/build/src/config.js.map +1 -1
- package/build/src/server.js +28 -10
- package/build/src/server.js.map +1 -1
- package/build/src/session.js +3 -2
- package/build/src/session.js.map +1 -1
- package/build/src/stats.d.ts +8 -21
- package/build/src/stats.js +67 -35
- package/build/src/stats.js.map +1 -1
- package/build/src/utils.d.ts +1 -1
- package/build/src/utils.js +9 -10
- package/build/src/utils.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +31 -28
- package/src/app.ts +184 -136
- package/src/config.ts +35 -28
- package/src/server.ts +33 -13
- package/src/session.ts +3 -1
- package/src/stats.ts +60 -27
- package/src/utils.ts +10 -12
package/src/app.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { getSessionThrottleIndex, startThrottle, stopThrottle } from '@vpalmisan
|
|
|
2
2
|
import { paramCase } from 'change-case'
|
|
3
3
|
import fs from 'fs'
|
|
4
4
|
import json5 from 'json5'
|
|
5
|
-
import wrap from 'word-wrap'
|
|
6
5
|
|
|
7
6
|
import { Config, getConfigDocs, loadConfig, loadConfigFromPrompt } from './config'
|
|
8
7
|
import { MediaPath, prepareFakeMedia } from './media'
|
|
@@ -23,163 +22,202 @@ import {
|
|
|
23
22
|
import { calculateVisqolScore } from './visqol'
|
|
24
23
|
import { calculateVmafScore, convertToIvf, prepareVideo } from './vmaf'
|
|
25
24
|
import path from 'path'
|
|
25
|
+
import { markedTerminal } from 'marked-terminal'
|
|
26
|
+
import { EventEmitter } from 'events'
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
|
+
const { marked } = require('marked')
|
|
30
|
+
marked.use(markedTerminal({ reflowText: true, tab: 2 }))
|
|
26
31
|
|
|
27
32
|
const log = logger('webrtcperf')
|
|
28
33
|
|
|
29
34
|
function showHelpOrVersion(): void {
|
|
30
|
-
if (process.argv.
|
|
35
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
31
36
|
const docs = getConfigDocs()
|
|
32
|
-
let out =
|
|
37
|
+
let out = marked.parse(`**Webrtcperf parameters**
|
|
38
|
+
|
|
39
|
+
\`--version\` It shows the package version.
|
|
40
|
+
`)
|
|
33
41
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
42
|
Object.entries(docs).forEach(([name, value]: [string, any]) => {
|
|
35
|
-
out +=
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
out += marked.parse(
|
|
44
|
+
`
|
|
45
|
+
\`--${paramCase(name)}\`
|
|
46
|
+
${value.doc}
|
|
47
|
+
Default value: \`${value.default}\`
|
|
48
|
+
`,
|
|
49
|
+
)
|
|
38
50
|
})
|
|
39
51
|
console.log(out)
|
|
40
52
|
process.exit(0)
|
|
41
|
-
} else if (process.argv.
|
|
53
|
+
} else if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
42
54
|
const version = json5.parse(fs.readFileSync(resolvePackagePath('package.json')).toString()).version
|
|
43
55
|
console.log(version)
|
|
44
56
|
process.exit(0)
|
|
45
57
|
}
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
export class Application extends EventEmitter {
|
|
61
|
+
readonly config: Config
|
|
62
|
+
readonly stats: Stats
|
|
63
|
+
readonly server?: Server
|
|
64
|
+
private mediaPaths: MediaPath[] = []
|
|
65
|
+
|
|
66
|
+
constructor(config: Config) {
|
|
67
|
+
super()
|
|
68
|
+
if (!config.startTimestamp) {
|
|
69
|
+
config.startTimestamp = Date.now()
|
|
70
|
+
}
|
|
71
|
+
this.config = config
|
|
72
|
+
this.stats = new Stats(config)
|
|
73
|
+
if (config.serverPort) {
|
|
74
|
+
this.server = new Server(config, this.stats)
|
|
56
75
|
}
|
|
57
76
|
}
|
|
58
77
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
await
|
|
64
|
-
} catch (err: unknown) {
|
|
65
|
-
log.error(`visqol score error: ${(err as Error).stack}`)
|
|
78
|
+
async start() {
|
|
79
|
+
log.debug(`start (runDuration: ${this.config.runDuration})`)
|
|
80
|
+
await this.stats.start()
|
|
81
|
+
if (this.server) {
|
|
82
|
+
await this.server.start()
|
|
66
83
|
}
|
|
67
|
-
|
|
68
|
-
}
|
|
84
|
+
const config = this.config
|
|
69
85
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
86
|
+
// Handle vmaf commands.
|
|
87
|
+
if (config.vmafPrepareVideo) {
|
|
88
|
+
await prepareVideo(config, true)
|
|
89
|
+
}
|
|
90
|
+
if (config.vmafProcessVideo) {
|
|
91
|
+
await convertToIvf(
|
|
92
|
+
config.vmafProcessVideo,
|
|
93
|
+
config.vmafVideoCrop,
|
|
94
|
+
config.vmafKeepSourceFiles,
|
|
95
|
+
config.vmafSkipDuplicated,
|
|
96
|
+
)
|
|
97
|
+
}
|
|
74
98
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
// Handle sessions.
|
|
100
|
+
if (config.sessions > 0) {
|
|
101
|
+
// Prepare fake video and audio.
|
|
102
|
+
if (config.videoPath && !this.mediaPaths.length) {
|
|
103
|
+
for (const videoPath of config.videoPath.split(',')) {
|
|
104
|
+
const ret = await prepareFakeMedia({ ...config, videoPath })
|
|
105
|
+
this.mediaPaths.push(ret)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
78
108
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
await server.start()
|
|
84
|
-
}
|
|
109
|
+
// Network throttle.
|
|
110
|
+
if (config.throttleConfig) {
|
|
111
|
+
await startThrottle(config.throttleConfig)
|
|
112
|
+
}
|
|
85
113
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const mediaPaths: MediaPath[] = []
|
|
90
|
-
if (config.videoPath) {
|
|
91
|
-
for (const videoPath of config.videoPath.split(',')) {
|
|
92
|
-
const ret = await prepareFakeMedia({ ...config, videoPath })
|
|
93
|
-
mediaPaths.push(ret)
|
|
114
|
+
// Download browser if necessary.
|
|
115
|
+
if (!config.chromiumUrl && !config.chromiumPath) {
|
|
116
|
+
await checkChromeExecutable()
|
|
94
117
|
}
|
|
95
|
-
}
|
|
96
118
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
119
|
+
// Start the local sessions.
|
|
120
|
+
if (config.randomAudioPeriod) {
|
|
121
|
+
startRandomActivateAudio(
|
|
122
|
+
this.stats.sessions,
|
|
123
|
+
config.randomAudioPeriod,
|
|
124
|
+
config.randomAudioProbability,
|
|
125
|
+
config.randomAudioRange,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
const spawnPeriod = 1000 / config.spawnRate
|
|
129
|
+
log.debug(`Starting ${config.sessions} sessions (spawnPeriod: ${spawnPeriod}ms)`)
|
|
130
|
+
const startTime = Date.now()
|
|
131
|
+
for (let i = 0; i < config.sessions; i += 1) {
|
|
132
|
+
const id = this.stats.consumeSessionId(config.tabsPerSession)
|
|
133
|
+
await this.startSession(id, spawnPeriod)
|
|
134
|
+
// If not the last session, sleep.
|
|
135
|
+
if (i < config.sessions - 1) {
|
|
136
|
+
await sleep(spawnPeriod)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
|
140
|
+
const spawnRate = (config.sessions * config.tabsPerSession) / elapsed
|
|
141
|
+
log.debug(`${config.sessions * config.tabsPerSession} pages started in ${elapsed}s (${spawnRate.toFixed(2)}/s)`)
|
|
100
142
|
}
|
|
101
143
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
await checkChromeExecutable()
|
|
144
|
+
if (config.runDuration || config.vmafPath || config.visqolPath) {
|
|
145
|
+
setTimeout(() => this.stop(), config.runDuration * 1000)
|
|
105
146
|
}
|
|
147
|
+
}
|
|
106
148
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
149
|
+
private async startSession(id: number, spawnPeriod: number) {
|
|
150
|
+
log.debug(`startSession ${id}`)
|
|
151
|
+
const throttleIndex = getSessionThrottleIndex(id)
|
|
152
|
+
const mediaPath = this.mediaPaths.length ? this.mediaPaths[id % this.mediaPaths.length] : undefined
|
|
153
|
+
const session = new Session({
|
|
154
|
+
...this.config,
|
|
155
|
+
mediaPath,
|
|
156
|
+
spawnPeriod,
|
|
157
|
+
id,
|
|
158
|
+
throttleIndex,
|
|
159
|
+
})
|
|
160
|
+
session.once('stop', () => {
|
|
161
|
+
console.warn(`Session ${id} stopped, reloading...`)
|
|
162
|
+
setTimeout(() => this.startSession(id, spawnPeriod), spawnPeriod)
|
|
163
|
+
})
|
|
164
|
+
this.stats.addSession(session)
|
|
165
|
+
await session.start()
|
|
166
|
+
}
|
|
125
167
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
168
|
+
private async postTest() {
|
|
169
|
+
log.debug('postTest')
|
|
170
|
+
|
|
171
|
+
// vmaf score.
|
|
172
|
+
if (this.config.vmafPath) {
|
|
173
|
+
console.log('Calculating VMAF score...')
|
|
174
|
+
try {
|
|
175
|
+
await calculateVmafScore(this.config)
|
|
176
|
+
} catch (err: unknown) {
|
|
177
|
+
log.error(`vmaf score error: ${(err as Error).stack}`)
|
|
178
|
+
}
|
|
134
179
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
await sleep(spawnPeriod)
|
|
180
|
+
|
|
181
|
+
// visqol score
|
|
182
|
+
if (this.config.visqolPath) {
|
|
183
|
+
console.log('Calculating Visqol score...')
|
|
184
|
+
try {
|
|
185
|
+
await calculateVisqolScore(this.config)
|
|
186
|
+
} catch (err: unknown) {
|
|
187
|
+
log.error(`visqol score error: ${(err as Error).stack}`)
|
|
144
188
|
}
|
|
145
189
|
}
|
|
146
|
-
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
|
147
|
-
const spawnRate = (config.sessions * config.tabsPerSession) / elapsed
|
|
148
|
-
log.debug(`${config.sessions * config.tabsPerSession} pages started in ${elapsed}s (${spawnRate.toFixed(2)}/s)`)
|
|
149
190
|
}
|
|
150
191
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
stop: async (): Promise<void> => {
|
|
154
|
-
log.debug('Stopping')
|
|
192
|
+
async stop(canceled = false) {
|
|
193
|
+
log.debug(`stop (canceled: ${canceled})`)
|
|
155
194
|
|
|
156
|
-
|
|
195
|
+
stopRandomActivateAudio()
|
|
157
196
|
|
|
158
|
-
|
|
197
|
+
await this.stats.stop()
|
|
159
198
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
199
|
+
if (this.config.throttleConfig) {
|
|
200
|
+
await stopThrottle()
|
|
201
|
+
}
|
|
163
202
|
|
|
164
|
-
|
|
203
|
+
stopTimers()
|
|
165
204
|
|
|
166
|
-
|
|
205
|
+
await this.postTest()
|
|
167
206
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
207
|
+
// Copy docker logs to data directory.
|
|
208
|
+
if (this.config.pageLogPath) {
|
|
209
|
+
try {
|
|
210
|
+
const logPath = await getDockerLogsPath()
|
|
211
|
+
const dataDir = path.dirname(this.config.pageLogPath)
|
|
212
|
+
await fs.promises.cp(logPath, path.resolve(dataDir, 'docker.log'))
|
|
213
|
+
} catch (err: unknown) {
|
|
214
|
+
log.debug(`docker logs not found: ${(err as Error).message}`)
|
|
177
215
|
}
|
|
216
|
+
}
|
|
178
217
|
|
|
179
|
-
|
|
218
|
+
this.server?.stop()
|
|
180
219
|
|
|
181
|
-
|
|
182
|
-
},
|
|
220
|
+
this.emit('stop', canceled)
|
|
183
221
|
}
|
|
184
222
|
}
|
|
185
223
|
|
|
@@ -189,39 +227,49 @@ export async function setupApplication(config: Config): Promise<{ stats: Stats;
|
|
|
189
227
|
async function main(): Promise<void> {
|
|
190
228
|
showHelpOrVersion()
|
|
191
229
|
|
|
192
|
-
|
|
193
|
-
process.argv[2] === '--prompt'
|
|
194
|
-
? await loadConfigFromPrompt(process.argv.slice(3).join(' '))
|
|
195
|
-
: await loadConfig(process.argv[2])
|
|
230
|
+
let configs: Config[]
|
|
196
231
|
|
|
197
|
-
if (
|
|
198
|
-
await
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
await convertToIvf(
|
|
204
|
-
config.vmafProcessVideo,
|
|
205
|
-
config.vmafVideoCrop,
|
|
206
|
-
config.vmafKeepSourceFiles,
|
|
207
|
-
config.vmafSkipDuplicated,
|
|
232
|
+
if (process.argv.slice(2).includes('--prompt')) {
|
|
233
|
+
const params = await loadConfigFromPrompt(
|
|
234
|
+
process.argv
|
|
235
|
+
.slice(2)
|
|
236
|
+
.filter(s => !['--prompt', '--dry-run'].includes(s))
|
|
237
|
+
.join(' '),
|
|
208
238
|
)
|
|
209
|
-
process.
|
|
239
|
+
if (process.argv.slice(2).includes('--dry-run')) {
|
|
240
|
+
console.log(json5.stringify(params, null, 2))
|
|
241
|
+
process.exit(0)
|
|
242
|
+
}
|
|
243
|
+
configs = await loadConfig(undefined, params)
|
|
244
|
+
} else {
|
|
245
|
+
configs = await loadConfig(process.argv[2])
|
|
210
246
|
}
|
|
211
247
|
|
|
212
|
-
|
|
248
|
+
if (!configs.length) throw new Error('No configuration found')
|
|
213
249
|
|
|
214
|
-
|
|
215
|
-
|
|
250
|
+
let application: Application
|
|
251
|
+
const runNext = () => {
|
|
252
|
+
const config = configs.splice(0, 1)[0]
|
|
216
253
|
|
|
217
|
-
|
|
254
|
+
application = new Application(config)
|
|
255
|
+
application.once('stop', canceled => {
|
|
256
|
+
if (!canceled && configs.length) {
|
|
257
|
+
log.info(`Application stopped, running next (${configs.length} left)...`)
|
|
258
|
+
runNext()
|
|
259
|
+
} else {
|
|
260
|
+
process.exit(0)
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
return application.start()
|
|
264
|
+
}
|
|
218
265
|
|
|
219
|
-
|
|
266
|
+
const stop = async () => {
|
|
267
|
+
console.log('Exiting...')
|
|
268
|
+
await application.stop(true)
|
|
220
269
|
}
|
|
221
270
|
registerExitHandler(() => stop())
|
|
222
271
|
|
|
223
|
-
|
|
224
|
-
setTimeout(stop, config.runDuration * 1000)
|
|
272
|
+
await runNext()
|
|
225
273
|
|
|
226
274
|
// Command line interface.
|
|
227
275
|
if (process.stdin && process.stdin.setRawMode) {
|
package/src/config.ts
CHANGED
|
@@ -6,6 +6,7 @@ import path, { join } from 'path'
|
|
|
6
6
|
import json5 from 'json5'
|
|
7
7
|
import yaml from 'yaml'
|
|
8
8
|
import toml from 'toml'
|
|
9
|
+
import fs from 'fs'
|
|
9
10
|
|
|
10
11
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
11
12
|
const puppeteer = require('puppeteer-core')
|
|
@@ -56,8 +57,7 @@ convict.addParser([
|
|
|
56
57
|
{ extension: 'toml', parse: toml.parse },
|
|
57
58
|
])
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
const configSchema = convict({
|
|
60
|
+
const configSchema = {
|
|
61
61
|
url: {
|
|
62
62
|
doc: `The page url to load.`,
|
|
63
63
|
format: String,
|
|
@@ -413,7 +413,7 @@ calculated using \`Date.now()\``,
|
|
|
413
413
|
showPageLog: {
|
|
414
414
|
doc: `If \`true\`, the pages console logs will be shown on console. Set to false to disable the page logs.`,
|
|
415
415
|
format: 'Boolean',
|
|
416
|
-
default:
|
|
416
|
+
default: false,
|
|
417
417
|
env: 'SHOW_PAGE_LOG',
|
|
418
418
|
arg: 'show-page-log',
|
|
419
419
|
},
|
|
@@ -445,7 +445,7 @@ on the console. Regexp string allowed.`,
|
|
|
445
445
|
userAgent: {
|
|
446
446
|
doc: `The user agent override.`,
|
|
447
447
|
format: String,
|
|
448
|
-
default:
|
|
448
|
+
default: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${puppeteer.PUPPETEER_REVISIONS.chrome} Safari/537.36`,
|
|
449
449
|
nullable: true,
|
|
450
450
|
env: 'USER_AGENT',
|
|
451
451
|
arg: 'user-agent',
|
|
@@ -456,7 +456,9 @@ If set, the files contents will be executed inside each opened tab page; \
|
|
|
456
456
|
the following global variables will be attached to the \`webrtcperf\` global object: \
|
|
457
457
|
\`WEBRTC_PERF_SESSION\` the session number (0-indexed); \
|
|
458
458
|
\`WEBRTC_PERF_TAB\` the tab number inside the same session (0-indexed); \
|
|
459
|
-
\`WEBRTC_PERF_INDEX\` the page absolute index (0-indexed).
|
|
459
|
+
\`WEBRTC_PERF_INDEX\` the page absolute index (0-indexed).
|
|
460
|
+
Suggested values:
|
|
461
|
+
- With meet.google.com: https://raw.githubusercontent.com/vpalmisano/webrtcperf/refs/heads/devel/examples/google-meet.js
|
|
460
462
|
`,
|
|
461
463
|
format: String,
|
|
462
464
|
default: '',
|
|
@@ -857,7 +859,7 @@ E.g. \`{ w: "iw-10", h: "ih-5", x: "10", y: '5' }\``,
|
|
|
857
859
|
env: 'VISQOL_KEEP_SOURCE_FILES',
|
|
858
860
|
arg: 'visqol-keep-source-files',
|
|
859
861
|
},
|
|
860
|
-
}
|
|
862
|
+
}
|
|
861
863
|
|
|
862
864
|
type ConfigDocs = Record<string, { doc: string; format: string; default: string }>
|
|
863
865
|
|
|
@@ -895,10 +897,10 @@ function formatDocs(
|
|
|
895
897
|
* It returns the formatted configuration docs.
|
|
896
898
|
*/
|
|
897
899
|
export function getConfigDocs(): ConfigDocs {
|
|
898
|
-
return formatDocs({}, null, configSchema.getSchema())
|
|
900
|
+
return formatDocs({}, null, convict(configSchema).getSchema())
|
|
899
901
|
}
|
|
900
902
|
|
|
901
|
-
const _schemaProperties = configSchema.getProperties()
|
|
903
|
+
const _schemaProperties = convict(configSchema).getProperties()
|
|
902
904
|
|
|
903
905
|
/** [[include:config.md]] */
|
|
904
906
|
export type Config = typeof _schemaProperties
|
|
@@ -907,7 +909,8 @@ export type Config = typeof _schemaProperties
|
|
|
907
909
|
* Loads the config object.
|
|
908
910
|
*/
|
|
909
911
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
910
|
-
export async function loadConfig(filePath?: string, values?: any): Promise<Config> {
|
|
912
|
+
export async function loadConfig(filePath?: string, values?: any): Promise<Config[]> {
|
|
913
|
+
const configs: Config[] = []
|
|
911
914
|
if (filePath) {
|
|
912
915
|
if (filePath.startsWith('http')) {
|
|
913
916
|
log.debug(`Loading config from url: ${filePath}`)
|
|
@@ -915,41 +918,45 @@ export async function loadConfig(filePath?: string, values?: any): Promise<Confi
|
|
|
915
918
|
if (!res?.data) {
|
|
916
919
|
throw new Error(`Failed to download configuration from: ${filePath}`)
|
|
917
920
|
}
|
|
918
|
-
|
|
921
|
+
values =
|
|
919
922
|
res.contentType === 'application/x-yaml'
|
|
920
923
|
? yaml.parse(res.data)
|
|
921
924
|
: res.contentType === 'application/toml'
|
|
922
925
|
? toml.parse(res.data)
|
|
923
926
|
: json5.parse(res.data)
|
|
924
|
-
configSchema.load(values)
|
|
925
927
|
} else if (existsSync(filePath)) {
|
|
926
928
|
log.debug(`Loading config from local file: ${filePath}`)
|
|
927
929
|
if (filePath.endsWith('.js') || filePath.endsWith('.mjs')) {
|
|
928
930
|
const module = await import(/* webpackIgnore: true */ path.resolve(filePath))
|
|
929
|
-
|
|
931
|
+
values = await module.default()
|
|
930
932
|
} else {
|
|
931
|
-
|
|
933
|
+
const data = String(await fs.promises.readFile(filePath))
|
|
934
|
+
values =
|
|
935
|
+
filePath.endsWith('.yml') || filePath.endsWith('.yaml')
|
|
936
|
+
? yaml.parse(data)
|
|
937
|
+
: filePath.endsWith('.toml')
|
|
938
|
+
? toml.parse(data)
|
|
939
|
+
: json5.parse(data)
|
|
932
940
|
}
|
|
933
941
|
}
|
|
934
|
-
} else if (values) {
|
|
935
|
-
log.debug('Loading config from values.')
|
|
936
|
-
configSchema.load(values)
|
|
937
|
-
} else {
|
|
938
|
-
log.debug('Loading config from default values.')
|
|
939
|
-
configSchema.load({})
|
|
940
942
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
943
|
+
if (!Array.isArray(values)) {
|
|
944
|
+
values = [values || {}]
|
|
945
|
+
}
|
|
946
|
+
for (const value of values) {
|
|
947
|
+
const schema = convict(configSchema)
|
|
948
|
+
schema.load(value || {})
|
|
949
|
+
schema.validate({ allowed: 'strict' })
|
|
950
|
+
configs.push(schema.getProperties())
|
|
951
|
+
}
|
|
952
|
+
log.debug('Using config:', configs)
|
|
953
|
+
return configs
|
|
947
954
|
}
|
|
948
955
|
|
|
949
956
|
function getFunctionDeclaration() {
|
|
950
957
|
const properties: Record<string, { type: string; description: string; nullable?: boolean }> = {}
|
|
951
958
|
const required: string[] = []
|
|
952
|
-
const schema = configSchema.getSchema()
|
|
959
|
+
const schema = convict(configSchema).getSchema()
|
|
953
960
|
|
|
954
961
|
Object.entries(schema._cvtProperties).forEach(([name, value]) => {
|
|
955
962
|
const { format, doc, nullable } = value as SchemaObj
|
|
@@ -996,8 +1003,8 @@ export async function loadConfigFromPrompt(prompt: string) {
|
|
|
996
1003
|
})
|
|
997
1004
|
if (response.functionCalls && response.functionCalls.length > 0) {
|
|
998
1005
|
const functionCall = response.functionCalls[0]
|
|
999
|
-
log.
|
|
1000
|
-
return
|
|
1006
|
+
log.debug('Using function call:', functionCall.name, functionCall.args)
|
|
1007
|
+
return functionCall.args
|
|
1001
1008
|
} else {
|
|
1002
1009
|
throw new Error('No function call found in the response. Please check the prompt and try again.')
|
|
1003
1010
|
}
|
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
|
|
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.
|
|
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
|
-
|
|
475
|
-
const sessionConfig =
|
|
476
|
-
const
|
|
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),
|
|
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
|
-
|
|
514
|
-
|
|
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(
|
|
519
|
-
cert: fs.readFileSync(
|
|
538
|
+
key: fs.readFileSync(keyPath),
|
|
539
|
+
cert: fs.readFileSync(crtPath),
|
|
520
540
|
},
|
|
521
541
|
this.app,
|
|
522
542
|
)
|
package/src/session.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { getSessionThrottleValues, throttleLauncher } from '@vpalmisano/throttler'
|
|
2
2
|
import assert from 'assert'
|
|
3
3
|
import axios from 'axios'
|
|
4
|
-
import chalk from 'chalk'
|
|
5
4
|
import EventEmitter from 'events'
|
|
6
5
|
import fs from 'fs'
|
|
7
6
|
import JSON5 from 'json5'
|
|
@@ -50,6 +49,9 @@ import {
|
|
|
50
49
|
} from './utils'
|
|
51
50
|
import { MediaPath } from './media'
|
|
52
51
|
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
|
+
const { default: chalk } = require('chalk-template')
|
|
54
|
+
|
|
53
55
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
54
56
|
const NavigatorHardwareConcurrency = require('puppeteer-extra-plugin-stealth/evasions/navigator.hardwareConcurrency')
|
|
55
57
|
|