Ulepszenie przyrostowe czasy testowania Rust
Docelowi odbiorcy |
|
Pochodzenie | Moje doświadczenie w Rust. |
Nastrój | Trochę udany; trochę rozczarowany. |
Język | Obcy. Moim pierwszym język jest angielki. Jeśli zobaczysz błąd, niezależnie od tego, czy chodzi o pisownię, gramatykę czy dobór słow, skomentuj to! |
Rust jest znany z wolnego czasu kompilacji. Spędziłem dużo czasu próbując poprawić przyrostowe czasy kompilacji testowania dla mojego projektu git-branchless w https://github.com/arxanas/git-branchless/pull/650. Oto dyskusja wyników.
Spis treści:
Streszczenie wykonawcze
- “Przyrostowe testowania” odnosi się tylko do zmiany kodu testu integracyjnego i przebudowanie. Kod źródłowy pozostaje niezmienony.
- Ostatecznie udało mi się skrócić przyrostowy czas testowania z ~6.9sek do ~1.7sek (~4x). Inne sposoby na skrócenie czasu kompilacji przyniosły niewielką lub żadną poprawę.
W celach informacyjnych, oto najlepsze artykuły dotyczące konceptualnego zrozumienia modelu kompilacji Rust i poprawy czasu kompilacji:
- Fast Rust Builds (matklad.github.io, 2021)
- How to Test (matklad.github.io, 2021)
- Why is my Rust build so slow? (fasterthanli.me, 2021)
- Tips for Faster Rust Builds (endler.dev, 2020-2022)
- W rzeczywistości, wówczas nie czytałem tego artykułu, ale przeczytałem kiedy napisałem ten artykuł.
Szczegóły projektu
Oto jak duży był mój projekt git-branchless przed pull request:
git-branchless/src
: 12060 linii.git-branchless/tests
: 12897 linii.- Zauważ, że w dużej mierze opiera się na “snapshot testing”, więc większość tych linii kodu to “multiline string literals”.
git-branchless-lib/src
: 12406 linii.
Dopuszczalne są długie czasy kompilacji i konsolidacja, ale aby zoptymalizować sprzężenie zwrotnego programowania, potrzebuję krótkich czasów testowania. W szczególności chcę iterować nad pracowaniem pewnego testu. (IntelliJ ma fajną funkcję automatycznego ponownego uruchamiania danego testu, gdy występuje zmiana w kodu źródła ale trwanie zbyt długo ponownej kompilacji zmniejsza jej przydatność.)
Na początek zbudowanie binary test_amend
(który testuje subcommand amend
) po dodaniu pojedynczego komentarza do jego pliku testu (bez zmian w library!) wymaga ~6.9sek:
$ hyperfine --warmup 3 --prepare 'echo "// @nocommit test" >>git-branchless/tests/command/test_amend.rs' 'cargo test --test mod --no-run'
Benchmark 1: cargo test --test mod --no-run
Time (mean ± σ): 6.927 s ± 0.123 s [User: 7.652 s, System: 1.738 s]
Range (min … max): 6.754 s … 7.161 s 10 runs
Tragedia! To nie duży projekt, a zmieniamy tylko testy, więc nie powinno to wymagać tak długiego czasu iteracji.
Podzielenie się na więcej crates
W pull request wyodrębniłem do dodatkowych dziewięciu crates, co daje przyrostowy czas kompilacji testu wynoszący ~1.7sek (~4x poprawa).
$ hyperfine --warmup 3 --prepare 'echo "// @nocommit test" >>git-branchless/tests/test_amend.rs' 'cargo test --test test_amend --no-run'
Benchmark 1: cargo test --test test_amend --no-run
Time (mean ± σ): 1.771 s ± 0.012 s [User: 1.471 s, System: 0.330 s]
Range (min … max): 1.750 s … 1.793 s 10 runs
Jeśli chodzi o poprawę względną, to znaczna, ale jeśli chodzi o poprawę bezwzględną, sądzę, że jest raczej słaba. Bym oczekiwał 100-200ms na parsing, macro expansion, typechecking, i code generation (bez optimacji) dla pliku tego rozmiaru (~1k linii, głownie długi wartości łańcuchowe).
Dodatkowe, dzielenie na wiele crates utrudnia rozpowszechnianie projektu za pośrednictwem crates.io:
- Każdy crate wymaga indywidualnego publikowania na crates.io (nie można opublikować crate z zależnościami Git w crates.io).
- Dlatego muszę zarządzać wersjami i licencji każdego crate.
- Według mojej ankiety wśród użytkowników, większość moich użytkowników instaluje program za pomocą
cargo
. Na pytanie “Jak zainstalowałeś git-branchless?”, odpowiedzi są następujące (stan na ):- 7/18 (38.9%): przez
cargo install git-branchless
. - 4/18 (22.2%): za pośrednictwem tradycyjnego systemowego menedżera pakietów.
- 4/18 (22.2%): przez Nix lub NixOS
- 2/18 (11.1%): klonując repozytorium i ręcznie budując i instalując.
- 1/18 (5.6%): przez artefakt kompilacji z GitHub Actions
- 7/18 (38.9%): przez
Niestety, muszę publicznie udostępniać wewnętrzne moduły na crates.io tylko po to, aby uzyskać rozsądne czasy kompilacji.
Czas bez operacji
Wtedy, mierzę czas bez operacji [ang. “no-op”] na ~350ms dla mniejszej testowe crate z niewieloma zależnościami:
$ hyperfine --warmup 3 'cargo test -p git-branchless-test --no-run'
Benchmark 1: cargo test -p git-branchless-test --no-run
Time (mean ± σ): 344.2 ms ± 3.5 ms [User: 246.2 ms, System: 91.9 ms]
Range (min … max): 340.4 ms … 351.0 ms 10 runs
To jest nieoczekiwane. Spodziewałbym się, że czas na kompilację bez operacji będzie podobny do git status
, może 15ms:
$ hyperfine --warmup 3 'git status'
Benchmark 1: git status
Time (mean ± σ): 13.5 ms ± 2.5 ms [User: 4.9 ms, System: 6.1 ms]
Range (min … max): 11.1 ms … 24.7 ms 197 runs
Tu może być jakiś głębszy problem. Dokumentacja cargo nextest
ostrzega, że niektóre systemy przeciw malware mogą wprowadzać sztuczne opóźnienie uruchamiania podczas sprawdzania plików wykonywalnych:
A typical sign of this happening is even the simplest of tests in cargo nextest run taking more than 0.2 seconds.
Zgodnie z dokumentacją, oznaczyłem moje oprogramowanie terminala jako “Developer Tools” w systemie macOS, ale nie udało mi się skrócić czasu kompilacji bez operacji.
Koniec profilowania?
Spróbowalem z subcommand crate, który niedawno stworzyłem bez wielu zaleźności:
$ hyperfine --warmup 3 --prepare 'echo "// @nocommit test" >>git-branchless-test/tests/test_test.rs' 'cargo test -p git-branchless-test --no-run'
Benchmark 1: cargo test -p git-branchless-test --no-run
Time (mean ± σ): 1.855 s ± 0.034 s [User: 1.476 s, System: 0.335 s]
Range (min … max): 1.831 s … 1.939 s 10 runs
Wykres czasu kompilacji według cargo build
nie pomaga. Tylko pokazuje to, że budowany test wymaga 100% czasu:

The timing graph for building the git-branchless-test
crate.
Koniec kolejnych pomysłów?
Oto niektóre pomysły, które nie zadziałały:
- Połączenie testów integracyjnych w jeden binary. (W każdym razie chętnie uruchamiam indywidualne binaries, jeśli to konieczne.)
- Zmniejszenie głównych monomorfozacji (wywołania
AsRef
itp. Pojawiły się wcargo llvm-lines
, ale ich zmniejszenie nie zdawało się poprawiać czasu kompilacji). - Używanie
sccache
. - Używanie
cargo nextest
. - Używanie zld/mold/sold.
- Ustawienie
profile.dev.split-debuginfo = “unpacked”
(dla systemu macOS). - Ustawienie
profile.dev.build-override.opt-level = 3
.- Niestety, jest dużo makr proceduralnych, zwłaszcza
#[instrument]
z cratetracing
.
- Niestety, jest dużo makr proceduralnych, zwłaszcza
- Ustawienie
profile.dev.debug = 0
. W rzeczywistości skraca czas kompilacji o 20ms, ale samo w sobie nie wystarczy.
Więc teraz utknąłem. To najwięcej, jak mogę skrócić przyrostowe czasy testowania Rust. Daj znać, jeśli masz jakieś inne pomysły.
Powiązane posty
Poniżej znajduje się kilka ręcznie wybranych postów, które mogą Cię zainteresować.
Date | Title | |
---|---|---|
07 Feb 2023 | (this post) | Ulepszenie przyrostowe czasy testowania Rust |
Chcesz zobaczyć więcej moich postów? Obserwuj mnie na Twitterze albo subskrybuj za pomocą RSS.