In the Worker

With my main-thread activity made non-blocking, I now had two bodies of work I could do in a worker — parsing the profile and calculating the layout. Doing these in the worker keeps the main thread free for any other activity I might want to do.

Parsing the profile

Receiving the profile as an ArrayBuffer was no issue — the heap profile format is just an extremely compact JSON format. TextDecoder.decode and JSON.parse work fine to transform it from an ArrayBuffer to an object. At that point, it can be happily shuttled off to our heap profile parser and inflated into a proper data structure.

Returning the heap as a Transferable needed a little more nuance. I return my nodes with additional data in a much more verbose format to minimize the deserialization necessary on the main thread. To accommodate this, I needed to create a separate wire format to dodge the string character limit (512MB on Chrome) for very large profiles. Once I have the nodes as this compact format, I just JSON.stringify and TextEncoder.encode to transfer the representation across.

This is an extremely fast way to transfer large objects to and from a worker.

Layout

The last piece of heavy lifting to do on the worker is to calculate the layout. I am still using d3-hierarchy’s pack layout, which is distributed as a piece of the self-contained hierarchy package. Applying the layout to the data in the worker is easy— just format the nodes in a hierarchy with a value assigned to each node and d3 will take care of the rest, returning a structure with the x, y, and radius of each circle.

That’s it! This is the engine that does all of the “magic” in the layout, in only 7 lines of code. Perfect… almost. These 7 lines of code are where we spend the vast majority of our time during the whole program. As I mentioned in part 2, one major disadvantage to circle packing is that it is quite computationally intensive.