返回 DeepSeek-Reasonix
history-cache-soak.mjs
根目录 / desktop / packaging / history-cache-soak.mjs
1 // Real renderer navigation and cache accounting in an unchanged macOS bundle.
2 // Usage: node desktop/packaging/history-cache-soak.mjs /path/Reasonix.app
3 // The bench URL only enables the existing diagnostic hook; all data, RPCs,
4 // writer fences, and shell/service identity checks remain production paths.
5 import assert from "node:assert/strict";
6 import { spawn } from "node:child_process";
7 import { once } from "node:events";
8 import { createHash } from "node:crypto";
9 import { createRequire } from "node:module";
10 import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
11 import { join, resolve } from "node:path";
12 import { tmpdir, platform, arch } from "node:os";
13 import { packagedSmokeEnv } from "./smoke-env.mjs";
14 import { parseServiceReady, waitForSmokeCondition } from "./smoke-poll.mjs";
15 import { closeAndVerify } from "./smoke-lifecycle.mjs";
16
17 assert.equal(platform(), "darwin");
18 const bundle = resolve(process.argv[2]);
19 const require = createRequire(new URL("../electron/package.json", import.meta.url));
20 const { _electron } = require("playwright");
21 const home = mkdtempSync(join(tmpdir(), "reasonix-history-cache-"));
22 const resultPath = `${home}.results.json`;
23 const dir = join(home, "sessions");
24 mkdirSync(dir);
25 writeFileSync(join(home, "config.toml"), `default_model = "fixture/model"\n[desktop]\nprovider_access = ["fixture"]\n[[providers]]\nname = "fixture"\nkind = "openai"\nbase_url = "http://127.0.0.1:1/v1"\nmodels = ["model"]\ndefault = "model"\napi_key_env = "HISTORY_FIXTURE_KEY"\n`);
26 const hash = path => createHash("sha256").update(readFileSync(path)).digest("hex");
27 const fixtures = Array.from({ length: 8 }, (_, id) => {
28 const name = `cache-${id}`;
29 const messages = Array.from({ length: 160 }, (_, turn) => [
30 { role: "user", content: `SOAK_${id}_QUESTION_${turn}` },
31 { role: "assistant", content: `SOAK_${id}_ANSWER_${turn}\n\n` + `**History ${id}/${turn}** with Unicode 🧭 and inline \`code\`.\n\n`.repeat(12)
32 + `\n\`\`\`svg\n<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40"><text x="2" y="25">${id}/${turn}</text></svg>\n\`\`\`\n` },
33 ]).flat();
34 const path = join(dir, `${name}.jsonl`);
35 writeFileSync(path, id % 2 === 0 ? messages.map(message => JSON.stringify(message)).join("\n") + "\n"
36 : '{"role":"user","content":"SUPERSEDED_CHECKPOINT"}\n');
37 const files = [path];
38 if (id % 2) {
39 const events = join(dir, `${name}.events.jsonl`);
40 writeFileSync(events, JSON.stringify({ messages, type: "replace", schema_version: 1 }) + "\n");
41 files.push(events);
42 }
43 return { id, name, path, files: files.map(path => ({ path, digest: hash(path) })) };
44 });
45 const report = { build: JSON.parse(readFileSync(join(bundle, "Contents/Resources/build.json"), "utf8")),
46 machine: { platform: platform(), arch: arch() }, complete: false, samples: [],
47 scope: "Real UI navigation, bidirectional paging, cache accounting, worker drain and SVG object URL release for checkpoint/schema-1 fixtures; not whole-process memory or idle-runtime qualification." };
48 let application, page, lease;
49 const invoke = (method, args = []) => page.evaluate(({ method, args }) => window.reasonixDesktop.invoke(method, args), { method, args });
50 const stats = () => page.evaluate(() => window.__reasonixPerf.stats());
51 async function sample(id, phase) {
52 await waitForSmokeCondition(async () => (await stats()).markdownWorker?.pending === 0);
53 const value = await stats();
54 const cache = value.transcriptCache;
55 assert.ok(cache && cache.residentSessions > 0, "real renderer did not populate transcript cache");
56 assert.ok(cache.residentSessions <= cache.maxResidentSessions, "resident-session budget exceeded");
57 assert.ok(cache.bodyBytes <= cache.bodyBudgetBytes, "body budget exceeded");
58 assert.ok(cache.markdownBytes <= cache.markdownBudgetBytes, "Markdown budget exceeded");
59 assert.ok(cache.residentWindowEntries <= cache.residentSessions * cache.windowMaxPages * 32, "resident history window exceeded");
60 const objectURLs = await page.evaluate(() => ({ ...window.__historyObjectURLs(), mountedSVG: document.querySelectorAll(".md-svg").length }));
61 report.samples.push({ fixture: id, phase, cache, markdownWorker: value.markdownWorker, objectURLs });
62 assert.ok(objectURLs.active <= objectURLs.mountedSVG, "unmounted SVG retained an object URL");
63 }
64 async function select(fixture) {
65 await page.locator(".project-tree__topic-main").filter({ has: page.getByText(fixture.name, { exact: true }) }).click();
66 await waitForSmokeCondition(async () => (await invoke("ListTabs")).some(tab => tab.active && tab.sessionPath === fixture.path));
67 await page.waitForFunction(id => document.querySelector(".chat-transcript")?.textContent?.includes(`SOAK_${id}_QUESTION_`), fixture.id);
68 const text = await page.locator(".chat-transcript").innerText();
69 for (const other of fixtures.filter(other => other !== fixture)) assert.equal(text.includes(`SOAK_${other.id}_`), false, "previous source remained visible");
70 assert.equal(text.includes("SUPERSEDED_CHECKPOINT"), false);
71 }
72 async function pageHistory(fixture) {
73 // Exercise the same buttons readers use, never call history RPCs directly or
74 // mutate scrollTop to bypass the transcript's navigation owner.
75 await page.locator(".chat-flow-scroll").hover();
76 await page.mouse.wheel(0, -1000000);
77 await page.waitForFunction(() => document.querySelector(".chat-flow-scroll")?.getAttribute("data-scroll-mode") === "reader");
78 for (let index = 0; index < 4; index++) {
79 const before = await page.locator(".chat-history-window").innerText();
80 await page.locator(".chat-older").click();
81 await page.waitForFunction(previous => {
82 const button = document.querySelector(".chat-older");
83 return button && !button.disabled && document.querySelector(".chat-history-window")?.innerText !== previous;
84 }, before);
85 report.samples.push({ fixture: fixture.id, phase: "page", range: await page.locator(".chat-history-window").innerText(), cache: (await stats()).transcriptCache });
86 }
87 assert.equal((await page.locator(".chat-column").innerText()).includes(`SOAK_${fixture.id}_QUESTION_159`), false, "newest page was not reclaimed");
88 await sample(fixture.id, "older");
89 await page.locator(".chat-history-newer button").last().click();
90 await page.waitForFunction(id => document.querySelector(".chat-column")?.textContent?.includes(`SOAK_${id}_QUESTION_159`), fixture.id);
91 await sample(fixture.id, "latest");
92 }
93 try {
94 lease = spawn("python3", ["-u", "-c", `import fcntl, sys
95 locks = [open(path, 'a+') for path in sys.argv[1:]]
96 for file in locks: fcntl.flock(file, fcntl.LOCK_EX | fcntl.LOCK_NB)
97 print('locked', flush=True)
98 sys.stdin.read()
99 `, ...fixtures.map(fixture => fixture.path + ".lease.lock")], { stdio: ["pipe", "pipe", "inherit"] });
100 await Promise.race([once(lease.stdout, "data").then(([data]) => assert.match(String(data), /locked/)),
101 once(lease, "exit").then(() => { throw new Error("fixture writer exited before locking"); })]);
102 application = await _electron.launch({ executablePath: join(bundle, "Contents/MacOS/Reasonix"),
103 env: { ...packagedSmokeEnv(process.env, home), HISTORY_FIXTURE_KEY: "local-fixture" } });
104 await waitForSmokeCondition(async () => {
105 for (const candidate of application.windows()) if (await candidate.evaluate(() => Boolean(window.reasonixDesktop)).catch(() => false)) { page = candidate; return true; }
106 return false;
107 });
108 assert.equal(await invoke("Version"), report.build.version);
109 assert.equal(await application.evaluate(({ app }) => app.isPackaged && !process.env.REASONIX_DEV), true);
110 const url = new URL(page.url());
111 url.searchParams.set("bench", "1");
112 // Observe native URL ownership without altering app/cache/RPC behavior.
113 // Each navigation gets a fresh counter; only content-free counts are saved.
114 await page.addInitScript(() => {
115 const active = new Set();
116 let created = 0, revoked = 0;
117 const create = URL.createObjectURL, revoke = URL.revokeObjectURL;
118 URL.createObjectURL = function(blob) {
119 const url = create.call(this, blob);
120 // Inline worker loaders revoke their JavaScript blob from inside the
121 // worker realm. A page-only observer cannot count those releases. Track
122 // SVG resources owned by the mounted history components specifically.
123 if (blob.type === "image/svg+xml") { active.add(url); created++; }
124 return url;
125 };
126 URL.revokeObjectURL = function(url) {
127 if (active.delete(url)) revoked++;
128 return revoke.call(this, url);
129 };
130 window.__historyObjectURLs = () => ({ kind: "image/svg+xml", created, revoked, active: active.size });
131 });
132 await page.goto(url.href);
133 await page.waitForFunction(() => Boolean(window.__reasonixPerf && window.reasonixDesktop));
134 assert.equal(await invoke("Version"), report.build.version);
135 const folder = page.locator(".project-tree__folder-main").first();
136 await folder.waitFor();
137 if (await folder.getAttribute("aria-expanded") !== "true") await folder.click();
138 await page.locator(".project-tree__topic-window-toggle").first().click();
139 await page.getByText("cache-0", { exact: true }).waitFor();
140 for (let round = 0; round < 4; round++) {
141 for (const fixture of round % 2 ? [...fixtures].reverse() : fixtures) {
142 await select(fixture);
143 await pageHistory(fixture);
144 }
145 console.log(`PASS native cache round ${round + 1}: eight sessions, older/latest paging and settled budgets`);
146 }
147 for (const fixture of [fixtures[0], fixtures[1], fixtures[0]]) { await select(fixture); await sample(fixture.id, "A-B-A"); }
148 assert.ok(report.samples.some(sample => sample.cache.historyEvictions > 0), "no resident-session eviction exercised");
149 assert.ok(report.samples.some(sample => sample.cache.reclaimedPages > 0), "no page reclamation exercised");
150 assert.ok(report.samples.some(sample => sample.cache.markdownBytes > 0), "no parsed Markdown cached");
151 assert.ok(report.samples.some(sample => sample.objectURLs?.created > 0), "no native SVG preview URL created");
152 assert.ok(report.samples.some(sample => sample.objectURLs?.revoked > 0), "no native SVG preview URL released");
153 for (const fixture of fixtures) for (const file of fixture.files) assert.equal(hash(file.path), file.digest, "authoritative source changed");
154 const ready = readFileSync(join(home, "desktop-shell/logs/shell.log"), "utf8").split("\n").reverse().map(parseServiceReady).find(Boolean);
155 assert.ok(ready);
156 await closeAndVerify(application, { shellPid: await application.evaluate(() => process.pid), servicePid: ready.pid });
157 application = undefined;
158 report.complete = true;
159 } catch (error) {
160 report.error = String(error);
161 if (page) writeFileSync(join(home, "failure.json"), JSON.stringify(await page.evaluate(() => ({ text: document.querySelector(".chat-column")?.innerText, stats: window.__reasonixPerf?.stats(), objectURLs: window.__historyObjectURLs?.() })).catch(error => ({ error: String(error) })), null, 2));
162 await page?.screenshot({ path: join(home, "failure.png"), fullPage: true }).catch(() => {});
163 throw error;
164 } finally {
165 writeFileSync(resultPath, JSON.stringify(report, null, 2));
166 await application?.close();
167 if (lease && lease.exitCode === null) { const exited = once(lease, "exit"); lease.stdin.end(); await exited; }
168 if (report.complete) rmSync(home, { recursive: true, force: true });
169 else console.error(`Native cache fixture retained at ${home}`);
170 console.log(`Native cache results: ${resultPath}`);
171 }
172
172 lines Plain Text