Skip to content
Draft
Show file tree
Hide file tree
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
3 changes: 1 addition & 2 deletions hls-graph/src/Development/IDE/Graph/Database.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ import Control.Concurrent.Extra (Barrier, newBarrier,
waitBarrierMaybe)
import Control.Concurrent.STM.Stats (atomically,
atomicallyNamed,
readTVar, readTVarIO,
writeTVar)
readTVarIO)
import Control.Exception (SomeException, try)
import Control.Monad (join, unless, void)
import Control.Monad.IO.Class (liftIO)
Expand Down
74 changes: 49 additions & 25 deletions hls-graph/src/Development/IDE/Graph/Internal/Database.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import Control.Concurrent.STM.Stats (STM, atomicallyNamed,
modifyTVar',
newTQueueIO,
newTVarIO, readTVar,
readTVarIO, retry)
readTVarIO, retry, writeTVar)
import Control.Concurrent.Async (mapConcurrently)
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class (MonadIO (liftIO))
Expand Down Expand Up @@ -480,32 +481,55 @@ transitiveDirtyListBottomUp database seeds = do
void $ State.runStateT (traverse_ go seeds) mempty
readIORef acc

-- the lefts are keys that are no longer affected, we can try to mark them clean
-- the rights are new affected keys, we need to mark them dirty
-- | A concurrent variant of 'transitiveDirtyListBottomUp' that computes the difference
-- between two sets of affected keys.
--
-- Returns:
-- * Right keys: newly affected keys that need to be marked dirty
-- * Left keys: previously affected keys that are no longer affected (can be marked clean)
--
-- The function traverses the reverse-dependency graph concurrently, processing independent
-- branches in parallel while maintaining bottom-up ordering (dependencies before dependents).
-- This improves performance on large dependency graphs by utilizing multiple cores.
--
-- Thread-safety is ensured by:
-- * Using TVar for shared state (visited set and accumulator)
-- * Atomic check-and-mark for the visited set
-- * mapConcurrently for parallel traversal of independent branches
transitiveDirtyListBottomUpDiff :: Database -> [Key] -> [Key] -> IO [Either Key Key]
transitiveDirtyListBottomUpDiff database seeds lastSeeds = do
acc <- newIORef []
let go1 x = do
seen <- State.get
if x `memberKeySet` seen
then pure ()
else do
State.put (insertKeySet x seen)
mnext <- lift $ atomically $ getRunTimeRDeps database x
traverse_ go1 (maybe mempty toListKeySet mnext)
lift $ modifyIORef' acc (Right x :)
let go2 x = do
seen <- State.get
if x `memberKeySet` seen
then pure ()
else do
State.put (insertKeySet x seen)
mnext <- lift $ atomically $ getRunTimeRDeps database x
traverse_ go2 (maybe mempty toListKeySet mnext)
lift $ modifyIORef' acc (Left x :)
-- traverse all seeds
void $ State.runStateT (do traverse_ go1 seeds; traverse_ go2 lastSeeds) mempty
readIORef acc
-- Use TVars for thread-safe concurrent access
accTVar <- newTVarIO []
seenTVar <- newTVarIO mempty

let -- Process a key and its dependencies concurrently
go :: (Key -> Either Key Key) -> Key -> IO ()
go wrapper x = do
alreadySeen <- atomically $ do
seen <- readTVar seenTVar
if x `memberKeySet` seen
then pure True
else do
writeTVar seenTVar (insertKeySet x seen)
pure False

unless alreadySeen $ do
-- Fetch dependencies
mnext <- atomically $ getRunTimeRDeps database x
let deps = maybe [] toListKeySet mnext

-- Process dependencies concurrently
unless (null deps) $ do
void $ mapConcurrently (go wrapper) deps

-- Add this key to accumulator after all dependencies are processed
atomically $ modifyTVar' accTVar (wrapper x :)

-- Process new seeds (Right) and old seeds (Left) concurrently
void $ mapConcurrently (go Right) seeds
void $ mapConcurrently (go Left) lastSeeds

readTVarIO accTVar
Comment on lines +501 to +532
Copy link
Owner

@soulomoon soulomoon Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually slower since we are using too many threads. We might do a bfs, using limited number of threads to speed up the computation @copilot



-- | Original spawnRefresh using the general pattern
Expand Down