Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,23 @@ permissions:

jobs:
cargo-deny:
name: Cargo advisories and licenses
name: Cargo advisories
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v7

# Reads `Cargo.lock` and the advisory database; it compiles nothing, which is
# why this job needs no Rust toolchain and no cargo cache.
#
# ⚠️ `advisories` alone, and it is the repository's only advisory net — Dependabot
# alerts are off. `licenses`, `bans` and `sources` stood here and never made a
# decision in 25 runs; the GPL-3.0 compatibility of the tree is a release-time
# review, not a per-pull-request gate.
- uses: EmbarkStudios/cargo-deny-action@v2
with:
manifest-path: src-tauri/Cargo.toml
# `licenses` is not decoration: DevBox ships under GPL-3.0-only, and a
# dependency that is not compatible with it cannot be distributed.
command: check advisories bans licenses sources
command: check advisories

npm-audit:
name: npm advisories
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Run all commands from the repo root (`package.json` there wraps both Angular and

- `npm run e2e:build` then `npm run test:e2e` — the end-to-end suite (WebdriverIO + `tauri-driver`), twelve scenarios under `e2e/specs/` against the **assembled** application. The build step is not optional after a change to `src/` or `src-tauri/`: the suite drives a binary with the front end compiled into it.

- `cargo bench` from `src-tauri/` — criterion, against a **file-backed** database of 8000 notes of ~13 kB (~104 MB, seeded once per group), below the command boundary. Deliberately **not in CI**, and not in `cargo test` either — cargo gives a `[[bench]]` target `test = false`, so the suite never builds it. `--save-baseline main` then `--baseline main` is the comparison the harness exists for, but that baseline lands in the gitignored `target/`: it is local to one machine and dies with `cargo clean`, so the durable record is the table in `docs/architecture.md`. ⚠️ `autobenches = false` and `bench = false` on the lib and both bins are load-bearing, and `Corpus` must not implement `Drop` itself — all three are explained in `docs/architecture.md` rather than in `Cargo.toml`. The baseline: `query_notes` costs **403 ms on 8000 notes**, which is past its own 150 ms debounce; the search still is not what costs; and `list_tags` is the only command that degrades faster than the corpus (×64 for ×10 the notes).

- `cargo clippy --all-targets -- -D warnings` and `cargo fmt --check` from `src-tauri/` — `Cargo.toml` forbids `unsafe_code`, denies `clippy::all` and warns on `clippy::pedantic`, `rust_2018_idioms` and `unreachable_pub`. The toolchain is pinned in `rust-toolchain.toml`, so a new stable release can't turn CI red on an untouched commit.

## Things that will bite you
Expand Down
77 changes: 77 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2010,6 +2010,83 @@ release that replayed it would pay for it twice.

## Testing

### Benchmarks

`cargo bench` from `src-tauri/`. They are **not in CI**: a timing assertion on a shared
runner flaps, and a suite that flaps is a suite everyone learns to ignore. They run
locally, on demand.

```bash
cargo bench -- --save-baseline main # before
cargo bench -- --baseline main # after
```

That comparison is the whole reason the harness is **criterion** and not divan, which is
lighter and pleasanter but does not offer it out of the box. ⚠️ The baseline it writes lives
in `src-tauri/target/criterion/`, which is gitignored: it is local to one machine and dies
with `cargo clean`. The table below is the durable record.

⚠️ **They run below the command boundary, not through Tauri.** A command is four lines —
validate, lock, delegate, translate the error — so `store::*` plus `view::*` plus the serde
round-trip captures nearly all of the cost. `tauri::test::mock_app` would drag the whole app
lifecycle in and buy only the IPC transport, which this codebase does not control. So these
numbers are **not** "the IPC is fast": they are what the work behind a command costs.
Serialisation is included on purpose — a `NotesView` over 8000 notes is a real `serde_json`
cost paid on every keystroke.

⚠️ **The corpus is file-backed, never `open_in_memory`.** An in-memory database has no pager
behind a file, no page cache doing real work and no I/O at all — it measures something the
application never does. `benches/corpus.rs` writes 8000 notes of ~13 kB into a temporary file
database — about 104 MB, seeded once per group — and its bodies are accented on purpose: a
pure-ASCII corpus would exercise only `fold`'s fast path.

⚠️ **`Corpus` does not implement `Drop`; its `TempDir` field does, and is declared last.**
Fields drop in declaration order but a `Drop` on the struct runs before all of them, so the
erasure fired while SQLite still held the file open — which Windows refuses to delete over,
and `let _ =` swallowed the error. Six groups leaked a corpus each, 600 MB a run.

Two Cargo details exist solely to make this work, and they are documented here rather than in
`Cargo.toml`: `autobenches = false` (or the shared corpus module is discovered as a benchmark
of its own and reported as entirely unused) and `bench = false` on the lib and both bins (or
cargo runs their built-in harness first, which rejects criterion's own flags).

#### The baseline

8000 notes, Windows, release profile with `lto = true`. The last column is the same benchmark
against the 800-note corpus this suite started on, which is what says whether a cost is linear
in the corpus or worse.

| Command | Cost | 800 → 8000 |
| ---------------------------------------------- | --------------- | ------------- |
| `query_notes`, search matching nothing | 406 ms | ×15.1 |
| `query_notes`, unfiltered | **403 ms** | ×14.7 |
| `query_notes`, search folding accents | 392 ms | ×15.2 |
| `export_notes` / `import_notes` | 377 ms / 98 ms | ×14.1 / ×11.8 |
| `list_tags` | 37.9 ms | **×64** |
| `delete_notes` then `restore_notes`, 100 notes | 10.4 ms | ×1.2 |
| `rename_tag` across the corpus | 8.2 ms | ×2.2 |
| `move_notes` / `tag_notes`, 100 notes | 5.1 ms / 4.2 ms | ×1.9 / ×1.8 |
| `list_trash` | 4.5 ms | ×10.5 |
| `update_note` | 1.5 ms | ×1.0 |
| `list_global_placeholders` | 1.5 µs | ×1.0 |

Three things worth reading off that table.

**`query_notes` has gone past the debounce.** It runs on every keystroke behind a 150 ms
debounce, and at 800 notes its 27 ms sat comfortably inside it. At 8000 it costs 403 ms: the
query fired for one keystroke is still running when the third one after it arrives. That is
what #21 is about, and this is the number that says the problem has stopped being theoretical.

**The search is still not what costs.** Folding accents is the _cheapest_ of the three
variants (392 ms against 403 ms unfiltered) — fewer notes survive to be serialised. Fetching
8000 × 13 kB out of SQLite and turning the view into JSON is the whole cost, and work aimed at
making the match faster would be aimed at the wrong half.

**`list_tags` is the one that degrades faster than the corpus.** ×64 for ×10 the data, the only
entry on the table that is markedly super-linear, and invisible at 800 notes where it cost
591 µs. It joins `note_tags` to `notes` to read one nullable column, so each of the 16 000 tag
rows dereferences a ~13 kB note row; the notes table was 10 MB at 800 notes and is ~104 MB at 8000. ⚠️ That is the likely cause and it is **not confirmed** — no query plan was taken.

Unit tests run with Vitest through the `@angular/build:unit-test` builder in a jsdom
environment (configured in `angular.json`'s `test` target and `vitest-base.config.ts`), so
no browser is needed. Specs sit next to the file they cover. Coverage thresholds are set at
Expand Down
156 changes: 152 additions & 4 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading