fix(danmuku): [PKG-DANMUKU-MASK-LOAD-01] recover stalled-frame rows and remove relaxed placement backlog

This commit is contained in:
Harvey Zhao committed 2026-09-14 22:13:08 +08:00
1 parent 475d38043e
commit 314a1c3163
27 files changed
+14045 -195

No files matched your search

+8 -1
View File
@@ -253,7 +253,14 @@ See refactor/changes/2026-09-14-PKG-MASK-05-native-checkpoint.md for exact limit
Its combination cases now include frozen/current Danmuku and a bounded 120-row
load while the actual model continues. `danmuku-combination-load.js` observes
real media/RAF and cleans up its test listeners; it does not seed eligible rows.
Candidate completeness failures remain failing under PKG-DANMUKU-MASK-LOAD-01.
The candidate must deliver all 120 rows exactly once and recycle their references;
original failures and the PKG-DANMUKU-MASK-LOAD-01 repair remain separate evidence.
For a Chromium diagnostic only, set `ARTPLAYER_MASK_PROFILE=1` to attach the actual
CDP CPU profile around the load. This never stops inference or patches the SDK.
Ordinary format acceptance runs without profiling. Final load snapshots also
record pending/reserved row IDs to distinguish missed sampling from placement
backlog. Use `ARTPLAYER_DANMUKU_ARTIFACT` with `ARTPLAYER_MASK_ARTIFACT` to bind both
candidate plugins to the exact main/legacy files being tested.
`danmuku-dpip.spec.js` also includes two-window load cases and both plugin versions.
Frozen-plugin retained DOM, candidate cleanup and unsupported API outcomes remain
distinct; the old plugin's observed loss is not complete-delivery acceptance.
+11 -9
View File
@@ -19,8 +19,8 @@ test.beforeAll(async () => {
for (const core of ['published', 'candidate']) {
for (const implementation of ['published', 'candidate']) {
for (const mode of [0, 1]) {
test(`${core} core / ${implementation} plugin / mode ${mode}: Worker waiting does not consume visible lifetime`, async ({ page }, testInfo) => {
for (const [mode, antiOverlap] of [0, 1].flatMap(mode => [true, false].map(antiOverlap => [mode, antiOverlap]))) {
test(`${core} core / ${implementation} plugin / mode ${mode} / antiOverlap ${antiOverlap}: placement preserves visible lifetime`, async ({ page }, testInfo) => {
await page.goto(`/test/player.html?core=${core}`)
await page.evaluate(() => {
const evidence = window.lifetimeEvidence = { posts: [], replies: [], visible: [], recycled: [] }
@@ -50,11 +50,11 @@ for (const core of ['published', 'candidate']) {
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
// Install after native playing, then start once. The published duplicate
// play/playing loop defect is covered elsewhere and is not this timing case.
await page.evaluate(async (mode) => {
await page.evaluate(async ({ mode, antiOverlap }) => {
const art = window.art
const evidence = window.lifetimeEvidence
art.on('artplayerPluginDanmuku:visible', row => evidence.visible.push({ wall: performance.now(), clock: Date.now(), startedAt: row.$lastStartTime, rest: row.$restTime, mode: row.mode, time: art.currentTime, transition: row.$ref.style.transition }))
art.plugins.add(window.artplayerPluginDanmuku({ danmuku: [], speed: 1, antiOverlap: false, heatmap: false, emitter: false }))
art.plugins.add(window.artplayerPluginDanmuku({ danmuku: [], speed: 1, antiOverlap, heatmap: false, emitter: false }))
const plugin = art.plugins.artplayerPluginDanmuku
const owner = plugin.config({})
const makeWait = owner.makeWait
@@ -65,16 +65,18 @@ for (const core of ['published', 'candidate']) {
}
await plugin.load([{ id: 'delayed', text: 'Full visible lifetime', time: art.currentTime + 0.4, mode }])
owner.start()
}, mode)
}, { mode, antiOverlap })
await expect.poll(() => page.evaluate(() => window.lifetimeEvidence.recycled.length)).toBe(1)
await page.click('#pause')
const evidence = await page.evaluate(() => ({ ...window.lifetimeEvidence, video: { width: window.art.video.videoWidth, time: window.art.currentTime } }))
await testInfo.attach('danmuku-lifetime', { contentType: 'application/json', body: JSON.stringify({ core, implementation, mode, sha256: hash(code), evidence, scope: 'Native video and real Worker geometry with controlled 1500ms request delay; does not assert naturally occurring Worker starvation.' }) })
expect(evidence.posts).toHaveLength(1)
expect(evidence.replies).toHaveLength(1)
const workerPlacement = implementation === 'published' || antiOverlap
await testInfo.attach('danmuku-lifetime', { contentType: 'application/json', body: JSON.stringify({ core, implementation, mode, antiOverlap, workerPlacement, sha256: hash(code), evidence, scope: 'Native video; Worker path has a controlled 1500ms request delay, candidate relaxed path must bypass it. Both candidate paths require full visible lifetime. Does not assert naturally occurring Worker starvation.' }) })
expect(evidence.posts).toHaveLength(workerPlacement ? 1 : 0)
expect(evidence.replies).toHaveLength(workerPlacement ? 1 : 0)
expect(evidence.visible).toHaveLength(1)
expect(evidence.video.width).toBeGreaterThan(0)
expect(evidence.replies[0].wall - evidence.posts[0].wall).toBeGreaterThanOrEqual(1400)
if (workerPlacement)
expect(evidence.replies[0].wall - evidence.posts[0].wall).toBeGreaterThanOrEqual(1400)
const lifetime = evidence.recycled[0].wall - evidence.visible[0].wall
if (implementation === 'published') {
// Preserve the historical defect as explicit evidence, not the candidate contract.
+17 -1
View File
@@ -265,7 +265,23 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
expect(restored.mode).toBe('alpha')
await testInfo.attach('actual-combination-page', { body: await page.screenshot(), contentType: 'image/png' })
const outputsBeforeLoad = await page.evaluate(() => window.maskCombination.times.length)
load = await observeDanmukuLoad(page, { label: 'Mask load', requireComplete: implementation === 'candidate' })
let profiler
if (process.env.ARTPLAYER_MASK_PROFILE === '1') {
assert.equal(testInfo.project.name, 'chromium', 'CDP profiling requires the Chromium diagnostic subset')
profiler = await page.context().newCDPSession(page)
await profiler.send('Profiler.enable')
await profiler.send('Profiler.start')
}
try {
load = await observeDanmukuLoad(page, { label: 'Mask load', requireComplete: implementation === 'candidate' })
}
finally {
if (profiler) {
const { profile } = await profiler.send('Profiler.stop')
await testInfo.attach('mask-cpu-profile', { contentType: 'application/json', body: JSON.stringify(profile) })
await profiler.detach()
}
}
load.modelOutputs = await page.evaluate(before => window.maskCombination.times.length - before, outputsBeforeLoad)
expect(load.modelOutputs).toBeGreaterThan(1)
await page.evaluate(() => window.art.destroy(false))
@@ -93,7 +93,7 @@ for (const implementation of ['published', 'candidate']) {
const visible = new Set(evidence.visible.map(row => row.id))
return { ...evidence, rows, missing: rows.filter(row => !visible.has(row.id)).map(row => row.id), media: { time: window.art.currentTime, width: window.art.video.videoWidth } }
})
await testInfo.attach('danmuku-timing-observation', { contentType: 'application/json', body: JSON.stringify({ implementation, scenario, sha256: hash(code), observation, scope: 'Diagnostic only: native timing with controlled load, original readys getter and unmodified queue states. Missing rows are reported, not waived as stability acceptance.' }) })
await testInfo.attach('danmuku-timing-observation', { contentType: 'application/json', body: JSON.stringify({ implementation, scenario, sha256: hash(code), observation, scope: 'Native timing with controlled load, original readys getter and unmodified queue states. Candidate must recover both CPU and asynchronous gaps; published CPU loss is observed separately. This is not sustained-load acceptance.' }) })
expect(observation.media.width).toBeGreaterThan(0)
expect(observation.rows).toHaveLength(3)
expect(observation.frames.length).toBeGreaterThan(2)
@@ -111,6 +111,10 @@ for (const implementation of ['published', 'candidate']) {
expect(observation.callbacks[index].start).toBeGreaterThanOrEqual(observation.callbacks[index - 1].end)
}
}
if (implementation === 'candidate') {
expect(observation.missing).toEqual([])
expect(observation.visible.map(row => row.id)).toEqual(['first', 'middle', 'sentinel'])
}
await page.evaluate(() => window.art.destroy())
})
}