1

I have a function that solves a puzzle in a browser. It might take a very long time to finish and I want to stop its execution if the runtime exceeds 30 seconds. Something like this:

function solve(puzzle) {
    // A loop to solve a puzzle that might take minutes to run
    while (notSolve){
        keepFindingSolution(); // This is not a loop
    }
    return solution;
}

let solution = solve(veryHardPuzzle);
if (solution && solution.solved) {
    // Do something
}
console.log("Other executions");

So, I don't want to block the UI thread when solving the function so that users can still interact with the website. I also want to get the solution from the solve(veryHardPuzzle) once it's done, or break the execution of the function if the time is out.

I tried some different approaches below, but none of them work for me:

Wrap the solve() function in setTimeout()

setTimeout(function () {
    let solution = solve(veryHardPuzzle);
    if (solution && solution.solved) {
        // Do something
    }
}, 30000);
console.log("Other executions");

This approach (from https://stackoverflow.com/a/26616321/6308776) doesn't block the UI thread, and the other executions are executed happily. However, it just basically waits 30 seconds before executing the inner solve(veryHardPuzzle) function (please correct me if I'm wrong). If the solve(veryHardPuzzle) function runs longer than 30 seconds then it would block the thread.

clearTimeout() once the solve() is done

let timerId = setTimeout(function () {
    let solution = solve(veryHardPuzzle);
    clearTimeout(timerId);
    if (solution && solution.solved) {
        // Do something
    }
}, 30000);
console.log("Other executions");

I thought that this would stop the timeout() after the solution is found, but it technically waits 30 seconds before the solver(veryHardPuzzle) is executed.

After doing some research, I realized setTimeout() might not the thing I want. Do you have any ideas or techniques on how to solve this?

Khang Vu
  • 329
  • 4
  • 13
  • 2
    You need to avoid aggressive `while-loop` and embrace `setInterval` approach. – Ele Nov 29 '18 at 23:14
  • That's interesting. I can see how to use setInterval() to stop my loop. I will try it and see if it works. – Khang Vu Nov 29 '18 at 23:16
  • It's not clear in your question why `solve()` isn't blocking the thread by itself. Is it async? The very first code you posted is going to block until the `while` loop is done. – Mark Nov 29 '18 at 23:19
  • Hi @Mark, no `solve()` is just a while loop so it's not async. It would probably block the thread by itself. – Khang Vu Nov 29 '18 at 23:21
  • 1
    Then wrapping it in a timeout isn't going to help. Your timeout code waits 30seconds before *starting* the puzzle, so maybe it only appears to not block. You only have one thread to work with in javascript. Maybe there's a solution using [web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) – Mark Nov 29 '18 at 23:22
  • The `setInterval` approach works for me. Thank you, everyone! – Khang Vu Dec 01 '18 at 06:19

2 Answers2

0

The easiest way to handle asynchronous (like) behavior is with a Promise.

In this case you could do something like this:

function doLongThing() {
  let success = false;
  return new Promise(function(resolve, reject) {

    //set timeout so if success is false after 30 seconds, reject promise.
    setTimeout(function () {
      if(!success) reject();
    }, 30000)

    //another set timeout to prevent blocking UX thread
    setTimeout(function () {
      //do long running thing
      let solution = solve(veryHardPuzzle);

      //thing finished
      success = true;

      //pass result into resolve method so consumer can use it
      resolve(solution);
    });
  })
}

then you could do this:

doLongThing().then(function(solution) {
  //do something with solution
}).catch(function() {
  //timed out
});

console.log("Other executions");
Will P.
  • 8,437
  • 3
  • 36
  • 45
  • 2
    I think the issue is that the `solve` code is synchronously working, so even if it is wrapped in a promise, the thread will be occupied by the solve function and block. – Ben Steward Nov 29 '18 at 23:18
  • 2
    I think this approach will hang the UX. – Ele Nov 29 '18 at 23:18
  • Thanks for your answer. I will try it soon and let you know if it works. But would using Promise block the main thread? – Khang Vu Nov 29 '18 at 23:19
  • 1
    See my edit, wrapping the long running part in another set timeout will guarantee it does not block the thread – Will P. Nov 29 '18 at 23:20
  • I think generators would do much better. – Nika Nov 29 '18 at 23:21
  • 1
    No, it just guarantees that it will not run until the setTimeout callback is called. The only true solution, if this is meant to be executed by the browser’s JS engine, is to actually use a separate thread. Otherwise, kicking the solver to a server is an option to consider. – Ben Steward Nov 29 '18 at 23:23
  • 2
    well, the only way to not block the main thread is to... rewrite the solve function directly to be async (if possible though). – Nika Nov 29 '18 at 23:24
  • I agree @Nika and since we don't have info on that function we can't solve the async problem. We can perform 'other executions' though, which I assumed was the question they were asking – Will P. Nov 29 '18 at 23:28
0

A few comments have hinted at this, but the answer is not in how you call solve, but rather in how solve iterates over each “try” (I’m assuming this is how your solver works). If after each try your solver will break out of the event loop (the essence of async JS), then the main thread will have opportunities to do the other work it needs to do like handle mouse clicks or paint the UI. There are a few ways to do this, but maybe Promises is the easiest to visualize and has the best browser support (though some polyfills may be necessary for some IE versions).

var timeIsUp = false

function trySolution (puzzle, solution) {
  return new Promise(function (resolve, reject) {
    if (!solution) reject('no solution found')

    if (timeIsUp) reject('time up!')

    if (puzzle + solution !== 'wrong') { // idk lol
      resolve({solution: solution, success: true})
    } else {
      resolve({solution: solution, success: false})
    }
  }
}

// solve is now recursive with each iteration async
function solve (puzzle, solutions) {
  trySolution(puzzle, solutions[0])
    .then(function (result) {
      if (result.success) {
        solve(puzzle, solutions.slice(1))
      }
    })
    .catch(function (err) {
      console.error(err)
    })
}

// do the deed
solve(puzzle, solutions) // puzzle is undefined error lol

setTimeout(function () {
  timeIsUp = true
}, 30000)
Ben Steward
  • 2,338
  • 1
  • 13
  • 23
  • That looks very promising. I'll try implementing it in the next couple of days and let you know soon. Thank you! – Khang Vu Nov 30 '18 at 05:15
  • My solution loop is so complicated that I couldn't try that method (it's recursion + loop). I converted recursion to loop and use `setInterval` and it works now! Thanks for your answer though. – Khang Vu Dec 01 '18 at 06:20
  • 1
    @Minh-KhangVu Something else to consider here is web workers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers I was surprised to find the support is very good for this powerful feature. – Ben Steward Dec 02 '18 at 05:03
  • Thank you for your reference. I'll check it out. – Khang Vu Dec 02 '18 at 05:09