Updating mapbox layer is causing a strange memory issue leading to flickering

Hi everyone,

I’ve tried to title this topic appropriately, but here I’ll explain more clearly the issue I’m encountering.

Recently, I decided to update a static map that uses circle packing (https://ico.hakai.org/) with something that is zoomable. I figured out how to recreate the circle packing map using mapbox as the basemap. Of course, this meant that as you zoom in the circle packing function would have to run repeatedly to update the circle positions. Using some existing examples on Observable I was able to accomplish this outcome. However, the map also includes a brush, to allow the user to filter on particular date ranges.

While, the brush seemed to work at updating the circle positions, without having to reload the basemap, I found that when I zoomed, after a few different data ranges had been selected, the circles would flicker and briefly showed the positions of previously selected date ranges.

I tracked it down to the output of the brush, which seemed to update correctly when used, but when zooming on the map, and printing the brush values to the console, I could see the old values being printed to the console.

It’s a bit hard to explain so I’ll post the actual notebook here, which tries to walk you though the problem.

When first loaded the zooming works well, but move the slider around a few times and then try zooming. I know there is a lot going on here! I’ve run this notebook through the Observable notebook visualizer and there didn’t seem to be any weird dependancies going on…

Thanks!

2 Likes

Here’s a forked version that seems to run more smoothly after adjusting the brush, though it doesn’t seem to help the flicker. I actually accomplished this by forking the Brush Filter X notebook and adjusting that code so that the brush only updates on release. This could clearly be improved since I doubt that it eliminates the leak; rather, it just limits it to one leak per adjustment. It looks to me like you’re using invalidation correctly so I don’t get that.

This is a great question, by the way, I’d love to see it more properly resolved.

1 Like

Nice that it only updates on release but yeah the flickering seems just as bad. Thanks for looking it over. Hopefully there’s someone who can help resolve this.
Cheers

Without looking at the notebook in more detail, I would suspect one of the following causes:

  • You’re recreating the WebGL context on each change. In this case you would have to at least ensure that the previous instance is destroyed to free up its resources. A good indicator for this are warning about too many WebGL contexts.
  • You’re adding layers / elements instead of updating, so that the amount of things that needs to be drawn increases with every change.

I don’t know. I have not seen any errors about too many WebGl contexts. To update the layers I’m using setData and then remove the layer before I add the new one. It works just fine if the brush doesn’t change the dates filtered on, but once I have a new date range to filter on, somehow the previous dates are being stored somewhere… The updateLayer function still seems to be removing and adding layers as I zoom.

I should be clear that, as far as I can see, the reason it’s flickering is because that on zoom, a previous data range filters the data differently, resulting in circles appearing in a different spot for an instant.

Hey @mbrownshoes !

First of all, that is a stunning map you made there! :tada:

The reason your map flickers is that the zoom/viewreset/move event listeners are added to the map with every input to the brush input view. That’s happening because the event listeners are defined in the cell that also defines the updateLayer function. By (the current) design, the updateLayer cell gets updated with every input to the brush view, and that’s why more and more event listeners are attached until the map starts flickering.

In practice, if you scrubbed the brush 50 times, a single zoom event will cause your updateLayer function to be run 50 times as well (yielding the exact same result with every iteration though).

There’s a simple hack you can use to circumvent this: If you add a mutable cell like the following to your notebook

mutable countReEvaluation = -1

and adjust the updateLayer cell in the following manner

{
  mutable countReEvaluation++;
  const updateLayer = () => {
    // ...
  }
  if (mutable countReEvaluation <= 0) {
    map.on("viewreset", updateLayer);
    map.on("move", updateLayer);
    map.on("moveend", updateLayer);
  }

The event listeners are only added to the map once!

While the above might be a simple hack that doesn’t require much refactoring, I would generally propose to use the “standard” approach for listening to input events non-reactively within a cell

viewof yourInput.addEventListener('input', () => {
      const curValue = viewof yourInput.value;
}

Here’s a simple implementation of that event listener from your Bear Tracker notebook :blush:

4 Likes

Nice analysis @chrispahm! I updated my fork to implement the simple hack and it works well. In addition, there’s no longer any need to use the brush filter that only updates on release so I reverted to the original brush.

1 Like

Thanks so much for your reply, and the detailed explanation of the issue. I’ve tried implementing your hack approach first, and while it does solve the issue of the flickering, I’m finding that after scrub the brush and then zoom, the original circle positions appear on the map, rather than the ones that should appear for the brushed dates.

It seems this is due to the limits dates in the updateLayer function being set to the defaultExtent of the brush. You can see this with the mutable debugLimits not updating to the current limits dates when zooming. Am I correct in assuming that the limits dates in the `UpdateLayer’ function are frozen at the original dates provided?

I’ll need to update the data within the updateLayer function when I zoom, and I’m not sure how to do that with either approach you mentioned.

Here is an updated notebook implementing the hack approach. Filtering seems to work but when you zoom, the original circles show up…

1 Like

Okay, I think I’ve got it. I found this post very helpful

I made the updateLayer function an individual cell, and in another cell I put this:

{

  map.on("viewreset", updateLayer);
  map.on("move", updateLayer);
  map.on("moveend", updateLayer);
  invalidation.then(() => {
    map.off("viewreset", updateLayer);
    map.off("move", updateLayer);
    map.off("moveend", updateLayer);
  });
}

I had tried previously using map.off to remove the old listener but I only did it for mapend. I hadn’t realized that I needed to remove each of the viewreset, move and movened events.

Here is the updated notebook. Mapbox updates on zoom / Mathew Brown | Observable

Thanks @chrispahm and others for your help solving this problem! I was stumped!

2 Likes

Awesome! Sorry I missed out on that closure issue, but good to hear you could solve it!

1 Like

No worries, thanks for figuring out what the issue was!