-
Notifications
You must be signed in to change notification settings - Fork 51
Initial Implementation of the Parquet Writer #215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sharmrj
wants to merge
19
commits into
DataHaskell:main
Choose a base branch
from
sharmrj:parquet-writer
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
85c6e4a
WIP: Initial sketch of the parquet writer design
sharmrj 89d7b6d
WIP Parquet Writer (does not compile)
sharmrj e8c2b85
Implemented HasBuffer instance for the file backed buffer; Cleaned up…
sharmrj c3fc6c3
Tests for `HasBuffer` instances
sharmrj f292f51
Changes to make Writer.hs compile so we can run tests
sharmrj 00f105b
fix conflicts with main
sharmrj 167ab08
Fixed ensureCapacity so that it works correctly with pinned ByteArray…
sharmrj 6fd2e80
implement a cleaned up Parquet Writer
sharmrj f76f518
added some comments
sharmrj ad47e3b
added benchmarks and a stress test
sharmrj 237a139
Optimized hotspots
sharmrj 995fc7b
ran fourmolu
sharmrj 74ff879
hlint
sharmrj ddab22c
fourmolu again
sharmrj bb46712
parquet.thrift was committed by accident
sharmrj db19108
made sure test parquet files can be recognized in CI.
sharmrj 31b054a
Changed dataframe-parquet.cabal after the recommendations from CI's c…
sharmrj fa0aeee
removed ghcoptions from dataframe-parquet-10gb stress so CI wont reje…
sharmrj 0479679
Removed the comment at the beginning of Writer.hs
sharmrj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| module Main (main) where | ||
|
|
||
| import Control.DeepSeq (NFData (rnf)) | ||
| import Criterion.Main (bench, defaultMain, envWithCleanup, whnfIO) | ||
| import DataFrame.IO.Parquet.Writer (writeParquet) | ||
| import DataFrame.Internal.DataFrame (DataFrame, forceDataFrame) | ||
| import DataFrame10GB (stressDataFrame) | ||
| import System.Directory (removeDirectoryRecursive) | ||
| import System.FilePath ((</>)) | ||
| import System.IO.Temp (createTempDirectory, getCanonicalTemporaryDirectory) | ||
|
|
||
| data BenchmarkEnvironment = BenchmarkEnvironment | ||
| { benchmarkDataFrame :: DataFrame | ||
| , benchmarkDirectory :: FilePath | ||
| , benchmarkOutput :: FilePath | ||
| } | ||
|
|
||
| instance NFData BenchmarkEnvironment where | ||
| rnf environment = | ||
| forceDataFrame (benchmarkDataFrame environment) `seq` | ||
| rnf (benchmarkDirectory environment) `seq` | ||
| rnf (benchmarkOutput environment) | ||
|
|
||
| prepareEnvironment :: IO BenchmarkEnvironment | ||
| prepareEnvironment = do | ||
| temporary <- getCanonicalTemporaryDirectory | ||
| directory <- createTempDirectory temporary "dataframe-parquet-writer-10gb" | ||
| pure | ||
| BenchmarkEnvironment | ||
| { benchmarkDataFrame = stressDataFrame | ||
| , benchmarkDirectory = directory | ||
| , benchmarkOutput = directory </> "benchmark.parquet" | ||
| } | ||
|
|
||
| cleanupEnvironment :: BenchmarkEnvironment -> IO () | ||
| cleanupEnvironment = removeDirectoryRecursive . benchmarkDirectory | ||
|
|
||
| main :: IO () | ||
| main = | ||
| defaultMain | ||
| [ envWithCleanup prepareEnvironment cleanupEnvironment $ \environment -> | ||
| -- Memory usage for this benchmark will be north of 20 GB. | ||
| bench "write 10 GiB dataframe" $ | ||
| whnfIO | ||
| ( writeParquet | ||
| (benchmarkOutput environment) | ||
| (benchmarkDataFrame environment) | ||
| ) | ||
| ] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| {-# LANGUAGE OverloadedRecordDot #-} | ||
| {-# LANGUAGE OverloadedStrings #-} | ||
|
|
||
| module DataFrame.IO.Parquet.Writer ( | ||
| writeParquet, | ||
| writeParquetWithOptions, | ||
| ParquetWriteOptions (..), | ||
| WriterStrategy (..), | ||
| defaultParquetWriteOptions, | ||
| ) where | ||
|
|
||
| import Control.Monad (when) | ||
| import qualified Data.ByteString as BS | ||
| import Data.IORef (IORef, modifyIORef', newIORef, readIORef, writeIORef) | ||
| import Data.Int (Int64) | ||
| import Data.Maybe (fromJust) | ||
| import qualified Data.Vector as VB | ||
| import DataFrame.IO.Parquet.Thrift | ||
| import DataFrame.IO.Parquet.Writer.ColumnChunkWriter ( | ||
| ColumnChunkState (..), | ||
| bufferedSize, | ||
| finalizePage, | ||
| initColumnState, | ||
| runColumnChunkWriter, | ||
| writeRowAndMaybeFinalize, | ||
| ) | ||
| import DataFrame.IO.Parquet.Writer.Encoder (Encoder (..)) | ||
| import DataFrame.IO.Parquet.Writer.Metadata (magic, rootSchemaElement) | ||
| import DataFrame.IO.Parquet.Writer.Options ( | ||
| ParquetWriteOptions (..), | ||
| WriterStrategy (..), | ||
| defaultParquetWriteOptions, | ||
| ) | ||
| import DataFrame.IO.Utils.RandomAccess ( | ||
| WritableBinaryHandle, | ||
| bufferResidency, | ||
| flushBufferToFile, | ||
| mallocBuffer, | ||
| onBuffer, | ||
| putByteString, | ||
| putWord32LE, | ||
| withWritableBinaryFile, | ||
| writeByteStringToFile, | ||
| ) | ||
| import DataFrame.Internal.DataFrame ( | ||
| DataFrame, | ||
| columnNames, | ||
| dataframeDimensions, | ||
| getColumn, | ||
| ) | ||
| import Pinch (enum, putField) | ||
| import qualified Pinch | ||
|
|
||
| writeParquet :: FilePath -> DataFrame -> IO () | ||
| writeParquet = writeParquetWithOptions defaultParquetWriteOptions | ||
|
|
||
| writeParquetWithOptions :: ParquetWriteOptions -> FilePath -> DataFrame -> IO () | ||
| writeParquetWithOptions opts path df = do | ||
| when (opts.strategy == TwoPass) $ | ||
| error "writeParquet: TwoPass strategy is not yet implemented" | ||
| let (nRows, _) = dataframeDimensions df | ||
| names = columnNames df | ||
| cols <- | ||
| VB.fromList | ||
| <$> mapM (\n -> initColumnState opts n (fromJust (getColumn n df))) names | ||
| withWritableBinaryFile path $ \out -> do | ||
| writeByteStringToFile out magic | ||
| fileOff <- newIORef 4 | ||
| rowGroupsRef <- newIORef [] -- Row Group Metadata | ||
| rgRowsRef <- newIORef 0 | ||
| let st = WriterState out cols fileOff rowGroupsRef rgRowsRef | ||
| interval = max 1 opts.batchRows | ||
| loop row | ||
| | row >= nRows = pure () | ||
| | otherwise = do | ||
| -- go row by row and append to a pgee | ||
| -- When a page is full (frome the page size writer option) flush it to its ColumnChunk | ||
| -- When all the columnChunks combined match or exceed the row group size option | ||
| -- flush all the columnchunks to file one by one | ||
| VB.forM_ cols (writeRowAndMaybeFinalize opts row) | ||
| modifyIORef' rgRowsRef (+ 1) | ||
| when ((row + 1) `mod` interval == 0) $ do | ||
| size <- bufferedSize cols | ||
| when (size >= opts.rowGroupSize) (finalizeRowGroup opts st) | ||
| loop (row + 1) | ||
| loop 0 | ||
| finalizeRowGroup opts st | ||
| writeFooter st nRows | ||
|
|
||
| data WriterState = WriterState | ||
| { wsOut :: !WritableBinaryHandle | ||
| , wsCols :: !(VB.Vector ColumnChunkState) | ||
| , wsFileOffset :: !(IORef Int64) | ||
| , wsRowGroups :: !(IORef [RowGroup]) | ||
| , wsRgRows :: !(IORef Int) | ||
| } | ||
|
|
||
| finalizeRowGroup :: ParquetWriteOptions -> WriterState -> IO () | ||
| finalizeRowGroup opts st = do | ||
| rgRows <- readIORef st.wsRgRows | ||
| when (rgRows > 0) $ do | ||
| VB.mapM_ (runColumnChunkWriter (finalizePage opts.compressionCodec)) st.wsCols | ||
| (chunksRev, total) <- | ||
| VB.foldM' | ||
| ( \(acc, totalSize) cs -> do | ||
| offset <- readIORef st.wsFileOffset | ||
| size <- bufferResidency (ckBuffer cs) | ||
| uncompressed <- readIORef (ckUncompressed cs) | ||
| flushBufferToFile st.wsOut (ckBuffer cs) | ||
| writeIORef st.wsFileOffset (offset + fromIntegral size) | ||
| writeIORef (ckUncompressed cs) 0 | ||
| let chunk = mkColumnChunk opts offset size uncompressed rgRows cs | ||
| pure (chunk : acc, totalSize + fromIntegral size) | ||
| ) | ||
| ([], 0 :: Int64) | ||
| st.wsCols | ||
| modifyIORef' st.wsRowGroups (mkRowGroup (reverse chunksRev) total rgRows :) | ||
| writeIORef st.wsRgRows 0 | ||
|
|
||
| mkColumnChunk :: | ||
| ParquetWriteOptions -> | ||
| Int64 -> | ||
| Int -> | ||
| Int64 -> | ||
| Int -> | ||
| ColumnChunkState -> | ||
| ColumnChunk | ||
| mkColumnChunk opts offset size uncompressed rgRows cs = | ||
| ColumnChunk | ||
| { cc_file_path = putField Nothing | ||
| , cc_file_offset = putField offset | ||
| , cc_meta_data = putField (Just metadata) | ||
| , cc_offset_index_offset = putField Nothing | ||
| , cc_offset_index_length = putField Nothing | ||
| , cc_column_index_offset = putField Nothing | ||
| , cc_column_index_length = putField Nothing | ||
| , cc_crypto_metadata = putField Nothing | ||
| , cc_encrypted_column_metadata = putField Nothing | ||
| } | ||
| where | ||
| metadata = | ||
| ColumnMetaData | ||
| { cmd_type = putField (ckEncoder cs).encType | ||
| , cmd_encodings = putField [PLAIN enum, RLE enum] | ||
| , cmd_path_in_schema = putField [ckName cs] | ||
| , cmd_codec = putField opts.compressionCodec | ||
| , cmd_num_values = putField (fromIntegral rgRows) | ||
| , cmd_total_uncompressed_size = putField uncompressed | ||
| , cmd_total_compressed_size = putField (fromIntegral size) | ||
| , cmd_key_value_metadata = putField Nothing | ||
| , cmd_data_page_offset = putField offset | ||
| , cmd_index_page_offset = putField Nothing | ||
| , cmd_dictionary_page_offset = putField Nothing | ||
| , cmd_statistics = putField Nothing | ||
| , cmd_encoding_stats = putField Nothing | ||
| , cmd_bloom_filter_offset = putField Nothing | ||
| , cmd_bloom_filter_length = putField Nothing | ||
| } | ||
|
|
||
| mkRowGroup :: [ColumnChunk] -> Int64 -> Int -> RowGroup | ||
| mkRowGroup chunks total rgRows = | ||
| RowGroup | ||
| { rg_columns = putField chunks | ||
| , rg_total_byte_size = putField total | ||
| , rg_num_rows = putField (fromIntegral rgRows) | ||
| , rg_sorting_columns = putField Nothing | ||
| , rg_file_offset = putField Nothing | ||
| , rg_total_compressed_size = putField (Just total) | ||
| , rg_ordinal = putField Nothing | ||
| } | ||
|
|
||
| writeFooter :: WriterState -> Int -> IO () | ||
| writeFooter st nRows = do | ||
| rowGroups <- reverse <$> readIORef st.wsRowGroups | ||
| let schemaElements = | ||
| rootSchemaElement (VB.length st.wsCols) : VB.toList (VB.map ckSchema st.wsCols) | ||
| metadata = | ||
| FileMetadata | ||
| { version = putField 1 | ||
| , schema = putField schemaElements | ||
| , num_rows = putField (fromIntegral nRows) | ||
| , row_groups = putField rowGroups | ||
| , key_value_metadata = putField Nothing | ||
| , created_by = putField (Just "dataframe-parquet") | ||
| , column_orders = putField Nothing | ||
| , encryption_algorithm = putField Nothing | ||
| , footer_signing_key_metadata = putField Nothing | ||
| } | ||
| footer = Pinch.encode Pinch.compactProtocol metadata | ||
| buffer <- mallocBuffer (BS.length footer + 8) | ||
| onBuffer buffer $ do | ||
| putByteString footer | ||
| putWord32LE (fromIntegral (BS.length footer)) | ||
| putByteString magic | ||
| flushBufferToFile st.wsOut buffer |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this evaluate it enough that this benchmark is meaningful?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think
whnfIOdoes enforce the IO action as it's just(). But I ran it withnfIOanyway just to make sure and got a similar result: