use std::{
cell::{Cell, RefCell},
rc::Rc,
time::{Duration, Instant},
};
use gpui_authoring::{
AnyWindowHandle, App, AppContext as _, Context, Entity, EntitySlotExt as _, FocusId,
FramePipeline, IntoElement, ParentElement as _, PreparedRoots, Render,
StandardImmediatePipeline, Styled as _, TestAppContext, Window, WindowMetrics, div, px,
};
use gpui_runtime::{FramePipelineExt, PhaseMetrics};
struct Counter(usize);
struct SlotView {
counter: Entity<Counter>,
}
impl Render for SlotView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(
self.counter
.slot("counter", |state, _window, _cx| {
div().w(px(state.0 as f32)).h(px(10.0))
})
.size_full(),
)
}
}
#[test]
fn an_instrumented_pipeline_measures_the_root_passes() {
let mut cx = TestAppContext::single();
let metrics = Rc::new(RefCell::new(PhaseMetrics::default()));
cx.update({
let metrics = metrics.clone();
move |cx| {
cx.set_frame_pipeline_factory(Rc::new(move |_| {
Box::new(StandardImmediatePipeline.instrumented(metrics.clone()))
}));
}
});
let counter = cx.new(|_| Counter(10));
let window: AnyWindowHandle = cx.add_window(move |_, _| SlotView { counter }).into();
cx.update_window(window, |_, window, cx| {
window.draw(cx).clear(cx);
})
.unwrap();
let metrics = *metrics.borrow();
assert!(metrics.frames > 0, "the pipeline drove a frame");
assert!(metrics.evaluate_passes > 0, "the roots were gathered");
assert_eq!(
metrics.evaluate_passes, metrics.layout_passes,
"every frame that gathered its roots laid them out"
);
assert_eq!(
metrics.layout_passes, metrics.paint_passes,
"every frame that laid its roots out painted them"
);
assert!(
metrics.root_passes() > Duration::ZERO,
"the passes were timed"
);
}
struct PacingPipeline {
frames: Rc<Cell<usize>>,
asks: Rc<Cell<usize>>,
allow: Rc<Cell<bool>>,
}
impl FramePipeline for PacingPipeline {
fn should_render(&mut self, is_dirty: bool, _metrics: &WindowMetrics) -> bool {
self.asks.set(self.asks.get() + 1);
self.allow.get() && is_dirty
}
fn begin_frame(&mut self, window: &mut Window<'_>, cx: &mut App) {
self.frames.set(self.frames.get() + 1);
window.begin_frame(cx);
}
}
#[test]
fn a_pipeline_written_outside_the_framework_can_defer_a_frame() {
let mut cx = TestAppContext::single();
let frames = Rc::new(Cell::new(0));
let asks = Rc::new(Cell::new(0));
let allow = Rc::new(Cell::new(false));
cx.update({
let frames = frames.clone();
let asks = asks.clone();
let allow = allow.clone();
move |cx| {
cx.set_frame_pipeline_factory(Rc::new(move |_| {
Box::new(PacingPipeline {
frames: frames.clone(),
asks: asks.clone(),
allow: allow.clone(),
})
}));
}
});
let counter = cx.new(|_| Counter(10));
let window = cx.add_window(move |_, _| SlotView { counter });
frames.set(0);
asks.set(0);
window.update(&mut cx, |_, _, cx| cx.notify()).unwrap();
assert!(asks.get() > 0, "the pipeline was asked about the frame");
assert_eq!(frames.get(), 0, "the deferred frame was not drawn");
allow.set(true);
cx.update(|_| {});
assert!(
frames.get() > 0,
"the deferred frame was drawn once allowed"
);
}
#[test]
fn a_throttled_pipeline_defers_a_frame_that_arrives_too_soon() {
let mut cx = TestAppContext::single();
let metrics = Rc::new(RefCell::new(PhaseMetrics::default()));
cx.update({
let metrics = metrics.clone();
move |cx| {
cx.set_frame_pipeline_factory(Rc::new(move |_| {
Box::new(
StandardImmediatePipeline
.max_fps(1)
.instrumented(metrics.clone()),
)
}));
}
});
let counter = cx.new(|_| Counter(10));
let window = cx.add_window(move |_, _| SlotView { counter });
metrics.borrow_mut().frames = 0;
window.update(&mut cx, |_, _, cx| cx.notify()).unwrap();
assert!(metrics.borrow().frames > 0, "the first frame was drawn");
metrics.borrow_mut().frames = 0;
window.update(&mut cx, |_, _, cx| cx.notify()).unwrap();
assert_eq!(
metrics.borrow().frames,
0,
"the frame that arrived too soon was deferred"
);
}
struct OutOfTreeThrottle<P> {
inner: P,
min_interval: Duration,
last_render: Option<Instant>,
}
impl<P> OutOfTreeThrottle<P> {
fn max_fps(inner: P, max_fps: u32) -> Self {
Self {
inner,
min_interval: Duration::from_secs_f64(1.0 / f64::from(max_fps.max(1))),
last_render: None,
}
}
}
impl<P: FramePipeline> FramePipeline for OutOfTreeThrottle<P> {
fn should_render(&mut self, is_dirty: bool, metrics: &WindowMetrics) -> bool {
if !self.inner.should_render(is_dirty, metrics) {
return false;
}
let now = Instant::now();
if let Some(last_render) = self.last_render
&& now.duration_since(last_render) < self.min_interval
{
return false;
}
self.last_render = Some(now);
true
}
fn begin_frame(&mut self, window: &mut Window<'_>, cx: &mut App) {
self.inner.begin_frame(window, cx);
}
fn evaluate_roots(&mut self, window: &mut Window<'_>, cx: &mut App) -> PreparedRoots {
self.inner.evaluate_roots(window, cx)
}
fn layout_roots(&mut self, window: &mut Window<'_>, roots: &mut PreparedRoots, cx: &mut App) {
self.inner.layout_roots(window, roots, cx);
}
fn paint_roots(&mut self, window: &mut Window<'_>, roots: PreparedRoots, cx: &mut App) {
self.inner.paint_roots(window, roots, cx);
}
fn finish_frame(&mut self, window: &mut Window<'_>, cx: &mut App) {
self.inner.finish_frame(window, cx);
}
fn complete_frame(&mut self, window: &mut Window<'_>, cx: &mut App) -> Option<FocusId> {
self.inner.complete_frame(window, cx)
}
fn end_frame(&mut self, window: &mut Window<'_>, cx: &mut App, focus: Option<FocusId>) {
self.inner.end_frame(window, cx, focus);
}
}
#[test]
fn a_decorator_written_outside_the_framework_can_cap_a_frame_rate() {
let mut cx = TestAppContext::single();
let metrics = Rc::new(RefCell::new(PhaseMetrics::default()));
cx.update({
let metrics = metrics.clone();
move |cx| {
cx.set_frame_pipeline_factory(Rc::new(move |_| {
Box::new(OutOfTreeThrottle::max_fps(
StandardImmediatePipeline.instrumented(metrics.clone()),
1,
))
}));
}
});
let counter = cx.new(|_| Counter(10));
let window = cx.add_window(move |_, _| SlotView { counter });
metrics.borrow_mut().frames = 0;
window.update(&mut cx, |_, _, cx| cx.notify()).unwrap();
assert!(metrics.borrow().frames > 0, "the first frame was drawn");
metrics.borrow_mut().frames = 0;
window.update(&mut cx, |_, _, cx| cx.notify()).unwrap();
assert_eq!(
metrics.borrow().frames,
0,
"the frame that arrived too soon was deferred"
);
}