From a24548ed3f250c5f34f0a5fe18b3d01b0b4dad3c Mon Sep 17 00:00:00 2001 From: zwim <36999612+zwim@users.noreply.github.com> Date: Mon, 10 Oct 2022 22:17:50 +0200 Subject: [PATCH] UIManager: Optimize binary insert (#9600) * Optimiz --- frontend/ui/uimanager.lua | 43 +++++-------- spec/unit/uimanager_bench.lua | 111 +++++++++++++++++++++++++++++++++- spec/unit/uimanager_spec.lua | 46 ++++++++++++++ 3 files changed, 172 insertions(+), 28 deletions(-) diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index 6651e7597..38865ac62 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -227,35 +227,24 @@ end -- Schedule an execution task; task queue is in ascending order function UIManager:schedule(sched_time, action, ...) - local p, s, e = 1, 1, #self._task_queue - if e ~= 0 then - -- Do a binary insert. - repeat - p = bit.rshift(e + s, 1) -- Not necessary to use (s + (e -s) / 2) here! - local p_time = self._task_queue[p].time - if sched_time > p_time then - if s == e then - p = e + 1 - break - elseif s + 1 == e then - s = e - else - s = p - end - elseif sched_time < p_time then - if s == p then - break - end - e = p - else - -- For fairness, it's better to make sure p+1 is strictly less than p. - -- Might want to revisit that in the future. - break - end - until e < s + local lo, hi = 1, #self._task_queue + -- Rightmost binary insertion + while lo <= hi do + -- NOTE: We should be (mostly) free from overflow here, thanks to LuaJIT's BitOp semantics. + -- For more fun details about this particular overflow, + -- c.f., https://ai.googleblog.com/2006/06/extra-extra-read-all-about-it-nearly.html + -- NOTE: For more fun reading about the binary search algo in general, + -- c.f., https://reprog.wordpress.com/2010/04/19/are-you-one-of-the-10-percent/ + local mid = bit.rshift(lo + hi, 1) + local mid_time = self._task_queue[mid].time + if sched_time >= mid_time then + lo = mid + 1 + else + hi = mid - 1 + end end - table.insert(self._task_queue, p, { + table.insert(self._task_queue, lo, { time = sched_time, action = action, argc = select("#", ...), diff --git a/spec/unit/uimanager_bench.lua b/spec/unit/uimanager_bench.lua index fab142475..f313d4851 100644 --- a/spec/unit/uimanager_bench.lua +++ b/spec/unit/uimanager_bench.lua @@ -7,6 +7,19 @@ local time = require("ui/time") local NB_TESTS = 40000 local noop = function() end +local function check() + for i = 1, #UIManager._task_queue-1 do + -- test for wrongly inserted time + assert.is_true(UIManager._task_queue[i].time <= UIManager._task_queue[i+1].time, + "time wrongly sorted") + if UIManager._task_queue[i].time == UIManager._task_queue[i+1].time then + -- for same time, test if later inserted action is after a former action + assert.is_true(UIManager._task_queue[i].action <= UIManager._task_queue[i+1].action, + "ragnarock") + end + end +end + describe("UIManager checkTasks benchmark", function() local now = time.now() local wait_until -- luacheck: no unused @@ -25,7 +38,7 @@ describe("UIManager checkTasks benchmark", function() end end) -describe("UIManager schedule benchmark", function() +describe("UIManager schedule simple benchmark", function() local now = time.now() UIManager:quit() UIManager._task_queue = {} @@ -35,6 +48,102 @@ describe("UIManager schedule benchmark", function() end end) +describe("UIManager schedule more sophiticated benchmark", function() + -- This BM is doing schedulings like the are done in real usage + -- with autosuspend, autodim, autowarmth and friends. + local now = time.now() + UIManager:quit() + + local function standby_dummy() end + local function autowarmth_dummy() end + local function dimmer_dummy() end + + local function someTaps() + for j = 1,10 do + -- insert some random times for entering standby + UIManager:schedule(now + time.s(j), standby_dummy) -- standby + UIManager:unschedule(standby_dummy) + end + end + + for i=1, NB_TESTS do + UIManager._task_queue = {} + UIManager:schedule(now + time.s(24*60*60), noop) -- shutdown + UIManager:schedule(now + time.s(15*60*60), noop) -- sleep + UIManager:schedule(now + time.s(55), noop) -- footer refresh + UIManager:schedule(now + time.s(130), noop) -- something + UIManager:schedule(now + time.s(10), noop) -- something else + + for j = 1,5 do + someTaps() + UIManager:schedule(now + time.s(15*60), autowarmth_dummy) -- autowarmth + UIManager:schedule(now + time.s(180), dimmer_dummy) -- dimmer + + now = now + 30 + UIManager:unschedule(dimmer_dummy) + UIManager:unschedule(autowarmth_dummy) -- remove autowarmth + end + end +end) + +describe("UIManager schedule massive collision tests", function() + print("Doing massive collision tests ......... this takes a lot of time") + UIManager:quit() + + for i = 1, 6 do + -- simple test (1000/10 collisions) + UIManager._task_queue = {} + for j = 1, 10 do + UIManager:schedule(math.random(10), j) + -- check() -- enabling this takes really long O(n^2) + end + check() + + -- armageddon test (10000 collisions) + UIManager._task_queue = {} + for j = 1, 1e5 do + UIManager:schedule(math.random(100), j) + -- check() -- enabling this takes really long O(n^2) + end + check() + end +end) + + +describe("UIManager schedule massive rediculous tests", function() + print("Doing massive rediculous collision tests ......... this takes really a lot time") + UIManager:quit() + + for i = 1, 6 do + -- simple test (1000 collisions) + UIManager._task_queue = {} + local offs = 0 + for j = 1, 1e3 do + UIManager:schedule(math.random(10), j + offs) + offs = offs + 1 + -- check() -- enabling this takes really long O(n^2) + end + check() + + -- simple (unknown number of collisions and times) + for j = 1, 1e4 do + UIManager:schedule(math.random(), j + offs) + offs = offs + 1 + -- check() -- enabling this takes really long O(n^2) + end + check() + + -- armageddon test (100 collisions) + for j = 1, 1e5 do + UIManager:schedule(math.random(math.random(100)), j + offs) + offs = offs + 1 + -- check() -- enabling this takes really long O(n^2) + end + check() + end + +end) + describe("UIManager unschedule benchmark", function() local now = time.now() UIManager:quit() diff --git a/spec/unit/uimanager_spec.lua b/spec/unit/uimanager_spec.lua index a470dbd87..ea0b6848d 100644 --- a/spec/unit/uimanager_spec.lua +++ b/spec/unit/uimanager_spec.lua @@ -113,6 +113,52 @@ describe("UIManager spec", function() assert.are.same('quux', UIManager._task_queue[5].action) end) + it("should insert new tasks with same times after existing tasks", function() + now = time.now() + UIManager:quit() + UIManager._task_queue = {} + + -- insert task "5s" between "now" and "10s" + UIManager:schedule(now, "now"); + assert.are.same("now", UIManager._task_queue[1].action) + UIManager:schedule(now + time.s(10), "10s"); + assert.are.same("10s", UIManager._task_queue[2].action) + UIManager:schedule(now + time.s(5), "5s"); + assert.are.same("5s", UIManager._task_queue[2].action) + + -- insert task at the end after "10s" + UIManager:scheduleIn(10, 'foo') -- is a bit later than "10s", as time.now() is used internally + assert.are.same('foo', UIManager._task_queue[4].action) + + -- insert task at the second last position after "10s" + UIManager:schedule(now + time.s(10), 'bar') + assert.are.same('bar', UIManager._task_queue[4].action) + + -- insert task at the second last position after "bar" + UIManager:schedule(now + time.s(10), 'baz') + assert.are.same('baz', UIManager._task_queue[5].action) + + -- insert task after "5s" + UIManager:schedule(now + time.s(5), 'nix') + assert.are.same('nix', UIManager._task_queue[3].action) + -- "barba" is later than "nix" anyway + UIManager:scheduleIn(5, 'barba') -- is a bit later than "5s", as time.now() is used internally + assert.are.same('barba', UIManager._task_queue[4].action) + + -- "papa" is shortly after "now" + UIManager:nextTick('papa') -- is a bit later than "now" + assert.are.same('papa', UIManager._task_queue[2].action) + + -- "mama is shedule now and inserted after "now" + UIManager:schedule(now, 'mama') + assert.are.same('mama', UIManager._task_queue[2].action) + + -- "letta" is shortly after "papa" + UIManager:tickAfterNext('letta') + assert.are.same("function", type(UIManager._task_queue[4].action)) + + end) + it("should unschedule all the tasks with the same action", function() now = time.now() UIManager:quit()