Document timing options

(#28)
This commit is contained in:
Lars Müller 2022-02-16 13:25:30 +01:00 committed by GitHub
parent f870d398bb
commit 429c277201
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

278
doc/timing.adoc Normal file

@ -0,0 +1,278 @@
= Timing and the Event Loop
include::../include/config.adoc[]
include::../include/types.adoc[]
:description: Minetest's timing functionality and API
:keywords: timing, event, loop, globalstep, step, after
Minetest's Lua API can be used to get time and date or to schedule "events" to run once or every server step.
== Settings
On servers::
The `dedicated_server_step` setting controls the time between server steps.
This is the maximum granularity (the most time spent between steps and the finest timing possible) of each game loop without a busywait.
In singleplayer::
Framerate determines server steps.
== Lua builtins
Not restricted by mod security, these functions are available to both SSMs and CSMs and allow getting times / dates.
* https://www.lua.org/manual/5.1/manual.html#pdf-os.clock[`os.clock`] - Approximate seconds CPU time used.
* https://www.lua.org/manual/5.1/manual.html#pdf-os.time[`os.time`] - The current time. Will usually be a UNIX timestamp in seconds.
* https://www.lua.org/manual/5.1/manual.html#pdf-os.difftime[`os.difftime`] - Difference between two `os.time` timestamps (usually simply `b - a`).
* https://www.lua.org/manual/5.1/manual.html#pdf-os.date[`os.date`] - Getting & formatting dates.
=== Example
You can use `os.clock` for accurate benchmarks of CPU time spent:
.Benchmarking with os.clock
====
[source,lua]
----
local function slow_sum(n)
local sum = 0
for i = 1, n do
sum = sum + i
end
return sum
end
local function benchmark(calls, func, ...)
local time = os.clock()
for _ = 1, calls do
func(...)
end
return (os.clock() - time) / calls -- seconds of CPU time spent per call
end
print("slow_sum(1e6) takes", benchmark(1e3, slow_sum, 1e6), "seconds per call")
----
====
== Functions
=== `minetest.get_us_time`
==== Usage
[source,lua]
----
time = minetest.get_us_time()
----
==== Returns
[%autowidth, frame=none]
|===
| `time` | `{type-number}` | System-dependent timestamp in microseconds since an arbitrary starting point (µs)
|===
TIP: Divide by `1e6` to convert `time` into seconds.
CAUTION: The returned `time` is not portable and not relative to any specific point in time across restarts - keep it only in memory for use while the game is running.
TIP: You can use the difference between ``minetest.get_us_time`` and the returned times to check whether a real-world timespan has passed, which is useful for rate limiting. For in-game timers, you might prefer adding up `dtime` or (if second precision is enough) using gametime.
=== Example
It is possible to use a simple while-loop to wait for a timespan smaller than the server step. This is called a "busywait".
.Busywait with minetest.get_us_time()
====
[source,lua]
----
local start = minetest.get_us_time()
repeat until minetest.get_us_time() - start > 1e3 -- Wait for 1000 µs to pass
----
====
WARNING: This blocks the server thread, possibly delaying the sending of packets that are sent each step and creating "lag". Use this only if absolutely necessary and only for very small timespans.
=== `minetest.get_gametime`
==== Usage
[source,lua]
----
time = minetest.get_gametime()
----
==== Returns
[%autowidth, frame=none]
|===
| `time` | `{type-number}` | Time passed "in-game" since world creation in seconds ("gametime"). Does not increase while the game is paused or not running. Gametime is stored with the world and continuously increases.
|===
==== Example
You can achieve higher precision by implementing gametime yourself, adding up `dtime`. The only tricky part about this is reading the initial gametime, which is only available at runtime and not at load time; the entire code may only run the the first server step:
.Writing a custom gametime function
====
[source,lua]
----
myapi = {}
minetest.after(0, function()
local gametime = minetest.get_gametime()
minetest.register_globalstep(function(dtime)
gametime = gametime + dtime
end)
function myapi.get_precise_gametime()
return gametime
end
end)
----
====
IMPORTANT: This naive implementation might be one server step ahead or behind `minetest.get_gametime`. Note that the initial gametime is rounded. Do not persist the values for this reason.
TIP: Use this implementation only for measuring in-game timespans.
=== `minetest.register_globalstep`
Calls the given callback every server step.
==== Usage
[source,lua]
----
minetest.register_globalstep(function(dtime)
-- do something, probably involving dtime
end)
----
==== Params
[%autowidth, frame=none]
|===
| `dtime` | `{type-number}` | Delta (elapsed) time since last step in seconds
|===
TIP: Use globalsteps to poll for an event for which there are no callbacks, such as player controls (`player:get_player_control()`).
CAUTION: As globalsteps run every server step, they are highly performance-critical and must be well optimized. If globalsteps take longer than the server step to complete, the server thread is blocked and the server becomes "laggy".
==== Examples
Globalsteps are ideal to run something periodically, which is often used to call expensive operations less frequently in order to keep server thread blockage - and thus lag - to a minimum.
.Running a globalstep on a timer
====
[source,lua]
----
-- period is in seconds
local function run_periodically(period, func)
local timer = 0
minetest.register_globalstep(function(dtime)
timer = timer + dtime
if timer > period then
func()
timer = 0
end
end)
end
run_periodically(5, function()
minetest.chat_send_all("5 seconds have passed!")
end)
----
This will call `func` after at least `timer` seconds have passed. Note that there is no "catchup": The timer is always reset to zero, no matter how "late" the call to func is - how large `timer - period` is. For catchup, you can simply set the timer to the leftover time instead: `timer = timer - period`. This will trigger calls in subsequent steps until the missed time is "caught up".
=== `minetest.after(time, func, ...)`
Will call `func(...)` after at least `time` seconds have passed.
TIP: Use `minetest.after(0, func)` to immediately do load-time stuff that is only possible at run-time, or to schedule something for the next server step.
[NOTE]
.Order of execution
====
Scheduled calls happen in reverse insertion order. This "defers" calls to `minetest.after` from within a callback until the next server step. Examples:
[source,lua]
----
minetest.after(0, error, "first")
minetest.after(0, error, "second")
----
This script will error with `second` after the first server step,
as the callbacks that are scheduled to run in the same server step are processed in reverse order.
The following snippet emulates a globalstep, sending `Hello World!` to chat every server step:
[source,lua]
----
local function after_step()
minetest.chat_send_all("Hello World!")
minetest.after(0, after_step)
end
minetest.after(0, after_step)
----
====
==== Usage
[source,lua]
----
job = minetest.after(time, func, ...)
if ... then
job:cancel()
end
----
==== Params
[%autowidth, frame=none]
|===
| `time` | `{type-number}` | Time in seconds that must have passed for the callback to be executed.
| `func` | `{type-function}` | Function to be called. Objects with the `__call` metamethod are supported as well.
| `...` | vararg | Arguments to be passed to `func` when it is called.
|===
==== Returns
[%autowidth, frame=none]
|===
| `job` | job object | Simple object providing a `job:cancel()` method to cancel a scheduled "job".
|===
CAUTION: `...` may be arbitrarily cut off at `nil` values, as Minetest uses a simple list to store the arguments. Don't include `nil`s in the arguments if possible or you may lose arguments.
[TIP]
.Using a closure for advanced function calls
====
If you have to call a function with ``nil``s in it's argument list, use a closure for reliable behavior:
[source,lua]
----
minetest.after(time, function()
func(nil, arg, arg2, nil, nil)
end)
----
====
NOTE: All scheduled callbacks are stored in a list until they are called. This list is traversed in linear time if *any* of the callbacks are executed. Excessive use of `minetest.after` may result in slow execution time.
WARNING: `job:cancel()` might cause a memory leak if used naively: The job - including the arguments `...` - is still kept in the job list until the job expires; only the function call isn't made. Example: `minetest.after(math.huge, error, "a super long string"):cancel()` - the string will stay in memory forever despite the job being cancelled, as `math.huge` is never reached.
== Entities
=== `entity:on_step(dtime, ...)`
Callback. Called per-entity every globalstep with the current `dtime`, allowing for comfortable per-entity timing.
==== Usage
[source,lua]
----
function entity:on_step(dtime, ...)
-- use dtime
end
----
==== Params
[%autowidth, frame=none]
|===
| `self` | entity | The entity itself (implicit parameter)
| `dtime` | `{type-number}` | Delta (passed) time since last step in seconds (same as globalstep)
| `...` | vararg | Other parameters irrelevant to timing (such as `moveresult`)
|===