|
| 1 | +// Benchmark for the lazy `@computed` decorator (#4616 / #4639). |
| 2 | +// |
| 3 | +// Shape: many instances of a class with several `@computed` getters where only |
| 4 | +// one getter is read per instance — the realistic "wide class, sparse access" |
| 5 | +// pattern. Compares construction heap, construction time, first-read time, and |
| 6 | +// re-read time. Run with `yarn perf-decorator` (requires a prior `yarn build`). |
| 7 | + |
| 8 | +/* eslint-disable @typescript-eslint/no-require-imports */ |
| 9 | +import * as path from "path" |
| 10 | +const distPath = path.resolve(__dirname, "..", "..", "..", "dist", "mobx.cjs.development.js") |
| 11 | +const mobx = require(distPath) as { |
| 12 | + computed: any |
| 13 | + makeObservable: any |
| 14 | + observable: any |
| 15 | +} |
| 16 | +const { computed, makeObservable, observable } = mobx |
| 17 | + |
| 18 | +const INSTANCES = 50_000 |
| 19 | +const GETTERS_PER_INSTANCE = 10 |
| 20 | +const RUNS = 3 |
| 21 | +const RE_READS = 5 |
| 22 | + |
| 23 | +class Wide { |
| 24 | + @observable accessor v = 1 |
| 25 | + |
| 26 | + @computed get c0() { |
| 27 | + return this.v + 0 |
| 28 | + } |
| 29 | + @computed get c1() { |
| 30 | + return this.v + 1 |
| 31 | + } |
| 32 | + @computed get c2() { |
| 33 | + return this.v + 2 |
| 34 | + } |
| 35 | + @computed get c3() { |
| 36 | + return this.v + 3 |
| 37 | + } |
| 38 | + @computed get c4() { |
| 39 | + return this.v + 4 |
| 40 | + } |
| 41 | + @computed get c5() { |
| 42 | + return this.v + 5 |
| 43 | + } |
| 44 | + @computed get c6() { |
| 45 | + return this.v + 6 |
| 46 | + } |
| 47 | + @computed get c7() { |
| 48 | + return this.v + 7 |
| 49 | + } |
| 50 | + @computed get c8() { |
| 51 | + return this.v + 8 |
| 52 | + } |
| 53 | + @computed get c9() { |
| 54 | + return this.v + 9 |
| 55 | + } |
| 56 | + |
| 57 | + constructor() { |
| 58 | + makeObservable(this) |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +const GETTERS = ["c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9"] as const |
| 63 | +if (GETTERS.length !== GETTERS_PER_INSTANCE) throw new Error("getter list mismatch") |
| 64 | + |
| 65 | +function forceGc() { |
| 66 | + if (typeof global.gc === "function") global.gc() |
| 67 | +} |
| 68 | + |
| 69 | +function heapMB(): number { |
| 70 | + return process.memoryUsage().heapUsed / (1024 * 1024) |
| 71 | +} |
| 72 | + |
| 73 | +type Sample = { |
| 74 | + constructHeapMB: number |
| 75 | + constructMs: number |
| 76 | + firstReadMs: number |
| 77 | + reReadMs: number |
| 78 | +} |
| 79 | + |
| 80 | +function bench(): Sample { |
| 81 | + forceGc() |
| 82 | + const heapBefore = heapMB() |
| 83 | + |
| 84 | + const t0 = performance.now() |
| 85 | + const instances: Wide[] = new Array(INSTANCES) |
| 86 | + for (let i = 0; i < INSTANCES; i++) instances[i] = new Wide() |
| 87 | + const t1 = performance.now() |
| 88 | + |
| 89 | + forceGc() |
| 90 | + const heapAfter = heapMB() |
| 91 | + |
| 92 | + const t2 = performance.now() |
| 93 | + let sink = 0 |
| 94 | + for (let i = 0; i < INSTANCES; i++) { |
| 95 | + sink += (instances[i] as any)[GETTERS[i % GETTERS_PER_INSTANCE]] |
| 96 | + } |
| 97 | + const t3 = performance.now() |
| 98 | + |
| 99 | + const t4 = performance.now() |
| 100 | + for (let r = 0; r < RE_READS; r++) { |
| 101 | + for (let i = 0; i < INSTANCES; i++) { |
| 102 | + sink += (instances[i] as any)[GETTERS[i % GETTERS_PER_INSTANCE]] |
| 103 | + } |
| 104 | + } |
| 105 | + const t5 = performance.now() |
| 106 | + |
| 107 | + if (sink === Number.NEGATIVE_INFINITY) console.log("unreachable") |
| 108 | + |
| 109 | + return { |
| 110 | + constructHeapMB: heapAfter - heapBefore, |
| 111 | + constructMs: t1 - t0, |
| 112 | + firstReadMs: t3 - t2, |
| 113 | + reReadMs: t5 - t4 |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +function fmt(n: number, digits = 1): string { |
| 118 | + return n.toFixed(digits).padStart(8) |
| 119 | +} |
| 120 | + |
| 121 | +function main() { |
| 122 | + console.log( |
| 123 | + `\nLazy @computed decorator benchmark — ${INSTANCES} instances × ` + |
| 124 | + `${GETTERS_PER_INSTANCE} @computed getters, 1 read/instance, ${RE_READS} re-reads.` |
| 125 | + ) |
| 126 | + console.log(`Node ${process.version}, ${RUNS} timed runs after 1 warmup.\n`) |
| 127 | + |
| 128 | + bench() // warmup |
| 129 | + |
| 130 | + const samples: Sample[] = [] |
| 131 | + for (let r = 0; r < RUNS; r++) samples.push(bench()) |
| 132 | + |
| 133 | + console.log("run | construct heap MB | construct ms | first-read ms | re-read ms") |
| 134 | + console.log("----+-------------------+--------------+---------------+-----------") |
| 135 | + samples.forEach((s, i) => { |
| 136 | + console.log( |
| 137 | + ` ${i + 1} | ${fmt(s.constructHeapMB)} | ${fmt(s.constructMs)} | ${fmt( |
| 138 | + s.firstReadMs |
| 139 | + )} | ${fmt(s.reReadMs)}` |
| 140 | + ) |
| 141 | + }) |
| 142 | + |
| 143 | + const avg = (pick: (s: Sample) => number) => |
| 144 | + samples.reduce((a, s) => a + pick(s), 0) / samples.length |
| 145 | + console.log( |
| 146 | + `avg | ${fmt(avg(s => s.constructHeapMB))} | ${fmt( |
| 147 | + avg(s => s.constructMs) |
| 148 | + )} | ${fmt(avg(s => s.firstReadMs))} | ${fmt(avg(s => s.reReadMs))}` |
| 149 | + ) |
| 150 | +} |
| 151 | + |
| 152 | +main() |
0 commit comments