donmccurdy / glTF-Transform

glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.
https://gltf-transform.dev
MIT License
1.4k stars 149 forks source link

Memory overhead in scenes with many objects #1527

Open donmccurdy opened 1 week ago

donmccurdy commented 1 week ago

tl;dr — Scenes with many (1M+) meshes/nodes/accessors cause higher than expected memory cost and potential OOM

While investigating a recent question on the Blender Stack Exchange, I noticed that a glTF file linked from the question[^1] caused an OOM when running gltf-transform inspect. The file contained 200 MB of uncompressed binary data, which isn't a problem, but more importantly some 500,000 distinct mesh primitives. If the Node.js memory limit is around 4GB, that implies a memory cost of something like 8kb per mesh primitive. I haven't measured the memory allocation causes in Node.js yet, but the number looked higher than expected.

I ran further tests by printing heap size incrementally during loading. Heap size went from nominal to 3.7GB after initializing accessors, and hit OOM while processing mesh primitives. Note, this is not the binary vertex data, which has very predictable binary size, but the property graph organizing the scene/nodes/meshes/accessors on top of that data. On a hunch, I next tried to disable all event dispatchers and event listeners (managed by property-graph). This reduced heap size after initializing accessors to 2.4GB, and the scene loaded. Without events, some graph edges are never connected to the Root, so this leaves us with two plausible causes:

  1. The events/listeners (and associated closures) cost more memory than expected
  2. The memory cost is due to graph size, not events/listeners, and disabling the events reduced memory because another 1.5–2M graph edges were never connected

My feeling is that (1) is easier to fix, but (2) is more likely, and some more investigation will be needed to determine if one of these, or something else, is the cause.

[^1]: cad_machine.gltf, 7M vertices, 0.5M mesh primitives, 1.3M accessors

donmccurdy commented 3 days ago

Results from memory profiling in Chrome devtools:

  1. Objects, 20%: 8.2M objects, all reported appear to be primitive objects. Mostly, just seeing the $attributes for Property instances here. A typical accessor holds about 120 bytes in the $attributes entry. I see a non-trivial number of empty objects: .extras={}, .extensions={}, .listeners={} or .listeners={dispose}, etc. These appear to cost a flat 56 bytes, both {} and {dispose}.
  2. Sets, 16%: 2.7M sets, one with a shallow size (presumably part of the graph itself). The rest are individually trivial. But there are many of them, at about 152 bytes each, which still adds up to 400 MB.
  3. Arrays, 10%: 1.5M arrays are allocated, none exceedingly large, mostly in the property-graph module.
  4. Functions, 5%: 2.7M functions, in total 5% of retained size. The vast majority of callbacks were registered in the property-graph module.
  5. Accessor, 4%: 1.4m accessors
  6. GraphEdge, 4%: 1.4m graph edges
  7. RefMap, 2%: 1.4M refmaps

I'm surprised that instances of common classes like Accessor and GraphEdge were not more significant memory consumers.

donmccurdy commented 3 days ago

Ideas possibly worth exploring to reduce memory usage: