FEATURED · 精选文章

gpui-fps 实战与源码解析:为 GPUI 应用接入零干扰的实时性能 HUD

发布时间 / 2026/9/14 8:00:05
来源 / 创域科博编辑部
栏目 / 资讯中心
gpui-fps 实战与源码解析:为 GPUI 应用接入零干扰的实时性能 HUD gpui-fps 实战与源码解析为 GPUI 应用接入零干扰的实时性能 HUD【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kitgpui-kit 仓库中的gpui-fps是一个面向任意 GPUI 应用的实时性能 HUDHeads-Up Display提供 FPS、帧耗时、掉帧率以及本进程的 GPU、CPU、内存用量。本篇基于 crates/fps/README.md 完整展开其用法并结合 crates/fps/src 下的源码逐行印证其设计读完你将掌握如何在任意 GPUI 应用中三步接入该 HUD、如何定制角点与帧预算以及每个读数MAX FPS、FRAME、P95、DROP、INV、GPU、CPU、MEM背后的采样口径、平台实现与防自扰机制。它显示什么数据从哪来HUD 的默认形态如下MAX FPS行背后是逐帧耗时折线FRAME/P95按帧预算着色┌──────────────────────────┐ │ ﹋﹋ MAX 118 FPS ﹋︿﹏﹋ │ ← 折线走在标题数字背后 │ FRAME 8.4 ms │ ← 一帧的典型成本 │ P95 14.1 ms │ ← 慢尾的成本 │ DROP 0.0% INV 1.0 │ │ GPU 31.0% │ │ CPU 142% MEM 84 MB │ └──────────────────────────┘帧数据来自 GPUI 自己的帧追踪gpui::profiler模块的FrameTimingCollector因此数字反映的是框架在Window::draw中实际花费的时间而不是从外部估计出来的近似值。crates/fps/src/sampler.rs 中的FrameSampler持有这个 collector并额外保存窗口 ID 与呈现时刻用于过滤和速率计算。追踪折线与FRAME读数都按帧预算frame budget着色预算内绿色、不超过两倍预算琥珀色、超出则红色。标题默认是MAX FPS——“对当前窗口做完整重绘所能维持的帧率”定义为1 / FRAME并被显示器刷新率封顶。它是推导出来的而不是数出来的唯一能“数”出这个值的方式是让窗口背靠背不停地画也就是每帧付一次完整布局与绘制而这笔账会记到应用头上。右键单击可切换到FPS窗口实际呈现的每秒帧数单击则把 HUD 折叠成一个只显示帧率的小标签。相关设计说明可对照 FPS Monitor 文档。该 crate 不依赖gpui-component从 crates/fps/Cargo.toml 可见它只依赖gpui开启profilerfeature与web-time因此可用于任何 GPUI 应用。接入三步1. 添加依赖[dependencies] gpui-fps { git https://github.com/longbridge/gpui-kit }当前仓库中该 crate 版本为0.6.1见 crates/fps/Cargo.toml。一个容易被踩的坑它必须与应用解析到同一个gpui。双方依赖同一版本gpui-pre就足够——Cargo 会统一它们但一旦出现[patch]、不同的gpui-pre版本或 Zed 仓库 checkout就会产生两个互不兼容的gpuicrate报错内容是Window类型不匹配而不是版本冲突排查起来很隐蔽。2. 渲染它fps_monitor返回一个元素把它放在 HUD 应该出现的位置即可父元素必须是relative()因为 HUD 用绝对定位use gpui::*; use gpui_fps::fps_monitor; struct Example { show_fps: bool, } impl Render for Example { fn render(mut self, window: mut Window, cx: mut ContextSelf) - impl IntoElement { div() .relative() .size_full() .child(your app) .when(self.show_fps, |this| this.child(fps_monitor(window, cx))) } }每个窗口最多调用一次——第二次调用会把同一个 monitor 渲染两遍。背后的 monitor 在首次使用时创建、之后复用每个窗口一个所以每帧从render里调用没有问题。无条件显示时去掉when直接.child(fps_monitor(window, cx))。源码印证crates/fps/src/lib.rs 中fps_monitor用一个全局MonitorsHashMapWindowId, EntityFpsMonitor按窗口 ID 缓存实体命中则复用、未命中则cx.new创建并插入最后返回FpsOverlay::new(monitor)。源码注释还点明了一个工程取舍表项在窗口关闭后不会被回收泄漏量是“每个曾显示过 HUD 的窗口一个小实体”不值得为它跟踪窗口关闭事件。3. 开关它没有toggle函数——开关标志留在你自己手里这样它可观察、也便于和你的设置放在一起持久化actions!(example, [ToggleFps]); // 启动时 cx.bind_keys([KeyBinding::new(cmd-alt-f, ToggleFps, None)]); // 在持有标志的元素上 div() .on_action(cx.listener(|this: mut Example, _: ToggleFps, _, cx| { this.show_fps !this.show_fps; cx.notify(); }))如果标志放在某个全局里例如窗口其他地方的菜单要切换它改动后记得调用window.refresh()——修改全局本身不会把视图标记为 dirty。仓库内的 GPUI Component story 正是这么做的AppState.show_fps_monitor由设置菜单中的FPS Monitor项切换并通过observe_global::AppState持久化到target/state.json。单击 HUD 可把它折叠为只显示帧率的小标签再点一次展开右键在MAX与实测FPS两个口径间切换这两处交互分别由 crates/fps/src/monitor.rs 中的on_click与on_mouse_down(MouseButton::Right)监听实现后者会stop_propagation以免触发折叠。定制自己组合 FpsMonitor 与 FpsOverlayfps_monitor不接受选项。要换角点或帧预算直接组合它使用的两个部件、自己渲染 monitor 即可——它本身就是一个普通 view放在状态栏和浮在窗口上一样合适use gpui_fps::{FpsMonitor, FpsOverlay}; let monitor cx.new(|cx| { FpsMonitor::new(window, cx) .capacity(240) // 折线保留的帧数默认 120 .frame_budget(Duration::from_micros(6_944)) // 144Hz默认 60Hz .show_resources(true) // GPU、CPU、内存默认 true .resource_interval(Duration::from_millis(500)) // 默认 500ms }); // 内嵌式 div().child(monitor.clone()) // 或钉在 relative 父元素的某个角 div().relative().child(FpsOverlay::new(monitor).anchor(Anchor::BottomLeft))各 builder 方法与默认值见 crates/fps/src/monitor.rs方法默认值说明capacity(usize)120折线保留的帧数set_capacity会立即丢弃超出的旧样本frame_budget(Duration)16_666_667ns60Hz 一帧折线基线与着色的依据frame_budget()会同步把图表 y 轴下限抬到预算的两倍show_resources(bool)true是否采样并显示 CPU/内存/GPUweb 上恒为关resource_interval(Duration)500ms资源重采样间隔会被向上钳制到sysinfo::MINIMUM_CPU_UPDATE_INTERVAL见 crates/fps/src/sampler.rs 的minimum_resource_interval调色板不可配置。README 给出的理由是对比度是“承重”的见下文背景不透明度的说明允许应用覆盖它等于允许应用把 HUD 弄成不可读。FpsOverlay侧对应anchor(Anchor)默认TopRight支持九个锚点与frame_budget(Duration)实现见 crates/fps/src/overlay.rs角点用两个偏移量定位以保持 overlay 与 HUD 同尺寸居中锚点则只在需要拉伸的那条轴上拉满。为什么没有continuous选项早期版本可以让窗口背靠背地画让计数器读起来像游戏那样跳。这个能力被移除了原因在 crates/fps/src/monitor.rs 的文档注释里写得很直白标记任何一个 view 为 dirty 调度的是窗口级绘制而 GPUI 会重新渲染Entity::cached边界之外的所有 view所以由 HUD 驱动帧循环意味着每帧付一次完整布局与绘制——一个空闲窗口上大约占满 60% 的单核——而 HUD 又会把这笔开销当作应用的开销上报。MAX FPS用FRAME推导出了同样的答案却一帧都不用画HUD 自身每 500ms 才请求一帧为了挪动数字并且把这一帧从读数中剔除机制见下文“自扰剔除”。每个读数的口径源码级Readout结构crates/fps/src/monitor.rs汇总了 HUD 上的全部数字update_readout每 500msREADOUT_INTERVAL重算一次。下面逐项说明其口径与“为什么”。更新节奏追踪逐帧读数每秒两次。折线跟随每一帧但数字只以 2Hz 重发布——逐帧重算时数字闪烁太快眼睛会追“变化”而不是“值”。FRAME取的是区间均值而不是最新一帧在这种节奏下最新一帧只是一个任意样本。MAX FPS是推导值且有刷新率封顶。计算在 crates/fps/src/monitor.rs 的sustainable_raterate 1 / mean_draw若平台能给出面板刷新周期则取min(rate, 1/period)。封顶补的是推导丢掉的那一半数呈现帧数天然不会超过刷新率帧随 vsync 进合成器界限免费获得而一帧画 3ms 会读成 333——一个谁也看不到的值。平台说不出刷新率时返回None宁可显示不封顶的读数也不猜一个对应测试the_headline_rate_is_what_a_frame_costs_and_the_panel_allowscrates/fps/src/monitor.rs验证了 3ms 帧在 60Hz 面板上读 60、20ms 帧读 50、无面板信息时读 333.3、零帧读 0 这四种情形。刷新率本身从平台查询crates/fps/src/refresh.rs 在 macOS 走 CoreGraphics 的display_mode在 Windows 走EnumDisplaySettingsW在 Wayland 上重新枚举输出并按 GPUI 使用的Uuid::new_v5(NAMESPACE_DNS, name)规则匹配输出名crates/fps/src/refresh.rs该模块文档明确解释了为什么不能从帧间隙反推刷新率——间隙是面板周期的整数倍只能给出下界。FRAME与P95均值与慢尾双视角。FRAME是保留帧的Window::draw平均耗时P95是 95% 保留帧低于的耗时实现见 crates/fps/src/sampler.rs 的percentile_draw——取最近秩round而不是插值保证 HUD 显示的每一个值都是一帧真实画出来的耗时。一串快帧会把均值拉下来盖住尖峰所以只看均值会在用户能感知的卡顿中读出“舒适”两条一起才同时回答“一帧通常花多少”和“它的慢尾花多少”。P95由构造对单个离群值稳健——单个离群值由折线和坐标轴显示。源码里专门有测试刻画这个分离the_percentile_separates_a_stutter_the_mean_absorbs18 帧 4ms 2 帧 80ms均值 12ms 而 P95 80ms与one_slow_frame_in_twenty_does_not_move_the_percentilecrates/fps/src/sampler.rs。DROP与INV两类冗余工作。DROP是超出预算的帧占比over_budget_ratio见 crates/fps/src/sampler.rs。INV是合并进一帧的 invalidation 平均数等于 1 表示每次被要求重绘都变成了一帧远大于 1 表示窗口被要求的频率远高于它能应答的频率多出来的就是被丢掉的工作。它完全不会体现在帧耗时上——每一帧真正画出来的都可能很快。它是 HUD 里唯一不着色的读数基线取决于应用怎么驱动自己的重绘动画每 tick 要一帧、数据流每条消息都失效都合理地大于 1这不是 HUD 能判定对错的。标题行不着色。掉帧的原因未必是应用慢——窗口空闲或被遮挡、显示器要求更少帧——给标题上色会把每一种都变成下方各行都会反驳的警报。“成本过高”是关于帧的问题帧的行已经用颜色回答了。冷启动与批量读取不会污染读数。FrameSampler有两个防御WARMUP_FRAMES 8的前置丢弃窗口最初几帧最贵——shader、字形图集、图标所有缓存都冷——实测首帧可达 100ms 对 16ms 的预算以及drained_backlog首次读取会排空进程启动以来 GPUI 记录的所有历史后开的 HUD 不该为没在场的历史背书。对应测试the_cold_start_never_reaches_the_readingscrates/fps/src/sampler.rs验证冷启动帧永远不进入均值、P95 与掉帧率。FPS与INTERVAL数的是呈现不是绘制。采样器区分FrameEvent::Draw与FrameEvent::Present后者按窗口 ID 过滤并用帧自己的present_end打时间戳而非采样时刻——HUD 只在窗口绘制时才读追踪按读取时刻打戳会把一批帧塌缩到一个瞬间速率就取决于 HUD 多久看一次而不是窗口多久呈现一次。速率公式是(n-1)/spann 帧界定 n-1 个区间测试fps_is_frames_divided_by_the_span_they_cover与fps_is_taken_from_when_frames_were_presented_not_when_they_were_readcrates/fps/src/sampler.rs分别验证这两点呈现时间戳保留在 1 秒滚动窗口FPS_WINDOW内窗口空闲后速率诚实地归零而折线历史仍留在屏上。自扰剔除HUD 自己引起的那帧不算账。HUD 每 500ms 用cx.notify()挪数字这在 GPUI 看来和其他 invalidation 一样窗口整帧重画、追踪记下这一画。若留入读数HUD 等于在测量自己。机制在 crates/fps/src/sampler.rs 的expect_own_frame时钟触发前先登记时刻与累计 notify 数随后第一帧若携带的 invalidation 数不超过登记的判定为“纯 HUD 帧”丢弃若与应用的失效合并了invalidation 数更多则这帧也是应用要的保留。被遮挡的窗口会把多次 notify 堆在恢复后的那一帧里一起应答因此登记的是计数而非布尔标志。这组行为有四个专门测试覆盖crates/fps/src/sampler.rs纯 HUD 帧不计样、合并帧仍计样、多次未应答 tick 仍是一帧 HUD 的、时钟触发前画出的帧属于应用。背景是刻意的高不透明度alpha 0.92。GPUI 无法读取元素下方的像素HUD 没有途径去适应它盖住的内容唯一能在任意窗口背景上保持可读的办法就是把那个背景挡在合成之外。0.92 保证纯白底下每个前景色都达到 4.5:1 对比度。按窗口过滤 追踪引用计数。GPUI 把帧耗时写入进程级缓冲区所以采样器必须按窗口 ID 过滤每个窗口需要自己的 monitorfps_monitor已替你做了。帧追踪是一个全局开关关闭时会清空缓冲区因此 monitor 用引用计数守护它最后一个 guard 才关且若首个 guard 获取前追踪已开启宿主应用自己开着做 profiling则永不关闭。实现是 crates/fps/src/lib.rs 的FrameTraceGuardacquire时以set_trace_enabled(true)的返回值判断所有权测试dropping_an_inner_guard_keeps_tracing_on_for_the_outer_guard验证内层销毁不影响外层。GPU / CPU / MEM 行平台采样实现三项资源读数由后台线程采样每个读数都是过去 3 秒RESOURCE_WINDOW默认间隔下约 6 个样本的均值——它们是两次采样之间就在漂移的量的粗样本按原始节奏发布会在描述同一稳态负载的相邻读数间跳百分之几十。ResourceProbe的sample()返回的正是窗口均值平均逻辑放在探针一侧它知道节奏渲染线程不做算术crates/fps/src/sampler.rs。由于refresh是一次阻塞的进程表遍历探针绝不放在渲染线程上运行web 上没有进程可采资源行整体不可用。CPU单核刻度刻意不除以核数。口径与top、Activity Monitor、Task Manager 的每进程列一致100 一个饱和的逻辑核铺到一个半核的进程读 140。若把 100 归一化为整机同样的工作在 4 核笔记本上读 12%、24 核桌面上读 2%所有有意义的值都被压进量程底部——钉住一个核的 UI 线程会看起来空闲。format_cpu的小数位策略10 保留一位见 crates/fps/src/monitor.rs 及测试formats_cpu_on_the_single_core_scale。MEM本进程“负责”的内存不是 RSS。RSS 会把进程映射的所有共享库只读页都算进去——对一个窗口化应用那是几百 MB 的图形栈代码进程既没分配也不能释放且机器上其他每个窗口也在映射它这个数字会随别的程序启动而变动。每个平台读自己活动监视器展示的那个计数器没有对应计数器的平台退回 RSS设计动机完整写在 crates/fps/src/memory.rsmacOS—proc_pid_rusage的ri_phys_footprintActivity Monitor 内存列与 jetsam 判决使用的计数器Windows—GetProcessMemoryInfo的PrivateUsage即 Task Manager 显示的 commit sizeLinux—/proc/self/status的RssAnon驻留匿名内存堆、栈、私有映射不含映射的文件。更贴切的对应量smaps_rollup的Private_Dirty需要在地址空间锁下遍历所有映射——单次读取约 425µs 对 5µs——HUD 不该为了几 MB 的偏移去扰动它测量的对象。该模块的测试a_reading_followswhat_the_process_allocates_in_bytescrates/fps/src/memory.rs通过分配 64MiB 压载并观察读数增长来验证各后端的单位正确性——Linux 计数器以 KiB 发布转换是各后端必须做对的一件事。GPU本进程的份额按平台读“每进程归因”的计数器。与旁边的 CPU 一致它是本进程的份额而非整机读数——数出合成器和其他窗口的负载只会随应用无法行动的原因而变动。各平台都不需要厂商 SDK 或提权crates/fps/src/gpu.rs 与平台后端macOS— IO registry 中本进程拥有的 accelerator client 的accumulatedGPUTime之和即 Activity Monitor GPU 列的计数器。crates/fps/src/gpu/macos.rs 中按IOUserClientCreator以pid id,前缀过滤 client两次读数间的增量除以墙钟时间得到占比文档注释说明了为什么不用PerformanceStatistics设备级会把别人的负载算进来和task_info的task_gpu_utilisationApple 芯片上恒为 0。Windows—GPU EnginePDH 计数器过滤到本进程自己的实例即 Task Manager GPU 列的来源。Linux—/proc/self/fdinfo的drm-engine-*计数即nvtop与intel_gpu_top读取的东西。crates/fps/src/gpu/linux.rs 按drm-client-id去重同一 client 的多个描述符重复同一总数并跳过drm-engine-capacity-*它数的是引擎个数而不是纳秒单测reads_engine_times_and_skips_the_capacity_counters专门验证。Windows 与 Linux 上多个引擎可同时运行render、copy、video decode所以读数取最忙的引擎类型而非求和——求和可能在 GPU 仍有余量时越过 100%crates/fps/src/gpu/linux.rs 用reduce(f64::max)实现外层再clamp(0..100)兜底crates/fps/src/gpu.rs。读不到就整行省略而不是显示一个平的 0。GpuProbe::new返回None表示该平台根本没有计数器、或本机不发布它此时 HUD 直接不放这一行sample返回None表示只是暂时取不到保留上一个读数。典型场景web、不发布AppUsage的 Intel Mac、把记账留在 NVML 而非fdinfo的 nvidia 专有驱动见 crates/fps/src/gpu/linux.rs。ResourceHistory::mean对 GPU 缺口也有专门处理只对有值的读数取平均一次瞬时缺口不会被读成向零的下沉测试a_gap_in_the_gpu_counter_does_not_read_as_a_dipcrates/fps/src/sampler.rs。运行示例与测试仓库自带一个可运行的示例cargo run -p fps_monitor它是 three.jswebgl_lines_colorsdemo 的移植——用向心 Catmull-Rom 样条平滑的 Hilbert 曲线曲线数量可调因此可以观察折线对真实渲染负载的反应。示例入口在 examples/fps_monitor/src/main.rs。测试覆盖集中在统计口径上crates/fps/src/sampler.rs 包含窗口过滤、容量淘汰、冷启动、呈现打戳、(n-1)/span、常见刷新率30/60/120/144Hz复现、P95 秩选择、invalidation 平均、资源历史窗口平均等约 20 个用例crates/fps/src/monitor.rs 覆盖sustainable_rate的封顶逻辑、builder 默认值与 CPU/内存格式化。小结接入gpui-fps只依赖gpuiprofilerfeature与应用的gpui解析一致即可fps_monitor(window, cx)放进relative()父元素每窗口一次开关标志留在应用侧全局态改动后记得window.refresh()。定制组合FpsMonitorcapacity/frame_budget/show_resources/resource_interval与FpsOverlayanchor/frame_budget调色板不可配。零自扰HUD 不驱动帧循环没有continuous模式标题是1/FRAME的推导值其每 500ms 的唯一一帧通过expect_own_frame从读数中剔除帧追踪开关按引用计数守护不抢占宿主应用。读数口径FPS数呈现1s 滚动窗、(n-1)/spanFRAME/P95按预算着色INV报告但不评判CPU单核刻度MEM是本进程私有内存而非 RSSGPU取本进程最忙引擎占比无每进程计数器时整行省略。参考路径crates/fps/README.md、crates/fps/src/lib.rs、crates/fps/src/monitor.rs、crates/fps/src/sampler.rs、crates/fps/src/overlay.rs、crates/fps/src/refresh.rs、crates/fps/src/gpu/macos.rs、crates/fps/src/gpu/linux.rs、crates/fps/src/memory.rs、examples/fps_monitor/src/main.rs。【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻