Background
A recent profiling of loading a serialized scene with babylonFileLoader shows that scene.getNodeById
takes about half of the CPU time in the whole loading process.
After looking into the source, I found that the only Parse
function that calls getNodeById
is AnimationGroup.Parse
, which called per targetedAnimation.
The scene.getNodeById
, underthehood, called getMeshById
, getTransformNodeById
, and other similar methods.
And taking getMeshById
as an example, which consumes most CPU time of getNodeById
in the profile, it’s a linear search over all meshes in the scene.
The scene can not be shared since it contains commerical data, but I can share some stats of it.
The serialized scene have ~2k transformNodes, ~6k meshes, ~4k of which are InstancedMesh, 5 animation groups with ~23k targetedAnimations (channels).
So, the time complexity of the AnimationGroup.Parse is O(m*n)
where m is the count of targetedAnimations, n is the count of meshes and transformNodes, which is ~210M id comparsion in this scene.
Description
In babylonFileLoader
, AnimationGroup.Parse is called after all meshes, transformNodes, and other stuff needed in getNodeById
parsed, and parsing AnimationGroups will not introduce new nodes.
So, it is possible to accelerate this parsing by using a prebuilt lookup table for all nodes before AnimationGroup.Parse
called. AnimationGroup.Parse
will use the lookup table if provided by the caller, via an extra argument, or some shared context, and if the lookup table does not exist, fallback to the getNodeById
method, so this would not be a breaking change.
Proposed change
This reduced time complexity of the AnimationGroup.Parse from O(m*n) to
O(m+n)`, but requires additional memory to build the lookup table.
diff --git a/packages/dev/core/src/Animations/animationGroup.ts b/packages/dev/core/src/Animations/animationGroup.ts
index 0530814d5a..f457e86f9c 100644
--- a/packages/dev/core/src/Animations/animationGroup.ts
+++ b/packages/dev/core/src/Animations/animationGroup.ts
@@ -997,7 +997,7 @@ export class AnimationGroup implements IDisposable {
* @param scene defines the scene that will receive the animationGroup
* @returns a new AnimationGroup
*/
- public static Parse(parsedAnimationGroup: any, scene: Scene): AnimationGroup {
+ public static Parse(parsedAnimationGroup: any, scene: Scene, nodeMap?: Map<Node["id"], Node>): AnimationGroup {
const animationGroup = new AnimationGroup(parsedAnimationGroup.name, scene, parsedAnimationGroup.weight, parsedAnimationGroup.playOrder);
for (let i = 0; i < parsedAnimationGroup.targetedAnimations.length; i++) {
const targetedAnimation = parsedAnimationGroup.targetedAnimations[i];
@@ -1010,7 +1010,7 @@ export class AnimationGroup implements IDisposable {
animationGroup.addTargetedAnimation(animation, morphTarget);
}
} else {
- const targetNode = scene.getNodeById(id);
+ const targetNode = nodeMap ? nodeMap.get(id) : scene.getNodeById(id);
if (targetNode != null) {
animationGroup.addTargetedAnimation(animation, targetNode);
diff --git a/packages/dev/core/src/Loading/Plugins/babylonFileLoader.ts b/packages/dev/core/src/Loading/Plugins/babylonFileLoader.ts
index a473590441..9e32ddcdec 100644
--- a/packages/dev/core/src/Loading/Plugins/babylonFileLoader.ts
+++ b/packages/dev/core/src/Loading/Plugins/babylonFileLoader.ts
@@ -423,11 +423,27 @@ const LoadAssetContainer = (scene: Scene, data: string, rootUrl: string, onError
}
}
+ const nodeMap = new Map<Node["id"], Node>();
+ for (let index = 0; index < scene.meshes.length; index++) {
+ if (!nodeMap.has(scene.meshes[index].id)) { // This follows the behavior of scene.getXXXById, which picks the first match
+ nodeMap.set(scene.meshes[index].id, scene.meshes[index]);
+ }
+ }
+ for (let index = 0; index < scene.transformNodes.length; index++) {
+ if (!nodeMap.has(scene.transformNodes[index].id)) {
+ nodeMap.set(scene.transformNodes[index].id, scene.transformNodes[index]);
+ }
+ }
+ for (let index = 0; index < scene.cameras.length; index++) {
+ if (!nodeMap.has(scene.cameras[index].id)) {
+ nodeMap.set(scene.cameras[index].id, scene.cameras[index]);
+ }
+ }
+ // TODO: all kinds of nodes
// Animation Groups
if (parsedData.animationGroups !== undefined && parsedData.animationGroups !== null) {
for (index = 0, cache = parsedData.animationGroups.length; index < cache; index++) {
const parsedAnimationGroup = parsedData.animationGroups[index];
- const animationGroup = AnimationGroup.Parse(parsedAnimationGroup, scene);
+ const animationGroup = AnimationGroup.Parse(parsedAnimationGroup, scene, nodeMap);
container.animationGroups.push(animationGroup);
animationGroup._parentContainer = container;
log += index === 0 ? "\n\tAnimationGroups:" : "";
Alternatives
- The lookup table, since
Node["id"]
isstring
, could be plain js object for better browser compatibility. - Optimize underlying
getMeshById
,getTransformNodeById
, and other methods to aO(1)
hash lookup, but sincescene.meshes
is exposed public as raw js array, this could be more challenging, or more breaking.