Desync due to Lua objects and GC (init phase)

v2.0.3 (and earlier)

See the repository for full code. The most minimal repro included there as a map

GitHub - Luashine/wc3-lua-global-desync: Imagine having reproducible test cases for your bug reports? - github repo

I assume it is the GC. Suggested workaround: always run full collections, but less often. Or run full collection on state mismatch (if its recoverable)?

Also please, increase the limits on the GC, it’s too slow in some cases and garbage keeps accumulating.

The culprit:

DDLib__TempLoc = Location(0., 0.) -- this is done in INIT -- ROOT LOCATION()

-- Create garbage
-- This is equivalent to putting globals in root, something the game does
for i = 1, 100 do -- garbage directly affects WHEN the desync happens
    local key
    if i % 2 == 0 then
        key = "garbage_" .. i .. "_luatable"
        _G[key] = {}
    else
        key = "garbage_" .. i .. "_jarray"
        _G[key] = __jarray(0)
    end
end

function DDLib__onInit()
  DDLib__TempLoc = Location(0., 0.) -- THIS LINE ALONE TOGETHER WITH SAME DEFINITION IN GLOBALS AT THE START WILL DESYNC
  -- REMOVE ONE OR THE OTHER AND THE DESYNC SEEMS TO STOP.
  -- GO FIGURE.
  -- YES, PLEASE DO FIGURE IT OUT
end
function main()
    SetRandomSeed(0xf0f0) -- Set seed that's kinda reliable in conjunction with human AI

    -- CAMERA AND MAP SETUP OMITTED FOR HIVE POST --
   
    InitBlizzard()
   

    DDLib__onInit() -- called from here
   
   
    InitGlobals()
    InitCustomTriggers()
    RunInitializationTriggers()
end

So here it is. It desynced in my testing only IF the following conditions were met:

  • Location is created in map root, where globals block would have been

  • That Location is overwritten in init phase, part of the main() call

    • it’s alright if created only in globals or only during init
  • There exists enough garbage to throw off the garbage collector and affect its iteration order (assumption). at the very least the created garbage affects the time when it finally desyncs

  • AI player must be playing Human (orc?)

  • for increased reliability: SetRandomSeed(0xf0f0) at the top of main()

The desync always seemed to coincide with AI’s order to start a new building or when the worker reached the construction site and laid the foundation. ±1s

Insights:

  • GetHandleId numeric IDs do get out of sync. Especially if it was going to desync. However, they could go out of sync and you can keep playing without desyncing. This is no bueno.
  • GetHandleId eventually resyncs
  • If you don’t overwrite location in Step 2 aka DDLib__onInit, but create another one – no desync
  • Creating 10000 trash object slowed down when the desync happened by about 15-30 seconds. 100 objects seem to be the right amount (remember, Lua GC is slow?)
  • GetLocalPlayer, if recycled by GC, will create a new lua userdata next time. The handle ID (handle to internal object) stays the same.
  • I assume the only damn way for the GC to desync the game is if it is run in incremental mode, so for player A it collects like the first 50% of the garbage and for player B it collects other 50% of the garbage, assuming that it can do the rest of the object during the next cycle.
    • of course, this assumes that the iteration order is not deterministic

In other words: wat??? That is, I still assume the GC works in a way that is generally not safe.


UPDATE: Could not reproduce in Battlenet b22998 or b23052 PTR. Still, it continues to always desync in LAN. :frowning:

2 Likes