packages feed

stm-ringbuffer 0.1.0.0 → 0.1.0.1

raw patch · 6 files changed

+119/−24 lines, 6 filesdep ~QuickCheckdep ~randomPVP: minor bump suggested

API additions: PVP suggests at least a minor version bump

Dependency ranges changed: QuickCheck, random

API changes (from Hackage documentation)

+ Control.Concurrent.STM.TRingBuffer: ringPushBack :: TRingBuffer a -> a -> STM ()
+ Control.Concurrent.STM.TRingBuffer: ringPushFront :: TRingBuffer a -> a -> STM ()

Files

CHANGELOG.md view
@@ -1,4 +1,8 @@-# Revision history for stm-queue+# Revision history for stm-ringbuffer++## 0.1.0.1 -- Unreleased++* Add `ringPushFront` and `ringPushBack`  ## 0.1.0.0 -- 2026-04-18 
README.md view
@@ -3,7 +3,7 @@ ## About This is an implementation of a ring buffer for STM. A wrapper exposing a TBQueue-like interface is provided, offering improved performance compared to the current implementation of TBQueue in the STM package. -This work is similar to https://github.com/haskell/stm/pull/70 . However, this implementation is hopefully correct and supports ringbuffers with size == 0.+This work is similar to https://github.com/haskell/stm/pull/70 , but fixes the implementation of flushing and supports ringbuffers with size == 0.  ## Usage Mixins can be used to quickly replace the standard TBQueue implementation with the one in this library. Add the following fields to your cabal file:@@ -21,9 +21,10 @@  It is recommended to add the following to your cabal.project.local file: ```-tests: True-benchmarks: True-semaphore: True+tests: true+benchmarks: true+semaphore: true+jobs: $ncpus ```  ## Benchmark results@@ -32,17 +33,25 @@ All   concurrent spsc     TBQueue:    OK-      119  ms ± 6.8 ms+      121  ms ± 7.4 ms     RB.TBQueue: OK-      109  ms ± 7.3 ms+      110  ms ± 7.0 ms   concurrent mpmc     TBQueue:    OK-      193  ms ±  14 ms+      194  ms ±  17 ms     RB.TBQueue: OK-      136  ms ± 7.0 ms+      138  ms ± 7.0 ms   burst     TBQueue:    OK-      114  ms ± 7.2 ms+      116  ms ± 7.1 ms     RB.TBQueue: OK-      108  ms ± 6.8 ms+      109  ms ± 7.6 ms+  write/flush+    TBQueue:    OK+      279  μs ±  13 μs+    RB.TBQueue: OK+      263  μs ±  14 μs++(Above results for M2 Macbook Pro with 16GB RAM and macOS 15.7.2) ```+
benchmark/StmBench.hs view
@@ -7,7 +7,7 @@ module StmBench (stmBench) where  import Control.Concurrent.STM-    ( atomically, newTBQueueIO, readTBQueue, writeTBQueue, TBQueue )+    ( atomically, flushTBQueue, newTBQueueIO, readTBQueue, writeTBQueue, TBQueue ) import Control.Concurrent.STM.TRingBuffer.TBQueue qualified as RB import Control.Concurrent.Async ( async, wait ) import Control.Monad ( replicateM, replicateM_ )@@ -31,24 +31,32 @@         [ bench "TBQueue" $ whnfAppIO (burst @TBQueue 1000) n         , bench "RB.TBQueue" $ whnfAppIO (burst @RB.TBQueue 1000) n         ]+    , bgroup "write/flush"+        [ bench "TBQueue" $ whnfAppIO (burst @TBQueue 1000) flushSz+        , bench "RB.TBQueue" $ whnfAppIO (burst @RB.TBQueue 1000) flushSz+        ]     ]   where     n = 2000000+    flushSz = 4096  class Channel c where     newc :: IO (c a)     readc :: c a -> IO a     writec :: c a -> a -> IO ()+    flushc :: c a -> IO [a]  instance Channel TBQueue where     newc = newTBQueueIO 4096     readc c = atomically $ readTBQueue c     writec c x = atomically $ writeTBQueue c x+    flushc c = atomically $ flushTBQueue c  instance Channel RB.TBQueue where     newc = RB.newTBQueueIO 4096     readc c = atomically $ RB.readTBQueue c     writec c x = atomically $ RB.writeTBQueue c x+    flushc c = atomically $ RB.flushTBQueue c  -- concurrent writing and reading with single producer, single consumer concurrentSpsc :: forall c. (Channel c) => Int -> IO ()@@ -75,3 +83,12 @@     replicateM_ k $ do         replicateM_ (n `div` k) $ writec c 1         replicateM_ (n `div` k) $ readc c++-- sequential push and flush+sequentialFlush :: forall c. (Channel c) => Int -> Int -> IO ()+sequentialFlush k n = do+   c :: c Int <- newc+   replicateM_ k $ do+     replicateM_ n $ writec c 1+     flushc c+
src/Control/Concurrent/STM/TRingBuffer.hs view
@@ -1,3 +1,7 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE InstanceSigs #-}+ -- | -- Module      :  Control.Concurrent.STM.TRingBuffer -- Copyright   :  (c) Greg Baimetov 2026@@ -9,10 +13,6 @@ -- 'TRingBuffer' is an STM ring buffer. The implementation allows for -- simultaneous operations on the front and back of the buffer. -{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE InstanceSigs #-}- module Control.Concurrent.STM.TRingBuffer   ( TRingBuffer,     new,@@ -23,6 +23,8 @@     size,     pushFront,     pushBack,+    ringPushFront,+    ringPushBack,     popFront,     popBack,     peekFront,@@ -43,6 +45,7 @@     writeTVar,   ) import Control.Concurrent.STM.TArray ()+import Control.Monad (void, when) import Data.Array.Base   ( MArray (newArray, unsafeRead, unsafeWrite),   )@@ -100,7 +103,10 @@       b <- readTVar back       pure $ (b + 1 - f) `mod` size --- | Is the ring buffer empty?+-- | Is the ring buffer empty? O(1). May be more efficien than+-- @+-- \\b -> (==0) \<\$\> length b+-- @ isEmpty :: TRingBuffer a -> STM Bool isEmpty MkTRingBuffer {arr, front, size} = testPure size True $ do   ix <- readTVar front@@ -109,7 +115,10 @@     Nothing -> True     Just _ -> False --- | Is the ring buffer full?+-- | Is the ring buffer full? O(1). May be more efficient than+-- @+-- \\b -> (== size b) \<\$\> length b+-- @ isFull :: TRingBuffer a -> STM Bool isFull MkTRingBuffer {arr, front, size} = testPure size True $ do   ix <- decr size <$> readTVar front@@ -139,6 +148,20 @@     Nothing -> do       unsafeWrite arr ix (Just e)       writeTVar back ix++-- | Push an element to the front of the buffer, overwriting the backmost element if the buffer is full. O(1)+ringPushFront :: TRingBuffer a -> a -> STM ()+ringPushFront ring item = do+    full <- isFull ring+    when full . void $ popBack ring+    pushFront ring item++-- | Push an element to the back of the buffer, overwriting the frontmost element if the buffer is full. O(1)+ringPushBack :: TRingBuffer a -> a -> STM ()+ringPushBack ring item = do+    full <- isFull ring+    when full . void $ popFront ring+    pushBack ring item  -- | Pop an element from the front of the buffer. Retry if the buffer is empty. O(1) popFront :: TRingBuffer b -> STM b
stm-ringbuffer.cabal view
@@ -1,14 +1,16 @@ cabal-version:   3.4 name:            stm-ringbuffer category:        Concurrency-version:         0.1.0.0+version:         0.1.0.1 license:         BSD-3-Clause author:          Greg Baimetov maintainer:      Greg Baimetov synopsis:  Ring buffer implementation in STM description:-  This package provides a ring buffer in STM and a wrapper which mimics the interface to TBQueue.+  This package provides a ring buffer in STM and a wrapper which mimics the interface to TBQueue (from STM).+  Compared to TBQueue, this package offers better performance as a bounded queue, especially when used with+  multiple producers and consumers, as well as push, pop, peek and flush operations on both ends.  copyright:       (c) Greg Baimetov 2026 build-type:      Simple@@ -28,7 +30,7 @@  source-repository head   type:git-  location: github.com/Greg-Bm/stm-ringbuffer.git+  location: https://github.com/Greg-Bm/stm-ringbuffer.git  library   hs-source-dirs:     src@@ -61,8 +63,8 @@     , base     , generic-random  >=1.5.0   && <1.6     , hspec           >=2.11.17 && <2.12-    , QuickCheck      >=2.16.0  && <2.17-    , random          >=1.3.1   && <1.4+    , QuickCheck      >=2.15.0  && <2.19+    , random          >=1.2     && <1.4     , stm     , stm-ringbuffer 
test/TestTRingBuffer.hs view
@@ -19,7 +19,9 @@       flushFront,       popBack,       peekBack,-      flushBack )+      flushBack,+      ringPushFront,+      ringPushBack ) import Control.Monad     ( Monad((>>), (>>=)), replicateM, replicateM_, forM_ ) import Data.Functor ( (<&>) )@@ -191,3 +193,41 @@       forM_ testSeq (atomically . pushBack buf)       atomically (flushBack buf)       atomically (isEmpty buf) `shouldReturn` True++  describe "ringPushBack" $ do+    it "behaves like pushBack when buffer is not full" $ do+      buf <- newIO 10+      forM_ [1 .. 5] (atomically . ringPushBack buf)+      atomically (flushFront buf) `shouldReturn` [1 .. 5]+    it "keeps the length at size when buffer is full" $ do+      buf <- newIO 3+      forM_ [1 .. 10] (atomically . ringPushBack buf)+      atomically (length buf) `shouldReturn` 3+    it "overwrites the frontmost element when buffer is full" $ do+      buf <- newIO 3+      forM_ [1 .. 10] (atomically . ringPushBack buf)+      atomically (flushFront buf) `shouldReturn` [8, 9, 10]+    it "does not retry when buffer is full" $ do+      buf <- newIO 3+      forM_ [1 .. 3] (atomically . pushBack buf)+      atomically ((ringPushBack buf 4 >> pure True) `orElse` pure False)+        `shouldReturn` True++  describe "ringPushFront" $ do+    it "behaves like pushFront when buffer is not full" $ do+      buf <- newIO 10+      forM_ [1 .. 5] (atomically . ringPushFront buf)+      atomically (flushBack buf) `shouldReturn` [1 .. 5]+    it "keeps the length at size when buffer is full" $ do+      buf <- newIO 3+      forM_ [1 .. 10] (atomically . ringPushFront buf)+      atomically (length buf) `shouldReturn` 3+    it "overwrites the backmost element when buffer is full" $ do+      buf <- newIO 3+      forM_ [1 .. 10] (atomically . ringPushFront buf)+      atomically (flushBack buf) `shouldReturn` [8, 9, 10]+    it "does not retry when buffer is full" $ do+      buf <- newIO 3+      forM_ [1 .. 3] (atomically . pushFront buf)+      atomically ((ringPushFront buf 4 >> pure True) `orElse` pure False)+        `shouldReturn` True