Files
Marcos Paulo cac6b0ed39
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: registry consumers (grammar, theme, pack), seed reader, wasm plugin host with approval, wanted scan, STIX and Lottie store lookups, plugin auto-disable persistence
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 18:35:47 -03:00

17 KiB

Measurements: where NotesCraft weight comes from (2026-10-21)

Raw numbers and the commands that produced them. Spec: spec.md. Nothing in the repo was changed: the build ran in a scratch copy and every number below can be reproduced with the commands in "Reproduce".

Machine: Apple Silicon, 10 cores, macOS 25.3. The machine was busy (load average 8 to 10 from other agents' builds), so compile wall times and CPU seconds are upper bounds. Binary sizes, section sizes, asset copy counts and RSS plateaus do not depend on load.

1. Release binary

Scratch copy /private/tmp/claude-501/scratch/nc (rsync of the tree without target*, .git, .notescraft, .mm-agents, dist). Pitfall: the pattern target* also drops crates/ui-egui/assets/icons/lucide/target.svg, which icon_catalogue.rs embeds, so the first build failed until the file was copied back. Profile is the repo's: lto = "thin", codegen-units = 1, no strip, no opt-level (so 3), no panic = "abort".

Item Bytes MiB
notescraft release binary (this tree) 40,235,208 38.4
installed /Applications/NotesCraft.app/Contents/MacOS/NotesCraft (older build) 37.9 M by ls 37.9
notescraft-cli release (dist/release) 12.9 M by ls 12.9
DMG (compressed, dist/release) 14.1 M by ls 14.1

Mach-O sections of the binary (size -m):

Section Bytes MiB Share of file
__TEXT,__text (code) 22,962,444 21.9 57 %
__TEXT,__const (read-only data: fonts, tables, packdump) 6,850,888 6.5 17 %
__LINKEDIT (symbol table, string table, fixups) 5,947,392 5.7 15 %
__eh_frame + __unwind_info 1,904,672 + 299,088 2.1 5 %
__gcc_except_tab 837,156 0.8 2 %
__DATA_CONST const 804,816 0.8 2 %
__cstring 273,834 0.3 0.7 %

1.1 Code by crate (cargo bloat --release --crates, .text 21.9 MiB)

Attribution is approximate (cargo-bloat guesses by symbol path; generic code lands in std and the caller).

Group .text Share of .text Notes
all notescraft_* crates 7.9 MiB 36 % ui_egui 4.2, engine 1.8, then each under 0.2
std (generic instantiations) 4.1 MiB 19 %
GPU: naga 0.90, wgpu_core 0.67, wgpu_hal 0.15, wgpu_types 0.05, wgpu 0.04 1.8 MiB 8 % required by eframe/wgpu
egui stack: egui 0.84, epaint 0.16, winit 0.18, egui_wgpu 0.05 1.3 MiB 6 % required
image codecs: image 0.42, image_webp 0.41, zune_jpeg 0.30, tiff 0.19, png 0.05 1.4 MiB 6 % notes embed images; tiff is pulled by arboard (macOS pasteboard)
text shaping: harfrust 0.37, skrifa 0.27, read_fonts 0.17 0.8 MiB 4 % required
serde_core 0.43, serde_json 0.30, toml_edit 0.22, toml 0.07 1.1 MiB 5 %
TLS and HTTP: rustls 0.34, ring 0.14, ureq 0.11, ureq_proto 0.05 0.65 MiB 3 % sync client; grammar fetch still shells out to curl
regex: regex_automata 0.33, regex_syntax 0.16, aho_corasick 0.10, fancy_regex 0.06 0.65 MiB 3 %
SVG: usvg 0.20, tiny_skia 0.14, resvg 0.06 0.45 MiB 2 %
syntect 0.10 MiB 0.4 % the packdump data is in __const, not here
wasmi 0 0 % not in the binary at all (see 1.4)

Workspace crates, .text in KiB, largest first: ui_egui 4300, engine 1840, bases 183, calendar 170, index 139, diagrams 131, pdf 110, knowledge 101, acp 88, vim 86, markdown 86, lint 77, editor 72, lottie 71, whiteboard 57, math 44, blocks 44, git 36, syntax 32, canvas 27, annotate 25, sync 21, backlinks 21, themes 19, templates 17, collab 16, regex 16, graph 11, i18n 7, plugins 1.5.

The "optional-looking" crates (bases, calendar, diagrams, pdf, knowledge, acp, vim, lottie, whiteboard, math, git, canvas, annotate, collab, graph) add up to 1.15 MiB of .text, 5 % of code. Their UI halves live inside ui_egui and their commands inside engine; the source-line share of the view folders that belong to them (whiteboard 5906, calendar 2883, graph 2072, media 3942, canvas 768, diagram 218 of 103,212 lines in ui-egui/src) is about 15 % of ui_egui, or 0.6 MiB. Estimate, not measured: compiling every optional feature out together would save 2.5 to 3.5 MiB (7 to 9 % of the file).

1.2 Embedded data (__const 6.5 MiB)

Counted by searching the binary for whole-file byte runs (python3 -I, bytes.count):

Asset Size Copies in binary Bytes in binary
Inter-Regular.ttf 411,640 2 (engine PDF export + ui-egui) 823,280
Inter-SemiBold.ttf 419,744 2 (same) 839,488
Inter-Medium.ttf 417,300 1 417,300
JetBrainsMono-Regular.ttf (only PDF export) 270,224 1 270,224
JetBrainsMono-NoLigatures.ttf (UI) 269,984 1 269,984
STIXTwoMath-Regular.ttf (math only) 375,596 1 375,596
subtotal NotesCraft fonts 2,995,872 (2.86 MiB)
egui default_fonts (feature of eframe in apps/notescraft/Cargo.toml): Hack-Regular 309,408, Ubuntu-Light 361,676, NotoEmoji-Regular 418,804, emoji-icon-font 324,132 1 each 1,414,020 (1.35 MiB)
all fonts 4,409,892 (4.2 MiB, 11 % of the file, 65 % of __const)
syntect default packdump (default_newlines.packdump 368,467 + metadata 4,976) 1 about 0.37 MiB
Lucide catalogue (278 SVG, ui-egui/assets/icons/lucide) 115,773 1 0.11 MiB
Lottie icons (65 JSON, assets/icons-anim) 147,922 1 0.14 MiB
UI icons (89 SVG, assets/icons) 31,228 1 0.03 MiB
25 embedded grammars (crates/syntax/grammars) 68,844 1 0.07 MiB
en.toml (only locale) 61,122 1 0.06 MiB

The PDF crate's own include_bytes! of the fonts sit inside #[cfg(test)], so they are not in the binary; the duplicate comes from crates/engine/src/builtin/export_commands.rs.

Findings: fonts are the biggest single data cost and larger than all fifteen optional crates together (4.2 MiB against 1.15 MiB). Two copies of Inter Regular and SemiBold waste 0.79 MiB; the egui default set is mostly unused (the app's fonts::install puts Inter first in the proportional chain and JetBrains Mono first in the monospace chain; Hack and Ubuntu-Light are only fallbacks; NotoEmoji matters for emoji). Everything else packable (Lucide, Lottie, grammars, locale, packdump) is about 0.8 MiB in total.

1.3 What stripping and optimisation do (measured, no code change)

Variant Bytes vs baseline
baseline (thin LTO, opt-level 3, symbols kept) 40,235,208 -
strip -x (drop local symbols) 35,767,528 -4.47 MB (-11.1 %)
strip (drop all symbols) 34,851,464 -5.38 MB (-13.4 %)
opt-level = "s" + strip = "symbols" (CARGO_PROFILE_RELEASE_*, own target dir) 32,088,240 -8.15 MB (-20.3 %)

packaging/linux/package.sh already runs strip; the macOS and Windows packaging do not, so the macOS app ships 5.4 MB of symbol table. opt-level = "s" removes another 2.8 MB; its speed cost was measured only for startup (section 2) and must be measured on the index build and a scroll benchmark before adopting.

1.4 The wasm plugin host (wired 2026-10-21, task 12)

Before: wasmi and ns_run_command were absent from the binary (nm | grep -c wasmi = 0) because Session::load_plugins had no caller outside tests. Now Session::apply_app_settings calls load_plugins and the app calls reload_plugin_commands, so the host is linked.

Measured on CARGO_PROFILE_RELEASE_STRIP=none cargo build --release -p notescraft (own target dir): 41.4 MB unstripped, 37.9 MB after strip -x. Symbol sizes from the address gaps of nm -n: wasmi* crates 492,364 bytes (0.47 MiB), notescraft_plugins 130,672 bytes (0.12 MiB), about 0.59 MiB together, plus the engine's bridge code and inlined generics (not attributable by name). That is inside the 0.8 to 1.2 MiB estimate of the spec, lower than expected. No before/after binary pair was built (the tree changes under several agents), so the number is the symbol sum, not a file-size diff.

2. Runtime

Method: measure.py (below) starts the binary with NOTESCRAFT_CONFIG_DIR pointing at a fresh scratch dir (the owner's settings are untouched) and a vault argument, samples ps -o rss=,time=,%cpu= every 0.1 s for 3 s then every 1 s, then kills it. vmmap -summary and footprint give the memory breakdown. Synthetic vaults in /private/tmp/claude-501/scratch/vaults: small 50 notes, big 6500 notes in 60 folders, 25 MB, 5 wikilinks, 3 tags and frontmatter each, a Rust fence in every tenth note.

Run RSS at 0.3 to 0.6 s RSS plateau CPU time to idle Notes
installed app, small 120 MB at 0.31 s, 131.6 at 0.62 s 132.0 MB 0.29 s (0.47 s after 20 s) idle CPU 0.0 to 0.1 %
new release, small (2 runs) 131 MB 131.0 and 131.2 MB 0.30 s
opt-level=s + strip, small 131 MB 131.2 MB similar no visible startup difference
new release, big, no flag 125.6 MB 125.7 MB 0.9 s over 60 s shows the large-folder warning (LargeFolderGate), vault not loaded
new release, big, --large-ok, first open (no index cache) 156.8 MB at 0.66 s 221.7 to 230 MB after 17 s 3.4 s at 5 s, 4.4 s at 70 s peak 494.3 MB at about 5 s (index build), then falls
new release, big, --large-ok, later open (cache in the vault) not sampled 378.6 MB 1.9 s
opt-level=s, big, --large-ok, later open not sampled 380.8 MB 2.1 s within noise of the line above

vmmap -summary of the release build on small after 8 s: physical footprint 72.4 MB (peak 189.6 MB during startup); MALLOC_SMALL 23.1 MB resident, IOSurface 11.5 MB (GPU surfaces), __DATA_CONST 17.1 MB resident, __DATA 7.7 MB, __TEXT 314 MB resident of 1.2 GB mapped (system frameworks from the shared cache, not the app's 33 MB). RSS (131 MB) is larger than the footprint because it counts shared system library pages.

Readings:

  • Cold start is already fast: about 0.3 s of CPU and a window at roughly 0.6 s (sampling resolution 0.3 s early on; time to first frame was not instrumented). A 21.9 MiB .text is mapped, not read, and only touched pages count.
  • The app's own binary is a small part of RSS (clean, evictable file pages). Removing 5 MB of symbols changes nothing at runtime (__LINKEDIT is not touched); removing 1 MiB of optional code changes RSS by well under 1 MB.
  • What is heavy is vault data: a 6500-note vault peaks at 494 MB while indexing and settles at 220 to 380 MB. That is index memory, unrelated to plugins and the registry. If "heavy" is the owner's complaint, an index-memory spec is worth more than this one (see spec, open question 9).
  • Eager work at startup that a lazy load could remove: fonts::install reads SFHebrew.ttf (64,604 B) and SFArabic.ttf (724,608 B) from disk on every launch into the heap; the script-on-demand loader already exists for the other scripts. Saves about 0.8 MB RSS and one disk read. Small.

2.1 Grammar set build (first fence in a note)

Example run in the scratch copy (release, notescraft-syntax), several runs each:

Step Time
SyntaxSet::load_defaults_newlines() 0.3 to 0.6 ms (lazy)
into_builder() (deserialise all 75 default syntaxes) 17.4 ms
build() with defaults only 60.3 ms
parse and add the 25 embedded grammars 30.3 to 31.8 ms
build() with defaults + embedded 62.6 to 66.2 ms
load::current() total 108 to 113 ms

It runs on first use inside the highlight cache worker, so it does not block the UI thread. The 25 NotesCraft grammars cost about 35 ms of the 110 ms (parse 31 ms plus 3 to 6 ms link); the other 75 ms belong to syntect's default pack. load::set_installed repeats the whole build for every install or removal. Moving the 25 grammars out removes 69 KB of binary and about 35 ms of the first highlight; it does not touch the 75 ms.

3. Compile-time weight

cargo build --release -p notescraft --timings (375 units; the first build aborted at ui-egui on the missing target.svg, the second finished; CPU seconds are the sum of per-unit durations from the two timing reports):

Metric Value
total unit CPU 1,151 s
workspace + craft-libs crates 392 s (34 %)
external crates 759 s (66 %)
ui-egui + notescraft bin after the dependencies were built (wall, loaded machine) 2 m 41 s
full opt-level=s + strip build, wall (loaded machine) 2 m 01 s
unique packages in cargo tree -p notescraft -e normal 386 (47 workspace or craft)
crates present in more than one version (cargo tree -d) 18

Top units: notescraft-ui-egui 101 s, notescraft-engine 98 s, notescraft bin 59 s, naga 42.5, wgpu-core 41.6, wasmi 37.1 (0 bytes shipped), read-fonts 35.0, regex-automata 33.4, vello_cpu 31.4, objc2-metal 30.0, objc2-foundation 29.2, skrifa 28.9, harfrust 28.2, ring build script 28.0, egui 21.9, egui_extras 21.4, zune-jpeg 21.0, image 19.9. The only compile cost a feature could remove without touching the UI is wasmi (3 %).

4. Ranking by measured cost (what a user could save)

Rank Item Measured or estimated saving Needs the registry?
1 strip symbols on macOS and Windows 5.38 MB (13.4 %) measured no, release profile
2 opt-level = "s" (after a perf check) 2.8 MB more, measured no
3 fonts: remove the duplicate Inter pair 0.79 MiB, measured copy count no
4 fonts: stop shipping Hack, Ubuntu-Light, emoji-icon-font through default_fonts (keep NotoEmoji) about 0.95 MiB; needs a visual check for tofu no
5 PDF export reuses JetBrainsMono-NoLigatures 0.26 MiB no
6 STIX Two Math as a font pack (math only) 0.36 MiB; math then needs a pack or a seed yes
7 Lottie icons, Lucide catalogue, 25 grammars 0.14 + 0.11 + 0.07 MiB; about 35 ms off the first highlight yes, each tiny
8 compile out every optional feature 2.5 to 3.5 MiB estimated; large refactor no (cargo features); the registry can only host the assets
9 lazy system fallback fonts 0.8 MB RSS no

Items 1 to 5 total about 10 MB (25 %) and need no registry. The registry-only items 6 and 7 are about 0.7 MiB (2 %).

Reproduce

export PATH="$HOME/.cargo/bin:$PATH"
S=/private/tmp/claude-501/scratch/nc
rsync -a --exclude 'target*' --exclude .git --exclude .notescraft --exclude .mm-agents --exclude dist ./ $S/
cp crates/ui-egui/assets/icons/lucide/target.svg $S/crates/ui-egui/assets/icons/lucide/   # the 'target*' exclude dropped it
cd $S && export CARGO_TARGET_DIR=/Volumes/Storage/Projects/notescraft/target/agent-bloat
cargo build --release -p notescraft --timings
cargo install cargo-bloat --locked --root /private/tmp/claude-501/scratch/tools          # not touching ~/.cargo
/private/tmp/claude-501/scratch/tools/bin/cargo-bloat bloat --release -p notescraft --crates -n 0
size -m $CARGO_TARGET_DIR/release/notescraft
cp $CARGO_TARGET_DIR/release/notescraft nc-strip && strip nc-strip && ls -l nc-strip
CARGO_PROFILE_RELEASE_OPT_LEVEL=s CARGO_PROFILE_RELEASE_STRIP=symbols \
  CARGO_TARGET_DIR=/Volumes/Storage/Projects/notescraft/target/agent-bloat-s cargo build --release -p notescraft
cargo tree -p notescraft --offline -e normal --prefix none | sort -u | wc -l     # 386
cargo tree -p notescraft --offline -e normal -d | grep -E '^[a-z_-]+ v' | sort -u | wc -l   # 18
# font copies: python3 -I, B = open(binary,'rb').read(); B.count(open(font,'rb').read())
# runtime: python3 -I measure.py <binary> <vault> <seconds> <label> [--large-ok]
#   Popen([bin, vault]), env NOTESCRAFT_CONFIG_DIR=<fresh dir>, ps -o rss=,time=,%cpu= -p PID
# memory: vmmap -summary PID ; footprint PID
# grammar timing: an example in the scratch copy calling notescraft_syntax::load::current() and syntect directly
# wasm host absent: python3 -I -c "B=open(bin,'rb').read(); print(B.count(b'wasmi'), B.count(b'ns_run_command'))"

Scratch artefacts (not in the repo): /private/tmp/claude-501/scratch/{nc,tools,vaults,measure.py,samples_*.txt,crates_bloat.txt}; build outputs in target/agent-bloat and target/agent-bloat-s (safe to delete, about 2 GB).

1.3 opt-level = "s" perf check (lead, 2026-10-21)

Adopted together with strip = "symbols" in the root [profile.release] (owner-approved). Installed binary: NotesCraft 30.27 MB (was 39.75 MB, -9.5 MB, -24 %), notescraft-cli 12.2 MB, DMG 18.9 MB (was about 23 MB), idle RSS 104 MB after 15 s on the installed app.

Checked: cargo test --release -p notescraft-ui-egui --test tab_switch_freeze under the new profile passes (no frame over 250 ms while opening, switching and closing 8 tabs of a 2000-note generated vault; whole test 5.6 s). NOT checked, so the 10 % rule of task 0.4 is only partly evidenced: index-build CPU time of the 6500-note vault, perf_code_blocks, the graph frame time. Run those before calling the profile final; revert opt-level to the default if any is more than 10 % slower.