Why is a Safari page breaking iOS rendering? - javascript

I know the title is not that explanatory but here is the story: I am developing a browser game, mostly using JavaScript and the Mapbox library.
Everything works well on desktop, Android and iOS but one problem appears on iOS: after letting the game run for a few minutes the phone suddenly begins to have graphic artifacts and display most of the text scrambled.
Here are some photos of what the phone begins too look like:
My question is: what exactly in my code can cause this? A memory leak? (LE: it turned out to actually be a memory leak)
The real question is: How comes that you can almost brick the entire phone by simply browsing a web page? Shouldn't Safari stop this, or at least the iOS ?
This is not a problem with this specific device, as this problem can be reproduced on different iPhone devices. (I'm not so sure about different iOS versions).
How I can reproduce the error:
Open the game (inside Safari).
Let it run for 3-4 minutes.
Slide down the notification center and everything goes crazy.
I have added a YouTube video showing how I can reproduce the error (on my iPhone 5C).
It seems that the issue first appears in the notification center (if you swipe down the menu from the top).
As for now, this problem seems to only occur on iPhone 5C iOS 9.2.1 (13D15). It also occurs on the new iOS 9.3 version.
In order to fix this issue I have to:
Close the Safari application (in which the game tab is open).
Lock the phone. After unlocking it everything is back to normal.
Some details about the game itself:
The game shows a Mapbox map and some units over it (markers).
A Node.js server runs at 1 tick/second and after each tick the updated game state is sent to the browser through Socket.io.
Every time the browser receives the game state it updates the markers accordingly.
*The game might also update markers if you zoom in or out or if you select them.
EDIT2:
Found the memory leak (as expected). After fixing this leak (check for undefined _icon) the issue no longer occurs. This means, that somewhere along those lines the Safari/iOS bug is triggered.
Here is what exactly was being called each tick, for each unit that was clustered (was hidden and grouped with others inside a MarkerCluster):
var $icon = $(marker._icon); // marker._icon is undefined because of the clustering
$icon.html('');
$icon.append($('<img class="markerIcon" src="' + options.iconUrl + '" />'));
var iconX = 10;
var iconY = -10;
var iconOffset = 0;
for(var v in this.icons) {
this.icons[v].css('z-index', + $icon.css('z-index') + 1);
this.icons[v].css('transform', 'translate3d(' + iconX + 'px,'
+ (iconY + iconOffset) + 'px,' + '0px)');
iconOffset += 20;
this.icons[v].appendTo($icon);
}
// Fire rate icons
this.attackRateCircle = $('<div class="circle"></div>');
this.attackRateCircle.circleProgress({
value: 0,
size: 16,
fill: { color: "#b5deff" },
emptyFill: 'rgba(0, 0, 0, 0.5)',
startAngle: -Math.PI / 2,
thickness: 4,
animation: false,
});
this.attackRateCircle.hide();
// Create and display the healthbar
this.healthBar = $('<div>').addClass('healthBar ');
this.healthBar.css('z-index', $icon.css('z-index'));
this.healthBarFill = $('<span class="fill">');
this.healthBar.append(this.healthBarFill);
$icon.append(this.healthBar);
$icon.append(this.attackRateCircle);
And this is the icons array:
this.icons = {
attack_order: $('<img src="img/attack.png" class="status_icon">'),
attack: $('<img src="img/damage.png" class="status_icon icon_damage">'),
hit: $('<img src="img/hit.png" class="status_icon icon_hit">'),
};
circleProgress call is from this library: https://github.com/kottenator/jquery-circle-progress
DEMO
Yay, I have been able to create a jsFiddle that reproduces the bug: https://jsfiddle.net/cte55cz7/14/
Open on Safari on iPhone 5C and wait a couple of minutes. On iPhone 6 and iPad mini the page crashes (as expected due to the memory leak)
Here's the same code in a HasteBin, for anyone who doesn't want to run it.

This memory leaks is probably due to how 'WebKit’s JS Engine' works [safari webkit-javascript llvm]
and really looks like to be a virtual memory buffer-overflow, having a direct impact on the remaining RAM (shared and used also by iOS to store User Interface graphical elements)
Relatively to the piece of code:
"[...]finding jQuery memory leaks is easy. Check the size of $.cache. If it’s too large, inspect it and see which entries stay and why. [...]" (http://javascript.info/tutorial/memory-leaks)
Let me expect that it is relative to this for loop :
for(var v in this.icons) {
this.icons[v].css('z-index', + $icon.css('z-index') + 1);
this.icons[v].css('transform', 'translate3d(' + iconX + 'px,'
+ (iconY + iconOffset) + 'px,' + '0px)');
iconOffset += 20;
this.icons[v].appendTo($icon);
}
Assuming inspection is done, and also assuming the fact that you find the entries, you may want to clean the data manually with removeData()
or you may use first $elem.detach() and then put $(elem).remove() in setTimeout.

Related

Checking canvas dimension limitations

Discovering the Web Audio Api I wanted to draw a waveform for sound files. Now, I am aware of the image dimension limitations in some browsers, and I tried to look them up, but they seem to be ever changing (or at least memory differences like Chrome Desktop vs Chrome Mobile).
I tried to look up how to test if an image, or a Canvas / 2D Context can be of a certain size with almost no success. However, when testing this thing in Firefox I did get an error in the console so I tried the following method:
wfWidth = source.buffer.duration*100;
document.getElementById("waveform").width = wfWidth;
wfCtx = document.getElementById("waveform").getContext("2d");
var success = false;
while(success == false)
{
try {
var temp = wfCtx.getImageData(0,0,10,10); // this produced an error further down the code, so I use this as my test case.
success = true;
} catch(err) {
wfWidth = wfWidth / 2;
document.getElementById("waveform").width = wfWidth;
wfCtx = document.getElementById("waveform").getContext("2d");
}
console.log(success);
}
This does seem to work in Firefox as the console first outputs false showing that the canvas is too big and then true after halving the width of the canvas and the waveform is shown.
However, in Google Chrome on Desktop the canvas seems to be of a certain size (as indicated by a scroll bar) but it is totally blank. When I right-click to save image, it is a 0 byte txt file. On Chrome Mobile (android) I get this little square sad face. Guess that method of checking doesn't work in Chrome.
What would be the best way to test if the canvas is, well, valid, and resize if it is not?

Is it possible to let chrome devtools give different values of battery level and charging?

I am debugging a javascript that i cannot modify and does different things according battery level and charging status on mobile devices.
I work with latest chrome version on android phone connected to a PC using an USB cable and chrome developer tools on PC.
My problem is that with this configuration with a script like this:
navigator.getBattery().then(function(battery) {
var level = battery.level;
console.log(battery);
});
i always have battery.charghing=true and battery.level=1 while i need to see the script behaviour using a lower battery level and battery.charging=false.
Is it possible to do it?
I cannot use debugger breakpoint because in that case the script has even different behaviour.
You can always hardcode your debug values or in this case create a complete copy of the original battery:
navigator.getBattery().then(function(battery) {
var level = battery.level;
console.log("Original battery:", battery);
//DEBUG START
battery = {
"charging": false,
"chargingTime": 100,
"dischargingTime": Infinity,
"level": 42,
};
//DEBUG END
console.log("Debug battery:", battery);
});

I can get audio.currentTime but not set it (in Google Chrome)

This is driving me crazy. Here is the code I use to set current time:
$("#audio").click(function(e) {
e.preventDefault();
mr_waveform_skip(e)
});
function mr_waveform_skip(event) {
clientX = event.clientX;
left = event.currentTarget.offsetLeft;
clickoffset = clientX - left;
percent = clickoffset/event.currentTarget.offsetWidth
audio_duration = audio_element.duration;
duration_seek = percent*audio_duration;
audio_element.currentTime = duration_seek;
// audio_element.currentTime = 10;
console.log('CLICK: ' + duration_seek + ' Element: ' + audio_element + ' CurrentTime: ' + audio_element.currentTime);
}
I cannot seem to set audio_element.currentTime only get it!
And worse, it works in fireFox! Chrome restarts at 0, no matter what.
This is what the above code produces in Firefox console:
CLICK: 63.82905432385121 Element: [object HTMLAudioElement] CurrentTime: 3.849546
And in Chrome:
CLICK: 63.82905432385121 Element: [object HTMLAudioElement] CurrentTime: 3.849546
See? The same one! We can see that Chromes sees the HTML audio element (since it can get the value). If I do audio_element.currentTime = 10; it still does not work (in Chrome, Firefox loyally restarts at 10)..
Your code works perfectly when I tested it with with an Ogg file from Wikipedia.. If that really is all of your code, then this problem appears to be caused by some kind of corruption or unexpected format in your media file.
You will not be able to fix this problem with code; you will need to produce a new media file that your browser can process correctly. Perhaps try using a different piece of audio software (or different settings) to produce or re-process the media file.
If the format isn't the problem:
I've bumped into this problem in Chrome a couple of times today. Wasted so much time trying to solve it. (Version 55.x)
The solution is to restart Chrome. After countless page refreshes the value is suddenly not set. It's probably some cache bug. I would stress that using a development server supporting range requests is also a good idea.
If I'm not running Node or Django I use : https://github.com/danvk/RangeHTTPServer

cytoscape.js "e.originalEvent.x" with different behavior on browsers

I'm trying to build a crossplatform tool with cytoscape.js, wich includes mobile devices, but I'm facing some problems when using the "tap" function to add a node:
cy.on('tap', function(e) {
if(e.cyTarget === cy) {
if($("input:radio[id=btnAddNode]")[0].checked) {
var idNum = cy.nodes().size(),
setID = idNum.toString(),
offset = $cy.offset(),
position = {
x: e.originalEvent.x - offset.left,
y: e.originalEvent.y - offset.top
};
console.log("e.originalEvent.x: " + e.originalEvent.x);
console.log("offset.left: " + offset.left);
console.log("e.originalEvent.y: " + e.originalEvent.y);
console.log("offset.top: " + offset.top);
cy.add([{
group: "nodes",
data: {
id: "n" + setID
},
renderedPosition: {
x: position.x,
y: position.y
},
}]);
} else {
//show node data
}
}
});
On Chrome for Windows 7 (v.32) and also on Safari it is working as expected.
On IE 9 it's inserting the node about 30px higher and 5px left than the point where we click (?!)
But on Firefox (v.26) and Safari for iOS it's not working, it put's the node on the top-left corner of the canvas. Firefox debug says that the "originalEvent.x" and "originalEvent.y" properties are undefined
"e.originalEvent.x: undefined"
"offset.left: 8"
"e.originalEvent.y: undefined"
"offset.top: 55.66667175292969"
So, is this property obsolete? If so, what should be the best way to improve this code?
Thanks in advance.
EDIT: Almost solved, noticed that using e.originalEvent.pageX and e.originalEvent.pageY instead works well over all desktop browsers and on Windows Phone 8, except on iOS :(
Though the events in Cytoscape.js are similar to jQuery events, they are not jQuery events. Properties like pageX are available through e.originalEvent, though they are not normalised to e. In the upcoming v2.1 (or if you make zip in the repo), events have been updated with e.cyPosition and e.cyRenderedPosition. These values respectively provide the model position and rendered (on screen, relative to cy.js instance) position.
Also note that I don't think your approach would work on iOS etc even using jQuery events, because touch events aren't normalised. e.cyPosition and e.cyRenderedPosition do support touch events, though.

OpenLayers latitude inaccurately captured in Webkit Mobile browsers

I am programming a page with a Map where I need to capture the location of the Tap/Click on a map and store the coordinates. I am using OpenLayers js. On desktop browsers (IE/FF/Chrome), this is working fine. On mobile devices, the tap is getting captured correctly on the default Android browser (both in real devices and emulators).
However on mobile webkit browsers (iPhone Safari & Android Chrome Beta), we are having a problem where the tap is getting captured for a few pixels higher (towards the north) of the actual tap. The error is not fixed (so, I can't just add 100 to the event's xy to recalibrate the top.)
Here is the code I am using as the clickhandler:
OpenLayers.Control.ClickHandler = OpenLayers.Class(OpenLayers.Control, {
defaultHandlerOptions: {
'single': true,
'double': false,
'pixelTolerance': 0,
'stopSingle': false,
'stopDouble': false
},
initialize: function(options) {
this.handlerOptions = OpenLayers.Util.extend(
{}, this.defaultHandlerOptions
);
OpenLayers.Control.prototype.initialize.apply(
this, arguments
);
this.handler = new OpenLayers.Handler.Click(
this, {
'click': this.trigger
}, this.handlerOptions
);
},
trigger: function(e) {
that.inputLonLat_EPSG4326 = null;
var lonlat = that.inputMap.getLonLatFromViewPortPx(e.xy);
that.logMessage("XY " + e.xy);
that.logMessage("LonLoat " + lonlat);
that.inputMarkers.clearMarkers();
that.inputMarkers.addMarker(new OpenLayers.Marker(lonlat,that.icons["green"].clone()));
lonlat.transform(that.inputMap.getProjectionObject(), new OpenLayers.Projection("EPSG:4326"));
that.inputLonLat_EPSG4326 = lonlat;
// For the moderation sections
$('#alertLatitude').val(that.inputLonLat_EPSG4326.lat);
$('#alertLongitude').val(that.inputLonLat_EPSG4326.lon);
//lonLat2.transform(that.inputMap.getProjectionObject(), new OpenLayers.Projection("EPSG:4326"));
that.logMessage("Stored lat " + that.inputLonLat_EPSG4326.lat);
that.logMessage("Stored lon " + that.inputLonLat_EPSG4326.lon);
that.callFunction(that.inputMapListener, e);
}
});
Should I be doing anything differently? Has anybody else seen the inaccuracy problem on mobile webkit browsers while using OpenLayers?
I finally found the reason this is happening. It seems that on webkit mobile browsers, the x,y that the library seems to be getting (or deriving) is based on the page and not on the element in which the map is housed. Hence the calculations are off. It seems that the inaccuracy can't be solved by adding the xy of the map element or some such DOM-based figure too (I tried it.)
I solved it by housing the Map in an IFrame and then have the iFrame housed within the map element. This way the x,y as received by the map click handler is accurate within the iFrame and hence the lat, long is also accurate. Since both the parent and iFrame are from the same domain, there are no issues communicating back and forth.
To complete the context, the iFrame based solution is definitely compatible with BB 9 & above, Android Chrome, Android Default and iPhone Safari, as tested by me.
Check out the solution at - http://suat1.vocanic.net//saralee/apps/dengue_alert/ and the iFrame at http://suat1.vocanic.net//saralee/apps/dengue_alert/mobileMap.php (WIP versions liable to change or break with time)

Categories

Resources