diff --git a/.gitignore b/.gitignore index 363496b5..d5fd457b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .zig-cache/ node_modules/ zig-out/ +.benchtree/ diff --git a/bench.sh b/bench.sh new file mode 100755 index 00000000..aa123f9e --- /dev/null +++ b/bench.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +# builds the last 8 commits and compares the performance of odiff +# against the current git HEAD + +function print_help() { + echo "usage: ./bench.sh [-n ] [--unstaged] [--help]" + echo "" + echo "-c, --clean : cleans the build directory (default: false)" + echo "-s, --skip-build : skips building a commit if it already exists (default: true)" + echo "-f, --from : compares the changes from the given commit (default: HEAD)" + echo "-n : number of commits to compare (default: 8)" + echo "-u, --unstaged : also builds and compares with unstaged changes (default: false)" + echo "-r, --runs : number of runs (default: 10)" + echo "-h, --help : prints this help message" +} + +# must be in sync with build.zig +exe_name="odiff" +function build_project() { + local id="$1" + echo "Building $id" + zig build -Doptimize=ReleaseFast --prefix-exe-dir "bench/$id" + if [[ $? -ne 0 ]]; then + echo "Failed to build $id" + return 1 + fi + echo "Successfully built $id" +} + +function checkout_and_build() { + local commit_id="$1" + local skip_if_exists="$2" + + echo "Checking out $commit_id" + if [[ $commit_id != "unstaged" ]]; then + if [[ $skip_if_exists == true ]]; then + if [[ -d "zig-out/bench/$commit_id" ]]; then + echo "Skipping build of $commit_id" + return + fi + fi + + if [[ ! -d ".benchtree" ]]; then + git worktree add ".benchtree" "$commit_id" + fi + pushd ".benchtree" + git checkout "$commit_id" + build_project "$commit_id" + if [[ $? -ne 0 ]]; then + popd + return 1 + fi + popd + mkdir -p "zig-out/bench/$commit_id" + mv ".benchtree/zig-out/bench/$commit_id/$exe_name" "zig-out/bench/$commit_id/$exe_name" + echo "Successfully built zig-out/bench/$commit_id/$exe_name" + else + build_project "$commit_id" + fi +} + +function get_commit_ids() { + local from="$1" + local commit_ids=() + + if $unstaged && [[ -n $(git diff --name-only) ]]; then + commit_ids+=("unstaged") + fi + + commit_ids+=($(git rev-parse "$from")) + if [[ $num_commits -gt 1 ]]; then + for i in $(seq 1 $(($num_commits-1))); do + commit_ids+=($(git rev-parse "$from"~"$i")) + done + fi + echo "${commit_ids[@]}" +} + +args=$(getopt -o "n:uh" --long "unstaged,help,runs,from" -n "bench.sh" -- "$@") + +num_commits=8 +unstaged=false +clean=false +skip_if_exists=true +runs=10 +from="HEAD" +while true; do + case "$1" in + -n) + num_commits="$2" + shift 2 + ;; + -c | --clean) + clean=true + shift + ;; + -s | --skip-build) + skip_if_exists=true + shift + ;; + -u | --unstaged) + unstaged=true + shift + ;; + -r | --runs) + runs="$2" + shift 2 + ;; + -f | --from) + from="$2" + shift 2 + ;; + -h | --help) + print_help + exit 0 + ;; + --) + shift + break + ;; + "") + break + ;; + *) + print_help + exit 1 + ;; + esac +done + +if $clean; then + echo "Cleaning output directory" + rm -rf "zig-out/bench" +fi + +commit_ids=($(get_commit_ids "$from")) +runnable_commit_ids=() +echo "Building $num_commits commits" +for commit_id in "${commit_ids[@]}"; do + checkout_and_build "$commit_id" "$skip_if_exists" + if [[ $? -eq 0 ]]; then + runnable_commit_ids+=("$commit_id") + fi +done + +# ensure that hyperfine is installed +if ! command -v hyperfine &> /dev/null; then + echo "hyperfine is not installed." + echo "Please install hyperfine using the following command:" + echo "cargo install hyperfine" + exit 1 +fi + +bench_cmd="hyperfine -i -N --warmup 1 --runs $runs " +options="./images/www.cypress.io.png ./images/www.cypress.io-1.png ./images/www.cypress-diff.png" +for commit_id in "${runnable_commit_ids[@]}"; do + bench_cmd+="\"$PWD/zig-out/bench/$commit_id/$exe_name $options\" " +done +eval "$bench_cmd" diff --git a/src/io/io.zig b/src/io/io.zig index c64e4f5b..43915d0c 100644 --- a/src/io/io.zig +++ b/src/io/io.zig @@ -170,13 +170,9 @@ pub fn saveImageWithFormat(img: Image, file_path: []const u8, format: ImageForma .truncate = true, }); defer file.close(); - var buffer: [1024 * 1024]u8 = undefined; - var file_writer = file.writer(&buffer); switch (format) { - .png => try png.save(img, &file_writer.interface), + .png => try png.save(img, file), else => return error.UnsupportedFormat, } - - try file_writer.interface.flush(); } diff --git a/src/io/png.zig b/src/io/png.zig index 2cad0ca3..b3a65e80 100644 --- a/src/io/png.zig +++ b/src/io/png.zig @@ -38,7 +38,7 @@ pub fn load(allocator: std.mem.Allocator, data: []const u8) !Image { }; } -pub fn save(img: Image, writer: *std.Io.Writer) !void { +pub fn save(img: Image, file: std.fs.File) !void { const ctx = c.spng_ctx_new(c.SPNG_CTX_ENCODER) orelse return error.OutOfMemory; defer c.spng_ctx_free(ctx); @@ -53,6 +53,10 @@ pub fn save(img: Image, writer: *std.Io.Writer) !void { }; if (c.spng_set_ihdr(ctx, &ihdr) != 0) return error.InvalidData; + if (c.spng_set_option(ctx, c.SPNG_FILTER_CHOICE, c.SPNG_DISABLE_FILTERING) != 0) return error.InvalidData; + + var buffer: [4096]u8 = undefined; + var file_writer = file.writer(&buffer); if (c.spng_set_png_stream( ctx, struct { @@ -66,11 +70,11 @@ pub fn save(img: Image, writer: *std.Io.Writer) !void { return 0; } }.writeFn, - @ptrCast(@alignCast(writer)), + @ptrCast(@alignCast(&file_writer.interface)), ) != 0) return error.InvalidData; - const pixel_data = img.slice(); - const res = c.spng_encode_image(ctx, @ptrCast(@alignCast(pixel_data.ptr)), pixel_data.len * @sizeOf(u32), c.SPNG_FMT_PNG, c.SPNG_ENCODE_FINALIZE); + const u8_slice: []u8 = @ptrCast(img.slice()); + const res = c.spng_encode_image(ctx, u8_slice.ptr, u8_slice.len, c.SPNG_FMT_PNG, c.SPNG_ENCODE_FINALIZE); if (res != 0) { const err_msg = std.mem.span(c.spng_strerror(res)); std.log.err("writePNG: failed to encode image {s}", .{err_msg});