dataframe-core-1.1.0.0: src/DataFrame/Internal/ColumnBuilder.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
{- | Mutable, growable column builders for high-throughput ingest. No
per-append @IORef@ traffic: hot counters live in an unboxed vector, payloads
double on demand, and validity is only materialized once a null is seen.
-}
module DataFrame.Internal.ColumnBuilder (
ColumnBuilder (..),
NumBuilder,
IntBuilder,
DoubleBuilder,
TextBuilder,
TextChunk (..),
newIntBuilder,
newDoubleBuilder,
newNumBuilder,
newTextBuilder,
appendInt,
appendDouble,
appendNum,
appendText,
appendTextSlice,
appendTextSliceFromPtr,
freezeTextChunk,
mergeColumns,
mergeTextChunks,
) where
import qualified Data.Text as T
import qualified Data.Text.Array as A
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import Control.Monad (when)
import Control.Monad.ST (ST)
import Data.Bits (shiftR)
import Data.STRef
import Data.Text.Internal (Text (..))
import Data.Word (Word8)
import DataFrame.Internal.Column hiding (mergeColumns)
import DataFrame.Internal.ColumnMerge (
TextChunk (..),
mergeColumns,
mergeTextChunks,
packValidity,
)
import Foreign.Ptr (Ptr)
{- | Operations shared by all column builders. A builder must not be used
again after 'freezeBuilder' (its storage is frozen in place, not copied).
-}
class ColumnBuilder b where
-- | Append a null row (sentinel payload + invalid bit).
appendNull :: b s -> ST s ()
-- | Rows appended so far.
builderLength :: b s -> ST s Int
-- | Freeze into a fully-forced 'Column'; bitmap only when a null was seen.
freezeBuilder :: b s -> ST s Column
-- Counter slots shared by the builders: rows, any-null flag, text bytes used.
cRows, cAnyNull, cBytes :: Int
cRows = 0
cAnyNull = 1
cBytes = 2
{- | Builder for unboxed numeric payloads ('Int', 'Double', ...). 'nbNull'
is the sentinel written into null slots (protected by the bitmap).
-}
data NumBuilder a s = NumBuilder
{ nbNull :: !a
, nbCounters :: !(VUM.MVector s Int)
, nbArrays :: !(STRef s (NumArrays a s))
}
data NumArrays a s = NumArrays
{ naData :: !(VUM.MVector s a)
, naValid :: !(VUM.MVector s Word8)
}
type IntBuilder = NumBuilder Int
type DoubleBuilder = NumBuilder Double
-- | New numeric builder with a row-capacity hint and a null sentinel.
newNumBuilder :: (VU.Unbox a) => a -> Int -> ST s (NumBuilder a s)
newNumBuilder nullValue hint = do
let cap = max 16 hint
counters <- VUM.replicate 2 0
dat <- VUM.unsafeNew cap
val <- VUM.unsafeNew cap
NumBuilder nullValue counters <$> newSTRef (NumArrays dat val)
newIntBuilder :: Int -> ST s (IntBuilder s)
newIntBuilder = newNumBuilder 0
newDoubleBuilder :: Int -> ST s (DoubleBuilder s)
newDoubleBuilder = newNumBuilder 0
appendNum :: (VU.Unbox a) => NumBuilder a s -> a -> ST s ()
appendNum b !x = do
n <- VUM.unsafeRead (nbCounters b) cRows
anyNull <- VUM.unsafeRead (nbCounters b) cAnyNull
NumArrays dat val <- reserveNum b n
VUM.unsafeWrite dat n x
when (anyNull /= 0) $ VUM.unsafeWrite val n 1
VUM.unsafeWrite (nbCounters b) cRows (n + 1)
{-# INLINE appendNum #-}
appendInt :: IntBuilder s -> Int -> ST s ()
appendInt = appendNum
{-# INLINE appendInt #-}
appendDouble :: DoubleBuilder s -> Double -> ST s ()
appendDouble = appendNum
{-# INLINE appendDouble #-}
-- Fetch the arrays, growing (doubling) first if row @n@ would not fit.
reserveNum :: (VU.Unbox a) => NumBuilder a s -> Int -> ST s (NumArrays a s)
reserveNum b n = do
arrs <- readSTRef (nbArrays b)
if n < VUM.length (naData arrs) then pure arrs else growNum b arrs
{-# INLINE reserveNum #-}
growNum ::
(VU.Unbox a) => NumBuilder a s -> NumArrays a s -> ST s (NumArrays a s)
growNum b (NumArrays dat val) = do
let cap = VUM.length dat
dat' <- VUM.unsafeGrow dat cap
val' <- VUM.unsafeGrow val cap
let arrs = NumArrays dat' val'
writeSTRef (nbArrays b) arrs
pure arrs
instance (Columnable a, VU.Unbox a) => ColumnBuilder (NumBuilder a) where
appendNull b = do
n <- VUM.unsafeRead (nbCounters b) cRows
anyNull <- VUM.unsafeRead (nbCounters b) cAnyNull
NumArrays dat val <- reserveNum b n
VUM.unsafeWrite dat n (nbNull b)
when (anyNull == 0) $ do
VUM.set (VUM.slice 0 n val) 1
VUM.unsafeWrite (nbCounters b) cAnyNull 1
VUM.unsafeWrite val n 0
VUM.unsafeWrite (nbCounters b) cRows (n + 1)
{-# INLINE appendNull #-}
builderLength b = VUM.unsafeRead (nbCounters b) cRows
freezeBuilder b = do
n <- VUM.unsafeRead (nbCounters b) cRows
anyNull <- VUM.unsafeRead (nbCounters b) cAnyNull
NumArrays dat val <- readSTRef (nbArrays b)
!vs <- freezeTrimmed n dat
if anyNull /= 0
then do
!bm <- packValidity n val
pure $! UnboxedColumn (Just bm) vs
else pure $! UnboxedColumn Nothing vs
-- Zero-copy freeze; copies to exact size when slack exceeds a quarter of n.
freezeTrimmed :: (VU.Unbox a) => Int -> VUM.MVector s a -> ST s (VU.Vector a)
freezeTrimmed n mv
| VUM.length mv - n <= n `shiftR` 2 = VU.unsafeFreeze (VUM.slice 0 n mv)
| otherwise = VU.freeze (VUM.slice 0 n mv)
{- | Builder for 'Text' columns. All field bytes go into one exponentially
grown byte array; rows are recorded as offsets, so an append is a memcpy
and freezing slices 'Text' values off the shared array without copying.
-}
data TextBuilder s = TextBuilder
{ tbCounters :: !(VUM.MVector s Int)
, tbArrays :: !(STRef s (TextArrays s))
}
data TextArrays s = TextArrays
{ taBytes :: !(A.MArray s)
, taByteCap :: !Int
, taOffsets :: !(VUM.MVector s Int)
-- ^ Row @i@ spans bytes @[offsets!i, offsets!(i+1))@.
, taValid :: !(VUM.MVector s Word8)
}
-- | New text builder with row-count and total-byte capacity hints.
newTextBuilder :: Int -> Int -> ST s (TextBuilder s)
newTextBuilder rowHint byteHint = do
let rcap = max 16 rowHint
bcap = max 64 byteHint
counters <- VUM.replicate 3 0
bytes <- A.new bcap
offsets <- VUM.unsafeNew (rcap + 1)
VUM.unsafeWrite offsets 0 0
val <- VUM.unsafeNew rcap
TextBuilder counters <$> newSTRef (TextArrays bytes bcap offsets val)
-- | Append @len@ raw bytes at @off@ in @src@ as one field (one memcpy).
appendTextSlice :: TextBuilder s -> A.Array -> Int -> Int -> ST s ()
appendTextSlice b src off len = do
(n, pos, arrs) <- reserveText b len
A.copyI len (taBytes arrs) pos src off
finishTextAppend b arrs n (pos + len)
{-# INLINE appendTextSlice #-}
-- | 'appendTextSlice' from foreign memory (e.g. an mmapped file buffer).
appendTextSliceFromPtr :: TextBuilder s -> Ptr Word8 -> Int -> ST s ()
appendTextSliceFromPtr b ptr len = do
(n, pos, arrs) <- reserveText b len
A.copyFromPointer (taBytes arrs) pos ptr len
finishTextAppend b arrs n (pos + len)
{-# INLINE appendTextSliceFromPtr #-}
-- | Append an already-decoded 'Text' (its bytes are UTF-8 already).
appendText :: TextBuilder s -> T.Text -> ST s ()
appendText b (Text src off len) = appendTextSlice b src off len
{-# INLINE appendText #-}
finishTextAppend :: TextBuilder s -> TextArrays s -> Int -> Int -> ST s ()
finishTextAppend b arrs n endPos = do
anyNull <- VUM.unsafeRead (tbCounters b) cAnyNull
when (anyNull /= 0) $ VUM.unsafeWrite (taValid arrs) n 1
VUM.unsafeWrite (taOffsets arrs) (n + 1) endPos
VUM.unsafeWrite (tbCounters b) cRows (n + 1)
VUM.unsafeWrite (tbCounters b) cBytes endPos
{-# INLINE finishTextAppend #-}
reserveText :: TextBuilder s -> Int -> ST s (Int, Int, TextArrays s)
reserveText b extra = do
n <- VUM.unsafeRead (tbCounters b) cRows
pos <- VUM.unsafeRead (tbCounters b) cBytes
arrs <- readSTRef (tbArrays b)
arrs' <-
if n < VUM.length (taValid arrs) && pos + extra <= taByteCap arrs
then pure arrs
else growText b arrs (n + 1) (pos + extra)
pure (n, pos, arrs')
{-# INLINE reserveText #-}
growText :: TextBuilder s -> TextArrays s -> Int -> Int -> ST s (TextArrays s)
growText b (TextArrays bytes bcap offsets val) needRows needBytes = do
let rcap = VUM.length val
(offsets', val') <-
if needRows > rcap
then do
let rcap' = max (2 * rcap) needRows
o <- VUM.unsafeGrow offsets (rcap' - rcap)
v <- VUM.unsafeGrow val (rcap' - rcap)
pure (o, v)
else pure (offsets, val)
(bytes', bcap') <-
if needBytes > bcap
then do
let cap' = max (2 * bcap) needBytes
bs <- A.resizeM bytes cap'
pure (bs, cap')
else pure (bytes, bcap)
let arrs = TextArrays bytes' bcap' offsets' val'
writeSTRef (tbArrays b) arrs
pure arrs
{- | Freeze a 'TextBuilder' into a raw 'TextChunk' for byte-level merging
('mergeTextChunks'): no 'T.Text' values are created until chunks merge.
-}
freezeTextChunk :: TextBuilder s -> ST s TextChunk
freezeTextChunk b = do
n <- VUM.unsafeRead (tbCounters b) cRows
anyNull <- VUM.unsafeRead (tbCounters b) cAnyNull
used <- VUM.unsafeRead (tbCounters b) cBytes
TextArrays bytes bcap offsets val <- readSTRef (tbArrays b)
when (used < bcap) (A.shrinkM bytes used)
arr <- A.unsafeFreeze bytes
offs <- VU.unsafeFreeze (VUM.slice 0 (n + 1) offsets)
bm <-
if anyNull /= 0
then Just <$> packValidity n val
else pure Nothing
pure (TextChunk arr used offs bm)
instance ColumnBuilder TextBuilder where
appendNull b = do
(n, pos, arrs) <- reserveText b 0
anyNull <- VUM.unsafeRead (tbCounters b) cAnyNull
when (anyNull == 0) $ do
VUM.set (VUM.slice 0 n (taValid arrs)) 1
VUM.unsafeWrite (tbCounters b) cAnyNull 1
VUM.unsafeWrite (taValid arrs) n 0
VUM.unsafeWrite (taOffsets arrs) (n + 1) pos
VUM.unsafeWrite (tbCounters b) cRows (n + 1)
VUM.unsafeWrite (tbCounters b) cBytes pos
{-# INLINE appendNull #-}
builderLength b = VUM.unsafeRead (tbCounters b) cRows
freezeBuilder b = do
chunk <- freezeTextChunk b
pure $! mergeTextChunks [chunk]