dataframe-core-2.6.0.0: src-internal/DataFrame/Internal/Column/Merge.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
-- | Concatenation of per-chunk 'Column's, merging text at the byte level.
module DataFrame.Internal.Column.Merge (
TextChunk (..),
concatColumns,
concatColumnsIO,
mergeTextChunks,
mergeTextChunksIO,
packedFromTextChunk,
concatValidity,
tcRows,
) where
import qualified Data.Text.Array as A
import qualified Data.Vector as VB
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import Control.Monad (foldM_, forM_, when)
import Control.Monad.ST (runST, stToIO)
import Data.Type.Equality (testEquality, (:~:) (Refl))
import DataFrame.Internal.Column (
Column (..),
Columnable,
isMergedColumn,
isPackedText,
materializeMerged,
materializePacked,
)
import DataFrame.Internal.Column.Bitmap (
Bitmap,
Validity (Validity),
concatValidity,
)
import DataFrame.Internal.Control.Concurrent (
forkJoin_,
parThreshold,
shouldParallelize,
)
import DataFrame.Internal.Data.PackedText (
PackedOffsets (..),
mkPackedContiguous,
offAt,
offCount,
packedRowOffsets,
)
import Type.Reflection (typeRep)
-- | A frozen text-builder chunk: UTF-8 bytes, row offsets and validity.
data TextChunk = TextChunk
{ tcBytes :: !A.Array
, tcUsed :: !Int
, tcOffsets :: !(VU.Vector Int)
, tcBitmap :: !(Maybe Bitmap)
}
tcRows :: TextChunk -> Int
tcRows c = VU.length (tcOffsets c) - 1
-- | Freeze a builder chunk directly into a packed-text column.
packedFromTextChunk :: TextChunk -> Column
packedFromTextChunk (TextChunk arr _used offs bm) =
PackedText bm (mkPackedContiguous arr offs)
-- | Merge text chunks into one packed-text 'Column' with one copy per chunk.
mergeTextChunks :: [TextChunk] -> Column
mergeTextChunks [] = error "DataFrame.Internal.ColumnMerge.mergeTextChunks: empty list"
mergeTextChunks [c] = packedFromTextChunk c
mergeTextChunks cs = runST $ do
let totalBytes = sum (map tcUsed cs)
totalRows = sum (map tcRows cs)
arr <- A.new (max 1 totalBytes)
offs <- VUM.unsafeNew (totalRows + 1)
VUM.unsafeWrite offs 0 0
let splice !byteBase !rowBase c = do
let n = tcRows c
co = tcOffsets c
A.copyI (tcUsed c) arr byteBase (tcBytes c) 0
forM_ [1 .. n] $ \i ->
VUM.unsafeWrite offs (rowBase + i) (byteBase + VU.unsafeIndex co i)
pure (byteBase + tcUsed c, rowBase + n)
foldM_ (\(b, r) c -> splice b r c) (0, 0) cs
farr <- A.unsafeFreeze arr
foffs <- VU.unsafeFreeze offs
let !bm = concatValidity [Validity (tcBitmap c) (tcRows c) | c <- cs]
pure (PackedText bm (mkPackedContiguous farr foffs))
-- | 'mergeTextChunks' with copies and offset rebases spread over workers.
mergeTextChunksIO :: [TextChunk] -> IO Column
mergeTextChunksIO [] = error "DataFrame.Internal.ColumnMerge.mergeTextChunksIO: empty list"
mergeTextChunksIO [c] = pure (packedFromTextChunk c)
mergeTextChunksIO cs
| not (shouldParallelize parThreshold totalRows) = pure (mergeTextChunks cs)
| otherwise = do
arr <- stToIO (A.new (max 1 totalBytes))
offs <- VUM.unsafeNew (totalRows + 1)
VUM.unsafeWrite offs 0 0
let byteBases = scanl (+) 0 (map tcUsed cs)
rowBases = scanl (+) 0 (map tcRows cs)
splice byteBase rowBase c = do
let n = tcRows c
co = tcOffsets c
go !i = when (i <= n) $ do
VUM.unsafeWrite offs (rowBase + i) (byteBase + VU.unsafeIndex co i)
go (i + 1)
stToIO (A.copyI (tcUsed c) arr byteBase (tcBytes c) 0)
go 1
forkJoin_ (zipWith3 splice byteBases rowBases cs)
farr <- stToIO (A.unsafeFreeze arr)
foffs <- VU.unsafeFreeze offs
let !bm = concatValidity [Validity (tcBitmap c) (tcRows c) | c <- cs]
pure (PackedText bm (mkPackedContiguous farr foffs))
where
totalBytes = sum (map tcUsed cs)
totalRows = sum (map tcRows cs)
-- | 'concatColumns', merging packed text in parallel.
concatColumnsIO :: [Column] -> IO Column
concatColumnsIO cols
| Just chunks <- mapM packedChunk cols = mergeTextChunksIO chunks
| otherwise = pure (concatColumns cols)
-- | Merge per-chunk columns into one column.
concatColumns :: [Column] -> Column
concatColumns [] = error "DataFrame.Internal.Column.Builder.concatColumns: empty list"
concatColumns [c] = c
-- Normalize the whole list: a packed or merged chunk demotes all to boxed.
concatColumns cols
| Just chunks <- mapM packedChunk cols = mergeTextChunks chunks
concatColumns cols@(c0 : _)
| any isMergedColumn cols = concatColumns (map materializeMerged cols)
| any isPackedText cols = concatColumns (map materializePacked cols)
concatColumns cols@(c0 : _) = case c0 of
PackedText _ _ -> concatColumns (map materializePacked cols)
MergedColumn _ _ -> concatColumns (map materializeMerged cols)
UnboxedColumn _ (_ :: VU.Vector a) ->
let parts = map (unboxedPart @a) cols
!merged = VU.concat (map snd parts)
!bm = concatValidity [Validity mb (VU.length v) | (mb, v) <- parts]
in UnboxedColumn bm merged
BoxedColumn _ (_ :: VB.Vector a) ->
let parts = map (boxedPart @a) cols
!merged = VB.concat (map snd parts)
!bm = concatValidity [Validity mb (VB.length v) | (mb, v) <- parts]
in BoxedColumn bm merged
unboxedPart ::
forall a. (Columnable a, VU.Unbox a) => Column -> (Maybe Bitmap, VU.Vector a)
unboxedPart (UnboxedColumn mb (v :: VU.Vector b)) =
case testEquality (typeRep @a) (typeRep @b) of
Just Refl -> (mb, v)
Nothing -> mergeMismatch
unboxedPart _ = mergeMismatch
boxedPart ::
forall a. (Columnable a) => Column -> (Maybe Bitmap, VB.Vector a)
boxedPart (BoxedColumn mb (v :: VB.Vector b)) =
case testEquality (typeRep @a) (typeRep @b) of
Just Refl -> (mb, v)
Nothing -> mergeMismatch
boxedPart _ = mergeMismatch
mergeMismatch :: a
mergeMismatch =
error
"DataFrame.Internal.Column.Builder.concatColumns: chunk column types differ"
-- | A packed-text column without a selection, viewed as a builder chunk.
packedChunk :: Column -> Maybe TextChunk
packedChunk (PackedText bm p) = case packedRowOffsets p of
Just (arr, offs) ->
let n = offCount offs - 1
offsV = case offs of
Offs64 v -> v
Offs32 v -> VU.map fromIntegral v
in Just (TextChunk arr (offAt offs n) offsV bm)
Nothing -> Nothing
packedChunk _ = Nothing