From 6d53f83286ffbd11cb954e5542c1d55388e5e1f2 Mon Sep 17 00:00:00 2001 From: NiLuJe Date: Tue, 30 Mar 2021 02:57:59 +0200 Subject: [PATCH] The great Input/GestureDetector/TimeVal spring cleanup (a.k.a., a saner main loop) (#7415) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ReaderDictionary: Port delay computations to TimeVal * ReaderHighlight: Port delay computations to TimeVal * ReaderView: Port delay computations to TimeVal * Android: Reset gesture detection state on APP_CMD_TERM_WINDOW. This prevents potentially being stuck in bogus gesture states when switching apps. * GestureDetector: * Port delay computations to TimeVal * Fixed delay computations to handle time warps (large and negative deltas). * Simplified timed callback handling to invalidate timers much earlier, preventing accumulating useless timers that no longer have any chance of ever detecting a gesture. * Fixed state clearing to handle the actual effective slots, instead of hard-coding slot 0 & slot 1. * Simplified timed callback handling in general, and added support for a timerfd backend for better performance and accuracy. * The improved timed callback handling allows us to detect and honor (as much as possible) the three possible clock sources usable by Linux evdev events. The only case where synthetic timestamps are used (and that only to handle timed callbacks) is limited to non-timerfd platforms where input events use a clock source that is *NOT* MONOTONIC. AFAICT, that's pretty much... PocketBook, and that's it? * Input: * Use the FFI module instead of re-declaring every constant * Fixed (verbose) debug logging of input events to actually translate said constants properly. * Completely reset gesture detection state on suspend. This should prevent bogus gesture detection on resume. * Refactored the waitEvent loop to make it easier to comprehend (hopefully) and much more efficient. Of specific note, it no longer does a crazy select spam every 100µs, instead computing and relying on sane timeouts, as afforded by switching the UI event/input loop to the MONOTONIC time base, and the refactored timed callbacks in GestureDetector. * reMarkable: Stopped enforcing synthetic timestamps on input events, as it should no longer be necessary. * TimeVal: * Refactored and simplified, especially as far as metamethods are concerned (based on ). * Added a host of new methods to query the various POSIX clock sources, and made :now default to MONOTONIC. * Removed the debug guard in __sub, as time going backwards can be a perfectly normal occurrence. * New methods: * Clock sources: :realtime, :monotonic, :monotonic_coarse, :realtime_coarse, :boottime * Utility: :tonumber, :tousecs, :tomsecs, :fromnumber, :isPositive, :isZero * UIManager: * Ported event loop & scheduling to TimeVal, and switched to the MONOTONIC time base. This ensures reliable and consistent scheduling, as time is ensured never to go backwards. * Added a :getTime() method, that returns a cached TimeVal:now(), updated at the top of every UI frame. It's used throughout the codebase to cadge a syscall in circumstances where we are guaranteed that a syscall would return a mostly identical value, because very few time has passed. The only code left that does live syscalls does it because it's actually necessary for accuracy, and the only code left that does that in a REALTIME time base is code that *actually* deals with calendar time (e.g., Statistics). * DictQuickLookup: Port delay computations to TimeVal * FootNoteWidget: Port delay computations to TimeVal * HTMLBoxWidget: Port delay computations to TimeVal * Notification: Port delay computations to TimeVal * TextBoxWidget: Port delay computations to TimeVal * AutoSuspend: Port to TimeVal * AutoTurn: * Fix it so that settings are actually honored. * Port to TimeVal * BackgroundRunner: Port to TimeVal * Calibre: Port benchmarking code to TimeVal * BookInfoManager: Removed unnecessary yield in the metadata extraction subprocess now that subprocesses get scheduled properly. * All in all, these changes reduced the CPU cost of a single tap by a factor of ten (!), and got rid of an insane amount of weird poll/wakeup cycles that must have been hell on CPU schedulers and batteries.. --- base | 2 +- .../apps/reader/modules/readerdictionary.lua | 9 +- .../apps/reader/modules/readerhighlight.lua | 11 +- frontend/apps/reader/modules/readerview.lua | 16 +- frontend/device/android/device.lua | 2 + frontend/device/gesturedetector.lua | 205 ++++--- frontend/device/input.lua | 513 ++++++++++++------ frontend/device/remarkable/device.lua | 3 - frontend/ui/gesturerange.lua | 20 +- frontend/ui/timeval.lua | 194 +++++-- frontend/ui/uimanager.lua | 107 ++-- frontend/ui/widget/dictquicklookup.lua | 3 +- frontend/ui/widget/footnotewidget.lua | 3 +- frontend/ui/widget/htmlboxwidget.lua | 7 +- frontend/ui/widget/notification.lua | 7 +- frontend/ui/widget/textboxwidget.lua | 10 +- frontend/ui/widget/virtualkeyboard.lua | 3 + plugins/autostandby.koplugin/main.lua | 2 +- plugins/autosuspend.koplugin/main.lua | 25 +- plugins/autoturn.koplugin/main.lua | 25 +- .../commandrunner.lua | 5 +- plugins/backgroundrunner.koplugin/main.lua | 37 +- plugins/calibre.koplugin/metadata.lua | 14 +- plugins/calibre.koplugin/search.lua | 19 +- .../coverbrowser.koplugin/bookinfomanager.lua | 1 - reader.lua | 2 - setupkoenv.lua | 2 +- spec/unit/autosuspend_spec.lua | 8 +- spec/unit/background_runner_spec.lua | 16 +- spec/unit/device_spec.lua | 6 +- spec/unit/mock_time.lua | 175 +++++- spec/unit/timeval_spec.lua | 39 +- spec/unit/uimanager_spec.lua | 68 +-- 33 files changed, 1068 insertions(+), 491 deletions(-) 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)