-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add cmp utility #88
Add cmp utility #88
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #88 +/- ##
==========================================
+ Coverage 81.01% 84.95% +3.94%
==========================================
Files 10 12 +2
Lines 4245 5824 +1579
Branches 397 480 +83
==========================================
+ Hits 3439 4948 +1509
- Misses 806 856 +50
- Partials 0 20 +20
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
could you please run your benchmarks with hyperfine? time is too limited |
Will do! Have begun working on fuzzing support, fwiw, need to do some refactoring as I currently use process::exit() as a shortcut in a lot of places. |
cf8ebf5
to
b60342c
Compare
Added fuzz implementation to the first utility commit, ran it overnight with no issues spotted. Replaced my hand crafted benchmarks with hyperfine output (and made sure to have the system free of memory pressure / cpu interference). I have tried to run the GNU tests, but the script fails here. I only changed it to also symlink diffutils to cmp and to run the cmp tests. Can anyone spot anything obvious before I start diving in to investigate?
diff --git a/tests/run-upstream-testsuite.sh b/tests/run-upstream-testsuite.sh
index cb59834..f75b0b3 100755
--- a/tests/run-upstream-testsuite.sh
+++ b/tests/run-upstream-testsuite.sh
@@ -21,7 +21,7 @@
# (e.g. 'dev' or 'test').
# Unless overridden by the $TESTS environment variable, all tests in the test
# suite will be run. Tests targeting a command that is not yet implemented
-# (e.g. cmp, diff3 or sdiff) are skipped.
+# (e.g. diff3 or sdiff) are skipped.
scriptpath=$(dirname "$(readlink -f "$0")")
rev=$(git rev-parse HEAD)
@@ -57,6 +57,7 @@ upstreamrev=$(git rev-parse HEAD)
mkdir src
cd src
ln -s "$binary" diff
+ln -s "$binary" cmp
cd ../tests
if [[ -n "$TESTS" ]]
@@ -82,9 +83,9 @@ for test in $tests
do
result="FAIL"
url="$urlroot$test?id=$upstreamrev"
- # Run only the tests that invoke `diff`,
+ # Run only the tests that invoke `diff` or `cmp`,
# because other binaries aren't implemented yet
- if ! grep -E -s -q "(cmp|diff3|sdiff)" "$test"
+ if ! grep -E -s -q "(diff3|sdiff)" "$test"
then
sh "$test" 1> stdout.txt 2> stderr.txt && result="PASS" || exitcode=1
json+="{\"test\":\"$test\",\"result\":\"$result\"," |
clippy is reporting 9 trivial errors, could you address them to get the CI results green? |
This is a regression that also affects main, so not introduced by your changes. I've filed #90 and am investigating it. |
With the fix in #91, I ran the test suite on your branch, and I'm seeing the following differences: @@ -3,9 +3,9 @@
basic PASS
bignum PASS
binary FAIL
- brief-vs-stat-zero-kernel-lies SKIP
+ brief-vs-stat-zero-kernel-lies FAIL
bug-64316 PASS
- cmp SKIP
+ cmp FAIL
colliding-file-names FAIL
diff3 SKIP
excess-slash FAIL
@@ -30,9 +30,9 @@
strip-trailing-cr FAIL
timezone PASS
colors FAIL
- y2038-vs-32bit SKIP
+ y2038-vs-32bit PASS
-Summary: TOTAL: 31 / PASS: 6 / FAIL: 20 / SKIP: 5
+Summary: TOTAL: 31 / PASS: 7 / FAIL: 22 / SKIP: 2
Results written to /home/osomon/build/uutils/diffutils/tests/test-results.json The failure in the |
This is in preparation for adding the other diffutils commands, cmp, diff3, sdiff. We use a similar strategy to uutils/coreutils, with the single binary acting as one of the supported tools if called through a symlink with the appropriate name. When using the multi-tool binary directly, the utility needds to be the first parameter.
Fixed the clippy complaints and quite close to getting the cmp test to pass locally. I am a bit confused, though, as my system's GNU cmp also doesn't pass the tests. I think the test infrastructure may not be sanitizing locale environments properly or there is something else on my Fedora it is not liking. I got the failures on our tool down to a minimum, but still hit these, which the GNU cmp built from git also hits. Wonder if there is a weird corner case in my system messing things up:
I checked with strace that the cmp being run was appropriate fwiw. The last error message from GNU cmp is a real wth xD |
I ran Note that the The following upstream commits seem relevant: 4ee8300 and 9c5fcbd (and they were added after the last upstream release). |
Comparing further the output of the We have two options here:
I think that both approaches are equally interesting and valid (one favours the bleeding edge, the other compatibility with released versions), so feel free to pick whichever you prefer (I can help with updating the test script if you decide to go that way). |
Getting there, I went with updating the implementation to allow huge numbers - forward looking probably makes more sense =) I just need to investigate this one now:
Will hopefully get to it this evening. Thanks for the help so far! |
5ff1efc
to
a924d5e
Compare
I looked into it, and it turns out it's an issue in the test suite runner, not in your code. I filed #92 to track it. |
I created another fuzz target for parameter parsing and left it running overnight with no issues, do you think having this target is also useful? I think the hand-crafted argument parsing is more likely to go wrong, now that I think about it. cat fuzz/fuzz_targets/fuzz_cmp_args.rs
#![no_main]
#[macro_use]
extern crate libfuzzer_sys;
use diffutilslib::cmp;
use std::ffi::OsString;
fuzz_target!(|x: Vec<OsString>| {
let _ = cmp::parse_params(x.into_iter().peekable());
}); Any thoughts on how we could deny corpus entries to make it more useful? I thought about denying certain sizes if they do not have any -x or --whatever, or at least one of the known parameters. Like, if x.len() > 4 it needs to have some known parameters, but that excludes having over 4 positional parameters quite often, I suppose. Could be worth the tradeoff, still. |
Yes, I think it is definitely useful. For the record, we implement argument parsing by hand because ready-made argument parsers like clap do not offer the flexibility we need to replicate the GNU diffutils arguments. We'd lose in compatibility what we would gain in code simplicity.
I don't have prior experience with this, but perhaps we could use a dictionary? One additional thought: I see that you wrote integration tests for the new cmp command. Those are good and they provide a decent code coverage, but I would also suggest writing unit tests for |
54aeb86
to
30b898b
Compare
Added a dictionary, but ended up going with some Corpus rejection as well, as using the dictionary requires passing parameters to cargo fuzz, may be forgotten. The github action looks like it would need to be split per target if we need to pass dictionaries there - which makes me realize I never added the fuzz target there, will fix right now. In addition to the param parsing I added yesterday, I also added a few more integration tests to cover more of the fast paths that had no coverage (some of them were covered by the GNU cmp tests, but good to have our own tests as well I suppose). |
a534480
to
5966d1d
Compare
I may need to set up a build on a Windows VM xD |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking really good, thanks for such a high-quality first contribution!
I have a handful of minor suggestions/questions, see inline.
The utility should support all the arguments supported by GNU cmp and perform slightly better. On a "bad" scenario, ~36M files which are completely different, our version runs in ~72% of the time of the original on my M1 Max: > hyperfine --warmup 1 -i --output=pipe \ 'cmp -l huge huge.3' Benchmark 1: cmp -l huge huge.3 Time (mean ± σ): 3.237 s ± 0.014 s [User: 2.891 s, System: 0.341 s] Range (min … max): 3.221 s … 3.271 s 10 runs Warning: Ignoring non-zero exit code. > hyperfine --warmup 1 -i --output=pipe \ '../target/release/diffutils cmp -l huge huge.3' Benchmark 1: ../target/release/diffutils cmp -l huge huge.3 Time (mean ± σ): 2.392 s ± 0.009 s [User: 1.978 s, System: 0.406 s] Range (min … max): 2.378 s … 2.406 s 10 runs Warning: Ignoring non-zero exit code. Our cmp runs in ~116% of the time when comparing libxul.so to the chromium-browser binary with -l and -b. In a best case scenario of comparing 2 files which are the same except for the last byte, our tool is slightly faster.
Octal conversion and simple integer to string both show up in profiling. This change improves comparing ~36M completely different files wth both -l and -b by ~11-13%.
This makes the code less readable, but gets us a massive improvement to performance. Comparing ~36M completely different files now takes ~40% of the time. Compared to GNU cmp, we now run the same comparison in ~26% of the time. This also improves comparing binary files. A comparison of chromium and libxul now takes ~60% of the time. We also beat GNU cmpi by about the same margin. Before: > hyperfine --warmup 1 -i --output=pipe \ '../target/release/diffutils cmp -l huge huge.3' Benchmark 1: ../target/release/diffutils cmp -l huge huge.3 Time (mean ± σ): 2.000 s ± 0.016 s [User: 1.603 s, System: 0.392 s] Range (min … max): 1.989 s … 2.043 s 10 runs Warning: Ignoring non-zero exit code. > hyperfine --warmup 1 -i --output=pipe \ '../target/release/diffutils cmp -l -b \ /usr/lib64/chromium-browser/chromium-browser \ /usr/lib64/firefox/libxul.so' Benchmark 1: ../target/release/diffutils cmp -l -b /usr/lib64/chromium-browser/chromium-browser /usr/lib64/firefox/libxul.so Time (mean ± σ): 24.704 s ± 0.162 s [User: 21.948 s, System: 2.700 s] Range (min … max): 24.359 s … 24.889 s 10 runs Warning: Ignoring non-zero exit code. After: > hyperfine --warmup 1 -i --output=pipe \ '../target/release/diffutils cmp -l huge huge.3' Benchmark 1: ../target/release/diffutils cmp -l huge huge.3 Time (mean ± σ): 849.5 ms ± 6.2 ms [User: 538.3 ms, System: 306.8 ms] Range (min … max): 839.4 ms … 857.7 ms 10 runs Warning: Ignoring non-zero exit code. > hyperfine --warmup 1 -i --output=pipe \ '../target/release/diffutils cmp -l -b \ /usr/lib64/chromium-browser/chromium-browser \ /usr/lib64/firefox/libxul.so' Benchmark 1: ../target/release/diffutils cmp -l -b /usr/lib64/chromium-browser/chromium-browser /usr/lib64/firefox/libxul.so Time (mean ± σ): 14.646 s ± 0.040 s [User: 12.328 s, System: 2.286 s] Range (min … max): 14.585 s … 14.702 s 10 runs Warning: Ignoring non-zero exit code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great now, thanks!
The utility should support all the functionality supported by GNU cmp and perform quite a bit better. I split the commits for the actual utility in 3, with the base one being a full implementation of the features of GNU cmp, integration tests included.
The commits on top of that add optimizations which remove some and then all of rust fmt usage for the potentially long-running loop when --verbose is passed in. I wanted to separate them, first of all because it gives us good base for debugging potential issues with the optimized version, but secondly because I would understand the project preferring the slower version with more readable code.
That said, I do think it makes sense to adopt the optimized version as the gains are massive - on the order of 100% gains on my M1 Max comparing ~36M files that are completely different. Following tests run after warming up the I/O cache:
Baseline - GNU cmp
Unoptimized diffutils - ~74% of the time
Optimized diffutils - 26% of the time