a post-component library for building user-interfaces on the web.
at main 615 lines 14 kB view raw
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})