Quick Memory

Benchmarks

Speed is the reason this app exists, so it should be measured in public.

How these were measured

These are numbers from one machine on one day, not a promise about yours. The hardware and conditions are right below — including how much memory was free and how busy the machine was, because a machine under pressure produces slower and noisier results and you deserve to know which you are reading before you read the numbers.

Machine and conditions

Processor
Apple M2 (Mac14,15)
Machine
8 cores, 8 GB
OS
Darwin 26.1
Toolchain
rustc 1.97.0 (2d8144b78 2026-07-07)
Memory free
46% at start
Load average
4.84 at start, across 8 cores — the load when the run began. The timed runs start only after the suite's own build has cleared, and the chips under each heading below say the load while those particular figures were taken.
Commit
0a6394c
Run
2026-09-02T21:41:04-0700

Build

A from-scratch release build — cargo clean --release first, so every dependency and the fat-LTO link are inside the number. The warm figure is what you wait for after changing one line of app code; it still relinks the whole binary, which is the price of lto = "fat" and codegen-units = 1.

ProfileThe cargo release profile every timed build uses, and the one the download is built with: lto = "fat" (link-time optimisation across the whole program), codegen-units = 1 (each crate compiled as one unit: slowest to build, fastest to run) and strip = true (debug symbols removed from the binary).
release: lto=fat, codegen-units=1, strip=true

Tests

How long the two test suites take to run — each as the command a developer types, timed when nothing needs compiling, so the figure is the suite itself and grows as tests are added. They are two numbers on purpose: cargo test --workspace is the app's correctness suite; the pixel suite is behind its own cargo feature and is the one that checks how anything looks.

Tests in the Rust suiteHow many tests the timed invocation ran, summed over cargo's own "test result" lines (one per test binary); ignored tests are not counted.
1,333
Tests in the pixel suiteHow many tests the timed invocation ran, summed over cargo's own "test result" lines (one per test binary); ignored tests are not counted.
21

Launch — best case

From exec to the first settled frame — the whole wait, including the dynamic linking that happens before qm runs a line of its own code. Each run gets a fresh copy of the vault and a fresh process, so nothing inherits a remembered scroll position or a previous run's index. These are best-case figures: an idle machine, and a file cache still holding the vault that was staged a moment earlier. That is the second launch, not the first — the first is measured separately below. Backlinks are indexed in the background and the window does not wait for them, so each row says when that landed rather than counting it as a step.

Empty vault

  • Process startTime between the harness spawning the process and the first statement of main(): exec, dyld loading and binding the dynamic libraries the binary links, and the Rust runtime's own start. Computed as the app's start timestamp (taken at the top of main) minus the harness's timestamp from just before it spawned the process. Unlike every phase below, it is not a gap between two timestamps the app takes itself — the app cannot time what happens before it runs — which is why the harness supplies one end of it. exec and dynamic linking, before qm's own code runs 16 ms
  • Platform readyFrom the first statement of main() to the platform_ready mark, which the app stamps as the first thing inside gpui's run callback: everything gpui does to bring up NSApplication, the Metal renderer and the text system before any of qm's own code runs. The floor under any launch, separated out so qm's share of startup is not confused with it. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. gpui brings up NSApplication, Metal and the text system 80 ms
  • UI readyFrom platform_ready to the ui_ready mark: gpui-component's initialisation, the theme, registering every key binding and building the application menu. Nothing has been read from the vault yet. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. theme, key bindings and the menu bar 19 ms
  • Scan vaultFrom ui_ready to the vault_scanned mark, stamped when the vault folder has been walked and every .md file indexed. Three things happen inside it, in order. First, the window's saved geometry is read. Second, gpui creates the macOS window and its Metal renderer — open_window makes the window before it builds the view inside it, and it is while building that view that the vault is scanned. Third, the scan itself, which reads directory entries only (file names and folders, skipping hidden ones), never the contents of a note; note text is read later, by opening a note and by the backlink index in the background. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. the macOS window is created, then the tree is walked and every .md file indexed 64 ms
  • Open first noteFrom vault_scanned to the first_file_open mark, stamped once the workspace has read the note it opens at launch (the last note open in this vault, or the note a new vault is given), parsed it into blocks, and placed the caret where it last was. Reading the file and building the markdown block map are both inside it; so is recording the vault in the known-vaults list; the background read for the backlink index is started here but not waited for; nothing has been painted yet. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. read the file, parse it into blocks 49 ms
  • Window readyFrom first_file_open to the window_opened mark, stamped when gpui's open_window returns. Inside it: the rest of building the view tree once the first note is ready (the root view that wraps the shell, the close-button handler, the benchmark's own frame callbacks), and then the first layout and paint of the note. gpui draws a window once — into a scene that is not yet presented — before open_window returns, so this phase holds the first time every visible block is laid out and the first glyphs are rasterised. That first layout is against the raw window size; the content column's real width is only known once this draw has measured its scroll viewport, which is what the next drawn frame uses. The macOS window itself was created earlier, inside the Scan vault phase. Nothing is on screen yet. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. the view tree is finished and the note is laid out and painted once, not yet on screen 34 ms
  • First display tickFrom window_opened to the first_frame mark. That mark is a callback registered before any frame, and gpui runs such callbacks at the start of the next display-link tick, before that tick's draw — so this phase is the wait between open_window returning and the display's first tick for this window: the window becoming visible and the display link starting. No layout happens inside it; the first frame that reaches the screen is drawn immediately after it. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. the window is visible and the display link has started; no layout happens here 11 ms
  • First frame on screenFrom the first_frame mark to the settled_frame mark, which is registered inside the first_frame callback and therefore fires at the start of the display-link tick after it. Inside: the draw of the first frame that reaches the screen, laid out against the scroll viewport the pre-open draw measured — the content column's real width, which that draw, laid out against the raw window size, did not yet know; this is why the page calls this the settled frame — then its presentation, and the wait for the following tick. By this mark the first settled frame is on screen; the figure includes up to one display refresh of waiting for the tick that stamps it. This mark plus the process-start phase is the headline bar above. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. drawn against the real viewport and presented; marked at the next tick 68 ms
Notes in vaultHow many .md files the vault scan found in the staged copy of the vault, as the app reported it once the window was up. A new empty vault shows more than zero because qm writes its starter notes into it.
2
Blocks in open noteHow many blocks — paragraphs, headings, list items, code fences, blank-line runs — the note that was opened at launch parsed into: what the first frame's layout had to consider.
17
Open note sizeThe size in bytes of the opened note's text as the app holds it in memory.
1.6 kB
Backlinks indexedWhen the backlink index landed: the backlinks_built mark, stamped on the main thread when the background read of every note's link sources finished and its result was installed, in milliseconds from the first statement of main(). It is a moment, not a phase: since 2026-08-17 the window does not wait for it, so it can land after the first frames, and "had not landed when the run ended" means the harness quit the app (which it does shortly after the second frame) before the mark was stamped. Earlier figures, taken when the index was built on the launch path, are not comparable with these.
321 ms after start, in the background

Moby Dick

  • Process startTime between the harness spawning the process and the first statement of main(): exec, dyld loading and binding the dynamic libraries the binary links, and the Rust runtime's own start. Computed as the app's start timestamp (taken at the top of main) minus the harness's timestamp from just before it spawned the process. Unlike every phase below, it is not a gap between two timestamps the app takes itself — the app cannot time what happens before it runs — which is why the harness supplies one end of it. exec and dynamic linking, before qm's own code runs 13 ms
  • Platform readyFrom the first statement of main() to the platform_ready mark, which the app stamps as the first thing inside gpui's run callback: everything gpui does to bring up NSApplication, the Metal renderer and the text system before any of qm's own code runs. The floor under any launch, separated out so qm's share of startup is not confused with it. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. gpui brings up NSApplication, Metal and the text system 68 ms
  • UI readyFrom platform_ready to the ui_ready mark: gpui-component's initialisation, the theme, registering every key binding and building the application menu. Nothing has been read from the vault yet. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. theme, key bindings and the menu bar 18 ms
  • Scan vaultFrom ui_ready to the vault_scanned mark, stamped when the vault folder has been walked and every .md file indexed. Three things happen inside it, in order. First, the window's saved geometry is read. Second, gpui creates the macOS window and its Metal renderer — open_window makes the window before it builds the view inside it, and it is while building that view that the vault is scanned. Third, the scan itself, which reads directory entries only (file names and folders, skipping hidden ones), never the contents of a note; note text is read later, by opening a note and by the backlink index in the background. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. the macOS window is created, then the tree is walked and every .md file indexed 73 ms
  • Open first noteFrom vault_scanned to the first_file_open mark, stamped once the workspace has read the note it opens at launch (the last note open in this vault, or the note a new vault is given), parsed it into blocks, and placed the caret where it last was. Reading the file and building the markdown block map are both inside it; so is recording the vault in the known-vaults list; the background read for the backlink index is started here but not waited for; nothing has been painted yet. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. read the file, parse it into blocks 31 ms
  • Window readyFrom first_file_open to the window_opened mark, stamped when gpui's open_window returns. Inside it: the rest of building the view tree once the first note is ready (the root view that wraps the shell, the close-button handler, the benchmark's own frame callbacks), and then the first layout and paint of the note. gpui draws a window once — into a scene that is not yet presented — before open_window returns, so this phase holds the first time every visible block is laid out and the first glyphs are rasterised. That first layout is against the raw window size; the content column's real width is only known once this draw has measured its scroll viewport, which is what the next drawn frame uses. The macOS window itself was created earlier, inside the Scan vault phase. Nothing is on screen yet. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. the view tree is finished and the note is laid out and painted once, not yet on screen 52 ms
  • First display tickFrom window_opened to the first_frame mark. That mark is a callback registered before any frame, and gpui runs such callbacks at the start of the next display-link tick, before that tick's draw — so this phase is the wait between open_window returning and the display's first tick for this window: the window becoming visible and the display link starting. No layout happens inside it; the first frame that reaches the screen is drawn immediately after it. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. the window is visible and the display link has started; no layout happens here 10 ms
  • First frame on screenFrom the first_frame mark to the settled_frame mark, which is registered inside the first_frame callback and therefore fires at the start of the display-link tick after it. Inside: the draw of the first frame that reaches the screen, laid out against the scroll viewport the pre-open draw measured — the content column's real width, which that draw, laid out against the raw window size, did not yet know; this is why the page calls this the settled frame — then its presentation, and the wait for the following tick. By this mark the first settled frame is on screen; the figure includes up to one display refresh of waiting for the tick that stamps it. This mark plus the process-start phase is the headline bar above. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. drawn against the real viewport and presented; marked at the next tick 70 ms
Notes in vaultHow many .md files the vault scan found in the staged copy of the vault, as the app reported it once the window was up. A new empty vault shows more than zero because qm writes its starter notes into it.
1
Blocks in open noteHow many blocks — paragraphs, headings, list items, code fences, blank-line runs — the note that was opened at launch parsed into: what the first frame's layout had to consider.
2,651
Open note sizeThe size in bytes of the opened note's text as the app holds it in memory.
1.2 MB
Backlinks indexedWhen the backlink index landed: the backlinks_built mark, stamped on the main thread when the background read of every note's link sources finished and its result was installed, in milliseconds from the first statement of main(). It is a moment, not a phase: since 2026-08-17 the window does not wait for it, so it can land after the first frames, and "had not landed when the run ended" means the harness quit the app (which it does shortly after the second frame) before the mark was stamped. Earlier figures, taken when the index was built on the launch path, are not comparable with these.
317 ms after start, in the background

Large vault

  • Process startTime between the harness spawning the process and the first statement of main(): exec, dyld loading and binding the dynamic libraries the binary links, and the Rust runtime's own start. Computed as the app's start timestamp (taken at the top of main) minus the harness's timestamp from just before it spawned the process. Unlike every phase below, it is not a gap between two timestamps the app takes itself — the app cannot time what happens before it runs — which is why the harness supplies one end of it. exec and dynamic linking, before qm's own code runs 14 ms
  • Platform readyFrom the first statement of main() to the platform_ready mark, which the app stamps as the first thing inside gpui's run callback: everything gpui does to bring up NSApplication, the Metal renderer and the text system before any of qm's own code runs. The floor under any launch, separated out so qm's share of startup is not confused with it. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. gpui brings up NSApplication, Metal and the text system 74 ms
  • UI readyFrom platform_ready to the ui_ready mark: gpui-component's initialisation, the theme, registering every key binding and building the application menu. Nothing has been read from the vault yet. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. theme, key bindings and the menu bar 17 ms
  • Scan vaultFrom ui_ready to the vault_scanned mark, stamped when the vault folder has been walked and every .md file indexed. Three things happen inside it, in order. First, the window's saved geometry is read. Second, gpui creates the macOS window and its Metal renderer — open_window makes the window before it builds the view inside it, and it is while building that view that the vault is scanned. Third, the scan itself, which reads directory entries only (file names and folders, skipping hidden ones), never the contents of a note; note text is read later, by opening a note and by the backlink index in the background. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. the macOS window is created, then the tree is walked and every .md file indexed 85 ms
  • Open first noteFrom vault_scanned to the first_file_open mark, stamped once the workspace has read the note it opens at launch (the last note open in this vault, or the note a new vault is given), parsed it into blocks, and placed the caret where it last was. Reading the file and building the markdown block map are both inside it; so is recording the vault in the known-vaults list; the background read for the backlink index is started here but not waited for; nothing has been painted yet. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. read the file, parse it into blocks 31 ms
  • Window readyFrom first_file_open to the window_opened mark, stamped when gpui's open_window returns. Inside it: the rest of building the view tree once the first note is ready (the root view that wraps the shell, the close-button handler, the benchmark's own frame callbacks), and then the first layout and paint of the note. gpui draws a window once — into a scene that is not yet presented — before open_window returns, so this phase holds the first time every visible block is laid out and the first glyphs are rasterised. That first layout is against the raw window size; the content column's real width is only known once this draw has measured its scroll viewport, which is what the next drawn frame uses. The macOS window itself was created earlier, inside the Scan vault phase. Nothing is on screen yet. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. the view tree is finished and the note is laid out and painted once, not yet on screen 19 ms
  • First display tickFrom window_opened to the first_frame mark. That mark is a callback registered before any frame, and gpui runs such callbacks at the start of the next display-link tick, before that tick's draw — so this phase is the wait between open_window returning and the display's first tick for this window: the window becoming visible and the display link starting. No layout happens inside it; the first frame that reaches the screen is drawn immediately after it. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. the window is visible and the display link has started; no layout happens here 9 ms
  • First frame on screenFrom the first_frame mark to the settled_frame mark, which is registered inside the first_frame callback and therefore fires at the start of the display-link tick after it. Inside: the draw of the first frame that reaches the screen, laid out against the scroll viewport the pre-open draw measured — the content column's real width, which that draw, laid out against the raw window size, did not yet know; this is why the page calls this the settled frame — then its presentation, and the wait for the following tick. By this mark the first settled frame is on screen; the figure includes up to one display refresh of waiting for the tick that stamps it. This mark plus the process-start phase is the headline bar above. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. drawn against the real viewport and presented; marked at the next tick 72 ms
Notes in vaultHow many .md files the vault scan found in the staged copy of the vault, as the app reported it once the window was up. A new empty vault shows more than zero because qm writes its starter notes into it.
1,977
Blocks in open noteHow many blocks — paragraphs, headings, list items, code fences, blank-line runs — the note that was opened at launch parsed into: what the first frame's layout had to consider.
124
Open note sizeThe size in bytes of the opened note's text as the app holds it in memory.
22.2 kB
Backlinks indexedWhen the backlink index landed: the backlinks_built mark, stamped on the main thread when the background read of every note's link sources finished and its result was installed, in milliseconds from the first statement of main(). It is a moment, not a phase: since 2026-08-17 the window does not wait for it, so it can land after the first frames, and "had not landed when the run ended" means the harness quit the app (which it does shortly after the second frame) before the mark was stamped. Earlier figures, taken when the index was built on the launch path, are not comparable with these.
had not landed when the run ended, 499 ms after start

One large picture

  • Process startTime between the harness spawning the process and the first statement of main(): exec, dyld loading and binding the dynamic libraries the binary links, and the Rust runtime's own start. Computed as the app's start timestamp (taken at the top of main) minus the harness's timestamp from just before it spawned the process. Unlike every phase below, it is not a gap between two timestamps the app takes itself — the app cannot time what happens before it runs — which is why the harness supplies one end of it. exec and dynamic linking, before qm's own code runs 13 ms
  • Platform readyFrom the first statement of main() to the platform_ready mark, which the app stamps as the first thing inside gpui's run callback: everything gpui does to bring up NSApplication, the Metal renderer and the text system before any of qm's own code runs. The floor under any launch, separated out so qm's share of startup is not confused with it. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. gpui brings up NSApplication, Metal and the text system 69 ms
  • UI readyFrom platform_ready to the ui_ready mark: gpui-component's initialisation, the theme, registering every key binding and building the application menu. Nothing has been read from the vault yet. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. theme, key bindings and the menu bar 18 ms
  • Scan vaultFrom ui_ready to the vault_scanned mark, stamped when the vault folder has been walked and every .md file indexed. Three things happen inside it, in order. First, the window's saved geometry is read. Second, gpui creates the macOS window and its Metal renderer — open_window makes the window before it builds the view inside it, and it is while building that view that the vault is scanned. Third, the scan itself, which reads directory entries only (file names and folders, skipping hidden ones), never the contents of a note; note text is read later, by opening a note and by the backlink index in the background. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. the macOS window is created, then the tree is walked and every .md file indexed 71 ms
  • Open first noteFrom vault_scanned to the first_file_open mark, stamped once the workspace has read the note it opens at launch (the last note open in this vault, or the note a new vault is given), parsed it into blocks, and placed the caret where it last was. Reading the file and building the markdown block map are both inside it; so is recording the vault in the known-vaults list; the background read for the backlink index is started here but not waited for; nothing has been painted yet. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. read the file, parse it into blocks 28 ms
  • Window readyFrom first_file_open to the window_opened mark, stamped when gpui's open_window returns. Inside it: the rest of building the view tree once the first note is ready (the root view that wraps the shell, the close-button handler, the benchmark's own frame callbacks), and then the first layout and paint of the note. gpui draws a window once — into a scene that is not yet presented — before open_window returns, so this phase holds the first time every visible block is laid out and the first glyphs are rasterised. That first layout is against the raw window size; the content column's real width is only known once this draw has measured its scroll viewport, which is what the next drawn frame uses. The macOS window itself was created earlier, inside the Scan vault phase. Nothing is on screen yet. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. the view tree is finished and the note is laid out and painted once, not yet on screen 25 ms
  • First display tickFrom window_opened to the first_frame mark. That mark is a callback registered before any frame, and gpui runs such callbacks at the start of the next display-link tick, before that tick's draw — so this phase is the wait between open_window returning and the display's first tick for this window: the window becoming visible and the display link starting. No layout happens inside it; the first frame that reaches the screen is drawn immediately after it. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. the window is visible and the display link has started; no layout happens here 10 ms
  • First frame on screenFrom the first_frame mark to the settled_frame mark, which is registered inside the first_frame callback and therefore fires at the start of the display-link tick after it. Inside: the draw of the first frame that reaches the screen, laid out against the scroll viewport the pre-open draw measured — the content column's real width, which that draw, laid out against the raw window size, did not yet know; this is why the page calls this the settled frame — then its presentation, and the wait for the following tick. By this mark the first settled frame is on screen; the figure includes up to one display refresh of waiting for the tick that stamps it. This mark plus the process-start phase is the headline bar above. Like every launch phase after Process start, this is the gap between two timestamps the app takes itself ("marks"), counted from the first statement of main(); the harness turns consecutive marks into phase costs. drawn against the real viewport and presented; marked at the next tick 67 ms
Notes in vaultHow many .md files the vault scan found in the staged copy of the vault, as the app reported it once the window was up. A new empty vault shows more than zero because qm writes its starter notes into it.
1
Blocks in open noteHow many blocks — paragraphs, headings, list items, code fences, blank-line runs — the note that was opened at launch parsed into: what the first frame's layout had to consider.
2
Open note sizeThe size in bytes of the opened note's text as the app holds it in memory.
53 B
Backlinks indexedWhen the backlink index landed: the backlinks_built mark, stamped on the main thread when the background read of every note's link sources finished and its result was installed, in milliseconds from the first statement of main(). It is a moment, not a phase: since 2026-08-17 the window does not wait for it, so it can land after the first frames, and "had not landed when the run ended" means the harness quit the app (which it does shortly after the second frame) before the mark was stamped. Earlier figures, taken when the index was built on the launch path, are not comparable with these.
285 ms after start, in the background

Launch — first one after you turn the Mac on

The same launches, with the file cache emptied first — purge(8), which Apple documents as the way to “approximate initial boot conditions with a cold disk buffer cache for performance analysis”. Nothing about qm is warm here: not the vault, not the app's own binary. This is the launch you get once a day, and for an app whose startup is mostly reading files it is the one worth knowing.

Empty vaultThis scenario's cold headline divided by its warm headline from the section above — the two figures in the brackets, as a ratio. Both are medians.
6.8× the warm figure (336 ms → 2.3 s)
Moby DickThis scenario's cold headline divided by its warm headline from the section above — the two figures in the brackets, as a ratio. Both are medians.
6.8× the warm figure (335 ms → 2.3 s)
Large vaultThis scenario's cold headline divided by its warm headline from the section above — the two figures in the brackets, as a ratio. Both are medians.
7.8× the warm figure (316 ms → 2.5 s)

Footprint

Memory held with the vault open and the first frames painted, and how big the app is to begin with. The memory figure is macOS's own phys_footprint — the number Activity Monitor shows in its Memory column, and the one the OS uses to decide who is using too much. Resident-set size, which ps reports, is not comparable between processes. The sizes are measured on a build without the benchmark harness, because that is what you download.

Download (QM.dmg)The size in bytes of the disk image script/make-dmg.sh produced from a release build made without the bench feature — the same commands ./deploy uses — which is the file the download page serves. Code signing and notarisation add a few kilobytes the harness cannot reproduce without the owner's identity, so the published file is very slightly larger than this.
7.1 MB
QM.app on diskThe sum of the sizes of every file inside QM.app as script/make-app.sh builds it from the same no-bench binary: file sizes, not disk blocks, so it is a fact about the app rather than about the disk it sits on.
16.1 MB
The binary itselfThe size in bytes of target/release/qm from a release build without the bench feature: a single stripped, fat-LTO binary, the executable inside QM.app.
14.1 MB

Selecting text

Holding Shift and pressing Down, and pressing Cmd+A, in the 1.2 MB note — timed from the keystroke to the frame that shows the highlight, which is the whole of what you wait for. The caret starts halfway down the book, not at the top. The first press of a gesture is its own figure because it is the one that changes what the editor is holding: a selection cannot leave the text its widget has, so the first Shift+Down that reaches past it grows the editor by a line (until 2026-08-18 it handed over the whole note; that cost now belongs to Cmd+A alone, which means the whole note by definition). Every press after it is the steady cost, and it is the one that repeats. Read these in frames. They end at a painted frame, because that is what you actually wait for — so on a 60 Hz display a press whose work fits inside a frame reads as ~17 ms however little it does, and one that misses a vsync reads as ~33 ms. The figure to notice is which side of one frame a press falls on, not the millisecond.

NoteThe size in bytes of the note the presses were made in, as the app holds it.
1.2 MB
Blocks in the noteHow many blocks the note parsed into — the size of the document the selection is growing through.
2,651
Cmd+A selectedHow many bytes the editor's selection spanned at the end of the Cmd+A run, reported by the app. It is published because a gesture that selected nothing would be fast for the wrong reason; this one selected the note.
1.2 MB
Worst single pressThe longest single press among the later presses (two to twenty) across all the runs: the worst keystroke-to-painted interval observed for Shift+Down.
33 ms

Quick open

Typing into Cmd+P re-matches every note in the vault, on the spot — quick open keeps no index and has no debounce, so what the matcher costs is what the keystroke costs. Four queries, because they are four different amounts of work: the empty one, which is the list Cmd+P opens with and which every note matches; a prefix; a substring that has to fall through to the path; and one that matches nothing. Measured in the library rather than through the window, and deliberately: the whole match fits inside a single painted frame at this vault size, so a figure taken from the app would read one frame — about 17 ms — until the day it got forty times worse. This one has no such floor. Before 2026-08-20 the empty query took 4.10 ms here; it takes about 0.13 ms now, and that difference is the point of measuring it this way. Each sample is a batch of matches rather than a single one — at this scale one match timed alone reads the scheduler, not the work.

Notes in the vaultHow many notes the matcher was run over, which is every note in the vault.
1,977
Rows the list can showHow many rows quick open will show, and so how many the matcher keeps.
50
Recency list depthHow many notes deep the most-recently-opened list was. It ranks ahead of path length, so it is part of the work every note is put through.
50
Matches for the empty queryHow many notes the empty query matched — every one of them, which is why that query is the worst case rather than the cheapest.
50

Switching notes

Opening one note after another, timed from the request to the frame that carries the new note — not to the moment the app decided to load it. Two vaults: two copies of the 1.2 MB book, so every switch reads, parses and draws a note the size of Moby Dick, and a vault of about 2,000 ordinary notes, which is what switching feels like in a real vault. The figure goes through the same call every route into a note ends at — quick open, a wikilink, back and forward, the file tree — so one number covers all of them; it does not include Cmd+P's own matching, which is a different question. qm never shows a note's name beside another note's text, and never shows a name over a blank page while it loads: the switch is one step, and a note that cannot be read does not replace the one you are in. Read these in frames, like the selection figures — they end at a painted frame, so on a 60 Hz display anything that fits inside one reads as ~17 ms however little it does.

Switches per runHow many notes each run opened in turn, after the app had started.
20
Of those, that changed the noteHow many of those requests actually changed the open note. It is published because asking for the note you are already in returns immediately, and a run full of those would be fast for the wrong reason. This equals the number of switches, so every sample is a real one.
20
Note switched into (big vault)The size of the note the last switch landed in, as the app holds it — the check that the big-note run really was switching into the big note.
1.2 MB
Notes in the large vaultHow many notes the large vault held, as the app reported it.
1,977

Search

Cmd+Shift+F to results on screen. qm keeps no content index — it reads the files at query time (ADR 0003) on a background thread, after a 150 ms debounce that is included below because you wait for it too. Matches appear as they are found, so the headline figure is time to the first ones; “whole vault scanned” below is when the last note was read, which is the number that grows with the vault. Read the single-note rows with care: results are noticed once per painted frame, and a frame of the 1.2 MB note costs about as long as the jump itself, so those two figures are quantised by the frame cost below and overstate the search work by up to one frame.

Common word, one big note

QueryThe word searched for, exactly as typed into the search field.
“whale”
Notes searchedHow many notes the vault held, as the app reported it — every one of them is read during the scan, because qm keeps no content index.
1
Matches foundHow many matches the scan had found when it completed. The scan stops collecting at 200 (the hit cap), so 200 means "at least 200".
1
Whole vault scannedFrom search_opened to the search_complete mark, stamped on the first painted frame after the background scan reported that it had read the last note — when qm could say "nothing" for a word that appears nowhere. Median of the runs.
307 ms
Jump to next matchAfter the scan completes, the harness jumps from match to match, one jump per painted frame, ten jumps by default; each jump opens the hit's note at the match and paints it. The figure is the median of those frame intervals, pooled across the runs: the steady cost of "next match".
21 ms
Worst jumpThe longest of those match-to-match frame intervals across all the runs.
21 ms

Word that appears nowhere

QueryThe word searched for, exactly as typed into the search field.
“zzqqxx”
Notes searchedHow many notes the vault held, as the app reported it — every one of them is read during the scan, because qm keeps no content index.
1
Matches foundHow many matches the scan had found when it completed. The scan stops collecting at 200 (the hit cap), so 200 means "at least 200".
0
Whole vault scannedFrom search_opened to the search_complete mark, stamped on the first painted frame after the background scan reported that it had read the last note — when qm could say "nothing" for a word that appears nowhere. Median of the runs.
307 ms
Jump to next matchAfter the scan completes, the harness jumps from match to match, one jump per painted frame, ten jumps by default; each jump opens the hit's note at the match and paints it. The figure is the median of those frame intervals, pooled across the runs: the steady cost of "next match".
Worst jumpThe longest of those match-to-match frame intervals across all the runs.

Word that appears nowhere, ~2,000 notes

QueryThe word searched for, exactly as typed into the search field.
“zzqqxx”
Notes searchedHow many notes the vault held, as the app reported it — every one of them is read during the scan, because qm keeps no content index.
1,977
Matches foundHow many matches the scan had found when it completed. The scan stops collecting at 200 (the hit cap), so 200 means "at least 200".
0
Whole vault scannedFrom search_opened to the search_complete mark, stamped on the first painted frame after the background scan reported that it had read the last note — when qm could say "nothing" for a word that appears nowhere. Median of the runs.
409 ms
Jump to next matchAfter the scan completes, the harness jumps from match to match, one jump per painted frame, ten jumps by default; each jump opens the hit's note at the match and paints it. The figure is the median of those frame intervals, pooled across the runs: the steady cost of "next match".
Worst jumpThe longest of those match-to-match frame intervals across all the runs.

Scrolling — not measured yet

Paging a 1.2 MB note from top to bottom is the one benchmark still outstanding. The document column is virtualised (ADR 0005) — only the rows in and near the viewport are laid out — so the traversal no longer lays out every block of the book on every frame; what it costs now is simply not yet measured. The harness for it is written and lives behind ./script/bench.py --only scroll; its figures will appear here, and in the history below, once it has been run end to end.

History

One figure, every run that measured it, in the order they happened — and under each point, what changed in the app between it and the point before. A solid point was taken under light load; a hollow one under busy or heavy load, or with the window covered, and is read with that in mind. The thin line through a point is its min–max across the runs; the point is the median. Newest first in the table.

Selecting text — Shift+Down, every press after the first

020 ms40 ms60 ms80 ms2026-08-172026-09-022026-08-17 07:41 · 4ad2d68 · 17 ms (16 ms–18 ms, 57 samples) · busy load (0.83 per core)2026-08-17 17:22 · f2eda09 · 17 ms (16 ms–18 ms, 57 samples) · light load (0.32 per core)2026-08-19 05:45 · d1f547f · 17 ms (16 ms–18 ms, 57 samples) · light load (0.26 per core)2026-08-19 08:06 · a45db7f · 17 ms (16 ms–17 ms, 57 samples) · busy load (0.41 per core)2026-08-19 08:23 · 2678a58 · 17 ms (16 ms–18 ms, 57 samples) · light load (0.34 per core)2026-08-19 09:00 · dd9d92b · 17 ms (16 ms–18 ms, 57 samples) · light load (0.33 per core)2026-08-19 09:22 · b6cd7c3 · 17 ms (16 ms–18 ms, 57 samples) · light load (0.32 per core)2026-08-19 22:48 · 7331c7c · 17 ms (16 ms–18 ms, 57 samples) · busy load (0.36 per core)2026-08-20 08:06 · 9086bff · 17 ms (16 ms–18 ms, 57 samples) · busy load (0.58 per core)2026-08-20 08:42 · 0c7ca69 · 17 ms (16 ms–18 ms, 57 samples) · busy load (0.37 per core)2026-08-20 15:01 · febb754 · 17 ms (16 ms–18 ms, 57 samples) · light load (0.34 per core)2026-08-20 18:42 · 1125002 · 17 ms (16 ms–18 ms, 57 samples) · light load (0.33 per core)2026-08-20 18:52 · 1125002 · 17 ms (16 ms–26 ms, 57 samples) · busy load (0.36 per core)2026-08-20 19:03 · 1125002 · 17 ms (16 ms–17 ms, 57 samples) · light load (0.32 per core)2026-08-20 19:18 · dcba9e2 · 17 ms (16 ms–33 ms, 57 samples) · busy load (0.36 per core)2026-08-20 19:31 · b3ad404 · 17 ms (16 ms–26 ms, 57 samples) · busy load (0.45 per core)2026-08-20 23:07 · 1904806 · 17 ms (16 ms–18 ms, 57 samples) · busy load (0.44 per core)2026-08-21 15:31 · fbca877 · 17 ms (16 ms–18 ms, 57 samples) · light load (0.32 per core)2026-08-21 21:27 · 1f80dd3 · 17 ms (16 ms–25 ms, 57 samples) · busy load (0.45 per core)2026-08-22 09:10 · 84b81dd · 17 ms (16 ms–25 ms, 57 samples) · light load (0.32 per core)2026-08-22 14:50 · 40d2a4c · 17 ms (16 ms–60 ms, 57 samples) · busy load (0.59 per core)2026-08-22 17:07 · f894903 · 17 ms (16 ms–42 ms, 57 samples) · busy load (0.46 per core)2026-08-27 07:59 · 5996a8b · 17 ms (16 ms–18 ms, 57 samples) · light load (0.33 per core)2026-08-27 08:17 · 5996a8b · 17 ms (16 ms–17 ms, 57 samples) · light load (0.32 per core)2026-09-02 21:41 · 0a6394c · 17 ms (16 ms–33 ms, 57 samples) · busy load (0.48 per core)2026-09-02 22:18 · 0a6394c · 17 ms (16 ms–27 ms, 57 samples) · busy load (0.38 per core)

26 measurements of this figure across 27 runs logged since 2026-08-17 07:41.

DateGit hashShift+Down, every press after the first (milliseconds)Most recent changes
2026-09-02 22:18 0a6394c 17 ms median of 57 · 16 ms–27 ms busy load (0.38 per core)

No code change since the run 37 minutes before: the same-hour re-run of launch and select that clears that run's verdicts, with 5996a8b's binary measured the minute before (run 22-18-27, an A/B, off the chart) — select_all 15.4 ms old against 15.6 ms new, pre-main 11–18 ms on both.

Nothing that could move a figure changed since 0a6394c — docs and site only.

2026-09-02 21:41 0a6394c 17 ms median of 57 · 16 ms–33 ms busy load (0.48 per core)

Six days of commits since the 2026-08-27 point: ⇧⌘F is the vault tree with a contents filter (82ec011); a launch asks to be the .md opener, with Don't ask again (0a6394c — a Launch Services query on the background executor, which this bare benchmark binary skips for want of a bundle); One Dark is the default theme (e58d200); ⌥↑/⌥↓ move a line and ⌥⌘digits style it (449e02d, 69ebd72); a press beside a line's text places the caret (209c8c6); an edit near the top and a paste that fits no longer scroll the page (11f6e85, 314ea91); list starts, bold in headings, link hit-testing and a selection's left edge changed how a line is drawn (f6a6f76, e93115f, 8c4bc48, 7ec7e62); the vendored crate's tests joined the suite (dba9d7e). Measured at 21:41 after an evening of builds, the load at 0.6/core as it began, and the three severe verdicts are that regime: select_all's first press (37 ms here) was 15.6 ms on the same binary 37 minutes later and 15.4 ms on 5996a8b's binary the minute before (runs 22-18-27 and 22-18-40, the same-hour A/B); pre-main is dynamic linking before any of qm runs, and both binaries put it at 11–18 ms in that hour; the Rust suite's time doubled under the same load, with more tests in it. The cold group was skipped: no sudo from a detached shell.

21 commits that could move a figure, since 5996a8b
  • 7251868 2026-08-27 Feedback gets its own word in the menu bar, and a road back to Owen (ADR 0032)
  • 6f4565e 2026-08-27 The beginning of a list line is where the line is drawn to begin
  • 449e02d 2026-08-27 A line moves over its neighbour: ⌥↑/⌥↓, byte for byte, selection and all (ADR 0034)
  • 69ebd72 2026-08-28 ⌥⌘1–6 / ⌥⌘0 style the line: the heading marker set, changed, or shed under a caret that keeps its character (ADR 0035)
  • 0872310 2026-08-29 ⌘H hides qm: the chord macOS routes through an app menu that had no Hide item
  • dba9d7e 2026-08-29 The vendored crate's tests run with everything else, so they cannot rot unnoticed
  • 7ec7e62 2026-08-29 A selection's left edge steps, it does not slope: the corner goes in both ways round
  • 2c606b8 2026-08-29 A blurb is a sentence and folds like one: the offers card's rows wrap inside the card
  • 02f0d6d 2026-08-29 Under the wordmark, the vault screen says Quick Memory
  • cf30575 2026-08-30 Keep newlines out of shape_line: the indent measure, and the title's Enter
  • 3d484dd 2026-08-30 Clear the lints: quiet MARKDOWN_TYPE under test, allow the stylistic clippy set
  • 11f6e85 2026-08-30 An edit near the top of the window no longer scrolls the page away
  • e58d200 2026-08-31 One Dark is the default theme (ADR 0016 amended)
  • e93115f 2026-08-31 Bold text carries the heading colour (owner, 2026-08-31)
  • 8c4bc48 2026-08-31 A link is followed only by a click on its own glyphs
  • f6a6f76 2026-08-31 A list starts where its author starts it (ADR 0037)
  • 82ec011 2026-09-01 Substring search is the vault tree with a contents filter (⇧⌘F)
  • ddb8e02 2026-09-01 Shift+Enter makes a line, never an item; ⌘B closes an open bold
  • 314ea91 2026-09-02 A paste that fits on screen no longer jumps the page
  • 209c8c6 2026-09-02 A press beside a line's text places the caret there, not a selection
  • 0a6394c 2026-09-02 A launch asks whether QM should open .md files, with Don't ask again (ADR 0021 amended)
2026-08-27 08:17 5996a8b 17 ms median of 57 · 16 ms–17 ms light load (0.32 per core)

The same build on a quiet machine. Its slower figures against 08-22 17:07 are the machine's slow regime, not the code: four A/B runs beside this one (is_default_binary: false) measured binaries from f894903+1 through HEAD at identical times today, and the log shows the same two regimes on near-identical code — 08-22's cold build was 598 s at 09:10 and 230 s at 17:07.

Nothing that could move a figure changed since 5996a8b — docs and site only.

2026-08-27 07:59 5996a8b 17 ms median of 57 · 16 ms–18 ms light load (0.33 per core)

Five days landed: tables — a grid you read, source you edit (ADR 0028), paste as a table (ADR 0031), and selection windows that take them whole; leading-space indents (ADR 0026); Cmd+F; the motion tasks 1150-1200; the privacy posture (ADR 0032). The run itself was contended; the 08:17 re-run is the one to read.

27 commits that could move a figure, since f894903
  • d42c24a 2026-08-22 The prepaint settle-net redraws when it changes the row count
  • 41695b0 2026-08-22 A frame with no input must equal the one before it: assert_settled (task 0001.1)
  • 796eb94 2026-08-22 A trace build: the last seconds of frames and their causes, on a key (task 0001.6)
  • 8791a10 2026-08-22 The trace build says what it did: ⌃⌘T or QM ▸ Write Trace, a toast, latest.jsonl
  • 94d5c1e 2026-08-22 Launch recipes are the bare binary: the picker chooses the vault
  • b81cd77 2026-08-22 The trace key is ⌃⇧⌘R: ⌃⌘T sat too close to the daily note's chord
  • c6ee6f4 2026-08-22 A steered drag moves its head before asking what it left: the 2026-08-22 flicker
  • bafb7ae 2026-08-22 A paragraph line's leading whitespace is hidden like a marker, and the first ink is what a parity test measures
  • 22058ce 2026-08-22 Leading spaces are an indent, on every row, in the file and on the screen (ADR 0026)
  • 61d16ce 2026-08-23 The editor draws its spaces; the renderer indents in glyph space; Enter gobbles (ADR 0026, refined)
  • 34407bf 2026-08-23 Option-arrows cross lines: the previous word's start, the next word's end (task 1150)
  • 6bdcb59 2026-08-23 Ctrl-D at a line's end deletes the newline, and Ctrl-H at its start joins upward (task 1160)
  • 749a13a 2026-08-23 A heading keeps its colour while it is edited, and inside a selection (task 1170)
  • b175ef9 2026-08-24 A heading inside a selection stays rendered: the input draws a line at a size of its own (task 1180)
  • 8787cb0 2026-08-24 Cmd-Right stops at the row's end, drawn on the row it was on (task 1190)
  • ffbf5aa 2026-08-24 Cmd-Left stops before the row's first non-space: the ink, not the margin (task 1200)
  • 910e56d 2026-08-24 The disk image is filled through a mountpoint of its own, not /Volumes/QM
  • 8cc65f0 2026-08-24 Spotlight also answers to "Quick Memory" and "Quick Markdown"
  • adf49ff 2026-08-26 Cmd+F finds in the open note: a docked bar, the selection as the mark (ADR 0027)
  • 4a222fe 2026-08-26 Shift+Enter is Enter: the same new line, list or not
  • e3a923a 2026-08-26 A table is a grid you read and source you edit (ADR 0028), the diagnostics dump, and the principles page
  • 21d2bc2 2026-08-26 A `-` under a line never grows it: setext headings are removed (ADR 0029)
  • 615a98b 2026-08-26 A checkbox is a capital letter: on the baseline, cap height tall (ADR 0030)
  • e1a9230 2026-08-26 A pasted table is a table (ADR 0031), and Edit gains its four table verbs
  • 826ed46 2026-08-26 Fixture replays now hold the round trip, and their bytes hold still
  • d445afe 2026-08-27 The privacy posture (ADR 0032): Obsidian waits for Quick Add, the captures wait for Debug mode, and no-network is a test
  • 5996a8b 2026-08-27 A drag takes a table the way the caret does: whole — the window's edges are edit units
2026-08-22 17:07 f894903 17 ms median of 57 · 16 ms–42 ms busy load (0.46 per core)

Rows off the screen are now counted, not shaped (ADR 0025): select-all on Moby Dick is 9.9 ms, down from 583 the day before — the whole-note handover counts each line's rows from its length and shapes only the screen around the caret, and the footprint after the press falls from 208 MB to 148. select-down is 13 → 4.6 ms for the same reason. The launch, build and switch figures are also lower, but that is the machine: this run was quiet (0.50 load/core) where 40d2a4c was contended (3.2), so read those as the baseline being slow, not this build being fast. The two select figures are the change. Cold-cache figures skipped this run (no sudo).

1 commit that could move a figure, since 40d2a4c
  • f894903 2026-08-22 Rows off the screen are counted, not shaped: ⌘A on Moby Dick 583 → 12 ms
2026-08-22 14:50 40d2a4c 17 ms median of 57 · 16 ms–60 ms busy load (0.59 per core)

The editor now wraps by shape_text, the rendered row's own function (ADR 0023), so a paragraph no longer re-wraps when the caret arrives on it; exactness shapes every line on ⌘A, and select-all on Moby Dick shows it at 583 ms against 22 (task 1120 holds the numbers). Also since 84b81dd: QM claims .md files and opens a note that arrives from the Finder (ADR 0021), the status bar says when no vault is open, qm-pixels reads a screenshot's blocks (ADR 0022) and now its rows, and the build stamp and qaqm report build time. The run was contended — load 3.2 across 8 cores — so the launch and test-suite figures are floors, not a best case.

8 commits that could move a figure, since 84b81dd
  • 463c1b5 2026-08-22 A list straight under a paragraph keeps its first item's column under a selection
  • ac585df 2026-08-22 qm-pixels reads a screenshot's █ blocks, and ADR 0022 says what the suite can and cannot see
  • 04b2e3f 2026-08-22 The build stamp watches only what is in the binary, and ./test_app prunes like every other builder
  • f5e25c5 2026-08-22 Review of 1100: the shrink across the shared byte is coverage, not a witness, and two numbers corrected
  • 5d96e19 2026-08-22 QM claims .md files, and a note that arrives from outside opens where it belongs
  • 0f23717 2026-08-22 The status bar says when no vault is open, and a sheet anyone can test task 1090 with
  • 43983d4 2026-08-22 A note with no vault open shows its full path in the status bar
  • 40d2a4c 2026-08-22 The editor wraps where the rendered text wraps: one shaping, not two measurements
2026-08-22 09:10 84b81dd 17 ms median of 57 · 16 ms–25 ms light load (0.32 per core)

Three commits since 1f80dd3, none on a measured path: About answers from every screen, the picker included (30f183a); the binary stamps whether its inputs had uncommitted edits and answers `qm --build-info`, for the new ./qaqm (97b584f); build.rs asks git where HEAD lives, so a worktree build stops rerunning the stamp (84b81dd). Measured while other sessions were building in worktrees — the machine's 15-minute load average was 14 — so the uniform ~1.4× across launch, search and both build times, pre_main included, is the machine and not the code; the headline figures were left at 1f80dd3's. tests.rust_ms 18.1 → 11.0 s is the same contention in reverse. 46 of 100 steady; re-measure on a quiet machine.

3 commits that could move a figure, since 1f80dd3
  • 30f183a 2026-08-21 QM ▸ About answers from every screen: the picker, and over the Settings or stray-file panel
  • 97b584f 2026-08-22 qaqm ▸ what is built, from which commit, with edits or without, and whether a rebuild would change it
  • 84b81dd 2026-08-22 build.rs ▸ asks git where HEAD lives, so a worktree build stops rerunning the stamp every time
2026-08-21 21:27 1f80dd3 17 ms median of 57 · 16 ms–25 ms busy load (0.45 per core)

Three commits since fbca877 (the shipped 8ee1007 is fbca877 plus its benchmark run): a selection that retreats releases the lines it left (f57cac9); a whole-text re-wrap of the editor starts from scratch, so a collapsed window keeps no stale rows — the 'extra spacing' regression the column patch exposed (44efa7f); every frame the document draws is recorded in test builds, with Enter above a list checked frame by frame (1f80dd3). Contended again (0.44/core) despite a two-minute wait: another build was running on the machine. 94 of 100 steady. tests.rust_ms 12.4 → 18.1 s is real, not noise — the new editor-box test walks thirty-two selection gestures (~5 s) and two frame tests joined the suite. select_all_moby's footprint 137 → 168 MB is the same two-state figure select_rows showed this morning.

3 commits that could move a figure, since fbca877
  • f57cac9 2026-08-21 A selection that retreats releases the lines it left, by keyboard and by mouse
  • 44efa7f 2026-08-21 A whole-text re-wrap of the editor starts from scratch, so a collapsed window keeps no stale rows
  • 1f80dd3 2026-08-21 Every frame the document draws is recorded in test builds, and Enter above a list is checked frame by frame
2026-08-21 15:31 fbca877 17 ms median of 57 · 16 ms–18 ms light load (0.32 per core)

Three commits since 1904806: Copy Path copies one shell word (aece833); a picture's corner drags it to a new size, the width written into the link as Obsidian's pipe (f3686b0); a selection across list items leaves every item where it was — the vendored Input takes a column per line, qm hides the markers in a grown window and overlays the chrome (fbca877). A quiet machine this time (0.17/core): the eleven launch-phase figures last night's contended run flagged are all back to 0.5–0.8× of it, which clears them as contention, not code. The one REGRESSED pair, select_rows_moby footprint 129 → 227 MB, is that figure's two states showing again (227 MB in b3ad404, 129 in 1904806, 227 here); a note of prose exercises none of today's list or picture code.

3 commits that could move a figure, since 1904806
  • aece833 2026-08-21 Copy Path copies one shell word: spaces and the shell's specials backslashed
  • f3686b0 2026-08-21 A picture's corner drags it to a new size, written into the link as Obsidian's pipe
  • fbca877 2026-08-21 A selection across list items leaves every item where it was, to the pixel
2026-08-20 23:07 1904806 17 ms median of 57 · 16 ms–18 ms busy load (0.44 per core)

Sixteen commits from several sessions. Product: a note opens off the drawing thread and lands whole (ADR 0018); quick open stops re-sorting the vault on every keystroke (1010 §2); hidden decoration ranges snap to character boundaries (1010 §3); the [[ popup and Enter-in-a-list fixes; and six picker/editor tickets — a What-is-a-vault card, Copy Diagnostics on every screen, the wordmark in Helvetica Neue, a guarded-folder explanation for an iCloud vault, and a per-line hanging indent in the vendored Input, whose wrapper now asks line_chrome for each line on a re-wrap. Harness: the pinned corpus (1050), the first-press gesture benchmark (1040), batched quick-open timing, and the build-cache prune in every builder. Measured contended — load 3.1 across 8 cores after the 180 s wait, with other sessions building — so the 1.5–1.7× launch-phase and first-press figures are the machine, not a change; re-run quiet to clear them.

16 commits that could move a figure, since b3ad404
  • b25f4ee 2026-08-20 The [[ popup never offers the note it is typed in
  • 0807705 2026-08-20 Enter in front of an item's text opens an item, not a blank line
  • 9086bff 2026-08-20 A benchmark for the one gesture the app is about (task 1040)
  • 0c7ca69 2026-08-20 Load then commit: a note's name and its text move together (ADR 0018)
  • febb754 2026-08-20 Open a note off the drawing thread, and land it whole (ADR 0018 step 4)
  • 1ad8edd 2026-08-20 Quick open stops re-sorting the whole vault on every keystroke (1010 §2)
  • 5f42197 2026-08-20 Hidden decoration ranges snap to character boundaries (1010 §3)
  • 1125002 2026-08-20 Pin the benchmark corpus in the bucket, and record which one a run measured (1050)
  • dcba9e2 2026-08-20 Time quick open in batches, because a tenth of a millisecond is not measurable
  • 5f6fbd9 2026-08-20 Every builder prunes the build cache, not only ./deploy
  • b3c779f 2026-08-20 The picker says what a vault is, for the reader who has not met the word
  • dd9a901 2026-08-20 The picker's Remove control is drawn in the name's colour, not the background's
  • 49e6cbb 2026-08-20 Copy Diagnostics answers on every screen, not only the note's
  • 648dab2 2026-08-20 The picker's wordmark is set in Helvetica Neue, whole Q and no version
  • b138cd6 2026-08-20 A vault macOS will not let qm read is explained, with the way out
  • 1904806 2026-08-20 A list keeps its hanging indent under a selection that crosses items
2026-08-20 19:31 b3ad404 17 ms median of 57 · 16 ms–26 ms busy load (0.45 per core)

Forward delete at a line's end joins the line below (b3ad404), through edit_document — one editor change, nothing on the launch, search or quick-open paths.

1 commit that could move a figure, since dcba9e2
  • b3ad404 2026-08-20 Forward delete at a line's end joins the line below
2026-08-20 19:18 dcba9e2 17 ms median of 57 · 16 ms–33 ms busy load (0.36 per core)

Quick open is timed in batches (dcba9e2): the per-keystroke figure was a tenth of a millisecond, under what the clock can tell apart, so the harness times a run of keystrokes and divides. A measurement change, not a product change.

1 commit that could move a figure, since 1125002
  • dcba9e2 2026-08-20 Time quick open in batches, because a tenth of a millisecond is not measurable
2026-08-20 19:03 1125002 17 ms median of 57 · 16 ms–17 ms light load (0.32 per core)

Nothing — the third of three serial runs of the same commit. Together they showed quick_open_vault2k moving 2.9x on an unchanged binary while every app figure held to 1.05x: the matcher answers in tenths of a millisecond and was being timed one match at a time, so the figure was reading the scheduler. Fixed in the next commit by batching; these three are the evidence.

Nothing that could move a figure changed since 1125002 — docs and site only.

2026-08-20 18:52 1125002 17 ms median of 57 · 16 ms–26 ms busy load (0.36 per core)

Nothing — the second of three serial runs of the same commit, to see what the suite's own repeatability is now the corpus is pinned. The app figures agree to 1.00-1.05x; quick_open does not, which is what sent me back to the harness.

Nothing that could move a figure changed since 1125002 — docs and site only.

2026-08-20 18:42 1125002 17 ms median of 57 · 16 ms–18 ms light load (0.33 per core)

Quick open stopped re-sorting the whole vault per keystroke (1010 §2); hidden decoration ranges snap to char boundaries (1010 §3); the 2,000-note corpus became a pinned artifact fetched from Tigris and verified by digest (1050). First run to record which corpus it measured. First of three serial runs of this commit.

3 commits that could move a figure, since febb754
  • 1ad8edd 2026-08-20 Quick open stops re-sorting the whole vault on every keystroke (1010 §2)
  • 5f42197 2026-08-20 Hidden decoration ranges snap to character boundaries (1010 §3)
  • 1125002 2026-08-20 Pin the benchmark corpus in the bucket, and record which one a run measured (1050)
2026-08-20 15:01 febb754 17 ms median of 57 · 16 ms–18 ms light load (0.34 per core)

ADR 0018 step 4: the note read and its block map moved off the drawing thread (closes task 1010 §1). The steady switch cost did not move — 16.69 and 16.66 ms, still one frame — so the note arrives in the very next painted frame even though the read now crosses to another thread and back. first_switch_ms moved both ways again (+18.9% moby, -12.1% vault2k); that figure is one sample per run with a ~10 ms spread and is not readable at three repeats. Launch stays synchronous by design. No cold figures: no TTY for sudo.

1 commit that could move a figure, since 0c7ca69
  • febb754 2026-08-20 Open a note off the drawing thread, and land it whole (ADR 0018 step 4)
2026-08-20 08:42 0c7ca69 17 ms median of 57 · 16 ms–18 ms busy load (0.37 per core)

ADR 0018 steps 1-3: a note's name and its text became one value, and opening one became load-then-commit. A note qm cannot read no longer opens as an empty one. Refactor only — the read is still synchronous — and the steady switch cost is unchanged at 16.7 ms on both corpora. No cold figures: no TTY for sudo.

1 commit that could move a figure, since 9086bff
  • 0c7ca69 2026-08-20 Load then commit: a note's name and its text move together (ADR 0018)
2026-08-20 08:06 9086bff 17 ms median of 57 · 16 ms–18 ms busy load (0.58 per core)

Added the note-switch benchmark itself (QM_BENCH=switch), so the gesture the app exists to get right finally has a figure. No app change: this is the baseline the ADR 0018 work is measured against. switch_moby and switch_vault2k both land on 16.7 ms — one frame at 60 Hz.

6 commits that could move a figure, since 7331c7c
  • 72728ce 2026-08-20 Enforce the vault boundary on every read and write (ADR 0017)
  • b85bdc8 2026-08-19 A selected item gives up its text and never its marker
  • 95ce6c2 2026-08-20 One panel at a time over the document, and ⌘P's field says what it searches
  • b25f4ee 2026-08-20 The [[ popup never offers the note it is typed in
  • 0807705 2026-08-20 Enter in front of an item's text opens an item, not a blank line
  • 9086bff 2026-08-20 A benchmark for the one gesture the app is about (task 1040)
2026-08-19 22:48 7331c7c 17 ms median of 57 · 16 ms–18 ms busy load (0.36 per core)

Six editor and shell commits, none on the launch path: list selections stop at an item's text so type/paste/delete keep the marker, triple-click selects the line, the active-line highlight skips blank rows, eleven light/dark themes behind a new Settings screen, and the shortcut help sheet. Run measured contended (load 1.69/core) — the uniform launch/search slowdowns are load, to be cleared by a quiet re-run.

6 commits that could move a figure, since b6cd7c3
  • b4fee75 2026-08-19 Every shortcut on one sheet, with a Help menu that knows the way there
  • 0b6215b 2026-08-19 Light, Dark, or the Mac's choice — a Settings screen where ⌘, expects one
  • ee1cab7 2026-08-19 Eleven themes, each with both halves, and the slate still first
  • 86f8d04 2026-08-19 The active-line highlight stays off blank rows the caret typed past
  • 429d112 2026-08-19 Triple-click selects the line, the way three clicks always have on a Mac
  • 7331c7c 2026-08-19 A selected item gives up its text and never its marker
2026-08-19 09:22 b6cd7c3 17 ms median of 57 · 16 ms–18 ms light load (0.32 per core)

One commit on a figure path, and it is the measuring apparatus, not the app: the footprint reader no longer throws a memory reading away when the kernel's lifetime maximum lags the present by a page (f91ef2a), so the select-all memory figure has three samples again — 130, 130 and 147 MB, median 130 — where the previous runs had one each; the 215 MB of the run before was that single sample, not a change in the app. The shipped binary and bundle are identical to the byte to the 09:00 run's; the download figure still moved, 6.68 to 6.97 MB, which is the disk image's own wobble on identical contents (task 0950) and not the app. Everything else steady.

1 commit that could move a figure, since dd9d92b
  • f91ef2a 2026-08-19 A memory reading is not thrown away because the kernel's peak lags the present
2026-08-19 09:00 dd9d92b 17 ms median of 57 · 16 ms–18 ms light load (0.33 per core)

One app change, and it changes what the empty-vault launch measures: a brand-new vault's welcome note now ends with a picture — Schiele's still life at 480×720, carried in the binary and written into the vault exactly as a paste would write it, under attachments/Pasted image <stamp>.jpg — so launch_empty opens a 17-block, 1.6 kB note with a JPEG to resolve and decode where it opened a 13-block, 1.1 kB one (dd9d92b); its document_bytes, footprint and first-paint figures move for that reason, and the binary is 96 KB larger. Nothing else on a measured path changed. The select-all memory figure, 215 MB against 147 MB, is one sample against one sample: the app had been discarding two of every three footprint readings on that scenario because the kernel's lifetime-maximum lags the present by a page when memory is growing at the instant of the read, and a sanity check mistook the lag for a struct-layout error — found while reading this run, fixed in f91ef2a; from the next run the figure has three samples and a spread.

1 commit that could move a figure, since 2678a58
  • dd9d92b 2026-08-19 The welcome note shows a picture, written as a paste would write it
2026-08-19 08:23 2678a58 17 ms median of 57 · 16 ms–18 ms light load (0.34 per core)

No change to the app: the only commit on a figure path is the harness gaining its tests group (2678a58), which adds two figures — the Rust suite and the pixel suite, each timed as the command a developer types when nothing needs compiling — and changes nothing that was already measured; this run is their first point, with no backfill behind it. The binary and bundle are byte-identical to the 08:06 run's (same sizes to the byte), so the one figure that moved, the download at 6.79 MB against 6.57 MB, is the disk image's compression of identical contents, not the app. Quiet machine (0.29 per core); 81 of 82 figures steady against the run 17 minutes earlier.

1 commit that could move a figure, since a45db7f
  • 2678a58 2026-08-19 The two test suites are benchmarks of their own
2026-08-19 08:06 a45db7f 17 ms median of 57 · 16 ms–17 ms busy load (0.41 per core)

One commit on a figure path, and it is not on any measured path: a fourth item on a picture's right-click menu, Show in Finder (6245b07) — a menu action, no change to launch, layout, search or selection. The harness is unchanged; this run exists because the owner's rule measures a free machine after every ticket. So the ~30% drop across nearly every figure against the 05:45 run — launch phases that share no code, search, the time before main() and even the cold build (445 s to 212 s, same compiler, same sources) — is the machine, not the app: the 05:45 run began at a load of 4.3 across 8 cores, minutes after a deploy; this one began at 3.3 and was let settle to 2.7 before measuring. Two runs of the same code 2.5 hours apart are the noise floor of this Mac, and the reason a single point is never a regression.

1 commit that could move a figure, since d1f547f
  • 6245b07 2026-08-19 Show in Finder: the fourth item on a right-clicked picture
2026-08-19 05:45 d1f547f 17 ms median of 57 · 16 ms–18 ms light load (0.26 per core)

One app change: pasting a picture now writes Obsidian's file name (Pasted image YYYYMMDDHHMMSS.png, with a counter on collision) and Obsidian's embed spelling ![[attachments/…]]; to make that safe, wikilink image targets resolve from the vault root rather than against the note, ImageLink records which dialect wrote a target so each resolves by its own rule, and a wikilink target is no longer percent-decoded, while ![](…) is read as before (2bc0c7c). That is the path that resolves and draws a picture in a note — exactly what the new launch_image scenario opens, and launch_image is the only group this run measured: the harness change that added it, and the 13 MB Schiele corpus it opens, were committed straight after as a7d78b6. Every other group in this run is carried over from the f2eda09 run and is not a measurement of this commit's binary.

The harness gained a corpus and a group: a 13 MB, 75-megapixel JPEG staged as a one-note vault with ![[attachments/…]] the way qm's own paste writes it, measured as launch_image (a7d78b6). Three changes alter what a launch figure means. The vault index: resolving one wikilink walked every path in the vault, so building the backlink index cost notes × links; resolution is now a name map decided once per change of vault membership, autosave no longer rebuilds the reverse map, and the read of every note's source moved to a background task whose landing fires backlinks_built — so backlink_index_ms is no longer a step on the way to first paint, and the phases after it no longer include the index (bb3d2e6). An empty vault now opens on a seeded demonstration note — lists, headings, an embed and a wikilink, plus the note it embeds — so launch_empty writes two files and renders a 13-block note where it rendered a one-line welcome (notes 1→2, blocks 2→13, 31→1130 bytes; 0a72c94). And every hard line is now a row: a blank line is an empty row, block structure adds no spacing, a mouse drag grows the editor over the lines it reaches instead of taking the note, and a heading inside a grown window draws at body size — which changes the on-screen height of every note (6f5faef, ADR 0014).
The line under the caret is drawn differently: the vendored Input accepts per-frame decorations and keeps a display rope with a two-way offset map (the identity when nothing is hidden), so the caret's line keeps its bullet, checkbox and bold with only the touched node's syntax revealed; lists share one renderer for rendered and edited rows; headings and lists take the rounded line height (24ecfd7), with a follow-up so gpui's shaper does not merge runs that differ only in font (612d1f5). Wrap and height bugs in the editor: it re-seeds after the column's width changes, deferred past the frame, which also covers the provisional width a launch's first frames lay out at (fd827c4); and its font is settled before its text so the first wrap is right and the old second-frame reflow is gone (ff71dfd). Pictures: undo no longer deletes a pasted file, which removes the reconcile pass after undo/redo and the in-memory copy kept for redo (f0bbfea); an image link whose file is missing is rewritten to a visible marker before the block reaches the markdown renderer (0a6450e); right-click on a picture offers copy, copy path and save (105c2fd). Off the hot paths: an Undo button on the note-deleted toast (a62609f); test-only changes (cd7d5dc, 12a0170).

15 commits that could move a figure, since f2eda09
  • 2bc0c7c 2026-08-17 Paste a picture the way Obsidian does: its name, and its embed
  • a7d78b6 2026-08-17 Measure what a note holding a very large picture costs: 82 MB
  • fd827c4 2026-08-17 A block re-wrapped when the caret arrived, and took the note with it
  • f0bbfea 2026-08-17 qm never deletes a picture
  • 0a6450e 2026-08-17 A picture that is not there says so inline, not only on its own line
  • 0a72c94 2026-08-17 A new vault opens on a note that shows what the editor does
  • a62609f 2026-08-17 The note-deleted toast carries an Undo button
  • 105c2fd 2026-08-17 Right-click a picture: copy it, copy its path, save a copy
  • cd7d5dc 2026-08-17 Pin the clock so the paste-collision test actually collides
  • bb3d2e6 2026-08-17 The vault that never opened: resolution was quadratic in the vault
  • 24ecfd7 2026-08-17 The line the caret is on keeps its bullet, its box and its bold
  • 12a0170 2026-08-18 The owner's caret at offset 52, as a test
  • 612d1f5 2026-08-18 Bold that reached the shaper and was thrown away
  • ff71dfd 2026-08-18 The empty row under the line the caret arrives in
  • 6f5faef 2026-08-19 A blank line is a row (ADR 0014)
2026-08-17 17:22 f2eda09 17 ms median of 57 · 16 ms–18 ms light load (0.32 per core)

Nothing in this interval touches the app, the vendored toolkit, the harness or the bundle: the binary measured here is built from the same sources as the one at 4ad2d68 (both runs report the same binary size). The four commits are bookkeeping around the benchmark itself — recording the 4ad2d68 run (250f1d5), filing task 0860 because the run-to-run comparison had reported a definition change as a speedup (c5bf2ff), archiving finished tasks (2ac93c8), and deleting the eighteen earlier runs measured under superseded definitions (f2eda09). With identical sources on both sides, any difference between these two runs is conditions, not code: the 4ad2d68 run carried a load qualifier and had no cold-cache pass; this one was quiet and the first with cold-cache figures.

Nothing that could move a figure changed since 4ad2d68 — docs and site only.

2026-08-17 07:41 4ad2d68 17 ms median of 57 · 16 ms–18 ms busy load (0.83 per core)

First measurement of this figure.