-
-
Notifications
You must be signed in to change notification settings - Fork 313
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve accuracy of set framerate in GLMakie (#3954)
* track time budget across frames * update changelog * add BudgetedTimer * add GC.safepoint * add tests * pass through min_sleep, update docstring --------- Co-authored-by: Simon <sdanisch@protonmail.com>
- Loading branch information
1 parent
4ce2495
commit 9c3f3e3
Showing
6 changed files
with
245 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
|
||
|
||
mutable struct BudgetedTimer | ||
callback::Any | ||
|
||
target_delta_time::Float64 | ||
min_sleep::Float64 | ||
budget::Float64 | ||
last_time::UInt64 | ||
|
||
running::Bool | ||
task::Union{Nothing, Task} | ||
|
||
function BudgetedTimer(callback, delta_time::Float64, running::Bool, task::Union{Nothing, Task}, min_sleep = 0.015) | ||
return new(callback, delta_time, min_sleep, 0.0, time_ns(), running, task) | ||
end | ||
end | ||
|
||
""" | ||
BudgetedTimer(target_delta_time) | ||
BudgetedTimer(callback, target_delta_time[, start = true]) | ||
A timer that keeps track of a time budget between invocations of `sleep(timer)`, | ||
`busysleep(timer)` or roundtrips of the timed task. The budget is then used to | ||
correct the next sleep so that the average sleep time matches the targeted delta | ||
time. | ||
To avoid lag spikes from hurrying the timer for multiple iterations/invocations | ||
only the difference to the nearest multiple of `target_delta_time` is counted. | ||
E.g. if two calls to `sleep(timer)` are 2.3 delta times apart, 0.3 will be | ||
relevant difference for the budget. | ||
""" | ||
function BudgetedTimer(delta_time::AbstractFloat; min_sleep = 0.015) | ||
return BudgetedTimer(identity, delta_time, false, nothing, min_sleep) | ||
end | ||
|
||
function BudgetedTimer(callback, delta_time::AbstractFloat, start = true; min_sleep = 0.015) | ||
timer = BudgetedTimer(callback, delta_time, true, nothing, min_sleep) | ||
if start | ||
timer.task = @async while timer.running | ||
timer.callback() | ||
sleep(timer) | ||
end | ||
end | ||
return timer | ||
end | ||
|
||
function start!(timer::BudgetedTimer) | ||
timer.budget = 0.0 | ||
timer.last_time = time_ns() | ||
timer.running = true | ||
timer.task = @async while timer.running | ||
timer.callback() | ||
sleep(timer) | ||
end | ||
return | ||
end | ||
|
||
function start!(callback, timer::BudgetedTimer) | ||
timer.callback = callback | ||
return start!(timer) | ||
end | ||
|
||
function stop!(timer::BudgetedTimer) | ||
timer.running = false | ||
return | ||
end | ||
|
||
function reset!(timer::BudgetedTimer, delta_time = timer.target_delta_time) | ||
timer.target_delta_time = delta_time | ||
timer.budget = 0.0 | ||
timer.last_time = time_ns() | ||
end | ||
|
||
function update_budget!(timer::BudgetedTimer) | ||
# The real time that has passed | ||
t = time_ns() | ||
time_passed = 1e-9 * (t - timer.last_time) | ||
# Update budget | ||
diff_to_target = timer.target_delta_time + timer.budget - time_passed | ||
if diff_to_target > -0.5 * timer.target_delta_time | ||
# used 0 .. 1.5 delta_time, keep difference (1 .. -0.5 delta times) as budget | ||
timer.budget = diff_to_target | ||
else | ||
# more than 1.5 delta_time used, get difference to next multiple of | ||
# delta_time as the budget | ||
timer.budget = ((diff_to_target - 0.5 * timer.target_delta_time) | ||
% timer.target_delta_time) + 0.5 * timer.target_delta_time | ||
end | ||
timer.last_time = t | ||
return | ||
end | ||
|
||
""" | ||
sleep(timer::BudgetedTimer) | ||
Sleep until one `timer.target_delta_time` has passed since the last call to | ||
`sleep(timer)` or `busysleep(timer)` with the current time budget included. | ||
This only relies on `Base.sleep()` for waiting. | ||
This always yields to other tasks. | ||
""" | ||
function Base.sleep(timer::BudgetedTimer) | ||
# time since last sleep | ||
time_passed = 1e-9 * (time_ns() - timer.last_time) | ||
# How much time we should sleep for considering the real time we slept | ||
# for in the last iteration and biasing for the minimum sleep time | ||
sleep_time = timer.target_delta_time + timer.budget - time_passed - 0.5 * timer.min_sleep | ||
if sleep_time > 0.0 | ||
sleep(sleep_time) | ||
else | ||
yield() | ||
end | ||
|
||
update_budget!(timer) | ||
return | ||
end | ||
|
||
""" | ||
busysleep(timer::BudgetedTimer) | ||
Sleep until one `timer.target_delta_time` has passed since the last call to | ||
`sleep(timer)` or `busysleep(timer)` with the current time budget included. | ||
This uses `Base.sleep()` for an initial longer sleep and a time-checking while | ||
loop for the remaining time for more precision. | ||
This always yields to other tasks. | ||
""" | ||
function busysleep(timer::BudgetedTimer) | ||
# use normal sleep as much as possible | ||
time_passed = 1e-9 * (time_ns() - timer.last_time) | ||
sleep_time = timer.target_delta_time - time_passed + timer.budget - timer.min_sleep | ||
if sleep_time > 0.0 | ||
sleep(sleep_time) | ||
else | ||
yield() | ||
end | ||
|
||
# busy sleep remaining time | ||
time_passed = 1e-9 * (time_ns() - timer.last_time) | ||
sleep_time = timer.target_delta_time - time_passed + timer.budget | ||
while time_ns() < timer.last_time + 1e9 * timer.target_delta_time + timer.budget | ||
yield() | ||
end | ||
|
||
update_budget!(timer) | ||
return | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
@testset "BudgetedTimer" begin | ||
|
||
t = time_ns() | ||
dt = 1.0 / 30.0 | ||
timer = Makie.BudgetedTimer(dt) | ||
|
||
@testset "Initialization" begin | ||
@test timer.target_delta_time == dt | ||
@test timer.budget == 0.0 | ||
@test t < timer.last_time < time_ns() | ||
end | ||
|
||
@testset "sleep()" begin | ||
sleep(timer) # just in case for compilation | ||
|
||
t = time_ns() | ||
for _ in 1:100 | ||
sleep(timer) | ||
end | ||
real_dt = 1e-9 * (time_ns() - t) | ||
|
||
@test 98 * dt < real_dt < 102 * dt | ||
end | ||
|
||
t = time_ns() | ||
dt = 0.03 | ||
|
||
@testset "reset!()" begin | ||
Makie.reset!(timer, dt) | ||
|
||
@test timer.target_delta_time == dt | ||
@test timer.budget == 0.0 | ||
@test t < timer.last_time < time_ns() | ||
end | ||
|
||
@testset "busysleep()" begin | ||
Makie.busysleep(timer) | ||
t = time_ns() | ||
for _ in 1:100 | ||
Makie.busysleep(timer) | ||
end | ||
real_dt = 1e-9 * (time_ns() - t) | ||
|
||
@test 99.9 * dt < real_dt < 100.1 * dt | ||
end | ||
|
||
@testset "callbacks" begin | ||
counter = 0 | ||
timer = Makie.BudgetedTimer(1.0 / 30.0, false) do | ||
global counter += 1 | ||
end | ||
sleep(0.5) | ||
@test counter == 0 | ||
|
||
t = time_ns() | ||
Makie.start!(timer) | ||
sleep(1.0) | ||
Makie.stop!(timer) | ||
real_dt = 1e-9 * (time_ns() - t) | ||
N = counter | ||
|
||
@test real_dt * 30.0 - 2 < counter < real_dt * 30.0 + 2 | ||
|
||
sleep(0.5) | ||
@test counter == N | ||
end | ||
end |