Skip to content

Performance improvements

Will Rogers edited this page Nov 25, 2020 · 1 revision

Results of Performance Testing

Our initial implementation of CS-Web-Proto had some significant problems which were causing problems as the number of PV-aware increased. Clearly this is not great for a control system. In this section I will detail the issues, using screenshots from the Chrome profiler in the hope that we will be able to identify these potential problems sooner in the future and avoid them.

We have made significant improvements from where we started!

Rendering Every Widget on Update

The philosophy behind React components is to only update the parts of the DOM which have changed. This makes sense because why would you waste time and computing power re-drawing things which haven't changed? Best to leave them alone. React accomplishes this using their "diffing" algorithm, which performs a basic check on the props coming into an object to see if they are the same as they were before. For primitives such as strings and numbers, this works quite well!

("this string" === "this string") === true

However, in JS, objects, arrays and functions can hold the same information but when compared, they are not identical.

({a: 1} === {a: 1}) === false

The reference of the value is different - basically this information exists in a different place in memory and is therefore not the same object/array/function. This causes a bit of a problem for React, as it means that if you are dynamically creating objects, or arrays, you can pass the same information, but trigger a render.

We have several hooks in the code which provide Widgets and PVWidgets with information. And these hooks were dynamically creating objects and triggering unnecessary renders.

The connection hook, useConnection, looks at our Redux store. When an update is made to the Redux store, it returns a brand new object with the new values of the store! So each hook saw this, assumed it needed to do something with this new object, and passed a new slice of the store with it's connected value down to the PVWidget it was connected to, triggering a render.

In the below image, 3 PV values update, one at a time, but each update causes all 3 PVWidgets to render! This means our render time is O(n^2) where n is the number of widgets!

O(n^2) renders!

Our rules hook was also causing a similar issue.

The fix was to create a custom comparison function for the selector which looks inside the object to see if we actually need to trigger a render or not. After that, one PV update triggered one PV:

One render per PV update

This reduced the time to render 40 PVs from 1105 ms to 269 ms, and put us on an O(n) time for renders, so the difference only widens as the number of PVs increases.

Throttling

The above change made a massive improvement to our performance, but you can see that React is committing changes and redrawing the screen on every PV update. This is fine when there are only a few updates coming in each second, but as you increase the number of updates to the numbers mentioned in the performance testing at the top of the page, this overhead starts to become a bit of a problem. Wouldn't it be better if we could group updates together and then perform updating all at once?

This is known as throttling as you are controlling the rate of updates. Given that most PVs will not update faster than 10Hz, a sensible throttling time for us is 0.1s, or 100ms.

In this image, 10 PVs are updated and the renders are all performed at once, meaning the browser only has to draw the DOM once.

Throttled PV updates collected together

This change also gave us a good boost in performance and is in line with performance optimisations used by other display managers and indeed many tools on the web.

For small numbers of PVs, this overhead is not particularly significant, but as the numbers increases it dominates over the render time of an individual widget. Throttling reduced the time taken to render 1000 PVs from 3687 ms to 508 ms. Essentially this is still and O(n) operation, but n is now the time taken to render a single widget, rather than the time taken to diff the entire tree plus draw it on the screen.

Separating the parts of a widget that update

See this pull request.

Calculating widget props is quite expensive. An initial implementation recalculated all props each time a PV value changed; separating ConnectingComponent meant that only the props that needed access to the PV value were recalculated, improving performance significantly.

Summary

Be careful when passing objects, arrays and functions as props! They must be handled with care and crafting a specific selector can improve performance massively.

Also be careful with the Redux store and make sure you are using it properly.

Throttling is not that difficult but will help reduce the workload if you are expecting a lot of updates.