Danas objavljujemo drugi dio prijevoda materijala o tome kako su u Dropboxu organizirali kontrolu tipova za nekoliko milijuna linija Python koda.
→
Službena podrška za tipove (PEP 484)
Proveli smo prve ozbiljne eksperimentalne radove s mypy u Dropboxu tijekom Hack Weeka 2014. Hack Week je događaj koji se u Dropboxu održava tijekom jednog tjedna. U to vrijeme zaposlenici mogu raditi na čemu god žele! Neki od najpoznatijih tehnoloških projekata u Dropboxu započeli su upravo na takvim događajima. Kao rezultat ovog eksperimenta, došli smo do zaključka da mypy izgleda obećavajuće, iako ovaj projekt još uvijek nije bio spreman za široku upotrebu.
Tada se u zraku osjećala ideja o standardizaciji sustava za davanje savjeta o tipovima u Pythonu. Kao što sam spomenuo, počevši od Pythona 3.0, mogli su se koristiti tipne anotacije za funkcije, ali to su bila samo proizvoljna izražavanja, bez određenih sintaktičkih i semantičkih pravila. Tijekom izvođenja programa, te se anotacije uglavnom jednostavno ignoriraju. Nakon Hack Weeka, počeli smo raditi na standardizaciji semantike. Ovaj rad doveo je do stvaranja (na ovom dokumentu su zajedno radili Guido van Rossum, Lukasz Langa i ja).
Naše motive mogli bismo razmotriti iz dva aspekta. Prvo, nadali smo se da će cijela ekosustav Pythona moći prihvatiti zajednički pristup korištenju tipnih savjeta (type hints — termin koji se u Pythonu koristi kao ekvivalent "tipnim anotacijama"). To bi, uzimajući u obzir moguće rizike, bilo bolje od korištenja mnoštva međusobno nespojivih pristupa. Drugo, željeli smo otvoreno raspraviti mehanizme anotiranja tipova s brojnim članovima Python zajednice. Djelomično, ta želja bila je uzrokovana time da ne bismo željeli izgledati kao "odmetnici" od osnovnih ideja jezika u očima široke mase Python programera. To je dinamički tipizirani jezik, poznat po "patkasti tipizaciji". U zajednici, na samom početku, nije moglo ne izazvati sumnju prema ideji statičke tipizacije. No, takav stav na kraju je oslabio — nakon što je postalo jasno da se ne planira učiniti statičku tipizaciju obveznom (i nakon što su ljudi shvatili da je ona zaista korisna).
Konačno usvojena sintaksa natuknica za tipove bila je vrlo slična onoj koju je tada podržavao mypy. Dokument PEP 484 objavljen je zajedno s Python 3.5 2015. godine. Python više nije bio jezik koji podržava samo dinamičko tipiziranje. Volim misliti o tom događaju kao o značajnoj prekretnici u povijesti Pythona.
Početak migracije
Krajem 2015. u Dropboxu je formiran tim od tri osobe za rad na mypyju. Tim su činili Guido van Rossum, Greg Price i David Fisher. Od tog trenutka situacija je počela vrlo brzo napredovati. Prva prepreka na putu rasta mypyja bila je izvedba. Kao što sam ranije spomenuo, u ranom razdoblju razvoja projekta razmišljao sam o prevođenju implementacije mypyja na jezik C, ali je ta ideja zasad izbačena iz liste. Bili smo zaglavljeni s tim da se za pokretanje sustava koristio interpreter CPython, koji nije bio dovoljno brz za alate poput mypyja. (Projekt PyPy, alternativna implementacija Pythona s JIT kompajlerom, također nam nije pomogao.)
Srećom, ovdje su nam pomogla neka algoritamska poboljšanja. Prvo snažno "ubrzanje" bilo je implementacija inkrementalne provjere. Ideja ovog poboljšanja bila je jednostavna: ako se sve ovisnosti modula od prethodnog pokretanja mypyja nisu promijenile, možemo koristiti, tijekom rada s ovisnostima, podatke pohranjene tijekom prethodne sesije. Trebalo je samo izvršiti provjeru tipova u izmijenjenim datotekama i u onim datotekama koje su od njih ovisile. Mypy je otišao čak i malo dalje: ako vanjski sučelje modula nije promijenjeno — mypy je smatrao da nije potrebno ponovo provjeravati druge module koji uvoze taj modul.
Inkrementalna provjera značajno nam je pomogla u anotiranju velikih količina postojećeg koda. Naime, ovaj proces obično uključuje mnogo iterativnih pokretanja mypy-a, jer se anotacije postupno dodaju u kod i postupno poboljšavaju. Prvo pokretanje mypy-a još uvijek je bilo vrlo sporo, jer je prilikom njegovog izvođenja trebalo provjeriti brojne ovisnosti. Tada smo, kako bismo poboljšali situaciju, implementirali mehanizam udaljenog keširanja. Ako mypy otkrije da je lokalni keš vjerojatno zastario, preuzima trenutnu snimku keša za cijelu kodnu bazu iz centraliziranog repozitorija. Zatim izvodi, koristeći ovu snimku, inkrementalnu provjeru. To nas je još jedan značajan korak unaprijedilo u povećanju performansi mypy-a.
To je bio period brzog i prirodnog uvođenja sustava provjere tipova u Dropbox. Do kraja 2016. godine imali smo već otprilike 420000 redaka Python koda s anotacijama tipova. Mnogi su korisnici s entuzijazmom prihvatili provjeru tipova. U Dropbox-u, mypy su koristile sve više timova programera.
Sve je tada izgledalo dobro, ali nam je još mnogo posla preostalo. Počeli smo provoditi periodične interne ankete među korisnicima kako bismo identificirali problematična mjesta projekta i razumjeli koje je pitanje potrebno riješiti prvo (ova praksa se koristi u tvrtki i danas). Najvažnija, kako je postalo jasno, bila su dva zadatka. Prvi je bio povećati pokrivenost koda tipovima, drugi je bio osigurati da mypy radi brže. Bilo je potpuno jasno da naš rad na ubrzavanju mypy-a i njegovom uvođenju u projekte tvrtke još uvijek nije bio završen. Svjesni važnosti ova dva zadatka, krenuli smo u njihovo rješavanje.
Više performansi!
Inkrementne provjere ubrzale su mypy, no ovaj alat još uvijek nije bio dovoljno brz. Mnoge inkrementne provjere trajale su oko minute. Razlog tome su bile ciklične ovisnosti. Ovo vjerojatno neće iznenaditi one koji su radili s velikim kodnim bazama napisanim na Pythonu. Imali smo setove od stotina modula, od kojih je svaki neizravno uvozio sve ostale. Ako bi se bilo koja datoteka u ciklusu uvoza promijenila, mypy bi morao obraditi sve datoteke uključene u taj ciklus, a često i bilo koji modul koji uvozi module iz ovog ciklusa. Jedan od takvih ciklusa bio je zloslutni "klubak ovisnosti", koji je bio uzrok mnogih neugodnosti u Dropboxu. Jednom je ta struktura sadržavala nekoliko stotina modula, a izravno ili neizravno ju je uvozilo mnoštvo testova, korištena je i u produkcijskom kodu.
Razmatrali smo mogućnost "razmatanja" cikličnih ovisnosti, ali nismo imali resurse za to. Bilo je previše koda s kojim nismo bili upoznati. Na kraju smo se odlučili za alternativni pristup. Odlučili smo da mypy radi brzo čak i u prisutnosti "klubaka ovisnosti". Ovu smo svrhu postigli pomoću mypy demona. Demon je serverski proces koji implementira dvije zanimljive mogućnosti. Prvo — drži u memoriji informacije o cijeloj kodnoj bazi. To znači da prilikom svakog pokretanja mypy ne treba učitavati predmemorirane podatke koji se odnose na tisuće uvezenih ovisnosti. Drugo — pažljivo analizira ovisnosti između funkcija i drugih entiteta na razini malih strukturalnih jedinica. Na primjer, ako funkcija foo poziva funkciju bar, postoji ovisnost foo od bar. Kada se datoteka promijeni — demon prvo, u izolaciji, obrađuje samo promijenjenu datoteku. Zatim gleda na promjene te datoteke, vidljive izvana, poput promijenjenih potpisivanja funkcija. Demon koristi detaljne informacije o uvozu samo za ponovno provjeravanje onih funkcija koje zaista koriste promijenjenu funkciju. Obično, s ovim pristupom, potrebno je provjeravati vrlo mali broj funkcija.
Implementacija svega ovoga bila je izazovna, jer je izvorna implementacija mypy bila usredotočena na obradu jednog datoteke odjednom. Morali smo se suočiti s brojnim rubnim situacijama, čije je nastajanje zahtijevalo ponovne provjere u slučajevima kada se u kodu nešto mijenjalo. Na primjer, to se događa kada se klasi dodijeli nova osnovna klasa. Nakon što smo postigli ono što smo htjeli, uspjeli smo smanjiti vrijeme izvršavanja većine inkrementalnih provjera na nekoliko sekundi. To nam se činilo velikom pobjedom.
Još više performansi!
Zajedno s udaljenim keširanjem, o kojem sam prethodno govorio, mypy demon gotovo potpuno je riješio probleme koji nastaju kada programer često pokreće provjeru tipova, unoseći promjene u mali broj datoteka. Međutim, performanse sustava u najmanje povoljnim uvjetima korištenja još uvijek su bile daleko od optimalnih. Čisto pokretanje mypy moglo je trajati više od 15 minuta. A to je bilo daleko više nego što bismo željeli. Svakog tjedna situacija se pogoršavala, jer su programeri nastavili pisati novi kod i dodavati anotacije postojećem kodu. Naši korisnici i dalje su željeli veću izvedbu, a mi smo s radošću bili spremni izaći im u susret.
Odlučili smo se vratiti na jednu od ranih ideja vezanih uz mypy. Naime, na pretvaranje Python koda u C kod. Eksperimenti s Cython-om (to je sustav koji omogućava prevođenje koda napisanog na Python-u u C kod) nisu nam omogućili nikakvo vidljivo ubrzanje, stoga smo odlučili oživjeti ideju pisanja vlastitog kompajlera. Kako je kodna baza mypy (napisana na Python-u) već sadržavala sve potrebne tipovne anotacije, činilo nam se vrijednim pokušati iskoristiti te anotacije za ubrzanje rada sustava. Brzo sam stvorio prototip kako bih provjerio tu ideju. Pokazao je više od 10 puta veće performanse na raznim mikro-benchmarcima. Naša ideja bila je kompilirati Python module u C module koristeći Cython, i pretvarati tipovne anotacije u provjere tipova koje se izvode tijekom izvođenja programa (obično se tipovne anotacije ignoriraju tijekom izvođenja programa i koriste ih samo sustavi za provjeru tipova). U stvari, planirali smo prevesti implementaciju mypy s Pythona na jezik koji je statički tipiziran, koji bi izgledao (i, većinom, radio) točno kao Python. (Ova vrsta međujezične migracije postala je svojevrsna tradicija projekta mypy. Prvotna implementacija mypy bila je napisana na Alore, zatim je bio sintaktički hibrid Java i Python-a).
Usmjerenje na API ekstenzija CPython-a bilo je ključno da ne izgubimo mogućnosti upravljanja projektom. Nismo trebali implementirati virtualnu mašinu ili bilo koje biblioteke koje su bile potrebne mypy-u. Osim toga, cijela ekosustava Python-a bi nam i dalje bila dostupna, svi alati (poput pytest-a) bi bili dostupni. To je značilo da bismo mogli nastaviti koristiti interpretirani Python kod tijekom razvoja, što bi nam omogućilo nastavak rada koristeći vrlo brz sustav unošenja promjena u kod i njegovo testiranje, umjesto da čekamo kompilaciju koda. Izgledalo je kao da nam odlično ide, da tako kažem, sjediti na dva stolca, i to nam se sviđalo.
Kompajler koji smo nazvali mypyc (s obzirom na to da kao prednji deo koristi mypy za analizu tipova) pokazao se kao prilično uspešan projekat. U suštini, postigli smo približno četverostruko ubrzanje čestih pokretanja mypy bez korišćenja keširanja. Razvoj jezgra projekta mypyc trajao je od malog tima, u kojem su bili Majkl Salivan, Ivan Levkivski, Hju Han i ja, oko 4 kalendarska meseca. Ovaj obim posla bio je daleko manji od onog koji bi bio potreban za prepisivanje mypy, na primer, na C++ ili Go. Osim toga, promena koju smo morali da izvršimo u projektu bila je mnogo manja nego što bi bila potrebna za prepisivanje na drugi jezik. Takođe smo se nadali da ćemo moći da dovedemo mypyc do nivoa koji će omogućiti drugim programerima iz Dropbox-a da ga koriste za kompilaciju i ubrzanje svog koda.
Da bismo postigli sličan nivo performansi, morali smo primeniti neka zanimljiva inženjerska rešenja. Tako kompajler može ubrzati izvršavanje mnogih operacija zahvaljujući korišćenju brzih niskonivojskih konstrukcija C. Na primer, poziv kompajlirane funkcije prevodi se u poziv C funkcije. Takav poziv se izvršava mnogo brže od poziva interpretirane funkcije. Neke operacije, kao što je pretraživanje u rečnicima, još uvek su se svodile na korišćenje običnih poziva C API-ja iz CPython-a, koji su nakon kompajlacije bili samo malo brži. Uspeli smo da se oslobodimo dodatnog opterećenja na sistem koje stvara interpretacija, ali to je u ovom slučaju donelo samo mali dobitak u pogledu performansi.
Kako bismo identificirali najčešće "spore" operacije, proveli smo profiliranje koda. Opremljeni tim podacima, pokušali smo ili optimizirati mypyc kako bi generirao brži C-kod za te operacije, ili prepisati odgovarajući Python kod koristeći brže operacije (ponekad jednostavno nismo imali razrješenje za određeni problem). Prepisivanje Python koda često se pokazalo lakšim rješenjem od implementacije automatskog izvršavanja iste transformacije u kompajleru. Dugoročno, željeli smo automatizirati mnoge od tih transformacija, ali u tom trenutku bili smo usmjereni na to da, s minimalnim naporom, ubrzamo mypy. Na tom putu skratili smo nekoliko procesa.
Nastavak slijedi…
Dragi čitatelji! Koji su dojmovi na vas ostavili mypy projekt kada ste prvi put saznali za njega?
Izvor: habr.com
