Skip to content

app: fizzy times its own startup, and a handover's - #365

Merged
foxnne merged 1 commit into
core/frame-causesfrom
app/startup-phases
Oct 10, 2026
Merged

foxnne merged 1 commit into
core/frame-causesfrom
app/startup-phases

Conversation

@foxnne

@foxnne foxnne commented Oct 10, 2026

Copy link
Copy Markdown
Collaborator

Part of plans/DASHBOARD_PLAN.md, "What performance work lacked": startup and handover phases. Stacked on #364 (it shares the report code); retarget to main once that merges.

What changes

fizzy times its own startup. core.profile.phase(io, name) stamps a moment of the app's start. fizzy stamps:

Each run's first frame logs one line:

info: startup: up 1288 ms after main (window shown at 1198 ms)

The phases in the report: core.profile.report includes them with .startup = true, and they're published like Lookback and FrameCauses: their own data key, no change to the profiler's layout or abi. The new instance of a handover, read that way:

.startup = .{
    .{ .phase = "main", .ms = 0.0 },
    .{ .phase = "lock deferred (handover)", .ms = 0.4 },
    .{ .phase = "window", .ms = 151.6 },
    .{ .phase = "editor", .ms = 175.6 },
    .{ .phase = "plugins", .ms = 208.4 },
    .{ .phase = "handover ready", .ms = 211.7 },
    .{ .phase = "shown", .ms = 219.5 },
    .{ .phase = "handover lock", .ms = 232.1 },
    .{ .phase = "first frame", .ms = 301.9 },
},

It already shows something to act on: the window is shown about 80 ms before its first frame is drawn. That's true of every launch, and in a handover the new window covers the old one for those 80 ms before it has drawn anything. A follow-up can show it after the first frame.

Measuring a restart used to take a window sampler and a pid poller running outside fizzy, on macOS only.

SDK impact

  • Core-only or additive: core.profile.phase, phases, Phases, ReportOptions.startup. No fingerprint move.

Verified

  • macOS:
    • A cold launch: the startup line above.
    • A handover: the table above, read from the new instance through the agent plugin. That used a local build of it against this tree; the agent's own tool for this waits for the SDK release, since it's pinned to 0.2.22.
    • The first version of this crashed at main: the clock went through dvui.io, which the backend hadn't set yet. So phase takes the io.
    • Gates: zig build, zig build test (533/534, 1 skipped), test-integration (375/375), check-web, test-sdk-version.
  • Windows, Linux: CI.

Follow-ups

  • Show a window after its first frame is drawn, not before, starting with a handover's.
  • The real process start (macOS proc_pidinfo, Windows GetProcessTimes, Linux /proc/self/stat), so main isn't time zero and dyld and static initialisation are counted.
  • The agent's fizzy_build on fizzy's repo reports the new instance's phases, once the SDK is released and the agent repinned.

🤖 Generated with Claude Code

fizzy stamps the moments its start takes (core.profile.phase): main, the single-instance lock,
the window, the editor, the plugins, the window shown, the first frame; and for an instance a
restart started beside the old one, the handover's ready and lock. The first frame logs how long
startup took, and the profiler's report has them on request (ReportOptions.startup). Measured by
the app itself on every platform, where a restart's gap was measured from outside with a window
sampler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@foxnne
foxnne merged commit 1d2829e into core/frame-causes Oct 10, 2026
10 checks passed
@foxnne
foxnne deleted the app/startup-phases branch October 10, 2026 22:19
@foxnne foxnne mentioned this pull request Oct 10, 2026
2 of 8 tasks
foxnne added a commit that referenced this pull request Oct 11, 2026
## What changes

`sdk_version` goes from 0.2.22 to 0.2.23. Merging tags `sdk-v0.2.23`,
publishes its tarball, and asks the store plugins to repin.

**Why now.** The plugin boundary's fingerprint moved since `sdk-v0.2.22`
(`0x1bf417f6…` → `0x65096e94…`, by #360), so store plugins built against
0.2.22 don't load in a fizzy built from `main`, and fizzyed.it/app is
built from `main`. Also, #355 moved the `plugins` service to version 2,
and the agent plugin's `fizzy_build` asks for version 1 until it repins.

## SDK impact

- [ ] None
- [ ] Core-only or additive: reaches plugins at the next SDK release
- [ ] Fingerprint moved: recorded in `sdk/src/version.zig`,
`sdk_version` untouched, PR labelled `sdk`
- [x] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the
last `sdk-v*` tag

**The `sdk` PRs since `sdk-v0.2.22`:**
- #360: a plugin's panic message reaches its crash report. This moves
the fingerprint.

**Additive, reaching plugins with this release:**
- #355: the `plugins` service is version 2, with `list` (each plugin's
identity, link, version, registration counts and status). A caller built
for version 1 is refused until it rebuilds, which this release's repin
does.
- #351: a crash report says whose code crashed (`sdk/plugin_sdk.zig`).

**Also reaching plugins (`core/`):**
- `core.viz`, graphics for live numbers: `line`, `bar`, `stat`, `Table`
(#345); `Pie`, `keyColor`, `palette` (#347); the table's rounded header
and shaded rows (#359).
- `core.profile`:
- `lookback()`: 30 s of costs and the frames over budget, plus
`ReportOptions.over_ms` / `.hitches` and `selfTimes` (#353).
- `frameCauses()` and `ReportOptions.causes`: why each frame happened
(#364).
- `phase` / `Phases` / `ReportOptions.startup`: startup timing (#365,
which landed inside #364's squash commit).
  - The profiler's `abi` stays 4.

## Verified

- [x] macOS:
  - `zig build` and `test-sdk-version` on this head.
  - `scripts/pack-sdk.sh` packs `fizzy-sdk-v0.2.23.tar.gz`.
- Every store plugin (fresh pulls of pixi, zig, drive, atlas and ghostty
`main`) builds against this tree's `sdk/` with `zig build --fork=<sdk>`
into a throwaway profile (31/31, and 28/28 for each of the other four).
The fingerprint moved, so each repin PR is a real rebuild, but none
should open as a draft for a broken build. The agent and chat plugins
build too (22/22, 19/19).
- [ ] Windows:
- [ ] Linux: CI.
- [ ] Web:

## Follow-ups

- Each repin PR merged and its plugin tagged (the plugin repos' tags
stay the maintainer's).
- `fizzyedit/agent` and `fizzyedit/chat` repin to the `sdk-v0.2.23`
tarball.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@foxnne foxnne mentioned this pull request Oct 13, 2026
3 of 4 tasks
foxnne added a commit that referenced this pull request Oct 13, 2026
Part of `plans/RESTART_AND_CRASHES_PLAN.md` (open items); the gap was
found with the startup phases from #365.

## What changes

**The window never appears empty.** fizzy showed its window at the end
of `AppInit`, before the first frame. On a launch it appeared empty for
the ~80 ms that frame took. In a restart's handover (#356), the new
window covered the old one before it had drawn anything.

Now:
- **Shown unseen** (`platform.window.showUndrawn`). A hidden window gets
nothing to present into, so it can't simply draw first and show
afterwards.
- **macOS:** shown fully transparent (`SDL_SetWindowOpacity` 0,
`alphaValue`).
- **Windows:** shown cloaked (`DWMWA_CLOAK`): shown and composited, but
not on screen. That avoids SDL's opacity, which would make the window
layered, which `win32_titlebar` already rules out (it breaks native
dialogs' input).
  - **Elsewhere:** shown as before.
- **The first frame** is drawn into it and presented. It then asks for
the next frame, so an idle launch is still revealed.
- **Revealed** as the next frame begins (`platform.window.reveal`). The
`shown` phase now comes after `first frame`, and the log says `startup:
shown N ms after main`.
- **A handover's new instance** signals "ready" only once its window is
on screen. The old instance quits only after that, so the overlap is
guaranteed by the order.
- Moving "ready" after the first frame exposed a bug:
`restart.pollReleased` ran in the new instance's first frame, with its
wait deadline still zero. It deleted the handover folder and took the
lock while the old instance still held it, so the new instance handed
its own launch over and exited. Polling now starts only once "ready" has
been signalled.

## SDK impact

- [x] None

## Verified

- [x] **macOS:**
- **Presented at opacity 0:** the backend's `Health` counters show the
first frame was presented while the window was transparent (presents 0 →
1 between the first frame's start and the reveal, skips unchanged). So
what is revealed has been drawn.
- **A handover's new instance**, its phases read through the agent:
`first frame` at 525.5 ms, `shown` 541.7, `handover ready` 541.7,
`handover lock` 566.0. Two handovers in a row each came up as a new
instance that answered.
- **The fix above** was found by a handover that stalled for 30 s: the
new instance exited about 300 ms in, and the old one gave up.
- **Gates:** `zig build`, `zig build test` (541/542, 1 skipped),
`test-integration` (383/383), `check-web`, `test-sdk-version`.
- [x] **Windows,** Windows 11 ARM64 VM, an `aarch64-windows-gnu` build
launched in the user's session, sampled every 50 ms: shown and cloaked
at 3852 ms, uncloaked 136 ms later. It is never left cloaked.
- [ ] Linux: CI. The window shows as before there.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant