summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEdward Thomson <ethomson@edwardthomson.com>2022-02-22 22:00:37 -0500
committerEdward Thomson <ethomson@edwardthomson.com>2022-02-27 23:44:19 -0500
commit93037bab1c6758c7939d16ea59b0e434e5ddea86 (patch)
tree0d555af7e23115becdc2f8707b105b60ca4fbbba
parente48bb3b708d75dc0a41590543a5f844509d87d8e (diff)
downloadlibgit2-93037bab1c6758c7939d16ea59b0e434e5ddea86.tar.gz
tests: add benchmark tests
Add a benchmark test suite that wraps hyperfine and is suitable for producing data about test runs of a CLI or A/B testing CLIs.
-rw-r--r--tests/README.md2
-rw-r--r--tests/benchmarks/README.md121
-rwxr-xr-xtests/benchmarks/benchmark.sh298
-rw-r--r--tests/benchmarks/benchmark_helpers.sh363
-rwxr-xr-xtests/benchmarks/hash-object__text_100kb7
-rwxr-xr-xtests/benchmarks/hash-object__text_10mb7
-rwxr-xr-xtests/benchmarks/hash-object__text_1kb7
-rwxr-xr-xtests/benchmarks/hash-object__text_cached_100kb7
-rwxr-xr-xtests/benchmarks/hash-object__text_cached_10mb7
-rwxr-xr-xtests/benchmarks/hash-object__text_cached_1kb7
-rwxr-xr-xtests/benchmarks/hash-object__write_text_100kb9
-rwxr-xr-xtests/benchmarks/hash-object__write_text_10mb9
-rwxr-xr-xtests/benchmarks/hash-object__write_text_1kb9
-rwxr-xr-xtests/benchmarks/hash-object__write_text_cached_100kb9
-rwxr-xr-xtests/benchmarks/hash-object__write_text_cached_10mb9
-rwxr-xr-xtests/benchmarks/hash-object__write_text_cached_1kb9
16 files changed, 880 insertions, 0 deletions
diff --git a/tests/README.md b/tests/README.md
index 5920b1547..460e045e3 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -2,6 +2,8 @@
These are the unit and integration tests for the libgit2 projects.
+* `benchmarks`
+ These are benchmark tests that excercise the CLI.
* `clar`
This is [clar](https://github.com/clar-test/clar) the common test framework.
* `headertest`
diff --git a/tests/benchmarks/README.md b/tests/benchmarks/README.md
new file mode 100644
index 000000000..f66b27aea
--- /dev/null
+++ b/tests/benchmarks/README.md
@@ -0,0 +1,121 @@
+# libgit2 benchmarks
+
+This folder contains the individual benchmark tests for libgit2,
+meant for understanding the performance characteristics of libgit2,
+comparing your development code to the existing libgit2 code, or
+comparing libgit2 to the git reference implementation.
+
+## Running benchmark tests
+
+Benchmark tests can be run in several different ways: running all
+benchmarks, running one (or more) suite of benchmarks, or running a
+single individual benchmark. You can target either an individual
+version of a CLI, or you can A/B test a baseline CLI against a test
+CLI.
+
+### Specifying the command-line interface to test
+
+By default, the `git` in your path is benchmarked. Use the
+`-c` (or `--cli`) option to specify the command-line interface
+to test.
+
+Example: `libgit2_bench --cli git2_cli` will run the tests against
+`git2_cli`.
+
+### Running tests to compare two different implementations
+
+You can compare a baseline command-line interface against a test
+command-line interface using the `-b (or `--baseline-cli`) option.
+
+Example: `libgit2_bench --baseline-cli git --cli git2_cli` will
+run the tests against both `git` and `git2_cli`.
+
+### Running individual benchmark tests
+
+Similar to how a test suite or individual test is specified in
+[clar](https://github.com/clar-test/clar), the `-s` (or `--suite`)
+option may be used to specify the suite or individual test to run.
+Like clar, the suite and test name are separated by `::`, and like
+clar, this is a prefix match.
+
+Examples:
+* `libgit2_bench -shash_object` will run the tests in the
+ `hash_object` suite.
+* `libgit2_bench -shash_object::random_1kb` will run the
+ `hash_object::random_1kb` test.
+* `libgit2_bench -shash_object::random` will run all the tests that
+ begin with `hash_object::random`.
+
+## Writing benchmark tests
+
+Benchmark tests are meant to be easy to write. Each individual
+benchmark is a shell script that allows it to do set up (eg, creating
+or cloning a repository, creating temporary files, etc), then running
+benchmarks, then teardown.
+
+The `benchmark_helpers.sh` script provides many helpful utility
+functions to allow for cross-platform benchmarking, as well as a
+wrapper for `hyperfine` that is suited to testing libgit2.
+Note that the helper script must be included first, at the beginning
+of the benchmark test.
+
+### Benchmark example
+
+This simplistic example compares the speed of running the `git help`
+command in the baseline CLI to the test CLI.
+
+```bash
+#!/bin/bash -e
+
+# include the benchmark library
+. "$(dirname "$0")/benchmark_helpers.sh"
+
+# run the "help" command; this will benchmark `git2_cli help`
+gitbench help
+```
+
+### Naming
+
+The filename of the benchmark itself is important. A benchmark's
+filename should be the name of the benchmark suite, followed by two
+underscores, followed by the name of the benchmark. For example,
+`hash-object__random_1kb` is the `random_1kb` test in the `hash-object`
+suite.
+
+### Options
+
+The `gitbench` function accepts several options.
+
+* `--sandbox <path>`
+ The name of a test resource (in the `tests/resources` directory).
+ This will be copied as-is to the sandbox location before test
+ execution. This is copied _before_ the `prepare` script is run.
+ This option may be specified multiple times.
+* `--repository <path>`
+ The name of a test resource repository (in the `tests/resources`
+ directory). This repository will be copied into a sandbox location
+ before test execution, and your test will run in this directory.
+ This is copied _before_ the `prepare` script is run.
+* `--prepare <script>`
+ A script to run before each invocation of the test is run. This can
+ set up data for the test that will _not_ be timed. This script is run
+ in bash on all platforms.
+
+ Several helper functions are available within the context of a prepare
+ script:
+
+ * `flush_disk_cache`
+ Calling this will flush the disk cache before each test run.
+ This should probably be run at the end of the `prepare` script.
+ * `create_random_file <path> [<size>]`
+ Calling this will populate a file at the given `path` with `size`
+ bytes of random data.
+ * `create_text_file <path> [<size>]`
+ Calling this will populate a file at the given `path` with `size`
+ bytes of predictable text, with the platform line endings. This
+ is preferred over random data as it's reproducible.
+
+* `--warmup <n>`
+ Specifies that the test should run `n` times before actually measuring
+ the timing; useful for "warming up" a cache.
+
diff --git a/tests/benchmarks/benchmark.sh b/tests/benchmarks/benchmark.sh
new file mode 100755
index 000000000..4a89807b7
--- /dev/null
+++ b/tests/benchmarks/benchmark.sh
@@ -0,0 +1,298 @@
+#!/bin/bash
+
+set -eo pipefail
+
+#
+# parse the command line
+#
+
+usage() { echo "usage: $(basename "$0") [--cli <path>] [--baseline-cli <path>] [--suite <suite>] [--json <path>] [--zip <path>] [--verbose] [--debug]"; }
+
+TEST_CLI="git"
+BASELINE_CLI=
+SUITE=
+JSON_RESULT=
+ZIP_RESULT=
+OUTPUT_DIR=
+VERBOSE=
+DEBUG=
+NEXT=
+
+for a in "$@"; do
+ if [ "${NEXT}" = "cli" ]; then
+ TEST_CLI="${a}"
+ NEXT=
+ elif [ "${NEXT}" = "baseline-cli" ]; then
+ BASELINE_CLI="${a}"
+ NEXT=
+ elif [ "${NEXT}" = "suite" ]; then
+ SUITE="${a}"
+ NEXT=
+ elif [ "${NEXT}" = "json" ]; then
+ JSON_RESULT="${a}"
+ NEXT=
+ elif [ "${NEXT}" = "zip" ]; then
+ ZIP_RESULT="${a}"
+ NEXT=
+ elif [ "${NEXT}" = "output-dir" ]; then
+ OUTPUT_DIR="${a}"
+ NEXT=
+ elif [ "${a}" = "c" ] || [ "${a}" = "--cli" ]; then
+ NEXT="cli"
+ elif [[ "${a}" == "-c"* ]]; then
+ TEST_CLI="${a/-c/}"
+ elif [ "${a}" = "b" ] || [ "${a}" = "--baseline-cli" ]; then
+ NEXT="baseline-cli"
+ elif [[ "${a}" == "-b"* ]]; then
+ BASELINE_CLI="${a/-b/}"
+ elif [ "${a}" = "-s" ] || [ "${a}" = "--suite" ]; then
+ NEXT="suite"
+ elif [[ "${a}" == "-s"* ]]; then
+ SUITE="${a/-s/}"
+ elif [ "${a}" = "-v" ] || [ "${a}" == "--verbose" ]; then
+ VERBOSE=1
+ elif [ "${a}" == "--debug" ]; then
+ VERBOSE=1
+ DEBUG=1
+ elif [ "${a}" = "-j" ] || [ "${a}" == "--json" ]; then
+ NEXT="json"
+ elif [[ "${a}" == "-j"* ]]; then
+ JSON_RESULT="${a/-j/}"
+ elif [ "${a}" = "-z" ] || [ "${a}" == "--zip" ]; then
+ NEXT="zip"
+ elif [[ "${a}" == "-z"* ]]; then
+ ZIP_RESULT="${a/-z/}"
+ elif [ "${a}" = "--output-dir" ]; then
+ NEXT="output-dir"
+ else
+ echo "$(basename "$0"): unknown option: ${a}" 1>&2
+ usage 1>&2
+ exit 1
+ fi
+done
+
+if [ "${NEXT}" != "" ]; then
+ usage 1>&2
+ exit 1
+fi
+
+if [ "${OUTPUT_DIR}" = "" ]; then
+ OUTPUT_DIR=${OUTPUT_DIR:="$(mktemp -d)"}
+ CLEANUP_DIR=1
+fi
+
+#
+# collect some information about the test environment
+#
+
+SYSTEM_OS=$(uname -s)
+if [ "${SYSTEM_OS}" = "Darwin" ]; then SYSTEM_OS="macOS"; fi
+
+SYSTEM_KERNEL=$(uname -v)
+
+fullpath() {
+ if [[ "$(uname -s)" == "MINGW"* && $(cygpath -u "${TEST_CLI}") == "/"* ]]; then
+ echo "${TEST_CLI}"
+ elif [[ "${TEST_CLI}" == "/"* ]]; then
+ echo "${TEST_CLI}"
+ else
+ which "${TEST_CLI}"
+ fi
+}
+
+cli_version() {
+ if [[ "$(uname -s)" == "MINGW"* ]]; then
+ $(cygpath -u "$1") --version
+ else
+ "$1" --version
+ fi
+}
+
+TEST_CLI_NAME=$(basename "${TEST_CLI}")
+TEST_CLI_PATH=$(fullpath "${TEST_CLI}")
+TEST_CLI_VERSION=$(cli_version "${TEST_CLI}")
+
+if [ "${BASELINE_CLI}" != "" ]; then
+ if [[ "${BASELINE_CLI}" == "/"* ]]; then
+ BASELINE_CLI_PATH="${BASELINE_CLI}"
+ else
+ BASELINE_CLI_PATH=$(which "${BASELINE_CLI}")
+ fi
+
+ BASELINE_CLI_NAME=$(basename "${BASELINE_CLI}")
+ BASELINE_CLI_PATH=$(fullpath "${BASELINE_CLI}")
+ BASELINE_CLI_VERSION=$(cli_version "${BASELINE_CLI}")
+fi
+
+#
+# run the benchmarks
+#
+
+echo "##############################################################################"
+if [ "${SUITE}" != "" ]; then
+ SUITE_PREFIX="${SUITE/::/__}"
+ echo "## Running ${SUITE} benchmarks"
+else
+ echo "## Running all benchmarks"
+fi
+echo "##############################################################################"
+echo ""
+
+if [ "${BASELINE_CLI}" != "" ]; then
+ echo "# Baseline CLI: ${BASELINE_CLI} (${BASELINE_CLI_VERSION})"
+fi
+echo "# Test CLI: ${TEST_CLI} (${TEST_CLI_VERSION})"
+echo ""
+
+BENCHMARK_DIR=${BENCHMARK_DIR:=$(dirname "$0")}
+ANY_FOUND=
+ANY_FAILED=
+
+indent() { sed "s/^/ /"; }
+time_in_ms() { if [ "$(uname -s)" = "Darwin" ]; then date "+%s000"; else date "+%s%N" ; fi; }
+humanize_secs() {
+ units=('s' 'ms' 'us' 'ns')
+ unit=0
+ time="${1}"
+
+ if [ "${time}" = "" ]; then
+ echo ""
+ return
+ fi
+
+ # bash doesn't do floating point arithmetic. ick.
+ while [[ "${time}" == "0."* ]] && [ "$((unit+1))" != "${#units[*]}" ]; do
+ time="$(echo | awk "{ print ${time} * 1000 }")"
+ unit=$((unit+1))
+ done
+
+ echo "${time} ${units[$unit]}"
+}
+
+TIME_START=$(time_in_ms)
+
+for TEST_PATH in "${BENCHMARK_DIR}"/*; do
+ TEST_FILE=$(basename "${TEST_PATH}")
+
+ if [ ! -f "${TEST_PATH}" ] || [ ! -x "${TEST_PATH}" ]; then
+ continue
+ fi
+
+ if [[ "${TEST_FILE}" != *"__"* ]]; then
+ continue
+ fi
+
+ if [[ "${TEST_FILE}" != "${SUITE_PREFIX}"* ]]; then
+ continue
+ fi
+
+ ANY_FOUND=1
+ TEST_NAME="${TEST_FILE/__/::}"
+
+ echo -n "${TEST_NAME}:"
+ if [ "${VERBOSE}" = "1" ]; then
+ echo ""
+ else
+ echo -n " "
+ fi
+
+ if [ "${DEBUG}" = "1" ]; then
+ SHOW_OUTPUT="--show-output"
+ fi
+
+ OUTPUT_FILE="${OUTPUT_DIR}/${TEST_FILE}.out"
+ JSON_FILE="${OUTPUT_DIR}/${TEST_FILE}.json"
+ ERROR_FILE="${OUTPUT_DIR}/${TEST_FILE}.err"
+
+ FAILED=
+ ${TEST_PATH} --cli "${TEST_CLI}" --baseline-cli "${BASELINE_CLI}" --json "${JSON_FILE}" ${SHOW_OUTPUT} >"${OUTPUT_FILE}" 2>"${ERROR_FILE}" || FAILED=1
+
+ if [ "${FAILED}" = "1" ]; then
+ if [ "${VERBOSE}" != "1" ]; then
+ echo "failed!"
+ fi
+
+ indent < "${ERROR_FILE}"
+ ANY_FAILED=1
+ continue
+ fi
+
+ # in verbose mode, just print the hyperfine results; otherwise,
+ # pull the useful information out of its json and summarize it
+ if [ "${VERBOSE}" = "1" ]; then
+ indent < "${OUTPUT_FILE}"
+ else
+ jq -r '[ .results[0].mean, .results[0].stddev, .results[1].mean, .results[1].stddev ] | @tsv' < "${JSON_FILE}" | while IFS=$'\t' read -r one_mean one_stddev two_mean two_stddev; do
+ one_mean=$(humanize_secs "${one_mean}")
+ one_stddev=$(humanize_secs "${one_stddev}")
+
+ if [ "${two_mean}" != "" ]; then
+ two_mean=$(humanize_secs "${two_mean}")
+ two_stddev=$(humanize_secs "${two_stddev}")
+
+ echo "${one_mean} ± ${one_stddev} vs ${two_mean} ± ${two_stddev}"
+ else
+ echo "${one_mean} ± ${one_stddev}"
+ fi
+ done
+ fi
+
+ # add our metadata to the hyperfine json result
+ jq ". |= { \"name\": \"${TEST_NAME}\" } + ." < "${JSON_FILE}" > "${JSON_FILE}.new" && mv "${JSON_FILE}.new" "${JSON_FILE}"
+done
+
+TIME_END=$(time_in_ms)
+
+if [ "$ANY_FOUND" != "1" ]; then
+ echo ""
+ echo "error: no benchmark suite \"${SUITE}\"."
+ echo ""
+ exit 1
+fi
+
+escape() {
+ echo "${1//\\/\\\\}"
+}
+
+# combine all the individual benchmark results into a single json file
+if [ "${JSON_RESULT}" != "" ]; then
+ if [ "${VERBOSE}" = "1" ]; then
+ echo ""
+ echo "# Writing JSON results: ${JSON_RESULT}"
+ fi
+
+ SYSTEM_JSON="{ \"os\": \"${SYSTEM_OS}\", \"kernel\": \"${SYSTEM_KERNEL}\" }"
+ TIME_JSON="{ \"start\": ${TIME_START}, \"end\": ${TIME_END} }"
+ TEST_CLI_JSON="{ \"name\": \"${TEST_CLI_NAME}\", \"path\": \"$(escape "${TEST_CLI_PATH}")\", \"version\": \"${TEST_CLI_VERSION}\" }"
+ BASELINE_CLI_JSON="{ \"name\": \"${BASELINE_CLI_NAME}\", \"path\": \"$(escape "${BASELINE_CLI_PATH}")\", \"version\": \"${BASELINE_CLI_VERSION}\" }"
+
+ if [ "${BASELINE_CLI}" != "" ]; then
+ EXECUTOR_JSON="{ \"baseline\": ${BASELINE_CLI_JSON}, \"cli\": ${TEST_CLI_JSON} }"
+ else
+ EXECUTOR_JSON="{ \"cli\": ${TEST_CLI_JSON} }"
+ fi
+
+ # add our metadata to all the test results
+ jq -n "{ \"system\": ${SYSTEM_JSON}, \"time\": ${TIME_JSON}, \"executor\": ${EXECUTOR_JSON}, \"tests\": [inputs] }" "${OUTPUT_DIR}"/*.json > "${JSON_RESULT}"
+fi
+
+# combine all the data into a zip if requested
+if [ "${ZIP_RESULT}" != "" ]; then
+ if [ "${VERBOSE}" = "1" ]; then
+ if [ "${JSON_RESULT}" = "" ]; then echo ""; fi
+ echo "# Writing ZIP results: ${ZIP_RESULT}"
+ fi
+
+ zip -jr "${ZIP_RESULT}" "${OUTPUT_DIR}" >/dev/null
+fi
+
+if [ "$CLEANUP_DIR" = "1" ]; then
+ rm -f "${OUTPUT_DIR}"/*.out
+ rm -f "${OUTPUT_DIR}"/*.err
+ rm -f "${OUTPUT_DIR}"/*.json
+ rmdir "${OUTPUT_DIR}"
+fi
+
+if [ "$ANY_FAILED" = "1" ]; then
+ exit 1
+fi
diff --git a/tests/benchmarks/benchmark_helpers.sh b/tests/benchmarks/benchmark_helpers.sh
new file mode 100644
index 000000000..14dbb43c1
--- /dev/null
+++ b/tests/benchmarks/benchmark_helpers.sh
@@ -0,0 +1,363 @@
+# variables that benchmark tests can set
+#
+
+set -eo pipefail
+
+#
+# command-line parsing
+#
+
+usage() { echo "usage: $(basename "$0") [--cli <path>] [--baseline-cli <path>] [--output-style <style>] [--json <path>]"; }
+
+NEXT=
+BASELINE_CLI=
+TEST_CLI="git"
+JSON=
+SHOW_OUTPUT=
+
+if [ "$CI" != "" ]; then
+ OUTPUT_STYLE="color"
+else
+ OUTPUT_STYLE="auto"
+fi
+
+#
+# parse the arguments to the outer script that's including us; these are arguments that
+# the `benchmark.sh` passes (or that a user could specify when running an individual test)
+#
+
+for a in "$@"; do
+ if [ "${NEXT}" = "cli" ]; then
+ TEST_CLI="${a}"
+ NEXT=
+ elif [ "${NEXT}" = "baseline-cli" ]; then
+ BASELINE_CLI="${a}"
+ NEXT=
+ elif [ "${NEXT}" = "output-style" ]; then
+ OUTPUT_STYLE="${a}"
+ NEXT=
+ elif [ "${NEXT}" = "json" ]; then
+ JSON="${a}"
+ NEXT=
+ elif [ "${a}" = "-c" ] || [ "${a}" = "--cli" ]; then
+ NEXT="cli"
+ elif [[ "${a}" == "-c"* ]]; then
+ TEST_CLI="${a/-c/}"
+ elif [ "${a}" = "-b" ] || [ "${a}" = "--baseline-cli" ]; then
+ NEXT="baseline-cli"
+ elif [[ "${a}" == "-b"* ]]; then
+ BASELINE_CLI="${a/-b/}"
+ elif [ "${a}" == "--output-style" ]; then
+ NEXT="output-style"
+ elif [ "${a}" = "-j" ] || [ "${a}" = "--json" ]; then
+ NEXT="json"
+ elif [[ "${a}" == "-j"* ]]; then
+ JSON="${a}"
+ elif [ "${a}" = "--show-output" ]; then
+ SHOW_OUTPUT=1
+ OUTPUT_STYLE=
+ else
+ echo "$(basename "$0"): unknown option: ${a}" 1>&2
+ usage 1>&2
+ exit 1
+ fi
+done
+
+if [ "${NEXT}" != "" ]; then
+ echo "$(basename "$0"): option requires a value: --${NEXT}" 1>&2
+ usage 1>&2
+ exit 1
+fi
+
+fullpath() {
+ FULLPATH="${1}"
+ if [[ "$(uname -s)" == "MINGW"* ]]; then FULLPATH="$(cygpath -u "${1}")"; fi
+
+ if [[ "${FULLPATH}" != *"/"* ]]; then
+ FULLPATH="$(which "${FULLPATH}")"
+ if [ "$?" != "0" ]; then exit 1; fi
+ else
+ FULLPATH="$(cd "$(dirname "${FULLPATH}")" && pwd)/$(basename "${FULLPATH}")"
+ fi
+
+ if [[ "$(uname -s)" == "MINGW"* ]]; then FULLPATH="$(cygpath -w "${FULLPATH}")"; fi
+ echo "${FULLPATH}"
+}
+
+resources_dir() {
+ cd "$(dirname "$0")/../resources" && pwd
+}
+
+temp_dir() {
+ if [ "$(uname -s)" == "Darwin" ]; then
+ mktemp -dt libgit2_bench
+ else
+ mktemp -dt libgit2_bench.XXXXXXX
+ fi
+}
+
+create_preparescript() {
+ # add some functions for users to use in preparation
+ cat >> "${SANDBOX_DIR}/prepare.sh" << EOF
+ set -e
+
+ SANDBOX_DIR="${SANDBOX_DIR}"
+ RESOURCES_DIR="$(resources_dir)"
+
+ create_text_file() {
+ FILENAME="\${1}"
+ SIZE="\${2}"
+
+ if [ "\${FILENAME}" = "" ]; then
+ echo "usage: create_text_file <name> [size]" 1>&2
+ exit 1
+ fi
+
+ if [ "\${SIZE}" = "" ]; then
+ SIZE="1024"
+ fi
+
+ if [[ "\$(uname -s)" == "MINGW"* ]]; then
+ EOL="\r\n"
+ EOL_LEN="2"
+ CONTENTS="This is a reproducible text file. (With Unix line endings.)\n"
+ CONTENTS_LEN="60"
+ else
+ EOL="\n"
+ EOL_LEN="1"
+ CONTENTS="This is a reproducible text file. (With DOS line endings.)\r\n"
+ CONTENTS_LEN="60"
+ fi
+
+ rm -f "\${FILENAME:?}"
+ touch "\${FILENAME}"
+
+ if [ "\${SIZE}" -ge "\$((\${CONTENTS_LEN} + \${EOL_LEN}))" ]; then
+ SIZE="\$((\${SIZE} - \${CONTENTS_LEN}))"
+ COUNT="\$(((\${SIZE} - \${EOL_LEN}) / \${CONTENTS_LEN}))"
+
+ if [ "\${SIZE}" -gt "\${EOL_LEN}" ]; then
+ dd if="\${FILENAME}" of="\${FILENAME}" bs="\${CONTENTS_LEN}" seek=1 count="\${COUNT}" 2>/dev/null
+ fi
+
+ SIZE="\$((\${SIZE} - (\${COUNT} * \${CONTENTS_LEN})))"
+ fi
+
+ while [ "\${SIZE}" -gt "\${EOL_LEN}" ]; do
+ echo -ne "." >> "\${FILENAME}"
+ SIZE="\$((\${SIZE} - 1))"
+ done
+
+ if [ "\${SIZE}" = "\${EOL_LEN}" ]; then
+ echo -ne "\${EOL}" >> "\${FILENAME}"
+ SIZE="\$((\${SIZE} - \${EOL_LEN}))"
+ else
+ while [ "\${SIZE}" -gt "0" ]; do
+ echo -ne "." >> "\${FILENAME}"
+ SIZE="\$((\${SIZE} - 1))"
+ done
+ fi
+ }
+
+ create_random_file() {
+ FILENAME="\${1}"
+ SIZE="\${2}"
+
+ if [ "\${FILENAME}" = "" ]; then
+ echo "usage: create_random_file <name> [size]" 1>&2
+ exit 1
+ fi
+
+ if [ "\${SIZE}" = "" ]; then
+ SIZE="1024"
+ fi
+
+ dd if="/dev/urandom" of="\${FILENAME}" bs="\${SIZE}" count=1 2>/dev/null
+ }
+
+ flush_disk_cache() {
+ if [ "\$(uname -s)" = "Darwin" ]; then
+ sync && sudo purge
+ elif [ "\$(uname -s)" = "Linux" ]; then
+ sync && echo 3 | sudo tee /proc/sys/vm/drop_caches >/dev/null
+ elif [[ "\$(uname -s)" == "MINGW"* ]]; then
+ PurgeStandbyList
+ fi
+ }
+
+ sandbox() {
+ RESOURCE="\${1}"
+
+ if [ "\${RESOURCE}" = "" ]; then
+ echo "usage: sandbox <path>" 1>&2
+ exit 1
+ fi
+
+ if [ ! -d "\${RESOURCES_DIR}/\${RESOURCE}" ]; then
+ echo "sandbox: the resource \"\${RESOURCE}\" does not exist"
+ exit 1
+ fi
+
+ rm -rf "\${SANDBOX_DIR:?}/\${RESOURCE}"
+ cp -R "\${RESOURCES_DIR}/\${RESOURCE}" "\${SANDBOX_DIR}/"
+ }
+
+ sandbox_repo() {
+ RESOURCE="\${1}"
+
+ sandbox "\${RESOURCE}"
+
+ if [ -d "\${SANDBOX_DIR}/\${RESOURCE}/.gitted" ]; then
+ mv "\${SANDBOX_DIR}/\${RESOURCE}/.gitted" "\${SANDBOX_DIR}/\${RESOURCE}/.git";
+ fi
+ if [ -f "\${SANDBOX_DIR}/\${RESOURCE}/gitattributes" ]; then
+ mv "\${SANDBOX_DIR}/\${RESOURCE}/gitattributes" "\${SANDBOX_DIR}/\${RESOURCE}/.gitattributes";
+ fi
+ if [ -f "\${SANDBOX_DIR}/\${RESOURCE}/gitignore" ]; then
+ mv "\${SANDBOX_DIR}/\${RESOURCE}/gitignore" "\${SANDBOX_DIR}/\${RESOURCE}/.gitignore";
+ fi
+ }
+
+ cd "\${SANDBOX_DIR}"
+EOF
+
+ if [ "${PREPARE}" != "" ]; then
+ echo "" >> "${SANDBOX_DIR}/prepare.sh"
+ echo "${PREPARE}" >> "${SANDBOX_DIR}/prepare.sh"
+ fi
+
+ echo "${SANDBOX_DIR}/prepare.sh"
+}
+
+create_runscript() {
+ SCRIPT_NAME="${1}"; shift
+ CLI_PATH="${1}"; shift
+
+ if [[ "${CHDIR}" = "/"* ]]; then
+ START_DIR="${CHDIR}"
+ elif [ "${CHDIR}" != "" ]; then
+ START_DIR="${SANDBOX_DIR}/${CHDIR}"
+ else
+ START_DIR="${SANDBOX_DIR}"
+ fi
+
+ # our run script starts by chdir'ing to the sandbox or repository directory
+ echo -n "cd \"${START_DIR}\" && \"${CLI_PATH}\"" >> "${SANDBOX_DIR}/${SCRIPT_NAME}.sh"
+
+ for a in "$@"; do
+ echo -n " \"${a}\"" >> "${SANDBOX_DIR}/${SCRIPT_NAME}.sh"
+ done
+
+ echo "${SANDBOX_DIR}/${SCRIPT_NAME}.sh"
+}
+
+gitbench_usage() { echo "usage: gitbench command..."; }
+
+#
+# this is the function that the outer script calls to actually do the sandboxing and
+# invocation of hyperfine.
+#
+gitbench() {
+ NEXT=
+
+ # this test should run the given command in preparation of the tests
+ # this preparation script will be run _after_ repository creation and
+ # _before_ flushing the disk cache
+ PREPARE=
+
+ # this test should run within the given directory; this is a
+ # relative path beneath the sandbox directory.
+ CHDIR=
+
+ # this test should run `n` warmups
+ WARMUP=0
+
+ if [ "$*" = "" ]; then
+ gitbench_usage 1>&2
+ exit 1
+ fi
+
+ for a in "$@"; do
+ if [ "${NEXT}" = "warmup" ]; then
+ WARMUP="${a}"
+ NEXT=
+ elif [ "${NEXT}" = "prepare" ]; then
+ PREPARE="${a}"
+ NEXT=
+ elif [ "${NEXT}" = "chdir" ]; then
+ CHDIR="${a}"
+ NEXT=
+ elif [ "${a}" = "--warmup" ]; then
+ NEXT="warmup"
+ elif [ "${a}" = "--prepare" ]; then
+ NEXT="prepare"
+ elif [ "${a}" = "--chdir" ]; then
+ NEXT="chdir"
+ elif [[ "${a}" == "--"* ]]; then
+ echo "unknown argument: \"${a}\"" 1>&2
+ gitbench_usage 1>&2
+ exit 1
+ else
+ break
+ fi
+
+ shift
+ done
+
+ if [ "${NEXT}" != "" ]; then
+ echo "$(basename "$0"): option requires a value: --${NEXT}" 1>&2
+ gitbench_usage 1>&2
+ exit 1
+ fi
+
+ # sanity check
+
+ for a in "${SANDBOX[@]}"; do
+ if [ ! -d "$(resources_dir)/${a}" ]; then
+ echo "$0: no resource '${a}' found" 1>&2
+ exit 1
+ fi
+ done
+
+ if [ "$REPOSITORY" != "" ]; then
+ if [ ! -d "$(resources_dir)/${REPOSITORY}" ]; then
+ echo "$0: no repository resource '${REPOSITORY}' found" 1>&2
+ exit 1
+ fi
+ fi
+
+ # set up our sandboxing
+
+ SANDBOX_DIR="$(temp_dir)"
+
+ if [ "${BASELINE_CLI}" != "" ]; then
+ BASELINE_CLI_PATH=$(fullpath "${BASELINE_CLI}")
+ BASELINE_RUN_SCRIPT=$(create_runscript "baseline" "${BASELINE_CLI_PATH}" "$@")
+ fi
+ TEST_CLI_PATH=$(fullpath "${TEST_CLI}")
+ TEST_RUN_SCRIPT=$(create_runscript "test" "${TEST_CLI_PATH}" "$@")
+
+ PREPARE_SCRIPT="$(create_preparescript)"
+ ARGUMENTS=("--prepare" "bash ${PREPARE_SCRIPT}" "--warmup" "${WARMUP}")
+
+ if [ "${OUTPUT_STYLE}" != "" ]; then
+ ARGUMENTS+=("--style" "${OUTPUT_STYLE}")
+ fi
+
+ if [ "${SHOW_OUTPUT}" != "" ]; then
+ ARGUMENTS+=("--show-output")
+ fi
+
+ if [ "$JSON" != "" ]; then
+ ARGUMENTS+=("--export-json" "${JSON}")
+ fi
+
+ if [ "${BASELINE_CLI}" != "" ]; then
+ ARGUMENTS+=("-n" "${BASELINE_CLI} $*" "bash ${BASELINE_RUN_SCRIPT}")
+ fi
+
+ ARGUMENTS+=("-n" "${TEST_CLI} $*" "bash ${TEST_RUN_SCRIPT}")
+
+ hyperfine "${ARGUMENTS[@]}"
+ rm -rf "${SANDBOX_DIR:?}"
+}
diff --git a/tests/benchmarks/hash-object__text_100kb b/tests/benchmarks/hash-object__text_100kb
new file mode 100755
index 000000000..d77f224b9
--- /dev/null
+++ b/tests/benchmarks/hash-object__text_100kb
@@ -0,0 +1,7 @@
+#!/bin/bash -e
+
+. "$(dirname "$0")/benchmark_helpers.sh"
+
+gitbench --prepare "create_text_file text_100kb 102400 &&
+ flush_disk_cache" \
+ hash-object "text_100kb"
diff --git a/tests/benchmarks/hash-object__text_10mb b/tests/benchmarks/hash-object__text_10mb
new file mode 100755
index 000000000..215afc6c3
--- /dev/null
+++ b/tests/benchmarks/hash-object__text_10mb
@@ -0,0 +1,7 @@
+#!/bin/bash -e
+
+. "$(dirname "$0")/benchmark_helpers.sh"
+
+gitbench --prepare "create_text_file text_10mb 10485760 &&
+ flush_disk_cache" \
+ hash-object "text_10mb"
diff --git a/tests/benchmarks/hash-object__text_1kb b/tests/benchmarks/hash-object__text_1kb
new file mode 100755
index 000000000..1348b2fea
--- /dev/null
+++ b/tests/benchmarks/hash-object__text_1kb
@@ -0,0 +1,7 @@
+#!/bin/bash -e
+
+. "$(dirname "$0")/benchmark_helpers.sh"
+
+gitbench --prepare "create_text_file text_1kb 1024 &&
+ flush_disk_cache" \
+ hash-object "text_1kb"
diff --git a/tests/benchmarks/hash-object__text_cached_100kb b/tests/benchmarks/hash-object__text_cached_100kb
new file mode 100755
index 000000000..7c2a84125
--- /dev/null
+++ b/tests/benchmarks/hash-object__text_cached_100kb
@@ -0,0 +1,7 @@
+#!/bin/bash -e
+
+. "$(dirname "$0")/benchmark_helpers.sh"
+
+gitbench --prepare "create_text_file text_100kb 102400" \
+ --warmup 5 \
+ hash-object "text_100kb"
diff --git a/tests/benchmarks/hash-object__text_cached_10mb b/tests/benchmarks/hash-object__text_cached_10mb
new file mode 100755
index 000000000..311ddca4c
--- /dev/null
+++ b/tests/benchmarks/hash-object__text_cached_10mb
@@ -0,0 +1,7 @@
+#!/bin/bash -e
+
+. "$(dirname "$0")/benchmark_helpers.sh"
+
+gitbench --prepare "create_text_file text_10mb 10485760" \
+ --warmup 5 \
+ hash-object "text_10mb"
diff --git a/tests/benchmarks/hash-object__text_cached_1kb b/tests/benchmarks/hash-object__text_cached_1kb
new file mode 100755
index 000000000..44c06ead7
--- /dev/null
+++ b/tests/benchmarks/hash-object__text_cached_1kb
@@ -0,0 +1,7 @@
+#!/bin/bash -e
+
+. "$(dirname "$0")/benchmark_helpers.sh"
+
+gitbench --prepare "create_text_file text_1kb 1024" \
+ --warmup 5 \
+ hash-object "text_1kb"
diff --git a/tests/benchmarks/hash-object__write_text_100kb b/tests/benchmarks/hash-object__write_text_100kb
new file mode 100755
index 000000000..fb72c0927
--- /dev/null
+++ b/tests/benchmarks/hash-object__write_text_100kb
@@ -0,0 +1,9 @@
+#!/bin/bash -e
+
+. "$(dirname "$0")/benchmark_helpers.sh"
+
+gitbench --prepare "sandbox_repo empty_standard_repo &&
+ create_text_file text_100kb 102400 &&
+ flush_disk_cache" \
+ --chdir "empty_standard_repo" \
+ hash-object -w "../text_100kb"
diff --git a/tests/benchmarks/hash-object__write_text_10mb b/tests/benchmarks/hash-object__write_text_10mb
new file mode 100755
index 000000000..9da091986
--- /dev/null
+++ b/tests/benchmarks/hash-object__write_text_10mb
@@ -0,0 +1,9 @@
+#!/bin/bash -e
+
+. "$(dirname "$0")/benchmark_helpers.sh"
+
+gitbench --prepare "sandbox_repo empty_standard_repo &&
+ create_text_file text_10mb 10485760 &&
+ flush_disk_cache" \
+ --chdir "empty_standard_repo" \
+ hash-object "../text_10mb"
diff --git a/tests/benchmarks/hash-object__write_text_1kb b/tests/benchmarks/hash-object__write_text_1kb
new file mode 100755
index 000000000..ca34393ad
--- /dev/null
+++ b/tests/benchmarks/hash-object__write_text_1kb
@@ -0,0 +1,9 @@
+#!/bin/bash -e
+
+. "$(dirname "$0")/benchmark_helpers.sh"
+
+gitbench --prepare "sandbox_repo empty_standard_repo &&
+ create_text_file text_1kb 1024 &&
+ flush_disk_cache" \
+ --chdir "empty_standard_repo" \
+ hash-object "../text_1kb"
diff --git a/tests/benchmarks/hash-object__write_text_cached_100kb b/tests/benchmarks/hash-object__write_text_cached_100kb
new file mode 100755
index 000000000..6803eeeae
--- /dev/null
+++ b/tests/benchmarks/hash-object__write_text_cached_100kb
@@ -0,0 +1,9 @@
+#!/bin/bash -e
+
+. "$(dirname "$0")/benchmark_helpers.sh"
+
+gitbench --prepare "sandbox_repo empty_standard_repo &&
+ create_text_file text_100kb 102400" \
+ --warmup 5 \
+ --chdir "empty_standard_repo" \
+ hash-object "../text_100kb"
diff --git a/tests/benchmarks/hash-object__write_text_cached_10mb b/tests/benchmarks/hash-object__write_text_cached_10mb
new file mode 100755
index 000000000..4335cc86e
--- /dev/null
+++ b/tests/benchmarks/hash-object__write_text_cached_10mb
@@ -0,0 +1,9 @@
+#!/bin/bash -e
+
+. "$(dirname "$0")/benchmark_helpers.sh"
+
+gitbench --prepare "sandbox_repo empty_standard_repo &&
+ create_text_file text_10mb 10485760" \
+ --warmup 5 \
+ --chdir "empty_standard_repo" \
+ hash-object "../text_10mb"
diff --git a/tests/benchmarks/hash-object__write_text_cached_1kb b/tests/benchmarks/hash-object__write_text_cached_1kb
new file mode 100755
index 000000000..8a4c5c97b
--- /dev/null
+++ b/tests/benchmarks/hash-object__write_text_cached_1kb
@@ -0,0 +1,9 @@
+#!/bin/bash -e
+
+. "$(dirname "$0")/benchmark_helpers.sh"
+
+gitbench --prepare "sandbox_repo empty_standard_repo &&
+ create_text_file text_1kb 1024" \
+ --warmup 5 \
+ --chdir "empty_standard_repo" \
+ hash-object "../text_1kb"