Jon Cage
Jon Cage

Reputation: 37500

How do I prevent my javascript locking up the browser?

I have a WebSocket that receives data at a rate of ~50hz. Each time an update is pushed to the browser, it turns the JSON data it's had published to it into some pretty charts.

$(document).ready(function() {
    console.log('Connecting web socket for status publishing')
    allRawDataPublisher = new ReconnectingWebSocket("ws://" + location.host + '/raw/allrawdata');

    var rawUnprocessedData = [];

    for (i = 0; i < 256; i++)
    {
        rawUnprocessedData.push({x:i, y:0});
    }

    var unprocessedRawChart = new CanvasJS.Chart("rawUnprocessedData",{
        title :{ text: "Raw Unprocessed Data"},
        axisX: { title: "Bin"},
        axisY: { title: "SNR"},
        data: [{ type: "line", dataPoints : rawUnprocessedData},{ type: "line", dataPoints : noiseFloorData}]
    });

    var updateChart = function (dps, newData, chart) {
        for (i = 0; i < 256; i++)
        {
          dps[i].y = newData[i];
        }
        chart.render();
    };

    allRawDataPublisher.onmessage = function (message) {
        jsonPayload = JSON.parse(message.data);
        var dataElements = jsonPayload["Raw Data Packet"]
        updateChart(rawUnprocessedData, dataElements["RAW_DATA"].Val, unprocessedRawChart)
    };

    unprocessedRawChart.render();
});

This works great when my laptop is plugged into a power socket but if I unplug the power, my laptop drops it's processing power (and the same issue occurs on lower-specc'd tablets, phones etc). When there's less processing power available, the browser (Chrome) completely locks up.

I'm guessing the javascript is receiving updates faster than the browser can render them and consequently locking the tab up.

If the browser is unable to update at the requested rate, I would like it to drop new data until it's ready to render a new update. Is there a standard way to check the browser has had enough time to render an update and drop new frames if that's not the case?

*[Edit]

I did some digging with Chrome's profiler which confirms that (as expected) it's re-drawing the chart that is taking the bulk of the processing power.

Upvotes: 3

Views: 248

Answers (1)

nicholaswmin
nicholaswmin

Reputation: 22959

You can do work between frames by using window.requestAnimationFrame.

The callback passed to this function will be called at a maximum of 60 times a second - or whichever number matches the refresh rate of your display.

It's also guaranteed to be called before the next repaint - and after the previous repaint has finished.

From MDN window.requestAnimationFrame()

The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint.

Here's an example on how it's used:

function renderChart () {
   // pull data from your array and do rendering here
   console.log('rendering...')
   requestAnimationFrame(renderChart)
}

requestAnimationFrame(renderChart)


However, it's better to render changes to your graph in batches, instead of doing rendering work for every single datum that comes through or on every frame.

Here's a Fiddle using Chart.js code that:

  • Pushes data to an Array every 100ms (10 Hz)
  • Renders data, in batches of 4 - every 1000ms (1 Hz)

const values = []
const ctx = document.getElementById('chartContainer').getContext('2d');
const chart = new Chart(ctx, {
  type: 'line',
  data: {
    labels: ['start'],
    datasets: [{
      label: 'mm of rain',
      data: [1],
      borderWidth: 1
    }]
  }
});

// Push 1 item every 100ms (10 Hz), simulates 
// your data coming through at a faster
// rate than you can render
setInterval(() => {
  values.push(Math.random())
}, 100)

// Pull last 4 items every 1 second (1 Hz)
setInterval(() => {
  // splice last 4 items, add them to the chart's data
  values.splice(values.length - 4, 4)
    .forEach((value, index) => {
      chart.data.labels.push(index)
      chart.data.datasets[0].data.push(value)
    })

  // finally, command the chart to update once!
  chart.update()
}, 1000)

Do note that the above concept needs to handle exceptions appropriately, otherwise the values Array will start accumulating so much data that the process runs out of memory.

You also have to be careful in the way you render your batches. If your value render rate is slower than the rate at which you fill the values Array, you will eventually run into memory issues.

Last but not least: I'm not really convinced you ever need to update a piece of data faster than 2 Hz, as I doubt the human brain can make useful interpretations at such a fast rate.

Upvotes: 2

Related Questions