-
Notifications
You must be signed in to change notification settings - Fork 46
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
Node Creation Order Redesign #129
Conversation
Exciting! |
Nice! :) |
Will be hard to do it cleanly now I think, will be so much better if you can live without it ;) I've mostly used up the time budget I allocated to cleaning this up 😬 When I was thinking about this issue, I figured that if needed, people could just |
Is that ok? I'll give it a little more thought, it might be doable, but I'd much prefer not to 😄 I am curious about the situations you want to do it, I guess I'd be more inclined to looking into it if it makes doing some relatively common things much easier. |
Okay fair enough ;) My use case is that I have my API designed also for people who don't want to use Reactive: visualization = visualize(..., color = user_signal) # user_signal might also be a default, non Signal node created by me)
set_arg!(visualization, :color, newvalue) # should push into the signal, regardless of what got there Ah well, I'll need a redesign anyways... |
I thought I'd quickly whip up an alternative for you @SimonDanisch but I found a bug in I've also pushed the branch (action_queues_redesign) to this (JuliaGizmos) repo, to make it easier to try out. Anyway here's one way you could do what (I think) you want using
then you can update it with e.g.
or
or
|
I think I can actually allow pushing to non-input nodes in a relatively clean way. Will confirm shortly. |
Ok yes, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't complete, but just a few things I noticed while checking the code.
doc/action queue design.md
Outdated
|
||
#### Filter | ||
|
||
Filter works by setting the filter node's active field to false when the filter condition is false. Downstream nodes (nodes later in the current `action_queue`) check if at least one of their parents has been active, if none of them have been active then the node will not run it's action, thus propagating the filter correctly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's -> its
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ta - once!
doc/action queue design.md
Outdated
|
||
#### Flatten | ||
|
||
Implementation is a bit fiddly. See comments in the code for how the graph is rewired. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe one sentence on the overall purpose of flatten
? flatten
is not "externally" documented elsewhere, I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could definitely flesh that doc out a bit, but low priority for now. The docstring for flatten has a bit of info. Please do let me know if you have questions about the implementation though.
src/core.jl
Outdated
isempty(maybe_root_node.roots) ? [maybe_root_node] : maybe_root_node.roots | ||
|
||
function getroots(parents) | ||
roots = Dict{Signal, Bool}() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use a Set
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know why I did that, I like Set
s, maybe I'll remember a reason...
src/core.jl
Outdated
warn("closing a non-leaf node is not a good idea") | ||
empty!(n.actions) | ||
""" | ||
remove the action associated with the node from all of it's roots' action_queues |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's->its
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Twice! :/
src/operators.jl
Outdated
prev_timestep = timestep | ||
end | ||
end | ||
let |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you still need the let
? (Here and elsewhere.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. Didn't want to mess with closures around the action functions where locals are defined, but I think you're right that they'd probably still work without the let
s
src/operators.jl
Outdated
#mostly correct but prob not 100% | ||
queue = action_queues[root] | ||
for action in subtree_actions | ||
deleteat!(queue, findin(queue, 6)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the 6 about?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol I have no idea! Need to work that out, how does this even work!?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's supposed to be action
. I guess it worked but would have caused some things to be re-run twice since in some cases since they weren't removed properly.
src/operators.jl
Outdated
created by calling `map(f, dest)`, `merge(dest, s1)`, `filter(f, dest)`, etc. | ||
before `bind!(src, dest)`) may be updated more than once when `src` updates. To | ||
ensure signals update just once per `push!`, call `bind!` before adding signals | ||
dependent on `dest`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this also an issue with the former implementation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so, this was a change late in the day, I should add it to the downsides on the PR notes once I've checked it.
Note this is only a problem if someone does the thing that the little note says not to do, and has a downstream op that has some side-effect that they don't want running more than once per-push, or perhaps some expensive op that would run twice and thus impact performance.
src/operators.jl
Outdated
end | ||
Note that signals dependent on `dest` added before calling `bind!` (e.g. those | ||
created by calling `map(f, dest)`, `merge(dest, s1)`, `filter(f, dest)`, etc. | ||
before `bind!(src, dest)`) may be updated more than once when `src` updates. To |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check argument order to bind!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
After spending some time with the code in this branch again, trying to fix the |
Simpler design is coming along well so far. Will update over the weekend. |
Everything's looking pretty good I think on the simpler design. Tests are passing, managed to restore the old |
Pushed my latest, its running reasonably fast, about the same as the earlier version, possibly slightly faster, but its using fewer WeakRefs which may affect things slightly, since I haven't fixed up how it interacts with Garbage Collection yet. Will do that tomorrow. There was talk about changing how Reactive behaves with Garbage collection, if someone has some ideas, now would be a good time, otherwise I'll just restore the previous behaviour. There's a new, still minimal, design doc here: https://github.com/JuliaGizmos/Reactive.jl/blob/action_queues_redesign/doc/Design%20Overview.md |
I think this is pretty much ready now |
doc/Design Overview.md
Outdated
|
||
#### Filter | ||
|
||
Filter works by setting the filter node's active field to false when the filter condition is false. Downstream/descendent nodes check if at least one of their parents has been active, if none of them have been active then the node will not run its action, thus propagating the filter correctly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
neat.
doc/Design Overview.md
Outdated
|
||
Each Signal also has a field `active` which stores whether or not the node was active (had its actions run) in the current push. A Signal will be set to active if it is `push!`ed to, or if any of its parent `Signal`s were active. | ||
|
||
On processing each `push!`, we run through `nodes` and execute the actions of each node if any of its parents were active. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the simplicity of this design very much. The global channel makes sure that two push!es don't overlap, so this seems correct.
1. Each signal has a field `preservers`, which is a `Dict{Signal, Int}`, which basically stores the number of times `preserve(x)` has been called on each of its child nodes `x` | ||
1. Crucially, this Dict holds an active reference to `x` which stops it from getting GC'd | ||
1. `unpreserve(x)` reduces the count of `preservers[x]` in all of x's parents, and if the count goes to 0, deletes the entry for (reference to) `x` in the `preservers` Dict thus freeing x for garbage collection. | ||
1. Both `preserve` and `unpreserve` are also called recursively on all parents/ancestors of `x`, this means that all ancestors of x in the signal graph will be preserved, until their parents are GC'd or `unpreserve` is called the same number of times as `preserve` was called on them, or any of their descendants. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great to have this explanation! 💯
src/core.jl
Outdated
nodes[n] = nothing | ||
function (::Type{Signal{T}}){T}(v, parents, actions, pres, name) | ||
id = length(nodes) + 1 | ||
n=new{T}(id, v, parents, false, Action[], pres, name, backtrace()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated, but just wondering if there's any serious problems with storing the backtrace anyway even in non-debug mode. This could potentially allow better error messages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems like a really good idea!
src/core.jl
Outdated
immutable Action | ||
recipient::WeakRef | ||
f::Function | ||
end | ||
|
||
const nodes = WeakRef[] #stores the nodes in order of creation (which is a topological order for execution of the nodes' actions) | ||
const edges = Vector{Int}[] #parents to children, useful for plotting graphs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. Should be easy to output graphviz "dot" strings from this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, also check this:
using Reactive, LightGraphs, Plots, PlotRecipes
pyplot()
s = Signal(1; name="sig 1")
m2 = map(x->2x, s; name="x2")
m3 = map(x->3x, s; name="x3")
function LightGraphs.DiGraph{T <: Integer}(edgelists::Vector{Vector{T}})
g = DiGraph(length(edgelists))
for (v1, edgelist) in enumerate(edgelists)
foreach(v2->add_edge!(g, v1, v2), edgelist)
end
g
end
g = DiGraph(Reactive.edges)
graphplot(g, arrow = arrow(:simple, :head, 1.5, 1.0) , names=map(nr->nr.value.name, Reactive.nodes), linewidth=5)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool :)
src/core.jl
Outdated
function add_action!(f, node, recipient) | ||
a = Action(WeakRef(recipient), f) | ||
function add_action!(f, node) | ||
a = Action(WeakRef(node), f) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems weird that the recipient is actually the sender. Why not just store a = f
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was planning to do that. action.recipient
isn't used anymore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed Actions, just using Functions now
src/core.jl
Outdated
cleanup_actions(node::Signal) = | ||
node.actions = filter(isrequired, node.actions) | ||
|
||
send_value!(node::Signal, x) = (node.value = x) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe this shouldn't be a function then. send_value!
is a misleading name. Tim Holy pointed this out before, but I wanted to implement this differently using channels. It doesn't make sense anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set_value!
could be useful to isolate the state change. I imagine it would get inlined everywhere and not affect performance, but I'll check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, just changing the name to set_value
would be an improvement I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set_value
or set_value!
?
println(io) | ||
println(io, "to node") | ||
print(io, " ") | ||
show(io, node) | ||
show(io, pushnode) | ||
println(io) | ||
println(io) | ||
println(io, "error at node: $error_node") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the backtrace can be used here (unrelated to this PR).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep
src/operators.jl
Outdated
end | ||
end | ||
add_action!(output) do | ||
output.value = f(map(value, inputs)...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Loving the simplification here!
src/operators.jl
Outdated
push!(output, value(input)) | ||
add_action!(output) do | ||
# only push when input is active (avoids it pushing to itself endlessly) | ||
input.active && push!(output, value(input)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this will be incorrect in the synchronous case. Probably good to check the mode and throw a warning here before the push... (unrelated to this PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I haven't given much thought to what will break in synchronous case. Will think about it.
# a signal that is the flatten parent sigsig's (c's) current value ("a" here) | ||
# then when the sigsig gets pushed another value, you want the map to | ||
# still update on changes to a, even after the map is "rewired" when a | ||
# new value is pushed to c. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
push!(sx, 3) | ||
Reactive.run_till_now() | ||
@fact value.([sx, s1x1, s1x2, s2x, s3x]) --> [3, 3, 3, 6, 9] | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
victory!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review!
Tests are failing on Travis, I'm guessing GC related, they work on my local. Will check it out tomorrow. |
While I was looking at GC issues, I noticed pushing to non-input nodes wasn't working, and as a result I also made some changes to the way GC'd nodes are removed from It's running reasonably fast. Maybe you wizards can make it run even faster - varies between 2.2x to 2.8x slower than non-reactive function calls on Simon's benchmark on my local. Before the changes to make it do the right thing with GC, it was closer to 2x slower. Ok now I really think this branch is almost ready to go - just need to squash the commits and clean up the commit messages. I guess I'll make it into 2 commits? One for the previous design and one for the new one? Anyway, I think it's simpler, faster, and more correct, so, yeah! 😄 |
f349770
to
82b6761
Compare
This is looking very good. The only minor comment I have is about It would be nice to bring this down to <= 10 meaningful commits. I understand you're short on spare time, so I'm fine with you doing whatever is the easiest option for you. Thanks for taking this up and putting in so much amazing effort! 🎉 |
Ha. I suppose we could add a warning like this when throttle is called: "Throttle's behaviour has changed, see
Yep I'll squash them to some smaller number in the next few days, see how I go.
Thanks to you and @timholy for the reviews on this stupidly large PR 😄 I'm really glad its worked out 🎉 |
Gets signals updating in a correct (topological) order (the order Signals are created), which fixes bugs in signals updating on certain signal graphs. See the last three testsets in node_order.jl for examples of where the current (breadth-first) and pre JuliaGizmos#113 (depth-first) designs had bugs. Makes async optional through Reactive.run_async(false) - though it's not well tested, so async is on by default. Adds some docs about the design and the garbage collection/preserve mechanism in doc/Design Overview.md and doc/dev notes.md Improves performance, runs faster than current master on @SimonDanisch's benchmark - included in benchmark/ReactBench.jl - and slightly (though not much) faster in synchronous mode. Is now around 2.5x slower than the non-reactive version on my machine (previous master was about 4x slower). Adds a PkgBenchmark - based off Shashi's speedups_maybe branch. Fixes a bug in error reporting. adds bound_srcs, bound_dests see OP of JuliaGizmos#119 Adds debounce - which behaves as per previous throttle Fixes throttle issue in JuliaGizmos#123, but note this changes the behaviour of throttle, use debounce if you want the previous behaviour.
e158ce3
to
e249069
Compare
Ok done. With all the merge commits in this (old) branch, rebase was making life difficult, so I just started afresh and used Anyway, I think it's cowabunga time! 😄 |
Works for me, merging! A bit sad that this won't help you much in the dark-green-squares-in-your-github-profile game though. 😜 |
Haha, just one light green square - very rough - not to mention still being 5th in the number of commits leaderboard for this repo. Regardless - party time, excellent! 🎉 🎉 🎉 |
Heroes are not measured in numbers of commits! Thanks, @JobJob! |
I'm very pleased to report that for both GtkReactive and a just-submitted rewrite of ImageView, this huge transition was absolutely seamless---only one line of test code will need changing (an explicit test of how signals print in GtkReactive). Importantly, all the "real" functionality Just Worked. Really amazing work, @JobJob! |
Great to hear! |
Finally got around to cleaning this branch up.
This PR:
thea correct (topological) order (the order Signals are created), which fixes bugs in signals updating on certain signal graphs. See the last three test sets here for examples of where the current (breadth-first) and pre Use an action queue/set to ensure actions are performed in the correct order and aren't repeated #113 (depth-first) designs had bugs.Reactive.run_async(false)
- though it's not well tested, so async is on by default.bound_srcs
,bound_dests
see OP of BindedTo #119debounce
- which behaves as per previous throttlethrottle
issue inthrottle
signal not updated #123, but note this changes the behaviour of throttle, use debounce if you want the previous behaviour.Downsides:
Can'tyou can nowpush!
to non input nodes - those created withSignal(...)
flatten
bind
implementation is a little bit more complicated, though may be more correct in some cases.Haven't checked if it plays nice with Garbage CollectionTodo:
debug_memory
code