fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
HOT / 10 minut

Wyciśnij z MSBuilda ostatnie soki


01.10.2019

Czym jest MSBuild?

Każdy programista, który używał środowiska .NET, zapewne spotkał się z nazwą MSBuild. Zadaniem tego oprogramowania jest tak poprowadzić proces tworzenia aplikacji, aby ściągnąć potrzebne zależności, zbudować projekty oraz wdrożyć je, gdy o to poprosimy.

Korzystamy z niego podczas kliknięcia Build, Clean lub Rebuild w Visual Studio.

Jednak jak każdy mechanizm ma on ukryte wady, możliwe ścieżki optymalizacji oraz sposoby na rozszerzenie o dodatkowe narzędzia w celu automatyzacji.

Jednocześnie brak znajomości mechanizmów, na które pozwala .NET, może spowodować, że proces wdrażania aplikacji zajmuje lwią część pracy. U klienta proces wdrożenia zmian na środowisko trwa 2 dni robocze, gdyż system wdraża ponad 50 systemów naraz (sic!).

Przejdźmy do mięsa.

Budowanie przyrostowe

Jeden z mechanizmów w MSBuild, który sprawdza, czy projekt został zbudowany. Jeśli sygnatura czasowa zgadza się z poprzednim stanem, to pomija projekt i go nie buduje. Większość problemów jest związana z tym, że manipulujemy plikami przed procesem budowania lub po nim, co powoduje, że mechanizm nie działa tak, jak powinien.

Trzeba mieć jak najwięcej projektów, które wykorzystują ten mechanizm, aby przyspieszyć czas budowania i ponowienia.

Ale jak? Po kolei.

Usuń mechanizmy bezmyślnego kopiowania

Pierwszy mechanizm, który psuje budowanie przyrostowe. Sam w sobie mechanizm kopiowania nie jest zły, ale NIGDY nie nadpisuj plików w folderze wynikowym danej aplikacji, bo naruszysz sygnatury czasowe plików, przez co proces budowania będzie ponawiany za każdym razem.

Przykładowy mechanizm kopiowania plików z folderu content do folderu głównego solucji, który psuje budowanie przyrostowe:

<ItemGroup>
   <ContentToCopy Include="$(MSBuildProjectDirectory)\content\*.*" />
</ItemGroup>
<Target Name="BeforeBuild">
   <Copy SourceFiles="@(ContentToCopy)"
         DestinationFiles="@(ContentToCopy->
           '$(SolutionDir)\%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>

oraz poprawiony mechanizm kopiowania:

<ItemGroup>
   <ContentToCopyInclude="$(MSBuildProjectDirectory)\content\*.*"
                  Inputs="@(ContentToCopy)"
                  Outputs="@(ContentToCopy->
                    '$(SolutionDir)\%(RecursiveDir)%(Filename)%(Extension)')" />
</ItemGroup>
<Target Name="BeforeBuild">
   <CopySourceFiles="@(ContentToCopy)"
         DestinationFiles="@(ContentToCopy->
           '$(SolutionDir)\%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>

Zmiana polega na dodaniu właściwości Inputs oraz Outputs. Zlecamy mechanizmowi budowania, aby sprawdził sygnatury czasowe plików w folderach na wejściu i wyjściu przed procesem kopiowania. Jeśli żaden plik nie zmienił się od ostatniego czasu budowania, to zadanie jest pomijane.

Innym sposobem zwiększenia wydajności jest wykorzystanie dysku podczas procesu budowania. Oczywiście im wolniejszy dysk, tym proces budowania jest dłuższy.

Warto jednak mieć na uwadze fakt, że niektóre środowiska, na których budujemy i wdrażamy aplikacje, mogą nie mieć wydajnych mechanizmów kopiowania, działać zdalnie lub najzwyczajniej w świecie trzeba używać protokołów sieciowych.

Klient wykorzystuje dyski sieciowe bazujące na HDD, przez co transfer jest średnio 10 razy wolniejszy niż na maszynach deweloperskich. Znaczną część procesu budowania i wdrażania zajmuje kopiowanie plików między systemami a środowiskami.

Jednak można to zoptymalizować.

Hard linki

Podczas budowania możemy wykorzystać mechanizm optymalizujący proces kopiowania plików. Ustawione flagi przenoszą pliki do bufora RAM-u i z niego wykonują proces zapisu i odczytu plików.

Proces jest skalowalny w zależności od ilości RAM-u na maszynie oraz plików współdzielonych między projektami. Przykładowo: u klienta udało się zejść z 135 MB/s do 10 MB/s zapisu/odczytu na wątek.

W celu wykorzystania tego rozwiązania w MSBuild.exe (lub odpowiednika, który go odpali) trzeba dodać właściwości:

MSBuild.exe <plik_projektu> /p:
               CreateHardLinksForCopyLocalIfPossible=true;
               CreateHardLinksForCopyFilesToOutputDirectoryIfPossible=true;
               CreateHardLinksForCopyAdditionalFilesIfPossible=true;
               CreateHardLinksForPublishFilesIfPossible=true;

Współdzielone foldery

Przy wykorzystaniu poprzedniej wskazówki będzie to ogromna optymalizacja. Jednak nigdy nie wykorzystuj folderu współdzielonego między aplikacjami na aplikacje wynikowe!

Każde nadpisanie pliku powoduje wyłączenie budowania przyrostowego. Wspólny odczyt? Tak! Wspólny zapis? Nie! Koniec kropka.

Skrypty budujące

Tutaj nie ma zaskoczenia – Visual Studio, Visual Studio Code czy Rider nie nadają się do budowania ogromnych solucji w .NET, więc zadania powinny być wykonywane z linii poleceń.

Ciekawostka: odpalenie 450 projektów w Visual Studio kończy się zawieszeniem systemu oraz błędem krytycznym VS.

Ogranicz informacje zwrotne

Zasada jest prosta: im mniej informacji zwrotnych, które MSBuild musi nam przekazać, tym wydajniejszy proces budowania.

Standardowo wystarczą nam informacje o błędach:

MSBuild.exe <plik_projektu> /clp:ErrorsOnly

Wykorzystaj nowy format projektów i silnik .NET Core.

Biblioteki, aplikacje windowsowe czy aplikacje konsolowe można zmigrować do nowego formatu. Obecnie ASP.NET jest z tego procesu wyłączony, ale najprawdopodobniej dołączy do tego grona w momencie opublikowania .NET 5.

Nowy format:

<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
      <TargetFramework>net472</TargetFramework>
   </PropertyGroup>
</Project>

.NET Standard oraz .NET Core są na nim zaimplementowane.

Powody są dwa. PackageReference i zbiorowe repozytorium, w którym przetrzymywane są paczki NuGet. Proces ściągania, rozwiązywania zależności i budowania w nowym formacie jest piekielnie szybki w porównaniu ze starym systemem.

 

Wyniki bazują na projektach kolejno dla 4 i 512 projektów w danej solucji.

Ciekawostka: u klienta dla 450+ projektów czas budowania przyrostowego zmalał z 9 minut do 44 sekund (miks starego i nowego formatu, jednak to nie ostatnie słowo!).

Zbiorowe ściąganie paczek NuGet

Proces ściągania paczek NuGet jest równoległy, więc jeśli budujesz wiele solucji naraz, to ściąganie tej samej paczki spowoduje zablokowanie procesu budowania całego rozwiązania.

Jednym ze sposobów jest generowanie tymczasowej solucji, której zadaniem jest indeksowanie projektów i ściąganie paczek.

Idąc dalej tym tokiem myślenia, zamiast wykorzystywać domyślną ścieżkę w starym formacie projektu, można stworzyć plik nuget.config, w którym dodamy klucz repositoryPath:

<config>
   <add key="repositoryPath"
        value="%userprofile%\.nuget\OldFormatPackages" />
</config>

Dzięki takiemu zabiegowi stary oraz nowy format będą korzystać z folderu .nuget i pozwoli to zaoszczędzić czas rozwiązywania zależności w obu formatach. Co więcej, hard linking pokaże tutaj swój potencjał optymalizacyjny.

Jeden aspekt. Co z tą ścieżką jest nie tak?

Gdy wskażemy relatywną ścieżkę do zależności to każdy deweloper w zespole musi mieć taką samą ilość kroków do folderu(na przykład, 5 folderów do tyłu i 3 do przodu).

Ścieżka absolutna? Każdy deweloper musi posiadać ten sam folder na swojej maszynie, więc warto o tym pamiętać.

Wykorzystaj lokalne repozytoria

Osobiście poszedłem dalej i nie korzystam w projektach ze źródeł takich jak nuget.org, offline i innych repozytoriów dostarczonych przez Microsoft jako domyślnych dla .NET poza… ściągnięciem potrzebnych paczek do lokalnego repozytorium.

Sprawa jest prosta – odczyt z dysku SSD jest szybszy niż z sieci, dodatkowo można pracować offline, mając dostęp do lokalnego serwera NuGet.

Takie samo rozwiązanie wykorzystuje mój klient. Posiada repozytorium lokalne (per maszyna) oraz firmowe.

Plusy są dwa.

Bezpieczeństwo, bo nie ma ruchu sieciowego na maszynach budujących, oraz wydajność ze względu na systemy bezpieczeństwa, które w czasie rzeczywistym monitorują ruch sieciowy. Przez to ściąganie paczek jest bardzo wolne i system bezpieczeństwa może zablokować akcję, jeśli wykryje podejrzany plik, a to spowoduje zatrzymanie całej machiny.

Stwórz wyjątki dla antywirusa

Każdy proces i folder, który służy do stworzenia aplikacji, powinien być wyłączony ze sprawdzania przez antywirus.

Procesy do białej listy:

  • node.exe
  • msbuild.exe
  • code.exe
  • dotnet.exe
  • devenv.exe
  • ServiceHub.Host.Node.x86.exe

Można wykluczyć procesy jako proces (nazwa.[rozszerzenie]) lub jako ścieżkę do procesu z procesem. Polecam drugie podejście ze względu na możliwy wektor ataku. Przy instalacji Visual Studio, Visual Studio Code, .NET Core SDK znajdziemy w folderach powyższe procesy.

Wykorzystaj równoległe budowanie

Kwintesencja optymalizacji – wiele projektów można budować równolegle. Kilka wskazówek, aby proces był szybki i nie blokował sam siebie:

  • Zawsze zapisuj (kopiuj) do pustego i unikatowego folderu.
  • Zmniejsz liczbę zależności między projektami do minimum.
  • Korzystaj z hard linking oraz wspólnych folderów do odczytu.
  • Ściągaj zbiorowo paczki NuGet, aby nie zablokować procesu.

Dlaczego kopiujemy do pustego folderu? Uprawnienia i nałożona flaga readonly. Często gęsto zdarza się, że plik zostanie zablokowany przez inny proces lub przez system, bo system budowania nie ma uprawnień do nadpisywania plików.

Równoległy proces budowania można odpalić za pomocą flag:

MSBuild.exe <plik_projektu> /m /nr:true /p:BuildInParallel=true;

Faktycznie wystarczy jedynie flaga /m, która mówi, żeby MSBuild użył wszystkich możliwych rdzeni CPU. BuildInParallel domyślnie jest ustawione na true, jednak specjalnie nadpisujemy, aby flagi w projektach były pominięte, gdyby jakaś się trafiła. Flaga /nr oznacza ponowne użycie tego samego procesu MSBuild, ponieważ dla każdego rdzenia przewidziany jest jeden proces MSBuild.exe i w zależności od ustawienia możemy użyć tego samego procesu lub stworzyć nowy. Flaga domyślnie jest włączona.

Podsumowanie

Dzisiaj zaprezentowałem Wam czysto teoretyczną wiedzę dotyczącą możliwych sposobów optymalizacji.

W kolejnym wpisie przygotuję rozwiązanie, które spina wszystkie powyższe rady… w celu optymalizacji (a jakże!) powtarzalnej pracy oraz możliwości rozwijania swoich własnych pomysłów i zapotrzebowań.

Jeśli macie jakieś pytania, to piszcie śmiało w komentarzach.

A tymczasem do następnego!

Nie przegap kolejnych postów!

Dołącz do ponad 9000 programistów w devstyle newsletter!

Tym samym wyrażasz zgodę na otrzymanie informacji marketingowych z devstyle.pl (doh...). Powered by ConvertKit

4
Dodaj komentarz

avatar
2 Comment threads
2 Thread replies
1 Followers
 
Most reacted comment
Hottest comment thread
3 Comment authors
Piotr CzechSzymonŁukasz Recent comment authors

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  Subscribe  
Notify of
Łukasz
Łukasz

Nic o copy if newer? O resources? A 450 projektów wkładające VS to bzdura. Trzy lata pracowałem z solution w którym było ich 480 i prawie półtora GB kodu.

Szymon
Szymon

W apakpicie:
Zbiorowe ściąganie paczek NuGet

Proces ściągania paczek NuGet jest równoległy, więc jeśli budujesz wiele solucji naraz, to ściąganie tej samej paczki spowoduje zablokowanie procesu budowania całego rozwiązania.

Napisałeś wiele solucji.

Czy nie maiłeś na myśli wiele projektów?

Pozdrawiam

Moja książka

Facebook

Zobacz również