Finn Eggers
Finn Eggers

Reputation: 945

JS/HTML: Update progress bar within async block

I am currently working on a small html-based project where I have a somewhat long running computation within javascript. I am not good with javascript so I am trying to find a solution here. Basically my long-running code looks something like this:

let simulation = new Simulation(system,iterations,step_size,callback_fn);
simulation.run();

with the simulation.run-function looking like this:

run() {
    ...
    for(let i = 0; i < this.steps; i++){
        ...
        this.callback(i+1);
    }
}

Usually the steps variable is about 1 million. My plan was to add a progress bar which shows the progress.

HTML:
<div class="progress mt-5">
    <div id="simulation-progress" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
</div>

Now my plan was to use the callback function which I am giving the simulation to update my progress bar:

(async () => {
    const iterations = 1000000;
    const step_size  = 1;

    simulation_progress.setAttribute('style', `width:0%`);

    setTimeout(function(){
        let simulation = new Simulation(
            system,
            iterations,
            step_size,
            e => {
                if(e % 100000 === 0){
                    simulation_progress.setAttribute('style', `width:${Math.round(100 * e / iterations)}%`);
                    console.log(e, Math.round(100 * e / iterations))
                }
            });
        simulation.run();

    }, 1);
})();

Basically I know that about 100000 steps equals about 1 second. Whenever the returned steps from my callback is a multiple of 100000 (steps % 100000 === 0), I try to update the progressbar. Sadly this does not seem to work. What happens is that the progress bar does not change until the simulation has finished its 1M steps. After those 1M steps, it jumps directly to 100%.

Does someone know what causes this behaviour? Does it have to do with updating a progressbar inside this async block?

I am very happy for any help

Upvotes: 2

Views: 2316

Answers (2)

do-me
do-me

Reputation: 2178

Two possible solutions with webworker.js or setInterval()

Depending on the level of complexity you'd like to add, there are two ways to make it work.

1. Webworker.js

This is the "proper" solution. The heavy work is outsourced to the webworker.js file so the UI - and the progress bar - aren't freezing meanwhile.

Simple example:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Async Progress Bar Example</title>
</head>
<body>
  <progress id="progressBar" value="0" max="100"></progress>
  <script src="main.js"></script>
</body>
</html>
// main.js
const worker = new Worker('webworker.js');

worker.onmessage = function(e) {
  const progressBar = document.querySelector('#progressBar');
  progressBar.value = e.data;
}

async function asyncFunction() {
  // simulate a long running task
  for (let i = 0; i < 100; i++) {
    await new Promise(resolve => setTimeout(resolve, 50));
    worker.postMessage(i);
  }
}

asyncFunction();
// webworker.js
onmessage = function(e) {
  postMessage(e.data + 1);
}

2. SetInterval()

If you would like to keep it as easy as possible (and e.g. in one file/script) and you don't care about UI freeze but simply would like the progress bar updating properly, consider this solution.

Simple example:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Async Progress Bar Example</title>
</head>
<body>
  <progress id="progressBar" value="0" max="100"></progress>
  <script src="main.js"></script>
</body>
</html>
// main.js
let progress = 0;
const progressBar = document.querySelector('#progressBar');

async function asyncFunction() {
  // simulate a long running task
  for (let i = 0; i < 100; i++) {
    await new Promise(resolve => setTimeout(resolve, 50));
    progress++;
  }
}

setInterval(() => {
  progressBar.value = progress;
}, 0);

asyncFunction();

Note that the interval of 0ms should already be enough to update the progress bar. In my case it worked perfectly. Also, the UI isn't freezing completely but with every iteration the browser responds to e.g. scrolling or similar. It's a little laggy but was sufficient for my use case. You could also combine it with a gray screen overlay so the user is being discouraged from doing anything until the calculation is ready.

Upvotes: 5

Franco Torres
Franco Torres

Reputation: 1106

Changes to DOM are painted only after the currently running task is completed, irrespective of how long it takes. There's a way to make this not to happen. In order to make it work and see the changes from time to time and not at the end, we wrap it in a setTimeout.

In your code there's a setTimeout but in the wrong place. In your code it just runs once, but it should run every time you set simulation_progress.setAttribute(...).

So to fix it try the following .js code:

(async () => {
  const iterations = 1000000;
  const step_size = 1;

  simulation_progress.setAttribute('style', `width:0%`);

  let simulation = new Simulation(
    system,
    iterations,
    step_size,
    e => {
      if (e % 100000 === 0) {
        setTimeout(() => {
          simulation_progress.setAttribute('style', `width:${Math.round(100 * e / iterations)}%`);
          console.log(e, Math.round(100 * e / iterations))
        });
      }
    });
  simulation.run();
})();

More information on: https://javascript.info/event-loop.

Upvotes: 2

Related Questions