Fling: the vsync clock, the frame ask, and a report that can say what it measured
Iris, from her phone: "some stuttering when flinging in particular. Harder to notice with my finger directly moving the scroll." Her fling phase was 103fps on a 120Hz screen at p50 6.3ms. Two of the four things found are corrections to the instrument, not the renderer. The swapchain acquire -- `get_current_texture`, which *blocks* until the compositor frees an image -- was inside the span the report called iris's CPU work, so a fling comfortably ahead of the display read as milliseconds of being slow. A frame is now three measured parts (`FrameParts`: build, acquire, submit), per phase as well as per run. And nothing could say a frame was never *produced*: `late` counts frames that cost too much, which a reader does not see, while a frame that never happens leaves the last one up for two refreshes, which is the stutter. `PhaseStats::missed` counts vsyncs nothing was drawn for. It closes on the emulator: 1548 frames + 452 missed over 33.0s at 60Hz is 1980 vsyncs. The other two are the frame loop. `Choreographer.postFrameCallback` schedules for the next vsync after the call, and iris asked at the *end* of the callback -- so any frame whose work ran past the boundary registered too late and got the vsync after, one frame over budget silently costing a second. It is asked for immediately after `tick_animations` now, on both backends. And the fling was advanced on `Instant::now()` rather than the vsync `do_frame` carries: frames are presented on an even cadence whatever clock computes them, so sampling the spline at "whenever the callback ran" moves the content unevenly with no frame late enough to appear in any report -- and a drag never had it, which is the asymmetry Iris described. `PointerClock` is `DeviceClock` and the view keeps one, anchored by whichever of a touch or a frame comes first, so a fling is advanced on the clock its velocity was measured on. `opt-level` for the Android release build goes from "s" to 3. The table in RUST.md picked "s" on bytes alone; over the same warm fling eight times iris's own per-frame work is p90 0.15ms/p99 0.42ms at "s" against p90 0.09ms/p99 0.26ms at 3, for 1.8 MB of arm64 APK. `app-rust/tests/fling_profile.rs` is the rig that established what a fling frame actually costs and is kept for next time (Iris: "please keep the profiling rig around for future use"): only one frame in six lays anything out, and the multi-millisecond spikes are all first-pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
227f5e1295
commit
144c402181
10 files changed
+477
-151
No files matched your search
+10
-2
@@ -1,7 +1,8 @@
|
||||
use crate::task::RequestRedraw;
|
||||
use iris_core::{UiData, UiRenderNode, UiRenderState, util::Vec2};
|
||||
use iris_core::{FrameParts, UiData, UiRenderNode, UiRenderState, util::Vec2};
|
||||
use pollster::FutureExt;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use wgpu::*;
|
||||
use winit::{dpi::PhysicalSize, window::Window};
|
||||
|
||||
@@ -28,7 +29,11 @@ impl UiRenderer {
|
||||
self.ui.update(&self.device, &self.queue, ui, render);
|
||||
}
|
||||
|
||||
pub fn draw(&mut self) {
|
||||
/// The two waits, so a desktop frame divides up the same way an
|
||||
/// Android one does -- see `AndroidRenderer::draw` for why the
|
||||
/// swapchain acquire is measured apart from the work.
|
||||
pub fn draw(&mut self) -> FrameParts {
|
||||
let acquire_start = Instant::now();
|
||||
let output = match self.surface.get_current_texture() {
|
||||
CurrentSurfaceTexture::Success(texture)
|
||||
| CurrentSurfaceTexture::Suboptimal(texture) => texture,
|
||||
@@ -39,6 +44,7 @@ impl UiRenderer {
|
||||
// comment was written about.
|
||||
other => panic!("no surface texture to draw into: {other:?}"),
|
||||
};
|
||||
let acquire = acquire_start.elapsed();
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&TextureViewDescriptor::default());
|
||||
@@ -60,6 +66,7 @@ impl UiRenderer {
|
||||
self.ui.draw(render_pass);
|
||||
}
|
||||
|
||||
let submit_start = Instant::now();
|
||||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
// Immediately before presenting, so the windowing system can schedule
|
||||
// the frame. On Wayland this is what ties the commit to the surface's
|
||||
@@ -69,6 +76,7 @@ impl UiRenderer {
|
||||
// starts, with nothing left to flush it.
|
||||
self.window.pre_present_notify();
|
||||
self.queue.present(output);
|
||||
FrameParts::waits(acquire, submit_start.elapsed())
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
|
||||
|
||||
Reference in new issue
Block a user