Newer
Older
import { test, equal, ok, notOk } from "zora"
import chalk from "chalk"
import { connect } from "../src/room.js"
import WasmCRDT from "../src/wasm-crdt.js"
import MockConnection, {
sendListener,
broadcastListener,
getEventListener,
} from "../src/connection/MockConnection.js"
createMessageReceivedEvent as _createMessageReceivedEvent,
handshake,
dotDraw,
dotErase,
Moritz Langenstein
committed
pathDraw,
pathErase,
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
} 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)
}
function parse(text) {
return JSON.parse(text, reviver)
}
function replacer(key, value) {
if (value instanceof Uint8Array) {
return "base64:" + Buffer.from(value).toString("base64")
}
return value
}
function reviver(key, value) {
if (typeof value == "string" && value.startsWith("base64:")) {
return Uint8Array.from(Buffer.from(value.slice("base64:".length), "base64"))
}
return value
}
// End: Adapted from https://github.com/jprichardson/buffer-json (MIT license)
let fs
async function initFileSystem() {
fs = await new Promise((resolve, reject) =>
window.webkitRequestFileSystem(
window.TEMPORARY,
1024 * 1024 * 100,
resolve,
reject,
),
)
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
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) {
console.debug(`\n ${title} (${iterations} iterations):\n`)
for (const title in results) {
Moritz Langenstein
committed
const {
timeLoc,
encodeRAM,
packets,
size,
timeRem,
decodeRAM,
events,
} = results[title]
const synchronisation = title == "synchronisation"
console.debug(
chalk` {yellow ⧗} {dim ${title}:} {yellow.inverse ${(
timeLoc /
(1e3 * (synchronisation ? 1 : iterations))
Moritz Langenstein
committed
).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 /
(1e3 * (synchronisation ? 1 : iterations))
Moritz Langenstein
committed
).toFixed(3)}ms ${synchronisation ? "total" : "/ it"}} + {red.inverse ${(
decodeRAM /
(1024 * 1024)
).toFixed(3)}MB} => {dim ${events} event(s)}\n`,
)
}
console.debug(`\n`)
}
Moritz Langenstein
committed
function writeBenchmarkHeader(filename, title, results) {
console.info(JSON.stringify({ filename, title, results }))
Moritz Langenstein
committed
}
function appendBenchmark(filename, iterations, results) {
console.info(JSON.stringify({ filename, iterations, results }))
Moritz Langenstein
committed
}
function captureHeapUsage() {
for (let i = 0; i < 10; i++) {
window.gc()
Moritz Langenstein
committed
}
return performance.memory.usedJSHeapSize
Moritz Langenstein
committed
}
function runBidirectionalBenchmark(
BENCHMARK,
FILENAME,
ITERATIONSLIST,
Moritz Langenstein
committed
BLOCKSIZE,
Moritz Langenstein
committed
addData,
eraseData,
addOnInitFrontend,
addBroadcastGroupTimeout,
addOnBroadcastGroup,
addPacketsFilename,
eraseOnInitFrontend,
eraseBroadcastGroupTimeout,
eraseOnBroadcastGroup,
erasePacketsFilename,
syncSendGroupTimeout,
syncOnSendGroup,
syncPacketsFilename,
addOnInitBackend,
addEventGroupTimeout,
addOnEventGroup,
addEventsFilename,
Moritz Langenstein
committed
eraseOnInitBackend,
eraseEventGroupTimeout,
eraseOnEventGroup,
eraseEventsFilename,
Moritz Langenstein
committed
syncEventGroupTimeout,
syncOnEventGroup,
syncEventsFilename,
Moritz Langenstein
committed
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
) {
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 = []
Moritz Langenstein
committed
let addRemRAM
let eraseRemTime = 0
let eraseEvents = []
Moritz Langenstein
committed
let eraseRemRAM
let disconnectUpdRAM // eslint-disable-line no-unused-vars
let connectSyncRAM // eslint-disable-line no-unused-vars
let syncRemTime = 0
let syncEvents = []
Moritz Langenstein
committed
let syncRemRAM
let disconnectSyncRAM // eslint-disable-line no-unused-vars
let timeout
let addEventListener = null
let eraseEventListener = null
Moritz Langenstein
committed
let room = null
let updateRoom = null
let syncRoom = null
return (
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve) => {
userID.uuid = "61a540d6-4522-48c7-a660-1ed501503cb7"
room = await connect("room", WasmCRDT, MockConnection)
Moritz Langenstein
committed
getEventListener(
"room",
"messageReceived",
)(createMessageReceivedEvent(handshake, "tw-ml"))
connectRAM = captureHeapUsage()
return resolve()
})
.then(
() =>
new Promise((resolve) => {
let broadcasts = 0
broadcastListener.callback = (channel, message) => {
currTime = window.performance.now()
Moritz Langenstein
committed
equal(channel, "crdt")
ok(message.message instanceof Uint8Array)
Moritz Langenstein
committed
addPackets[addPackets.length - 1].push(message)
addSize += message.message.length
clearTimeout(timeout)
timeout = setTimeout(() => {
broadcasts += 1
addLocTime += (currTime - prevTime) * 1e3
Moritz Langenstein
committed
prevTime = window.performance.now()
Moritz Langenstein
committed
addOnBroadcastGroup(
room,
addPackets,
pathIDs,
addData,
ITERATIONS,
broadcasts,
resolve,
)
}, addBroadcastGroupTimeout)
}
prevTime = window.performance.now()
Moritz Langenstein
committed
Moritz Langenstein
committed
addOnInitFrontend(
room,
addPackets,
pathIDs,
addData,
ITERATIONS,
BLOCKSIZE,
)
Moritz Langenstein
committed
}),
)
.then(async () => {
Moritz Langenstein
committed
broadcastListener.callback = null
await dumpBSON(addPacketsFilename, addPackets)
Moritz Langenstein
committed
addPackets = null
addRAM = captureHeapUsage()
})
.then(
() =>
new Promise((resolve) => {
let broadcasts = 0
broadcastListener.callback = (channel, message) => {
currTime = window.performance.now()
Moritz Langenstein
committed
equal(channel, "crdt")
ok(message.message instanceof Uint8Array)
Moritz Langenstein
committed
erasePackets[erasePackets.length - 1].push(message)
eraseSize += message.message.length
clearTimeout(timeout)
timeout = setTimeout(() => {
broadcasts += 1
eraseLocTime += (currTime - prevTime) * 1e3
Moritz Langenstein
committed
prevTime = window.performance.now()
Moritz Langenstein
committed
eraseOnBroadcastGroup(
room,
erasePackets,
pathIDs,
eraseData,
ITERATIONS,
broadcasts,
resolve,
)
}, eraseBroadcastGroupTimeout)
}
prevTime = window.performance.now()
Moritz Langenstein
committed
Moritz Langenstein
committed
eraseOnInitFrontend(
room,
erasePackets,
pathIDs,
eraseData,
ITERATIONS,
BLOCKSIZE,
)
Moritz Langenstein
committed
}),
)
.then(async () => {
Moritz Langenstein
committed
broadcastListener.callback = null
await dumpBSON(erasePacketsFilename, erasePackets)
Moritz Langenstein
committed
erasePackets = null
eraseRAM = captureHeapUsage()
})
.then(
() =>
new Promise((resolve) => {
sendListener.callback = (uid, channel, message) => {
const currTime = window.performance.now()
equal(uid, remoteUserID)
equal(channel, "crdt")
ok(message.message instanceof Uint8Array)
syncLocTime = (currTime - prevTime) * 1e3
Moritz Langenstein
committed
syncPackets.push(message)
syncSize += message.message.length
clearTimeout(timeout)
timeout = setTimeout(
() => syncOnSendGroup(syncPackets, resolve),
syncSendGroupTimeout,
)
}
prevTime = window.performance.now()
Moritz Langenstein
committed
getEventListener(
"room",
"messageReceived",
)(createMessageReceivedEvent(syncStep1))
}),
)
.then(async () => {
Moritz Langenstein
committed
sendListener.callback = null
await dumpBSON(syncPacketsFilename, syncPackets)
Moritz Langenstein
committed
syncPackets = null
syncRAM = captureHeapUsage()
room.disconnect()
room = null
disconnectRAM = captureHeapUsage()
})
.then(
() =>
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve) => {
userID.uuid = "5c9e550b-3de8-4a32-80e1-80c08c19891a"
updateRoom = await connect("update", WasmCRDT, MockConnection)
Moritz Langenstein
committed
getEventListener(
"update",
"messageReceived",
)(createMessageReceivedEvent(handshake, "tw-ml"))
connectUpdRAM = captureHeapUsage()
addPackets = await loadBSON(addPacketsFilename)
Moritz Langenstein
committed
return resolve()
}),
)
.then(
() =>
new Promise((resolve) => {
let broadcasts = 0
let currTime
const timeoutCallback = () => {
broadcasts += 1
addRemTime += (currTime - prevTime) * 1e3
Moritz Langenstein
committed
prevTime = window.performance.now()
Moritz Langenstein
committed
addOnEventGroup(
addPackets,
ITERATIONS,
broadcasts,
resolve,
addEvents,
pathIDs,
addData,
)
Moritz Langenstein
committed
}
addEventListener = (event) => {
currTime = window.performance.now()
Moritz Langenstein
committed
addEvents.push(addEventsCache ? { add: event } : null)
Moritz Langenstein
committed
clearTimeout(timeout)
timeout = setTimeout(timeoutCallback, addEventGroupTimeout)
}
eraseEventListener = (event) => {
currTime = window.performance.now()
Moritz Langenstein
committed
addEvents.push(addEventsCache ? { erase: event } : null)
Moritz Langenstein
committed
clearTimeout(timeout)
timeout = setTimeout(timeoutCallback, addEventGroupTimeout)
}
updateRoom.addEventListener(
"addOrUpdatePath",
addEventListener,
)
updateRoom.addEventListener(
"removedIntervalsChange",
eraseEventListener,
)
Moritz Langenstein
committed
prevTime = window.performance.now()
Moritz Langenstein
committed
addOnInitBackend(addPackets, BLOCKSIZE)
Moritz Langenstein
committed
}),
)
.then(async () => {
updateRoom.removeEventListener(
"addOrUpdatePath",
addEventListener,
)
addEventListener = null
updateRoom.removeEventListener(
"removedIntervalsChange",
eraseEventListener,
)
eraseEventListener = null
Moritz Langenstein
committed
addPackets = null
await dumpBSON(addEventsFilename, addEvents)
addEvents = null
Moritz Langenstein
committed
addRemRAM = captureHeapUsage()
erasePackets = await loadBSON(erasePacketsFilename)
Moritz Langenstein
committed
})
.then(
() =>
new Promise((resolve) => {
let broadcasts = 0
let currTime
const timeoutCallback = () => {
broadcasts += 1
eraseRemTime += (currTime - prevTime) * 1e3
Moritz Langenstein
committed
prevTime = window.performance.now()
Moritz Langenstein
committed
eraseOnEventGroup(
erasePackets,
ITERATIONS,
broadcasts,
resolve,
eraseEvents,
pathIDs,
addData,
eraseData,
Moritz Langenstein
committed
)
}
eraseEventListener = (event) => {
currTime = window.performance.now()
Moritz Langenstein
committed
eraseEvents.push(eraseEventsCache ? { erase: event } : null)
Moritz Langenstein
committed
clearTimeout(timeout)
timeout = setTimeout(
timeoutCallback,
eraseEventGroupTimeout,
)
}
updateRoom.addEventListener(
"removedIntervalsChange",
eraseEventListener,
)
Moritz Langenstein
committed
prevTime = window.performance.now()
Moritz Langenstein
committed
eraseOnInitBackend(erasePackets, BLOCKSIZE)
Moritz Langenstein
committed
}),
)
.then(async () => {
updateRoom.removeEventListener(
"removedIntervalsChange",
eraseEventListener,
)
eraseEventListener = null
Moritz Langenstein
committed
erasePackets = null
await dumpBSON(eraseEventsFilename, eraseEvents)
eraseEvents = null
Moritz Langenstein
committed
eraseRemRAM = captureHeapUsage()
updateRoom.disconnect()
updateRoom = null
disconnectUpdRAM = captureHeapUsage()
})
.then(
() =>
// eslint-disable-next-line no-async-promise-executor
new Promise(async (resolve) => {
userID.uuid = "a2108f84-3785-4696-8dd5-fb89b38d4f7f"
syncRoom = await connect("sync", WasmCRDT, MockConnection)
Moritz Langenstein
committed
getEventListener(
"sync",
"messageReceived",
)(createMessageReceivedEvent(handshake, "tw-ml"))
connectSyncRAM = captureHeapUsage()
syncPackets = await loadBSON(syncPacketsFilename)
Moritz Langenstein
committed
return resolve()
}),
)
.then(
() =>
new Promise((resolve) => {
addEventListener = (event) => {
const currTime = window.performance.now()
Moritz Langenstein
committed
syncRemTime = (currTime - prevTime) * 1e3
syncEvents.push(syncEventsCache ? { add: event } : null)
Moritz Langenstein
committed
clearTimeout(timeout)
timeout = setTimeout(
() =>
syncOnEventGroup(
resolve,
syncEvents,
pathIDs,
addData,
eraseData,
),
Moritz Langenstein
committed
syncEventGroupTimeout,
)
}
eraseEventListener = (event) => {
const currTime = window.performance.now()
Moritz Langenstein
committed
syncRemTime = (currTime - prevTime) * 1e3
syncEvents.push(syncEventsCache ? { erase: event } : null)
Moritz Langenstein
committed
clearTimeout(timeout)
timeout = setTimeout(
() =>
syncOnEventGroup(
resolve,
syncEvents,
pathIDs,
addData,
eraseData,
),
Moritz Langenstein
committed
syncEventGroupTimeout,
)
}
syncRoom.addEventListener("addOrUpdatePath", addEventListener)
syncRoom.addEventListener(
"removedIntervalsChange",
eraseEventListener,
)
Moritz Langenstein
committed
prevTime = window.performance.now()
Moritz Langenstein
committed
for (const syncPacket of syncPackets) {
getEventListener(
"sync",
"messageReceived",
)(createMessageReceivedEvent(syncPacket))
}
}),
)
.then(async () => {
syncRoom.removeEventListener("addOrUpdatePath", addEventListener)
addEventListener = null
syncRoom.removeEventListener(
"removedIntervalsChange",
eraseEventListener,
)
eraseEventListener = null
Moritz Langenstein
committed
syncPackets = null
await dumpBSON(syncEventsFilename, syncEvents)
syncEvents = null
Moritz Langenstein
committed
syncRemRAM = captureHeapUsage()
syncRoom.disconnect()
syncRoom = null
disconnectSyncRAM = captureHeapUsage()
})
.then(async () => {
Moritz Langenstein
committed
if (!BENCHMARK) {
return
}
addPackets = (await loadBSON(addPacketsFilename)).reduce(
Moritz Langenstein
committed
(sum, packets) => sum + packets.length,
0,
)
erasePackets = (await loadBSON(erasePacketsFilename)).reduce(
Moritz Langenstein
committed
(sum, packets) => sum + packets.length,
0,
)
syncPackets = (await loadBSON(syncPacketsFilename)).length
Moritz Langenstein
committed
addEvents = (await loadBSON(addEventsFilename)).length
eraseEvents = (await loadBSON(eraseEventsFilename)).length
syncEvents = (await loadBSON(syncEventsFilename)).length
Moritz Langenstein
committed
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
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(),
)
}
Moritz Langenstein
committed
function addOnInitFrontendSequential(
room,
addPackets,
pathIDs,
addData,
ITERATIONS, // eslint-disable-line no-unused-vars
BLOCKSIZE, // eslint-disable-line no-unused-vars
) {
addPackets.push([])
const drawPathID = room.addPath(addData[0])
pathIDs.push(drawPathID)
Moritz Langenstein
committed
for (let i = 1; i < addData.length; i++) {
room.extendPath(drawPathID, addData[i])
}
room.endPath(drawPathID)
Moritz Langenstein
committed
}
function addOnBroadcastGroupSequential(
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])
}
room.endPath(drawPathID)
Moritz Langenstein
committed
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
} else {
resolve()
}
}
function eraseOnInitFrontendSequential(
room,
erasePackets,
pathIDs,
eraseData,
ITERATIONS, // eslint-disable-line no-unused-vars
BLOCKSIZE, // eslint-disable-line no-unused-vars
) {
erasePackets.push([])
const erasePathID = pathIDs[0]
for (let i = 0; i < eraseData.length; i++) {
room.extendErasureIntervals(erasePathID, eraseData[i][0], eraseData[i][1])
}
}
function eraseOnBroadcastGroupSequential(
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()
}
}
function syncOnSendGroup(syncPackets, resolve) {
resolve()
}
function addOnInitBackendSequential(
addPackets,
BLOCKSIZE, // eslint-disable-line no-unused-vars
) {
Moritz Langenstein
committed
for (const packet of addPackets[0]) {
getEventListener(
"update",
"messageReceived",
)(createMessageReceivedEvent(packet))
}
}
function addOnEventGroupSequential(
addPackets,
ITERATIONS,
broadcasts,
resolve,
addEvents, // eslint-disable-line no-unused-vars
pathIDs, // eslint-disable-line no-unused-vars
addData, // eslint-disable-line no-unused-vars
Moritz Langenstein
committed
) {
if (broadcasts >= ITERATIONS) {
return resolve()
}
for (const packet of addPackets[broadcasts]) {
getEventListener(
"update",
"messageReceived",
)(createMessageReceivedEvent(packet))
}
}
function eraseOnInitBackendSequential(
erasePackets,
BLOCKSIZE, // eslint-disable-line no-unused-vars
) {
Moritz Langenstein
committed
for (const packet of erasePackets[0]) {
getEventListener(
"update",
"messageReceived",
)(createMessageReceivedEvent(packet))
}
}
function eraseOnEventGroupSequential(
erasePackets,
ITERATIONS,
broadcasts,
resolve,
eraseEvents, // eslint-disable-line no-unused-vars
pathIDs, // eslint-disable-line no-unused-vars
addData, // eslint-disable-line no-unused-vars
eraseData, // eslint-disable-line no-unused-vars
Moritz Langenstein
committed
) {
if (broadcasts >= ITERATIONS) {
return resolve()
}
for (const packet of erasePackets[broadcasts]) {
getEventListener(
"update",
"messageReceived",
)(createMessageReceivedEvent(packet))
}
}
function syncOnEventGroup(
resolve,
syncEvents, // eslint-disable-line no-unused-vars
pathIDs, // eslint-disable-line no-unused-vars
addData, // eslint-disable-line no-unused-vars
eraseData, // eslint-disable-line no-unused-vars
) {
Moritz Langenstein
committed
resolve()
}
function addOnInitFrontendParallel(
room,
addPackets,
pathIDs,
addData,
ITERATIONS,
BLOCKSIZE,
) {
addPackets.push([])
// Necessary to allow yjs to execute transactions (majority of processing time)
function addPath(sj) {
if (sj >= ITERATIONS) return
for (let j = sj; j < Math.min(sj + BLOCKSIZE, ITERATIONS); j++) {
const drawPathID = room.addPath(addData[0])
pathIDs.push(drawPathID)
Moritz Langenstein
committed
for (let i = 1; i < addData.length; i++) {
room.extendPath(drawPathID, addData[i])
}