Why does this Javascript sketch lag compared to the Java sketch?

So, I ported a Java Processing sketch to p5.js. The sketches should be simple to run - just basic image rasterization. I tried my best to optimize the JS code but have been unsuccessful at getting any decent performance out of it (max framerate reached is under 10). In comparison, the Java code runs flawlessly even though it is unoptimized. Can someone tell me why?

EDIT: Could someone confirm that the JS code does lag for them too?

p5.js

// OPTIMISATIONS
 p5.disableFriendlyErrors = true;

// SETUP VARIABLES
var CANVAS_SIZE = 900;
var MAX_FRAME_RATE = 60;
var paused = false;

// GLOBAL VARIABLES
var TEXT_SIZE = 20;
var tiles = 80;
var PD;
var origin_x;
var origin_y;
var img;
var swansea;

const scale = (value, x1, y1, x2, y2) => (value - x1) * (y2 - x2) / (y1 - x1) + x2;

function preload() {
    /*
    The image will only load if a server
    is hosted, else you'll get a 
    'Cross-Origin Request Blocked' error.
    Simplest way to solve this is to use 
    the Live Server plugin for VSCode.
    */
    img = loadImage('data/backgroundless.png');
    img.loadPixels();
    swansea = loadFont('data/Swansea-q3pd.ttf');
}

function setup() {
    createCanvas(CANVAS_SIZE, CANVAS_SIZE, WEBGL);
    frameRate(MAX_FRAME_RATE);
    smooth();

    PD = pixelDensity();
    origin_x = -width/2;
    origin_y = -height/2;

    img.resize(CANVAS_SIZE, CANVAS_SIZE);

    textFont(swansea);
    textSize(TEXT_SIZE);
}

function draw() {
    /*
    Using WEBGL moves the origin from the 
    top left of the Canvas to its center 
    so we must translate every drawing to
    the top left, i.e. (-width/2, -height/2).
    */
    translate(origin_x, origin_y);

    background('#f1f1f1');

    var tile_size = width/tiles;

    push();
    translate(-origin_x, -origin_y);
    rotateY(radians(frameCount));

    push();
    noStroke();
    for (let i=0; i<tiles; i++) {
        for (let j=0; j<tiles; j++) {
            let x = Math.floor(i * tile_size);
            let y = Math.floor(j * tile_size);

            let idx = (x + y * width) * PD * 4;

            let c = color(
                img.pixels[idx],
                img.pixels[idx+1],
                img.pixels[idx+2],
                img.pixels[idx+3]
            );

            if (img.pixels[idx]==0 && img.pixels[idx+1]==0 && img.pixels[idx+2]==0) {
                continue;
            }

            let b = 1-brightness(c)/255;

            let z = scale(b, 0, 1, -100, 100);

            push();
            translate(origin_x+x, origin_y+y, z);
            fill(c);
            sphere(tile_size*b*0.8, 3, 3);
            pop();
        }
    }
    pop();
    pop();

    // Print framerate on the screen.
    let fps = frameRate();
    fill(0);
    stroke(0);
    text("FPS: " + fps.toFixed(2), 0, TEXT_SIZE);
}

function mousePressed() {
    // Pauses the draw() function to reduce 
    // computation overhead when not needed.
    paused ? loop() : noLoop();
    paused = !paused;
}

Processing Java

PImage img;

void setup() {
  size(900, 900, P3D);
  img = loadImage("data/backgroundless.png");
  img.resize(900, 900);
}

void draw() {
  background(#f1f1f1);
  noStroke();
  sphereDetail(3);
  float tiles = 100;
  float tileSize = width/tiles;
  push();
  translate(width/2,height/2);
  rotateY(radians(frameCount));
  
  for (int x = 0; x < tiles; x++) {
    for (int y = 0; y < tiles; y++) {
      color c = img.get(int(x*tileSize),int(y*tileSize));
      float b = map(brightness(c),0,255,1,0);
      float z = map(b,0,1,-150,150);
      
      if (red(c)==0 && green(c)==0 && blue(c)==0) {
          continue;
      }
      
      push();
      translate(x*tileSize - width/2, y*tileSize - height/2, z);
      fill(c);
      sphere(tileSize*b*0.8);
      pop();
      
    }
  }
  pop();
}

You may use this image as the file backgroundless.png;

Hello and welcome @the0ne,

I get bad performance as well on my MacBook Air (2018) running Safari. About 11 fps. I can’t tell you why, for sure, without profiling and doing an in-depth review of both versions. But, even with the fast and highly optimized JavaScript engines in today’s browsers, it’s not surprising to see a performance hit when going from Java to JavaScript. Implementation differences between Processing and p5.js could also be part of the explanation. Hard to tell without doing a deep dive…

That said, there’s some optimization to be done to the p5.js version of the sketch. draw is called 60 times per second, so you’ll want to do as little work as possible there.

Variables like x, y, and c for each sphere is calculated repeatedly, but the result will always be the same. Determining them once (in setup) got me from ~11 fps to ~20 fps.

2 Likes

Thank you for replying to my post. Now that I know that script lags universally and it isn’t just a local issue, I’m going to investigate further into how I can optimize it further.

If I understand correctly, you’ve made arranged the spheres in advance and just rotate the arrangement in the draw function. Could you please share your code for the same?

I’m sorry, I didn’t save my modifications. But the idea is to make the calculations needed for every sphere once and save the result to an array. Then, you can loop over the cached values during draw:

cachedTileValues.forEach(function(tileValues) {
  push();
  translate(tileValues.x, tileValues.y, tileValues.z);
  fill(tileValues.c);
  // The following calculation could also be cached.
  sphere(tileValues.size * tileValues.b * 0.8)
  pop();
});

The above is untested code and may contain errors. Think of it as pseudocode – just an example. Now, all you have to do is figure out how to populate tileValues with the correct values during setup.

2 Likes