Add memory management and load-only options for JS/WASM

- Add reset() and getMemoryStats() to TinyUSDZLoaderNative (binding.cc)
  - reset() clears render scene, layers, assets, and all cached data
  - getMemoryStats() returns memory usage statistics for debugging

- Update clearScene() in materialx.js to properly free memory on reload
  - Dispose Three.js geometries, materials, and textures
  - Call nativeLoader.reset() to free WASM memory
  - Reset GUI state for normal display mode

- Add command line argument support to load-test-node.js

- Add new options to test-stream-load.js:
  - --load-only: Load USD as Layer only (no scene conversion)
  - --compare-modes: Compare load-only vs full load performance
  - Fix --compare to clear memory between tests with reset() and GC
  - Add memory usage reporting to all examples

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Syoyo Fujita
2025-12-15 05:20:53 +09:00
parent a8030af492
commit 6f3fcdf835
4 changed files with 376 additions and 8 deletions

View File

@@ -2864,6 +2864,69 @@ class TinyUSDZLoaderNative {
em_resolver_.clear();
}
/// Reset all state - clears render scene, assets, and all cached data
/// Call this before loading a new USD file to free memory
void reset() {
// Clear loaded flag
loaded_ = false;
loaded_as_layer_ = false;
composited_ = false;
// Clear strings
filename_.clear();
warn_.clear();
error_.clear();
// Clear render scene (meshes, materials, textures, buffers, etc.)
render_scene_ = tinyusdz::tydra::RenderScene();
// Clear layers
layer_ = tinyusdz::Layer();
composed_layer_ = tinyusdz::Layer();
// Clear USDZ asset
usdz_asset_ = tinyusdz::USDZAsset();
// Clear asset resolver cache
em_resolver_.clear();
// Clear reordered mesh cache
reordered_mesh_cache_.clear();
// Reset parsing progress
parsing_progress_.reset();
}
/// Get memory usage statistics
emscripten::val getMemoryStats() const {
emscripten::val stats = emscripten::val::object();
// Count meshes
stats.set("numMeshes", static_cast<int>(render_scene_.meshes.size()));
stats.set("numMaterials", static_cast<int>(render_scene_.materials.size()));
stats.set("numTextures", static_cast<int>(render_scene_.textures.size()));
stats.set("numImages", static_cast<int>(render_scene_.images.size()));
stats.set("numBuffers", static_cast<int>(render_scene_.buffers.size()));
stats.set("numNodes", static_cast<int>(render_scene_.nodes.size()));
stats.set("numLights", static_cast<int>(render_scene_.lights.size()));
// Estimate buffer memory
size_t bufferMemory = 0;
for (const auto &buf : render_scene_.buffers) {
bufferMemory += buf.data.size();
}
stats.set("bufferMemoryBytes", static_cast<double>(bufferMemory));
stats.set("bufferMemoryMB", static_cast<double>(bufferMemory) / (1024.0 * 1024.0));
// Asset cache count
stats.set("assetCacheCount", static_cast<int>(em_resolver_.cache.size()));
// Reordered mesh cache count
stats.set("reorderedMeshCacheCount", static_cast<int>(reordered_mesh_cache_.size()));
return stats;
}
void setAsset(const std::string &name, const std::string &binary) {
em_resolver_.add(name, binary);
}
@@ -3844,6 +3907,10 @@ EMSCRIPTEN_BINDINGS(tinyusdz_module) {
&TinyUSDZLoaderNative::assetExists)
.function("clearAssets",
&TinyUSDZLoaderNative::clearAssets)
.function("reset",
&TinyUSDZLoaderNative::reset)
.function("getMemoryStats",
&TinyUSDZLoaderNative::getMemoryStats)
.function("layerToString",
&TinyUSDZLoaderNative::layerToString)

View File

@@ -123,7 +123,24 @@ function checkMemory64Support() {
console.log("memory64:", checkMemory64Support());
const usd_filename = "../../models/suzanne-subd-lv4.usdc";
// Parse command line arguments
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Usage: node load-test-node.js <path-to-usd-file>');
console.log('');
console.log('Examples:');
console.log(' node load-test-node.js model.usdz');
console.log(' node load-test-node.js ../../models/suzanne-subd-lv4.usdc');
process.exit(1);
}
const usd_filename = args[0];
// Check if file exists
if (!fs.existsSync(usd_filename)) {
console.error(`Error: File not found: ${usd_filename}`);
process.exit(1);
}
async function initScene() {
@@ -134,11 +151,17 @@ async function initScene() {
// Option 1: Use traditional file loading (existing method)
console.log("\n=== Traditional file loading ===");
console.log(`File: ${usd_filename}`);
const f = loadFile(usd_filename);
if (!f) {
console.error('Failed to load file');
process.exit(1);
}
const url = URL.createObjectURL(f);
//const usd = await loader.loadTestAsync(url);
const usd = await loader.loadAsync(url);
//console.log("Traditional loading completed");
console.log("Load completed");
reportMemUsage();
/*
@@ -183,4 +206,7 @@ Object [WebAssembly] {
*/
initScene();
initScene().catch(err => {
console.error('Error:', err);
process.exit(1);
});

View File

@@ -602,24 +602,96 @@ async function reloadMaterials() {
}
function clearScene() {
// If showing normals, the mesh materials are MeshNormalMaterial instances
// and the original materials are stored in originalMaterialsMap
// Dispose the normal materials first before clearing sceneRoot
if (showingNormals && sceneRoot) {
sceneRoot.traverse(obj => {
if (obj.isMesh && obj.material) {
// This is a MeshNormalMaterial - dispose it
if (obj.material.dispose) {
obj.material.dispose();
}
}
});
}
originalMaterialsMap.clear();
showingNormals = false;
// Reset GUI checkbox state to match
if (settings.showNormals) {
settings.showNormals = false;
// Update GUI if it exists
if (gui) {
gui.controllersRecursive().forEach(controller => {
if (controller.property === 'showNormals') {
controller.updateDisplay();
}
});
}
}
// Clear Three.js scene objects
if (sceneRoot) {
sceneRoot.traverse(obj => {
if (obj.isMesh) {
obj.geometry?.dispose();
// Dispose material if not already disposed (when not in normal mode)
if (obj.material && obj.material.dispose) {
if (Array.isArray(obj.material)) {
obj.material.forEach(m => m.dispose());
} else {
obj.material.dispose();
}
}
}
});
scene.remove(sceneRoot);
sceneRoot = null;
}
currentMaterials.forEach(mat => mat.dispose());
// Clear material references (already disposed above)
currentMaterials = [];
materialData = [];
// Dispose textures in cache
textureCache.forEach((texture) => {
if (texture && texture.dispose) {
texture.dispose();
}
});
textureCache.clear();
// Reset upAxis to default
currentFileUpAxis = 'Y';
currentSceneMetadata = null;
// Clear WASM memory - reset the native loader to free render scene, assets, etc.
if (nativeLoader) {
// Log memory stats before clearing (for debugging)
try {
const stats = nativeLoader.getMemoryStats();
console.log('Memory before reset:', stats);
} catch (e) {
// getMemoryStats may not exist in older builds
}
// Reset WASM state to free memory
try {
nativeLoader.reset();
console.log('WASM memory reset complete');
} catch (e) {
// reset() may not exist in older builds, try clearAssets as fallback
try {
nativeLoader.clearAssets();
console.log('WASM assets cleared (fallback)');
} catch (e2) {
console.warn('Could not clear WASM memory:', e2);
}
}
// Set to null to allow GC - a new instance will be created on next load
nativeLoader = null;
}
}
function fitCameraToScene() {

View File

@@ -9,6 +9,16 @@
import { TinyUSDZLoader } from './src/tinyusdz/TinyUSDZLoader.js';
// ============================================================
// Memory Usage Reporting (Node.js only)
// ============================================================
function reportMemUsage() {
const used = process.memoryUsage();
for (let key in used) {
console.log(`${key}: ${Math.round(used[key] / 1024 / 1024 * 100) / 100} MB`);
}
}
// ============================================================
// Example 1: Stream fetch from URL (Browser/Node.js with fetch)
// ============================================================
@@ -43,6 +53,7 @@ async function exampleStreamFetch(url) {
if (loadOk) {
console.log('USD loaded successfully from cached asset!');
console.log(`Total time: ${(performance.now() - startTime).toFixed(2)} ms`);
reportMemUsage();
} else {
console.error('Failed to load USD:', usd.error());
}
@@ -89,6 +100,7 @@ async function exampleStreamFile(filePath) {
if (loadOk) {
console.log('USD loaded successfully from cached asset!');
console.log(`Total time: ${(performance.now() - startTime).toFixed(2)} ms`);
reportMemUsage();
} else {
console.error('Failed to load USD:', usd.error());
}
@@ -123,6 +135,7 @@ async function exampleLoadWithStreaming(url) {
console.log('\n');
console.log(`Total time: ${(performance.now() - startTime).toFixed(2)} ms`);
console.log('USD object loaded:', usd ? 'success' : 'failed');
reportMemUsage();
return usd;
} catch (error) {
@@ -164,6 +177,7 @@ async function exampleStreamMultiple(assets) {
}
console.log(`Total time: ${(performance.now() - startTime).toFixed(2)} ms`);
reportMemUsage();
return results;
} catch (error) {
@@ -173,7 +187,137 @@ async function exampleStreamMultiple(assets) {
}
// ============================================================
// Example 5: Compare streaming vs traditional loading
// Example 5: USD Load Only (no scene conversion for Three.js)
// ============================================================
// This mode loads USD as a Layer only, without converting to RenderScene.
// Useful for measuring pure USD parsing memory usage.
async function exampleLoadOnly(filePath) {
console.log('=== USD Load Only (No Scene Conversion) ===');
console.log(`File: ${filePath}`);
console.log('Mode: Layer-only parsing (no RenderScene conversion)');
const loader = new TinyUSDZLoader();
await loader.init({ useMemory64: true });
const fs = await import('fs');
const fileData = fs.readFileSync(filePath);
const fileSizeMB = (fileData.length / (1024 * 1024)).toFixed(2);
console.log(`File size: ${fileSizeMB} MB`);
const startTime = performance.now();
try {
const usd = new loader.native_.TinyUSDZLoaderNative();
// Load as Layer only - no RenderScene conversion
const loadOk = usd.loadAsLayerFromBinary(fileData, filePath);
const loadTime = performance.now() - startTime;
if (loadOk) {
console.log(`\nUSD Layer loaded successfully!`);
console.log(`Load time: ${loadTime.toFixed(2)} ms`);
// Try to get memory stats if available
try {
const stats = usd.getMemoryStats();
console.log('\nWASM Memory Stats:');
console.log(` Meshes: ${stats.numMeshes} (should be 0 for layer-only)`);
console.log(` Materials: ${stats.numMaterials}`);
console.log(` Buffer Memory: ${stats.bufferMemoryMB?.toFixed(2) || 0} MB`);
} catch (e) {
// getMemoryStats may not exist
}
} else {
console.error('Failed to load USD:', usd.error());
}
console.log('\nNode.js Memory Usage:');
reportMemUsage();
return { success: loadOk, loadTime };
} catch (error) {
console.error('Load only failed:', error);
throw error;
}
}
// ============================================================
// Example 6: Compare full load vs load-only
// ============================================================
async function exampleCompareLoadModes(filePath) {
console.log('=== Compare Load Modes ===');
console.log(`File: ${filePath}`);
const loader = new TinyUSDZLoader();
await loader.init({ useMemory64: true });
const fs = await import('fs');
const fileData = fs.readFileSync(filePath);
const fileSizeMB = (fileData.length / (1024 * 1024)).toFixed(2);
console.log(`File size: ${fileSizeMB} MB`);
// Mode 1: Load Only (Layer parsing, no scene conversion)
console.log('\n--- Mode 1: Load Only (Layer) ---');
const loadOnlyStart = performance.now();
const usd1 = new loader.native_.TinyUSDZLoaderNative();
const ok1 = usd1.loadAsLayerFromBinary(fileData, filePath);
const loadOnlyTime = performance.now() - loadOnlyStart;
console.log(` Load time: ${loadOnlyTime.toFixed(2)} ms`);
console.log(` Load result: ${ok1 ? 'success' : 'failed'}`);
console.log(' Memory (Layer only):');
reportMemUsage();
// Reset for fair comparison
usd1.reset();
// Force GC if available
if (global.gc) {
global.gc();
console.log(' (GC triggered)');
}
// Mode 2: Full Load (with RenderScene conversion)
console.log('\n--- Mode 2: Full Load (with Scene Conversion) ---');
const fullLoadStart = performance.now();
const usd2 = new loader.native_.TinyUSDZLoaderNative();
const ok2 = usd2.loadFromBinary(fileData, filePath);
const fullLoadTime = performance.now() - fullLoadStart;
console.log(` Load time: ${fullLoadTime.toFixed(2)} ms`);
console.log(` Load result: ${ok2 ? 'success' : 'failed'}`);
// Get render scene stats
try {
const stats = usd2.getMemoryStats();
console.log(' RenderScene Stats:');
console.log(` Meshes: ${stats.numMeshes}`);
console.log(` Materials: ${stats.numMaterials}`);
console.log(` Textures: ${stats.numTextures}`);
console.log(` Images: ${stats.numImages}`);
console.log(` Buffers: ${stats.numBuffers}`);
console.log(` Buffer Memory: ${stats.bufferMemoryMB?.toFixed(2) || 0} MB`);
} catch (e) {
// getMemoryStats may not exist
}
console.log(' Memory (Full load):');
reportMemUsage();
// Summary
console.log('\n--- Summary ---');
console.log(` Load Only (Layer): ${loadOnlyTime.toFixed(2)} ms`);
console.log(` Full Load (RenderScene): ${fullLoadTime.toFixed(2)} ms`);
console.log(` Scene conversion overhead: ${(fullLoadTime - loadOnlyTime).toFixed(2)} ms`);
if (fullLoadTime > 0) {
console.log(` Overhead ratio: ${((fullLoadTime / loadOnlyTime - 1) * 100).toFixed(1)}% slower`);
}
}
// ============================================================
// Example 7: Compare streaming vs traditional loading
// ============================================================
async function exampleComparePerformance(filePath) {
console.log('=== Performance Comparison ===');
@@ -182,12 +326,15 @@ async function exampleComparePerformance(filePath) {
const loader = new TinyUSDZLoader();
await loader.init({ useMemory64: true });
const fs = await import('fs');
// Traditional loading (read entire file into JS, then copy to WASM)
console.log('\n--- Traditional Loading ---');
const traditionalStart = performance.now();
const fs = await import('fs');
const fileData = fs.readFileSync(filePath);
let fileData = fs.readFileSync(filePath);
const fileSizeMB = (fileData.length / (1024 * 1024)).toFixed(2);
console.log(` File size: ${fileSizeMB} MB`);
const traditionalReadTime = performance.now() - traditionalStart;
const usd1 = new loader.native_.TinyUSDZLoaderNative();
@@ -197,6 +344,43 @@ async function exampleComparePerformance(filePath) {
console.log(` Read time: ${traditionalReadTime.toFixed(2)} ms`);
console.log(` Total time: ${traditionalTotalTime.toFixed(2)} ms`);
console.log(` Load result: ${ok1 ? 'success' : 'failed'}`);
console.log(' Memory after traditional load:');
reportMemUsage();
// Clear memory before streaming test for fair comparison
console.log('\n--- Clearing Memory ---');
// Reset WASM memory (clear render scene, assets, caches)
try {
usd1.reset();
console.log(' WASM memory reset');
} catch (e) {
// reset() may not exist in older builds
try {
usd1.clearAssets();
console.log(' WASM assets cleared (fallback)');
} catch (e2) {
console.log(' Could not clear WASM memory');
}
}
// Clear JS file data reference
fileData = null;
// Try to trigger garbage collection if available
// Run with: node --expose-gc test-stream-load.js file.usd --compare
if (global.gc) {
global.gc();
console.log(' GC triggered');
} else {
console.log(' GC not available (run with --expose-gc for manual GC)');
}
// Small delay to allow GC to run
await new Promise(resolve => setTimeout(resolve, 100));
console.log(' Memory after cleanup:');
reportMemUsage();
// Streaming loading (chunks transferred directly to WASM)
console.log('\n--- Streaming Loading ---');
@@ -215,12 +399,15 @@ async function exampleComparePerformance(filePath) {
console.log(` Transfer time: ${streamingTransferTime.toFixed(2)} ms`);
console.log(` Total time: ${streamingTotalTime.toFixed(2)} ms`);
console.log(` Load result: ${ok2 ? 'success' : 'failed'}`);
console.log(' Memory after streaming load:');
reportMemUsage();
// Summary
console.log('\n--- Summary ---');
console.log(` Traditional: ${traditionalTotalTime.toFixed(2)} ms`);
console.log(` Streaming: ${streamingTotalTime.toFixed(2)} ms`);
console.log(` Memory benefit: Streaming frees JS memory chunk-by-chunk`);
console.log(' Note: Run with "node --expose-gc" for accurate memory comparison');
}
// ============================================================
@@ -230,10 +417,18 @@ async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Usage: node test-stream-load.js <path-to-usd-file> [--compare]');
console.log('Usage: node test-stream-load.js <path-to-usd-file> [options]');
console.log('');
console.log('Options:');
console.log(' --load-only Load USD only (no scene conversion) - measure pure parsing');
console.log(' --compare-modes Compare load-only vs full load with scene conversion');
console.log(' --compare Compare streaming vs traditional loading');
console.log(' --url Treat input as URL (auto-detected for http/https)');
console.log('');
console.log('Examples:');
console.log(' node test-stream-load.js model.usdz');
console.log(' node test-stream-load.js model.usdz --load-only');
console.log(' node test-stream-load.js model.usdz --compare-modes');
console.log(' node test-stream-load.js model.usdz --compare');
console.log(' node test-stream-load.js https://example.com/model.usdz --url');
process.exit(1);
@@ -242,6 +437,8 @@ async function main() {
const input = args[0];
const isUrl = args.includes('--url') || input.startsWith('http://') || input.startsWith('https://');
const doCompare = args.includes('--compare');
const doLoadOnly = args.includes('--load-only');
const doCompareModes = args.includes('--compare-modes');
try {
if (isUrl) {
@@ -251,7 +448,11 @@ async function main() {
await exampleLoadWithStreaming(input);
} else {
// File-based examples
if (doCompare) {
if (doLoadOnly) {
await exampleLoadOnly(input);
} else if (doCompareModes) {
await exampleCompareLoadModes(input);
} else if (doCompare) {
await exampleComparePerformance(input);
} else {
await exampleStreamFile(input);
@@ -277,5 +478,7 @@ export {
exampleStreamFile,
exampleLoadWithStreaming,
exampleStreamMultiple,
exampleLoadOnly,
exampleCompareLoadModes,
exampleComparePerformance
};