From ecc56a61e6d10f407794424aa0d0b6e0d4b944da Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 23 Apr 2025 21:30:28 +0200 Subject: [PATCH] Don't update the visible canvas at 60 fps (bug 1936605) Instead, we update the visible canvas every 500ms. With large canvas, updating at 60fps lead to a lot gfx transactions and it can take a lot of time. For example, with wuppertal_2012.pdf on Windows, displaying it at 150% takes around 14 min !!! without this patch when it takes only around 14 sec with. Even at 30% it helps to improve the performance by around 20%. --- web/app.js | 9 +++++-- web/app_options.js | 5 ++++ web/base_pdf_page_view.js | 53 ++++++++++++++++++++++++++++++++++++++- web/pdf_viewer.js | 6 +++++ 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/web/app.js b/web/app.js index 1e245ec38..39f3a0071 100644 --- a/web/app.js +++ b/web/app.js @@ -326,8 +326,6 @@ const PDFViewerApplication = { } } if (params.has("pdfbug")) { - AppOptions.setAll({ pdfBug: true, fontExtraProperties: true }); - const enabled = params.get("pdfbug").split(","); try { await loadPDFBug(); @@ -335,6 +333,12 @@ const PDFViewerApplication = { } catch (ex) { console.error("_parseHashParams:", ex); } + + const debugOpts = { pdfBug: true, fontExtraProperties: true }; + if (globalThis.StepperManager?.enabled) { + debugOpts.minDurationToUpdateCanvas = 0; + } + AppOptions.setAll(debugOpts); } // It is not possible to change locale for the (various) extension builds. if ( @@ -519,6 +523,7 @@ const PDFViewerApplication = { enableHWA, supportsPinchToZoom: this.supportsPinchToZoom, enableAutoLinking: AppOptions.get("enableAutoLinking"), + minDurationToUpdateCanvas: AppOptions.get("minDurationToUpdateCanvas"), })); renderingQueue.setViewer(pdfViewer); diff --git a/web/app_options.js b/web/app_options.js index df4b6a57b..4d4674a7e 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -299,6 +299,11 @@ const defaultOptions = { value: 2 ** 25, kind: OptionKind.VIEWER, }, + minDurationToUpdateCanvas: { + /** @type {number} */ + value: 500, // ms + kind: OptionKind.VIEWER, + }, forcePageColors: { /** @type {boolean} */ value: false, diff --git a/web/base_pdf_page_view.js b/web/base_pdf_page_view.js index fa37052a6..b7c650586 100644 --- a/web/base_pdf_page_view.js +++ b/web/base_pdf_page_view.js @@ -21,12 +21,18 @@ class BasePDFPageView { #loadingId = null; + #minDurationToUpdateCanvas = 0; + #renderError = null; #renderingState = RenderingStates.INITIAL; #showCanvas = null; + #startTime = 0; + + #tempCanvas = null; + canvas = null; /** @type {null | HTMLDivElement} */ @@ -51,6 +57,7 @@ class BasePDFPageView { this.id = options.id; this.pageColors = options.pageColors || null; this.renderingQueue = options.renderingQueue; + this.#minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500; } get renderingState() { @@ -71,6 +78,9 @@ class BasePDFPageView { switch (state) { case RenderingStates.PAUSED: this.div.classList.remove("loading"); + // Display the canvas as it has been drawn. + this.#startTime = 0; + this.#showCanvas?.(false); break; case RenderingStates.RUNNING: this.div.classList.add("loadingIcon"); @@ -82,10 +92,12 @@ class BasePDFPageView { this.div.classList.add("loading"); this.#loadingId = null; }, 0); + this.#startTime = Date.now(); break; case RenderingStates.INITIAL: case RenderingStates.FINISHED: this.div.classList.remove("loadingIcon", "loading"); + this.#startTime = 0; break; } } @@ -100,10 +112,41 @@ class BasePDFPageView { // have a final flash we just display it once all the drawing is done. const updateOnFirstShow = !prevCanvas && !hasHCM && !hideUntilComplete; - const canvas = (this.canvas = document.createElement("canvas")); + let canvas = (this.canvas = document.createElement("canvas")); this.#showCanvas = isLastShow => { if (updateOnFirstShow) { + let tempCanvas = this.#tempCanvas; + if (!isLastShow && this.#minDurationToUpdateCanvas > 0) { + // We draw on the canvas at 60fps (in using `requestAnimationFrame`), + // so if the canvas is large, updating it at 60fps can be a way too + // much and can cause some serious performance issues. + // To avoid that we only update the canvas every + // `this.#minDurationToUpdateCanvas` ms. + + if (Date.now() - this.#startTime < this.#minDurationToUpdateCanvas) { + return; + } + if (!tempCanvas) { + tempCanvas = this.#tempCanvas = canvas; + canvas = this.canvas = canvas.cloneNode(false); + onShow(canvas); + } + } + + if (tempCanvas) { + const ctx = canvas.getContext("2d", { + alpha: false, + }); + ctx.drawImage(tempCanvas, 0, 0); + if (isLastShow) { + this.#resetTempCanvas(); + } else { + this.#startTime = Date.now(); + } + return; + } + // Don't add the canvas until the first draw callback, or until // drawing is complete when `!this.renderingQueue`, to prevent black // flickering. @@ -152,6 +195,14 @@ class BasePDFPageView { canvas.remove(); canvas.width = canvas.height = 0; this.canvas = null; + this.#resetTempCanvas(); + } + + #resetTempCanvas() { + if (this.#tempCanvas) { + this.#tempCanvas.width = this.#tempCanvas.height = 0; + this.#tempCanvas = null; + } } async _drawCanvas(options, onCancel, onFinish) { diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 59c56acf8..751decf5c 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -139,6 +139,8 @@ function isValidAnnotationEditorMode(mode) { * The default value is `true`. * @property {boolean} [enableAutoLinking] - Enable creation of hyperlinks from * text that look like URLs. The default value is `true`. + * @property {number} [minDurationToUpdateCanvas] - Minimum duration to wait + * before updating the canvas. The default value is `500`. */ class PDFPageViewBuffer { @@ -243,6 +245,8 @@ class PDFViewer { #eventAbortController = null; + #minDurationToUpdateCanvas = 0; + #mlManager = null; #scrollTimeoutId = null; @@ -342,6 +346,7 @@ class PDFViewer { this.#enableHWA = options.enableHWA || false; this.#supportsPinchToZoom = options.supportsPinchToZoom !== false; this.#enableAutoLinking = options.enableAutoLinking !== false; + this.#minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500; this.defaultRenderingQueue = !options.renderingQueue; if ( @@ -1003,6 +1008,7 @@ class PDFViewer { layerProperties: this._layerProperties, enableHWA: this.#enableHWA, enableAutoLinking: this.#enableAutoLinking, + minDurationToUpdateCanvas: this.#minDurationToUpdateCanvas, }); this._pages.push(pageView); }