Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

STM debouncer #2466

Closed
wants to merge 6 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 48 additions & 23 deletions ghcide/src/Development/IDE/Core/Debouncer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ module Development.IDE.Core.Debouncer
) where

import Control.Concurrent.Async
import Control.Concurrent.STM.Stats (atomically, atomicallyNamed)
import Control.Exception
import Control.Monad (join)
import Data.Foldable (traverse_)
import Control.Concurrent.STM
import Control.Concurrent.STM.Stats (atomicallyNamed)
import Control.Monad (join, void)
import Data.Hashable
import qualified Focus
import GHC.Conc (unsafeIOToSTM)
import qualified StmContainers.Map as STM
import System.Time.Extra

Expand All @@ -35,24 +34,50 @@ newAsyncDebouncer = Debouncer . asyncRegisterEvent <$> STM.newIO

-- | Register an event that will fire after the given delay if no other event
-- for the same key gets registered until then.
--
-- If there is a pending event for the same key, the pending event will be killed.
-- Events are run unmasked so it is up to the user of `registerEvent`
-- to mask if required.
asyncRegisterEvent :: (Eq k, Hashable k) => STM.Map k (Async ()) -> Seconds -> k -> IO () -> IO ()
asyncRegisterEvent d 0 k fire = do
join $ atomically $ do
prev <- STM.focus Focus.lookupAndDelete k d
return $ traverse_ cancel prev
fire
asyncRegisterEvent d delay k fire = mask_ $ do
a <- asyncWithUnmask $ \unmask -> unmask $ do
sleep delay
fire
atomically $ STM.delete k d
do
prev <- atomicallyNamed "debouncer" $ STM.focus (Focus.lookup <* Focus.insert a) k d
traverse_ cancel prev
asyncRegisterEvent
:: (Eq k, Hashable k)
=> STM.Map k (TVar (Maybe (Seconds, IO())))
-> Seconds
-> k
-> IO ()
-> IO ()
asyncRegisterEvent d delay k fire = join $ atomicallyNamed "debouncer - register" $ do
-- The previous TVar for this key, if any
prev <- STM.lookup k d
pepeiborra marked this conversation as resolved.
Show resolved Hide resolved
case prev of
Just v -> do
current <- readTVar v
case current of
-- Not empty, means that there is a thread running the actions
Just _ -> writeTVar v (Just (delay, fire)) >> return (pure ())
-- Empty = no thread. We need to start one for running the action
Nothing -> writeTVar v (Just (delay, fire)) >> return (restart v)

-- No previous TVar, we need to insert one and restart a thread for running the action
Nothing
| delay == 0 -> return fire
pepeiborra marked this conversation as resolved.
Show resolved Hide resolved
| otherwise -> do
var <- newTVar (Just (delay, fire))
STM.insert var k d
return (restart var)
where
-- | Restart a thread to run the action stored in the given TVar
-- Once the action is done, the thread dies.
-- Assumes the Tvar is not empty
restart var =
void $ async $
join $ atomicallyNamed "debouncer - sleep" $ do
contents <- readTVar var
case contents of
Nothing -> error "impossible"
Just (s,act) -> do
-- sleep for the given delay
-- If the TVar is written while sleeping,
-- the transaction will restart
unsafeIOToSTM $ sleep s
-- we are done - empty the TVar before exiting
writeTVar var Nothing
return act

-- | Debouncer used in the DAML CLI compiler that emits events immediately.
noopDebouncer :: Debouncer k
Expand Down