diff --git a/mods/CORE/mcl_scheduler/init.lua b/mods/CORE/mcl_scheduler/init.lua index 0e731804c..1e3adc831 100644 --- a/mods/CORE/mcl_scheduler/init.lua +++ b/mods/CORE/mcl_scheduler/init.lua @@ -1,204 +1,39 @@ ---[[ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) -== Radix/Finger queue class +mcl_scheduler = {} +local mod = mcl_scheduler -This is a queue based off the concepts behind finger trees (https://en.wikipedia.org/wiki/Finger_tree), -the radix sort (https://en.wikipedia.org/wiki/Radix_sort) and funnel sort (https://en.wikipedia.org/wiki/Funnelsort) +dofile(modpath.."/queue.lua") -This algorithm has O(1) deletion and O(k) insertion (k porportional to the time in the future) and uses log_4(n) tree nodes. +function mod.test() + local t = mod.queue.new() -At the top level of the queue, there is a 20-element array of linked lists containing tasks that are scheduled to -start running in the current second. Removing the next linked list of tasks from this array is an O(1) operation. + local test_times = { 15, 1, 7, 34, 50, 150, 14, 18, 20, 20, 15, } -The queue then has a one-sided finger tree of 20-element lists that are used to replace the initial queue when the -second rolls over. + local start_time = minetest.get_us_time() + for _,time in pairs(test_times) do + t:add_task({ time = time }) -]] - -function Class() - local cls = {} - cls.mt = { __index = cls } - function cls:new(...) - local inst = setmetatable({}, cls.mt) - local construct = inst.construct - if construct then inst:construct(...) end - return inst + local stop_time = minetest.get_us_time() + print("took "..tostring(stop_time - start_time).."us") + start_time = stop_time end - return cls -end -local inner_queue = Class() -function inner_queue:construct(level) - self.level = level - self.items = {} - self.unsorted_count = 0 - self.slots = 4 + print(dump(t:tick())) + print(dump(t)) - -- Precompute slot size - local slot_size = 20 - for i = 2,level do - slot_size = slot_size * 4 - end - self.slot_size = slot_size -end -function inner_queue:get() - local slots = self.slots - local slot = 5 - slots - local ret = self.items[slot] - self.items[slot] = nil + local start_time = minetest.get_us_time() + for i=1,60 do + local s = t:tick() + print("time="..tostring(i+1)) + print(dump(s)) - -- Take a way a slot, then refill if needed - slots = slots - 1 - if slots == 0 then - if self.next_level then - local next_level_get = self.next_level:get() - if next_level_get then - self.items = next_level_get.items - else - self.items = {} - end - slots = 4 - end - end - self.slots = slots - - return ret or self:init_slot_list() -end -function inner_queue:add_tasks(tasks, time) - local task = tasks - local slot_size = self.slot_size - local level = self.level - local slots = self.slots - - while task do - local t = task.time - t = t - time - - if t > slot_size * slots then - -- Add to list for next level in the finger tree - task.time = t - local curr_task = task - task = task.next - curr_task.next = self.first_unsorted - local count = self.unsorted_count + 1 - if count > 20 then - if not self.next_level then - self.next_level = inner_queue:new(self.level + 1) - end - self.next_level:add_tasks(curr_task, slot_size * slots) - self.first_unsorted = nil - self.unsorted_count = 0 - else - self.first_unsorted = curr_task - self.unsorted_count = count - end - else - -- Task belongs in a slot on this level - local slot = math.floor(t / slot_size) + 1 + ( slots - 4 ) - t = t % slot_size - task.time = t - local curr_task = task - task = task.next - - local list = self.items[slot] or self:init_slot_list() - self.items[slot] = list - - if level == 1 then - curr_task.next = list[t] - list[t] = curr_task - else - list:add_tasks(curr_task, (slot - 1) * slot_size) - end - end - end -end -function inner_queue:init_slot_list() - local level = self.level - if level == 1 then - return { {}, {}, {}, {} } - else - local r = {} - for i=1,4 do - r[i] = inner_queue:new(self.level - 1) - end + local stop_time = minetest.get_us_time() + print("took "..tostring(stop_time - start_time).."us") + start_time = stop_time end end -local queue = Class() -function queue:construct() - self.items = {} - self.unsorted_count = 0 - self.m_tick = 1 - self.next_level = inner_queue:new(1) -end -function queue:add_task(task) - -- Adjust time to align with the start of the current second - task.time = task.time + self.m_tick - - -- Handle task in current seccond - if task.time <= 20 then - task.next = self.items[task.time] - self.items[task.time] = task - return - end - - local count = self.unsorted_count + 1 - if count > 20 then - -- Push to next level - self.unsorted_count = 0 - else - -- Add to the list of tasks for later time slots - task.next = self.first_unsorted - self.first_unsorted = task - self.unsorted_count = count - end -end -function queue:tick() - -- Get the tasks for this tick - local ret = self.items[self.m_tick] - self.items[self.m_tick] = nil - self.m_tick = self.m_tick + 1 - - -- Handle second rollover - if self.m_tick == 21 then - -- Push items to next level - if self.first_unsorted then - self.next_level:add_tasks(self.first_unsorted, 20) - self.first_unsorted = nil - self.unsorted_count = 0 - end - - self.items = self.next_level:get() - self.m_tick = 1 - end - - return ret or {} -end - -local t = queue.new() - -local test_times = { 15, 1, 7, 34, 50, 150, 14, 18, 20, 20, 15, } - -local start_time = minetest.get_us_time() -for _,time in pairs(test_times) do - t:add_task({ time = time }) - - local stop_time = minetest.get_us_time() - print("took "..tostring(stop_time - start_time).."us") - start_time = stop_time -end - -print(dump(t:tick())) -print(dump(t)) - -local start_time = minetest.get_us_time() -for i=1,60 do - local s = t:tick() - print("time="..tostring(i+1)) - print(dump(s)) - - local stop_time = minetest.get_us_time() - print("took "..tostring(stop_time - start_time).."us") - start_time = stop_time -end +mod.test() diff --git a/mods/CORE/mcl_scheduler/queue.lua b/mods/CORE/mcl_scheduler/queue.lua new file mode 100644 index 000000000..1646fa5bb --- /dev/null +++ b/mods/CORE/mcl_scheduler/queue.lua @@ -0,0 +1,180 @@ +local mod = mcl_scheduler + +--[[ + +== Radix/Finger queue class + +This is a queue based off the concepts behind finger trees (https://en.wikipedia.org/wiki/Finger_tree), +the radix sort (https://en.wikipedia.org/wiki/Radix_sort) and funnel sort (https://en.wikipedia.org/wiki/Funnelsort) + +This algorithm has O(1) deletion and O(k) insertion (k porportional to the time in the future) and uses log_4(n) tree nodes. + +At the top level of the queue, there is a 20-element array of linked lists containing tasks that are scheduled to +start running in the current second. Removing the next linked list of tasks from this array is an O(1) operation. + +The queue then has a one-sided finger tree of 20-element lists that are used to replace the initial queue when the +second rolls over. + +]] + +function Class() + local cls = {} + cls.mt = { __index = cls } + function cls:new(...) + local inst = setmetatable({}, cls.mt) + local construct = inst.construct + if construct then inst:construct(...) end + return inst + end + return cls +end + +local inner_queue = Class() +function inner_queue:construct(level) + self.level = level + self.items = {} + self.unsorted_count = 0 + self.slots = 4 + + -- Precompute slot size + local slot_size = 20 + for i = 2,level do + slot_size = slot_size * 4 + end + self.slot_size = slot_size +end +function inner_queue:get() + local slots = self.slots + local slot = 5 - slots + local ret = self.items[slot] + self.items[slot] = nil + + -- Take a way a slot, then refill if needed + slots = slots - 1 + if slots == 0 then + if self.next_level then + local next_level_get = self.next_level:get() + if next_level_get then + self.items = next_level_get.items + else + self.items = {} + end + slots = 4 + end + end + self.slots = slots + + return ret or self:init_slot_list() +end +function inner_queue:add_tasks(tasks, time) + local task = tasks + local slot_size = self.slot_size + local level = self.level + local slots = self.slots + + while task do + local t = task.time + t = t - time + + if t > slot_size * slots then + -- Add to list for next level in the finger tree + task.time = t + local curr_task = task + task = task.next + curr_task.next = self.first_unsorted + local count = self.unsorted_count + 1 + if count > 20 then + if not self.next_level then + self.next_level = inner_queue:new(self.level + 1) + end + self.next_level:add_tasks(curr_task, slot_size * slots) + self.first_unsorted = nil + self.unsorted_count = 0 + else + self.first_unsorted = curr_task + self.unsorted_count = count + end + else + -- Task belongs in a slot on this level + local slot = math.floor(t / slot_size) + 1 + ( slots - 4 ) + t = t % slot_size + task.time = t + local curr_task = task + task = task.next + + local list = self.items[slot] or self:init_slot_list() + self.items[slot] = list + + if level == 1 then + curr_task.next = list[t] + list[t] = curr_task + else + list:add_tasks(curr_task, (slot - 1) * slot_size) + end + end + end +end +function inner_queue:init_slot_list() + local level = self.level + if level == 1 then + return { {}, {}, {}, {} } + else + local r = {} + for i=1,4 do + r[i] = inner_queue:new(self.level - 1) + end + end +end + +local queue = Class() +mod.queue = queue +function queue:construct() + self.items = {} + self.unsorted_count = 0 + self.m_tick = 1 + self.next_level = inner_queue:new(1) +end +function queue:add_task(task) + -- Adjust time to align with the start of the current second + task.time = task.time + self.m_tick + + -- Handle task in current seccond + if task.time <= 20 then + task.next = self.items[task.time] + self.items[task.time] = task + return + end + + local count = self.unsorted_count + 1 + if count > 20 then + -- Push to next level + self.unsorted_count = 0 + else + -- Add to the list of tasks for later time slots + task.next = self.first_unsorted + self.first_unsorted = task + self.unsorted_count = count + end +end +function queue:tick() + -- Get the tasks for this tick + local ret = self.items[self.m_tick] + self.items[self.m_tick] = nil + self.m_tick = self.m_tick + 1 + + -- Handle second rollover + if self.m_tick == 21 then + -- Push items to next level + if self.first_unsorted then + self.next_level:add_tasks(self.first_unsorted, 20) + self.first_unsorted = nil + self.unsorted_count = 0 + end + + self.items = self.next_level:get() + self.m_tick = 1 + end + + return ret or {} +end +