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.
- Streszczenie wykonawcze
- Szczegóły projektu
- Podzielenie się na więcej crates
- Czas bez operacji
- Koniec profilowania?
- Koniec kolejnych pomysłów?
- Komentarze
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:
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.