mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-19 03:11:20 -08:00
test(danmuku): [PKG-DANMUKU-07] verify native load and shared layer stability
This commit is contained in:
1 parent
4f559cb31d
commit
00e81309ca
14 files changed
+6724
-11
No files matched your search
@@ -136,6 +136,14 @@ legacy build targets remain es2020 and es2015; syntax lowering does not polyfill
|
||||
|
||||
## Scheduling ownership
|
||||
|
||||
The root `art.template.$danmuku` belongs to the core template, not this plugin.
|
||||
The Mask plugin captures this layer and the core video, and listens to core
|
||||
ready/destroy; it does not read Danmuku queues, owners or plugin events. Renderer
|
||||
cleanup owns individual comment nodes. Reset/replacement must preserve the root
|
||||
identity and external mask styles; hide/show owns only its visibility opacity.
|
||||
CSS-mask boundary tests do not establish model inference, SDK cleanup or complete
|
||||
Mask integration. Those remain responsibilities of the Mask migration tasks.
|
||||
|
||||
Keep at most one pending RAF and one asynchronous preparation batch. Native `play`
|
||||
and `playing` both invoke the existing start path and retain its events, but
|
||||
must not create competing loops. Ready items precede wait items; wait selection
|
||||
|
||||
@@ -53,7 +53,10 @@
|
||||
`setBackend` fulfilled false 视为已执行 fallback。
|
||||
|
||||
registrar 捕获 `art.template.$video/$danmuku`,订阅 ready→start、destroy→stop。
|
||||
必须在 Danmuku 插件建立 `$danmuku` 后注册;demo 正是先 Danmuku 再 Mask。
|
||||
需要核心模板已经提供 `$danmuku/$video`;demo 使用先 Danmuku 再 Mask 的顺序。
|
||||
2026-09-13(PKG-DANMUKU-07)纠正初始记录:`$danmuku` 由核心模板创建,
|
||||
不是 Danmuku 插件创建,因此 demo 顺序本身不构成 Mask 对 Danmuku 注册的硬依赖。
|
||||
Mask 源码不读取 Danmuku 队列、owner 或插件事件;完整模型组合仍待 Mask 验收。
|
||||
start 建立 2D canvas 并设置 `$danmuku.style`:maskMode=alpha、maskSize=contain、
|
||||
maskRepeat=no-repeat、backgroundSize=contain、backgroundRepeat=no-repeat。
|
||||
每轮以视频固有尺寸推理,白色前景/黑色背景生成 binary mask,drawMask 后将 RGB
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,66 @@
|
||||
# PKG-DANMUKU-07 弹幕稳定性与共享层边界
|
||||
|
||||
已完成本任务限定的验收,任务及提交状态以tasks.json和Git为准。此项对12后的生产
|
||||
实现增加原生负载和资源验收,没有改变运行时、声明、依赖、版本或构建内容。
|
||||
|
||||
## 验收范围
|
||||
|
||||
- 候选核心中比较固定npm5.3.0插件与候选源码,三引擎,每秒2和20条,各三轮
|
||||
14秒真实媒体播放。每轮追加12秒的时间戳序列,三轮总计72/720条;其余时间
|
||||
让弹幕自然显示和到期。保留真实RAF、视频时钟、Worker与原始readys getter。
|
||||
- 候选逐项检查采样/显示无丢失,三轮总显示和回收均恰好一次;队列追加语义、
|
||||
每条对象只属于一个对应状态池、空闲池唯一性、根层节点和空闲池一致均受断言保护。
|
||||
包装原始节点getter只记录身份,确认实际重复使用旧节点,并且有空闲节点时
|
||||
不绕过池分配新节点;Worker必须存在且唯一,正常请求全部回复,销毁恰好一次。
|
||||
- 旧插件结果保留为对照观察,没有把其可能的历史缺陷豁免用于候选断言。
|
||||
每轮帧间隔单独统计;包含暂停/加载转换的总采样间隔另列,不能当纯播放卡顿。
|
||||
可选performance.memory是未强制GC的读数,不能证明泄漏或作跨引擎内存排名。
|
||||
- 两核心、三引擎运行已有资源用例:共享挂载、初始化回滚、发送取消/锁、原始
|
||||
错误、destroy(false)、替换加载移除被移动的节点、热图实例隔离。
|
||||
- 新增两核心的CSS mask边界用例:注册前就取得核心根层,以固定内联SVG mask
|
||||
验证emit/reset/无参替换load/hide/show/destroy(false)中的层引用和样式保持,
|
||||
销毁后两帧没有晚请求和新子节点。此用例设置资源测试的ready前置状态、采用
|
||||
原生0.25播放速率,不能当时间戳负载证据;负载用例没有这些设置。
|
||||
|
||||
## 问题处置与兼容性
|
||||
|
||||
早期原生时间窗口诊断区分了两个来源:800ms主线程完全阻塞,及600ms用户异步
|
||||
回调使旧调度停止采样。11独立修复隐藏Worker等待吞掉显示寿命;12独立修复异步
|
||||
放置期间的采样和到期维护。07继续检查这些修复后的实际负载及资源状态。
|
||||
没有把公开±0.1秒窗口改成无限追赶,因此完全阻塞时未被采样的时间点仍可错过。
|
||||
|
||||
本轮首个测试版本错误地把`load(rows)`当作替换,第二轮队列断言收到48/480而
|
||||
错误预期24/240。核实原实现后修正累计队列预期,当轮显示数仍精确24/240;没有
|
||||
修改生产语义。初步12项通过后,经独立审查补足Worker非空、节点身份复用、
|
||||
逐条状态归属和跨轮恰好一次的断言,最终需按补强后的文件重新验证。
|
||||
|
||||
Mask仅依赖核心提供的video/弹幕根层及ready/destroy,不读取弹幕队列、Owner
|
||||
或插件事件。旧契约文档误称根层由Danmuku建立,已依据核心模板纠正并注明日期。
|
||||
固定发布归档和生产Mask源码未改。Mask自身晚推理复活RAF、模型dispose、并发
|
||||
start、模型和完整组合问题仍由Mask迁移任务处理,不能从本CSS测试判定通过。
|
||||
|
||||
## 维护与重跑
|
||||
|
||||
```sh
|
||||
yarn test:browser test/browser/danmuku-stability.spec.js test/browser/danmuku-mask-boundary.spec.js test/browser/danmuku-resources.spec.js --workers=2
|
||||
```
|
||||
|
||||
负载测试的90秒单例deadline来自三轮42秒媒体预算及设置/销毁开销;没有扩大
|
||||
已有短用例超时。每次运行必须等待进程终态,归档report/results,再运行另一格式。
|
||||
正常来源是当前源码UMD;两个新用例均支持ARTPLAYER_DANMUKU_ARTIFACT。
|
||||
|
||||
包内ARCHITECTURE.md与refactor/danmuku-stability.md记录根层归属、诊断及限制。
|
||||
07未修改生产,因此复用12已记录的264项源码/136项产物单测、严格类型/构建,及
|
||||
其异步/几何/生命周期验证;不声称这些由本次持续负载套件重新完成。
|
||||
08完整组合、09最终分发、真实Mask模型/设备以及全项目发布复盘继续保留。
|
||||
回退本任务只撤销测试和文档,不应回退11/12已交付修复。
|
||||
|
||||
## 最终结果
|
||||
|
||||
补强后的66项浏览器检查全部通过(12项负载、6项CSS边界、48项资源),0重试、
|
||||
0跳过。每个负载用例完成三轮42秒原生媒体推进,候选两档各72/720条均完整且
|
||||
只显示/回收一次。精确节点复用、堆读数和帧间隔见
|
||||
[机器证据](../baselines/danmuku-stability-validation.json)。
|
||||
新测试lint通过;影响分析检查通过,保留Danmuku/Mask组合验证边,仅纠正其
|
||||
“Danmuku创建根层”的错误原因,不降低CI范围。测试由现有Playwright目录自动发现。
|
||||
未运行远端CI、未推送或发布。
|
||||
@@ -0,0 +1,70 @@
|
||||
# PKG-DANMUKU-07 稳定性工作记录
|
||||
|
||||
本任务已在12后的运行时完成下述限定范围验收,任务与提交状态以tasks.json及Git为准。
|
||||
06类型检查、11/12修复与本任务证据分别维护;真实模型、完整组合和发布验收仍待后续任务。
|
||||
|
||||
## 已执行的时间窗口诊断
|
||||
|
||||
`test/browser/danmuku-timing-diagnostic.spec.js`在候选核心中比较实际npm5.3.0插件与
|
||||
06候选源码,使用本地90秒视频的短片段、原生RAF/媒体时钟和真实Worker。独立RAF
|
||||
观察与实际readys getter采样分别记录;getter只包装读取,不修改返回数组或状态。
|
||||
每例3条弹幕,后置sentinel证明间隔后仍能正常发送。12项诊断通过意味着取证完成,
|
||||
不代表漏掉弹幕的结果可以作为稳定性通过。
|
||||
|
||||
| 场景 | Chromium | Firefox | Windows WebKit | 新旧共同结果 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 注入800ms主线程工作 | 最大媒体帧间隔约800ms | 约777–809ms | 约838–856ms | 前两条未进入任何readys采样,sentinel显示 |
|
||||
| beforeVisible等待600ms | 原生帧间隔约19–21ms,采样间隔616–618ms | 帧44ms,采样583–609ms | 帧63–64ms,采样622–647ms | 第一条与sentinel显示,中间条未采样 |
|
||||
|
||||
这解释了两种不同来源:前者浏览器主线程无法及时调度,后者浏览器仍在出帧,但
|
||||
插件等待当前异步准备完成而不再读取新的候选窗口。±0.1秒和串行回调都有明确
|
||||
历史语义;后续方案必须比较seek、hide/show、stop/start和false重试边界,不能
|
||||
直接改成无限追赶或并行回调。当前不据此声称全部真实负载问题已解决。
|
||||
|
||||
报告位于refactor/.cache/danmuku07-timing-diagnostic.json,日志及附件保留同名前缀。
|
||||
候选是11计时修复之前的06源码,具体构建SHA在每项danmuku-timing-observation中;
|
||||
后续变更需说明哪些观察仍适用,不把旧报告冒充新提交的验证。
|
||||
|
||||
## 当前下一步
|
||||
|
||||
以下为本轮执行入口的历史清单,现已通过最终66项原生用例落实本任务范围。
|
||||
下一实施阶段为08组合或其他已就绪包任务;09分发和真实Mask模型/设备仍不能据此关闭。
|
||||
|
||||
1. 11计时修复和12采样修复已分别提交并核验;当前HEAD为
|
||||
`4f559cb31d6b69a37439c6db2d32ca28ae5c78f3`,恢复本任务。
|
||||
12源码/main/legacy各102项浏览器通过,慢回调中间行已恢复;CPU完全阻塞
|
||||
的未采样窗口仍保留。见[12证据](baselines/danmuku-frame-sampling-validation.json)。
|
||||
2. 对实际持续播放记录scheduled、readys、callback、placed、visible、recycled及
|
||||
原生RAF/媒体间隔,区分false、拒绝、轨道满、取消和错过采样。
|
||||
3. 比较正常密度、密集队列和连续生命周期后的节点池、Worker、监听器与计时器。
|
||||
已有受控三小时快进只证明离散状态,不是真实三小时播放或内存测量。
|
||||
4. 根据证据决定是否需要兼容内部修复,并为每个独立修复保留任务和commit。
|
||||
5. 明确Mask可依赖的当前队列/DOM/事件边界,再交付07验收与后续08组合矩阵。
|
||||
|
||||
不增加任意弹幕上限,不通过修改媒体时钟、预置ready或放宽断言伪造负载通过。
|
||||
|
||||
## 本轮新增的持续播放与边界取证
|
||||
|
||||
`test/browser/danmuku-stability.spec.js` 使用候选核心、实际npm5.3.0/候选插件、
|
||||
原生视频/RAF/Worker,每秒2或20条,各三轮14秒实际媒体推进。每轮追加12秒
|
||||
的时间戳序列,余下时间用于自然显示和回收;逐条记录采样/显示/回收,同时记录
|
||||
状态池、节点/空闲池、真实帧和可选未强制GC的堆内存读数。不是三小时耐久测试,
|
||||
也不把不同浏览器的performance.memory当作可比的泄漏证明。
|
||||
|
||||
首轮Chromium两个用例因测试误把`load(rows)`当作替换,在第二轮队列断言失败:
|
||||
收到48/480,错误预期24/240。核对运行实现后按既有追加契约修正为累计队列数,
|
||||
当轮显示数量仍严格断言24/240;没有改变生产API。失败报告与trace保留在
|
||||
`refactor/.cache/danmuku07-stability-first.json`及同前缀results中。
|
||||
|
||||
Mask静态审查已确认其直接依赖只有核心video/root层/ready/destroy,未读取弹幕
|
||||
队列或Owner。已纠正旧契约对根层创建归属的错误描述;核心模板及节点绑定为依据。
|
||||
后续CSS mask边界测试明确不加载真实模型,不代替Mask03/05的候选生命周期与组合验收。
|
||||
|
||||
最终补强了非空Worker、实际节点复用、每条状态归属和跨轮显示/回收恰好一次检查。
|
||||
12负载、6共享mask层、48资源测试共66项在三引擎全部通过,0重试/跳过。
|
||||
见[变更记录](changes/2026-09-13-PKG-DANMUKU-07-stability.md)和
|
||||
[机器证据](baselines/danmuku-stability-validation.json)。
|
||||
|
||||
本次候选每秒2条用例三轮72条只分配3个节点、实际复用69次;每秒20条三轮720条
|
||||
分配21–22个节点、复用698–699次。旧版对应分配6及24–25个节点。两者该正常
|
||||
同步回调负载下都完整显示;差异是本次原生样本观察,不是通用吞吐或长期内存承诺。
|
||||
@@ -121,7 +121,7 @@
|
||||
{
|
||||
"dependency": "artplayer-plugin-danmuku",
|
||||
"consumer": "artplayer-plugin-danmuku-mask",
|
||||
"reason": "Mask renders into the $danmuku element supplied by the danmuku plugin",
|
||||
"reason": "Mask and Danmuku share the core-owned $danmuku layer; retain combination validation for root identity, styles and lifecycle",
|
||||
"evidence": [
|
||||
"packages/artplayer-plugin-danmuku-mask/src/index.js",
|
||||
"docs/assets/example/danmuku.mask.js"
|
||||
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 231 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 68 / doing 15 / blocked 0 / done 148 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 68 / doing 14 / blocked 0 / done 149 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -315,7 +315,7 @@
|
||||
| PKG-DANMUKU-04 | artplayer-plugin-danmuku<br>整理时钟、队列与轨道调度 | PKG-DANMUKU-03, CORE-10 | danmuku 调度器和确定性时钟测试 | seek/暂停/倍率/长时间运行无顺序和碰撞回归 | H | done |
|
||||
| PKG-DANMUKU-05 | artplayer-plugin-danmuku<br>整理 DOM 渲染、设置、热力图与 worker | PKG-DANMUKU-04, CORE-14, CORE-18 | renderer/setting/heatmap/worker 职责及资源归属 | mount/icons/设置和 worker 协议保持,销毁无后台工作 | H | done |
|
||||
| PKG-DANMUKU-06 | artplayer-plugin-danmuku<br>迁移 TS 与公开声明 | PKG-DANMUKU-05, ENG-04, ENG-06 | 自有模块、worker 消息、option/item/result 类型 | 旧参数、扩展字段和事件消费通过,vendored 边界清楚 | H | done |
|
||||
| PKG-DANMUKU-07 | artplayer-plugin-danmuku<br>弹幕能力稳定性验收 | PKG-DANMUKU-06, PKG-DANMUKU-10, PKG-DANMUKU-11, PKG-DANMUKU-12 | 负载/渲染/资源对比及 mask 可依赖的稳定边界 | 在试点核心中旧插件 API 与关键帧路径通过 | H | doing |
|
||||
| PKG-DANMUKU-07 | artplayer-plugin-danmuku<br>弹幕能力稳定性验收 | PKG-DANMUKU-06, PKG-DANMUKU-10, PKG-DANMUKU-11, PKG-DANMUKU-12 | 负载/渲染/资源对比及 mask 可依赖的稳定边界 | 在试点核心中旧插件 API 与关键帧路径通过 | H | done |
|
||||
| PKG-DANMUKU-08 | artplayer-plugin-danmuku<br>完成新旧核心与组合验收 | PKG-DANMUKU-07, CORE-22 | 最终核心/旧核心、mask/fullscreen/PiP 组合报告 | 性能无未解释退化,反复装卸无累计资源 | H | todo |
|
||||
| PKG-DANMUKU-09 | artplayer-plugin-danmuku<br>完成分发、示例与文档 | PKG-DANMUKU-08, ENG-07 | danmuku.js、README、声明、worker 与产物 | tarball 和静态 icons 等旧调用通过,有独立回退版本 | H | todo |
|
||||
| PKG-DANMUKU-10 | artplayer-plugin-danmuku<br>修复 issue958 密集热力图过高与曲线裁平 | PKG-DANMUKU-05 | 自动密度缩放与曲线边界、历史自定义坐标兼容及真实浏览器红绿证据 | 16000条三种分布在两核心三引擎和source/main/legacy不遮挡;显式轴和自定义points保持旧解释,有单独commit | M | done |
|
||||
@@ -562,6 +562,7 @@
|
||||
- PKG-DANMUKU-04: [记录](changes/2026-09-13-PKG-DANMUKU-04-scheduler.md) [记录](baselines/danmuku-scheduler-validation.json)
|
||||
- PKG-DANMUKU-05: [记录](changes/2026-09-13-PKG-DANMUKU-05-resources.md) [记录](baselines/danmuku-resources-validation.json)
|
||||
- PKG-DANMUKU-06: [记录](changes/2026-09-13-PKG-DANMUKU-06-types.md) [记录](baselines/danmuku-types-validation.json)
|
||||
- PKG-DANMUKU-07: [记录](changes/2026-09-13-PKG-DANMUKU-07-stability.md) [记录](baselines/danmuku-stability-validation.json) [记录](danmuku-stability.md)
|
||||
- PKG-CANVAS-01: [记录](changes/2026-09-12-PKG-CANVAS-01-contract.md) [记录](baselines/canvas-contract.md) [记录](baselines/canvas-release.json) [记录](canvas-validation.md) [记录](baselines/canvas-contract-validation.json)
|
||||
- PKG-CANVAS-02: [记录](changes/2026-09-12-PKG-CANVAS-02-tests.md) [记录](baselines/canvas-behavior-validation.json) [记录](canvas-validation.md)
|
||||
- PKG-CANVAS-03: [记录](changes/2026-09-12-PKG-CANVAS-03-lifecycle.md) [记录](baselines/canvas-lifecycle-validation.json) [记录](canvas-validation.md)
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-DANMUKU-07 弹幕稳定性阶段验收完成
|
||||
|
||||
12后的源码在三引擎完成新旧插件两档负载对照,每例三轮42秒原生播放;候选
|
||||
72/720条全部采样、显示并回收一次,真实节点复用、状态池唯一归属和Worker清理通过。
|
||||
两核心共享CSS mask根层与已有资源回归通过;最终12+6+48共66项,0重试/跳过。
|
||||
首轮错误测试把load(rows)当替换,修正为历史追加契约并保留失败报告;独立审查
|
||||
补足复用与跨轮重复断言。Mask不依赖队列,根层属于核心;纠正文档和影响映射原因,
|
||||
保留组合CI范围。lint及影响检查通过,没有生产/声明/依赖变化,复用12的相关验证。
|
||||
见[变更](changes/2026-09-13-PKG-DANMUKU-07-stability.md)和
|
||||
[证据](baselines/danmuku-stability-validation.json)。DANMUKU-LOAD-01按已解释来源及
|
||||
11/12独立修复闭环;CPU阻塞下旧窗口不追赶的边界明确保留,不声称无条件无漏弹幕。
|
||||
231项:149 done、14 doing、68 todo。08完整组合、09分发、真实Mask模型和设备/
|
||||
发布复盘继续;独立本地提交,不推送、不发布。
|
||||
|
||||
## PKG-DANMUKU-12 持续采样与串行放置完成
|
||||
|
||||
异步beforeVisible/Worker等待期间继续原生RAF采样和可见寿命维护,以内部身份
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
| DANMUKU-SOURCE-01 | open / 已复现 | Danmuku registry Git associations differ from releases and static icon provenance remains incomplete | PKG-DANMUKU-09 |
|
||||
| DANMUKU-TRACK-01 | resolved / 已复现 | Native simultaneous fixed Danmuku comments can overlap with duplicate start loops and repeated Worker IDs | PKG-DANMUKU-04, PKG-DANMUKU-05, PKG-DANMUKU-07 |
|
||||
| DANMUKU-INPUT-02 | resolved / 已复现 | Intermediate explicit-zero correction regressed accepted NaN timestamp fallback | PKG-DANMUKU-04 |
|
||||
| DANMUKU-LOAD-01 | open / 待取证 | Native lifecycle fixtures sometimes miss timestamp eligibility during concurrent workload; real frame/window behavior needs load evidence | PKG-DANMUKU-07 |
|
||||
| DANMUKU-LOAD-01 | resolved / 已复现 | Native lifecycle fixtures sometimes miss timestamp eligibility during concurrent workload; real frame/window behavior needs load evidence | PKG-DANMUKU-07 |
|
||||
| DANMUKU-HEATMAP-02 | resolved / 已复现 | Issue958 dense automatic heatmaps exceed the fixed Y domain and flatten across video | PKG-DANMUKU-10 |
|
||||
| DANMUKU-TIMING-01 | resolved / 已复现 | Hidden Worker wait is deducted from visible lifetime and can recycle a comment immediately after visible | PKG-DANMUKU-11 |
|
||||
| DANMUKU-SAMPLING-01 | resolved / 已复现 | Serial asynchronous placement stops native frame eligibility sampling and delays existing visible expiry | PKG-DANMUKU-12 |
|
||||
+13
-4
@@ -5290,18 +5290,27 @@
|
||||
{
|
||||
"id": "DANMUKU-LOAD-01",
|
||||
"title": "Native lifecycle fixtures sometimes miss timestamp eligibility during concurrent workload; real frame/window behavior needs load evidence",
|
||||
"confirmation": "unverified",
|
||||
"status": "open",
|
||||
"confirmation": "reproduced",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-DANMUKU-07"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/changes/2026-09-13-PKG-DANMUKU-05-resources.md",
|
||||
"refactor/baselines/danmuku-resources-validation.json",
|
||||
"test/browser/danmuku-scheduler.spec.js"
|
||||
"test/browser/danmuku-scheduler.spec.js",
|
||||
"refactor/baselines/danmuku-stability-validation.json",
|
||||
"refactor/baselines/danmuku-frame-sampling-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Capture native RAF/media-time gaps and scheduled/eligible/visible item counts under realistic load before deciding whether the old plus/minus 0.1 second eligibility rule needs a compatible correction.",
|
||||
"closureCriteria": "Real load runs explain missing scheduled items with frame and media evidence, and any confirmed implementation defect has a regression and compatible fix; seeding ready in lifecycle fixtures is not closure evidence."
|
||||
"closureCriteria": "Real load runs explain missing scheduled items with frame and media evidence, and any confirmed implementation defect has a regression and compatible fix; seeding ready in lifecycle fixtures is not closure evidence.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/baselines/danmuku-stability-validation.json",
|
||||
"refactor/changes/2026-09-13-PKG-DANMUKU-07-stability.md",
|
||||
"refactor/baselines/danmuku-frame-sampling-validation.json",
|
||||
"refactor/baselines/danmuku-lifetime-validation.json"
|
||||
],
|
||||
"resolutionRationale": "Native timing evidence distinguishes unobserved CPU-blocked windows from asynchronous scheduler starvation. Tasks 11/12 fix the reproduced implementation defects. Final three-engine real-load runs keep candidate scheduled rows observed/displayed/recycled exactly once, with actual node reuse and state/Worker ownership; unchanged historical CPU-gap semantics and full model/device/release scope remain explicit."
|
||||
},
|
||||
{
|
||||
"id": "DANMUKU-HEATMAP-02",
|
||||
|
||||
+6
-2
@@ -3384,11 +3384,15 @@
|
||||
"PKG-DANMUKU-11",
|
||||
"PKG-DANMUKU-12"
|
||||
],
|
||||
"status": "doing",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "负载/渲染/资源对比及 mask 可依赖的稳定边界",
|
||||
"acceptance": "在试点核心中旧插件 API 与关键帧路径通过",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-13-PKG-DANMUKU-07-stability.md",
|
||||
"baselines/danmuku-stability-validation.json",
|
||||
"danmuku-stability.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-DANMUKU-08",
|
||||
|
||||
@@ -147,3 +147,14 @@ rows and serial callbacks; the fixed published baseline retains its missing midd
|
||||
row. CPU-block observations remain diagnostic, not no-loss acceptance. Both it and
|
||||
`danmuku-lifetime.spec.js` accept the same artifact override. The latter verifies
|
||||
the full visible lifetime after a controlled delay to an actual Worker request.
|
||||
|
||||
`danmuku-stability.spec.js` runs three native 14-second media cycles at 2 or 20
|
||||
rows per second, appending through the public load(rows) contract. The candidate
|
||||
must deliver and recycle every row once, preserve state membership and reuse
|
||||
actual node identities. Its 90-second per-case deadline budgets the 42 seconds
|
||||
of media plus setup/teardown; it is not a relaxation of other tests. Heap readings
|
||||
remain observations. No media/RAF clock is replaced or ready state seeded here.
|
||||
`danmuku-mask-boundary.spec.js` verifies the core-owned root and external CSS mask
|
||||
across commands and destroy(false), with actual video and placement Worker. It
|
||||
uses explicit ready rows for resource setup and a fixed SVG mask; it does not
|
||||
load the Mask SDK/model or provide timestamp-delivery evidence.
|
||||
@@ -0,0 +1,176 @@
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { compilePackage } from '../helpers/load.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
let candidate
|
||||
test.beforeAll(async () => {
|
||||
candidate = process.env.ARTPLAYER_DANMUKU_ARTIFACT
|
||||
? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8')
|
||||
: await compilePackage('artplayer-plugin-danmuku', 'umd')
|
||||
})
|
||||
|
||||
async function setup(page, core, testInfo) {
|
||||
const external = []
|
||||
const origin = new URL(testInfo.project.use.baseURL).origin
|
||||
await page.route('**/*', (route) => {
|
||||
if (new URL(route.request().url()).origin !== origin) {
|
||||
external.push(route.request().url())
|
||||
return route.abort()
|
||||
}
|
||||
return route.continue()
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: candidate })
|
||||
await page.evaluate(() => {
|
||||
window.maskBoundaryEvidence = { snapshots: [], workers: [], visible: [], errors: [] }
|
||||
const NativeWorker = window.Worker
|
||||
window.Worker = class extends NativeWorker {
|
||||
constructor(url, options) {
|
||||
super(url, options)
|
||||
this.observation = { requests: [], replies: [], terminated: 0 }
|
||||
window.maskBoundaryEvidence.workers.push(this.observation)
|
||||
this.addEventListener('message', ({ data }) => this.observation.replies.push({ id: data.id, result: data.result ?? null }))
|
||||
}
|
||||
|
||||
postMessage(message, ...args) {
|
||||
this.observation.requests.push({ type: message.type, id: message.id })
|
||||
return super.postMessage(message, ...args)
|
||||
}
|
||||
|
||||
terminate() {
|
||||
this.observation.terminated++
|
||||
return super.terminate()
|
||||
}
|
||||
}
|
||||
window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true })
|
||||
window.maskBoundaryLayer = window.art.template.$danmuku
|
||||
const layer = window.maskBoundaryLayer
|
||||
// Fixed CSS input tests Danmuku's shared-layer contract. No Mask plugin,
|
||||
// model, TensorFlow backend or segmentation output is loaded or simulated.
|
||||
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360"><rect width="320" height="360" fill="white"/></svg>'
|
||||
layer.style.maskImage = `url("data:image/svg+xml,${encodeURIComponent(svg)}")`
|
||||
Object.assign(layer.style, { maskMode: 'alpha', maskSize: 'contain', maskRepeat: 'no-repeat', backgroundSize: 'contain', backgroundRepeat: 'no-repeat' })
|
||||
window.maskBoundarySnapshot = (label) => {
|
||||
const properties = ['mask-image', 'mask-mode', 'mask-size', 'mask-repeat', 'background-size', 'background-repeat']
|
||||
const computed = getComputedStyle(layer)
|
||||
const snapshot = {
|
||||
label,
|
||||
sameLayer: window.art.template.$danmuku === layer,
|
||||
connected: layer.isConnected,
|
||||
styles: properties.map(name => ({ name, value: layer.style.getPropertyValue(name), priority: layer.style.getPropertyPriority(name), computed: computed.getPropertyValue(name) })),
|
||||
children: layer.children.length,
|
||||
opacity: layer.style.opacity,
|
||||
time: window.art.currentTime,
|
||||
}
|
||||
window.maskBoundaryEvidence.snapshots.push(snapshot)
|
||||
return snapshot
|
||||
}
|
||||
window.maskBoundaryBase = window.maskBoundarySnapshot('before-registration')
|
||||
window.art.on('artplayerPluginDanmuku:visible', item => window.maskBoundaryEvidence.visible.push(item.id))
|
||||
window.art.on('artplayerPluginDanmuku:error', error => window.maskBoundaryEvidence.errors.push(String(error)))
|
||||
window.art.plugins.add(window.artplayerPluginDanmuku({ danmuku: [], heatmap: false, speed: 8, margin: [10, 10], fontSize: 20 }))
|
||||
window.maskBoundaryPlugin = window.art.plugins.artplayerPluginDanmuku
|
||||
document.querySelector('#play').onclick = () => window.art.play()
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady && window.art.template.$video.videoWidth > 0)).toBe(true)
|
||||
// Real slow playback leaves room for lifecycle checks on the finite fixture.
|
||||
await page.evaluate(() => window.art.playbackRate = 0.25)
|
||||
await testInfo.attach('danmuku-mask-boundary-inputs', {
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ core, artifact: process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'source UMD', sha256: hash(candidate), media: '/test/pattern.mp4', scope: 'Shared root-layer identity and CSS preservation with fixed inline SVG mask; native video, DOM, RAF and placement Worker. No Mask SDK/model/end-to-end acceptance. Eligible rows are explicit resource-test preconditions, not timestamp-delivery evidence.' }),
|
||||
})
|
||||
return external
|
||||
}
|
||||
|
||||
async function assertLayer(page, label) {
|
||||
const { baseline, snapshot } = await page.evaluate(label => ({ baseline: window.maskBoundaryBase, snapshot: window.maskBoundarySnapshot(label) }), label)
|
||||
expect(snapshot.sameLayer, label).toBe(true)
|
||||
expect(snapshot.connected, label).toBe(true)
|
||||
expect(snapshot.styles, label).toEqual(baseline.styles)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
async function renderRow(page, id) {
|
||||
await page.evaluate(async (id) => {
|
||||
const owner = await window.maskBoundaryPlugin.emit({ id, text: id, mode: 1, time: 0 })
|
||||
// Establish eligibility, then require actual placement Worker and visible DOM.
|
||||
owner.setState(owner.queue[owner.queue.length - 1], 'ready')
|
||||
window.maskBoundaryOwner = owner
|
||||
}, id)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(id => window.maskBoundaryEvidence.visible.includes(id), id)).toBe(true)
|
||||
await expect.poll(() => page.evaluate((id) => {
|
||||
const node = [...window.maskBoundaryLayer.children].find(node => node.dataset.id === id)
|
||||
if (!node)
|
||||
return false
|
||||
const rect = node.getBoundingClientRect()
|
||||
return node.dataset.state === 'emit' && getComputedStyle(node).visibility === 'visible' && rect.width > 0 && rect.height > 0
|
||||
}, id)).toBe(true)
|
||||
}
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
if (page.isClosed())
|
||||
return
|
||||
const evidence = await page.evaluate(() => {
|
||||
if (window.maskBoundaryEvidence && window.art && !window.art.isDestroy)
|
||||
window.art.destroy(false)
|
||||
return window.maskBoundaryEvidence ?? null
|
||||
})
|
||||
await testInfo.attach('danmuku-mask-boundary-evidence', { contentType: 'application/json', body: JSON.stringify(evidence) })
|
||||
})
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: Danmuku retains the core layer and external CSS mask across commands and retained-HTML destroy`, async ({ page }, testInfo) => {
|
||||
const external = await setup(page, core, testInfo)
|
||||
const initial = await assertLayer(page, 'after-registration')
|
||||
const image = initial.styles.find(item => item.name === 'mask-image')
|
||||
expect(image.value).toContain('data:image/svg+xml,')
|
||||
expect(image.computed).toContain('data:image/svg+xml,')
|
||||
await renderRow(page, 'MASK-BOUNDARY-EMIT')
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0)
|
||||
await assertLayer(page, 'after-native-emit')
|
||||
await testInfo.attach('danmuku-mask-boundary-playing', { contentType: 'image/png', body: await page.screenshot() })
|
||||
|
||||
const reset = await page.evaluate(() => {
|
||||
const owner = window.maskBoundaryPlugin.reset()
|
||||
return { sameOwner: owner === window.maskBoundaryOwner, states: owner.queue.map(item => item.$state) }
|
||||
})
|
||||
expect(reset).toEqual({ sameOwner: true, states: ['wait'] })
|
||||
await assertLayer(page, 'after-reset')
|
||||
|
||||
const replacement = await page.evaluate(async () => {
|
||||
window.maskBoundaryPlugin.config({ danmuku: [{ id: 'CONFIGURED-REPLACEMENT', text: 'CONFIGURED-REPLACEMENT', time: 100 }] })
|
||||
const owner = await window.maskBoundaryPlugin.load()
|
||||
return { sameOwner: owner === window.maskBoundaryOwner, ids: owner.queue.map(item => item.id) }
|
||||
})
|
||||
expect(replacement).toEqual({ sameOwner: true, ids: ['CONFIGURED-REPLACEMENT'] })
|
||||
await assertLayer(page, 'after-no-argument-load')
|
||||
await renderRow(page, 'MASK-BOUNDARY-AFTER-LOAD')
|
||||
await assertLayer(page, 'after-replacement-native-emit')
|
||||
|
||||
await page.evaluate(() => window.maskBoundaryPlugin.hide())
|
||||
expect((await assertLayer(page, 'after-hide')).opacity).toBe('0')
|
||||
await page.evaluate(() => window.maskBoundaryPlugin.show())
|
||||
expect((await assertLayer(page, 'after-show')).opacity).toBe('1')
|
||||
|
||||
const beforeDestroy = await page.evaluate(() => ({ workers: window.maskBoundaryEvidence.workers, errors: window.maskBoundaryEvidence.errors }))
|
||||
expect(beforeDestroy.errors).toEqual([])
|
||||
expect(beforeDestroy.workers).toHaveLength(1)
|
||||
expect(beforeDestroy.workers[0].requests.length).toBeGreaterThanOrEqual(2)
|
||||
expect(beforeDestroy.workers[0].replies.length).toBeGreaterThanOrEqual(2)
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
const destroyed = await assertLayer(page, 'after-destroy-false')
|
||||
expect(destroyed.children).toBe(0)
|
||||
// Use the native frame clock to detect immediate late plugin writes after cleanup.
|
||||
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))))
|
||||
const settled = await assertLayer(page, 'after-destroy-native-frames')
|
||||
expect(settled.children).toBe(0)
|
||||
const terminal = await page.evaluate(() => ({ workers: window.maskBoundaryEvidence.workers, errors: window.maskBoundaryEvidence.errors }))
|
||||
expect(terminal.errors).toEqual([])
|
||||
expect(terminal.workers[0].terminated).toBe(1)
|
||||
expect(terminal.workers[0].requests).toEqual(beforeDestroy.workers[0].requests)
|
||||
expect(external).toEqual([])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { compilePackage } from '../helpers/load.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementations = new Map()
|
||||
test.beforeAll(async () => {
|
||||
const release = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/danmuku-release.json', import.meta.url), 'utf8')).release
|
||||
const member = `package/${release.manifest.main.replace(/^\.\//u, '')}`
|
||||
const published = readMember(await ensureArchive(release), member)
|
||||
assert.equal(hash(published), release.files[member])
|
||||
implementations.set('published', published.toString())
|
||||
implementations.set('candidate', process.env.ARTPLAYER_DANMUKU_ARTIFACT
|
||||
? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8')
|
||||
: await compilePackage('artplayer-plugin-danmuku', 'umd'))
|
||||
})
|
||||
|
||||
for (const implementation of ['published', 'candidate']) {
|
||||
for (const rate of [2, 20]) {
|
||||
test(`${implementation}: ${rate} rows per second across three native playback cycles`, async ({ page }, testInfo) => {
|
||||
// Three 14-second media cycles plus setup/teardown; this is a bounded soak,
|
||||
// not the ordinary short browser test deadline or a synthetic clock jump.
|
||||
test.setTimeout(90000)
|
||||
await page.goto('/test/player.html?core=candidate')
|
||||
const code = implementations.get(implementation)
|
||||
await page.addScriptTag({ content: code })
|
||||
await page.evaluate(() => window.createPlayer('/assets/sample/video.mp4'))
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
await page.click('#pause')
|
||||
await page.evaluate(() => {
|
||||
const art = window.art
|
||||
const evidence = window.stabilityEvidence = { workers: [], frames: [], samples: [], visible: [], recycled: [], callbacks: [], cycles: [], errors: [], allocations: 0, reuses: 0, bypassedPool: 0 }
|
||||
const NativeWorker = window.Worker
|
||||
window.Worker = class extends NativeWorker {
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this.record = { posts: 0, replies: 0, terminated: 0, errors: [] }
|
||||
evidence.workers.push(this.record)
|
||||
this.addEventListener('message', () => this.record.replies++)
|
||||
this.addEventListener('error', event => this.record.errors.push(event.message))
|
||||
}
|
||||
|
||||
postMessage(...args) {
|
||||
this.record.posts++
|
||||
return super.postMessage(...args)
|
||||
}
|
||||
|
||||
terminate() {
|
||||
this.record.terminated++
|
||||
return super.terminate()
|
||||
}
|
||||
}
|
||||
art.on('artplayerPluginDanmuku:error', error => evidence.errors.push(String(error)))
|
||||
art.on('artplayerPluginDanmuku:visible', row => evidence.visible.push({ id: row.id, time: art.currentTime, wall: performance.now() }))
|
||||
art.plugins.add(window.artplayerPluginDanmuku({
|
||||
danmuku: [],
|
||||
heatmap: false,
|
||||
emitter: false,
|
||||
speed: 1,
|
||||
antiOverlap: false,
|
||||
beforeVisible(row) {
|
||||
evidence.callbacks.push({ id: row.id, time: art.currentTime })
|
||||
return true
|
||||
},
|
||||
}))
|
||||
const owner = window.stabilityOwner = art.plugins.artplayerPluginDanmuku.config({})
|
||||
const acquire = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(owner), '$ref').get
|
||||
const acquired = new WeakSet()
|
||||
Object.defineProperty(owner, '$ref', { configurable: true, get() {
|
||||
const available = new Set(this.$refs)
|
||||
const ref = acquire.call(this)
|
||||
if (available.size && !available.has(ref))
|
||||
evidence.bypassedPool++
|
||||
if (acquired.has(ref))
|
||||
evidence.reuses++
|
||||
else evidence.allocations++
|
||||
acquired.add(ref)
|
||||
return ref
|
||||
} })
|
||||
const getter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(owner), 'readys').get
|
||||
Object.defineProperty(owner, 'readys', { configurable: true, get() {
|
||||
const rows = getter.call(this)
|
||||
evidence.samples.push({ time: art.currentTime, wall: performance.now(), ids: rows.map(row => row.id) })
|
||||
return rows
|
||||
} })
|
||||
const makeWait = owner.makeWait.bind(owner)
|
||||
owner.makeWait = (row) => {
|
||||
if (row.$state === 'emit')
|
||||
evidence.recycled.push({ id: row.id, time: art.currentTime })
|
||||
return makeWait(row)
|
||||
}
|
||||
const frame = (wall) => {
|
||||
if (art.playing)
|
||||
evidence.frames.push({ wall, time: art.currentTime })
|
||||
window.stabilityFrame = requestAnimationFrame(frame)
|
||||
}
|
||||
window.stabilityFrame = requestAnimationFrame(frame)
|
||||
})
|
||||
try {
|
||||
for (let cycle = 0; cycle < 3; cycle++) {
|
||||
await page.evaluate(async ({ cycle, rate }) => {
|
||||
const art = window.art
|
||||
const base = art.currentTime
|
||||
const rows = Array.from({ length: 12 * rate }, (_, index) => ({ id: `${cycle}-${index}`, text: `Row ${index}`, time: base + 0.5 + index / rate, mode: index % 3 }))
|
||||
await art.plugins.artplayerPluginDanmuku.load(rows)
|
||||
window.stabilityEnd = base + 14
|
||||
window.stabilityEvidence.cycles.push({ cycle, base, rows: rows.map(row => ({ id: row.id, time: row.time })), startWall: performance.now() })
|
||||
}, { cycle, rate })
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime >= window.stabilityEnd), { timeout: 22000 }).toBe(true)
|
||||
await page.click('#pause')
|
||||
const snapshot = await page.evaluate(() => {
|
||||
const owner = window.stabilityOwner
|
||||
const record = window.stabilityEvidence.cycles.at(-1)
|
||||
const ids = new Set(record.rows.map(row => row.id))
|
||||
const visible = window.stabilityEvidence.visible.filter(row => ids.has(row.id))
|
||||
const sampled = new Set(window.stabilityEvidence.samples.flatMap(sample => sample.ids).filter(id => ids.has(id)))
|
||||
const shown = new Set(visible.map(row => row.id))
|
||||
const refs = owner.queue.map(row => row.$ref).filter(Boolean)
|
||||
const queue = new Set(owner.queue)
|
||||
const members = Object.values(owner.states).flat()
|
||||
const memory = performance.memory
|
||||
Object.assign(record, {
|
||||
endWall: performance.now(),
|
||||
time: window.art.currentTime,
|
||||
decodedWidth: window.art.video.videoWidth,
|
||||
visibleCount: visible.length,
|
||||
uniqueVisible: shown.size,
|
||||
missedSampling: record.rows.filter(row => !sampled.has(row.id)).map(row => row.id),
|
||||
sampledNotVisible: record.rows.filter(row => sampled.has(row.id) && !shown.has(row.id)).map(row => row.id),
|
||||
queue: owner.queue.length,
|
||||
states: Object.fromEntries(Object.entries(owner.states).map(([state, rows]) => [state, rows.length])),
|
||||
nodes: owner.$danmuku.children.length,
|
||||
pooled: owner.$refs.length,
|
||||
uniquePool: new Set(owner.$refs).size,
|
||||
referenced: refs.length,
|
||||
distinctReferences: new Set(refs).size,
|
||||
uniqueStateMembers: new Set(members).size,
|
||||
validStateMembers: Object.entries(owner.states).every(([state, rows]) => rows.every(row => queue.has(row) && row.$state === state)),
|
||||
heap: memory ? { used: memory.usedJSHeapSize, total: memory.totalJSHeapSize } : null,
|
||||
})
|
||||
return record
|
||||
})
|
||||
expect(snapshot.decodedWidth).toBeGreaterThan(0)
|
||||
expect(snapshot.time - snapshot.base).toBeGreaterThanOrEqual(14)
|
||||
// load(rows) appends by public contract; old rows stay in the queue.
|
||||
expect(snapshot.queue).toBe((cycle + 1) * 12 * rate)
|
||||
expect(Object.values(snapshot.states).reduce((sum, count) => sum + count, 0)).toBe(snapshot.queue)
|
||||
if (implementation === 'candidate') {
|
||||
expect(snapshot.missedSampling).toEqual([])
|
||||
expect(snapshot.sampledNotVisible).toEqual([])
|
||||
expect(snapshot.visibleCount).toBe(12 * rate)
|
||||
expect(snapshot.uniqueVisible).toBe(12 * rate)
|
||||
expect(snapshot.referenced).toBe(0)
|
||||
expect(snapshot.uniquePool).toBe(snapshot.pooled)
|
||||
expect(snapshot.nodes).toBe(snapshot.pooled)
|
||||
expect(snapshot.uniqueStateMembers).toBe(snapshot.queue)
|
||||
expect(snapshot.validStateMembers).toBe(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
const evidence = await page.evaluate(() => {
|
||||
cancelAnimationFrame(window.stabilityFrame)
|
||||
window.art.destroy()
|
||||
return { ...window.stabilityEvidence, destroyed: window.art.isDestroy, retainedPlayerRoots: document.querySelectorAll('.art-video-player').length }
|
||||
})
|
||||
await testInfo.attach('danmuku-stability-observation', { contentType: 'application/json', body: JSON.stringify({ implementation, rate, sha256: hash(code), evidence, scope: 'Three bounded real media cycles; published defects are observed, candidate delivery and resource ownership are asserted. Optional unforced heap readings are not leak proof or comparable across engines.' }) })
|
||||
expect(evidence.destroyed).toBe(true)
|
||||
expect(evidence.retainedPlayerRoots).toBe(0)
|
||||
expect(evidence.errors).toEqual([])
|
||||
expect(evidence.workers).toHaveLength(1)
|
||||
if (implementation === 'candidate' && evidence.cycles.length === 3) {
|
||||
const expected = evidence.cycles.flatMap(cycle => cycle.rows.map(row => row.id)).sort()
|
||||
expect(evidence.visible.map(row => row.id).sort()).toEqual(expected)
|
||||
expect(evidence.recycled.map(row => row.id).sort()).toEqual(expected)
|
||||
expect(evidence.bypassedPool).toBe(0)
|
||||
expect(evidence.reuses).toBeGreaterThan(0)
|
||||
expect(evidence.allocations + evidence.reuses).toBe(expected.length)
|
||||
expect(evidence.cycles.at(-1).nodes).toBe(evidence.allocations)
|
||||
}
|
||||
for (const worker of evidence.workers) {
|
||||
expect(worker.terminated).toBe(1)
|
||||
expect(worker.errors).toEqual([])
|
||||
if (implementation === 'candidate')
|
||||
expect(worker.posts).toBe(worker.replies)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user