---
title: Startup trace (single-file launch performance log for bug reports)
---
# Startup trace (single-file launch performance log for bug reports)

## What it is

A dedicated log file — `startup.log` — that captures the timeline of the most recent app launch in one self-contained, human-readable file. It exists so a user reporting "the app feels slow to start" can attach exactly **one** file to a bug report, and any agent (or outside AI like ChatGPT) reading that pasted file can immediately tell which startup phase was slow without seeing the rest of the codebase.

The file lives next to the regular logs:

- **Windows**: `%APPDATA%/omniscio/logs/startup.log`
- **macOS**: `~/Library/Application Support/omniscio/logs/startup.log`
- **Linux**: `~/.config/omniscio/logs/startup.log`

Each launch overwrites the file. The previous five launches are preserved as `startup.log.1` (most recent) through `startup.log.5` (oldest).

It is **separate from `main.log`** on purpose: `main.log` is a rolling firehose of every log line during app runtime, useful for debugging behavior _during_ use; `startup.log` is a pinpoint trace of the launch timeline only, so a user pasting it into a chat with no other context still gets a useful answer.

## Where to find it

**Settings → Diagnostics** — the **Startup Trace** card with its **Reveal Startup Log** button.

### Reading it from an E2E test

In E2E the app's `<userData>` is the per-test temp directory, and the fixture deletes that directory
at teardown — so this file is written and then destroyed on every single test. Read it BEFORE
teardown, or it is gone: `tests/perf/boot-trace.spec.ts` does exactly that and prints the per-phase
breakdown, which is how the ~9.2s migration replay on every test was found on 2026-09-28. That spec
lives in the default-off `perf` tier, so a normal run does not pay for it.

## How it behaves

### How to use it

### To attach it to a bug report

1. Open **Settings → Diagnostics**. You'll see a card titled **Startup Trace** with a single button: **Reveal Startup Log**.
2. Click it. Omniscio opens your file manager with `startup.log` highlighted.
3. Drag the file into the bug report (or paste its contents into a chat).

The file is plain text, ~30–80 lines, opens in any editor. The very top has a self-describing header so an LLM reading a pasted copy with no other context understands what each section means.

### To compare against a previous launch

After you click Reveal Startup Log you'll see the rotated backups in the same folder (`startup.log.1`, `startup.log.2`, etc.). Open `startup.log.1` to see the previous launch. Anything older was rotated out the next time you opened the app.

### What you'll see in the file

Four sections, in order:

```
═══════════════════════════════════════════════════════════════
 Omniscio STARTUP TRACE
 …self-describing header (version, platform, mode, instance)…
═══════════════════════════════════════════════════════════════

────────────────────────────────────────────────
 MAIN PROCESS PHASES (relative to app.whenReady)
────────────────────────────────────────────────
+0ms      app.whenReady
+12ms     setupLogging done                [+12ms phase]
+45ms     db init                          [+33ms phase]
+78ms     IPC handlers registered          [+33ms phase]
+412ms    createWindow returned            [+334ms phase]
…

────────────────────────────────────────────────
 RENDERER PHASES (relative to mainTsxStart)
────────────────────────────────────────────────
+0ms      mainTsxStart
+24ms     ipcModuleLoad
+512ms    jsLoaded
+520ms    bootstrapStart
+820ms    bootstrapEnd
+820ms    hydrateEnd (visible)

────────────────────────────────────────────────
 SLOWEST 5 PHASES (main process)
────────────────────────────────────────────────
1. IPC handlers registered → createWindow returned         334ms
2. setupLogging done → db init                              33ms
…

────────────────────────────────────────────────
 SUMMARY
────────────────────────────────────────────────
app.whenReady → last-main-mark:    412ms
htmlParsed → renderer-hydrated:    820ms
Renderer report received:           2026-05-08T18:42:11.123Z
```

The "Slowest 5 Phases" section is the most useful for triage — it sorts phases by gap-from-previous-mark so you can immediately see where time was spent.

### How it works

Recorder lives in [/src/main/services/startup-trace.ts](/src/main/services/startup-trace.ts). It's a small in-memory state machine with three public functions:

- `initStartupTrace()` — called once from [/src/main/index.ts](/src/main/index.ts) right after the `startupT0` timestamp. Rotates `startup.log` → `startup.log.1` → … → `startup.log.5` (drops the oldest), then writes the self-describing header banner.
- `recordStartupMark(label, elapsedMs)` — called from inside `startupMark()` in `index.ts` at every existing `[perf:startup]` instrumentation site. Writes one line to disk per mark with elapsed-since-launch and gap-since-previous-mark.
- `recordRendererTimings(timings)` — called by the IPC handler `STARTUP_TRACE_REPORT_RENDERER` after the renderer has finished its initial bootstrap. Appends the Renderer Phases section, the Slowest 5 Phases summary, and the final Summary block, then marks the trace flushed (idempotent — a renderer reload is a no-op).

The renderer side lives in [/src/renderer/src/App.tsx](/src/renderer/src/App.tsx). Inside the Electron-only branch of the initial `Promise.all([fetchSettings(), fetchProjects(), fetchSessions(), fetchDividers()]).then()`, it reads `window.__perfTimings` (populated by inline scripts in `index.html` for `htmlParsed`, `ipcModuleLoad`, `mainTsxStart`, `jsLoaded`) plus its own `bootstrapStart` / `bootstrapEnd` / `hydrateEnd` measurements, and posts everything in one IPC call to `STARTUP_TRACE_REPORT_RENDERER`. The handler validates with Zod (`startupTraceReportRendererSchema` in `src/shared/ipc-schemas.ts` — caps each field at 5 minutes to prevent runaway values), then forwards to `recordRendererTimings`.

A 60-second watchdog timer in `index.ts` calls `flushStartupTraceWithoutRenderer()` if the renderer never reports — that way a hung renderer still leaves a trace with whatever main-process marks were captured before the hang.

The `Reveal Startup Log` button in Settings → Diagnostics uses three IPC channels:

- `STARTUP_TRACE_REPORT_RENDERER` — renderer → main, reports `__perfTimings`
- `STARTUP_TRACE_GET_PATH` — main returns the absolute path so the renderer can show it
- `STARTUP_TRACE_REVEAL` — main calls `shell.showItemInFolder(getStartupLogPath())`

All three are registered in [/src/main/ipc/log-handlers.ts](/src/main/ipc/log-handlers.ts) alongside the existing log/crash-dump handlers.

The recorder's writes are append-only with `appendFileSync` (synchronous so a crash mid-startup still leaves a partial trace on disk) and every write is wrapped in a try/catch — a failure to write must never block startup itself. If the log directory cannot be created, the trace silently no-ops.

### Why a separate file (not just `main.log`)

`main.log` is invaluable but the wrong shape for this problem:

- It rotates by size, so a long debug session may have evicted the launch you care about by the time you check.
- It interleaves every log line from every subsystem, making the launch timeline hard to spot.
- It's only meaningful with the rest of the app's context — pasting it into ChatGPT yields wishy-washy answers.

`startup.log` solves all three: rotation is per-launch (not per-MB), the file contains _only_ launch timings, and the self-describing header makes it interpretable in isolation.

### Limitations

- The trace is recorded **once per launch**. If the user does something inside the app that's slow, the regular `main.log` is the right place to look — `startup.log` won't have new entries.
- The renderer section appears only after the renderer has fully bootstrapped. If the renderer crashes during init, the watchdog flushes the file at the 60-second mark with a `(Renderer timings not received before flush.)` note — useful as a diagnostic ("we got to phase X but never made it past renderer hydration").
- The trace shares the same `<userData>/logs/` folder as `main.log`. The **Export Logs** button at Settings → Diagnostics → Actions packages everything in that folder _plus_ crash dumps into a single ZIP, so attaching the export ZIP to a bug report includes the startup trace automatically.

## Related

- [logs-and-debugging.md](logs-and-debugging.md) — the broader logging system (rolling `main.log`, in-app Debug Log Viewer, crash dumps, ZIP export).
- [heap-snapshot-diagnostics.md](heap-snapshot-diagnostics.md) — the on-demand heap snapshot endpoint for memory bloat investigation; complementary to startup trace (one is launch performance, the other is steady-state memory).
