Skip to content

Fix: Robot Loading Unit Test Fail [AARD-2003] #1222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: dev
Choose a base branch
from
Draft
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
269 changes: 210 additions & 59 deletions fission/src/mirabuf/MirabufLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ export function unzipMira(buff: Uint8Array): Uint8Array {
}

class MirabufCachingService {
// Request deduplication: track ongoing fetch requests to prevent race conditions
private static _ongoingRequests = new Map<string, Promise<MirabufCacheInfo | undefined>>()

// Storage operation locks to prevent concurrent storage race conditions
private static _storageOperations = new Map<string, Promise<MirabufCacheInfo | undefined>>()
private static _idCounter = 0

/**
* Get the map of mirabuf keys and paired MirabufCacheInfo from local storage
*
Expand Down Expand Up @@ -124,6 +131,27 @@ class MirabufCachingService {
const target = map[fetchLocation]
if (target) return target
}

// Check if there's already an ongoing request for this URL
const requestKey = `${fetchLocation}:${miraType ?? "unknown"}`
const ongoingRequest = this._ongoingRequests.get(requestKey)
if (ongoingRequest) {
return ongoingRequest
}

const requestPromise = this._fetchAndCache(fetchLocation, miraType)
this._ongoingRequests.set(requestKey, requestPromise)
requestPromise.finally(() => {
this._ongoingRequests.delete(requestKey)
})

return requestPromise
}

private static async _fetchAndCache(
fetchLocation: string,
miraType?: MiraType
): Promise<MirabufCacheInfo | undefined> {
try {
// grab file remote
const resp = await fetch(encodeURI(fetchLocation), import.meta.env.DEV ? { cache: "no-store" } : undefined)
Expand All @@ -138,17 +166,55 @@ class MirabufCachingService {

const cached = await MirabufCachingService.storeInCache(fetchLocation, miraBuff, miraType)

if (cached) return cached
if (cached) {
// Verify that the cached data is actually retrievable
const verification = await MirabufCachingService.get(cached.id, cached.miraType)
if (verification) {
return cached
} else {
console.warn(
`Storage verification failed for "${fetchLocation}" - data not retrievable despite successful cache operation`
)
}
}

console.warn(`Primary caching failed for "${fetchLocation}", creating emergency fallback`)
globalAddToast("error", "Cache Fallback", `Unable to cache "${fetchLocation}". Using raw buffer instead.`)

globalAddToast("error", "Cache Fallback", `Unable to cache “${fetchLocation}”. Using raw buffer instead.`)
// Emergency fallback: return raw buffer wrapped in MirabufCacheInfo
const fallbackId = `${Date.now()}_${++this._idCounter}_fallback`
const resolvedMiraType =
miraType ?? (this.assemblyFromBuffer(miraBuff).dynamic ? MiraType.ROBOT : MiraType.FIELD)

// fallback: return raw buffer wrapped in MirabufCacheInfo
return {
id: Date.now().toString(),
miraType: miraType ?? (this.assemblyFromBuffer(miraBuff).dynamic ? MiraType.ROBOT : MiraType.FIELD),
const fallbackInfo: MirabufCacheInfo = {
id: fallbackId,
miraType: resolvedMiraType,
cacheKey: fetchLocation,
buffer: miraBuff,
}

// Store fallback in memory cache so get() can find it later
const cache = resolvedMiraType == MiraType.ROBOT ? backUpRobots : backUpFields
cache[fallbackId] = fallbackInfo

// Also try to update localStorage for future reference
try {
const map: MapCache = this.getCacheMap(resolvedMiraType)
map[fetchLocation] = {
id: fallbackId,
miraType: resolvedMiraType,
cacheKey: fetchLocation,
name: `Fallback: ${fetchLocation}`,
}
window.localStorage.setItem(
resolvedMiraType == MiraType.ROBOT ? robotsDirName : fieldsDirName,
JSON.stringify(map)
)
} catch (lsError) {
console.warn(`Fallback localStorage update failed:`, lsError)
}

return fallbackInfo
} catch (e) {
console.warn("Caching failed", e)
return undefined
Expand Down Expand Up @@ -326,37 +392,84 @@ class MirabufCachingService {
* @returns {Promise<mirabufAssembly | undefined>} Promise with the result of the promise. Assembly of the mirabuf file if successful, undefined if not.
*/
public static async get(id: MirabufCacheID, miraType: MiraType): Promise<mirabuf.Assembly | undefined> {
try {
// Get buffer from hashMap. If not in hashMap, check OPFS. Otherwise, buff is undefined
const cache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields
const buff =
cache[id]?.buffer ??
(await (async () => {
const fileHandle = canOPFS
? await (miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle).getFileHandle(id, {
create: false,
})
: undefined
return fileHandle ? await fileHandle.getFile().then(x => x.arrayBuffer()) : undefined
})())

// If we have buffer, get assembly
if (buff) {
const assembly = this.assemblyFromBuffer(buff)
World.analyticsSystem?.event("Cache Get", {
key: id,
type: miraType == MiraType.ROBOT ? "robot" : "field",
assemblyName: assembly.info!.name!,
fileSize: buff.byteLength,
})
return assembly
} else {
console.error(`Failed to find arrayBuffer for id: ${id}`)
// Retry logic to handle race conditions where storage might still be in progress
const maxRetries = 3
const retryDelay = 50 // ms

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const cache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields
let buff: ArrayBuffer | undefined

// Try memory cache first (most reliable and fastest)
if (cache[id]?.buffer) {
buff = cache[id].buffer
} else if (canOPFS) {
// Try OPFS as fallback
try {
const fileHandle = await (
miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle
).getFileHandle(id, { create: false })
const file = await fileHandle.getFile()
buff = await file.arrayBuffer()

// If we found it in OPFS but not in memory, update memory cache for next time
if (buff && !cache[id]?.buffer) {
if (cache[id]) {
cache[id].buffer = buff
} else {
// Create a minimal cache entry if it doesn't exist
cache[id] = {
id: id,
miraType: miraType,
cacheKey: `opfs_recovered_${id}`,
buffer: buff,
}
}
}
} catch (opfsError) {
console.debug(`OPFS retrieval failed for ${id}:`, opfsError)
}
}

// If we have buffer, return assembly
if (buff) {
const assembly = this.assemblyFromBuffer(buff)
World.analyticsSystem?.event("Cache Get", {
key: id,
type: miraType == MiraType.ROBOT ? "robot" : "field",
assemblyName: assembly.info!.name!,
fileSize: buff.byteLength,
})
return assembly
}

// If we didn't find the buffer and this isn't the last attempt, wait and retry
if (attempt < maxRetries - 1) {
console.debug(
`Cache miss for ${id}, retrying in ${retryDelay}ms (attempt ${attempt + 1}/${maxRetries})`
)
await new Promise(resolve => setTimeout(resolve, retryDelay))
continue
}

// Last attempt failed
console.error(`Failed to find arrayBuffer for id: ${id} after ${maxRetries} attempts`)
return undefined
} catch (e) {
console.error(`Failed to find file for id ${id} (attempt ${attempt + 1}):`, e)

// If this is the last attempt, give up
if (attempt === maxRetries - 1) {
return undefined
}

// Wait before retrying
await new Promise(resolve => setTimeout(resolve, retryDelay))
}
} catch (e) {
console.error(`Failed to find file\n${e}`)
return undefined
}

return undefined
}

/**
Expand Down Expand Up @@ -476,24 +589,58 @@ class MirabufCachingService {
miraBuff: ArrayBuffer,
miraType?: MiraType,
name?: string
): Promise<MirabufCacheInfo | undefined> {
// Check if there's already an ongoing storage operation for this key
const storageKey = `${key}:${miraType ?? "unknown"}`
const ongoingStorage = this._storageOperations.get(storageKey)
if (ongoingStorage) {
return ongoingStorage
}

const storagePromise = this._performStoreInCache(key, miraBuff, miraType, name)
this._storageOperations.set(storageKey, storagePromise)

storagePromise.finally(() => {
this._storageOperations.delete(storageKey)
})

return storagePromise
}

private static async _performStoreInCache(
key: string,
miraBuff: ArrayBuffer,
miraType?: MiraType,
name?: string
): Promise<MirabufCacheInfo | undefined> {
try {
const backupID = Date.now().toString()
// Generate unique ID using timestamp + counter to avoid collisions
const backupID = `${Date.now()}_${++this._idCounter}`

if (!miraType) {
console.debug("Double loading")
miraType = this.assemblyFromBuffer(miraBuff).dynamic ? MiraType.ROBOT : MiraType.FIELD
}

// Local cache map
const map: MapCache = this.getCacheMap(miraType)
const info: MirabufCacheInfo = {
// Create the cache info that will be used consistently across all storage locations
const cacheInfo: MirabufCacheInfo = {
id: backupID,
miraType: miraType,
cacheKey: key,
name: name,
}
map[key] = info
window.localStorage.setItem(miraType == MiraType.ROBOT ? robotsDirName : fieldsDirName, JSON.stringify(map))

const memoryInfo: MirabufCacheInfo = {
id: backupID,
miraType: miraType,
cacheKey: key,
buffer: miraBuff,
name: name,
}

// Store in memory cache FIRST (most reliable)
const cache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields
cache[backupID] = memoryInfo

World.analyticsSystem?.event("Cache Store", {
name: name ?? "-",
Expand All @@ -502,29 +649,33 @@ class MirabufCachingService {
fileSize: miraBuff.byteLength,
})

// Store buffer
// Store in OPFS (can fail silently)
if (canOPFS) {
// Store in OPFS
const fileHandle = await (
miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle
).getFileHandle(backupID, { create: true })
const writable = await fileHandle.createWritable()
await writable.write(miraBuff)
await writable.close()
try {
const fileHandle = await (
miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle
).getFileHandle(backupID, { create: true })
const writable = await fileHandle.createWritable()
await writable.write(miraBuff)
await writable.close()
} catch (opfsError) {
console.warn(`OPFS storage failed for ${key}, using fallback:`, opfsError)
}
}

// Store in hash
const cache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields
const mapInfo: MirabufCacheInfo = {
id: backupID,
miraType: miraType,
cacheKey: key,
buffer: miraBuff,
name: name,
// Update localStorage cache map LAST (after memory cache is secured)
try {
const map: MapCache = this.getCacheMap(miraType)
map[key] = cacheInfo
window.localStorage.setItem(
miraType == MiraType.ROBOT ? robotsDirName : fieldsDirName,
JSON.stringify(map)
)
} catch (lsError) {
console.warn(`localStorage update failed for ${key}:`, lsError)
}
cache[backupID] = mapInfo

return info
return cacheInfo
} catch (e) {
console.error("Failed to cache mira " + e)
World.analyticsSystem?.exception("Failed to store in cache")
Expand Down
42 changes: 35 additions & 7 deletions fission/src/test/physics/PhysicsSystemRobotSpawning.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,23 @@ import PhysicsSystem, { LayerReserve } from "@/systems/physics/PhysicsSystem"

describe("Mirabuf Physics Loading", () => {
test("Body Loading (Dozer)", async () => {
const assembly = await MirabufCachingService.cacheRemote("/api/mira/robots/Dozer_v9.mira", MiraType.ROBOT).then(
x => MirabufCachingService.get(x!.id, MiraType.ROBOT)
)
const parser = new MirabufParser(assembly!)
const cacheInfo = await MirabufCachingService.cacheRemote("/api/mira/robots/Dozer_v9.mira", MiraType.ROBOT)

if (!cacheInfo) {
console.warn("Warning: Remote mirabuf file not accessible - cacheRemote returned undefined")
console.warn("This may indicate network issues or API unavailability in the test environment")
throw new Error("Failed to cache remote mirabuf file")
}

const assembly = await MirabufCachingService.get(cacheInfo.id, MiraType.ROBOT)

if (!assembly) {
console.warn(`Warning: Failed to load mirabuf assembly from cache with ID: ${cacheInfo.id}`)
console.warn("This may indicate storage issues or fallback mechanism problems")
throw new Error("Failed to load mirabuf assembly from cache")
}

const parser = new MirabufParser(assembly)
const physSystem = new PhysicsSystem()
const mapping = physSystem.createBodiesFromParser(parser, new LayerReserve())

Expand All @@ -23,11 +36,26 @@ describe("Mirabuf Physics Loading", () => {
* Mira File: https://synthesis.autodesk.com/api/mira/private/Multi-Joint_Wheels_v0.mira
*/
test("Body Loading (Multi-Joint Wheels)", async () => {
const assembly = await MirabufCachingService.cacheRemote(
const cacheInfo = await MirabufCachingService.cacheRemote(
"/api/mira/private/Multi-Joint_Wheels_v0.mira",
MiraType.ROBOT
).then(x => MirabufCachingService.get(x!.id, MiraType.ROBOT))
const parser = new MirabufParser(assembly!)
)

if (!cacheInfo) {
console.warn("Warning: Remote mirabuf file not accessible - cacheRemote returned undefined")
console.warn("This may indicate network issues or API unavailability in the test environment")
throw new Error("Failed to cache remote mirabuf file")
}

const assembly = await MirabufCachingService.get(cacheInfo.id, MiraType.ROBOT)

if (!assembly) {
console.warn(`Warning: Failed to load mirabuf assembly from cache with ID: ${cacheInfo.id}`)
console.warn("This may indicate storage issues or fallback mechanism problems")
throw new Error("Failed to load mirabuf assembly from cache")
}

const parser = new MirabufParser(assembly)
const physSystem = new PhysicsSystem()
const mapping = physSystem.createBodiesFromParser(parser, new LayerReserve())

Expand Down
Loading