Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions packages/viewer/src/systems/wall/wall-progressive-budget.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ describe('progressive wall budget', () => {
expect(shouldDeferWallRebuild(heavy.id, nodes, 0, 100)).toBe(false)
})

test('initial build drains under a 48 ms budget without the count cap; interactive tiers unchanged', () => {
expect(shouldDeferWallRebuild(cheap.id, nodes, 40, 30, true)).toBe(false)
expect(shouldDeferWallRebuild(cheap.id, nodes, 40, 48, true)).toBe(true)
expect(shouldDeferWallRebuild(cheap.id, nodes, 8, 0, true)).toBe(false)
expect(shouldDeferWallRebuild(heavy.id, nodes, 1, 0, true)).toBe(true)
expect(shouldDeferWallRebuild(cheap.id, nodes, 8, 0)).toBe(true)
expect(shouldDeferWallRebuild(cheap.id, nodes, 1, 8)).toBe(true)
})

test('counts item cutout proxies but skips ordinary items', () => {
const item = { id: 'item_budget-test', type: 'item' } as AnyNode
const wall = { ...heavy, children: [...heavy.children.slice(0, 5), item.id] }
Expand Down Expand Up @@ -366,7 +375,7 @@ test('reattaching mid-drain preserves the hydration counters and one continuous
try {
hydrate(12)
const token = useScene.getState().hydrationToken
rebuildCost = 4
rebuildCost = 24
runWallBuildFrame()
expect(stats().firstBuilds).toBe(2)
unsubscribe()
Expand All @@ -379,7 +388,7 @@ test('reattaching mid-drain preserves the hydration counters and one continuous
rebuildCost = 0
runWallBuildFrame()
expect(stats().firstBuilds).toBe(12)
expect(spans).toEqual([28])
expect(spans).toEqual([68])
} finally { stop() }
})

Expand Down Expand Up @@ -447,9 +456,9 @@ test('setScene starts initial build; more than eight cheap walls drain in one fr
expect(stats().drainedExits).toBe(1)
})

test('checks the eight millisecond budget between walls and skips first-build neighbour invalidation across frames', () => {
test('checks the initial-build time budget between walls and skips first-build neighbour invalidation across frames', () => {
hydrate(12)
rebuildCost = 4
rebuildCost = 24
runWallBuildFrame()
expect(stats().wallsConsumedThisFrame).toBe(2)
expect(stats().budgetExits).toBe(1)
Expand Down Expand Up @@ -695,7 +704,7 @@ test('canvas and live-state owner precede the lazy wall consumer; remount retain
const nodes = Object.fromEntries([level, ...walls].map(node => [node.id, node]))
const meshes = walls.map(wall => {
const mesh = new Mesh(new BoxGeometry())
mesh.geometry.addEventListener('dispose', () => { now += 4 })
mesh.geometry.addEventListener('dispose', () => { now += 24 })
sceneRegistry.nodes.set(wall.id, mesh)
sceneRegistry.byType.wall.add(wall.id)
return mesh
Expand Down
14 changes: 12 additions & 2 deletions packages/viewer/src/systems/wall/wall-system.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,12 @@ const DRAG_FLUSH_MS = 80
const MAX_WALL_REBUILDS_PER_FRAME = 8
const WALL_PROGRESSIVE_DIRTY_THRESHOLD = MAX_WALL_REBUILDS_PER_FRAME
const WALL_PROGRESSIVE_TIME_BUDGET_MS = 8
// Initial build (see wall-build-lifecycle) has no interactive gesture to protect:
// the frame is dominated by rendering the still-unbatched scene, so every extra
// frame spent draining walls costs a full scene render. Three display frames of
// wall work per frame drains a 1,600-wall scene in ~1/6 of the frames while every
// frame stays two orders of magnitude under a perceptible freeze.
const WALL_INITIAL_BUILD_TIME_BUDGET_MS = 48
const HEAVY_WALL_OPENINGS = 6
let lastWallDirtyAtMs = 0
let unmountedFrames = 0
Expand All @@ -628,7 +634,10 @@ function wallRebuildExitReason(
): 'cap' | 'budget' | 'heavy' | null {
if (!initialBuild && rebuiltThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) return 'cap'
if (rebuiltThisFrame === 0) return null
if (elapsedMs >= WALL_PROGRESSIVE_TIME_BUDGET_MS) return 'budget'
const budgetMs = initialBuild
? WALL_INITIAL_BUILD_TIME_BUDGET_MS
: WALL_PROGRESSIVE_TIME_BUDGET_MS
if (elapsedMs >= budgetMs) return 'budget'
const wall = nodes[wallId as AnyNodeId]
if (wall?.type !== 'wall') return null
let cutouts = 0
Expand All @@ -654,8 +663,9 @@ export function shouldDeferWallRebuild(
nodes: Record<AnyNodeId, AnyNode>,
rebuiltThisFrame: number,
elapsedMs: number,
initialBuild = false,
): boolean {
return wallRebuildExitReason(wallId, nodes, rebuiltThisFrame, elapsedMs) !== null
return wallRebuildExitReason(wallId, nodes, rebuiltThisFrame, elapsedMs, initialBuild) !== null
}

/** Rebuilds this system still owes — neighbours deferred during a drag. */
Expand Down
5 changes: 3 additions & 2 deletions wiki/architecture/systems.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ Unavailable walls do not continually postpone the pending-neighbour quiet clock.
`isWallInitialBuildActive()` and `getPendingWallRebuildCount()` remain readable
without `?perf`.

Initial build consumes walls under the existing **8 ms budget**, checked between
walls, without the interactive **8 walls/frame** cap. A wall with at least six
Initial build consumes walls under a **48 ms budget** (about three display frames; the
interactive tier keeps **8 ms**), checked between walls, without the interactive
**8 walls/frame** cap. A wall with at least six
opening cutouts occupies its own frame. Each wall's first build during active
initial build skips adjacency scanning and neighbour re-invalidation because the
hydrated inputs are stable and its neighbours are queued for their own first
Expand Down