diff --git a/.gitignore b/.gitignore
index 174b618b9ddb65d99bcce1488e24bc8bec4bd02e..66c5fc96b346fa69bfbbefce0b84adae5e9f1434 100644
--- a/.gitignore
+++ b/.gitignore
@@ -167,6 +167,7 @@ typings/
 
 # react / gatsby (customised)
 public/js
+public/benchmarks.html
 
 # vuepress build output
 .vuepress/dist
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 751523dc927761761ac93f02f576255b921ecb07..9a1558a8447288471a828f5cbe2b4c920fb7b8dd 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -117,7 +117,9 @@ benchmark:
     - master
   script:
     - apt-get -y install gnuplot
-    - npm run test-benchmark
+    - npm run build:bench
+    - cp __benchmarks__/benchmarks.html public/benchmarks.html
+    - npm run benchmarks
     - npm run plot
   artifacts:
     paths:
diff --git a/__benchmarks__/benchmarks.html b/__benchmarks__/benchmarks.html
new file mode 100644
index 0000000000000000000000000000000000000000..5a7b264ef1b61bd8c791bd09ed268d31ce1e6dd2
--- /dev/null
+++ b/__benchmarks__/benchmarks.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+  </head>
+  <body>
+    <script src="js/benchmarks.js"></script>
+  </body>
+</html>
diff --git a/__tests__/benchmark.test.js b/__benchmarks__/benchmarks.js
similarity index 66%
rename from __tests__/benchmark.test.js
rename to __benchmarks__/benchmarks.js
index 96617ef4036186c86964a82a120ebbddcfe9883d..7dd3befc821a6c8f2ae8da09198b0d1a4cf29c13 100644
--- a/__tests__/benchmark.test.js
+++ b/__benchmarks__/benchmarks.js
@@ -1,36 +1,94 @@
-import fs from "fs"
+import { test, equal, ok, notOk } from "zora"
+
 import chalk from "chalk"
-import gc from "expose-gc/function"
 
 import { connect } from "../src/room.js"
-
+import WasmCRDT from "../src/wasm-crdt.js"
 import MockConnection, {
-  getUserID,
-  getPeerHandle,
-  getPeerFootprint,
-  send,
+  userID,
   sendListener,
-  broadcast,
   broadcastListener,
-  terminatePeer,
-  destructor,
-  addEventListener,
   getEventListener,
 } from "../src/connection/MockConnection.js"
 
 import {
-  createMessageReceivedEvent,
+  createMessageReceivedEvent as _createMessageReceivedEvent,
   handshake,
-  syncStep1,
-  syncDone,
   dotDraw,
   dotErase,
   pathDraw,
   pathErase,
-} from "./benchmark.data.js"
-
-// Adapted from https://github.com/jprichardson/buffer-json (MIT license)
+} from "./data.js"
+
+const remoteUserID = "392bf960-1d18-4482-bbbf-1c85e0132c9a"
+
+const createMessageReceivedEvent = (message, channel = "crdt") =>
+  _createMessageReceivedEvent(message, channel, remoteUserID)
+
+const syncStep1 = {
+  uuid: "6e20b20d-e1d8-405d-8a61-d56cb1c47a24",
+  message: Uint8Array.of(
+    130,
+    164,
+    116,
+    121,
+    112,
+    101,
+    171,
+    115,
+    121,
+    110,
+    99,
+    32,
+    115,
+    116,
+    101,
+    112,
+    32,
+    49,
+    167,
+    109,
+    101,
+    115,
+    115,
+    97,
+    103,
+    101,
+    196,
+    3,
+    0,
+    0,
+    0,
+  ),
+  slice: 0,
+  length: 1,
+  compressed: false,
+}
+const syncDone = {
+  message: Uint8Array.of(
+    129,
+    164,
+    116,
+    121,
+    112,
+    101,
+    169,
+    115,
+    121,
+    110,
+    99,
+    32,
+    100,
+    111,
+    110,
+    101,
+  ),
+  slice: 0,
+  length: 1,
+  compressed: false,
+}
 
+// Start: Adapted from https://github.com/jprichardson/buffer-json (MIT license)
 function stringify(value, space) {
   return JSON.stringify(value, replacer, space)
 }
@@ -52,17 +110,73 @@ function reviver(key, value) {
   }
   return value
 }
+// End: Adapted from https://github.com/jprichardson/buffer-json (MIT license)
+
+let fs
 
-function dumpBSON(filename, data) {
-  return fs.writeFileSync(filename, stringify(data))
+async function initFileSystem() {
+  fs = await new Promise((resolve, reject) =>
+    window.webkitRequestFileSystem(
+      window.TEMPORARY,
+      1024 * 1024 * 100,
+      resolve,
+      reject,
+    ),
+  )
 }
 
-function loadBSON(filename) {
-  return parse(fs.readFileSync(filename))
+async function dumpBSON(filename, data) {
+  await new Promise((resolve) => {
+    fs.root.getFile(
+      filename,
+      { create: false },
+      (fileEntry) => {
+        fileEntry.remove(resolve, console.error)
+      },
+      resolve,
+    )
+  })
+
+  await new Promise((resolve) => {
+    fs.root.getFile(
+      filename,
+      { create: true, exclusive: true },
+      (fileEntry) => {
+        fileEntry.createWriter((fileWriter) => {
+          fileWriter.onwriteend = resolve
+          fileWriter.onerror = console.error
+
+          const blob = new Blob([stringify(data)], { type: "text/plain" })
+
+          fileWriter.write(blob)
+        }, console.error)
+      },
+      console.error,
+    )
+  })
+}
+
+async function loadBSON(filename) {
+  return await new Promise((resolve) => {
+    fs.root.getFile(
+      filename,
+      {},
+      (fileEntry) => {
+        fileEntry.file((file) => {
+          const reader = new FileReader()
+
+          reader.onloadend = () => resolve(parse(reader.result))
+
+          reader.readAsText(file)
+        }, console.error)
+      },
+      console.error,
+    )
+  })
 }
 
 function printBenchmark(title, iterations, results) {
-  process.stdout.write(`\n  ${title} (${iterations} iterations):\n`)
+  console.debug(`\n  ${title} (${iterations} iterations):\n`)
 
   for (const title in results) {
     const {
@@ -76,10 +190,10 @@ function printBenchmark(title, iterations, results) {
     } = results[title]
     const synchronisation = title == "synchronisation"
 
-    process.stdout.write(
+    console.debug(
       chalk`    {yellow ⧗} {dim ${title}:} {yellow.inverse ${(
         timeLoc /
-        (1e6 * (synchronisation ? 1 : iterations))
+        (1e3 * (synchronisation ? 1 : iterations))
       ).toFixed(3)}ms ${synchronisation ? "total" : "/ it"}} + {red.inverse ${(
         encodeRAM /
         (1024 * 1024)
@@ -87,7 +201,7 @@ function printBenchmark(title, iterations, results) {
         3,
       )}MB} => {dim ${packets} packet(s)} => {magenta.inverse ${size}B} => {yellow.inverse ${(
         timeRem /
-        (1e6 * (synchronisation ? 1 : iterations))
+        (1e3 * (synchronisation ? 1 : iterations))
       ).toFixed(3)}ms ${synchronisation ? "total" : "/ it"}} + {red.inverse ${(
         decodeRAM /
         (1024 * 1024)
@@ -95,60 +209,23 @@ function printBenchmark(title, iterations, results) {
     )
   }
 
-  process.stdout.write(`\n`)
+  console.debug(`\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`,
-  )
+  console.info(JSON.stringify({ filename, title, results }))
 }
 
 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`)
+  console.info(JSON.stringify({ filename, iterations, results }))
 }
 
 function captureHeapUsage() {
   for (let i = 0; i < 10; i++) {
-    gc()
+    window.gc()
   }
 
-  return process.memoryUsage().heapUsed
+  return performance.memory.usedJSHeapSize
 }
 
 function runBidirectionalBenchmark(
@@ -248,7 +325,9 @@ function runBidirectionalBenchmark(
         return (
           // eslint-disable-next-line no-async-promise-executor
           new Promise(async (resolve) => {
-            room = await connect("room", MockConnection)
+            userID.uuid = "61a540d6-4522-48c7-a660-1ed501503cb7"
+
+            room = await connect("room", WasmCRDT, MockConnection)
             getEventListener(
               "room",
               "messageReceived",
@@ -264,10 +343,10 @@ function runBidirectionalBenchmark(
                   let broadcasts = 0
 
                   broadcastListener.callback = (channel, message) => {
-                    currTime = process.hrtime()
+                    currTime = window.performance.now()
 
-                    expect(channel).toEqual("y-js")
-                    expect(message.message instanceof Uint8Array).toBe(true)
+                    equal(channel, "crdt")
+                    ok(message.message instanceof Uint8Array)
 
                     addPackets[addPackets.length - 1].push(message)
                     addSize += message.message.length
@@ -276,11 +355,9 @@ function runBidirectionalBenchmark(
                     timeout = setTimeout(() => {
                       broadcasts += 1
 
-                      addLocTime +=
-                        (currTime[0] - prevTime[0]) * 1e9 +
-                        (currTime[1] - prevTime[1])
+                      addLocTime += (currTime - prevTime) * 1e3
 
-                      prevTime = process.hrtime()
+                      prevTime = window.performance.now()
 
                       addOnBroadcastGroup(
                         room,
@@ -294,7 +371,7 @@ function runBidirectionalBenchmark(
                     }, addBroadcastGroupTimeout)
                   }
 
-                  prevTime = process.hrtime()
+                  prevTime = window.performance.now()
 
                   addOnInitFrontend(
                     room,
@@ -306,10 +383,10 @@ function runBidirectionalBenchmark(
                   )
                 }),
             )
-            .then(() => {
+            .then(async () => {
               broadcastListener.callback = null
 
-              dumpBSON(addPacketsFilename, addPackets)
+              await dumpBSON(addPacketsFilename, addPackets)
               addPackets = null
 
               addRAM = captureHeapUsage()
@@ -320,10 +397,10 @@ function runBidirectionalBenchmark(
                   let broadcasts = 0
 
                   broadcastListener.callback = (channel, message) => {
-                    currTime = process.hrtime()
+                    currTime = window.performance.now()
 
-                    expect(channel).toEqual("y-js")
-                    expect(message.message instanceof Uint8Array).toBe(true)
+                    equal(channel, "crdt")
+                    ok(message.message instanceof Uint8Array)
 
                     erasePackets[erasePackets.length - 1].push(message)
                     eraseSize += message.message.length
@@ -332,11 +409,9 @@ function runBidirectionalBenchmark(
                     timeout = setTimeout(() => {
                       broadcasts += 1
 
-                      eraseLocTime +=
-                        (currTime[0] - prevTime[0]) * 1e9 +
-                        (currTime[1] - prevTime[1])
+                      eraseLocTime += (currTime - prevTime) * 1e3
 
-                      prevTime = process.hrtime()
+                      prevTime = window.performance.now()
 
                       eraseOnBroadcastGroup(
                         room,
@@ -350,7 +425,7 @@ function runBidirectionalBenchmark(
                     }, eraseBroadcastGroupTimeout)
                   }
 
-                  prevTime = process.hrtime()
+                  prevTime = window.performance.now()
 
                   eraseOnInitFrontend(
                     room,
@@ -362,10 +437,10 @@ function runBidirectionalBenchmark(
                   )
                 }),
             )
-            .then(() => {
+            .then(async () => {
               broadcastListener.callback = null
 
-              dumpBSON(erasePacketsFilename, erasePackets)
+              await dumpBSON(erasePacketsFilename, erasePackets)
               erasePackets = null
 
               eraseRAM = captureHeapUsage()
@@ -374,15 +449,13 @@ function runBidirectionalBenchmark(
               () =>
                 new Promise((resolve) => {
                   sendListener.callback = (uid, channel, message) => {
-                    const currTime = process.hrtime()
+                    const currTime = window.performance.now()
 
-                    expect(uid).toEqual("moritz")
-                    expect(channel).toEqual("y-js")
-                    expect(message.message instanceof Uint8Array).toBe(true)
+                    equal(uid, remoteUserID)
+                    equal(channel, "crdt")
+                    ok(message.message instanceof Uint8Array)
 
-                    syncLocTime =
-                      (currTime[0] - prevTime[0]) * 1e9 +
-                      (currTime[1] - prevTime[1])
+                    syncLocTime = (currTime - prevTime) * 1e3
 
                     syncPackets.push(message)
                     syncSize += message.message.length
@@ -394,7 +467,7 @@ function runBidirectionalBenchmark(
                     )
                   }
 
-                  prevTime = process.hrtime()
+                  prevTime = window.performance.now()
 
                   getEventListener(
                     "room",
@@ -402,10 +475,10 @@ function runBidirectionalBenchmark(
                   )(createMessageReceivedEvent(syncStep1))
                 }),
             )
-            .then(() => {
+            .then(async () => {
               sendListener.callback = null
 
-              dumpBSON(syncPacketsFilename, syncPackets)
+              await dumpBSON(syncPacketsFilename, syncPackets)
               syncPackets = null
 
               syncRAM = captureHeapUsage()
@@ -419,7 +492,9 @@ function runBidirectionalBenchmark(
               () =>
                 // eslint-disable-next-line no-async-promise-executor
                 new Promise(async (resolve) => {
-                  updateRoom = await connect("update", MockConnection)
+                  userID.uuid = "5c9e550b-3de8-4a32-80e1-80c08c19891a"
+
+                  updateRoom = await connect("update", WasmCRDT, MockConnection)
                   getEventListener(
                     "update",
                     "messageReceived",
@@ -427,7 +502,7 @@ function runBidirectionalBenchmark(
 
                   connectUpdRAM = captureHeapUsage()
 
-                  addPackets = loadBSON(addPacketsFilename)
+                  addPackets = await loadBSON(addPacketsFilename)
 
                   return resolve()
                 }),
@@ -442,11 +517,9 @@ function runBidirectionalBenchmark(
                   const timeoutCallback = () => {
                     broadcasts += 1
 
-                    addRemTime +=
-                      (currTime[0] - prevTime[0]) * 1e9 +
-                      (currTime[1] - prevTime[1])
+                    addRemTime += (currTime - prevTime) * 1e3
 
-                    prevTime = process.hrtime()
+                    prevTime = window.performance.now()
 
                     addOnEventGroup(
                       addPackets,
@@ -460,7 +533,7 @@ function runBidirectionalBenchmark(
                   }
 
                   addEventListener = (event) => {
-                    currTime = process.hrtime()
+                    currTime = window.performance.now()
 
                     addEvents.push(addEventsCache ? { add: event } : null)
 
@@ -468,7 +541,7 @@ function runBidirectionalBenchmark(
                     timeout = setTimeout(timeoutCallback, addEventGroupTimeout)
                   }
                   eraseEventListener = (event) => {
-                    currTime = process.hrtime()
+                    currTime = window.performance.now()
 
                     addEvents.push(addEventsCache ? { erase: event } : null)
 
@@ -485,12 +558,12 @@ function runBidirectionalBenchmark(
                     eraseEventListener,
                   )
 
-                  prevTime = process.hrtime()
+                  prevTime = window.performance.now()
 
                   addOnInitBackend(addPackets, BLOCKSIZE)
                 }),
             )
-            .then(() => {
+            .then(async () => {
               updateRoom.removeEventListener(
                 "addOrUpdatePath",
                 addEventListener,
@@ -505,12 +578,12 @@ function runBidirectionalBenchmark(
 
               addPackets = null
 
-              dumpBSON(addEventsFilename, addEvents)
+              await dumpBSON(addEventsFilename, addEvents)
               addEvents = null
 
               addRemRAM = captureHeapUsage()
 
-              erasePackets = loadBSON(erasePacketsFilename)
+              erasePackets = await loadBSON(erasePacketsFilename)
             })
             .then(
               () =>
@@ -522,11 +595,9 @@ function runBidirectionalBenchmark(
                   const timeoutCallback = () => {
                     broadcasts += 1
 
-                    eraseRemTime +=
-                      (currTime[0] - prevTime[0]) * 1e9 +
-                      (currTime[1] - prevTime[1])
+                    eraseRemTime += (currTime - prevTime) * 1e3
 
-                    prevTime = process.hrtime()
+                    prevTime = window.performance.now()
 
                     eraseOnEventGroup(
                       erasePackets,
@@ -541,7 +612,7 @@ function runBidirectionalBenchmark(
                   }
 
                   eraseEventListener = (event) => {
-                    currTime = process.hrtime()
+                    currTime = window.performance.now()
 
                     eraseEvents.push(eraseEventsCache ? { erase: event } : null)
 
@@ -557,12 +628,12 @@ function runBidirectionalBenchmark(
                     eraseEventListener,
                   )
 
-                  prevTime = process.hrtime()
+                  prevTime = window.performance.now()
 
                   eraseOnInitBackend(erasePackets, BLOCKSIZE)
                 }),
             )
-            .then(() => {
+            .then(async () => {
               updateRoom.removeEventListener(
                 "removedIntervalsChange",
                 eraseEventListener,
@@ -571,7 +642,7 @@ function runBidirectionalBenchmark(
 
               erasePackets = null
 
-              dumpBSON(eraseEventsFilename, eraseEvents)
+              await dumpBSON(eraseEventsFilename, eraseEvents)
               eraseEvents = null
 
               eraseRemRAM = captureHeapUsage()
@@ -585,7 +656,9 @@ function runBidirectionalBenchmark(
               () =>
                 // eslint-disable-next-line no-async-promise-executor
                 new Promise(async (resolve) => {
-                  syncRoom = await connect("sync", MockConnection)
+                  userID.uuid = "a2108f84-3785-4696-8dd5-fb89b38d4f7f"
+
+                  syncRoom = await connect("sync", WasmCRDT, MockConnection)
                   getEventListener(
                     "sync",
                     "messageReceived",
@@ -593,7 +666,7 @@ function runBidirectionalBenchmark(
 
                   connectSyncRAM = captureHeapUsage()
 
-                  syncPackets = loadBSON(syncPacketsFilename)
+                  syncPackets = await loadBSON(syncPacketsFilename)
 
                   return resolve()
                 }),
@@ -602,11 +675,9 @@ function runBidirectionalBenchmark(
               () =>
                 new Promise((resolve) => {
                   addEventListener = (event) => {
-                    const currTime = process.hrtime()
+                    const currTime = window.performance.now()
 
-                    syncRemTime =
-                      (currTime[0] - prevTime[0]) * 1e9 +
-                      (currTime[1] - prevTime[1])
+                    syncRemTime = (currTime - prevTime) * 1e3
 
                     syncEvents.push(syncEventsCache ? { add: event } : null)
 
@@ -624,11 +695,9 @@ function runBidirectionalBenchmark(
                     )
                   }
                   eraseEventListener = (event) => {
-                    const currTime = process.hrtime()
+                    const currTime = window.performance.now()
 
-                    syncRemTime =
-                      (currTime[0] - prevTime[0]) * 1e9 +
-                      (currTime[1] - prevTime[1])
+                    syncRemTime = (currTime - prevTime) * 1e3
 
                     syncEvents.push(syncEventsCache ? { erase: event } : null)
 
@@ -652,7 +721,7 @@ function runBidirectionalBenchmark(
                     eraseEventListener,
                   )
 
-                  prevTime = process.hrtime()
+                  prevTime = window.performance.now()
 
                   for (const syncPacket of syncPackets) {
                     getEventListener(
@@ -662,7 +731,7 @@ function runBidirectionalBenchmark(
                   }
                 }),
             )
-            .then(() => {
+            .then(async () => {
               syncRoom.removeEventListener("addOrUpdatePath", addEventListener)
               addEventListener = null
 
@@ -674,7 +743,7 @@ function runBidirectionalBenchmark(
 
               syncPackets = null
 
-              dumpBSON(syncEventsFilename, syncEvents)
+              await dumpBSON(syncEventsFilename, syncEvents)
               syncEvents = null
 
               syncRemRAM = captureHeapUsage()
@@ -684,24 +753,24 @@ function runBidirectionalBenchmark(
 
               disconnectSyncRAM = captureHeapUsage()
             })
-            .then(() => {
+            .then(async () => {
               if (!BENCHMARK) {
                 return
               }
 
-              addPackets = loadBSON(addPacketsFilename).reduce(
+              addPackets = (await loadBSON(addPacketsFilename)).reduce(
                 (sum, packets) => sum + packets.length,
                 0,
               )
-              erasePackets = loadBSON(erasePacketsFilename).reduce(
+              erasePackets = (await loadBSON(erasePacketsFilename)).reduce(
                 (sum, packets) => sum + packets.length,
                 0,
               )
-              syncPackets = loadBSON(syncPacketsFilename).length
+              syncPackets = (await loadBSON(syncPacketsFilename)).length
 
-              addEvents = loadBSON(addEventsFilename).length
-              eraseEvents = loadBSON(eraseEventsFilename).length
-              syncEvents = loadBSON(syncEventsFilename).length
+              addEvents = (await loadBSON(addEventsFilename)).length
+              eraseEvents = (await loadBSON(eraseEventsFilename)).length
+              syncEvents = (await loadBSON(syncEventsFilename)).length
 
               const results = {
                 addPath: {
@@ -753,11 +822,13 @@ function addOnInitFrontendSequential(
   addPackets.push([])
 
   const drawPathID = room.addPath(addData[0])
-
   pathIDs.push(drawPathID)
+
   for (let i = 1; i < addData.length; i++) {
     room.extendPath(drawPathID, addData[i])
   }
+
+  room.endPath(drawPathID)
 }
 
 function addOnBroadcastGroupSequential(
@@ -778,6 +849,8 @@ function addOnBroadcastGroupSequential(
     for (let i = 1; i < addData.length; i++) {
       room.extendPath(drawPathID, addData[i])
     }
+
+    room.endPath(drawPathID)
   } else {
     resolve()
   }
@@ -919,11 +992,13 @@ function addOnInitFrontendParallel(
 
     for (let j = sj; j < Math.min(sj + BLOCKSIZE, ITERATIONS); j++) {
       const drawPathID = room.addPath(addData[0])
-
       pathIDs.push(drawPathID)
+
       for (let i = 1; i < addData.length; i++) {
         room.extendPath(drawPathID, addData[i])
       }
+
+      room.endPath(drawPathID)
     }
 
     setTimeout(addPath, 0, sj + BLOCKSIZE)
@@ -1060,13 +1135,13 @@ function syncOnSendGroupVerify(syncPackets, resolve) {
       JSON.stringify(Object.assign({}, packet, { uuid: undefined })) ==
         JSON.stringify(syncDone)
     ) {
-      expect(syncDonePacket).toEqual(-1)
+      equal(syncDonePacket, -1)
 
       syncDonePacket = i
     }
   })
 
-  expect(syncDonePacket).toEqual(syncPackets.length - 1)
+  equal(syncDonePacket, syncPackets.length - 1)
 
   resolve()
 }
@@ -1084,7 +1159,7 @@ function addOnEventGroupVerify(
   const updateIntervals = {}
 
   for (const event of addEvents) {
-    expect(!event.add + !event.erase).toBe(1)
+    equal(!event.add + !event.erase, 1)
 
     if (event.add) {
       const {
@@ -1101,8 +1176,8 @@ function addOnEventGroupVerify(
     }
   }
 
-  expect(updatePaths).toStrictEqual({ [pathIDs[0]]: addData })
-  expect(updateIntervals).toStrictEqual({ [pathIDs[0]]: {} })
+  equal(updatePaths, { [pathIDs[0]]: addData })
+  equal(updateIntervals, { [pathIDs[0]]: {} })
 
   resolve()
 }
@@ -1120,8 +1195,8 @@ function eraseOnEventGroupVerify(
   const updateIntervals = {}
 
   for (const event of eraseEvents) {
-    expect(!event.add).toBe(true)
-    expect(!event.erase).toBe(false)
+    ok(!event.add)
+    notOk(!event.erase)
 
     const {
       detail: { id, intervals },
@@ -1130,7 +1205,7 @@ function eraseOnEventGroupVerify(
     updateIntervals[id] = intervals
   }
 
-  expect(updateIntervals).toStrictEqual({
+  equal(updateIntervals, {
     [pathIDs[0]]: Object.assign(
       {},
       new Array(addData.length).fill([[0, 0 + (addData.length > 1)]]),
@@ -1151,7 +1226,7 @@ function syncOnEventGroupVerify(
   const syncIntervals = {}
 
   for (const event of syncEvents) {
-    expect(!event.add + !event.erase).toBe(1)
+    equal(!event.add + !event.erase, 1)
 
     if (event.add) {
       const {
@@ -1168,8 +1243,8 @@ function syncOnEventGroupVerify(
     }
   }
 
-  expect(syncPaths).toStrictEqual({ [pathIDs[0]]: addData })
-  expect(syncIntervals).toStrictEqual({
+  equal(syncPaths, { [pathIDs[0]]: addData })
+  equal(syncIntervals, {
     [pathIDs[0]]: Object.assign(
       {},
       new Array(addData.length).fill([[0, 0 + (addData.length > 1)]]),
@@ -1179,171 +1254,169 @@ function syncOnEventGroupVerify(
   resolve()
 }
 
-describe("drawing app mesh", () => {
-  beforeEach(() => {
-    getUserID.mockClear()
-    getPeerHandle.mockClear()
-    getPeerFootprint.mockClear()
-    send.mockClear()
-    broadcast.mockClear()
-    terminatePeer.mockClear()
-    destructor.mockClear()
-    addEventListener.mockClear()
-    MockConnection.mockClear()
-
-    captureHeapUsage()
-  })
+test("benchmark", async (t) => {
+  await initFileSystem()
 
   const ITERATIONSLIST = [10, 25, 50, 75, 100, 250, 500]
-  const BLOCKSIZE = 10
-
-  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" /* FILENAME */,
-      ITERATIONSLIST /* ITERATIONSLIST */,
-      BLOCKSIZE /* BLOCKSIZE */,
-      dotDraw /* addData */,
-      dotErase /* eraseData */,
-      addOnInitFrontendSequential /* addOnInitFrontend */,
-      100 /* addBroadcastGroupTimeout */,
-      addOnBroadcastGroupSequential /* addOnBroadcastGroup */,
-      ".dot-seq-add-packets.json" /* addPacketsFilename */,
-      eraseOnInitFrontendSequential /* eraseOnInitFrontend */,
-      100 /* eraseBroadcastGroupTimeout */,
-      eraseOnBroadcastGroupSequential /* eraseOnBroadcastGroup */,
-      ".dot-seq-erase-packets.json" /* erasePacketsFilename */,
-      1000 /* syncSendGroupTimeout */,
-      syncOnSendGroup /* syncOnSendGroup */,
-      ".dot-seq-sync-packets.json" /* syncPacketsFilename */,
-      addOnInitBackendSequential /* addOnInitBackend */,
-      100 /* addEventGroupTimeout */,
-      addOnEventGroupSequential /* addOnEventGroup */,
-      false /* addEventsCache */,
-      ".dot-seq-add-events.json" /* addEventsFilename */,
-      eraseOnInitBackendSequential /* eraseOnInitBackend */,
-      100 /* eraseEventGroupTimeout */,
-      eraseOnEventGroupSequential /* eraseOnEventGroupTimeout */,
-      false /* eraseEventsCache */,
-      ".dot-seq-erase-events.json" /* eraseEventsFilename */,
-      1000 /* syncEventGroupTimeout */,
-      syncOnEventGroup /* syncOnEventGroup */,
-      false /* syncEventsCache */,
-      ".dot-seq-sync-events.json" /* syncEventsFilename */,
-    )
-  })
+  const BLOCKSIZE = 1000 //10
+
+  await t.test(
+    "benchmarks a dot draw and erase update sequentially",
+    async (/*t*/) => {
+      return runBidirectionalBenchmark(
+        "dot draw and erase [sequential]" /* BENCHMARK */,
+        "plots/dot-seq-benchmark.tsv" /* FILENAME */,
+        ITERATIONSLIST /* ITERATIONSLIST */,
+        BLOCKSIZE /* BLOCKSIZE */,
+        dotDraw /* addData */,
+        dotErase /* eraseData */,
+        addOnInitFrontendSequential /* addOnInitFrontend */,
+        100 /* addBroadcastGroupTimeout */,
+        addOnBroadcastGroupSequential /* addOnBroadcastGroup */,
+        ".dot-seq-add-packets.json" /* addPacketsFilename */,
+        eraseOnInitFrontendSequential /* eraseOnInitFrontend */,
+        100 /* eraseBroadcastGroupTimeout */,
+        eraseOnBroadcastGroupSequential /* eraseOnBroadcastGroup */,
+        ".dot-seq-erase-packets.json" /* erasePacketsFilename */,
+        1000 /* syncSendGroupTimeout */,
+        syncOnSendGroup /* syncOnSendGroup */,
+        ".dot-seq-sync-packets.json" /* syncPacketsFilename */,
+        addOnInitBackendSequential /* addOnInitBackend */,
+        100 /* addEventGroupTimeout */,
+        addOnEventGroupSequential /* addOnEventGroup */,
+        false /* addEventsCache */,
+        ".dot-seq-add-events.json" /* addEventsFilename */,
+        eraseOnInitBackendSequential /* eraseOnInitBackend */,
+        100 /* eraseEventGroupTimeout */,
+        eraseOnEventGroupSequential /* eraseOnEventGroupTimeout */,
+        false /* eraseEventsCache */,
+        ".dot-seq-erase-events.json" /* eraseEventsFilename */,
+        1000 /* syncEventGroupTimeout */,
+        syncOnEventGroup /* syncOnEventGroup */,
+        false /* syncEventsCache */,
+        ".dot-seq-sync-events.json" /* syncEventsFilename */,
+      )
+    },
+  )
 
-  it("benchmarks a dot draw and erase update in parallel", () => {
-    return runBidirectionalBenchmark(
-      "dot draw and erase [parallel]" /* BENCHMARK */,
-      "plots/dot-par-benchmark.tsv" /* FILENAME */,
-      ITERATIONSLIST /* ITERATIONSLIST */,
-      BLOCKSIZE /* BLOCKSIZE */,
-      dotDraw /* addData */,
-      dotErase /* eraseData */,
-      addOnInitFrontendParallel /* addOnInitFrontend */,
-      1000 /* addBroadcastGroupTimeout */,
-      addOnBroadcastGroupParallel /* addOnBroadcastGroup */,
-      ".dot-par-add-packets.json" /* addPacketsFilename */,
-      eraseOnInitFrontendParallel /* eraseOnInitFrontend */,
-      1000 /* eraseBroadcastGroupTimeout */,
-      eraseOnBroadcastGroupParallel /* eraseOnBroadcastGroup */,
-      ".dot-par-erase-packets.json" /* erasePacketsFilename */,
-      1000 /* syncSendGroupTimeout */,
-      syncOnSendGroup /* syncOnSendGroup */,
-      ".dot-par-sync-packets.json" /* syncPacketsFilename */,
-      addOnInitBackendParallel /* addOnInitBackend */,
-      1000 /* addEventGroupTimeout */,
-      addOnEventGroupParallel /* addOnEventGroup */,
-      false /* addEventsCache */,
-      ".dot-par-add-events.json" /* addEventsFilename */,
-      eraseOnInitBackendParallel /* eraseOnInitBackend */,
-      1000 /* eraseEventGroupTimeout */,
-      eraseOnEventGroupParallel /* eraseOnEventGroupTimeout */,
-      false /* eraseEventsCache */,
-      ".dot-par-erase-events.json" /* eraseEventsFilename */,
-      1000 /* syncEventGroupTimeout */,
-      syncOnEventGroup /* syncOnEventGroup */,
-      false /* syncEventsCache */,
-      ".dot-par-sync-events.json" /* syncEventsFilename */,
-    )
-  })
+  await t.test(
+    "benchmarks a dot draw and erase update in parallel",
+    async (/*t*/) => {
+      return runBidirectionalBenchmark(
+        "dot draw and erase [parallel]" /* BENCHMARK */,
+        "plots/dot-par-benchmark.tsv" /* FILENAME */,
+        ITERATIONSLIST /* ITERATIONSLIST */,
+        BLOCKSIZE /* BLOCKSIZE */,
+        dotDraw /* addData */,
+        dotErase /* eraseData */,
+        addOnInitFrontendParallel /* addOnInitFrontend */,
+        1000 /* addBroadcastGroupTimeout */,
+        addOnBroadcastGroupParallel /* addOnBroadcastGroup */,
+        ".dot-par-add-packets.json" /* addPacketsFilename */,
+        eraseOnInitFrontendParallel /* eraseOnInitFrontend */,
+        1000 /* eraseBroadcastGroupTimeout */,
+        eraseOnBroadcastGroupParallel /* eraseOnBroadcastGroup */,
+        ".dot-par-erase-packets.json" /* erasePacketsFilename */,
+        1000 /* syncSendGroupTimeout */,
+        syncOnSendGroup /* syncOnSendGroup */,
+        ".dot-par-sync-packets.json" /* syncPacketsFilename */,
+        addOnInitBackendParallel /* addOnInitBackend */,
+        1000 /* addEventGroupTimeout */,
+        addOnEventGroupParallel /* addOnEventGroup */,
+        false /* addEventsCache */,
+        ".dot-par-add-events.json" /* addEventsFilename */,
+        eraseOnInitBackendParallel /* eraseOnInitBackend */,
+        1000 /* eraseEventGroupTimeout */,
+        eraseOnEventGroupParallel /* eraseOnEventGroupTimeout */,
+        false /* eraseEventsCache */,
+        ".dot-par-erase-events.json" /* eraseEventsFilename */,
+        1000 /* syncEventGroupTimeout */,
+        syncOnEventGroup /* syncOnEventGroup */,
+        false /* syncEventsCache */,
+        ".dot-par-sync-events.json" /* syncEventsFilename */,
+      )
+    },
+  )
 
-  it("benchmarks a path draw and erase update sequentially", () => {
-    return runBidirectionalBenchmark(
-      "path draw and erase [sequential]" /* BENCHMARK */,
-      "plots/path-seq-benchmark.tsv" /* FILENAME */,
-      ITERATIONSLIST /* ITERATIONSLIST */,
-      BLOCKSIZE /* BLOCKSIZE */,
-      pathDraw /* addData */,
-      pathErase /* eraseData */,
-      addOnInitFrontendSequential /* addOnInitFrontend */,
-      100 /* addBroadcastGroupTimeout */,
-      addOnBroadcastGroupSequential /* addOnBroadcastGroup */,
-      ".path-seq-add-packets.json" /* addPacketsFilename */,
-      eraseOnInitFrontendSequential /* eraseOnInitFrontend */,
-      100 /* eraseBroadcastGroupTimeout */,
-      eraseOnBroadcastGroupSequential /* eraseOnBroadcastGroup */,
-      ".path-seq-erase-packets.json" /* erasePacketsFilename */,
-      1000 /* syncSendGroupTimeout */,
-      syncOnSendGroup /* syncOnSendGroup */,
-      ".path-seq-sync-packets.json" /* syncPacketsFilename */,
-      addOnInitBackendSequential /* addOnInitBackend */,
-      100 /* addEventGroupTimeout */,
-      addOnEventGroupSequential /* addOnEventGroup */,
-      false /* addEventsCache */,
-      ".path-seq-add-events.json" /* addEventsFilename */,
-      eraseOnInitBackendSequential /* eraseOnInitBackend */,
-      100 /* eraseEventGroupTimeout */,
-      eraseOnEventGroupSequential /* eraseOnEventGroupTimeout */,
-      false /* eraseEventsCache */,
-      ".path-seq-erase-events.json" /* eraseEventsFilename */,
-      1000 /* syncEventGroupTimeout */,
-      syncOnEventGroup /* syncOnEventGroup */,
-      false /* syncEventsCache */,
-      ".path-seq-sync-events.json" /* syncEventsFilename */,
-    )
-  })
+  await t.test(
+    "benchmarks a path draw and erase update sequentially",
+    async (/*t*/) => {
+      return runBidirectionalBenchmark(
+        "path draw and erase [sequential]" /* BENCHMARK */,
+        "plots/path-seq-benchmark.tsv" /* FILENAME */,
+        ITERATIONSLIST /* ITERATIONSLIST */,
+        BLOCKSIZE /* BLOCKSIZE */,
+        pathDraw /* addData */,
+        pathErase /* eraseData */,
+        addOnInitFrontendSequential /* addOnInitFrontend */,
+        100 /* addBroadcastGroupTimeout */,
+        addOnBroadcastGroupSequential /* addOnBroadcastGroup */,
+        ".path-seq-add-packets.json" /* addPacketsFilename */,
+        eraseOnInitFrontendSequential /* eraseOnInitFrontend */,
+        100 /* eraseBroadcastGroupTimeout */,
+        eraseOnBroadcastGroupSequential /* eraseOnBroadcastGroup */,
+        ".path-seq-erase-packets.json" /* erasePacketsFilename */,
+        1000 /* syncSendGroupTimeout */,
+        syncOnSendGroup /* syncOnSendGroup */,
+        ".path-seq-sync-packets.json" /* syncPacketsFilename */,
+        addOnInitBackendSequential /* addOnInitBackend */,
+        100 /* addEventGroupTimeout */,
+        addOnEventGroupSequential /* addOnEventGroup */,
+        false /* addEventsCache */,
+        ".path-seq-add-events.json" /* addEventsFilename */,
+        eraseOnInitBackendSequential /* eraseOnInitBackend */,
+        100 /* eraseEventGroupTimeout */,
+        eraseOnEventGroupSequential /* eraseOnEventGroupTimeout */,
+        false /* eraseEventsCache */,
+        ".path-seq-erase-events.json" /* eraseEventsFilename */,
+        1000 /* syncEventGroupTimeout */,
+        syncOnEventGroup /* syncOnEventGroup */,
+        false /* syncEventsCache */,
+        ".path-seq-sync-events.json" /* syncEventsFilename */,
+      )
+    },
+  )
 
-  it("benchmarks a path draw and erase update in parallel", () => {
-    return runBidirectionalBenchmark(
-      "path draw and erase [parallel]" /* BENCHMARK */,
-      "plots/path-par-benchmark.tsv" /* FILENAME */,
-      ITERATIONSLIST /* ITERATIONSLIST */,
-      BLOCKSIZE /* BLOCKSIZE */,
-      pathDraw /* addData */,
-      pathErase /* eraseData */,
-      addOnInitFrontendParallel /* addOnInitFrontend */,
-      5000 /* addBroadcastGroupTimeout */,
-      addOnBroadcastGroupParallel /* addOnBroadcastGroup */,
-      ".path-par-add-packets.json" /* addPacketsFilename */,
-      eraseOnInitFrontendParallel /* eraseOnInitFrontend */,
-      5000 /* eraseBroadcastGroupTimeout */,
-      eraseOnBroadcastGroupParallel /* eraseOnBroadcastGroup */,
-      ".path-par-erase-packets.json" /* erasePacketsFilename */,
-      5000 /* syncSendGroupTimeout */,
-      syncOnSendGroup /* syncOnSendGroup */,
-      ".path-par-sync-packets.json" /* syncPacketsFilename */,
-      addOnInitBackendParallel /* addOnInitBackend */,
-      5000 /* addEventGroupTimeout */,
-      addOnEventGroupParallel /* addOnEventGroup */,
-      false /* addEventsCache */,
-      ".path-par-add-events.json" /* addEventsFilename */,
-      eraseOnInitBackendParallel /* eraseOnInitBackend */,
-      5000 /* eraseEventGroupTimeout */,
-      eraseOnEventGroupParallel /* eraseOnEventGroupTimeout */,
-      false /* eraseEventsCache */,
-      ".path-par-erase-events.json" /* eraseEventsFilename */,
-      5000 /* syncEventGroupTimeout */,
-      syncOnEventGroup /* syncOnEventGroup */,
-      false /* syncEventsCache */,
-      ".path-par-sync-events.json" /* syncEventsFilename */,
-    )
-  })
+  await t.test(
+    "benchmarks a path draw and erase update in parallel",
+    async (/*t*/) => {
+      return runBidirectionalBenchmark(
+        "path draw and erase [parallel]" /* BENCHMARK */,
+        "plots/path-par-benchmark.tsv" /* FILENAME */,
+        ITERATIONSLIST /* ITERATIONSLIST */,
+        BLOCKSIZE /* BLOCKSIZE */,
+        pathDraw /* addData */,
+        pathErase /* eraseData */,
+        addOnInitFrontendParallel /* addOnInitFrontend */,
+        5000 /* addBroadcastGroupTimeout */,
+        addOnBroadcastGroupParallel /* addOnBroadcastGroup */,
+        ".path-par-add-packets.json" /* addPacketsFilename */,
+        eraseOnInitFrontendParallel /* eraseOnInitFrontend */,
+        5000 /* eraseBroadcastGroupTimeout */,
+        eraseOnBroadcastGroupParallel /* eraseOnBroadcastGroup */,
+        ".path-par-erase-packets.json" /* erasePacketsFilename */,
+        5000 /* syncSendGroupTimeout */,
+        syncOnSendGroup /* syncOnSendGroup */,
+        ".path-par-sync-packets.json" /* syncPacketsFilename */,
+        addOnInitBackendParallel /* addOnInitBackend */,
+        5000 /* addEventGroupTimeout */,
+        addOnEventGroupParallel /* addOnEventGroup */,
+        false /* addEventsCache */,
+        ".path-par-add-events.json" /* addEventsFilename */,
+        eraseOnInitBackendParallel /* eraseOnInitBackend */,
+        5000 /* eraseEventGroupTimeout */,
+        eraseOnEventGroupParallel /* eraseOnEventGroupTimeout */,
+        false /* eraseEventsCache */,
+        ".path-par-erase-events.json" /* eraseEventsFilename */,
+        5000 /* syncEventGroupTimeout */,
+        syncOnEventGroup /* syncOnEventGroup */,
+        false /* syncEventsCache */,
+        ".path-par-sync-events.json" /* syncEventsFilename */,
+      )
+    },
+  )
 
-  it("communicates a single draw and erase update", () => {
+  await t.test("communicates a single draw and erase update", async (/*t*/) => {
     return runBidirectionalBenchmark(
       null /* BENCHMARK */,
       null /* FILENAME */,
@@ -1379,7 +1452,7 @@ describe("drawing app mesh", () => {
     )
   })
 
-  it("communicates a path draw and erase update", () => {
+  await t.test("communicates a path draw and erase update", async (/*t*/) => {
     return runBidirectionalBenchmark(
       null /* BENCHMARK */,
       null /* FILENAME */,
diff --git a/__tests__/benchmark.data.js b/__benchmarks__/data.js
similarity index 94%
rename from __tests__/benchmark.data.js
rename to __benchmarks__/data.js
index 86df444825ed4c4c413f9c8ac57aa43fd4d526c8..e91d0b99e85d387f8304c6c2fd92d0e877d74215 100644
--- a/__tests__/benchmark.data.js
+++ b/__benchmarks__/data.js
@@ -127,22 +127,22 @@ export const syncDone = {
   compressed: true,
 }
 
-export const dotDraw = [[209, 88, 5.000000000000001, "#0000ff"]]
+export const dotDraw = [[209, 88, 5.0, "#0000ff"]]
 export const dotErase = [[0, [[0, 0]]]]
 
 export const pathDraw = [
-  [229, 147, 5.000000000000001, "#0000ff"],
-  [239, 149, 5.000000000000001, "#0000ff"],
-  [265, 154, 5.000000000000001, "#0000ff"],
-  [329, 158, 5.000000000000001, "#0000ff"],
-  [428, 168, 5.000000000000001, "#0000ff"],
-  [559, 172, 5.000000000000001, "#0000ff"],
-  [689, 176, 5.000000000000001, "#0000ff"],
-  [789, 176, 5.000000000000001, "#0000ff"],
-  [871, 178, 5.000000000000001, "#0000ff"],
-  [915, 179, 5.000000000000001, "#0000ff"],
-  [937, 179, 5.000000000000001, "#0000ff"],
-  [942, 179, 5.000000000000001, "#0000ff"],
+  [229, 147, 5.0, "#0000ff"],
+  [239, 149, 5.0, "#0000ff"],
+  [265, 154, 5.0, "#0000ff"],
+  [329, 158, 5.0, "#0000ff"],
+  [428, 168, 5.0, "#0000ff"],
+  [559, 172, 5.0, "#0000ff"],
+  [689, 176, 5.0, "#0000ff"],
+  [789, 176, 5.0, "#0000ff"],
+  [871, 178, 5.0, "#0000ff"],
+  [915, 179, 5.0, "#0000ff"],
+  [937, 179, 5.0, "#0000ff"],
+  [942, 179, 5.0, "#0000ff"],
 ]
 export const pathErase = [
   [0, [[0, 0.030367582231477598]]],
diff --git a/__benchmarks__/puppeteer.js b/__benchmarks__/puppeteer.js
new file mode 100644
index 0000000000000000000000000000000000000000..275d7c8eac6961d06a8166e886e53556bb9c631f
--- /dev/null
+++ b/__benchmarks__/puppeteer.js
@@ -0,0 +1,74 @@
+import puppeteer from "puppeteer"
+import fs from "fs"
+;(async () => {
+  const browser = await puppeteer.launch({
+    headless: true,
+    args: ["--js-flags=--expose-gc"],
+  })
+  const page = await browser.newPage()
+
+  const done = new Promise((resolve) => {
+    page.on("console", (msg) => {
+      if (msg.type() == "debug") {
+        process.stderr.write(msg.text())
+      } else if (msg.type() == "info") {
+        const { filename, title, iterations, results } = JSON.parse(msg.text())
+
+        if (title) {
+          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`)
+          }
+
+          fs.writeFileSync(
+            filename,
+            `# Benchmark: ${title}\n# ${columns.join("\t")}\n`,
+          )
+        } else {
+          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)
+          }
+
+          fs.appendFileSync(filename, `${columns.join("\t")}\n`)
+        }
+      } else if (msg.type() == "log") {
+        if (msg.text().startsWith("# failure")) {
+          resolve()
+        }
+
+        process.stdout.write(msg.text() + "\n")
+      }
+    })
+  })
+
+  await page.goto("http://localhost:3000/benchmarks.html").catch(console.error)
+
+  await done
+
+  await browser.close()
+})()
diff --git a/package-lock.json b/package-lock.json
index 479b4d4f8adad9eacbcb9126a6fb55505decfa3f..24c0945598f4769fdbc1b536b177ac62e28577ae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -961,6 +961,15 @@
       "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
       "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
     },
+    "agent-base": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
+      "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
+      "dev": true,
+      "requires": {
+        "es6-promisify": "^5.0.0"
+      }
+    },
     "aggregate-error": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
@@ -1009,6 +1018,12 @@
       "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
       "dev": true
     },
+    "ansi-escape": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-escape/-/ansi-escape-1.1.0.tgz",
+      "integrity": "sha1-ithZ6Epp4P+Rd5aUeTqS5OjAXpk=",
+      "dev": true
+    },
     "ansi-escapes": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz",
@@ -2585,6 +2600,12 @@
       "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
       "dev": true
     },
+    "buffer-shims": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz",
+      "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=",
+      "dev": true
+    },
     "buffer-xor": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
@@ -3886,6 +3907,21 @@
         "is-symbol": "^1.0.2"
       }
     },
+    "es6-promise": {
+      "version": "4.2.8",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+      "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
+      "dev": true
+    },
+    "es6-promisify": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+      "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
+      "dev": true,
+      "requires": {
+        "es6-promise": "^4.0.3"
+      }
+    },
     "escape-html": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -4361,6 +4397,18 @@
         }
       }
     },
+    "extract-zip": {
+      "version": "1.6.7",
+      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz",
+      "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=",
+      "dev": true,
+      "requires": {
+        "concat-stream": "1.6.2",
+        "debug": "2.6.9",
+        "mkdirp": "0.5.1",
+        "yauzl": "2.4.1"
+      }
+    },
     "extsprintf": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
@@ -4440,6 +4488,15 @@
         "bser": "2.1.1"
       }
     },
+    "fd-slicer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
+      "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
+      "dev": true,
+      "requires": {
+        "pend": "~1.2.0"
+      }
+    },
     "figgy-pudding": {
       "version": "3.5.1",
       "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz",
@@ -5654,6 +5711,33 @@
       "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
       "dev": true
     },
+    "https-proxy-agent": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz",
+      "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==",
+      "dev": true,
+      "requires": {
+        "agent-base": "^4.3.0",
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        }
+      }
+    },
     "human-signals": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
@@ -8095,6 +8179,12 @@
         "json-parse-better-errors": "^1.0.1"
       }
     },
+    "parse-ms": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz",
+      "integrity": "sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0=",
+      "dev": true
+    },
     "parse-passwd": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
@@ -8209,6 +8299,12 @@
         "sha.js": "^2.4.8"
       }
     },
+    "pend": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+      "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
+      "dev": true
+    },
     "performance-now": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -8321,6 +8417,15 @@
         }
       }
     },
+    "pretty-ms": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-3.2.0.tgz",
+      "integrity": "sha512-ZypexbfVUGTFxb0v+m1bUyy92DHe5SyYlnyY0msyms5zd3RwyvNgyxZZsXXgoyzlxjx5MiqtXUdhUfvQbe0A2Q==",
+      "dev": true,
+      "requires": {
+        "parse-ms": "^1.0.0"
+      }
+    },
     "private": {
       "version": "0.1.8",
       "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
@@ -8379,6 +8484,12 @@
         "ipaddr.js": "1.9.0"
       }
     },
+    "proxy-from-env": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
+      "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=",
+      "dev": true
+    },
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@@ -8386,9 +8497,9 @@
       "dev": true
     },
     "psl": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/psl/-/psl-1.6.0.tgz",
-      "integrity": "sha512-SYKKmVel98NCOYXpkwUqZqh0ahZeeKfmisiLIcEZdsb+WbLv02g/dI5BUmZnIyOe7RzZtLax81nnb2HbvC2tzA==",
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz",
+      "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==",
       "dev": true
     },
     "public-encrypt": {
@@ -8444,6 +8555,45 @@
       "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
       "dev": true
     },
+    "puppeteer": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-2.0.0.tgz",
+      "integrity": "sha512-t3MmTWzQxPRP71teU6l0jX47PHXlc4Z52sQv4LJQSZLq1ttkKS2yGM3gaI57uQwZkNaoGd0+HPPMELZkcyhlqA==",
+      "dev": true,
+      "requires": {
+        "debug": "^4.1.0",
+        "extract-zip": "^1.6.6",
+        "https-proxy-agent": "^3.0.0",
+        "mime": "^2.0.3",
+        "progress": "^2.0.1",
+        "proxy-from-env": "^1.0.0",
+        "rimraf": "^2.6.1",
+        "ws": "^6.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "mime": {
+          "version": "2.4.4",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
+          "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==",
+          "dev": true
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        }
+      }
+    },
     "qrcode-terminal": {
       "version": "0.10.0",
       "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.10.0.tgz",
@@ -8501,6 +8651,12 @@
         "unpipe": "1.0.0"
       }
     },
+    "re-emitter": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/re-emitter/-/re-emitter-1.1.3.tgz",
+      "integrity": "sha1-+p4xn/3u6zWycpbvDz03TawvUqc=",
+      "dev": true
+    },
     "react-is": {
       "version": "16.12.0",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
@@ -9396,12 +9552,12 @@
       "dev": true
     },
     "source-map-resolve": {
-      "version": "0.5.2",
-      "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz",
-      "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==",
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
+      "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
       "dev": true,
       "requires": {
-        "atob": "^2.1.1",
+        "atob": "^2.1.2",
         "decode-uri-component": "^0.2.0",
         "resolve-url": "^0.2.1",
         "source-map-url": "^0.4.0",
@@ -9464,6 +9620,15 @@
       "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
       "dev": true
     },
+    "split": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/split/-/split-1.0.0.tgz",
+      "integrity": "sha1-xDlc5oOrzSVLwo/h2rtuXCfc/64=",
+      "dev": true,
+      "requires": {
+        "through": "2"
+      }
+    },
     "split-string": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@@ -9769,6 +9934,80 @@
         }
       }
     },
+    "tap-out": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/tap-out/-/tap-out-2.1.0.tgz",
+      "integrity": "sha512-LJE+TBoVbOWhwdz4+FQk40nmbIuxJLqaGvj3WauQw3NYYU5TdjoV3C0x/yq37YAvVyi+oeBXmWnxWSjJ7IEyUw==",
+      "dev": true,
+      "requires": {
+        "re-emitter": "1.1.3",
+        "readable-stream": "2.2.9",
+        "split": "1.0.0",
+        "trim": "0.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+          "dev": true
+        },
+        "process-nextick-args": {
+          "version": "1.0.7",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+          "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "2.2.9",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz",
+          "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=",
+          "dev": true,
+          "requires": {
+            "buffer-shims": "~1.0.0",
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~1.0.6",
+            "string_decoder": "~1.0.0",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
+          "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        }
+      }
+    },
+    "tap-summary": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/tap-summary/-/tap-summary-4.0.0.tgz",
+      "integrity": "sha1-kyFIYrpGfFCgnzeOoOXtxd0EQz0=",
+      "dev": true,
+      "requires": {
+        "ansi-escape": "^1.0.1",
+        "commander": "^2.9.0",
+        "figures": "^2.0.0",
+        "pretty-ms": "^3.0.0",
+        "tap-out": "^2.0.0"
+      },
+      "dependencies": {
+        "figures": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+          "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "^1.0.5"
+          }
+        }
+      }
+    },
     "tapable": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
@@ -10697,6 +10936,12 @@
       "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
       "dev": true
     },
+    "trim": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
+      "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=",
+      "dev": true
+    },
     "trim-right": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
@@ -11584,6 +11829,15 @@
         "decamelize": "^1.2.0"
       }
     },
+    "yauzl": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
+      "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
+      "dev": true,
+      "requires": {
+        "fd-slicer": "~1.0.1"
+      }
+    },
     "yeast": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
@@ -11593,7 +11847,28 @@
       "version": "file:src/yjs",
       "requires": {
         "debug": "^2.6.3"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        }
       }
+    },
+    "zora": {
+      "version": "3.1.8",
+      "resolved": "https://registry.npmjs.org/zora/-/zora-3.1.8.tgz",
+      "integrity": "sha512-AArEyKiLWi3eLXW2uRbfPvANfSQgV8VHoCuXCihCTQyUv7brFrghGbsUqKxqucc+QodQ1G2+O8Gpsz8RVpeiRQ==",
+      "dev": true
     }
   }
 }
diff --git a/package.json b/package.json
index ab69222048ae995b8b9005bc228da2b79926fead..476d27c897498ab9898bb378de66ddc44ed29786 100644
--- a/package.json
+++ b/package.json
@@ -13,12 +13,13 @@
     "build": "webpack --config webpack.prod.js",
     "build:analyze": "webpack --env.analyze --config webpack.prod.js",
     "build:dev": "webpack --config webpack.dev.js",
+    "build:bench": "webpack --config webpack.bench.js",
     "watch": "webpack --watch --config webpack.dev.js",
     "start": "node --experimental-modules src/server.js",
-    "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 src/drawing-crdt",
-    "test-coverage": "jest --coverage --testPathIgnorePatterns __tests__/*.data.js src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker src/drawing-crdt",
-    "test-benchmark": "jest --testPathPattern .*benchmark.test.js --testPathIgnorePatterns .*.data.js src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker src/drawing-crdt --runInBand",
+    "test": "jest --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker",
+    "test-changed": "jest --only-changed --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker src/drawing-crdt",
+    "test-coverage": "jest --coverage --testPathIgnorePatterns src/liowebrtc src/rtcpeerconnection src/signalbuddy src/yjs src/tiny-worker src/drawing-crdt",
+    "benchmarks": "node --experimental-modules __benchmarks__/puppeteer.js | npx tap-summary",
     "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:*",
@@ -62,6 +63,8 @@
     "jest": "^24.9.0",
     "npm-run-all": "^4.1.5",
     "prettier": "^1.18.2",
+    "puppeteer": "^2.0.0",
+    "tap-summary": "^4.0.0",
     "testcafe": "^1.5.0",
     "tiny-worker": "file:src/tiny-worker",
     "webpack": "^4.41.0",
@@ -69,7 +72,8 @@
     "webpack-cli": "^3.3.9",
     "webpack-merge": "^4.2.2",
     "webpack-preprocessor-loader": "^1.1.2",
-    "yaeti": "^1.0.2"
+    "yaeti": "^1.0.2",
+    "zora": "^3.1.8"
   },
   "pre-commit": [
     "lint",
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..4ff127ef17c8b3eb9e4b5a78ba4ddc76d42f98fd
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/src/connection/MockConnection.js b/src/connection/MockConnection.js
index 2fa9825675d0f5e13fe8d0be23d871fed665fb25..f01abd3fcdbaade836da741762095d875c30942c 100644
--- a/src/connection/MockConnection.js
+++ b/src/connection/MockConnection.js
@@ -1,66 +1,62 @@
-export const getUserID = jest.fn((uid) => uid)
-export const getPeerHandle = jest.fn((/*uid*/) => undefined)
-export const getPeerFootprint = jest.fn((/*uid*/) =>
-  Promise.resolve(Date.now()))
+export const userID = { uuid: null }
 
 export const sendListener = { callback: null }
-export const send = jest.fn((uid, channel, message) => {
-  if (sendListener.callback) {
-    sendListener.callback(uid, channel, message)
-  }
-})
-const sendMockClear = send.mockClear
-send.mockClear = () => {
-  sendListener.callback = null
-  sendMockClear()
-}
-
 export const broadcastListener = { callback: null }
-export const broadcast = jest.fn((channel, message) => {
-  if (broadcastListener.callback) {
-    broadcastListener.callback(channel, message)
-  }
-})
-const broadcastMockClear = broadcast.mockClear
-broadcast.mockClear = () => {
-  broadcastListener.callback = null
-  broadcastMockClear()
-}
-
-export const terminatePeer = jest.fn()
-export const destructor = jest.fn(() => eventListeners.clear())
 
 const eventListeners = new Map()
 export const getEventListener = (room, event) =>
   eventListeners.get(`${room}:${event}`)
-export const addEventListener = jest.fn((room, event, callback) =>
-  eventListeners.set(`${room}:${event}`, callback),
-)
-const addEventListenerMockClear = addEventListener.mockClear
-addEventListener.mockClear = () => {
-  eventListeners.clear()
-  addEventListenerMockClear()
-}
 
-const MockConnection = jest.fn().mockImplementation(({ room }) => {
-  setTimeout(
-    () =>
-      getEventListener(room, "roomJoined") &&
-      getEventListener(room, "roomJoined")(),
-    0,
-  )
+class MockConnection {
+  constructor({ room }) {
+    this.room = room
+
+    setTimeout(
+      () =>
+        getEventListener(room, "roomJoined") &&
+        getEventListener(room, "roomJoined")(),
+      0,
+    )
+  }
+
+  getUserID() {
+    return userID.uuid
+  }
+
+  getPeerHandle(/*uid*/) {
+    return undefined
+  }
 
-  return {
-    getUserID: () => getUserID(room),
-    getPeerHandle,
-    getPeerFootprint,
-    send,
-    broadcast,
-    terminatePeer,
-    destructor,
-    addEventListener: (event, callback) =>
-      addEventListener(room, event, callback),
+  getPeerFootprint(/*uid*/) {
+    return Promise.resolve(Date.now())
   }
-})
+
+  send(uid, channel, message) {
+    if (sendListener.callback) {
+      sendListener.callback(uid, channel, message)
+    }
+  }
+
+  broadcast(channel, message) {
+    if (broadcastListener.callback) {
+      broadcastListener.callback(channel, message)
+    }
+  }
+
+  terminatePeer() {
+    // Twiddle thumbs
+  }
+
+  destructor() {
+    sendListener.callback = null
+    broadcastListener.callback = null
+
+    eventListeners.clear()
+  }
+
+  addEventListener(event, callback) {
+    eventListeners.set(`${this.room}:${event}`, callback)
+  }
+}
 
 export default MockConnection
diff --git a/src/drawing-crdt b/src/drawing-crdt
index 5a239921c305a8b3251b3ef393fd79930df4bf8e..c8d9e04fc2b710fc2152407013f24b044c5c91c7 160000
--- a/src/drawing-crdt
+++ b/src/drawing-crdt
@@ -1 +1 @@
-Subproject commit 5a239921c305a8b3251b3ef393fd79930df4bf8e
+Subproject commit c8d9e04fc2b710fc2152407013f24b044c5c91c7
diff --git a/src/queue.js b/src/queue.js
index 8ba36ca246b06915ec07614328053c9287581561..b728c51dace01cd362934861af9efa34730f8afd 100644
--- a/src/queue.js
+++ b/src/queue.js
@@ -20,7 +20,14 @@ onmessage = (event) => {
 
   if (event.data.method == "send" || event.data.method == "broadcast") {
     let message = event.data.message
-    const compressed = typeof message == "object"
+    const compressed = !(
+      message == undefined ||
+      message instanceof String ||
+      typeof message == "string" ||
+      message instanceof Uint8Array ||
+      message.message instanceof Uint8Array ||
+      message.message == undefined
+    )
     const uuid = uuidv4()
 
     //console.log("send in", message)
diff --git a/src/wasm-crdt.js b/src/wasm-crdt.js
index 0adcf0eaabc3703d870038a6dadf8aee9da41178..0b5bf4d719a02aa761bd0c47dd63976d70df4ac2 100644
--- a/src/wasm-crdt.js
+++ b/src/wasm-crdt.js
@@ -63,7 +63,8 @@ export default class WasmCRDTWrapper {
     this.interval = setInterval(() => {
       this.crdt.fetch_events()
       this.crdt.fetch_deltas()
-    }, 16)
+      // TODO: pass an option here
+    }, 0)
   }
 
   destroy() {
diff --git a/webpack.bench.js b/webpack.bench.js
new file mode 100644
index 0000000000000000000000000000000000000000..60a8fc0e8ac3fdb506f951db198956ecb73af2f8
--- /dev/null
+++ b/webpack.bench.js
@@ -0,0 +1,18 @@
+const merge = require("webpack-merge")
+const common = require("./webpack.common.js")
+const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
+  .BundleAnalyzerPlugin
+
+module.exports = (env) => {
+  const config = merge(common, {
+    mode: "production",
+    entry: {
+      benchmarks: "./__benchmarks__/benchmarks.js",
+    },
+    plugins: env && env.analyze ? [new BundleAnalyzerPlugin()] : [],
+  })
+
+  delete config.entry.app
+
+  return config
+}