Skip to content

Commit fb12403

Browse files
committed
More tweaks
1 parent 8a8b899 commit fb12403

6 files changed

Lines changed: 983 additions & 43 deletions

File tree

docs/Profiling Component Paint.md

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
# Measuring Component Paint Speed
2+
3+
This guide explains how to use YUP's built-in paint profiling system to measure the rendering cost of individual components and identify bottlenecks in your UI.
4+
5+
## Overview
6+
7+
YUP's `PaintProfiler` is a process-wide singleton that records per-component paint timings broken into four categories:
8+
9+
| Category | Meaning |
10+
|---|---|
11+
| **self** | Time inside the component's own `paint()` callback |
12+
| **children** | Time spent painting all direct and indirect children |
13+
| **framework** | Time for framework bookkeeping (clip setup, transform, etc.) |
14+
| **total** | Full elapsed time for the complete paint pass |
15+
16+
Samples are stored in a per-component ring buffer (default capacity: 300 frames). Statistical summaries — min, max, mean, p50, p95, p99 — are computed on demand from the stored samples.
17+
18+
---
19+
20+
## Step 1 — Enable the Build Flag
21+
22+
Paint profiling is compiled out by default. Add the preprocessor definition to your target in CMake:
23+
24+
```cmake
25+
yup_standalone_app (
26+
TARGET_NAME MyApp
27+
DEFINITIONS
28+
YUP_ENABLE_COMPONENT_PAINT_PROFILING=1
29+
MODULES
30+
yup::yup_gui
31+
# ... other modules
32+
)
33+
```
34+
35+
Without this flag every profiling call is a no-op and produces no overhead in release builds.
36+
37+
---
38+
39+
## Step 2 — Start a Session
40+
41+
The recommended API is `PaintProfiler::startSession()`, which returns a `ScopedSession`. The session enables profiling on the entire component subtree rooted at the component you pass in, and disables it automatically when the handle is destroyed.
42+
43+
```cpp
44+
#if YUP_ENABLE_COMPONENT_PAINT_PROFILING
45+
std::unique_ptr<yup::PaintProfiler::ScopedSession> profileSession;
46+
#endif
47+
48+
// In your component constructor or initialisation:
49+
#if YUP_ENABLE_COMPONENT_PAINT_PROFILING
50+
profileSession = yup::PaintProfiler::getInstance().startSession (*this);
51+
#endif
52+
```
53+
54+
Guard every profiling call with `#if YUP_ENABLE_COMPONENT_PAINT_PROFILING` so the code compiles and runs correctly in builds without the flag.
55+
56+
### Session options
57+
58+
Pass a `PaintProfileOptions` struct to control the session's behaviour:
59+
60+
```cpp
61+
yup::PaintProfileOptions options;
62+
options.sampleCapacity = 600; // retain 600 frames of history
63+
options.minimumSampleMicros = 50.0; // discard samples shorter than 50 µs
64+
options.includeBounds = true; // record component bounds per sample
65+
options.includeRepaintArea = true; // record dirty rect per sample
66+
options.includeInvisibleComponents = false; // should invisible components be included
67+
options.recordSkippedSelfPaint = true; // track child-only cost even with no paint()
68+
69+
profileSession = yup::PaintProfiler::getInstance().startSession (*this, options);
70+
```
71+
72+
---
73+
74+
## Step 3 — Take Snapshots
75+
76+
A `Snapshot` is an immutable, point-in-time view of every registered component's statistics. Call `createSnapshot()` on the session at whatever rate you need — a 10 Hz timer is typical for a dashboard display.
77+
78+
```cpp
79+
void timerCallback() override
80+
{
81+
#if YUP_ENABLE_COMPONENT_PAINT_PROFILING
82+
if (profileSession == nullptr || profileSession->isPaused())
83+
return;
84+
85+
auto snap = profileSession->createSnapshot (yup::PaintProfileTimeKind::total, 32);
86+
// use snap ...
87+
#endif
88+
}
89+
```
90+
91+
`createSnapshot` accepts:
92+
- **sortBy** — which time kind determines the descending sort order (default `total`).
93+
- **histogramBuckets** — bucket count for the global frame histogram (default 32).
94+
95+
The returned `Snapshot` contains:
96+
97+
```cpp
98+
struct Snapshot
99+
{
100+
uint64 frameIndex; // frame counter at snapshot time
101+
std::vector<ComponentEntry> components; // one entry per registered component
102+
PaintProfileSummary globalFrameTotal; // per-frame total across all components
103+
PaintProfileHistogram globalFrameHistogram;
104+
};
105+
```
106+
107+
Each `ComponentEntry` gives you:
108+
109+
```cpp
110+
struct ComponentEntry
111+
{
112+
String name; // component title at snapshot time
113+
PaintProfileStats* stats; // live pointer — may be stale after destruction
114+
PaintProfileSummary self;
115+
PaintProfileSummary children;
116+
PaintProfileSummary framework;
117+
PaintProfileSummary total;
118+
};
119+
```
120+
121+
A `PaintProfileSummary` exposes: `lastMicros`, `minMicros`, `maxMicros`, `meanMicros`, `p50Micros`, `p95Micros`, `p99Micros`, and `sampleCount`.
122+
123+
---
124+
125+
## Step 4 — Interpret the Data
126+
127+
### Performance thresholds
128+
129+
| Range | Meaning |
130+
|---|---|
131+
| < 500 µs | Normal — no action needed |
132+
| 500 µs – 2 ms | Warm — worth investigating if sustained |
133+
| > 2 ms | Hot — likely causing dropped frames at 60 Hz |
134+
135+
At 60 Hz, the full frame budget is ~16.7 ms. A single component that consistently takes > 2 ms for its own paint is a significant contributor.
136+
137+
### Reading a summary
138+
139+
```cpp
140+
const auto& entry = snap.components[0];
141+
142+
// Is the component itself expensive, or is it due to children?
143+
double selfCost = entry.self.p95Micros;
144+
double childrenCost = entry.children.p95Micros;
145+
146+
// p95 is the most useful signal: it captures spikes while ignoring outliers
147+
double worstNormal = entry.total.p95Micros;
148+
```
149+
150+
Use **p95** as the primary signal. `maxMicros` is useful for catching spikes, but a single GC or OS event can inflate it. `meanMicros` smooths over spikes that matter.
151+
152+
### Log a snapshot to the console
153+
154+
```cpp
155+
#if YUP_ENABLE_COMPONENT_PAINT_PROFILING
156+
auto snap = profileSession->createSnapshot (yup::PaintProfileTimeKind::total, 32);
157+
158+
yup::Logger::outputDebugString ("Paint profile — frame " + yup::String (snap.frameIndex));
159+
yup::Logger::outputDebugString (yup::String::formatted ("%-30s %9s %9s %9s %9s", "Widget", "last", "mean", "p95", "max"));
160+
for (const auto& entry : snap.components)
161+
{
162+
yup::Logger::outputDebugString (
163+
yup::String::formatted ("%-30s %7.2f ms %7.2f ms %7.2f ms %7.2f ms",
164+
entry.name.toRawUTF8(),
165+
entry.total.lastMicros / 1000.0,
166+
entry.total.meanMicros / 1000.0,
167+
entry.total.p95Micros / 1000.0,
168+
entry.total.maxMicros / 1000.0));
169+
}
170+
#endif
171+
```
172+
173+
---
174+
175+
## Step 5 — Inspect Raw Samples
176+
177+
When you need more detail than aggregated statistics, pull the ring buffer directly:
178+
179+
```cpp
180+
#if YUP_ENABLE_COMPONENT_PAINT_PROFILING
181+
if (auto* stats = myComponent.getPaintProfileStats())
182+
{
183+
const auto samples = stats->copySamples(); // chronological order, oldest first
184+
185+
for (const auto& s : samples)
186+
{
187+
// s.selfMicros, s.childrenMicros, s.frameworkMicros, s.totalMicros
188+
// s.frameIndex, s.paintIndex
189+
// s.componentBounds, s.repaintArea
190+
// s.renderContinuous, s.selfPaintSkipped
191+
}
192+
193+
auto summary = stats->summarize (yup::PaintProfileTimeKind::total);
194+
auto histogram = stats->createHistogram (yup::PaintProfileTimeKind::self, 32);
195+
}
196+
#endif
197+
```
198+
199+
`copySamples()` returns samples in chronological order regardless of the ring-buffer write position.
200+
201+
---
202+
203+
## Step 6 — Reset and Pause
204+
205+
Clear accumulated history after a layout change or before a timed benchmark:
206+
207+
```cpp
208+
profileSession->reset(); // clears all ring buffers for this session's components
209+
```
210+
211+
Temporarily suppress recording without destroying the session:
212+
213+
```cpp
214+
profileSession->setPaused (true);
215+
// ... do something that should not be measured ...
216+
profileSession->setPaused (false);
217+
```
218+
219+
---
220+
221+
## Tips for Accurate Measurements
222+
223+
- **Warm up first.** Discard the first second of data after a `reset()` — the JIT-equivalent effects (GPU shader compilation, OS scheduling) inflate early samples.
224+
- **Isolate one change at a time.** Use `setPaused(true)` on the session while switching components so the history stays clean.
225+
- **Prefer p95 over max.** A single OS preemption can produce a multi-millisecond outlier that distorts `maxMicros`.
226+
- **Separate self from children.** A high `total` with a low `self` means the component's own paint is fine but its children are costly — recurse down the tree.
227+
- **Check `renderContinuous`.** Samples with `renderContinuous = true` in the ring buffer indicate the component is requesting continuous repaints (animation loops). Every such component adds baseline CPU pressure even when nothing is animating visually.
228+
- **Use `setOpaque(true)`.** Opaque components allow the renderer to skip painting the background beneath them. It is one of the cheapest paint optimisations available.

examples/graphics/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ yup_standalone_app (
6262
TARGET_APP_NAMESPACE "org.yup"
6363
DEFINITIONS
6464
YUP_EXAMPLE_GRAPHICS_RIVE_FILE="${rive_file}"
65+
YUP_ENABLE_COMPONENT_PAINT_PROFILING=1
6566
${additional_definitions}
6667
PRELOAD_FILES
6768
"${CMAKE_CURRENT_LIST_DIR}/${rive_file}@${rive_file}"

0 commit comments

Comments
 (0)