Ten post jest częścią cyklu postów na blogu, w których opisujemy wprowadzane przez nas zmiany w architekturze Narzędzi deweloperskich i sposób ich tworzenia.
Po migracji do modułów JavaScript i migracji do komponentów internetowych kontynuujemy dziś serię postów na blogu o zmianach, które wprowadzamy w architekturze DevTools i sposobie jej tworzenia. (Jeśli jeszcze go nie widzieliście, to mamy dla Was film o modernizacji architektury Narzędzi dla programistów pod kątem współczesnej sieci, w którym znajdziecie 14 wskazówek dotyczących ulepszania projektów internetowych).
W tym poście opiszemy 13-miesięczną drogę od sprawdzającej typy w kompilatorze Closure do TypeScript.
Wprowadzenie
Ze względu na rozmiar kodu źródłowego DevTools i konieczność zapewnienia pewności inżynierom, którzy nad nim pracują, używanie sprawdzacza typów jest koniecznością. W tym celu w 2013 r. w Narzędziach deweloperskich zastosowaliśmy kompilator Closure Compiler. Dzięki użyciu Closure inżynierowie zajmujący się narzędziami dla deweloperów mogli wprowadzać zmiany bez obaw. Kompilator Closure przeprowadzał sprawdzanie typów, aby mieć pewność, że wszystkie integracje z systemem są dobrze typowane.
Z czasem jednak w nowoczesnym tworzeniu stron internetowych popularne stały się alternatywne sprawdzacze typów. Dwa warte uwagi przykłady to TypeScript i Flow. Co więcej, TypeScript stał się oficjalnym językiem programowania w Google. Podczas gdy te nowe sprawdzania typu zyskują na popularności, zauważyliśmy też, że wprowadzamy regresje, które powinny zostać wykryte przez sprawdzanie typu. Dlatego postanowiliśmy ponownie ocenić nasz wybór sprawdzacza typów i określić kolejne kroki w rozwoju DevTools.
Sprawdzanie poprawności typów
Ponieważ w Narzędziach deweloperskich był już używany sprawdzacz typów, pytanie, na które musieliśmy odpowiedzieć, brzmiało:
Czy nadal używać Closure Compiler, czy przejść na nowy sprawdzacz typów?
Aby odpowiedzieć na to pytanie, musieliśmy ocenić sprawdzacze typów pod kątem kilku cech. Korzystamy z sprawdzarki typów, aby zapewnić bezpieczeństwo programistom, dlatego najważniejszy jest dla nas aspekt poprawności typów. Innymi słowy: jak niezawodny jest sprawdzacz typów w wykrywaniu rzeczywistych problemów?
Nasz zespół skupił się na regresjach, które zostały wdrożone, i na ustaleniu ich głównych przyczyn. Zakładamy, że ponieważ używaliśmy już kompilatora Closure, nie wykrył on tych problemów. Dlatego musimy ustalić, czy jakikolwiek inny sprawdzacz typu mógłby to zrobić.
Typowanie w TypeScript
TypeScript był oficjalnie obsługiwanym językiem programowania w Google i bardzo szybko zyskywał na popularności, dlatego postanowiliśmy najpierw go przetestować. TypeScript był ciekawym wyborem, ponieważ zespół TypeScript używa DevTools jako jednego z projektów testowych, aby śledzić zgodność z sprawdzaniem typów JavaScriptu. Wyniki testu referencyjnego pokazały, że TypeScript wykrył dużą liczbę problemów z typami, których kompilator Closure nie wykrywał. Wiele z tych problemów było prawdopodobnie główną przyczyną regresji, które wprowadzaliśmy. To z kolei skłoniło nas do przekonania, że TypeScript może być odpowiednim rozwiązaniem dla Narzędzi deweloperskich.
Podczas migracji na moduły JavaScriptu odkryliśmy, że kompilator Closure wykrywa więcej problemów niż wcześniej. Przejście na standardowy format modułu zwiększyło zdolność Closure do analizowania kodu źródłowego, a w konsekwencji skuteczność sprawdzarek typów. Zespół TypeScript używał jednak wersji podstawowej DevTools, która powstała przed migracją modułów JavaScript. Dlatego musieliśmy sprawdzić, czy przejście na moduły JavaScriptu zmniejszyło też liczbę błędów wykrywanych przez kompilator TypeScript.
Ocenianie TypeScript
Narzędzia deweloperskie istnieją od ponad dekady. W tym czasie rozwinęły się do rozmiarów i bogactwa funkcji typowych dla aplikacji internetowych. W momencie pisania tego posta DevTools zawierało około 150 tys. wierszy kodu JavaScript własnego wydawcy. Gdy uruchomiliśmy kompilator TypeScript na naszym kodzie źródłowym, liczba błędów była przytłaczająca. Udało nam się ustalić, że chociaż kompilator TypeScript generował mniej błędów związanych z rozwiązaniem kodu (około 2000 błędów), w naszej bazie kodu wciąż występowało 6000 błędów związanych ze zgodnością typów.
Okazało się, że TypeScript potrafi rozwiązywać typy, ale w naszym kodzie źródłowym znalazł znaczną liczbę niezgodności typów.
Ręczne sprawdzenie tych błędów wykazało, że TypeScript (w większości przypadków) był poprawny.
TypeScript mógł wykryć te typy, a Closure nie, ponieważ kompilator Closure często uznawał typ za Any
, podczas gdy TypeScript przeprowadzał wnioskowanie na podstawie przypisów i wywnioskował dokładniejszy typ.
W związku z tym TypeScript lepiej rozumiał strukturę naszych obiektów i wykrywał problematyczne użycia.
Ważnym elementem jest to, że kompilator Closure w DevTools często używał funkcji @unrestricted
.
Dodanie adnotacji do klasy za pomocą @unrestricted
wyłącza w przypadku tej klasy ścisłe kontrole właściwości kompilatora Closure, co oznacza, że deweloper może w dowolnym momencie rozszerzyć definicję klasy bez zabezpieczenia typów.
Nie udało nam się znaleźć kontekstu historycznego, który wyjaśniałby, dlaczego w kodzie źródłowym narzędzi deweloperskich dominowało użycie @unrestricted
. W wyniku tego kompilator Closure działał w mniej bezpiecznym trybie w przypadku dużych części kodu źródłowego.
Analiza naszych regresji z uwzględnieniem błędów typów wykrytych przez TypeScript również wykazała nakładanie się, co skłoniło nas do przekonania, że TypeScript mógł zapobiec tym problemom (pod warunkiem, że same typy były prawidłowe).
Nawiązywanie połączenia any
W tym momencie musieliśmy zdecydować, czy ulepszać korzystanie z Closure Compiler, czy przejść na TypeScript. (Ponieważ Flow nie było obsługiwane ani w Google, ani w Chromium, musieliśmy zrezygnować z tej opcji). Na podstawie rozmów z inżynierami Google pracującymi nad narzędziami JavaScript/TypeScript oraz ich rekomendacji zdecydowaliśmy się wybrać kompilator TypeScript. (niedawno opublikowaliśmy też post na blogu na temat migacji Puppeteer do TypeScript).
Głównym powodem wyboru kompilatora TypeScript była większa poprawność typów, a inne zalety to wsparcie wewnętrznych zespołów TypeScript w Google oraz funkcje języka TypeScript, takie jak interfaces
(w przeciwieństwie do typedefs
w JSDoc).
Wybór kompilatora TypeScript oznaczał, że musieliśmy znacznie zainwestować w kod źródłowy DevTools i jego wewnętrzną architekturę. Szacujemy, że na migrację do TypeScript (planowaną na III kwartał 2020 r.) potrzebujemy co najmniej roku.
Przeprowadzanie migracji
Najważniejsze pytanie, które pozostało: jak przejdziemy na TypeScript? Mamy 150 tys. wierszy kodu i nie możemy go przenieść jednym ruchem. Wiedzieliśmy też,że uruchomienie TypeScript w naszej bazie kodu spowoduje tysiące błędów.
Rozważaliśmy kilka opcji:
- Uzyskaj wszystkie błędy TypeScript i porównaj je z „złotym” wyjściem. To podejście byłoby podobne do tego, którego używa zespół TypeScript. Największą wadą tego podejścia jest duża liczba konfliktów podczas łączenia, ponieważ dziesiątki inżynierów pracują nad tym samym kodem źródłowym.
- Ustaw wszystkie problematyczne typy na
any
. To spowoduje, że TypeScript będzie ignorować błędy. Nie wybraliśmy tej opcji, ponieważ celem migracji była poprawność typu, a wyłączenie mogłoby to utrudnić. - Ręcznie napraw wszystkie błędy TypeScript. Oznacza to konieczność naprawienia tysięcy błędów, co jest czasochłonne.
Pomimo dużego oczekiwanego nakładu pracy wybraliśmy opcję 3. Wybraliśmy tę opcję z dodatkowych powodów: na przykład pozwoliła nam ona sprawdzić cały kod i przeprowadzić raz na 10 lat przegląd wszystkich funkcji, w tym ich implementacji. Z perspektywy biznesowej nie oferowaliśmy nowej wartości, tylko utrzymywaliśmy status quo. Z tego powodu trudno było uznać opcję 3 za prawidłową.
Byliśmy jednak przekonani, że dzięki zastosowaniu TypeScripta zapobieglibyśmy przyszłym problemom, zwłaszcza regresjom. Dlatego argument nie brzmiał „dodajemy nowej wartości biznesowej”, lecz „dbamy o to, aby nie stracić uzyskanej wartości biznesowej”.
Obsługa JavaScriptu przez kompilator TypeScript
Po uzyskaniu zgody i opracowaniu planu uruchamiania kompilatorów Closure i TypeScript na tym samym kodzie JavaScriptu zaczęliśmy od małych plików. Nasz sposób działania był głównie od dołu do góry: zaczynaliśmy od kodu głównego i przechodziliśmy w górę architektury, aż do paneli wysokiego poziomu.
Udało nam się równolegle wykonywać naszą pracę dzięki temu, że dodaliśmy @ts-nocheck
do każdego pliku w DevTools. Proces „poprawiania kodu TypeScript” polegałby na usunięciu adnotacji @ts-nocheck
i naprawieniu wszystkich błędów znalezionych przez TypeScript. Oznacza to, że byliśmy pewni, że każdy plik został sprawdzony i rozwiązano jak najwięcej problemów.
Ogólnie to podejście działało dobrze, ale wiązało się z kilkoma problemami. Wystąpiło kilka błędów w kompilatorze TypeScript, ale większość z nich była niejasna:
- Opcjonalny parametr o typie funkcji, który zwraca
any
, jest traktowany jako wymagany: #38551 - Przypisanie właściwości do metody statycznej klasy powoduje przerwanie deklaracji: #38553
- Deklaracja podklasy z konstruktorem bez argumentów i superklasy z konstruktorem z argumentami pomija konstruktor podrzędny: #41397
Te błędy wskazują, że w 99% przypadków kompilator TypeScript stanowi solidną podstawę do dalszego rozwoju. Tak, te niejasne błędy czasami powodowały problemy w przypadku DevTools, ale w większości przypadków były na tyle niejasne, że mogliśmy je łatwo obejść.
Jedynym problemem, który wprowadził nas w błąd, było niedeterministyczne generowanie plików .tsbuildinfo
: #37156.
W Chromium wymagamy, aby 2 kompilacje tego samego commita Chromium dawały dokładnie ten sam wynik.
Nasi inżynierowie zajmujący się kompilacją Chromium odkryli, że dane wyjściowe .tsbuildinfo
nie były deterministyczne: crbug.com/1054494.
Aby obejść ten problem, musieliśmy wprowadzić poprawki w pliku .tsbuildinfo
(który zawierał w podstawie dane w formacie JSON) i przetworzyć go w dalszym procesie, aby zwrócił on wyniki deterministyczne: https://crrev.com/c/2091448Na szczęście zespół TypeScript rozwiązał problem na poziomie źródłowym, dzięki czemu mogliśmy wkrótce usunąć obejście. Dziękujemy zespołowi TypeScript za chęć przyjmowania zgłoszeń błędów i ich szybkie rozwiązywanie.
Ogólnie jesteśmy zadowoleni z poprawności kompilatora TypeScript. Mamy nadzieję, że Devtools jako duży projekt JavaScriptu open source przyczynił się do ugruntowania obsługi JavaScriptu w TypeScript.
Analiza następstw
Udało nam się znacznie ograniczyć występowanie tego typu błędów i powoli zwiększać ilość kodu sprawdzanego przez TypeScript. Jednak w sierpniu 2020 r. (9 miesięcy po rozpoczęciu migracji) sprawdziliśmy postępy i stwierdziliśmy, że nie uda nam się dotrzymać terminu przy obecnym tempie. Jeden z naszych inżynierów stworzył wykres analizy, aby pokazać postępy w „przekształcaniu kodu na TypeScript” (ta nazwa została nadana tej migracji).
Postęp migracji TypeScript – śledzenie linii kodu, które wymagają przeniesienia
Szacunki dotyczące daty, kiedy osiągniemy zerową liczbę wierszy, wahały się od lipca do grudnia 2021 r., czyli prawie rok po naszym terminie. Po rozmowach z zarządem i innymi inżynierami uzgodniliśmy, że zwiększymy liczbę inżynierów pracujących nad migracją do obsługi kompilatora TypeScript. Było to możliwe, ponieważ zaprojektowaliśmy migrację tak, aby można było ją przeprowadzać równolegle, dzięki czemu wielu inżynierów pracujących nad różnymi plikami nie wchodziło ze sobą w kolizję.
W tym momencie proces przekształcania kodu w TypeScript stał się zadaniem całego zespołu. Dzięki dodatkowej pomocy udało nam się zakończyć migrację pod koniec listopada 2020 r., czyli 13 miesięcy po jej rozpoczęciu i ponad rok wcześniej, niż przewidywała nasza początkowa prognoza.
Łącznie 18 inżynierów przesłało 771 list zmian (podobnych do Pull Request). Nasz błąd śledzenia (https://crbug.com/1011811) ma ponad 1200 komentarzy (prawie wszystkie to automatyczne posty z list zmian). Nasze arkusz śledzenia zawierał ponad 500 wierszy z plikami, które miały zostać przekonwertowane na TypeScript, z przypisanymi do nich osobami i informacją, na której liście zmian zostały „konwertowane”.
Zmniejszenie wpływu wydajności kompilatora TypeScript na wydajność
Największym problemem, z którym obecnie się borykamy, jest powolna praca kompilatora TypeScript. Biorąc pod uwagę liczbę inżynierów pracujących nad Chromium i Narzędziami deweloperskimi, to wąskie gardło jest kosztowne. Niestety nie udało nam się zidentyfikować tego ryzyka przed migracją. Dopiero po przeniesieniu większości plików do TypeScript odkryliśmy zauważalny wzrost czasu spędzanego na kompilacjach Chromium: https://crbug.com/1139220
Zgłosiliśmy ten problem zespołowi Microsoft TypeScript, ale jego członkowie uznali, że to zachowanie jest celowe. Mamy nadzieję, że zespół Chromium ponownie się nad tym zastanowi, ale tymczasem pracujemy nad tym, aby jak najbardziej ograniczyć wpływ spowolnienia działania na stronie Chromium.
Niestety dostępne obecnie rozwiązania nie zawsze są odpowiednie dla osób spoza Google. Wkład w Chromium w ramach projektów open source jest bardzo ważny (zwłaszcza w przypadku zespołu Microsoft Edge), dlatego aktywnie szukamy alternatyw, które będą działać dla wszystkich autorów. W tej chwili nie znaleźliśmy odpowiedniego rozwiązania.
Obecny stan TypeScript w Narzędziach deweloperskich
W tej chwili usunęliśmy z naszego kodu źródłowego sprawdzacz typu kompilatora Closure i korzystamy tylko z kompilatora TypeScript. Umożliwia nam to pisanie plików w języku TypeScript i korzystanie z funkcji specyficznych dla tego języka (takich jak interfejsy, typy ogólne itp.), co pomaga nam na co dzień. Mamy coraz większą pewność, że kompilator TypeScript wykryje błędy typów i wsteczniejsze błędy, co było naszym celem, gdy zaczynaliśmy prace nad migracją. Ta migracja, jak wiele innych, była powolna, wymagała subtelności i często była trudna, ale przyniosła korzyści, więc uważamy, że było warto.
Pobieranie kanałów podglądu
Rozważ użycie jako domyślnej przeglądarki deweloperskiej przeglądarki Chrome w wersji Canary, Dev lub Beta. Te kanały wersji wstępnej zapewniają dostęp do najnowszych funkcji DevTools, umożliwiają testowanie najnowocześniejszych interfejsów API platformy internetowej i pomagają znaleźć problemy w witrynie, zanim zrobią to użytkownicy.
Kontakt z zespołem Narzędzi deweloperskich w Chrome
Aby omówić nowe funkcje, aktualizacje lub inne kwestie związane z Narzędziami deweloperskimi, skorzystaj z tych opcji.
- Przesyłaj opinie i prośby o dodanie funkcji na stronie crbug.com.
- Zgłoś problem z Narzędziami deweloperskimi, klikając Więcej opcji > Pomoc > Zgłoś problem z Narzędziami deweloperskimi w Narzędziach deweloperskich.
- Wyślij tweeta do @ChromeDevTools.
- Dodaj komentarze do filmów w YouTube z serii „Co nowego w Narzędziach deweloperskich” lub Wskazówki dotyczące Narzędzi deweloperskich.