dataframe-parquet-1.6.0.0: src/DataFrame/IO/Utils/RandomAccess.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE OverloadedRecordDot #-}
module DataFrame.IO.Utils.RandomAccess (
uncurry3,
Range (..),
RandomAccess (..),
ReaderIO (runReaderIO),
LocalFile,
MMappedFile,
ChunkBuffer (..),
unsafeToByteString,
WritableBinaryHandle,
openWritableBinaryFile,
withWritableBinaryFile,
atomicallyWriteFile,
MemoryBuffer (..),
ensureCapacity,
mallocBuffer,
writeByteString,
appendTextArraySlice,
writeWord8,
writeWord32LE,
writeWord64LE,
writeInteger64,
writeWord32At,
writeWord64At,
writeInteger64At,
writeFloatLE,
writeDoubleLE,
bufferResidency,
bufferToByteString,
unsafeBufferView,
flushBufferToBuffer,
resetPosition,
flushBufferToFile,
writeByteStringToFile,
) where
import Control.Exception (bracket, bracketOnError, finally, throw)
import Control.Monad (when)
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.ST (RealWorld, ST, stToIO)
import Data.Bits (shiftR)
import Data.ByteString.Internal (ByteString (BS, PS), create)
import qualified Data.ByteString.Unsafe as BU
import Data.Int (Int64)
import Data.Primitive.ByteArray (
MutableByteArray (..),
copyMutableByteArray,
getSizeofMutableByteArray,
mutableByteArrayContents,
newPinnedByteArray,
withMutableByteArrayContents,
writeByteArray,
)
import Data.STRef (STRef, newSTRef, readSTRef, writeSTRef)
import qualified Data.Text.Array as TA
import qualified Data.Vector.Storable as VS
import Data.Word (Word32, Word64, Word8)
import DataFrame.IO.Parquet.Seeking (
FileBufferedOrSeekable,
fGet,
fSeek,
readLastBytes,
)
import Foreign (castForeignPtr, castPtr, copyBytes, plusPtr)
import GHC.Float (castDoubleToWord64, castFloatToWord32)
import GHC.ForeignPtr (ForeignPtr (..), ForeignPtrContents (PlainPtr))
import GHC.Ptr (Ptr (..))
import System.Directory (copyPermissions, doesFileExist, removeFile, renameFile)
import System.FilePath (takeDirectory)
import System.IO (
BufferMode (NoBuffering),
Handle,
IOMode (WriteMode),
SeekMode (AbsoluteSeek),
hClose,
hPutBuf,
hSetBinaryMode,
hSetBuffering,
openBinaryFile,
openBinaryTempFileWithDefaultPermissions,
)
uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d
uncurry3 f (a, b, c) = f a b c
data Range = Range {offset :: !Integer, length :: !Int} deriving (Eq, Show)
class (Monad m) => RandomAccess m where
readBytes :: Range -> m ByteString
readRanges :: [Range] -> m [ByteString]
readRanges = mapM readBytes
readSuffix :: Int -> m ByteString
newtype ReaderIO r a = ReaderIO {runReaderIO :: r -> IO a}
instance Functor (ReaderIO r) where
fmap f (ReaderIO run) = ReaderIO $ fmap f . run
instance Applicative (ReaderIO r) where
pure a = ReaderIO $ \_ -> pure a
(ReaderIO fg) <*> (ReaderIO fa) = ReaderIO $ \r -> do
a <- fa r
g <- fg r
pure (g a)
instance Monad (ReaderIO r) where
return = pure
(ReaderIO ma) >>= f = ReaderIO $ \r -> do
a <- ma r
runReaderIO (f a) r
instance MonadIO (ReaderIO r) where
liftIO io = ReaderIO $ const io
type LocalFile = ReaderIO FileBufferedOrSeekable
instance RandomAccess LocalFile where
readBytes (Range offset' length') = ReaderIO $ \handle -> do
fSeek handle AbsoluteSeek offset'
fGet handle length'
readSuffix n = ReaderIO (readLastBytes $ fromIntegral n)
-- | Bytes of one column chunk, already read; @base@ is their file offset.
data ChunkBuffer = ChunkBuffer !Integer !ByteString
instance RandomAccess (ReaderIO ChunkBuffer) where
readBytes (Range offset' length') = ReaderIO $ \(ChunkBuffer base bs) ->
pure (BU.unsafeTake length' (BU.unsafeDrop (fromInteger (offset' - base)) bs))
readSuffix _ = ReaderIO $ \_ -> error "ChunkBuffer: no file footer"
type MMappedFile = ReaderIO (VS.Vector Word8)
-- The instance exists but we don't have the means to mmap the file currently
instance RandomAccess MMappedFile where
readBytes (Range offset' length') =
ReaderIO $
pure . unsafeToByteString . VS.slice (fromInteger offset') length'
readSuffix n =
ReaderIO $ \v ->
let len = VS.length v
n' = min n len
start = len - n'
in pure . unsafeToByteString $ VS.slice start n' v
unsafeToByteString :: VS.Vector Word8 -> ByteString
unsafeToByteString v = PS (castForeignPtr ptr) offset' len
where
(ptr, offset', len) = VS.unsafeToForeignPtr v
-- Writer buffers are pinned MutableByteArrays so hPutBuf can take a Ptr.
newtype WritableBinaryHandle = WritableBinaryHandle {unHandle :: Handle}
openWritableBinaryFile :: FilePath -> IO WritableBinaryHandle
openWritableBinaryFile filepath = do
h <- openBinaryFile filepath WriteMode
hSetBinaryMode h True
hSetBuffering h NoBuffering
pure . WritableBinaryHandle $ h
atomicallyWriteFile ::
FilePath ->
(FilePath -> IO a) ->
IO a
atomicallyWriteFile path action =
bracketOnError
openAction
removeFile
( \tmpFile -> do
result <- action tmpFile
renameFile tmpFile path
pure result
)
where
openAction =
bracketOnError
( openBinaryTempFileWithDefaultPermissions
(takeDirectory path)
"dataframe-parquet.incomplete"
)
(\(tmpFile, h) -> hClose h `finally` removeFile tmpFile)
( \(tmpFile, h) -> do
hClose h
destinationExists <- doesFileExist path
when destinationExists (copyPermissions path tmpFile)
pure tmpFile
)
withWritableBinaryFile ::
FilePath ->
(WritableBinaryHandle -> IO a) ->
IO a
withWritableBinaryFile filepath =
bracket
(openWritableBinaryFile filepath)
(hClose . unHandle)
data MemoryBuffer s = MemoryBuffer
{ arrayRef :: !(STRef s (MutableByteArray s))
, positionRef :: !(STRef s Int)
}
mallocBuffer :: Int -> ST s (MemoryBuffer s)
mallocBuffer capacity
| capacity < 0 = throw (userError "mallocBuffer: negative capacity")
| otherwise = do
array <- newPinnedByteArray capacity
MemoryBuffer <$> newSTRef array <*> newSTRef 0
-- Pinned arrays cannot grow in place, so allocate a new one and copy.
ensureCapacity :: MemoryBuffer s -> Int -> ST s (MutableByteArray s)
ensureCapacity buffer needed = do
array <- readSTRef buffer.arrayRef
maxSize <- getSizeofMutableByteArray array
if needed <= maxSize
then pure array
else do
position <- readSTRef buffer.positionRef
grown <- newPinnedByteArray (needed + (needed `div` 2))
copyMutableByteArray grown 0 array 0 position
writeSTRef buffer.arrayRef grown
pure grown
{-# INLINE ensureCapacity #-}
writeWord8 :: MemoryBuffer s -> Word8 -> ST s ()
writeWord8 buffer b = do
position <- readSTRef buffer.positionRef
array <- ensureCapacity buffer (position + 1)
writeByteArray array position b
writeSTRef buffer.positionRef (position + 1)
{-# INLINE writeWord8 #-}
-- Reading the ByteString's pointer needs IO, so this stays out of ST.
writeByteString :: MemoryBuffer RealWorld -> ByteString -> IO ()
writeByteString buffer bs =
BU.unsafeUseAsCStringLen bs $ \(source, len) -> do
position <- stToIO (readSTRef buffer.positionRef)
array <- stToIO (ensureCapacity buffer (position + len))
withMutableByteArrayContents array $ \dst ->
copyBytes (dst `plusPtr` position) (castPtr source) len
stToIO (writeSTRef buffer.positionRef (position + len))
{-# INLINE writeByteString #-}
writeWord32LE :: MemoryBuffer s -> Word32 -> ST s ()
writeWord32LE buffer w = do
position <- readSTRef buffer.positionRef
writeWord32At buffer position w
writeSTRef buffer.positionRef (position + 4)
{-# INLINE writeWord32LE #-}
writeWord64LE :: MemoryBuffer s -> Word64 -> ST s ()
writeWord64LE buffer w = do
position <- readSTRef buffer.positionRef
writeWord64At buffer position w
writeSTRef buffer.positionRef (position + 8)
{-# INLINE writeWord64LE #-}
writeWord32At :: MemoryBuffer s -> Int -> Word32 -> ST s ()
writeWord32At buffer position w = do
array <- ensureCapacity buffer (position + 4)
writeByteArray array position (fromIntegral w :: Word8)
writeByteArray array (position + 1) (fromIntegral (w `shiftR` 8) :: Word8)
writeByteArray array (position + 2) (fromIntegral (w `shiftR` 16) :: Word8)
writeByteArray array (position + 3) (fromIntegral (w `shiftR` 24) :: Word8)
{-# INLINE writeWord32At #-}
writeWord64At :: MemoryBuffer s -> Int -> Word64 -> ST s ()
writeWord64At buffer position w = do
array <- ensureCapacity buffer (position + 8)
writeByteArray array position (fromIntegral w :: Word8)
writeByteArray array (position + 1) (fromIntegral (w `shiftR` 8) :: Word8)
writeByteArray array (position + 2) (fromIntegral (w `shiftR` 16) :: Word8)
writeByteArray array (position + 3) (fromIntegral (w `shiftR` 24) :: Word8)
writeByteArray array (position + 4) (fromIntegral (w `shiftR` 32) :: Word8)
writeByteArray array (position + 5) (fromIntegral (w `shiftR` 40) :: Word8)
writeByteArray array (position + 6) (fromIntegral (w `shiftR` 48) :: Word8)
writeByteArray array (position + 7) (fromIntegral (w `shiftR` 56) :: Word8)
{-# INLINE writeWord64At #-}
writeInteger64 :: MemoryBuffer s -> Integer -> ST s ()
writeInteger64 buffer value = do
position <- readSTRef buffer.positionRef
newPosition <- writeInteger64At buffer position value
writeSTRef buffer.positionRef newPosition
{-# INLINE writeInteger64 #-}
writeInteger64At :: MemoryBuffer s -> Int -> Integer -> ST s Int
writeInteger64At buffer position value
| value < toInteger (minBound :: Int64) = outOfRange
| value > toInteger (maxBound :: Int64) = outOfRange
| otherwise = do
writeWord64At buffer position (fromIntegral value)
pure (position + 8)
where
outOfRange =
throw (userError "writeParquet: Integer value is outside the INT64 range")
{-# INLINE writeInteger64At #-}
writeFloatLE :: MemoryBuffer s -> Float -> ST s ()
writeFloatLE buffer = writeWord32LE buffer . castFloatToWord32
{-# INLINE writeFloatLE #-}
writeDoubleLE :: MemoryBuffer s -> Double -> ST s ()
writeDoubleLE buffer = writeWord64LE buffer . castDoubleToWord64
{-# INLINE writeDoubleLE #-}
flushBufferToBuffer :: MemoryBuffer s -> MemoryBuffer s -> ST s ()
flushBufferToBuffer source destination
| source.arrayRef == destination.arrayRef = pure ()
| otherwise = do
sourceArray <- readSTRef source.arrayRef
sourcePosition <- readSTRef source.positionRef
destinationPosition <- readSTRef destination.positionRef
destinationArray <-
ensureCapacity destination (destinationPosition + sourcePosition)
copyMutableByteArray
destinationArray
destinationPosition
sourceArray
0
sourcePosition
writeSTRef destination.positionRef (destinationPosition + sourcePosition)
writeSTRef source.positionRef 0
{-# INLINE flushBufferToBuffer #-}
bufferToByteString :: MemoryBuffer RealWorld -> IO ByteString
bufferToByteString buffer = do
array <- stToIO (readSTRef buffer.arrayRef)
position <- stToIO (readSTRef buffer.positionRef)
create position $ \dst ->
withMutableByteArrayContents array $ \src ->
copyBytes dst (castPtr src) position
-- | The buffer's contents as a ByteString, without copying.
unsafeBufferView :: MemoryBuffer RealWorld -> IO ByteString
unsafeBufferView buffer = do
array@(MutableByteArray array#) <- stToIO (readSTRef buffer.arrayRef)
position <- stToIO (readSTRef buffer.positionRef)
let Ptr address# = mutableByteArrayContents array
pure (BS (ForeignPtr address# (PlainPtr array#)) position)
bufferResidency :: MemoryBuffer s -> ST s Int
bufferResidency buffer = readSTRef buffer.positionRef
{-# INLINE bufferResidency #-}
resetPosition :: MemoryBuffer s -> ST s ()
resetPosition buffer = writeSTRef buffer.positionRef 0
{-# INLINE resetPosition #-}
-- Flush in 256 KiB chunks; larger blocks gave no write speed in dd tests.
flushBufferToFile :: WritableBinaryHandle -> MemoryBuffer RealWorld -> IO ()
flushBufferToFile (WritableBinaryHandle h) buffer = do
array <- stToIO (readSTRef buffer.arrayRef)
position <- stToIO (readSTRef buffer.positionRef)
withMutableByteArrayContents array $ \ptr -> do
let chunkSize = 262144
go offset
| offset >= position = pure ()
| otherwise = do
let n = min chunkSize (position - offset)
hPutBuf h (ptr `plusPtr` offset) n
go (offset + n)
go 0
stToIO (resetPosition buffer)
writeByteStringToFile :: WritableBinaryHandle -> ByteString -> IO ()
writeByteStringToFile (WritableBinaryHandle h) bs =
BU.unsafeUseAsCStringLen bs $ \(source, len) -> do
let chunkSize = 262144
go offset
| offset >= len = pure ()
| otherwise = do
let n = min chunkSize (len - offset)
hPutBuf h (source `plusPtr` offset) n
go (offset + n)
go 0
appendTextArraySlice :: MemoryBuffer s -> TA.Array -> Int -> Int -> ST s ()
appendTextArraySlice buffer source offset count
| count < 0 = throw (userError "appendTextArraySlice: negative length")
| otherwise = do
position <- readSTRef buffer.positionRef
array <- ensureCapacity buffer (position + count)
withMutableByteArrayContents array $ \destination ->
TA.copyToPointer source offset (destination `plusPtr` position) count
writeSTRef buffer.positionRef (position + count)
{-# INLINE appendTextArraySlice #-}