From 15a532a1cae9e0f45ae8c2cbc137994ccd358263 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Mon, 26 Oct 2026 14:17:22 +0800 Subject: [PATCH] =?UTF-8?q?perf(=E6=80=A7=E8=83=BD=E6=8F=90=E5=8D=87):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AD=97=E4=BD=93=E5=8A=A0=E8=BD=BD=E4=B8=8E?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=B8=B2=E6=9F=93=E8=B0=83=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo_host.js | 15 +++++++- assets/webui/ofdgo_view.js | 71 ++++++++++++++++++++++---------------- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/assets/webui/ofdgo_host.js b/assets/webui/ofdgo_host.js index 92dbe21..f184fcf 100644 --- a/assets/webui/ofdgo_host.js +++ b/assets/webui/ofdgo_host.js @@ -64,6 +64,12 @@ class BrowserScheduler { if (data.id == null || data.type) return; const key = requestKey(worker, data); const started = worker.requests.get(key); + const operation = worker.operations.get(key), owner = this.owner(worker, data); + if (started !== undefined && data.ok && owner) { + const elapsed = performance.now() - started; + if (operation?.name === "ofdgoOpen") owner.openTime = elapsed; + if (operation?.name === "ofdgoRenderPage") owner.pageTime = owner.pageTime ? owner.pageTime * 0.75 + elapsed * 0.25 : elapsed; + } worker.requests.delete(key); worker.operations.delete(key); if (this.memory > this.budget) this.limit = Math.max(1, this.limit - 1); @@ -343,6 +349,8 @@ class BrowserEngine { this.source = null; this.pageSource = null; this.parallelism = 1; + this.startupTime = 0; + this.primary.pageTime = 0; for (const slot of this.slots) this.closeSlot(slot); } @@ -355,6 +363,7 @@ class BrowserEngine { } createSlot(source) { + const started = performance.now(); const worker = createEngineWorker({page:true, url:this.workerURL}); worker.setForeground(this.foreground); worker.pausePages(this.pagesPaused); @@ -382,6 +391,7 @@ class BrowserEngine { let opened = await this.callSlot(slot, "ofdgoOpen", [source.file, source.fonts, source.annotations]); if ((source.document || 0) !== (opened.data.documentIndex || 0)) opened = await this.callSlot(slot, "ofdgoSelectDocument", [source.document]); if ((source.version || "") !== (opened.data.documentVersion || "")) await this.callSlot(slot, "ofdgoSelectVersion", [source.version || ""]); + if (this.slots.has(slot)) this.startupTime = performance.now() - started; }); slot.ready.catch(() => {}); return slot; @@ -428,7 +438,10 @@ class BrowserEngine { } let slot = preferred || [...this.slots].filter(slot => !slot.busy).sort((a,b) => Math.abs(a.lastPage-data.args[0])-Math.abs(b.lastPage-data.args[0]))[0]; const memory = engineScheduler.memory; - if (!slot && this.foreground && !this.pagesPaused && this.slots.size < this.parallelism - 1 && memory + Math.max(128 << 20, this.memory) <= this.budget) slot = this.createSlot(this.source); + const pending = [...this.requests.values()].filter(slot => slot === null).length; + const worthwhile = (this.primary.pageTime || 0) * pending >= Math.max(50, this.startupTime || this.primary.openTime || 0); + if (!slot && worthwhile && this.foreground && !this.pagesPaused + && this.slots.size < this.parallelism - 1 && memory + Math.max(128 << 20, this.memory) <= this.budget) slot = this.createSlot(this.source); if (!slot) { this.requests.set(data.id, null); this.primary.postMessage(data, transfer); diff --git a/assets/webui/ofdgo_view.js b/assets/webui/ofdgo_view.js index 2e934b0..e6e0514 100644 --- a/assets/webui/ofdgo_view.js +++ b/assets/webui/ofdgo_view.js @@ -529,33 +529,33 @@ export function createPageView(context) { }, async loadSVGFonts(fonts, openSeq) { - const state = context.state; - for (const font of fonts) { - if (openSeq !== context.state.openSeq) { - return; - } - if (state.svgFonts.has(font.name)) { - continue; - } - const key = `${openSeq}:${font.name}`; - let loading = state.svgFontLoads.get(key); - if (!loading) { - loading = pageView.loadSVGFont(font, openSeq); - state.svgFontLoads.set(key, loading); - } - try { await loading; } - finally { if (state.svgFontLoads.get(key) === loading) state.svgFontLoads.delete(key); } + const state = context.state, loadSeq = state.pageLoadSeq; + const current = () => state === context.state && openSeq === state.openSeq && loadSeq === state.pageLoadSeq; + for (let offset = 0; offset < fonts.length && current(); offset += 4) { + const results = await Promise.allSettled(fonts.slice(offset, offset + 4).map(async font => { + if (state.svgFonts.has(font.name)) return; + const key = `${openSeq}:${loadSeq}:${font.name}`; + let loading = state.svgFontLoads.get(key); + if (!loading) { + loading = pageView.loadSVGFont(font, openSeq, loadSeq); + state.svgFontLoads.set(key, loading); + } + try { await loading; } + finally { if (state.svgFontLoads.get(key) === loading) state.svgFontLoads.delete(key); } + })); + const failed = results.find(result => result.status === "rejected"); + if (failed) throw failed.reason; } }, - async loadSVGFont(font, openSeq) { + async loadSVGFont(font, openSeq, loadSeq = context.state.pageLoadSeq) { const data = font.data ? { bytes: font.data } : await context.callWASM("ofdgoSVGFontData", font.name); - if (openSeq !== context.state.openSeq) { + if (openSeq !== context.state.openSeq || loadSeq !== context.state.pageLoadSeq) { return; } const face = new FontFace(font.name, data.bytes, { weight: String(font.weight), style: font.style }); await face.load(); - if (openSeq !== context.state.openSeq) { + if (openSeq !== context.state.openSeq || loadSeq !== context.state.pageLoadSeq) { return; } document.fonts.add(face); @@ -610,23 +610,26 @@ export function createPageView(context) { 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); + task.text ||= pageView.createTextLayerParts(task.page.text); + const text = task.text.next(); + if (!text.done) continue; + pageView.mountPageSVG(task.index, task.page, task.openSeq, text.value); } 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); } + state.pagePresentQueue.delete(task.index); } while (state.pagePresentQueue.size && performance.now() - started < 6); pageView.schedulePagePresentation(); }); }, - mountPageSVG(index, page, openSeq = context.state.openSeq) { + mountPageSVG(index, page, openSeq = context.state.openSeq, textLayer = null) { if (openSeq !== context.state.openSeq) { return; } @@ -638,7 +641,7 @@ export function createPageView(context) { const svg = pageView.parseSVG(page.svg, `p${openSeq}-${index}`); svg.classList.add("ofd-svg"); surface.replaceChildren(svg); - surface.append(pageView.createTextLayer(page.text)); + surface.append(textLayer || pageView.createTextLayer(page.text)); if (context.state.editorInfo && pageView.displayMode() === "svg") { context.mountEditorObjects(index, page, surface); } @@ -1621,12 +1624,11 @@ export function createPageView(context) { return next; }; const attrs = ["clip-path", "fill", "filter", "href", "marker-end", "marker-mid", "marker-start", "mask", "stroke", "style", "xlink:href"]; - for (const attr of attrs) { - for (const node of svg.querySelectorAll(`[${attr === "xlink:href" ? "*|href" : attr}*="#"]`)) { + const selector = attrs.map(attr => `[${attr === "xlink:href" ? "*|href" : attr}*="#"]`).join(","); + for (const node of svg.querySelectorAll(selector)) { + for (const attr of attrs) { const value = node.getAttribute(attr); - if (!value) { - continue; - } + if (!value?.includes("#")) continue; const next = replaceRef(value); if (next !== value) { node.setAttribute(attr, next); @@ -1636,11 +1638,19 @@ export function createPageView(context) { }, createTextLayer(text) { + const parts = pageView.createTextLayerParts(text); + let next; + do { next = parts.next(); } while (!next.done); + return next.value; + }, + + *createTextLayerParts(text) { const layer = document.createElement("div"); layer.className = "text-layer"; const measure = textMeasure ||= document.createElement("canvas").getContext("2d"); measure.font = "100px sans-serif"; const widths = new Map(); + let count = 0; for (const run of text.runs || []) { const line = document.createElement("span"); line.className = "text-run"; @@ -1658,10 +1668,11 @@ export function createPageView(context) { width = measure.measureText(span.textContent).width || 1; widths.set(span.textContent, width); } - const [a, b, c, d, e, f] = item.matrix.map((value) => value * MM_TO_PX); - span.style.transform = `matrix(${a / width}, ${b / width}, ${c / 100}, ${d / 100}, ${e}, ${f})`; + const [a, b, c, d, e, f] = item.matrix; + span.style.transform = `matrix(${a * MM_TO_PX / width}, ${b * MM_TO_PX / width}, ${c * MM_TO_PX / 100}, ${d * MM_TO_PX / 100}, ${e * MM_TO_PX}, ${f * MM_TO_PX})`; line.append(span); end = item.end; + if (++count % 128 === 0) yield; } const breaks = chars.slice(end).filter((char) => char === "\n").join(""); if (breaks) {