From c9325209601e217ca327f91b322360bcc5ca3102 Mon Sep 17 00:00:00 2001 From: Nick Terrell Date: Thu, 1 Sep 2016 15:22:19 -0700 Subject: [PATCH] Add PZstandard to contrib/ --- contrib/pzstd/ErrorHolder.h | 55 +++ contrib/pzstd/Makefile | 71 +++ contrib/pzstd/Options.cpp | 182 ++++++++ contrib/pzstd/Options.h | 60 +++ contrib/pzstd/Pzstd.cpp | 462 ++++++++++++++++++++ contrib/pzstd/Pzstd.h | 93 ++++ contrib/pzstd/README.md | 47 ++ contrib/pzstd/SkippableFrame.cpp | 30 ++ contrib/pzstd/SkippableFrame.h | 64 +++ contrib/pzstd/bench.cpp | 146 +++++++ contrib/pzstd/images/Cspeed.png | Bin 0 -> 58612 bytes contrib/pzstd/images/Dspeed.png | Bin 0 -> 26335 bytes contrib/pzstd/main.cpp | 34 ++ contrib/pzstd/test/Makefile | 46 ++ contrib/pzstd/test/OptionsTest.cpp | 179 ++++++++ contrib/pzstd/test/PzstdTest.cpp | 112 +++++ contrib/pzstd/test/RoundTrip.h | 89 ++++ contrib/pzstd/test/RoundTripTest.cpp | 88 ++++ contrib/pzstd/utils/Buffer.h | 99 +++++ contrib/pzstd/utils/FileSystem.h | 61 +++ contrib/pzstd/utils/Likely.h | 28 ++ contrib/pzstd/utils/Range.h | 130 ++++++ contrib/pzstd/utils/ScopeGuard.h | 50 +++ contrib/pzstd/utils/ThreadPool.h | 58 +++ contrib/pzstd/utils/WorkQueue.h | 144 ++++++ contrib/pzstd/utils/test/BufferTest.cpp | 89 ++++ contrib/pzstd/utils/test/Makefile | 41 ++ contrib/pzstd/utils/test/RangeTest.cpp | 82 ++++ contrib/pzstd/utils/test/ScopeGuardTest.cpp | 28 ++ contrib/pzstd/utils/test/ThreadPoolTest.cpp | 67 +++ contrib/pzstd/utils/test/WorkQueueTest.cpp | 176 ++++++++ 31 files changed, 2811 insertions(+) create mode 100644 contrib/pzstd/ErrorHolder.h create mode 100644 contrib/pzstd/Makefile create mode 100644 contrib/pzstd/Options.cpp create mode 100644 contrib/pzstd/Options.h create mode 100644 contrib/pzstd/Pzstd.cpp create mode 100644 contrib/pzstd/Pzstd.h create mode 100644 contrib/pzstd/README.md create mode 100644 contrib/pzstd/SkippableFrame.cpp create mode 100644 contrib/pzstd/SkippableFrame.h create mode 100644 contrib/pzstd/bench.cpp create mode 100644 contrib/pzstd/images/Cspeed.png create mode 100644 contrib/pzstd/images/Dspeed.png create mode 100644 contrib/pzstd/main.cpp create mode 100644 contrib/pzstd/test/Makefile create mode 100644 contrib/pzstd/test/OptionsTest.cpp create mode 100644 contrib/pzstd/test/PzstdTest.cpp create mode 100644 contrib/pzstd/test/RoundTrip.h create mode 100644 contrib/pzstd/test/RoundTripTest.cpp create mode 100644 contrib/pzstd/utils/Buffer.h create mode 100644 contrib/pzstd/utils/FileSystem.h create mode 100644 contrib/pzstd/utils/Likely.h create mode 100644 contrib/pzstd/utils/Range.h create mode 100644 contrib/pzstd/utils/ScopeGuard.h create mode 100644 contrib/pzstd/utils/ThreadPool.h create mode 100644 contrib/pzstd/utils/WorkQueue.h create mode 100644 contrib/pzstd/utils/test/BufferTest.cpp create mode 100644 contrib/pzstd/utils/test/Makefile create mode 100644 contrib/pzstd/utils/test/RangeTest.cpp create mode 100644 contrib/pzstd/utils/test/ScopeGuardTest.cpp create mode 100644 contrib/pzstd/utils/test/ThreadPoolTest.cpp create mode 100644 contrib/pzstd/utils/test/WorkQueueTest.cpp diff --git a/contrib/pzstd/ErrorHolder.h b/contrib/pzstd/ErrorHolder.h new file mode 100644 index 000000000..4a81a068c --- /dev/null +++ b/contrib/pzstd/ErrorHolder.h @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#pragma once + +#include +#include +#include + +namespace pzstd { + +// Coordinates graceful shutdown of the pzstd pipeline +class ErrorHolder { + std::atomic error_; + std::string message_; + + public: + ErrorHolder() : error_(false) {} + + bool hasError() noexcept { + return error_.load(); + } + + void setError(std::string message) noexcept { + // Given multiple possibly concurrent calls, exactly one will ever succeed. + bool expected = false; + if (error_.compare_exchange_strong(expected, true)) { + message_ = std::move(message); + } + } + + bool check(bool predicate, std::string message) noexcept { + if (!predicate) { + setError(std::move(message)); + } + return !hasError(); + } + + std::string getError() noexcept { + error_.store(false); + return std::move(message_); + } + + ~ErrorHolder() { + if (hasError()) { + throw std::logic_error(message_); + } + } +}; +} diff --git a/contrib/pzstd/Makefile b/contrib/pzstd/Makefile new file mode 100644 index 000000000..512a76292 --- /dev/null +++ b/contrib/pzstd/Makefile @@ -0,0 +1,71 @@ +# ########################################################################## +# Copyright (c) 2016-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. +# ########################################################################## + +ZSTDDIR = ../../lib +PROGDIR = ../../programs + +CPPFLAGS = -I$(ZSTDDIR) -I$(ZSTDDIR)/common -I$(ZSTDDIR)/dictBuilder -I$(PROGDIR) -I. +CFLAGS ?= -O3 +CFLAGS += -Wall -Wextra -Wcast-qual -Wcast-align -Wstrict-aliasing=1 \ + -Wswitch-enum -Wdeclaration-after-statement -Wstrict-prototypes -Wundef \ + -std=c++11 +CFLAGS += $(MOREFLAGS) +FLAGS = $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) + + +ZSTDCOMMON_FILES := $(ZSTDDIR)/common/*.c +ZSTDCOMP_FILES := $(ZSTDDIR)/compress/zstd_compress.c $(ZSTDDIR)/compress/fse_compress.c $(ZSTDDIR)/compress/huf_compress.c +ZSTDDECOMP_FILES := $(ZSTDDIR)/decompress/huf_decompress.c +ZSTD_FILES := $(ZSTDDECOMP_FILES) $(ZSTDCOMMON_FILES) $(ZSTDCOMP_FILES) + + +# Define *.exe as extension for Windows systems +ifneq (,$(filter Windows%,$(OS))) +EXT =.exe +else +EXT = +endif + +.PHONY: default all test clean + +default: pzstd + +all: pzstd + + +libzstd.a: $(ZSTD_FILES) + $(MAKE) -C $(ZSTDDIR) libzstd + @cp $(ZSTDDIR)/libzstd.a . + + +Pzstd.o: Pzstd.h Pzstd.cpp ErrorHolder.h utils/*.h + $(CXX) $(FLAGS) -c Pzstd.cpp -o $@ + +SkippableFrame.o: SkippableFrame.h SkippableFrame.cpp utils/*.h + $(CXX) $(FLAGS) -c SkippableFrame.cpp -o $@ + +Options.o: Options.h Options.cpp + $(CXX) $(FLAGS) -c Options.cpp -o $@ + +main.o: main.cpp *.h utils/*.h + $(CXX) $(FLAGS) -c main.cpp -o $@ + +pzstd: libzstd.a Pzstd.o SkippableFrame.o Options.o main.o + $(CXX) $(FLAGS) $^ -o $@$(EXT) + +test: libzstd.a Pzstd.o Options.o SkippableFrame.o + $(MAKE) -C utils/test test + $(MAKE) -C test test + +clean: + $(MAKE) -C $(ZSTDDIR) clean + $(MAKE) -C utils/test clean + $(MAKE) -C test clean + @$(RM) libzstd.a *.o pzstd$(EXT) + @echo Cleaning completed diff --git a/contrib/pzstd/Options.cpp b/contrib/pzstd/Options.cpp new file mode 100644 index 000000000..dc6aeef14 --- /dev/null +++ b/contrib/pzstd/Options.cpp @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#include "Options.h" + +#include + +namespace pzstd { + +namespace { +unsigned parseUnsigned(const char* arg) { + unsigned result = 0; + while (*arg >= '0' && *arg <= '9') { + result *= 10; + result += *arg - '0'; + ++arg; + } + return result; +} + +const std::string zstdExtension = ".zst"; +constexpr unsigned defaultCompressionLevel = 3; +constexpr unsigned maxNonUltraCompressionLevel = 19; + +void usage() { + std::fprintf(stderr, "Usage:\n"); + std::fprintf(stderr, "\tpzstd [args] FILE\n"); + std::fprintf(stderr, "Parallel ZSTD options:\n"); + std::fprintf(stderr, "\t-n/--num-threads #: Number of threads to spawn\n"); + std::fprintf(stderr, "\t-p/--pzstd-headers: Write pzstd headers to enable parallel decompression\n"); + + std::fprintf(stderr, "ZSTD options:\n"); + std::fprintf(stderr, "\t-u/--ultra : enable levels beyond %i, up to %i (requires more memory)\n", maxNonUltraCompressionLevel, ZSTD_maxCLevel()); + std::fprintf(stderr, "\t-h/--help : display help and exit\n"); + std::fprintf(stderr, "\t-V/--version : display version number and exit\n"); + std::fprintf(stderr, "\t-d/--decompress : decompression\n"); + std::fprintf(stderr, "\t-f/--force : overwrite output\n"); + std::fprintf(stderr, "\t-o/--output file : result stored into `file`\n"); + std::fprintf(stderr, "\t-c/--stdout : write output to standard output\n"); + std::fprintf(stderr, "\t-# : # compression level (1-%d, default:%d)\n", maxNonUltraCompressionLevel, defaultCompressionLevel); +} +} // anonymous namespace + +Options::Options() + : numThreads(0), + maxWindowLog(23), + compressionLevel(defaultCompressionLevel), + decompress(false), + overwrite(false), + pzstdHeaders(false) {} + +bool Options::parse(int argc, const char** argv) { + bool ultra = false; + for (int i = 1; i < argc; ++i) { + const char* arg = argv[i]; + // Arguments with a short option + char option = 0; + if (!std::strcmp(arg, "--num-threads")) { + option = 'n'; + } else if (!std::strcmp(arg, "--pzstd-headers")) { + option = 'p'; + } else if (!std::strcmp(arg, "--ultra")) { + option = 'u'; + } else if (!std::strcmp(arg, "--version")) { + option = 'V'; + } else if (!std::strcmp(arg, "--help")) { + option = 'h'; + } else if (!std::strcmp(arg, "--decompress")) { + option = 'd'; + } else if (!std::strcmp(arg, "--force")) { + option = 'f'; + } else if (!std::strcmp(arg, "--output")) { + option = 'o'; + } else if (!std::strcmp(arg, "--stdout")) { + option = 'c'; + }else if (arg[0] == '-' && arg[1] != 0) { + // Parse the compression level or short option + if (arg[1] >= '0' && arg[1] <= '9') { + compressionLevel = parseUnsigned(arg + 1); + continue; + } + option = arg[1]; + } else if (inputFile.empty()) { + inputFile = arg; + continue; + } else { + std::fprintf(stderr, "Invalid argument: %s.\n", arg); + return false; + } + + switch (option) { + case 'n': + if (++i == argc) { + std::fprintf(stderr, "Invalid argument: -n requires an argument.\n"); + return false; + } + numThreads = parseUnsigned(argv[i]); + if (numThreads == 0) { + std::fprintf(stderr, "Invalid argument: # of threads must be > 0.\n"); + } + break; + case 'p': + pzstdHeaders = true; + break; + case 'u': + ultra = true; + maxWindowLog = 0; + break; + case 'V': + std::fprintf(stderr, "ZSTD version: %s.\n", ZSTD_VERSION_STRING); + return false; + case 'h': + usage(); + return false; + case 'd': + decompress = true; + break; + case 'f': + overwrite = true; + break; + case 'o': + if (++i == argc) { + std::fprintf(stderr, "Invalid argument: -o requires an argument.\n"); + return false; + } + outputFile = argv[i]; + break; + case 'c': + outputFile = '-'; + break; + default: + std::fprintf(stderr, "Invalid argument: %s.\n", arg); + return false; + } + } + // Determine input file if not specified + if (inputFile.empty()) { + inputFile = "-"; + } + // Determine output file if not specified + if (outputFile.empty()) { + if (inputFile == "-") { + std::fprintf( + stderr, + "Invalid arguments: Reading from stdin, but -o not provided.\n"); + return false; + } + // Attempt to add/remove zstd extension from the input file + if (decompress) { + int stemSize = inputFile.size() - zstdExtension.size(); + if (stemSize > 0 && inputFile.substr(stemSize) == zstdExtension) { + outputFile = inputFile.substr(0, stemSize); + } else { + std::fprintf( + stderr, "Invalid argument: Unable to determine output file.\n"); + return false; + } + } else { + outputFile = inputFile + zstdExtension; + } + } + // Check compression level + { + unsigned maxCLevel = ultra ? ZSTD_maxCLevel() : maxNonUltraCompressionLevel; + if (compressionLevel > maxCLevel) { + std::fprintf( + stderr, "Invalid compression level %u.\n", compressionLevel); + } + } + // Check that numThreads is set + if (numThreads == 0) { + std::fprintf(stderr, "Invalid arguments: # of threads not specified.\n"); + return false; + } + return true; +} +} diff --git a/contrib/pzstd/Options.h b/contrib/pzstd/Options.h new file mode 100644 index 000000000..47c5f78a6 --- /dev/null +++ b/contrib/pzstd/Options.h @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#pragma once + +#define ZSTD_STATIC_LINKING_ONLY +#include "zstd.h" +#undef ZSTD_STATIC_LINKING_ONLY + +#include +#include + +namespace pzstd { + +struct Options { + unsigned numThreads; + unsigned maxWindowLog; + unsigned compressionLevel; + bool decompress; + std::string inputFile; + std::string outputFile; + bool overwrite; + bool pzstdHeaders; + + Options(); + Options( + unsigned numThreads, + unsigned maxWindowLog, + unsigned compressionLevel, + bool decompress, + const std::string& inputFile, + const std::string& outputFile, + bool overwrite, + bool pzstdHeaders) + : numThreads(numThreads), + maxWindowLog(maxWindowLog), + compressionLevel(compressionLevel), + decompress(decompress), + inputFile(inputFile), + outputFile(outputFile), + overwrite(overwrite), + pzstdHeaders(pzstdHeaders) {} + + bool parse(int argc, const char** argv); + + ZSTD_parameters determineParameters() const { + ZSTD_parameters params = ZSTD_getParams(compressionLevel, 0, 0); + if (maxWindowLog != 0 && params.cParams.windowLog > maxWindowLog) { + params.cParams.windowLog = maxWindowLog; + params.cParams = ZSTD_adjustCParams(params.cParams, 0, 0); + } + return params; + } +}; +} diff --git a/contrib/pzstd/Pzstd.cpp b/contrib/pzstd/Pzstd.cpp new file mode 100644 index 000000000..84f6a2e4c --- /dev/null +++ b/contrib/pzstd/Pzstd.cpp @@ -0,0 +1,462 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#include "Pzstd.h" +#include "SkippableFrame.h" +#include "utils/FileSystem.h" +#include "utils/Range.h" +#include "utils/ScopeGuard.h" +#include "utils/ThreadPool.h" +#include "utils/WorkQueue.h" + +#include +#include +#include +#include + +namespace pzstd { + +namespace { +#ifdef _WIN32 +const std::string nullOutput = "nul"; +#else +const std::string nullOutput = "/dev/null"; +#endif +} + +using std::size_t; + +size_t pzstdMain(const Options& options, ErrorHolder& errorHolder) { + // Open the input file and attempt to determine its size + FILE* inputFd = stdin; + size_t inputSize = 0; + if (options.inputFile != "-") { + inputFd = std::fopen(options.inputFile.c_str(), "rb"); + if (!errorHolder.check(inputFd != nullptr, "Failed to open input file")) { + return 0; + } + std::error_code ec; + inputSize = file_size(options.inputFile, ec); + if (ec) { + inputSize = 0; + } + } + auto closeInputGuard = makeScopeGuard([&] { std::fclose(inputFd); }); + + // Check if the output file exists and then open it + FILE* outputFd = stdout; + if (options.outputFile != "-") { + if (!options.overwrite && options.outputFile != nullOutput) { + outputFd = std::fopen(options.outputFile.c_str(), "rb"); + if (!errorHolder.check(outputFd == nullptr, "Output file exists")) { + return 0; + } + } + outputFd = std::fopen(options.outputFile.c_str(), "wb"); + if (!errorHolder.check( + outputFd != nullptr, "Failed to open output file")) { + return 0; + } + } + auto closeOutputGuard = makeScopeGuard([&] { std::fclose(outputFd); }); + + // WorkQueue outlives ThreadPool so in the case of error we are certain + // we don't accidently try to call push() on it after it is destroyed. + WorkQueue> outs; + size_t bytesWritten; + { + // Initialize the thread pool with numThreads + ThreadPool executor(options.numThreads); + if (!options.decompress) { + // Add a job that reads the input and starts all the compression jobs + executor.add( + [&errorHolder, &outs, &executor, inputFd, inputSize, &options] { + asyncCompressChunks( + errorHolder, + outs, + executor, + inputFd, + inputSize, + options.numThreads, + options.determineParameters()); + }); + // Start writing + bytesWritten = + writeFile(errorHolder, outs, outputFd, options.pzstdHeaders); + } else { + // Add a job that reads the input and starts all the decompression jobs + executor.add([&errorHolder, &outs, &executor, inputFd] { + asyncDecompressFrames(errorHolder, outs, executor, inputFd); + }); + // Start writing + bytesWritten = writeFile( + errorHolder, outs, outputFd, /* writeSkippableFrames */ false); + } + } + return bytesWritten; +} + +/// Construct a `ZSTD_inBuffer` that points to the data in `buffer`. +static ZSTD_inBuffer makeZstdInBuffer(const Buffer& buffer) { + return ZSTD_inBuffer{buffer.data(), buffer.size(), 0}; +} + +/** + * Advance `buffer` and `inBuffer` by the amount of data read, as indicated by + * `inBuffer.pos`. + */ +void advance(Buffer& buffer, ZSTD_inBuffer& inBuffer) { + auto pos = inBuffer.pos; + inBuffer.src = static_cast(inBuffer.src) + pos; + inBuffer.size -= pos; + inBuffer.pos = 0; + return buffer.advance(pos); +} + +/// Construct a `ZSTD_outBuffer` that points to the data in `buffer`. +static ZSTD_outBuffer makeZstdOutBuffer(Buffer& buffer) { + return ZSTD_outBuffer{buffer.data(), buffer.size(), 0}; +} + +/** + * Split `buffer` and advance `outBuffer` by the amount of data written, as + * indicated by `outBuffer.pos`. + */ +Buffer split(Buffer& buffer, ZSTD_outBuffer& outBuffer) { + auto pos = outBuffer.pos; + outBuffer.dst = static_cast(outBuffer.dst) + pos; + outBuffer.size -= pos; + outBuffer.pos = 0; + return buffer.splitAt(pos); +} + +/** + * Stream chunks of input from `in`, compress it, and stream it out to `out`. + * + * @param errorHolder Used to report errors and check if an error occured + * @param in Queue that we `pop()` input buffers from + * @param out Queue that we `push()` compressed output buffers to + * @param maxInputSize An upper bound on the size of the input + * @param parameters The zstd parameters to use for compression + */ +static void compress( + ErrorHolder& errorHolder, + std::shared_ptr in, + std::shared_ptr out, + size_t maxInputSize, + ZSTD_parameters parameters) { + auto guard = makeScopeGuard([&] { out->finish(); }); + // Initialize the CCtx + std::unique_ptr ctx( + ZSTD_createCStream(), ZSTD_freeCStream); + if (!errorHolder.check(ctx != nullptr, "Failed to allocate ZSTD_CStream")) { + return; + } + { + auto err = ZSTD_initCStream_advanced(ctx.get(), nullptr, 0, parameters, 0); + if (!errorHolder.check(!ZSTD_isError(err), ZSTD_getErrorName(err))) { + return; + } + } + + // Allocate space for the result + auto outBuffer = Buffer(ZSTD_compressBound(maxInputSize)); + auto zstdOutBuffer = makeZstdOutBuffer(outBuffer); + { + Buffer inBuffer; + // Read a buffer in from the input queue + while (in->pop(inBuffer) && !errorHolder.hasError()) { + auto zstdInBuffer = makeZstdInBuffer(inBuffer); + // Compress the whole buffer and send it to the output queue + while (!inBuffer.empty() && !errorHolder.hasError()) { + if (!errorHolder.check( + !outBuffer.empty(), "ZSTD_compressBound() was too small")) { + return; + } + // Compress + auto err = + ZSTD_compressStream(ctx.get(), &zstdOutBuffer, &zstdInBuffer); + if (!errorHolder.check(!ZSTD_isError(err), ZSTD_getErrorName(err))) { + return; + } + // Split the compressed data off outBuffer and pass to the output queue + out->push(split(outBuffer, zstdOutBuffer)); + // Forget about the data we already compressed + advance(inBuffer, zstdInBuffer); + } + } + } + // Write the epilog + size_t bytesLeft; + do { + if (!errorHolder.check( + !outBuffer.empty(), "ZSTD_compressBound() was too small")) { + return; + } + bytesLeft = ZSTD_endStream(ctx.get(), &zstdOutBuffer); + if (!errorHolder.check( + !ZSTD_isError(bytesLeft), ZSTD_getErrorName(bytesLeft))) { + return; + } + out->push(split(outBuffer, zstdOutBuffer)); + } while (bytesLeft != 0 && !errorHolder.hasError()); +} + +/** + * Calculates how large each independently compressed frame should be. + * + * @param size The size of the source if known, 0 otherwise + * @param numThreads The number of threads available to run compression jobs on + * @param params The zstd parameters to be used for compression + */ +static size_t +calculateStep(size_t size, size_t numThreads, const ZSTD_parameters& params) { + size_t step = 1ul << (params.cParams.windowLog + 2); + // If file size is known, see if a smaller step will spread work more evenly + if (size != 0) { + size_t newStep = size / numThreads; + if (newStep != 0) { + step = std::min(step, newStep); + } + } + return step; +} + +namespace { +enum class FileStatus { Continue, Done, Error }; +} // anonymous namespace + +/** + * Reads `size` data in chunks of `chunkSize` and puts it into `queue`. + * Will read less if an error or EOF occurs. + * Returns the status of the file after all of the reads have occurred. + */ +static FileStatus +readData(BufferWorkQueue& queue, size_t chunkSize, size_t size, FILE* fd) { + Buffer buffer(size); + while (!buffer.empty()) { + auto bytesRead = + std::fread(buffer.data(), 1, std::min(chunkSize, buffer.size()), fd); + queue.push(buffer.splitAt(bytesRead)); + if (std::feof(fd)) { + return FileStatus::Done; + } else if (std::ferror(fd) || bytesRead == 0) { + return FileStatus::Error; + } + } + return FileStatus::Continue; +} + +void asyncCompressChunks( + ErrorHolder& errorHolder, + WorkQueue>& chunks, + ThreadPool& executor, + FILE* fd, + size_t size, + size_t numThreads, + ZSTD_parameters params) { + auto chunksGuard = makeScopeGuard([&] { chunks.finish(); }); + + // Break the input up into chunks of size `step` and compress each chunk + // independently. + size_t step = calculateStep(size, numThreads, params); + auto status = FileStatus::Continue; + while (status == FileStatus::Continue && !errorHolder.hasError()) { + // Make a new input queue that we will put the chunk's input data into. + auto in = std::make_shared(); + auto inGuard = makeScopeGuard([&] { in->finish(); }); + // Make a new output queue that compress will put the compressed data into. + auto out = std::make_shared(); + // Start compression in the thread pool + executor.add([&errorHolder, in, out, step, params] { + return compress( + errorHolder, std::move(in), std::move(out), step, params); + }); + // Pass the output queue to the writer thread. + chunks.push(std::move(out)); + // Fill the input queue for the compression job we just started + status = readData(*in, ZSTD_CStreamInSize(), step, fd); + } + errorHolder.check(status != FileStatus::Error, "Error reading input"); +} + +/** + * Decompress a frame, whose data is streamed into `in`, and stream the output + * to `out`. + * + * @param errorHolder Used to report errors and check if an error occured + * @param in Queue that we `pop()` input buffers from. It contains + * exactly one compressed frame. + * @param out Queue that we `push()` decompressed output buffers to + */ +static void decompress( + ErrorHolder& errorHolder, + std::shared_ptr in, + std::shared_ptr out) { + auto guard = makeScopeGuard([&] { out->finish(); }); + // Initialize the DCtx + std::unique_ptr ctx( + ZSTD_createDStream(), ZSTD_freeDStream); + if (!errorHolder.check(ctx != nullptr, "Failed to allocate ZSTD_DStream")) { + return; + } + { + auto err = ZSTD_initDStream(ctx.get()); + if (!errorHolder.check(!ZSTD_isError(err), ZSTD_getErrorName(err))) { + return; + } + } + + const size_t outSize = ZSTD_DStreamOutSize(); + Buffer inBuffer; + size_t returnCode = 0; + // Read a buffer in from the input queue + while (in->pop(inBuffer) && !errorHolder.hasError()) { + auto zstdInBuffer = makeZstdInBuffer(inBuffer); + // Decompress the whole buffer and send it to the output queue + while (!inBuffer.empty() && !errorHolder.hasError()) { + // Allocate a buffer with at least outSize bytes. + Buffer outBuffer(outSize); + auto zstdOutBuffer = makeZstdOutBuffer(outBuffer); + // Decompress + returnCode = + ZSTD_decompressStream(ctx.get(), &zstdOutBuffer, &zstdInBuffer); + if (!errorHolder.check( + !ZSTD_isError(returnCode), ZSTD_getErrorName(returnCode))) { + return; + } + // Pass the buffer with the decompressed data to the output queue + out->push(split(outBuffer, zstdOutBuffer)); + // Advance past the input we already read + advance(inBuffer, zstdInBuffer); + if (returnCode == 0) { + // The frame is over, prepare to (maybe) start a new frame + ZSTD_initDStream(ctx.get()); + } + } + } + if (!errorHolder.check(returnCode <= 1, "Incomplete block")) { + return; + } + // We've given ZSTD_decompressStream all of our data, but there may still + // be data to read. + while (returnCode == 1) { + // Allocate a buffer with at least outSize bytes. + Buffer outBuffer(outSize); + auto zstdOutBuffer = makeZstdOutBuffer(outBuffer); + // Pass in no input. + ZSTD_inBuffer zstdInBuffer{nullptr, 0, 0}; + // Decompress + returnCode = + ZSTD_decompressStream(ctx.get(), &zstdOutBuffer, &zstdInBuffer); + if (!errorHolder.check( + !ZSTD_isError(returnCode), ZSTD_getErrorName(returnCode))) { + return; + } + // Pass the buffer with the decompressed data to the output queue + out->push(split(outBuffer, zstdOutBuffer)); + } +} + +void asyncDecompressFrames( + ErrorHolder& errorHolder, + WorkQueue>& frames, + ThreadPool& executor, + FILE* fd) { + auto framesGuard = makeScopeGuard([&] { frames.finish(); }); + // Split the source up into its component frames. + // If we find our recognized skippable frame we know the next frames size + // which means that we can decompress each standard frame in independently. + // Otherwise, we will decompress using only one decompression task. + const size_t chunkSize = ZSTD_DStreamInSize(); + auto status = FileStatus::Continue; + while (status == FileStatus::Continue && !errorHolder.hasError()) { + // Make a new input queue that we will put the frames's bytes into. + auto in = std::make_shared(); + auto inGuard = makeScopeGuard([&] { in->finish(); }); + // Make a output queue that decompress will put the decompressed data into + auto out = std::make_shared(); + + size_t frameSize; + { + // Calculate the size of the next frame. + // frameSize is 0 if the frame info can't be decoded. + Buffer buffer(SkippableFrame::kSize); + auto bytesRead = std::fread(buffer.data(), 1, buffer.size(), fd); + if (bytesRead == 0 && status != FileStatus::Continue) { + break; + } + buffer.subtract(buffer.size() - bytesRead); + frameSize = SkippableFrame::tryRead(buffer.range()); + in->push(std::move(buffer)); + } + // Start decompression in the thread pool + executor.add([&errorHolder, in, out] { + return decompress(errorHolder, std::move(in), std::move(out)); + }); + // Pass the output queue to the writer thread + frames.push(std::move(out)); + if (frameSize == 0) { + // We hit a non SkippableFrame ==> not compressed by pzstd or corrupted + // Pass the rest of the source to this decompression task + while (status == FileStatus::Continue && !errorHolder.hasError()) { + status = readData(*in, chunkSize, chunkSize, fd); + } + break; + } + // Fill the input queue for the decompression job we just started + status = readData(*in, chunkSize, frameSize, fd); + } + errorHolder.check(status != FileStatus::Error, "Error reading input"); +} + +/// Write `data` to `fd`, returns true iff success. +static bool writeData(ByteRange data, FILE* fd) { + while (!data.empty()) { + data.advance(std::fwrite(data.begin(), 1, data.size(), fd)); + if (std::ferror(fd)) { + return false; + } + } + return true; +} + +size_t writeFile( + ErrorHolder& errorHolder, + WorkQueue>& outs, + FILE* outputFd, + bool writeSkippableFrames) { + size_t bytesWritten = 0; + std::shared_ptr out; + // Grab the output queue for each decompression job (in order). + while (outs.pop(out) && !errorHolder.hasError()) { + if (writeSkippableFrames) { + // If we are compressing and want to write skippable frames we can't + // start writing before compression is done because we need to know the + // compressed size. + // Wait for the compressed size to be available and write skippable frame + SkippableFrame frame(out->size()); + if (!writeData(frame.data(), outputFd)) { + errorHolder.setError("Failed to write output"); + return bytesWritten; + } + bytesWritten += frame.kSize; + } + // For each chunk of the frame: Pop it from the queue and write it + Buffer buffer; + while (out->pop(buffer) && !errorHolder.hasError()) { + if (!writeData(buffer.range(), outputFd)) { + errorHolder.setError("Failed to write output"); + return bytesWritten; + } + bytesWritten += buffer.size(); + } + } + return bytesWritten; +} +} diff --git a/contrib/pzstd/Pzstd.h b/contrib/pzstd/Pzstd.h new file mode 100644 index 000000000..617aecb3f --- /dev/null +++ b/contrib/pzstd/Pzstd.h @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#pragma once + +#include "ErrorHolder.h" +#include "Options.h" +#include "utils/Buffer.h" +#include "utils/Range.h" +#include "utils/ThreadPool.h" +#include "utils/WorkQueue.h" +#define ZSTD_STATIC_LINKING_ONLY +#include "zstd.h" +#undef ZSTD_STATIC_LINKING_ONLY + +#include +#include + +namespace pzstd { +/** + * Runs pzstd with `options` and returns the number of bytes written. + * An error occurred if `errorHandler.hasError()`. + * + * @param options The pzstd options to use for (de)compression + * @param errorHolder Used to report errors and coordinate early shutdown + * if an error occured + * @returns The number of bytes written. + */ +std::size_t pzstdMain(const Options& options, ErrorHolder& errorHolder); + +/** + * Streams input from `fd`, breaks input up into chunks, and compresses each + * chunk independently. Output of each chunk gets streamed to a queue, and + * the output queues get put into `chunks` in order. + * + * @param errorHolder Used to report errors and coordinate early shutdown + * @param chunks Each compression jobs output queue gets `pushed()` here + * as soon as it is available + * @param executor The thread pool to run compression jobs in + * @param fd The input file descriptor + * @param size The size of the input file if known, 0 otherwise + * @param numThreads The number of threads in the thread pool + * @param parameters The zstd parameters to use for compression + */ +void asyncCompressChunks( + ErrorHolder& errorHolder, + WorkQueue>& chunks, + ThreadPool& executor, + FILE* fd, + std::size_t size, + std::size_t numThreads, + ZSTD_parameters parameters); + +/** + * Streams input from `fd`. If pzstd headers are available it breaks the input + * up into independent frames. It sends each frame to an independent + * decompression job. Output of each frame gets streamed to a queue, and + * the output queues get put into `frames` in order. + * + * @param errorHolder Used to report errors and coordinate early shutdown + * @param frames Each decompression jobs output queue gets `pushed()` here + * as soon as it is available + * @param executor The thread pool to run compression jobs in + * @param fd The input file descriptor + */ +void asyncDecompressFrames( + ErrorHolder& errorHolder, + WorkQueue>& frames, + ThreadPool& executor, + FILE* fd); + +/** + * Streams input in from each queue in `outs` in order, and writes the data to + * `outputFd`. + * + * @param errorHolder Used to report errors and coordinate early exit + * @param outs A queue of output queues, one for each + * (de)compression job. + * @param outputFd The file descriptor to write to + * @param writeSkippableFrames Should we write pzstd headers? + * @returns The number of bytes written + */ +std::size_t writeFile( + ErrorHolder& errorHolder, + WorkQueue>& outs, + FILE* outputFd, + bool writeSkippableFrames); +} diff --git a/contrib/pzstd/README.md b/contrib/pzstd/README.md new file mode 100644 index 000000000..1a5a0105d --- /dev/null +++ b/contrib/pzstd/README.md @@ -0,0 +1,47 @@ +# Parallel Zstandard (PZstandard) + +Parallel Zstandard provides Zstandard format compatible compression and decompression that is able to utilize multiple cores. +It breaks the input up into equal sized chunks and compresses each chunk independently into a Zstandard frame. +It then concatenates the frames together to produce the final compressed output. +Optionally, with the `-p` option, PZstandard will write a 12 byte header for each frame that is a skippable frame in the Zstandard format, which tells PZstandard the size of the next compressed frame. +When `-p` is specified for compression, PZstandard can decompress the output in parallel. + +## Usage + +Basic usage + + pzstd input-file -o output-file -n num-threads [ -p ] -# # Compression + pzstd -d input-file -o output-file -n num-threads # Decompression + +PZstandard also supports piping and fifo pipes + + cat input-file | pzstd -n num-threads [ -p ] -# -c > /dev/null + +For more options + + pzstd --help + +## Benchmarks + +As a reference, PZstandard and Pigz were compared on an Intel Core i7 @ 3.1 GHz, each using 4 threads, with the [Silesia compression corpus](http://sun.aei.polsl.pl/~sdeor/index.php?page=silesia). + +Compression Speed vs Ratio with 4 Threads | Decompression Speed with 4 Threads +------------------------------------------|----------------------------------- +![Compression Speed vs Ratio](images/Cspeed.png "Compression Speed vs Ratio") | ![Decompression Speed](images/Dspeed.png "Decompression Speed") + +The test procedure was to run each of the following commands 2 times for each compression level, and take the minimum time. + + time ./pzstd -# -n 4 -p -c silesia.tar > silesia.tar.zst + time ./pzstd -d -n 4 -c silesia.tar.zst > /dev/null + + time pigz -# -p 4 -k -c silesia.tar > silesia.tar.gz + time pigz -d -p 4 -k -c silesia.tar.gz > /dev/null + +PZstandard was tested using compression levels 1-19, and Pigz was tested using compression levels 1-9. +Pigz cannot do parallel decompression, it simply does each of reading, decompression, and writing on separate threads. + +## Tests + +Tests require that you have [gtest](https://github.com/google/googletest) installed. +Modify `GTEST_INC` and `GTEST_LIB` in `test/Makefile` and `utils/test/Makefile` to work for your install of gtest. +Then run `make test` in the `contrib/pzstd` directory. diff --git a/contrib/pzstd/SkippableFrame.cpp b/contrib/pzstd/SkippableFrame.cpp new file mode 100644 index 000000000..20ad4cc8e --- /dev/null +++ b/contrib/pzstd/SkippableFrame.cpp @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#include "SkippableFrame.h" +#include "common/mem.h" +#include "utils/Range.h" + +#include + +using namespace pzstd; + +SkippableFrame::SkippableFrame(std::uint32_t size) : frameSize_(size) { + MEM_writeLE32(data_.data(), kSkippableFrameMagicNumber); + MEM_writeLE32(data_.data() + 4, kFrameContentsSize); + MEM_writeLE32(data_.data() + 8, frameSize_); +} + +/* static */ std::size_t SkippableFrame::tryRead(ByteRange bytes) { + if (bytes.size() < SkippableFrame::kSize || + MEM_readLE32(bytes.begin()) != kSkippableFrameMagicNumber || + MEM_readLE32(bytes.begin() + 4) != kFrameContentsSize) { + return 0; + } + return MEM_readLE32(bytes.begin() + 8); +} diff --git a/contrib/pzstd/SkippableFrame.h b/contrib/pzstd/SkippableFrame.h new file mode 100644 index 000000000..9dc95c1f5 --- /dev/null +++ b/contrib/pzstd/SkippableFrame.h @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#pragma once + +#include "utils/Range.h" + +#include +#include +#include +#include + +namespace pzstd { +/** + * We put a skippable frame before each frame. + * It contains a skippable frame magic number, the size of the skippable frame, + * and the size of the next frame. + * Each skippable frame is exactly 12 bytes in little endian format. + * The first 8 bytes are for compatibility with the ZSTD format. + * If we have N threads, the output will look like + * + * [0x184D2A50|4|size1] [frame1 of size size1] + * [0x184D2A50|4|size2] [frame2 of size size2] + * ... + * [0x184D2A50|4|sizeN] [frameN of size sizeN] + * + * Each sizeX is 4 bytes. + * + * These skippable frames should allow us to skip through the compressed file + * and only load at most N pages. + */ +class SkippableFrame { + public: + static constexpr std::size_t kSize = 12; + + private: + std::uint32_t frameSize_; + std::array data_; + static constexpr std::uint32_t kSkippableFrameMagicNumber = 0x184D2A50; + // Could be improved if the size fits in less bytes + static constexpr std::uint32_t kFrameContentsSize = kSize - 8; + + public: + // Write the skippable frame to data_ in LE format. + explicit SkippableFrame(std::uint32_t size); + + // Read the skippable frame from bytes in LE format. + static std::size_t tryRead(ByteRange bytes); + + ByteRange data() const { + return {data_.data(), data_.size()}; + } + + // Size of the next frame. + std::size_t frameSize() const { + return frameSize_; + } +}; +} diff --git a/contrib/pzstd/bench.cpp b/contrib/pzstd/bench.cpp new file mode 100644 index 000000000..56bad3915 --- /dev/null +++ b/contrib/pzstd/bench.cpp @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#include "ErrorHolder.h" +#include "Options.h" +#include "Pzstd.h" +#include "utils/FileSystem.h" +#include "utils/Range.h" +#include "utils/ScopeGuard.h" +#include "utils/ThreadPool.h" +#include "utils/WorkQueue.h" + +#include +#include +#include + +using namespace pzstd; + +namespace { +// Prints how many ns it was in scope for upon destruction +// Used for rough estimates of how long things took +struct BenchmarkTimer { + using Clock = std::chrono::system_clock; + Clock::time_point start; + FILE* fd; + + explicit BenchmarkTimer(FILE* fd = stdout) : fd(fd) { + start = Clock::now(); + } + + ~BenchmarkTimer() { + auto end = Clock::now(); + size_t ticks = + std::chrono::duration_cast(end - start) + .count(); + ticks = std::max(ticks, size_t{1}); + for (auto tmp = ticks; tmp < 100000; tmp *= 10) { + std::fprintf(fd, " "); + } + std::fprintf(fd, "%zu | ", ticks); + } +}; +} + +// Code I used for benchmarking + +void testMain(const Options& options) { + if (!options.decompress) { + if (options.compressionLevel < 10) { + std::printf("0"); + } + std::printf("%u | ", options.compressionLevel); + } else { + std::printf(" d | "); + } + if (options.numThreads < 10) { + std::printf("0"); + } + std::printf("%u | ", options.numThreads); + + FILE* inputFd = std::fopen(options.inputFile.c_str(), "rb"); + if (inputFd == nullptr) { + std::abort(); + } + size_t inputSize = 0; + if (inputFd != stdin) { + std::error_code ec; + inputSize = file_size(options.inputFile, ec); + if (ec) { + inputSize = 0; + } + } + FILE* outputFd = std::fopen(options.outputFile.c_str(), "wb"); + if (outputFd == nullptr) { + std::abort(); + } + auto guard = makeScopeGuard([&] { + std::fclose(inputFd); + std::fclose(outputFd); + }); + + WorkQueue> outs; + ErrorHolder errorHolder; + size_t bytesWritten; + { + ThreadPool executor(options.numThreads); + BenchmarkTimer timeIncludingClose; + if (!options.decompress) { + executor.add( + [&errorHolder, &outs, &executor, inputFd, inputSize, &options] { + asyncCompressChunks( + errorHolder, + outs, + executor, + inputFd, + inputSize, + options.numThreads, + options.determineParameters()); + }); + bytesWritten = writeFile(errorHolder, outs, outputFd, true); + } else { + executor.add([&errorHolder, &outs, &executor, inputFd] { + asyncDecompressFrames(errorHolder, outs, executor, inputFd); + }); + bytesWritten = writeFile( + errorHolder, outs, outputFd, /* writeSkippableFrames */ false); + } + } + if (errorHolder.hasError()) { + std::fprintf(stderr, "Error: %s.\n", errorHolder.getError().c_str()); + std::abort(); + } + std::printf("%zu\n", bytesWritten); +} + +int main(int argc, const char** argv) { + if (argc < 3) { + return 1; + } + Options options(0, 23, 0, false, "", "", true, true); + // Benchmarking code + for (size_t i = 0; i < 2; ++i) { + for (size_t compressionLevel = 1; compressionLevel <= 16; + compressionLevel <<= 1) { + for (size_t numThreads = 1; numThreads <= 16; numThreads <<= 1) { + options.numThreads = numThreads; + options.compressionLevel = compressionLevel; + options.decompress = false; + options.inputFile = argv[1]; + options.outputFile = argv[2]; + testMain(options); + options.decompress = true; + options.inputFile = argv[2]; + options.outputFile = std::string(argv[1]) + ".d"; + testMain(options); + std::fflush(stdout); + } + } + } + return 0; +} diff --git a/contrib/pzstd/images/Cspeed.png b/contrib/pzstd/images/Cspeed.png new file mode 100644 index 0000000000000000000000000000000000000000..516d09807b304fa64bf563eb0468fc3b2a1a59c2 GIT binary patch literal 58612 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 Q4ZigCP981(!zJSX0Nq4p)&Kwi literal 0 HcmV?d00001 diff --git a/contrib/pzstd/images/Dspeed.png b/contrib/pzstd/images/Dspeed.png new file mode 100644 index 0000000000000000000000000000000000000000..e48881bcd05b70a41121ae03a09621a7e6abb7d2 GIT binary patch literal 26335 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 literal 0 HcmV?d00001 diff --git a/contrib/pzstd/main.cpp b/contrib/pzstd/main.cpp new file mode 100644 index 000000000..7ff2cef74 --- /dev/null +++ b/contrib/pzstd/main.cpp @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#include "ErrorHolder.h" +#include "Options.h" +#include "Pzstd.h" +#include "utils/FileSystem.h" +#include "utils/Range.h" +#include "utils/ScopeGuard.h" +#include "utils/ThreadPool.h" +#include "utils/WorkQueue.h" + +using namespace pzstd; + +int main(int argc, const char** argv) { + Options options; + if (!options.parse(argc, argv)) { + return 1; + } + + ErrorHolder errorHolder; + pzstdMain(options, errorHolder); + + if (errorHolder.hasError()) { + std::fprintf(stderr, "Error: %s.\n", errorHolder.getError().c_str()); + return 1; + } + return 0; +} diff --git a/contrib/pzstd/test/Makefile b/contrib/pzstd/test/Makefile new file mode 100644 index 000000000..3b0ffec89 --- /dev/null +++ b/contrib/pzstd/test/Makefile @@ -0,0 +1,46 @@ +# ########################################################################## +# Copyright (c) 2016-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. +# ########################################################################## + +# Set GTEST_INC and GTEST_LIB to work with your install of gtest +GTEST_INC ?= -isystem googletest/googletest/include +GTEST_LIB ?= -L googletest/build/googlemock/gtest + +# Define *.exe as extension for Windows systems +ifneq (,$(filter Windows%,$(OS))) +EXT =.exe +else +EXT = +endif + +PZSTDDIR = .. +PROGDIR = ../../../programs +ZSTDDIR = ../../../lib + +CPPFLAGS = -I$(PZSTDDIR) $(GTEST_INC) $(GTEST_LIB) -I$(ZSTDDIR)/common -I$(PROGDIR) + +CFLAGS ?= -O3 +CFLAGS += -std=c++11 +CFLAGS += $(MOREFLAGS) +FLAGS = $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) + +datagen.o: $(PROGDIR)/datagen.* + $(CXX) $(FLAGS) $(PROGDIR)/datagen.c -c -o $@ + +%: %.cpp *.h datagen.o + $(CXX) $(FLAGS) -lgtest -lgtest_main $@.cpp datagen.o $(PZSTDDIR)/libzstd.a $(PZSTDDIR)/Pzstd.o $(PZSTDDIR)/SkippableFrame.o $(PZSTDDIR)/Options.o -o $@$(EXT) + +.PHONY: test clean + +test: OptionsTest PzstdTest RoundTripTest + @./OptionsTest$(EXT) + @./PzstdTest$(EXT) + @./RoundTripTest$(EXT) + +clean: + @rm -f datagen.o OptionsTest PzstdTest RoundTripTest diff --git a/contrib/pzstd/test/OptionsTest.cpp b/contrib/pzstd/test/OptionsTest.cpp new file mode 100644 index 000000000..1479d6cd2 --- /dev/null +++ b/contrib/pzstd/test/OptionsTest.cpp @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#include "Options.h" + +#include +#include + +using namespace pzstd; + +namespace pzstd { +bool operator==(const Options& lhs, const Options& rhs) { + return lhs.numThreads == rhs.numThreads && + lhs.maxWindowLog == rhs.maxWindowLog && + lhs.compressionLevel == rhs.compressionLevel && + lhs.decompress == rhs.decompress && lhs.inputFile == rhs.inputFile && + lhs.outputFile == rhs.outputFile && lhs.overwrite == rhs.overwrite && + lhs.pzstdHeaders == rhs.pzstdHeaders; +} +} + +TEST(Options, ValidInputs) { + { + Options options; + std::array args = { + {nullptr, "--num-threads", "5", "-o", "-", "-f"}}; + EXPECT_TRUE(options.parse(args.size(), args.data())); + Options expected = {5, 23, 3, false, "-", "-", true, false}; + EXPECT_EQ(expected, options); + } + { + Options options; + std::array args = { + {nullptr, "-n", "1", "input", "-19", "-p"}}; + EXPECT_TRUE(options.parse(args.size(), args.data())); + Options expected = {1, 23, 19, false, "input", "input.zst", false, true}; + EXPECT_EQ(expected, options); + } + { + Options options; + std::array args = {{nullptr, + "--ultra", + "-22", + "-n", + "1", + "--output", + "x", + "-d", + "x.zst", + "-f"}}; + EXPECT_TRUE(options.parse(args.size(), args.data())); + Options expected = {1, 0, 22, true, "x.zst", "x", true, false}; + EXPECT_EQ(expected, options); + } + { + Options options; + std::array args = {{nullptr, + "--num-threads", + "100", + "hello.zst", + "--decompress", + "--force"}}; + EXPECT_TRUE(options.parse(args.size(), args.data())); + Options expected = {100, 23, 3, true, "hello.zst", "hello", true, false}; + EXPECT_EQ(expected, options); + } + { + Options options; + std::array args = {{nullptr, "-", "-n", "1", "-c"}}; + EXPECT_TRUE(options.parse(args.size(), args.data())); + Options expected = {1, 23, 3, false, "-", "-", false, false}; + EXPECT_EQ(expected, options); + } + { + Options options; + std::array args = {{nullptr, "-", "-n", "1", "--stdout"}}; + EXPECT_TRUE(options.parse(args.size(), args.data())); + Options expected = {1, 23, 3, false, "-", "-", false, false}; + EXPECT_EQ(expected, options); + } + { + Options options; + std::array args = {{nullptr, + "-n", + "1", + "-", + "-5", + "-o", + "-", + "-u", + "-d", + "--pzstd-headers"}}; + EXPECT_TRUE(options.parse(args.size(), args.data())); + Options expected = {1, 0, 5, true, "-", "-", false, true}; + } + { + Options options; + std::array args = { + {nullptr, "silesia.tar", "-o", "silesia.tar.pzstd", "-n", "2"}}; + EXPECT_TRUE(options.parse(args.size(), args.data())); + Options expected = { + 2, 23, 3, false, "silesia.tar", "silesia.tar.pzstd", false, false}; + } +} + +TEST(Options, BadNumThreads) { + { + Options options; + std::array args = {{nullptr, "-o", "-"}}; + EXPECT_FALSE(options.parse(args.size(), args.data())); + } + { + Options options; + std::array args = {{nullptr, "-n", "0", "-o", "-"}}; + EXPECT_FALSE(options.parse(args.size(), args.data())); + } + { + Options options; + std::array args = {{nullptr, "-n", "-o", "-"}}; + EXPECT_FALSE(options.parse(args.size(), args.data())); + } +} + +TEST(Options, BadCompressionLevel) { + { + Options options; + std::array args = {{nullptr, "x", "-20"}}; + EXPECT_FALSE(options.parse(args.size(), args.data())); + } + { + Options options; + std::array args = {{nullptr, "x", "-u", "-23"}}; + EXPECT_FALSE(options.parse(args.size(), args.data())); + } +} + +TEST(Options, InvalidOption) { + { + Options options; + std::array args = {{nullptr, "x", "-x"}}; + EXPECT_FALSE(options.parse(args.size(), args.data())); + } +} + +TEST(Options, BadOutputFile) { + { + Options options; + std::array args = {{nullptr, "notzst", "-d", "-n", "1"}}; + EXPECT_FALSE(options.parse(args.size(), args.data())); + } + { + Options options; + std::array args = {{nullptr, "-n", "1"}}; + EXPECT_FALSE(options.parse(args.size(), args.data())); + } + { + Options options; + std::array args = {{nullptr, "-", "-n", "1"}}; + EXPECT_FALSE(options.parse(args.size(), args.data())); + } +} + +TEST(Options, Extras) { + { + Options options; + std::array args = {{nullptr, "-h"}}; + EXPECT_FALSE(options.parse(args.size(), args.data())); + } + { + Options options; + std::array args = {{nullptr, "-V"}}; + EXPECT_FALSE(options.parse(args.size(), args.data())); + } +} diff --git a/contrib/pzstd/test/PzstdTest.cpp b/contrib/pzstd/test/PzstdTest.cpp new file mode 100644 index 000000000..a6eb74596 --- /dev/null +++ b/contrib/pzstd/test/PzstdTest.cpp @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#include "datagen.h" +#include "Pzstd.h" +#include "test/RoundTrip.h" +#include "utils/ScopeGuard.h" + +#include +#include +#include +#include + +using namespace std; +using namespace pzstd; + +TEST(Pzstd, SmallSizes) { + for (unsigned len = 1; len < 1028; ++len) { + std::string inputFile = std::tmpnam(nullptr); + auto guard = makeScopeGuard([&] { std::remove(inputFile.c_str()); }); + { + static uint8_t buf[1028]; + RDG_genBuffer(buf, len, 0.5, 0.0, 42); + auto fd = std::fopen(inputFile.c_str(), "wb"); + auto written = std::fwrite(buf, 1, len, fd); + std::fclose(fd); + ASSERT_EQ(written, len); + } + for (unsigned headers = 0; headers <= 1; ++headers) { + for (unsigned numThreads = 1; numThreads <= 4; numThreads *= 2) { + for (unsigned level = 1; level <= 8; level *= 8) { + auto errorGuard = makeScopeGuard([&] { + guard.dismiss(); + std::fprintf(stderr, "file: %s\n", inputFile.c_str()); + std::fprintf(stderr, "pzstd headers: %u\n", headers); + std::fprintf(stderr, "# threads: %u\n", numThreads); + std::fprintf(stderr, "compression level: %u\n", level); + }); + Options options; + options.pzstdHeaders = headers; + options.overwrite = true; + options.inputFile = inputFile; + options.numThreads = numThreads; + options.compressionLevel = level; + ASSERT_TRUE(roundTrip(options)); + errorGuard.dismiss(); + } + } + } + } +} + +TEST(Pzstd, LargeSizes) { + for (unsigned len = 1 << 20; len <= (1 << 24); len *= 2) { + std::string inputFile = std::tmpnam(nullptr); + auto guard = makeScopeGuard([&] { std::remove(inputFile.c_str()); }); + { + std::unique_ptr buf(new uint8_t[len]); + RDG_genBuffer(buf.get(), len, 0.5, 0.0, 42); + auto fd = std::fopen(inputFile.c_str(), "wb"); + auto written = std::fwrite(buf.get(), 1, len, fd); + std::fclose(fd); + ASSERT_EQ(written, len); + } + for (unsigned headers = 0; headers <= 1; ++headers) { + for (unsigned numThreads = 1; numThreads <= 16; numThreads *= 4) { + for (unsigned level = 1; level <= 4; level *= 2) { + auto errorGuard = makeScopeGuard([&] { + guard.dismiss(); + std::fprintf(stderr, "file: %s\n", inputFile.c_str()); + std::fprintf(stderr, "pzstd headers: %u\n", headers); + std::fprintf(stderr, "# threads: %u\n", numThreads); + std::fprintf(stderr, "compression level: %u\n", level); + }); + Options options; + options.pzstdHeaders = headers; + options.overwrite = true; + options.inputFile = inputFile; + options.numThreads = numThreads; + options.compressionLevel = level; + ASSERT_TRUE(roundTrip(options)); + errorGuard.dismiss(); + } + } + } + } +} + +TEST(Pzstd, ExtremelyCompressible) { + std::string inputFile = std::tmpnam(nullptr); + auto guard = makeScopeGuard([&] { std::remove(inputFile.c_str()); }); + { + std::unique_ptr buf(new uint8_t[10000]); + std::memset(buf.get(), 'a', 10000); + auto fd = std::fopen(inputFile.c_str(), "wb"); + auto written = std::fwrite(buf.get(), 1, 10000, fd); + std::fclose(fd); + ASSERT_EQ(written, 10000); + } + Options options; + options.pzstdHeaders = false; + options.overwrite = true; + options.inputFile = inputFile; + options.numThreads = 1; + options.compressionLevel = 1; + ASSERT_TRUE(roundTrip(options)); +} diff --git a/contrib/pzstd/test/RoundTrip.h b/contrib/pzstd/test/RoundTrip.h new file mode 100644 index 000000000..829c95cac --- /dev/null +++ b/contrib/pzstd/test/RoundTrip.h @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#pragma once + +#include "Options.h" +#include "Pzstd.h" +#include "utils/ScopeGuard.h" + +#include +#include +#include +#include + +namespace pzstd { + +inline bool check(std::string source, std::string decompressed) { + std::unique_ptr sBuf(new std::uint8_t[1024]); + std::unique_ptr dBuf(new std::uint8_t[1024]); + + auto sFd = std::fopen(source.c_str(), "rb"); + auto dFd = std::fopen(decompressed.c_str(), "rb"); + auto guard = makeScopeGuard([&] { + std::fclose(sFd); + std::fclose(dFd); + }); + + size_t sRead, dRead; + + do { + sRead = std::fread(sBuf.get(), 1, 1024, sFd); + dRead = std::fread(dBuf.get(), 1, 1024, dFd); + if (std::ferror(sFd) || std::ferror(dFd)) { + return false; + } + if (sRead != dRead) { + return false; + } + + for (size_t i = 0; i < sRead; ++i) { + if (sBuf.get()[i] != dBuf.get()[i]) { + return false; + } + } + } while (sRead == 1024); + if (!std::feof(sFd) || !std::feof(dFd)) { + return false; + } + return true; +} + +inline bool roundTrip(Options& options) { + std::string source = options.inputFile; + std::string compressedFile = std::tmpnam(nullptr); + std::string decompressedFile = std::tmpnam(nullptr); + auto guard = makeScopeGuard([&] { + std::remove(compressedFile.c_str()); + std::remove(decompressedFile.c_str()); + }); + + { + options.outputFile = compressedFile; + options.decompress = false; + ErrorHolder errorHolder; + pzstdMain(options, errorHolder); + if (errorHolder.hasError()) { + errorHolder.getError(); + return false; + } + } + { + options.decompress = true; + options.inputFile = compressedFile; + options.outputFile = decompressedFile; + ErrorHolder errorHolder; + pzstdMain(options, errorHolder); + if (errorHolder.hasError()) { + errorHolder.getError(); + return false; + } + } + return check(source, decompressedFile); +} +} diff --git a/contrib/pzstd/test/RoundTripTest.cpp b/contrib/pzstd/test/RoundTripTest.cpp new file mode 100644 index 000000000..01c1c8113 --- /dev/null +++ b/contrib/pzstd/test/RoundTripTest.cpp @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#include "datagen.h" +#include "Options.h" +#include "test/RoundTrip.h" +#include "utils/ScopeGuard.h" + +#include +#include +#include +#include +#include + +using namespace std; +using namespace pzstd; + +namespace { +string +writeData(size_t size, double matchProba, double litProba, unsigned seed) { + std::unique_ptr buf(new uint8_t[size]); + RDG_genBuffer(buf.get(), size, matchProba, litProba, seed); + string file = tmpnam(nullptr); + auto fd = std::fopen(file.c_str(), "wb"); + auto guard = makeScopeGuard([&] { std::fclose(fd); }); + auto bytesWritten = std::fwrite(buf.get(), 1, size, fd); + if (bytesWritten != size) { + std::abort(); + } + return file; +} + +template +string generateInputFile(Generator& gen) { + // Use inputs ranging from 1 Byte to 2^16 Bytes + std::uniform_int_distribution size{1, 1 << 16}; + std::uniform_real_distribution<> prob{0, 1}; + return writeData(size(gen), prob(gen), prob(gen), gen()); +} + +template +Options generateOptions(Generator& gen, const string& inputFile) { + Options options; + options.inputFile = inputFile; + options.overwrite = true; + + std::bernoulli_distribution pzstdHeaders{0.75}; + std::uniform_int_distribution numThreads{1, 32}; + std::uniform_int_distribution compressionLevel{1, 10}; + + options.pzstdHeaders = pzstdHeaders(gen); + options.numThreads = numThreads(gen); + options.compressionLevel = compressionLevel(gen); + + return options; +} +} + +int main(int argc, char** argv) { + std::mt19937 gen(std::random_device{}()); + + auto newlineGuard = makeScopeGuard([] { std::fprintf(stderr, "\n"); }); + for (unsigned i = 0; i < 10000; ++i) { + if (i % 100 == 0) { + std::fprintf(stderr, "Progress: %u%%\r", i / 100); + } + auto inputFile = generateInputFile(gen); + auto inputGuard = makeScopeGuard([&] { std::remove(inputFile.c_str()); }); + for (unsigned i = 0; i < 10; ++i) { + auto options = generateOptions(gen, inputFile); + if (!roundTrip(options)) { + std::fprintf(stderr, "numThreads: %u\n", options.numThreads); + std::fprintf(stderr, "level: %u\n", options.compressionLevel); + std::fprintf(stderr, "decompress? %u\n", (unsigned)options.decompress); + std::fprintf( + stderr, "pzstd headers? %u\n", (unsigned)options.pzstdHeaders); + std::fprintf(stderr, "file: %s\n", inputFile.c_str()); + return 1; + } + } + } + return 0; +} diff --git a/contrib/pzstd/utils/Buffer.h b/contrib/pzstd/utils/Buffer.h new file mode 100644 index 000000000..ab25bac9c --- /dev/null +++ b/contrib/pzstd/utils/Buffer.h @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#pragma once + +#include "utils/Range.h" + +#include +#include +#include + +namespace pzstd { + +/** + * A `Buffer` has a pointer to a shared buffer, and a range of the buffer that + * it owns. + * The idea is that you can allocate one buffer, and write chunks into it + * and break off those chunks. + * The underlying buffer is reference counted, and will be destroyed when all + * `Buffer`s that reference it are destroyed. + */ +class Buffer { + std::shared_ptr buffer_; + MutableByteRange range_; + + static void delete_buffer(unsigned char* buffer) { + delete[] buffer; + } + + public: + /// Construct an empty buffer that owns no data. + explicit Buffer() {} + + /// Construct a `Buffer` that owns a new underlying buffer of size `size`. + explicit Buffer(std::size_t size) + : buffer_(new unsigned char[size], delete_buffer), + range_(buffer_.get(), buffer_.get() + size) {} + + explicit Buffer(std::shared_ptr buffer, MutableByteRange data) + : buffer_(buffer), range_(data) {} + + Buffer(Buffer&&) = default; + Buffer& operator=(Buffer&&) & = default; + + /** + * Splits the data into two pieces: [begin, begin + n), [begin + n, end). + * Their data both points into the same underlying buffer. + * Modifies the original `Buffer` to point to only [begin + n, end). + * + * @param n The offset to split at. + * @returns A buffer that owns the data [begin, begin + n). + */ + Buffer splitAt(std::size_t n) { + auto firstPiece = range_.subpiece(0, n); + range_.advance(n); + return Buffer(buffer_, firstPiece); + } + + /// Modifies the buffer to point to the range [begin + n, end). + void advance(std::size_t n) { + range_.advance(n); + } + + /// Modifies the buffer to point to the range [begin, end - n). + void subtract(std::size_t n) { + range_.subtract(n); + } + + /// Returns a read only `Range` pointing to the `Buffer`s data. + ByteRange range() const { + return range_; + } + /// Returns a mutable `Range` pointing to the `Buffer`s data. + MutableByteRange range() { + return range_; + } + + const unsigned char* data() const { + return range_.data(); + } + + unsigned char* data() { + return range_.data(); + } + + std::size_t size() const { + return range_.size(); + } + + bool empty() const { + return range_.empty(); + } +}; +} diff --git a/contrib/pzstd/utils/FileSystem.h b/contrib/pzstd/utils/FileSystem.h new file mode 100644 index 000000000..deae0b5b7 --- /dev/null +++ b/contrib/pzstd/utils/FileSystem.h @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#pragma once + +#include "utils/Range.h" + +#include +#include +#include + +// A small subset of `std::filesystem`. +// `std::filesystem` should be a drop in replacement. +// See http://en.cppreference.com/w/cpp/filesystem for documentation. + +namespace pzstd { + +using file_status = struct stat; + +/// http://en.cppreference.com/w/cpp/filesystem/status +inline file_status status(StringPiece path, std::error_code& ec) noexcept { + file_status status; + if (stat(path.data(), &status)) { + ec.assign(errno, std::generic_category()); + } else { + ec.clear(); + } + return status; +} + +/// http://en.cppreference.com/w/cpp/filesystem/is_regular_file +inline bool is_regular_file(file_status status) noexcept { + return S_ISREG(status.st_mode); +} + +/// http://en.cppreference.com/w/cpp/filesystem/is_regular_file +inline bool is_regular_file(StringPiece path, std::error_code& ec) noexcept { + return is_regular_file(status(path, ec)); +} + +/// http://en.cppreference.com/w/cpp/filesystem/file_size +inline std::uintmax_t file_size( + StringPiece path, + std::error_code& ec) noexcept { + auto stat = status(path, ec); + if (ec) { + return -1; + } + if (!is_regular_file(stat)) { + ec.assign(ENOTSUP, std::generic_category()); + return -1; + } + ec.clear(); + return stat.st_size; +} +} diff --git a/contrib/pzstd/utils/Likely.h b/contrib/pzstd/utils/Likely.h new file mode 100644 index 000000000..c8ea102b1 --- /dev/null +++ b/contrib/pzstd/utils/Likely.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** + * Compiler hints to indicate the fast path of an "if" branch: whether + * the if condition is likely to be true or false. + * + * @author Tudor Bosman (tudorb@fb.com) + */ + +#pragma once + +#undef LIKELY +#undef UNLIKELY + +#if defined(__GNUC__) && __GNUC__ >= 4 +#define LIKELY(x) (__builtin_expect((x), 1)) +#define UNLIKELY(x) (__builtin_expect((x), 0)) +#else +#define LIKELY(x) (x) +#define UNLIKELY(x) (x) +#endif diff --git a/contrib/pzstd/utils/Range.h b/contrib/pzstd/utils/Range.h new file mode 100644 index 000000000..3df15976d --- /dev/null +++ b/contrib/pzstd/utils/Range.h @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** + * A subset of `folly/Range.h`. + * All code copied verbatiam modulo formatting + */ +#pragma once + +#include "utils/Likely.h" + +#include +#include +#include +#include + +namespace pzstd { + +namespace detail { +/* + *Use IsCharPointer::type to enable const char* or char*. + *Use IsCharPointer::const_type to enable only const char*. +*/ +template +struct IsCharPointer {}; + +template <> +struct IsCharPointer { + typedef int type; +}; + +template <> +struct IsCharPointer { + typedef int const_type; + typedef int type; +}; + +} // namespace detail + +template +class Range { + Iter b_; + Iter e_; + + public: + using size_type = std::size_t; + using iterator = Iter; + using const_iterator = Iter; + using value_type = typename std::remove_reference< + typename std::iterator_traits::reference>::type; + using reference = typename std::iterator_traits::reference; + + constexpr Range() : b_(), e_() {} + constexpr Range(Iter begin, Iter end) : b_(begin), e_(end) {} + + constexpr Range(Iter begin, size_type size) : b_(begin), e_(begin + size) {} + + template ::type = 0> + /* implicit */ Range(Iter str) : b_(str), e_(str + std::strlen(str)) {} + + template ::const_type = 0> + /* implicit */ Range(const std::string& str) + : b_(str.data()), e_(b_ + str.size()) {} + + // Allow implicit conversion from Range to Range if From is + // implicitly convertible to To. + template < + class OtherIter, + typename std::enable_if< + (!std::is_same::value && + std::is_convertible::value), + int>::type = 0> + constexpr /* implicit */ Range(const Range& other) + : b_(other.begin()), e_(other.end()) {} + + Range(const Range&) = default; + Range(Range&&) = default; + + Range& operator=(const Range&) & = default; + Range& operator=(Range&&) & = default; + + constexpr size_type size() const { + return e_ - b_; + } + bool empty() const { + return b_ == e_; + } + Iter data() const { + return b_; + } + Iter begin() const { + return b_; + } + Iter end() const { + return e_; + } + + void advance(size_type n) { + if (UNLIKELY(n > size())) { + throw std::out_of_range("index out of range"); + } + b_ += n; + } + + void subtract(size_type n) { + if (UNLIKELY(n > size())) { + throw std::out_of_range("index out of range"); + } + e_ -= n; + } + + Range subpiece(size_type first, size_type length = std::string::npos) const { + if (UNLIKELY(first > size())) { + throw std::out_of_range("index out of range"); + } + + return Range(b_ + first, std::min(length, size() - first)); + } +}; + +using ByteRange = Range; +using MutableByteRange = Range; +using StringPiece = Range; +} diff --git a/contrib/pzstd/utils/ScopeGuard.h b/contrib/pzstd/utils/ScopeGuard.h new file mode 100644 index 000000000..5a333e0ab --- /dev/null +++ b/contrib/pzstd/utils/ScopeGuard.h @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#pragma once + +#include + +namespace pzstd { + +/** + * Dismissable scope guard. + * `Function` must be callable and take no parameters. + * Unless `dissmiss()` is called, the callable is executed upon destruction of + * `ScopeGuard`. + * + * Example: + * + * auto guard = makeScopeGuard([&] { cleanup(); }); + */ +template +class ScopeGuard { + Function function; + bool dismissed; + + public: + explicit ScopeGuard(Function&& function) + : function(std::move(function)), dismissed(false) {} + + void dismiss() { + dismissed = true; + } + + ~ScopeGuard() noexcept { + if (!dismissed) { + function(); + } + } +}; + +/// Creates a scope guard from `function`. +template +ScopeGuard makeScopeGuard(Function&& function) { + return ScopeGuard(std::forward(function)); +} +} diff --git a/contrib/pzstd/utils/ThreadPool.h b/contrib/pzstd/utils/ThreadPool.h new file mode 100644 index 000000000..a1d1fc0b9 --- /dev/null +++ b/contrib/pzstd/utils/ThreadPool.h @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#pragma once + +#include "utils/WorkQueue.h" + +#include +#include +#include +#include + +namespace pzstd { +/// A simple thread pool that pulls tasks off its queue in FIFO order. +class ThreadPool { + std::vector threads_; + + WorkQueue> tasks_; + + public: + /// Constructs a thread pool with `numThreads` threads. + explicit ThreadPool(std::size_t numThreads) { + threads_.reserve(numThreads); + for (std::size_t i = 0; i < numThreads; ++i) { + threads_.emplace_back([&] { + std::function task; + while (tasks_.pop(task)) { + task(); + } + }); + } + } + + /// Finishes all tasks currently in the queue. + ~ThreadPool() { + tasks_.finish(); + for (auto& thread : threads_) { + thread.join(); + } + } + + /** + * Adds `task` to the queue of tasks to execute. Since `task` is a + * `std::function<>`, it cannot be a move only type. So any lambda passed must + * not capture move only types (like `std::unique_ptr`). + * + * @param task The task to execute. + */ + void add(std::function task) { + tasks_.push(std::move(task)); + } +}; +} diff --git a/contrib/pzstd/utils/WorkQueue.h b/contrib/pzstd/utils/WorkQueue.h new file mode 100644 index 000000000..3d926cc80 --- /dev/null +++ b/contrib/pzstd/utils/WorkQueue.h @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#pragma once + +#include "utils/Buffer.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace pzstd { + +/// Unbounded thread-safe work queue. +template +class WorkQueue { + // Protects all member variable access + std::mutex mutex_; + std::condition_variable cv_; + + std::queue queue_; + bool done_; + + public: + /// Constructs an empty work queue. + WorkQueue() : done_(false) {} + + /** + * Push an item onto the work queue. Notify a single thread that work is + * available. If `finish()` has been called, do nothing and return false. + * + * @param item Item to push onto the queue. + * @returns True upon success, false if `finish()` has been called. An + * item was pushed iff `push()` returns true. + */ + bool push(T item) { + { + std::lock_guard lock(mutex_); + if (done_) { + return false; + } + queue_.push(std::move(item)); + } + cv_.notify_one(); + return true; + } + + /** + * Attempts to pop an item off the work queue. It will block until data is + * available or `finish()` has been called. + * + * @param[out] item If `pop` returns `true`, it contains the popped item. + * If `pop` returns `false`, it is unmodified. + * @returns True upon success. False if the queue is empty and + * `finish()` has been called. + */ + bool pop(T& item) { + std::unique_lock lock(mutex_); + while (queue_.empty() && !done_) { + cv_.wait(lock); + } + if (queue_.empty()) { + assert(done_); + return false; + } + item = std::move(queue_.front()); + queue_.pop(); + return true; + } + + /** + * Promise that `push()` won't be called again, so once the queue is empty + * there will never any more work. + */ + void finish() { + { + std::lock_guard lock(mutex_); + assert(!done_); + done_ = true; + } + cv_.notify_all(); + } + + /// Blocks until `finish()` has been called (but the queue may not be empty). + void waitUntilFinished() { + std::unique_lock lock(mutex_); + while (!done_) { + cv_.wait(lock); + // If we were woken by a push, we need to wake a thread waiting on pop(). + if (!done_) { + lock.unlock(); + cv_.notify_one(); + lock.lock(); + } + } + } +}; + +/// Work queue for `Buffer`s that knows the total number of bytes in the queue. +class BufferWorkQueue { + WorkQueue queue_; + std::atomic size_; + + public: + BufferWorkQueue() : size_(0) {} + + void push(Buffer buffer) { + size_.fetch_add(buffer.size()); + queue_.push(std::move(buffer)); + } + + bool pop(Buffer& buffer) { + bool result = queue_.pop(buffer); + if (result) { + size_.fetch_sub(buffer.size()); + } + return result; + } + + void finish() { + queue_.finish(); + } + + /** + * Blocks until `finish()` has been called. + * + * @returns The total number of bytes of all the `Buffer`s currently in the + * queue. + */ + std::size_t size() { + queue_.waitUntilFinished(); + return size_.load(); + } +}; +} diff --git a/contrib/pzstd/utils/test/BufferTest.cpp b/contrib/pzstd/utils/test/BufferTest.cpp new file mode 100644 index 000000000..66ec961e2 --- /dev/null +++ b/contrib/pzstd/utils/test/BufferTest.cpp @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#include "utils/Buffer.h" +#include "utils/Range.h" + +#include +#include + +using namespace pzstd; + +namespace { +void deleter(const unsigned char* buf) { + delete[] buf; +} +} + +TEST(Buffer, Constructors) { + Buffer empty; + EXPECT_TRUE(empty.empty()); + EXPECT_EQ(0, empty.size()); + + Buffer sized(5); + EXPECT_FALSE(sized.empty()); + EXPECT_EQ(5, sized.size()); + + Buffer moved(std::move(sized)); + EXPECT_FALSE(sized.empty()); + EXPECT_EQ(5, sized.size()); + + Buffer assigned; + assigned = std::move(moved); + EXPECT_FALSE(sized.empty()); + EXPECT_EQ(5, sized.size()); +} + +TEST(Buffer, BufferManagement) { + std::shared_ptr buf(new unsigned char[10], deleter); + { + Buffer acquired(buf, MutableByteRange(buf.get(), buf.get() + 10)); + EXPECT_EQ(2, buf.use_count()); + Buffer moved(std::move(acquired)); + EXPECT_EQ(2, buf.use_count()); + Buffer assigned; + assigned = std::move(moved); + EXPECT_EQ(2, buf.use_count()); + + Buffer split = assigned.splitAt(5); + EXPECT_EQ(3, buf.use_count()); + + split.advance(1); + assigned.subtract(1); + EXPECT_EQ(3, buf.use_count()); + } + EXPECT_EQ(1, buf.use_count()); +} + +TEST(Buffer, Modifiers) { + Buffer buf(10); + { + unsigned char i = 0; + for (auto& byte : buf.range()) { + byte = i++; + } + } + + auto prefix = buf.splitAt(2); + + ASSERT_EQ(2, prefix.size()); + EXPECT_EQ(0, *prefix.data()); + + ASSERT_EQ(8, buf.size()); + EXPECT_EQ(2, *buf.data()); + + buf.advance(2); + EXPECT_EQ(4, *buf.data()); + + EXPECT_EQ(9, *(buf.range().end() - 1)); + + buf.subtract(2); + EXPECT_EQ(7, *(buf.range().end() - 1)); + + EXPECT_EQ(4, buf.size()); +} diff --git a/contrib/pzstd/utils/test/Makefile b/contrib/pzstd/utils/test/Makefile new file mode 100644 index 000000000..4c6906330 --- /dev/null +++ b/contrib/pzstd/utils/test/Makefile @@ -0,0 +1,41 @@ +# ########################################################################## +# Copyright (c) 2016-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. +# ########################################################################## + +GTEST_INC ?= -isystem googletest/googletest/include +GTEST_LIB ?= -L googletest/build/googlemock/gtest + +# Define *.exe as extension for Windows systems +ifneq (,$(filter Windows%,$(OS))) +EXT =.exe +else +EXT = +endif + +PZSTDDIR = ../.. + +CPPFLAGS = -I$(PZSTDDIR) $(GTEST_INC) $(GTEST_LIB) +CFLAGS ?= -O3 +CFLAGS += -std=c++11 +CFLAGS += $(MOREFLAGS) +FLAGS = $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) + +%: %.cpp + $(CXX) $(FLAGS) -lgtest -lgtest_main $^ -o $@$(EXT) + +.PHONY: test clean + +test: BufferTest RangeTest ScopeGuardTest ThreadPoolTest WorkQueueTest + @./BufferTest$(EXT) + @./RangeTest$(EXT) + @./ScopeGuardTest$(EXT) + @./ThreadPoolTest$(EXT) + @./WorkQueueTest$(EXT) + +clean: + @rm -f BufferTest RangeTest ScopeGuardTest ThreadPoolTest WorkQueueTest diff --git a/contrib/pzstd/utils/test/RangeTest.cpp b/contrib/pzstd/utils/test/RangeTest.cpp new file mode 100644 index 000000000..c761c8aff --- /dev/null +++ b/contrib/pzstd/utils/test/RangeTest.cpp @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#include "utils/Range.h" + +#include +#include + +using namespace pzstd; + +// Range is directly copied from folly. +// Just some sanity tests to make sure everything seems to work. + +TEST(Range, Constructors) { + StringPiece empty; + EXPECT_TRUE(empty.empty()); + EXPECT_EQ(0, empty.size()); + + std::string str = "hello"; + { + Range piece(str.begin(), str.end()); + EXPECT_EQ(5, piece.size()); + EXPECT_EQ('h', *piece.data()); + EXPECT_EQ('o', *(piece.end() - 1)); + } + + { + StringPiece piece(str.data(), str.size()); + EXPECT_EQ(5, piece.size()); + EXPECT_EQ('h', *piece.data()); + EXPECT_EQ('o', *(piece.end() - 1)); + } + + { + StringPiece piece(str); + EXPECT_EQ(5, piece.size()); + EXPECT_EQ('h', *piece.data()); + EXPECT_EQ('o', *(piece.end() - 1)); + } + + { + StringPiece piece(str.c_str()); + EXPECT_EQ(5, piece.size()); + EXPECT_EQ('h', *piece.data()); + EXPECT_EQ('o', *(piece.end() - 1)); + } +} + +TEST(Range, Modifiers) { + StringPiece range("hello world"); + ASSERT_EQ(11, range.size()); + + { + auto hello = range.subpiece(0, 5); + EXPECT_EQ(5, hello.size()); + EXPECT_EQ('h', *hello.data()); + EXPECT_EQ('o', *(hello.end() - 1)); + } + { + auto hello = range; + hello.subtract(6); + EXPECT_EQ(5, hello.size()); + EXPECT_EQ('h', *hello.data()); + EXPECT_EQ('o', *(hello.end() - 1)); + } + { + auto world = range; + world.advance(6); + EXPECT_EQ(5, world.size()); + EXPECT_EQ('w', *world.data()); + EXPECT_EQ('d', *(world.end() - 1)); + } + + std::string expected = "hello world"; + EXPECT_EQ(expected, std::string(range.begin(), range.end())); + EXPECT_EQ(expected, std::string(range.data(), range.size())); +} diff --git a/contrib/pzstd/utils/test/ScopeGuardTest.cpp b/contrib/pzstd/utils/test/ScopeGuardTest.cpp new file mode 100644 index 000000000..0c4dc0357 --- /dev/null +++ b/contrib/pzstd/utils/test/ScopeGuardTest.cpp @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#include "utils/ScopeGuard.h" + +#include + +using namespace pzstd; + +TEST(ScopeGuard, Dismiss) { + { + auto guard = makeScopeGuard([&] { EXPECT_TRUE(false); }); + guard.dismiss(); + } +} + +TEST(ScopeGuard, Executes) { + bool executed = false; + { + auto guard = makeScopeGuard([&] { executed = true; }); + } + EXPECT_TRUE(executed); +} diff --git a/contrib/pzstd/utils/test/ThreadPoolTest.cpp b/contrib/pzstd/utils/test/ThreadPoolTest.cpp new file mode 100644 index 000000000..9b9868cb1 --- /dev/null +++ b/contrib/pzstd/utils/test/ThreadPoolTest.cpp @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#include "utils/ThreadPool.h" + +#include +#include +#include +#include + +using namespace pzstd; + +TEST(ThreadPool, Ordering) { + std::vector results; + + { + ThreadPool executor(1); + for (int i = 0; i < 100; ++i) { + executor.add([ &results, i ] { results.push_back(i); }); + } + } + + for (int i = 0; i < 100; ++i) { + EXPECT_EQ(i, results[i]); + } +} + +TEST(ThreadPool, AllJobsFinished) { + std::atomic numFinished{0}; + std::atomic start{false}; + { + ThreadPool executor(5); + for (int i = 0; i < 1000; ++i) { + executor.add([ &numFinished, &start ] { + while (!start.load()) { + // spin + } + ++numFinished; + }); + } + start.store(true); + } + EXPECT_EQ(1000, numFinished.load()); +} + +TEST(ThreadPool, AddJobWhileJoining) { + std::atomic done{false}; + { + ThreadPool executor(1); + executor.add([&executor, &done] { + while (!done.load()) { + std::this_thread::yield(); + } + // Sleep for a second to be sure that we are joining + std::this_thread::sleep_for(std::chrono::seconds(1)); + executor.add([] { + EXPECT_TRUE(false); + }); + }); + done.store(true); + } +} diff --git a/contrib/pzstd/utils/test/WorkQueueTest.cpp b/contrib/pzstd/utils/test/WorkQueueTest.cpp new file mode 100644 index 000000000..1b548d160 --- /dev/null +++ b/contrib/pzstd/utils/test/WorkQueueTest.cpp @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +#include "utils/Buffer.h" +#include "utils/WorkQueue.h" + +#include +#include +#include +#include + +using namespace pzstd; + +namespace { +struct Popper { + WorkQueue* queue; + int* results; + std::mutex* mutex; + + void operator()() { + int result; + while (queue->pop(result)) { + std::lock_guard lock(*mutex); + results[result] = result; + } + } +}; +} + +TEST(WorkQueue, SingleThreaded) { + WorkQueue queue; + int result; + + queue.push(5); + EXPECT_TRUE(queue.pop(result)); + EXPECT_EQ(5, result); + + queue.push(1); + queue.push(2); + EXPECT_TRUE(queue.pop(result)); + EXPECT_EQ(1, result); + EXPECT_TRUE(queue.pop(result)); + EXPECT_EQ(2, result); + + queue.push(1); + queue.push(2); + queue.finish(); + EXPECT_TRUE(queue.pop(result)); + EXPECT_EQ(1, result); + EXPECT_TRUE(queue.pop(result)); + EXPECT_EQ(2, result); + EXPECT_FALSE(queue.pop(result)); + + queue.waitUntilFinished(); +} + +TEST(WorkQueue, SPSC) { + WorkQueue queue; + const int max = 100; + + for (int i = 0; i < 10; ++i) { + queue.push(i); + } + + std::thread thread([ &queue, max ] { + int result; + for (int i = 0;; ++i) { + if (!queue.pop(result)) { + EXPECT_EQ(i, max); + break; + } + EXPECT_EQ(i, result); + } + }); + + std::this_thread::yield(); + for (int i = 10; i < max; ++i) { + queue.push(i); + } + queue.finish(); + + thread.join(); +} + +TEST(WorkQueue, SPMC) { + WorkQueue queue; + std::vector results(10000, -1); + std::mutex mutex; + std::vector threads; + for (int i = 0; i < 100; ++i) { + threads.emplace_back(Popper{&queue, results.data(), &mutex}); + } + + for (int i = 0; i < 10000; ++i) { + queue.push(i); + } + queue.finish(); + + for (auto& thread : threads) { + thread.join(); + } + + for (int i = 0; i < 10000; ++i) { + EXPECT_EQ(i, results[i]); + } +} + +TEST(WorkQueue, MPMC) { + WorkQueue queue; + std::vector results(10000, -1); + std::mutex mutex; + std::vector popperThreads; + for (int i = 0; i < 100; ++i) { + popperThreads.emplace_back(Popper{&queue, results.data(), &mutex}); + } + + std::vector pusherThreads; + for (int i = 0; i < 10; ++i) { + auto min = i * 1000; + auto max = (i + 1) * 1000; + pusherThreads.emplace_back( + [ &queue, min, max ] { + for (int i = min; i < max; ++i) { + queue.push(i); + } + }); + } + + for (auto& thread : pusherThreads) { + thread.join(); + } + queue.finish(); + + for (auto& thread : popperThreads) { + thread.join(); + } + + for (int i = 0; i < 10000; ++i) { + EXPECT_EQ(i, results[i]); + } +} + +TEST(BufferWorkQueue, SizeCalculatedCorrectly) { + { + BufferWorkQueue queue; + queue.finish(); + EXPECT_EQ(0, queue.size()); + } + { + BufferWorkQueue queue; + queue.push(Buffer(10)); + queue.finish(); + EXPECT_EQ(10, queue.size()); + } + { + BufferWorkQueue queue; + queue.push(Buffer(10)); + queue.push(Buffer(5)); + queue.finish(); + EXPECT_EQ(15, queue.size()); + } + { + BufferWorkQueue queue; + queue.push(Buffer(10)); + queue.push(Buffer(5)); + queue.finish(); + Buffer buffer; + queue.pop(buffer); + EXPECT_EQ(5, queue.size()); + } +}