Skip to main content

ferroui_base/diagnostics/
perf_counters.rs

1//! Counters of the work the framework does per operation: how many property
2//! changes are raised and how many of them anybody listens to, how many
3//! styles are evaluated and how many match, how many resources are looked
4//! up, how many bindings and text layouts are created. The performance
5//! designs (`docs/porting/performance/designs`) each start by reading them.
6//!
7//! An addition of the port (DEVIATIONS.md, Diagnostics): upstream has no
8//! counterpart. The counters exist only with the feature `perf-counters`.
9//! Without it [`perf_count!`](crate::perf_count) and
10//! [`perf_count_virtual!`](crate::perf_count_virtual) expand to no code, the
11//! functions of this module have empty bodies and
12//! [`PerfCountersSnapshot`] has no size; no type of the framework gains a
13//! field either way.
14//!
15//! The counters are per thread (the thread of the dispatcher is the one that
16//! is measured), and counting changes no behaviour. Read them around the
17//! measured work:
18//!
19//! ```ignore
20//! let before = perf_counters::snapshot();
21//! // ... the work ...
22//! let counted = perf_counters::snapshot().since(&before);
23//! println!("{}", counted.report_per(rows, "row"));
24//! ```
25
26use std::fmt;
27
28/// Whether the counters are compiled in (the feature `perf-counters`).
29pub const ENABLED: bool = cfg!(feature = "perf-counters");
30
31macro_rules! perf_counters {
32    ($($(#[$meta:meta])* $name:ident => $label:literal,)*) => {
33        /// What is counted. Each counter names, in its documentation, the
34        /// function that counts it.
35        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36        #[repr(usize)]
37        pub enum PerfCounter {
38            $($(#[$meta])* $name,)*
39        }
40
41        impl PerfCounter {
42            /// Every counter, in the order of the report.
43            pub const ALL: &'static [PerfCounter] = &[$(PerfCounter::$name,)*];
44
45            /// The name of the counter in the report.
46            pub const fn name(self) -> &'static str {
47                match self {
48                    $(PerfCounter::$name => $label,)*
49                }
50            }
51        }
52    };
53}
54
55perf_counters! {
56    // --- property changes (design 02) ---
57    /// `FerroObject::raise_property_changed`: every notification built and
58    /// passed to the `on_property_changed` chain of the class.
59    PropertyChangesRaised => "property changes raised",
60    /// `FerroObject::raise_property_changed`: the ones of an effective
61    /// value, which go on to the handlers of the property and the listeners
62    /// of the object.
63    PropertyChangesEffective => "property changes of an effective value",
64    /// `PropertyChangedObservable::notify`: the property has at least one
65    /// handler (the class handlers and the subscriptions to the property).
66    PropertyChangesWithPropertyHandlers => "property changes with handlers of the property",
67    /// `FerroObject::raise_property_changed`: the object has at least one
68    /// listener of its property changes.
69    PropertyChangesWithObjectListeners => "property changes with listeners of the object",
70
71    // --- inheritance (design 03) ---
72    /// `ValueStore::set_inheritance_parent`: the inheritance ancestor of an
73    /// object changed.
74    InheritanceAncestorChanges => "inheritance ancestor changes",
75    /// `ValueStore::set_inheritance_parent`: the inherited properties of the
76    /// old and the new ancestor chain that were paired.
77    InheritedValuesCompared => "inherited values compared on an ancestor change",
78    /// `ValueStore::set_inheritance_parent`: the ones whose old and new
79    /// value differ, each of which starts a walk of the subtree.
80    InheritedValuesDiffering => "inherited values differing on an ancestor change",
81    /// `ValueStore::inherited_value_changed`: the objects such a walk
82    /// reaches (the object itself and its inheritance descendants, once per
83    /// property).
84    InheritedValueWalkVisits => "objects visited by inherited value walks",
85
86    // --- tree attachment, styles and resources (design 04) ---
87    /// `StyledElement::on_attached_to_logical_tree_core`: an element that
88    /// was not in a logical tree enters one.
89    LogicalTreeAttachments => "elements attached to a logical tree",
90    /// `StyledElement::on_detached_from_logical_tree_core`: an element
91    /// leaves its logical tree.
92    LogicalTreeDetachments => "elements detached from a logical tree",
93    /// `StyledElement::get_effective_theme`: the implicit theme was not
94    /// known and was looked up as a resource.
95    ImplicitThemeLookups => "implicit theme lookups",
96    /// `StyledElement::apply_styles`: a style host between the element and
97    /// the root was asked for its styles.
98    StyleHostsWalked => "style hosts walked",
99    /// `Style::try_attach_checked` and `ContainerQuery::try_attach_checked`:
100    /// a style with setters (a query with children) was matched against an
101    /// element.
102    StylesEvaluated => "styles evaluated",
103    /// The same functions: the selector (the query) matched and the style
104    /// was instanced on the element.
105    StylesMatched => "styles matched",
106    /// `ControlTheme::try_attach_checked`: a control theme was tested
107    /// against the style key of an element.
108    ControlThemesEvaluated => "control themes evaluated",
109    /// `ControlTheme::try_attach_checked`: it applies and was instanced.
110    ControlThemesMatched => "control themes matched",
111    /// `StyleBase::try_attach_instance`: a style instance was added to the
112    /// value store of an element.
113    StyleInstancesAttached => "style instances attached",
114    /// `StyleBase::try_attach_instance`: the instance was built (its setters
115    /// instanced) instead of the shared instance of the style being taken.
116    StyleInstancesCreated => "style instances created",
117    /// `IResourceHost::try_find_resource`: a lookup up the logical tree.
118    ResourceLookups => "resource lookups",
119    /// `IResourceHost::try_find_resource`: the hosts such a lookup asked
120    /// (`try_get_resource` of the host and of each styling parent).
121    ResourceHostsProbed => "resource hosts probed",
122
123    // --- bindings (design 05) ---
124    /// `FerroObject::bind_binding_with_anchor`: a binding was instanced on
125    /// a property of an object.
126    BindingsInstanced => "bindings instanced",
127    /// `UntypedBindingExpressionBase::new` and `TypedBindingExpression::new`:
128    /// a binding expression was created (by a binding, a setter, a template
129    /// binding or a dynamic resource).
130    BindingExpressionsCreated => "binding expressions created",
131    /// `TemplateBindingExpression::new`.
132    TemplateBindingExpressionsCreated => "template binding expressions created",
133    /// `DynamicResourceExpression::new` (the XAML runtime library).
134    DynamicResourceExpressionsCreated => "dynamic resource expressions created",
135    /// `UntypedBindingExpressionBase::publish_value` of a running
136    /// expression and `TypedBindingExpression::publish_value`.
137    BindingValuesPublished => "binding values published",
138
139    // --- text (design 06) ---
140    /// `TextLayout::from_text_source`.
141    TextLayoutsCreated => "text layouts created",
142    /// `TextFormatterImpl::format_line_with_cache`: a line was asked for.
143    TextLinesFormatted => "text lines formatted",
144    /// `TextFormatterImpl::format_line_with_cache`: the shaped runs of the
145    /// line came from the text run cache.
146    TextRunCacheHits => "text run cache hits",
147    /// `TextFormatterImpl::format_line_with_cache`: a cache was given, did
148    /// not hold the line, and the shaped runs were added to it.
149    TextRunCacheMisses => "text run cache misses",
150    /// `TextShaper::shape_text`: a run was shaped by the platform shaper.
151    TextRunsShaped => "text runs shaped",
152
153    // --- containers (the controls crate) ---
154    /// `VirtualizingStackPanel::recycle_element`: a container left the
155    /// viewport, was cleared and went to the recycle pool.
156    ContainersRecycled => "containers recycled",
157    /// `VirtualizingStackPanel::get_recycled_element`: a container of the
158    /// pool was prepared for another item.
159    ContainersReused => "containers reused",
160    /// `VirtualizingStackPanel::create_element`: a container was created.
161    ContainersCreated => "containers created",
162    /// `ContentPresenter::update_child_with`: the child of a content
163    /// presenter was replaced by another control or removed.
164    ContentPresenterChildrenReplaced => "content presenter children replaced",
165}
166
167/// The number of counters.
168#[cfg(feature = "perf-counters")]
169const COUNT: usize = PerfCounter::ALL.len();
170
171/// Counts one occurrence of `$counter` (a variant of
172/// [`PerfCounter`](crate::diagnostics::perf_counters::PerfCounter)) on this
173/// thread.
174#[cfg(feature = "perf-counters")]
175#[macro_export]
176macro_rules! perf_count {
177    ($counter:ident) => {
178        $crate::diagnostics::perf_counters::count($crate::diagnostics::perf_counters::PerfCounter::$counter)
179    };
180}
181
182/// Counts one occurrence of `$counter` (a variant of
183/// [`PerfCounter`](crate::diagnostics::perf_counters::PerfCounter)) on this
184/// thread. Without the feature `perf-counters` it only names the counter,
185/// so that a misspelt one does not compile: there is no code.
186#[cfg(not(feature = "perf-counters"))]
187#[macro_export]
188macro_rules! perf_count {
189    ($counter:ident) => {{
190        let _ = $crate::diagnostics::perf_counters::PerfCounter::$counter;
191    }};
192}
193
194/// Counts one virtual call of the member `$member` (a string literal,
195/// `"Class::member"`) on this thread.
196#[cfg(feature = "perf-counters")]
197#[macro_export]
198macro_rules! perf_count_virtual {
199    ($member:expr) => {
200        $crate::diagnostics::perf_counters::count_virtual_call($member)
201    };
202}
203
204/// Counts one virtual call of the member `$member` (a string literal,
205/// `"Class::member"`) on this thread. Without the feature `perf-counters`
206/// it expands to the unit value: there is no code.
207#[cfg(not(feature = "perf-counters"))]
208#[macro_export]
209macro_rules! perf_count_virtual {
210    ($member:expr) => {
211        ()
212    };
213}
214
215#[cfg(feature = "perf-counters")]
216mod counting {
217    use super::{PerfCounter, COUNT};
218    use std::cell::{Cell, RefCell};
219    use std::collections::HashMap;
220
221    crate::ferro_thread_local! {
222        static COUNTS: [Cell<u64>; COUNT] = const { [const { Cell::new(0) }; COUNT] };
223        static VIRTUAL_CALLS: RefCell<HashMap<&'static str, u64>> = RefCell::new(HashMap::new());
224    }
225
226    #[inline]
227    pub(super) fn count(counter: PerfCounter) {
228        // A thread that is ending has no counters left: nothing is counted.
229        let _ = COUNTS.try_with(|counts| {
230            let cell = &counts[counter as usize];
231            cell.set(cell.get() + 1);
232        });
233    }
234
235    pub(super) fn count_virtual_call(member: &'static str) {
236        let _ = VIRTUAL_CALLS.try_with(|calls| {
237            // The table is never borrowed across a call of the framework;
238            // a call made while it is read would not be counted.
239            if let Ok(mut calls) = calls.try_borrow_mut() {
240                *calls.entry(member).or_insert(0) += 1;
241            }
242        });
243    }
244
245    pub(super) fn counts() -> [u64; COUNT] {
246        COUNTS.with(|counts| std::array::from_fn(|index| counts[index].get()))
247    }
248
249    pub(super) fn virtual_calls() -> Vec<(&'static str, u64)> {
250        let mut calls: Vec<(&'static str, u64)> =
251            VIRTUAL_CALLS.with(|calls| calls.borrow().iter().map(|(member, count)| (*member, *count)).collect());
252        super::sort_virtual_calls(&mut calls);
253        calls
254    }
255
256    pub(super) fn reset() {
257        COUNTS.with(|counts| {
258            for cell in counts.iter() {
259                cell.set(0);
260            }
261        });
262        VIRTUAL_CALLS.with(|calls| calls.borrow_mut().clear());
263    }
264}
265
266/// The most called member first; members with the same count by name.
267#[cfg(feature = "perf-counters")]
268fn sort_virtual_calls(calls: &mut [(&'static str, u64)]) {
269    calls.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0)));
270}
271
272/// Counts one occurrence of `counter` on this thread. Call it through
273/// [`perf_count!`](crate::perf_count).
274#[cfg(feature = "perf-counters")]
275#[inline]
276pub fn count(counter: PerfCounter) {
277    counting::count(counter)
278}
279
280/// Counts one occurrence of `counter` on this thread: nothing, since the
281/// feature `perf-counters` is off.
282#[cfg(not(feature = "perf-counters"))]
283#[inline(always)]
284pub fn count(_counter: PerfCounter) {}
285
286/// Counts one virtual call of `member` (`"Class::member"`) on this thread.
287/// Call it through [`perf_count_virtual!`](crate::perf_count_virtual).
288#[cfg(feature = "perf-counters")]
289#[inline]
290pub fn count_virtual_call(member: &'static str) {
291    counting::count_virtual_call(member)
292}
293
294/// Counts one virtual call of `member` on this thread: nothing, since the
295/// feature `perf-counters` is off.
296#[cfg(not(feature = "perf-counters"))]
297#[inline(always)]
298pub fn count_virtual_call(_member: &'static str) {}
299
300/// The counters of this thread at one moment, or the difference of two such
301/// moments ([`since`](Self::since)). Without the feature `perf-counters` it
302/// holds nothing and every count reads zero.
303#[derive(Clone, Debug, Default, PartialEq, Eq)]
304pub struct PerfCountersSnapshot {
305    #[cfg(feature = "perf-counters")]
306    counts: Option<[u64; COUNT]>,
307    #[cfg(feature = "perf-counters")]
308    virtual_calls: Vec<(&'static str, u64)>,
309}
310
311// Without the feature the snapshot is nothing at all.
312#[cfg(not(feature = "perf-counters"))]
313const _: () = assert!(std::mem::size_of::<PerfCountersSnapshot>() == 0);
314
315/// The counters of this thread now.
316pub fn snapshot() -> PerfCountersSnapshot {
317    #[cfg(feature = "perf-counters")]
318    {
319        PerfCountersSnapshot { counts: Some(counting::counts()), virtual_calls: counting::virtual_calls() }
320    }
321    #[cfg(not(feature = "perf-counters"))]
322    {
323        PerfCountersSnapshot {}
324    }
325}
326
327/// Sets every counter of this thread to zero.
328pub fn reset() {
329    #[cfg(feature = "perf-counters")]
330    counting::reset();
331}
332
333impl PerfCountersSnapshot {
334    /// The count of `counter`.
335    pub fn get(&self, counter: PerfCounter) -> u64 {
336        #[cfg(feature = "perf-counters")]
337        {
338            self.counts.map_or(0, |counts| counts[counter as usize])
339        }
340        #[cfg(not(feature = "perf-counters"))]
341        {
342            let _ = counter;
343            0
344        }
345    }
346
347    /// The virtual calls by member (`"Class::member"`, the class that
348    /// declares the member), the most called first.
349    pub fn virtual_calls(&self) -> &[(&'static str, u64)] {
350        #[cfg(feature = "perf-counters")]
351        {
352            &self.virtual_calls
353        }
354        #[cfg(not(feature = "perf-counters"))]
355        {
356            &[]
357        }
358    }
359
360    /// The number of virtual calls, of every member.
361    pub fn virtual_calls_total(&self) -> u64 {
362        self.virtual_calls().iter().map(|(_, count)| *count).sum()
363    }
364
365    /// What was counted between `earlier` and this snapshot.
366    pub fn since(&self, earlier: &PerfCountersSnapshot) -> PerfCountersSnapshot {
367        #[cfg(feature = "perf-counters")]
368        {
369            let counts: [u64; COUNT] = std::array::from_fn(|index| {
370                let counter = PerfCounter::ALL[index];
371                self.get(counter).saturating_sub(earlier.get(counter))
372            });
373            let mut virtual_calls: Vec<(&'static str, u64)> = self
374                .virtual_calls
375                .iter()
376                .map(|(member, count)| {
377                    let before =
378                        earlier.virtual_calls.iter().find(|(other, _)| other == member).map_or(0, |(_, count)| *count);
379                    (*member, count.saturating_sub(before))
380                })
381                .filter(|(_, count)| *count > 0)
382                .collect();
383            sort_virtual_calls(&mut virtual_calls);
384            PerfCountersSnapshot { counts: Some(counts), virtual_calls }
385        }
386        #[cfg(not(feature = "perf-counters"))]
387        {
388            let _ = earlier;
389            PerfCountersSnapshot {}
390        }
391    }
392
393    /// These counts and the ones of `other` together: the sum of what was
394    /// counted over several stretches of work.
395    pub fn plus(&self, other: &PerfCountersSnapshot) -> PerfCountersSnapshot {
396        #[cfg(feature = "perf-counters")]
397        {
398            let counts: [u64; COUNT] = std::array::from_fn(|index| {
399                let counter = PerfCounter::ALL[index];
400                self.get(counter) + other.get(counter)
401            });
402            let mut virtual_calls = self.virtual_calls.clone();
403            for (member, count) in &other.virtual_calls {
404                match virtual_calls.iter_mut().find(|(existing, _)| *existing == *member) {
405                    Some((_, total)) => *total += count,
406                    None => virtual_calls.push((*member, *count)),
407                }
408            }
409            sort_virtual_calls(&mut virtual_calls);
410            PerfCountersSnapshot { counts: Some(counts), virtual_calls }
411        }
412        #[cfg(not(feature = "perf-counters"))]
413        {
414            let _ = other;
415            PerfCountersSnapshot {}
416        }
417    }
418
419    /// The counters, one a line; then the virtual calls by member.
420    pub fn report(&self) -> String {
421        self.format(None)
422    }
423
424    /// The counters with, next to each, the count divided by `divisor` (the
425    /// rows recycled, the frames rendered): `unit` names what it counts.
426    pub fn report_per(&self, divisor: u64, unit: &str) -> String {
427        self.format(Some((divisor, unit)))
428    }
429
430    fn format(&self, per: Option<(u64, &str)>) -> String {
431        if !ENABLED {
432            return String::from("perf counters: not compiled in (the feature `perf-counters` is off)\n");
433        }
434
435        let line = |name: &str, count: u64| match per {
436            Some((divisor, unit)) if divisor > 0 => {
437                format!("  {name}: {count} ({:.2} per {unit})\n", count as f64 / divisor as f64)
438            }
439            _ => format!("  {name}: {count}\n"),
440        };
441        let mut text = String::new();
442        for counter in PerfCounter::ALL {
443            text.push_str(&line(counter.name(), self.get(*counter)));
444        }
445        text.push_str(&line("virtual calls", self.virtual_calls_total()));
446        for (member, count) in self.virtual_calls() {
447            text.push_str(&line(&format!("  {member}"), *count));
448        }
449        text
450    }
451}
452
453impl fmt::Display for PerfCountersSnapshot {
454    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455        f.write_str(&self.report())
456    }
457}
458
459// Without the feature the counting macros are constant expressions of the
460// unit type: there is nothing to run.
461#[cfg(not(feature = "perf-counters"))]
462const _: () = {
463    crate::perf_count!(PropertyChangesRaised);
464    crate::perf_count_virtual!("FerroObject::on_property_changed")
465};