Profiling your app

Find out where your frames go with BUI_PROFILE, choose a renderer with numbers, and avoid the few slow paths that matter.

  • Python
  • Rust
  • JavaScript

Most BUI apps never need tuning: an idle window costs nothing, and a typical frame takes well under a millisecond. When an app does feel slow, it’s usually one of a handful of causes, and BUI can tell you which.

This page shows how to measure, how to read the numbers, and what to change.

Measure with BUI_PROFILE

Run your app with BUI_PROFILE set, and BUI prints frame times to stderr, averaged over every 60 frames:

BUI_PROFILE=1 python my_app.py # PowerShell: $env:BUI_PROFILE="1"; python my_app.py
BUI_PROFILE=1 cargo run --release
BUI_PROFILE=1 node app.js

Any value turns it on, even 0; unset it to turn it off. When it’s off, it costs one check per phase.

There are two kinds of output, depending on your app.

Rust and JavaScript: per-phase times

Rust apps, and JavaScript apps (which run on the Rust crate), print a line like this every 60 frames:

frame ms: frame interval … render … cascade … build … boxes … taffy … place … paint … flush … widgets+hover …

Each … is an average in milliseconds. Phases that took no time are left out. In frame order:

Phase What it times
frame interval Wall time between frames: rebuild, kit render and present together. While animating on a 60 Hz display, expect about 16.7 ms (one refresh).
render Your render() code building the element tree
cascade Matching CSS selectors and computing styles
build Turning elements into layout nodes
boxes Building the box tree (blocks, line boxes, flex and grid items)
taffy Layout, in taffy
place Placing laid-out boxes on the page
paint Building the display list
flush Handing the display list to the kit’s canvas
widgets+hover Updating Blender widget nodes, and the rest of the frame

Windows native window: raster and present

Apps on the native Windows host print the kit’s own times too (Python, C and Dear ImGui print only these):

kit ms: raster … present … (width x height)
  • raster is the time the kit spent rendering: any layout, then drawing the dirty regions.
  • present is the time to put the changed tiles on screen.

On Linux and macOS, only Rust and JavaScript apps print profile lines.

Choose a renderer with numbers

Run the same interaction twice, once per renderer, and compare:

BUI_PROFILE=1 BUI_GPU=0 python my_app.py
BUI_PROFILE=1 BUI_GPU=1 python my_app.py

Rules of thumb, from the benchmarks:

  • Small tool windows: stay on the CPU. It’s lighter on memory, and frames are already tiny.
  • Large or HiDPI windows, or lots of animation: try the GPU. The software raster’s cost grows with the number of pixels; the GPU’s barely does. At 3,000 animated cells, the GPU cut blenderui’s CPU per frame from 22.0 to 11.6 ms.
  • Memory-tight machines: the GPU renderer needs about 10 MB more RAM, plus GPU memory.

Rendering explains the trade-offs in depth.

Common slow paths

Re-rendering big lists every frame (JavaScript)

In React, a list held in useState re-renders every item when it changes. BUI’s hooks hold signals: pass the signal itself into JSX ({count}), and a change updates that one node without re-rendering the component.

At 3,000 cells with one changing per frame, React with signals costs about what the plain API does, while re-rendering costs more. On QuickJS, which has no JIT, re-rendering drops to 20 FPS (19.7 measured), against 58.7 with signals.

  • Keep big lists on signals.
  • Or build with --bun if you must re-render: Bun’s JIT keeps it at 55.8 FPS.

Measuring React’s development build

React’s development build is much heavier: re-rendering 3,000 cells took about 184 MB. Always measure with NODE_ENV=production, which is what your users run.

Layout changes instead of value changes

Setting a widget’s value redraws that widget. Adding, removing or resizing widgets runs a layout pass and redraws the whole window. Both are fast, but if something changes every frame, make it a value, not a structure.

Polling from your code

A timer that sets the same value every few milliseconds keeps the event loop awake, even when nothing changes on screen. Prefer callbacks, on_change, and signals.

Big canvases on one thread

On Windows, canvases with 64 or more items draw in parallel bands, using as few threads as meet the frame deadline (up to 8). If you suspect threading, compare with BUI_RASTER_THREADS=1: the pixels are identical either way, only the time changes. On Linux and macOS the canvas draws on one thread.

Debug builds

C and Rust debug builds are far slower than release builds. Profile with --release (Rust) or a Release CMake build. The CMake build defaults to Release when you don’t choose.

Headless checks in tests

You can put a frame budget in your tests, so a slowdown fails the build. BUI’s own Python suite does this: it renders the full components demo headlessly and requires full frames to fit a 120 Hz budget (under 8.3 ms). Headless testing shows how.

Next steps

Edit this page on GitHub

Last updated