art with code

2009-11-14

Optimizing a small JavaScript loop

I recently had to optimize a visibility-checking loop, as it was causing some slowness when dealing with several hundreds of images. Executive summary: profile with Firebug, cache regex results and unchanging DOM properties.

I started with a loop like this:

imgs.forEach(function(e) {
if (isVisible(e)) {
if (e.src.match(/\/transparent\.gif$/)) {
e.src = e.getAttribute('origsrc');
}
} else if (!e.src.match(/\/transparent\.gif$/)) {
e.src = '/transparent.gif';
}
});

isVisible = function(e) {
var ctop = window.scrollY;
var cbottom = ctop + window.innerHeight;
var etop = elem.offsetTop;
var ebottom = etop + elem.offsetHeight;
var top = Math.max(ctop, etop);
var bottom = Math.min(cbottom, ebottom);
return (bottom - top) > -1200;
}

Runtime for 400 images: 35ms. Gah!

Profile says that a lot of time is spent in isVisible. But there's really nothing there that looks expensive. Let's try caching window.scrollY and window.innerHeight anyhow.

var wsy = window.scrollY;
var wih = window.innerHeight;

imgs.forEach(function(e) {
if (isVisible(e, wsy, wih)) {
...

isVisible = function(e, wsy, wih) {
var ctop = wsy;
var cbottom = ctop + wih;
...

Runtime came down to 23 ms. Whoah, that's a third off the execution time. Maybe property access is super expensive, let's see if caching elem.offsetTop and elem.offsetHeight would help as well.

isVisible = function(e, wsy, wih) {
...
if (e.cachedTop == null || !e.complete) {
e.cachedTop = e.offsetTop;
e.cachedBottom = e.cachedTop + e.offsetHeight;
}
var etop = e.cachedTop
var ebottom = e.cachedBottom
...
}

Runtime down to 15 ms. Excellent.

Profile says that the forEach loop is using a lot of time. Maybe it's the regexes. How about using RegExp.test instead of match. The docs say it should be faster.

if ((/\/transparent\.gif$/).test(e.src)) {
...
} else if (!(/\/transparent\.gif$/).test(e.src)) {


Runtime down to 12 ms. RegExp.test is indeed faster than String.match. Next, we could do away with the regexp altogether as we control which images are transparent and which are not...


imgs.forEach(function(e) {
if (e.trans == null)
e.trans = (/\/transparent\.gif$/).test(e.src);
if (isVisible(e, wsy, wih)) {
if (e.trans) {
e.src = e.getAttribute('origsrc');
e.trans = false;
e.cachedTop = e.cachedBottom = null;
}
} else if (!e.trans) {
e.src = '/transparent.gif';
e.trans = true;
e.cachedTop = e.cachedBottom = null;
}
});

Okay, now we're down to 5 ms. Finally, let's try replacing the forEach-loop with a plain for-loop.

for (var i=0; i<imgs.length; i++) {
var e = imgs[i];
...
}

Down to 3 ms. Not all that much in absolute terms, but relative to 5 ms it's 40% less. I wonder if CRAZY MICRO-OPTIMIZATIONS would do anything.

for (var i=0, l=imgs.length, e=imgs[0]; i<l; e=imgs[++i]) {
...
}

Down to ... 3 ms. No change. Well, it might be 0.3 ms faster but my measurements have a larger error than that. Ditch the micro-optimizations for readability.

And 'lo! The final code that is 10x faster than what we started with:

var wsy = window.scrollY;
var wih = window.innerHeight;

for (var i=0; i<imgs.length; i++) {
var e = imgs[i];
if (e.trans == null)
e.trans = (/\/transparent\.gif$/).test(e.src);
if (isVisible(e, wsy, wih)) {
if (e.trans) {
e.src = e.getAttribute('origsrc');
e.trans = false;
e.cachedTop = e.cachedBottom = null;
}
} else if (!e.trans) {
e.src = '/transparent.gif';
e.trans = true;
e.cachedTop = e.cachedBottom = null;
}
}

isVisible = function(e, wsy, wih) {
var ctop = wsy;
var cbottom = ctop + wih;
if (e.cachedTop == null || !e.complete) {
e.cachedTop = e.offsetTop;
e.cachedBottom = e.cachedTop + e.offsetHeight;
}
var etop = e.cachedTop
var ebottom = e.cachedBottom
var top = Math.max(ctop, etop);
var bottom = Math.min(cbottom, ebottom);
return (bottom - top) > -1200;
}

Does it work right? It seems to, at least. So maybe! Or maybe not! Isn't caching fun?

[Edit] The offsetTop and offsetHeight caching didn't work on my case, so I had to remove them. Final runtime 11 ms, or only 3x faster. Maybe some day I figure out why it didn't work...

[Edit] People have suggested using var l = imgs.length; while (l--) { ... }, which not only looks odd and reverses the iteration order, but also generates the same code as a for-loop would (on WebKit at least), according to Oliver Hunt. Well, the codegen test is for var i=0; while (i < imgs.length) i++; but i assume it also applies for for (var l=imgs.length; l--;).

[Edit] It's also somewhat strange to see suggestions that focus on optimizing all these weird things like "use object properties directly instead of local variables", instead of the possibly big things: inline isVisible, replace Math.max and Math.min with a > b ? a : b and a < b ? a : b. Well. I'll try them all and post a follow-up. Stay tuned!

[Edit] Follow-up.

2009-11-11

Canvas-generated hexa background


Wrote that for the new fancy Metatunnel page (while procrastinating on writing an essay...) Here's the JS to boggle over:

function polygon(n) {
var a = [];
for (var i=0; i<n; i++)
a.push({x:Math.cos(2*Math.PI*(i/n)), y:Math.sin(2*Math.PI*(i/n))});
return a;
}
function id(i){ return i }
function lineTo(ctx){return function (p){ ctx.lineTo(p.x, p.y) }}
function linePath(ctx, path){ path.map(lineTo(ctx)) }
function drawPath(ctx, path, close){
ctx.beginPath(); linePath(ctx, path); if (close) ctx.closePath() }
function strokePath(ctx, path, close){ drawPath(ctx, path, true); ctx.stroke() }
function fillPath(ctx, path){ drawPath(ctx, path); ctx.fill() }
function scale(x,y,p){return {x:p.x*x, y:p.y*y}}
function scalePoint(x,y){return function(p){ return scale(x,y,p) }}
function translate(x,y,p){return {x:p.x+x, y:p.y+y}}
function translatePoint(x,y){return function(p){ return translate(x,y,p) }}
function scalePath(x,y,path){ return path.map(scalePoint(x,y)) }
function translatePath(x,y,path){ return path.map(translatePoint(x,y)) }
function clonePath(path){ return path.map(id) }

function wavePoint(p) {
var np = {
x: (p.x + 1.5*Math.cos((p.x+p.y) / 15)),
y: (p.y + 1.5*Math.sin((p.x+p.y) / 15))
}
var d = Math.sqrt(np.x*np.x + np.y*np.y);
var s = Math.pow(1/(d+1),0.7);
return translate(5, 24, scale(s*30, s*30, np));
}
function wavePath(path){ return path.map(wavePoint) }
function drawbg() {
var canvas = document.createElement('canvas');
canvas.width = 1400;
canvas.height = 700;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#333';
ctx.fillRect(0,0,canvas.width,canvas.height);
var g = ctx.createRadialGradient(50,50,0, 50,50,1400);
g.addColorStop(0,'rgba(0,192,255,1)');
g.addColorStop(1.0,'rgba(255,0,255,1)');
ctx.fillStyle = g;
var hex = polygon(6);
var s = Math.sin(Math.PI/3);
var c = Math.cos(Math.PI/3);
for (var i=-2; i<120; i++) {
for (var j=0; j<8; j++) {
if (Math.random()+Math.random() < Math.pow((Math.sqrt(i*i+j*j) / 55),2)) continue;
var x = i*(1.2+c);
var y = j*(2.4*s) + (i%2 ? 1.2*s : 0);
fillPath(ctx, scalePath(10, 7, wavePath(translatePath(x,y,hex))));
}
}
document.body.style.background = '#333 url('+canvas.toDataURL()+') no-repeat top left';
}

2009-11-10

Metatunnel WebGL now works on WebKit as well


Made the Metatunnel demo WebGL port work on WebKit as well, tested to work with the latest Chromium Linux build.

2009-11-07

Done with canvas3d-tests WebGL port

As far as I know, the canvas3d-tests are now all ported to WebGL. However, I'm using a mix of GLSL 1.20 and 1.30 in the tests, which probably isn't a good idea. Porting all shaders to 1.20 or whatever GLSL ES uses as its base would be a good next step.

Hmm, I have an apigen in the git repo that generates GL constant -checking C++ wrappers for OpenGL functions. That could be hacked a bit to make it create better autotests.

The biggest issue with the current tests is that they only cover a small portion of the API, as I focused on writing only evil tests manually. The rest of the API gets banged by the API fuzzer to see if anything crashes. Evil tests then are me trying to cause a segfault or read back information I shouldn't be able to, and for general array indexing validation. I wrote them for things that do array access, read back data from OpenGL, or that seem iffy in some other way.

So. TODO: Port shaders to 1.20, better autotests, file bugs.

2009-11-05

Started porting Canvas3D tests to WebGL

I started porting Canvas3D tests over to WebGL this morning (apparently insomnia helps.) I've been intending to do that since the summer, but never really got started. Better late than never, eh.

I've been testing the Firefox implementation. If there's a way to test WebKit's WebGL on Linux, I'd be most interested in hearing that.

The Firefox implementation has changed somewhat from the last time I worked on it, so I found some new bugs too :) BindFramebuffer is checking its argument against wrong constants, Tex[Sub]Image2D is quite broken for non-image arguments and some functions don't bork on bad data. Plus a fuzzer segfault and an on-exit segfault (didn't trace those yet.)

I still have around half the existing tests to port... Something to do this afternoon.

2009-11-04

Visitor stats for October

About 1250 visits.

Browser market share: 50% Firefox, 15% Explorer, 14% Chrome, 12% Safari and 6% Opera.

OS market share: 56% Windows, 22% Linux, 20% Mac, 1% iPhone.

The smallest screen resolution was 170x180. The largest was 3840x1024. Most people had something between 800x480 and 1920x1200.

Visitors by continent: 44% Europe, 33% North America, 12% Asia, 6% South America, 3% Oceania, 1% Africa.

By country: 28% US, 7% France, 7% Germany, 5% UK, 5% Canada, 4% Brazil, 3% Japan, 3% Finland, 3% Spain, 3% India.

Hmm...

2009-10-30

Hook Champ tips


Played way too much Hook Champ last week. It's a racing game disguised as a grappling hook platformer. Made by some friends of mine. So let me tell you how to get decent scores.

The fast way


First off, buy all the equipment upgrades. Then, find all the treasures in the map. A coin is worth two seconds. A jewel is worth ten seconds. If you see a stalactite hanging from the roof, that's a sign that you should go down there. Check out suspicious chimneys with rocket boots.

  1. Don't hit the walls - they kill your horizontal speed. Sometimes you can't avoid it though, but those places are mostly long drops and chimneys.
  2. Use the shotgun to accelerate - good places for that are at the start of the level and after upwards chimneys. It doesn't help as much in downwards chimneys because you can convert the downward motion to forward motion.
  3. Don't hit the floor when swinging, it slows you down.
  4. Use the rocket boots instead of trampolines, that way you don't have to go down to touch the trampoline. In multi-rocket jumps, don't wait for your upwards motion to stop before rocketing again.
  5. Don't hit the ceiling when swinging, it slows you down.
  6. Don't swing too high, it slows you down. In straight corridors, release at around the time the rope points straight down.

When you are breaking through walls, you want to hit between two blocks. If you hit only one block, you'll likely stand up on the block below and that stops your motion.

If you manage to get a coin to follow you, you can use it to see what is fast and what is slow. If the coin catches up to you, you went slow. If you outrun the coin, you're going at a good speed.

Zummm, that is all. Been working on our crazy web app project this week, to the detriment of other projects. Such is life.

Blog Archive