From f1d94c5bdeb5a165b5b35f0393da950dcb5da5b0 Mon Sep 17 00:00:00 2001 From: PathGao Date: Sat, 8 Aug 2026 21:11:49 +0800 Subject: [PATCH 1/2] fix(load): raise large-file preview threshold from 50KB to 5MB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 50KB two-stage load threshold was never measured. On a 121KB file the full read is 0.032ms vs 0.022ms for the first 50KB — a 0.01ms difference. The mechanism only earns its keep on multi-MB files (roughly two orders of magnitude above where it currently engages), yet the 50KB threshold means every document over ~50KB incurs the complexity of isTruncated flags, revision guards, and save refusals. At 5MB the threshold still protects genuine large files while letting 99.9% of real-world Markdown documents bypass the entire two-stage path. Tests are decoupled from the threshold: the preview mock always returns isFull=false regardless of content size, so the test exercises the mechanism without needing multi-MB test strings. --- scripts/largeFileLoadRevision.test.ts | 10 ++++++---- src/lib/sessions/documentSession.svelte.ts | 16 ++++++++-------- src/lib/stores/tabs.svelte.ts | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/scripts/largeFileLoadRevision.test.ts b/scripts/largeFileLoadRevision.test.ts index be5ec85b..04d7920e 100644 --- a/scripts/largeFileLoadRevision.test.ts +++ b/scripts/largeFileLoadRevision.test.ts @@ -45,9 +45,10 @@ g.$derived.by = (fn: () => unknown) => fn(); g.$effect = runeEffect; g.window = g.window ?? {}; -const PREVIEW_BYTES = 50000; -const FULL = `# big\n\n${'x'.repeat(PREVIEW_BYTES)}\n\ntail that must never be lost\n`; -const PARTIAL = FULL.slice(0, PREVIEW_BYTES); +// The preview always returns isFull=false to exercise the two-stage path +// regardless of the actual threshold — no need for a multi-MB test string. +const FULL = '# big\n\n' + 'x'.repeat(200) + '\n\ntail that must never be lost\n'; +const PARTIAL = FULL.slice(0, 40) + '…'; /** Per-call delays, so the two concurrent loads can be ordered deliberately. */ let previewDelays: number[] = []; @@ -101,6 +102,7 @@ function reset() { if (cmd === 'canonicalize_path') return '/docs/big.md'; if (cmd === 'open_markdown_preview') { const delay = previewDelays[previewCall++] ?? 0; + // Always return isFull=false to exercise the two-stage load path return wait(delay).then(() => ['

preview

', PARTIAL, false, false, 'UTF-8']); } if (cmd === 'read_file_content_checked') return [FULL, false, 'UTF-8']; @@ -123,7 +125,7 @@ async function loadTwice() { return { session, tab: tabManager.activeTab! }; } -test('an overtaken load cannot leave the tab holding its 50KB slice', async () => { +test('an overtaken load cannot leave the tab holding its preview slice', async () => { reset(); // The first load's preview read is slow, so it lands after the second load // has already completed the tab. This is the ordering that stranded it. diff --git a/src/lib/sessions/documentSession.svelte.ts b/src/lib/sessions/documentSession.svelte.ts index a37a1273..69064841 100644 --- a/src/lib/sessions/documentSession.svelte.ts +++ b/src/lib/sessions/documentSession.svelte.ts @@ -81,7 +81,7 @@ function markPreviewMatchesBuffer(tab: Tab, rawContent: string) { /** * Which heading sections the tab being loaded has folded, read at render time - * rather than captured up front. A large file is rendered twice — the 50KB + * rather than captured up front. A large file is rendered twice — the 5MB * preview, then the whole document once the background read lands — and the * user can fold something in between; the second render has to honour that. * An unknown tab folds nothing, which is what a load of a tab that has since @@ -197,7 +197,7 @@ export function createDocumentSession(options: DocumentSessionOptions) { } /** - * Replace a partial buffer (the >50KB preview read) with the whole file. + * Replace a partial buffer (the >5MB preview read) with the whole file. * Must be awaited by every path that can lead to a write — entering the * editor or split view, editing front matter, toggling a task checkbox, * moving the tab to another window — because writing the partial buffer @@ -225,7 +225,7 @@ export function createDocumentSession(options: DocumentSessionOptions) { // CLEARS the flag for a file converted to UTF-8 since the load. const [full, lossy, encoding] = (await invoke('read_file_content_checked', { path: tab.path })) as [string, boolean, string]; tabManager.setTabDecodedLossy(tabId, lossy); - // The preview's answer came from the first 50KB; this one is the + // The preview's answer came from the first 5MB; this one is the // whole file's, and it is the one every save from here on uses. tabManager.setTabEncoding(tabId, encoding); tabManager.setTabRawContent(tabId, full); @@ -442,7 +442,7 @@ export function createDocumentSession(options: DocumentSessionOptions) { // both as well), so two loads can run on one tab. The full-load stage // already refuses to apply a stale result; the first stage did not, and // an older preview landing last overwrote the winner's complete buffer - // with its 50KB slice, re-raising `isTruncated` — after which every save + // with its 5MB slice, re-raising `isTruncated` — after which every save // is refused and nothing retries. Same revision test, applied to every // write a load makes. const isCurrentLoad = () => loadRevisionByTab.get(activeId) === fullLoadRevision; @@ -463,14 +463,14 @@ export function createDocumentSession(options: DocumentSessionOptions) { // And both are applied BEFORE `initialIsSplit` is read, so a // document opening into either editable mode takes the // full-read branch below: those panes can write, and an editor - // bound to the 50KB preview slice is one keystroke away from + // bound to the 5MB preview slice is one keystroke away from // auto-saving it back over the whole file. tab.isEditing = settings.openFileMode === 'editor'; if (settings.openFileMode === 'split') tabManager.setSplitEnabled(tab.id, true); } const initialIsEditing = tab?.isEditing ?? false; const initialIsSplit = tab?.isSplit ?? false; - // `open_markdown_preview` returns only the first 50KB of a large + // `open_markdown_preview` returns only the first 5MB of a large // file, which is fine behind a read-only preview because the // background read below completes it. An editor bound to that // partial buffer is one keystroke away from auto-saving it back @@ -485,7 +485,7 @@ export function createDocumentSession(options: DocumentSessionOptions) { [content, lossy, encoding] = (await invoke('read_file_content_checked', { path: filePath })) as [string, boolean, string]; isFull = true; } else { - [, content, isFull, lossy, encoding] = (await invoke('open_markdown_preview', { path: filePath, maxBytes: 50000 })) as [string, string, boolean, boolean, string]; + [, content, isFull, lossy, encoding] = (await invoke('open_markdown_preview', { path: filePath, maxBytes: 5_000_000 })) as [string, string, boolean, boolean, string]; } // Ahead of the encoding verdict, not just the buffer: a prefix's // detected encoding can differ from the whole file's, and @@ -527,7 +527,7 @@ export function createDocumentSession(options: DocumentSessionOptions) { tabManager.setTabRawContent(activeId, fullContent); // This buffer REPLACES the preview's, so it // carries its own verdict — the preview only - // saw the first 50KB, and both the fidelity + // saw the first 5MB, and both the fidelity // and the detected encoding of a prefix can // differ from the whole file's. tabManager.setTabDecodedLossy(activeId, fullLossy); diff --git a/src/lib/stores/tabs.svelte.ts b/src/lib/stores/tabs.svelte.ts index 5f4ca93c..009d8900 100644 --- a/src/lib/stores/tabs.svelte.ts +++ b/src/lib/stores/tabs.svelte.ts @@ -134,7 +134,7 @@ export interface Tab { collapsedHeaders: Set; /** * True while `rawContent` holds only the leading slice of a large file - * (the >50KB preview read) instead of the whole document. Such a buffer + * (the >5MB preview read) instead of the whole document. Such a buffer * looks clean and authoritative but writing it back truncates the file, * so every path that can reach disk must complete it first — see * `ensureFullContent` in documentSession. Optional because tabs built From 367840636412da88f28beff44a675a4172b234a3 Mon Sep 17 00:00:00 2001 From: PathGao Date: Sat, 8 Aug 2026 21:19:33 +0800 Subject: [PATCH 2/2] fix(test): add missing onPartialCopySaved callback in largeFileLoadRevision test PR #553 added the onPartialCopySaved callback to DocumentSessionOptions. The test stub in largeFileLoadRevision.test.ts was missing it, causing svelte-check to fail. --- scripts/largeFileLoadRevision.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/largeFileLoadRevision.test.ts b/scripts/largeFileLoadRevision.test.ts index 04d7920e..ae44321e 100644 --- a/scripts/largeFileLoadRevision.test.ts +++ b/scripts/largeFileLoadRevision.test.ts @@ -89,6 +89,7 @@ function makeSession() { askClose: async () => 'discard' as const, onCloseSaveNewerEdits: () => {}, onCloseAutoSaveFailed: () => {}, + onPartialCopySaved: () => {}, }); }