Skip to content
Snippets Groups Projects
benchmark.test.js 17.39 KiB
import chalk from "chalk"

import { connect } from "../src/room.js"

import MockConnection, {
  getUserID,
  getPeerHandle,
  getPeerFootprint,
  send,
  sendListener,
  broadcast,
  broadcastListener,
  terminatePeer,
  destructor,
  addEventListener,
  eventListeners,
} from "../src/connection/MockConnection.js"

import {
  handshake,
  syncStep1,
  syncDone,
  dotDraw,
  dotErase,
  pathDraw,
  pathErase,
} from "./benchmark.data.js"

function printBenchmark(title, iterations, results) {
  process.stdout.write(`\n  ${title} (${iterations} iterations):\n`)

  for (const title in results) {
    const { time, packets, size } = results[title]
    const synchronisation = title == "synchronisation"

    process.stdout.write(
      chalk`    {yellow ⧗} {dim ${title}:} {yellow.inverse ${(
        time /
        (1e6 * (synchronisation ? 1 : iterations))
      ).toFixed(3)}ms ${
        synchronisation ? "total" : "/ it"
      }} {magenta.inverse ${size}B} {dim in ${packets} packet(s)}\n`,
    )
  }

  process.stdout.write(`\n`)
}

let room = null

describe("drawing app mesh", () => {
  beforeEach(async () => {
    getUserID.mockClear()
    getPeerHandle.mockClear()
    getPeerFootprint.mockClear()
    send.mockClear()
    broadcast.mockClear()
    terminatePeer.mockClear()
    destructor.mockClear()
    addEventListener.mockClear()
    MockConnection.mockClear()

    room = await connect("data", MockConnection)
    eventListeners.get("messageReceived")({ detail: handshake })
  })

  afterEach(() => {
    room.disconnect()
    room = null
  })

  it("benchmarks a single draw and erase update sequentially", () => {
    const ITERATIONS = 1000

    jest.setTimeout(ITERATIONS * 10)

    const dotIDs = []
    let prevTime

    let broadcasts = 0

    let addTime = 0
    let addPackets = 0
    let addSize = 0
    let eraseTime = 0
    let erasePackets = 0
    let eraseSize = 0
    let syncTime
    let syncPackets = 0
    let syncSize = 0

    let timeout

    return new Promise((resolve) => {
      broadcastListener.callback = (channel, message) => {
        const currTime = process.hrtime()

        broadcasts += 1

        if (broadcasts <= ITERATIONS) {
          addTime +=
            (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])
          addPackets += 1
          addSize += message.message.length
        } else {
          eraseTime +=
            (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])
          erasePackets += 1
          eraseSize += message.message.length
        }

        prevTime = process.hrtime()

        if (broadcasts < ITERATIONS) {
          dotIDs.push(room.addPath(dotDraw[0]))
        } else if (broadcasts < ITERATIONS * 2) {
          room.extendErasureIntervals(
            dotIDs[broadcasts - ITERATIONS],
            dotErase[0][0],
            dotErase[0][1],
          )
        } else {
          resolve()
        }
      }

      prevTime = process.hrtime()

      dotIDs.push(room.addPath(dotDraw[0]))
    }).then(
      () =>
        new Promise((resolve) => {
          sendListener.callback = (uid, channel, message) => {
            const currTime = process.hrtime()
            syncTime =
              (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])

            syncPackets += 1
            syncSize += message.message.length
            clearTimeout(timeout)
            timeout = setTimeout(() => {
              printBenchmark("single draw and erase [sequential]", ITERATIONS, {
                addPath: { time: addTime, packets: addPackets, size: addSize },
                extendErasureIntervals: {
                  time: eraseTime,
                  packets: erasePackets,
                  size: eraseSize,
                },
                synchronisation: {
                  time: syncTime,
                  packets: syncPackets,
                  size: syncSize,
                },
              })

              resolve()
            }, 1000)
          }

          prevTime = process.hrtime()

          eventListeners.get("messageReceived")({ detail: syncStep1 })
        }),
    )
  })

  it("benchmarks a single draw and erase update in parallel", () => {
    const ITERATIONS = 1000

    jest.setTimeout(ITERATIONS * 10)

    const dotIDs = []
    let prevTime

    let addTime
    let addPackets = 0
    let addSize = 0
    let eraseTime
    let erasePackets = 0
    let eraseSize = 0
    let syncTime
    let syncPackets = 0
    let syncSize = 0

    let timeout

    return new Promise((resolve) => {
      broadcastListener.callback = (channel, message) => {
        const currTime = process.hrtime()

        addTime =
          (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])
        addPackets += 1
        addSize += message.message.length

        clearTimeout(timeout)
        timeout = setTimeout(() => resolve(), 1000)
      }

      prevTime = process.hrtime()

      for (let i = 0; i < ITERATIONS; i++) {
        dotIDs.push(room.addPath(dotDraw[0]))
      }
    })
      .then(
        () =>
          new Promise((resolve) => {
            broadcastListener.callback = (channel, message) => {
              const currTime = process.hrtime()

              eraseTime =
                (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])
              erasePackets += 1
              eraseSize += message.message.length

              clearTimeout(timeout)
              timeout = setTimeout(() => {
                resolve()
              }, 1000)
            }

            prevTime = process.hrtime()

            for (let i = 0; i < ITERATIONS; i++) {
              room.extendErasureIntervals(
                dotIDs[i],
                dotErase[0][0],
                dotErase[0][1],
              )
            }
          }),
      )
      .then(
        () =>
          new Promise((resolve) => {
            sendListener.callback = (uid, channel, message) => {
              const currTime = process.hrtime()
              syncTime =
                (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])

              syncPackets += 1
              syncSize += message.message.length

              clearTimeout(timeout)
              timeout = setTimeout(() => {
                printBenchmark("single draw and erase [parallel]", ITERATIONS, {
                  addPath: {
                    time: addTime,
                    packets: addPackets,
                    size: addSize,
                  },
                  extendErasureIntervals: {
                    time: eraseTime,
                    packets: erasePackets,
                    size: eraseSize,
                  },
                  synchronisation: {
                    time: syncTime,
                    packets: syncPackets,
                    size: syncSize,
                  },
                })

                resolve()
              }, 1000)
            }

            prevTime = process.hrtime()

            eventListeners.get("messageReceived")({ detail: syncStep1 })
          }),
      )
  })

  it("communicates a single draw and erase update", () => {
    let dotID

    let syncPackets = 0
    let syncDonePacket = -1

    let timeout

    return new Promise((resolve) => {
      broadcastListener.callback = (channel, message) => {
        expect(channel).toEqual("y-js")
        expect(message.message instanceof Uint8Array).toBe(true)
        resolve()
      }

      dotID = room.addPath(dotDraw[0])
    })
      .then(
        () =>
          new Promise((resolve) => {
            broadcastListener.callback = (channel, message) => {
              expect(channel).toEqual("y-js")
              expect(message.message instanceof Uint8Array).toBe(true)
              resolve()
            }

            room.extendErasureIntervals(dotID, dotErase[0][0], dotErase[0][1])
          }),
      )
      .then(
        () =>
          new Promise((resolve) => {
            sendListener.callback = (uid, channel, message) => {
              expect(uid).toEqual("tiger")
              expect(channel).toEqual("y-js")
              expect(message.message instanceof Uint8Array).toBe(true)

              syncPackets += 1

              if (
                message.message.length == syncDone.message.message.length &&
                JSON.stringify(Object.assign(message, { uuid: undefined })) ==
                  JSON.stringify(syncDone.message)
              ) {
                expect(syncDonePacket).toEqual(-1)

                syncDonePacket = syncPackets
              }

              clearTimeout(timeout)
              timeout = setTimeout(() => {
                expect(syncDonePacket).toEqual(syncPackets)

                resolve()
              }, 1000)
            }

            eventListeners.get("messageReceived")({ detail: syncStep1 })
          }),
      )
  })

  it("benchmarks a path draw and erase update sequentially", () => {
    const ITERATIONS = 1000

    jest.setTimeout(ITERATIONS * 30000)

    const pathIDs = []
    let prevTime
    let currTime

    let broadcasts = 0

    let addTime = 0
    let addPackets = 0
    let addSize = 0
    let eraseTime = 0
    let erasePackets = 0
    let eraseSize = 0
    let syncTime
    let syncPackets = 0
    let syncSize = 0

    let timeout

    return new Promise((resolve) => {
      broadcastListener.callback = (channel, message) => {
        currTime = process.hrtime()

        clearTimeout(timeout)
        timeout = setTimeout(() => {
          broadcasts += 1

          if (broadcasts <= ITERATIONS) {
            addTime +=
              (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])
            addPackets += 1
            addSize += message.message.length
          } else {
            eraseTime +=
              (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])
            erasePackets += 1
            eraseSize += message.message.length
          }

          prevTime = process.hrtime()

          if (broadcasts < ITERATIONS) {
            const tmpPathID = room.addPath(pathDraw[0])
            pathIDs.push(tmpPathID)
            for (let i = 1; i < pathDraw.lenth; i++) {
              room.extendPath(tmpPathID, pathDraw[i])
            }
          } else if (broadcasts < ITERATIONS * 2) {
            const tmpPathID = pathIDs[broadcasts - ITERATIONS]

            for (let i = 0; i < pathErase.length; i++) {
              room.extendErasureIntervals(
                tmpPathID,
                pathErase[i][0],
                pathErase[i][1],
              )
            }
          } else {
            resolve()
          }
        }, 100)
      }

      prevTime = process.hrtime()

      const tmpPathID = room.addPath(pathDraw[0])
      pathIDs.push(tmpPathID)
      for (let i = 1; i < pathDraw.lenth; i++) {
        room.extendPath(tmpPathID, pathDraw[i])
      }
    }).then(
      () =>
        new Promise((resolve) => {
          sendListener.callback = (uid, channel, message) => {
            const currTime = process.hrtime()
            syncTime =
              (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])
            syncPackets += 1
            syncSize += message.message.length

            clearTimeout(timeout)
            timeout = setTimeout(() => {
              printBenchmark("path draw and erase [sequential]", ITERATIONS, {
                addPath: { time: addTime, packets: addPackets, size: addSize },
                extendErasureIntervals: {
                  time: eraseTime,
                  packets: erasePackets,
                  size: eraseSize,
                },
                synchronisation: {
                  time: syncTime,
                  packets: syncPackets,
                  size: syncSize,
                },
              })

              resolve()
            }, 1000)
          }

          prevTime = process.hrtime()

          eventListeners.get("messageReceived")({ detail: syncStep1 })
        }),
    )
  })

  it("benchmarks a path draw and erase update in parallel", () => {
    const ITERATIONS = 1000
    const BLOCKSIZE = 10

    jest.setTimeout(ITERATIONS * 200)

    const pathIDs = []
    let prevTime

    let addTime
    let addPackets = 0
    let addSize = 0
    let eraseTime
    let erasePackets = 0
    let eraseSize = 0
    let syncTime
    let syncPackets = 0
    let syncSize = 0

    let timeout

    return new Promise((resolve) => {
      broadcastListener.callback = (channel, message) => {
        const currTime = process.hrtime()

        addTime =
          (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])
        addPackets += 1
        addSize += message.message.length

        clearTimeout(timeout)
        timeout = setTimeout(() => resolve(), 1000)
      }

      prevTime = process.hrtime()

      for (let j = 0; j < ITERATIONS; j++) {
        const tmpPathID = room.addPath(pathDraw[0])
        pathIDs.push(tmpPathID)
        for (let i = 1; i < pathDraw.lenth; i++) {
          room.extendPath(tmpPathID, pathDraw[i])
        }
      }
    })
      .then(
        () =>
          new Promise((resolve) => {
            broadcastListener.callback = (channel, message) => {
              const currTime = process.hrtime()

              eraseTime =
                (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])
              erasePackets += 1
              eraseSize += message.message.length

              clearTimeout(timeout)
              timeout = setTimeout(() => {
                resolve()
              }, 10000)
            }

            // Necessary to allow yjs to execute transactions (majority of processing time)
            function erasePath(sj) {
              if (sj >= ITERATIONS) return

              for (let j = sj; j < Math.min(sj + BLOCKSIZE, ITERATIONS); j++) {
                const tmpPathID = pathIDs[j]

                for (let i = 0; i < pathErase.length; i++) {
                  room.extendErasureIntervals(
                    tmpPathID,
                    pathErase[i][0],
                    pathErase[i][1],
                  )
                }
              }

              setTimeout(erasePath, 0, sj + BLOCKSIZE)
            }

            prevTime = process.hrtime()
            erasePath(0)
          }),
      )
      .then(
        () =>
          new Promise((resolve) => {
            sendListener.callback = (uid, channel, message) => {
              const currTime = process.hrtime()
              syncTime =
                (currTime[0] - prevTime[0]) * 1e9 + (currTime[1] - prevTime[1])

              syncPackets += 1
              syncSize += message.message.length

              clearTimeout(timeout)
              timeout = setTimeout(() => {
                printBenchmark("path draw and erase [parallel]", ITERATIONS, {
                  addPath: {
                    time: addTime,
                    packets: addPackets,
                    size: addSize,
                  },
                  extendErasureIntervals: {
                    time: eraseTime,
                    packets: erasePackets,
                    size: eraseSize,
                  },
                  synchronisation: {
                    time: syncTime,
                    packets: syncPackets,
                    size: syncSize,
                  },
                })

                resolve()
              }, 1000)
            }

            prevTime = process.hrtime()

            eventListeners.get("messageReceived")({ detail: syncStep1 })
          }),
      )
  })

  it("communicates a path draw and erase update", () => {
    let pathID

    let syncPackets = 0
    let syncDonePacket = -1

    let timeout

    return new Promise((resolve) => {
      broadcastListener.callback = (channel, message) => {
        expect(channel).toEqual("y-js")
        expect(message.message instanceof Uint8Array).toBe(true)

        clearTimeout(timeout)
        timeout = setTimeout(() => resolve(), 1000)
      }

      pathID = room.addPath(pathDraw[0])
      for (let i = 1; i < pathDraw.lenth; i++) {
        room.extendPath(pathID, pathDraw[i])
      }
    })
      .then(
        () =>
          new Promise((resolve) => {
            broadcastListener.callback = (channel, message) => {
              expect(channel).toEqual("y-js")
              expect(message.message instanceof Uint8Array).toBe(true)

              clearTimeout(timeout)
              timeout = setTimeout(() => resolve(), 1000)
            }

            for (let i = 0; i < pathErase.length; i++) {
              room.extendErasureIntervals(
                pathID,
                pathErase[i][0],
                pathErase[i][1],
              )
            }
          }),
      )
      .then(
        () =>
          new Promise((resolve) => {
            sendListener.callback = (uid, channel, message) => {
              expect(uid).toEqual("tiger")
              expect(channel).toEqual("y-js")
              expect(message.message instanceof Uint8Array).toBe(true)

              syncPackets += 1

              if (
                message.message.length == syncDone.message.message.length &&
                JSON.stringify(Object.assign(message, { uuid: undefined })) ==
                  JSON.stringify(syncDone.message)
              ) {
                expect(syncDonePacket).toEqual(-1)

                syncDonePacket = syncPackets
              }

              clearTimeout(timeout)
              timeout = setTimeout(() => {
                expect(syncDonePacket).toEqual(syncPackets)

                resolve()
              }, 1000)
            }

            eventListeners.get("messageReceived")({ detail: syncStep1 })
          }),
      )
  })
})