Polling — Simple, but Wasteful

As stated in the previous article, the problem at hand is to display the application's current internal state.

Let's discuss two very simple techniques for achieving that. They can serve as a baseline to compare more sophisticated methods against:

  • game loop
  • polling

Game Loop — the Simplest Thing That Works

The most straightforward approach is commonly used in computer games. It is known as a game loop, also called simulation loop or frame loop. No framework or special library is needed for that.

The application reads the data periodically and renders it to its display. This is how many games work. For some, you can even see the rate of renderings per second as fps (frames per second).

This way, the application state doesn't have to know how the display works, and the display just reads the state via its public interface. It offers excellent encapsulation, doesn't require any unusual way of writing code, and locality isn't adversely affected by this pattern.

The code to display the current score in a game might look something like this:

function render(state) {
screen.prepareEmptyFrame(); // create empty display, but don't show it yet
const score = state.calculateScore(); // calculate score
screen.drawText("Score: " + score); // draw score on screen
screen.showFrame(); // show fully rendered frame to the user
}
async function loop() {
while (running) {
const state = await fetchState();
render(state);
await sleep(16); // wait for about 16 ms, which should result in 60 fps
}
}

Sometimes This Is a Good Choice

Suppose your state changes constantly and updates the display in ways that are hard to reason about or isolate.

For example, a computer game might display a 3D landscape that changes in real time. Animations run, tree leaves move in the wind, birds fly in the sky, shadows are cast onto other objects, and reflections are visible in the water. Almost every pixel somehow depends on each aspect of your state. If you had to draw this as a dependency graph, it would almost be a complete bipartite graph.

bipartite cluster_state State cluster_pixels Pixels t1 Tree position p1 Pixel 1 t1->p1 p2 Pixel 2 t1->p2 p3 Pixel 3 t1->p3 p4 Pixel 4 t1->p4 t2 Sun position t2->p1 t2->p2 t2->p3 t2->p4 t3 Bird position t3->p1 t3->p2 t3->p3 t3->p4 t4 Wind speed t4->p1 t4->p2 t4->p3 t4->p4

In this situation, just re‑rendering everything over and over is not only simple, but also probably the best solution you can choose.

This is why games and other simulation software use this all the time. This pattern is called the game loop.

When you know how large your state is, you can usually estimate how long the render phase will take. This method has somewhat predictable performance and low complexity.

Usually Unacceptable

While the software engineering part is very straightforward, this approach is unacceptable for most applications. Their state doesn't change that often. Many applications will just do nothing without user input.

For example, a calculator shows some buttons and a readout for the current number. But unless you press one of the buttons, there is no need to update anything. It might be similar for an ERP system or a CAD program.

Many programs need to pull data from the network. They should only pull the data that is needed. After all, retrieving the data from a server somewhere on the internet will cost bandwidth and cause load on the server.

Even on the local machine, updating too many parts of the screen over and over costs CPU cycles and electrical power. So how can we reduce this cost?

Polling

Most applications do not need a full redraw every iteration and could therefore be optimized substantially. But how can we know if the display should be updated in a given iteration? To solve this, we keep the loop, but make the render conditional: each iteration reads the value, compares it with the value from the last render, and changes the display only if it differs.

This strategy is called polling: reading a value over and over, and acting only when it has changed. The small piece of bookkeeping it adds is the last rendered value. The practice of comparing the current value with the last rendered one on every tick is called dirty checking. The system asks "has this changed?" on every iteration, instead of re‑rendering the whole screen.

let lastRenderedState = null;
async function poll() {
while (running) {
const state = await fetchState();
if (isDifferent(state, lastRenderedState)) {
lastRenderedState = state;
render(state);
}
await sleep(16); // wait for about 16 ms, which should result in 60 fps
}
}

Compared with the game loop, two things have changed. An iteration without change is now much cheaper: just one read and one comparison. The display is left alone.

When You Should Use Polling

Many applications display data from a service and might not have any influence on how that data can be obtained. For example, many applications pull data from an HTTP endpoint. You make a request and the HTTP server will respond right away, providing the current data. To display up‑to‑date data automatically, the application might need to call this endpoint repeatedly. Otherwise the user has to trigger a refresh, whenever he wants to make sure the data is current.

Having many instances of that application requesting data repeatedly puts a lot of load on the server. So this method is to be avoided when possible, but is still sometimes necessary.

One well‑known way to optimize this is long‑polling. Here, the server will simply wait with the response until a change has occurred.

Discussion

Performance

CPU

Let's put some numbers on the performance cost. Suppose the following variables are defined:

rr
time to render a state
gg
time to retrieve the state
cc
time to compare two states
ll
likelihood that the state has changed since the last poll
ff
frames per second

Then the game loop would have a cost of f⋅(g+r)f \cdot (g + r).

Polling would have a cost of f⋅(g+c+r⋅l)f \cdot (g + c + r \cdot l).

So this is an improvement when ll (the likelihood that something changed) is small and cc is much smaller than rr, meaning comparing two states is much faster than rendering a state.

Memory

For the game loop, one instance of the state is enough. For polling, we need to keep two copies of the state. While this can be optimized, memory usage will increase and more load might be put on garbage collection (if available in your programming language).

Complexity

Comparing State

Comparing two states can vary greatly in complexity. Composite data structures like lists, maps, and arrays can be a bit harder to compare, especially when the order of elements isn't known.

Comparing unordered sets or maps is a classic example of this cost.

During development, the state will evolve often as new features get added. The state will also be refactored. Most of these modifications make it necessary to update the isDifferent function. When adding a property to an object, this is something that can easily be missed.

The two approaches discussed in this article have treated the application state and display as one atomic entity. But splitting it into multiple parts offers tremendous optimization potential for the methods we have discussed so far and beyond.

Summary

Game loops redraw constantly and are simple but wasteful for low‑change applications. Polling adds a cheap dirty‑check to redraw only when state actually changes, reducing CPU and battery usage when the change likelihood is low.