|
| 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. |
0 commit comments