a post-component library for building user-interfaces on the web.
1import { html } from 'dhtml'
2import { invalidate } from 'dhtml/client'
3import { bench } from '../../../scripts/test/test.ts'
4import { setup } from './setup.ts'
5
6// ==============================
7// Data Structures
8// ==============================
9
10class TableState {
11 items: TableItemState[]
12
13 constructor(rows: number, cols: number) {
14 this.items = []
15 for (let i = 0; i < rows; i++) {
16 const props: string[] = []
17 for (let j = 0; j < cols; j++) {
18 props.push(`${i}:${j}`)
19 }
20 this.items.push(new TableItemState(i, false, props))
21 }
22 }
23
24 remove_all() {
25 this.items = []
26 }
27
28 sort_by_column(col: number) {
29 this.items.sort((a, b) => a.props[col].localeCompare(b.props[col]))
30 }
31
32 filter(nth: number) {
33 this.items = this.items.filter((_, i) => (i + 1) % nth !== 0)
34 }
35
36 async activate(nth: number) {
37 for (let i = 0; i < this.items.length; i++) {
38 this.items[i].active = (i + 1) % nth === 0
39 }
40 await invalidate(...this.items)
41 }
42
43 render() {
44 return html`
45 <table class="Table">
46 <tbody>
47 ${this.items}
48 </tbody>
49 </table>
50 `
51 }
52}
53
54class TableItemState {
55 id: number
56 active: boolean
57 props: string[]
58
59 constructor(id: number, active: boolean, props: string[]) {
60 this.id = id
61 this.active = active
62 this.props = props
63 }
64
65 render() {
66 function cell(text: string) {
67 return html`<td
68 class="TableCell"
69 onclick=${(e: Event) => {
70 console.log('Clicked' + text)
71 e.stopPropagation()
72 }}
73 >
74 ${text}
75 </td>`
76 }
77
78 return html`
79 <tr class=${this.active ? 'TableRow active' : 'TableRow'} data-id=${this.id}>
80 ${cell('#' + this.id)} ${this.props.map(prop => cell(prop))}
81 </tr>
82 `
83 }
84}
85
86// ==============================
87// Animation Data Structures
88// ==============================
89
90class AnimState {
91 items: AnimBoxState[]
92
93 constructor(count: number) {
94 this.items = []
95 for (let i = 0; i < count; i++) {
96 this.items.push(new AnimBoxState(i, 0))
97 }
98 }
99
100 async advance_each(nth: number) {
101 const renderables: AnimBoxState[] = []
102 for (let i = 0; i < this.items.length; i++) {
103 if ((i + 1) % nth === 0) {
104 this.items[i].time++
105 renderables.push(this.items[i])
106 }
107 }
108 await invalidate(...renderables)
109 }
110
111 render() {
112 return html`<div class="Anim">${this.items}</div>`
113 }
114}
115
116class AnimBoxState {
117 id: number
118 time: number
119
120 constructor(id: number, time: number) {
121 this.id = id
122 this.time = time
123 }
124
125 render() {
126 return html`
127 <div
128 class="AnimBox"
129 data-id=${this.id}
130 style=${`
131 border-radius: ${this.time % 10}px;
132 background: rgba(0,0,0,${0.5 + (this.time % 10) / 10});
133 `}
134 ></div>
135 `
136 }
137}
138
139// ==============================
140// Tree Data Structures
141// ==============================
142
143class TreeState {
144 root: TreeNodeState
145
146 constructor(hierarchy: number[]) {
147 let id_counter = 0
148 function create_node(depth: number, max_depth: number): TreeNodeState {
149 const id = id_counter++
150
151 if (depth === max_depth) {
152 return new TreeNodeState(id, false, null)
153 }
154
155 const children: TreeNodeState[] = []
156 const child_count = hierarchy[depth]
157
158 for (let i = 0; i < child_count; i++) {
159 children.push(create_node(depth + 1, max_depth))
160 }
161
162 return new TreeNodeState(id, true, children)
163 }
164
165 this.root = create_node(0, hierarchy.length - 1)
166 }
167
168 remove_all() {
169 this.root.children = []
170 }
171
172 async reverse() {
173 const renderables: TreeNodeState[] = []
174 const reverse_children = (node: TreeNodeState) => {
175 if (node.container && node.children) {
176 node.children.reverse()
177 for (const child of node.children) {
178 if (child.container) {
179 reverse_children(child)
180 }
181 }
182 renderables.push(node)
183 }
184 }
185
186 reverse_children(this.root)
187 await invalidate(...renderables)
188 }
189
190 async insert_first(n: number) {
191 const renderables: TreeNodeState[] = []
192 function insert_at_containers(node: TreeNodeState, id_counter: { value: number }) {
193 if (node.container && node.children) {
194 const new_nodes: TreeNodeState[] = []
195 for (let i = 0; i < n; i++) {
196 new_nodes.push(new TreeNodeState(id_counter.value++, false, null))
197 }
198 node.children.unshift(...new_nodes)
199
200 for (const child of node.children) {
201 if (child.container) {
202 insert_at_containers(child, id_counter)
203 }
204 }
205 renderables.push(node)
206 }
207 }
208
209 // Find the highest ID to start creating new IDs from
210 let max_id = 0
211 const find_max_id = (node: TreeNodeState) => {
212 max_id = Math.max(max_id, node.id)
213 if (node.container && node.children) {
214 for (const child of node.children) {
215 find_max_id(child)
216 }
217 }
218 }
219
220 find_max_id(this.root)
221 const id_counter = { value: max_id + 1 }
222
223 insert_at_containers(this.root, id_counter)
224 await invalidate(...renderables)
225 }
226
227 async insert_last(n: number) {
228 const renderables: TreeNodeState[] = []
229 function insert_at_containers(node: TreeNodeState, id_counter: { value: number }) {
230 if (node.container && node.children) {
231 const new_nodes: TreeNodeState[] = []
232 for (let i = 0; i < n; i++) {
233 new_nodes.push(new TreeNodeState(id_counter.value++, false, null))
234 }
235 node.children.push(...new_nodes)
236
237 for (const child of node.children) {
238 if (child.container) {
239 insert_at_containers(child, id_counter)
240 }
241 }
242 renderables.push(node)
243 }
244 }
245
246 // Find the highest ID to start creating new IDs from
247 let max_id = 0
248 const find_max_id = (node: TreeNodeState) => {
249 max_id = Math.max(max_id, node.id)
250 if (node.container && node.children) {
251 for (const child of node.children) {
252 find_max_id(child)
253 }
254 }
255 }
256
257 find_max_id(this.root)
258 const id_counter = { value: max_id + 1 }
259
260 insert_at_containers(this.root, id_counter)
261 await invalidate(...renderables)
262 }
263
264 async remove_first(n: number) {
265 const renderables: TreeNodeState[] = []
266 const remove_from_containers = (node: TreeNodeState) => {
267 if (node.container && node.children) {
268 node.children.splice(0, Math.min(n, node.children.length))
269
270 for (const child of node.children) {
271 if (child.container) {
272 remove_from_containers(child)
273 }
274 }
275
276 renderables.push(node)
277 }
278 }
279
280 remove_from_containers(this.root)
281 await invalidate(...renderables)
282 }
283
284 async remove_last(n: number) {
285 const renderables: TreeNodeState[] = []
286 const remove_from_containers = (node: TreeNodeState) => {
287 if (node.container && node.children) {
288 const length = node.children.length
289 node.children.splice(Math.max(0, length - n), Math.min(n, length))
290
291 for (const child of node.children) {
292 if (child.container) {
293 remove_from_containers(child)
294 }
295 }
296
297 renderables.push(node)
298 }
299 }
300
301 remove_from_containers(this.root)
302 await invalidate(...renderables)
303 }
304
305 async move_from_end_to_start(n: number) {
306 const renderables: TreeNodeState[] = []
307 const move_in_containers = (node: TreeNodeState) => {
308 if (node.container && node.children && node.children.length > n) {
309 const length = node.children.length
310 const moved = node.children.splice(length - n, n)
311 node.children.unshift(...moved)
312
313 for (const child of node.children) {
314 if (child.container) {
315 move_in_containers(child)
316 }
317 }
318
319 renderables.push(node)
320 }
321 }
322
323 move_in_containers(this.root)
324 await invalidate(...renderables)
325 }
326
327 async move_from_start_to_end(n: number) {
328 const renderables: TreeNodeState[] = []
329 const move_in_containers = (node: TreeNodeState) => {
330 if (node.container && node.children && node.children.length > n) {
331 const moved = node.children.splice(0, n)
332 node.children.push(...moved)
333
334 for (const child of node.children) {
335 if (child.container) {
336 move_in_containers(child)
337 }
338 }
339
340 renderables.push(node)
341 }
342 }
343
344 move_in_containers(this.root)
345 await invalidate(...renderables)
346 }
347
348 // Worst case scenarios
349 async kivi_worst_case() {
350 await this.remove_first(1)
351 await this.remove_last(1)
352 await this.reverse()
353 }
354
355 async snabbdom_worst_case() {
356 const renderables: TreeNodeState[] = []
357 const transform = (node: TreeNodeState) => {
358 if (node.container && node.children && node.children.length > 2) {
359 const first = node.children.shift()
360 if (first) {
361 const secondToLast = node.children.splice(node.children.length - 2, 1)[0]
362 node.children.push(first, secondToLast)
363 }
364
365 for (const child of node.children) {
366 if (child.container) {
367 transform(child)
368 }
369 }
370
371 renderables.push(node)
372 }
373 }
374
375 transform(this.root)
376 await invalidate(...renderables)
377 }
378
379 async react_worst_case() {
380 await this.remove_first(1)
381 await this.remove_last(1)
382 await this.move_from_end_to_start(1)
383 }
384
385 async virtual_dom_worst_case() {
386 await this.move_from_start_to_end(2)
387 }
388
389 render() {
390 return html`<div class="Tree">${this.root}</div>`
391 }
392}
393
394class TreeNodeState {
395 id: number
396 container: boolean
397 children: TreeNodeState[] | null
398
399 constructor(id: number, container: boolean, children: TreeNodeState[] | null) {
400 this.id = id
401 this.container = container
402 this.children = children
403 }
404
405 render() {
406 if (!this.container) {
407 return html`<li class="TreeLeaf">${this.id}</li>`
408 }
409
410 return html`
411 <ul class="TreeNode">
412 ${this.children}
413 </ul>
414 `
415 }
416}
417
418// ==============================
419// Benchmark Cases
420// ==============================
421
422function bench_setup(name: string, fn: (root: ReturnType<typeof setup>['root']) => void | Promise<void>): void {
423 bench(name, async () => {
424 const { root, el } = setup()
425 try {
426 await fn(root)
427 } finally {
428 root.render(null)
429 el.remove()
430 }
431 })
432}
433
434// Table Benchmark Cases
435bench_setup('table/small/render', async root => {
436 const state = new TableState(15, 4)
437 root.render(state)
438})
439
440bench_setup('table/small/removeAll', async root => {
441 const state = new TableState(15, 4)
442 root.render(state)
443 state.remove_all()
444 await invalidate(state)
445})
446
447bench_setup('table/small/sort', async root => {
448 const state = new TableState(15, 4)
449 root.render(state)
450 state.sort_by_column(1)
451 await invalidate(state)
452})
453
454bench_setup('table/small/filter', async root => {
455 const state = new TableState(15, 4)
456 root.render(state)
457 state.filter(4)
458 await invalidate(state)
459})
460
461bench_setup('table/small/activate', async root => {
462 const state = new TableState(15, 4)
463 root.render(state)
464 await state.activate(4)
465})
466
467bench_setup('table/large/render', async root => {
468 const state = new TableState(100, 4)
469 root.render(state)
470})
471
472bench_setup('table/large/removeAll', async root => {
473 const state = new TableState(100, 4)
474 root.render(state)
475 state.remove_all()
476 await invalidate(state)
477})
478
479bench_setup('table/large/sort', async root => {
480 const state = new TableState(100, 4)
481 root.render(state)
482 state.sort_by_column(1)
483 await invalidate(state)
484})
485
486bench_setup('table/large/filter', async root => {
487 const state = new TableState(100, 4)
488 root.render(state)
489 state.filter(16)
490 await invalidate(state)
491})
492
493bench_setup('table/large/activate', async root => {
494 const state = new TableState(100, 4)
495 root.render(state)
496 await state.activate(16)
497})
498
499// Animation Benchmark Cases
500bench_setup('anim/small/advance', async root => {
501 const state = new AnimState(30)
502 root.render(state)
503 await state.advance_each(4)
504})
505
506bench_setup('anim/large/advance', async root => {
507 const state = new AnimState(100)
508 root.render(state)
509 await state.advance_each(16)
510})
511
512// Tree Benchmark Cases - Small
513bench_setup('tree/small/render', async root => {
514 const state = new TreeState([5, 10])
515 root.render(state)
516})
517
518bench_setup('tree/small/removeAll', async root => {
519 const state = new TreeState([5, 10])
520 root.render(state)
521 state.remove_all()
522 await invalidate(state)
523})
524
525bench_setup('tree/small/reverse', async root => {
526 const state = new TreeState([5, 10])
527 root.render(state)
528 await state.reverse()
529})
530
531bench_setup('tree/small/insertFirst', async root => {
532 const state = new TreeState([5, 10])
533 root.render(state)
534 await state.insert_first(2)
535})
536
537bench_setup('tree/small/insertLast', async root => {
538 const state = new TreeState([5, 10])
539 root.render(state)
540 await state.insert_last(2)
541})
542
543bench_setup('tree/small/removeFirst', async root => {
544 const state = new TreeState([5, 10])
545 root.render(state)
546 await state.remove_first(2)
547})
548
549bench_setup('tree/small/removeLast', async root => {
550 const state = new TreeState([5, 10])
551 root.render(state)
552 await state.remove_last(2)
553})
554
555bench_setup('tree/small/moveFromEndToStart', async root => {
556 const state = new TreeState([5, 10])
557 root.render(state)
558 await state.move_from_end_to_start(2)
559})
560
561bench_setup('tree/small/moveFromStartToEnd', async root => {
562 const state = new TreeState([5, 10])
563 root.render(state)
564 await state.move_from_start_to_end(2)
565})
566
567bench_setup('tree/small/no_change', async root => {
568 const state = new TreeState([5, 10])
569 root.render(state)
570 await invalidate(state)
571})
572
573// Tree Benchmark Cases - Large
574bench_setup('tree/large/render', async root => {
575 const state = new TreeState([50, 10])
576 root.render(state)
577})
578
579bench_setup('tree/large/removeAll', async root => {
580 const state = new TreeState([50, 10])
581 root.render(state)
582 state.remove_all()
583 await invalidate(state)
584})
585
586bench_setup('tree/large/reverse', async root => {
587 const state = new TreeState([50, 10])
588 root.render(state)
589 await state.reverse()
590})
591
592// Worst Case Scenarios
593bench_setup('tree/worst_case/kivi', async root => {
594 const state = new TreeState([10, 10])
595 root.render(state)
596 await state.kivi_worst_case()
597})
598
599bench_setup('tree/worst_case/snabbdom', async root => {
600 const state = new TreeState([10, 10])
601 root.render(state)
602 await state.snabbdom_worst_case()
603})
604
605bench_setup('tree/worst_case/react', async root => {
606 const state = new TreeState([10, 10])
607 root.render(state)
608 await state.react_worst_case()
609})
610
611bench_setup('tree/worst_case/virtual_dom', async root => {
612 const state = new TreeState([10, 10])
613 root.render(state)
614 await state.virtual_dom_worst_case()
615})