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};