From be3ccb8ef4e8ae7f0362cbad3d2919d56c3c4433 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 5 Aug 2024 13:14:19 +0200 Subject: [PATCH 1/4] remove console log --- frontend/components/CellInput.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/components/CellInput.js b/frontend/components/CellInput.js index 35773c436d..91065a7b9b 100644 --- a/frontend/components/CellInput.js +++ b/frontend/components/CellInput.js @@ -461,8 +461,6 @@ export const CellInput = ({ if (show_static_fake) return if (dom_node_ref.current == null) return - console.log("Rendering cell input", cell_id) - const keyMapSubmit = (/** @type {EditorView} */ cm) => { autocomplete.closeCompletion(cm) on_submit() From ab4c1a6e4fd323373e209d69dc84ef5ae41afa8b Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Tue, 6 Aug 2024 11:43:55 +0200 Subject: [PATCH 2/4] Performance: skip status sync throttle during run --- src/evaluation/Run.jl | 10 ++- src/evaluation/Throttled.jl | 98 ++++++++++++++++++++++-------- src/evaluation/WorkspaceManager.jl | 2 +- src/webserver/Authentication.jl | 3 +- src/webserver/Dynamic.jl | 2 +- src/webserver/SessionActions.jl | 7 +-- src/webserver/Status.jl | 2 +- test/Throttled.jl | 55 ++++++++++++++++- 8 files changed, 138 insertions(+), 41 deletions(-) diff --git a/src/evaluation/Run.jl b/src/evaluation/Run.jl index f5237014e2..8081af65ee 100644 --- a/src/evaluation/Run.jl +++ b/src/evaluation/Run.jl @@ -1,5 +1,6 @@ import REPL: ends_with_semicolon import .Configuration +import .Throttled import ExpressionExplorer: is_joined_funcname """ @@ -119,10 +120,13 @@ function run_reactive_core!( # Save the notebook. In most cases, this is the only time that we save the notebook, so any state changes that influence the file contents (like `depends_on_disabled_cells`) should be behind this point. (More saves might happen if a macro expansion or package using happens.) save && save_notebook(session, notebook) - + # 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.) # flush_send_notebook_changes_throttled, - send_notebook_changes_throttled, flush_notebook_changes = throttled(1.0 / 20) do + send_notebook_changes_throttled = Throttled.throttled(1.0 / 20; runtime_multiplier=2.0) do + # We will do a state sync now, so that means that we can delay the status_tree state sync loop, see https://github.com/fonsp/Pluto.jl/issues/2978 + force_throttle_without_run(notebook.status_tree.update_listener_ref[]) + # State sync: send_notebook_changes!(ClientRequest(; session, notebook)) end send_notebook_changes_throttled() @@ -229,8 +233,8 @@ function run_reactive_core!( end notebook.wants_to_interrupt = false - flush_notebook_changes() Status.report_business_finished!(run_status) + flush(send_notebook_changes_throttled) return new_order end diff --git a/src/evaluation/Throttled.jl b/src/evaluation/Throttled.jl index f0fa10352d..ecb6b5c4d0 100644 --- a/src/evaluation/Throttled.jl +++ b/src/evaluation/Throttled.jl @@ -1,3 +1,54 @@ +module Throttled + +import Base.Threads + + +struct ThrottledFunction + f::Function + timeout::Real + runtime_multiplier::Float64 + tlock::ReentrantLock + iscoolnow::Ref{Bool} + run_later::Ref{Bool} + last_runtime::Ref{Float64} +end + +"Run the function now" +function Base.flush(tf::ThrottledFunction) + lock(tf.tlock) do + tf.run_later[] = false + tf.last_runtime[] = @elapsed result = tf.f() + result + end +end + +"Start the cooldown period. If at the end, a run_later[] is set, then we run the function and schedule the next cooldown period." +function schedule(tf::ThrottledFunction) + # if the last runtime was quite long, increase the sleep period to match. + Timer(tf.timeout + tf.last_runtime[] * tf.runtime_multiplier) do _t + if tf.run_later[] + flush(tf) + schedule(tf) + else + tf.iscoolnow[] = true + end + end +end + +function (tf::ThrottledFunction)() + if tf.iscoolnow[] + tf.iscoolnow[] = false + flush(tf) + schedule(tf) + else + tf.run_later[] = true + end + nothing +end + + + + """ throttled(f::Function, timeout::Real) @@ -17,35 +68,26 @@ function throttled(f::Function, timeout::Real) iscoolnow = Ref(false) run_later = Ref(false) - function flush() - lock(tlock) do - run_later[] = false - f() - end - end - - function schedule() - @async begin - sleep(timeout) - if run_later[] - flush() - end - iscoolnow[] = true - end - end - schedule() + tf = ThrottledFunction(f, timeout, runtime_multiplier, tlock, iscoolnow, run_later, last_runtime) + + # we initialize hot, and start the cooldown period immediately + schedule(tf) + + return tf +end - function throttled_f() - if iscoolnow[] - iscoolnow[] = false - flush() - schedule() - else - run_later[] = true - end - end +""" +Given a throttled function, skip any pending run if hot (but let the cooldown period continue), or start the cooldown period if cool. This forces the throttled function to not fire for a little while. - return throttled_f, flush +Argument should be the first function returned by `throttled`. +""" +function force_throttle_without_run(tf::ThrottledFunction) + # (we can access variables from the function closure hihi) + tf.run_later[] = false + if tf.iscoolnow[] + tf.iscoolnow[] = false + schedule(tf) + end end @@ -68,4 +110,6 @@ function simple_leading_throttle(f, delay::Real) f(args...;kwargs...) end end +end + end \ No newline at end of file diff --git a/src/evaluation/WorkspaceManager.jl b/src/evaluation/WorkspaceManager.jl index 9183fc55cc..3308fbf7e8 100644 --- a/src/evaluation/WorkspaceManager.jl +++ b/src/evaluation/WorkspaceManager.jl @@ -181,7 +181,7 @@ function start_relaying_self_updates((session, notebook)::SN, run_channel) end function start_relaying_logs((session, notebook)::SN, log_channel) - update_throttled, flush_throttled = Pluto.throttled(0.1) do + update_throttled = Pluto.Throttled.throttled(0.1) do Pluto.send_notebook_changes!(Pluto.ClientRequest(; session, notebook)) end diff --git a/src/webserver/Authentication.jl b/src/webserver/Authentication.jl index 45644703d5..146ce2fea1 100644 --- a/src/webserver/Authentication.jl +++ b/src/webserver/Authentication.jl @@ -1,3 +1,4 @@ +import .Throttled """ Return whether the `request` was authenticated in one of two ways: @@ -29,7 +30,7 @@ function is_authenticated(session::ServerSession, request::HTTP.Request) end # Function to log the url with secret on the Julia CLI when a request comes to the server without the secret. Executes at most once every 5 seconds -const log_secret_throttled = simple_leading_throttle(5) do session::ServerSession, request::HTTP.Request +const log_secret_throttled = Throttled.simple_leading_throttle(5) do session::ServerSession, request::HTTP.Request host = HTTP.header(request, "Host") target = request.target url = Text(string(HTTP.URI(HTTP.URI("http://$host/"); query=Dict("secret" => session.secret)))) diff --git a/src/webserver/Dynamic.jl b/src/webserver/Dynamic.jl index a0ef4bbcd1..fbaf9ba8b5 100644 --- a/src/webserver/Dynamic.jl +++ b/src/webserver/Dynamic.jl @@ -414,7 +414,7 @@ end responses[:reset_shared_state] = function response_reset_shared_state(🙋::ClientRequest) delete!(current_state_for_clients, 🙋.initiator.client) - send_notebook_changes!(🙋; commentary=Dict(:from_reset => true)) + send_notebook_changes!(🙋; commentary=Dict(:from_reset => true)) end responses[:run_multiple_cells] = function response_run_multiple_cells(🙋::ClientRequest) diff --git a/src/webserver/SessionActions.jl b/src/webserver/SessionActions.jl index a8ef828d12..b3d01b89bc 100644 --- a/src/webserver/SessionActions.jl +++ b/src/webserver/SessionActions.jl @@ -1,6 +1,6 @@ module SessionActions -import ..Pluto: Pluto, Status, ServerSession, Notebook, Cell, emptynotebook, tamepath, new_notebooks_directory, without_pluto_file_extension, numbered_until_new, cutename, readwrite, update_save_run!, update_nbpkg_cache!, update_from_file, wait_until_file_unchanged, putnotebookupdates!, putplutoupdates!, load_notebook, clientupdate_notebook_list, WorkspaceManager, try_event_call, NewNotebookEvent, OpenNotebookEvent, ShutdownNotebookEvent, @asynclog, ProcessStatus, maybe_convert_path_to_wsl, move_notebook!, throttled +import ..Pluto: Pluto, Status, ServerSession, Notebook, Cell, emptynotebook, tamepath, new_notebooks_directory, without_pluto_file_extension, numbered_until_new, cutename, readwrite, update_save_run!, update_nbpkg_cache!, update_from_file, wait_until_file_unchanged, putnotebookupdates!, putplutoupdates!, load_notebook, clientupdate_notebook_list, WorkspaceManager, try_event_call, NewNotebookEvent, OpenNotebookEvent, ShutdownNotebookEvent, @asynclog, ProcessStatus, maybe_convert_path_to_wsl, move_notebook!, Throttled using FileWatching import ..Pluto.DownloadCool: download_cool import HTTP @@ -186,10 +186,9 @@ function add(session::ServerSession, notebook::Notebook; run_async::Bool=true) end end - notebook.status_tree.update_listener_ref[] = first(throttled(1.0 / 20) do - # TODO: this throttle should be trailing + notebook.status_tree.update_listener_ref[] = Throttled.throttled(1.0 / 8; runtime_multiplier=4.0) do Pluto.send_notebook_changes!(Pluto.ClientRequest(; session, notebook)) - end) + end return notebook end diff --git a/src/webserver/Status.jl b/src/webserver/Status.jl index 4c37c15de8..72bfc023c5 100644 --- a/src/webserver/Status.jl +++ b/src/webserver/Status.jl @@ -8,7 +8,7 @@ Base.@kwdef mutable struct Business started_at::Union{Nothing,Float64}=nothing finished_at::Union{Nothing,Float64}=nothing subtasks::Dict{Symbol,Business}=Dict{Symbol,Business}() - update_listener_ref::Ref{Function}=Ref{Function}(_default_update_listener) + update_listener_ref::Ref{Any}=Ref{Any}(_default_update_listener) lock::Threads.SpinLock=Threads.SpinLock() end diff --git a/test/Throttled.jl b/test/Throttled.jl index 48285ebe5f..385ff61b43 100644 --- a/test/Throttled.jl +++ b/test/Throttled.jl @@ -1,4 +1,5 @@ -import Pluto:throttled +import Pluto: Throttled +using Pluto.WorkspaceManager: poll @testset "Throttled" begin x = Ref(0) @@ -11,7 +12,7 @@ import Pluto:throttled @test x[] == 1 dt = 4 / 100 - ft, flush = throttled(f, dt) + ft = Throttled.throttled(f, dt) for x in 1:10 ft() @@ -76,9 +77,57 @@ import Pluto:throttled ft() ft() @test x[] == 12 - flush() + flush(ft) @test x[] == 13 sleep(2dt) @test x[] == 13 + #### + + ft() + @test poll(2dt, dt/60) do + x[] == 14 + end + # immediately fire again, right after the last fire + ft() + ft() + # this should not do anything, because we are still in the cooldown period + @test x[] == 14 + # not even after a little while + sleep(0.1dt) + @test x[] == 14 + + # but eventually, our call should get queued + sleep(dt) + @test x[] == 15 + sleep(2dt) + + #### + x[] = 0 + Throttled.force_throttle_without_run(ft) + @test x[] == 0 + ft() + @test x[] == 0 + sleep(.1dt) + @test x[] == 0 + sleep(2dt) + @test x[] == 1 + + + ft() + @test x[] == 2 + sleep(.1dt) + ft() + Throttled.force_throttle_without_run(ft) + @test x[] == 2 + sleep(2dt) + @test x[] == 2 + + + ft() + @test x[] == 3 + sleep(2dt) + + #### + end \ No newline at end of file From a6809791bbee27a95318b83df709ddd123cc5906 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Tue, 6 Aug 2024 11:50:22 +0200 Subject: [PATCH 3/4] fix --- src/evaluation/Run.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/evaluation/Run.jl b/src/evaluation/Run.jl index 8081af65ee..81be9f736b 100644 --- a/src/evaluation/Run.jl +++ b/src/evaluation/Run.jl @@ -125,7 +125,7 @@ function run_reactive_core!( # flush_send_notebook_changes_throttled, send_notebook_changes_throttled = Throttled.throttled(1.0 / 20; runtime_multiplier=2.0) do # We will do a state sync now, so that means that we can delay the status_tree state sync loop, see https://github.com/fonsp/Pluto.jl/issues/2978 - force_throttle_without_run(notebook.status_tree.update_listener_ref[]) + Throttled.force_throttle_without_run(notebook.status_tree.update_listener_ref[]) # State sync: send_notebook_changes!(ClientRequest(; session, notebook)) end From e51dd930bcbc47abef27c84483e5190b2085680b Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Tue, 6 Aug 2024 12:01:49 +0200 Subject: [PATCH 4/4] fix? --- src/evaluation/Throttled.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/evaluation/Throttled.jl b/src/evaluation/Throttled.jl index a481069f30..43c49aa447 100644 --- a/src/evaluation/Throttled.jl +++ b/src/evaluation/Throttled.jl @@ -91,6 +91,8 @@ function force_throttle_without_run(tf::ThrottledFunction) end end +force_throttle_without_run(::Function) = nothing + """ simple_leading_throttle(f, delay::Real)