X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=lib%2Frbot%2Ftimer.rb;h=03a4c91e268505750b91fc0979079c987e2d6ebd;hb=a4ff366eea4c88083be8a3d30cc6395f17b55fe2;hp=64b060bae65814a4cfe01e5f6f645fa15e6b5e72;hpb=2a96c9198c1f6e13407d0999083f6ce5e0bc06fa;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/lib/rbot/timer.rb b/lib/rbot/timer.rb index 64b060ba..03a4c91e 100644 --- a/lib/rbot/timer.rb +++ b/lib/rbot/timer.rb @@ -4,11 +4,11 @@ module Timer class Action # when this action is due next (updated by tick()) - attr_accessor :in + attr_reader :in # is this action blocked? if so it won't be run attr_accessor :blocked - + # period:: how often (seconds) to run the action # data:: optional data to pass to the proc # once:: optional, if true, this action will be run once then removed @@ -22,11 +22,29 @@ module Timer @func = func @data = data @once = once + @last_tick = Time.new + end + + def tick + diff = Time.new - @last_tick + @in -= diff + @last_tick = Time.new + end + + def inspect + "#<#{self.class}:#{@period}s:#{@once ? 'once' : 'repeat'}>" + end + + def due? + @in <= 0 end # run the action by calling its proc def run @in += @period + # really short duration timers can overrun and leave @in negative, + # for these we set @in to @period + @in = @period if @in <= 0 if(@data) @func.call(@data) else @@ -39,14 +57,19 @@ module Timer # timer handler, manage multiple Action objects, calling them when required. # The timer must be ticked by whatever controls it, i.e. regular calls to # tick() at whatever granularity suits your application's needs. - # Alternatively you can call run(), and the timer will tick itself, but this - # blocks so you gotta do it in a thread (remember ruby's threads block on - # syscalls so that can suck). + # + # Alternatively you can call run(), and the timer will spawn a thread and + # tick itself, intelligently shutting down the thread if there are no + # pending actions. class Timer - def initialize - @timers = Array.new + def initialize(granularity = 0.1) + @granularity = granularity + @timers = Hash.new @handle = 0 @lasttime = 0 + @should_be_running = false + @thread = false + @next_action_time = 0 end # period:: how often (seconds) to run the action @@ -55,25 +78,29 @@ module Timer # # add an action to the timer def add(period, data=nil, &func) + debug "adding timer, period #{period}" @handle += 1 @timers[@handle] = Action.new(period, data, &func) + start_on_add return @handle end - # period:: how often (seconds) to run the action + # period:: how long (seconds) until the action is run # data:: optional data to pass to the action's proc # func:: associate a block with add() to perform the action # # add an action to the timer which will be run just once, after +period+ def add_once(period, data=nil, &func) + debug "adding one-off timer, period #{period}" @handle += 1 @timers[@handle] = Action.new(period, data, true, &func) + start_on_add return @handle end # remove action with handle +handle+ from the timer def remove(handle) - @timers.delete_at(handle) + @timers.delete(handle) end # block action with handle +handle+ @@ -85,39 +112,94 @@ module Timer def unblock(handle) @timers[handle].blocked = false end - + # you can call this when you know you're idle, or you can split off a # thread and call the run() method to do it for you. def tick - if(@lasttime != 0) - diff = (Time.now - @lasttime).to_f - @lasttime = Time.now - @timers.compact.each { |timer| - timer.in = timer.in - diff - } - @timers.compact.each { |timer| - if (!timer.blocked) - if(timer.in <= 0) - if(timer.run) - # run once - @timers.delete(timer) - end - end - end - } - else + if(@lasttime == 0) # don't do anything on the first tick @lasttime = Time.now + return end + @next_action_time = 0 + diff = (Time.now - @lasttime).to_f + @lasttime = Time.now + @timers.each { |key,timer| + timer.tick + next if timer.blocked + if(timer.due?) + if(timer.run) + # run once + @timers.delete(key) + end + end + if @next_action_time == 0 || timer.in < @next_action_time + @next_action_time = timer.in + end + } + #debug "ticked. now #{@timers.length} timers remain" + #debug "next timer due at #{@next_action_time}" end - # the timer will tick() itself. this blocks, so run it in a thread, and - # watch out for blocking syscalls + # for backwards compat - this is a bit primitive def run(granularity=0.1) while(true) sleep(granularity) tick end end + + def running? + @thread && @thread.alive? + end + + # return the number of seconds until the next action is due, or 0 if + # none are outstanding - will only be accurate immediately after a + # tick() + def next_action_time + @next_action_time + end + + # start the timer, it spawns a thread to tick the timer, intelligently + # shutting down if no events remain and starting again when needed. + def start + return if running? + @should_be_running = true + start_thread unless @timers.empty? + end + + # stop the timer from ticking + def stop + @should_be_running = false + stop_thread + end + + private + + def start_on_add + if running? + stop_thread + start_thread + elsif @should_be_running + start_thread + end + end + + def stop_thread + return unless running? + @thread.kill + end + + def start_thread + return if running? + @thread = Thread.new do + while(true) + tick + exit if @timers.empty? + sleep(@next_action_time) + end + end + end + end end