packages feed

http3-0.1.1: Network/QPACK/Table/Dynamic.hs

{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}

module Network.QPACK.Table.Dynamic (
    -- * Dynamic table
    DynamicTable,
    newDynamicTableForEncoding,
    newDynamicTableForDecoding,

    -- * Capacity
    isTableReady,
    getTableCapacity,
    setTableCapacity,
    getMaxNumOfEntries,

    -- * Entry
    insertEntryToDecoder,
    insertEntryToEncoder,
    toDynamicEntry,

    -- * Section
    Section (..),
    insertSection,
    getAndDelSection,
    increaseReference,
    decreaseReference,

    -- * Streams
    getMaxBlockedStreams,
    setMaxBlockedStreams,
    tryIncreaseStreams,
    decreaseStreams,

    -- * Blocked streams
    insertBlockedStreamE,
    deleteBlockedStreamE,
    checkBlockedStreams,

    -- * Required insert count
    getRequiredInsertCount,
    clearRequiredInsertCount,
    checkRequiredInsertCount,
    checkRequiredInsertCountNB,
    updateRequiredInsertCount,

    -- * Known received count
    incrementKnownReceivedCount,
    updateKnownReceivedCount,
    wouldSectionBeBlocked,
    wouldInstructionBeBlocked,
    setInsersionPointToKnownReceivedCount,

    -- * Points
    getBasePoint,
    setBasePointToInsersionPoint,
    getInsertionPoint,
    getInsertionPointSTM,

    -- * Draining
    isDraining,
    adjustDrainingPoint,
    checkTailDuplication,
    tailDuplication,
    duplicate,
    tryDrop,

    -- * Dropping
    canInsertEntry,

    -- * Accessing
    getLruCache,
    getRevIndex,
    getHuffmanDecoder,
    sendIns,

    -- * Max header size
    getMaxHeaderSize,
    setMaxHeaderSize,

    -- * Debug
    qpackDebug,
    getDebugQPACK,
    setDebugQPACK,
    printReferences,
    checkHIndex,
    checkAbsoluteIndex,

    -- * QIF
    getImmediateAck,
    setImmediateAck,
) where

import Control.Concurrent
import Control.Concurrent.STM
import Data.Array.Base (unsafeRead, unsafeWrite)
import Data.Array.IO (IOArray, newArray)
import Data.IORef
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IntMap
import Data.Set (Set)
import qualified Data.Set as Set -- Set.size is O(1), IntSet.size is O(n)
import Imports
import Network.Control
import Network.HPACK.Internal (
    Entry,
    HuffmanDecoder,
    Index,
    Size,
    decodeH,
    dummyEntry,
    entryFieldValue,
    entryHeaderName,
    entrySize,
    maxNumbers,
 )
import System.IO.Unsafe (unsafePerformIO)

import Network.QPACK.Table.RevIndex
import Network.QPACK.Types
import Network.QUIC (StreamId)

----------------------------------------------------------------

type Table = TArray Index Entry
data Section = Section RequiredInsertCount [AbsoluteIndex]
data Reference = Reference Int Int deriving (Show)

{- FOURMOLU_DISABLE -}
data CodeInfo
    = EncodeInfo
        { revIndex            :: RevIndex -- Reverse index
        , requiredInsertCount :: IORef RequiredInsertCount
        , droppingPoint       :: IORef AbsoluteIndex
        , drainingPoint       :: IORef AbsoluteIndex
        , knownReceivedCount  :: TVar Int
        , referenceCounters   :: IORef (IOArray Index Reference)
        , sections            :: IORef (IntMap Section)
        , lruCache            :: LRUCacheRef (FieldName, FieldValue) ()
        , immediateAck        :: IORef Bool -- for QIF
        , blockedStreamsE     :: IORef (Set Int)
        }
    | DecodeInfo
        { huffmanDecoder  :: HuffmanDecoder  -- only for encoder instruction handler
        , blockedStreamsD :: IORef Int
        }
{- FOURMOLU_ENABLE -}

-- | Dynamic table for QPACK.
{- FOURMOLU_DISABLE -}
data DynamicTable = DynamicTable
    { codeInfo          :: CodeInfo
    , insertionPoint    :: TVar InsertionPoint
    , maxNumOfEntries   :: TVar Int
    , circularTable     :: TVar Table
    , basePoint         :: IORef BasePoint
    , debugQPACK        :: IORef Bool
    , capaReady         :: IORef Bool
    , tableSize         :: TVar Size
    , maxTableSize      :: IORef Size
    , sendIns           :: ByteString -> IO ()
    , maxHeaderSize     :: IORef Int
    , maxBlockedStreams :: IORef Int
    }
{- FOURMOLU_ENABLE -}

----------------------------------------------------------------

{- FOURMOLU_DISABLE -}
-- | Creating 'DynamicTable' for encoding.
newDynamicTableForEncoding
    :: (ByteString -> IO ())
    -> IO DynamicTable
newDynamicTableForEncoding sendEI = do
    arr <- newArray (0, 0) $ Reference 0 0
    info <- do
        revIndex            <- newRevIndex
        requiredInsertCount <- newIORef 0
        droppingPoint       <- newIORef 0
        drainingPoint       <- newIORef 0
        knownReceivedCount  <- newTVarIO 0
        referenceCounters   <- newIORef arr
        sections            <- newIORef IntMap.empty
        lruCache            <- newLRUCacheRef 0
        immediateAck        <- newIORef False
        blockedStreamsE     <- newIORef Set.empty
        return EncodeInfo{..}
    newDynamicTable info sendEI
{- FOURMOLU_ENABLE -}

-- | Creating 'DynamicTable' for decoding.
newDynamicTableForDecoding
    :: Size
    -- ^ The size of temporary buffer for Huffman decoding
    -> (ByteString -> IO ())
    -> IO DynamicTable
newDynamicTableForDecoding huftmpsiz sendDI = do
    gcbuf <- mallocPlainForeignPtrBytes huftmpsiz
    let huffmanDecoder = decodeH gcbuf huftmpsiz
    blockedStreamsD <- newIORef 0
    newDynamicTable DecodeInfo{..} sendDI

newDynamicTable :: CodeInfo -> (ByteString -> IO ()) -> IO DynamicTable
newDynamicTable info send = do
    tbl <- atomically $ newArray (0, 0) dummyEntry
    let codeInfo = info
    insertionPoint <- newTVarIO 0
    maxNumOfEntries <- newTVarIO 0
    circularTable <- newTVarIO tbl
    basePoint <- newIORef 0
    debugQPACK <- newIORef False
    capaReady <- newIORef False
    tableSize <- newTVarIO 0
    maxTableSize <- newIORef 0
    let sendIns = send
    maxHeaderSize <- newIORef maxBound
    maxBlockedStreams <- newIORef 0
    return DynamicTable{..}

----------------------------------------------------------------

isTableReady :: DynamicTable -> IO Bool
isTableReady DynamicTable{..} = readIORef capaReady

getTableCapacity :: DynamicTable -> IO Int
getTableCapacity DynamicTable{..} = readTVarIO tableSize

setTableCapacity :: DynamicTable -> Int -> IO ()
setTableCapacity dyntbl@DynamicTable{..} maxsiz = do
    qpackDebug dyntbl $ putStrLn $ "setTableCapacity " ++ show maxsiz
    when (maxN >= 1) $ do
        writeIORef maxTableSize maxsiz
        tbl <- atomically $ newArray (0, end) dummyEntry
        atomically $ do
            writeTVar maxNumOfEntries maxN
            writeTVar circularTable tbl
        case codeInfo of
            EncodeInfo{..} -> do
                arr <- newArray (0, end) $ Reference 0 0
                writeIORef referenceCounters arr
                setLRUCapacity lruCache (maxN * 4)
            _ -> return ()
        writeIORef capaReady True
  where
    maxN = maxNumbers maxsiz
    end = maxN - 1

getMaxNumOfEntries :: DynamicTable -> IO Int
getMaxNumOfEntries DynamicTable{..} = readTVarIO maxNumOfEntries

----------------------------------------------------------------

insertEntryToEncoder :: Entry -> DynamicTable -> IO AbsoluteIndex
insertEntryToEncoder ent dyntbl@DynamicTable{..} = do
    InsertionPoint insp <- atomically $ do
        x <- readTVar insertionPoint
        writeTVar insertionPoint (x + 1)
        return x
    maxN <- readTVarIO maxNumOfEntries
    let i = insp `mod` maxN
    table <- readTVarIO circularTable
    atomically $ unsafeWrite table i ent
    let revtbl = getRevIndex dyntbl
    let ai = AbsoluteIndex insp
    insertRevIndex ent ai revtbl
    atomically $ modifyTVar' tableSize (+ entrySize ent)
    dropIfNecessary dyntbl
    resetReference dyntbl ai
    return ai

insertEntryToDecoder :: Entry -> DynamicTable -> STM AbsoluteIndex
insertEntryToDecoder ent DynamicTable{..} = do
    x@(InsertionPoint insp) <- readTVar insertionPoint
    writeTVar insertionPoint (x + 1)
    maxN <- readTVar maxNumOfEntries
    let i = insp `mod` maxN
    table <- readTVar circularTable
    unsafeWrite table i ent
    modifyTVar' tableSize (+ entrySize ent)
    return $ AbsoluteIndex insp

toDynamicEntry :: DynamicTable -> AbsoluteIndex -> STM Entry
toDynamicEntry DynamicTable{..} (AbsoluteIndex idx) = do
    maxN <- readTVar maxNumOfEntries
    let i = idx `mod` maxN
    table <- readTVar circularTable
    unsafeRead table i

----------------------------------------------------------------

insertSection :: DynamicTable -> StreamId -> Section -> IO ()
insertSection DynamicTable{..} sid section = atomicModifyIORef' sections ins
  where
    ins m =
        let m' = IntMap.insert sid section m
         in (m', ())
    EncodeInfo{..} = codeInfo

getAndDelSection :: DynamicTable -> StreamId -> IO (Maybe Section)
getAndDelSection DynamicTable{..} sid = atomicModifyIORef' sections getAndDel
  where
    getAndDel m =
        let (msec, m') = IntMap.updateLookupWithKey f sid m
         in (m', msec)
    f _ _ = Nothing -- delete the entry if found
    EncodeInfo{..} = codeInfo

increaseReference :: DynamicTable -> AbsoluteIndex -> IO ()
increaseReference = modifyReference $ \(Reference c t) -> Reference (c + 1) (t + 1)

decreaseReference :: DynamicTable -> AbsoluteIndex -> IO ()
decreaseReference = modifyReference $ \(Reference c t) -> Reference (c - 1) t

modifyReference
    :: (Reference -> Reference) -> DynamicTable -> AbsoluteIndex -> IO ()
modifyReference func DynamicTable{..} (AbsoluteIndex idx) = do
    maxN <- readTVarIO maxNumOfEntries
    let i = idx `mod` maxN
    arr <- readIORef referenceCounters
    -- modifyArray' is not provided by GHC 9.4 or earlier, sigh.
    x <- unsafeRead arr i
    let x' = func x
    unsafeWrite arr i x'
  where
    EncodeInfo{..} = codeInfo

resetReference :: DynamicTable -> AbsoluteIndex -> IO ()
resetReference DynamicTable{..} (AbsoluteIndex idx) = do
    maxN <- readTVarIO maxNumOfEntries
    let i = idx `mod` maxN
    arr <- readIORef referenceCounters
    unsafeWrite arr i $ Reference 0 0
  where
    EncodeInfo{..} = codeInfo

----------------------------------------------------------------

getMaxBlockedStreams :: DynamicTable -> IO Int
getMaxBlockedStreams DynamicTable{..} = readIORef maxBlockedStreams

setMaxBlockedStreams :: DynamicTable -> Int -> IO ()
setMaxBlockedStreams DynamicTable{..} n = writeIORef maxBlockedStreams n

-- Decoder

tryIncreaseStreams :: DynamicTable -> IO Bool
tryIncreaseStreams DynamicTable{..} = do
    lim <- readIORef maxBlockedStreams
    curr <- atomicModifyIORef' blockedStreamsD (\n -> (n + 1, n + 1))
    return (curr <= lim)
  where
    DecodeInfo{..} = codeInfo

decreaseStreams :: DynamicTable -> IO ()
decreaseStreams DynamicTable{..} = atomicModifyIORef' blockedStreamsD (\n -> (n - 1, ()))
  where
    DecodeInfo{..} = codeInfo

----------------------------------------------------------------

getBlockedStreamsE :: DynamicTable -> IO Int
getBlockedStreamsE DynamicTable{..} =
    Set.size <$> readIORef blockedStreamsE
  where
    EncodeInfo{..} = codeInfo

insertBlockedStreamE :: DynamicTable -> StreamId -> IO ()
insertBlockedStreamE DynamicTable{..} sid =
    modifyIORef' blockedStreamsE (Set.insert sid)
  where
    EncodeInfo{..} = codeInfo

deleteBlockedStreamE :: DynamicTable -> StreamId -> IO ()
deleteBlockedStreamE DynamicTable{..} sid =
    modifyIORef' blockedStreamsE (Set.delete sid)
  where
    EncodeInfo{..} = codeInfo

checkBlockedStreams :: DynamicTable -> IO Bool
checkBlockedStreams dyntbl = do
    maxBlocked <- getMaxBlockedStreams dyntbl
    blocked <- getBlockedStreamsE dyntbl
    -- The next one would be blocked, so <, not <=
    return $ blocked < maxBlocked

----------------------------------------------------------------

getRequiredInsertCount :: DynamicTable -> IO RequiredInsertCount
getRequiredInsertCount DynamicTable{..} = readIORef requiredInsertCount
  where
    EncodeInfo{..} = codeInfo

clearRequiredInsertCount :: DynamicTable -> IO ()
clearRequiredInsertCount DynamicTable{..} = writeIORef requiredInsertCount 0
  where
    EncodeInfo{..} = codeInfo

checkRequiredInsertCount :: DynamicTable -> RequiredInsertCount -> IO ()
checkRequiredInsertCount DynamicTable{..} (RequiredInsertCount reqip) = atomically $ do
    InsertionPoint ip <- readTVar insertionPoint
    -- RequiredInsertCount is index + 1
    -- InsertionPoin is index + 1
    -- So, equal is necessary.
    check (reqip <= ip)

checkRequiredInsertCountNB :: DynamicTable -> RequiredInsertCount -> IO Bool
checkRequiredInsertCountNB DynamicTable{..} (RequiredInsertCount reqip) = atomically $ do
    InsertionPoint ip <- readTVar insertionPoint
    return (reqip <= ip)

absoluteIndexToRequiredInsertCount :: AbsoluteIndex -> RequiredInsertCount
absoluteIndexToRequiredInsertCount (AbsoluteIndex idx) =
    RequiredInsertCount (idx + 1)

updateRequiredInsertCount :: DynamicTable -> AbsoluteIndex -> IO ()
updateRequiredInsertCount DynamicTable{..} aidx = do
    let newric = absoluteIndexToRequiredInsertCount aidx
    oldric <- readIORef requiredInsertCount
    when (newric > oldric) $ writeIORef requiredInsertCount newric
  where
    EncodeInfo{..} = codeInfo

----------------------------------------------------------------

incrementKnownReceivedCount :: DynamicTable -> Int -> IO ()
incrementKnownReceivedCount DynamicTable{..} n =
    atomically $ modifyTVar' knownReceivedCount (+ n)
  where
    EncodeInfo{..} = codeInfo

updateKnownReceivedCount :: DynamicTable -> RequiredInsertCount -> IO ()
updateKnownReceivedCount DynamicTable{..} (RequiredInsertCount reqInsCnt) =
    atomically $ modifyTVar' knownReceivedCount $ \krc -> max reqInsCnt krc
  where
    EncodeInfo{..} = codeInfo

wouldSectionBeBlocked :: DynamicTable -> RequiredInsertCount -> IO Bool
wouldSectionBeBlocked DynamicTable{..} (RequiredInsertCount reqip) = atomically $ do
    krc <- readTVar knownReceivedCount
    return (reqip > krc)
  where
    EncodeInfo{..} = codeInfo

wouldInstructionBeBlocked :: DynamicTable -> AbsoluteIndex -> IO Bool
wouldInstructionBeBlocked DynamicTable{..} (AbsoluteIndex ai) = atomically $ do
    krc <- readTVar knownReceivedCount
    return (ai > krc)
  where
    EncodeInfo{..} = codeInfo

setInsersionPointToKnownReceivedCount :: DynamicTable -> IO ()
setInsersionPointToKnownReceivedCount dyntbl@DynamicTable{..} = do
    InsertionPoint ai <- getInsertionPoint dyntbl
    atomically $ writeTVar knownReceivedCount ai
  where
    EncodeInfo{..} = codeInfo

----------------------------------------------------------------

getBasePoint :: DynamicTable -> IO BasePoint
getBasePoint DynamicTable{..} = readIORef basePoint

setBasePointToInsersionPoint :: DynamicTable -> IO ()
setBasePointToInsersionPoint DynamicTable{..} = do
    InsertionPoint ip <- readTVarIO insertionPoint
    writeIORef basePoint $ BasePoint ip

getInsertionPoint :: DynamicTable -> IO InsertionPoint
getInsertionPoint DynamicTable{..} = readTVarIO insertionPoint

getInsertionPointSTM :: DynamicTable -> STM InsertionPoint
getInsertionPointSTM DynamicTable{..} = readTVar insertionPoint

----------------------------------------------------------------

isDraining :: DynamicTable -> AbsoluteIndex -> IO Bool
isDraining DynamicTable{..} ai = do
    di <- readIORef drainingPoint
    return (ai <= di)
  where
    EncodeInfo{..} = codeInfo

adjustDrainingPoint :: DynamicTable -> IO ()
adjustDrainingPoint DynamicTable{..} = do
    InsertionPoint beg <- readTVarIO insertionPoint
    AbsoluteIndex end <- readIORef droppingPoint
    let num = beg - end
        space = max 1 (num !>>. 4)
        end' = beg - num + space
    writeIORef drainingPoint $ AbsoluteIndex end'
    table <- readTVarIO circularTable
    maxN <- readTVarIO maxNumOfEntries
    loop end end' table maxN
  where
    EncodeInfo{..} = codeInfo
    loop :: Int -> Int -> Table -> Int -> IO ()
    loop ai lim table maxN
        | ai == lim = return ()
        | otherwise = do
            let i = ai `mod` maxN
            ent <- atomically $ unsafeRead table i
            deleteRevIndex revIndex ent $ AbsoluteIndex ai
            loop (ai + 1) lim table maxN

tailDuplicationThreshold :: Int
tailDuplicationThreshold = 10

checkTailDuplication :: DynamicTable -> IO (Maybe AbsoluteIndex)
checkTailDuplication DynamicTable{..} = do
    dai@(AbsoluteIndex ai) <- readIORef droppingPoint
    arr <- readIORef referenceCounters
    maxN <- readTVarIO maxNumOfEntries
    let i = ai `mod` maxN
    -- modifyArray' is not provided by GHC 9.4 or earlier, sigh.
    Reference current total <- unsafeRead arr i
    if current == 0 && total >= tailDuplicationThreshold
        then return $ Just dai
        else return Nothing
  where
    EncodeInfo{..} = codeInfo

tailDuplication :: DynamicTable -> IO AbsoluteIndex
tailDuplication dyntbl@DynamicTable{..} = do
    dai <- readIORef droppingPoint
    ndai <- duplicate dyntbl dai
    dropIfNecessary dyntbl
    return ndai
  where
    EncodeInfo{..} = codeInfo

duplicate :: DynamicTable -> AbsoluteIndex -> IO AbsoluteIndex
duplicate dyntbl@DynamicTable{..} (AbsoluteIndex ai) = do
    maxN <- readTVarIO maxNumOfEntries
    let i = ai `mod` maxN
    table <- readTVarIO circularTable
    ent <- atomically $ unsafeRead table i
    insertEntryToEncoder ent dyntbl

----------------------------------------------------------------

canInsertEntry :: DynamicTable -> Entry -> Maybe AbsoluteIndex -> IO Bool
canInsertEntry DynamicTable{..} ent mai = do
    let siz = entrySize ent
    tblsiz <- readTVarIO tableSize
    maxtblsiz <- readIORef maxTableSize
    if tblsiz + siz <= maxtblsiz
        then
            return True
        else do
            AbsoluteIndex ai <- readIORef droppingPoint
            InsertionPoint lim <- readTVarIO insertionPoint
            loop ai lim (tblsiz + siz - maxtblsiz)
  where
    myself = case mai of
        Nothing -> -1 -- trick: this index does not exist
        Just (AbsoluteIndex i) -> i
    EncodeInfo{..} = codeInfo
    loop ai lim requiredSize
        | requiredSize <= 0 = return True
        | otherwise = do
            if ai < lim && ai /= myself -- don't drop the referred entry
                then do
                    maxN <- readTVarIO maxNumOfEntries
                    let i = ai `mod` maxN
                    refs <- readIORef referenceCounters
                    Reference current total <- unsafeRead refs i
                    if current == 0 && total >= 1
                        then do
                            table <- readTVarIO circularTable
                            dent <- atomically $ unsafeRead table i
                            let siz = entrySize dent

                            loop (ai + 1) lim (requiredSize - siz)
                        else return False
                else return False

dropIfNecessary :: DynamicTable -> IO ()
dropIfNecessary dyntbl@DynamicTable{..} = loop
  where
    loop = do
        tblsize <- readTVarIO tableSize
        maxtblsize <- readIORef maxTableSize
        unless (tblsize <= maxtblsize) $ do
            dropped <- tryDrop dyntbl
            if dropped then loop else error "dropIfNecessary"

tryDrop :: DynamicTable -> IO Bool
tryDrop dyntbl@DynamicTable{..} = do
    maxN <- readTVarIO maxNumOfEntries
    dai@(AbsoluteIndex ai) <- readIORef droppingPoint
    InsertionPoint lim <- readTVarIO insertionPoint
    if ai < lim
        then do
            let i = ai `mod` maxN
            refs <- readIORef referenceCounters
            Reference current total <- unsafeRead refs i
            if current == 0 && total >= 1
                then do
                    table <- readTVarIO circularTable
                    ent <- atomically $ do
                        e <- unsafeRead table i
                        unsafeWrite table i dummyEntry
                        return e
                    let siz = entrySize ent
                    atomically $ modifyTVar' tableSize $ subtract siz
                    modifyIORef' droppingPoint (+ 1)
                    qpackDebug dyntbl $ do
                        putStrLn $
                            "DROPPED (AbsoluteIndex "
                                ++ show ai
                                ++ ") "
                                ++ show (entryHeaderName ent)
                                ++ " "
                                ++ show (entryFieldValue ent)
                        tblsiz <- readTVarIO tableSize
                        putStrLn $ "    tblsiz: " ++ show tblsiz
                    deleteRevIndex revIndex ent dai
                    return True
                else return False
        else return False
  where
    EncodeInfo{..} = codeInfo

----------------------------------------------------------------

getLruCache :: DynamicTable -> LRUCacheRef (FieldName, FieldValue) ()
getLruCache DynamicTable{..} = lruCache
  where
    EncodeInfo{..} = codeInfo

{-# INLINE getRevIndex #-}
getRevIndex :: DynamicTable -> RevIndex
getRevIndex DynamicTable{..} = revIndex
  where
    EncodeInfo{..} = codeInfo

-- only for encoder instruction handler
getHuffmanDecoder :: DynamicTable -> HuffmanDecoder
getHuffmanDecoder DynamicTable{..} = huffmanDecoder
  where
    DecodeInfo{..} = codeInfo

----------------------------------------------------------------

getMaxHeaderSize :: DynamicTable -> IO Int
getMaxHeaderSize DynamicTable{..} = readIORef maxHeaderSize

setMaxHeaderSize :: DynamicTable -> Int -> IO ()
setMaxHeaderSize DynamicTable{..} n = writeIORef maxHeaderSize n

----------------------------------------------------------------

qpackDebug :: DynamicTable -> IO () -> IO ()
qpackDebug DynamicTable{..} action = do
    debug <- readIORef debugQPACK
    when debug $ withMVar stdoutLock $ \_ -> action

getDebugQPACK :: DynamicTable -> IO Bool
getDebugQPACK DynamicTable{..} = readIORef debugQPACK

setDebugQPACK :: DynamicTable -> Bool -> IO ()
setDebugQPACK DynamicTable{..} b = writeIORef debugQPACK b

{-# NOINLINE stdoutLock #-}
stdoutLock :: MVar ()
stdoutLock = unsafePerformIO $ newMVar ()

printReferences :: DynamicTable -> IO ()
printReferences DynamicTable{..} = do
    AbsoluteIndex start <- readIORef droppingPoint
    InsertionPoint end <- readTVarIO insertionPoint
    maxN <- readTVarIO maxNumOfEntries
    arr <- readIORef referenceCounters
    putStr "Refs: "
    loop start end arr maxN
    putStr "\n"
  where
    loop :: Int -> Int -> IOArray Index Reference -> Int -> IO ()
    loop start end arr maxN
        | start < end = do
            r <- unsafeRead arr (start `mod` maxN)
            putStr $ show start ++ ": " ++ showReference r ++ ", "
            loop (start + 1) end arr maxN
        | otherwise = return ()
    EncodeInfo{..} = codeInfo
    showReference (Reference c t) = show c ++ "(" ++ show t ++ ")"

-- For decoder
checkHIndex :: DynamicTable -> HIndex -> IO ()
checkHIndex _ (SIndex _) = return ()
checkHIndex DynamicTable{..} (DIndex (AbsoluteIndex ai)) = do
    InsertionPoint ip <- readTVarIO insertionPoint
    maxN <- readTVarIO maxNumOfEntries
    if ip - maxN <= ai && ai < ip
        then return ()
        else error "checkHIndex"

-- For encoder
checkAbsoluteIndex :: DynamicTable -> AbsoluteIndex -> String -> IO ()
checkAbsoluteIndex DynamicTable{..} (AbsoluteIndex ai) tag = do
    InsertionPoint beg <- readTVarIO insertionPoint
    AbsoluteIndex end <- readIORef droppingPoint
    maxN <- readTVarIO maxNumOfEntries
    table <- readTVarIO circularTable
    let calcSize i acc
            | i == beg = return acc
            | otherwise = do
                siz <- entrySize <$> atomically (unsafeRead table (i `mod` maxN))
                calcSize (i + 1) (acc + siz)
    if end <= ai && ai < beg
        then do
            size <- calcSize end 0
            size0 <- readTVarIO tableSize
            when (size /= size0) $ error $ "checkAbsoluteIndex: size /= size0) " ++ tag
            lim <- readIORef maxTableSize
            when (size > lim) $ error $ "checkAbsoluteIndex: size > lim " ++ tag
            putStrLn $ "    check: tblsiz: " ++ show size ++ " " ++ show ai ++ " " ++ tag
        else
            error $
                "checkAbsoluteIndex (3) "
                    ++ tag
                    ++ " <= "
                    ++ show end
                    ++ " "
                    ++ show ai
                    ++ " < "
                    ++ show beg
  where
    EncodeInfo{..} = codeInfo

----------------------------------------------------------------

getImmediateAck :: DynamicTable -> IO Bool
getImmediateAck DynamicTable{..} = readIORef immediateAck
  where
    EncodeInfo{..} = codeInfo

setImmediateAck :: DynamicTable -> Bool -> IO ()
setImmediateAck DynamicTable{..} b = writeIORef immediateAck b
  where
    EncodeInfo{..} = codeInfo