| ... |
... |
@@ -12,7 +12,8 @@ module GHC.Driver.Concurrency |
|
12
|
12
|
-- * Concurrent worker scheduling
|
|
13
|
13
|
, ConcurrentWorkerEnv(..)
|
|
14
|
14
|
, mapConcurrentWorkers
|
|
15
|
|
- , concurrentTraversal_DF
|
|
|
15
|
+ , NodeExpander(..)
|
|
|
16
|
+ , concurrentTraversal
|
|
16
|
17
|
)
|
|
17
|
18
|
where
|
|
18
|
19
|
|
| ... |
... |
@@ -20,12 +21,16 @@ import GHC.Prelude |
|
20
|
21
|
|
|
21
|
22
|
import GHC.Driver.MakeSem
|
|
22
|
23
|
import GHC.Driver.Pipeline.LogQueue
|
|
23
|
|
- ( LogQueueQueue, finishLogQueue, initLogQueue, logThread
|
|
24
|
|
- , newLogQueue, newLogQueueQueue, parLogAction )
|
|
|
24
|
+ ( LogQueue, LogQueueQueue, finishLogQueue, initLogQueue, logThread
|
|
|
25
|
+ , newLogQueue, newLogQueueQueue, parLogAction, printLogs )
|
|
25
|
26
|
import GHC.Utils.Logger
|
|
26
|
27
|
( Logger, makeThreadSafe, pushLogHook )
|
|
|
28
|
+import GHC.Utils.Misc
|
|
|
29
|
+ ( HasDebugCallStack )
|
|
|
30
|
+import GHC.Utils.Outputable
|
|
|
31
|
+ ( Outputable(..), text, (<+>) )
|
|
27
|
32
|
import GHC.Utils.Panic
|
|
28
|
|
- ( panic )
|
|
|
33
|
+ ( massertPpr, pprPanic )
|
|
29
|
34
|
import GHC.Utils.TmpFs
|
|
30
|
35
|
( TmpFs, forkTmpFsFrom, mergeTmpFsInto, withLocalTmpFS )
|
|
31
|
36
|
|
| ... |
... |
@@ -36,31 +41,30 @@ import System.Semaphore |
|
36
|
41
|
import Control.Concurrent
|
|
37
|
42
|
( ThreadId, forkIOWithUnmask, killThread, myThreadId )
|
|
38
|
43
|
import Control.Concurrent.MVar
|
|
39
|
|
- ( MVar, newEmptyMVar, newMVar, putMVar, takeMVar )
|
|
|
44
|
+ ( MVar, newMVar, putMVar, takeMVar )
|
|
40
|
45
|
import GHC.Conc
|
|
41
|
46
|
( labelThread )
|
|
42
|
47
|
#else
|
|
43
|
48
|
import Control.Concurrent
|
|
44
|
49
|
( ThreadId, forkIOWithUnmask, killThread, myThreadId
|
|
45
|
|
- , newQSem, signalQSem, waitQSem, MVar, takeMVar, putMVar, newEmptyMVar )
|
|
46
|
|
-import Control.Monad
|
|
47
|
|
- ( unless )
|
|
|
50
|
+ , newQSem, signalQSem, waitQSem )
|
|
48
|
51
|
import qualified Control.Monad.Catch as MC
|
|
49
|
52
|
import GHC.Conc
|
|
50
|
53
|
( getNumCapabilities, getNumProcessors, labelThread, setNumCapabilities )
|
|
51
|
54
|
#endif
|
|
52
|
55
|
import Control.Concurrent.STM
|
|
53
|
|
- ( TVar, atomically, check, modifyTVar', newTVarIO, readTVar, writeTVar )
|
|
|
56
|
+ ( TVar, atomically, check, modifyTVar', newTVarIO, readTVar, readTVarIO
|
|
|
57
|
+ , writeTVar )
|
|
54
|
58
|
import Control.Exception
|
|
55
|
59
|
( AsyncException(ThreadKilled), SomeAsyncException, SomeException
|
|
56
|
|
- , finally, fromException, mask, mask_, onException
|
|
|
60
|
+ , catch, finally, fromException, mask, mask_, onException
|
|
57
|
61
|
, throwIO, try, uninterruptibleMask_ )
|
|
58
|
62
|
import Control.Monad
|
|
59
|
|
- ( replicateM )
|
|
|
63
|
+ ( unless )
|
|
60
|
64
|
import Data.Foldable
|
|
61
|
|
- ( for_ )
|
|
|
65
|
+ ( for_, traverse_ )
|
|
62
|
66
|
import Data.IORef
|
|
63
|
|
- ( IORef, atomicModifyIORef', modifyIORef', newIORef, readIORef, writeIORef )
|
|
|
67
|
+ ( IORef, atomicModifyIORef', modifyIORef', newIORef, readIORef )
|
|
64
|
68
|
import qualified Data.Map as Map
|
|
65
|
69
|
import qualified Data.Sequence as Seq
|
|
66
|
70
|
import qualified Data.Set as Set
|
| ... |
... |
@@ -211,72 +215,163 @@ withWorkerLimit logger tmpfs report_semaphore_failure limit action |
|
211
|
215
|
action conc parent_work_env
|
|
212
|
216
|
|
|
213
|
217
|
--------------------------------------------------------------------------------
|
|
214
|
|
--- * Scheduling concurrent workers
|
|
|
218
|
+-- * Monotone data structures
|
|
215
|
219
|
--------------------------------------------------------------------------------
|
|
216
|
220
|
|
|
217
|
|
--- | Internal scheduler abstraction with two capabilities:
|
|
|
221
|
+-- | A map that only ever grows, and whose entries are written at most once.
|
|
|
222
|
+newtype MonotoneMap k v = MonotoneMap ( IORef ( Map.Map k v ) )
|
|
|
223
|
+
|
|
|
224
|
+newMonotoneMap :: Map.Map k v -> IO ( MonotoneMap k v )
|
|
|
225
|
+newMonotoneMap initial = MonotoneMap <$> newIORef initial
|
|
|
226
|
+
|
|
|
227
|
+-- | The outcome of inserting into a 'MonotoneMap' or a 'MonotoneSet'.
|
|
|
228
|
+data InsertionResult
|
|
|
229
|
+ -- | The key was absent before the insertion.
|
|
|
230
|
+ = Inserted
|
|
|
231
|
+ -- | The key was already present; the container is unchanged.
|
|
|
232
|
+ | AlreadyPresent
|
|
|
233
|
+
|
|
|
234
|
+-- | Write an entry into a 'MonotoneMap' unless the key is already present.
|
|
|
235
|
+insertMonotoneMap :: Ord k => MonotoneMap k v -> k -> v -> IO InsertionResult
|
|
|
236
|
+insertMonotoneMap ( MonotoneMap ref ) k v =
|
|
|
237
|
+ atomicModifyIORef' ref \ m ->
|
|
|
238
|
+ case Map.insertLookupWithKey ( \ _ _ old -> old ) k v m of
|
|
|
239
|
+ ( Nothing , m' ) -> ( m', Inserted )
|
|
|
240
|
+ ( Just _ , _ ) -> ( m , AlreadyPresent )
|
|
|
241
|
+
|
|
|
242
|
+-- | Write a new entry into a 'MonotoneMap'.
|
|
218
|
243
|
--
|
|
219
|
|
--- - spawn a new worker thread
|
|
220
|
|
--- - wait for a worker thread to complete
|
|
221
|
|
-data Scheduler r = Scheduler
|
|
222
|
|
- { spawnWorker :: (ConcurrentWorkerEnv -> IO r) -> IO ()
|
|
223
|
|
- -- ^ Spawn one concurrent worker.
|
|
224
|
|
- --
|
|
225
|
|
- -- The worker does not hold a token of the concurrency semaphore: the
|
|
226
|
|
- -- worker action should use 'withConcurrency' around the work whose
|
|
227
|
|
- -- concurrency should be limited.
|
|
228
|
|
- , awaitWorker :: IO (Either SomeException r)
|
|
229
|
|
- -- ^ Wait for one worker to complete.
|
|
230
|
|
- --
|
|
231
|
|
- -- Will crash if there are no outstanding workers.
|
|
232
|
|
- }
|
|
|
244
|
+-- Panics if the entry is already present.
|
|
|
245
|
+insertMonotoneMap_new
|
|
|
246
|
+ :: ( HasDebugCallStack, Ord k, Outputable k )
|
|
|
247
|
+ => MonotoneMap k v -> k -> v -> IO ()
|
|
|
248
|
+insertMonotoneMap_new mm k v =
|
|
|
249
|
+ insertMonotoneMap mm k v >>= \case
|
|
|
250
|
+ Inserted -> pure ()
|
|
|
251
|
+ AlreadyPresent -> pprPanic "monotone map: duplicate key" $ ppr k
|
|
|
252
|
+
|
|
|
253
|
+-- | The contents of a monotone map.
|
|
|
254
|
+freezeMonotoneMap :: MonotoneMap k v -> IO ( Map.Map k v )
|
|
|
255
|
+freezeMonotoneMap ( MonotoneMap ref ) = readIORef ref
|
|
|
256
|
+
|
|
|
257
|
+-- | A set that only ever grows.
|
|
|
258
|
+newtype MonotoneSet k = MonotoneSet ( IORef ( Set.Set k ) )
|
|
|
259
|
+
|
|
|
260
|
+newMonotoneSet :: Set.Set k -> IO ( MonotoneSet k )
|
|
|
261
|
+newMonotoneSet initial = MonotoneSet <$> newIORef initial
|
|
|
262
|
+
|
|
|
263
|
+-- | Add an element, unless it is already present.
|
|
|
264
|
+insertMonotoneSet :: Ord k => MonotoneSet k -> k -> IO InsertionResult
|
|
|
265
|
+insertMonotoneSet ( MonotoneSet ref ) k =
|
|
|
266
|
+ atomicModifyIORef' ref \ s ->
|
|
|
267
|
+ if k `Set.member` s
|
|
|
268
|
+ then ( s , AlreadyPresent )
|
|
|
269
|
+ else ( Set.insert k s , Inserted )
|
|
|
270
|
+
|
|
|
271
|
+--------------------------------------------------------------------------------
|
|
|
272
|
+-- * Pools of concurrent workers
|
|
|
273
|
+--------------------------------------------------------------------------------
|
|
233
|
274
|
|
|
234
|
|
--- | Internal implementation of a concurrent worker scheduler.
|
|
|
275
|
+-- | The order in which logging should happen when using concurrent workers.
|
|
|
276
|
+data LogOrder
|
|
|
277
|
+ -- | Log as we go.
|
|
|
278
|
+ --
|
|
|
279
|
+ -- Only valid when workers are spawned in a deterministic order.
|
|
|
280
|
+ = LogAsWeGo
|
|
|
281
|
+ -- | Accumulate logs per worker. Once all work is done, sort the logs
|
|
|
282
|
+ -- before proceeding.
|
|
|
283
|
+ --
|
|
|
284
|
+ -- Used when workers may be spawned in a non-deterministic order.
|
|
|
285
|
+ | SortLogs
|
|
|
286
|
+
|
|
|
287
|
+-- | A pool of concurrent workers with a given worker key type, supporting two
|
|
|
288
|
+-- operations:
|
|
235
|
289
|
--
|
|
236
|
|
--- Usage of this function requires the following:
|
|
|
290
|
+-- - spawning a new worker,
|
|
|
291
|
+-- - waiting on all workers to finish.
|
|
237
|
292
|
--
|
|
238
|
|
--- - all spawn/await actions are performed by a single thread,
|
|
239
|
|
--- - we never wait for more workers than were spawned,
|
|
240
|
|
--- - no worker outlives 'run_schedule'.
|
|
241
|
|
-run_schedule
|
|
242
|
|
- :: forall r a
|
|
243
|
|
- . String
|
|
244
|
|
- -- ^ thread label for workers
|
|
|
293
|
+-- See Note [Deterministic concurrent workers].
|
|
|
294
|
+data WorkerPool worker_key =
|
|
|
295
|
+ WorkerPool
|
|
|
296
|
+ { spawnWorker :: worker_key -> ( ConcurrentWorkerEnv -> IO () ) -> IO ()
|
|
|
297
|
+ -- ^ Spawn one worker with the given worker key.
|
|
|
298
|
+ --
|
|
|
299
|
+ -- May be called from inside another worker.
|
|
|
300
|
+ --
|
|
|
301
|
+ -- The worker does not hold a token of the concurrency semaphore: the
|
|
|
302
|
+ -- worker action should use 'withConcurrency' around the work whose
|
|
|
303
|
+ -- concurrency should be limited.
|
|
|
304
|
+ --
|
|
|
305
|
+ -- An exception escaping the action stops further workers from being
|
|
|
306
|
+ -- spawned, and is rethrown by 'waitForWorkers'.
|
|
|
307
|
+ , waitForWorkers :: IO ()
|
|
|
308
|
+ -- ^ Wait until all workers are done, throwing an exception if any
|
|
|
309
|
+ -- worker failed (which exception is thrown is not deterministic).
|
|
|
310
|
+ }
|
|
|
311
|
+
|
|
|
312
|
+-- | Internal implementation of a pool of concurrent workers.
|
|
|
313
|
+run_pool
|
|
|
314
|
+ :: forall worker_key a
|
|
|
315
|
+ . ( HasDebugCallStack, Ord worker_key, Outputable worker_key )
|
|
|
316
|
+ => String -- ^ thread label for workers
|
|
|
317
|
+ -> LogOrder
|
|
245
|
318
|
-> Concurrency
|
|
246
|
319
|
-> ConcurrentWorkerEnv
|
|
247
|
|
- -> (Scheduler r -> IO a)
|
|
248
|
|
- -- ^ worker action
|
|
|
320
|
+ -> ( WorkerPool worker_key -> IO a )
|
|
249
|
321
|
-> IO a
|
|
250
|
|
-run_schedule worker_label conc parent_work_env withScheduler =
|
|
|
322
|
+run_pool worker_label log_order conc parent_work_env withPool =
|
|
251
|
323
|
case conc of
|
|
252
|
324
|
|
|
253
|
325
|
Serial -> do
|
|
254
|
|
- results_var <- newIORef Seq.empty
|
|
|
326
|
+ queued_var <- newIORef $ Seq.empty @( worker_key, ConcurrentWorkerEnv -> IO () )
|
|
|
327
|
+ logs_var <- newMonotoneMap $ Map.empty @worker_key @LogQueue
|
|
|
328
|
+
|
|
255
|
329
|
let
|
|
256
|
|
- spawnWorker :: (ConcurrentWorkerEnv -> IO r) -> IO ()
|
|
257
|
|
- spawnWorker action = do
|
|
258
|
|
- res <- try @SomeException $
|
|
|
330
|
+ spawnWorker :: worker_key -> ( ConcurrentWorkerEnv -> IO () ) -> IO ()
|
|
|
331
|
+ spawnWorker worker_key action =
|
|
|
332
|
+ modifyIORef' queued_var ( Seq.|> ( worker_key, action ) )
|
|
|
333
|
+
|
|
|
334
|
+ run_worker :: worker_key -> ( ConcurrentWorkerEnv -> IO () ) -> IO ()
|
|
|
335
|
+ run_worker worker_key action = case log_order of
|
|
|
336
|
+ LogAsWeGo ->
|
|
259
|
337
|
workerEnv_withLocalTmpFS parent_work_env action
|
|
260
|
|
- case res of
|
|
261
|
|
- Left e
|
|
262
|
|
- | Just _ <- fromException @SomeAsyncException e
|
|
263
|
|
- -> throwIO e
|
|
264
|
|
- _ -> modifyIORef' results_var (Seq.|> res)
|
|
265
|
|
-
|
|
266
|
|
- awaitWorker :: IO (Either SomeException r)
|
|
267
|
|
- awaitWorker =
|
|
268
|
|
- readIORef results_var >>= \case
|
|
269
|
|
- res Seq.:<| rest -> do
|
|
270
|
|
- writeIORef results_var rest
|
|
271
|
|
- pure res
|
|
272
|
|
- Seq.Empty ->
|
|
273
|
|
- panic "run_schedule: no outstanding job"
|
|
274
|
|
-
|
|
275
|
|
- withScheduler $ Scheduler { spawnWorker, awaitWorker }
|
|
|
338
|
+ SortLogs -> do
|
|
|
339
|
+ -- Use a log queue for consistency with the concurrent case.
|
|
|
340
|
+ lq <- newLogQueue
|
|
|
341
|
+ insertMonotoneMap_new logs_var worker_key lq
|
|
|
342
|
+ let
|
|
|
343
|
+ worker_work_env :: ConcurrentWorkerEnv
|
|
|
344
|
+ worker_work_env =
|
|
|
345
|
+ parent_work_env
|
|
|
346
|
+ { cwe_logger = pushLogHook ( const ( parLogAction lq ) )
|
|
|
347
|
+ ( cwe_logger parent_work_env ) }
|
|
|
348
|
+ workerEnv_withLocalTmpFS worker_work_env action
|
|
|
349
|
+ `finally` finishLogQueue lq
|
|
|
350
|
+
|
|
|
351
|
+ waitForWorkers :: IO ()
|
|
|
352
|
+ waitForWorkers = do
|
|
|
353
|
+ next <- atomicModifyIORef' queued_var \ queued ->
|
|
|
354
|
+ case queued of
|
|
|
355
|
+ work Seq.:<| rest -> ( rest , Just work )
|
|
|
356
|
+ Seq.Empty -> ( queued, Nothing )
|
|
|
357
|
+ case next of
|
|
|
358
|
+ Nothing -> pure ()
|
|
|
359
|
+ Just ( worker_key, action ) ->
|
|
|
360
|
+ run_worker worker_key action *> waitForWorkers
|
|
|
361
|
+
|
|
|
362
|
+ print_logs :: IO ()
|
|
|
363
|
+ print_logs = do
|
|
|
364
|
+ logs <- freezeMonotoneMap logs_var
|
|
|
365
|
+ for_ ( Map.elems logs ) $ printLogs ( cwe_logger parent_work_env )
|
|
|
366
|
+
|
|
|
367
|
+ withPool ( WorkerPool { spawnWorker, waitForWorkers } )
|
|
|
368
|
+ `finally` print_logs
|
|
276
|
369
|
|
|
277
|
370
|
Concurrent ( ConcurrencyEnv { ce_next_log_queue_id, ce_log_queue_queue } ) -> do
|
|
278
|
|
- worker_tids_var <- newTVarIO $ Set.empty @ThreadId
|
|
279
|
|
- all_results_vars_var <- newIORef $ Seq.empty @(MVar (Either SomeException r))
|
|
|
371
|
+ worker_tids_var <- newTVarIO $ Set.empty @ThreadId
|
|
|
372
|
+ failure_var <- newTVarIO $ Nothing @SomeException
|
|
|
373
|
+ logs_var <- newMonotoneMap $ Map.empty @worker_key @LogQueue
|
|
|
374
|
+ last_spawned_var <- newIORef $ Nothing @worker_key
|
|
280
|
375
|
|
|
281
|
376
|
let
|
|
282
|
377
|
wait_for_workers :: IO ()
|
| ... |
... |
@@ -291,92 +386,118 @@ run_schedule worker_label conc parent_work_env withScheduler = |
|
291
|
386
|
for_ tids killThread
|
|
292
|
387
|
wait_for_workers
|
|
293
|
388
|
|
|
294
|
|
- awaitWorker :: IO (Either SomeException r)
|
|
295
|
|
- awaitWorker =
|
|
296
|
|
- readIORef all_results_vars_var >>= \case
|
|
297
|
|
- first_worker_res_var Seq.:<| rest -> do
|
|
298
|
|
- writeIORef all_results_vars_var rest
|
|
299
|
|
- -- block on the earliest-spawned outstanding worker
|
|
300
|
|
- takeMVar first_worker_res_var
|
|
301
|
|
- Seq.Empty ->
|
|
302
|
|
- panic "run_schedule: no outstanding job"
|
|
303
|
|
-
|
|
304
|
|
- spawnWorker :: (ConcurrentWorkerEnv -> IO r) -> IO ()
|
|
305
|
|
- spawnWorker action = mask_ do
|
|
306
|
|
-
|
|
307
|
|
- worker_res_var <- newEmptyMVar
|
|
308
|
|
-
|
|
309
|
|
- -- TmpFs
|
|
310
|
|
- lcl_tmpfs <- forkTmpFsFrom (cwe_tmpfs parent_work_env)
|
|
311
|
|
-
|
|
312
|
|
- -- LogQueue
|
|
313
|
|
- lq <- do
|
|
314
|
|
- job_id <- atomicModifyIORef' ce_next_log_queue_id \n -> (n + 1, n)
|
|
315
|
|
- lq <- newLogQueue job_id
|
|
316
|
|
- atomically $ initLogQueue ce_log_queue_queue lq
|
|
317
|
|
- pure lq
|
|
|
389
|
+ -- Record a worker failure, preventing any further work from starting.
|
|
|
390
|
+ record_failure :: SomeException -> IO ()
|
|
|
391
|
+ record_failure e =
|
|
|
392
|
+ atomically $ modifyTVar' failure_var \ failure ->
|
|
|
393
|
+ case failure of
|
|
|
394
|
+ Nothing -> Just e
|
|
|
395
|
+ Just {} -> failure
|
|
318
|
396
|
|
|
319
|
|
- let
|
|
|
397
|
+ waitForWorkers :: IO ()
|
|
|
398
|
+ waitForWorkers = do
|
|
|
399
|
+ wait_for_workers
|
|
|
400
|
+ traverse_ throwIO =<< readTVarIO failure_var
|
|
|
401
|
+
|
|
|
402
|
+ -- Create the log queue of a worker, ordering it according to the worker key.
|
|
|
403
|
+ new_worker_log_queue :: worker_key -> IO LogQueue
|
|
|
404
|
+ new_worker_log_queue worker_key = do
|
|
|
405
|
+ lq <- newLogQueue
|
|
|
406
|
+ case log_order of
|
|
|
407
|
+ LogAsWeGo -> do
|
|
|
408
|
+ last_spawned <-
|
|
|
409
|
+ atomicModifyIORef' last_spawned_var \ last_spawned ->
|
|
|
410
|
+ ( Just worker_key, last_spawned )
|
|
|
411
|
+ massertPpr ( all ( < worker_key ) last_spawned ) $
|
|
|
412
|
+ text "run_pool: LogAsWeGo workers spawned out of order:"
|
|
|
413
|
+ <+> ppr last_spawned <+> text "then" <+> ppr worker_key
|
|
|
414
|
+ job_id <- atomicModifyIORef' ce_next_log_queue_id \ n -> ( n + 1, n )
|
|
|
415
|
+ atomically $ initLogQueue ce_log_queue_queue job_id lq
|
|
|
416
|
+ SortLogs ->
|
|
|
417
|
+ insertMonotoneMap_new logs_var worker_key lq
|
|
|
418
|
+ pure lq
|
|
|
419
|
+
|
|
|
420
|
+ -- Hand the log queues over for printing, in worker key order.
|
|
|
421
|
+ release_queued_logs :: IO ()
|
|
|
422
|
+ release_queued_logs = do
|
|
|
423
|
+ queued <- freezeMonotoneMap logs_var
|
|
|
424
|
+ unless ( Map.null queued ) do
|
|
|
425
|
+ first_id <-
|
|
|
426
|
+ atomicModifyIORef' ce_next_log_queue_id \ n ->
|
|
|
427
|
+ ( n + Map.size queued, n )
|
|
|
428
|
+ atomically $
|
|
|
429
|
+ for_ ( zip [ first_id .. ] ( Map.elems queued ) ) \ ( job_id, lq ) ->
|
|
|
430
|
+ initLogQueue ce_log_queue_queue job_id lq
|
|
|
431
|
+
|
|
|
432
|
+ spawnWorker :: worker_key -> ( ConcurrentWorkerEnv -> IO () ) -> IO ()
|
|
|
433
|
+ spawnWorker worker_key action = mask_ do
|
|
|
434
|
+ failure <- readTVarIO failure_var
|
|
|
435
|
+ case failure of
|
|
|
436
|
+ -- A worker has failed: don't start any more work.
|
|
|
437
|
+ Just {} -> pure ()
|
|
|
438
|
+ Nothing -> do
|
|
|
439
|
+
|
|
|
440
|
+ -- TmpFs
|
|
|
441
|
+ lcl_tmpfs <- forkTmpFsFrom ( cwe_tmpfs parent_work_env )
|
|
|
442
|
+
|
|
|
443
|
+ -- LogQueue
|
|
|
444
|
+ lq <- new_worker_log_queue worker_key
|
|
320
|
445
|
|
|
321
|
|
- worker_work_env :: ConcurrentWorkerEnv
|
|
322
|
|
- worker_work_env =
|
|
323
|
|
- parent_work_env
|
|
324
|
|
- { cwe_tmpfs = lcl_tmpfs
|
|
325
|
|
- , cwe_logger = pushLogHook (const (parLogAction lq))
|
|
326
|
|
- (cwe_logger parent_work_env)
|
|
327
|
|
- }
|
|
328
|
|
-
|
|
329
|
|
- -- Run a worker action and record its result.
|
|
330
|
|
- run_worker_and_record :: IO r -> IO ()
|
|
331
|
|
- run_worker_and_record worker_action = do
|
|
332
|
|
- res <- try @SomeException worker_action
|
|
333
|
|
- case res of
|
|
334
|
|
- Left e
|
|
335
|
|
- -- Worker is being cancelled: don't record anything.
|
|
336
|
|
- | Just ThreadKilled <- fromException e
|
|
337
|
|
- -> pure ()
|
|
338
|
|
- _ -> putMVar worker_res_var res
|
|
339
|
|
-
|
|
340
|
|
- -- Record that a worker thread is done.
|
|
341
|
|
- mark_worker_done :: ThreadId -> IO ()
|
|
342
|
|
- mark_worker_done tid =
|
|
343
|
|
- uninterruptibleMask_ do
|
|
344
|
|
- -- Uninterruptible: the deletion below /must/ occur.
|
|
345
|
|
- -- An uninterruptible mask is OK as we only ever block for (GAP) below.
|
|
346
|
|
- mergeTmpFsInto lcl_tmpfs $ cwe_tmpfs parent_work_env
|
|
347
|
|
- finishLogQueue lq
|
|
348
|
|
- atomically do
|
|
349
|
|
- tids <- readTVar worker_tids_var
|
|
350
|
|
- check $ tid `Set.member` tids
|
|
351
|
|
- -- Ensure we never end up with a dead ThreadId in 'worker_tids_var'
|
|
352
|
|
- -- (if the worker thread finishes before the parent thread has
|
|
353
|
|
- -- the time to add its ThreadId to 'worker_tids_var').
|
|
354
|
|
-
|
|
355
|
|
- writeTVar worker_tids_var $ Set.delete tid tids
|
|
356
|
|
-
|
|
357
|
|
- run_worker :: (forall b. IO b -> IO b) -> IO ()
|
|
358
|
|
- run_worker unmask = do
|
|
359
|
|
- tid <- myThreadId
|
|
360
|
|
- labelThread tid worker_label
|
|
361
|
446
|
let
|
|
362
|
|
- worker_action :: IO r
|
|
363
|
|
- worker_action = unmask $ action worker_work_env
|
|
364
|
447
|
|
|
365
|
|
- run_worker_and_record worker_action `finally`
|
|
366
|
|
- mark_worker_done tid
|
|
367
|
|
-
|
|
368
|
|
- worker_tid <-
|
|
369
|
|
- forkIOWithUnmask run_worker
|
|
370
|
|
- `onException` finishLogQueue lq
|
|
371
|
|
- -- Very short (GAP) between forking the thread and recording its ThreadId.
|
|
372
|
|
- atomically $ modifyTVar' worker_tids_var $ Set.insert worker_tid
|
|
373
|
|
- modifyIORef' all_results_vars_var (Seq.|> worker_res_var)
|
|
374
|
|
-
|
|
375
|
|
- mask \ restore -> do
|
|
376
|
|
- result <- restore (withScheduler $ Scheduler { spawnWorker, awaitWorker })
|
|
377
|
|
- `onException` cancel_workers
|
|
378
|
|
- restore wait_for_workers `onException` cancel_workers
|
|
379
|
|
- pure result
|
|
|
448
|
+ worker_work_env :: ConcurrentWorkerEnv
|
|
|
449
|
+ worker_work_env =
|
|
|
450
|
+ parent_work_env
|
|
|
451
|
+ { cwe_tmpfs = lcl_tmpfs
|
|
|
452
|
+ , cwe_logger = pushLogHook ( const ( parLogAction lq ) )
|
|
|
453
|
+ ( cwe_logger parent_work_env )
|
|
|
454
|
+ }
|
|
|
455
|
+
|
|
|
456
|
+ -- Record that a worker thread is done.
|
|
|
457
|
+ mark_worker_done :: ThreadId -> IO ()
|
|
|
458
|
+ mark_worker_done tid =
|
|
|
459
|
+ uninterruptibleMask_ do
|
|
|
460
|
+ -- Uninterruptible: the deletion below /must/ occur.
|
|
|
461
|
+ -- An uninterruptible mask is OK as we only ever block for (GAP) below.
|
|
|
462
|
+ mergeTmpFsInto lcl_tmpfs $ cwe_tmpfs parent_work_env
|
|
|
463
|
+ finishLogQueue lq
|
|
|
464
|
+ atomically do
|
|
|
465
|
+ tids <- readTVar worker_tids_var
|
|
|
466
|
+ check $ tid `Set.member` tids
|
|
|
467
|
+ -- Ensure we never end up with a dead ThreadId in 'worker_tids_var'
|
|
|
468
|
+ -- (if the worker thread finishes before the parent thread has
|
|
|
469
|
+ -- the time to add its ThreadId to 'worker_tids_var').
|
|
|
470
|
+
|
|
|
471
|
+ writeTVar worker_tids_var $ Set.delete tid tids
|
|
|
472
|
+
|
|
|
473
|
+ handle_worker_exception :: SomeException -> IO ()
|
|
|
474
|
+ handle_worker_exception e
|
|
|
475
|
+ -- Worker is being cancelled: not a failure to report.
|
|
|
476
|
+ | Just ThreadKilled <- fromException e
|
|
|
477
|
+ = pure ()
|
|
|
478
|
+ | otherwise
|
|
|
479
|
+ = record_failure e
|
|
|
480
|
+
|
|
|
481
|
+ run_worker :: ( forall b. IO b -> IO b ) -> IO ()
|
|
|
482
|
+ run_worker unmask = do
|
|
|
483
|
+ tid <- myThreadId
|
|
|
484
|
+ labelThread tid worker_label
|
|
|
485
|
+ ( unmask ( action worker_work_env )
|
|
|
486
|
+ `catch` handle_worker_exception )
|
|
|
487
|
+ `finally` mark_worker_done tid
|
|
|
488
|
+
|
|
|
489
|
+ worker_tid <-
|
|
|
490
|
+ forkIOWithUnmask run_worker
|
|
|
491
|
+ `onException` finishLogQueue lq
|
|
|
492
|
+ -- Very short (GAP) between forking the thread and recording its ThreadId.
|
|
|
493
|
+ atomically $ modifyTVar' worker_tids_var $ Set.insert worker_tid
|
|
|
494
|
+
|
|
|
495
|
+ ( `finally` release_queued_logs ) $
|
|
|
496
|
+ mask \ restore -> do
|
|
|
497
|
+ result <- restore ( withPool $ WorkerPool { spawnWorker, waitForWorkers } )
|
|
|
498
|
+ `onException` cancel_workers
|
|
|
499
|
+ restore wait_for_workers `onException` cancel_workers
|
|
|
500
|
+ pure result
|
|
380
|
501
|
|
|
381
|
502
|
--------------------------------------------------------------------------------
|
|
382
|
503
|
-- * Derived scheduling functionality
|
| ... |
... |
@@ -387,7 +508,9 @@ run_schedule worker_label conc parent_work_env withScheduler = |
|
387
|
508
|
-- Workers run to completion (no early abort); the first exception
|
|
388
|
509
|
-- (in input order) is rethrown at the end.
|
|
389
|
510
|
mapConcurrentWorkers
|
|
390
|
|
- :: String -- ^ thread label for workers
|
|
|
511
|
+ :: forall a b
|
|
|
512
|
+ . HasDebugCallStack
|
|
|
513
|
+ => String -- ^ thread label for workers
|
|
391
|
514
|
-> Concurrency
|
|
392
|
515
|
-> ConcurrentWorkerEnv
|
|
393
|
516
|
-> (ConcurrentWorkerEnv -> a -> IO b)
|
| ... |
... |
@@ -398,67 +521,111 @@ mapConcurrentWorkers |
|
398
|
521
|
-> [a]
|
|
399
|
522
|
-> IO [b]
|
|
400
|
523
|
mapConcurrentWorkers worker_label conc work_env f xs =
|
|
401
|
|
- run_schedule worker_label conc work_env \ scheduler -> do
|
|
402
|
|
- for_ xs \ x -> spawnWorker scheduler \ worker_env -> f worker_env x
|
|
403
|
|
- results <- replicateM (length xs) (awaitWorker scheduler)
|
|
404
|
|
- either throwIO pure (sequence results)
|
|
|
524
|
+ -- LogAsWeGo: workers are keyed by their position in the input list and
|
|
|
525
|
+ -- spawned in that same order, so their output can be printed as it is produced.
|
|
|
526
|
+ run_pool worker_label LogAsWeGo conc work_env \ pool -> do
|
|
|
527
|
+ results <- newMonotoneMap $ Map.empty @Int @( Either SomeException b )
|
|
|
528
|
+ for_ ( zip [ 0 .. ] xs ) \ ( i, x ) ->
|
|
|
529
|
+ spawnWorker pool i \ worker_env -> do
|
|
|
530
|
+ res <- try @SomeException $ f worker_env x
|
|
|
531
|
+ case res of
|
|
|
532
|
+ Left e
|
|
|
533
|
+ -- Take care to avoid swallowing async exceptions.
|
|
|
534
|
+ | Just _ <- fromException @SomeAsyncException e
|
|
|
535
|
+ -> throwIO e
|
|
|
536
|
+ _ -> insertMonotoneMap_new results i res
|
|
|
537
|
+ waitForWorkers pool
|
|
|
538
|
+ all_results <- freezeMonotoneMap results
|
|
|
539
|
+ massertPpr ( Map.size all_results == length xs ) $
|
|
|
540
|
+ text "mapConcurrentWorkers: missing results"
|
|
|
541
|
+ either throwIO pure $ sequence $ Map.elems all_results
|
|
|
542
|
+
|
|
|
543
|
+-- | How to expand a node in a graph for 'concurrentTraversal'.
|
|
|
544
|
+data NodeExpander k n v =
|
|
|
545
|
+ NodeExpander
|
|
|
546
|
+ { nodeKey :: n -> k
|
|
|
547
|
+ -- ^ The identity of a node.
|
|
|
548
|
+ , expandNode :: ConcurrentWorkerEnv -> n -> IO ( v, [n] )
|
|
|
549
|
+ -- ^ Expand a node into its result and the children to visit next.
|
|
|
550
|
+ --
|
|
|
551
|
+ -- To guarantee determinism, the children must be a pure function of the
|
|
|
552
|
+ -- input, and IO effects must not observably depend on the order in
|
|
|
553
|
+ -- which nodes are expanded.
|
|
|
554
|
+ --
|
|
|
555
|
+ -- NB: workers do not hold semaphore tokens by default; use
|
|
|
556
|
+ -- 'withConcurrency' to acquire one
|
|
|
557
|
+ }
|
|
405
|
558
|
|
|
406
|
|
--- | Depth-first traversal with on-the-fly expansion of nodes.
|
|
|
559
|
+-- | Deterministically traverse a graph whose nodes are discovered as they are
|
|
|
560
|
+-- expanded.
|
|
407
|
561
|
--
|
|
408
|
|
--- Each expansion step is handled by a worker thread under the given
|
|
409
|
|
--- concurrency control.
|
|
410
|
|
---
|
|
411
|
|
--- Deterministic: expansions are consumed in the order the nodes were
|
|
412
|
|
--- discovered, so the traversal is a function of the node graph alone.
|
|
413
|
|
---
|
|
414
|
|
--- Fails fast: the first worker exception cancels the outstanding workers and
|
|
415
|
|
--- is rethrown.
|
|
416
|
|
-concurrentTraversal_DF
|
|
417
|
|
- :: forall k n r
|
|
418
|
|
- . Ord k
|
|
|
562
|
+-- Fails fast: once a worker throws an exception, no further work is started,
|
|
|
563
|
+-- and the exception is rethrown once the outstanding workers finish.
|
|
|
564
|
+concurrentTraversal
|
|
|
565
|
+ :: forall k n v
|
|
|
566
|
+ . ( HasDebugCallStack, Ord k, Outputable k )
|
|
419
|
567
|
=> String -- ^ thread label for workers
|
|
420
|
568
|
-> Concurrency
|
|
421
|
569
|
-> ConcurrentWorkerEnv
|
|
422
|
|
- -> Map.Map k r
|
|
|
570
|
+ -> NodeExpander k n v
|
|
|
571
|
+ -> Map.Map k v
|
|
423
|
572
|
-- ^ results known ahead of time (no expansion needed)
|
|
424
|
573
|
-> [n]
|
|
425
|
574
|
-- ^ root nodes
|
|
426
|
|
- -> (n -> k)
|
|
427
|
|
- -- ^ node key from node
|
|
428
|
|
- -> (ConcurrentWorkerEnv -> n -> IO (r, [n]))
|
|
429
|
|
- -- ^ worker action: expand a node into its result and the children to visit next
|
|
430
|
|
- --
|
|
431
|
|
- -- NB: workers do not hold semaphore tokens by default; use
|
|
432
|
|
- -- 'withConcurrency' to acquire one
|
|
433
|
|
- -> IO (Map.Map k r)
|
|
434
|
|
-concurrentTraversal_DF worker_label conc work_env base_map roots key expand =
|
|
435
|
|
- run_schedule worker_label conc work_env \ scheduler -> do
|
|
|
575
|
+ -> IO ( Map.Map k v )
|
|
|
576
|
+concurrentTraversal
|
|
|
577
|
+ worker_label conc work_env
|
|
|
578
|
+ ( NodeExpander { nodeKey, expandNode } )
|
|
|
579
|
+ base_map roots
|
|
|
580
|
+ =
|
|
|
581
|
+ -- SortLogs: nodes are discovered in an order that depends on the schedule,
|
|
|
582
|
+ -- so the workers' logs must be ordered before being printed.
|
|
|
583
|
+ run_pool worker_label SortLogs conc work_env \ pool -> do
|
|
|
584
|
+
|
|
|
585
|
+ -- The keys whose expansion has been started.
|
|
|
586
|
+ claims <- newMonotoneSet $ Map.keysSet base_map
|
|
|
587
|
+ results <- newMonotoneMap base_map
|
|
|
588
|
+
|
|
436
|
589
|
let
|
|
437
|
|
- expand_node :: n -> ConcurrentWorkerEnv -> IO (k, (r, [n]))
|
|
438
|
|
- expand_node node worker_env = do
|
|
439
|
|
- res <- expand worker_env node
|
|
440
|
|
- pure (key node, res)
|
|
441
|
|
-
|
|
442
|
|
- go
|
|
443
|
|
- :: Map.Map k r -- expanded nodes and their results
|
|
444
|
|
- -> Set.Set k -- nodes currently being expanded
|
|
445
|
|
- -> [n] -- discovered nodes, to expand next
|
|
446
|
|
- -> IO (Map.Map k r)
|
|
447
|
|
- go !visited !pending (node : worklist)
|
|
448
|
|
- | k `Set.member` pending || k `Map.member` visited
|
|
449
|
|
- = go visited pending worklist
|
|
450
|
|
- | otherwise
|
|
451
|
|
- = do spawnWorker scheduler (expand_node node)
|
|
452
|
|
- go visited (Set.insert k pending) worklist
|
|
|
590
|
+ discover :: n -> IO ()
|
|
|
591
|
+ discover node =
|
|
|
592
|
+ -- Claim the work for this node to avoid any other worker duplicating it.
|
|
|
593
|
+ insertMonotoneSet claims key >>= \case
|
|
|
594
|
+ AlreadyPresent -> pure ()
|
|
|
595
|
+ Inserted ->
|
|
|
596
|
+ spawnWorker pool key \ worker_env -> do
|
|
|
597
|
+ ( result, children ) <- expandNode worker_env node
|
|
|
598
|
+ insertMonotoneMap_new results key result
|
|
|
599
|
+ for_ children discover
|
|
453
|
600
|
where
|
|
454
|
|
- k = key node
|
|
455
|
|
- go visited pending []
|
|
456
|
|
- | Set.null pending
|
|
457
|
|
- = pure visited
|
|
458
|
|
- | otherwise
|
|
459
|
|
- = awaitWorker scheduler >>= \case
|
|
460
|
|
- Left e -> throwIO e
|
|
461
|
|
- Right (k, (result, children)) ->
|
|
462
|
|
- go (Map.insert k result visited) (Set.delete k pending) children
|
|
463
|
|
-
|
|
464
|
|
- go base_map Set.empty roots |
|
|
601
|
+ key = nodeKey node
|
|
|
602
|
+
|
|
|
603
|
+ for_ roots discover
|
|
|
604
|
+ waitForWorkers pool
|
|
|
605
|
+ freezeMonotoneMap results
|
|
|
606
|
+
|
|
|
607
|
+{- Note [Deterministic concurrent workers]
|
|
|
608
|
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
609
|
+To provide deterministic output when doing graph traversal (as in downsweep)
|
|
|
610
|
+despite using concurrent workers, we ensure that nothing can observe the order
|
|
|
611
|
+in which workers do their work:
|
|
|
612
|
+
|
|
|
613
|
+ 1. Any chunk of work is performed at most once: every worker atomically claims
|
|
|
614
|
+ ownership of the work it is going to do before it starts that work.
|
|
|
615
|
+
|
|
|
616
|
+ 2. Workers report results by writing to a 'MonotoneMap', whose entries are
|
|
|
617
|
+ written at most once. Other outputs (such as logging output) is accumulated
|
|
|
618
|
+ in a deterministic order and reported at the end.
|
|
|
619
|
+
|
|
|
620
|
+This scheme allows us to retain maximum concurrency: it allows new edges to be
|
|
|
621
|
+discovered by any worker and immediately processed.
|
|
|
622
|
+
|
|
|
623
|
+For this scheme to provide deterministic output, we require that:
|
|
|
624
|
+
|
|
|
625
|
+ * The expansion of a node is a pure function of the node.
|
|
|
626
|
+ * The work itself should not observably depend on when it was run.
|
|
|
627
|
+
|
|
|
628
|
+Failure is not deterministic: which worker's exception is reported depends on
|
|
|
629
|
+the schedule. When deterministic error messages are desired, the workers should
|
|
|
630
|
+return an error value instead (as 'mapConcurrentWorkers' does).
|
|
|
631
|
+-} |