大家好我是搬砖工呀。先祝大家十一快乐,假日漫漫。我们今天继续来学习 GPUI 的最最新变种gpui-fast。GPUI 以其强大的性能特别是虚拟不定长列表的完美支持赢得了开发者的一致好评。昨天一个读者热心在评论区提醒我说 GPUI 只是及时模式Immediate Mode并没有保留模式Retained Mode。哎看来我们都被他项目主页骗了主页说有的呀。这不今天这个真保留模式就出来了。https://github.com/longbridge/gpui-fastgpui-fast依然是长桥隆重推出的 GPUI 的Retained Mode版本。用来解决以下问题通过保留模式显著降低 CPU 的占用最高可达到 -80%强悍的改进一个有 60 个 Panel 的窗口每个 Panel 有 60 个 Label 标签我们每帧改变不同的 Panel 数量。每帧 Panel 改变数量GPUIGPUI-fastNone, still3.31 ms0.19 ms (−94%)One6.72 ms0.71 ms (−89%)Six7.27 ms1.46 ms (−80%)All sixty10.33 ms7.38 ms (−29%)除了全部变动真的有 −80% 的 CPU 降低很强。特别对关心续航的移动平台简直太合适了。演示GPUI Fast 强悍的保留模式我们文章分以下几个部分GPUI-FastGPUI-Fast相比原GPUI只努力做好两件事Retained Mode保留模式只重画自上帧以来变了的部分。已经实现。Window composition窗口合成下一步把 WebView 这类原生视图画进 GPUI 窗口内部同时 GPUI 自己的 Popover、菜单、对话框仍然盖在它们之上。他们准备把上游还在评审的 zed#62379 搬到这个实验分支上先试。为何不直接提交pr这两件事都要对 GPUI 做比较深的改动按现在 Zed 的评审速度先在这里实验尝鲜再逐块往上游 Zed 的 GPUI 提。改动原则fast 整个仓库坚持在两条规则下运行GPUI 现有 API 不变。新能力加在它旁边应用不必重写 UI 代码就能选用。GPUI 自己的代码尽量少改。gpui-fast 的代码住在上游文件旁边的fast/目录里上游文件只留小的钩子上游的改动照旧 merge 进来。每一处改动读起来都像“对当前 GPUI 的一小段 diff”可以一块一块交给上游。其他分支区别注意区分 pre 和 fast。项目定位gpui-pre把上游 GPUI 的未修改快照发布到 crates.io让 GPUI Kit 这类库能依赖一个已发布的 GPUI。gpui-fast 是另一个实验不属于它。gpui-ce社区维护的 GPUIgpui-fast焦点更窄只为 GPUI 试 Retained Mode 和 window composition。进了上游的东西最终会惠及上面两个项目和 Zed 本身。接入 Fast就是换一行依赖[dependencies] gpui { git https://github.com/longbridge/gpui-fast }以上是如何使用 GPUI-Fast下面我们来梳理这个项目存在的必要性。GPUI 的问题GPUI 自己的 README 文件里自我描述是“a hybrid immediate and retained mode, GPU accelerated, UI framework for Rust”—— 它并不承认自己是纯立即模式那个 “retained” 的一半就是AnyView::cached老外有时候是真的脚滑呀再一起往下看1.1 一帧要读三遍元素树GPUI 中一帧渲染的三个过程方法做什么写进哪里request_layoutview 渲染成 elementelement 向 Taffy 申请布局节点元素树的形状prepaintTaffy 计算布局元素被摆放注册 hitbox、dispatch node 与监听器hitbox / dispatch / listenerpaint元素把图元写进场景交给 GPUscene primitives一帧的产物存放在窗口Frame上的扁平、只追加的数组里hitbox、dispatch node、listener、element state以及场景的图元。窗口同时持有两帧——屏上的那帧、正在画的那帧——画完交换。最大问题在于上游默认每次都把这三个过程从头调用一遍。渲染每个 view、建一棵新的元素树、建一棵新的 Taffy 树、重新 shape 文本、重画整个场景——哪怕只是表格里一个数字变了。这就是GPUI的最大问题他几乎每帧都是全窗口渲染。1.2 GPUI唯一的CacheGPUI 里确实有一个跳过工作的机制但它窄得可怜一个被缓存的 viewAnyView::cached会记住自己上一帧写进 Frame 的区间PrepaintStateIndex、PaintIndex当它没被 notify且bounds 没变时直接把这些区间从上一帧复制过来而不是重画自己 —— 复制由Window::reuse_prepaint/Window::reuse_paint完成。这其实就是保留模式的种子。但它有三个硬限制最小单位是“整棵子树”。没有比“缓存这一整块”更细的选项也不能在一个必须重建的子 view 两侧复用父级。判定条件只有两个有没有被 notify、画在不在原位。它不看这个 view 实际读了什么。盖不到的地方太多—— 未显式cached的 view 一律每帧重建而这正是绝大多数 view。1.3 “改一点点”很昂贵其一逐层向上重建。notify 一个 view会把它周围一圈也标脏 —— 因为要走到它就必须经过它们。上游的选择是“那就全都重建”。于是一个根 view 里挂着侧栏、工具栏和页面页面深处的一点改动会重建整窗。其二布局与文本的缓存非常脆。上游在每帧结束时清空整棵 Taffy 树而 Taffy 的set_style/set_children/set_node_context无条件把该节点及其所有祖先的布局缓存标脏 —— 值没变也照脏。文本更麻烦一个测量自己的文本叶子持有一个闭包给节点换新闭包就会脏掉它和它上面的每一层而文本恰是每帧都可能变的那部分表格里跳动的价格。fast 的架构设计fast 保留 GPUI 帧的渲染设计而不是另建一棵 UI 树。但它有如下约束条件。2.0 三条约束fast 有以下三条约束约束原话含义它换来了什么从头画的帧就是规格一个保留帧正确当且仅当它等于上游 GPUI 在同一状态下从头画出的帧没有第二套“正确”要讨论而且它可以被机械测试公开 API 保持上游的为上游 GPUI 写的代码原样编译运行不需要任何 opt-in除了 cached view 上游本来就要求的那一条规则应用迁移成本 ≈ 0也逼着实现不能靠“让用户配合”来简化上游的代码是上游的逻辑住在fast/上游文件只留小钩子由script/check-upstream检查Zed 的改动持续 merge 得进来且每块改动可以单独提上游它保留的是“帧的工作”不是一棵并行的 UI 树。2.1 fast 只是把现有机制泛化GPUIgpui-fast 的做法只有显式cached的 view 会被复用每个view任何摆放的EntityV: Render或AnyView都是一棵保留子树判定没被 notify bounds 没变判定它实际依赖的东西都没变 画在原位复用是“全有或全无”可以在必须重建的嵌套 view 两侧复用父级splice只有 prepaint / paint 的区间被复制布局节点、文本测量、路径三角化、场景重叠顺序都跨帧保留说明一下就是 fast 加强了对一帧变动的检测不再是 GPUI 的简单粗暴更细致更精细。2.2 下一帧怎么把它认回来任何跨帧保留的东西都必须能和下一帧来申请它的那个 element 对上。GPUI Fast 没有为此新建一棵持久的 view 节点树而是复用了 GPUI 已有的两套身份view →GlobalElementId。一个保留 view 的记录由它的GlobalElementId找回 —— 从根到这个 view 的ElementId路径。路径里包含它自己的 entity id因为上游为每个 view 都压入ElementId::View(entity_id)。这恰好就是上游给 element state滚动位置、hover 状态、文本输入状态用的身份所以**“一个 view 被保留”与“上游认为它是同一个 element”是同一个条件**。hash 只算一次并缓存每帧为每个查找去哈希一条路径太贵但相等性比较的是完整路径先比 hash 再比路径。两个不同 view 的 hash 碰撞不会让其中一个复用另一个的记录。布局节点 → 一个 64 位的 key。它只是“用来找到缓存条目的钥匙”不是身份路径哈希、或元素的ElementId、或它在uniform_list/list里的下标。命中的节点在被使用之前要被对齐三件事style与本次请求比对比对转换到 Taffy 时读到的每个字段的指纹debug 构建会对每个指纹命中再做一次完整比较不同才重写children按节点 id 列表精确比较不同才重写测量一个测量过的叶节点只有在新输入与它当初测量时的输入相同同样的文本、runs、文本样式或“在所有 Taffy 用过的约束下重测都得到同样尺寸”时才保留测量结果。一个自己测过自己、却被一个不测量的 element 认领走的节点会失去它的测量。所以两个跨帧 hash 到同一个 key 的 element拿到的是一个 style / children / 测量都被改写成第二次所要求的内容的节点 ——结果是同样的布局代价是一次节点改写。帧内第二个想认领同一个 key 的 element 会拿到一个全新节点而不是共享。key 决定“试着用哪份缓存”正确性不依赖它。为什么不做持久化节点树。最直觉的替代方案是维护一棵持久的 view 节点树比如放在 slot map 里每个节点持有自己的布局子树和渲染输出。GPUI 的元素树每帧重建是设计使然而它已有的状态本来就带身份再建一棵平行树会重复那套身份并要求重构窗口的绘制代码 —— 那会直接违反第三条约束让这个 fork 变得难以保持合并。性能的问题不在重建节点树上而在对节点数的三次调用上。这里我在移植 GPUI 的时候也是一样的处理。通过全局 ID 来区分上下两帧是同一个 View。2.3 必须重画复用只有在“影响这个 view 绘制结果的每一处变化都是可观测的”时才安全。文档记录了四类会被记下的读访问过的实体、读过的全局量带 generation只问存在性的cx.has_global只依赖存在性、带 version 的ScrollHandle/ListState、以及窗口的环境输入mouse_position/modifiers/capslock输入事件后比对取值变了就重画读过它们的 view。关键在于读被记成两个集合subtree dependencies它读到的全部包含嵌套在它里面的 view 读的 → 回答“这棵子树能原样复用吗”own dependencies它自己读的排除其中保留的嵌套 View → 回答“它本身没变只是里面某个 view 变了”这个区分是整节设计的支点它把下面两件事分开了它自己变了 → 重建它 它没变、里面某个变了 → 把那个内层 view 拼进去splice外层不动注意特别是第二点我们在滚动里面滚动里面的内容确实变化但是滚动的 View 本身大小可视没用变化的GPUI 这时候已经带着外层 View 一起刷新了。其中第二类最值得单独拎出来一个实体被 update 但没 notifycx.subscribe/cx.observe的处理器每次事件都会更新它的订阅者不管它是否关心这个事件只对“自上一帧起被 notify 过的 view 内部”算变了。这恰好等于上游会整棵重建的那一批所以行为等价、成本更小。反过来一个实体被 notify 但没被 update滚轮、拖动滚动条、动画会被重画而只读过它的 view不算变 —— 因为它持有的东西没变。另外两类失效来源也在这里画在哪里bounds、content mask、继承的文本样式、opacity 任一不同输出就不能直接复制若布局节点仍有效它在原节点上按父级给的大小重建不牵连四周以及怎么被 hover / 交互只有发生交互的最内层 view 重建外层从上一帧画过去。例外窗口刷新时window.refresh()、resize、焦点变化、拖动时、inspector 取元素时、辅助功能激活时什么都不保留每帧从头画。2.4 如何重绘对每个摆放的 view一帧会从五条路径里选一条路径何时做什么Reused它依赖的都没变且画在原位布局取自保留的节点prepaint / paint 区间、以及嵌套子树的记录整段从上一帧复制Spliced它变脏只是因为某个嵌套 view 变了输出按段从上一帧复制只在缺口处重建那个变了的内层 viewBuilt at its retained layout它读的没变但它移动了、或继承的属性变了在保留的节点上重新渲染若它要的布局不同就在父级给的边界内重新布局下一帧转为从头建Prebuilt内层 view 已经为一次“结果没能拼接”的 splice 提前建好外层直接接管这个已建好的内层 view而不是把它建两遍Built其余一切按上游的方式渲染、布局、prepaint、paint并记录它读了什么这条 splice 的逻辑要说清两句话它只是优化永远有回退。如果那个变了的内层 view 要求不同的布局新节点、被改写的 style 或 children、不再成立的测量外层就退回完整重建并接管已经建好的内层 view即 Prebuilt 那一行绝不建两次。内层 view 能单独重建有个前提它必须是以Entity或AnyView摆放的 —— 也就是 view 几乎总是被摆放的方式。2.5 布局与文本把 Taffy 节点跨帧留着本身不是优化。因为set_style、set_children、set_node_context会无条件把该节点及其每一个祖先的布局缓存标脏。真正的优化是“值没变就不写”每个保留节点记住“上一帧收到的那次请求”style 指纹、children 列表、文本叶子的测量输入先比再写。文本要单独处理因为它的输入是已知且可比较的其它测量叶子大多不行只能每帧换闭包、每帧脏Carry同一位置、同样的 text / runs / text style → 新 element 直接继承上一帧的测量结果节点保持 clean。只改了颜色或装饰时测量照样继承装饰就地改写。Replay文本变了表格里跳动的价格→ 节点保留一张“自上次被标脏以来Taffy 用哪些约束测过它、各得到什么尺寸”的日志新文本用同样的约束、同样的顺序重测。如果每个尺寸都一样那么 Taffy 为该节点和它上面每一层缓存的东西依然成立节点保持 clean。只有尺寸真的变了的文本才脏到它的行、它的列表和上面的窗口。2.6 场景与其它保留项重叠顺序场景给每个图元一个 ordering比它压住的所有更早图元的最大 ordering 大 1这一步上游用 R-tree 做空间查询。GPUI-fast 换成均匀网格几千个小 bounds 的场景下更快更重要的是复用子树的图元 bounds 与上一帧相同所以它们的 ordering 是回放而不是重算 —— 一帧里什么都没动时这步从“每个图元一次查询”变成“一次复制”。dispatch node 的复用区间一次写入路径sparkline、折线、环形按相对首点三角化后缓存三角形glyph run 每个 run 只算一次渲染element id 与绝对 bounds 每帧缓存、不再重复哈希。指纹节点 style 是否变了由 style 的 64 位指纹判定。release 构建信任它debug 构建会把每个命中再做一次完整比较专门用来抓被指纹漏掉的字段。2.7 应用要做什么安全的复用 要求 影响渲染的每一处变化都是可观测的这是任何 reactive / 增量系统的边界不是 GPUI Fast 特有的。状态谁负责实体读、update、notify、全局量、ScrollHandle/ListState、hover 与交互、窗口的指针位置 / 修饰键 / caps lockGPUI Fast 自动跟踪变化只失效读过它的 viewRcRefCellT/CellT在实体之外、原子量、线程局部或静态可变状态、Instant::now()、文件与网络状态以及任何其它内部可变性应用自己负责。它们不是“不支持”改变它们的东西必须同时触发一次 GPUI 通知cx.notify()或一次带 notify 的entity.update否则 view 会一直显示上次构建时的内容 —— 上游对 cached view 本来就有同样要求还有两条“每帧都变 每帧都重建”的坑值得抄进 code review 清单在 prepaint / paint 里 notify 实体或写全局量即使值相同也算变了一个每帧都 notify 自己状态的、可调整大小的面板会让所有读该状态的 view 都无法被保留。排查用一条环境变量就够GPUI_VIEW_RETENTION0它让每个 view 都从头画用来“排除 / 确认”保留模式本身是不是那个可疑点。Fast 的保留模式第二节讲的是“它怎么做到”这一节讲“它对外承诺什么、怎么证明、怎么测量”。3.1 一个 view 的依赖清单以“被什么更新”为主线重新排一遍因为这决定了你会不会写错代码依赖种类细节被什么更新updated 且 notified在非绘制期由任务、监听器、action 触发→ 对所有读过它的 view 算变了notified 且正在绘制中→ 同样updated 但没 notify→ 只对“自上一帧起被 notify 过的 view 内部”算变了因为上游会连它下面的一切一起重建notified 但没 updated→ 它自己被重画只读过它的 view 不算变。一个只想重画自己的定时器应该cx.notify(entity_id)而不是去 update 拥有这个驱动器的 view读到了什么访问过的实体含没被 observe 的 model、含自己的实体读过的全局量cx.has_global只依赖存在性ListState/ScrollHandle的 version窗口的指针位置、修饰键、caps lock画在哪里bounds、content mask、继承的文本样式、opacity被什么 hover 过view 记录自己被画到时各 hitbox 的 hover 状态只有最内层那个 view 重建两个必须记住的写法约束靠内部可变性改实体内容是不行的entity.read(cx).cell.borrow_mut()这样改读它的 view 看不到变化 —— 必须用update。窗口每帧会向聚焦的文本输入查一堆东西是否接受文本、选区、某个范围的 bounds走ElementInputHandler。这些查询会 update 那个输入的实体但不算 change除非它在被问的时候 notify。3.2 每棵保留子树一份记录每一帧为每棵保留子树保存一份记录它的 hitbox / dispatch node / listener / 图元写到了哪里、它读了什么、被哪些 hover 画过、持有哪些布局节点。记录放在帧里而不是放在 element state 里—— 因为它指向的那些区间属于某一帧。这个细节带来一个关键能力同一棵子树被复用时它的记录、以及嵌套在它里面的所有子树的记录会被一起复制并平移到副本落地的位置。这就是“以后外层不得不重建时内层仍然能单独复用”的原因—— 否则一个被重建的 view 会把它里面的一切都重建一遍。3.3 Cached 视图Entity::cached(style)/AnyView::cached(style)是上游 API行为按上游文档走。在这个实现里它也是普通的保留子树所以当它读过的实体或全局量变了它同样会被重建——而不只是“看有没有被 notify”它被 notify 时会在它保留的布局节点上单独重建外层从上一帧画过去——就像任何嵌套 view 一样。3.4 验证因为“从头画的帧就是标准”验证就是把两者直接对比手段做什么Oracle 测试crates/gpui/src/fast/tests/oracle.rs驱动两个窗口走同一串随机历史一个增量绘制一个每帧忘掉保留的一切。要求每一帧完整场景都相同layers、shadows、quads、underlines、单色 / 亚像素 / 多色 sprite即文本、图标、图片及其重叠顺序还有 paths每个 hitbox 的 bounds、content mask、behavior 也要相同。它还断言增量那一侧真的复用了布局节点与子树防止“靠从不保留”蒙过去单元测试fast/tests/与代码同文件逐个机制覆盖复用、按每类依赖触发的重建、被移动的 view、拼接、layout key、文本 carry 与 replay、bounds 网格、dispatch 复制、布局节点不泄漏gpui_perf --headless --verify每个基准场景保留开 / 关同步跑逐帧比对 quads、text、icon、image sprite 与 underline 的 bounds、clip、color不比对 paths 与 shadows那份由 oracle 管GPUI_VIEW_RETENTION0运行时把每帧都从头画用来在应用里定位问题但文档把边界也写清楚了这些检查建立的是“对它们跑过的那些状态变化序列保留输出等于从头输出”。它们无法覆盖应用在依赖跟踪之外读的隐藏状态——一个读了隐藏状态、而该状态变化时没有通知的 view任何这类测试都看不见。4. 上游同步一个被我们重写过的文件每次同步都会冲突一个只有一行钩子的文件几乎永远不冲突。所以整条流程的设计目标就一句让上游更新落下来时冲突只落在钩子行上。4.1 执行规则逻辑只放在crates/crate/src/fast/一个主题一个文件fast/retained.rs、fast/dependencies.rs、fast/layout.rs……一个主题需要多个文件时用fast/topic/。其它上游 crate 需要时也有自己的src/fast/。新增能力的测试放fast/tests/topic.rs或该主题文件的底部。上游文件只放小的钩子而且每个钩子都要点名fast一个持有fast/里定义的结构的字段一行调用crate::fast::...或者一个方法体只转发给它。即使写成方法更短也要写全路径—— 写成crate::fast::dependencies::note_notify(mut self.entities, id)而不是self.entities.note_notify(id)这样合并的人一眼就能分辨哪一行是我们的一次可见性放开fn→pub(crate) fn好让fast/能碰到上游的东西一个叫fast_...的标识符当需要把一个普通类型的字段 / 参数 / 局部变量穿过上游代码时mod行当我们把某个上游文件整体换成自己的重写时用#[path fast/file.rs] mod name;重定向 —— 原上游文件保持上游原样、不被使用。上游文件里不许有新类型、新算法、新簿记、新测试也不许重排、重命名、重排格式不需要改的代码保持与上游逐字节一致。fast/之外一律写全路径crate::fast::topic::Name不许出现use crate::fast::…好让每个钩子自己说明代码住在哪。唯一例外是gpui.rs一行一个地导出test-support条目。任何 glob import 或 re-exportpub use fast::*都不允许。不新增公开 API。gpui-fast 改的是“GPUI 怎么画”不是“GPUI 提供什么”它的公开 API 就是上游的。测试与gpui_perf需要的东西只在test-support下编译一行一个从gpui.rs导出。新文件只放fast/内或自家的 crate如crates/gpui_perf基准、示例、测量应用文档放docs/。哪些目录属于上游记录在根目录的UPSTREAM文件里本仓库crates/里除gpui_perf之外的每一个目录加上tooling/perf—— 一共 26 条。zed_repository https://github.com/zed-industries/zed zed_commit 7960b2a7c9568e90fbe0727332149e5b2a5fd57a import_commit 11a44c4882f32789a8292b3f0682ffa4e6b6e79e # 保存那次导入、原样未改的提交import_commit那一行很关键因为有了它git show import_commit:path就能直接拿到上游版本的任何文件不需要本地有一个 zed checkout。4.2script/check-upstream把规则变成会失败的检查它把上游目录里的每个文件与“上游自己的版本”逐文件比较默认取自己历史里的import_commit加--zed path就对一个真实的 Zed checkout检查工作区所以要在提交前跑。命中任一条即失败上游目录里新增 / 删除了src/fast/之外的文件LICENSE*除外某个 hunk 增加超过8行--max-hunk某个文件增加超过40行、或删除超过20行一个改过的.rs文件的某个 hunk加进去的行没有一行点名fast一个路径如crate::fast::...、mod fast;、#[path fast/...]或一个以fast_开头的标识符——一处提及即可覆盖整个 hunk因为 rustfmt 可能把一次调用折成几行。只删行的 hunk、只改use或空行的 hunk 豁免二进制文件有差异任何文件fast/也包括glob-importfastfast/之外的文件出现use fast只允许 public 的pub use fast::…导出。除了 glob 规则任何src/fast/目录下的文件从不检查。只因为可见性放开而与上游不同的一行按定义就是钩子它被记在表格的pub(crate)列不占任何预算。有正当理由的例外一行一条写进script/upstream-allowlist带上理由也可以抬高预算hunkN/addedN/removedN或允许任意改动any。通常的理由只有两类钩子密集的枢纽文件某个上游函数体被换成了对fast/的转发例如crates/gpui/src/view.rs removed210 # ViewElements cache-by-bounds is replaced by fast::retaineds retained views删除的行最贵。上游对被我们删掉的代码做的每一次改动都会在每次同步时冲突而且必须手工移植回fast/。实测数字文档写作时28 个受跟踪的上游文件与上游不同其中crates/gpui/src里的19 个文件合计 464 / −737 行——删得比加得多因为有几个方法体被换成了对fast/的一次调用。4.3 取一个新上游五步选新 commit。在一个 zed checkout 里定下新 commit在本仓库从上一个 vendor commit第一次是import_commit开分支按UPSTREAM里的tracked目录整体替换提交为新的 vendor commitzed: import hash——这个提交里只有上游代码。合并。把这个分支 merge 进我们的分支。冲突应该只落在钩子行上保留上游的代码把我们那一行钩子放回去。手工移植。对每个用#[path fast/…]重定向过的文件看上游在原始文件里改了什么git diff 旧 vendor commit 新 vendor commit -- file手工移植进我们的同名实现。这类文件不会冲突所以最容易漏。更新记录。把UPSTREAM里的zed_commit与import_commit换成新的上游增删了我们用到的 crate就同步改tracked。验证。跑script/check-upstream、测试与 clippy。检查失败时的修法也写好了把改动挪进一个fast/模块在原处留一个点名它的钩子用git diff import_commit -- file看看到底哪里不一样。5. 性能全部为 release 构建、Linux、主线程每帧 CPU。“上游”是同一份代码把每个 view 从头画等于GPUI_VIEW_RETENTION0。headless60 个面板 view × 每面板 64 个 labelretained_bench每帧被 notify 的面板数上游从头画gpui-fast变化0静止3.31 ms0.19 ms−94%1 个6.72 ms0.71 ms−89%6 个7.27 ms1.46 ms−80%全部 60 个10.33 ms7.38 ms−29%真实窗口里的gpui_perfshowcase组件画廊按 GPUI Kit 的写法243 页的侧栏、每页把状态放在带 key 的实体里的组件分区、5000 行数据表、5000 条消息的gpui::list、每 2 秒变一次并被大多数 view 读的全局实体以及一个被行情流更新的、停靠着的行情工作区分别在 gpui-fast 与上游gpui-pre0.3.7 快照上构建按 32 px/帧滚动相当于快速拖动滚动条场景上游 gpui-pregpui-fast变化一个旋转动画在跑5.91 ms / 85% CPU0.73 ms / 11% CPU−88%滚动侧栏5.95 ms / 86%0.79 ms / 12%−87%滚动一页组件5.96 ms / 82%0.79 ms / 12%−87%滚动数据表3.07 ms / 44%0.54 ms / 8%−82%每 33 ms 刷新表格11% CPU2% CPU—滚动列表2.66 ms / 39%0.39 ms / 6%−85%持续灌入行情报价5.06 ms / 43%1.35 ms / 15%−73%滚动工作区的自选列表5.23 ms / 77%1.33 ms / 24%−75%在工作区的自选列表上悬停5.16 ms / 42%1.32 ms / 14%−74%复现命令cargorun-pgpui_perf--release----autocargorun-pgpui_perf--release--no-default-features--featuresupstream ----auto总结个人理解Fast 在 GPUI 的原基础提供了一套工程化的解决方案可以在应用层不需要改变的前提下自动识别变化区域大大降低 CPU 的占用。对下游用户并不需要太多的改动。以上就是今天全部内容多多关注点赞您的支持是我更新的最大动力。我们下期见。