|
| 1 | +// Memory + speed benchmark for supercluster. |
| 2 | +// |
| 3 | +// Run: node --expose-gc bench.js |
| 4 | +// |
| 5 | +// Metrics: |
| 6 | +// ms median build time |
| 7 | +// alloc held + Σ(bytes freed by GC during build), via v8.GCProfiler |
| 8 | +// peak max usedHeapSize before any in-build GC |
| 9 | +// held heapUsed + external after multi-pass forced GC, vs baseline |
| 10 | +// |
| 11 | +// Each iteration's build runs inside buildAndMeasure() so the local `idx` |
| 12 | +// goes away with the call frame on return. Without this, V8 keeps the |
| 13 | +// previous iteration's index alive on the loop body's stack frame and |
| 14 | +// `held` measures incorrectly (often near zero or negative). |
1 | 15 |
|
2 | | -import Supercluster from './index.js'; |
3 | 16 | import v8 from 'v8'; |
| 17 | +import Supercluster from './index.js'; |
| 18 | + |
| 19 | +if (!global.gc) { console.error('run with --expose-gc'); process.exit(2); } |
| 20 | + |
| 21 | +const N = 1_000_000; |
| 22 | +const ITER = 3; |
| 23 | +const OPTS = {log: true, maxZoom: 17}; |
4 | 24 |
|
5 | 25 | const points = []; |
6 | | -for (let i = 0; i < 1000000; i++) { |
| 26 | +for (let i = 0; i < N; i++) { |
7 | 27 | points.push({ |
8 | 28 | type: 'Feature', |
9 | | - properties: { |
10 | | - index: i |
11 | | - }, |
12 | | - geometry: { |
13 | | - type: 'Point', |
14 | | - coordinates: [ |
15 | | - -180 + 360 * Math.random(), |
16 | | - -80 + 160 * Math.random() |
17 | | - ] |
18 | | - } |
| 29 | + properties: {index: i}, |
| 30 | + geometry: {type: 'Point', coordinates: [ |
| 31 | + -180 + 360 * Math.random(), |
| 32 | + -80 + 160 * Math.random() |
| 33 | + ]} |
19 | 34 | }); |
20 | 35 | } |
21 | 36 |
|
22 | | -global.gc(); |
23 | | -const size = v8.getHeapStatistics().used_heap_size; |
| 37 | +function settle() { |
| 38 | + for (let i = 0; i < 8; i++) global.gc(); |
| 39 | +} |
| 40 | + |
| 41 | +function snap() { |
| 42 | + const m = process.memoryUsage(); |
| 43 | + return m.heapUsed + m.external; |
| 44 | +} |
24 | 45 |
|
25 | | -const index = new Supercluster({ |
26 | | - log: true, |
27 | | - maxZoom: 6, |
28 | | - // map: props => ({sum: props.index}), |
29 | | - // reduce: (accumulated, props) => { accumulated.sum += props.sum; } |
30 | | -}).load(points); |
| 46 | +function fmt(b) { |
| 47 | + const mb = Math.abs(b) / (1024 * 1024); |
| 48 | + if (mb < 1) return `${(b / 1024).toFixed(0)} KB`; |
| 49 | + if (mb < 100) return `${(b / (1024 * 1024)).toFixed(1)} MB`; |
| 50 | + return `${(b / (1024 * 1024)).toFixed(0)} MB`; |
| 51 | +} |
| 52 | + |
| 53 | +function buildAndMeasure(baseline) { |
| 54 | + const prof = new v8.GCProfiler(); |
| 55 | + prof.start(); |
| 56 | + const t0 = performance.now(); |
| 57 | + const idx = new Supercluster(OPTS).load(points); |
| 58 | + const ms = performance.now() - t0; |
| 59 | + const stats = prof.stop().statistics; |
| 60 | + const ext = process.memoryUsage().external; |
| 61 | + |
| 62 | + let freed = 0; |
| 63 | + let peakHeap = 0; |
| 64 | + for (const e of stats) { |
| 65 | + const before = e.beforeGC.heapStatistics.usedHeapSize; |
| 66 | + const after = e.afterGC.heapStatistics.usedHeapSize; |
| 67 | + if (before > after) freed += before - after; |
| 68 | + if (before > peakHeap) peakHeap = before; |
| 69 | + } |
| 70 | + |
| 71 | + settle(); |
| 72 | + const held = snap() - baseline; |
| 73 | + // feed a read of idx into the returned value; V8 can't elide a property |
| 74 | + // load whose result escapes, so idx stays live across settle() |
| 75 | + return {ms, alloc: held + freed, peak: peakHeap + ext - baseline, held, trees: idx.trees.length}; |
| 76 | +} |
| 77 | + |
| 78 | +console.log(`--- warmup (${N.toLocaleString()} points, maxZoom ${OPTS.maxZoom}) ---`); |
| 79 | +(function warmup() { new Supercluster(OPTS).load(points); })(); |
| 80 | +settle(); |
| 81 | +const baseline = snap(); |
| 82 | + |
| 83 | +const samples = []; |
| 84 | +for (let i = 0; i < ITER; i++) { |
| 85 | + console.log(`\n--- iteration ${i + 1} of ${ITER} ---`); |
| 86 | + const s = buildAndMeasure(baseline); |
| 87 | + settle(); |
| 88 | + console.log(`alloc=${fmt(s.alloc)} peak=${fmt(s.peak)} held=${fmt(s.held)}`); |
| 89 | + samples.push(s); |
| 90 | +} |
31 | 91 |
|
32 | | -global.gc(); |
33 | | -console.log(`memory used: ${Math.round((v8.getHeapStatistics().used_heap_size - size) / 1024)} KB`); |
| 92 | +const pick = (key, agg) => agg(samples.map(s => s[key])); |
| 93 | +const max = arr => Math.max(...arr); |
| 94 | +const median = arr => arr.sort((a, b) => a - b)[arr.length >> 1]; |
34 | 95 |
|
35 | | -index.getClusters([-180, -90, 180, 90], 0).map(f => JSON.stringify(f.properties)); |
| 96 | +console.log(`\n--- results (median ms/held, max alloc/peak across ${ITER} iterations) ---`); |
| 97 | +console.log(`ms: ${pick('ms', median).toFixed(0)}`); |
| 98 | +console.log(`alloc: ${fmt(pick('alloc', max))}`); |
| 99 | +console.log(`peak: ${fmt(pick('peak', max))}`); |
| 100 | +console.log(`held: ${fmt(pick('held', median))}`); |
0 commit comments