Skip to content
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

PureComputedObservable to prevent memory leaks and improve performance? #1

Open
piever opened this issue Aug 6, 2020 · 2 comments
Open

Comments

@piever
Copy link

piever commented Aug 6, 2020

Even though it is not the latest or most popular framework, I really like Knockout.js as an inspiration for observables and bindings.

What we are struggling with are, as far as I understand, "computed observables", that is to say lift(f, obs::Observable...). I think that the key issue is that we implement lift with onany, and do it eagerly. I'll list below a few more thoughts that hopefully can help.

What you have implemented here seems like ComputedObservable from Knockout. Given that we have an abstract type AbstractObservable in Observables, I think this can peacefully coexist with the old Observable (which is the particular case with no inputs). Do you think it's possible to have it respect the Observables.AbstractObservable interface, while having an explicit list of input functions? I think of it as some sort of Observable with extra data. We could then change lift to actually give you this, rather than the plain Observable. In its finalizer, it could clear the input function from its input observables as soon as it goes out of scope.

OTOH, I believe that the biggest gains are to be had in the case where f (the function of the computing observable) is pure, i.e., it has no side effects. There, many optimizations become possible. See pure computed observable in Knockout.

I would suggest to try and focus on pure observables, making them lazy somehow. That is to say, the input function would be called only on getindex, provided that some of the inputs have changed. These can have no callbacks (or we should make sure that they are eager when they have at least a callback). My hope is that one can make a chain of these lazy pure observables from input to output, and then the drawing would be triggered by the render loop. I've made a POC implementation here, see test file for an example usage.

@jkrumbiegel
Copy link
Owner

jkrumbiegel commented Aug 20, 2020

I thought about this some more, and now I think my approach from Observables2 is also not necessarily a solution. It's problematic that any closure can trigger arbitrary observables, without being registered as a direct input. So the dependency graph I could obtain with my registration approach would still not reflect all the real connections.

Example:

x = Node(1)
y = Node(2)
z = lift(y) do y
    x[] = y + 2 # this connection from y to x would not be registered in Observables2 either, and is hard to detect correctly
    y * 2
end

Your approach with pure observables sounds interesting for computation pipelines, but for a lot of purposes, purity is not an option if you manipulate tons of plot object attributes.

As I see it, one of the main issues now is that it's easy to do something like lift(scene.px_area) without taking care that as long as that scene lives, no objects referenced in the closure can be freed. I've been using these types of closures a lot with MakieLayout, because I need to react to scene size changes. So definitely there are many unnoticed problems hidden under the surface, which only become apparent if a scene lives longer, and more and more objects are added to and deleted from it.

I think a couple of things could be worth thinking about:

  • change the return value of on from a Function to a ObservableFunction which keeps references to the input Observables and can be disconnected with disconnect!(obs_func). That would make it easier to disconnect, as you wouldn't have to store the input observables to call off on them and the returned Function.
  • ObservableFunction could also be made "weak" in a sense that it could call disconnect! when it is garbage collected. This could be useful because people could store the returned ObservableFunctions when they care about the connection they made being stable. They could store them in some array for example. And then when they are no longer of interest, that array can just be emptied, for example, causing all observables to disconnect.

@piever
Copy link
Author

piever commented Aug 21, 2020

There is a fair amount of discussion in JuliaGizmos/Observables.jl#1, which seems exactly the issues you are describing (how Observables plays with garbage collection), esp. JuliaGizmos/Observables.jl#1 (comment) may be useful.

Your approach with pure observables sounds interesting for computation pipelines, but for a lot of purposes, purity is not an option if you manipulate tons of plot object attributes.

Thinking more about it, it's not exactly about "purity". I suspect that has longs as callbacks are "idempotent" (that is, calling f twice is the same as calling it once, it'll just reset some fields to the same value) this should work. Conceptually the difference is whether you want updates to be "pulled" (by a render loop) or pushed by the observables. It seems that we are in a bit of an odd scenario with Makie where we do both. But these considerations may be a bit unrelated to what you're trying to do here, which seems more like the issue linked above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants