locked
Memory Leak in Bing Maps v8 RRS feed

  • Question

  • I created a simple page that frequently adds and removes pushpins on a Bing 8 map. I noticed that when I add mouseover/mouseout event handlers on the points, the memory is never reclaimed. Is there a different way to remove the event handlers than using Microsoft.Maps.Events.removeHandler()?

    Here is the jsfiddle I created to show the behavior: https://jsfiddle.net/jessewien/mc0dpsmw/ 

    If you open task manager you can see the memory continue to increase.

    Tuesday, February 14, 2017 2:41 PM

Answers

  • Have you tried adding the event handlers to the layer rather than to the individual pushpins? This would be much more efficient and would significantly reduce the number of event handlers your app uses. Note that is very unlikely the memory leak is due to the event handlers, and more likely that the additional memory usage is from the map generating map tiles from your data and storing it in memory. This is done as a performance enhancement that allows for fast rendering of large data sets on the HTML5 canvas.

    [Blog] [twitter] [LinkedIn]


    Tuesday, February 14, 2017 2:48 PM
  • Its not really a memory leak, this is by design. What the map does is creates tiles from your data. Initially when the map loads there is no data. After your load data it creates tiles. If you remove your data from the map the tiles are still in memory and are updated/reused rather than removed. This makes it a bit faster for rendering when you add new data to your map.

    [Blog] [twitter] [LinkedIn]


    Tuesday, February 14, 2017 7:12 PM
  • I wrote a nice long response, then my browser crashed... arg!

    Long story short, the issue is that in your code sample you are creating an infobox for each pushpin, and never disposing of them. As such the number of infoboxes in memory increases by 200 every time you add/remove your pushpins. This is why you are seeing a huge jump in memory usage. Additionally, your event handlers are referencing the local infobox variable, this is the #1 way memory leaks occur. Note that you can pass the infobox as a property of the pushpin. All shapes in Bing Maps have a metadata property that you can use to assign any data you want to link to that shape.

    Additionally, I highly recommend you use a single infobox to power all your pushpins. This will significantly reduce the resources required by your app. Infoboxes are rendered using DOM elements, and a lot of DOM elements causes performance issues. This is why shape rendering in V8 uses an HTML5 canvas rather than SVG's and img tags like V7. This allows us to render 10 times more data on the map than we could in V7. Here is a code sample that shows how to reuse an infobox across multiple pushpins: https://msdn.microsoft.com/en-us/library/mt750274.aspx


    [Blog] [twitter] [LinkedIn]

    Tuesday, February 14, 2017 11:00 PM
  • There are a couple issues in your code that are still generating memory leaks. However, the main thing needed to ensure no memory leaks from the maps side of thing is to use Layers for your data and add events to the layer. map.entities uses EntityCollections which are deprecated in V8. They are only made available for backwards compatibility and for use in simple apps. Going through and migrating your JavaScript to use Layer's and testing I found the memory leaks disappeared. Here is the code:

    document.getElementById('printoutPanel').innerHTML = 'Deleting in 3 secs...';
    var iterationCount = 0;
    var keepPlotting = true;
    var addMouseEvents = true;
    var addCustomIcons = true
    var map, loc, eventHandle, eventHandle2;
    var markerEventListeners = [];
    var pushpin;
    var tooltip = '<div id="infoboxText"><p id="infoboxDescription">{tooltext}</p></div>';
    var dataLayer;
    
    // This is the callback that Bing calls when the map is ready.
    function loadMapScenario() {
      map = new Microsoft.Maps.Map(document.getElementById('myMap'), {
        credentials: 'Your Bing Maps Key'
      });
      
      dataLayer = new Microsoft.Maps.Layer();
      map.layers.insert(dataLayer);
      
      Microsoft.Maps.Events.addHandler(dataLayer, 'mouseover', function(e) {
      if(e.target.metadata && e.target.metadata.infobox){
            e.target.metadata.infobox.setMap(map);
            }
          });
    
       Microsoft.Maps.Events.addHandler(dataLayer, 'mouseout', function(e) {
            if(e.target.metadata && e.target.metadata.infobox){
            e.target.metadata.infobox.setMap(null);
            }
          });
          
      dataLayer.add(Microsoft.Maps.TestDataGenerator.getPushpins(200, map.getBounds()));
      addMouseEvents = document.getElementById("mouseEvents").checked;
      addCustomIcons = document.getElementById("customIcons").checked;
      window.setTimeout(removeMarkers, 3000);
    }
    
    // This function will remove the markers and then plot again in 2 seconds.
    function removeMarkers() {
      // Remove pushpin infoboxes
      var pins = dataLayer.getPrimitives();
      for (var i = pins.length- 1; i >= 0; i--) {
        var pushpin = pins[i];
        if(pushpin.metadata && pushpin.metadata.infobox){    
       	 	pushpin.metadata.infobox.setMap(null);
        	pushpin.metadata = null;
        }    
      }
      dataLayer.clear();
      iterationCount++;
    
      // Either plot again or stop.
      if (keepPlotting) {
        document.getElementById('printoutPanel').innerHTML = 'Pushpins removed... plotting again.';
        window.setTimeout(plotAgain, 2000);
      } else {
        document.getElementById('printoutPanel').innerHTML = 'Plotting stopped.';
      }
    }
    
    // This will plot the markers, and add infobox handlers if requested.
    function plotAgain() {
      addMouseEvents = document.getElementById("mouseEvents").checked;
      addCustomIcons = document.getElementById("customIcons").checked;
    
    var pins;
      if (addCustomIcons) {
        pins = Microsoft.Maps.TestDataGenerator.getPushpins(200, map.getBounds(), {
          icon: 'https://www.bingmapsportal.com/Content/images/poi_custom.png'
        });
      } else {
        pins = Microsoft.Maps.TestDataGenerator.getPushpins(200, map.getBounds());
      }
      
      dataLayer.add(pins);
    
      if (addMouseEvents) {
      
        for (var i = pins.length - 1; i >= 0; i--) {
          pushpin = pins[i];
          loc = new Microsoft.Maps.Location(40.1336, -75.7385);
         
          var infobox = new Microsoft.Maps.Infobox(loc);
          infobox.setHtmlContent(tooltip.replace('{tooltext}', ('infobox ' + i)));
    
     			pushpin.metadata = {
          	infobox: infobox
          };
        }
      }
      document.getElementById('printoutPanel').innerHTML = 'Deleting pushpins in about 3 seconds...';
      window.setTimeout(removeMarkers, 3000);
    }
    
    
    function startPlotting() {
      keepPlotting = true;
      plotAgain();
    }
    
    function stopPlotting() {
      keepPlotting = false;
    }
    


    [Blog] [twitter] [LinkedIn]

    • Proposed as answer by Ricky_Brundritt Wednesday, February 15, 2017 6:11 PM
    • Marked as answer by JesseJW Wednesday, February 15, 2017 6:12 PM
    Wednesday, February 15, 2017 6:11 PM

All replies

  • Have you tried adding the event handlers to the layer rather than to the individual pushpins? This would be much more efficient and would significantly reduce the number of event handlers your app uses. Note that is very unlikely the memory leak is due to the event handlers, and more likely that the additional memory usage is from the map generating map tiles from your data and storing it in memory. This is done as a performance enhancement that allows for fast rendering of large data sets on the HTML5 canvas.

    [Blog] [twitter] [LinkedIn]


    Tuesday, February 14, 2017 2:48 PM
  • Thank you for the quick response. Are you suggesting that memory is leaked from map tiles when points are added/removed from the map? The memory is leaked without having to zoom or pan, as seen in the jsfiddle. 

    I have this implemented in a somewhat complex application that has worked fine with previous Bing maps versions (no memory leak). I didn't switch to adding the events to the layer as it seemed like a bigger change than I was hoping for in migrating to Bing 8.

    Tuesday, February 14, 2017 3:00 PM
  • Its not really a memory leak, this is by design. What the map does is creates tiles from your data. Initially when the map loads there is no data. After your load data it creates tiles. If you remove your data from the map the tiles are still in memory and are updated/reused rather than removed. This makes it a bit faster for rendering when you add new data to your map.

    [Blog] [twitter] [LinkedIn]


    Tuesday, February 14, 2017 7:12 PM
  • There are 2 things about your response that I don't understand:

    1.) If the data is reused, why do I see the memory continue to increase significantly each time I remove/add pushpins?

    2.) Does it make sense to you that the memory is only lost when event handlers are added? There is no memory loss when pushpins are simply added and removed when they do not have handlers.

    For reference again, here is the jsfiddle that shows the ever increasing memory usage:

    https://jsfiddle.net/jessewien/mc0dpsmw/ 

    Thank you.

    Tuesday, February 14, 2017 7:49 PM
  • I wrote a nice long response, then my browser crashed... arg!

    Long story short, the issue is that in your code sample you are creating an infobox for each pushpin, and never disposing of them. As such the number of infoboxes in memory increases by 200 every time you add/remove your pushpins. This is why you are seeing a huge jump in memory usage. Additionally, your event handlers are referencing the local infobox variable, this is the #1 way memory leaks occur. Note that you can pass the infobox as a property of the pushpin. All shapes in Bing Maps have a metadata property that you can use to assign any data you want to link to that shape.

    Additionally, I highly recommend you use a single infobox to power all your pushpins. This will significantly reduce the resources required by your app. Infoboxes are rendered using DOM elements, and a lot of DOM elements causes performance issues. This is why shape rendering in V8 uses an HTML5 canvas rather than SVG's and img tags like V7. This allows us to render 10 times more data on the map than we could in V7. Here is a code sample that shows how to reuse an infobox across multiple pushpins: https://msdn.microsoft.com/en-us/library/mt750274.aspx


    [Blog] [twitter] [LinkedIn]

    Tuesday, February 14, 2017 11:00 PM
  • Ricky, I truly appreciate your time and detailed explanations. Reusing a single infobox seems like a great solution as long as we don't need to support opening multiple infoboxes at the same time.

    Back to the memory issue, I tried tweaking the jsfiddle so that the infoboxes are stored in an array (defined at the top) and then each infobox is manually removed from the map with setMap(null) and the array cleared afterward. Doing this, the memory still jumps by nearly 10K each time I add/remove 200 pushpins.

    If there's a way that I could eliminate the memory issue and get this working, it would be ideal for a quick fix that gets me migrated to Bing 8. Later, I could convert everything over to the Layer approach and single infobox where possible.

    Wednesday, February 15, 2017 2:27 PM
  • There are a couple issues in your code that are still generating memory leaks. However, the main thing needed to ensure no memory leaks from the maps side of thing is to use Layers for your data and add events to the layer. map.entities uses EntityCollections which are deprecated in V8. They are only made available for backwards compatibility and for use in simple apps. Going through and migrating your JavaScript to use Layer's and testing I found the memory leaks disappeared. Here is the code:

    document.getElementById('printoutPanel').innerHTML = 'Deleting in 3 secs...';
    var iterationCount = 0;
    var keepPlotting = true;
    var addMouseEvents = true;
    var addCustomIcons = true
    var map, loc, eventHandle, eventHandle2;
    var markerEventListeners = [];
    var pushpin;
    var tooltip = '<div id="infoboxText"><p id="infoboxDescription">{tooltext}</p></div>';
    var dataLayer;
    
    // This is the callback that Bing calls when the map is ready.
    function loadMapScenario() {
      map = new Microsoft.Maps.Map(document.getElementById('myMap'), {
        credentials: 'Your Bing Maps Key'
      });
      
      dataLayer = new Microsoft.Maps.Layer();
      map.layers.insert(dataLayer);
      
      Microsoft.Maps.Events.addHandler(dataLayer, 'mouseover', function(e) {
      if(e.target.metadata && e.target.metadata.infobox){
            e.target.metadata.infobox.setMap(map);
            }
          });
    
       Microsoft.Maps.Events.addHandler(dataLayer, 'mouseout', function(e) {
            if(e.target.metadata && e.target.metadata.infobox){
            e.target.metadata.infobox.setMap(null);
            }
          });
          
      dataLayer.add(Microsoft.Maps.TestDataGenerator.getPushpins(200, map.getBounds()));
      addMouseEvents = document.getElementById("mouseEvents").checked;
      addCustomIcons = document.getElementById("customIcons").checked;
      window.setTimeout(removeMarkers, 3000);
    }
    
    // This function will remove the markers and then plot again in 2 seconds.
    function removeMarkers() {
      // Remove pushpin infoboxes
      var pins = dataLayer.getPrimitives();
      for (var i = pins.length- 1; i >= 0; i--) {
        var pushpin = pins[i];
        if(pushpin.metadata && pushpin.metadata.infobox){    
       	 	pushpin.metadata.infobox.setMap(null);
        	pushpin.metadata = null;
        }    
      }
      dataLayer.clear();
      iterationCount++;
    
      // Either plot again or stop.
      if (keepPlotting) {
        document.getElementById('printoutPanel').innerHTML = 'Pushpins removed... plotting again.';
        window.setTimeout(plotAgain, 2000);
      } else {
        document.getElementById('printoutPanel').innerHTML = 'Plotting stopped.';
      }
    }
    
    // This will plot the markers, and add infobox handlers if requested.
    function plotAgain() {
      addMouseEvents = document.getElementById("mouseEvents").checked;
      addCustomIcons = document.getElementById("customIcons").checked;
    
    var pins;
      if (addCustomIcons) {
        pins = Microsoft.Maps.TestDataGenerator.getPushpins(200, map.getBounds(), {
          icon: 'https://www.bingmapsportal.com/Content/images/poi_custom.png'
        });
      } else {
        pins = Microsoft.Maps.TestDataGenerator.getPushpins(200, map.getBounds());
      }
      
      dataLayer.add(pins);
    
      if (addMouseEvents) {
      
        for (var i = pins.length - 1; i >= 0; i--) {
          pushpin = pins[i];
          loc = new Microsoft.Maps.Location(40.1336, -75.7385);
         
          var infobox = new Microsoft.Maps.Infobox(loc);
          infobox.setHtmlContent(tooltip.replace('{tooltext}', ('infobox ' + i)));
    
     			pushpin.metadata = {
          	infobox: infobox
          };
        }
      }
      document.getElementById('printoutPanel').innerHTML = 'Deleting pushpins in about 3 seconds...';
      window.setTimeout(removeMarkers, 3000);
    }
    
    
    function startPlotting() {
      keepPlotting = true;
      plotAgain();
    }
    
    function stopPlotting() {
      keepPlotting = false;
    }
    


    [Blog] [twitter] [LinkedIn]

    • Proposed as answer by Ricky_Brundritt Wednesday, February 15, 2017 6:11 PM
    • Marked as answer by JesseJW Wednesday, February 15, 2017 6:12 PM
    Wednesday, February 15, 2017 6:11 PM
  • Thank you.
    Wednesday, February 15, 2017 6:13 PM