gallery/vendor/i-slint-core/properties/ffi.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

664 lines
23 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::*;
use crate::graphics::{Brush, Color};
use crate::items::PropertyAnimation;
use core::ffi::c_void;
#[repr(C)]
/// Has the same layout as PropertyHandle
pub struct PropertyHandleOpaque(PropertyHandle);
/// Initialize the first pointer of the Property. Does not initialize the content.
/// `out` is assumed to be uninitialized
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_init(out: *mut PropertyHandleOpaque) {
unsafe { core::ptr::write(out, PropertyHandleOpaque(PropertyHandle::default())) };
}
/// To be called before accessing the value
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_update(handle: &PropertyHandleOpaque, val: *mut c_void) {
unsafe {
let handle = Pin::new_unchecked(&handle.0);
handle.update(val);
handle.register_as_dependency_to_current_binding();
}
}
/// Register this property as a dependency of the current tracking scope
/// without evaluating any binding.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_register_as_dependency(handle: &PropertyHandleOpaque) {
unsafe {
let handle = Pin::new_unchecked(&handle.0);
handle.register_as_dependency_to_current_binding();
}
}
/// Mark the fact that the property was changed and that its binding need to be removed, and
/// the dependencies marked dirty.
/// To be called after the `value` has been changed
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_set_changed(
handle: &PropertyHandleOpaque,
value: *const c_void,
) {
unsafe {
if !handle.0.access(|b| {
b.is_some_and(|b| (b.vtable.intercept_set)(&*b as *const BindingHolder, value))
}) {
handle.0.remove_binding();
}
handle.0.mark_dirty();
}
}
fn make_c_function_binding(
binding: extern "C" fn(*mut c_void, *mut c_void),
user_data: *mut c_void,
drop_user_data: Option<extern "C" fn(*mut c_void)>,
intercept_set: Option<
extern "C" fn(user_data: *mut c_void, pointer_to_value: *const c_void) -> bool,
>,
intercept_set_binding: Option<
extern "C" fn(user_data: *mut c_void, new_binding: *mut c_void) -> bool,
>,
) -> impl BindingCallable<c_void> {
struct CFunctionBinding<T> {
binding_function: extern "C" fn(*mut c_void, *mut T),
user_data: *mut c_void,
drop_user_data: Option<extern "C" fn(*mut c_void)>,
intercept_set:
Option<extern "C" fn(user_data: *mut c_void, pointer_to_value: *const T) -> bool>,
intercept_set_binding:
Option<extern "C" fn(user_data: *mut c_void, new_binding: *mut c_void) -> bool>,
}
impl<T> Drop for CFunctionBinding<T> {
fn drop(&mut self) {
if let Some(x) = self.drop_user_data {
x(self.user_data)
}
}
}
unsafe impl<T> BindingCallable<T> for CFunctionBinding<T> {
fn evaluate(self: Pin<&Self>, value: &mut T) -> BindingResult {
(self.binding_function)(self.user_data, value as *mut T);
BindingResult::KeepBinding
}
fn intercept_set(self: Pin<&Self>, value: &T) -> bool {
match self.intercept_set {
None => false,
Some(intercept_set) => intercept_set(self.user_data, value as *const T),
}
}
unsafe fn intercept_set_binding(self: Pin<&Self>, new_binding: *mut BindingHolder) -> bool {
match self.intercept_set_binding {
None => false,
Some(intercept_set_b) => intercept_set_b(self.user_data, new_binding.cast()),
}
}
}
CFunctionBinding {
binding_function: binding,
user_data,
drop_user_data,
intercept_set,
intercept_set_binding,
}
}
/// Set a binding
///
/// The current implementation will do usually two memory allocation:
/// 1. the allocation from the calling code to allocate user_data
/// 2. the box allocation within this binding
///
/// It might be possible to reduce that by passing something with a
/// vtable, so there is the need for less memory allocation.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_set_binding(
handle: &PropertyHandleOpaque,
binding: extern "C" fn(user_data: *mut c_void, pointer_to_value: *mut c_void),
user_data: *mut c_void,
drop_user_data: Option<extern "C" fn(*mut c_void)>,
intercept_set: Option<
extern "C" fn(user_data: *mut c_void, pointer_to_value: *const c_void) -> bool,
>,
intercept_set_binding: Option<
extern "C" fn(user_data: *mut c_void, new_binding: *mut c_void) -> bool,
>,
) {
let binding = make_c_function_binding(
binding,
user_data,
drop_user_data,
intercept_set,
intercept_set_binding,
);
unsafe { handle.0.set_binding(binding) };
}
/// Set a binding using an already allocated building holder
///
/// (take ownership of the binding)
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_set_binding_internal(
handle: &PropertyHandleOpaque,
binding: *mut c_void,
) {
handle.0.set_binding_impl(binding.cast());
}
/// Delete a binding. The pointer must be a pointer to a binding (so a BindingHolder)
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_delete_binding(binding: *mut c_void) {
let b = binding as *mut BindingHolder;
unsafe { ((*b).vtable.drop)(b) };
}
/// Evaluate a raw binding
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_evaluate_binding(binding: *mut c_void, value: *mut c_void) {
let b = binding as *mut BindingHolder;
unsafe { ((*b).vtable.evaluate)(b, value) };
}
/// Call `intercept_set` on a raw binding, returning whether the binding accepted the write
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_intercept_set_binding(
binding: *mut c_void,
value: *const c_void,
) -> bool {
let b = binding as *mut BindingHolder;
unsafe { ((*b).vtable.intercept_set)(b, value) }
}
/// Returns whether the property behind this handle is marked as dirty
#[unsafe(no_mangle)]
pub extern "C" fn slint_property_is_dirty(handle: &PropertyHandleOpaque) -> bool {
handle.0.access(|binding| binding.is_some_and(|b| b.dirty.get()))
}
/// Marks the property as dirty and notifies dependencies.
#[unsafe(no_mangle)]
pub extern "C" fn slint_property_mark_dirty(handle: &PropertyHandleOpaque) {
handle.0.mark_dirty()
}
/// Returns true if a binding is currently being evaluated, so that property
/// accesses register dependencies.
#[unsafe(no_mangle)]
pub extern "C" fn slint_property_is_currently_tracking() -> bool {
crate::properties::is_currently_tracking()
}
/// Marks the property as dirty and notifies dependencies.
#[unsafe(no_mangle)]
pub extern "C" fn slint_property_set_constant(handle: &PropertyHandleOpaque) {
handle.0.set_constant()
}
/// Destroy handle
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_drop(handle: *mut PropertyHandleOpaque) {
unsafe {
core::ptr::drop_in_place(handle);
}
}
fn c_set_animated_value<T: InterpolatedPropertyValue + Clone>(
handle: &PropertyHandleOpaque,
from: T,
to: T,
animation_data: &PropertyAnimation,
) {
let d = RefCell::new(properties_animations::PropertyValueAnimationData::new(
from,
Some(to),
animation_data.clone(),
));
// Safety: The BindingCallable is for type T
unsafe {
handle.0.set_binding(move |val: &mut T| {
let (value, finished) = d.borrow_mut().compute_interpolated_value();
*val = value;
if finished {
BindingResult::RemoveBinding
} else {
crate::animations::CURRENT_ANIMATION_DRIVER
.with(|driver| driver.set_has_active_animations());
BindingResult::KeepBinding
}
})
};
handle.0.mark_dirty();
}
/// Internal function to set up a property animation to the specified target value for an integer property.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_set_animated_value_int(
handle: &PropertyHandleOpaque,
from: i32,
to: i32,
animation_data: &PropertyAnimation,
) {
c_set_animated_value(handle, from, to, animation_data)
}
/// Internal function to set up a property animation to the specified target value for a float property.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_set_animated_value_float(
handle: &PropertyHandleOpaque,
from: f32,
to: f32,
animation_data: &PropertyAnimation,
) {
c_set_animated_value(handle, from, to, animation_data)
}
/// Internal function to set up a property animation to the specified target value for a color property.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_set_animated_value_color(
handle: &PropertyHandleOpaque,
from: Color,
to: Color,
animation_data: &PropertyAnimation,
) {
c_set_animated_value(handle, from, to, animation_data);
}
/// Internal function to set up a property animation to the specified target value for a brush property.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_set_animated_value_brush(
handle: &PropertyHandleOpaque,
from: &Brush,
to: &Brush,
animation_data: &PropertyAnimation,
) {
c_set_animated_value(handle, from.clone(), to.clone(), animation_data);
}
unsafe fn c_set_animated_binding<T: InterpolatedPropertyValue + Clone>(
handle: &PropertyHandleOpaque,
binding: extern "C" fn(*mut c_void, *mut T),
user_data: *mut c_void,
drop_user_data: Option<extern "C" fn(*mut c_void)>,
transition_data: extern "C" fn(
user_data: *mut c_void,
start_instant: &mut *mut u64,
) -> PropertyAnimation,
) {
unsafe {
let binding = core::mem::transmute::<
extern "C" fn(*mut c_void, *mut T),
extern "C" fn(*mut c_void, *mut c_void),
>(binding);
let original_binding = PropertyHandle {
handle: Cell::new(
(alloc_binding_holder(make_c_function_binding(
binding,
user_data,
drop_user_data,
None,
None,
)) as *mut ())
.map_addr(|a| a | 0b10),
),
};
let animation_data = RefCell::new(properties_animations::PropertyValueAnimationData::new(
T::default(),
None,
PropertyAnimation::default(),
));
handle.0.set_binding(properties_animations::AnimatedBindingCallable::<T, _> {
original_binding,
state: Cell::new(properties_animations::AnimatedBindingState::NotAnimating),
animation_data,
compute_animation_details: move || -> properties_animations::AnimationDetail {
// The transition_data function receives a *mut *mut u64 pointer for the
// timestamp.
// If the function sets the pointer to nullptr, it doesn't provide a start_time.
// Otherwise, we assume it has written a value to the start_instant.
// This basically models a `&mut Option<u64>`, which is then converted to an
// `Option<Instant>`
let mut start_instant = 0u64;
let mut start_instant_ref = &mut start_instant as *mut u64;
let anim = transition_data(user_data, &mut start_instant_ref);
let start_instant = if start_instant_ref.is_null() {
None
} else {
Some(crate::animations::Instant(start_instant))
};
(anim, start_instant)
},
dirty_time: Cell::new(crate::animations::current_tick()),
carried_velocity: Cell::new(0.0),
});
handle.0.mark_dirty();
}
}
/// Internal function to set up a property animation between values produced by the specified binding for an integer property.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_set_animated_binding_int(
handle: &PropertyHandleOpaque,
binding: extern "C" fn(*mut c_void, *mut core::ffi::c_int),
user_data: *mut c_void,
drop_user_data: Option<extern "C" fn(*mut c_void)>,
transition_data: extern "C" fn(
user_data: *mut c_void,
start_instant: &mut *mut u64,
) -> PropertyAnimation,
) {
unsafe {
c_set_animated_binding(handle, binding, user_data, drop_user_data, transition_data);
}
}
/// Internal function to set up a property animation between values produced by the specified binding for a float property.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_set_animated_binding_float(
handle: &PropertyHandleOpaque,
binding: extern "C" fn(*mut c_void, *mut f32),
user_data: *mut c_void,
drop_user_data: Option<extern "C" fn(*mut c_void)>,
transition_data: extern "C" fn(
user_data: *mut c_void,
start_instant: &mut *mut u64,
) -> PropertyAnimation,
) {
unsafe {
c_set_animated_binding(handle, binding, user_data, drop_user_data, transition_data);
}
}
/// Internal function to set up a property animation between values produced by the specified binding for a color property.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_set_animated_binding_color(
handle: &PropertyHandleOpaque,
binding: extern "C" fn(*mut c_void, *mut Color),
user_data: *mut c_void,
drop_user_data: Option<extern "C" fn(*mut c_void)>,
transition_data: extern "C" fn(
user_data: *mut c_void,
start_instant: &mut *mut u64,
) -> PropertyAnimation,
) {
unsafe {
c_set_animated_binding(handle, binding, user_data, drop_user_data, transition_data);
}
}
/// Internal function to set up a property animation between values produced by the specified binding for a brush property.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_set_animated_binding_brush(
handle: &PropertyHandleOpaque,
binding: extern "C" fn(*mut c_void, *mut Brush),
user_data: *mut c_void,
drop_user_data: Option<extern "C" fn(*mut c_void)>,
transition_data: extern "C" fn(
user_data: *mut c_void,
start_instant: &mut *mut u64,
) -> PropertyAnimation,
) {
unsafe {
c_set_animated_binding(handle, binding, user_data, drop_user_data, transition_data);
}
}
/// Internal function to set up a state binding on a Property<StateInfo>.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_set_state_binding(
handle: &PropertyHandleOpaque,
binding: extern "C" fn(*mut c_void) -> i32,
user_data: *mut c_void,
drop_user_data: Option<extern "C" fn(*mut c_void)>,
) {
struct CStateBinding {
binding: extern "C" fn(*mut c_void) -> i32,
user_data: *mut c_void,
drop_user_data: Option<extern "C" fn(*mut c_void)>,
}
impl Drop for CStateBinding {
fn drop(&mut self) {
if let Some(x) = self.drop_user_data {
x(self.user_data)
}
}
}
impl CStateBinding {
fn call(&self) -> i32 {
(self.binding)(self.user_data)
}
}
let c_state_binding = CStateBinding { binding, user_data, drop_user_data };
let bind_callable = StateInfoBinding {
dirty_time: Cell::new(None),
binding: move || c_state_binding.call(),
_phantom: core::marker::PhantomData::<fn() -> StateInfo>,
};
unsafe { handle.0.set_binding(bind_callable) }
}
#[repr(C)]
/// Opaque type representing the PropertyTracker
pub struct PropertyTrackerOpaque {
dependencies: usize,
dep_nodes: usize,
vtable: usize,
dirty: bool,
}
static_assertions::assert_eq_align!(PropertyTrackerOpaque, PropertyTracker);
static_assertions::assert_eq_size!(PropertyTrackerOpaque, PropertyTracker);
/// Initialize the first pointer of the PropertyTracker.
/// `out` is assumed to be uninitialized
/// slint_property_tracker_drop need to be called after that
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_tracker_init(out: *mut PropertyTrackerOpaque) {
unsafe {
core::ptr::write(out as *mut PropertyTracker, PropertyTracker::default());
}
}
/// Call the callback with the user data. Any properties access within the callback will be registered.
/// Any currently evaluated bindings or property trackers will be notified if accessed properties are changed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_tracker_evaluate(
handle: *const PropertyTrackerOpaque,
callback: extern "C" fn(user_data: *mut c_void),
user_data: *mut c_void,
) {
unsafe { Pin::new_unchecked(&*(handle as *const PropertyTracker)) }
.evaluate(|| callback(user_data))
}
/// Call the callback with the user data. Any properties access within the callback will be registered.
/// Any currently evaluated bindings or property trackers will be not notified if accessed properties are changed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_tracker_evaluate_as_dependency_root(
handle: *const PropertyTrackerOpaque,
callback: extern "C" fn(user_data: *mut c_void),
user_data: *mut c_void,
) {
unsafe { Pin::new_unchecked(&*(handle as *const PropertyTracker)) }
.evaluate_as_dependency_root(|| callback(user_data))
}
/// Query if the property tracker is dirty
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_tracker_is_dirty(
handle: *const PropertyTrackerOpaque,
) -> bool {
unsafe { (*(handle as *const PropertyTracker)).is_dirty() }
}
/// Destroy handle
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_property_tracker_drop(handle: *mut PropertyTrackerOpaque) {
unsafe { core::ptr::drop_in_place(handle as *mut PropertyTracker) };
}
#[repr(C)]
/// Opaque type representing the ChangeTracker
pub struct ChangeTrackerOpaque {
_inner: *const c_void,
}
static_assertions::assert_eq_align!(ChangeTrackerOpaque, ChangeTracker);
static_assertions::assert_eq_size!(ChangeTrackerOpaque, ChangeTracker);
/// Construct a ChangeTracker
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_change_tracker_construct(ct: *mut ChangeTrackerOpaque) {
unsafe { core::ptr::write(ct as *mut ChangeTracker, ChangeTracker::default()) };
}
/// Drop a ChangeTracker
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_change_tracker_drop(ct: *mut ChangeTrackerOpaque) {
unsafe { core::ptr::drop_in_place(ct as *mut ChangeTracker) };
}
/// initialize the change tracker
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_change_tracker_init(
ct: *const ChangeTrackerOpaque,
user_data: *mut c_void,
drop_user_data: extern "C" fn(user_data: *mut c_void),
eval_fn: extern "C" fn(user_data: *mut c_void) -> bool,
notify_fn: extern "C" fn(user_data: *mut c_void),
) {
let ct = unsafe { &*ct.cast::<ChangeTracker>() };
#[allow(non_camel_case_types)]
struct C_ChangeTrackerInner {
user_data: *mut c_void,
drop_user_data: extern "C" fn(user_data: *mut c_void),
eval_fn: extern "C" fn(user_data: *mut c_void) -> bool,
notify_fn: extern "C" fn(user_data: *mut c_void),
}
impl Drop for C_ChangeTrackerInner {
fn drop(&mut self) {
(self.drop_user_data)(self.user_data);
}
}
unsafe fn drop(_self: *mut BindingHolder) {
core::mem::drop(unsafe {
Box::from_raw(_self as *mut BindingHolder<C_ChangeTrackerInner>)
});
}
unsafe fn evaluate(_self: *const BindingHolder, _value: *mut c_void) -> BindingResult {
let _self_raw = _self;
let _self = _self as *mut BindingHolder<C_ChangeTrackerInner>;
let inner = unsafe { core::ptr::addr_of_mut!((*_self).binding).as_mut().unwrap() };
unsafe { *(*core::ptr::addr_of!((*_self).dep_nodes)).get() = Default::default() };
let notify = super::current_binding_storage::set(Some(_self_raw), || {
(inner.eval_fn)(inner.user_data)
});
if notify {
(inner.notify_fn)(inner.user_data);
}
BindingResult::KeepBinding
}
const VT: &BindingVTable = &BindingVTable {
drop,
evaluate,
mark_dirty: ChangeTracker::mark_dirty,
intercept_set: |_, _| false,
intercept_set_binding: |_, _| false,
velocity: |_| None,
};
ct.clear();
let inner = C_ChangeTrackerInner { user_data, drop_user_data, eval_fn, notify_fn };
let holder = BindingHolder {
dependencies: Cell::new(core::ptr::null_mut()),
dep_nodes: Default::default(),
vtable: 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 { ct.set_internal(raw as *mut BindingHolder) };
let inner = unsafe { core::ptr::addr_of_mut!((*raw).binding).as_mut().unwrap() };
super::current_binding_storage::set(Some(raw as *const BindingHolder), || {
(inner.eval_fn)(inner.user_data)
});
}
/// return the current animation tick for the `animation-tick` function
#[unsafe(no_mangle)]
pub extern "C" fn slint_animation_tick() -> u64 {
crate::animations::animation_tick()
}
#[cfg(test)]
mod ffi_change_tracker_leak_test {
use super::*;
use crate::properties::ChangeTracker;
use alloc::boxed::Box;
use core::cell::Cell;
use core::pin::Pin;
// What the generated C++ stores for a `changed` handler: the watched
// property and the last seen value.
struct EvalState {
prop: *const Property<i32>,
last: Cell<i32>,
}
extern "C" fn eval_fn(user_data: *mut c_void) -> bool {
let st = unsafe { &*(user_data as *const EvalState) };
let v = unsafe { Pin::new_unchecked(&*st.prop) }.get();
let changed = v != st.last.get();
st.last.set(v);
changed
}
extern "C" fn notify_fn(_user_data: *mut c_void) {}
extern "C" fn drop_fn(_user_data: *mut c_void) {}
// The dependency nodes must not accumulate across re-evaluations.
#[test]
fn ffi_change_tracker_does_not_leak_dep_nodes() {
let prop = Box::pin(Property::new(0));
let state = EvalState { prop: &*prop as *const _, last: Cell::new(0) };
let ct = ChangeTracker::default();
unsafe {
slint_change_tracker_init(
&ct as *const ChangeTracker as *const ChangeTrackerOpaque,
&state as *const EvalState as *mut c_void,
drop_fn,
eval_fn,
notify_fn,
);
}
assert_eq!(ct.test_dep_node_count(), 1);
for i in 1..=200 {
prop.as_ref().set(i);
ChangeTracker::run_change_handlers();
assert_eq!(ct.test_dep_node_count(), 1, "leaked a DependencyNode at iteration {i}");
}
}
}