Skip to content

Commit bae24ae

Browse files
r1tsuucodex
andcommitted
benchmark: add world generation throughput benchmark
Add a worldgen benchmark to the shared benchmark runner and report both raw terrain-height sampling cost and full chunk generation cost. Update the README with the new benchmark command. Co-authored-by: Codex <codex@openai.com>
1 parent baca427 commit bae24ae

3 files changed

Lines changed: 159 additions & 1 deletion

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,13 @@ Use `bun run dev:full` to start a dedicated WebSocket server alongside the clien
5959
| ------------------------------- | ------------------------------------------------ |
6060
| `bun run benchmark` | Build native libraries, then run all benchmarks |
6161
| `bun run benchmark -- lighting` | Build native libraries, then run only `lighting` |
62+
| `bun run benchmark -- worldgen` | Build native libraries, then run only `worldgen` |
6263

6364
Pass benchmark-specific flags after the benchmark name:
6465

6566
```sh
6667
bun run benchmark -- lighting --warmup=5 --rounds=40
68+
bun run benchmark -- worldgen --fixtures=8 --rounds=25
6769
```
6870

6971
### Asset Generation

apps/cli/src/benchmark.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { CliBenchmark } from './benchmarks/types.ts'
22

33
import { lightingBenchmark } from './benchmarks/lighting.ts'
4+
import { worldgenBenchmark } from './benchmarks/worldgen.ts'
45

5-
const benchmarks: readonly CliBenchmark[] = [lightingBenchmark]
6+
const benchmarks: readonly CliBenchmark[] = [lightingBenchmark, worldgenBenchmark]
67

78
const argv = Bun.argv.slice(2)
89
const requestedName = argv[0] && !argv[0].startsWith('--') ? argv[0] : null
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { parseCliFlagValue } from '@craftvale/core/shared'
2+
import { CHUNK_SIZE, createGeneratedChunk, getTerrainHeight } from '@craftvale/core/shared'
3+
4+
import type { CliBenchmark } from './types.ts'
5+
6+
const DEFAULT_SEED = 1337
7+
const DEFAULT_WARMUP_ROUNDS = 10
8+
const DEFAULT_MEASURED_ROUNDS = 60
9+
const DEFAULT_FIXTURE_COORDS = [
10+
{ x: -8, z: -8 },
11+
{ x: -8, z: -4 },
12+
{ x: -8, z: 0 },
13+
{ x: -8, z: 4 },
14+
{ x: -4, z: -8 },
15+
{ x: -4, z: -4 },
16+
{ x: -4, z: 0 },
17+
{ x: -4, z: 4 },
18+
{ x: 0, z: -8 },
19+
{ x: 0, z: -4 },
20+
{ x: 0, z: 0 },
21+
{ x: 0, z: 4 },
22+
{ x: 4, z: -8 },
23+
{ x: 4, z: -4 },
24+
{ x: 4, z: 0 },
25+
{ x: 4, z: 4 },
26+
] as const
27+
28+
interface BenchmarkResult {
29+
meanMs: number
30+
medianMs: number
31+
minMs: number
32+
maxMs: number
33+
}
34+
35+
const parsePositiveIntegerFlag = (
36+
argv: readonly string[],
37+
flagName: string,
38+
fallback: number,
39+
): number => {
40+
const value = parseCliFlagValue(argv, flagName)
41+
if (value === null) {
42+
return fallback
43+
}
44+
45+
const parsed = Number.parseInt(value, 10)
46+
if (!Number.isFinite(parsed) || parsed <= 0) {
47+
throw new Error(`Expected --${flagName} to be a positive integer, got "${value}".`)
48+
}
49+
50+
return parsed
51+
}
52+
53+
const measure = (
54+
runRound: () => void,
55+
warmupRounds: number,
56+
measuredRounds: number,
57+
): BenchmarkResult => {
58+
for (let round = 0; round < warmupRounds; round += 1) {
59+
runRound()
60+
}
61+
62+
const samples: number[] = []
63+
for (let round = 0; round < measuredRounds; round += 1) {
64+
const startedAt = performance.now()
65+
runRound()
66+
samples.push(performance.now() - startedAt)
67+
}
68+
69+
const totalMs = samples.reduce((sum, value) => sum + value, 0)
70+
const sorted = [...samples].sort((left, right) => left - right)
71+
72+
return {
73+
meanMs: totalMs / samples.length,
74+
medianMs: sorted[Math.floor(sorted.length / 2)] ?? 0,
75+
minMs: sorted[0] ?? 0,
76+
maxMs: sorted[sorted.length - 1] ?? 0,
77+
}
78+
}
79+
80+
const formatMs = (value: number): string => `${value.toFixed(3)} ms`
81+
82+
const runWorldgenBenchmark = (argv: readonly string[]): void => {
83+
const warmupRounds = parsePositiveIntegerFlag(argv, 'warmup', DEFAULT_WARMUP_ROUNDS)
84+
const measuredRounds = parsePositiveIntegerFlag(argv, 'rounds', DEFAULT_MEASURED_ROUNDS)
85+
const seed = parsePositiveIntegerFlag(argv, 'seed', DEFAULT_SEED)
86+
const fixtureCount = parsePositiveIntegerFlag(argv, 'fixtures', DEFAULT_FIXTURE_COORDS.length)
87+
const fixtureCoords = DEFAULT_FIXTURE_COORDS.slice(0, fixtureCount)
88+
if (fixtureCoords.length === 0) {
89+
throw new Error('Worldgen benchmark requires at least one fixture chunk.')
90+
}
91+
92+
let lastHeightChecksum = 0
93+
const runHeightSamplingRound = (): void => {
94+
let checksum = 0
95+
for (const coord of fixtureCoords) {
96+
const originX = coord.x * CHUNK_SIZE
97+
const originZ = coord.z * CHUNK_SIZE
98+
for (let localZ = 0; localZ < CHUNK_SIZE; localZ += 1) {
99+
for (let localX = 0; localX < CHUNK_SIZE; localX += 1) {
100+
checksum += getTerrainHeight(seed, originX + localX, originZ + localZ)
101+
}
102+
}
103+
}
104+
lastHeightChecksum = checksum
105+
}
106+
107+
let lastChunkChecksum = 0
108+
const runChunkGenerationRound = (): void => {
109+
let checksum = 0
110+
for (const coord of fixtureCoords) {
111+
const chunk = createGeneratedChunk(coord, seed)
112+
checksum += chunk.heightmap[0] ?? 0
113+
checksum += chunk.heightmap[chunk.heightmap.length - 1] ?? 0
114+
checksum += chunk.blocks[0] ?? 0
115+
checksum += chunk.blocks[chunk.blocks.length - 1] ?? 0
116+
}
117+
lastChunkChecksum = checksum
118+
}
119+
120+
runHeightSamplingRound()
121+
runChunkGenerationRound()
122+
123+
const heightSampling = measure(runHeightSamplingRound, warmupRounds, measuredRounds)
124+
const chunkGeneration = measure(runChunkGenerationRound, warmupRounds, measuredRounds)
125+
const perChunkHeightSamplingMs = heightSampling.meanMs / fixtureCoords.length
126+
const perChunkGenerationMs = chunkGeneration.meanMs / fixtureCoords.length
127+
const nonHeightGenerationMs = Math.max(0, chunkGeneration.meanMs - heightSampling.meanMs)
128+
129+
console.log('Worldgen benchmark')
130+
console.log(
131+
`Fixture: ${fixtureCoords.length} generated chunks, seed ${seed}, ${warmupRounds} warmup round(s), ${measuredRounds} measured round(s)`,
132+
)
133+
console.log(
134+
`Height sampling mean: ${formatMs(heightSampling.meanMs)} (${formatMs(perChunkHeightSamplingMs)} per chunk)`,
135+
)
136+
console.log(
137+
`Height sampling range: ${formatMs(heightSampling.minMs)} min / ${formatMs(heightSampling.medianMs)} median / ${formatMs(heightSampling.maxMs)} max`,
138+
)
139+
console.log(
140+
`Full chunk generation mean: ${formatMs(chunkGeneration.meanMs)} (${formatMs(perChunkGenerationMs)} per chunk)`,
141+
)
142+
console.log(
143+
`Full chunk generation range: ${formatMs(chunkGeneration.minMs)} min / ${formatMs(chunkGeneration.medianMs)} median / ${formatMs(chunkGeneration.maxMs)} max`,
144+
)
145+
console.log(`Estimated non-height work: ${formatMs(nonHeightGenerationMs)} per round`)
146+
console.log(`Height checksum: ${lastHeightChecksum}`)
147+
console.log(`Chunk checksum: ${lastChunkChecksum}`)
148+
}
149+
150+
export const worldgenBenchmark: CliBenchmark = {
151+
name: 'worldgen',
152+
description:
153+
'Measure deterministic terrain-height sampling and full chunk generation throughput on a fixed chunk fixture set.',
154+
run: runWorldgenBenchmark,
155+
}

0 commit comments

Comments
 (0)