Memory Leak Observed during webviewer model switching

Issue:
Switching between models leads to increasing memory usage. The growth pattern differs between size of models, with bigger models severe leaks.

Description:

Version Initial Load After Each Switch After 2–3 Switches Summary
Model 1 (Small) 400 MB +150–200 MB 1200–1500 MB Memory grows steadily on each switch; never drops
Model 2 (Big) 900 MB hike +1500–1700 MB 4000 MB Severe memory leak observed

Observation:

  • Small Models shows moderate memory growth but remains manageable.

  • Big models (not optimized ) exhibits a rapid and severe memory leak with each switch.

  • but after viewer shutdown and switch to different model the increase memory is not reducing

Hello @raja.r,

What are the specific functions that you are calling to switch models?

Thanks,
Tino

import EvmUtils from "./common/evmUtils.js";

export const _disposeViewer = async (viewer) => {
    if (!viewer) return;

    try {
        if (viewer.waitForIdle) {
            await viewer.waitForIdle();
        }
        if (viewer.shutdown) {
            await viewer.shutdown();
            await viewer.waitForIdle();
        }

        viewer = null;
        console.log("Viewer disposed successfully.");
    } catch (err) {
        console.error("Error while disposing viewer:", err);
    }
};

const _cleanupAllCanvases = (iafViewer) => {
    try {
        const widgetIds = Object.values({
            View3d: EvmUtils.EVMMode.View3d,
            View2d: EvmUtils.EVMMode.View2d,
        })

        widgetIds.forEach((widgetId) => {
            const container = iafViewer.evmElementIdManager.getEvmElementById(widgetId);
            if (!container) return;

            const canvases = container.querySelectorAll("canvas");
            canvases.forEach((canvas) => {
                // Clear 2D context
                const ctx = canvas.getContext("2d");
                if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);

                // Reset dimensions
                canvas.width = 0;
                canvas.height = 0;
            });
        });
    } catch (error) {
        console.error("Error in canvas cleanup:", error);
    }
};


export const cleanupAllResources = async (iafViewer) => {
    console.log('IafViewerDBM.cleanupAllResources', 'Starting comprehensive cleanup');

    try {
        if (iafViewer) {

            await _disposeViewer(iafViewer, iafViewer._viewer)
            await _disposeViewer(iafViewer, iafViewer._viewer2d)
        }

        _cleanupAllCanvases(iafViewer);

        if (typeof window !== 'undefined' && window.gc) {
            try {
                window.gc();
            } catch (error) {
                // GC not available or failed
            }
        }

        console.log('IafViewerDBM.cleanupAllResources', 'Cleanup completed successfully');

    } catch (error) {
        console.error('IafViewerDBM.cleanupAllResources', 'Error during cleanup:', error);
    }
}

The code you provided is doing the clean-up work. But how are you loading subsequent models? For example:

await hwv.model.clear();

await hwv.model.loadSubtreeFromScsFile(
	hwv.model.getAbsoluteRootNode(),
	"microengine.scs",
	optional_matrix
);

Also, which type of Stream Cache files are you loading SC/SCZ or SCS?

  _createViewer() {
    console.log ('Iaf3DViewer _createViewer');

    if (!this.props.view3d.enable) return;
    const { fileSet, model, authToken, wsUri, settings } = this.props;
    let viewerPromise;
    let streamCutoffScale = 0.5;
    this._viewerInitialized = true;
    if (settings && settings.hasOwnProperty("streamCutoffScale"))
      streamCutoffScale = parseFloat(settings.streamCutoffScale);

    // this.props.openNotification ("The model is being loaded (" + this.props.graphicsResources.views[0].title + ")");
    NotificationStore.notifyModelIsBeingLoaded(this, 0);

    const perfLogger = new IafPerfLogger("viewer.defaultView3d");

    // RRP:- Duplicate as it is called from modelStructure ready.
    // this.initalizeModelComposer();

    viewerPromise = this._hwvManager.createRemoteViewer(
      this.evmElementIdManager.getEvmUuid(EvmUtils.EVMMode.View3d),
      model,
      fileSet,
      authToken,
      wsUri,
      streamCutoffScale,
      0
    );

    viewerPromise.then((viewer) => {
      this._viewer = viewer;
      this._boundingBoxMarkup = new IafBoundingBoxMarkup(this._viewer);
      this.graphicsResources = this.props.graphicsResources;
      this._viewer.graphicsResources = this.graphicsResources;
      this.graphicsResources.setviewer(this, this._viewer);
      this._isolateZoomHelper = new ViewerIsolateZoomHelper(viewer, this);

      // Once the viewer is instantiated, we can set the state to true to have the React update the DOM
      this.setState({
        hwvInstantiated: true,
      });

      //use our own Select Operator

      this.selectOperator = new SelectOperator(
        this._viewer,
        this._viewer.noteTextManager,
        this
      );
      this.selectOperatorId = this._viewer.registerCustomOperator(
        this.selectOperator
      );
      this.iafCuttingPlanesUtils = new IafCuttingPlanesUtils(this._viewer
                                                        , this.newToolbarElement ? this.newToolbarElement.current : undefined);

      this._viewer.operatorManager.set(this.selectOperatorId, 1);
      this.measurementManager = new IafMeasurementManager(this._viewer,"Measurement3d",this.projectId);
      // Storing the callback in its own function to avoid registering a bound callback
      // (more difficult to unregister that in HC)
      //let parent = this;
      this._viewer.setCallbacks({
        // Add new callbacks at the top, so that they don't accidentally replace the earlier ones
        // 10-12-23 ATK MK-149 Added viewer callbacks for error handling
        modelStructureHeaderParsed: async (fileName, fileType) => {
          console.log ('viewer.callbacks.modelStructureHeaderParsed'
            , '/fileName', fileName
            , '/fileType', fileType
          );
        },
        missingModel: async (modelPath) => {
          console.log ('viewer.callbacks.missingModel'
            , '/modelPath', modelPath
          );
        },
        modelLoadBegin: async () => {
          console.log ('viewer.callbacks.modelLoadBegin'
          );
        },
        modelLoadFailure: async (modelName, reason, error) => {
          // TOBE: Reviewed for performace projects.
          NotificationStore.notifyModelIsMissing(this, modelName, true);
          console.error('viewer.callbacks.modelLoadFailure'
            , '/modelName', modelName
            , '/reason', reason
            , '/error', error
          );
        },
        modelLoaded: async (modelRootIds, source) => {
          console.log ('viewer.callbacks.modelLoaded'
            , '/modelRootIds', modelRootIds
            , '/source', source
          );
        },
        modelStructureLoadBegin: async () => {
          console.log ('viewer.callbacks.modelStructureLoadBegin'
          );
        },
        modelStructureLoadEnd: async () => {
          console.log ('viewer.callbacks.modelStructureLoadEnd'
          );
        },
        modelStructureParseBegin: async () => {
          console.log ('viewer.callbacks.modelStructureParseBegin'
          );
        },
        modelSwitchStart: async (clearOnly) => {
          console.log ('viewer.callbacks.modelSwitchStart'
            , '/clearOnly', clearOnly
          );
        },
        modelSwitched: async (clearOnly, modelRootIds) => {
          console.log ('viewer.callbacks.modelSwitched'
            , '/clearOnly', clearOnly
            , '/modelRootIds', modelRootIds
          );
        },
        viewLoaded: async (view) => {
          console.log ('viewer.callbacks.viewLoaded'
            , '/view', view
          );          
        },
        websocketConnectionClosed: async () => {
          console.log ('viewer.callbacks.websocketConnectionClosed'
          );                    
        },
        webGlContextLost: async () => {
          console.log ('viewer.callbacks.webGlContextLost'
          );                    
        },
        streamingActivated: () => {
          // if (!this.state.isModelStructureReady) this.setModelIsLoaded(this._viewer, false);
        },
        streamingDeactivated: async () => {
          // if (!this.state.isModelStructureReady) this.setModelIsLoaded(this._viewer, true);
        },
        addCuttingSection: async (cuttingSection /* : Communicator.CuttingSection */) => {
          console.log ('viewer.Callbacks.addCuttingSection', '/cuttingSection', cuttingSection);
        },
        removeCuttingSection: async () => {
          console.log ('viewer.Callbacks.removeCuttingSection');
        },
        cuttingPlaneDragStart: async (cuttingSection /* : Communicator.CuttingSection */,
                                      planeIndex /* : number */) => {
          console.log ('viewer.Callbacks.cuttingPlaneDragStart', 
                        '/cuttingSection', cuttingSection,
                        '/planeIndex', cuttingSection.planeIndex);
        },
        cuttingPlaneDragEnd: async (cuttingSection /* : Communicator.CuttingSection */,
                                    planeIndex /* : number */) => {
          // console.log ('viewer.Callbacks.cuttingPlaneDragEnd', 
          //               '/cuttingSection', cuttingSection,
          //               '/cuttingSection.planeIndex.logical', cuttingSection.planeIndex.logical,
          //               '/planeIndex', cuttingSection.planeIndex,
          //               '/d', cuttingSection.getPlane(0).d);
          this.iafCuttingPlanesUtils.onDragCuttingPlaneFromGraphics(cuttingSection);

        },
        cuttingPlaneDrag: async (cuttingSection /* : Communicator.CuttingSection */,
                                  planeIndex /* : number */) => {
          // console.log ('viewer.Callbacks.cuttingPlaneDrag', 
          //               '/cuttingSection', cuttingSection,
          //               '/cuttingSection.planeIndex.logical', cuttingSection.planeIndex.logical,
          //               '/planeIndex', cuttingSection.planeIndex,
          //               '/d', cuttingSection.getPlane(0).d
          //               );
          this.iafCuttingPlanesUtils.onDragCuttingPlaneFromGraphics(cuttingSection);
        },
        // ATK: To Do: Get rid of the following as it appears redundant
        // sceneReady: () => {
        //   const camera = this._viewer.view.getCamera();
        //   // set saved intial camera position
        //   if (this.props.settings.initialCameraPosition) {
        //     let newCamera = window.Communicator.Camera.fromJson(
        //       this.props.settings.initialCameraPosition
        //     );
        //     this._viewer.view.setCamera(newCamera);
        //   }
        // },
        subtreeLoaded: (modelRootIds, source) => console.log('subtreeLoaded', source, modelRootIds),
        subtreeDeleted: (modelRootIds) => console.log('subtreeDeleted', modelRootIds),
        modelStructureReady: async () => {
          perfLogger.end();
          iafCallbackModelStructureReady(this);
        },
        measurementValueSet: async (m) => {
          const { multiplier } = this.state;
          let mValue = m.getMeasurementValue();
          let uMultiplier = m.getUnitMultiplier();
          let mmUnits = mValue * uMultiplier;
          let newUnitMul = multiplier;
          m.setUnitMultiplier(newUnitMul);

          m._measurementValue = mmUnits / newUnitMul;
          m.setMeasurementText(
            Communicator.Util.formatWithUnit(m._measurementValue, newUnitMul)
          );
        },

        selectionArray: async (selectionEvents) => {
          if (selectionEvents.length === 0) return;

          //selectionEvents only represents the latest select event
          //use viewer.selectionManager.getResults to get current selected nodes
          // or selectionEvents[0]._selection._nodeId

          let id = this.getActiveSelectionNodeId(this._viewer, selectionEvents);
          let elementId = id ? this.getClosestElementIds([id]) : undefined;
          // let csdlOffset = this._viewer.model.getNodeIdOffset(id);

          if (!id || !_.size(elementId)) {
            console.log ('Viewer.selectionArray.3D'
              , 'Invalid callback'
              , '!id || !_.size(elementId)'
              , '/id', JSON.stringify(id)
              // , '/csdlOffset', csdlOffset
              , '/elementId', JSON.stringify(elementId)
            );
            // HSK PLAT-4891: Bug - 3D Text Markup is editable only on creation
            // this.markupManager && this.markupManager.drawTextOperator.selectTextboxItem(id);
            NotificationStore.notifyNoBimAssociationFound(this);
            return;
          }
        
          console.log ('Viewer.selectionArray.3D'
            , '/id', id
            , '/this.prevSelection', JSON.stringify(this.prevSelection)
            , '/elementId', JSON.stringify(elementId)
          );
        
          return this.handleElementGraphicsSelection(elementId, false);
        },
        sceneReady: async () => {
          console.log ('viewer.callbacks.sceneReady');
          let camPos, target, upVec;
          /*camPos = new window.Communicator.Point3(0, 200, 400);
          target = new window.Communicator.Point3(0, 1, 0);
          upVec = new window.Communicator.Point3(0.2, 1, 0.5);
          const defaultCam = window.Communicator.Camera.create(
            camPos, target, upVec, 1, 720, 720, 0.01);
          this._viewer.view.setCamera(defaultCam);*/

          // Background color for viewers
          this._viewer.view.setBackgroundColor(
            new window.Communicator.Color(252, 252, 252),
            new window.Communicator.Color(230, 230, 230)
          );

          //check
          // if opacity works
          this.setNodeSelectionColor(this._viewer, bdSelectColor);
          this.setNodeSelectionOutlineColor(this._viewer, bdSelectColor);
          this.setNodeElementSelectionColor(this._viewer, faceSelectColor);
          this.setNodeElementSelectionOutlineColor(this._viewer, lineSelectColor);

          //PLAT-575: turn off parent selecting for now as it's not a helpful feature now
          this._viewer.selectionManager.setSelectParentIfSelected(false);
          //Default draw mode to Shaded rather than WireframeOnShaded
          this._viewer.view.setDrawMode(Communicator.DrawMode.Shaded);
          //if silhouetted edges are enabled, be sure to disable them in glass mode
          this._viewer.view.setSilhouetteEnabled(true);
          // set ambient occlusion mode and radius
          this._viewer.view.setAmbientOcclusionEnabled(true);
          this._viewer.view.setAmbientOcclusionRadius(0.01);
          //this._viewer.view.setBloomEnabled(true)
          //this._viewer.view.setBloomIntensityScale(0.1)
          
          try {
            // Calling initializeOperators to set up navigation operators
            await this.initializeOperators();
            console.log('Navigation operators successfully initialized.');
          } catch (error) {
            console.error('Error initializing navigation operators:', error.message);
          }
          
          // if(this._drawMode === IafDrawMode.Glass)
          //   this.setXrayModeSettings()
          
          // object of IafSavedViews class
          this.savedViewsManager = new IafSavedViews();
          // Create Navigation cube
          if(this.props.isShowNavCube || typeof this.props.isShowNavCube  === "undefined"){
            await this._viewer.view.getNavCube().enable();
          await this._viewer.view
            .getNavCube()
            .setAnchor(window.Communicator.OverlayAnchor.LowerRightCorner);
          }
          if (
            this.isElementThemingIdExists(this.props.colorGroups) ||
            this.props?.sliceElementIds?.length > 0
          )
            this.forceUpdateViewerElements = true;

          this.isSceneReady = true;
          console.log("Web Viewer has been initialized.");
        },
        timeout: () => {
          console.log ('viewer.callbacks.timeout');
          if (
            window.confirm(
              "This page timed out due to inactivity. Press OK to refresh."
            )
          ) {
            location.reload();
          }
        },
        camera: (camera) => {
          iafCallbackCamera(this, camera);
        },
        // HSK PLAT-4861: UX - Move markup options to under Measurements (Annotations)
        measurementCreated: (measurement) => {
          !this.markupManager.repeatLastMode && this._viewer.operatorManager.set(this.selectOperatorId, IafOperatorUtils.IafOperatorPosition.Operation);
        }
      });
      // window.addEventListener("resize", this.onResize);
      // this.evmElementIdManager.getEvmElementById(EvmUtils.EVMMode.View3d).addEventListener("contextmenu", (e) => contextmenu(e, this));
      // added click listner to close active submenu when clicked on viewer
      // this.evmElementIdManager.getEvmElementById(EvmUtils.EVMMode.View3d).addEventListener("mousedown", (e) => mousedown(this));
      // document.addEventListener("click", (e) => {this.contextMenu.style.display = "none";})
    });
  }
    createRemoteViewer(containerId, model, fileSet , authToken, wsUri, streamCutoffScale, defaultViewIndex) {
    let uri = wsUri + '/graphicssvc?fileSetId=' + fileSet._id +
      '&nsfilter=' + model._namespaces[0] +
      '&token=' + authToken + '&serverVersion=4'

    // let fileName = fileSet && _.size(fileSet._files) > 0 ? fileSet._files[0]._fileName : model._name + '.scz'
    let fileName = IafUtils.buildFileName(model, fileSet, defaultViewIndex ? defaultViewIndex : 0);

    let viewerPromise = new Promise((resolve) => {
      let params = {
        containerId: containerId,
        streamMode: window.Communicator.StreamingMode.All,
        rendererType: window.Communicator.RendererType.Client,
        streamCutoffScale: streamCutoffScale,
        //memoryLimit: 512,
        //empty: true
        endpointUri: uri,
        boundingPreviewMode: Communicator.BoundingPreviewMode.None,
        // model: window.Communicator.EmptyModelName//model._name + '.scz'
        model: fileName//model._name + '.scz'
      };
      console.log ('ViewerManager.createRemoteViewer', params);


      // PLG-1263: EVM 1.0 - Multiple instances of viewer - Hack to wait for div to be created
      IafUtils.waitForElement(containerId).then ((container) => {
        let _viewer = new window.Communicator.WebViewer(params);
        _viewer.start();
        resolve(_viewer);
      })
    })
      
    return viewerPromise;
  }


2023_SP1 usign hoops communicator

A few things to note:

  • You are using an older version of Communicator (HC 2023 SP1) that is more than two full generations behind (current version is HC 2025.6.1). There have been server improvements and bug fixes between these releases.
  • You can try calling curl -X GET http://localhost:11182/api/spawns to confirm that the number of spawns is equal to the active viewer sessions. NOTE: replace “localhost” with your hostname.
  • It’s unclear why you are calling waitForIdle() after shutting down the viewer. Please see more information in our docs about this function. It is used to check if the model has fully loaded in the viewer (which is a moot point if it was shut down).
  • Let’s try deleting the viewer after shutdown:
this._viewer.shutdown();
delete this._viewer;