Halaman ini memberikan ringkasan umum tentang masalah dan tantangan tertentu dalam menulis aturan Bazel yang efisien.
Persyaratan Ringkasan
- Asumsi: Bertujuan untuk Keakuratan, Throughput, Kemudahan Penggunaan & Latensi
- Asumsi: Repositori Skala Besar
- Asumsi: Bahasa Deskripsi seperti BUILD
- Historis: Pemisahan Keras antara Pemuatan, Analisis, dan Eksekusi sudah Tidak berlaku, tetapi masih memengaruhi API
- Intrinsik: Eksekusi Jarak Jauh dan Penyimpanan dalam Cache Sulit
- Intrinsik: Menggunakan Informasi Perubahan untuk Build Inkremental yang Benar dan Cepat memerlukan Pola Coding yang Tidak Biasa
- Intrinsik: Menghindari Konsumsi Memori dan Waktu Kuadrat Sulit
Asumsi
Berikut adalah beberapa asumsi yang dibuat tentang sistem build, seperti kebutuhan akan kebenaran, kemudahan penggunaan, throughput, dan repositori skala besar. Bagian berikut membahas asumsi ini dan menawarkan panduan untuk memastikan aturan ditulis dengan cara yang efektif.
Targetkan ketepatan, throughput, kemudahan penggunaan, & latensi
Kita mengasumsikan bahwa sistem build harus benar terlebih dahulu dan terutama terkait build inkremental. Untuk hierarki sumber tertentu, output build yang sama harus selalu sama, terlepas dari tampilan hierarki output tersebut. Dalam perkiraan pertama, ini berarti Bazel perlu mengetahui setiap input yang masuk ke langkah build tertentu, sehingga dapat menjalankan ulang langkah tersebut jika input berubah. Ada batasan untuk seberapa akurat Bazel dapat diperoleh, karena membocorkan beberapa informasi seperti tanggal / waktu build, dan mengabaikan jenis perubahan tertentu seperti perubahan pada atribut file. Sandboxing membantu memastikan ketepatan dengan mencegah pembacaan ke file input yang tidak dideklarasikan. Selain batas intrinsik sistem, ada beberapa masalah kebenaran yang diketahui, yang sebagian besar terkait dengan Fileset atau aturan C++, yang merupakan masalah yang sulit. Kami memiliki upaya jangka panjang untuk memperbaikinya.
Sasaran kedua sistem build adalah memiliki throughput tinggi; kita secara permanen mendorong batas-batas yang dapat dilakukan dalam alokasi mesin saat ini untuk layanan eksekusi jarak jauh. Jika layanan eksekusi jarak jauh kelebihan beban, tidak ada yang dapat menyelesaikan pekerjaan.
Kemudahan penggunaan adalah hal berikutnya. Dari beberapa pendekatan yang benar dengan jejak layanan eksekusi jarak jauh yang sama (atau mirip), kami memilih pendekatan yang lebih mudah digunakan.
Latensi menunjukkan waktu yang diperlukan dari memulai build hingga mendapatkan hasil
yang diinginkan, baik itu log pengujian dari pengujian yang lulus atau gagal, atau pesan
error bahwa file BUILD
memiliki kesalahan ketik.
Perhatikan bahwa sasaran ini sering kali tumpang-tindih; latensi adalah fungsi throughput layanan eksekusi jarak jauh, begitu juga dengan ketepatan yang relevan untuk kemudahan penggunaan.
Repositori skala besar
Sistem build perlu beroperasi pada skala repositori besar dengan skala
besar berarti tidak muat di satu hard drive, sehingga tidak mungkin
melakukan checkout penuh di hampir semua komputer developer. Build berukuran sedang
harus membaca dan menguraikan puluhan ribu file BUILD
, serta mengevaluasi
ratusan ribu glob. Meskipun secara teoritis dapat membaca semua
file BUILD
di satu mesin, kami belum dapat melakukannya dalam
waktu dan memori yang wajar. Oleh karena itu, file BUILD
harus dapat dimuat dan diuraikan secara independen.
Bahasa deskripsi seperti BUILD
Dalam konteks ini, kita mengasumsikan bahasa konfigurasi yang
secara kasar mirip dengan file BUILD
dalam deklarasi aturan library dan biner
serta interdependensi mereka. File BUILD
dapat dibaca dan diuraikan secara independen,
dan kami bahkan menghindari melihat file sumber jika memungkinkan (kecuali untuk
eksistensi).
Bersejarah
Ada perbedaan antara versi Bazel yang menyebabkan tantangan dan beberapa di antaranya diuraikan di bagian berikut.
Pemisahan yang ketat antara pemuatan, analisis, dan eksekusi sudah tidak berlaku, tetapi masih memengaruhi API
Secara teknis, aturan cukup mengetahui file input dan output tindakan tepat sebelum tindakan dikirim ke eksekusi jarak jauh. Namun, code base Bazel asli memiliki pemisahan paket pemuatan yang ketat, lalu menganalisis aturan menggunakan konfigurasi (flag command line, pada dasarnya), dan baru kemudian menjalankan tindakan apa pun. Perbedaan ini masih menjadi bagian dari API aturan saat ini, meskipun inti Bazel tidak lagi memerlukannya (detail selengkapnya di bawah).
Artinya, rules API memerlukan deskripsi deklaratif antarmuka aturan (atribut yang dimiliki, jenis atribut). Ada beberapa pengecualian saat API mengizinkan kode kustom berjalan selama fase pemuatan untuk menghitung nama implisit file output dan nilai implisit atribut. Misalnya, aturan java_library bernama 'foo' secara implisit menghasilkan output bernama 'libfoo.jar', yang dapat direferensikan dari aturan lain dalam grafik build.
Selain itu, analisis aturan tidak dapat membaca file sumber atau memeriksa output tindakan; sebagai gantinya, analisis aturan perlu menghasilkan grafik bipartit terarah parsial dari langkah-langkah build dan nama file output yang hanya ditentukan dari aturan itu sendiri dan dependensinya.
Intrinsik
Ada beberapa properti intrinsik yang membuat penulisan aturan menjadi sulit dan beberapa yang paling umum dijelaskan di bagian berikut.
Eksekusi dan penyimpanan dalam cache jarak jauh sulit dilakukan
Eksekusi jarak jauh dan penyimpanan dalam cache meningkatkan waktu build di repositori besar dengan kira-kira dua urutan magnitudo dibandingkan dengan menjalankan build di satu mesin. Namun, skala yang diperlukan untuk menjalankannya sangat besar: layanan eksekusi jarak jauh Google dirancang untuk menangani sejumlah besar permintaan per detik, dan protokol ini dengan cermat menghindari perjalanan bolak-balik yang tidak perlu serta pekerjaan yang tidak perlu di sisi layanan.
Saat ini, protokol mengharuskan sistem build mengetahui semua input untuk tindakan tertentu sebelumnya; sistem build kemudian menghitung sidik jari tindakan yang unik, dan meminta hit cache ke penjadwal. Jika hit cache ditemukan, penjadwal akan membalas dengan ringkasan file output; file itu sendiri akan ditangani oleh ringkasan nanti. Namun, hal ini akan menimbulkan batasan pada aturan Bazel, yang perlu mendeklarasikan semua file input terlebih dahulu.
Menggunakan informasi perubahan untuk build inkremental yang benar dan cepat memerlukan pola coding yang tidak biasa
Di atas, kita berpendapat bahwa agar benar, Bazel perlu mengetahui semua file input yang masuk ke langkah build untuk mendeteksi apakah langkah build tersebut masih yang terbaru. Hal yang sama berlaku untuk pemuatan paket dan analisis aturan, dan kami telah mendesain Skyframe untuk menangani hal ini secara umum. Skyframe adalah library grafik dan framework evaluasi yang menggunakan node sasaran (seperti 'build //foo with these options'), dan membaginya menjadi bagian penyusunnya, yang kemudian dievaluasi dan digabungkan untuk menghasilkan hasil ini. Sebagai bagian dari proses ini, Skyframe membaca paket, menganalisis aturan, dan menjalankan tindakan.
Di setiap node, Skyframe melacak node mana yang digunakan node tertentu untuk menghitung output-nya sendiri, mulai dari node sasaran hingga file input (yang juga merupakan node Skyframe). Dengan menampilkan grafik ini secara eksplisit dalam memori, sistem build dapat mengidentifikasi dengan tepat node mana yang terpengaruh oleh perubahan tertentu pada file input (termasuk pembuatan atau penghapusan file input), dengan melakukan pekerjaan minimal untuk memulihkan hierarki output ke status yang diinginkan.
Sebagai bagian dari hal ini, setiap node melakukan proses penemuan dependensi. Setiap node dapat mendeklarasikan dependensi, lalu menggunakan konten dependensi tersebut untuk mendeklarasikan dependensi lebih lanjut. Pada prinsipnya, hal ini dipetakan dengan baik ke model thread per node. Namun, build berukuran sedang berisi ratusan ribuan node Skyframe, yang tidak mudah dilakukan dengan teknologi Java saat ini (dan karena alasan historis, saat ini kita terikat untuk menggunakan Java, sehingga tidak ada thread ringan dan tidak ada kelanjutan).
Sebagai gantinya, Bazel menggunakan kumpulan thread berukuran tetap. Namun, hal ini berarti bahwa jika node mendeklarasikan dependensi yang belum tersedia, kita mungkin harus membatalkan evaluasi tersebut dan memulai ulang (mungkin di thread lain), saat dependensi tersedia. Hal ini, pada gilirannya, berarti node tidak boleh melakukan hal ini secara berlebihan; node yang mendeklarasikan dependensi N secara serial berpotensi dimulai ulang N kali, yang menghabiskan waktu O(N^2). Sebagai gantinya, kita bertujuan untuk mendeklarasikan dependensi secara massal di awal, yang terkadang memerlukan pengaturan ulang kode, atau bahkan memisahkan node menjadi beberapa node untuk membatasi jumlah mulai ulang.
Perhatikan bahwa teknologi ini saat ini tidak tersedia di rules API; sebagai gantinya, rules API masih ditentukan menggunakan konsep lama dari fase pemuatan, analisis, dan eksekusi. Namun, batasan dasarnya adalah semua akses ke node lain harus melalui framework agar dapat melacak dependensi yang sesuai. Terlepas dari bahasa yang digunakan untuk menerapkan sistem build atau bahasa yang digunakan untuk menulis aturan (tidak harus sama), penulis aturan tidak boleh menggunakan library atau pola standar yang mengabaikan Skyframe. Untuk Java, hal ini berarti menghindari java.io.File serta bentuk refleksi apa pun, dan library apa pun yang melakukannya. Library yang mendukung injeksi dependensi antarmuka tingkat rendah ini masih perlu disiapkan dengan benar untuk Skyframe.
Hal ini sangat menyarankan untuk menghindari eksposur penulis aturan ke runtime bahasa lengkap di awal. Bahaya penggunaan API tersebut secara tidak sengaja terlalu besar - beberapa bug Bazel sebelumnya disebabkan oleh aturan yang menggunakan API yang tidak aman, meskipun aturan tersebut ditulis oleh tim Bazel atau pakar domain lainnya.
Menghindari konsumsi waktu dan memori kuadrat sangat sulit
Lebih buruk lagi, selain persyaratan yang diberlakukan oleh Skyframe, batasan historis penggunaan Java, dan keusangan API aturan, penggunaan waktu kuadrat atau konsumsi memori secara tidak sengaja adalah masalah dasar dalam sistem build apa pun berdasarkan aturan library dan biner. Ada dua pola yang sangat umum yang menyebabkan konsumsi memori kuadrat (dan karenanya konsumsi waktu kuadrat).
Rantai Aturan Library - Pertimbangkan kasus rantai aturan library A bergantung pada B, bergantung pada C, dan seterusnya. Kemudian, kita ingin menghitung beberapa properti melalui penutupan transitif aturan ini, seperti classpath runtime Java, atau perintah penaut C++ untuk setiap library. Secara naif, kita dapat menggunakan implementasi daftar standar; tetapi, hal ini sudah menyebabkan konsumsi memori kuadrat: library pertama berisi satu entri di classpath, dua entri kedua, tiga entri ketiga, dan seterusnya, sehingga total entri 1+2+3+...+N = O(N^2).
Aturan Biner yang Bergantung pada Aturan Library yang Sama - Pertimbangkan kasus saat sekumpulan biner yang bergantung pada aturan library yang sama — seperti jika Anda memiliki sejumlah aturan pengujian yang menguji kode library yang sama. Misalnya, dari N aturan, setengah aturan adalah aturan biner, dan setengah lainnya adalah aturan library. Sekarang pertimbangkan bahwa setiap biner membuat salinan beberapa properti yang dihitung melalui penutupan transitif aturan library, seperti classpath runtime Java, atau command line penaut C++. Misalnya, kode ini dapat memperluas representasi string command line dari tindakan link C++. N/2 salinan elemen N/2 adalah memori O(N^2).
Class koleksi kustom untuk menghindari kompleksitas kuadrat
Bazel sangat terpengaruh oleh kedua skenario ini, jadi kami memperkenalkan kumpulan
class koleksi kustom yang secara efektif mengompresi informasi dalam memori dengan
menghindari penyalinan di setiap langkah. Hampir semua struktur data ini telah menetapkan
semantik, jadi kami menyebutnya
depset
(juga dikenal sebagai NestedSet
dalam implementasi internal). Sebagian besar
perubahan untuk mengurangi konsumsi memori Bazel selama beberapa tahun terakhir adalah
perubahan untuk menggunakan depset, bukan apa pun yang sebelumnya digunakan.
Sayangnya, penggunaan depset tidak otomatis menyelesaikan semua masalah; terutama, bahkan hanya melakukan iterasi pada depset di setiap aturan akan kembali memperkenalkan konsumsi waktu kuadrat. Secara internal, NestedSets juga memiliki beberapa metode bantuan untuk memfasilitasi interoperabilitas dengan class koleksi normal; sayangnya, meneruskan NestedSet secara tidak sengaja ke salah satu metode ini akan menyebabkan perilaku penulisan ulang, dan memperkenalkan kembali konsumsi memori kuadrat.