-
Notifications
You must be signed in to change notification settings - Fork 0
Parallel Processing
To follow along with this section, start with tag v2.1.11
.
One of the biggest limitations that we run into when programming in the browser is that there is only one thread. Almost every platform which supports rendering user interfaces has a separate thread for rendering and the one rule you never break is that you should never do serious work on the UI thread. When you do, you will inevitably run into a situation where the UI becomes unresponsive.
In JavaScript you can't avoid this because there is only one thread. To get around this problem we avoid doing big chunks of work. Everything has to be broken down into small, fast functions which can interleave with drawing code.
In the browser, you may not notice this problem until you start to add animations. Animations can make a large chunk of processing visible.
Even if you can rewrite your code to make is smaller and faster and make it interleave better with the animation code, you still only have one thread. At some point you will reach the limit of what you can do.
Web workers provide another thread on which processing can take place. They have a simple API and they are supported on most modern browsers and mobile devices.
But Web Workers are not without problems. The first problem is that a Web Worker requires you to decide ahead of time what code will run in the Web Worker. You can't just start a thread at runtime. You must plan ahead.
The other big problem with Web Workers is that you cannot share data between them. You may only pass messages and messages are copied. If you are familiar with Clojure's philosophy, this is not considered to be awesome.
This means that if you want to use Web Workers you have to come up with a way to use them that is efficient. You must have a programming model that does not require lots of state to be copied between Web Workers. You have to decide where to put state. Most JavaScript frameworks require that state be in the thread which can access the DOM. Web Workers cannot access the DOM. If most of the application's state must live in the main JavaScript thread then Web Workers are much harder to utilize.
It just so happens that pedestal-app is a perfect fit for Web Workers. This is true for several reasons:
- state and rendering are already isolated from each other
- the application and the renderer communicate by sending messages
- only diffs are transmitted to the renderer
- the renderer does not need to have access to application state
This means that with pedestal-app, the entire dataflow engine can be run in a Web Worker leaving the main thread free to handle only rendering.
This was first used on project where we had so many animations, we were stressing out that an update which we received every 500 ms was taking 50-100 ms to process. Switching to a Web Worker solved the problem and gave us 400 ms of leg room for additional processing without slowing down the animations.
Pedestal-app has an input queue and the renderer consumes data from the app model queue. To use Web Workers, you simply create an abstraction which allows these queues to cross the Web Worker boundary.
You can also run all of the service code in the Web Worker so it is not competing with processing on the main thread. The only part of the system that a service communicates with is the application dataflow and Web Workers have access to the XmlHttpRequest.
In this section you will create two new aspects and update one existing aspect. There will be a new aspect which runs the simulated service in a web worker and one that runs in development mode on a Web Worker using the real service. Finally, you will update the production aspect to only run on a Web Worker with advanced compilation.
It will be good to keep the Development aspect as it is so that you can debug more easily. Web Workers do not allow you to log to the console which can make print-style debugging difficult.
Speaking of console logging, before making any other changes, you will
need to remove any console logging from code which will run in a Web
Worker. the only place where this is a problem in this project is the
tutorial-client/simulated/services
namespace. Update the
services-fn
removing the use of js/console
.
(defn services-fn [message input-queue]
(when (and (= (msg/topic message) [:active-game]) (:value message))
(start-game-simulation input-queue)))
This first change will be the simplest. You will add a new aspect
named :worker-ui
which is just like the :ui
aspect except that it
runs in a Web Worker.
:worker-ui {:uri "/tutorial-client-dev-worker-ui.html"
:name "Worker UI"
:order 3
:out-file "tutorial-client-dev-worker-ui.js"
:main 'tutorial_client.simulated.worker_start
:output-root :tools-public
:template "application.html"
:workers {"sim_worker" #{#"tutorial_client/behavior.js"
#"tutorial_client/worker/.*"
#"tutorial_client/simulated/worker.js"
#"tutorial_client/simulated/services.js"}}}
This is the same as any other aspect except that it contains a
:workers
key. The value of the :workers
key is a map of file names
to a set of regular expressions which identify the source files which
should be compiled into a single worker source file with this name.
In the above example, the compiled worker source file will be named
sim_worker.js
.
The other thing to notice about this aspect is that the main function
is located in a new namespace named
tutorial-client.simulated.worker_start
. The remaining work required
to run in a Web Worker will be done in the main
function of that
namespace.
Create the file
tutorial-client/app/src/tutorial_client/simulated/worker_start.cljs
and write the following code.
(ns tutorial-client.simulated.worker-start
(:require [io.pedestal.app.util.web-workers :as ww]
[tutorial-client.rendering :as rendering]))
(defn ^:export main []
(ww/run-on-web-worker! "/generated-js/sim_worker.js"
:render {:type :push :id "content" :config rendering/render-config}))
This code defines what runs on the main JavaScript thread. The helper
function run-on-web-worker!
will wire everything up for us. For more
advanced usage, refer to the implementation of run-on-web-worker!
and wire everything up yourself. This function will cause the code in
/generated-js/sim_worker.js
to run on the Web Worker and will create
the channels of communication between the main thread and the worker
thread. The provided renderer will receive all rendering deltas
generated by the application running.
Remember that rendering functions are handed an input queue. The queue that will be handed to this renderer will convey input messages across the Web Worker boundary to the real input queue.
The code that gets loaded in the web worker must also have an entry
point. That entry point is defined in the ClojureScript namespace
tutorial-client.simulated.worker
. Create the file
tutorial-client/app/src/tutorial_client/simulated/worker.cljs
with the following code.
(ns tutorial-client.simulated.worker
(:require [tutorial-client.worker.init :as init]
[tutorial-client.simulated.services :as services]))
(init/init! services/->MockServices services/services-fn)
This just calls the init!
functions passing in a services
constructor and function which can consume the effects queue. The
tutorial-client.worker.init
namespace is where the real work
happens. Create the ClojureScript file
tutorial-client/app/src/tutorial_client/worker/init.cljs
with the following code.
(ns tutorial-client.worker.init
(:require [io.pedestal.app.protocols :as p]
[io.pedestal.app :as app]
[tutorial-client.behavior :as behavior]
[tutorial-client.post-processing :as post]
[tutorial-client.clock :as clock]
[cljs.reader :as reader]
[io.pedestal.app.render :as render]))
(defn init! [services-ctor effects-fn]
(let [app (app/build (post/add-post-processors behavior/example-app))
services (services-ctor app)
render-fn (fn [deltas _]
(doseq [d deltas]
(js/postMessage (pr-str d))))
app-model (render/consume-app-model app render-fn)]
(app/consume-effects app effects-fn)
(app/begin app)
(clock/increment-game-clock (:input app))
(js/addEventListener "message"
(fn [e]
(p/put-message (:input app)
(reader/read-string (.-data e))))
false)
(p/start services)))
This code is similar to the code found in other main functions which create an application and start it running. There are only two differences. The render function that is used here will send the deltas that it receives to the main JavaScript thread. An event listener is created which will receive messages from the main JavaScript thread and place them on the input queue. This is how the Web Worker receives input and produces rendering output.
With these changes in place you can now restart and run the Worker UI aspect. You should now have a fully functioning application running within a Web Worker.
The real win will come when you use this in the production version of the application. Most of the rendering delays happened when lots of new messages were received from remote players.
The :worker-development
aspect is shown below.
:worker-development {:uri "/tutorial-client-worker-dev.html"
:use-api-server? true
:name "Worker Development"
:out-file "tutorial-client-worker-dev.js"
:main 'tutorial_client.worker_start
:order 5
:template "application.html"
:workers {"worker" #{#"tutorial_client/behavior.js"
#"tutorial_client/worker/.*"
#"tutorial_client/worker.js"
#"tutorial_client/services.js"}}}
This configuration uses tutorial-client.worker-start
as the starting
point and will rely on a worker file named worker.js
The production config is similar.
:production {:uri "/tutorial-client.html"
:use-api-server? true
:name "Production"
:optimizations :advanced
:out-file "tutorial-client.js"
:main 'tutorial_client.worker_start
:order 7
:compiler-options {:externs ["app/externs/game.js"]}
:workers {"worker" #{#"tutorial_client/behavior.js"
#"tutorial_client/worker/.*"
#"tutorial_client/worker.js"
#"tutorial_client/services.js"}}}
Before you move on from the config file there is one more issue to
consider. There is already one ClojureScript namespace named
tutorial_client.simulated.worker
which contains code which will run
as soon as the file is loaded. There will soon be another one in
tutorial_client.worker
. These files are only meant to be run in the
Web Worker.
As things are now, these files will be compiled into the production
version of the application and can cause a lot of problems. To exclude
these files form the build, add an :ignore
to the :build
section
of the config.
:ignore [#"tutorial_client.simulated.worker.js"
#"tutorial_client.worker.js"]
The starting point for the main JavaScript thread is in the
tutorial-client.worker-start
namespace.
(ns tutorial-client.worker-start
(:require [io.pedestal.app.util.web-workers :as ww]
[tutorial-client.rendering :as rendering]))
(defn ^:export main []
(ww/run-on-web-worker! "/generated-js/worker.js"
:render {:type :push :id "content" :config rendering/render-config}))
This is the same as before except that it loads a different worker source file and it uses a different renderer.
The starting point for the Web Worker is located in
tutorial-client.worker
.
(ns tutorial-client.worker
(:require [tutorial-client.worker.init :as init]
[tutorial-client.services :as services]))
(init/init! services/->Services services/services-fn)
This is all. With these changes in place, run the application in the same way that you did before and notice the difference in performance. You can also compare performance by running the Development aspect and then running the Worker Development aspect.
This concludes Part 2 of the pedestal-app tutorial. You are now a pedestal-app master. More sections may be added in the future but this is all for now. We hope that this has helped you to understand not only how pedestal-app works but also why it works the way it does. We also hope that this will help you to know how you can contribute to pedestal-app as it moves forward.
The tag for this step is v2.1.12
.