packages feed

pure-borrow-0.1.0.0: src/Control/Concurrent/Queue/ChaseLev.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoFieldSelectors #-}

module Control.Concurrent.Queue.ChaseLev (
  ChaseLevDeq,
  newDeq,
  pushFront,
  pushFronts,
  tryPopBack,
  stealHalf,
  StealResult (..),
  tryPopFront,
  estimateSize,
  capacity,
  close,
  isClosed,
) where

import Control.Monad (forM_, unless, (<$!>))
import Data.Atomics (loadLoadBarrier, storeLoadBarrier, writeBarrier)
import Data.Bits ((.&.))
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NE
import Data.Primitive.Array (MutableArray)
import Data.Primitive.Array qualified as Array
import Data.Primitive.PrimVar (PrimVar, casInt, newPrimVar, readPrimVar, writePrimVar)
import Data.Vector qualified as V
import GHC.Exts (RealWorld)
import Math.NumberTheory.Logarithms (intLog2')

data ChaseLevDeq a = CL
  { top :: {-# UNPACK #-} !(PrimVar RealWorld Int)
  , activeArray :: !(IORef (MutableArray RealWorld a))
  , bottom :: {-# UNPACK #-} !(PrimVar RealWorld Int)
  , closed :: {-# UNPACK #-} !(IORef Bool)
  , estimatedSize :: {-# UNPACK #-} !(PrimVar RealWorld Int)
  }

data Stat = Stat {top, bottom :: !Int}

newDeq :: IO (ChaseLevDeq a)
newDeq = do
  !top <- newPrimVar 0
  !activeArray <- newIORef =<< Array.newArray 32 undefined
  !closed <- newIORef False
  !bottom <- newPrimVar 0
  !estimatedSize <- newPrimVar 0
  pure CL {..}

getStat :: ChaseLevDeq a -> IO Stat
{-# INLINE getStat #-}
getStat dq = Stat <$> readPrimVar dq.top <*> readPrimVar dq.bottom

{-# INLINE capacity #-}
capacity :: ChaseLevDeq a -> IO Int
capacity = fmap Array.sizeofMutableArray . readIORef . (.activeArray)

occupancy :: Stat -> Int
{-# INLINE occupancy #-}
occupancy = (-) <$> (.bottom) <*> (.top)

pushFront :: ChaseLevDeq a -> a -> IO ()
pushFront q = pushFronts q . (: [])

-- | Puts the last element on the front.
pushFronts :: ChaseLevDeq a -> [a] -> IO ()
pushFronts _ [] = pure ()
pushFronts q !a = do
  let !n = length a
  closed <- readIORef q.closed
  unless closed do
    !capa <- capacity q
    !stat <- getStat q
    let !size = occupancy stat
    arr <-
      if size + n >= capa - 1
        then do
          let !start = stat.top .&. (capa - 1)
              !end = stat.bottom .&. (capa - 1)
              !newCapa = (2 * capa) `max` (2 ^ (intLog2' (size + n) + 1))
          oldArr <- readIORef q.activeArray
          newArr <- Array.newArray newCapa undefined
          if end >= start
            then do
              Array.copyMutableArray newArr start oldArr start size
            else do
              let !lhSize = capa - start
                  !rhSize = size - lhSize
              Array.copyMutableArray newArr start oldArr start lhSize
              Array.copyMutableArray newArr (start + lhSize) oldArr 0 rhSize
          writeIORef q.activeArray newArr
          pure newArr
        else readIORef q.activeArray

    let !curCapa = Array.sizeofMutableArray arr
    forM_ (zip [0 ..] a) $ \(!i, !x) ->
      Array.writeArray arr ((stat.bottom + i) .&. (curCapa - 1)) x
    writeBarrier
    writePrimVar q.bottom $! stat.bottom + n
    writePrimVar q.estimatedSize $! size + n

{- |
  * @Nothing@         — closed (end-of-stream)
  * @Just Nothing@    — open and empty (would block)
  * @Just (Just a)@   — got an element
-}
tryPopFront :: ChaseLevDeq a -> IO (Maybe (Maybe a))
tryPopFront q = do
  !b <- subtract 1 <$> readPrimVar q.bottom
  writePrimVar q.bottom b
  storeLoadBarrier
  !t <- readPrimVar q.top

  !arr <- readIORef q.activeArray
  let !capa = Array.sizeofMutableArray arr

  -- NOTE: Do not force, otherwise undefined will hit
  task <- Array.readArray arr (b .&. (capa - 1))
  if
    | b == t -> do
        -- last one element - might be stolen!
        let !t' = t + 1
        !old <- casInt q.top t t'
        let !success = old == t
        writePrimVar q.bottom t'
        writePrimVar q.estimatedSize 0
        if success
          then pure $ Just $ Just task
          else do
            closed <- readIORef q.closed
            if closed
              then pure Nothing
              else pure $ Just Nothing
    | b > t -> do
        writePrimVar q.estimatedSize $! b - t
        pure $ Just $ Just task
    | otherwise -> do
        writePrimVar q.bottom t
        writePrimVar q.estimatedSize 0
        pure $ Just Nothing

data StealResult a = Found a | Empty | Race
  deriving (Show, Eq, Ord)

{- |
  * @Nothing@         — closed (end-of-stream)
  * @Just Nothing@    — open and empty (would block)
  * @Just (Just a)@   — got an element
-}
tryPopBack :: ChaseLevDeq a -> IO (Maybe (StealResult a))
tryPopBack q = do
  !t <- readPrimVar q.top
  loadLoadBarrier
  !b <- readPrimVar q.bottom
  if t >= b
    then do
      closed <- readIORef q.closed
      if closed
        then pure Nothing
        else pure $ Just Empty
    else do
      arr <- readIORef q.activeArray
      let !capa = Array.sizeofMutableArray arr
      -- NOTE: we must not force, otherwise undefined will hit
      task <- Array.readArray arr (t .&. (capa - 1))
      let !t' = t + 1
      !old <- casInt q.top t t'
      let !success = old == t
      if success
        then do
          writePrimVar q.estimatedSize $! b - t - 1
          pure $! Just $ Found task
        else pure $ Just Race

-- | back-element first
stealHalf :: ChaseLevDeq a -> IO (Maybe (StealResult (NonEmpty a)))
stealHalf q = do
  !t <- readPrimVar q.top
  loadLoadBarrier
  !b <- readPrimVar q.bottom
  if t >= b
    then do
      closed <- readIORef q.closed
      if closed
        then pure Nothing
        else pure $ Just Empty
    else do
      let !avail = b - t
          !count = if avail == 1 then 1 else avail `quot` 2
          !t' = t + count
      arr <- readIORef q.activeArray
      let !capa = Array.sizeofMutableArray arr
      -- NOTE: we must not force, otherwise undefined will hit
      tasks <- V.generateM count \i ->
        Array.readArray arr $ (t + i) .&. (capa - 1)
      !old <- casInt q.top t t'
      let !success = old == t
      if success
        then do
          writePrimVar q.estimatedSize $! avail - count
          pure $! Just $ Found $ NE.fromList $ V.toList tasks
        else pure $ Just Race

estimateSize :: ChaseLevDeq a -> IO Int
{-# INLINE estimateSize #-}
estimateSize q = readPrimVar q.estimatedSize

close :: ChaseLevDeq a -> IO ()
{-# INLINE close #-}
close q = writeIORef q.closed True

isClosed :: ChaseLevDeq a -> IO Bool
isClosed q = do
  closed <- readIORef q.closed
  if closed
    then do
      !size <- occupancy <$!> getStat q
      pure $! size == 0
    else pure False