Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throttle calls to "window.history.replaceState" to fix Mobile Safari error message #5613

Merged
merged 8 commits into from
Nov 15, 2017

Conversation

lucaswoj
Copy link
Contributor

@lucaswoj lucaswoj commented Nov 7, 2017

As of iOS 9.3, Mobile Safari throws an error if window.history.replaceState is called more than 100 times per 30 seconds. This PR throttles all calls to window.history.replaceState to avoid triggering this error message.

Benchmark scores: https://bl.ocks.org/anonymous/raw/9a02ed1c37970d9d6c697f667b56527a/

fixes #5612

Next Steps

  • briefly describe the changes in this PR
  • Fix failing unit tests
  • (Debug custom util.throttle & write tests for util.throttle) or find acceptable throttle implementation
  • Post benchmark scores
  • Debug moveend deboucing
  • manually test the debug page

Test Harness

<!DOCTYPE html>
<html>
<head>
    <title>Mapbox GL JS debug page</title>
    <meta charset='utf-8'>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <link rel='stylesheet' href='/dist/mapbox-gl.css' />
    <style>
        body { margin: 0; padding: 0; }
        html, body, #map { height: 100%; }
    </style>
</head>

<body>
<div id='map'></div>

<script src='/dist/mapbox-gl-dev.js'></script>
<script src='/debug/access_token_generated.js'></script>
<script>

var map = window.map = new mapboxgl.Map({
    container: 'map',
    zoom: 12.5,
    center: [-77.01866, 38.888],
    style: 'mapbox://styles/mapbox/streets-v10',
    hash: true
});

setInterval(() => {
    map.flyTo({
        bearing: map.getBearing() + 1
    });
}, 100)


</script>
</body>
</html>

Copy link
Member

@mourner mourner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, why is it called so often in the first place? _updateHash is called on moveend. If moveend has been called 100 times in 30 seconds, and given that moveend itself is debounced (called only once in case of many quick successive movements of the map), there might be a bug lurking there of which this is only a symptom.
Also, I'd prefer bringing back the little util.throttle we had before but removed at some point than including a part of lodash for the first time in the GL JS build.

@lucaswoj
Copy link
Contributor Author

lucaswoj commented Nov 8, 2017

I wonder, why is it called so often in the first place? _updateHash is called on moveend. If moveend has been called 100 times in 30 seconds, and given that moveend itself is debounced (called only once in case of many quick successive movements of the map), there might be a bug lurking there of which this is only a symptom.

The provided test harness demonstrates a realistic use case that would trigger the error message even if we revamp the panning moveend debouncing. I will investigate further to figure out why panning produces so many moveend events.

Also, I'd prefer bringing back the little util.throttle we had before but removed at some point than including a part of lodash for the first time in the GL JS build.

I experienced a bug in our util.throttle implementation (replaceState was still being invoked more often that it should've) which went away when I switched lodash.throttle. I couldn't find any tests for util.throttle.

I will put more time into debugging util.throttle but we may want to consider a not-invented-here solution.

@lucaswoj
Copy link
Contributor Author

lucaswoj commented Nov 8, 2017

I must confess I'm disappointed by the size of lodash.throttle: 16 KB, 430 LOC (unminified and with comments).

@lucaswoj
Copy link
Contributor Author

lucaswoj commented Nov 9, 2017

I wonder, why is it called so often in the first place? _updateHash is called on moveend. If moveend has been called 100 times in 30 seconds, and given that moveend itself is debounced (called only once in case of many quick successive movements of the map), there might be a bug lurking there of which this is only a symptom.

The problem is that DragPanHandler calls Camera#panBy which calls Camera#panTo which calls Camera#easeTo which calls Camera#fire('moveend', ...) in an unthrottled way. My view is that the real root issue here is #3357.

unthrottledFunction();
} else if (!pending) {
pending = true;
// eslint-disable-next-line no-use-before-define
Copy link
Member

@mourner mourner Nov 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ESLint docs on the rule state that:

In ES6, block-level bindings (let and const) introduce a “temporal dead zone” where a ReferenceError will be thrown with any attempt to access the variable before its declaration.

So if this code weren't transpiled (which may happen in future) it would throw an error in theory. So perhaps let's rewrite to either use function declaration instead of arrows, or declare one of the vars at the top with let?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoa! I've been out in the woods too long. Fixed in 20f3fe0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

"history.replaceState" error on Mobile Safari
2 participants