From 534f6e03c1838580ecdc3133a4d57147e52e1d75 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Wed, 11 Nov 2026 15:50:10 +0800 Subject: [PATCH 1/2] perf: add repeatable scene benchmarks and validate borrowed dictionary keys --- docs/performance-study.md | 57 ++++++++++++++++++ src/runtime/endor_host/tests.rs | 101 ++++++++++++++++++++++++++++++++ vendor/aster | 2 +- 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 docs/performance-study.md diff --git a/docs/performance-study.md b/docs/performance-study.md new file mode 100644 index 0000000..63d02fc --- /dev/null +++ b/docs/performance-study.md @@ -0,0 +1,57 @@ +# 场景交互性能研究 + +## 结论 + +性能问题有可验证的解决路径,但单改 Aster 字典不能让当前世界 UI 达到稳定 60 FPS。本次保留去除字符串键比较临时分配的小改动,交付可重复基准;面板纹理缓存实验因当前示例收益不足而撤回。没有实现 GPU 后端或宣称完成性能目标。 + +## 方法 + +在 Apple M4 Pro、macOS 15.6 本机使用 release 构建,固定 1024×800 逻辑视口、1× / 2× 像素比例,分别加载 world-ui 和 scene3d 示例。每组执行 33 次真实 scene-orbit 运行时分发,前 3 次作为预热,统计后 30 次的均值、P50、P95。每次先测分发到文档更新,再测变化后的软件绘制,最后测相同显示列表的缓存绘制。没有运行 Console 编译来代替原生事件。 + +```sh +cargo test --release --locked scene_interaction_performance -- --ignored --nocapture --test-threads=1 +``` + +这是无窗口的运行时与软件渲染基准,包含回调、快照、布局和运行时收尾,但不包含本地 HTTP、OS 事件排队、窗口呈现或输入到显示延迟。阶段耗时不能简单当作实测 FPS。时间是本机样本,不是跨设备保证;基准默认 ignored,普通回归不以绝对耗时断言通过。 + +## 对照结果 + +世界 UI、2× 像素比例,单位 ms: + +| 条件 | 更新均值 | 更新 P95 | 变化绘制均值 | 变化绘制 P95 | +| --- | ---: | ---: | ---: | ---: | +| 原始 Aster | 8.456 | 8.843 | 18.704 | 20.742 | +| 借用字符串键比较 | 6.842 | 7.226 | 18.598 | 20.665 | +| 再加面板纹理缓存实验 | 6.823 | 7.053 | 18.410 | 20.482 | + +去掉字符串键比较的分配后,本组更新均值减少约 19%,绘制基本不变。面板缓存的绘制收益约 1%,不足以证明值得在本次增加缓存与失效复杂度,因此撤回。 + +作为对照,普通 scene3d 示例在 2× 下更新约 0.94 ms、变化绘制约 6.15 ms;它的视口尺寸、覆盖像素和 UI 复杂度不同,不能据此把两个示例的全部差值都归给某一层。 + +## Aster 发现与保留的修改 + +`EndorDictObject` 用插入有序的数组存储,`dict_position` 逐项调用 `key_equal`。原实现每次比较都会把内联文本或托管 String 转成新的 String;查找名称时会重复分配和复制候选键。 + +本次只改为借用字符串切片比较。保留线性查找、原 key_equal 分支、对象检查、只读依赖观察、键的插入身份与顺序、写入验证和引用计数流程。新增测试覆盖空文本、普通文本、Unicode、托管 / 内联字符串等价替换、删除和引用释放。 + +没有把通用字典直接改成 HashMap,也没有绕过 heap.replace_payload 的事务性边验证。前者还需处理托管键相等 / 哈希与顺序语义;后者涉及 GC、失败原子性和只读保护。它们不属于本次已经证明安全的改动。 + +## 尚未解决的主要成本 + +1. 世界 UI 的摄像机变化后,当前统计仍为 measured=8、arranged=88;普通 3D 为 measured=0、arranged=14。数据证明相机更新没有彻底绕开排版。 +2. 3D 变化帧仍在窗口线程逐像素进行 CPU 绘制。当前世界 UI 的绘制本身已超过 60 Hz 的 16.7 ms 预算,其他 CPU 优化不能消除这个事实。 +3. 原生连续输入仍逐项经本地 HTTP 发送,有界队列不等于按显示帧合并;真实排队和呈现延迟需要另外测量。 + +## 后续实施顺序与条件 + +1. **相机更新专用路径。** 复用已有 Mount 几何、UI 排版和绘制结果,仅更新视图 / 投影、可见性和命中映射。必须检查源 measure / arrange / paint 的实际读取依赖;源函数若读取相机,应继续正确失效。不能无条件跳过所有布局或绘制。验收:不依赖相机的 UI 在旋转时零测量、零重新排版,依赖相机的源协议回归仍通过。 +2. **按帧收集连续输入。** 原生操作直接进入进程内通道,连续手势按捕获会话 / 目标与顺序合并;点击、页面切换和运行时替换保持边界。外部 HTTP API 保留严格版本校验。验收包括缩放锚点、非交换手势顺序、嵌套捕获与过期会话,不能只检查最终数值。 +3. **GPU 合成与独立渲染。** 现有不可变网格、变换、相机、深度和 UV 数据可以成为渲染输入;VM 对象留在运行时线程。GPU 应直接向窗口表面呈现,同时合成 2D / 3D 内容,避免每帧 GPU→CPU 回读后再交给 softbuffer。跨线程队列只保留最新可呈现快照;纹理、缓冲与缓存均需资源预算和正确失效。 + +建议验收目标:固定示例 2× 下相机更新 P95 ≤2 ms、GPU 绘制 P95 ≤8 ms,另外记录真实窗口的帧间隔和输入到呈现延迟。目标尚未达到,也没有用无窗口基准宣称原生 60 FPS。 + +## 检查说明 + +Aster 全工作区测试发现 compiler 的 classes 测试中两项解析失败(第三行 expected EOF),以及 core 的 managed_coroutine_hard_budget_accumulates_and_error_releases_frames 指令预算失败。将对象实现恢复为未优化的基线后,这三项失败均能复现,说明不是本次借用字符串比较引入;不把全工作区标为全绿。对象层测试通过,Aster 全工作区 Clippy / 格式通过;GUI 为 186 项通过、1 项默认忽略的基准,GUI Clippy / 格式通过。该基准已经另行在 release 模式实际执行。 + +本次没有修改用户指向的 builtins.rs 第 176 行(该处是函数对象定义)。实际热路径修改位于 Aster 的 store_builtins.rs。 diff --git a/src/runtime/endor_host/tests.rs b/src/runtime/endor_host/tests.rs index 3901bcc..b074e9f 100644 --- a/src/runtime/endor_host/tests.rs +++ b/src/runtime/endor_host/tests.rs @@ -4,6 +4,107 @@ use crate::document::{NamespaceState, RenderCommand}; const EXAMPLE: &str = include_str!("../../../examples/hello.endor"); const PAGE: &str = include_str!("../../../examples/page.endor"); +/// Opt-in release benchmark; run alone to avoid test-thread interference. +/// cargo test --release --locked scene_interaction_performance -- --ignored --nocapture --test-threads=1 +#[test] +#[ignore] +fn scene_interaction_performance() { + use std::time::Instant; + fn summary(mut values: Vec) -> String { + values.sort_by(f64::total_cmp); + format!( + "mean={:.3} p50={:.3} p95={:.3}ms", + values.iter().sum::() / values.len() as f64, + values[values.len() / 2], + values[(values.len() * 95).div_ceil(100) - 1] + ) + } + for (name, source) in [ + ("world-ui", include_str!("../../../examples/world-ui.endor")), + ("scene3d", include_str!("../../../examples/scene3d.endor")), + ] { + for dpi in [1., 2.] { + let (mut session, mut document) = ReplSession::load_with_viewport( + source, + "app.main", + 1, + 988, + Some(crate::layout::Size { + width: 1024., + height: 800., + }), + ) + .unwrap(); + let (width, height) = ((1024. * dpi) as u32, (800. * dpi) as u32); + let mut pixels = vec![0; (width * height) as usize]; + let mut updates = Vec::new(); + let mut draws = Vec::new(); + let mut cached = Vec::new(); + for sample in 0..33 { + let target = document + .frame + .mounts + .iter() + .find(|mount| mount.scene3d_viewport) + .unwrap(); + let input = crate::events::Input { + window: 1, + session: document.frame.session, + layout_version: document.frame.layout_version, + mount: target.id, + kind: "scene-orbit".into(), + point: Some([target.x + target.width / 2., target.y + target.height / 2.]), + delta: Some([1., 0.]), + press_point: None, + tab_drop: None, + screen_position: None, + }; + let start = Instant::now(); + document = session.dispatch(&input).unwrap().document.unwrap(); + let update = start.elapsed().as_secs_f64() * 1000.; + let list = document.display_list(dpi).unwrap(); + let start = Instant::now(); + crate::software_renderer::SoftwareRenderer::render( + &list, + &mut pixels, + width, + height, + ) + .unwrap(); + let draw = start.elapsed().as_secs_f64() * 1000.; + let start = Instant::now(); + crate::software_renderer::SoftwareRenderer::render( + &list, + &mut pixels, + width, + height, + ) + .unwrap(); + if sample >= 3 { + updates.push(update); + draws.push(draw); + cached.push(start.elapsed().as_secs_f64() * 1000.); + } + } + println!( + "PERF {name} dpi={dpi} update {} changed_draw {} cached_draw {}", + summary(updates), + summary(draws), + summary(cached) + ); + for stat in ["measured", "arranged", "snapshotVisited"] { + println!( + "PERFSTAT {name} {stat}={}", + session + .execute(&format!("ui.stats().{stat}"), "app.main") + .unwrap() + .value + ); + } + } + } +} + #[test] fn actual_mount_version_reads_still_bypass_measurement_cache() { let (mut session, initial) = ReplSession::load( diff --git a/vendor/aster b/vendor/aster index f3e1670..fd247ac 160000 --- a/vendor/aster +++ b/vendor/aster @@ -1 +1 @@ -Subproject commit f3e1670c580a6985262311847b16752df2129b21 +Subproject commit fd247aca1fdcb74f7240a6b47c622791f17b9004 -- 2.52.0 From 723e41706be1dc8b72deae684c7c6ebb9116b2ee Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 02:21:34 +0800 Subject: [PATCH 2/2] fix: keep current Aster API when updating performance study --- docs/performance-study.md | 8 +++++++- vendor/aster | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/performance-study.md b/docs/performance-study.md index 63d02fc..eee4447 100644 --- a/docs/performance-study.md +++ b/docs/performance-study.md @@ -2,7 +2,7 @@ ## 结论 -性能问题有可验证的解决路径,但单改 Aster 字典不能让当前世界 UI 达到稳定 60 FPS。本次保留去除字符串键比较临时分配的小改动,交付可重复基准;面板纹理缓存实验因当前示例收益不足而撤回。没有实现 GPU 后端或宣称完成性能目标。 +性能问题有可验证的解决路径,但单改 Aster 字典不能让当前世界 UI 达到稳定 60 FPS。本 PR 交付可重复基准和研究记录;去除字符串键比较临时分配的改动仍在独立 Aster 分支,未随本 PR 集成;面板纹理缓存实验因当前示例收益不足而撤回。没有实现 GPU 后端或宣称完成性能目标。 ## 方法 @@ -55,3 +55,9 @@ cargo test --release --locked scene_interaction_performance -- --ignored --nocap Aster 全工作区测试发现 compiler 的 classes 测试中两项解析失败(第三行 expected EOF),以及 core 的 managed_coroutine_hard_budget_accumulates_and_error_releases_frames 指令预算失败。将对象实现恢复为未优化的基线后,这三项失败均能复现,说明不是本次借用字符串比较引入;不把全工作区标为全绿。对象层测试通过,Aster 全工作区 Clippy / 格式通过;GUI 为 186 项通过、1 项默认忽略的基准,GUI Clippy / 格式通过。该基准已经另行在 release 模式实际执行。 本次没有修改用户指向的 builtins.rs 第 176 行(该处是函数对象定义)。实际热路径修改位于 Aster 的 store_builtins.rs。 + +## PR #6 主线兼容修复 + +2026-11-12:合入最新 GUI main,保留 main 的 Aster f3e1670,而非旧性能分支 fd247ac。旧引用缺少 GUI 目前使用的精确读取依赖接口,不能为性能实验回退。上文数值是原有实验记录,不代表当前主线已经采用借用键优化。本 PR 的最终差异仅包括本研究记录和默认 ignored 的基准;Aster 优化需单独更新其分支并审阅后再集成。PR 标题中的 WIP 将移除,交付范围为可运行基准,不承诺上述后续性能目标。 + +本次兼容修复检查:GUI `cargo test --locked` 为 186 passed / 1 ignored;Clippy(全部 target,警告视为错误)、格式和差异空白检查通过。最初沙箱内 9 项 HTTP 测试因不能监听本地端口失败;允许回环网络后全套重跑通过。本次未重跑历史 release 性能采样,也未修改 Aster。 diff --git a/vendor/aster b/vendor/aster index fd247ac..f3e1670 160000 --- a/vendor/aster +++ b/vendor/aster @@ -1 +1 @@ -Subproject commit fd247aca1fdcb74f7240a6b47c622791f17b9004 +Subproject commit f3e1670c580a6985262311847b16752df2129b21 -- 2.52.0