perf: faster startup and long pages
- open the window while the notes folder loads; rebuild the search index after the page list is sent - load mermaid and katex on demand - vault.list reads files in parallel batches - resolveLink uses a per-page-list lookup table - very long pages (>300 blocks) skip layout of off-screen blocks - perf-large: add a long-page case, fix the tasks poll Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fcad23afa9
commit
d5d25eaf46
10 files changed
+147
-64
No files matched your search
+9
-3
@@ -67,7 +67,11 @@ async function openVault(dir) {
|
||||
vault = new Vault(dir, { trash: (p) => shell.trashItem(p) });
|
||||
db = openDb(path.join(dir, '.notes', 'index.sqlite'));
|
||||
const pages = await vault.list();
|
||||
db.rebuild(pages);
|
||||
// The index is only a cache of the files, so refresh it once the window has its page list.
|
||||
const opened = vault;
|
||||
setTimeout(async () => {
|
||||
try { if (vault === opened) db.rebuild(await vault.list()); } catch (err) { console.error('Could not rebuild the search index', err); }
|
||||
}, 300).unref();
|
||||
const trashDays = prefs().trashDays;
|
||||
if (trashDays > 0) vault.purgeOlderThan(trashDays).catch((err) => console.error('Could not clear old trash', err));
|
||||
stopWatch = vault.watch(async (ids) => {
|
||||
@@ -162,7 +166,9 @@ function need() {
|
||||
|
||||
const handle = (channel, fn) => ipcMain.handle(channel, (_event, ...args) => fn(...args));
|
||||
|
||||
handle('vault:get', () => snapshot());
|
||||
// The window is created while the folder is still opening; the page's first question waits for it.
|
||||
let opening = null;
|
||||
handle('vault:get', async () => { if (opening) await opening.catch(() => {}); return snapshot(); });
|
||||
handle('vault:list', () => snapshot());
|
||||
handle('vault:pick', async (kind) => {
|
||||
let dir;
|
||||
@@ -757,7 +763,7 @@ app.whenReady().then(async () => {
|
||||
|
||||
const s = settings.get();
|
||||
if (s.vault && fs.existsSync(s.vault)) {
|
||||
try { await openVault(s.vault); } catch (err) { console.error('Could not open notes folder', err); }
|
||||
opening = openVault(s.vault).catch((err) => { console.error('Could not open notes folder', err); });
|
||||
}
|
||||
createWindow();
|
||||
applyPrefs();
|
||||
|
||||
+56
-45
@@ -74,57 +74,68 @@ class Vault {
|
||||
isRecent(id) { const t = this.recent.get(id); return t !== undefined && Date.now() - t < 1500; }
|
||||
|
||||
async list() {
|
||||
const pages = new Map();
|
||||
// Each folder yields its pages (files first, then its subfolders, in directory order). Files are read in
|
||||
// parallel batches and subfolders are walked together; the order of the result stays the same.
|
||||
const readPage = async (dir, parent, e) => {
|
||||
const canvas = /\.canvas$/i.test(e.name);
|
||||
const title = canvas ? e.name.slice(0, -7) : e.name.slice(0, -3);
|
||||
const id = parent ? `${parent}/${canvas ? e.name : title}` : canvas ? e.name : title;
|
||||
const full = path.join(dir, e.name);
|
||||
try {
|
||||
const st = await fsp.stat(full);
|
||||
const cached = this.cache.get(full);
|
||||
let page;
|
||||
if (cached && cached.mtime === st.mtimeMs) {
|
||||
page = cached.page;
|
||||
// update ID and parent in case it moved/renamed but mtime stayed the same
|
||||
page.id = id;
|
||||
page.parent = parent || null;
|
||||
page.title = title;
|
||||
} else {
|
||||
const raw = await fsp.readFile(full, 'utf8');
|
||||
// A canvas's "body" is its text and the notes on it as [[links]], for search, backlinks and the graph.
|
||||
const { meta, body } = canvas ? { meta: {}, body: canvasText(raw) } : parseFrontMatter(raw);
|
||||
// Front matter beyond the page's own keys: a database record's property values (see src/lib/database.js).
|
||||
const props = {};
|
||||
for (const k of Object.keys(meta)) if (!PAGE_KEYS.has(k)) props[k] = meta[k];
|
||||
page = {
|
||||
...(canvas ? { kind: 'canvas' } : {}),
|
||||
props,
|
||||
...(meta['ink-database'] ? { db: true } : {}),
|
||||
id, parent: parent || null, title, exists: true, mtime: st.mtimeMs,
|
||||
icon: meta.icon || null, cover: meta.cover || null, tags: asArray(meta.tags),
|
||||
created: meta.created || today(st.birthtime && st.birthtime.getTime() ? st.birthtime : st.mtime),
|
||||
body
|
||||
};
|
||||
this.cache.set(full, { mtime: st.mtimeMs, page });
|
||||
}
|
||||
return page;
|
||||
} catch { return null; /* unreadable file: skip */ }
|
||||
};
|
||||
const walk = async (dir, parent) => {
|
||||
let entries;
|
||||
try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch { return; }
|
||||
for (const e of entries) {
|
||||
if (e.name.startsWith('.') || !e.isFile() || !/\.(md|canvas)$/i.test(e.name)) continue;
|
||||
const canvas = /\.canvas$/i.test(e.name);
|
||||
const title = canvas ? e.name.slice(0, -7) : e.name.slice(0, -3);
|
||||
const id = parent ? `${parent}/${canvas ? e.name : title}` : canvas ? e.name : title;
|
||||
const full = path.join(dir, e.name);
|
||||
try {
|
||||
const st = await fsp.stat(full);
|
||||
const cached = this.cache.get(full);
|
||||
let page;
|
||||
if (cached && cached.mtime === st.mtimeMs) {
|
||||
page = cached.page;
|
||||
// update ID and parent in case it moved/renamed but mtime stayed the same
|
||||
page.id = id;
|
||||
page.parent = parent || null;
|
||||
page.title = title;
|
||||
} else {
|
||||
const raw = await fsp.readFile(full, 'utf8');
|
||||
// A canvas's "body" is its text and the notes on it as [[links]], for search, backlinks and the graph.
|
||||
const { meta, body } = canvas ? { meta: {}, body: canvasText(raw) } : parseFrontMatter(raw);
|
||||
// Front matter beyond the page's own keys: a database record's property values (see src/lib/database.js).
|
||||
const props = {};
|
||||
for (const k of Object.keys(meta)) if (!PAGE_KEYS.has(k)) props[k] = meta[k];
|
||||
page = {
|
||||
...(canvas ? { kind: 'canvas' } : {}),
|
||||
props,
|
||||
...(meta['ink-database'] ? { db: true } : {}),
|
||||
id, parent: parent || null, title, exists: true, mtime: st.mtimeMs,
|
||||
icon: meta.icon || null, cover: meta.cover || null, tags: asArray(meta.tags),
|
||||
created: meta.created || today(st.birthtime && st.birthtime.getTime() ? st.birthtime : st.mtime),
|
||||
body
|
||||
};
|
||||
this.cache.set(full, { mtime: st.mtimeMs, page });
|
||||
}
|
||||
pages.set(id, page);
|
||||
} catch { /* unreadable file: skip */ }
|
||||
try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch { return []; }
|
||||
const files = entries.filter((e) => !e.name.startsWith('.') && e.isFile() && /\.(md|canvas)$/i.test(e.name));
|
||||
const out = [];
|
||||
const taken = new Set();
|
||||
for (let i = 0; i < files.length; i += 64) {
|
||||
const batch = await Promise.all(files.slice(i, i + 64).map((e) => readPage(dir, parent, e)));
|
||||
for (const page of batch) if (page) { out.push(page); taken.add(page.id); }
|
||||
}
|
||||
for (const e of entries) {
|
||||
if (e.name.startsWith('.') || !e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
|
||||
const dirs = entries.filter((e) => !e.name.startsWith('.') && e.isDirectory() && !SKIP_DIRS.has(e.name));
|
||||
const subs = await Promise.all(dirs.map((e) => walk(path.join(dir, e.name), parent ? `${parent}/${e.name}` : e.name)));
|
||||
dirs.forEach((e, i) => {
|
||||
const id = parent ? `${parent}/${e.name}` : e.name;
|
||||
if (!pages.has(id)) {
|
||||
pages.set(id, { id, parent: parent || null, title: e.name, exists: false, mtime: 0, icon: null, cover: null, tags: [], created: null, body: '' });
|
||||
if (!taken.has(id)) {
|
||||
out.push({ id, parent: parent || null, title: e.name, exists: false, mtime: 0, icon: null, cover: null, tags: [], created: null, body: '' });
|
||||
taken.add(id);
|
||||
}
|
||||
await walk(path.join(dir, e.name), id);
|
||||
}
|
||||
for (const p of subs[i]) out.push(p);
|
||||
});
|
||||
return out;
|
||||
};
|
||||
await walk(this.root, '');
|
||||
const pages = new Map();
|
||||
for (const p of await walk(this.root, '')) pages.set(p.id, p);
|
||||
const active = new Set([...pages.values()].filter(p => p.exists).map(p => this.abs(p.id)));
|
||||
for (const k of this.cache.keys()) {
|
||||
if (!active.has(k)) this.cache.delete(k);
|
||||
|
||||
Reference in new issue
Block a user