# Large data and performance

> Python GUI performance with Pyzed: show hundreds of thousands of rows with virtual lists and tables, and skip unchanged views with @pz.memo.

Source: https://albakore.github.io/pyzed/guides/performance/

Pyzed calls `view()` after every message and turns the widget tree it returns into Iced
widgets. For most apps that takes well under a millisecond. Two things make it slow: building
**many rows** that are not even on screen, and rebuilding **big subtrees** that did not change.
Pyzed has a tool for each.

## Fast by default

You don't have to turn any of this on. Pyzed does it in every app, following what fast native editors do to keep
their UI fast: start drawing as soon as possible, and don't redo work nobody asked for.

- **Fast start.** `import pyzed` loads only what an app needs to open a window. The designer,
  the API reference, asyncio and the package metadata load the first time you use them.
- **Fonts in parallel.** Scanning the system fonts starts the moment Pyzed is imported, on its
  own thread, while Python runs the rest of your imports.
- **Native GPU APIs first.** The GPU renderer starts Vulkan, Metal or DirectX 12. The OpenGL
  backend, slower to start and heavier on memory, is started only when none of those works.
- **Cheap widget trees.** Building containers, buttons and theme colors costs less, and the
  native core allocates with [mimalloc](https://github.com/microsoft/mimalloc), which is much
  faster than the system allocator with the many small objects every `view()` creates.
- **Shared pieces of components.** `ui.button`, `ui.icon_button`, `ui.kbd` and tooltips reuse the
  widgets that only depend on the theme and on their text, icon or keys. Widgets never change once
  built, so one copy serves every button that says "Open". A view of 1,000 buttons and labels
  builds in about a third less time.
- **The CPU renderer when there is no real GPU.** On a virtual machine, a remote desktop or a
  computer without a graphics driver, the GPU that Vulkan, DirectX or OpenGL offer is an emulation
  running on the CPU (Mesa's llvmpipe and lavapipe, Windows' WARP). There Pyzed draws with its CPU
  renderer (tiny-skia) instead: it is faster and takes about 100 MB less memory.
- **No rebuilds for keys nobody listens to.** Key presses and releases only reach your app
  (and rebuild the view) when a window defines `on_key` or `on_key_up`. Typing in a field
  rebuilds the view once per key instead of three times.
- **Hovering costs nothing.** Buttons, list rows and anything styled with `hover=` light up
  under the pointer on their own, without a message or a new `view()`. Pointer callbacks
  (`on_move`, `on_enter`, `on_exit`, `mouse_move`...) run as soon as the pointer moves, and when
  one returns [`pz.UNCHANGED`](#callbacks-that-change-nothing-on-screen) nothing is rebuilt nor
  redrawn. The library's own components and Pyzed Design do that whenever a move changes nothing.
- **Styles resolved once.** `widget.style(...)` caches what it resolves by its properties and the
  theme, so the same hover color on every row of a list is worked out once, not once per row per
  `view()`.
- **Markdown parsed once.** A long chat or document in `pz.markdown` blocks is parsed the first time
  it is drawn and kept while it is on screen, however many blocks there are.
- **Long lists build what is in view.** `ui.table`, `ui.virtual_list` and Pyzed Design's List box
  (from 50 items) only build the rows you can see.
- **Components are drawn when they change.** A [`pz.Component`](#components-drawn-only-when-they-change)
  whose props, state and context are the same as last time gives back its last drawing instead
  of running `render()` again. A view of 500 rows of components rebuilds in 4 ms instead of 26 ms
  when one of them changes.
- **So are form controls.** A form made in Pyzed Design, running or on the designer's canvas,
  only builds again the controls whose properties changed. The designer does the same with its
  console, outline and toolbox: its view went from 21 ms to 4.5 ms with an 85-control form and
  400 log lines.
- **Files are listed off the UI thread.** The Open dialog, the agent's `@` list and the shader file
  picker read the project folder once and then refresh it on a worker thread, so typing in them
  never waits for the disk.

Measured with the [benchmark app](https://albakore.github.io/pyzed/why/benchmarks/) (640×480 window, a label and a
button per row, Xvfb without a GPU, medians of 5 launches) on the same machine:

| | Before | Now |
| --- | --- | --- |
| `import pyzed` | 178 ms | 78 ms |
| First frame, 10 rows | 454 ms | 323 ms |
| Update of 1,000 rows | 40.7 ms | 34.9 ms |
| Memory, 10 rows (RSS) | 174 MB | 146 MB |

:::note
`WGPU_BACKEND=gl` (or `vulkan`, `metal`, `dx12`) still picks a GPU backend yourself,
`ICED_BACKEND=wgpu` keeps the GPU renderer even on an emulated GPU (what [shaders](https://albakore.github.io/pyzed/shading/shaders/)
need), and `ICED_BACKEND=tiny-skia` always uses the CPU renderer. A project can set them for good in
`[tool.pyzed.rendering]` of its `pyproject.toml`, or in **Settings → Project → Rendering**
([Rendering](https://albakore.github.io/pyzed/editor/settings/#rendering)).
:::

## Virtual lists and tables

A virtual list builds only the rows in view. Rows are built when they scroll into view and
dropped when they leave it, so a list of a million items costs the same as one of fifty.

```python
import pyzed as pz
from pyzed import ui

class Logs(pz.App):
    def __init__(self):
        self.lines = [f"[{i:06}] request handled" for i in range(500_000)]

    def view(self):
        return ui.virtual_list(
            self.lines,
            lambda line: pz.text(line, font="mono", size=12),
            item_height=20,
            id="logs",
        )
```

- [`ui.virtual_list(items, render)`](https://albakore.github.io/pyzed/library/virtual-list/) is a themed, scrolling list:
  `render(item)` returns the widget for one item.
- [`ui.table`](https://albakore.github.io/pyzed/library/table/) is virtualized too: pass it all your rows.
- [`pz.virtual_column(count, item, item_height=...)`](https://albakore.github.io/pyzed/reference/widgets/#virtual_column)
  is the building block: `item(index)` returns row `index`. Put it inside any `pz.scrollable`.

Every row has the same height (`item_height`), which is what lets Pyzed know where each row
goes without building it. To scroll to row `i`, scroll the list's `id` to
`i * (item_height + spacing)`:

```python
pz.Command.scroll_to("logs", 0, 1_000 * 20)   # row 1000
```

Measured on a 200 000-row table, building the view went from 3.7 s to 0.2 ms.

:::tip
`items` and `rows` should be lists (or other sequences): other iterables are copied into a list
on every `view()`. Sorting a huge table on every `view()` is the remaining cost; keep the rows
sorted in your state, or sort them in a `@pz.memo` function.
:::

## Callbacks that change nothing on screen

A message makes Pyzed call `view()` again and lay out and draw the window. That is what you want
after a click, but a pointer that moves over a big view sends a message per move. When a callback
only remembers something that is not drawn, return `pz.UNCHANGED`: the message ends there, with no
`view()`, no layout and no redraw.

```python
class Canvas(pz.App):
    def __init__(self):
        self.pointer = (0, 0)   # used when the menu opens, not drawn
        self.hovered = None     # drawn highlighted

    def moved(self, x, y):
        self.pointer = (x, y)
        hovered = self.item_at(x, y)
        if hovered == self.hovered:
            return pz.UNCHANGED  # nothing on screen depends on the move
        self.hovered = hovered   # this one is drawn: the view is rebuilt

    def view(self):
        return pz.mouse_area(self.board(), on_move=self.moved)
```

It works for pointer callbacks (`on_move`, `on_enter`, `on_exit` of `pz.mouse_area` and the
`mouse_move`, `mouse_enter`, `mouse_leave`... events of `.on(...)`), messages posted with `pz.post`,
and anything your `update()` returns. Return it only when nothing that `view()`, `title` or
`subscriptions()` read has changed: the window keeps showing what it showed.

:::tip
For a highlight under the pointer you don't need a callback at all: `.style(hover={"background": "ghost_hover"})`
draws it in the native widget.
:::

## Components drawn only when they change

Every [component](https://albakore.github.io/pyzed/guides/components/) keeps its last drawing. On the next `view()` it is
reused, without calling `render()`, while all of these are the same:

- its **props** and its **state** (any attribute you set on `self`). Lists, dicts, sets and plain
  objects are compared by their contents, so `self.items.append(x)` is seen;
- the values it reads with `pz.use_context` (or `self.context`);
- the theme, the language, the window size and the stylesheet;
- the components drawn inside it: when a child changes its state, every component that contains
  it is drawn again too.

Callbacks you pass as props or create in `render()` don't get in the way: they are compared by
their code and the values they captured, so a new `lambda: self.pick(i)` with the same `i` counts
as the same callback.

```python
class Row(pz.Component):
    name: str = ""
    stars: int = 0

    def render(self):  # runs only when name, stars or the Rating inside change
        return pz.row([ui.label(self.name), Rating(value=self.stars)])
```

Two cases need a hand, because the component can't see what changed:

- `render()` reads something that is neither a prop, nor state, nor context: the clock, a global,
  data changed by another object. Call `self.refresh()` when it changes, or put `cache = False` on
  the class to draw it on every `view()` (live meters do that).
- A value whose changes can't be compared (a NumPy array, a native handle, a huge object graph)
  turns the cache off for that component on its own: it is drawn every time, as before.

A component that gets widgets as `children` is drawn whenever the parent builds them again, since
they are new widgets each time; it costs no more than drawing it.

## Threads

Your `view()`, `update()` and callbacks run on the UI thread, one at a time, as in every native
toolkit. Python runs one thread at a time anyway (the GIL), so the fastest UI is the one that
does less on that thread, which is what the caches above are for. What Pyzed does on other
threads:

- Iced lays out and renders the frame in Rust, outside the GIL, and the GPU draws it.
- System fonts are scanned, project files are listed and the designer's agents, language servers
  and builds run on worker threads.
- Your slow work belongs there too: [async tasks](https://albakore.github.io/pyzed/guides/async/) or a `threading.Thread`
  that sends its result with `pz.post(...)`.

## Memoized subtrees

`@pz.memo` caches what a function returns by its arguments. While the arguments are equal, the
next `view()` reuses the same widget tree instead of building it again:

```python
@pz.memo
def sidebar(files: tuple, selected: str):
    return pz.column([ui.list_item(f, icon="file", selected=f == selected) for f in files])

class Editor(pz.App):
    def view(self):
        return pz.row([sidebar(tuple(self.files), self.selected), self.editor()])
```

How arguments are compared:

| Argument | Compared by |
| --- | --- |
| Numbers, strings, tuples, frozen dataclasses... | Value (their hash). |
| Lists, dicts and sets | Their contents. |
| Anything else (your objects, `self`) | Identity: mutating it in place does not refresh the cache. |

The current theme and language are always part of the key, so switching them rebuilds every
memoized subtree.

:::caution
A memoized function only sees its arguments. If it reads `self.something` or a global, the
cached subtree keeps the old value until the arguments change. Pass everything it depends on,
or a version number you bump when the data changes.
:::

- `@pz.memo(maxsize=256)` keeps more results (64 by default). It is handy on a function called
  once per row, such as the `render` of a virtual list.
- `function.cache_clear()` empties one function's cache, `pz.clear_memos()` all of them.
- `pz.memo_stats()` returns `{"hits": ..., "misses": ...}` to check that a subtree is reused.

## Other tips

- Keep `view()` free of slow work: load files, query databases and sort data in handlers or
  [async tasks](https://albakore.github.io/pyzed/guides/async/), and keep the results in your state.
- Draw thousands of shapes with one [canvas](https://albakore.github.io/pyzed/guides/canvas/) instead of thousands of widgets.
- `pz.every` subscriptions call `view()` on every tick: stop them when nothing moves.
