Files
ArtPlayer/refactor/scripts/profile-core-layout.mjs

151 lines
5.9 KiB
JavaScript

import assert from 'node:assert/strict'
import { spawn } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { chromium } from '@playwright/test'
const directory = fs.mkdtempSync('refactor/.cache/core22-profile-')
const port = 8086
const env = { ...process.env, ARTPLAYER_BROWSER_PORT: String(port) }
delete env.ARTPLAYER_BROWSER_ARTIFACTS
const server = spawn(process.execPath, ['test/browser/server.mjs'], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
let serverLog = ''
server.stdout.on('data', (chunk) => {
serverLog += chunk
})
server.stderr.on('data', (chunk) => {
serverLog += chunk
})
let browser
try {
await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Server startup timeout')), 60000)
const close = (code) => {
clearTimeout(timer)
reject(new Error(`Server exited ${code}: ${serverLog}`))
}
server.once('exit', close)
server.stdout.on('data', () => {
if (serverLog.includes(`Browser test server: http://127.0.0.1:${port}`)) {
clearTimeout(timer)
server.removeListener('exit', close)
resolve()
}
})
})
browser = await chromium.launch()
const page = await browser.newPage({ viewport: { width: 960, height: 720 } })
const errors = []
page.on('pageerror', error => errors.push(error.message))
await page.goto(`http://127.0.0.1:${port}/test/player.html?core=candidate&chapter=published`)
const manifest = await (await page.request.get(`http://127.0.0.1:${port}/test/manifest.json`)).json()
fs.writeFileSync(path.join(directory, 'candidate.js'), await (await page.request.get(`http://127.0.0.1:${port}/candidate/artplayer.js`)).body())
await page.evaluate(() => {
window.profileSamples = []
window.runProfile = async function (count) {
function constructProfilePlayer() {
return new window.Artplayer({ container: '.player', url: '/assets/sample/video.mp4', muted: true })
}
function destroyProfilePlayer(art) {
art.destroy()
}
for (let index = 0; index < count; index++) {
const start = performance.now()
const art = constructProfilePlayer()
const constructed = performance.now()
try {
await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Ready timed out')), 10000)
art.once('ready', () => {
clearTimeout(timer)
resolve()
})
})
await art.play()
await new Promise(resolve => art.video.requestVideoFrameCallback(() => resolve()))
art.pause()
const beforeDestroy = performance.now()
destroyProfilePlayer(art)
window.profileSamples.push({ constructorMs: constructed - start, destroyMs: performance.now() - beforeDestroy })
}
finally {
art.destroy()
}
}
}
})
await page.evaluate(() => window.runProfile(10))
await page.evaluate(() => {
window.profileSamples = []
})
const client = await page.context().newCDPSession(page)
await client.send('Profiler.enable')
await client.send('Profiler.setSamplingInterval', { interval: 100 })
await client.send('Profiler.start')
await page.evaluate(() => window.runProfile(60))
const { profile } = await client.send('Profiler.stop')
const nodes = new Map(profile.nodes.map(node => [node.id, node]))
const parents = new Map()
for (const node of profile.nodes) {
for (const child of node.children || []) parents.set(child, node.id)
}
const totals = { constructor: 0, destroy: 0, other: 0 }
const groups = { constructor: new Map(), destroy: new Map() }
for (let index = 0; index < profile.samples.length; index++) {
const leaf = nodes.get(profile.samples[index])
let node = leaf
let group = 'other'
while (node) {
if (node.callFrame.functionName === 'constructProfilePlayer') {
group = 'constructor'
break
}
if (node.callFrame.functionName === 'destroyProfilePlayer') {
group = 'destroy'
break
}
node = nodes.get(parents.get(node.id))
}
const duration = profile.timeDeltas[index]
totals[group] += duration
if (group !== 'other') {
const frame = leaf.callFrame
const key = `${frame.functionName || '(anonymous)'}:${frame.url}:${frame.lineNumber + 1}`
const result = groups[group].get(key) || { frame, sampledUs: 0, samples: 0 }
result.sampledUs += duration
result.samples++
groups[group].set(key, result)
}
}
const summary = {
kind: 'diagnostic-cpu-sampling',
browser: browser.version(),
node: process.version,
manifest,
intervalRequestedUs: 100,
warmup: 10,
repetitions: 60,
totalsUs: totals,
groups: Object.fromEntries(Object.entries(groups).map(([name, values]) => [name, [...values.values()].sort((a, b) => b.sampledUs - a.sampledUs)])),
timings: await page.evaluate(() => window.profileSamples),
errors,
limit: 'Instrumented Chromium source-build diagnostic only; not an uninstrumented published/candidate performance comparison. Native frames and sampling gaps may be charged to their JS caller. Timings must not be used for performance acceptance.',
}
assert.deepEqual(errors, [])
assert.equal(await page.evaluate(() => window.Artplayer.instances.length), 0)
fs.writeFileSync(path.join(directory, 'profile.cpuprofile'), JSON.stringify(profile))
fs.writeFileSync(path.join(directory, 'summary.json'), `${JSON.stringify(summary, null, 2)}\n`)
console.log(directory)
for (const [name, rows] of Object.entries(summary.groups)) console.log(name, totals[name], rows.slice(0, 12).map(row => ({ name: row.frame.functionName, line: row.frame.lineNumber + 1, sampledUs: row.sampledUs, samples: row.samples })))
}
finally {
try {
await browser?.close()
}
finally {
server.kill()
fs.writeFileSync(path.join(directory, 'server.log'), serverLog)
}
}