| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- import assert from 'node:assert/strict'
- import { createRegionLoadingController } from '../../src/modules/cockpit/services/regionLoading.js'
- function event() {
- const listeners = new Set()
- return { addEventListener(fn) { listeners.add(fn); return () => listeners.delete(fn) }, raise(value) { for (const fn of listeners) fn(value) } }
- }
- const flush = async () => { await Promise.resolve(); await Promise.resolve() }
- let clock = 0, front = 0
- const loads = [], removed = [], models = new Map()
- const controller = createRegionLoadingController({
- regions: [0, 1].map(id => ({ id, contentCount: 1, decodedGeometryBytes: 1024 })),
- budget: { maxContents: 2, maxDecodedBytes: 2048 },
- distance: r => r.id === front ? 10 : Infinity, readCameraKey: () => String(front), now: () => clock,
- load: async r => { loads.push(r.id); const m = { id: r.id, show: false, tilesLoaded: true, covered: true, tileVisible: event() }; models.set(r.id, m); return m },
- add() {}, remove: m => removed.push(m.id), coverageReady: e => e.instance.covered,
- canStart: () => true, requestRender() {}, setTimer: () => 1, clearTimer() {},
- })
- try {
- controller.refresh(); await flush(); controller.onRender()
- const a = models.get(0)
- a.tileVisible.raise({ content: { trianglesLength: 1 } }); controller.onRender()
- assert.equal(controller.detailReady(0), true, 'Initial A coverage was rendered')
- clock = 200; front = 1; controller.refresh(); await flush(); controller.onRender()
- clock = 216; controller.onRender() // next frame has an unused show slot
- clock = 400; front = 0; controller.refresh(); await flush()
- assert.deepEqual(loads, [0, 1], 'A -> B -> A within the existing budget must reuse ready A, not reload it')
- assert.equal(models.get(0), a, 'Reuse the existing model instance')
- assert.equal(a.show, false, 'Cached readiness must not show before a fresh renderer pass')
- assert.equal(controller.snapshot().find(e => e.id === 0).awaitingRender, true)
- a.tilesLoaded = a.covered = false // renderer discovers an uncovered part of the new view
- controller.onRender()
- assert.equal(a.show, false, 'Fresh view readiness overrides the cached ready flag')
- assert.equal(controller.detailReady(0), false, 'Reused model must render again before coarse yields')
- a.tilesLoaded = a.covered = true; clock = 416; controller.onRender()
- assert.equal(controller.detailReady(0), false)
- a.tileVisible.raise({ content: { trianglesLength: 1 } }); clock = 432; controller.onRender()
- assert.equal(controller.detailReady(0), true)
- assert.deepEqual(removed, [], 'No eviction needed while both models fit the original budget')
- console.log('PASS bounded residency: ready A -> B -> A reuses A and re-proves visible coverage')
- } finally { controller.detach() }
- async function scene(options, run) {
- const costs = options.costs || [1, 1, 1]
- const budget = options.budget || { maxContents: 2, maxDecodedBytes: 2048 }
- let time = 0, pose = 0, visible = [0], parentReady = true, startAllowed = true, renderRequested = false
- const loaded = [], added = [], disposed = [], byId = new Map(), waiting = new Map()
- const control = createRegionLoadingController({
- regions: costs.map((cost, id) => ({ id, contentCount: cost, decodedGeometryBytes: cost * 1024 })),
- budget, distance: r => visible.includes(r.id) ? r.id + 1 : Infinity,
- readCameraKey: () => String(pose), now: () => time, canStart: () => parentReady && startAllowed,
- canRetain: () => parentReady,
- coverageReady: e => e.instance.covered,
- load: r => {
- loaded.push(r.id)
- const m = { id: r.id, show: false, covered: options.ready !== false, tilesLoaded: options.ready !== false,
- tileVisible: event(), tileFailed: event() }
- byId.set(r.id, m)
- return (options.deferred || options.deferredIds?.includes(r.id)) ? new Promise((resolve, reject) => waiting.set(r.id, { resolve: () => resolve(m), reject })) : Promise.resolve(m)
- },
- add: m => added.push(m.id), remove: m => disposed.push(m.id),
- requestRender: () => { renderRequested = true }, setTimer: () => 1, clearTimer() {},
- })
- const h = {
- control, loaded, added, disposed, byId,
- async view(ids, elapsed = 200, moving = true) {
- time += elapsed; visible = ids; if (moving) pose++
- control.refresh(); await flush(); control.onRender(); await flush()
- },
- async resolve(id) { waiting.get(id).resolve(); waiting.delete(id); await flush() },
- async reject(id) { waiting.get(id).reject(new Error('load rejected')); waiting.delete(id); await flush() },
- parent(value) { parentReady = value; control.refresh() },
- allowStarts(value) { startAllowed = value; control.refresh() },
- renderPending: () => renderRequested,
- clearRenderRequest() { renderRequested = false },
- checkBudget() {
- const rows = control.snapshot()
- assert.ok(rows.reduce((n, e) => n + e.contents, 0) <= (budget.maxContents ?? 1200))
- assert.ok(rows.reduce((n, e) => n + e.decodedGeometryEstimate, 0) <= (budget.maxDecodedBytes ?? 256 * 1024 * 1024))
- if (budget.maxRegions !== undefined) assert.ok(rows.length <= Math.ceil(budget.maxRegions))
- },
- }
- control.refresh(); await flush(); control.onRender(); await flush()
- try { await run(h) } finally {
- control.detach()
- for (const wait of waiting.values()) wait.resolve()
- await flush()
- }
- }
- await scene({}, async h => {
- await h.view([1])
- const a = h.byId.get(0)
- assert.equal(a.show, false)
- assert.equal(a.preloadWhenHidden, false, 'Parked ready model does not preload or render')
- assert.equal(h.control.detailReady(0), false, 'Cached region cannot mask coarse')
- assert.equal(h.control.snapshot().find(e => e.id === 0).cached, true)
- assert.equal(h.control.frontierSnapshot().cachedRegions, 1)
- h.clearRenderRequest()
- await h.view([0])
- assert.equal(h.renderPending(), true, 'Reactivation wakes requestRenderMode')
- assert.equal(a.preloadWhenHidden, true)
- assert.deepEqual(h.loaded, [0, 1])
- h.checkBudget()
- })
- await scene({}, async h => {
- await h.view([1]); await h.view([0]); await h.view([2])
- assert.deepEqual(h.disposed, [1], 'Least recently wanted hidden cache yields first')
- assert.equal(h.control.snapshot().find(e => e.id === 0).cached, true)
- await h.view([0])
- assert.deepEqual(h.loaded, [0, 1, 2], 'Most recently reused cache survives pressure')
- h.checkBudget()
- })
- await scene({}, async h => {
- await h.view([1]); await h.view([0]); await h.view([0, 2])
- assert.deepEqual(h.disposed, [1], 'Cache yields instead of evicting the active near target')
- assert.equal(h.byId.get(0).show, true)
- h.checkBudget()
- })
- for (const budget of [{ maxContents: 1 }, { maxDecodedBytes: 1024 }, { maxRegions: 1 }]) {
- await scene({ budget }, async h => {
- await h.view([1])
- assert.deepEqual(h.disposed, [0], 'Content, bytes and optional count limits include cached models')
- assert.deepEqual(h.control.snapshot().map(e => e.id), [1])
- h.checkBudget()
- })
- }
- await scene({ deferred: true, budget: { maxContents: 1, maxDecodedBytes: 1024 } }, async h => {
- await h.view([1], 1000, false)
- assert.deepEqual(h.loaded, [0], 'Withdrawn unresolved initialization still reserves its budget')
- h.checkBudget()
- await h.resolve(0)
- assert.deepEqual(h.added, [], 'Late withdrawn initialization never attaches')
- await h.view([1], 1000, false)
- assert.deepEqual(h.loaded, [0, 1], 'Released initialization reservation permits the new target')
- h.checkBudget()
- })
- await scene({ ready: false }, async h => {
- await h.view([1])
- assert.deepEqual(h.disposed, [0], 'Incomplete initial stream is not retained')
- assert.equal(h.control.frontierSnapshot().cachedRegions, 0)
- })
- await scene({ costs: [0, 0, 0] }, async h => {
- await h.view([1])
- assert.deepEqual(h.disposed, [0], 'Unknown zero-cost models cannot accumulate unbounded caches')
- })
- await scene({}, async h => {
- await h.view([1])
- h.byId.get(0).tileFailed.raise({ message: 'late cached failure' })
- h.control.refresh()
- assert.deepEqual(h.disposed, [0], 'Failure removes a cached instance')
- assert.equal(h.control.frontierSnapshot().cachedRegions, 0)
- })
- await scene({}, async h => {
- await h.view([1]); h.parent(false)
- assert.deepEqual(h.disposed, [0], 'Unavailable parent releases hidden caches')
- assert.equal(h.control.frontierSnapshot().cachedRegions, 0)
- })
- await scene({}, async h => {
- await h.view([1]); await h.view([], 1000, false)
- assert.equal(h.control.snapshot().length, 0, 'Settled scene without demand releases residency')
- })
- await scene({}, async h => {
- await h.view([1]); h.control.setPipeline(true)
- assert.equal(h.control.snapshot().length, 0)
- assert.deepEqual(h.disposed.slice().sort(), [0, 1], 'Pipeline releases active and cached models')
- h.control.detach()
- assert.equal(h.disposed.length, 2, 'Dispose does not release a model twice')
- })
- await scene({ costs: [1, 1, 3], budget: { maxContents: 3, maxDecodedBytes: 3072 }, deferredIds: [1] }, async h => {
- await h.view([1]) // ready A is cached; B is still initializing.
- await h.view([2], 1000, false)
- assert.deepEqual(h.loaded, [0, 1], 'Outstanding B prevents a full-budget C from starting')
- assert.deepEqual(h.disposed.slice(), [], 'Do not evict A when even clearing caches cannot yet admit C')
- await h.resolve(1)
- await h.view([2], 1000, false)
- assert.deepEqual(h.loaded, [0, 1, 2], 'C starts after the obsolete reservation resolves')
- assert.deepEqual(h.disposed.slice().sort(), [0, 1])
- h.checkBudget()
- })
- await scene({ deferred: true }, async h => {
- await h.view([0, 1, 2], 1000, false)
- assert.deepEqual(h.loaded, [0, 1], 'Two unresolved initializations fill the shared budget')
- h.checkBudget()
- await h.view([2], 1000, false)
- assert.deepEqual(h.loaded, [0, 1])
- await h.resolve(0)
- await h.view([2], 1000, false)
- assert.deepEqual(h.loaded, [0, 1, 2], 'Only released reservation is replaced')
- h.checkBudget()
- })
- await scene({ deferredIds: [1] }, async h => {
- await h.view([1]); await h.reject(1); await h.view([2])
- assert.deepEqual(h.loaded, [0, 1, 2], 'Rejected load releases capacity for the next target')
- assert.equal(h.control.snapshot().find(e => e.id === 0).cached, true, 'Unrelated valid cache survives rejection')
- h.checkBudget()
- })
- await scene({ deferredIds: [1] }, async h => {
- await h.view([1]); h.control.detach(); await h.resolve(1)
- assert.deepEqual(h.added, [0], 'Disposed controller never attaches a late model')
- assert.deepEqual(h.disposed.slice().sort(), [0, 1], 'Cache and late model are each released once')
- assert.equal(h.control.snapshot().length, 0)
- })
- await scene({}, async h => {
- await h.view([1])
- const a = h.byId.get(0)
- assert.equal(h.control.snapshot().find(e => e.id === 0).cached, true)
- h.allowStarts(false) // parent is alive/visible, but temporarily not ready for new loads.
- assert.deepEqual(h.disposed.slice(), [], 'A closed start gate must not invalidate a ready cache')
- await h.view([0])
- assert.deepEqual(h.loaded, [0, 1], 'Valid cached A can return while new starts are paused')
- assert.equal(h.byId.get(0), a)
- await h.view([2])
- assert.deepEqual(h.loaded, [0, 1], 'The independent start gate still blocks a new region')
- h.parent(false)
- assert.equal(h.control.snapshot().length, 0, 'A genuinely invalid parent releases caches')
- })
- console.log('PASS residency boundaries: hidden state, wakeup, LRU, budgets, pending reservation, failure and release')
|