gallery/vendor/i-slint-core/properties/change_tracker.rs
Greg Pomerantz 2f4768f354 Fix 120Hz scroll jank end-to-end: render-ahead present + full-cache eviction off the UI thread
Three root causes, all fixed and verified on the Pixel 9 Pro:

1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the
   render to the vsync wake; RENDER_AHEAD presents the previous frame at
   the start of render() (phase = wake + ~1.1 ms) and holds the new
   frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never
   merely dropped, before the next acquire (AlreadyAcquired panic at
   launch otherwise); discard_pending_frame presents too.

2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled
   through a ~20-cell viewport forever (137 decodes/20 s) and each
   eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes,
   ~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone
   cooldown on evicted paths (idle prefetch skips, urgent tap bypasses)
   + off-thread reaper for the freed blobs (insert: 21 ms -> 4 us).

3. (earlier) Skia final flush + Mailbox present mode (vendor, M3).

Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK
lines for cross-thread closure timing and push-site tagging.

Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact
120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero
>33 ms); PRES phase median ~994 us, zero late frames. Docs updated
(docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference).

Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch),
vkshim profiling tooling, backend made a non-optional dep, debuggable
flag for simpleperf (REMOVE before shipping).
2026-10-30 02:49:23 -04:00

451 lines
15 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
use super::{BindingHolder, BindingResult, BindingVTable, DependencyListHead, DependencyNode};
use alloc::boxed::Box;
use core::cell::{Cell, UnsafeCell};
use core::ffi::c_void;
use core::marker::PhantomPinned;
use core::pin::Pin;
use core::ptr::addr_of;
// TODO a pinned thread local key?
crate::thread_local! {static CHANGED_NODES : Pin<Box<DependencyListHead>> = Box::pin(DependencyListHead::default()) }
struct ChangeTrackerInner<T, EvalFn, NotifyFn, Data> {
eval_fn: EvalFn,
notify_fn: NotifyFn,
/// The value. Borrowed-mut when `evaluating` is true
value: UnsafeCell<T>,
data: Data,
/// When true, we are currently running eval_fn or notify_fn and we shouldn't be dropped
evaluating: Cell<bool>,
}
/// A change tracker is used to run a callback when a property value changes.
///
/// The Change Tracker must be initialized with the [`Self::init`] method.
///
/// When the property changes, the ChangeTracker is added to a thread local list, and the notify
/// callback is called when the [`Self::run_change_handlers()`] method is called
#[derive(Debug)]
pub struct ChangeTracker {
/// (Actually a `BindingHolder<ChangeTrackerInner>`)
inner: Cell<*mut BindingHolder>,
}
impl Default for ChangeTracker {
fn default() -> Self {
Self { inner: Cell::new(core::ptr::null_mut()) }
}
}
impl Drop for ChangeTracker {
fn drop(&mut self) {
self.clear();
}
}
impl ChangeTracker {
/// Initialize the change tracker with the given data and callbacks.
///
/// The `data` is any struct that is going to be passed to the functor.
/// The `eval_fn` is a function that queries and return the property.
/// And the `notify_fn` is the callback run if the property is changed
pub fn init<
Data: 'static,
T: Default + PartialEq,
EF: Fn(&Data) -> T + 'static,
NF: Fn(&Data, &T) + 'static,
>(
&self,
data: Data,
eval_fn: EF,
notify_fn: NF,
) {
self.init_impl(data, eval_fn, notify_fn, false);
}
/// Initialize the change tracker with the given data and callbacks.
///
/// Same as [`Self::init`], but the first eval function is called in a future evaluation of the event loop.
/// This means that the change tracker will consider the value as default initialized, and the eval function will
/// be called the first time if the initial value is not equal to the default constructed value.
pub fn init_delayed<
Data: 'static,
T: Default + PartialEq,
EF: Fn(&Data) -> T + 'static,
NF: Fn(&Data, &T) + 'static,
>(
&self,
data: Data,
eval_fn: EF,
notify_fn: NF,
) {
self.init_impl(data, eval_fn, notify_fn, true);
}
fn init_impl<
Data: 'static,
T: Default + PartialEq,
EF: Fn(&Data) -> T + 'static,
NF: Fn(&Data, &T) + 'static,
>(
&self,
data: Data,
eval_fn: EF,
notify_fn: NF,
delayed: bool,
) {
self.clear();
let inner = ChangeTrackerInner {
eval_fn,
notify_fn,
value: T::default().into(),
data,
evaluating: false.into(),
};
unsafe fn evaluate<
T: PartialEq,
EF: Fn(&Data) -> T + 'static,
NF: Fn(&Data, &T) + 'static,
Data: 'static,
>(
_self: *const BindingHolder,
_value: *mut c_void,
) -> BindingResult {
unsafe {
let _self_raw = _self;
let _self = _self as *const BindingHolder<ChangeTrackerInner<T, EF, NF, Data>>;
let inner = core::ptr::addr_of!((*_self).binding).as_ref().unwrap();
*(*core::ptr::addr_of!((*_self).dep_nodes)).get() = Default::default();
assert!(!inner.evaluating.get());
inner.evaluating.set(true);
let new_value = super::current_binding_storage::set(Some(_self_raw), || {
(inner.eval_fn)(&inner.data)
});
{
// Safety: We just set `evaluating` to true which means we can borrow
let inner_value = &mut *inner.value.get();
if new_value != *inner_value {
*inner_value = new_value;
(inner.notify_fn)(&inner.data, inner_value);
}
}
if !inner.evaluating.replace(false) {
// `drop` from the vtable was called while evaluating. Do it now.
core::mem::drop(Box::from_raw(
_self as *mut BindingHolder<ChangeTrackerInner<T, EF, NF, Data>>,
));
}
BindingResult::KeepBinding
}
}
unsafe fn drop<T, EF, NF, Data>(_self: *mut BindingHolder) {
unsafe {
let _self = _self as *mut BindingHolder<ChangeTrackerInner<T, EF, NF, Data>>;
let evaluating = core::ptr::addr_of!((*_self).binding)
.as_ref()
.unwrap()
.evaluating
.replace(false);
if !evaluating {
core::mem::drop(Box::from_raw(_self));
}
}
}
trait HasBindingVTable {
const VT: &'static BindingVTable;
}
impl<T: PartialEq, EF: Fn(&Data) -> T + 'static, NF: Fn(&Data, &T) + 'static, Data: 'static>
HasBindingVTable for ChangeTrackerInner<T, EF, NF, Data>
{
const VT: &'static BindingVTable = &BindingVTable {
drop: drop::<T, EF, NF, Data>,
evaluate: evaluate::<T, EF, NF, Data>,
mark_dirty: ChangeTracker::mark_dirty,
intercept_set: |_, _| false,
intercept_set_binding: |_, _| false,
velocity: |_| None,
};
}
let holder = BindingHolder {
dependencies: Cell::new(core::ptr::null_mut()),
dep_nodes: Default::default(),
vtable: <ChangeTrackerInner<T, EF, NF, Data> as HasBindingVTable>::VT,
dirty: Cell::new(false),
is_two_way_binding: false,
pinned: PhantomPinned,
binding: inner,
#[cfg(slint_debug_property)]
debug_name: "<ChangeTracker>".into(),
};
let raw = Box::into_raw(Box::new(holder));
unsafe { self.set_internal(raw as *mut BindingHolder) };
if delayed {
// Safety: raw is valid and we own it
unsafe {
let dep_nodes = &mut *(*core::ptr::addr_of!((*raw).dep_nodes)).get();
let node = dep_nodes.push_front(DependencyNode::new(raw as *const BindingHolder));
CHANGED_NODES.with(|changed_nodes| {
changed_nodes.append(node);
});
}
return;
}
let value = unsafe {
let inner = core::ptr::addr_of!((*raw).binding).as_ref().unwrap();
super::current_binding_storage::set(Some(raw as *const BindingHolder), || {
(inner.eval_fn)(&inner.data)
})
};
unsafe {
*core::ptr::addr_of_mut!((*raw).binding).as_mut().unwrap().value.get_mut() = value
};
}
/// Test helper: the number of dependency nodes the bound holder currently owns.
#[cfg(test)]
pub(crate) fn test_dep_node_count(&self) -> usize {
let inner = self.inner.get();
if inner.is_null() {
return 0;
}
unsafe { (*(*core::ptr::addr_of!((*inner).dep_nodes)).get()).iter().count() }
}
/// Clear the change tracker.
/// No notify function will be called after this.
pub fn clear(&self) {
let inner = self.inner.get();
if !inner.is_null() {
unsafe {
let drop = (*core::ptr::addr_of!((*inner).vtable)).drop;
drop(inner);
}
self.inner.set(core::ptr::null_mut());
}
}
/// Run all the change handlers that were queued, looping until no more
/// handlers are pending (up to an internal limit).
pub fn run_change_handlers() {
for _ in 0..10 {
if !Self::run_change_handlers_once() {
return;
}
}
crate::debug_log!("Slint: long changed callback chain detected");
}
/// Run one round of pending change handlers.
/// Returns `true` if any handler was executed.
pub fn run_change_handlers_once() -> bool {
CHANGED_NODES.with(|list| {
if list.is_empty() {
return false;
}
let old_list = DependencyListHead::default();
let old_list = core::pin::pin!(old_list);
DependencyListHead::swap(list.as_ref(), old_list.as_ref());
while let Some(node) = old_list.take_head() {
unsafe {
((*addr_of!((*node).vtable)).evaluate)(
node as *mut BindingHolder,
core::ptr::null_mut(),
);
}
}
true
})
}
pub(super) unsafe fn mark_dirty(_self: *const BindingHolder, _was_dirty: bool) {
unsafe {
// Take dep_nodes out so we can iterate without alias conflicts.
let dep_nodes = &mut *(*_self).dep_nodes.get();
let node_head = core::mem::take(dep_nodes);
if let Some(node) = node_head.iter().next() {
node.remove();
CHANGED_NODES.with(|changed_nodes| {
changed_nodes.append(node);
});
}
// Restore the list.
let other = core::mem::replace(dep_nodes, node_head);
debug_assert!(other.iter().next().is_none());
}
}
pub(super) unsafe fn set_internal(&self, raw: *mut BindingHolder) {
self.inner.set(raw);
}
}
#[test]
fn change_tracker() {
use super::Property;
use std::rc::Rc;
let prop1 = Rc::pin(Property::new(42));
let prop2 = Rc::pin(Property::<i32>::default());
prop2.as_ref().set_binding({
let prop1 = prop1.clone();
move || prop1.as_ref().get() * 2
});
let change1 = ChangeTracker::default();
let change2 = ChangeTracker::default();
let state = Rc::new(core::cell::RefCell::new(std::string::String::new()));
change1.init(
(state.clone(), prop1.clone()),
|(_, prop1)| prop1.as_ref().get(),
|(state, _), val| {
*state.borrow_mut() += &std::format!(":1({val})");
},
);
change2.init(
(state.clone(), prop2.clone()),
|(_, prop2)| prop2.as_ref().get(),
|(state, _), val| {
*state.borrow_mut() += &std::format!(":2({val})");
},
);
assert_eq!(state.borrow().as_str(), "");
prop1.as_ref().set(10);
assert_eq!(state.borrow().as_str(), "");
prop1.as_ref().set(30);
assert_eq!(state.borrow().as_str(), "");
ChangeTracker::run_change_handlers();
assert_eq!(state.borrow().as_str(), ":1(30):2(60)");
ChangeTracker::run_change_handlers();
assert_eq!(state.borrow().as_str(), ":1(30):2(60)");
prop1.as_ref().set(1);
assert_eq!(state.borrow().as_str(), ":1(30):2(60)");
ChangeTracker::run_change_handlers();
assert_eq!(state.borrow().as_str(), ":1(30):2(60):1(1):2(2)");
}
/// test for issue #8741
#[test]
fn delete_from_eval_fn() {
use std::cell::RefCell;
use std::rc::Rc;
use std::string::String;
let change = Rc::<RefCell<Option<ChangeTracker>>>::new(Some(ChangeTracker::default()).into());
let xyz = RefCell::new(String::from("*"));
let result = Rc::new(RefCell::new(String::new()));
let result2 = result.clone();
// The change event are run in reverse order as they are created, so this one shouldn't be ever called as it is being destroyed from `change`
let another = Rc::<RefCell<Option<ChangeTracker>>>::new(Some(ChangeTracker::default()).into());
another.borrow().as_ref().unwrap().init_delayed(
(),
|()| unreachable!(),
move |(), &()| unreachable!(),
);
change.borrow().as_ref().unwrap().init_delayed(
change.clone(),
|x| {
x.borrow_mut().take().unwrap();
String::from("hi")
},
move |x, val| {
assert!(x.borrow().is_none());
assert_eq!(val, "hi");
xyz.borrow_mut().push_str("+");
assert!(xyz.borrow().as_str().starts_with("*+"));
result2.replace(xyz.borrow().clone());
another.borrow_mut().take().unwrap();
},
);
assert_eq!(result.borrow().as_str(), "");
ChangeTracker::run_change_handlers();
assert_eq!(result.borrow().as_str(), "*+");
ChangeTracker::run_change_handlers();
assert_eq!(result.borrow().as_str(), "*+");
}
#[test]
fn change_multiple_dependencies() {
use super::Property;
use std::cell::RefCell;
use std::rc::Rc;
use std::string::String;
let prop1 = Rc::pin(Property::new(1));
let prop2 = Rc::pin(Property::new(2));
let prop3 = Rc::pin(Property::new(3));
let prop4 = Rc::pin(Property::new(4));
let prop_with_deps = Rc::pin(Property::new(5));
let result = Rc::new(RefCell::new(String::new()));
let change_tracker = ChangeTracker::default();
change_tracker.init(
result.clone(),
{
let prop1 = prop1.clone();
let prop2 = prop2.clone();
let prop3 = prop3.clone();
let prop4 = prop4.clone();
let prop_with_deps = prop_with_deps.clone();
move |_| {
prop1.as_ref().get()
+ prop2.as_ref().get()
+ prop3.as_ref().get()
+ prop4.as_ref().get()
+ prop_with_deps.as_ref().get()
}
},
move |result, val| {
*result.borrow_mut() += &std::format!("[{val}]");
},
);
assert_eq!(result.borrow().as_str(), "");
ChangeTracker::run_change_handlers();
assert_eq!(result.borrow().as_str(), "");
prop_with_deps.as_ref().set_binding({
let prop1 = prop1.clone();
let prop2 = prop2.clone();
move || prop1.as_ref().get() + prop2.as_ref().get()
});
assert_eq!(result.borrow().as_str(), "");
ChangeTracker::run_change_handlers();
assert_eq!(prop_with_deps.as_ref().get(), 3);
assert_eq!(result.borrow().as_str(), "[13]"); // 1 + 2 + 3 + 4 + 3
ChangeTracker::run_change_handlers();
assert_eq!(result.borrow().as_str(), "[13]");
prop1.as_ref().set(10);
assert_eq!(result.borrow().as_str(), "[13]");
ChangeTracker::run_change_handlers();
assert_eq!(result.borrow().as_str(), "[13][31]"); // 10 + 2 + 3 + 4 + 12
prop2.as_ref().set(20);
prop3.as_ref().set(30);
assert_eq!(result.borrow().as_str(), "[13][31]");
ChangeTracker::run_change_handlers();
assert_eq!(result.borrow().as_str(), "[13][31][94]"); // 10 + 20 + 30 + 4 + 30
ChangeTracker::run_change_handlers();
assert_eq!(result.borrow().as_str(), "[13][31][94]");
// just swap prop1 and prop2, doesn't change the outcome
prop1.as_ref().set(20);
prop2.as_ref().set(10);
ChangeTracker::run_change_handlers();
assert_eq!(result.borrow().as_str(), "[13][31][94]");
}