Best practice? multiple small globalsteps vs one large one

Post Reply
User avatar
Linuxdirk
Member
Posts: 3219
Joined: Wed Sep 17, 2014 11:21
In-game: Linuxdirk
Location: Germany
Contact:

Best practice? multiple small globalsteps vs one large one

by Linuxdirk » Post

Currently I am developing a mod that registers 4 globalsteps with different timings (every tick, every 0.5 seconds, every 2 seconds and every 60 seconds by counting a timer value and checking it via if like described in the dev wiki). All of the globalsteps functions iterate over all connected players and perform different smaller actions with every player.

My question is: Is it better to have four different small globalsteps performing their actions or would it better to have one large globalstep with 4 different timers performing all of the actions within one iteration over all players every tick?

User avatar
rubenwardy
Moderator
Posts: 6978
Joined: Tue Jun 12, 2012 18:11
GitHub: rubenwardy
IRC: rubenwardy
In-game: rubenwardy
Location: Bristol, United Kingdom
Contact:

Re: Best practice? multiple small globalsteps vs one large o

by rubenwardy » Post

You should use minetest.after for timed events, it uses a priority queue.

You should spread intensive actions out across ticks to reduce server jitter. If the tasks are small it's fine to run in a single tick however
Renewed Tab (my browser add-on) | Donate | Mods | Minetest Modding Book

Hello profile reader

User avatar
Linuxdirk
Member
Posts: 3219
Joined: Wed Sep 17, 2014 11:21
In-game: Linuxdirk
Location: Germany
Contact:

Re: Best practice? multiple small globalsteps vs one large o

by Linuxdirk » Post

rubenwardy wrote:You should use minetest.after
I am not quite sure about this … Is minetest.after more or less consistent/secure than using globalsteps? The latest release version (0.4.16 as of today) is my target platform.
rubenwardy wrote:You should spread intensive actions out across ticks to reduce server jitter.
So it is better to run multiple small things instead of one giant piece of code checking and doing everything? The actions I perform do not depend on each other. So having a registered globalstep being skipped a few ticks isn’t that important.
rubenwardy wrote:If the tasks are small it's fine to run in a single tick however
As said all tasks iterate over all players, let me extend that. The task gets the name from the player object and passes it and a second value (a localized entry from a global table) to a function.

Code: Select all

local timer = 0
local interval = my_global_table.interval

minetest.register_globalstep(function(dtime)
    timer = timer + dtime
    if timer >= interval then
        timer = 0
        for _,player in ipairs(minetest.get_connected_players()) do
            -- do stuff here
        end
    end
end)
(There are some more localized values, this is just a minimal working example of what I do.) Basically all tasks check a single thing. The heaviest might be checking for player movement.

Code: Select all

local m = player:get_player_control()
if m.up or m.down or m.left or m.right then
    -- do stuff here
end
The function that will be executed when all checks passed gets the player object (as I recently learned passing around player objects is considered inconsistent), gets some custom player attributes, does some calculations and writes the result as custom player attribute and modifies a HUD element.

For me it is important to not affect performance too much.

User avatar
rubenwardy
Moderator
Posts: 6978
Joined: Tue Jun 12, 2012 18:11
GitHub: rubenwardy
IRC: rubenwardy
In-game: rubenwardy
Location: Bristol, United Kingdom
Contact:

Re: Best practice? multiple small globalsteps vs one large o

by rubenwardy » Post

Linuxdirk wrote:
rubenwardy wrote:You should use minetest.after
I am not quite sure about this … Is minetest.after more or less consistent/secure than using globalsteps? The latest release version (0.4.16 as of today) is my target platform.
Most of those also apply to global steps. For the case of running something every N seconds, minetest.after is better. Minetest.after isn't precise on older versions however.
Linuxdirk wrote:
rubenwardy wrote:You should spread intensive actions out across ticks to reduce server jitter.
So it is better to run multiple small things instead of one giant piece of code checking and doing everything? The actions I perform do not depend on each other. So having a registered globalstep being skipped a few ticks isn’t that important.
I'd tend to put related things that run in the same interval together, and only split them up if it causes dropped server steps when the timer runs.
Linuxdirk wrote:
rubenwardy wrote:If the tasks are small it's fine to run in a single tick however
As said all tasks iterate over all players, let me extend that. The task gets the name from the player object and passes it and a second value (a localized entry from a global table) to a function.

*snip*

(There are some more localized values, this is just a minimal working example of what I do.) Basically all tasks check a single thing. The heaviest might be checking for player movement.

The function that will be executed when all checks passed gets the player object (as I recently learned passing around player objects is considered inconsistent), gets some custom player attributes, does some calculations and writes the result as custom player attribute and modifies a HUD element.

For me it is important to not affect performance too much.
Looping through players isn't that slow, it's the content that matters. Anything that sends packets - such as HUD, player physics modifiers, and object modifiers - should be spread out as much as possible, as they don't tend to be batched and so can end up with a lot of packet sending. For example, on CTF there's 5*3 hud elements which are updated every time someone is killed - that's 30*5*3=450 packets. It's vital that those elements aren't updated every loop, but only when needed.

It's still worth avoiding running stuff every tick if you can.

At the end of the day, the best way to optimise is to use a profiler. I'd go with the simple implementation first, then if you see dropped steps every time the interval function returns then you know it's taking too long.
Renewed Tab (my browser add-on) | Donate | Mods | Minetest Modding Book

Hello profile reader

User avatar
Linuxdirk
Member
Posts: 3219
Joined: Wed Sep 17, 2014 11:21
In-game: Linuxdirk
Location: Germany
Contact:

Re: Best practice? multiple small globalsteps vs one large o

by Linuxdirk » Post

rubenwardy wrote:For the case of running something every N seconds, minetest.after is better. Minetest.after isn't precise on older versions however.
What older versions? <0.4.16? As I develop for the latest release I’m fine with it if it is less precise in older versions. What’s the best way to use it? something like this?

Code: Select all

local do_stuff = function ()
    minetest.after(60, do_stuff)
    for _,player in ipairs(minetest.get_connected_players()) do
        -- do stuff here
    end
end
And then initially start in by running the function once at server start?

User avatar
sorcerykid
Member
Posts: 1847
Joined: Fri Aug 26, 2016 15:36
GitHub: sorcerykid
In-game: Nemo
Location: Illinois, USA

Re: Best practice? multiple small globalsteps vs one large o

by sorcerykid » Post

rubenwardy wrote:You should use minetest.after for timed events, it uses a priority queue.
I don't think that's true. Based on my research, minetest.after does not perform any prioritization or optimization. It simply loops through the job queue in its entirety every server tick.

https://github.com/minetest/minetest/bl ... ter.lua#L4
rubenwardy wrote:Minetest.after isn't precise on older versions however.
In previous implementations, minetest.after was actually more precise because the job expiration was determined with microsecond precision by a monotonic clock.

https://github.com/minetest/minetest/bl ... /after.lua

Now, rather than the precision of the monotonic clock, a local "time" variable is being incremented by dtime each globalstep. To my understanding, dtime is relatively inaccurate and imprecise. Moreover, performing hundreds of floating point calculations every minute on a single variable is likely to induce gradual rounding errors and therefore perpetual drift of the calculated time.
rubenwardy wrote:For the case of running something every N seconds, minetest.after is better.
In my view, minetest.after is really only suitable for one shot events. It's not a particularly robust facility for high-scale, routine job control. Unlike node timers, that can be stopped, restarted, and even validated, none of that is possible with minetest.after. Also native support for periodic tasks is lacking, which necessitates recursive calls to minetest.after, which is not stylistically clean nor intuitive.

I personally would recommend consolidating all of your tasks into the fewest number of globalstep registrations possible (preferably one or two, if possible), and then apportioning them accordingly. For example, this is the abbreviated code for my weather simulator mod.

Code: Select all

local timer = 0

minetest.register_globalstep( function( dtime )
        -- Multiple lightning strikes can occur randomly in a series after thunder
        -- (but only only one lightning strike at a time of course)
        if cur_thunder and cur_thunder > 0 then
                :
                :
        end

        -- Update sky to reflect degree of precipitation every 3 seconds, offset by 0.7 seconds
        -- (this should help distribute the CPU and network load)
        if v_lightning == 0 or ( timer + 7 ) % 30  == 0 then
                :
                :
        -- During lightning, recalculate sky every 0.1 seconds to allow realistic visual effects
        -- (should have negligible performance impact due to randomness and rarity)
        elseif v_lightning then
                :
                :
        end

        -- Get current weather conditions via perlin noise function every 2.0 seconds
        -- Calculate temp fade and sky fade relative to offset from middway
        if timer % 20 == 0 then
                :
                :
        end

        -- Iterate through all players and start/stop environment sounds and particle fx
        -- only when weather conditions change or player moves indoors/outdoors
        if timer % 10 == 0 then
                :
                :
        end

        -- Export weather data at 10 second intervals offset by 0.3 seconds for charting of
        -- weather conditions via polygraph mod
        if ( timer + 3 ) % 100 == 0 then
                :
                :
        end

        timer = timer + 1
end )
Even as complex as this function appears, it is very efficient. Benchmarking this specific block of code over a 24-hour period on my production server, the total time spent was only about 3.7 seconds.

Post Reply

Who is online

Users browsing this forum: No registered users and 5 guests