From 4b858a4948db43c96c0b19cbac39cead8a42a2aa Mon Sep 17 00:00:00 2001
From: Moritz Langenstein <ml5717@ic.ac.uk>
Date: Wed, 18 Dec 2019 18:00:30 +0000
Subject: [PATCH] (ml5717) GC, heap usage, gnuplot output and vertical slice of
 refactoring benchmarks

---
 .gitignore                  |  36 +-
 .gitlab-ci.yml              |   5 +
 __tests__/benchmark.test.js | 726 ++++++++++++++++++++++++++++++++++--
 package-lock.json           | 312 +++++++++-------
 package.json                |   6 +-
 plot-scripts/demo.p         |  14 +
 plots/.gitkeep              |   0
 7 files changed, 908 insertions(+), 191 deletions(-)
 create mode 100644 plot-scripts/demo.p
 create mode 100644 plots/.gitkeep

diff --git a/.gitignore b/.gitignore
index c8f936e..cd44005 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,22 +11,26 @@ src/signalbuddy
 src/yjs
 src/tiny-worker
 
-# Temporary test dump files
-dot-seq-add.json
-dot-seq-erase.json
-dot-seq-sync.json
-
-dot-par-add.json
-dot-par-erase.json
-dot-par-sync.json
-
-path-seq-add.json
-path-seq-erase.json
-path-seq-sync.json
-
-path-par-add.json
-path-par-erase.json
-path-par-sync.json
+# Temporary benchmark dump files
+.dot-seq-add.json
+.dot-seq-erase.json
+.dot-seq-sync.json
+
+.dot-par-add.json
+.dot-par-erase.json
+.dot-par-sync.json
+
+.path-seq-add.json
+.path-seq-erase.json
+.path-seq-sync.json
+
+.path-par-add.json
+.path-par-erase.json
+.path-par-sync.json
+
+# Benchmark output files
+plots/*.tsv
+plots/*.pdf
 
 ### macOS ###
 # General
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f2c95d0..ac98c25 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -111,4 +111,9 @@ benchmark:
   dependencies:
     - npm_install
   script:
+    - apt install gnuplot
     - npm run test-benchmark
+    - npm run plot
+  artifacts:
+    paths:
+      - plots/
diff --git a/__tests__/benchmark.test.js b/__tests__/benchmark.test.js
index e1fb945..129368f 100644
--- a/__tests__/benchmark.test.js
+++ b/__tests__/benchmark.test.js
@@ -1,5 +1,6 @@
 import fs from "fs"
 import chalk from "chalk"
+import gc from "expose-gc/function"
 
 import { connect } from "../src/room.js"
 
@@ -21,11 +22,11 @@ import {
   createMessageReceivedEvent,
   handshake,
   syncStep1,
-  syncDone,
+  //syncDone,
   dotDraw,
   dotErase,
-  pathDraw,
-  pathErase,
+  //pathDraw,
+  //pathErase,
 } from "./benchmark.data.js"
 
 // Adapted from https://github.com/jprichardson/buffer-json (MIT license)
@@ -64,27 +65,562 @@ function printBenchmark(title, iterations, results) {
   process.stdout.write(`\n  ${title} (${iterations} iterations):\n`)
 
   for (const title in results) {
-    const { timeLoc, packets, size, timeRem, events } = results[title]
+    const {
+      timeLoc,
+      encodeRAM,
+      packets,
+      size,
+      timeRem,
+      decodeRAM,
+      events,
+    } = results[title]
     const synchronisation = title == "synchronisation"
 
     process.stdout.write(
       chalk`    {yellow ⧗} {dim ${title}:} {yellow.inverse ${(
         timeLoc /
         (1e6 * (synchronisation ? 1 : iterations))
-      ).toFixed(3)}ms ${
-        synchronisation ? "total" : "/ it"
-      }} => {dim ${packets} packet(s)} => {magenta.inverse ${size}B} => {yellow.inverse ${(
+      ).toFixed(3)}ms ${synchronisation ? "total" : "/ it"}} + {red.inverse ${(
+        encodeRAM /
+        (1024 * 1024)
+      ).toFixed(
+        3,
+      )}MB} => {dim ${packets} packet(s)} => {magenta.inverse ${size}B} => {yellow.inverse ${(
         timeRem /
         (1e6 * (synchronisation ? 1 : iterations))
-      ).toFixed(3)}ms ${
-        synchronisation ? "total" : "/ it"
-      }} => {dim ${events} event(s)}\n`,
+      ).toFixed(3)}ms ${synchronisation ? "total" : "/ it"}} + {red.inverse ${(
+        decodeRAM /
+        (1024 * 1024)
+      ).toFixed(3)}MB} => {dim ${events} event(s)}\n`,
     )
   }
 
   process.stdout.write(`\n`)
 }
 
+function writeBenchmarkHeader(filename, title, results) {
+  const columns = ["iterations"]
+
+  for (const title of results) {
+    columns.push(`${title}_timeLoc`)
+    columns.push(`${title}_encodeRAM`)
+    columns.push(`${title}_packets`)
+    columns.push(`${title}_size`)
+    columns.push(`${title}_timeRem`)
+    columns.push(`${title}_decodeRAM`)
+    columns.push(`${title}_events`)
+  }
+
+  return fs.writeFileSync(
+    filename,
+    `# Benchmark: ${title}\n# ${columns.join("\t")}\n`,
+  )
+}
+
+function appendBenchmark(filename, iterations, results) {
+  const columns = [iterations]
+
+  for (const title in results) {
+    const {
+      timeLoc,
+      encodeRAM,
+      packets,
+      size,
+      timeRem,
+      decodeRAM,
+      events,
+    } = results[title]
+
+    columns.push(timeLoc)
+    columns.push(encodeRAM)
+    columns.push(packets)
+    columns.push(size)
+    columns.push(timeRem)
+    columns.push(decodeRAM)
+    columns.push(events)
+  }
+
+  return fs.appendFileSync(filename, `${columns.join("\t")}\n`)
+}
+
+function captureHeapUsage() {
+  for (let i = 0; i < 10; i++) {
+    gc()
+  }
+
+  return process.memoryUsage().heapUsed
+}
+
+function runBidirectionalBenchmark(
+  BENCHMARK,
+  FILENAME,
+  ITERATIONSLIST,
+
+  addData,
+  eraseData,
+
+  addOnInitFrontend,
+  addBroadcastGroupTimeout,
+  addOnBroadcastGroup,
+  addPacketsFilename,
+
+  eraseOnInitFrontend,
+  eraseBroadcastGroupTimeout,
+  eraseOnBroadcastGroup,
+  erasePacketsFilename,
+
+  syncSendGroupTimeout,
+  syncOnSendGroup,
+  syncPacketsFilename,
+
+  addOnInitBackend,
+  addEventGroupTimeout,
+  addOnEventGroup,
+
+  eraseOnInitBackend,
+  eraseEventGroupTimeout,
+  eraseOnEventGroup,
+
+  syncEventGroupTimeout,
+  syncOnEventGroup,
+) {
+  if (BENCHMARK && FILENAME) {
+    writeBenchmarkHeader(FILENAME, BENCHMARK, [
+      "addPath",
+      "extendErasureIntervals",
+      "synchronisation",
+    ])
+  }
+
+  return ITERATIONSLIST.reduce(
+    (promise, ITERATIONS) =>
+      promise.then(() => {
+        const pathIDs = []
+        let prevTime
+        let currTime
+
+        let connectRAM // eslint-disable-line no-unused-vars
+        let addLocTime = 0
+        let addPackets = []
+        let addSize = 0
+        let addRAM
+        let eraseLocTime = 0
+        let erasePackets = []
+        let eraseSize = 0
+        let eraseRAM
+        let syncLocTime
+        let syncPackets = []
+        let syncSize = 0
+        let syncRAM
+        let disconnectRAM // eslint-disable-line no-unused-vars
+
+        let connectUpdRAM // eslint-disable-line no-unused-vars
+        let addRemTime = 0
+        let addEvents = 0
+        let addRemRAM
+        let eraseRemTime = 0
+        let eraseEvents = 0
+        let eraseRemRAM
+        let disconnectUpdRAM // eslint-disable-line no-unused-vars
+
+        let connectSyncRAM // eslint-disable-line no-unused-vars
+        let syncRemTime = 0
+        let syncEvents = 0
+        let syncRemRAM
+        let disconnectSyncRAM // eslint-disable-line no-unused-vars
+
+        let timeout
+
+        let room = null
+        let updateRoom = null
+        let syncRoom = null
+
+        return (
+          // eslint-disable-next-line no-async-promise-executor
+          new Promise(async (resolve) => {
+            room = await connect("room", MockConnection)
+            getEventListener(
+              "room",
+              "messageReceived",
+            )(createMessageReceivedEvent(handshake, "tw-ml"))
+
+            connectRAM = captureHeapUsage()
+
+            return resolve()
+          })
+            .then(
+              () =>
+                new Promise((resolve) => {
+                  let broadcasts = 0
+
+                  broadcastListener.callback = (channel, message) => {
+                    currTime = process.hrtime()
+
+                    addPackets[addPackets.length - 1].push(message)
+                    addSize += message.message.length
+
+                    clearTimeout(timeout)
+                    timeout = setTimeout(() => {
+                      broadcasts += 1
+
+                      addLocTime +=
+                        (currTime[0] - prevTime[0]) * 1e9 +
+                        (currTime[1] - prevTime[1])
+
+                      prevTime = process.hrtime()
+
+                      addOnBroadcastGroup(
+                        room,
+                        addPackets,
+                        pathIDs,
+                        addData,
+                        ITERATIONS,
+                        broadcasts,
+                        resolve,
+                      )
+                    }, addBroadcastGroupTimeout)
+                  }
+
+                  prevTime = process.hrtime()
+
+                  addOnInitFrontend(room, addPackets, pathIDs, addData)
+                }),
+            )
+            .then(() => {
+              broadcastListener.callback = null
+
+              dumpBSON(addPacketsFilename, addPackets)
+              addPackets = null
+
+              addRAM = captureHeapUsage()
+            })
+            .then(
+              () =>
+                new Promise((resolve) => {
+                  let broadcasts = 0
+
+                  broadcastListener.callback = (channel, message) => {
+                    currTime = process.hrtime()
+
+                    erasePackets[erasePackets.length - 1].push(message)
+                    eraseSize += message.message.length
+
+                    clearTimeout(timeout)
+                    timeout = setTimeout(() => {
+                      broadcasts += 1
+
+                      eraseLocTime +=
+                        (currTime[0] - prevTime[0]) * 1e9 +
+                        (currTime[1] - prevTime[1])
+
+                      prevTime = process.hrtime()
+
+                      eraseOnBroadcastGroup(
+                        room,
+                        erasePackets,
+                        pathIDs,
+                        eraseData,
+                        ITERATIONS,
+                        broadcasts,
+                        resolve,
+                      )
+                    }, eraseBroadcastGroupTimeout)
+                  }
+
+                  prevTime = process.hrtime()
+
+                  eraseOnInitFrontend(room, erasePackets, pathIDs, eraseData)
+                }),
+            )
+            .then(() => {
+              broadcastListener.callback = null
+
+              dumpBSON(erasePacketsFilename, erasePackets)
+              erasePackets = null
+
+              eraseRAM = captureHeapUsage()
+            })
+            .then(
+              () =>
+                new Promise((resolve) => {
+                  sendListener.callback = (uid, channel, message) => {
+                    const currTime = process.hrtime()
+                    syncLocTime =
+                      (currTime[0] - prevTime[0]) * 1e9 +
+                      (currTime[1] - prevTime[1])
+
+                    syncPackets.push(message)
+                    syncSize += message.message.length
+
+                    clearTimeout(timeout)
+                    timeout = setTimeout(
+                      () => syncOnSendGroup(syncPackets, resolve),
+                      syncSendGroupTimeout,
+                    )
+                  }
+
+                  prevTime = process.hrtime()
+
+                  getEventListener(
+                    "room",
+                    "messageReceived",
+                  )(createMessageReceivedEvent(syncStep1))
+                }),
+            )
+            .then(() => {
+              sendListener.callback = null
+
+              dumpBSON(syncPacketsFilename, syncPackets)
+              syncPackets = null
+
+              syncRAM = captureHeapUsage()
+
+              room.disconnect()
+              room = null
+
+              disconnectRAM = captureHeapUsage()
+            })
+            .then(
+              () =>
+                // eslint-disable-next-line no-async-promise-executor
+                new Promise(async (resolve) => {
+                  updateRoom = await connect("update", MockConnection)
+                  getEventListener(
+                    "update",
+                    "messageReceived",
+                  )(createMessageReceivedEvent(handshake, "tw-ml"))
+
+                  connectUpdRAM = captureHeapUsage()
+
+                  addPackets = loadBSON(addPacketsFilename)
+
+                  return resolve()
+                }),
+            )
+            .then(
+              () =>
+                new Promise((resolve) => {
+                  let broadcasts = 0
+
+                  let currTime
+
+                  const timeoutCallback = () => {
+                    broadcasts += 1
+
+                    addRemTime +=
+                      (currTime[0] - prevTime[0]) * 1e9 +
+                      (currTime[1] - prevTime[1])
+
+                    prevTime = process.hrtime()
+
+                    addOnEventGroup(addPackets, ITERATIONS, broadcasts, resolve)
+                  }
+
+                  updateRoom.addEventListener("addOrUpdatePath", () => {
+                    currTime = process.hrtime()
+
+                    addEvents += 1
+
+                    clearTimeout(timeout)
+                    timeout = setTimeout(timeoutCallback, addEventGroupTimeout)
+                  })
+
+                  updateRoom.addEventListener("removedIntervalsChange", () => {
+                    currTime = process.hrtime()
+
+                    addEvents += 1
+
+                    clearTimeout(timeout)
+                    timeout = setTimeout(timeoutCallback, addEventGroupTimeout)
+                  })
+
+                  prevTime = process.hrtime()
+
+                  addOnInitBackend(addPackets)
+                }),
+            )
+            .then(() => {
+              addPackets = null
+
+              addRemRAM = captureHeapUsage()
+
+              erasePackets = loadBSON(erasePacketsFilename)
+            })
+            .then(
+              () =>
+                new Promise((resolve) => {
+                  let broadcasts = 0
+
+                  let currTime
+
+                  const timeoutCallback = () => {
+                    broadcasts += 1
+
+                    eraseRemTime +=
+                      (currTime[0] - prevTime[0]) * 1e9 +
+                      (currTime[1] - prevTime[1])
+
+                    prevTime = process.hrtime()
+
+                    eraseOnEventGroup(
+                      erasePackets,
+                      ITERATIONS,
+                      broadcasts,
+                      resolve,
+                    )
+                  }
+
+                  updateRoom.addEventListener("removedIntervalsChange", () => {
+                    currTime = process.hrtime()
+
+                    eraseEvents += 1
+
+                    clearTimeout(timeout)
+                    timeout = setTimeout(
+                      timeoutCallback,
+                      eraseEventGroupTimeout,
+                    )
+                  })
+
+                  prevTime = process.hrtime()
+
+                  eraseOnInitBackend(erasePackets)
+                }),
+            )
+            .then(() => {
+              erasePackets = null
+
+              eraseRemRAM = captureHeapUsage()
+
+              updateRoom.disconnect()
+              updateRoom = null
+
+              disconnectUpdRAM = captureHeapUsage()
+            })
+            .then(
+              () =>
+                // eslint-disable-next-line no-async-promise-executor
+                new Promise(async (resolve) => {
+                  syncRoom = await connect("sync", MockConnection)
+                  getEventListener(
+                    "sync",
+                    "messageReceived",
+                  )(createMessageReceivedEvent(handshake, "tw-ml"))
+
+                  connectSyncRAM = captureHeapUsage()
+
+                  syncPackets = loadBSON(syncPacketsFilename)
+
+                  return resolve()
+                }),
+            )
+            .then(
+              () =>
+                new Promise((resolve) => {
+                  syncRoom.addEventListener("addOrUpdatePath", () => {
+                    const currTime = process.hrtime()
+
+                    syncRemTime =
+                      (currTime[0] - prevTime[0]) * 1e9 +
+                      (currTime[1] - prevTime[1])
+                    syncEvents += 1
+
+                    clearTimeout(timeout)
+                    timeout = setTimeout(
+                      () => syncOnEventGroup(resolve),
+                      syncEventGroupTimeout,
+                    )
+                  })
+
+                  syncRoom.addEventListener("removedIntervalsChange", () => {
+                    const currTime = process.hrtime()
+
+                    syncRemTime =
+                      (currTime[0] - prevTime[0]) * 1e9 +
+                      (currTime[1] - prevTime[1])
+                    syncEvents += 1
+
+                    clearTimeout(timeout)
+                    timeout = setTimeout(
+                      () => syncOnEventGroup(resolve),
+                      syncEventGroupTimeout,
+                    )
+                  })
+
+                  prevTime = process.hrtime()
+
+                  for (const syncPacket of syncPackets) {
+                    getEventListener(
+                      "sync",
+                      "messageReceived",
+                    )(createMessageReceivedEvent(syncPacket))
+                  }
+                }),
+            )
+            .then(() => {
+              syncPackets = null
+
+              syncRemRAM = captureHeapUsage()
+
+              syncRoom.disconnect()
+              syncRoom = null
+
+              disconnectSyncRAM = captureHeapUsage()
+            })
+            .then(() => {
+              if (!BENCHMARK) {
+                return
+              }
+
+              addPackets = loadBSON(addPacketsFilename).reduce(
+                (sum, packets) => sum + packets.length,
+                0,
+              )
+              erasePackets = loadBSON(erasePacketsFilename).reduce(
+                (sum, packets) => sum + packets.length,
+                0,
+              )
+              syncPackets = loadBSON(syncPacketsFilename).length
+
+              const results = {
+                addPath: {
+                  timeLoc: addLocTime,
+                  encodeRAM: addRAM,
+                  packets: addPackets,
+                  size: addSize,
+                  timeRem: addRemTime,
+                  decodeRAM: addRemRAM,
+                  events: addEvents,
+                },
+                extendErasureIntervals: {
+                  timeLoc: eraseLocTime,
+                  encodeRAM: eraseRAM,
+                  packets: erasePackets,
+                  size: eraseSize,
+                  timeRem: eraseRemTime,
+                  decodeRAM: eraseRemRAM,
+                  events: eraseEvents,
+                },
+                synchronisation: {
+                  timeLoc: syncLocTime,
+                  encodeRAM: syncRAM,
+                  packets: syncPackets,
+                  size: syncSize,
+                  timeRem: syncRemTime,
+                  decodeRAM: syncRemRAM,
+                  events: syncEvents,
+                },
+              }
+
+              printBenchmark(BENCHMARK, ITERATIONS, results)
+              appendBenchmark(FILENAME, ITERATIONS, results)
+            })
+        )
+      }),
+    Promise.resolve(),
+  )
+}
+
 describe("drawing app mesh", () => {
   beforeEach(() => {
     getUserID.mockClear()
@@ -96,13 +632,156 @@ describe("drawing app mesh", () => {
     destructor.mockClear()
     addEventListener.mockClear()
     MockConnection.mockClear()
-  })
 
-  it("benchmarks a single draw and erase update sequentially", () => {
-    const ITERATIONS = 1000
+    captureHeapUsage()
+  })
 
-    jest.setTimeout(ITERATIONS * 400)
+  const ITERATIONSLIST = [10, 25, 50, 75, 100, 250, 500]
+  const BLOCKSIZE = 10 // eslint-disable-line no-unused-vars
+
+  jest.setTimeout(1000 * 60 * 60)
+
+  it("benchmarks a dot draw and erase update sequentially", () => {
+    return runBidirectionalBenchmark(
+      "dot draw and erase [sequential]" /* BENCHMARK */,
+      "plots/dot-seq-benchmark.tsv",
+      ITERATIONSLIST /* ITERATIONSLIST */,
+      dotDraw /* addData */,
+      dotErase /* eraseData */,
+      function addOnInitFrontend(room, addPackets, pathIDs, addData) {
+        addPackets.push([])
+
+        const drawPathID = room.addPath(addData[0])
+
+        pathIDs.push(drawPathID)
+        for (let i = 1; i < addData.length; i++) {
+          room.extendPath(drawPathID, addData[i])
+        }
+      } /* addOnInitFrontend */,
+      100 /* addBroadcastGroupTimeout */,
+      function addOnBroadcastGroup(
+        room,
+        addPackets,
+        pathIDs,
+        addData,
+        ITERATIONS,
+        broadcasts,
+        resolve,
+      ) {
+        if (broadcasts < ITERATIONS) {
+          addPackets.push([])
+
+          const drawPathID = room.addPath(addData[0])
+          pathIDs.push(drawPathID)
+
+          for (let i = 1; i < addData.length; i++) {
+            room.extendPath(drawPathID, addData[i])
+          }
+        } else {
+          resolve()
+        }
+      } /* addOnBroadcastGroup */,
+      ".dot-seq-add.json" /* addPacketsFilename */,
+      function eraseOnInitFrontend(room, erasePackets, pathIDs, eraseData) {
+        erasePackets.push([])
+
+        const erasePathID = pathIDs[0]
+
+        for (let i = 0; i < eraseData.length; i++) {
+          room.extendErasureIntervals(
+            erasePathID,
+            eraseData[i][0],
+            eraseData[i][1],
+          )
+        }
+      } /* eraseOnInitFrontend */,
+      100 /* eraseBroadcastGroupTimeout */,
+      function eraseOnBroadcastGroup(
+        room,
+        erasePackets,
+        pathIDs,
+        eraseData,
+        ITERATIONS,
+        broadcasts,
+        resolve,
+      ) {
+        if (broadcasts < ITERATIONS) {
+          erasePackets.push([])
+
+          const erasePathID = pathIDs[broadcasts]
+
+          for (let i = 0; i < eraseData.length; i++) {
+            room.extendErasureIntervals(
+              erasePathID,
+              eraseData[i][0],
+              eraseData[i][1],
+            )
+          }
+        } else {
+          resolve()
+        }
+      } /* eraseOnBroadcastGroup */,
+      ".dot-seq-erase.json" /* erasePacketsFilename */,
+      1000 /* syncSendGroupTimeout */,
+      function syncOnSendGroup(syncPackets, resolve) {
+        resolve()
+      } /* syncOnSendGroup */,
+      ".dot-seq-sync.json" /* syncPacketsFilename */,
+      function addOnInitBackend(addPackets) {
+        for (const packet of addPackets[0]) {
+          getEventListener(
+            "update",
+            "messageReceived",
+          )(createMessageReceivedEvent(packet))
+        }
+      } /* addOnInitBackend */,
+      100 /* addEventGroupTimeout */,
+      function addOnEventGroup(addPackets, ITERATIONS, broadcasts, resolve) {
+        if (broadcasts >= ITERATIONS) {
+          return resolve()
+        }
+
+        for (const packet of addPackets[broadcasts]) {
+          getEventListener(
+            "update",
+            "messageReceived",
+          )(createMessageReceivedEvent(packet))
+        }
+      } /* addOnEventGroupTimeout */,
+      function eraseOnInitBackend(erasePackets) {
+        for (const packet of erasePackets[0]) {
+          getEventListener(
+            "update",
+            "messageReceived",
+          )(createMessageReceivedEvent(packet))
+        }
+      } /* eraseOnInitBackend */,
+      100 /* eraseEventGroupTimeout */,
+      function eraseOnEventGroup(
+        erasePackets,
+        ITERATIONS,
+        broadcasts,
+        resolve,
+      ) {
+        if (broadcasts >= ITERATIONS) {
+          return resolve()
+        }
+
+        for (const packet of erasePackets[broadcasts]) {
+          getEventListener(
+            "update",
+            "messageReceived",
+          )(createMessageReceivedEvent(packet))
+        }
+      } /* eraseOnEventGroupTimeout */,
+      1000 /* syncEventGroupTimeout */,
+      function snycOnEventGroup(resolve) {
+        resolve()
+      } /* syncOnEventGroup */,
+    )
+  })
 
+  /*it("benchmarks a single draw and erase update sequentially", () => {
     const dotIDs = []
     let prevTime
 
@@ -403,10 +1082,6 @@ describe("drawing app mesh", () => {
   })
 
   it("benchmarks a single draw and erase update in parallel", () => {
-    const ITERATIONS = 1000
-
-    jest.setTimeout(ITERATIONS * 30)
-
     const dotIDs = []
     let prevTime
 
@@ -714,8 +1389,6 @@ describe("drawing app mesh", () => {
   })
 
   it("communicates a single draw and erase update", () => {
-    jest.setTimeout(20000)
-
     let dotID
 
     let addPackets = []
@@ -1005,10 +1678,6 @@ describe("drawing app mesh", () => {
   })
 
   it("benchmarks a path draw and erase update sequentially", () => {
-    const ITERATIONS = 1000
-
-    jest.setTimeout(ITERATIONS * 1500)
-
     const pathIDs = []
     let prevTime
     let currTime
@@ -1344,11 +2013,6 @@ describe("drawing app mesh", () => {
   })
 
   it("benchmarks a path draw and erase update in parallel", () => {
-    const ITERATIONS = 1000
-    const BLOCKSIZE = 10
-
-    jest.setTimeout(ITERATIONS * 800)
-
     const pathIDs = []
     let prevTime
 
@@ -1686,8 +2350,6 @@ describe("drawing app mesh", () => {
   })
 
   it("communicates a path draw and erase update", () => {
-    jest.setTimeout(15000)
-
     let pathID
 
     let addPackets = []
@@ -1988,5 +2650,5 @@ describe("drawing app mesh", () => {
         syncRoom.disconnect()
         syncRoom = null
       })
-  })
+  })*/
 })
diff --git a/package-lock.json b/package-lock.json
index b01e3a0..2e16192 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,15 +14,15 @@
       }
     },
     "@babel/core": {
-      "version": "7.7.4",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.4.tgz",
-      "integrity": "sha512-+bYbx56j4nYBmpsWtnPUsKW3NdnYxbqyfrP2w9wILBuHzdfIKz9prieZK0DFPyIzkjYVUe4QkusGL07r5pXznQ==",
+      "version": "7.7.5",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.5.tgz",
+      "integrity": "sha512-M42+ScN4+1S9iB6f+TL7QBpoQETxbclx+KNoKJABghnKYE+fMzSGqst0BZJc8CpI625bwPwYgUyRvxZ+0mZzpw==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.5.5",
         "@babel/generator": "^7.7.4",
         "@babel/helpers": "^7.7.4",
-        "@babel/parser": "^7.7.4",
+        "@babel/parser": "^7.7.5",
         "@babel/template": "^7.7.4",
         "@babel/traverse": "^7.7.4",
         "@babel/types": "^7.7.4",
@@ -114,9 +114,9 @@
       }
     },
     "@babel/helper-module-transforms": {
-      "version": "7.7.4",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.7.4.tgz",
-      "integrity": "sha512-ehGBu4mXrhs0FxAqN8tWkzF8GSIGAiEumu4ONZ/hD9M88uHcD+Yu2ttKfOCgwzoesJOJrtQh7trI5YPbRtMmnA==",
+      "version": "7.7.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.7.5.tgz",
+      "integrity": "sha512-A7pSxyJf1gN5qXVcidwLWydjftUN878VkalhXX5iQDuGyiGK3sOrrKKHF4/A4fwHtnsotv/NipwAeLzY4KQPvw==",
       "dev": true,
       "requires": {
         "@babel/helper-module-imports": "^7.7.4",
@@ -188,9 +188,9 @@
       }
     },
     "@babel/parser": {
-      "version": "7.7.4",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.4.tgz",
-      "integrity": "sha512-jIwvLO0zCL+O/LmEJQjWA75MQTWwx3c3u2JOTDK5D3/9egrWRRA0/0hk9XXywYnXZVVpzrBYeIQTmhwUaePI9g==",
+      "version": "7.7.5",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.5.tgz",
+      "integrity": "sha512-KNlOe9+/nk4i29g0VXgl8PEXIRms5xKLJeuZ6UptN0fHv+jDiriG+y94X6qAgWTR0h3KaoM1wK5G5h7MHFRSig==",
       "dev": true
     },
     "@babel/plugin-syntax-object-rest-spread": {
@@ -203,12 +203,12 @@
       }
     },
     "@babel/plugin-transform-modules-commonjs": {
-      "version": "7.7.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.7.4.tgz",
-      "integrity": "sha512-k8iVS7Jhc367IcNF53KCwIXtKAH7czev866ThsTgy8CwlXjnKZna2VHwChglzLleYrcHz1eQEIJlGRQxB53nqA==",
+      "version": "7.7.5",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.7.5.tgz",
+      "integrity": "sha512-9Cq4zTFExwFhQI6MT1aFxgqhIsMWQWDVwOgLzl7PTWJHsNaqFvklAU+Oz6AQLAS0dJKTwZSOCo20INwktxpi3Q==",
       "dev": true,
       "requires": {
-        "@babel/helper-module-transforms": "^7.7.4",
+        "@babel/helper-module-transforms": "^7.7.5",
         "@babel/helper-plugin-utils": "^7.0.0",
         "@babel/helper-simple-access": "^7.7.4",
         "babel-plugin-dynamic-import-node": "^2.3.0"
@@ -578,9 +578,9 @@
       }
     },
     "@types/babel__generator": {
-      "version": "7.6.0",
-      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.0.tgz",
-      "integrity": "sha512-c1mZUu4up5cp9KROs/QAw0gTeHrw/x7m52LcnvMxxOZ03DmLwPV0MlGmlgzV3cnSdjhJOZsj7E7FHeioai+egw==",
+      "version": "7.6.1",
+      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz",
+      "integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==",
       "dev": true,
       "requires": {
         "@babel/types": "^7.0.0"
@@ -678,9 +678,9 @@
       "dev": true
     },
     "@types/node": {
-      "version": "12.12.14",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.14.tgz",
-      "integrity": "sha512-u/SJDyXwuihpwjXy7hOOghagLEV1KdAST6syfnOk6QZAMzZuWZqXy5aYYZbh8Jdpd4escVFP0MvftHNDb9pruA=="
+      "version": "12.12.20",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.20.tgz",
+      "integrity": "sha512-VAe+DiwpnC/g448uN+/3gRl4th0BTdrR9gSLIOHA+SUQskaYZQDOHG7xmjiE7JUhjbXnbXytf6Ih+/pA6CtMFQ=="
     },
     "@types/stack-utils": {
       "version": "1.0.1",
@@ -1125,9 +1125,9 @@
       "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
     },
     "asar": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/asar/-/asar-2.0.1.tgz",
-      "integrity": "sha512-Vo9yTuUtyFahkVMFaI6uMuX6N7k5DWa6a/8+7ov0/f8Lq9TVR0tUjzSzxQSxT1Y+RJIZgnP7BVb6Uhi+9cjxqA==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/asar/-/asar-2.0.3.tgz",
+      "integrity": "sha512-QdHKO+HOYVtE4B/M3up3i4LSJeJgsa2CTVBrjBf9GgLUPGGUFZowcdJ5yE4gOJuRAHNdqB9JFeRfFfaOu5x8Rw==",
       "dev": true,
       "requires": {
         "chromium-pickle-js": "^0.2.0",
@@ -2357,6 +2357,16 @@
       "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
       "dev": true
     },
+    "bindings": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+      "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "file-uri-to-path": "1.0.0"
+      }
+    },
     "blob": {
       "version": "0.0.5",
       "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
@@ -2700,9 +2710,9 @@
       "dev": true
     },
     "caniuse-lite": {
-      "version": "1.0.30001012",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001012.tgz",
-      "integrity": "sha512-7RR4Uh04t9K1uYRWzOJmzplgEOAXbfK72oVNokCdMzA67trrhPzy93ahKk1AWHiA0c58tD2P+NHqxrA8FZ+Trg==",
+      "version": "1.0.30001016",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001016.tgz",
+      "integrity": "sha512-yYQ2QfotceRiH4U+h1Us86WJXtVHDmy3nEKIdYPsZCYnOV5/tMgGbmoIlrMzmh2VXlproqYtVaKeGDBkMZifFA==",
       "dev": true
     },
     "canvas-renderer": {
@@ -3171,9 +3181,9 @@
       "dev": true
     },
     "core-js": {
-      "version": "2.6.10",
-      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.10.tgz",
-      "integrity": "sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA==",
+      "version": "2.6.11",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
+      "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
       "dev": true
     },
     "core-util-is": {
@@ -3629,9 +3639,9 @@
       "dev": true
     },
     "electron-to-chromium": {
-      "version": "1.3.320",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.320.tgz",
-      "integrity": "sha512-GVyRfGaKs/Vsf915WDaK5NG9vfud8nJFyapyQcrVS+sp8IeMpfml/YMvhthXsSOLlc0rzwdtnkNJE/+q4EPbTA==",
+      "version": "1.3.322",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.322.tgz",
+      "integrity": "sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA==",
       "dev": true
     },
     "elegant-spinner": {
@@ -3746,12 +3756,9 @@
           "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
         },
         "ws": {
-          "version": "7.2.0",
-          "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz",
-          "integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==",
-          "requires": {
-            "async-limiter": "^1.0.0"
-          }
+          "version": "7.2.1",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz",
+          "integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A=="
         }
       }
     },
@@ -3851,9 +3858,9 @@
       }
     },
     "es-abstract": {
-      "version": "1.16.2",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.2.tgz",
-      "integrity": "sha512-jYo/J8XU2emLXl3OLwfwtuFfuF2w6DYPs+xy9ZfVyPkDcrauu6LYrw/q2TyCtrbc/KUdCiC5e9UajRhgNkVopA==",
+      "version": "1.17.0-next.1",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0-next.1.tgz",
+      "integrity": "sha512-7MmGr03N7Rnuid6+wyhD9sHNE2n4tFSwExnU2lQl3lIo2ShXWGePY80zYaoMOmILWv57H0amMjZGHNzzGG70Rw==",
       "dev": true,
       "requires": {
         "es-to-primitive": "^1.2.1",
@@ -3864,6 +3871,7 @@
         "is-regex": "^1.0.4",
         "object-inspect": "^1.7.0",
         "object-keys": "^1.1.1",
+        "object.assign": "^4.1.0",
         "string.prototype.trimleft": "^2.1.0",
         "string.prototype.trimright": "^2.1.0"
       }
@@ -4192,6 +4200,12 @@
         "jest-regex-util": "^24.9.0"
       }
     },
+    "expose-gc": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/expose-gc/-/expose-gc-1.0.0.tgz",
+      "integrity": "sha512-ecOHrdm+zyOCGIwX18/1RHkUWgxDqGGRiGhaNC+42jReTtudbm2ID/DMa/wpaHwqy5YQHPZvsDqRM2F2iZ0uVA==",
+      "dev": true
+    },
     "express": {
       "version": "4.17.1",
       "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
@@ -4382,9 +4396,9 @@
       }
     },
     "fast-json-stable-stringify": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
-      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
       "dev": true
     },
     "fast-levenshtein": {
@@ -4403,12 +4417,12 @@
       }
     },
     "fb-watchman": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz",
-      "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz",
+      "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==",
       "dev": true,
       "requires": {
-        "bser": "^2.0.0"
+        "bser": "2.1.1"
       }
     },
     "figgy-pudding": {
@@ -4435,6 +4449,13 @@
         "flat-cache": "^2.0.1"
       }
     },
+    "file-uri-to-path": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+      "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+      "dev": true,
+      "optional": true
+    },
     "filesize": {
       "version": "3.6.1",
       "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
@@ -4618,14 +4639,15 @@
       "dev": true
     },
     "fsevents": {
-      "version": "1.2.9",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
-      "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
+      "version": "1.2.11",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.11.tgz",
+      "integrity": "sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==",
       "dev": true,
       "optional": true,
       "requires": {
+        "bindings": "^1.5.0",
         "nan": "^2.12.1",
-        "node-pre-gyp": "^0.12.0"
+        "node-pre-gyp": "*"
       },
       "dependencies": {
         "abbrev": {
@@ -4673,7 +4695,7 @@
           }
         },
         "chownr": {
-          "version": "1.1.1",
+          "version": "1.1.3",
           "bundled": true,
           "dev": true,
           "optional": true
@@ -4703,7 +4725,7 @@
           "optional": true
         },
         "debug": {
-          "version": "4.1.1",
+          "version": "3.2.6",
           "bundled": true,
           "dev": true,
           "optional": true,
@@ -4730,12 +4752,12 @@
           "optional": true
         },
         "fs-minipass": {
-          "version": "1.2.5",
+          "version": "1.2.7",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
-            "minipass": "^2.2.1"
+            "minipass": "^2.6.0"
           }
         },
         "fs.realpath": {
@@ -4761,7 +4783,7 @@
           }
         },
         "glob": {
-          "version": "7.1.3",
+          "version": "7.1.6",
           "bundled": true,
           "dev": true,
           "optional": true,
@@ -4790,7 +4812,7 @@
           }
         },
         "ignore-walk": {
-          "version": "3.0.1",
+          "version": "3.0.3",
           "bundled": true,
           "dev": true,
           "optional": true,
@@ -4809,7 +4831,7 @@
           }
         },
         "inherits": {
-          "version": "2.0.3",
+          "version": "2.0.4",
           "bundled": true,
           "dev": true,
           "optional": true
@@ -4851,7 +4873,7 @@
           "optional": true
         },
         "minipass": {
-          "version": "2.3.5",
+          "version": "2.9.0",
           "bundled": true,
           "dev": true,
           "optional": true,
@@ -4861,12 +4883,12 @@
           }
         },
         "minizlib": {
-          "version": "1.2.1",
+          "version": "1.3.3",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
-            "minipass": "^2.2.1"
+            "minipass": "^2.9.0"
           }
         },
         "mkdirp": {
@@ -4879,24 +4901,24 @@
           }
         },
         "ms": {
-          "version": "2.1.1",
+          "version": "2.1.2",
           "bundled": true,
           "dev": true,
           "optional": true
         },
         "needle": {
-          "version": "2.3.0",
+          "version": "2.4.0",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
-            "debug": "^4.1.0",
+            "debug": "^3.2.6",
             "iconv-lite": "^0.4.4",
             "sax": "^1.2.4"
           }
         },
         "node-pre-gyp": {
-          "version": "0.12.0",
+          "version": "0.14.0",
           "bundled": true,
           "dev": true,
           "optional": true,
@@ -4910,7 +4932,7 @@
             "rc": "^1.2.7",
             "rimraf": "^2.6.1",
             "semver": "^5.3.0",
-            "tar": "^4"
+            "tar": "^4.4.2"
           }
         },
         "nopt": {
@@ -4924,13 +4946,22 @@
           }
         },
         "npm-bundled": {
-          "version": "1.0.6",
+          "version": "1.1.1",
+          "bundled": true,
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "npm-normalize-package-bin": "^1.0.1"
+          }
+        },
+        "npm-normalize-package-bin": {
+          "version": "1.0.1",
           "bundled": true,
           "dev": true,
           "optional": true
         },
         "npm-packlist": {
-          "version": "1.4.1",
+          "version": "1.4.7",
           "bundled": true,
           "dev": true,
           "optional": true,
@@ -5001,7 +5032,7 @@
           "optional": true
         },
         "process-nextick-args": {
-          "version": "2.0.0",
+          "version": "2.0.1",
           "bundled": true,
           "dev": true,
           "optional": true
@@ -5042,7 +5073,7 @@
           }
         },
         "rimraf": {
-          "version": "2.6.3",
+          "version": "2.7.1",
           "bundled": true,
           "dev": true,
           "optional": true,
@@ -5069,7 +5100,7 @@
           "optional": true
         },
         "semver": {
-          "version": "5.7.0",
+          "version": "5.7.1",
           "bundled": true,
           "dev": true,
           "optional": true
@@ -5122,18 +5153,18 @@
           "optional": true
         },
         "tar": {
-          "version": "4.4.8",
+          "version": "4.4.13",
           "bundled": true,
           "dev": true,
           "optional": true,
           "requires": {
             "chownr": "^1.1.1",
             "fs-minipass": "^1.2.5",
-            "minipass": "^2.3.4",
-            "minizlib": "^1.1.1",
+            "minipass": "^2.8.6",
+            "minizlib": "^1.2.1",
             "mkdirp": "^0.5.0",
             "safe-buffer": "^5.1.2",
-            "yallist": "^3.0.2"
+            "yallist": "^3.0.3"
           }
         },
         "util-deprecate": {
@@ -5158,7 +5189,7 @@
           "optional": true
         },
         "yallist": {
-          "version": "3.0.3",
+          "version": "3.1.1",
           "bundled": true,
           "dev": true,
           "optional": true
@@ -5323,12 +5354,12 @@
       "dev": true
     },
     "graphlib": {
-      "version": "2.1.7",
-      "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.7.tgz",
-      "integrity": "sha512-TyI9jIy2J4j0qgPmOOrHTCtpPqJGN/aurBwc6ZT+bRii+di1I+Wv3obRhVrmBEXet+qkMaEX67dXrwsd3QQM6w==",
+      "version": "2.1.8",
+      "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
+      "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
       "dev": true,
       "requires": {
-        "lodash": "^4.17.5"
+        "lodash": "^4.17.15"
       }
     },
     "growly": {
@@ -5751,9 +5782,9 @@
       "dev": true
     },
     "inquirer": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz",
-      "integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==",
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.1.tgz",
+      "integrity": "sha512-V1FFQ3TIO15det8PijPLFR9M9baSlnRs9nL7zWu1MNVA2T9YVl9ZbrHJhYs7e9X8jeMZ3lr2JH/rdHFgNCBdYw==",
       "dev": true,
       "requires": {
         "ansi-escapes": "^4.2.1",
@@ -5765,7 +5796,7 @@
         "lodash": "^4.17.15",
         "mute-stream": "0.0.8",
         "run-async": "^2.2.0",
-        "rxjs": "^6.4.0",
+        "rxjs": "^6.5.3",
         "string-width": "^4.1.0",
         "strip-ansi": "^5.1.0",
         "through": "^2.3.6"
@@ -6037,12 +6068,12 @@
       "dev": true
     },
     "is-regex": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
-      "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
+      "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
       "dev": true,
       "requires": {
-        "has": "^1.0.1"
+        "has": "^1.0.3"
       }
     },
     "is-stream": {
@@ -7806,13 +7837,13 @@
       }
     },
     "object.getownpropertydescriptors": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
-      "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz",
+      "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==",
       "dev": true,
       "requires": {
-        "define-properties": "^1.1.2",
-        "es-abstract": "^1.5.1"
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.0-next.1"
       }
     },
     "object.pick": {
@@ -8318,9 +8349,9 @@
       "dev": true
     },
     "psl": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/psl/-/psl-1.5.0.tgz",
-      "integrity": "sha512-4vqUjKi2huMu1OJiLhi3jN6jeeKvMZdI1tYgi/njW5zV52jNLgSAZSdN16m9bJFe61/cT8ulmw4qFitV9QRsEA==",
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.6.0.tgz",
+      "integrity": "sha512-SYKKmVel98NCOYXpkwUqZqh0ahZeeKfmisiLIcEZdsb+WbLv02g/dI5BUmZnIyOe7RzZtLax81nnb2HbvC2tzA==",
       "dev": true
     },
     "public-encrypt": {
@@ -8703,9 +8734,9 @@
       "dev": true
     },
     "resolve": {
-      "version": "1.13.1",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz",
-      "integrity": "sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==",
+      "version": "1.14.0",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.14.0.tgz",
+      "integrity": "sha512-uviWSi5N67j3t3UKFxej1loCH0VZn5XuqdNxoLShPcYPw6cUZn74K1VRj+9myynRX03bxIBEkwlkob/ujLsJVw==",
       "dev": true,
       "requires": {
         "path-parse": "^1.0.6"
@@ -8946,9 +8977,9 @@
       }
     },
     "sdp": {
-      "version": "2.10.0",
-      "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.10.0.tgz",
-      "integrity": "sha512-H+VjfyQpRz9GezhshJmkXTtCAT9/2g9az3GFDPYfGOz0eAOQU1fCrL3S9Dq/eUT9FtOyLi/czdR9PzK3fKUYOQ=="
+      "version": "2.11.0",
+      "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.11.0.tgz",
+      "integrity": "sha512-3goRWORJWhZyNQWsAH3X0Z4pb4MhZFBRT/665m+LzYmmp2E9w2ifTbPl1J2EejOSrTJ10Kr3obY0yHZEN11Iaw=="
     },
     "semver": {
       "version": "6.3.0",
@@ -8984,9 +9015,9 @@
       }
     },
     "serialize-javascript": {
-      "version": "1.9.1",
-      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz",
-      "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==",
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
+      "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==",
       "dev": true
     },
     "serve-static": {
@@ -9525,9 +9556,9 @@
       }
     },
     "stream-shift": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz",
-      "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
+      "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
       "dev": true
     },
     "string-length": {
@@ -9580,14 +9611,13 @@
       }
     },
     "string.prototype.padend": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz",
-      "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.0.tgz",
+      "integrity": "sha512-3aIv8Ffdp8EZj8iLwREGpQaUZiPyrWrpzMBHvkiSW/bK/EGve9np07Vwy7IJ5waydpGXzQZu/F8Oze2/IWkBaA==",
       "dev": true,
       "requires": {
-        "define-properties": "^1.1.2",
-        "es-abstract": "^1.4.3",
-        "function-bind": "^1.0.2"
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.0-next.1"
       }
     },
     "string.prototype.trimleft": {
@@ -9719,9 +9749,9 @@
       "dev": true
     },
     "terser": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-4.4.1.tgz",
-      "integrity": "sha512-e05giplw+8sIYh50qXYHZmr0b76O5dOSm9JwSDebGFLri4ItYzxsnumiAK+yuI56R+H7uIjT9KbVEKNkrprzHw==",
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-4.4.3.tgz",
+      "integrity": "sha512-0ikKraVtRDKGzHrzkCv5rUNDzqlhmhowOBqC0XqUHFpW+vJ45+20/IFBcebwKfiS2Z9fJin6Eo+F1zLZsxi8RA==",
       "dev": true,
       "requires": {
         "commander": "^2.20.0",
@@ -9730,16 +9760,16 @@
       }
     },
     "terser-webpack-plugin": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz",
-      "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==",
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
+      "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
       "dev": true,
       "requires": {
         "cacache": "^12.0.2",
         "find-cache-dir": "^2.1.0",
         "is-wsl": "^1.1.0",
         "schema-utils": "^1.0.0",
-        "serialize-javascript": "^1.7.0",
+        "serialize-javascript": "^2.1.2",
         "source-map": "^0.6.1",
         "terser": "^4.1.2",
         "webpack-sources": "^1.4.0",
@@ -9840,9 +9870,9 @@
       },
       "dependencies": {
         "@types/node": {
-          "version": "10.17.6",
-          "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.6.tgz",
-          "integrity": "sha512-0a2X6cgN3RdPBL2MIlR6Lt0KlM7fOFsutuXcdglcOq6WvLnYXgPQSh0Mx6tO1KCAE8MxbHSOSTWDoUxRq+l3DA==",
+          "version": "10.17.11",
+          "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.11.tgz",
+          "integrity": "sha512-dNd2pp8qTzzNLAs3O8nH3iU9DG9866KHq9L3ISPB7DOGERZN81nW/5/g/KzMJpCU8jrbCiMRBzV9/sCEdRosig==",
           "dev": true
         },
         "chalk": {
@@ -10152,9 +10182,9 @@
           }
         },
         "nanoid": {
-          "version": "2.1.7",
-          "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.7.tgz",
-          "integrity": "sha512-fmS3qwDldm4bE01HCIRqNk+f255CNjnAoeV3Zzzv0KemObHKqYgirVaZA9DtKcjogicWjYcHkJs4D5A8CjnuVQ==",
+          "version": "2.1.8",
+          "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.8.tgz",
+          "integrity": "sha512-g1z+n5s26w0TGKh7gjn7HCqurNKMZWzH08elXzh/gM/csQHd/UqDV6uxMghQYg9IvqRPm1QpeMk50YMofHvEjQ==",
           "dev": true
         },
         "npm-run-path": {
@@ -10619,9 +10649,9 @@
       }
     },
     "tree-kill": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz",
-      "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+      "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
       "dev": true
     },
     "trim-right": {
@@ -10709,15 +10739,15 @@
       "dev": true
     },
     "typescript": {
-      "version": "3.7.2",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz",
-      "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==",
+      "version": "3.7.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz",
+      "integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==",
       "dev": true
     },
     "uglify-js": {
-      "version": "3.7.1",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.1.tgz",
-      "integrity": "sha512-pnOF7jY82wdIhATVn87uUY/FHU+MDUdPLkmGFvGoclQmeu229eTkbG5gjGGBi3R7UuYYSEeYXY/TTY5j2aym2g==",
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.2.tgz",
+      "integrity": "sha512-uhRwZcANNWVLrxLfNFEdltoPNhECUR3lc+UdJoG9CBpMcSnKyWA94tc3eAujB1GcMY5Uwq8ZMp4qWpxWYDQmaA==",
       "dev": true,
       "optional": true,
       "requires": {
@@ -10978,9 +11008,9 @@
       "dev": true
     },
     "webpack": {
-      "version": "4.41.2",
-      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.2.tgz",
-      "integrity": "sha512-Zhw69edTGfbz9/8JJoyRQ/pq8FYUoY0diOXqW0T6yhgdhCv6wr0hra5DwwWexNRns2Z2+gsnrNcbe9hbGBgk/A==",
+      "version": "4.41.3",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.3.tgz",
+      "integrity": "sha512-EcNzP9jGoxpQAXq1VOoTet0ik7/VVU1MovIfcUSAjLowc7GhcQku/sOXALvq5nPpSei2HF6VRhibeJSC3i/Law==",
       "dev": true,
       "requires": {
         "@webassemblyjs/ast": "1.8.5",
@@ -11003,7 +11033,7 @@
         "node-libs-browser": "^2.2.1",
         "schema-utils": "^1.0.0",
         "tapable": "^1.1.3",
-        "terser-webpack-plugin": "^1.4.1",
+        "terser-webpack-plugin": "^1.4.3",
         "watchpack": "^1.6.0",
         "webpack-sources": "^1.4.1"
       },
diff --git a/package.json b/package.json
index 049c501..ec0cef4 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
     "test": "jest --testPathIgnorePatterns .*.data.js .*benchmark.test.js src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker",
     "test-changed": "jest --only-changed --testPathIgnorePatterns __tests__/*.data.js src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker",
     "test-coverage": "jest --coverage --testPathIgnorePatterns __tests__/*.data.js src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker",
-    "test-benchmark": "jest --testPathPattern .*benchmark.test.js --testPathIgnorePatterns .*.data.js src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker",
+    "test-benchmark": "jest --testPathPattern .*benchmark.test.js --testPathIgnorePatterns .*.data.js src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker --runInBand",
     "test-e2e:peer1": "testcafe chrome:headless __e2e_tests__/peer1.e2e.js",
     "test-e2e:peer2": "testcafe chrome:headless __e2e_tests__/peer2.e2e.js",
     "test-e2e": "run-p test-e2e:*",
@@ -27,7 +27,8 @@
     "format": "prettier --ignore-path .gitignore --check --write '**/*.{html,js,json,md}'",
     "format-check": "prettier --ignore-path .gitignore --check '**/*.{html,js,json,md}'",
     "lint": "eslint --ignore-path .gitignore '**/*.js'",
-    "validate": "npm ls"
+    "validate": "npm ls",
+    "plot": "find plot-scripts/ -maxdepth 1 -type f -name '*.p' -exec gnuplot {} \\;"
   },
   "dependencies": {
     "@ungap/event-target": "^0.1.0",
@@ -55,6 +56,7 @@
     "eslint": "^6.5.1",
     "eslint-config-prettier": "^6.5.0",
     "eslint-plugin-testcafe": "^0.2.1",
+    "expose-gc": "^1.0.0",
     "jest": "^24.9.0",
     "npm-run-all": "^4.1.5",
     "prettier": "^1.18.2",
diff --git a/plot-scripts/demo.p b/plot-scripts/demo.p
new file mode 100644
index 0000000..e0fcdf5
--- /dev/null
+++ b/plot-scripts/demo.p
@@ -0,0 +1,14 @@
+set xlabel "Iterations"
+set ylabel "Time [ms]"
+set title "addPath() performance scalability"
+set key inside bottom right
+
+set terminal dumb size 120, 30
+set autoscale
+
+plot "plots/dot-seq-benchmark.tsv" using 1:($2/(1e6*$1)) with lines title "dot [sequential]"
+
+set terminal pdf
+set output "plots/dot-seq-benchmark.pdf"
+
+plot "plots/dot-seq-benchmark.tsv" using 1:($2/(1e6*$1)) with lines title "dot [sequential]"
diff --git a/plots/.gitkeep b/plots/.gitkeep
new file mode 100644
index 0000000..e69de29
-- 
GitLab