Skip to content

Trailing throttle syncs to front-end #1036

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

Merged
merged 6 commits into from
Apr 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@
"prettier.printWidth": 160,
"prettier.tabWidth": 4,
"prettier.semi": false,
"prettier.quoteProps": "consistent"
"prettier.quoteProps": "consistent",
"julia.format.calls": false,
"julia.format.comments": false,
"julia.format.curly": false,
"julia.format.docs": false,
"julia.format.indents": false,
"julia.format.iterOps": false
}
60 changes: 46 additions & 14 deletions src/evaluation/Run.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import REPL: ends_with_semicolon
import REPL:ends_with_semicolon
import .Configuration
import .ExpressionExplorer: FunctionNameSignaturePair, is_joined_funcname, UsingsImports, external_package_names

Expand Down Expand Up @@ -47,7 +47,8 @@ function run_reactive!(session::ServerSession, notebook::Notebook, old_topology:
end

# Send intermediate updates to the clients at most 20 times / second during a reactive run. (The effective speed of a slider is still unbounded, because the last update is not throttled.)
send_notebook_changes_throttled = throttled(1.0/20, 0.0/20) do
# flush_send_notebook_changes_throttled,
send_notebook_changes_throttled, flush_notebook_changes = throttled(1.0 / 20) do
send_notebook_changes!(ClientRequest(session=session, notebook=notebook))
Copy link
Collaborator Author

@pankgeorg pankgeorg Mar 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code will run at most once per 1/20 sec, @async.
BUT there is no guarantee I can think of that code that mutates (notebook, session) will not run right before/right after/inbetween.

e.g. runsingle! at line 80-something

end
send_notebook_changes_throttled()
Expand Down Expand Up @@ -90,7 +91,7 @@ function run_reactive!(session::ServerSession, notebook::Notebook, old_topology:
end

notebook.wants_to_interrupt = false
send_notebook_changes!(ClientRequest(session=session, notebook=notebook))
flush_notebook_changes()
# allow other `run_reactive!` calls to be executed
put!(notebook.executetoken)
return new_order
Expand Down Expand Up @@ -193,20 +194,51 @@ update_run!(args...) = update_save_run!(args...; save=false)



"Create a throttled function, which calls the given function `f` at most once per given interval `max_delay`.
"""
throttled(f::Function, timeout::Real)

It is _leading_ (`f` is invoked immediately) and _not trailing_ (calls during a cooldown period are ignored).
Return a function that when invoked, will only be triggered at most once
during `timeout` seconds.
The throttled function will run as much as it can, without ever
going more than once per `wait` duration.
Inspired by FluxML
See: https://github.com/FluxML/Flux.jl/blob/8afedcd6723112ff611555e350a8c84f4e1ad686/src/utils.jl#L662
"""
function throttled(f::Function, timeout::Real)
tlock = ReentrantLock()
iscoolnow = false
run_later = false

An optional third argument sets an initial cooldown period, default is `0`. With a non-zero value, the throttle is no longer _leading_."
function throttled(f::Function, max_delay::Real, initial_offset::Real=0)
local last_run_at = time() - max_delay + initial_offset
# return f
() -> begin
now = time()
if now - last_run_at >= max_delay
function flush()
lock(tlock) do
run_later = false
f()
last_run_at = now
end
nothing
end

function schedule()
@async begin
sleep(timeout)
if run_later
flush()
end
iscoolnow = true
end
end
schedule()

function throttled_f()
if iscoolnow
iscoolnow = false
flush()
schedule()
else
run_later = true
end
end

return throttled_f, flush
end



84 changes: 84 additions & 0 deletions test/Throttled.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Pluto:throttled

@testset "Throttled" begin
x = Ref(0)

function f()
x[] += 1
end
f()
# f was not throttled
@test x[] == 1

dt = 4 / 100
ft, flush = throttled(f, dt)

for x in 1:10
ft()
end
# we have an initial cooldown period in which f should not fire...
# ...so x is still 1...
@test x[] == 1
sleep(2dt)
# ...but after a delay, the call should go through.
@test x[] == 2

# sleep(0) ## ASYNC MAGIC :(

# at this point, the *initial* cooldown period is over
# and the cooldown period for the first throttled calls is over

for x in 1:10
ft()
end
# we want to send plots to the user as soon as they are available,
# so no leading timeout
@test x[] == 3
# the 2nd until 10th calls were still queued
sleep(2dt)
@test x[] == 4


for x in 1:5
ft()
sleep(1.5dt)
end
@test x[] == 9
sleep(2dt)
@test x[] == 9


###

# "call 1"
ft()
# no leading timeout, immediately set to 10
@test x[] == 10

sleep(.5dt)

# "call 2"
ft()
# throttled
@test x[] == 10

sleep(.7dt)

# we waited 1.2dt > dt seconds since "call 1", which should have started the dt cooldown. "call 2" landed during that calldown, and should have triggered by now
@test x[] == 11


sleep(2dt)
@test x[] == 11

###

ft()
ft()
@test x[] == 12
flush()
@test x[] == 13
sleep(2dt)
@test x[] == 13

end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ include("./Notebook.jl")
include("./Configuration.jl")
include("./Analysis.jl")
include("./Firebasey.jl")
include("./Throttled.jl")

# TODO: test PlutoRunner functions like:
# - from_this_notebook
Expand Down