dataframe-parquet-1.5.1.0: src/DataFrame/IO/Utils/RandomAccess.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedRecordDot #-}
module DataFrame.IO.Utils.RandomAccess (
uncurry3,
Range (..),
RandomAccess (..),
ReaderIO (runReaderIO),
LocalFile,
MMappedFile,
unsafeToByteString,
WritableBinaryHandle,
openWritableBinaryFile,
withWritableBinaryFile,
atomicallyWriteFile,
MemoryBuffer (..),
ensureCapacity,
mallocBuffer,
writeByteString,
appendTextArraySlice,
writeWord8,
writeWord32LE,
writeWord64LE,
writeInteger64,
writeWord32At,
writeWord64At,
writeInteger64At,
writeFloatLE,
writeDoubleLE,
bufferResidency,
bufferToByteString,
flushBufferToBuffer,
resetPosition,
flushBufferToFile,
writeByteStringToFile,
) where
import Control.Exception (bracket, bracketOnError, finally)
import Control.Monad (when)
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Primitive (RealWorld)
import Control.Monad.ST (stToIO)
import Data.Bits (shiftR)
import qualified Data.ByteString as BS
import Data.ByteString.Internal (ByteString (PS), create)
import qualified Data.ByteString.Unsafe as BU
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Int (Int64)
import Data.Primitive.ByteArray (
MutableByteArray,
copyMutableByteArray,
getSizeofMutableByteArray,
newPinnedByteArray,
withMutableByteArrayContents,
writeByteArray,
)
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 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)
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 Buffer -----------------------------------------------------------------
-- Refer to DataFrame.IO.Parquet.Writer for a justification of what we're doing here
-- There's some overlap here with what's going on in Seeking.hs, so, if this bothers
-- us, eventually someone will have to come back and reconcile the writer buffer
-- approach with the reader oriented patterns in Seeking.hs.
--
-- We're using MutableByteArrays here for convenience and because we don't need
-- the more powerful abstractions vector provides (which uses ByteArrays internally)
--
-- since we want to use hPutBuf, we're going to need a Ptr, which means are ByteArrya
-- must be pinned. Now growing pinned arrays can be problematic, but in the vast majority
-- of cases we shouldn't be growing more than once, if that. See the docs for
-- Data.Primitive.ByteArray.byteArrayContents.
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 = MemoryBuffer
{ arrayRef :: !(IORef (MutableByteArray RealWorld))
, positionRef :: !(IORef Int)
}
mallocBuffer :: Int -> IO MemoryBuffer
mallocBuffer capacity
| capacity < 0 = ioError $ userError "mallocBuffer: negative capacity"
| otherwise = do
array <- newPinnedByteArray capacity
MemoryBuffer <$> newIORef array <*> newIORef 0
-- We're using pinned ByteArrays so we must
-- not use the grow function brovided by Data.Primitive
-- instead we must alloocate a new pinned ByteArray.
-- We might have been worried about heap fragmentation
-- because a single pinned object in a 4KB GHC block can
-- keep the whole plock alive but oyr buffers will tend to
-- be much larger than that.
-- But the memory usage will temporarily spike to 2.5x the size of
-- the buffer, but it should be fine since the current writer is single threaded
-- and grows *should* be rare.
-- If it becomes an issue we should start tracking an array of pointers
-- to buffers intsead of replacing them wholesale so grwoing a buffer
-- is just a matter of adding a new buffer to the array (which we can
-- pre-allocate to three elements to begin with and grow it only on the
-- off chance that a buffer required more than three grows).
ensureCapacity :: MemoryBuffer -> Int -> IO (MutableByteArray RealWorld)
ensureCapacity buffer needed = do
array <- readIORef buffer.arrayRef
maxSize <- getSizeofMutableByteArray array
if needed <= maxSize
then pure array
else do
position <- readIORef buffer.positionRef
grown <- newPinnedByteArray (needed + (needed `div` 2))
copyMutableByteArray grown 0 array 0 position
writeIORef buffer.arrayRef grown
pure grown
{-# INLINE ensureCapacity #-}
writeWord8 :: MemoryBuffer -> Word8 -> IO ()
writeWord8 buffer b = do
position <- readIORef buffer.positionRef
array <- ensureCapacity buffer (position + 1)
writeByteArray array position b
writeIORef buffer.positionRef (position + 1)
{-# INLINE writeWord8 #-}
writeByteString :: MemoryBuffer -> ByteString -> IO ()
writeByteString buffer bs =
BU.unsafeUseAsCStringLen bs $ \(source, len) -> do
position <- readIORef buffer.positionRef
array <- ensureCapacity buffer (position + len)
withMutableByteArrayContents array $ \dst ->
copyBytes (dst `plusPtr` position) (castPtr source) len
writeIORef buffer.positionRef (position + len)
{-# INLINE writeByteString #-}
writeWord32LE :: MemoryBuffer -> Word32 -> IO ()
writeWord32LE buffer w = do
position <- readIORef buffer.positionRef
writeWord32At buffer position w
writeIORef buffer.positionRef (position + 4)
{-# INLINE writeWord32LE #-}
writeWord64LE :: MemoryBuffer -> Word64 -> IO ()
writeWord64LE buffer w = do
position <- readIORef buffer.positionRef
writeWord64At buffer position w
writeIORef buffer.positionRef (position + 8)
{-# INLINE writeWord64LE #-}
writeWord32At :: MemoryBuffer -> Int -> Word32 -> IO ()
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 -> Int -> Word64 -> IO ()
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 -> Integer -> IO ()
writeInteger64 buffer value = do
position <- readIORef buffer.positionRef
newPosition <- writeInteger64At buffer position value
writeIORef buffer.positionRef newPosition
{-# INLINE writeInteger64 #-}
writeInteger64At :: MemoryBuffer -> Int -> Integer -> IO 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 =
ioError (userError "writeParquet: Integer value is outside the INT64 range")
{-# INLINE writeInteger64At #-}
writeFloatLE :: MemoryBuffer -> Float -> IO ()
writeFloatLE buffer = writeWord32LE buffer . castFloatToWord32
{-# INLINE writeFloatLE #-}
writeDoubleLE :: MemoryBuffer -> Double -> IO ()
writeDoubleLE buffer = writeWord64LE buffer . castDoubleToWord64
{-# INLINE writeDoubleLE #-}
flushBufferToBuffer :: MemoryBuffer -> MemoryBuffer -> IO ()
flushBufferToBuffer source destination
| source.arrayRef == destination.arrayRef = pure ()
| otherwise = do
sourceArray <- readIORef source.arrayRef
sourcePosition <- readIORef source.positionRef
destinationPosition <- readIORef destination.positionRef
destinationArray <-
ensureCapacity destination (destinationPosition + sourcePosition)
copyMutableByteArray
destinationArray
destinationPosition
sourceArray
0
sourcePosition
writeIORef destination.positionRef (destinationPosition + sourcePosition)
writeIORef source.positionRef 0
{-# INLINE flushBufferToBuffer #-}
bufferToByteString :: MemoryBuffer -> IO ByteString
bufferToByteString buffer = do
array <- readIORef buffer.arrayRef
position <- readIORef buffer.positionRef
create position $ \dst ->
withMutableByteArrayContents array $ \src ->
copyBytes dst (castPtr src) position
bufferResidency :: MemoryBuffer -> IO Int
bufferResidency buffer = readIORef buffer.positionRef
{-# INLINE bufferResidency #-}
resetPosition :: MemoryBuffer -> IO ()
resetPosition buffer = writeIORef buffer.positionRef 0
{-# INLINE resetPosition #-}
-- I tested write speeds by doing (on Apple Silicon)
-- `dd if=/dev/zero of=test bs={$n}k oflag=direct conv=fdatasync
-- Results:
--
-- ```
-- | block size | data (GiB) | time (s) | GiB/s |
-- |------------|------------|-----------|-------|
-- | 4k | 4.00 | 2.371 | 1.69 |
-- | 8k | 4.00 | 1.486 | 2.69 |
-- | 16k | 4.00 | 1.045 | 3.83 |
-- | 32k | 4.00 | 0.740 | 5.40 |
-- | 64k | 4.00 | 0.675 | 5.92 |
-- | 128k | 4.00 | 0.669 | 5.98 |
-- | 256k | 4.00 | 0.664 | 6.03 |
-- | 512k | 4.00 | 0.670 | 5.97 |
-- | 1024k | 4.00 | 0.664 | 6.02 |
-- | 4096k | 4.00 | 0.668 | 5.99 |
-- ```
-- So when writing to a file to minimize syscall overhead while
-- trying not to create dirty pages in the kernel page cache, we'll
-- be flushing in 256 KiB chunks.
flushBufferToFile :: WritableBinaryHandle -> MemoryBuffer -> IO ()
flushBufferToFile (WritableBinaryHandle h) buffer = do
array <- readIORef buffer.arrayRef
position <- readIORef 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
writeIORef buffer.positionRef 0
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 -> TA.Array -> Int -> Int -> IO ()
appendTextArraySlice buffer source offset count
| count < 0 = ioError $ userError "appendTextArraySlice: negative length"
| otherwise = do
position <- readIORef buffer.positionRef
array <- ensureCapacity buffer (position + count)
withMutableByteArrayContents array $ \destination ->
stToIO (TA.copyToPointer source offset (destination `plusPtr` position) count)
writeIORef buffer.positionRef (position + count)
{-# INLINE appendTextArraySlice #-}