18

Edit: I've reported this as a Chromium bug: https://bugs.chromium.org/p/chromium/issues/detail?id=668257

I'm creating a little canvas game in JS with enemies that can shoot. For testing, I created a flag, declared globally as let fancy = true;, to determine whether or not to use a "fancy" targeting algorithm. I made it so that pressing P will toggle this flag. My main function, frame, calls another function, autoShoot, five times per second. autoShoot uses the fancy flag.

Today, something strange started happening; I don't remember what change introduced it. Sometimes, when I press P, autoShoot acts like fancy didn't get toggled. I did some debugging and discovered that the new, toggled value is reflected inside frame, but in autoShoot, the value isn't updated. It happens intermittently, and sometimes the value in autoShoot will fix itself (without me having done anything).

I've reduced the code to the following, which still exhibits the problem for me. Try pressing P a bunch of times. For me, the two values get "out of sync" and display differently after pressing P just once or twice:

Screenshot after pressing P on my computer

(I'm running Chrome "Version 54.0.2840.99 m" on Windows 10.)

const canvas = document.getElementById("c");
const width = 0;
const height = 0;
const ctx = canvas.getContext("2d");
const ratio =1;// (window.devicePixelyRatio||1)/(ctxFOOOOOOOOFOOOOOOOOOFOOOOO||1);
canvas.width = width*ratio;
canvas.height = height*ratio;
canvas.style.width = width+"px";
canvas.style.height = height+"px";
ctx.scale(ratio, ratio);

function testSet(id, val) {
  console.log(id+": "+val);
  document.getElementById(id).innerText = val;
}


let fancy = true;
document.body.addEventListener("keydown", function(e) {
  if (e.keyCode == 80) {
    fancy = !fancy;
    console.log("Set fancy to: "+fancy);
  }
});

let bullets = Array(2000);
let lastTime = 0, shotTimer = 0;
function frame(time) {
  const dt = (time - lastTime)/1000;
  lastTime = time;
  
  if ((shotTimer -= dt) <= 0) {
    testSet("frame", fancy);
    autoShoot();
    shotTimer = 0.2;
  }
  for (let b of bullets) {}
  
  requestAnimationFrame(frame);
}
function autoShoot() {
  testSet("autoShoot", fancy);
}

requestAnimationFrame(frame);
<code>
  fancy (frame)     = <span id="frame"></span><br>
  fancy (autoShoot) = <span id="autoShoot"></span>
</code>
<canvas id="c"></canvas>

Playing around, here are some observations:

  • removing any of the following causes the issue to go away:
    • any line in the code at the top dealing with the canvas, even just the comment after const ratio
    • the empty for...of loop: for (let b of bullets) {}
    • changing let fancy = to var fancy = or just fancy =
    • putting the whole thing out of the global scope (by using IIFE, onload handler, or block scope)
  • Increasing the size of the bullets array increases the frequency that the issue occurs. I think it's because it makes frame take longer to execute; originally, bullets.length was only 20, but each loop iteration did some stuff to update the bullet, etc.

Does this happen on your computers? Is there any logical explanation for this? I've tried restarting my browser, no change.

qxz
  • 3,814
  • 1
  • 14
  • 29
  • 2
    nope, never gets out of synch in firefox – Jaromanda X Nov 22 '16 at 01:22
  • The question is a little complicated... it helps if you provide a simpler full example that reproduces the problem – Elias Soares Nov 22 '16 at 01:22
  • I've actually trimmed that down to a minimal example that still produces the behavior. Removing anything else makes it go away :P – qxz Nov 22 '16 at 01:23
  • 1
    oddly, in chrum, `fancy (autoShoot)` is **always** true – Jaromanda X Nov 22 '16 at 01:25
  • 2
    Rock solid for me in [this fiddle](https://jsfiddle.net/db4bnLd6/), in both Firefox and Chrome. – Pointy Nov 22 '16 at 01:25
  • The purpose of *let* is to provide variable scope. If you need a global, just use *var* – Tim Grant Nov 22 '16 at 01:26
  • 2
    I can reproduce. `autoShoot` takes some time to follow the update. Weird. – Bergi Nov 22 '16 at 01:27
  • @Pointy That fiddle works for me too, but the snippet on SO doesn't. Weird. – qxz Nov 22 '16 at 01:29
  • @TimGrant Thanks, I'll just change my globals to `var` in the meantime... – qxz Nov 22 '16 at 01:30
  • Well personally I have come to be highly suspect of Chrome's `console` implementation, so I'm ready to blame it for just about anything. – Pointy Nov 22 '16 at 01:31
  • 1
    The glitch shows up in the DOM updates too, though, and obviously it was messing with my game logic before I put any `log`s in – qxz Nov 22 '16 at 01:31
  • It works for me when I comment out `for (let b of bullets) {}`. – hughes Nov 22 '16 at 01:45
  • @hughes Same, but why? – qxz Nov 22 '16 at 01:46
  • 1
    The difference between here and there is jsfiddle wraps the JS in a `window.onload=function(){...}`, which would explain why chrome gets out of sync. Try adding it inside an onload like jsfiddle does to get it working. – Ultimater Nov 22 '16 at 01:47
  • No idea yet, but it also works for me when `bullets` has length 99 or less – hughes Nov 22 '16 at 01:49
  • 2
    @Ultimater I'm no longer worried about getting it working; I just swapped `let` for `var`. Now I'm just curious as to why the heck this is happening. – qxz Nov 22 '16 at 01:50
  • It appears that Chrome only wants to read upto a certain amount of bytes of the code, comment or not, before it starts acting up and making them go out of sync. After playing with it for a while, I'm fairly confident this is a Chrome bug. No idea why though... – Ultimater Nov 22 '16 at 02:20
  • There a known scoping issues if variables in the global scope are named the same as an `id` of a DOM element. Does it still happen if you rename your functions? (It still wouldn't explain why there's a difference between `frame` and `autoShoot` though). – Bergi Nov 22 '16 at 15:47
  • @Bergi In the original code, the only DOM element with an `id` is the ``, and renaming the functions doesn't change anything. – qxz Nov 22 '16 at 16:23
  • @qxz Yeah, that's what I guessed, just wanted to be sure – Bergi Nov 22 '16 at 16:24
  • @Bergi Out of curiosity though, do you have a link/reference about that bug? – qxz Nov 22 '16 at 16:29
  • So it seems you found a curious Chrome bug. Report it to https://bugs.chromium.org/p/chromium/issues/ if you want it to be fixed. – Oriol Nov 22 '16 at 19:23
  • 2
    Reported: https://bugs.chromium.org/p/chromium/issues/detail?id=668257 – qxz Nov 23 '16 at 21:01
  • Without changing any code, the occurence of the bug toggles at 1024 chars/bytes for me. But I guess it's coincidence ;) – Martin Schneider Nov 24 '16 at 22:36

2 Answers2

2

As everybody commented out it seems to be a Chrome issue.

I've tried to reproduce the same issue on Chrome version 45.0.2454.85 m (64-bit) and 44.0.2403.107 m (32-bit) (enabling strict mode of course) but I've not succeded. But on version 54.0.2840.99 m (64-bit) it is there.

And I noticed that changing requestAnimationFrame to something like setInterval also makes the problem totally go away.

So, I assume that this strange behaviour has something to do with Chrome's requestAnimationFrame on newer versions, and maybe block scoping nature of let, and function hoisting.

I can't say from which version of Chrome we can see this kind of "bug", but I can assume that it can be the version 52, because in this version many changes occured, like new method of Garbage collection, native support for es6 and es7 etc. For more information you can watch this video from I/O 2016.

Maybe, new Garbage collection method is causing this issue, because as they told in the above mentioned video it is connected with browser frames, something like v8 does GC when the browser is idle, in order not to touch the drawing of frames etc. And as we know that requestAnimationFrame is a method which calls a callback on the next frame drawing, maybe in this process these weird thing's happening. But this is just an assumption and I've no competence to say something serious about this:)

sehrob
  • 1,034
  • 12
  • 24
  • 1
    There shouldn't be any garbage produced by the script in the question, everything can be statically allocated (or on the stack), so nothing to collect. – Bergi Nov 22 '16 at 19:12
0

I'm on Chrome 54.0.2840.98 on Mac and it happens too. I think it's a scoping issue, because if I wrap the declaration following the let statement into a {…} block, then the snippet works fine and both values change immediately after key press.

const canvas = document.getElementById("c");
const width = 0;
const height = 0;
const ctx = canvas.getContext("2d");
const ratio =1;// (window.devicePixelyRatio||1)/(ctxFOOOOOOOOFOOOOOOOOOFOOOOO||1);
canvas.width = width*ratio;
canvas.height = height*ratio;
canvas.style.width = width+"px";
canvas.style.height = height+"px";
ctx.scale(ratio, ratio);

function testSet(id, val) {
  console.log(id+": "+val);
  document.getElementById(id).innerText = val;
}


let fancy = true;
{
  document.body.addEventListener("keydown", function(e) {
    if (e.keyCode == 80) {
      fancy = !fancy;
      console.log("Set fancy to: "+fancy);
    }
  });

  let bullets = Array(2000);
  let lastTime = 0, shotTimer = 0;
  function frame(time) {
    const dt = (time - lastTime)/1000;
    lastTime = time;
  
    if ((shotTimer -= dt) <= 0) {
      testSet("frame", fancy);
      autoShoot();
      shotTimer = 0.2;
    }
    for (let b of bullets) {}
  
    requestAnimationFrame(frame);
  }
  function autoShoot() {
    testSet("autoShoot", fancy);
  }

  requestAnimationFrame(frame);
}
<code>
  fancy (frame)     = <span id="frame"></span><br>
  fancy (autoShoot) = <span id="autoShoot"></span>
</code>
<canvas id="c"></canvas>
RWAM
  • 6,760
  • 3
  • 32
  • 45
  • Why not put the `let fancy` inside the block scope as well? – Bergi Nov 22 '16 at 15:50
  • What exactly do you mean by "*Without the block scope it's limited to the keydown listener only.*"? What is limited? – Bergi Nov 22 '16 at 15:51
  • @Bergi good questions ;) I'm not an expert in ES6, but I know the `let` statement in relation with block scopes. And my approach was based on this, so I play with it. – RWAM Nov 22 '16 at 16:05
  • 2
    Yeah, the OP did find various solutions that avoid the problem while playing around as well, but what he is asking for is an explanation of the observed behaviour. – Bergi Nov 22 '16 at 16:13
  • Even though this answer doesn't explain it, it is interesting that the problem goes away even with the `let` still in global scope... – qxz Nov 22 '16 at 16:27