Memory Issue - ArcGIS 4.14 & Angular - Memory not getting freed

1023
7
08-10-2020 08:10 AM
AndrewMurdoch1
Occasional Contributor II

Good Day

I've noticed a memory issue where, at least in Angular 9, once your destroy the view / map objects, the memory usage is extremely high, ~100 MB / map.  I’ve Prepared a test repo, that initializes a map and allows you to jump to another page, when I run this code and jump between the pages the memory isn't being freed correctly, hopefully this helps:

 

GitHub - docmur/angular-esri-memory-test 

My destroy functions, of our main application is:

nullifyComponents() {
  console.log('Destroying the ESRI View and ESRI Map');
  if (this._map) {
    this._map.removeAll();
    this._map.destroy();
    this._map = null;
    delete this._map;
  } else {
    console.log('Map is NULL');
  }

  if (this._view) {
    this._view.map = null;
    this._view.container = null;
    this._view.destroy();
    this._view = null;
    delete this._view;
  } else {
    console.log('View is NULL');
  }
  
    this._BasemapGallery.destroy();
    this._BasemapGallery = null;
    this.EsriMap = null;
    this.EsriMapView = null;
    this.FeatureLayer = null;
    this.SimpleMarkerSymbol = null;
    this.SimpleFillSymbol = null;
    this.SimpleLineSymbol = null;
    this.Color = null;
    this.Graphic = null;
    this.WatchUtil = null;
    this.BasemapGallery = null;
    this.Home = null;
    this.Expand = null;
    this.Print = null;
    this.PrintTask = null;
    this.PrintTemplate = null;
    this.PrintParameters = null;
    this._BasemapGallery = null;
}

removeByTag(tag) {
  const element = document.getElementById(tag);
  while (element[0]) {
    element[0].parentNode.removeChild(element[0])
  }
}

@HostListener('window:beforeunload')
async ngOnDestroy() {
  try {
    /* Get rid over everything else */
    this.removeAll();

    /* Destroy and remove memory references to the maps */
    this.nullifyComponents();

    this.mapViewEl = null;
    delete this.mapViewEl;

    this.removeByTag('mapElement');
  } catch (error) {
    console.log(error);
  }
}

When I look at the memory tab I see:

When I run a heap stack trace I see:


Among another objects.  Is there another way to assure the memory is getting freed? 

If I navigate back to the maps and away, it will add another 100 MB / map, until all the memory is used up.  The function removeAll is setting all internal variables to null to assure they don't reference any of the ESRI components.    The version of the library in use is 4.14. I’ve tried everything I can think of to free the memory including deleting the DOM elements in ngOnDestroy, and after they’re removed, the memory still remains.

I've seen this across Windows, Linux and Firefox / Chromium based browsers.

Thanks

0 Kudos
7 Replies
AndyGup
Esri Regular Contributor

Hi Andrew (good name, by the way),  as a best practice I recommend creating an Angular service that lets you reuse/recycle MapViews and their associated DOM nodes, and that you also limit the number of MapViews. Each additional MapView initialized within your application will consume more memory up until you hit the browser limit for WebGL contexts (typically 16), or the browser tab might automatically restart because of high-memory usage.

There's no guarantee that your approach is actually deleting "all" code references that are internal and external to ArcGIS API for JavaScript modules, and therefore there are things that can't be garbage collected by the browser. This issue isn't related to Angular specifically, it's just how JavaScript works. I'm guessing from your code above there's a really good chance the app is creating what's called detached DOM nodes, which is one type of major memory leak. they can't be garbage collected even if you try to null them out in your code. You can see these detached nodes when you do a JS Heap snapshot and search for the word "detached". 

Reference: https://developers.google.com/web/tools/chrome-devtools/memory-problems#discover_detached_dom_tree_m... 

AndrewMurdoch1
Occasional Contributor II

Thank you for the response, I'll take a look.

0 Kudos
AndrewMurdoch1
Occasional Contributor II

Good Day

Solid name

Do you have an example of how to use a service to maintain the same view across several maps?

I wrote this function, which lives in a service (esri-service.ts), which is separate from the esri-map-controller.ts:

initMap(mapContainer: ElementRef) {
  return new Promise( (r, j) => {
    loadModules([
      'esri/Map',
      'esri/views/MapView',
    ]).then(([
      EsriMap,
      EsriMapView,
    ]) => {

      if (this._map === null && this._view === null) {
        console.log('\n\nAllocating new Map!!!\n\n');
        const mapProperties: esri.MapProperties = {
          basemap: this._basemap,
        };

        this._map = new EsriMap(mapProperties);

        const mapViewProperties: esri.MapViewProperties = {
          container: mapContainer.nativeElement,
          center: this._center,
          zoom: this._zoom,
          map: this._map
        };

        this._view = new EsriMapView(mapViewProperties);

        r({
          map: this._map,
          view: this._view
        });
      } else {
        console.log('\n\nUsing Existing!!!\n\n');

        const reAllocateMap = false;
        if (reAllocateMap) {
          const mapProperties: esri.MapProperties = {
            basemap: this._basemap,
          };

          this._map = new EsriMap(mapProperties);
          this._view.map = this._map;
        }

        this._view.container = mapContainer.nativeElement;

        r({
          map: this._map,
          view: this._view
        });
      }
    });
  })
}

The first time I call this I get a map and view and everything works fine, but when I call it a second time to get the view, then swap the ElementRef, the map appears but I can't interact with it.  If I change the code and allocate a new map object then assign it into the view, I get a fully interactive view and map back, but the memory usage goes way up again, which is the problem I'm trying to work around.

When I look at the detached memory, I see a bunch of references to MapView.js and engine.js from an arcgis 4.15 / 4.14 url, which I think is coming from EsriMapView and EsriMap, but I can't do anything with them except set them to null in the ngOnDestroy, because if I try to call .destroy() an exception is thrown that the function doesn't exist.

Could there be an issue with EsriMapView getting properly destroyed?  If you reference the sample project I made, it has this same issue, and I noticed the same problem on the angular-cli-map project.


0 Kudos
AndyGup
Esri Regular Contributor

I don't know of any Angular examples. However, here is a simple codepen that demonstrates the basic concepts behind swapping MapViews: https://codepen.io/andygup/pen/VwYjGRM. You'll need to apply similar concepts in your app.

AndrewMurdoch1
Occasional Contributor II

Good Afternoon

I'll give this a try and let you know how it works.

0 Kudos
AndrewMurdoch1
Occasional Contributor II

Found a good enough workaround,  if you call window.location.reload() inside ngOnDestroy of any page that hosts the maps, the memory will be freed, which is what we're doing.

0 Kudos
AndyGup
Esri Regular Contributor

Gotcha, full page refresh would definitely free the memory up however I'm sure that's not your preferred approach. So, some good news is we just added a view.destroy() method in the /next build of the JS API that you can test it out before the next release (4.17 - Early October 2020): https://github.com/Esri/feedback-js-api-next. If you have a chance to test it, let us know how if you run into any issues?

// Unset map from the view so that it is not destroyed
const map = view.map;
view.map = null;

// Coming in 4.17 - destroy the view and all attached resources
view.destroy();‍‍‍‍‍‍