Browse Source

Syncing optimizations (#1554)

* Syncing optimizations

* Add comment
Pete Hunt 5 years ago
parent
commit
4696c9ee0e
4 changed files with 71 additions and 15 deletions
  1. 20 0
      package-lock.json
  2. 11 9
      package.json
  3. 39 5
      src/components/App.tsx
  4. 1 1
      src/components/Portal.tsx

+ 20 - 0
package-lock.json

@@ -2017,6 +2017,21 @@
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz",
       "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA=="
     },
+    "@types/lodash": {
+      "version": "4.14.150",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.150.tgz",
+      "integrity": "sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w==",
+      "dev": true
+    },
+    "@types/lodash.throttle": {
+      "version": "4.1.6",
+      "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.6.tgz",
+      "integrity": "sha512-/UIH96i/sIRYGC60NoY72jGkCJtFN5KVPhEMMMTjol65effe1gPn0tycJqV5tlSwMTzX8FqzB5yAj0rfGHTPNg==",
+      "dev": true,
+      "requires": {
+        "@types/lodash": "*"
+      }
+    },
     "@types/minimatch": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@@ -9635,6 +9650,11 @@
         "lodash._reinterpolate": "^3.0.0"
       }
     },
+    "lodash.throttle": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
+      "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
+    },
     "lodash.uniq": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",

+ 11 - 9
package.json

@@ -21,9 +21,18 @@
   "dependencies": {
     "@sentry/browser": "5.15.5",
     "@sentry/integrations": "5.15.5",
+    "@testing-library/jest-dom": "5.5.0",
+    "@testing-library/react": "10.0.4",
+    "@types/jest": "25.2.1",
+    "@types/nanoid": "2.1.0",
+    "@types/react": "16.9.34",
+    "@types/react-dom": "16.9.7",
+    "@types/socket.io-client": "1.4.32",
     "browser-nativefs": "0.7.1",
     "i18next-browser-languagedetector": "4.1.1",
+    "lodash.throttle": "4.1.1",
     "nanoid": "2.1.11",
+    "node-sass": "4.14.0",
     "open-color": "1.7.0",
     "points-on-curve": "0.2.0",
     "pwacompat": "2.0.11",
@@ -32,17 +41,10 @@
     "react-scripts": "3.4.1",
     "roughjs": "4.2.3",
     "socket.io-client": "2.3.0",
-    "node-sass": "4.14.0",
-    "typescript": "3.8.3",
-    "@types/jest": "25.2.1",
-    "@types/nanoid": "2.1.0",
-    "@types/react": "16.9.34",
-    "@types/react-dom": "16.9.7",
-    "@types/socket.io-client": "1.4.32",
-    "@testing-library/jest-dom": "5.5.0",
-    "@testing-library/react": "10.0.4"
+    "typescript": "3.8.3"
   },
   "devDependencies": {
+    "@types/lodash.throttle": "4.1.6",
     "asar": "3.0.3",
     "eslint": "6.8.0",
     "eslint-config-prettier": "6.11.0",

+ 39 - 5
src/components/App.tsx

@@ -133,6 +133,8 @@ import {
   saveUsernameToLocalStorage,
 } from "../data/localStorage";
 
+import throttle from "lodash.throttle";
+
 /**
  * @param func handler taking at most single parameter (event).
  */
@@ -163,11 +165,14 @@ const gesture: Gesture = {
   initialScale: null,
 };
 
+const SYNC_FULL_SCENE_INTERVAL_MS = 20000;
+
 class App extends React.Component<any, AppState> {
   canvas: HTMLCanvasElement | null = null;
   rc: RoughCanvas | null = null;
   portal: Portal = new Portal(this);
   lastBroadcastedOrReceivedSceneVersion: number = -1;
+  broadcastedElementVersions: Map<string, number> = new Map();
   removeSceneCallback: SceneStateCallbackRemover | null = null;
 
   actionManager: ActionManager;
@@ -474,6 +479,10 @@ class App extends React.Component<any, AppState> {
     }
   });
 
+  queueBroadcastAllElements = throttle(() => {
+    this.broadcastScene(SCENE.UPDATE, /* syncAll */ true);
+  }, SYNC_FULL_SCENE_INTERVAL_MS);
+
   componentDidUpdate() {
     if (this.state.isCollaborating && !this.portal.socket) {
       this.initializeSocketClient({ showLoadingState: true });
@@ -555,7 +564,8 @@ class App extends React.Component<any, AppState> {
       getDrawingVersion(globalSceneState.getElementsIncludingDeleted()) >
       this.lastBroadcastedOrReceivedSceneVersion
     ) {
-      this.broadcastScene(SCENE.UPDATE);
+      this.broadcastScene(SCENE.UPDATE, /* syncAll */ false);
+      this.queueBroadcastAllElements();
     }
 
     history.record(this.state, globalSceneState.getElementsIncludingDeleted());
@@ -1020,19 +1030,43 @@ class App extends React.Component<any, AppState> {
   };
 
   // maybe should move to Portal
-  broadcastScene = (sceneType: SCENE.INIT | SCENE.UPDATE) => {
+  broadcastScene = (sceneType: SCENE.INIT | SCENE.UPDATE, syncAll: boolean) => {
+    if (sceneType === SCENE.INIT && !syncAll) {
+      throw new Error("syncAll must be true when sending SCENE.INIT");
+    }
+
+    let syncableElements = getSyncableElements(
+      globalSceneState.getElementsIncludingDeleted(),
+    );
+
+    if (!syncAll) {
+      // sync out only the elements we think we need to to save bandwidth.
+      // periodically we'll resync the whole thing to make sure no one diverges
+      // due to a dropped message (server goes down etc).
+      syncableElements = syncableElements.filter(
+        (syncableElement) =>
+          !this.broadcastedElementVersions.has(syncableElement.id) ||
+          syncableElement.version >
+            this.broadcastedElementVersions.get(syncableElement.id)!,
+      );
+    }
+
     const data: SocketUpdateDataSource[typeof sceneType] = {
       type: sceneType,
       payload: {
-        elements: getSyncableElements(
-          globalSceneState.getElementsIncludingDeleted(),
-        ),
+        elements: syncableElements,
       },
     };
     this.lastBroadcastedOrReceivedSceneVersion = Math.max(
       this.lastBroadcastedOrReceivedSceneVersion,
       getDrawingVersion(globalSceneState.getElementsIncludingDeleted()),
     );
+    for (const syncableElement of syncableElements) {
+      this.broadcastedElementVersions.set(
+        syncableElement.id,
+        syncableElement.version,
+      );
+    }
     return this.portal._broadcastSocketData(data as SocketUpdateData);
   };
 

+ 1 - 1
src/components/Portal.tsx

@@ -29,7 +29,7 @@ class Portal {
       }
     });
     this.socket.on("new-user", async (_socketID: string) => {
-      this.app.broadcastScene(SCENE.INIT);
+      this.app.broadcastScene(SCENE.INIT, /* syncAll */ true);
     });
     this.socket.on("room-user-change", (clients: string[]) => {
       this.app.setCollaborators(clients);