2016-11-06 20:12:24 +00:00
|
|
|
|
|
|
|
|
|
|
2016-08-30 07:40:21 +00:00
|
|
|
|
## Solving The CPU Hog Problem
|
|
|
|
|
|
|
|
|
|
Sometimes a handler has a lot of CPU intensive work to do, and
|
|
|
|
|
getting through it will take a while.
|
|
|
|
|
|
|
|
|
|
When a handler hogs the CPU, nothing else can happen. Browsers
|
|
|
|
|
only give us one thread of execution and that CPU-hogging handler
|
|
|
|
|
owns it, and it isn't giving it up. The UI will be frozen and
|
|
|
|
|
there will be no processing of any other handlers (eg: `on-success`
|
|
|
|
|
of POSTs), etc, etc. Nothing.
|
|
|
|
|
|
|
|
|
|
And a frozen UI is a problem. GUI repaints are not happening. And
|
|
|
|
|
user interactions are not being processed.
|
|
|
|
|
|
|
|
|
|
How are we to show progress updates like "Hey, X% completed"? Or
|
|
|
|
|
how can we handle the user clicking on that "Cancel" button trying
|
|
|
|
|
to stop this long running process?
|
|
|
|
|
|
|
|
|
|
We need a means by which long running handlers can hand control
|
|
|
|
|
back for "other" processing every so often, while still continuing
|
|
|
|
|
on with their computation.
|
|
|
|
|
|
2016-08-30 14:20:50 +00:00
|
|
|
|
## The re-frame Solution
|
2016-08-30 07:40:21 +00:00
|
|
|
|
|
2016-08-30 14:20:50 +00:00
|
|
|
|
__First__, all long running, CPU-hogging processes are put in event handlers.
|
|
|
|
|
Not in subscriptions. Not in components. Not hard to do,
|
|
|
|
|
but worth establishing as a rule, right up front.
|
2016-08-30 07:40:21 +00:00
|
|
|
|
|
|
|
|
|
__Second__, you must be able to break up that CPU
|
|
|
|
|
work into chunks. You need a way to do part of the work, pause,
|
|
|
|
|
then resume from where you left off. (More in min).
|
|
|
|
|
|
|
|
|
|
In a perfect world, each chunk would take something like
|
|
|
|
|
16ms (60 fps). If you go longer, say 50ms or 100ms, it is no train
|
|
|
|
|
smash, but UI responsiveness will degrade and animations, like
|
|
|
|
|
busy spinners, will get jerky. Shorter is better, but less than
|
|
|
|
|
16ms delivers no added smoothness.
|
|
|
|
|
|
|
|
|
|
__Third__, within our handler, after it completes one unit (chunk)
|
|
|
|
|
of work, it should not continue straight on with the next. Instead,
|
|
|
|
|
it should do a `dispatch` to itself and, in the event vector,
|
|
|
|
|
include something like the following:
|
|
|
|
|
|
|
|
|
|
1. a flag to say the work is not finished
|
|
|
|
|
2. the working state so far; and
|
|
|
|
|
3. what chunk to do next.
|
|
|
|
|
|
|
|
|
|
## A Sketch
|
|
|
|
|
|
|
|
|
|
Here's an `-fx` handler which counts up to some number in chunks:
|
|
|
|
|
```clj
|
|
|
|
|
(re-frame.core/reg-event-fx
|
|
|
|
|
:count-to
|
|
|
|
|
(fn
|
|
|
|
|
[{db :db} [_ first-time so-far finish-at]]
|
|
|
|
|
(if first-time
|
|
|
|
|
;; We are at the beginning, so:
|
|
|
|
|
;; - modify db, causing popup of Modal saying "Working ..."
|
|
|
|
|
;; - begin iterative dispatch. Give initial version of "so-far"
|
2016-09-12 00:25:20 +00:00
|
|
|
|
{:dispatch [:count-to false {:counter 0} finish-at] ;; dispatch to self
|
2016-08-30 07:40:21 +00:00
|
|
|
|
:db (assoc db :we-are-working true)}
|
|
|
|
|
(if (> (:counter so-far) finish-at)
|
|
|
|
|
;; We are finished:
|
|
|
|
|
;; - take away the state which causes the modal to be up
|
|
|
|
|
;; - store the result of the calculation
|
|
|
|
|
{:db (-> db
|
|
|
|
|
(assoc :fruits-of-labour (:counter so-far)) ;; remember the result
|
|
|
|
|
(assoc :we-are-working false))} ;; no more modal
|
|
|
|
|
;; Still more work to do
|
|
|
|
|
;; - run the calculation
|
|
|
|
|
;; - redispatch, passing in new running state
|
|
|
|
|
(let [new-so-far (update so-far :counter inc)]
|
|
|
|
|
{:dispatch [:count-to false new-so-far finish-at]}))))
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Why Does A Redispatch Work?
|
|
|
|
|
|
2016-08-30 14:20:50 +00:00
|
|
|
|
A `dispatched` event is handled asynchronously. It is queued
|
|
|
|
|
and not actioned straight away.
|
2016-08-30 07:40:21 +00:00
|
|
|
|
|
|
|
|
|
And here's the key: **After handling current events, re-frame yields control
|
|
|
|
|
to the browser**, allowing it to render any pending DOM changes, etc. After
|
|
|
|
|
it is finished, the browser will hand control back to the re-frame router
|
|
|
|
|
loop, which will then handle any other queued events
|
|
|
|
|
which, in our case, would include the event we just dispatched to perform
|
|
|
|
|
the next chunk of work.
|
|
|
|
|
|
|
|
|
|
When the next dispatch is handled, a next chunk of work will be done, and then another
|
|
|
|
|
`dispatch` will happen. And so on. `dispatch` after `dispatch`. Chunk
|
|
|
|
|
after chunk. In 16ms increments if we are very careful (or some small amount
|
|
|
|
|
of time less than, say, 100ms). But with the browser getting a look-in after each iteration.
|
|
|
|
|
|
2016-08-30 14:20:50 +00:00
|
|
|
|
### Variations
|
2016-08-30 07:40:21 +00:00
|
|
|
|
|
|
|
|
|
As we go, the handler could be updating some value in `app-db` which indicates
|
|
|
|
|
progress, and this state would then be rendered into the UI.
|
|
|
|
|
|
|
|
|
|
At a certain point, when all the work is done, the handler will likely put the
|
|
|
|
|
fruits of its computational labour into `app-db` and clear any flags which might, for example,
|
|
|
|
|
cause a modal dialog to be showing progress. And the process would then be done.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Cancel Button
|
|
|
|
|
|
|
|
|
|
It is a flexible pattern. For example, it can be tweaked to handle a "Cancel' button ...
|
|
|
|
|
|
|
|
|
|
If there was a “Cancel” button to be clicked, we might
|
|
|
|
|
`(dispatch [:cancel-it])` and then have this event’s handler tweak the `app-db`
|
|
|
|
|
by adding `:abandonment-required` flags. When a chunk-processing-handler
|
|
|
|
|
next begins, it could check for this `:abandonment-required` flag, and,
|
|
|
|
|
if found, stop the CPU intensive process (and clear the abandonment flags).
|
|
|
|
|
When the abandonment-flags
|
|
|
|
|
are set, the UI could show "Abandoning process ..." and thus appear responsive
|
|
|
|
|
to the user's click on “Cancel”.
|
|
|
|
|
|
|
|
|
|
That's just one approach. You can adapt the pattern as necessary.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Further Notes
|
|
|
|
|
|
|
|
|
|
Going to this trouble is completely unnecessary if the long running
|
|
|
|
|
task involves I/O (GET, POST, HTML5 database action?) because the
|
|
|
|
|
browser will handle I/O in another thread and give UI activities plenty of look in.
|
|
|
|
|
|
|
|
|
|
You only need to go to this trouble if it is your code which is
|
|
|
|
|
hogging the CPU.
|
|
|
|
|
|
|
|
|
|
## Forcing A One Off Render
|
|
|
|
|
|
|
|
|
|
Imagine you have a process which takes, say, 5 seconds, and chunking
|
|
|
|
|
is just too much effort.
|
|
|
|
|
|
|
|
|
|
You lazily decide to leave the UI unresponsive for that short period.
|
|
|
|
|
Except,
|
|
|
|
|
you aren't totally lazy. If there was a button which kicked off
|
|
|
|
|
this 5 second process, and the user clicks it, you’d like the UI to
|
|
|
|
|
show a response. Perhaps it could show a modal popup thing saying
|
|
|
|
|
“Doing X for you”.
|
|
|
|
|
|
|
|
|
|
At this point, you still have a small problem to solve. You want
|
|
|
|
|
the UI to show your modal message before you then hog the CPU for
|
|
|
|
|
5 seconds.
|
|
|
|
|
|
|
|
|
|
Updating the UI means altering `app-db`. Remember, the UI is a
|
|
|
|
|
function of the data in `app-db`. Only changes to `app-db` cause UI
|
|
|
|
|
changes.
|
|
|
|
|
|
|
|
|
|
So, to show that Modal, you’ll need to `assoc` some value into `app-db`
|
|
|
|
|
and have that new value change what is rendered in your reagent components.
|
|
|
|
|
|
|
|
|
|
You might be tempted to do this:
|
|
|
|
|
```clj
|
|
|
|
|
(re-frame.core/reg-event-db
|
|
|
|
|
:process-x
|
|
|
|
|
(fn
|
|
|
|
|
[db event-v]
|
|
|
|
|
(assoc db :processing-X true) ;; hog the CPU
|
|
|
|
|
(do-long-process-x))) ;; update state, so reagent components render a modal
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
But that is just plain wrong.
|
|
|
|
|
That `assoc` into `db` is not returned (and it must be for a `-db` handler).
|
|
|
|
|
And, even if that did somehow work,
|
|
|
|
|
then you continue hogging the thread with `do-long-process-x`. There's no
|
|
|
|
|
chance for any UI updates because the handler never gives up control. This
|
|
|
|
|
handler owns the thread right through.
|
|
|
|
|
|
|
|
|
|
Ahhh, you think. I know what to do! I'll use that pattern I read
|
|
|
|
|
about in the Wiki, and `re-dispatch` within an`-fx` handler:
|
|
|
|
|
```clj
|
|
|
|
|
(re-frame.core/reg-event-fx
|
|
|
|
|
:process-x
|
|
|
|
|
(fn
|
|
|
|
|
[{db :db} event-v]
|
|
|
|
|
{:dispatch [:do-work-process-x] ;; do processing later, give CPU back to browser.
|
|
|
|
|
:db (assoc db :processing-X true)})) ;; ao the modal gets rendered
|
|
|
|
|
|
|
|
|
|
(re-frame.core/reg-event-db
|
|
|
|
|
:do-work-process-x
|
|
|
|
|
(fn [db _]
|
|
|
|
|
(do-long-process-x db))) ;; return a new db, presumably containing work done
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
So close. But it still won’t work. There's a little wrinkle.
|
|
|
|
|
|
|
|
|
|
That event handler for `:process-x` will indeed give back control
|
|
|
|
|
to the browser. BUT, because of the way reagent works, that `assoc` on `db`
|
|
|
|
|
won't trigger DOM updates until the next animation frame runs, which is 16ms away.
|
|
|
|
|
|
|
|
|
|
So, you will be yielding control to the browser, but for next 16ms
|
|
|
|
|
there won't appear to be anything to do. And, by then, your CPU hogging
|
|
|
|
|
code will have got control back, and will keep control for the next 5
|
|
|
|
|
seconds. That nice little Dialog telling you the button was clicked and
|
|
|
|
|
action is being taken won't show.
|
|
|
|
|
|
|
|
|
|
In these kinds of cases, where you are only going to give the UI
|
|
|
|
|
**one chance to update** (not a repeated chances every few milli seconds),
|
|
|
|
|
then you had better be sure the DOM is fully synced.
|
|
|
|
|
|
|
|
|
|
To do this, you put meta data on the event being dispatched:
|
|
|
|
|
```clj
|
|
|
|
|
(re-frame.core/reg-event-fx
|
|
|
|
|
:process-x
|
|
|
|
|
(fn
|
|
|
|
|
[{db :db} event-v]
|
|
|
|
|
{:dispatch ^:flush-dom [:do-work-process-x] ;; <--- NOW WITH METADATA
|
|
|
|
|
:db (assoc db :processing-X true)})) ;; ao the modal gets rendered
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Notice the `^:flush-dom` metadata on the event being dispatched. Use
|
|
|
|
|
that when you want the UI to be fully updated before the event dispatch
|
|
|
|
|
is handled.
|
|
|
|
|
|
|
|
|
|
You only need this technique when you:
|
|
|
|
|
|
|
|
|
|
1. want the DOM to be fully updated
|
|
|
|
|
2. because you are going to hog the CPU for a while and not give it back. One chunk of work.
|
|
|
|
|
|
|
|
|
|
If you handle via multiple chunks you don't have to do this, because
|
|
|
|
|
you are repeatedly handing back control to the browser/UI. Its just
|
|
|
|
|
when you are going to tie up the CPU for a one, longish chunk.
|
2016-12-22 10:15:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
|
|
|
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
|
|
|
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|