Add continuous auto-save to IndexedDB for physics recordings
All checks were successful
Build / build (push) Successful in 1m26s

Modified physics recorder to automatically save all captured data to IndexedDB:
- Auto-saves every 10 seconds in batches (non-blocking)
- Creates unique session ID for each gameplay session
- Buffers snapshots between saves to minimize IndexedDB writes
- Saves any remaining buffered data on disposal
- All data preserved indefinitely in browser storage

Technical implementation:
- Auto-save buffer collects snapshots between 10-second intervals
- performAutoSave() copies buffer and clears immediately (non-blocking)
- Async save happens in background without impacting frame rate
- Session ID format: "session-{timestamp}" for easy identification
- Ring buffer (30s) still available for quick exports

Recording flow:
1. Recording starts when XR pose is set
2. Every frame adds snapshot to ring buffer AND auto-save buffer
3. Every 10 seconds, auto-save buffer is flushed to IndexedDB
4. On disposal, any remaining buffered frames are saved
5. All data accessible via existing storage APIs

Performance: Minimal impact - saves happen async every 10s, not per frame.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-11-08 05:39:09 -06:00
parent e473e3d03e
commit 88d380fa3f
6 changed files with 83 additions and 2 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -74,6 +74,13 @@ export class PhysicsRecorder {
// IndexedDB storage // IndexedDB storage
private _storage: PhysicsStorage | null = null; private _storage: PhysicsStorage | null = null;
// Auto-save to IndexedDB
private _autoSaveEnabled: boolean = true;
private _autoSaveBuffer: PhysicsSnapshot[] = [];
private _autoSaveInterval: number = 10000; // Save every 10 seconds
private _lastAutoSaveTime: number = 0;
private _currentSessionId: string = "";
constructor(scene: Scene) { constructor(scene: Scene) {
this._scene = scene; this._scene = scene;
@ -86,6 +93,7 @@ export class PhysicsRecorder {
/** /**
* Start the ring buffer recorder (always capturing last 30 seconds) * Start the ring buffer recorder (always capturing last 30 seconds)
* Also starts auto-save to IndexedDB
*/ */
public startRingBuffer(): void { public startRingBuffer(): void {
if (this._isEnabled) { if (this._isEnabled) {
@ -95,16 +103,22 @@ export class PhysicsRecorder {
this._isEnabled = true; this._isEnabled = true;
this._startTime = performance.now(); this._startTime = performance.now();
this._lastAutoSaveTime = performance.now();
this._frameNumber = 0; this._frameNumber = 0;
// Create unique session ID for this recording
this._currentSessionId = `session-${Date.now()}`;
// Hook into physics update observable // Hook into physics update observable
this._scene.onAfterPhysicsObservable.add(() => { this._scene.onAfterPhysicsObservable.add(() => {
if (this._isEnabled) { if (this._isEnabled) {
this.captureFrame(); this.captureFrame();
this.checkAutoSave();
} }
}); });
debugLog("PhysicsRecorder: Ring buffer started (30 second capacity)"); debugLog("PhysicsRecorder: Recording started (ring buffer + auto-save to IndexedDB)");
debugLog(`PhysicsRecorder: Session ID: ${this._currentSessionId}`);
} }
/** /**
@ -237,6 +251,11 @@ export class PhysicsRecorder {
this._longRecording.push(snapshot); this._longRecording.push(snapshot);
} }
// Add to auto-save buffer if enabled
if (this._autoSaveEnabled) {
this._autoSaveBuffer.push(snapshot);
}
this._frameNumber++; this._frameNumber++;
// Track performance // Track performance
@ -251,6 +270,61 @@ export class PhysicsRecorder {
} }
} }
/**
* Check if it's time to auto-save to IndexedDB
*/
private checkAutoSave(): void {
if (!this._autoSaveEnabled || !this._storage) {
return;
}
const now = performance.now();
const timeSinceLastSave = now - this._lastAutoSaveTime;
// Save every 10 seconds
if (timeSinceLastSave >= this._autoSaveInterval && this._autoSaveBuffer.length > 0) {
this.performAutoSave();
this._lastAutoSaveTime = now;
}
}
/**
* Save buffered snapshots to IndexedDB
*/
private async performAutoSave(): Promise<void> {
if (!this._storage || this._autoSaveBuffer.length === 0) {
return;
}
// Copy buffer and clear it immediately to avoid blocking next frame
const snapshotsToSave = [...this._autoSaveBuffer];
this._autoSaveBuffer = [];
// Create a recording from the buffered snapshots
const metadata: RecordingMetadata = {
startTime: snapshotsToSave[0].timestamp,
endTime: snapshotsToSave[snapshotsToSave.length - 1].timestamp,
frameCount: snapshotsToSave.length,
recordingDuration: snapshotsToSave[snapshotsToSave.length - 1].timestamp - snapshotsToSave[0].timestamp,
physicsUpdateRate: this._physicsUpdateRate
};
const recording: PhysicsRecording = {
metadata,
snapshots: snapshotsToSave
};
try {
// Save to IndexedDB with session ID as name
await this._storage.saveRecording(this._currentSessionId, recording);
const duration = (metadata.recordingDuration / 1000).toFixed(1);
debugLog(`PhysicsRecorder: Auto-saved ${snapshotsToSave.length} frames (${duration}s) to IndexedDB`);
} catch (error) {
debugLog("PhysicsRecorder: Error during auto-save", error);
}
}
/** /**
* Export last N seconds from ring buffer * Export last N seconds from ring buffer
*/ */
@ -512,11 +586,18 @@ export class PhysicsRecorder {
/** /**
* Dispose of recorder resources * Dispose of recorder resources
*/ */
public dispose(): void { public async dispose(): Promise<void> {
// Save any remaining buffered data before disposing
if (this._autoSaveBuffer.length > 0) {
debugLog(`PhysicsRecorder: Saving ${this._autoSaveBuffer.length} remaining frames before disposal`);
await this.performAutoSave();
}
this.stopRingBuffer(); this.stopRingBuffer();
this.stopLongRecording(); this.stopLongRecording();
this._ringBuffer = []; this._ringBuffer = [];
this._longRecording = []; this._longRecording = [];
this._autoSaveBuffer = [];
if (this._storage) { if (this._storage) {
this._storage.close(); this._storage.close();