Compare commits

...
2 Commits
Author SHA1 Message Date
xiaoqidun 1061b07942 fix(问题修复): 修复页面刷新与缩略图竞态
build.yaml / build (push) Successful in 45s
2026-10-26 13:45:47 +08:00
xiaoqidun 01e29618f4 perf(性能提升): 优化快速翻页与分帧呈现 2026-10-26 13:38:14 +08:00
2 changed files with 99 additions and 41 deletions

No files matched your search

+4 -2
View File
@@ -155,11 +155,14 @@ function createDocumentState() {
selectedPages: new Set(), selectedPages: new Set(),
documentSelection: null, documentSelection: null,
pageInFlight: new Map(), pageInFlight: new Map(),
pageLoadSeq: 0,
pageRenderQueue: [], pageRenderQueue: [],
pagePrefetchIndex: null, pagePrefetchIndex: null,
pagePrefetchDirection: 1, pagePrefetchDirection: 1,
pageRenderRunning: false, pageRenderRunning: false,
pageRenderWorkers: 0, pageRenderWorkers: 0,
pagePresentQueue: new Map(),
pagePresentFrame: 0,
pageObserver: null, pageObserver: null,
visiblePages: new Set(), visiblePages: new Set(),
scrollFrame: 0, scrollFrame: 0,
@@ -168,7 +171,6 @@ function createDocumentState() {
thumbnailFrame: 0, thumbnailFrame: 0,
pageDragIndex: null, pageDragIndex: null,
pageListCurrent: null, pageListCurrent: null,
thumbnailInFlight: new Set(),
thumbnailObserver: null, thumbnailObserver: null,
visibleThumbnails: new Set(), visibleThumbnails: new Set(),
exportFormats: [], exportFormats: [],
@@ -12369,7 +12371,7 @@ async function callTabWASM(tab, name, ...args) {
} }
if (name === "ofdgoLoadImport" && ArrayBuffer.isView(args[0]) && !nativeHost) transfer.push(args[0].buffer); if (name === "ofdgoLoadImport" && ArrayBuffer.isView(args[0]) && !nativeHost) transfer.push(args[0].buffer);
const message = {id, name, args}; const message = {id, name, args};
if (name === "ofdgoRenderPage" && [...state.pageInFlight.values()].some(task => task.index === args[0] && task.priority >= 6)) message.preload = true; if (name === "ofdgoRenderPage" && [...state.pageInFlight.values()].some(task => task.index === args[0] && pageView.pageRenderPriority(task, state) >= 6)) message.preload = true;
tab.worker.postMessage(message, transfer); tab.worker.postMessage(message, transfer);
} catch (err) { } catch (err) {
wasmRequests.delete(id); wasmRequests.delete(id);
+95 -39
View File
@@ -47,8 +47,9 @@ export function createPageView(context) {
} else if (state.pageWindow && (!pageView.pageShell(index) || pageView.pageShell(index).style.visibility === "hidden")) { } else if (state.pageWindow && (!pageView.pageShell(index) || pageView.pageShell(index).style.visibility === "hidden")) {
pageView.scrollToPage(index); pageView.scrollToPage(index);
} }
pageView.prunePageRenderQueue();
const page = await pageView.renderFlowPage(index, { throwError: true, openSeq, priority: 0 }); const page = await pageView.renderFlowPage(index, { throwError: true, openSeq, priority: 0 });
if (openSeq !== state.openSeq || index !== state.pageIndex) { if (!page || openSeq !== state.openSeq || index !== state.pageIndex) {
return null; return null;
} }
if (options.scroll !== false) { if (options.scroll !== false) {
@@ -221,7 +222,12 @@ export function createPageView(context) {
}, },
resetPageLoading() { resetPageLoading() {
context.state.pageLoadSeq++;
context.releaseMediaPlayers(); context.releaseMediaPlayers();
if (context.state.pagePresentFrame) cancelAnimationFrame(context.state.pagePresentFrame);
context.state.pagePresentFrame = 0;
for (const task of context.state.pagePresentQueue.values()) task.resolve(null);
context.state.pagePresentQueue.clear();
for (const [id, request] of context.wasmRequests) { for (const [id, request] of context.wasmRequests) {
if (request.tab === context.documentTab() && request.name === "ofdgoRenderPage") context.wasmWorker?.postMessage({ type: "cancel", id }); if (request.tab === context.documentTab() && request.name === "ofdgoRenderPage") context.wasmWorker?.postMessage({ type: "cancel", id });
} }
@@ -301,29 +307,28 @@ export function createPageView(context) {
async renderFlowPage(index, options = {}) { async renderFlowPage(index, options = {}) {
const openSeq = options.openSeq || context.state.openSeq; const openSeq = options.openSeq || context.state.openSeq;
if (openSeq !== context.state.openSeq) { const loadSeq = context.state.pageLoadSeq;
const current = () => openSeq === context.state.openSeq && loadSeq === context.state.pageLoadSeq;
if (!current()) {
return null; return null;
} }
try { try {
const page = await pageView.loadPageData(index, { openSeq, priority: options.priority ?? 2, const page = await pageView.loadPageData(index, { openSeq, priority: options.priority ?? 2, view: true,
refresh: context.state.editing && context.state.pageCache.get(index)?.previewOnly }); refresh: context.state.editing && context.state.pageCache.get(index)?.previewOnly });
if (openSeq !== context.state.openSeq) { if (!current()) {
return null; return null;
} }
if (page) { if (page) {
const shell = pageView.pageShell(index); await pageView.queuePagePresentation(index, page, openSeq, true);
if (shell && !shell.classList.contains("rendered") && (!context.state.pageObserver || context.state.visiblePages.has(index) || index === context.state.pageIndex)) {
pageView.mountPageSVG(index, page, openSeq);
}
pageView.updateThumbnail(index, openSeq);
} }
if (!current()) return null;
pageView.trimPageCache(); pageView.trimPageCache();
return page; return page;
} catch (err) { } catch (err) {
if (openSeq === context.state.openSeq) { if (current()) {
pageView.markFlowPageError(index, err); pageView.markFlowPageError(index, err);
} }
if (options.throwError && openSeq === context.state.openSeq) { if (options.throwError && current()) {
throw err; throw err;
} }
return null; return null;
@@ -332,6 +337,7 @@ export function createPageView(context) {
loadPageData(index, options = {}) { loadPageData(index, options = {}) {
const openSeq = options.openSeq || context.state.openSeq; const openSeq = options.openSeq || context.state.openSeq;
const loadSeq = context.state.pageLoadSeq;
if (openSeq !== context.state.openSeq) { if (openSeq !== context.state.openSeq) {
return Promise.resolve(null); return Promise.resolve(null);
} }
@@ -340,7 +346,8 @@ export function createPageView(context) {
const current = context.state.pageInFlight.get(key); const current = context.state.pageInFlight.get(key);
if (current) { if (current) {
current.priority = Math.min(current.priority, priority); current.priority = Math.min(current.priority, priority);
if (current.preview && !options.preview) return current.promise.then(() => openSeq === context.state.openSeq ? pageView.loadPageData(index, options) : null); current.view &&= !!options.view;
if (current.preview && !options.preview) return current.promise.then(page => page && openSeq === context.state.openSeq && loadSeq === context.state.pageLoadSeq ? pageView.loadPageData(index, options) : null);
return current.promise; return current.promise;
} }
if (!options.refresh && context.state.pageCache.has(index) && (options.preview || !context.state.pageCache.get(index).previewOnly)) { if (!options.refresh && context.state.pageCache.has(index) && (options.preview || !context.state.pageCache.get(index).previewOnly)) {
@@ -355,7 +362,7 @@ export function createPageView(context) {
resolve = done; resolve = done;
reject = fail; reject = fail;
}); });
const task = { key, index, openSeq, priority, preview: !!options.preview, refresh: options.refresh, resolve, reject, promise }; const task = { key, index, openSeq, priority, view: !!options.view, preview: !!options.preview, refresh: options.refresh, resolve, reject, promise };
context.state.pageInFlight.set(key, task); context.state.pageInFlight.set(key, task);
context.state.pageRenderQueue.push(task); context.state.pageRenderQueue.push(task);
pageView.schedulePageRender(); pageView.schedulePageRender();
@@ -370,16 +377,16 @@ export function createPageView(context) {
return; return;
} }
context.state.pageRenderRunning = true; context.state.pageRenderRunning = true;
const tab = context.documentTab(), openSeq = tab.state.openSeq; const tab = context.documentTab(), openSeq = tab.state.openSeq, loadSeq = tab.state.pageLoadSeq;
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (context.activeTab === tab && openSeq === tab.state.openSeq) pageView.processPageRenderQueue(); if (context.activeTab === tab && openSeq === tab.state.openSeq && loadSeq === tab.state.pageLoadSeq) pageView.processPageRenderQueue();
}); });
}, },
async processPageRenderQueue() { async processPageRenderQueue() {
const tab = context.documentTab(), openSeq = tab.state.openSeq; const tab = context.documentTab(), openSeq = tab.state.openSeq, loadSeq = tab.state.pageLoadSeq;
await context.nextFrame(); await context.nextFrame();
if (context.activeTab !== tab || openSeq !== tab.state.openSeq) return; if (context.activeTab !== tab || openSeq !== tab.state.openSeq || loadSeq !== tab.state.pageLoadSeq) return;
const available = (nativeHost?.parallelism || context.wasmWorker?.parallelism || 1) - tab.state.pageRenderWorkers; const available = (nativeHost?.parallelism || context.wasmWorker?.parallelism || 1) - tab.state.pageRenderWorkers;
await Promise.all(Array.from({ length: Math.max(0, available) }, () => pageView.processPageRenderTasks())); await Promise.all(Array.from({ length: Math.max(0, available) }, () => pageView.processPageRenderTasks()));
}, },
@@ -460,6 +467,7 @@ export function createPageView(context) {
trimPageCache() { trimPageCache() {
const protectedPages = new Set([context.state.pageIndex, ...context.state.visiblePages, ...context.state.visibleThumbnails, ...context.state.selectedPages]); const protectedPages = new Set([context.state.pageIndex, ...context.state.visiblePages, ...context.state.visibleThumbnails, ...context.state.selectedPages]);
for (const index of context.state.pagePresentQueue.keys()) protectedPages.add(index);
for (const view of [context.state.pageWindow, context.state.thumbnailWindow]) { for (const view of [context.state.pageWindow, context.state.thumbnailWindow]) {
for (const index of view?.nodes.keys() || []) protectedPages.add(index); for (const index of view?.nodes.keys() || []) protectedPages.add(index);
} }
@@ -556,7 +564,66 @@ export function createPageView(context) {
}, },
comparePageRenderTask(a, b) { comparePageRenderTask(a, b) {
return a.priority - b.priority || Math.abs(a.index - context.state.pageIndex) - Math.abs(b.index - context.state.pageIndex); return pageView.pageRenderPriority(a) - pageView.pageRenderPriority(b) || Math.abs(a.index - context.state.pageIndex) - Math.abs(b.index - context.state.pageIndex);
},
pageRenderPriority(task, state = context.state) {
if (!task.view || task.refresh) return task.priority;
if (task.index === state.pageIndex) return 0;
if (Math.floor(task.index / state.pageLayout) === Math.floor(state.pageIndex / state.pageLayout)) return 1;
if (!task.preview) {
if (state.visiblePages.has(task.index)) return 2;
if (state.pageWindow?.nodes.has(task.index)) return 3;
}
if (state.visibleThumbnails.has(task.index) || state.thumbnailWindow?.nodes.has(task.index)) return 5;
return 6;
},
queuePagePresentation(index, page, openSeq, flow = false) {
const state = context.state;
if (openSeq !== state.openSeq || state.pageCache.get(index) !== page) return Promise.resolve(null);
let shell = flow ? pageView.pageShell(index) : null;
let thumb = context.el.pageList.querySelector(`[data-page-index="${index}"] .thumb-paper`);
if (shell?.classList.contains("rendered") || state.pageObserver && !state.visiblePages.has(index) && index !== state.pageIndex) shell = null;
if (thumb?.dataset.renderKey === `${openSeq}:${index}` || state.thumbnailObserver && !state.visibleThumbnails.has(index) && index !== state.pageIndex) thumb = null;
if (!shell && !thumb) return Promise.resolve(null);
const queue = state.pagePresentQueue, current = queue.get(index);
if (current?.page === page) {
if (shell) { current.shell = shell; current.preview = false; }
current.thumb = thumb;
return current.promise;
}
current?.resolve(null);
let resolve, reject;
const promise = new Promise((done, fail) => { resolve = done; reject = fail; });
queue.set(index, { index, page, openSeq, shell, thumb, view: true, preview: !shell, resolve, reject, promise });
pageView.schedulePagePresentation();
return promise;
},
schedulePagePresentation() {
const tab = context.documentTab(), state = tab.state, openSeq = state.openSeq, loadSeq = state.pageLoadSeq;
if (state.pagePresentFrame || !state.pagePresentQueue.size) return;
state.pagePresentFrame = requestAnimationFrame(() => {
if (context.activeTab !== tab || openSeq !== state.openSeq || loadSeq !== state.pageLoadSeq) return;
state.pagePresentFrame = 0;
const started = performance.now();
do {
const task = [...state.pagePresentQueue.values()].sort(pageView.comparePageRenderTask)[0];
state.pagePresentQueue.delete(task.index);
try {
if (state.pageCache.get(task.index) === task.page) {
if (task.shell && pageView.pageShell(task.index) === task.shell && !task.shell.classList.contains("rendered")
&& (!state.pageObserver || state.visiblePages.has(task.index) || task.index === state.pageIndex)) {
pageView.mountPageSVG(task.index, task.page, task.openSeq);
}
if (task.thumb && context.el.pageList.querySelector(`[data-page-index="${task.index}"] .thumb-paper`) === task.thumb) pageView.updateThumbnail(task.index, task.openSeq);
}
task.resolve(null);
} catch (err) { task.reject(err); }
} while (state.pagePresentQueue.size && performance.now() - started < 6);
pageView.schedulePagePresentation();
});
}, },
mountPageSVG(index, page, openSeq = context.state.openSeq) { mountPageSVG(index, page, openSeq = context.state.openSeq) {
@@ -698,7 +765,7 @@ export function createPageView(context) {
prunePageRenderQueue() { prunePageRenderQueue() {
context.state.pageRenderQueue = context.state.pageRenderQueue.filter(task => { context.state.pageRenderQueue = context.state.pageRenderQueue.filter(task => {
if (task.priority < 2 || Math.abs(task.index - context.state.pageIndex) <= 2 if (!task.view || task.refresh || Math.abs(task.index - context.state.pageIndex) <= 2
|| context.state.pageWindow?.nodes.has(task.index) || context.state.thumbnailWindow?.nodes.has(task.index)) return true; || context.state.pageWindow?.nodes.has(task.index) || context.state.thumbnailWindow?.nodes.has(task.index)) return true;
context.state.pageInFlight.delete(task.key); context.state.pageInFlight.delete(task.key);
task.resolve(null); task.resolve(null);
@@ -744,6 +811,7 @@ export function createPageView(context) {
if (index !== context.state.pageIndex && !context.state.openActionTask?.navigating) context.state.openActionTask = null; if (index !== context.state.pageIndex && !context.state.openActionTask?.navigating) context.state.openActionTask = null;
if (context.state.composite && context.state.composite.index !== index) context.resetCompositeScope(); if (context.state.composite && context.state.composite.index !== index) context.resetCompositeScope();
context.state.pageIndex = index; context.state.pageIndex = index;
pageView.prunePageRenderQueue();
if (context.state.editing) context.state.openActionPage = context.state.doc?.pages[index]?.id; if (context.state.editing) context.state.openActionPage = context.state.doc?.pages[index]?.id;
if (context.state.fitMode === "width" && context.state.pageLayout === 1) { if (context.state.fitMode === "width" && context.state.pageLayout === 1) {
context.state.scale = pageView.fitWidthScale(pageView.currentPageInfo()); context.state.scale = pageView.fitWidthScale(pageView.currentPageInfo());
@@ -1056,7 +1124,6 @@ export function createPageView(context) {
resetThumbnails() { resetThumbnails() {
context.state.visibleThumbnails.clear(); context.state.visibleThumbnails.clear();
context.state.thumbnailInFlight.clear();
if (context.state.thumbnailObserver) { if (context.state.thumbnailObserver) {
context.state.thumbnailObserver.disconnect(); context.state.thumbnailObserver.disconnect();
context.state.thumbnailObserver = null; context.state.thumbnailObserver = null;
@@ -1156,34 +1223,23 @@ export function createPageView(context) {
}, },
async renderThumbnail(index, openSeq = context.state.openSeq) { async renderThumbnail(index, openSeq = context.state.openSeq) {
if (openSeq !== context.state.openSeq || !context.state.doc) { const loadSeq = context.state.pageLoadSeq;
const current = () => openSeq === context.state.openSeq && loadSeq === context.state.pageLoadSeq;
if (!current() || !context.state.doc) {
return; return;
} }
if (context.state.pageCache.has(index)) {
pageView.updateThumbnail(index, openSeq);
return;
}
const key = `${openSeq}:${index}`;
if (context.state.thumbnailInFlight.has(key)) {
return;
}
context.state.thumbnailInFlight.add(key);
try { try {
const page = await pageView.loadPageData(index, { openSeq, priority: 5, preview: true }); const page = await pageView.loadPageData(index, { openSeq, priority: 5, preview: true, view: true });
if (openSeq !== context.state.openSeq) { if (!current() || !page) {
return; return;
} }
if (!page) { await pageView.queuePagePresentation(index, page, openSeq);
return; if (!current()) return;
}
pageView.updateThumbnail(index, openSeq);
pageView.trimPageCache(); pageView.trimPageCache();
} catch { } catch {
if (openSeq === context.state.openSeq) { if (current()) {
pageView.markThumbnailError(index); pageView.markThumbnailError(index);
} }
} finally {
context.state.thumbnailInFlight.delete(key);
} }
}, },