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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 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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 L^>bP0l+XkKsbHG{ 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()); + } +}