performance issues for many nodes
armanbilge opened this issue ยท 11 comments
This was really easy, but seems that we will need to do something more clever.
calico/calico/src/main/scala/calico/dsl.scala
Line 385 in d052dd2
ok, I was being hasty. just making a bunch of todos and checking a box, which doesn't involve replaceChildren
seems to be very slow. So something more fundamental is at play here.
Sadly, I wonder if the FS2 Signal
implementation is just too heavy for the browser. For example, every time a Signal
updates its listeners, they all have to re-subscribe to receive the next event. This makes for an elegant implementation, but means a lot of allocations ...
@mn98 just mentioned to me that they were a bit spooked by this issue :) here are a few thoughts on it:
- I think Calico deserves its own
Signal
implementation, that's optimized for working with lenses and some other things. I'm hopeful this would solve the TodoMVC problems, I just haven't sat down to give it a try! Indeed, I've optimized nothing at this point, so there may be many things to improve. - There could be better patterns for state management in Calico than what I've currently done for the TodoMVC.
- However, it's possible that the FS2 pull-based model is not ideal for frontend UI, due to the additional overhead of re-subscribing (pulling) after each event. Push-based frameworks can listen passively.
If you are running into performance problems with Calico, please open an issue or comment here. The extra datapoints will help point us in the right direction :)
Hi @armanbilge . I recently had a play with both laminar and calico, trying to implement the same small POC with both. Purely out of curiosity, I don't do any front end development currently.
I noticed that the script evaluation for calico was much slower than laminar (eg 100ms vs 1 second for initial render).
Some of my calico usage may not be optimal. I'm happy to share my example project with you if want.
@lukestephenson thanks for trying calico! Sure that would be great, would be great to take a look. Are these numbers with fullOptJS
/ fullLinkJS
?
I was using fastOptJS
. It is a lot faster with fullOptJS
. I hadn't expected much difference. I thought it would only affect the size of the bundle (and initial download), not the execution of the scripts.
@lukestephenson thanks for reporting back, good to know!
Currently Cats Effect enables several debugging features in dev mode (namely, tracing and fiber dumps) that are otherwise disabled in production. Due to the significant performance hit we've decided to disable these by default in Cats Effect 3.4.0. See:
Until then, you can add the following to your HTML to disable tracing in dev mode as well.
<script type="text/javascript">
var process = { env: { 'CATS_EFFECT_TRACING_MODE': 'NONE' } };
</script>
ok. While performance is good for my trivial use case, I can see some behaviour I wasn't expecting. I'll try to describe that:
Here is my simplified (and contrived) use case:
- I'm rendering a table that is 5 * 5.
- To represent the cells of that table I'm using a
Signal[List[List[String]]]
. - Now the input to my component is a
Signal[Position]
, where position iscase class Position(x, y)
. That signal representing which cell to render the message "hello" gets transformed into something like (simplified here to a 2 * 2 grid):
List(
List("hello", ""),
List("", "")
)
- And then as I'm building up the dom, that signal gets mapped down to represent individual rows and then the individual cells.
Now if I add a dom.console.log
to where the signal is transforming from the Position => List[List[String]]
, what I see is that transformation happens 30 times (5 times for the 5 rows, then 25 times for the individual cells).
While my transformation is fast, I can see this getting expensive pretty quickly.
Is this what this issue was originally raised for?
@lukestephenson sorry, I couldn't completely follow your example. Would you mind sharing a bit more code, particularly where you transform the Signal[Position]
to the Signal[List[List[String]]]
?
In general however there are many low-hanging optimizations possible for Signal
. Calico currently uses the Signal
implementation from FS2 which just does the naive thing :) e.g. for map
:
In these instances the Signal
will propagate the event, even if the mapped value hasn't actually changed.
This can definitely be substantially improved, either in FS2 or perhaps Calico should ship its own Signal
implementation optimized for these common use-cases. I'm just operating a bit lazily, waiting for folks to complain :) perhaps it's time to circle back to this.
Thanks again for giving this a try and sharing feedback!
I've shared a repo with you. It's not a minimal reproduction, so has a bit of noise beyond the use case I outlined above. I can try to find some time to narrow that down though to a smaller use case if it isn't easy enough to follow.
@lukestephenson thanks for that, actually it was perfect :) very easy to run and follow.
Yes that helps: IIUC essentially you are mapping from Grid => Row
and then mapping again from Row => Cell
.
I think what is surprising is that each Signal[Cell]
is actually subscribing directly to the Signal[Grid]
and fuses the transform Grid => Row => Cell
. This is why the Grid => Row
transformation is running so frequently: the Signal[Row]
intermediary has been elided!
Alternatively, we could actually materialize a Signal[Row]
, that caches the result of the transform Grid => Row
. Then, Signal[Cell]
s could subscribe to the Signal[Row]
and only require the Row => Cell
transform by reusing the cached Row
. This has the advantages of caching, with the disadvantages of a bigger memory footprint and more active subscriptions. But for a sufficiently expensive transformation across many subscribers this would be amortized :)
In fact, you can try this out for yourself by explicitly creating a SignallingRef[Row]
and doing something like:
gridSigRef.map(getRow).foreach(rowSigRef.set).compile.drain.background // Resource[IO, Unit]
It should be pretty easy to add some syntax to Calico for this, that would directly return a Resource[IO, Signal[IO, Row]]
. I will look into that shortly :) update, see: