-
-
Notifications
You must be signed in to change notification settings - Fork 313
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
Improve accuracy of set framerate in GLMakie #3954
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
b542c7c
track time budget across frames
ffreyer 6cbc60e
update changelog
ffreyer 02854a2
Merge branch 'master' into ff/frame_time_budget
SimonDanisch a852203
add BudgetedTimer
ffreyer 303e9c3
add GC.safepoint
ffreyer 0bb063e
Merge branch 'ff/frame_time_budget' of https://github.com/MakieOrg/Ma…
ffreyer eb25e51
add tests
ffreyer 07b5632
Merge branch 'master' into ff/frame_time_budget
ffreyer a955409
Merge branch 'master' into ff/frame_time_budget
ffreyer 0f4fd91
Merge branch 'master' into ff/frame_time_budget
SimonDanisch 78b9530
pass through min_sleep, update docstring
ffreyer caaebbe
Merge branch 'ff/frame_time_budget' of https://github.com/MakieOrg/Ma…
ffreyer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 tried timing
sleep(0.0001)
here but it ended up being inconsistent. Most are 0.015s for me, but some are <0.005s and I assume async tasks and GC can also prolong them. So I just switched to a default value instead.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.
Since I wasn't sure anymore I did some testing on what effect a wrong
min_sleep
has. Doesn't seem like there are any significant changes: