@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/app.min.js +1 -1
- package/build/src/app.d.ts +13 -4
- package/build/src/app.js +161 -124
- package/build/src/app.js.map +1 -1
- package/build/src/config.d.ts +1 -1
- package/build/src/config.js +30 -24
- package/build/src/config.js.map +1 -1
- package/build/src/docker.d.ts +1 -0
- package/build/src/docker.js +123 -0
- package/build/src/docker.js.map +1 -0
- package/build/src/server.js +28 -10
- package/build/src/server.js.map +1 -1
- package/build/src/session.js +12 -5
- package/build/src/session.js.map +1 -1
- package/build/src/stats.d.ts +8 -21
- package/build/src/stats.js +48 -17
- package/build/src/stats.js.map +1 -1
- package/build/src/utils.d.ts +6 -2
- package/build/src/utils.js +31 -24
- package/build/src/utils.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +28 -26
- package/src/app.ts +171 -137
- package/src/config.ts +28 -23
- package/src/docker.ts +131 -0
- package/src/server.ts +33 -13
- package/src/session.ts +12 -5
- package/src/stats.ts +56 -25
- package/src/utils.ts +32 -25
package/src/app.ts
CHANGED
|
@@ -23,6 +23,8 @@ import { calculateVisqolScore } from './visqol'
|
|
|
23
23
|
import { calculateVmafScore, convertToIvf, prepareVideo } from './vmaf'
|
|
24
24
|
import path from 'path'
|
|
25
25
|
import { markedTerminal } from 'marked-terminal'
|
|
26
|
+
import { EventEmitter } from 'events'
|
|
27
|
+
import { runWithDocker } from './docker'
|
|
26
28
|
|
|
27
29
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
30
|
const { marked } = require('marked')
|
|
@@ -56,141 +58,167 @@ Default value: \`${value.default}\`
|
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
export class Application extends EventEmitter {
|
|
62
|
+
readonly config: Config
|
|
63
|
+
readonly stats: Stats
|
|
64
|
+
readonly server?: Server
|
|
65
|
+
private mediaPaths: MediaPath[] = []
|
|
66
|
+
|
|
67
|
+
constructor(config: Config) {
|
|
68
|
+
super()
|
|
69
|
+
if (!config.startTimestamp) {
|
|
70
|
+
config.startTimestamp = Date.now()
|
|
71
|
+
}
|
|
72
|
+
this.config = config
|
|
73
|
+
this.stats = new Stats(config)
|
|
74
|
+
if (config.serverPort) {
|
|
75
|
+
this.server = new Server(config, this.stats)
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
await
|
|
75
|
-
} catch (err: unknown) {
|
|
76
|
-
log.error(`visqol score error: ${(err as Error).stack}`)
|
|
79
|
+
async start() {
|
|
80
|
+
log.debug(`start (runDuration: ${this.config.runDuration})`)
|
|
81
|
+
await this.stats.start()
|
|
82
|
+
if (this.server) {
|
|
83
|
+
await this.server.start()
|
|
77
84
|
}
|
|
78
|
-
|
|
79
|
-
}
|
|
85
|
+
const config = this.config
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
// Handle vmaf commands.
|
|
88
|
+
if (config.vmafPrepareVideo) {
|
|
89
|
+
await prepareVideo(config, true)
|
|
90
|
+
}
|
|
91
|
+
if (config.vmafProcessVideo) {
|
|
92
|
+
await convertToIvf(
|
|
93
|
+
config.vmafProcessVideo,
|
|
94
|
+
config.vmafVideoCrop,
|
|
95
|
+
config.vmafKeepSourceFiles,
|
|
96
|
+
config.vmafSkipDuplicated,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
85
99
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
// Handle sessions.
|
|
101
|
+
if (config.sessions > 0) {
|
|
102
|
+
// Prepare fake video and audio.
|
|
103
|
+
if (config.videoPath && !this.mediaPaths.length) {
|
|
104
|
+
for (const videoPath of config.videoPath.split(',')) {
|
|
105
|
+
const ret = await prepareFakeMedia({ ...config, videoPath })
|
|
106
|
+
this.mediaPaths.push(ret)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
89
109
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
await server.start()
|
|
95
|
-
}
|
|
110
|
+
// Network throttle.
|
|
111
|
+
if (config.throttleConfig) {
|
|
112
|
+
await startThrottle(config.throttleConfig)
|
|
113
|
+
}
|
|
96
114
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const mediaPaths: MediaPath[] = []
|
|
101
|
-
if (config.videoPath) {
|
|
102
|
-
for (const videoPath of config.videoPath.split(',')) {
|
|
103
|
-
const ret = await prepareFakeMedia({ ...config, videoPath })
|
|
104
|
-
mediaPaths.push(ret)
|
|
115
|
+
// Download browser if necessary.
|
|
116
|
+
if (!config.chromiumUrl && !config.chromiumPath) {
|
|
117
|
+
await checkChromeExecutable()
|
|
105
118
|
}
|
|
106
|
-
}
|
|
107
119
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
// Start the local sessions.
|
|
121
|
+
if (config.randomAudioPeriod) {
|
|
122
|
+
startRandomActivateAudio(
|
|
123
|
+
this.stats.sessions,
|
|
124
|
+
config.randomAudioPeriod,
|
|
125
|
+
config.randomAudioProbability,
|
|
126
|
+
config.randomAudioRange,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
const spawnPeriod = 1000 / config.spawnRate
|
|
130
|
+
log.debug(`Starting ${config.sessions} sessions (spawnPeriod: ${spawnPeriod}ms)`)
|
|
131
|
+
const startTime = Date.now()
|
|
132
|
+
for (let i = 0; i < config.sessions; i += 1) {
|
|
133
|
+
const id = this.stats.consumeSessionId(config.tabsPerSession)
|
|
134
|
+
await this.startSession(id, spawnPeriod)
|
|
135
|
+
// If not the last session, sleep.
|
|
136
|
+
if (i < config.sessions - 1) {
|
|
137
|
+
await sleep(spawnPeriod)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
|
141
|
+
const spawnRate = (config.sessions * config.tabsPerSession) / elapsed
|
|
142
|
+
log.debug(`${config.sessions * config.tabsPerSession} pages started in ${elapsed}s (${spawnRate.toFixed(2)}/s)`)
|
|
111
143
|
}
|
|
112
144
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
await checkChromeExecutable()
|
|
145
|
+
if (config.runDuration || config.vmafPath || config.visqolPath) {
|
|
146
|
+
setTimeout(() => this.stop(), config.runDuration * 1000)
|
|
116
147
|
}
|
|
148
|
+
}
|
|
117
149
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
private async startSession(id: number, spawnPeriod: number) {
|
|
151
|
+
log.debug(`startSession ${id}`)
|
|
152
|
+
const throttleIndex = getSessionThrottleIndex(id)
|
|
153
|
+
const mediaPath = this.mediaPaths.length ? this.mediaPaths[id % this.mediaPaths.length] : undefined
|
|
154
|
+
const session = new Session({
|
|
155
|
+
...this.config,
|
|
156
|
+
mediaPath,
|
|
157
|
+
spawnPeriod,
|
|
158
|
+
id,
|
|
159
|
+
throttleIndex,
|
|
160
|
+
})
|
|
161
|
+
session.once('stop', () => {
|
|
162
|
+
console.warn(`Session ${id} stopped, reloading...`)
|
|
163
|
+
setTimeout(() => this.startSession(id, spawnPeriod), spawnPeriod)
|
|
164
|
+
})
|
|
165
|
+
this.stats.addSession(session)
|
|
166
|
+
await session.start()
|
|
167
|
+
}
|
|
136
168
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
169
|
+
private async postTest() {
|
|
170
|
+
log.debug('postTest')
|
|
171
|
+
|
|
172
|
+
// vmaf score.
|
|
173
|
+
if (this.config.vmafPath) {
|
|
174
|
+
console.log('Calculating VMAF score...')
|
|
175
|
+
try {
|
|
176
|
+
await calculateVmafScore(this.config)
|
|
177
|
+
} catch (err: unknown) {
|
|
178
|
+
log.error(`vmaf score error: ${(err as Error).stack}`)
|
|
179
|
+
}
|
|
145
180
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
await sleep(spawnPeriod)
|
|
181
|
+
|
|
182
|
+
// visqol score
|
|
183
|
+
if (this.config.visqolPath) {
|
|
184
|
+
console.log('Calculating Visqol score...')
|
|
185
|
+
try {
|
|
186
|
+
await calculateVisqolScore(this.config)
|
|
187
|
+
} catch (err: unknown) {
|
|
188
|
+
log.error(`visqol score error: ${(err as Error).stack}`)
|
|
155
189
|
}
|
|
156
190
|
}
|
|
157
|
-
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
|
158
|
-
const spawnRate = (config.sessions * config.tabsPerSession) / elapsed
|
|
159
|
-
log.debug(`${config.sessions * config.tabsPerSession} pages started in ${elapsed}s (${spawnRate.toFixed(2)}/s)`)
|
|
160
191
|
}
|
|
161
192
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
stop: async (): Promise<void> => {
|
|
165
|
-
log.debug('Stopping')
|
|
193
|
+
async stop(canceled = false) {
|
|
194
|
+
log.debug(`stop (canceled: ${canceled})`)
|
|
166
195
|
|
|
167
|
-
|
|
196
|
+
stopRandomActivateAudio()
|
|
168
197
|
|
|
169
|
-
|
|
198
|
+
await this.stats.stop()
|
|
170
199
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
200
|
+
if (this.config.throttleConfig) {
|
|
201
|
+
await stopThrottle()
|
|
202
|
+
}
|
|
174
203
|
|
|
175
|
-
|
|
204
|
+
stopTimers()
|
|
176
205
|
|
|
177
|
-
|
|
206
|
+
await this.postTest()
|
|
178
207
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
208
|
+
// Copy docker logs to data directory.
|
|
209
|
+
if (this.config.pageLogPath) {
|
|
210
|
+
try {
|
|
211
|
+
const logPath = await getDockerLogsPath()
|
|
212
|
+
const dataDir = path.dirname(this.config.pageLogPath)
|
|
213
|
+
await fs.promises.cp(logPath, path.resolve(dataDir, 'docker.log'))
|
|
214
|
+
} catch (err: unknown) {
|
|
215
|
+
log.debug(`docker logs not found: ${(err as Error).message}`)
|
|
188
216
|
}
|
|
217
|
+
}
|
|
189
218
|
|
|
190
|
-
|
|
219
|
+
this.server?.stop()
|
|
191
220
|
|
|
192
|
-
|
|
193
|
-
},
|
|
221
|
+
this.emit('stop', canceled)
|
|
194
222
|
}
|
|
195
223
|
}
|
|
196
224
|
|
|
@@ -200,52 +228,58 @@ export async function setupApplication(config: Config): Promise<{ stats: Stats;
|
|
|
200
228
|
async function main(): Promise<void> {
|
|
201
229
|
showHelpOrVersion()
|
|
202
230
|
|
|
203
|
-
|
|
231
|
+
const argv = process.argv.slice(2)
|
|
204
232
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
233
|
+
// Handle docker run.
|
|
234
|
+
if (argv.includes('--docker')) {
|
|
235
|
+
try {
|
|
236
|
+
await runWithDocker(argv)
|
|
237
|
+
} catch (err: unknown) {
|
|
238
|
+
log.error(`runWithDocker error: ${(err as Error).stack}`)
|
|
239
|
+
process.exit(1)
|
|
240
|
+
}
|
|
241
|
+
process.exit(0)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let configs: Config[]
|
|
245
|
+
|
|
246
|
+
// Handle prompt.
|
|
247
|
+
if (argv.includes('--prompt')) {
|
|
248
|
+
const params = await loadConfigFromPrompt(argv.filter(s => !['--prompt', '--dry-run'].includes(s)).join(' '))
|
|
212
249
|
if (process.argv.slice(2).includes('--dry-run')) {
|
|
213
250
|
console.log(json5.stringify(params, null, 2))
|
|
214
251
|
process.exit(0)
|
|
215
252
|
}
|
|
216
|
-
|
|
253
|
+
configs = await loadConfig(undefined, params)
|
|
217
254
|
} else {
|
|
218
|
-
|
|
255
|
+
configs = await loadConfig(process.argv[2])
|
|
219
256
|
}
|
|
220
257
|
|
|
221
|
-
if (
|
|
222
|
-
await prepareVideo(config, true)
|
|
223
|
-
process.exit(0)
|
|
224
|
-
}
|
|
258
|
+
if (!configs.length) throw new Error('No configuration found')
|
|
225
259
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
config.vmafVideoCrop,
|
|
230
|
-
config.vmafKeepSourceFiles,
|
|
231
|
-
config.vmafSkipDuplicated,
|
|
232
|
-
)
|
|
233
|
-
process.exit(0)
|
|
234
|
-
}
|
|
260
|
+
let application: Application
|
|
261
|
+
const runNext = () => {
|
|
262
|
+
const config = configs.splice(0, 1)[0]
|
|
235
263
|
|
|
236
|
-
|
|
264
|
+
application = new Application(config)
|
|
265
|
+
application.once('stop', canceled => {
|
|
266
|
+
if (!canceled && configs.length) {
|
|
267
|
+
log.info(`Application stopped, running next (${configs.length} left)...`)
|
|
268
|
+
runNext()
|
|
269
|
+
} else {
|
|
270
|
+
process.exit(0)
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
return application.start()
|
|
274
|
+
}
|
|
237
275
|
|
|
238
|
-
const stop = async ()
|
|
276
|
+
const stop = async () => {
|
|
239
277
|
console.log('Exiting...')
|
|
240
|
-
|
|
241
|
-
await stopApplication()
|
|
242
|
-
|
|
243
|
-
process.exit(0)
|
|
278
|
+
await application.stop(true)
|
|
244
279
|
}
|
|
245
280
|
registerExitHandler(() => stop())
|
|
246
281
|
|
|
247
|
-
|
|
248
|
-
setTimeout(stop, config.runDuration * 1000)
|
|
282
|
+
await runNext()
|
|
249
283
|
|
|
250
284
|
// Command line interface.
|
|
251
285
|
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,
|
|
@@ -859,7 +859,7 @@ E.g. \`{ w: "iw-10", h: "ih-5", x: "10", y: '5' }\``,
|
|
|
859
859
|
env: 'VISQOL_KEEP_SOURCE_FILES',
|
|
860
860
|
arg: 'visqol-keep-source-files',
|
|
861
861
|
},
|
|
862
|
-
}
|
|
862
|
+
}
|
|
863
863
|
|
|
864
864
|
type ConfigDocs = Record<string, { doc: string; format: string; default: string }>
|
|
865
865
|
|
|
@@ -897,10 +897,10 @@ function formatDocs(
|
|
|
897
897
|
* It returns the formatted configuration docs.
|
|
898
898
|
*/
|
|
899
899
|
export function getConfigDocs(): ConfigDocs {
|
|
900
|
-
return formatDocs({}, null, configSchema.getSchema())
|
|
900
|
+
return formatDocs({}, null, convict(configSchema).getSchema())
|
|
901
901
|
}
|
|
902
902
|
|
|
903
|
-
const _schemaProperties = configSchema.getProperties()
|
|
903
|
+
const _schemaProperties = convict(configSchema).getProperties()
|
|
904
904
|
|
|
905
905
|
/** [[include:config.md]] */
|
|
906
906
|
export type Config = typeof _schemaProperties
|
|
@@ -909,7 +909,8 @@ export type Config = typeof _schemaProperties
|
|
|
909
909
|
* Loads the config object.
|
|
910
910
|
*/
|
|
911
911
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
912
|
-
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[] = []
|
|
913
914
|
if (filePath) {
|
|
914
915
|
if (filePath.startsWith('http')) {
|
|
915
916
|
log.debug(`Loading config from url: ${filePath}`)
|
|
@@ -917,41 +918,45 @@ export async function loadConfig(filePath?: string, values?: any): Promise<Confi
|
|
|
917
918
|
if (!res?.data) {
|
|
918
919
|
throw new Error(`Failed to download configuration from: ${filePath}`)
|
|
919
920
|
}
|
|
920
|
-
|
|
921
|
+
values =
|
|
921
922
|
res.contentType === 'application/x-yaml'
|
|
922
923
|
? yaml.parse(res.data)
|
|
923
924
|
: res.contentType === 'application/toml'
|
|
924
925
|
? toml.parse(res.data)
|
|
925
926
|
: json5.parse(res.data)
|
|
926
|
-
configSchema.load(values)
|
|
927
927
|
} else if (existsSync(filePath)) {
|
|
928
928
|
log.debug(`Loading config from local file: ${filePath}`)
|
|
929
929
|
if (filePath.endsWith('.js') || filePath.endsWith('.mjs')) {
|
|
930
930
|
const module = await import(/* webpackIgnore: true */ path.resolve(filePath))
|
|
931
|
-
|
|
931
|
+
values = await module.default()
|
|
932
932
|
} else {
|
|
933
|
-
|
|
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)
|
|
934
940
|
}
|
|
935
941
|
}
|
|
936
|
-
} else if (values) {
|
|
937
|
-
log.debug('Loading config from values.')
|
|
938
|
-
configSchema.load(values)
|
|
939
|
-
} else {
|
|
940
|
-
log.debug('Loading config from default values.')
|
|
941
|
-
configSchema.load({})
|
|
942
942
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
|
949
954
|
}
|
|
950
955
|
|
|
951
956
|
function getFunctionDeclaration() {
|
|
952
957
|
const properties: Record<string, { type: string; description: string; nullable?: boolean }> = {}
|
|
953
958
|
const required: string[] = []
|
|
954
|
-
const schema = configSchema.getSchema()
|
|
959
|
+
const schema = convict(configSchema).getSchema()
|
|
955
960
|
|
|
956
961
|
Object.entries(schema._cvtProperties).forEach(([name, value]) => {
|
|
957
962
|
const { format, doc, nullable } = value as SchemaObj
|
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 development 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
|
+
}
|