-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uncached Computed? #151
Comments
IMO the uncached version of this: let doubled = new State.Computed(() => someSignal.get() * 2); Is just this: let double = () => someSignal.get() * 2; I don't think there's anything that the proposal needs to spec in this regard. |
An uncached computed is still a signal so it can participate in the signal graph.
This is confusing since presumably the calculation is almost always slower than an identify check v. the previous value. Is the idea that the reduction in memory use / gc cost from not storing the previous value improved performance? |
I'm not the best person to ask about cacheless computed 😅 In Ember, the move away from a cached-computed was This is likely tangential to the request for cacheless computed, and I probs should have omitted that context -- I'm still learning what these are used for. I've updated the post to specify some TODOs for us to figure out |
Having uncached computeds is very useful specifically because for some derived values, rerunning it every time you access it is simply cheaper than propagating the change through the reactive graph. Think It is really good to have all reactive values implement the same interface, whether a Signal, Computed, or derived signal/uncached computed value, both in terms of DX (as in Solid’s “to access the value you call it as a function) and in terms of framework internals (as in Solid’s “if you pass the renderer a function, it treats it as a reactive value”) What this implies, to me, would be that if the proposal offers an interface for signals there should be a cheap wrapper for uncached computeds/derived signals that implements that same interface. (In Solid this is just a function because all signals are functions.) |
Is this necessary for a proposal that is meant to be mainly used by framework authors though? As I understand it anyway. |
@fabiospampinato mentions that zero-arg functions are effectively uncached computeds. Because dependency tracking is contextual, it doesn't matter whether there's extra stack frames in between the consumer (current reactive context) and the signal being read. |
In my opinion, there are two reasons to support uncached computeds:
Both of them amount to the same thing: there are very real scenarios that don't need caching, and where caching clearly creates extra overhead. In this sort of design, I find the argument "can't you just cache it anyway" to be fairly weak as a motivation for adding additional overhead to the design of the lowest-level available primitive for interacting with "tracking frames." |
I should add that several people have pressed me to identify any semantic problem with building in caching at the lowest level, and I haven't been able to. As far as I can tell, you can always throw away a computed when you're done with it. In some cases, you might throw away the computed immediately (when using I would be persuaded if someone demonstrated that these sources of overhead are negligible in practice. That said, in a lowest-level design like The argument in favor of the higher level design comes down to "This low-level primitive would have a slightly simpler surface area if we coupled these concerns, and we think that smaller surface area is worth the potential cost in added overhead." If you think about it, that's a somewhat strange way to approach the design space 😄 |
@wycats Do you have a particular low-level design in mind here? I'm definitely sympathetic to these concerns, and I think we might well be able to find some lower-level more orthogonal primitives if we decided it was worth it. Pieces I've previously been turning around, in this general area: (1.) A minimal overhead way to observe the tracking effects of a piece of code, without creating a throwaway Computed. This might be as simple as: Signal.withTracker: <R>(track: <T>(signal: State<T> | Computed<T>) => T, fn: () => R) => R A version where the (2.) "Expert nodes" (to borrow incremental's terminology) which subsume both Computed and Watcher. These would manually manage their dependencies like Watchers, and get to override any or all of:
If you have introspection to iterate over your dependencies, this is enough to implement Computed (including its Are we interested in exploring either of these directions? Or does the much simpler "acts like a function, has the API of a Signal" primitive cover the cases we're interested in, without any of these "more primitive" primitives being needed? |
I thought this for a moment, but it’s very important to not overlook that the example does not participate in the graph. The benefit of a stateless (no cache) computed signal is the ability to control memory performance and still participate in the graph (stay on the control-flow aspect of signal graphs). This is the reason why Flash computed are stateless by default (see @flash-js/core on NPM). Processing large state within the graph will not consume memory if a computed is stateless. This is useful when you want to use some computed/derived value from state in two separate code paths in your data pipeline without consuming more memory. There isn’t a clear way to propagate a derived value in more than one direction in your state graph when using an anonymous function without doubling compute time. Of course, if a computed signal is used frequently making compute time more expensive then caching is available as an opt in strategy. Flash library proposes this as a type of computer signal called “reducers”. Not only is this useful for trading off time complexity for space complexity in your app, but it is also useful for other control flow mechanisms like batching for back pressure and other control-flow mechanisms that you couldn’t do with a single computed signal as proposed by this spec. A bit out of the scope of my main point, but relevant as it is a case in point for stateless computed signals. |
As an API design for uncached computeds: what if we keep it simple and allow |
In many circumstances, the overhead of maintaining a cache is less performant than calculating the values fresh each computation.
Example, if you all you need is a derived value at the edge of rendering:
^ will be more expensive than
in classes this would be the equivelent of a getter:
Ember actually had started with cached-by-default-no-opt-out computed properties, and when we moved away from that we saw massive performance gains.
Todo:
The text was updated successfully, but these errors were encountered: