|
| 1 | +""" |
| 2 | +Benchmarks for keyed v-for reconciliation. |
| 3 | +
|
| 4 | +Stresses ListFragment's keyed reconciliation path: the LIS computation |
| 5 | +and Fragment.anchor() lookups used to reposition/insert fragments. |
| 6 | +
|
| 7 | +Each round replaces the whole `items` list in one reactive assignment |
| 8 | +(the idiomatic way to reorder/replace a collection), rather than doing |
| 9 | +many individual in-place mutations, which would each trigger their own |
| 10 | +full reconciliation pass and dominate the measurement with unrelated |
| 11 | +per-mutation dependency-tracking cost. |
| 12 | +""" |
| 13 | + |
| 14 | +import gc |
| 15 | +import random |
| 16 | + |
| 17 | +import pytest |
| 18 | +from observ import reactive |
| 19 | + |
| 20 | +from collagraph import Collagraph, EventLoopType |
| 21 | +from collagraph.renderers import DictRenderer |
| 22 | + |
| 23 | +SIZES = [10, 100, 1_000] |
| 24 | +SIZE_IDS = ["10", "100", "1k"] |
| 25 | +PATTERNS = ["append", "prepend", "reverse", "shuffle"] |
| 26 | + |
| 27 | + |
| 28 | +def _new_items(items, pattern): |
| 29 | + if pattern == "append": |
| 30 | + return [*items, {"id": len(items), "text": "x"}] |
| 31 | + if pattern == "prepend": |
| 32 | + return [{"id": len(items), "text": "x"}, *items] |
| 33 | + if pattern == "reverse": |
| 34 | + return list(reversed(items)) |
| 35 | + if pattern == "shuffle": |
| 36 | + shuffled = list(items) |
| 37 | + random.Random(0).shuffle(shuffled) |
| 38 | + return shuffled |
| 39 | + raise ValueError(pattern) |
| 40 | + |
| 41 | + |
| 42 | +def _apply(gui, state, items): |
| 43 | + # `gui` is unused but must be kept as a live argument: it holds the |
| 44 | + # only strong reference to the mounted fragment tree, so passing it |
| 45 | + # through keeps that tree alive for the duration of the timed call. |
| 46 | + # GC is disabled during the timed call so that collection pauses |
| 47 | + # (triggered by the allocation burst) don't dominate the variance. |
| 48 | + gc.disable() |
| 49 | + try: |
| 50 | + state["items"] = items |
| 51 | + finally: |
| 52 | + gc.enable() |
| 53 | + |
| 54 | + |
| 55 | +@pytest.mark.timeout(timeout=0) |
| 56 | +@pytest.mark.benchmark(group="keyed_list_reconcile") |
| 57 | +@pytest.mark.parametrize("n", SIZES, ids=SIZE_IDS) |
| 58 | +@pytest.mark.parametrize("pattern", PATTERNS) |
| 59 | +def test_keyed_list_reconcile(benchmark, parse_source, pattern, n): |
| 60 | + App, _ = parse_source( |
| 61 | + """ |
| 62 | + <node v-for="item in items" :key="item['id']" :text="item['text']" /> |
| 63 | +
|
| 64 | + <script> |
| 65 | + import collagraph as cg |
| 66 | +
|
| 67 | + class App(cg.Component): |
| 68 | + pass |
| 69 | + </script> |
| 70 | + """ |
| 71 | + ) |
| 72 | + |
| 73 | + def setup(): |
| 74 | + initial = [{"id": i, "text": str(i)} for i in range(n)] |
| 75 | + state = reactive({"items": list(initial)}) |
| 76 | + gui = Collagraph(renderer=DictRenderer(), event_loop_type=EventLoopType.SYNC) |
| 77 | + gui.render(App, {"type": "root"}, state=state) |
| 78 | + new_items = _new_items(initial, pattern) |
| 79 | + return (gui, state, new_items), {} |
| 80 | + |
| 81 | + benchmark.pedantic(_apply, setup=setup, warmup_rounds=1, rounds=30) |
0 commit comments