mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
perf(android): queue fragmented scrcpy video packets (#20230)
* perf(android): queue fragmented scrcpy video packets * fix(android): release consumed scrcpy chunk storage * fix(android): bound queued scrcpy fragment count - Coalesce pending video fragments once more than MAX_PENDING_CHUNKS (1024) are queued, so a large frame delivered in tiny socket chunks cannot retain millions of Buffer objects below the 16 MiB byte guard. - Add a regression test feeding a 256 KiB frame one byte at a time and asserting the retained fragment count stays bounded. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import { transform } from 'esbuild'
|
||||
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
||||
import { summarizeBenchmarkSamples } from './benchmark-sample-summary.mjs'
|
||||
|
||||
// git show <ref>:src/main/emulator/android/scrcpy-video-frame-parser.ts | node config/scripts/scrcpy-frame-buffering-benchmark.mjs
|
||||
async function load(source) {
|
||||
const { code } = await transform(source, { loader: 'ts', format: 'esm' })
|
||||
return import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`)
|
||||
}
|
||||
const before = (await load(readFileSync(0, 'utf8'))).parseScrcpyVideoFrames
|
||||
const after = (
|
||||
await load(readFileSync('src/main/emulator/android/scrcpy-video-frame-parser.ts', 'utf8'))
|
||||
).parseScrcpyVideoFrames
|
||||
const { RelayFrameBuffer } = await load(readFileSync('src/shared/relay-frame-buffer.ts', 'utf8'))
|
||||
|
||||
function reader(arm) {
|
||||
if (arm === 'before') {
|
||||
let pending = Buffer.alloc(0)
|
||||
return {
|
||||
read(chunk) {
|
||||
// Match the baseline session's copy before invoking its production parser.
|
||||
const result = before(Buffer.alloc(0), Buffer.concat([pending, chunk]))
|
||||
pending = result.pending
|
||||
return result.frames
|
||||
},
|
||||
pending: () => pending
|
||||
}
|
||||
}
|
||||
const pending = new RelayFrameBuffer()
|
||||
return {
|
||||
read(chunk) {
|
||||
// Include the session's mandatory ownership copy in the queued parser arm.
|
||||
if (chunk.length > 0) {
|
||||
pending.append(Buffer.from(chunk))
|
||||
}
|
||||
return after(pending)
|
||||
},
|
||||
pending: () => (pending.length > 0 ? pending.peek(pending.length) : Buffer.alloc(0))
|
||||
}
|
||||
}
|
||||
|
||||
function packet(size, meta = 123n) {
|
||||
const frame = Buffer.alloc(size + 12, 7)
|
||||
frame.writeBigUInt64BE(meta, 0)
|
||||
frame.writeUInt32BE(size, 8)
|
||||
return frame
|
||||
}
|
||||
|
||||
let seed = 42
|
||||
const random = (max) => {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
return seed % max
|
||||
}
|
||||
let differentialChunks = 0
|
||||
for (let trial = 0; trial < 1000; trial += 1) {
|
||||
const stream = Buffer.concat(
|
||||
Array.from({ length: 1 + random(8) }, (_, index) =>
|
||||
packet(random(256), (BigInt(random(4)) << 62n) | BigInt(index))
|
||||
)
|
||||
)
|
||||
const oldReader = reader('before')
|
||||
const newReader = reader('after')
|
||||
for (let offset = 0; offset < stream.length;) {
|
||||
const size = 1 + random(128)
|
||||
const chunk = stream.subarray(offset, offset + size)
|
||||
assert.deepEqual(newReader.read(chunk), oldReader.read(chunk))
|
||||
assert.deepEqual(newReader.pending(), oldReader.pending())
|
||||
assert.deepEqual(newReader.read(Buffer.alloc(0)), oldReader.read(Buffer.alloc(0)))
|
||||
differentialChunks += 1
|
||||
offset += size
|
||||
}
|
||||
}
|
||||
|
||||
const results = []
|
||||
for (const frameBytes of [32, 4096, 65_536, 1_048_576]) {
|
||||
const frame = packet(frameBytes)
|
||||
const expected = reader('before').read(frame)
|
||||
for (const chunkBytes of new Set([frame.length, 65_536, 4096, 1024])) {
|
||||
if (chunkBytes > frame.length) {
|
||||
continue
|
||||
}
|
||||
const chunks = []
|
||||
for (let offset = 0; offset < frame.length; offset += chunkBytes) {
|
||||
chunks.push(frame.subarray(offset, offset + chunkBytes))
|
||||
}
|
||||
const iterations = Math.max(10, Math.floor(4_194_304 / frameBytes))
|
||||
function run(arm, repeats) {
|
||||
const parser = reader(arm)
|
||||
let frames
|
||||
const started = performance.now()
|
||||
for (let iteration = 0; iteration < repeats; iteration += 1) {
|
||||
for (const chunk of chunks) {
|
||||
frames = parser.read(chunk)
|
||||
}
|
||||
}
|
||||
const ms = performance.now() - started
|
||||
assert.deepEqual(frames, expected)
|
||||
assert.equal(parser.pending().length, 0)
|
||||
return ms
|
||||
}
|
||||
run('before', iterations)
|
||||
run('after', iterations)
|
||||
/** @type {{ before: number[], after: number[] }} */
|
||||
const samples = { before: [], after: [] }
|
||||
for (const pair of buildCounterbalancedSchedule(8, 'before', 'after')) {
|
||||
for (const arm of pair) {
|
||||
samples[arm].push(run(arm, iterations))
|
||||
}
|
||||
}
|
||||
results.push({
|
||||
frameBytes,
|
||||
chunkBytes,
|
||||
iterations,
|
||||
meanMicrosecondsPerFrame: Object.fromEntries(
|
||||
Object.entries(samples).map(([arm, values]) => [
|
||||
arm,
|
||||
(values.reduce((sum, ms) => sum + ms, 0) * 1000) / values.length / iterations
|
||||
])
|
||||
),
|
||||
before: summarizeBenchmarkSamples(samples.before),
|
||||
after: summarizeBenchmarkSamples(samples.after)
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ node: process.version, platform: process.platform, differentialChunks, results },
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user