Skip to content

Commit 2861538

Browse files
committed
Add macOS bench
1 parent e6650cb commit 2861538

4 files changed

Lines changed: 159 additions & 41 deletions

File tree

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ bidiff cycle old_file new_file
3535

3636
## Benchmarks
3737

38-
System: AMD Ryzen Threadripper 2950X (32 cores), 60 GiB RAM, Linux 6.12.
38+
### System: AMD Ryzen Threadripper 2950X (32 cores), 60 GiB RAM, Linux 6.12.
3939

4040
Default settings (1 MiB chunks, file-backed hash table). Memory column shows peak anonymous RSS during diffing.
4141

@@ -46,7 +46,7 @@ Default settings (1 MiB chunks, file-backed hash table). Memory column shows pea
4646
| Firefox 71.0b11 → b12 | 198 MiB | 10.9 MiB | 5.49% | 0.14s | 0.76s | 18.6 MiB | 0.73s | 147 MiB |
4747
| Chrome 78.0.3904.97 → 108 | 145 MiB | 8.3 MiB | 5.71% | 0.11s | 0.79s | 16.9 MiB | 0.75s | 147 MiB |
4848

49-
### With `--max` (zstd level 22)
49+
#### With `--max` (zstd level 22)
5050

5151
Smaller patches at the cost of much slower diff times. Patch application speed is similar.
5252

@@ -57,6 +57,17 @@ Smaller patches at the cost of much slower diff times. Patch application speed i
5757
| Firefox 71.0b11 → b12 | 198 MiB | 8.3 MiB | 4.20% | 0.11s | 1m 2s | 62.5 MiB | 58.5s | 189 MiB |
5858
| Chrome 78.0.3904.97 → 108 | 145 MiB | 5.6 MiB | 3.84% | 0.09s | 1m 18s | 57.6 MiB | 1m 21s | 186 MiB |
5959

60+
### System: Apple M2 Max (12 cores), 32 GiB RAM, macOS 26.1.
61+
62+
Default settings (1 MiB chunks, file-backed hash table). Memory column shows peak anonymous RSS during diffing.
63+
64+
| Test case | New size | Patch size | Ratio | Patch time | Diff time | Memory | Diff time (RAM) | Memory (RAM) |
65+
|-----------|----------|------------|-------|------------|-----------|--------|-----------------|--------------|
66+
| Wine 4.18 → 4.19 | 201.4 MiB | 249 KiB | 0.120% | 0.194s | 0.258s | 25.7 MiB | 0.198s | 149.7 MiB |
67+
| Linux 5.3 → 5.4 | 894.7 MiB | 6.8 MiB | 0.762% | 0.918s | 1.400s | 51.0 MiB | 1.004s | 563.4 MiB |
68+
| Firefox 71.0b11 → b12 | 197.8 MiB | 10.9 MiB | 5.486% | 0.118s | 0.614s | 19.0 MiB | 0.578s | 147.9 MiB |
69+
| Chrome 78.0.3904.97 → 108 | 145.4 MiB | 57.0 MiB | 39.213% | 0.064s | 0.642s | 12.9 MiB | 0.640s | 13.0 MiB |
70+
6071
### Comparison with bidiff 1.1, bsdiff, and xdelta3
6172

6273
bidiff 1.1 (suffix arrays, single-threaded scan), bsdiff 4.3, xdelta3 3.0.11. Same test system.

bench/bench.sh

Lines changed: 112 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,39 @@ BIDIFF="$ROOT/target/release/bidiff"
1010

1111
mkdir -p "$DATA"
1212

13+
# Portable sha256 (Linux has sha256sum, macOS has shasum)
14+
sha256() {
15+
if command -v sha256sum >/dev/null 2>&1; then
16+
sha256sum "$1" | cut -d' ' -f1
17+
else
18+
shasum -a 256 "$1" | cut -d' ' -f1
19+
fi
20+
}
21+
22+
# ── System info ──────────────────────────────────────────────────────────────
23+
24+
system_info() {
25+
local cpu cores ram_gib os_name
26+
case "$(uname -s)" in
27+
Darwin)
28+
cpu=$(sysctl -n machdep.cpu.brand_string 2>/dev/null)
29+
cores=$(sysctl -n hw.ncpu 2>/dev/null)
30+
ram_gib=$(sysctl -n hw.memsize 2>/dev/null | awk '{printf "%g", $1 / 1073741824}')
31+
os_name="$(sw_vers -productName) $(sw_vers -productVersion)"
32+
;;
33+
Linux)
34+
cpu=$(grep -m1 'model name' /proc/cpuinfo | sed 's/.*: //')
35+
cores=$(nproc)
36+
ram_gib=$(awk '/MemTotal/ {printf "%g", $2 / 1048576}' /proc/meminfo)
37+
os_name="Linux $(uname -r | cut -d- -f1)"
38+
;;
39+
*)
40+
cpu="unknown"; cores="?"; ram_gib="?"; os_name="$(uname -s)"
41+
;;
42+
esac
43+
echo "System: ${cpu} (${cores} cores), ${ram_gib} GiB RAM, ${os_name}."
44+
}
45+
1346
# ── Download helpers ─────────────────────────────────────────────────────────
1447

1548
download() {
@@ -21,7 +54,7 @@ download() {
2154
echo " downloading: $(basename "$dest")"
2255
curl -fSL --progress-bar -o "$dest" "$url"
2356
local actual
24-
actual=$(sha256sum "$dest" | cut -d' ' -f1)
57+
actual=$(sha256 "$dest")
2558
if [ "$actual" != "$sha256" ]; then
2659
echo "ERROR: checksum mismatch for $dest"
2760
echo " expected: $sha256"
@@ -88,9 +121,11 @@ extract_chrome() {
88121
return
89122
fi
90123
echo " extracting: $(basename "$dest") from $(basename "$deb")"
91-
dpkg-deb --fsys-tarfile "$deb" | tar xf - --to-stdout ./opt/google/chrome/chrome > "$dest"
124+
local data_tar
125+
data_tar=$(ar t "$deb" | grep '^data\.tar')
126+
ar p "$deb" "$data_tar" | tar xf - -O ./opt/google/chrome/chrome > "$dest"
92127
local actual
93-
actual=$(sha256sum "$dest" | cut -d' ' -f1)
128+
actual=$(sha256 "$dest")
94129
if [ "$actual" != "$sha256" ]; then
95130
echo "ERROR: checksum mismatch for $dest"
96131
echo " expected: $sha256"
@@ -157,12 +192,12 @@ add_pair "Chrome 78.0.3904.97 → 108" "$DATA/chrome-78.0.3904.97" "$DATA/chrome
157192
parse_cycle() {
158193
local line="$1"
159194
# Extract fields by keyword
160-
PATCH_SIZE=$(echo "$line" | grep -oP 'patch \K[0-9.]+ [A-Za-z]+')
161-
RATIO=$(echo "$line" | grep -oP '[0-9.]+(?=% of)')
162-
NEW_SIZE=$(echo "$line" | grep -oP '% of \K[0-9.]+ [A-Za-z]+')
163-
DTIME_RAW=$(echo "$line" | grep -oP 'dtime \K[0-9.]+[a-zµ]+')
164-
PTIME_RAW=$(echo "$line" | grep -oP 'ptime \K[0-9.]+[a-zµ]+')
165-
ANON=$(echo "$line" | grep -oP 'anon \K[0-9.]+ [A-Za-z]+' || echo "")
195+
PATCH_SIZE=$(echo "$line" | grep -oE 'patch [0-9.]+ [A-Za-z]+' | sed 's/^patch //')
196+
RATIO=$(echo "$line" | grep -oE '[0-9.]+% of' | sed 's/% of//')
197+
NEW_SIZE=$(echo "$line" | grep -oE '% of [0-9.]+ [A-Za-z]+' | sed 's/^% of //')
198+
DTIME_RAW=$(echo "$line" | grep -oE 'dtime [^ ]+' | sed 's/^dtime //')
199+
PTIME_RAW=$(echo "$line" | grep -oE 'ptime [^ ]+' | sed 's/^ptime //')
200+
ANON=$(echo "$line" | grep -oE 'anon [0-9.]+ [A-Za-z]+' | sed 's/^anon //' || echo "")
166201
}
167202

168203
# Convert Rust Duration debug format to seconds
@@ -179,6 +214,16 @@ to_seconds() {
179214
fi
180215
}
181216

217+
# Average a list of numeric values (passed as arguments)
218+
average() {
219+
local sum=0 n=0
220+
for v in "$@"; do
221+
sum=$(awk "BEGIN {printf \"%.6f\", $sum + $v}")
222+
n=$((n + 1))
223+
done
224+
awk "BEGIN {printf \"%.3f\", $sum / $n}"
225+
}
226+
182227
# Format seconds for display: seconds if <60, Xm Ys if >=60
183228
fmt_time() {
184229
local secs="$1"
@@ -208,6 +253,15 @@ declare -a R_MEM=()
208253
declare -a R_DTIME_RAM=()
209254
declare -a R_MEM_RAM=()
210255

256+
RUNS=5
257+
258+
# On macOS, run one extra warmup iteration (discarded) to avoid prefetch distortion
259+
if [ "$(uname -s)" = "Darwin" ]; then
260+
WARMUP=1
261+
else
262+
WARMUP=0
263+
fi
264+
211265
for i in "${!NAMES[@]}"; do
212266
name="${NAMES[$i]}"
213267
older="${OLDERS[$i]}"
@@ -216,53 +270,73 @@ for i in "${!NAMES[@]}"; do
216270
echo ""
217271
echo "--- $name ---"
218272

219-
# Default mode (file-backed hash table)
220-
echo " file-backed + anon measurement..."
221-
output=$("$BIDIFF" cycle "$older" "$newer" --with-anon 2>/dev/null)
222-
echo " $output"
223-
parse_cycle "$output"
273+
# Timing runs: 5x each, no --with-anon so numbers aren't contaminated
274+
declare -a dtimes=() ptimes=() dtimes_ram=()
275+
276+
TOTAL=$((WARMUP + RUNS))
277+
echo " file-backed (timing, ${RUNS} runs)..."
278+
for r in $(seq 1 $TOTAL); do
279+
output=$("$BIDIFF" cycle "$older" "$newer" 2>/dev/null)
280+
if [ "$r" -le "$WARMUP" ]; then
281+
echo " warmup: $output"
282+
continue
283+
fi
284+
actual=$((r - WARMUP))
285+
echo " run $actual/$RUNS: $output"
286+
parse_cycle "$output"
287+
dtimes+=("$(to_seconds "$DTIME_RAW")")
288+
ptimes+=("$(to_seconds "$PTIME_RAW")")
289+
done
224290

225291
R_NAME+=("$name")
226292
R_NEW_SIZE+=("$NEW_SIZE")
227293
R_PATCH_SIZE+=("$PATCH_SIZE")
228294
R_RATIO+=("$RATIO")
229-
R_PTIME+=("$(to_seconds "$PTIME_RAW")")
230-
R_DTIME+=("$(to_seconds "$DTIME_RAW")")
295+
R_DTIME+=("$(average "${dtimes[@]}")")
296+
R_PTIME+=("$(average "${ptimes[@]}")")
297+
298+
echo " RAM (timing, ${RUNS} runs)..."
299+
for r in $(seq 1 $TOTAL); do
300+
output=$("$BIDIFF" cycle "$older" "$newer" --ram 2>/dev/null)
301+
if [ "$r" -le "$WARMUP" ]; then
302+
echo " warmup: $output"
303+
continue
304+
fi
305+
actual=$((r - WARMUP))
306+
echo " run $actual/$RUNS: $output"
307+
parse_cycle "$output"
308+
dtimes_ram+=("$(to_seconds "$DTIME_RAW")")
309+
done
310+
311+
R_DTIME_RAM+=("$(average "${dtimes_ram[@]}")")
312+
313+
unset dtimes ptimes dtimes_ram
314+
315+
# Memory measurement runs (single run each, separate from timing)
316+
echo " file-backed (memory)..."
317+
output=$("$BIDIFF" cycle "$older" "$newer" --with-anon 2>/dev/null)
318+
echo " $output"
319+
parse_cycle "$output"
231320
R_MEM+=("$ANON")
232321

233-
# RAM mode
234-
echo " RAM mode + anon measurement..."
322+
echo " RAM (memory)..."
235323
output=$("$BIDIFF" cycle "$older" "$newer" --ram --with-anon 2>/dev/null)
236324
echo " $output"
237325
parse_cycle "$output"
238-
239-
R_DTIME_RAM+=("$(to_seconds "$DTIME_RAW")")
240326
R_MEM_RAM+=("$ANON")
241327
done
242328

243329
# ── Print summary table ─────────────────────────────────────────────────────
244330

245331
echo ""
246332
echo ""
247-
echo "=== Results ==="
333+
system_info
334+
echo ""
335+
echo "Default settings (1 MiB chunks, file-backed hash table). Memory column shows peak anonymous RSS during diffing."
248336
echo ""
249-
printf "| %-28s | %8s | %12s | %6s | %10s | %10s | %10s | %15s | %12s |\n" \
250-
"Test case" "New size" "Patch size" "Ratio" "Patch time" "Diff time" "Memory" "Diff time (RAM)" "Memory (RAM)"
251-
printf "|%-30s|%10s|%14s|%8s|%12s|%12s|%12s|%17s|%14s|\n" \
252-
"------------------------------" "----------" "--------------" "--------" "------------" "------------" "------------" "-----------------" "--------------"
337+
echo "| Test case | New size | Patch size | Ratio | Patch time | Diff time | Memory | Diff time (RAM) | Memory (RAM) |"
338+
echo "|-----------|----------|------------|-------|------------|-----------|--------|-----------------|--------------|"
253339

254340
for i in "${!R_NAME[@]}"; do
255-
printf "| %-28s | %8s | %12s | %5s%% | %10s | %10s | %10s | %15s | %12s |\n" \
256-
"${R_NAME[$i]}" \
257-
"${R_NEW_SIZE[$i]}" \
258-
"${R_PATCH_SIZE[$i]}" \
259-
"${R_RATIO[$i]}" \
260-
"$(fmt_time "${R_PTIME[$i]}")" \
261-
"$(fmt_time "${R_DTIME[$i]}")" \
262-
"${R_MEM[$i]}" \
263-
"$(fmt_time "${R_DTIME_RAM[$i]}")" \
264-
"${R_MEM_RAM[$i]}"
341+
echo "| ${R_NAME[$i]} | ${R_NEW_SIZE[$i]} | ${R_PATCH_SIZE[$i]} | ${R_RATIO[$i]}% | $(fmt_time "${R_PTIME[$i]}") | $(fmt_time "${R_DTIME[$i]}") | ${R_MEM[$i]} | $(fmt_time "${R_DTIME_RAM[$i]}") | ${R_MEM_RAM[$i]} |"
265342
done
266-
267-
echo ""
268-
echo "Done."

cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ rayon = "1.11"
2525

2626
libc = "0.2"
2727
tempfile = "3"
28+
mach-sys = "0.5.4"

cli/src/main.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,9 @@ struct Cycle {
8383
with_anon: bool,
8484
}
8585

86-
/// Read anonymous RSS (heap + anonymous mmap) from /proc/self/status.
86+
/// Read anonymous RSS (heap + anonymous mmap).
8787
/// Excludes file-backed mmap pages — only counts "real" memory allocations.
88+
#[cfg(target_os = "linux")]
8889
fn read_rss_anon() -> u64 {
8990
std::fs::read_to_string("/proc/self/status")
9091
.ok()
@@ -100,6 +101,37 @@ fn read_rss_anon() -> u64 {
100101
.unwrap_or(0)
101102
}
102103

104+
/// Read anonymous RSS via mach task_info (TASK_VM_INFO).
105+
/// The `internal` field counts anonymous memory in bytes.
106+
#[cfg(target_os = "macos")]
107+
fn read_rss_anon() -> u64 {
108+
use mach_sys::kern_return::KERN_SUCCESS;
109+
use mach_sys::task::task_info;
110+
use mach_sys::task_info::{task_vm_info_t, TASK_VM_INFO, TASK_VM_INFO_COUNT};
111+
use mach_sys::traps::mach_task_self;
112+
use std::mem::MaybeUninit;
113+
unsafe {
114+
let mut info = MaybeUninit::<task_vm_info_t>::uninit();
115+
let mut count = TASK_VM_INFO_COUNT;
116+
let kr = task_info(
117+
mach_task_self(),
118+
TASK_VM_INFO,
119+
info.as_mut_ptr() as *mut _,
120+
&mut count,
121+
);
122+
if kr != KERN_SUCCESS {
123+
return 0;
124+
}
125+
let info = info.assume_init();
126+
info.internal as u64
127+
}
128+
}
129+
130+
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
131+
fn read_rss_anon() -> u64 {
132+
0
133+
}
134+
103135
fn format_size(bytes: u64) -> String {
104136
if bytes >= 1024 * 1024 {
105137
format!("{:.1} MiB", bytes as f64 / (1024.0 * 1024.0))

0 commit comments

Comments
 (0)