Skip to content

Hardening candidates: v2.31.0 #262

Description

@jeiel85

Candidates surfaced by what shipped in v2.31.0 — the sidecar metadata mode (#216) and the relative-time localisation. Filed at tag time as a tracker; v2.31.0 ships as it is.

Sidecar mode — cost and scale

  • The index is rewritten in full on every note save. writeNoteSidecar reads this device's entries, updates one, and writes the whole file back. On a folder of a thousand notes that is a ~150 KB read plus a ~150 KB write per autosave — once a second while typing — and a sync client watching the folder sees the index change each time. SidecarStore already skips a write whose bytes are unchanged, which only helps when nothing moved. An in-memory dirty set flushed on a coarser trigger would fix it; the honest reason it is not done yet is that the flush points need thinking about, since an unflushed entry is the Search results show same notes multiple times #140 duplicate again. Impact: sync traffic and battery on large folders — the setups this mode is aimed at.
    • Half of this is done in perf(sync): stop re-reading the sidecar index on every note save (#262) #266: this device's own index is now held in memory, so the read is gone and a lost-update window closed with it. The write is unchanged — SidecarStore.write still encodes every entry and replaces the whole file, so the ~150 KB write per autosave and the sync client seeing the index touched each time both stand. The dirty-set flush is what remains.
  • Every sidecar write and read lists the whole folder. Both writeNoteSidecar and SidecarStore.readAll call listFiles() on the SAF tree, which is the expensive call in this API. The frontmatter path grew MirrorFileCache for exactly this reason (Hardening candidates: v2.28.2 #222); the sidecar path has no equivalent. Impact: save latency proportional to folder size.
    • Reduced in perf(sync): stop re-reading the sidecar index on every note save (#262) #266, not closed. A save used to list the folder three times and parse every index twice; it now lists once in writeNoteSidecar (plus findFile inside SidecarStore.write, which walks the tree too). The merged view is consulted only for a note this device has not written before. Still proportional to folder size, just with a smaller constant — the MirrorFileCache equivalent does not exist.
  • A note deleted on another device leaves its entry in that device's index for ever. Nothing prunes entries whose note is gone, and deleteNote only touches our own. Two devices will each carry the other's stale rows. Harmless today — a stale entry is only consulted by filename, and a missing file matches nothing — but the file grows monotonically. Impact: index size drifts up over a folder's lifetime.

Sidecar mode — correctness edges

  • Adoption compares filenames case-insensitively, but a case-sensitive filesystem can hold both. adoptUnclaimedFile and the recorded-name lookup both use equals(ignoreCase = true), which is right for exFAT and a Windows share and wrong for ext4, where Notes.md and notes.md are two files. The frontmatter path made the same choice deliberately (Notes get saved several times #213) and inherits the same edge. Impact: on a Linux-backed sync, a note could adopt the wrong file of a case-differing pair.
  • A conflict copy records the remote hash under the original note's entry. That is what stops the copy being taken again every pass — the sidecar equivalent of remoteSeenAt — but it means the entry briefly describes content the local note does not have. Correct, and documented, yet it makes contentHash mean "last wrote or accepted" rather than "last wrote". Worth a second look if anything else ever reads that field. Impact: none observed; a trap for future readers.
  • Switching modes is not resumable. SidecarMigration walks the folder rewriting files; a process death halfway leaves some files converted and some not. The import path tolerates that (it strips a stray header and uses its id), so the folder is not corrupt — but the setting has already flipped or not, and nothing reports which files were missed. Impact: a half-converted folder with no way to see it, on a switch over a large folder.

Localisation

  • Nothing prevents the next hardcoded string. The relative-time labels were Korean in every locale for as long as they existed, in two separate copies, and were found only because someone went looking for something else. UntranslatedStringTest and ResourceParityTest check that resources exist and agree across locales; neither can see a literal that never became a resource. A lint rule or a source scan for user-facing literals would. Impact: the same defect class recurs silently — this was its third instance after Sync reconcile: conflict copies multiply every pass, and four related mirror defects #217.
    • Done in test: fail the build on user-facing text that never became a resource (#262) #263. HardcodedStringTest scans src/main for the two shapes the defect took: Hangul in any source literal (which is what would have caught formatRelative, whose literals never went near a UI call), and a literal handed straight to Text(), text = or contentDescription =. Both allowlists name real exceptions, and a third test fails once an entry stops matching, so neither list can rot into a blanket exemption.

Release process

  • Store notes overran the 500-character Play cap for the third release running. Hardening candidates: v2.26.1 #167, then de/es/fr in v2.30.0, then en-US here. verify-release-notes.ps1 catches it, but only after the files are written, and it warns below 50 characters of headroom — four of six locales are in that band right now. Authoring them against a live count, or shortening the house style for these notes, would stop the rewrite cycle. Impact: an avoidable retry at every cut.
  • launch-smoke fails on emulator install often enough to be noise. Three of the last four runs failed with Failure calling service package: Broken pipe or Can't find service: package before the app was ever installed, and passed on a straight re-run. It is deliberately non-required so it does not block a merge, but a check that flakes this often trains people to ignore it. Worth either hardening the install step or dropping the job. Impact: a red check that carries no information.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions