Browse Source

fix: disable render throttling by default & during resize (#5451)

David Luzar 2 years ago
parent
commit
5bc40402a6

+ 12 - 2
src/components/App.tsx

@@ -166,7 +166,7 @@ import {
   isAndroid,
   isAndroid,
 } from "../keys";
 } from "../keys";
 import { distance2d, getGridPoint, isPathALoop } from "../math";
 import { distance2d, getGridPoint, isPathALoop } from "../math";
-import { renderSceneThrottled } from "../renderer/renderScene";
+import { renderScene } from "../renderer/renderScene";
 import { invalidateShapeForElement } from "../renderer/renderElement";
 import { invalidateShapeForElement } from "../renderer/renderElement";
 import {
 import {
   calculateScrollCenter,
   calculateScrollCenter,
@@ -286,6 +286,10 @@ let currentScrollBars: ScrollBars = { horizontal: null, vertical: null };
 let touchTimeout = 0;
 let touchTimeout = 0;
 let invalidateContextMenu = false;
 let invalidateContextMenu = false;
 
 
+// remove this hack when we can sync render & resizeObserver (state update)
+// to rAF. See #5439
+let THROTTLE_NEXT_RENDER = true;
+
 let lastPointerUp: ((event: any) => void) | null = null;
 let lastPointerUp: ((event: any) => void) | null = null;
 const gesture: Gesture = {
 const gesture: Gesture = {
   pointers: new Map(),
   pointers: new Map(),
@@ -858,6 +862,7 @@ class App extends React.Component<AppProps, AppState> {
 
 
     if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
     if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
       this.resizeObserver = new ResizeObserver(() => {
       this.resizeObserver = new ResizeObserver(() => {
+        THROTTLE_NEXT_RENDER = false;
         // recompute device dimensions state
         // recompute device dimensions state
         // ---------------------------------------------------------------------
         // ---------------------------------------------------------------------
         this.refreshDeviceState(this.excalidrawContainerRef.current!);
         this.refreshDeviceState(this.excalidrawContainerRef.current!);
@@ -1221,7 +1226,7 @@ class App extends React.Component<AppProps, AppState> {
         );
         );
       });
       });
 
 
-    renderSceneThrottled(
+    renderScene(
       renderingElements,
       renderingElements,
       this.state,
       this.state,
       this.state.selectionElement,
       this.state.selectionElement,
@@ -1259,8 +1264,13 @@ class App extends React.Component<AppProps, AppState> {
 
 
         this.scheduleImageRefresh();
         this.scheduleImageRefresh();
       },
       },
+      THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true,
     );
     );
 
 
+    if (!THROTTLE_NEXT_RENDER) {
+      THROTTLE_NEXT_RENDER = true;
+    }
+
     this.history.record(this.state, this.scene.getElementsIncludingDeleted());
     this.history.record(this.state, this.scene.getElementsIncludingDeleted());
 
 
     // Do not notify consumers if we're still loading the scene. Among other
     // Do not notify consumers if we're still loading the scene. Among other

+ 2 - 0
src/excalidraw-app/index.tsx

@@ -83,6 +83,8 @@ import { jotaiStore, useAtomWithInitialValue } from "../jotai";
 import { reconcileElements } from "./collab/reconciliation";
 import { reconcileElements } from "./collab/reconciliation";
 import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
 import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
 
 
+window.EXCALIDRAW_THROTTLE_RENDER = true;
+
 const isExcalidrawPlusSignedUser = document.cookie.includes(
 const isExcalidrawPlusSignedUser = document.cookie.includes(
   COOKIES.AUTH_STATE_COOKIE,
   COOKIES.AUTH_STATE_COOKIE,
 );
 );

+ 1 - 0
src/global.d.ts

@@ -14,6 +14,7 @@ interface Window {
   __EXCALIDRAW_SHA__: string | undefined;
   __EXCALIDRAW_SHA__: string | undefined;
   EXCALIDRAW_ASSET_PATH: string | undefined;
   EXCALIDRAW_ASSET_PATH: string | undefined;
   EXCALIDRAW_EXPORT_SOURCE: string;
   EXCALIDRAW_EXPORT_SOURCE: string;
+  EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
   gtag: Function;
   gtag: Function;
 }
 }
 
 

+ 44 - 5
src/renderer/renderScene.ts

@@ -181,7 +181,7 @@ const renderLinearPointHandles = (
   context.restore();
   context.restore();
 };
 };
 
 
-export const renderScene = (
+export const _renderScene = (
   elements: readonly NonDeletedExcalidrawElement[],
   elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
   appState: AppState,
   selectionElement: NonDeletedExcalidrawElement | null,
   selectionElement: NonDeletedExcalidrawElement | null,
@@ -572,8 +572,7 @@ export const renderScene = (
   return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
   return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
 };
 };
 
 
-/** renderScene throttled to animation framerate */
-export const renderSceneThrottled = throttleRAF(
+const renderSceneThrottled = throttleRAF(
   (
   (
     elements: readonly NonDeletedExcalidrawElement[],
     elements: readonly NonDeletedExcalidrawElement[],
     appState: AppState,
     appState: AppState,
@@ -582,9 +581,9 @@ export const renderSceneThrottled = throttleRAF(
     rc: RoughCanvas,
     rc: RoughCanvas,
     canvas: HTMLCanvasElement,
     canvas: HTMLCanvasElement,
     renderConfig: RenderConfig,
     renderConfig: RenderConfig,
-    callback?: (data: ReturnType<typeof renderScene>) => void,
+    callback?: (data: ReturnType<typeof _renderScene>) => void,
   ) => {
   ) => {
-    const ret = renderScene(
+    const ret = _renderScene(
       elements,
       elements,
       appState,
       appState,
       selectionElement,
       selectionElement,
@@ -598,6 +597,46 @@ export const renderSceneThrottled = throttleRAF(
   { trailing: true },
   { trailing: true },
 );
 );
 
 
+/** renderScene throttled to animation framerate */
+export const renderScene = <T extends boolean = false>(
+  elements: readonly NonDeletedExcalidrawElement[],
+  appState: AppState,
+  selectionElement: NonDeletedExcalidrawElement | null,
+  scale: number,
+  rc: RoughCanvas,
+  canvas: HTMLCanvasElement,
+  renderConfig: RenderConfig,
+  callback?: (data: ReturnType<typeof _renderScene>) => void,
+  /** Whether to throttle rendering. Defaults to false.
+   * When throttling, no value is returned. Use the callback instead. */
+  throttle?: T,
+): T extends true ? void : ReturnType<typeof _renderScene> => {
+  if (throttle) {
+    renderSceneThrottled(
+      elements,
+      appState,
+      selectionElement,
+      scale,
+      rc,
+      canvas,
+      renderConfig,
+      callback,
+    );
+    return undefined as T extends true ? void : ReturnType<typeof _renderScene>;
+  }
+  const ret = _renderScene(
+    elements,
+    appState,
+    selectionElement,
+    scale,
+    rc,
+    canvas,
+    renderConfig,
+  );
+  callback?.(ret);
+  return ret as T extends true ? void : ReturnType<typeof _renderScene>;
+};
+
 const renderTransformHandles = (
 const renderTransformHandles = (
   context: CanvasRenderingContext2D,
   context: CanvasRenderingContext2D,
   renderConfig: RenderConfig,
   renderConfig: RenderConfig,

+ 1 - 1
src/tests/contextmenu.test.tsx

@@ -39,7 +39,7 @@ const mouse = new Pointer("mouse");
 // Unmount ReactDOM from root
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
 
-const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 beforeEach(() => {
 beforeEach(() => {
   localStorage.clear();
   localStorage.clear();
   renderScene.mockClear();
   renderScene.mockClear();

+ 1 - 1
src/tests/dragCreate.test.tsx

@@ -14,7 +14,7 @@ import { reseed } from "../random";
 // Unmount ReactDOM from root
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
 
-const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 beforeEach(() => {
 beforeEach(() => {
   localStorage.clear();
   localStorage.clear();
   renderScene.mockClear();
   renderScene.mockClear();

+ 1 - 1
src/tests/move.test.tsx

@@ -16,7 +16,7 @@ import { KEYS } from "../keys";
 // Unmount ReactDOM from root
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
 
-const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 beforeEach(() => {
 beforeEach(() => {
   localStorage.clear();
   localStorage.clear();
   renderScene.mockClear();
   renderScene.mockClear();

+ 1 - 1
src/tests/multiPointCreate.test.tsx

@@ -14,7 +14,7 @@ import { reseed } from "../random";
 // Unmount ReactDOM from root
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
 
-const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 beforeEach(() => {
 beforeEach(() => {
   localStorage.clear();
   localStorage.clear();
   renderScene.mockClear();
   renderScene.mockClear();

+ 1 - 1
src/tests/regressionTests.test.tsx

@@ -20,7 +20,7 @@ import { t } from "../i18n";
 
 
 const { h } = window;
 const { h } = window;
 
 
-const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 
 
 const mouse = new Pointer("mouse");
 const mouse = new Pointer("mouse");
 const finger1 = new Pointer("touch", 1);
 const finger1 = new Pointer("touch", 1);

+ 1 - 1
src/tests/resize.test.tsx

@@ -18,7 +18,7 @@ const mouse = new Pointer("mouse");
 // Unmount ReactDOM from root
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
 
-const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 beforeEach(() => {
 beforeEach(() => {
   localStorage.clear();
   localStorage.clear();
   renderScene.mockClear();
   renderScene.mockClear();

+ 1 - 1
src/tests/selection.test.tsx

@@ -16,7 +16,7 @@ import { Keyboard, Pointer } from "./helpers/ui";
 // Unmount ReactDOM from root
 // Unmount ReactDOM from root
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 
 
-const renderScene = jest.spyOn(Renderer, "renderSceneThrottled");
+const renderScene = jest.spyOn(Renderer, "renderScene");
 beforeEach(() => {
 beforeEach(() => {
   localStorage.clear();
   localStorage.clear();
   renderScene.mockClear();
   renderScene.mockClear();