I’ve been experimenting with using vega-lite in clojurescript to create some simple animated graphs using the reagent components provided by OZ from @Christopher_Small and the underlying Hanami library from @jsa-aerial.
See the visualisations I created here. The visualisation in the first tab performs OK.
The graphs are animated by updating the atom containing the data for the graph which automatically updates the graph in the DOM.
I’m finding though that with more complex animated graphs the memory used by my javascript very quickly starts to mount to 100s of MB! With each update of the graph the heap size increases. See for example animation in these tabs:
Is there a problem with garbage disposal in the Oz implementation of vega-lite reagent components that we could address? Or is the issue the way I have coded these visualisations.
I can’t find a function in the Hanami library to include individual vega-lite reagent components in hiccup but I wonder if the underlying hanami library allows for more memory efficient updating of the graphs?
I have no idea how to solve it but I took a look at the performance profiles and it does seem like a memory leak on refresh (the number of points graphed seem pretty much irrelevant)
Another route is to just use the View API and treat the chart as a custom component with retained state that you manage. I have done a bit of lower level work trying to get vega performant for live updating stuff typically as part of concurrent visualizations or dashboards (or in conjuction with libs like cesium). I did not go the same route Oz did (although I used vega-tools for spec parsing, and based my original reagent component off of Oz’s early example). Instead, I use the view api and retain my own internal database of the existing views (vega View objects), and then use vega changesets to push updates to the View’s data (all wrapped with cljs functions to ease the plumbing). This ends up being substantially more efficient in practice (both in rendering and apparently memory control) although there are still some walls that Vega runs into due to its internal model (e.g. the DAG that defines transforms and views has to be run on every changeset, with user-defined transforms being particularly expensive if you are not careful).
Oz could maybe try some of the approaches mentioned in the vega thread (specifically calling finalize after invoking vegaEmbed). The current problem with Oz may be that it retains no reference to the view that was created, so on component-will-update, you have nothing to invoke finalize on (this could be remedied). There is apparently a view-callback keyword option you can pass a function via, which will expose the View object.
I have been looking at Hanami to see how they handle using vega-lite but can’t get it running. Here is my attempt at getting hanami working with shadow-cljs:
Would be cool if @jsa-aerial had some feedback of where I might be going wrong.
@jsa-aerial can correct me if I am misreading, but it looks like the update model in hanami is following a similar style that Oz used: namely leveraging vegaEmbed and reagent component update methods:
I expect that if the issues I mentioned before are causing the memory leak (namely Vega’s expectation of having finalize invoked to allow gc), then you will see the same problem in hanami since the reliance on vega is similar.
I noticed folks in the vega issues thread still getting memory leaks, and that it was unresolved to date. I am betting vegaEmbed is the prime suspect for the moment.
OMG; Thanks so much for raising this issue @jamiepratt. And doubly so to @zcaudate and @joinr for helping to track down the issue. I think you’re right @joinr; Makes a ton of sense that it’s just not GCing without finalizing being called.
I have a feeling that always calling this after creating the view would be problematic for certain applications that need to register their own callbacks or signal handlers. But I think we could probably use React’s componentWillUnmount to call finalize, which should do the right thing.