diff --git a/base b/base index fd30a6558..3a754c728 160000 --- a/base +++ b/base @@ -1 +1 @@ -Subproject commit fd30a65587c5bc7d5830e6a40fe10aa147332945 +Subproject commit 3a754c72885c6538cb0418071b4546dbafee8724 diff --git a/frontend/apps/reader/modules/readerdictionary.lua b/frontend/apps/reader/modules/readerdictionary.lua index 7636da9e7..c8e66922e 100644 --- a/frontend/apps/reader/modules/readerdictionary.lua +++ b/frontend/apps/reader/modules/readerdictionary.lua @@ -13,6 +13,7 @@ local LuaData = require("luadata") local MultiConfirmBox = require("ui/widget/multiconfirmbox") local NetworkMgr = require("ui/network/manager") local SortWidget = require("ui/widget/sortwidget") +local TimeVal = require("ui/timeval") local Trapper = require("ui/trapper") local UIManager = require("ui/uimanager") local ffiUtil = require("ffi/util") @@ -109,7 +110,7 @@ function ReaderDictionary:init() -- Allow quick interruption or dismiss of search result window -- with tap if done before this delay. After this delay, the -- result window is shown and dismiss prevented for a few 100ms - self.quick_dismiss_before_delay = 3 + self.quick_dismiss_before_delay = TimeVal:new{ sec = 3 } -- Gather info about available dictionaries if not available_ifos then @@ -846,9 +847,9 @@ function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, box, li self:showLookupInfo(word, self.lookup_msg_delay) - self._lookup_start_ts = ffiUtil.getTimestamp() + self._lookup_start_tv = UIManager:getTime() local results = self:startSdcv(word, dict_names, fuzzy_search) - if results and results.lookup_cancelled and ffiUtil.getDuration(self._lookup_start_ts) <= self.quick_dismiss_before_delay then + if results and results.lookup_cancelled and TimeVal:now() - self._lookup_start_tv <= self.quick_dismiss_before_delay then -- If interrupted quickly just after launch, don't display anything -- (this might help avoiding refreshes and the need to dismiss -- after accidental long-press when holding a device). @@ -907,7 +908,7 @@ function ReaderDictionary:showDict(word, results, box, link) self:dismissLookupInfo() if results and results[1] then UIManager:show(self.dict_window) - if not results.lookup_cancelled and self._lookup_start_ts and ffiUtil.getDuration(self._lookup_start_ts) > self.quick_dismiss_before_delay then + if not results.lookup_cancelled and self._lookup_start_tv and TimeVal:now() - self._lookup_start_tv > self.quick_dismiss_before_delay then -- If the search took more than a few seconds to be done, discard -- queued and coming up events to avoid a voluntary dismissal -- (because the user felt the result would not come) to kill the diff --git a/frontend/apps/reader/modules/readerhighlight.lua b/frontend/apps/reader/modules/readerhighlight.lua index c26b75b07..ccf17bdb3 100644 --- a/frontend/apps/reader/modules/readerhighlight.lua +++ b/frontend/apps/reader/modules/readerhighlight.lua @@ -338,14 +338,14 @@ end -- to ensure current highlight has not already been cleared, and that we -- are not going to clear a new highlight function ReaderHighlight:getClearId() - self.clear_id = TimeVal.now() -- can act as a unique id + self.clear_id = UIManager:getTime() -- can act as a unique id return self.clear_id end function ReaderHighlight:clear(clear_id) if clear_id then -- should be provided by delayed call to clear() if clear_id ~= self.clear_id then - -- if clear_id is no more valid, highlight has already been + -- if clear_id is no longer valid, highlight has already been -- cleared since this clear_id was given return end @@ -681,7 +681,7 @@ function ReaderHighlight:_resetHoldTimer(clear) if clear then self.hold_last_tv = nil else - self.hold_last_tv = TimeVal.now() + self.hold_last_tv = UIManager:getTime() end end @@ -1124,9 +1124,8 @@ end function ReaderHighlight:onHoldRelease() local long_final_hold = false if self.hold_last_tv then - local hold_duration = TimeVal.now() - self.hold_last_tv - hold_duration = hold_duration.sec + hold_duration.usec/1000000 - if hold_duration > 3.0 then + local hold_duration = UIManager:getTime() - self.hold_last_tv + if hold_duration > TimeVal:new{ sec = 3 } then -- We stayed 3 seconds before release without updating selection long_final_hold = true end diff --git a/frontend/apps/reader/modules/readerview.lua b/frontend/apps/reader/modules/readerview.lua index 2d0962f64..21399fc2d 100644 --- a/frontend/apps/reader/modules/readerview.lua +++ b/frontend/apps/reader/modules/readerview.lua @@ -13,6 +13,7 @@ local OverlapGroup = require("ui/widget/overlapgroup") local ReaderDogear = require("apps/reader/modules/readerdogear") local ReaderFlipping = require("apps/reader/modules/readerflipping") local ReaderFooter = require("apps/reader/modules/readerfooter") +local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") local dbg = require("dbg") local logger = require("logger") @@ -71,7 +72,7 @@ local ReaderView = OverlapGroup:extend{ -- in flipping state flipping_visible = false, -- to ensure periodic flush of settings - settings_last_save_ts = nil, + settings_last_save_tv = nil, } function ReaderView:init() @@ -954,17 +955,17 @@ function ReaderView:onCloseDocument() end function ReaderView:onReaderReady() - self.settings_last_save_ts = os.time() + self.settings_last_save_tv = UIManager:getTime() end function ReaderView:onResume() -- As settings were saved on suspend, reset this on resume, -- as there's no need for a possibly immediate save. - self.settings_last_save_ts = os.time() + self.settings_last_save_tv = UIManager:getTime() end function ReaderView:checkAutoSaveSettings() - if not self.settings_last_save_ts then -- reader not yet ready + if not self.settings_last_save_tv then -- reader not yet ready return end if G_reader_settings:nilOrFalse("auto_save_settings_interval_minutes") then @@ -973,9 +974,10 @@ function ReaderView:checkAutoSaveSettings() end local interval = G_reader_settings:readSetting("auto_save_settings_interval_minutes") - local now_ts = os.time() - if now_ts - self.settings_last_save_ts >= interval*60 then - self.settings_last_save_ts = now_ts + interval = TimeVal:new{ sec = interval*60 } + local now_tv = UIManager:getTime() + if now_tv - self.settings_last_save_tv >= interval then + self.settings_last_save_tv = now_tv -- I/O, delay until after the pageturn UIManager:tickAfterNext(function() self.ui:saveSettings() diff --git a/frontend/device/android/device.lua b/frontend/device/android/device.lua index 312e0a7cf..f2c97f1e4 100644 --- a/frontend/device/android/device.lua +++ b/frontend/device/android/device.lua @@ -143,6 +143,8 @@ function Device:init() or ev.code == C.APP_CMD_INIT_WINDOW or ev.code == C.APP_CMD_WINDOW_REDRAW_NEEDED then this.device.screen:_updateWindow() + elseif ev.code == C.APP_CMD_TERM_WINDOW then + this.device.input:resetState() elseif ev.code == C.APP_CMD_CONFIG_CHANGED then -- orientation and size changes if android.screen.width ~= android.getScreenWidth() diff --git a/frontend/device/gesturedetector.lua b/frontend/device/gesturedetector.lua index 83f1c329e..c7ea97769 100644 --- a/frontend/device/gesturedetector.lua +++ b/frontend/device/gesturedetector.lua @@ -47,6 +47,11 @@ local TimeVal = require("ui/timeval") local logger = require("logger") local util = require("util") +-- We're going to need some clockid_t constants +local ffi = require("ffi") +local C = ffi.C +require("ffi/posix_h") + -- default values (all the time parameters are in microseconds) local TAP_INTERVAL = 0 * 1000 local DOUBLE_TAP_INTERVAL = 300 * 1000 @@ -56,11 +61,17 @@ local PAN_DELAYED_INTERVAL = 500 * 1000 local SWIPE_INTERVAL = 900 * 1000 -- current values local ges_tap_interval = G_reader_settings:readSetting("ges_tap_interval") or TAP_INTERVAL +ges_tap_interval = TimeVal:new{ usec = ges_tap_interval } local ges_double_tap_interval = G_reader_settings:readSetting("ges_double_tap_interval") or DOUBLE_TAP_INTERVAL +ges_double_tap_interval = TimeVal:new{ usec = ges_double_tap_interval } local ges_two_finger_tap_duration = G_reader_settings:readSetting("ges_two_finger_tap_duration") or TWO_FINGER_TAP_DURATION +ges_two_finger_tap_duration = TimeVal:new{ usec = ges_two_finger_tap_duration } local ges_hold_interval = G_reader_settings:readSetting("ges_hold_interval") or HOLD_INTERVAL +ges_hold_interval = TimeVal:new{ usec = ges_hold_interval } local ges_pan_delayed_interval = G_reader_settings:readSetting("ges_pan_delayed_interval") or PAN_DELAYED_INTERVAL +ges_pan_delayed_interval = TimeVal:new{ usec = ges_pan_delayed_interval } local ges_swipe_interval = G_reader_settings:readSetting("ges_swipe_interval") or SWIPE_INTERVAL +ges_swipe_interval = TimeVal:new{ usec = ges_swipe_interval } local GestureDetector = { -- must be initialized with the Input singleton class @@ -85,7 +96,7 @@ local GestureDetector = { }, -- states are stored in separated slots states = {}, - hold_timer_id = {}, + pending_hold_timer = {}, track_ids = {}, tev_stacks = {}, -- latest feeded touch event in each slots @@ -97,6 +108,8 @@ local GestureDetector = { detectings = {}, -- for single/double tap last_taps = {}, + -- for timestamp clocksource detection + clock_id = nil, } function GestureDetector:new(o) @@ -155,23 +168,42 @@ end tap2 is the later tap --]] function GestureDetector:isTapBounce(tap1, tap2, interval) + -- NOTE: If time went backwards, make the delta infinite to avoid misdetections, + -- as we can no longer compute a sensible value... local tv_diff = tap2.timev - tap1.timev + if not tv_diff:isPositive() then + tv_diff = TimeVal:new{ sec = math.huge } + end return ( math.abs(tap1.x - tap2.x) < self.SINGLE_TAP_BOUNCE_DISTANCE and math.abs(tap1.y - tap2.y) < self.SINGLE_TAP_BOUNCE_DISTANCE and - (tv_diff.sec == 0 and (tv_diff.usec) < interval) + tv_diff < interval ) end function GestureDetector:isDoubleTap(tap1, tap2) local tv_diff = tap2.timev - tap1.timev + if not tv_diff:isPositive() then + tv_diff = TimeVal:new{ sec = math.huge } + end return ( math.abs(tap1.x - tap2.x) < self.DOUBLE_TAP_DISTANCE and math.abs(tap1.y - tap2.y) < self.DOUBLE_TAP_DISTANCE and - (tv_diff.sec == 0 and (tv_diff.usec) < ges_double_tap_interval) + tv_diff < ges_double_tap_interval ) end +-- Takes TimeVals as input, not a tev +function GestureDetector:isHold(t1, t2) + local tv_diff = t2 - t1 + if not tv_diff:isPositive() then + tv_diff = TimeVal:new{ sec = 0 } + end + -- NOTE: We cheat by not checking a distance because we're only checking that in tapState, + -- which already ensures a stationary finger, by elimination ;). + return tv_diff >= ges_hold_interval +end + function GestureDetector:isTwoFingerTap() if self.last_tevs[0] == nil or self.last_tevs[1] == nil then return false @@ -181,14 +213,20 @@ function GestureDetector:isTwoFingerTap() local y_diff0 = math.abs(self.last_tevs[0].y - self.first_tevs[0].y) local y_diff1 = math.abs(self.last_tevs[1].y - self.first_tevs[1].y) local tv_diff0 = self.last_tevs[0].timev - self.first_tevs[0].timev + if not tv_diff0:isPositive() then + tv_diff0 = TimeVal:new{ sec = math.huge } + end local tv_diff1 = self.last_tevs[1].timev - self.first_tevs[1].timev + if not tv_diff1:isPositive() then + tv_diff1 = TimeVal:new{ sec = math.huge } + end return ( x_diff0 < self.TWO_FINGER_TAP_REGION and x_diff1 < self.TWO_FINGER_TAP_REGION and y_diff0 < self.TWO_FINGER_TAP_REGION and y_diff1 < self.TWO_FINGER_TAP_REGION and - tv_diff0.sec == 0 and tv_diff0.usec < ges_two_finger_tap_duration and - tv_diff1.sec == 0 and tv_diff1.usec < ges_two_finger_tap_duration + tv_diff0 < ges_two_finger_tap_duration and + tv_diff1 < ges_two_finger_tap_duration ) end @@ -227,7 +265,10 @@ end function GestureDetector:isSwipe(slot) if not self.first_tevs[slot] or not self.last_tevs[slot] then return end local tv_diff = self.last_tevs[slot].timev - self.first_tevs[slot].timev - if (tv_diff.sec == 0) and (tv_diff.usec < ges_swipe_interval) then + if not tv_diff:isPositive() then + tv_diff = TimeVal:new{ sec = math.huge } + end + if tv_diff < ges_swipe_interval then local x_diff = self.last_tevs[slot].x - self.first_tevs[slot].x local y_diff = self.last_tevs[slot].y - self.first_tevs[slot].y if x_diff ~= 0 or y_diff ~= 0 then @@ -254,49 +295,54 @@ end function GestureDetector:clearState(slot) self.states[slot] = self.initialState - self.hold_timer_id[slot] = nil + self.pending_hold_timer[slot] = nil self.detectings[slot] = false self.first_tevs[slot] = nil self.last_tevs[slot] = nil self.multiswipe_directions = {} self.multiswipe_type = nil + + -- Also clear any pending hold callbacks on that slot. + -- (single taps call this, so we can't clear double_tap callbacks without being caught in an obvious catch-22 ;)). + self.input:clearTimeout(slot, "hold") end function GestureDetector:setNewInterval(type, interval) if type == "ges_tap_interval" then - ges_tap_interval = interval + ges_tap_interval = TimeVal:new{ usec = interval } elseif type == "ges_double_tap_interval" then - ges_double_tap_interval = interval + ges_double_tap_interval = TimeVal:new{ usec = interval } elseif type == "ges_two_finger_tap_duration" then - ges_two_finger_tap_duration = interval + ges_two_finger_tap_duration = TimeVal:new{ usec = interval } elseif type == "ges_hold_interval" then - ges_hold_interval = interval + ges_hold_interval = TimeVal:new{ usec = interval } elseif type == "ges_pan_delayed_interval" then - ges_pan_delayed_interval = interval + ges_pan_delayed_interval = TimeVal:new{ usec = interval } elseif type == "ges_swipe_interval" then - ges_swipe_interval = interval + ges_swipe_interval = TimeVal:new{ usec = interval } end end function GestureDetector:getInterval(type) if type == "ges_tap_interval" then - return ges_tap_interval + return ges_tap_interval:tousecs() elseif type == "ges_double_tap_interval" then - return ges_double_tap_interval + return ges_double_tap_interval:tousecs() elseif type == "ges_two_finger_tap_duration" then - return ges_two_finger_tap_duration + return ges_two_finger_tap_duration:tousecs() elseif type == "ges_hold_interval" then - return ges_hold_interval + return ges_hold_interval:tousecs() elseif type == "ges_pan_delayed_interval" then - return ges_pan_delayed_interval + return ges_pan_delayed_interval:tousecs() elseif type == "ges_swipe_interval" then - return ges_swipe_interval + return ges_swipe_interval:tousecs() end end function GestureDetector:clearStates() - self:clearState(0) - self:clearState(1) + for k, _ in pairs(self.states) do + self:clearState(k) + end end function GestureDetector:initialState(tev) @@ -320,10 +366,61 @@ function GestureDetector:initialState(tev) end end +--[[-- +Attempts to figure out which clock source tap events are using... +]] +function GestureDetector:probeClockSource(timev) + -- We'll check if that timestamp is +/- 2.5s away from the three potential clock sources supported by evdev. + -- We have bigger issues than this if we're parsing events more than 3s late ;). + local threshold = TimeVal:new{ sec = 2, usec = 500000 } + + -- Start w/ REALTIME, because it's the easiest to detect ;). + local realtime = TimeVal:realtime_coarse() + -- clock-threshold <= timev <= clock+threshold + if timev >= realtime - threshold and timev <= realtime + threshold then + self.clock_id = C.CLOCK_REALTIME + logger.info("GestureDetector:probeClockSource: Touch event timestamps appear to use CLOCK_REALTIME") + return + end + + -- Then MONOTONIC, as it's (hopefully) more common than BOOTTIME (and also guaranteed to be an usable clock source) + local monotonic = TimeVal:monotonic_coarse() + if timev >= monotonic - threshold and timev <= monotonic + threshold then + self.clock_id = C.CLOCK_MONOTONIC + logger.info("GestureDetector:probeClockSource: Touch event timestamps appear to use CLOCK_MONOTONIC") + return + end + + -- Finally, BOOTTIME + local boottime = TimeVal:boottime() + if timev >= boottime - threshold and timev <= boottime + threshold then + self.clock_id = C.CLOCK_BOOTTIME + logger.info("GestureDetector:probeClockSource: Touch event timestamps appear to use CLOCK_BOOTTIME") + return + end + + -- If we're here, the detection was inconclusive :/ + self.clock_id = -1 + logger.info("GestureDetector:probeClockSource: Touch event clock source detection was inconclusive") +end + +function GestureDetector:getClockSource() + return self.clock_id +end + +function GestureDetector:resetClockSource() + self.clock_id = nil +end + --[[-- Handles both single and double tap. --]] function GestureDetector:tapState(tev) + -- Attempt to detect the clock source for these events (we reset it on suspend to discriminate MONOTONIC from BOOTTIME). + if not self.clock_id then + self:probeClockSource(tev.timev) + end + logger.dbg("in tap state...") local slot = tev.slot if tev.id == -1 then @@ -397,7 +494,7 @@ function GestureDetector:handleDoubleTap(tev) local tap_interval = self.input.tap_interval_override or ges_tap_interval -- We do tap bounce detection even when double tap is enabled (so, double tap -- is triggered when: ges_tap_interval <= delay < ges_double_tap_interval) - if tap_interval > 0 and self.last_taps[slot] ~= nil and self:isTapBounce(self.last_taps[slot], cur_tap, tap_interval) then + if not tap_interval:isZero() and self.last_taps[slot] ~= nil and self:isTapBounce(self.last_taps[slot], cur_tap, tap_interval) then logger.dbg("tap bounce detected in slot", slot, ": ignored") -- Simply ignore it, and clear state as this is the end of a touch event -- (this doesn't clear self.last_taps[slot], so a 3rd tap can be detected @@ -410,6 +507,7 @@ function GestureDetector:handleDoubleTap(tev) self:isDoubleTap(self.last_taps[slot], cur_tap) then -- it is a double tap self:clearState(slot) + self.input:clearTimeout(slot, "double_tap") ges_ev.ges = "double_tap" self.last_taps[slot] = nil logger.dbg("double tap detected in slot", slot) @@ -431,16 +529,9 @@ function GestureDetector:handleDoubleTap(tev) -- may be the start of a double tap. We'll send it as a single tap after -- a timer if no second tap happened in the double tap delay. logger.dbg("set up single/double tap timer") - -- deadline should be calculated by adding current tap time and the interval - -- (No need to compute self._has_real_clock_time_ev_time here, we should always - -- have been thru handleNonTap() where it is computed, before getting here) - local ref_time = self._has_real_clock_time_ev_time and cur_tap.timev or TimeVal:now() - local deadline = ref_time + TimeVal:new{ - sec = 0, - usec = not self.input.disable_double_tap and ges_double_tap_interval or 0, - } - self.input:setTimeout(function() - logger.dbg("in single/double tap timer", self.last_taps[slot] ~= nil) + -- setTimeout will handle computing the deadline in the least lossy way possible given the platform. + self.input:setTimeout(slot, "double_tap", function() + logger.dbg("in single/double tap timer, single tap:", self.last_taps[slot] ~= nil) -- double tap will set last_tap to nil so if it is not, then -- user has not double-tap'ed: it's a single tap if self.last_taps[slot] ~= nil then @@ -449,7 +540,7 @@ function GestureDetector:handleDoubleTap(tev) logger.dbg("single tap detected in slot", slot, ges_ev.pos) return ges_ev end - end, deadline) + end, tev.timev, ges_double_tap_interval) -- we are already at the end of touch event -- so reset the state self:clearState(slot) @@ -461,34 +552,21 @@ function GestureDetector:handleNonTap(tev) -- switched from other state, probably from initialState -- we return nil in this case self.states[slot] = self.tapState - if self._has_real_clock_time_ev_time == nil then - if tev.timev.sec < TimeVal:now().sec - 600 then - -- ev.timev is probably the uptime since device boot - -- (which might pause on suspend) that we can't use - -- with setTimeout(): we'll use TimeVal:now() - self._has_real_clock_time_ev_time = false - logger.info("event times are not real clock time: some adjustments will be made") - else - -- assume they are real clock time - self._has_real_clock_time_ev_time = true - logger.info("event times are real clock time: no adjustment needed") - end - end logger.dbg("set up hold timer") - local ref_time = self._has_real_clock_time_ev_time and tev.timev or TimeVal:now() - local deadline = ref_time + TimeVal:new{ - sec = 0, usec = ges_hold_interval - } - -- Be sure the following setTimeout only react to this tapState - local hold_timer_id = tev.timev - self.hold_timer_id[slot] = hold_timer_id - self.input:setTimeout(function() - if self.states[slot] == self.tapState and self.hold_timer_id[slot] == hold_timer_id then - -- timer set in tapState, so we switch to hold + -- Invalidate previous hold timers on that slot so that the following setTimeout will only react to *this* tapState. + self.input:clearTimeout(slot, "hold") + self.pending_hold_timer[slot] = true + self.input:setTimeout(slot, "hold", function() + -- If the pending_hold_timer we set on our first switch to tapState on this slot (e.g., first finger down event), + -- back when the timer was setup, is still relevant (e.g., the slot wasn't run through clearState by a finger up gesture), + -- then check that we're still in a stationary finger down state (e.g., tapState). + if self.pending_hold_timer[slot] and self.states[slot] == self.tapState then + -- That means we can switch to hold logger.dbg("hold gesture detected in slot", slot) + self.pending_hold_timer[slot] = nil return self:switchState("holdState", tev, true) end - end, deadline) + end, tev.timev, ges_hold_interval) return { ges = "touch", pos = Geom:new{ @@ -499,12 +577,10 @@ function GestureDetector:handleNonTap(tev) time = tev.timev, } else - -- it is not end of touch event, see if we need to switch to - -- other states + -- We're still inside a stream of input events, see if we need to switch to other states. if (tev.x and math.abs(tev.x - self.first_tevs[slot].x) >= self.PAN_THRESHOLD) or (tev.y and math.abs(tev.y - self.first_tevs[slot].y) >= self.PAN_THRESHOLD) then - -- if user's finger moved long enough in X or - -- Y distance, we switch to pan state + -- If user's finger moved far enough on the X or Y axes, switch to pan state. return self:switchState("panState", tev) end end @@ -594,6 +670,9 @@ function GestureDetector:handlePan(tev) else local pan_direction, pan_distance = self:getPath(slot) local tv_diff = self.last_tevs[slot].timev - self.first_tevs[slot].timev + if not tv_diff:isPositive() then + tv_diff = TimeVal:new{ sec = math.huge } + end local pan_ev = { ges = "pan", @@ -620,7 +699,7 @@ function GestureDetector:handlePan(tev) -- delayed pan, used where necessary to reduce potential activation of panning -- when swiping is intended (e.g., for the menu or for multiswipe) - if not ((tv_diff.sec == 0) and (tv_diff.usec < ges_pan_delayed_interval)) then + if not (tv_diff < ges_pan_delayed_interval) then pan_ev.relative_delayed.x = tev.x - self.first_tevs[slot].x pan_ev.relative_delayed.y = tev.y - self.first_tevs[slot].y pan_ev.distance_delayed = pan_distance @@ -774,7 +853,7 @@ end function GestureDetector:holdState(tev, hold) logger.dbg("in hold state...") local slot = tev.slot - -- when we switch to hold state, we pass additional param "hold" + -- When we switch to hold state, we pass an additional boolean param "hold". if tev.id ~= -1 and hold and self.last_tevs[slot].x and self.last_tevs[slot].y then self.states[slot] = self.holdState return { diff --git a/frontend/device/input.lua b/frontend/device/input.lua index 32016912e..bfe9878a4 100644 --- a/frontend/device/input.lua +++ b/frontend/device/input.lua @@ -13,44 +13,22 @@ local input = require("ffi/input") local logger = require("logger") local _ = require("gettext") +-- We're going to need a few constants... +local ffi = require("ffi") +local C = ffi.C +require("ffi/posix_h") +require("ffi/linux_input_h") + -- luacheck: push -- luacheck: ignore --- constants from -local EV_SYN = 0 -local EV_KEY = 1 -local EV_ABS = 3 -local EV_MSC = 4 --- for frontend SDL event handling -local EV_SDL = 53 -- ASCII code for S - -- key press event values (KEY.value) local EVENT_VALUE_KEY_PRESS = 1 local EVENT_VALUE_KEY_REPEAT = 2 local EVENT_VALUE_KEY_RELEASE = 0 --- Synchronization events (SYN.code). -local SYN_REPORT = 0 -local SYN_CONFIG = 1 -local SYN_MT_REPORT = 2 - --- For single-touch events (ABS.code). -local ABS_X = 00 -local ABS_Y = 01 -local ABS_PRESSURE = 24 - --- For multi-touch events (ABS.code). -local ABS_MT_SLOT = 47 -local ABS_MT_TOUCH_MAJOR = 48 -local ABS_MT_WIDTH_MAJOR = 50 - -local ABS_MT_POSITION_X = 53 -local ABS_MT_POSITION_Y = 54 -local ABS_MT_TRACKING_ID = 57 -local ABS_MT_PRESSURE = 58 - -- For Kindle Oasis orientation events (ABS.code) --- the ABS code of orientation event will be adjusted to -24 from 24(ABS_PRESSURE) --- as ABS_PRESSURE is also used to detect touch input in KOBO devices. +-- the ABS code of orientation event will be adjusted to -24 from 24 (C.ABS_PRESSURE) +-- as C.ABS_PRESSURE is also used to detect touch input in KOBO devices. local ABS_OASIS_ORIENTATION = -24 local DEVICE_ORIENTATION_PORTRAIT_LEFT = 15 local DEVICE_ORIENTATION_PORTRAIT_RIGHT = 17 @@ -61,9 +39,6 @@ local DEVICE_ORIENTATION_PORTRAIT_ROTATED = 20 local DEVICE_ORIENTATION_LANDSCAPE = 21 local DEVICE_ORIENTATION_LANDSCAPE_ROTATED = 22 --- For the events of the Forma accelerometer (MSC.code) -local MSC_RAW = 0x03 - -- For the events of the Forma accelerometer (MSC.value) local MSC_RAW_GSENSOR_PORTRAIT_DOWN = 0x17 local MSC_RAW_GSENSOR_PORTRAIT_UP = 0x18 @@ -73,6 +48,56 @@ local MSC_RAW_GSENSOR_LANDSCAPE_LEFT = 0x1a local MSC_RAW_GSENSOR_BACK = 0x1b local MSC_RAW_GSENSOR_FRONT = 0x1c +-- For debug logging of ev.type +local linux_evdev_type_map = { + [C.EV_SYN] = "EV_SYN", + [C.EV_KEY] = "EV_KEY", + [C.EV_REL] = "EV_REL", + [C.EV_ABS] = "EV_ABS", + [C.EV_MSC] = "EV_MSC", + [C.EV_SW] = "EV_SW", + [C.EV_LED] = "EV_LED", + [C.EV_SND] = "EV_SND", + [C.EV_REP] = "EV_REP", + [C.EV_FF] = "EV_FF", + [C.EV_PWR] = "EV_PWR", + [C.EV_FF_STATUS] = "EV_FF_STATUS", + [C.EV_MAX] = "EV_MAX", + [C.EV_SDL] = "EV_SDL", +} + +-- For debug logging of ev.code +local linux_evdev_syn_code_map = { + [C.SYN_REPORT] = "SYN_REPORT", + [C.SYN_CONFIG] = "SYN_CONFIG", + [C.SYN_MT_REPORT] = "SYN_MT_REPORT", + [C.SYN_DROPPED] = "SYN_DROPPED", +} + +local linux_evdev_abs_code_map = { + [C.ABS_X] = "ABS_X", + [C.ABS_Y] = "ABS_Y", + [C.ABS_PRESSURE] = "ABS_PRESSURE", + [C.ABS_MT_SLOT] = "ABS_MT_SLOT", + [C.ABS_MT_TOUCH_MAJOR] = "ABS_MT_TOUCH_MAJOR", + [C.ABS_MT_TOUCH_MINOR] = "ABS_MT_TOUCH_MINOR", + [C.ABS_MT_WIDTH_MAJOR] = "ABS_MT_WIDTH_MAJOR", + [C.ABS_MT_WIDTH_MINOR] = "ABS_MT_WIDTH_MINOR", + [C.ABS_MT_ORIENTATION] = "ABS_MT_ORIENTATION", + [C.ABS_MT_POSITION_X] = "ABS_MT_POSITION_X", + [C.ABS_MT_POSITION_Y] = "ABS_MT_POSITION_Y", + [C.ABS_MT_TOOL_TYPE] = "ABS_MT_TOOL_TYPE", + [C.ABS_MT_BLOB_ID] = "ABS_MT_BLOB_ID", + [C.ABS_MT_TRACKING_ID] = "ABS_MT_TRACKING_ID", + [C.ABS_MT_PRESSURE] = "ABS_MT_PRESSURE", + [C.ABS_MT_DISTANCE] = "ABS_MT_DISTANCE", + [C.ABS_MT_TOOL_X] = "ABS_MT_TOOL_X", + [C.ABS_MT_TOOL_Y] = "ABS_MT_TOOL_Y", +} + +local linux_evdev_msc_code_map = { + [C.MSC_RAW] = "MSC_RAW", +} -- luacheck: pop local _internal_clipboard_text = nil -- holds the last copied text @@ -236,72 +261,148 @@ end --- Catalog of predefined hooks. function Input:adjustTouchSwitchXY(ev) - if ev.type == EV_ABS then - if ev.code == ABS_X then - ev.code = ABS_Y - elseif ev.code == ABS_Y then - ev.code = ABS_X - elseif ev.code == ABS_MT_POSITION_X then - ev.code = ABS_MT_POSITION_Y - elseif ev.code == ABS_MT_POSITION_Y then - ev.code = ABS_MT_POSITION_X + if ev.type == C.EV_ABS then + if ev.code == C.ABS_X then + ev.code = C.ABS_Y + elseif ev.code == C.ABS_Y then + ev.code = C.ABS_X + elseif ev.code == C.ABS_MT_POSITION_X then + ev.code = C.ABS_MT_POSITION_Y + elseif ev.code == C.ABS_MT_POSITION_Y then + ev.code = C.ABS_MT_POSITION_X end end end function Input:adjustTouchScale(ev, by) - if ev.type == EV_ABS then - if ev.code == ABS_X or ev.code == ABS_MT_POSITION_X then + if ev.type == C.EV_ABS then + if ev.code == C.ABS_X or ev.code == C.ABS_MT_POSITION_X then ev.value = by.x * ev.value end - if ev.code == ABS_Y or ev.code == ABS_MT_POSITION_Y then + if ev.code == C.ABS_Y or ev.code == C.ABS_MT_POSITION_Y then ev.value = by.y * ev.value end end end function Input:adjustTouchMirrorX(ev, width) - if ev.type == EV_ABS - and (ev.code == ABS_X or ev.code == ABS_MT_POSITION_X) then + if ev.type == C.EV_ABS + and (ev.code == C.ABS_X or ev.code == C.ABS_MT_POSITION_X) then ev.value = width - ev.value end end function Input:adjustTouchMirrorY(ev, height) - if ev.type == EV_ABS - and (ev.code == ABS_Y or ev.code == ABS_MT_POSITION_Y) then + if ev.type == C.EV_ABS + and (ev.code == C.ABS_Y or ev.code == C.ABS_MT_POSITION_Y) then ev.value = height - ev.value end end function Input:adjustTouchTranslate(ev, by) - if ev.type == EV_ABS then - if ev.code == ABS_X or ev.code == ABS_MT_POSITION_X then + if ev.type == C.EV_ABS then + if ev.code == C.ABS_X or ev.code == C.ABS_MT_POSITION_X then ev.value = by.x + ev.value end - if ev.code == ABS_Y or ev.code == ABS_MT_POSITION_Y then + if ev.code == C.ABS_Y or ev.code == C.ABS_MT_POSITION_Y then ev.value = by.y + ev.value end end end function Input:adjustKindleOasisOrientation(ev) - if ev.type == EV_ABS and ev.code == ABS_PRESSURE then + if ev.type == C.EV_ABS and ev.code == C.ABS_PRESSURE then ev.code = ABS_OASIS_ORIENTATION end end -function Input:setTimeout(cb, tv_out) +function Input:setTimeout(slot, ges, cb, origin, delay) local item = { + slot = slot, + gesture = ges, callback = cb, - deadline = tv_out, } + + -- We're going to need the clock source id for these events from GestureDetector + local clock_id = self.gesture_detector:getClockSource() + local deadline + + -- If we're on a platform with the timerfd backend, handle that + local timerfd + if input.setTimer then + -- If GestureDetector's clock source probing was inconclusive, do this on the UI timescale instead. + if clock_id == -1 then + deadline = TimeVal:now() + delay + else + deadline = origin + delay + end + -- What this does is essentially to ask the kernel to wake us up when the timer expires, + -- instead of ensuring that ourselves via a polling timeout. + -- This ensures perfect accuracy, and allows it to be computed in the event's own timescale. + timerfd = input.setTimer(clock_id, deadline.sec, deadline.usec) + end + if timerfd then + -- It worked, tweak the table a bit to make it clear the deadline will be handled by the kernel + item.timerfd = timerfd + -- We basically only need this for the sorting ;). + item.deadline = deadline + else + -- No timerfd, we'll compute a poll timeout ourselves. + if clock_id == C.CLOCK_MONOTONIC then + -- If the event's clocksource is monotonic, we can use it directly. + deadline = origin + delay + else + -- Otherwise, fudge it by using a current timestamp in the UI's timescale (MONOTONIC). + -- This isn't the end of the world in practice (c.f., #7415). + deadline = TimeVal:now() + delay + end + item.deadline = deadline + end table.insert(self.timer_callbacks, item) - table.sort(self.timer_callbacks, function(v1,v2) + + -- NOTE: While the timescale is monotonic, we may interleave timers based on different delays, so we still need to sort... + table.sort(self.timer_callbacks, function(v1, v2) return v1.deadline < v2.deadline end) end +-- Clear all timeouts for a specific slot (and a specific gesture, if ges is set) +function Input:clearTimeout(slot, ges) + for i = #self.timer_callbacks, 1, -1 do + local item = self.timer_callbacks[i] + if item.slot == slot and (not ges or item.gesture == ges) then + -- If the timerfd backend is in use, close the fd and free the list's node, too. + if item.timerfd then + input.clearTimer(item.timerfd) + end + table.remove(self.timer_callbacks, i) + end + end +end + +function Input:clearTimeouts() + -- If the timerfd backend is in use, close the fds, too + if input.setTimer then + for _, item in ipairs(self.timer_callbacks) do + if item.timerfd then + input.clearTimer(item.timerfd) + end + end + end + + self.timer_callbacks = {} +end + +-- Reset the gesture parsing state to a blank slate +function Input:resetState() + if self.gesture_detector then + self.gesture_detector:clearStates() + -- Resets the clock source probe + self.gesture_detector:resetClockSource() + end + self:clearTimeouts() +end + function Input:handleKeyBoardEv(ev) local keycode = self.event_map[ev.code] if not keycode then @@ -434,7 +535,7 @@ From kernel document: For type B devices, the kernel driver should associate a slot with each identified contact, and use that slot to propagate changes for the contact. Creation, replacement and destruction of contacts is achieved by modifying -the ABS_MT_TRACKING_ID of the associated slot. A non-negative tracking id +the C.ABS_MT_TRACKING_ID of the associated slot. A non-negative tracking id is interpreted as a contact, and the value -1 denotes an unused slot. A tracking id not previously present is considered new, and a tracking id no longer present is considered removed. Since only changes are propagated, @@ -443,29 +544,29 @@ end. Upon receiving an MT event, one simply updates the appropriate attribute of the current slot. --]] function Input:handleTouchEv(ev) - if ev.type == EV_ABS then + if ev.type == C.EV_ABS then if #self.MTSlots == 0 then table.insert(self.MTSlots, self:getMtSlot(self.cur_slot)) end - if ev.code == ABS_MT_SLOT then + if ev.code == C.ABS_MT_SLOT then self:addSlotIfChanged(ev.value) - elseif ev.code == ABS_MT_TRACKING_ID then + elseif ev.code == C.ABS_MT_TRACKING_ID then if self.snow_protocol then self:addSlotIfChanged(ev.value) end self:setCurrentMtSlot("id", ev.value) - elseif ev.code == ABS_MT_POSITION_X then + elseif ev.code == C.ABS_MT_POSITION_X then self:setCurrentMtSlot("x", ev.value) - elseif ev.code == ABS_MT_POSITION_Y then + elseif ev.code == C.ABS_MT_POSITION_Y then self:setCurrentMtSlot("y", ev.value) -- code to emulate mt protocol on kobos -- we "confirm" abs_x, abs_y only when pressure ~= 0 - elseif ev.code == ABS_X then + elseif ev.code == C.ABS_X then self:setCurrentMtSlot("abs_x", ev.value) - elseif ev.code == ABS_Y then + elseif ev.code == C.ABS_Y then self:setCurrentMtSlot("abs_y", ev.value) - elseif ev.code == ABS_PRESSURE then + elseif ev.code == C.ABS_PRESSURE then if ev.value ~= 0 then self:setCurrentMtSlot("id", 1) self:confirmAbsxy() @@ -474,8 +575,8 @@ function Input:handleTouchEv(ev) self:setCurrentMtSlot("id", -1) end end - elseif ev.type == EV_SYN then - if ev.code == SYN_REPORT then + elseif ev.type == C.EV_SYN then + if ev.code == C.SYN_REPORT then for _, MTSlot in pairs(self.MTSlots) do self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time)) if self.snow_protocol then @@ -517,49 +618,49 @@ function Input:handleTouchEvPhoenix(ev) -- Hack on handleTouchEV for the Kobo Aura -- It seems to be using a custom protocol: -- finger 0 down: - -- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 0); - -- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 1); - -- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 1); - -- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, x1); - -- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_Y, y1); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 0); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 1); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 1); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_X, x1); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_Y, y1); -- input_mt_sync (elan_touch_data.input); -- finger 1 down: - -- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 1); - -- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 1); - -- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 1); - -- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, x2); - -- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_Y, y2); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 1); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 1); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 1); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_X, x2); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_Y, y2); -- input_mt_sync (elan_touch_data.input); -- finger 0 up: - -- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 0); - -- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 0); - -- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 0); - -- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, last_x); - -- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_Y, last_y); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 0); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 0); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 0); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_X, last_x); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_Y, last_y); -- input_mt_sync (elan_touch_data.input); -- finger 1 up: - -- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 1); - -- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 0); - -- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 0); - -- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, last_x2); - -- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_Y, last_y2); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 1); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 0); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 0); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_X, last_x2); + -- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_Y, last_y2); -- input_mt_sync (elan_touch_data.input); - if ev.type == EV_ABS then + if ev.type == C.EV_ABS then if #self.MTSlots == 0 then table.insert(self.MTSlots, self:getMtSlot(self.cur_slot)) end - if ev.code == ABS_MT_TRACKING_ID then + if ev.code == C.ABS_MT_TRACKING_ID then self:addSlotIfChanged(ev.value) self:setCurrentMtSlot("id", ev.value) - elseif ev.code == ABS_MT_TOUCH_MAJOR and ev.value == 0 then + elseif ev.code == C.ABS_MT_TOUCH_MAJOR and ev.value == 0 then self:setCurrentMtSlot("id", -1) - elseif ev.code == ABS_MT_POSITION_X then + elseif ev.code == C.ABS_MT_POSITION_X then self:setCurrentMtSlot("x", ev.value) - elseif ev.code == ABS_MT_POSITION_Y then + elseif ev.code == C.ABS_MT_POSITION_Y then self:setCurrentMtSlot("y", ev.value) end - elseif ev.type == EV_SYN then - if ev.code == SYN_REPORT then + elseif ev.type == C.EV_SYN then + if ev.code == C.SYN_REPORT then for _, MTSlot in pairs(self.MTSlots) do self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time)) end @@ -578,23 +679,23 @@ end function Input:handleTouchEvLegacy(ev) -- Single Touch Protocol. Some devices emit both singletouch and multitouch events. -- In those devices the 'handleTouchEv' function doesn't work as expected. Use this function instead. - if ev.type == EV_ABS then + if ev.type == C.EV_ABS then if #self.MTSlots == 0 then table.insert(self.MTSlots, self:getMtSlot(self.cur_slot)) end - if ev.code == ABS_X then + if ev.code == C.ABS_X then self:setCurrentMtSlot("x", ev.value) - elseif ev.code == ABS_Y then + elseif ev.code == C.ABS_Y then self:setCurrentMtSlot("y", ev.value) - elseif ev.code == ABS_PRESSURE then + elseif ev.code == C.ABS_PRESSURE then if ev.value ~= 0 then self:setCurrentMtSlot("id", 1) else self:setCurrentMtSlot("id", -1) end end - elseif ev.type == EV_SYN then - if ev.code == SYN_REPORT then + elseif ev.type == C.EV_SYN then + if ev.code == C.SYN_REPORT then for _, MTSlot in pairs(self.MTSlots) do self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time)) end @@ -651,7 +752,7 @@ end --- Accelerometer on the Forma, c.f., drivers/hwmon/mma8x5x.c function Input:handleMiscEvNTX(ev) local rotation_mode, screen_mode - if ev.code == MSC_RAW then + if ev.code == C.MSC_RAW then if ev.value == MSC_RAW_GSENSOR_PORTRAIT_UP then -- i.e., UR rotation_mode = framebuffer.ORIENTATION_PORTRAIT @@ -774,90 +875,198 @@ end --- Main event handling. -function Input:waitEvent(timeout_us) +-- `now` corresponds to UIManager:getTime() (a TimeVal), and it's just been updated by UIManager. +-- `deadline` (a TimeVal) is the absolute deadline imposed by UIManager:handleInput() (a.k.a., our main event loop ^^): +-- it's either nil (meaning block forever waiting for input), or the earliest UIManager deadline (in most cases, that's the next scheduled task, +-- in much less common cases, that's the earliest of UIManager.INPUT_TIMEOUT (currently, only KOSync ever sets it) or UIManager.ZMQ_TIMEOUT if there are pending ZMQs). +function Input:waitEvent(now, deadline) + -- On the first iteration of the loop, we don't need to update now, we're following closely (a couple ms at most) behind UIManager. local ok, ev - -- wrapper for input.waitForEvents that will retry for some cases + -- Wrapper around the platform-specific input.waitForEvent (which itself is generally poll-like). + -- Speaking of input.waitForEvent, it can return: + -- * true, ev: When an input event was read. ev is a table mapped after the input_event struct. + -- * false, errno, timerfd: When no input event was read, possibly for benign reasons. + -- One such common case is after a polling timeout, in which case errno is C.ETIME. + -- If the timerfd backend is in use, and the early return was caused by a timerfd expiring, + -- it returns false, C.ETIME, timerfd; where timerfd is a C pointer (i.e., light userdata) + -- to the timerfd node that expired (so as to be able to free it later, c.f., input/timerfd-callbacks.h). + -- Otherwise, errno is the actual error code from the backend (e.g., select's errno for the C backend). + -- * nil: When something terrible happened (e.g., fatal poll/read failure). We abort in such cases. while true do if #self.timer_callbacks > 0 then - local wait_deadline = TimeVal:now() + TimeVal:new{ - usec = timeout_us - } - -- we don't block if there is any timer, set wait to 10us + -- If we have timers set, we need to honor them once we're done draining the input events. while #self.timer_callbacks > 0 do - ok, ev = pcall(input.waitForEvent, 100) + -- Choose the earliest deadline between the next timer deadline, and our full timeout deadline. + local deadline_is_timer = false + local poll_deadline + -- If the timer's deadline is handled via timerfd, that's easy + if self.timer_callbacks[1].timerfd then + -- We use the ultimate deadline, as the kernel will just signal us when the timer expires during polling. + poll_deadline = deadline + else + if not deadline then + -- If we don't actually have a full timeout deadline, just honor the timer's. + poll_deadline = self.timer_callbacks[1].deadline + deadline_is_timer = true + else + if self.timer_callbacks[1].deadline < deadline then + poll_deadline = self.timer_callbacks[1].deadline + deadline_is_timer = true + else + poll_deadline = deadline + end + end + end + local poll_timeout + -- With the timerfd backend, poll_deadline is set to deadline, which might be nil, in which case, + -- we can happily block forever, like in the no timer_callbacks branch below ;). + if poll_deadline then + -- If we haven't hit that deadline yet, poll until it expires, otherwise, + -- have select return immediately so that we trip a timeout. + now = now or TimeVal:now() + if poll_deadline > now then + -- Deadline hasn't been blown yet, honor it. + poll_timeout = poll_deadline - now + else + -- We've already blown the deadline: make select return immediately (most likely straight to timeout) + poll_timeout = TimeVal:new{ sec = 0 } + end + end + + local timerfd + ok, ev, timerfd = input.waitForEvent(poll_timeout and poll_timeout.sec, poll_timeout and poll_timeout.usec) + -- We got an actual input event, go and process it if ok then break end - local tv_now = TimeVal:now() - if (not timeout_us or tv_now < wait_deadline) then - -- check whether timer is up - if tv_now >= self.timer_callbacks[1].deadline then + + -- If we've drained all pending input events, causing waitForEvent to time out, check our timers + if ok == false and ev == C.ETIME then + -- Check whether the earliest timer to finalize a Gesture detection is up. + -- If we were woken up by a timerfd, or if our actual select deadline was the timer itself, + -- we're guaranteed to have reached it. + -- But if it was a task deadline instead, we to have to check it against the current time. + if timerfd or (deadline_is_timer or TimeVal:now() >= self.timer_callbacks[1].deadline) then local touch_ges = self.timer_callbacks[1].callback() table.remove(self.timer_callbacks, 1) + -- If it was a timerfd, we also need to close the fd. + -- NOTE: The fact that deadlines are sorted *should* ensure that the timerfd that expired + -- is actually the first of the list without us having to double-check that... + if timerfd then + input.clearTimer(timerfd) + end if touch_ges then - -- Do we really need to clear all setTimeout after - -- decided a gesture? FIXME - self.timer_callbacks = {} + -- The timers we'll encounter are for finalizing a hold or (if enabled) double tap gesture, + -- as such, it makes no sense to try to detect *multiple* subsequent gestures. + -- This is why we clear the full list of timers on the first match ;). + self:clearTimeouts() self:gestureAdjustHook(touch_ges) return Event:new("Gesture", self.gesture_detector:adjustGesCoordinate(touch_ges) ) - end -- EOF if touch_ges - end -- EOF if deadline reached - else - break - end -- EOF if not exceed wait timeout + end -- if touch_ges + end -- if poll_deadline reached + end -- if poll returned ETIME + + -- Refresh now on the next iteration (e.g., when we have multiple timers to check) + now = nil end -- while #timer_callbacks > 0 else - ok, ev = pcall(input.waitForEvent, timeout_us) - end -- EOF if #timer_callbacks > 0 - if ok then - break - end + -- If there aren't any timers, just block for the requested amount of time. + -- deadline may be nil, in which case waitForEvent blocks indefinitely (i.e., until the next input event ;)). + local poll_timeout + -- If UIManager put us on deadline, enforce it, otherwise, block forever. + if deadline then + -- Convert that absolute deadline to value relative to *now*, as we may loop multiple times between UI ticks. + now = now or TimeVal:now() + if deadline > now then + -- Deadline hasn't been blown yet, honor it. + poll_timeout = deadline - now + else + -- Deadline has been blown: make select return immediately. + poll_timeout = TimeVal:new{ sec = 0 } + end + end - -- ev does contain an error message: - local timeout_err_msg = "Waiting for input failed: timeout\n" - -- ev may not be equal to timeout_err_msg, but it may ends with it - -- ("./ffi/SDL2_0.lua:110: Waiting for input failed: timeout" on the emulator) - if ev and ev.sub and ev:sub(-timeout_err_msg:len()) == timeout_err_msg then - -- don't report an error on timeout - ev = nil + ok, ev = input.waitForEvent(poll_timeout and poll_timeout.sec, poll_timeout and poll_timeout.usec) + end -- if #timer_callbacks > 0 + + -- Handle errors + if ok then + -- We're good, process the event and go back to UIManager. break - elseif ev == "application forced to quit" then - --- @todo return an event that can be handled - os.exit(0, true) - end - logger.warn("got error waiting for events:", ev) - if ev ~= "Waiting for input failed: 4\n" then - -- we only abort if the error is not EINTR + elseif ok == false then + if ev == C.ETIME then + -- Don't report an error on ETIME, and go back to UIManager + ev = nil + break + elseif ev == C.EINTR then -- luacheck: ignore + -- Retry on EINTR + else + -- Warn, report, and go back to UIManager + logger.warn("Polling for input events returned an error:", ev) + break + end + elseif ok == nil then + -- Something went horribly wrong, abort. + logger.err("Polling for input events failed catastrophically") + local UIManager = require("ui/uimanager") + UIManager:abort() break end + + -- We'll need to refresh now on the next iteration, if there is one. + now = nil end if ok and ev then - if DEBUG.is_on and ev then + if DEBUG.is_on then DEBUG:logEv(ev) - logger.dbg(string.format( - "%s event => type: %d, code: %d(%s), value: %s, time: %d.%d", - ev.type == EV_KEY and "key" or "input", - ev.type, ev.code, self.event_map[ev.code], tostring(ev.value), - ev.time.sec, ev.time.usec)) + if ev.type == C.EV_KEY then + logger.dbg(string.format( + "key event => code: %d (%s), value: %s, time: %d.%d", + ev.code, self.event_map[ev.code], ev.value, + ev.time.sec, ev.time.usec)) + elseif ev.type == C.EV_SYN then + logger.dbg(string.format( + "input event => type: %d (%s), code: %d (%s), value: %s, time: %d.%d", + ev.type, linux_evdev_type_map[ev.type], ev.code, linux_evdev_syn_code_map[ev.code], ev.value, + ev.time.sec, ev.time.usec)) + elseif ev.type == C.EV_ABS then + logger.dbg(string.format( + "input event => type: %d (%s), code: %d (%s), value: %s, time: %d.%d", + ev.type, linux_evdev_type_map[ev.type], ev.code, linux_evdev_abs_code_map[ev.code], ev.value, + ev.time.sec, ev.time.usec)) + elseif ev.type == C.EV_MSC then + logger.dbg(string.format( + "input event => type: %d (%s), code: %d (%s), value: %s, time: %d.%d", + ev.type, linux_evdev_type_map[ev.type], ev.code, linux_evdev_msc_code_map[ev.code], ev.value, + ev.time.sec, ev.time.usec)) + else + logger.dbg(string.format( + "input event => type: %d (%s), code: %d, value: %s, time: %d.%d", + ev.type, linux_evdev_type_map[ev.type], ev.code, ev.value, + ev.time.sec, ev.time.usec)) + end end self:eventAdjustHook(ev) - if ev.type == EV_KEY then + if ev.type == C.EV_KEY then return self:handleKeyBoardEv(ev) - elseif ev.type == EV_ABS and ev.code == ABS_OASIS_ORIENTATION then + elseif ev.type == C.EV_ABS and ev.code == ABS_OASIS_ORIENTATION then return self:handleOasisOrientationEv(ev) - elseif ev.type == EV_ABS or ev.type == EV_SYN then + elseif ev.type == C.EV_ABS or ev.type == C.EV_SYN then return self:handleTouchEv(ev) - elseif ev.type == EV_MSC then + elseif ev.type == C.EV_MSC then return self:handleMiscEv(ev) - elseif ev.type == EV_SDL then + elseif ev.type == C.EV_SDL then return self:handleSdlEv(ev) else - -- some other kind of event that we do not know yet + -- Received some other kind of event that we do not know how to specifically handle yet return Event:new("GenericInput", ev) end - elseif not ok and ev then + elseif ok == false and ev then return Event:new("InputError", ev) + elseif ok == nil then + -- No ok and no ev? Hu oh... + return Event:new("InputError", "Catastrophic") end end diff --git a/frontend/device/remarkable/device.lua b/frontend/device/remarkable/device.lua index 1e8068e69..83c4539aa 100644 --- a/frontend/device/remarkable/device.lua +++ b/frontend/device/remarkable/device.lua @@ -1,5 +1,4 @@ local Generic = require("device/generic/device") -- <= look at this file! -local TimeVal = require("ui/timeval") local logger = require("logger") local function yes() return true end @@ -58,7 +57,6 @@ local Remarkable1 = Remarkable:new{ function Remarkable1:adjustTouchEvent(ev, by) if ev.type == EV_ABS then - ev.time = TimeVal:now() -- Mirror X and Y and scale up both X & Y as touch input is different res from -- display if ev.code == ABS_MT_POSITION_X then @@ -81,7 +79,6 @@ local Remarkable2 = Remarkable:new{ } function Remarkable2:adjustTouchEvent(ev, by) - ev.time = TimeVal:now() if ev.type == EV_ABS then -- Mirror Y and scale up both X & Y as touch input is different res from -- display diff --git a/frontend/ui/gesturerange.lua b/frontend/ui/gesturerange.lua index 43d47345c..c39917297 100644 --- a/frontend/ui/gesturerange.lua +++ b/frontend/ui/gesturerange.lua @@ -3,9 +3,9 @@ local TimeVal = require("ui/timeval") local GestureRange = { -- gesture matching type ges = nil, - -- spatial range limits the gesture emitting position + -- spatial range, limits the gesture emitting position range = nil, - -- temproal range limits the gesture emitting rate + -- temporal range, limits the gesture emitting rate rate = nil, -- scale limits of this gesture scale = nil, @@ -23,12 +23,11 @@ function GestureRange:match(gs) return false end if self.range then - -- sometimes widget dimenension is not available when creating a gesturerage - -- for some action, now we accept a range function that will be later called - -- and the result of which will be used to check gesture match + -- Sometimes the widget's dimensions are not available when creating a GestureRange + -- for some action, so we accept a range function that will only be called at match() time instead. -- e.g. range = function() return self.dimen end - -- for inputcontainer given that the x and y field of `self.dimen` is only - -- filled when the inputcontainer is painted into blitbuffer + -- That's because most widgets' dimensions are only set at paintTo() time: + -- e.g., with InputContainer, the x and y fields of `self.dimen`. local range if type(self.range) == "function" then range = self.range() @@ -41,10 +40,9 @@ function GestureRange:match(gs) end if self.rate then - -- This filed restraints the upper limit rate(matches per second). - -- It's most useful for e-ink devices with less powerfull CPUs and - -- screens that cannot handle gesture events that otherwise will be - -- generated + -- This field sets up rate-limiting (in matches per second). + -- It's mostly useful for e-Ink devices with less powerful CPUs + -- and screens that cannot handle the amount of gesture events that would otherwise be generated. local last_time = self.last_time or TimeVal:new{} if gs.time - last_time > TimeVal:new{usec = 1000000 / self.rate} then self.last_time = gs.time diff --git a/frontend/ui/timeval.lua b/frontend/ui/timeval.lua index 4394f6dd0..a1220df2d 100644 --- a/frontend/ui/timeval.lua +++ b/frontend/ui/timeval.lua @@ -8,20 +8,47 @@ A simple module to module to compare and do arithmetic with time values. -- Do some stuff. -- You can add and subtract `TimeVal` objects. local tv_duration = TimeVal:now() - tv_start - -- If you need more precision (like 2.5 s), - -- you can add the milliseconds to the seconds. - local tv_duration_seconds_float = tv_duration.sec + tv_duration.usec/1000000 + -- And convert that object to various more human-readable formats, e.g., + print(string.format("Stuff took %.3fms", tv_duration:tomsecs())) ]] -local dbg = require("dbg") +local ffi = require("ffi") +require("ffi/posix_h") local util = require("ffi/util") +local C = ffi.C + +-- We prefer CLOCK_MONOTONIC_COARSE if it's available and has a decent resolution, +-- as we generally don't need nano/micro second precision, +-- and it can be more than twice as fast as CLOCK_MONOTONIC/CLOCK_REALTIME/gettimeofday... +local PREFERRED_MONOTONIC_CLOCKID = C.CLOCK_MONOTONIC +-- Ditto for REALTIME (for :realtime_coarse only, :realtime uses gettimeofday ;)). +local PREFERRED_REALTIME_CLOCKID = C.CLOCK_REALTIME +if ffi.os == "Linux" then + -- Unfortunately, it was only implemented in Linux 2.6.32, and we may run on older kernels than that... + -- So, just probe it to see if we can rely on it. + local probe_ts = ffi.new("struct timespec") + if C.clock_getres(C.CLOCK_MONOTONIC_COARSE, probe_ts) == 0 then + -- Now, it usually has a 1ms resolution on modern x86_64 systems, + -- but it only provides a 10ms resolution on all my armv7 devices :/. + if probe_ts.tv_sec == 0 and probe_ts.tv_nsec <= 1000000 then + PREFERRED_MONOTONIC_CLOCKID = C.CLOCK_MONOTONIC_COARSE + end + end + if C.clock_getres(C.CLOCK_REALTIME_COARSE, probe_ts) == 0 then + if probe_ts.tv_sec == 0 and probe_ts.tv_nsec <= 1000000 then + PREFERRED_REALTIME_CLOCKID = C.CLOCK_REALTIME_COARSE + end + end + probe_ts = nil --luacheck: ignore +end + --[[-- -TimeVal object. +TimeVal object. Maps to a POSIX struct timeval (). @table TimeVal @int sec floored number of seconds -@int usec remaining number of milliseconds +@int usec number of microseconds past that second. ]] local TimeVal = { sec = 0, @@ -47,7 +74,7 @@ function TimeVal:new(from_o) if o.usec == nil then o.usec = 0 elseif o.usec > 1000000 then - o.sec = o.sec + math.floor(o.usec/1000000) + o.sec = o.sec + math.floor(o.usec / 1000000) o.usec = o.usec % 1000000 end setmetatable(o, self) @@ -55,55 +82,39 @@ function TimeVal:new(from_o) return o end - +-- Based on function TimeVal:__lt(time_b) - if self.sec < time_b.sec then - return true - elseif self.sec > time_b.sec then - return false + if self.sec == time_b.sec then + return self.usec < time_b.usec else - -- self.sec == time_b.sec - if self.usec < time_b.usec then - return true - else - return false - end + return self.sec < time_b.sec end end function TimeVal:__le(time_b) - if self.sec < time_b.sec then - return true - elseif self.sec > time_b.sec then - return false + if self.sec == time_b.sec then + return self.usec <= time_b.usec else - -- self.sec == time_b.sec - if self.usec > time_b.usec then - return false - else - return true - end + return self.sec <= time_b.sec end end function TimeVal:__eq(time_b) - if self.sec == time_b.sec and self.usec == time_b.usec then - return true + if self.sec == time_b.sec then + return self.usec == time_b.usec else return false end end +-- If sec is negative, time went backwards! function TimeVal:__sub(time_b) local diff = TimeVal:new{} diff.sec = self.sec - time_b.sec diff.usec = self.usec - time_b.usec - if diff.sec < 0 and diff.usec > 0 then - diff.sec = diff.sec + 1 - diff.usec = diff.usec - 1000000 - elseif diff.sec > 0 and diff.usec < 0 then + if diff.usec < 0 then diff.sec = diff.sec - 1 diff.usec = diff.usec + 1000000 end @@ -111,48 +122,127 @@ function TimeVal:__sub(time_b) return diff end -dbg:guard(TimeVal, '__sub', - function(self, time_b) - assert(self.sec > time_b.sec or (self.sec == time_b.sec and self.usec >= time_b.usec), - "Subtract the first timeval from the latest, not vice versa.") - end) - function TimeVal:__add(time_b) local sum = TimeVal:new{} sum.sec = self.sec + time_b.sec sum.usec = self.usec + time_b.usec - if sum.usec > 1000000 then - sum.usec = sum.usec - 1000000 - sum.sec = sum.sec + 1 - end - if sum.sec < 0 and sum.usec > 0 then + if sum.usec >= 1000000 then sum.sec = sum.sec + 1 sum.usec = sum.usec - 1000000 - elseif sum.sec > 0 and sum.usec < 0 then - sum.sec = sum.sec - 1 - sum.usec = sum.usec + 1000000 end return sum end --[[-- -Creates a new TimeVal object based on the current time. +Creates a new TimeVal object based on the current wall clock time. +(e.g., gettimeofday / clock_gettime(CLOCK_REALTIME). + +This is a simple wrapper around util.gettime() to get all the niceties of a TimeVal object. +If you don't need sub-second precision, prefer os.time(). +Which means that, yes, this is a fancier POSIX Epoch ;). @usage local TimeVal = require("ui/timeval") - local tv_start = TimeVal:now() + local tv_start = TimeVal:realtime() -- Do some stuff. -- You can add and substract `TimeVal` objects. - local tv_duration = TimeVal:now() - tv_start + local tv_duration = TimeVal:realtime() - tv_start @treturn TimeVal ]] -function TimeVal:now() +function TimeVal:realtime() local sec, usec = util.gettime() return TimeVal:new{sec = sec, usec = usec} end +--[[-- +Creates a new TimeVal object based on the current value from the system's MONOTONIC clock source. +(e.g., clock_gettime(CLOCK_MONOTONIC).) + +POSIX guarantees that this clock source will *never* go backwards (but it *may* return the same value multiple times). +On Linux, this will not account for time spent with the device in suspend (unlike CLOCK_BOOTTIME). + +@treturn TimeVal +]] +function TimeVal:monotonic() + local timespec = ffi.new("struct timespec") + C.clock_gettime(C.CLOCK_MONOTONIC, timespec) + + -- TIMESPEC_TO_TIMEVAL + return TimeVal:new{sec = tonumber(timespec.tv_sec), usec = math.floor(tonumber(timespec.tv_nsec / 1000))} +end + +--- Ditto, but w/ CLOCK_MONOTONIC_COARSE if it's available and has a 1ms resolution or better (uses CLOCK_MONOTONIC otherwise). +function TimeVal:monotonic_coarse() + local timespec = ffi.new("struct timespec") + C.clock_gettime(PREFERRED_MONOTONIC_CLOCKID, timespec) + + -- TIMESPEC_TO_TIMEVAL + return TimeVal:new{sec = tonumber(timespec.tv_sec), usec = math.floor(tonumber(timespec.tv_nsec / 1000))} +end + +--- Ditto, but w/ CLOCK_REALTIME_COARSE if it's available and has a 1ms resolution or better (uses CLOCK_REALTIME otherwise). +function TimeVal:realtime_coarse() + local timespec = ffi.new("struct timespec") + C.clock_gettime(PREFERRED_REALTIME_CLOCKID, timespec) + + -- TIMESPEC_TO_TIMEVAL + return TimeVal:new{sec = tonumber(timespec.tv_sec), usec = math.floor(tonumber(timespec.tv_nsec / 1000))} +end + +--- Ditto, but w/ CLOCK_BOOTTIME (will return a TimeVal set to 0, 0 if the clock source is unsupported, as it's 2.6.39+) +function TimeVal:boottime() + local timespec = ffi.new("struct timespec") + C.clock_gettime(C.CLOCK_BOOTTIME, timespec) + + -- TIMESPEC_TO_TIMEVAL + return TimeVal:new{sec = tonumber(timespec.tv_sec), usec = math.floor(tonumber(timespec.tv_nsec / 1000))} +end + +--[[-- Alias for `monotonic_coarse`. + +The assumption being anything that requires accurate timestamps expects a monotonic clock source. +This is certainly true for KOReader's UI scheduling. +]] +TimeVal.now = TimeVal.monotonic_coarse + +--- Converts a TimeVal object to a Lua (decimal) number (sec.usecs) (accurate to the ms, rounded to 4 decimal places) +function TimeVal:tonumber() + -- Round to 4 decimal places + return math.floor((self.sec + self.usec / 1000000) * 10000) / 10000 +end + +--- Converts a TimeVal object to a Lua (int) number (resolution: 1µs) +function TimeVal:tousecs() + return math.floor(self.sec * 1000000 + self.usec + 0.5) +end + +--[[-- Converts a TimeVal object to a Lua (int) number (resolution: 1ms). + +(Mainly useful when computing a time lapse for benchmarking purposes). +]] +function TimeVal:tomsecs() + return self:tousecs() / 1000 +end + +--- Converts a Lua (decimal) number (sec.usecs) to a TimeVal object +function TimeVal:fromnumber(seconds) + local sec = math.floor(seconds) + local usec = math.floor((seconds - sec) * 1000000 + 0.5) + return TimeVal:new{sec = sec, usec = usec} +end + +--- Checks if a TimeVal object is positive +function TimeVal:isPositive() + return self.sec >= 0 +end + +--- Checks if a TimeVal object is zero +function TimeVal:isZero() + return self.sec == 0 and self.usec == 0 +end + return TimeVal diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index 5ffdc1588..9b4304ca9 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -5,6 +5,7 @@ This module manages widgets. local Device = require("device") local Event = require("ui/event") local Geom = require("ui/geometry") +local TimeVal = require("ui/timeval") local dbg = require("dbg") local logger = require("logger") local ffiUtil = require("ffi/util") @@ -13,7 +14,6 @@ local _ = require("gettext") local Input = Device.input local Screen = Device.screen -local MILLION = 1000000 local DEFAULT_FULL_REFRESH_COUNT = 6 -- there is only one instance of this @@ -29,6 +29,7 @@ local UIManager = { event_handlers = nil, _running = true, + _now = TimeVal:now(), _window_stack = {}, _task_queue = {}, _task_queue_dirty = false, @@ -512,13 +513,11 @@ end function UIManager:schedule(time, action, ...) local p, s, e = 1, 1, #self._task_queue if e ~= 0 then - local us = time[1] * MILLION + time[2] -- do a binary insert repeat p = math.floor(s + (e - s) / 2) - local ptime = self._task_queue[p].time - local ptus = ptime[1] * MILLION + ptime[2] - if us > ptus then + local p_time = self._task_queue[p].time + if time > p_time then if s == e then p = e + 1 break @@ -527,7 +526,7 @@ function UIManager:schedule(time, action, ...) else s = p end - elseif us < ptus then + elseif time < p_time then e = p if s == e then break @@ -549,29 +548,23 @@ function UIManager:schedule(time, action, ...) end dbg:guard(UIManager, 'schedule', function(self, time, action) - assert(time[1] >= 0 and time[2] >= 0, "Only positive time allowed") + assert(time.sec >= 0, "Only positive time allowed") assert(action ~= nil) end) --[[-- Schedules a task to be run a certain amount of seconds from now. -@number seconds scheduling delay in seconds (supports decimal values) +@number seconds scheduling delay in seconds (supports decimal values, 1ms resolution). @func action reference to the task to be scheduled (may be anonymous) @param ... optional arguments passed to action @see unschedule ]] function UIManager:scheduleIn(seconds, action, ...) - local when = { ffiUtil.gettime() } - local s = math.floor(seconds) - local usecs = (seconds - s) * MILLION - when[1] = when[1] + s - when[2] = when[2] + usecs - if when[2] >= MILLION then - when[1] = when[1] + 1 - when[2] = when[2] - MILLION - end + -- We might run significantly late inside an UI frame, so we can't use the cached value here. + -- It would also cause some bad interactions with the way nextTick & co behave. + local when = TimeVal:now() + TimeVal:fromnumber(seconds) self:schedule(when, action, ...) end dbg:guard(UIManager, 'scheduleIn', @@ -1049,7 +1042,7 @@ function UIManager:discardEvents(set_or_seconds) self._discard_events_till = nil return end - local usecs + local delay if set_or_seconds == true then -- Use an adequate delay to account for device refresh duration -- so any events happening in this delay (ie. before a widget @@ -1059,17 +1052,15 @@ function UIManager:discardEvents(set_or_seconds) -- sometimes > 500ms on some devices/temperatures. -- So, block for 400ms (to have it displayed) + 400ms -- for user reaction to it - usecs = 800000 + delay = TimeVal:new{ usec = 800000 } else -- On non-eInk screen, display is usually instantaneous - usecs = 400000 + delay = TimeVal:new{ usec = 400000 } end else -- we expect a number - usecs = set_or_seconds * MILLION + delay = TimeVal:new{ sec = set_or_seconds } end - local now = { ffiUtil.gettime() } - local now_us = now[1] * MILLION + now[2] - self._discard_events_till = now_us + usecs + self._discard_events_till = self._now + delay end --[[-- @@ -1082,9 +1073,7 @@ function UIManager:sendEvent(event) -- Ensure discardEvents if self._discard_events_till then - local now = { ffiUtil.gettime() } - local now_us = now[1] * MILLION + now[2] - if now_us < self._discard_events_till then + if TimeVal:now() < self._discard_events_till then return else self._discard_events_till = nil @@ -1159,8 +1148,7 @@ function UIManager:broadcastEvent(event) end function UIManager:_checkTasks() - local now = { ffiUtil.gettime() } - local now_us = now[1] * MILLION + now[2] + self._now = TimeVal:now() local wait_until = nil -- task.action may schedule other events @@ -1172,11 +1160,8 @@ function UIManager:_checkTasks() break end local task = self._task_queue[1] - local task_us = 0 - if task.time ~= nil then - task_us = task.time[1] * MILLION + task.time[2] - end - if task_us <= now_us then + local task_tv = task.time or TimeVal:new{} + if task_tv <= self._now then -- remove from table table.remove(self._task_queue, 1) -- task is pending to be executed right now. do it. @@ -1191,7 +1176,26 @@ function UIManager:_checkTasks() end end - return wait_until, now + return wait_until, self._now +end + +--[[-- +Returns a TimeVal object corresponding to the last UI tick. + +This is essentially a cached TimeVal:now(), computed at the top of every iteration of the main UI loop, +(right before checking/running scheduled tasks). +This is mainly useful to compute/schedule stuff in the same time scale as the UI loop (i.e., MONOTONIC), +without having to resort to a syscall. +It should never be significantly stale (i.e., it should be precise enough), +unless you're blocking the UI for a significant amount of time in the same UI tick. + +Prefer the appropriate TimeVal method for your needs if you require perfect accuracy +(e.g., when you're actually working on the event loop *itself* (UIManager, Input, GestureDetector)). + +This is *NOT* wall clock time (REALTIME). +]] +function UIManager:getTime() + return self._now end -- precedence of refresh modes: @@ -1580,29 +1584,35 @@ function UIManager:handleInput() self:processZMQs() -- Figure out how long to wait. + -- Ultimately, that'll be the earliest of INPUT_TIMEOUT, ZMQ_TIMEOUT or the next earliest scheduled task. + local deadline -- Default to INPUT_TIMEOUT (which may be nil, i.e. block until an event happens). local wait_us = self.INPUT_TIMEOUT - -- If there's a timed event pending, that puts an upper bound on how long to wait. - if wait_until then - wait_us = math.min( - wait_us or math.huge, - (wait_until[1] - now[1]) * MILLION - + (wait_until[2] - now[2])) - end - -- If we have any ZMQs registered, ZMQ_TIMEOUT is another upper bound. if #self._zeromqs > 0 then wait_us = math.min(wait_us or math.huge, self.ZMQ_TIMEOUT) end + -- We pass that on as an absolute deadline, not a relative wait time. + if wait_us then + deadline = now + TimeVal:new{ usec = wait_us } + end + + -- If there's a scheduled task pending, that puts an upper bound on how long to wait. + if wait_until and (not deadline or wait_until < deadline) then + -- ^ We don't have a TIMEOUT induced deadline, making the choice easy. + -- ^ We have a task scheduled for *before* our TIMEOUT induced deadline. + deadline = wait_until + end + -- If allowed, entering standby (from which we can wake by input) must trigger in response to event -- this function emits (plugin), or within waitEvent() right after (hardware). -- Anywhere else breaks preventStandby/allowStandby invariants used by background jobs while UI is left running. self:_standbyTransition() -- wait for next event - local input_event = Input:waitEvent(wait_us) + local input_event = Input:waitEvent(now, deadline) -- delegate input_event to handler if input_event then @@ -1672,6 +1682,9 @@ end function UIManager:_beforeSuspend() self:flushSettings() self:broadcastEvent(Event:new("Suspend")) + + -- Reset gesture detection state to a blank slate (anything power-management related emits KEY events, which don't need gesture detection). + Input:resetState() end -- The common operations that should be performed after resuming the device. @@ -1772,5 +1785,11 @@ function UIManager:restartKOReader() self._exit_code = 85 end +--- Sanely abort KOReader (e.g., exit sanely, but with a non-zero return code). +function UIManager:abort() + self:quit() + self._exit_code = 1 +end + UIManager:init() return UIManager diff --git a/frontend/ui/widget/dictquicklookup.lua b/frontend/ui/widget/dictquicklookup.lua index 116c365fa..505651b98 100644 --- a/frontend/ui/widget/dictquicklookup.lua +++ b/frontend/ui/widget/dictquicklookup.lua @@ -20,6 +20,7 @@ local ScrollHtmlWidget = require("ui/widget/scrollhtmlwidget") local ScrollTextWidget = require("ui/widget/scrolltextwidget") local Size = require("ui/size") local TextWidget = require("ui/widget/textwidget") +local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") local VerticalGroup = require("ui/widget/verticalgroup") local VerticalSpan = require("ui/widget/verticalspan") @@ -153,7 +154,7 @@ function DictQuickLookup:init() -- callback function when HoldReleaseText is handled as args args = function(text, hold_duration) local lookup_target - if hold_duration < 3.0 then + if hold_duration < TimeVal:new{ sec = 3 } then -- do this lookup in the same domain (dict/wikipedia) lookup_target = self.is_wiki and "LookupWikipedia" or "LookupWord" else diff --git a/frontend/ui/widget/footnotewidget.lua b/frontend/ui/widget/footnotewidget.lua index 2fc35ce98..7f26eb78a 100644 --- a/frontend/ui/widget/footnotewidget.lua +++ b/frontend/ui/widget/footnotewidget.lua @@ -13,6 +13,7 @@ local InputContainer = require("ui/widget/container/inputcontainer") local LineWidget = require("ui/widget/linewidget") local ScrollHtmlWidget = require("ui/widget/scrollhtmlwidget") local Size = require("ui/size") +local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") local VerticalGroup = require("ui/widget/verticalgroup") local VerticalSpan = require("ui/widget/verticalspan") @@ -194,7 +195,7 @@ function FootnoteWidget:init() -- callback function when HoldReleaseText is handled as args args = function(text, hold_duration) if self.dialog then - local lookup_target = hold_duration < 3.0 and "LookupWord" or "LookupWikipedia" + local lookup_target = hold_duration < TimeVal:new{ sec = 3 } and "LookupWord" or "LookupWikipedia" self.dialog:handleEvent( Event:new(lookup_target, text) ) diff --git a/frontend/ui/widget/htmlboxwidget.lua b/frontend/ui/widget/htmlboxwidget.lua index 448533cf1..9eadfd25c 100644 --- a/frontend/ui/widget/htmlboxwidget.lua +++ b/frontend/ui/widget/htmlboxwidget.lua @@ -9,7 +9,7 @@ local GestureRange = require("ui/gesturerange") local InputContainer = require("ui/widget/container/inputcontainer") local Mupdf = require("ffi/mupdf") local Screen = Device.screen -local TimeVal = require("ui/timeval") +local UIManager = require("ui/uimanager") local logger = require("logger") local util = require("util") @@ -165,7 +165,7 @@ function HtmlBoxWidget:onHoldStartText(_, ges) return false -- let event be processed by other widgets end - self.hold_start_tv = TimeVal.now() + self.hold_start_tv = UIManager:getTime() return true end @@ -229,8 +229,7 @@ function HtmlBoxWidget:onHoldReleaseText(callback, ges) return false end - local hold_duration = TimeVal.now() - self.hold_start_tv - hold_duration = hold_duration.sec + (hold_duration.usec/1000000) + local hold_duration = UIManager:getTime() - self.hold_start_tv local page = self.document:openPage(self.page_number) local lines = page:getPageText() diff --git a/frontend/ui/widget/notification.lua b/frontend/ui/widget/notification.lua index caf09ac3a..d0bfec93e 100644 --- a/frontend/ui/widget/notification.lua +++ b/frontend/ui/widget/notification.lua @@ -13,6 +13,7 @@ local InputContainer = require("ui/widget/container/inputcontainer") local RectSpan = require("ui/widget/rectspan") local Size = require("ui/size") local TextWidget = require("ui/widget/textwidget") +local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") local VerticalGroup = require("ui/widget/verticalgroup") local Input = Device.input @@ -73,7 +74,7 @@ function Notification:init() local notif_height = self.frame:getSize().h self:_cleanShownStack() - table.insert(Notification._nums_shown, os.time()) + table.insert(Notification._nums_shown, UIManager:getTime()) self.num = #Notification._nums_shown self[1] = VerticalGroup:new{ @@ -101,9 +102,9 @@ function Notification:_cleanShownStack(num) -- to follow what is happening). -- As a sanity check, we also forget those shown for -- more than 30s in case no close event was received. - local expire_ts = os.time() - 30 + local expire_tv = UIManager:getTime() - TimeVal:new{ sec = 30 } for i=#Notification._nums_shown, 1, -1 do - if Notification._nums_shown[i] and Notification._nums_shown[i] > expire_ts then + if Notification._nums_shown[i] and Notification._nums_shown[i] > expire_tv then break -- still shown (or not yet expired) end table.remove(Notification._nums_shown, i) diff --git a/frontend/ui/widget/textboxwidget.lua b/frontend/ui/widget/textboxwidget.lua index e3522d66f..4dff989e3 100644 --- a/frontend/ui/widget/textboxwidget.lua +++ b/frontend/ui/widget/textboxwidget.lua @@ -24,7 +24,6 @@ local RenderText = require("ui/rendertext") local RightContainer = require("ui/widget/container/rightcontainer") local Size = require("ui/size") local TextWidget = require("ui/widget/textwidget") -local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") local Math = require("optmath") local logger = require("logger") @@ -1790,7 +1789,7 @@ function TextBoxWidget:onHoldStartText(_, ges) return false -- let event be processed by other widgets end - self.hold_start_tv = TimeVal.now() + self.hold_start_tv = UIManager:getTime() return true end @@ -1822,8 +1821,7 @@ function TextBoxWidget:onHoldReleaseText(callback, ges) return false end - local hold_duration = TimeVal.now() - self.hold_start_tv - hold_duration = hold_duration.sec + hold_duration.usec/1000000 + local hold_duration = UIManager:getTime() - self.hold_start_tv -- If page contains an image, check if Hold is on this image and deal -- with it directly @@ -1917,7 +1915,7 @@ function TextBoxWidget:onHoldReleaseText(callback, ges) -- to consider when looking for word boundaries) local selected_text = self._xtext:getSelectedWords(sel_start_idx, sel_end_idx, 50) - logger.dbg("onHoldReleaseText (duration:", hold_duration, ") :", + logger.dbg("onHoldReleaseText (duration:", hold_duration:tonumber(), ") :", sel_start_idx, ">", sel_end_idx, "=", selected_text) callback(selected_text, hold_duration) return true @@ -1935,7 +1933,7 @@ function TextBoxWidget:onHoldReleaseText(callback, ges) end local selected_text = table.concat(self.charlist, "", sel_start_idx, sel_end_idx) - logger.dbg("onHoldReleaseText (duration:", hold_duration, ") :", sel_start_idx, ">", sel_end_idx, "=", selected_text) + logger.dbg("onHoldReleaseText (duration:", hold_duration:tonumber(), ") :", sel_start_idx, ">", sel_end_idx, "=", selected_text) callback(selected_text, hold_duration) return true end diff --git a/frontend/ui/widget/virtualkeyboard.lua b/frontend/ui/widget/virtualkeyboard.lua index 8b0bc09c5..4129be48d 100644 --- a/frontend/ui/widget/virtualkeyboard.lua +++ b/frontend/ui/widget/virtualkeyboard.lua @@ -16,6 +16,7 @@ local InputContainer = require("ui/widget/container/inputcontainer") local KeyboardLayoutDialog = require("ui/widget/keyboardlayoutdialog") local Size = require("ui/size") local TextWidget = require("ui/widget/textwidget") +local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") local VerticalGroup = require("ui/widget/verticalgroup") local VerticalSpan = require("ui/widget/verticalspan") @@ -588,6 +589,7 @@ function VirtualKeyPopup:init() }, } self.tap_interval_override = G_reader_settings:readSetting("ges_tap_interval_on_keyboard") or 0 + self.tap_interval_override = TimeVal:new{ usec = self.tap_interval_override } if Device:hasDPad() then self.key_events.PressKey = { {"Press"}, doc = "select key" } @@ -699,6 +701,7 @@ function VirtualKeyboard:init() self.max_layer = keyboard.max_layer self:initLayer(self.keyboard_layer) self.tap_interval_override = G_reader_settings:readSetting("ges_tap_interval_on_keyboard") or 0 + self.tap_interval_override = TimeVal:new{ usec = self.tap_interval_override } if Device:hasDPad() then self.key_events.PressKey = { {"Press"}, doc = "select key" } end diff --git a/plugins/autostandby.koplugin/main.lua b/plugins/autostandby.koplugin/main.lua index 2ecfbe3e7..0d2e50a8a 100644 --- a/plugins/autostandby.koplugin/main.lua +++ b/plugins/autostandby.koplugin/main.lua @@ -63,7 +63,7 @@ function AutoStandby:addToMainMenu(menu_items) } end --- We've received touch/key event, so delay stadby accordingly +-- We've received touch/key event, so delay standby accordingly function AutoStandby:onInputEvent() logger.dbg("AutoStandby:onInputevent() instance=", tostring(self)) local config = self.settings.data diff --git a/plugins/autosuspend.koplugin/main.lua b/plugins/autosuspend.koplugin/main.lua index b68397979..be1dd4ba1 100644 --- a/plugins/autosuspend.koplugin/main.lua +++ b/plugins/autosuspend.koplugin/main.lua @@ -10,6 +10,7 @@ if not Device:isCervantes() and end local PluginShare = require("pluginshare") +local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") local WidgetContainer = require("ui/widget/container/widgetcontainer") local logger = require("logger") @@ -24,7 +25,7 @@ local AutoSuspend = WidgetContainer:new{ is_doc_only = false, autoshutdown_timeout_seconds = G_reader_settings:readSetting("autoshutdown_timeout_seconds") or default_autoshutdown_timeout_seconds, auto_suspend_timeout_seconds = G_reader_settings:readSetting("auto_suspend_timeout_seconds") or default_auto_suspend_timeout_seconds, - last_action_sec = os.time(), + last_action_tv = TimeVal:now(), standby_prevented = false, } @@ -48,9 +49,11 @@ function AutoSuspend:_schedule(shutdown_only) delay_suspend = self.auto_suspend_timeout_seconds delay_shutdown = self.autoshutdown_timeout_seconds else - local now_ts = os.time() - delay_suspend = self.last_action_sec + self.auto_suspend_timeout_seconds - now_ts - delay_shutdown = self.last_action_sec + self.autoshutdown_timeout_seconds - now_ts + local now_tv = UIManager:getTime() + delay_suspend = self.last_action_tv + TimeVal:new{ sec = self.auto_suspend_timeout_seconds } - now_tv + delay_suspend = delay_suspend:tonumber() + delay_shutdown = self.last_action_tv + TimeVal:new{ sec = self.autoshutdown_timeout_seconds } - now_tv + delay_shutdown = delay_shutdown:tonumber() end -- Try to shutdown first, as we may have been woken up from suspend just for the sole purpose of doing that. @@ -79,9 +82,9 @@ end function AutoSuspend:_start() if self:_enabled() or self:_enabledShutdown() then - local now_ts = os.time() - logger.dbg("AutoSuspend: start at", now_ts) - self.last_action_sec = now_ts + local now_tv = UIManager:getTime() + logger.dbg("AutoSuspend: start at", now_tv:tonumber()) + self.last_action_tv = now_tv self:_schedule() end end @@ -89,9 +92,9 @@ end -- Variant that only re-engages the shutdown timer for onUnexpectedWakeupLimit function AutoSuspend:_restart() if self:_enabledShutdown() then - local now_ts = os.time() - logger.dbg("AutoSuspend: restart at", now_ts) - self.last_action_sec = now_ts + local now_tv = UIManager:getTime() + logger.dbg("AutoSuspend: restart at", now_tv:tonumber()) + self.last_action_tv = now_tv self:_schedule(true) end end @@ -108,7 +111,7 @@ end function AutoSuspend:onInputEvent() logger.dbg("AutoSuspend: onInputEvent") - self.last_action_sec = os.time() + self.last_action_tv = UIManager:getTime() end function AutoSuspend:onSuspend() diff --git a/plugins/autoturn.koplugin/main.lua b/plugins/autoturn.koplugin/main.lua index bfe22362f..ccf1fd679 100644 --- a/plugins/autoturn.koplugin/main.lua +++ b/plugins/autoturn.koplugin/main.lua @@ -1,6 +1,7 @@ local Device = require("device") local Event = require("ui/event") local PluginShare = require("pluginshare") +local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") local WidgetContainer = require("ui/widget/container/widgetcontainer") local logger = require("logger") @@ -10,11 +11,11 @@ local T = require("ffi/util").template local AutoTurn = WidgetContainer:new{ name = "autoturn", is_doc_only = true, - autoturn_sec = G_reader_settings:readSetting("autoturn_timeout_seconds") or 0, - autoturn_distance = G_reader_settings:readSetting("autoturn_distance") or 1, - enabled = G_reader_settings:isTrue("autoturn_enabled"), + autoturn_sec = 0, + autoturn_distance = 1, + enabled = false, settings_id = 0, - last_action_sec = os.time(), + last_action_tv = TimeVal:now(), } function AutoTurn:_enabled() @@ -34,7 +35,8 @@ function AutoTurn:_schedule(settings_id) return end - local delay = self.last_action_sec + self.autoturn_sec - os.time() + local delay = self.last_action_tv + TimeVal:new{ sec = self.autoturn_sec } - UIManager:getTime() + delay = delay:tonumber() if delay <= 0 then if UIManager:getTopWidget() == "ReaderUI" then @@ -57,10 +59,10 @@ end function AutoTurn:_start() if self:_enabled() then - local now_ts = os.time() - logger.dbg("AutoTurn: start at", now_ts) + local now_tv = UIManager:getTime() + logger.dbg("AutoTurn: start at", now_tv:tonumber()) PluginShare.pause_auto_suspend = true - self.last_action_sec = now_ts + self.last_action_tv = now_tv self:_schedule(self.settings_id) local text @@ -83,7 +85,10 @@ end function AutoTurn:init() UIManager.event_hook:registerWidget("InputEvent", self) - self.autoturn_sec = self.settings + self.autoturn_sec = G_reader_settings:readSetting("autoturn_timeout_seconds") or 0 + self.autoturn_distance = G_reader_settings:readSetting("autoturn_distance") or 1 + self.enabled = G_reader_settings:isTrue("autoturn_enabled") + self.settings_id = 0 self.ui.menu:registerToMainMenu(self) self:_deprecateLastTask() self:_start() @@ -96,7 +101,7 @@ end function AutoTurn:onInputEvent() logger.dbg("AutoTurn: onInputEvent") - self.last_action_sec = os.time() + self.last_action_tv = UIManager:getTime() end -- We do not want autoturn to turn pages during the suspend process. diff --git a/plugins/backgroundrunner.koplugin/commandrunner.lua b/plugins/backgroundrunner.koplugin/commandrunner.lua index 56a2a134d..f90b9fd8a 100644 --- a/plugins/backgroundrunner.koplugin/commandrunner.lua +++ b/plugins/backgroundrunner.koplugin/commandrunner.lua @@ -1,4 +1,5 @@ local logger = require("logger") +local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") local CommandRunner = { @@ -36,7 +37,7 @@ function CommandRunner:start(job) assert(self.pio == nil) assert(self.job == nil) self.job = job - self.job.start_sec = os.time() + self.job.start_tv = UIManager:getTime() assert(type(self.job.executable) == "string") local command = self:createEnvironment() .. " " .. "sh plugins/backgroundrunner.koplugin/luawrapper.sh " .. @@ -76,7 +77,7 @@ function CommandRunner:poll() UIManager:allowStandby() self.pio:close() self.pio = nil - self.job.end_sec = os.time() + self.job.end_tv = TimeVal:now() local job = self.job self.job = nil return job diff --git a/plugins/backgroundrunner.koplugin/main.lua b/plugins/backgroundrunner.koplugin/main.lua index b462a9317..a9fa3de08 100644 --- a/plugins/backgroundrunner.koplugin/main.lua +++ b/plugins/backgroundrunner.koplugin/main.lua @@ -9,13 +9,15 @@ end local CommandRunner = require("commandrunner") local PluginShare = require("pluginshare") +local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") local WidgetContainer = require("ui/widget/container/widgetcontainer") local logger = require("logger") local _ = require("gettext") -- BackgroundRunner is an experimental feature to execute non-critical jobs in --- background. A job is defined as a table in PluginShare.backgroundJobs table. +-- the background. +-- A job is defined as a table in PluginShare.backgroundJobs table. -- It contains at least following items: -- when: number, string or function -- number: the delay in seconds @@ -26,9 +28,9 @@ local _ = require("gettext") -- executed immediately. -- -- repeated: boolean or function or nil or number --- boolean: true to repeated the job once it finished. --- function: if the return value of the function is true, repeated the job --- once it finished. If the function throws an error, it equals to +-- boolean: true to repeat the job once it finished. +-- function: if the return value of the function is true, repeat the job +-- once it finishes. If the function throws an error, it equals to -- return false. -- nil: same as false. -- number: times to repeat. @@ -70,9 +72,10 @@ local _ = require("gettext") -- bad_command: boolean, whether the command is not found. Not available for -- function executable. -- blocked: boolean, whether the job is blocked. --- start_sec: number, the os.time() when the job was started. --- end_sec: number, the os.time() when the job was stopped. --- insert_sec: number, the os.time() when the job was inserted into queue. +-- start_tv: number, the TimeVal when the job was started. +-- end_tv: number, the TimeVal when the job was stopped. +-- insert_tv: number, the TimeVal when the job was inserted into queue. +-- (All of them in the monotonic time scale, like the main event loop & task queue). local BackgroundRunner = { jobs = PluginShare.backgroundJobs, @@ -114,7 +117,9 @@ end function BackgroundRunner:_finishJob(job) assert(self ~= nil) if type(job.executable) == "function" then - job.timeout = ((job.end_sec - job.start_sec) > 1) + local tv_diff = job.end_tv - job.start_tv + local threshold = TimeVal:new{ sec = 1 } + job.timeout = (tv_diff > threshold) end job.blocked = job.timeout if not job.blocked and self:_shouldRepeat(job) then @@ -136,7 +141,7 @@ function BackgroundRunner:_executeJob(job) CommandRunner:start(job) return true elseif type(job.executable) == "function" then - job.start_sec = os.time() + job.start_tv = UIManager:getTime() local status, err = pcall(job.executable) if status then job.result = 0 @@ -144,7 +149,7 @@ function BackgroundRunner:_executeJob(job) job.result = 1 job.exception = err end - job.end_sec = os.time() + job.end_tv = TimeVal:now() self:_finishJob(job) return true else @@ -171,10 +176,10 @@ function BackgroundRunner:_execute() local round = 0 while #self.jobs > 0 do local job = table.remove(self.jobs, 1) - if job.insert_sec == nil then - -- Jobs are first inserted to jobs table from external users. So - -- they may not have insert_sec field. - job.insert_sec = os.time() + if job.insert_tv == nil then + -- Jobs are first inserted to jobs table from external users. + -- So they may not have an insert field. + job.insert_tv = UIManager:getTime() end local should_execute = false local should_ignore = false @@ -187,7 +192,7 @@ function BackgroundRunner:_execute() end elseif type(job.when) == "number" then if job.when >= 0 then - should_execute = ((os.time() - job.insert_sec) >= job.when) + should_execute = ((UIManager:getTime() - job.insert_tv) >= TimeVal:fromnumber(job.when)) else should_ignore = true end @@ -248,7 +253,7 @@ end function BackgroundRunner:_insert(job) assert(self ~= nil) - job.insert_sec = os.time() + job.insert_tv = UIManager:getTime() table.insert(self.jobs, job) end diff --git a/plugins/calibre.koplugin/metadata.lua b/plugins/calibre.koplugin/metadata.lua index f499ca48d..7ffc89fa5 100644 --- a/plugins/calibre.koplugin/metadata.lua +++ b/plugins/calibre.koplugin/metadata.lua @@ -8,6 +8,7 @@ of storing it. @module koplugin.calibre.metadata --]]-- +local TimeVal = require("ui/timeval") local lfs = require("libs/libkoreader-lfs") local rapidjson = require("rapidjson") local logger = require("logger") @@ -232,14 +233,13 @@ end -- in a given path. It will find calibre files if they're on disk and -- try to load info from them. --- NOTE: you should care about the books table, because it could be huge. +-- NOTE: Take special notice of the books table, because it could be huge. -- If you're not working with the metadata directly (ie: in wireless connections) -- you should copy relevant data to another table and free this one to keep things tidy. function CalibreMetadata:init(dir, is_search) if not dir then return end - local socket = require("socket") - local start = socket.gettime() + local start = TimeVal:now() self.path = dir local ok_meta, ok_drive, file_meta, file_drive = findCalibreFiles(dir) self.driveinfo = file_drive @@ -256,13 +256,13 @@ function CalibreMetadata:init(dir, is_search) local msg if is_search then - msg = string.format("(search) in %f milliseconds: %d books", - (socket.gettime() - start) * 1000, #self.books) + msg = string.format("(search) in %.3f milliseconds: %d books", + (TimeVal:now() - start):tomsecs(), #self.books) else local deleted_count = self:prune() self:cleanUnused() - msg = string.format("in %f milliseconds: %d books. %d pruned", - (socket.gettime() - start) * 1000, #self.books, deleted_count) + msg = string.format("in %.3f milliseconds: %d books. %d pruned", + (TimeVal:now() - start):tomsecs(), #self.books, deleted_count) end logger.info(string.format("calibre info loaded from disk %s", msg)) return true diff --git a/plugins/calibre.koplugin/search.lua b/plugins/calibre.koplugin/search.lua index 6cda85c93..7eac14c07 100644 --- a/plugins/calibre.koplugin/search.lua +++ b/plugins/calibre.koplugin/search.lua @@ -16,9 +16,9 @@ local Menu = require("ui/widget/menu") local Persist = require("persist") local Screen = require("device").screen local Size = require("ui/size") +local TimeVal = require("ui/timeval") local UIManager = require("ui/uimanager") local logger = require("logger") -local socket = require("socket") local util = require("util") local _ = require("gettext") local T = require("ffi/util").template @@ -323,7 +323,7 @@ function CalibreSearch:find(option) end -- measure time elapsed searching - local start = socket.gettime() + local start = TimeVal:now() if option == "find" then local books = self:findBooks(self.search_value) local result = self:bookCatalog(books) @@ -331,9 +331,8 @@ function CalibreSearch:find(option) else self:browse(option,1) end - local elapsed = socket.gettime() - start - logger.info(string.format("search done in %f milliseconds (%s, %s, %s, %s, %s)", - elapsed * 1000, + logger.info(string.format("search done in %.3f milliseconds (%s, %s, %s, %s, %s)", + (TimeVal:now() - start):tomsecs(), option == "find" and "books" or option, "case sensitive: " .. tostring(not self.case_insensitive), "title: " .. tostring(self.find_by_title), @@ -556,8 +555,8 @@ end -- get metadata from cache or calibre files function CalibreSearch:getMetadata() - local start = socket.gettime() - local template = "metadata: %d books imported from %s in %f milliseconds" + local start = TimeVal:now() + local template = "metadata: %d books imported from %s in %.3f milliseconds" -- try to load metadata from cache if self.cache_metadata then @@ -581,8 +580,7 @@ function CalibreSearch:getMetadata() end end if is_newer then - local elapsed = socket.gettime() - start - logger.info(string.format(template, #cache, "cache", elapsed * 1000)) + logger.info(string.format(template, #cache, "cache", (TimeVal:now() - start):tomsecs())) return cache else logger.warn("cache is older than metadata, ignoring it") @@ -607,8 +605,7 @@ function CalibreSearch:getMetadata() end self.cache_books:save(serialized_table) end - local elapsed = socket.gettime() - start - logger.info(string.format(template, #books, "calibre", elapsed * 1000)) + logger.info(string.format(template, #books, "calibre", (TimeVal:now() - start):tomsecs())) return books end diff --git a/plugins/coverbrowser.koplugin/bookinfomanager.lua b/plugins/coverbrowser.koplugin/bookinfomanager.lua index 4a0c732ae..8e9dc839c 100644 --- a/plugins/coverbrowser.koplugin/bookinfomanager.lua +++ b/plugins/coverbrowser.koplugin/bookinfomanager.lua @@ -691,7 +691,6 @@ function BookInfoManager:extractInBackground(files) local cover_specs = files[idx].cover_specs logger.dbg(" BG extracting:", filepath) self:extractBookInfo(filepath, cover_specs) - FFIUtil.usleep(100000) -- give main process 100ms of free cpu to do its processing end logger.dbg(" BG extraction done") end diff --git a/reader.lua b/reader.lua index 484d383ce..35e56427c 100755 --- a/reader.lua +++ b/reader.lua @@ -46,8 +46,6 @@ if lang_locale then _.changeLang(lang_locale) end -local dummy = require("ffi/posix_h") - -- Try to turn the C blitter on/off, and synchronize setting so that UI config reflects real state local bb = require("ffi/blitbuffer") bb:setUseCBB(is_cbb_enabled) diff --git a/setupkoenv.lua b/setupkoenv.lua index 0fc14c385..7b370e578 100644 --- a/setupkoenv.lua +++ b/setupkoenv.lua @@ -8,7 +8,7 @@ package.cpath = -- set search path for 'ffi.load()' local ffi = require("ffi") -local dummy = require("ffi/posix_h") +require("ffi/posix_h") local C = ffi.C if ffi.os == "Windows" then C._putenv("PATH=libs;common;") diff --git a/spec/unit/autosuspend_spec.lua b/spec/unit/autosuspend_spec.lua index 1c1794294..a0492d3cc 100644 --- a/spec/unit/autosuspend_spec.lua +++ b/spec/unit/autosuspend_spec.lua @@ -16,6 +16,8 @@ describe("AutoSuspend", function() UIManager._run_forever = true G_reader_settings:saveSetting("auto_suspend_timeout_seconds", 10) require("mock_time"):install() + -- Reset UIManager:getTime() + UIManager:handleInput() end) after_each(function() @@ -36,7 +38,6 @@ describe("AutoSuspend", function() mock_time:increase(6) UIManager:handleInput() assert.stub(UIManager.suspend).was.called(1) - mock_time:uninstall() end) it("should be able to deprecate last task", function() @@ -56,7 +57,6 @@ describe("AutoSuspend", function() mock_time:increase(5) UIManager:handleInput() assert.stub(UIManager.suspend).was.called(1) - mock_time:uninstall() end) end) @@ -74,6 +74,8 @@ describe("AutoSuspend", function() UIManager._run_forever = true G_reader_settings:saveSetting("autoshutdown_timeout_seconds", 10) require("mock_time"):install() + -- Reset UIManager:getTime() + UIManager:handleInput() end) after_each(function() @@ -94,7 +96,6 @@ describe("AutoSuspend", function() mock_time:increase(6) UIManager:handleInput() assert.stub(UIManager.poweroff_action).was.called(1) - mock_time:uninstall() end) it("should be able to deprecate last task", function() @@ -114,7 +115,6 @@ describe("AutoSuspend", function() mock_time:increase(5) UIManager:handleInput() assert.stub(UIManager.poweroff_action).was.called(1) - mock_time:uninstall() end) end) end) diff --git a/spec/unit/background_runner_spec.lua b/spec/unit/background_runner_spec.lua index 6659b88fc..a9c08f4f5 100644 --- a/spec/unit/background_runner_spec.lua +++ b/spec/unit/background_runner_spec.lua @@ -132,7 +132,7 @@ describe("BackgroundRunner widget tests", function() table.insert(PluginShare.backgroundJobs, job) notifyBackgroundJobsUpdated() - while job.end_sec == nil do + while job.end_tv == nil do MockTime:increase(2) UIManager:handleInput() end @@ -157,7 +157,7 @@ describe("BackgroundRunner widget tests", function() table.insert(PluginShare.backgroundJobs, job) notifyBackgroundJobsUpdated() - while job.end_sec == nil do + while job.end_tv == nil do MockTime:increase(2) UIManager:handleInput() end @@ -171,11 +171,11 @@ describe("BackgroundRunner widget tests", function() ENV1 = "yes", ENV2 = "no", } - job.end_sec = nil + job.end_tv = nil table.insert(PluginShare.backgroundJobs, job) notifyBackgroundJobsUpdated() - while job.end_sec == nil do + while job.end_tv == nil do MockTime:increase(2) UIManager:handleInput() end @@ -206,7 +206,7 @@ describe("BackgroundRunner widget tests", function() table.insert(PluginShare.backgroundJobs, job) notifyBackgroundJobsUpdated() - while job.end_sec == nil do + while job.end_tv == nil do MockTime:increase(2) UIManager:handleInput() end @@ -216,12 +216,12 @@ describe("BackgroundRunner widget tests", function() assert.is_false(job.timeout) assert.is_false(job.bad_command) - job.end_sec = nil + job.end_tv = nil env2 = "no" table.insert(PluginShare.backgroundJobs, job) notifyBackgroundJobsUpdated() - while job.end_sec == nil do + while job.end_tv == nil do MockTime:increase(2) UIManager:handleInput() end @@ -244,7 +244,7 @@ describe("BackgroundRunner widget tests", function() table.insert(PluginShare.backgroundJobs, job) notifyBackgroundJobsUpdated() - while job.end_sec == nil do + while job.end_tv == nil do MockTime:increase(2) UIManager:handleInput() end diff --git a/spec/unit/device_spec.lua b/spec/unit/device_spec.lua index 3d2582b8d..4e7562bf4 100644 --- a/spec/unit/device_spec.lua +++ b/spec/unit/device_spec.lua @@ -94,13 +94,13 @@ describe("device module", function() type = EV_ABS, code = ABS_X, value = y, - time = TimeVal:now(), + time = TimeVal:realtime(), } local ev_y = { type = EV_ABS, code = ABS_Y, value = Screen:getWidth()-x, - time = TimeVal:now(), + time = TimeVal:realtime(), } kobo_dev.input:eventAdjustHook(ev_x) @@ -273,7 +273,7 @@ describe("device module", function() mock_ffi_input = require('ffi/input') stub(mock_ffi_input, "waitForEvent") - mock_ffi_input.waitForEvent.returns({ + mock_ffi_input.waitForEvent.returns(true, { type = 3, time = { usec = 450565, diff --git a/spec/unit/mock_time.lua b/spec/unit/mock_time.lua index a326f3a80..666b63f34 100644 --- a/spec/unit/mock_time.lua +++ b/spec/unit/mock_time.lua @@ -1,36 +1,181 @@ require("commonrequire") +local TimeVal = require("ui/timeval") +local ffi = require("ffi") +local dummy = require("ffi/posix_h") local logger = require("logger") +local util = require("ffi/util") + +local C = ffi.C local MockTime = { original_os_time = os.time, original_util_time = nil, - value = os.time(), + original_tv_realtime = nil, + original_tv_realtime_coarse = nil, + original_tv_monotonic = nil, + original_tv_monotonic_coarse = nil, + original_tv_boottime = nil, + original_tv_now = nil, + monotonic = 0, + realtime = 0, + boottime = 0, } function MockTime:install() assert(self ~= nil) - local util = require("ffi/util") if self.original_util_time == nil then self.original_util_time = util.gettime assert(self.original_util_time ~= nil) end + if self.original_tv_realtime == nil then + self.original_tv_realtime = TimeVal.realtime + assert(self.original_tv_realtime ~= nil) + end + if self.original_tv_realtime_coarse == nil then + self.original_tv_realtime_coarse = TimeVal.realtime_coarse + assert(self.original_tv_realtime_coarse ~= nil) + end + if self.original_tv_monotonic == nil then + self.original_tv_monotonic = TimeVal.monotonic + assert(self.original_tv_monotonic ~= nil) + end + if self.original_tv_monotonic_coarse == nil then + self.original_tv_monotonic_coarse = TimeVal.monotonic_coarse + assert(self.original_tv_monotonic_coarse ~= nil) + end + if self.original_tv_boottime == nil then + self.original_tv_boottime = TimeVal.boottime + assert(self.original_tv_boottime ~= nil) + end + if self.original_tv_now == nil then + self.original_tv_now = TimeVal.now + assert(self.original_tv_now ~= nil) + end + + -- Store both REALTIME & MONOTONIC clocks + self.realtime = os.time() + local timespec = ffi.new("struct timespec") + C.clock_gettime(C.CLOCK_MONOTONIC_COARSE, timespec) + self.monotonic = tonumber(timespec.tv_sec) + os.time = function() --luacheck: ignore - logger.dbg("MockTime:os.time: ", self.value) - return self.value + logger.dbg("MockTime:os.time: ", self.realtime) + return self.realtime end util.gettime = function() - logger.dbg("MockTime:util.gettime: ", self.value) - return self.value, 0 + logger.dbg("MockTime:util.gettime: ", self.realtime) + return self.realtime, 0 + end + TimeVal.realtime = function() + logger.dbg("MockTime:TimeVal.realtime: ", self.realtime) + return TimeVal:new{ sec = self.realtime } + end + TimeVal.realtime_coarse = function() + logger.dbg("MockTime:TimeVal.realtime_coarse: ", self.realtime) + return TimeVal:new{ sec = self.realtime } + end + TimeVal.monotonic = function() + logger.dbg("MockTime:TimeVal.monotonic: ", self.monotonic) + return TimeVal:new{ sec = self.monotonic } + end + TimeVal.monotonic_coarse = function() + logger.dbg("MockTime:TimeVal.monotonic_coarse: ", self.monotonic) + return TimeVal:new{ sec = self.monotonic } + end + TimeVal.boottime = function() + logger.dbg("MockTime:TimeVal.boottime: ", self.boottime) + return TimeVal:new{ sec = self.boottime } + end + TimeVal.now = function() + logger.dbg("MockTime:TimeVal.now: ", self.monotonic) + return TimeVal:new{ sec = self.monotonic } end end function MockTime:uninstall() assert(self ~= nil) - local util = require("ffi/util") os.time = self.original_os_time --luacheck: ignore if self.original_util_time ~= nil then util.gettime = self.original_util_time end + if self.original_tv_realtime ~= nil then + TimeVal.realtime = self.original_tv_realtime + end + if self.original_tv_realtime_coarse ~= nil then + TimeVal.realtime_coarse = self.original_tv_realtime_coarse + end + if self.original_tv_monotonic ~= nil then + TimeVal.monotonic = self.original_tv_monotonic + end + if self.original_tv_monotonic_coarse ~= nil then + TimeVal.monotonic_coarse = self.original_tv_monotonic_coarse + end + if self.original_tv_boottime ~= nil then + TimeVal.boottime = self.original_tv_boottime + end + if self.original_tv_now ~= nil then + TimeVal.now = self.original_tv_now + end +end + +function MockTime:set_realtime(value) + assert(self ~= nil) + if type(value) ~= "number" then + return false + end + self.realtime = math.floor(value) + logger.dbg("MockTime:set_realtime ", self.realtime) + return true +end + +function MockTime:increase_realtime(value) + assert(self ~= nil) + if type(value) ~= "number" then + return false + end + self.realtime = math.floor(self.realtime + value) + logger.dbg("MockTime:increase_realtime ", self.realtime) + return true +end + +function MockTime:set_monotonic(value) + assert(self ~= nil) + if type(value) ~= "number" then + return false + end + self.monotonic = math.floor(value) + logger.dbg("MockTime:set_monotonic ", self.monotonic) + return true +end + +function MockTime:increase_monotonic(value) + assert(self ~= nil) + if type(value) ~= "number" then + return false + end + self.monotonic = math.floor(self.monotonic + value) + logger.dbg("MockTime:increase_monotonic ", self.monotonic) + return true +end + +function MockTime:set_boottime(value) + assert(self ~= nil) + if type(value) ~= "number" then + return false + end + self.boottime = math.floor(value) + logger.dbg("MockTime:set_boottime ", self.boottime) + return true +end + +function MockTime:increase_boottime(value) + assert(self ~= nil) + if type(value) ~= "number" then + return false + end + self.boottime = math.floor(self.boottime + value) + logger.dbg("MockTime:increase_boottime ", self.boottime) + return true end function MockTime:set(value) @@ -38,8 +183,12 @@ function MockTime:set(value) if type(value) ~= "number" then return false end - self.value = math.floor(value) - logger.dbg("MockTime:set ", self.value) + self.realtime = math.floor(value) + logger.dbg("MockTime:set (realtime) ", self.realtime) + self.monotonic = math.floor(value) + logger.dbg("MockTime:set (monotonic) ", self.monotonic) + self.boottime = math.floor(value) + logger.dbg("MockTime:set (boottime) ", self.boottime) return true end @@ -48,8 +197,12 @@ function MockTime:increase(value) if type(value) ~= "number" then return false end - self.value = math.floor(self.value + value) - logger.dbg("MockTime:increase ", self.value) + self.realtime = math.floor(self.realtime + value) + logger.dbg("MockTime:increase (realtime) ", self.realtime) + self.monotonic = math.floor(self.monotonic + value) + logger.dbg("MockTime:increase (monotonic) ", self.monotonic) + self.boottime = math.floor(self.boottime + value) + logger.dbg("MockTime:increase (boottime) ", self.boottime) return true end diff --git a/spec/unit/timeval_spec.lua b/spec/unit/timeval_spec.lua index bf6dd1d31..02626714f 100644 --- a/spec/unit/timeval_spec.lua +++ b/spec/unit/timeval_spec.lua @@ -20,25 +20,42 @@ describe("TimeVal module", function() local timev2 = TimeVal:new{ sec = 10, usec = 6000} local timev3 = TimeVal:new{ sec = 10, usec = 50000000} - assert.is.same({sec = 15,usec = 11000}, timev1 + timev2) - assert.is.same({sec = 65,usec = 5000}, timev1 + timev3) + assert.is.same({sec = 15, usec = 11000}, timev1 + timev2) + assert.is.same({sec = 65, usec = 5000}, timev1 + timev3) end) it("should subtract", function() local timev1 = TimeVal:new{ sec = 5, usec = 5000} local timev2 = TimeVal:new{ sec = 10, usec = 6000} - assert.is.same({sec = 5,usec = 1000}, timev2 - timev1) - assert.is.same({sec = -5,usec = -1000}, timev1 - timev2) - end) + assert.is.same({sec = 5, usec = 1000}, timev2 - timev1) + local backwards_sub = timev1 - timev2 + assert.is.same({sec = -6, usec = 999000}, backwards_sub) - it("should guard against reverse subtraction logic", function() - dbg:turnOn() - TimeVal = package.reload("ui/timeval") - local timev1 = TimeVal:new{ sec = 5, usec = 5000} - local timev2 = TimeVal:new{ sec = 10, usec = 5000} + -- Check that to/from float conversions behave, even for negative values. + assert.is.same(-5.001, backwards_sub:tonumber()) + assert.is.same({sec = -6, usec = 999000}, TimeVal:fromnumber(-5.001)) + + local tv = TimeVal:new{ sec = -6, usec = 1000 } + assert.is.same(-5.999, tv:tonumber()) + assert.is.same({sec = -6, usec = 1000}, TimeVal:fromnumber(-5.999)) + + -- We lose precision because of rounding if we go higher resolution than a ms... + tv = TimeVal:new{ sec = -6, usec = 101 } + assert.is.same(-5.9999, tv:tonumber()) + assert.is.same({sec = -6, usec = 100}, TimeVal:fromnumber(-5.9999)) + -- ^ precision loss + + tv = TimeVal:new{ sec = -6, usec = 11 } + assert.is.same(-6, tv:tonumber()) + -- ^ precision loss + assert.is.same({sec = -6, usec = 10}, TimeVal:fromnumber(-5.99999)) + -- ^ precision loss - assert.has.errors(function() return timev1 - timev2 end) + tv = TimeVal:new{ sec = -6, usec = 1 } + assert.is.same(-6, tv:tonumber()) + -- ^ precision loss + assert.is.same({sec = -6, usec = 1}, TimeVal:fromnumber(-5.999999)) end) it("should derive sec and usec from more than 1 sec worth of usec", function() diff --git a/spec/unit/uimanager_spec.lua b/spec/unit/uimanager_spec.lua index 84e8cad53..14b3a6b02 100644 --- a/spec/unit/uimanager_spec.lua +++ b/spec/unit/uimanager_spec.lua @@ -1,22 +1,22 @@ describe("UIManager spec", function() - local UIManager, util + local TimeVal, UIManager local now, wait_until local noop = function() end setup(function() require("commonrequire") - util = require("ffi/util") + TimeVal = require("ui/timeval") UIManager = require("ui/uimanager") end) it("should consume due tasks", function() - now = { util.gettime() } - local future = { now[1] + 60000, now[2] } - local future2 = {future[1] + 5, future[2]} + now = TimeVal:now() + local future = TimeVal:new{ sec = now.sec + 60000, usec = now.usec } + local future2 = TimeVal:new{ sec = future.sec + 5, usec = future.usec} UIManager:quit() UIManager._task_queue = { - { time = {now[1] - 10, now[2] }, action = noop, args = {}, argc = 0 }, - { time = {now[1], now[2] - 5 }, action = noop, args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = noop, args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = noop, args = {}, argc = 0 }, { time = now, action = noop, args = {}, argc = 0 }, { time = future, action = noop, args = {}, argc = 0 }, { time = future2, action = noop, args = {}, argc = 0 }, @@ -28,26 +28,26 @@ describe("UIManager spec", function() end) it("should calcualte wait_until properly in checkTasks routine", function() - now = { util.gettime() } - local future = { now[1] + 60000, now[2] } + now = TimeVal:now() + local future = TimeVal:new{ sec = now.sec + 60000, usec = now.usec } UIManager:quit() UIManager._task_queue = { - { time = {now[1] - 10, now[2] }, action = noop, args = {}, argc = 0 }, - { time = {now[1], now[2] - 5 }, action = noop, args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = noop, args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = noop, args = {}, argc = 0 }, { time = now, action = noop, args = {}, argc = 0 }, { time = future, action = noop, args = {}, argc = 0 }, - { time = {future[1] + 5, future[2]}, action = noop, args = {}, argc = 0 }, + { time = TimeVal:new{ sec = future.sec + 5, usec = future.usec }, action = noop, args = {}, argc = 0 }, } wait_until, now = UIManager:_checkTasks() assert.are.same(future, wait_until) end) it("should return nil wait_until properly in checkTasks routine", function() - now = { util.gettime() } + now = TimeVal:now() UIManager:quit() UIManager._task_queue = { - { time = {now[1] - 10, now[2] }, action = noop, args = {}, argc = 0 }, - { time = {now[1], now[2] - 5 }, action = noop, args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = noop, args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = noop, args = {}, argc = 0 }, { time = now, action = noop, args = {}, argc = 0 }, } wait_until, now = UIManager:_checkTasks() @@ -55,7 +55,7 @@ describe("UIManager spec", function() end) it("should insert new task properly in empty task queue", function() - now = { util.gettime() } + now = TimeVal:now() UIManager:quit() UIManager._task_queue = {} assert.are.same(0, #UIManager._task_queue) @@ -65,8 +65,8 @@ describe("UIManager spec", function() end) it("should insert new task properly in single task queue", function() - now = { util.gettime() } - local future = { now[1]+10000, now[2] } + now = TimeVal:now() + local future = TimeVal:new{ sec = now.sec + 10000, usec = now.usec } UIManager:quit() UIManager._task_queue = { { time = future, action = '1', args = {}, argc = 0 }, @@ -90,59 +90,59 @@ describe("UIManager spec", function() end) it("should insert new task in ascendant order", function() - now = { util.gettime() } + now = TimeVal:now() UIManager:quit() UIManager._task_queue = { - { time = {now[1] - 10, now[2] }, action = '1', args = {}, argc = 0 }, - { time = {now[1], now[2] - 5 }, action = '2', args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = '1', args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = '2', args = {}, argc = 0 }, { time = now, action = '3', args = {}, argc = 0 }, } -- insert into the tail slot UIManager:scheduleIn(10, 'foo') assert.are.same('foo', UIManager._task_queue[4].action) -- insert into the second slot - UIManager:schedule({now[1]-5, now[2]}, 'bar') + UIManager:schedule(TimeVal:new{ sec = now.sec - 5, usec = now.usec }, 'bar') assert.are.same('bar', UIManager._task_queue[2].action) -- insert into the head slot - UIManager:schedule({now[1]-15, now[2]}, 'baz') + UIManager:schedule(TimeVal:new{ sec = now.sec - 15, usec = now.usec }, 'baz') assert.are.same('baz', UIManager._task_queue[1].action) -- insert into the last second slot UIManager:scheduleIn(5, 'qux') assert.are.same('qux', UIManager._task_queue[6].action) -- insert into the middle slot - UIManager:schedule({now[1], now[2]-1}, 'quux') + UIManager:schedule(TimeVal:new{ sec = now.sec, usec = now.usec - 1 }, 'quux') assert.are.same('quux', UIManager._task_queue[5].action) end) it("should unschedule all the tasks with the same action", function() - now = { util.gettime() } + now = TimeVal:now() UIManager:quit() UIManager._task_queue = { - { time = {now[1] - 15, now[2] }, action = '3', args = {}, argc = 0 }, - { time = {now[1] - 10, now[2] }, action = '1', args = {}, argc = 0 }, - { time = {now[1], now[2] - 6 }, action = '3', args = {}, argc = 0 }, - { time = {now[1], now[2] - 5 }, action = '2', args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec - 15, usec = now.usec }, action = '3', args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = '1', args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec, usec = now.usec - 6 }, action = '3', args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = '2', args = {}, argc = 0 }, { time = now, action = '3', args = {}, argc = 0 }, } -- insert into the tail slot UIManager:unschedule('3') assert.are.same({ - { time = {now[1] - 10, now[2] }, action = '1', args = {}, argc = 0 }, - { time = {now[1], now[2] - 5 }, action = '2', args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = '1', args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = '2', args = {}, argc = 0 }, }, UIManager._task_queue) end) it("should not have race between unschedule and _checkTasks", function() - now = { util.gettime() } + now = TimeVal:now() local run_count = 0 local task_to_remove = function() run_count = run_count + 1 end UIManager:quit() UIManager._task_queue = { - { time = { now[1], now[2]-5 }, action = task_to_remove, args = {}, argc = 0 }, + { time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = task_to_remove, args = {}, argc = 0 }, { - time = { now[1]-10, now[2] }, + time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = function() run_count = run_count + 1 UIManager:unschedule(task_to_remove)