From f55583b6d335dd4d460041f90795913e60fe5bac Mon Sep 17 00:00:00 2001 From: Alisue Date: Sun, 6 Jul 2025 20:21:35 +0900 Subject: [PATCH] fix: remove session compression to improve performance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Session saving/loading was using brotli compression which added unnecessary overhead. Sessions are now stored uncompressed in memory for better performance. Changes: - Remove brotli compression dependency - Make savePickerSession and loadPickerSession synchronous - Update all dependent files to handle uncompressed sessions - Update documentation to reflect the change 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- denops/fall/extension/previewer/session.ts | 7 +- denops/fall/extension/source/session.ts | 4 +- denops/fall/main/picker.ts | 6 +- denops/fall/session.ts | 97 ++++------------------ denops/fall/session_test.ts | 76 ++++++++--------- doc/fall.txt | 2 +- 6 files changed, 61 insertions(+), 131 deletions(-) diff --git a/denops/fall/extension/previewer/session.ts b/denops/fall/extension/previewer/session.ts index fe8d52e..df379a2 100644 --- a/denops/fall/extension/previewer/session.ts +++ b/denops/fall/extension/previewer/session.ts @@ -2,17 +2,16 @@ import type { PreviewItem } from "jsr:@vim-fall/core@^0.3.0/item"; import type { Previewer } from "jsr:@vim-fall/core@^0.3.0/previewer"; import { definePreviewer } from "jsr:@vim-fall/std@^0.10.0/previewer"; import type { Detail } from "../source/session.ts"; -import { decompressPickerSession } from "../../session.ts"; export function session(): Previewer { - return definePreviewer(async (_denops, { item }, { signal }) => { + return definePreviewer((_denops, { item }, { signal }) => { if (!item || signal?.aborted) { return undefined; } try { - // Decompress the session to access its data - const session = await decompressPickerSession(item.detail); + // Access the session data directly + const session = item.detail; const lines: string[] = []; diff --git a/denops/fall/extension/source/session.ts b/denops/fall/extension/source/session.ts index 9f3d3c5..5a1615e 100644 --- a/denops/fall/extension/source/session.ts +++ b/denops/fall/extension/source/session.ts @@ -1,9 +1,9 @@ import type { Source } from "jsr:@vim-fall/core@^0.3.0/source"; import type { DetailUnit, IdItem } from "jsr:@vim-fall/core@^0.3.0/item"; -import type { PickerSessionCompressed } from "../../session.ts"; +import type { PickerSession } from "../../session.ts"; import { listPickerSessions } from "../../session.ts"; -export type Detail = PickerSessionCompressed; +export type Detail = PickerSession; export function session(): Source { return { diff --git a/denops/fall/main/picker.ts b/denops/fall/main/picker.ts index 278786a..412eb8c 100644 --- a/denops/fall/main/picker.ts +++ b/denops/fall/main/picker.ts @@ -149,7 +149,7 @@ async function resumePicker( // Parse filter ({name}#{indexFromLatest}) const [filterName, filterNumberStr = "1"] = filter.split("#", 2); const filterNumber = Number(filterNumberStr); - const session = await loadPickerSession({ + const session = loadPickerSession({ name: filterName, number: filterNumber, }); @@ -212,12 +212,12 @@ async function startPicker( stack.defer(() => { zindex -= Picker.ZINDEX_ALLOCATION; }); - stack.defer(async () => { + stack.defer(() => { const name = pickerParams.name; if (SESSION_EXCLUDE_SOURCES.includes(name)) { return; } - await savePickerSession({ + savePickerSession({ name, args, context: itemPicker.context, diff --git a/denops/fall/session.ts b/denops/fall/session.ts index 0cdb0b6..5145ec0 100644 --- a/denops/fall/session.ts +++ b/denops/fall/session.ts @@ -1,14 +1,13 @@ import type { Detail } from "jsr:@vim-fall/core@^0.3.0/item"; -import { brotli } from "jsr:@deno-library/compress@^0.5.6"; import type { PickerContext } from "./picker.ts"; /** - * In-memory storage for compressed picker sessions. + * In-memory storage for picker sessions. * Sessions are stored in chronological order (oldest first). */ // deno-lint-ignore no-explicit-any -const sessions: PickerSessionCompressed[] = []; +const sessions: PickerSession[] = []; /** * Maximum number of sessions to keep in memory. @@ -28,88 +27,24 @@ export type PickerSession = { readonly context: PickerContext; }; -/** - * Compressed version of PickerSession where the context is stored as binary data. - * This reduces memory usage when storing multiple sessions. - * @template T - The type of item detail in the picker - */ -export type PickerSessionCompressed = - & Omit, "context"> - & { - /** Brotli-compressed binary representation of the context */ - context: Uint8Array; - }; - -/** - * Compresses a picker session by converting its context to brotli-compressed binary data. - * This is used internally to reduce memory usage when storing sessions. - * @template T - The type of item detail in the picker - * @param session - The session to compress - * @returns A promise that resolves to the compressed session - */ -async function compressPickerSession( - session: PickerSession, -): Promise> { - const encoder = new TextEncoder(); - // Convert Set to Array for JSON serialization - const contextForSerialization = { - ...session.context, - selection: Array.from(session.context.selection), - }; - return { - ...session, - context: await brotli.compress( - encoder.encode(JSON.stringify(contextForSerialization)), - ), - }; -} - -/** - * Decompresses a picker session by converting its binary context back to structured data. - * @template T - The type of item detail in the picker - * @param compressed - The compressed session to decompress - * @returns A promise that resolves to the decompressed session - */ -export async function decompressPickerSession( - compressed: PickerSessionCompressed, -): Promise> { - const decoder = new TextDecoder(); - const decompressedContext = JSON.parse( - decoder.decode(await brotli.uncompress(compressed.context)), - ); - // Convert selection array back to Set - return { - ...compressed, - context: { - ...decompressedContext, - selection: new Set(decompressedContext.selection), - }, - }; -} - /** * Lists all stored picker sessions in reverse chronological order (newest first). - * @returns A readonly array of compressed sessions + * @returns A readonly array of sessions */ -export function listPickerSessions(): readonly PickerSessionCompressed< - Detail ->[] { +export function listPickerSessions(): readonly PickerSession[] { return sessions.slice().reverse(); // Return a copy in reverse order } /** * Saves a picker session to the in-memory storage. - * The session is compressed before storage to reduce memory usage. * If the storage exceeds MAX_SESSION_COUNT, the oldest session is removed. * @template T - The type of item detail in the picker * @param session - The session to save - * @returns A promise that resolves when the session is saved */ -export async function savePickerSession( +export function savePickerSession( session: PickerSession, -): Promise { - const compressed = await compressPickerSession(session); - sessions.push(compressed); +): void { + sessions.push(session); if (sessions.length > MAX_SESSION_COUNT) { sessions.shift(); // Keep only the last MAX_SESSION_COUNT sessions } @@ -130,29 +65,25 @@ export type LoadPickerSessionOptions = { * @template T - The type of item detail in the picker * @param indexFromLatest - The index from the latest session (0 = most recent, 1 = second most recent, etc.) * @param options - Options to filter sessions - * @returns A promise that resolves to the decompressed session, or undefined if not found + * @returns The session, or undefined if not found * @example * ```ts * // Load the most recent session - * const session1 = await loadPickerSession(); + * const session1 = loadPickerSession(); * * // Load the second most recent session - * const session2 = await loadPickerSession({ number: 2 }); + * const session2 = loadPickerSession({ number: 2 }); * * // Load the most recent session with name "file" - * const session3 = await loadPickerSession({ name: "file", number: 1 }); + * const session3 = loadPickerSession({ name: "file", number: 1 }); * ``` */ -export async function loadPickerSession( +export function loadPickerSession( { name, number: indexFromLatest }: LoadPickerSessionOptions = {}, -): Promise | undefined> { +): PickerSession | undefined { const filteredSessions = name ? sessions.filter((s) => s.name === name) : sessions; const index = filteredSessions.length - (indexFromLatest ?? 1); - const compressed = filteredSessions.at(index); - if (!compressed) { - return undefined; - } - return await decompressPickerSession(compressed); + return filteredSessions.at(index) as PickerSession | undefined; } diff --git a/denops/fall/session_test.ts b/denops/fall/session_test.ts index 5634c7e..2a14eb8 100644 --- a/denops/fall/session_test.ts +++ b/denops/fall/session_test.ts @@ -43,9 +43,9 @@ Deno.test("session management", async (t) => { assertEquals(Array.isArray(sessions), true); }); - await t.step("savePickerSession stores a session", async () => { + await t.step("savePickerSession stores a session", () => { const session = createMockSession("test", 1); - await savePickerSession(session); + savePickerSession(session); const sessions = listPickerSessions(); assertEquals(sessions.length >= 1, true); @@ -55,11 +55,11 @@ Deno.test("session management", async (t) => { await t.step( "listPickerSessions returns sessions in reverse chronological order", - async () => { + () => { // Save multiple sessions - await savePickerSession(createMockSession("test1", 1)); - await savePickerSession(createMockSession("test2", 2)); - await savePickerSession(createMockSession("test3", 3)); + savePickerSession(createMockSession("test1", 1)); + savePickerSession(createMockSession("test2", 2)); + savePickerSession(createMockSession("test3", 3)); const sessions = listPickerSessions(); // Most recent session should be first @@ -71,10 +71,10 @@ Deno.test("session management", async (t) => { await t.step( "loadPickerSession retrieves the most recent session by default", - async () => { - await savePickerSession(createMockSession("recent", 99)); + () => { + savePickerSession(createMockSession("recent", 99)); - const session = await loadPickerSession(); + const session = loadPickerSession(); assertExists(session); assertEquals(session.name, "recent"); assertEquals(session.args, ["arg1-99", "arg2-99"]); @@ -85,46 +85,46 @@ Deno.test("session management", async (t) => { await t.step( "loadPickerSession retrieves session by index from latest", - async () => { + () => { // Clear and add fresh sessions for (let i = 0; i < 5; i++) { - await savePickerSession(createMockSession(`session${i}`, i)); + savePickerSession(createMockSession(`session${i}`, i)); } // Index 1 = most recent (session4) - const session0 = await loadPickerSession({ number: 1 }); + const session0 = loadPickerSession({ number: 1 }); assertExists(session0); assertEquals(session0.name, "session4"); // Index 2 = second most recent (session3) - const session1 = await loadPickerSession({ number: 2 }); + const session1 = loadPickerSession({ number: 2 }); assertExists(session1); assertEquals(session1.name, "session3"); // Index 3 = third most recent (session2) - const session2 = await loadPickerSession({ number: 3 }); + const session2 = loadPickerSession({ number: 3 }); assertExists(session2); assertEquals(session2.name, "session2"); }, ); - await t.step("loadPickerSession filters by name", async () => { + await t.step("loadPickerSession filters by name", () => { // Add sessions with different names - await savePickerSession(createMockSession("file", 1)); - await savePickerSession(createMockSession("buffer", 2)); - await savePickerSession(createMockSession("file", 3)); - await savePickerSession(createMockSession("buffer", 4)); - await savePickerSession(createMockSession("file", 5)); + savePickerSession(createMockSession("file", 1)); + savePickerSession(createMockSession("buffer", 2)); + savePickerSession(createMockSession("file", 3)); + savePickerSession(createMockSession("buffer", 4)); + savePickerSession(createMockSession("file", 5)); // Load most recent "file" session - const fileSession = await loadPickerSession({ name: "file" }); + const fileSession = loadPickerSession({ name: "file" }); assertExists(fileSession); assertEquals(fileSession.name, "file"); assertEquals(fileSession.context.query, "query-5"); assertEquals(fileSession.context.cursor, 5); // 5 % 10 // Load second most recent "file" session - const fileSession2 = await loadPickerSession({ + const fileSession2 = loadPickerSession({ name: "file", number: 2, }); @@ -134,7 +134,7 @@ Deno.test("session management", async (t) => { assertEquals(fileSession2.context.cursor, 3); // 3 % 10 // Load most recent "buffer" session - const bufferSession = await loadPickerSession({ name: "buffer" }); + const bufferSession = loadPickerSession({ name: "buffer" }); assertExists(bufferSession); assertEquals(bufferSession.name, "buffer"); assertEquals(bufferSession.context.query, "query-4"); @@ -143,20 +143,20 @@ Deno.test("session management", async (t) => { await t.step( "loadPickerSession returns undefined for non-existent session", - async () => { + () => { // Try to load a session with an index beyond available sessions - const session = await loadPickerSession({ number: 1000 }); + const session = loadPickerSession({ number: 1000 }); assertEquals(session, undefined); // Try to load a session with a name that doesn't exist - const namedSession = await loadPickerSession({ name: "non-existent" }); + const namedSession = loadPickerSession({ name: "non-existent" }); assertEquals(namedSession, undefined); }, ); await t.step( - "compression and decompression preserve session data", - async () => { + "session data is preserved without compression", + () => { const testItems: IdItem[] = [ { id: "test-1", value: "test item 1", detail: { foo: "bar" } }, { id: "test-2", value: "test item 2", detail: { baz: 42 } }, @@ -180,8 +180,8 @@ Deno.test("session management", async (t) => { }, }; - await savePickerSession(sessionWithCustomContext); - const loadedSession = await loadPickerSession({ + savePickerSession(sessionWithCustomContext); + const loadedSession = loadPickerSession({ name: "compression-test", }); @@ -208,10 +208,10 @@ Deno.test("session management", async (t) => { }, ); - await t.step("respects MAX_SESSION_COUNT limit", async () => { + await t.step("respects MAX_SESSION_COUNT limit", () => { // Save more than MAX_SESSION_COUNT (100) sessions for (let i = 0; i < 105; i++) { - await savePickerSession(createMockSession(`session-limit-${i}`, i)); + savePickerSession(createMockSession(`session-limit-${i}`, i)); } const sessions = listPickerSessions(); @@ -227,7 +227,7 @@ Deno.test("session management", async (t) => { assertEquals(newestSession.name, "session-limit-104"); }); - await t.step("handles minimal context gracefully", async () => { + await t.step("handles minimal context gracefully", () => { const sessionWithMinimalContext = createMockSession("minimal-context", 1); const minimalSession: PickerSession = { ...sessionWithMinimalContext, @@ -244,8 +244,8 @@ Deno.test("session management", async (t) => { }, }; - await savePickerSession(minimalSession); - const loaded = await loadPickerSession({ name: "minimal-context" }); + savePickerSession(minimalSession); + const loaded = loadPickerSession({ name: "minimal-context" }); assertExists(loaded); assertEquals(loaded.context.query, ""); @@ -256,7 +256,7 @@ Deno.test("session management", async (t) => { assertEquals(loaded.context.cursor, 0); }); - await t.step("handles special characters in session data", async () => { + await t.step("handles special characters in session data", () => { const specialItems: IdItem[] = [ { id: "emoji", value: "😀 emoji test 🎉", detail: { unicode: "✨" } }, { id: "backslash", value: "backslash \\ test", detail: {} }, @@ -278,8 +278,8 @@ Deno.test("session management", async (t) => { }, }; - await savePickerSession(sessionWithSpecialChars); - const loaded = await loadPickerSession({ name: "special-chars" }); + savePickerSession(sessionWithSpecialChars); + const loaded = loadPickerSession({ name: "special-chars" }); assertExists(loaded); assertEquals( diff --git a/doc/fall.txt b/doc/fall.txt index 4db4a0e..b97eeed 100644 --- a/doc/fall.txt +++ b/doc/fall.txt @@ -448,7 +448,7 @@ COMMAND *fall-command* - "@action" (action selection picker) - "@session" (the session picker itself) - Sessions are stored in compressed format to minimize memory usage. + Sessions are stored in memory for quick access. > " Open the session picker :FallSession