Catat Tanggalnya: BazelCon 2023 akan diadakan pada 24-25 Oktober di Google Munich! Pelajari lebih lanjut

Tantangan Aturan Penulisan

Laporkan masalah Lihat sumber

Halaman ini memberikan ringkasan umum tentang masalah dan tantangan spesifik dari penulisan aturan Bazel yang efisien.

Persyaratan Ringkasan

  • Asumsi: Bertujuan untuk Ketepatan, Throughput, Kemudahan Penggunaan & amp; 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 dan Caching Jarak Jauh itu Sulit
  • Intrinsik: Penggunaan Informasi Perubahan untuk Build Inkremental yang Benar dan Cepat memerlukan Pola Coding yang Tidak Biasa
  • Intrinsik: Menghindari Waktu Kuadrat dan Konsumsi Memori Sulit

Asumsi

Berikut adalah beberapa asumsi yang dibuat tentang sistem build, seperti kebutuhan koreksi, kemudahan penggunaan, throughput, dan repositori skala besar. Bagian berikut membahas asumsi dan menawarkan panduan untuk memastikan aturan ditulis secara efektif.

Targetkan ketepatan, throughput, kemudahan penggunaan & amp; latensi

Kami berasumsi bahwa sistem build harus diutamakan dan paling tepat dalam hal build build inkremental. Untuk hierarki sumber tertentu, output build yang sama harus selalu sama, terlepas dari seperti apa tampilan hierarki output-nya. Pada perkiraan pertama, ini berarti Bazel perlu mengetahui setiap input yang masuk ke langkah build tertentu, sehingga dapat menjalankan kembali langkah tersebut jika ada input yang berubah. Ada batasan terkait cara mendapatkan Bazel yang benar, karena metode ini membocorkan beberapa informasi seperti tanggal / waktu build, dan mengabaikan jenis perubahan tertentu seperti perubahan pada atribut file. Sandbox membantu memastikan ketepatan dengan mencegah pembacaan ke file input yang tidak dideklarasikan. Selain batas intrinsik sistem, ada beberapa masalah ketepatan yang diketahui, yang sebagian besar terkait dengan fileset atau aturan C++, yang keduanya adalah masalah yang sulit. Kami berupaya jangka panjang untuk memperbaiki masalah ini.

Tujuan kedua dari sistem build adalah memiliki throughput yang tinggi; kami mendorong secara permanen batasan 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 akan muncul di urutan berikutnya. Dari beberapa pendekatan yang benar dengan jejak layanan yang sama (atau yang serupa) dari layanan eksekusi jarak jauh, kami memilih pendekatan yang lebih mudah digunakan.

Latensi menunjukkan waktu yang diperlukan mulai dari memulai build hingga mendapatkan hasil yang diinginkan, baik log pengujian dari pengujian yang lulus maupun gagal, atau pesan error bahwa ada kesalahan ketik pada file BUILD.

Perhatikan bahwa sasaran ini sering kali tumpang tindih; latensi adalah fungsi throughput dari layanan eksekusi jarak jauh dan ketepatannya relevan dengan kemudahan penggunaan.

Repositori skala besar

Sistem build perlu beroperasi pada skala repositori besar dengan skala besar jika tidak sesuai dengan satu hard drive, sehingga tidak mungkin melakukan checkout penuh di hampir semua mesin developer. Build berukuran sedang harus membaca dan mengurai puluhan ribu file BUILD, dan mengevaluasi ratusan ribu glob. Meskipun secara teori dimungkinkan untuk membaca semua file BUILD pada satu mesin, kami belum dapat melakukannya dalam waktu dan memori yang wajar. Dengan demikian, sangat penting agar file BUILD dapat dimuat dan diuraikan secara terpisah.

Bahasa deskripsi yang mirip dengan BUILD

Dalam konteks ini, kami mengasumsikan bahasa konfigurasi yang kira-kira mirip dengan file BUILD dalam deklarasi library dan aturan biner serta keterkaitannya. File BUILD dapat dibaca dan diuraikan secara terpisah, dan kami bahkan tidak akan melihat file sumber kapan pun kami bisa (kecuali untuk keberadaan).

Bersejarah

Ada perbedaan antara versi Bazel yang menyebabkan tantangan dan beberapa di antaranya diuraikan di bagian berikut.

Pemisahan yang sulit antara pemuatan, analisis, dan eksekusi sudah tidak berlaku tetapi masih memengaruhi API

Secara teknis, aturan cukup diketahui 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 (tanda command line pada dasarnya) dan baru menjalankan tindakan apa pun. Perbedaan ini masih menjadi bagian dari API aturan saat ini, meskipun inti Bazel tidak lagi memerlukannya (detail selengkapnya ada di bawah).

Artinya, API aturan memerlukan deskripsi deklaratif dari antarmuka aturan (atribut apa yang dimilikinya, jenis atribut). Ada beberapa pengecualian ketika API mengizinkan kode kustom dijalankan 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 dirujuk dari aturan lain dalam grafik build.

Selain itu, analisis aturan tidak dapat membaca file sumber apa pun atau memeriksa output tindakan. Sebagai gantinya, analisis perlu membuat grafik bipartit terarah sebagian dari langkah-langkah build dan nama file output yang hanya ditentukan dari aturan itu sendiri dan dependensinya.

Intrinsik

Ada beberapa properti intrinsik yang membuat aturan penulisan menjadi sulit dan beberapa yang paling umum dijelaskan di bagian berikut.

Eksekusi dan cache dari jarak jauh itu sulit

Eksekusi dan caching jarak jauh meningkatkan waktu build di repositori besar dengan sekitar dua urutan besarnya dibandingkan dengan menjalankan build di satu mesin. Namun, skala yang perlu dilakukan sangat mengejutkan: layanan eksekusi jarak jauh Google dirancang untuk menangani banyak permintaan per detik, dan protokol dengan cermat menghindari bolak-balik yang tidak perlu serta pekerjaan yang tidak perlu di sisi layanan.

Pada saat ini, protokol mengharuskan sistem build mengetahui semua input ke tindakan tertentu sebelumnya; sistem build kemudian menghitung sidik jari tindakan unik, dan meminta penjadwal cache yang ditemukan. Jika ditemukan cache ditemukan, penjadwal akan membalas dengan ringkasan file output; nantinya file tersebut akan ditangani oleh digest. Namun, hal ini menerapkan batasan pada aturan Bazel, yang harus mendeklarasikan semua file input terlebih dahulu.

Menggunakan informasi perubahan untuk build inkremental yang benar dan cepat membutuhkan pola coding yang tidak biasa

Di atas, kami berpendapat bahwa agar benar, Bazel perlu mengetahui semua file input yang dimasukkan ke langkah build untuk mendeteksi apakah langkah build tersebut masih diperbarui. 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 the options'), dan memecahnya menjadi bagian-bagian konstituen, yang kemudian dievaluasi dan digabungkan untuk menghasilkan hasil ini. Sebagai bagian dari proses ini, Skyframe membaca paket, menganalisis aturan, dan menjalankan tindakan.

Pada setiap node, Skyframe melacak dengan tepat node mana yang digunakan oleh node tertentu untuk menghitung output-nya sendiri, mulai dari node sasaran hingga file input (yang juga merupakan node Skyframe). Memiliki grafik ini yang secara eksplisit terwakili dalam memori memungkinkan sistem build mengidentifikasi dengan tepat node mana yang terpengaruh oleh perubahan tertentu pada file input (termasuk pembuatan atau penghapusan file input), melakukan jumlah pekerjaan minimal untuk memulihkan hierarki output ke keadaan 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, konfigurasi ini dipetakan dengan baik ke model thread per node. Namun, build berukuran sedang berisi ratusan ribu node Skyframe, yang tidak mudah dilakukan dengan teknologi Java saat ini (dan karena alasan historis, saat ini kita terikat untuk menggunakan Java, jadi 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. Ini, pada gilirannya, berarti node tidak boleh melakukan ini secara berlebihan; node yang mendeklarasikan dependensi N secara serial dapat berpotensi dimulai ulang N kali, dengan biaya waktu O(N^2). Sebagai gantinya, kami bertujuan untuk deklarasi dependensi di muka dalam jumlah besar, yang terkadang memerlukan pengaturan ulang kode, atau bahkan membagi node menjadi beberapa node untuk membatasi jumlah mulai ulang.

Perlu diperhatikan bahwa teknologi ini saat ini tidak tersedia di API aturan. Sebagai gantinya, API aturan tetap ditentukan menggunakan konsep fase pemuatan, analisis, dan eksekusi yang lama. Namun, pembatasan mendasarnya adalah semua akses ke node lain harus melalui framework tersebut agar bisa melacak dependensi yang terkait. Apa pun bahasa yang digunakan untuk menerapkan sistem build atau yang membuat aturan (tidak harus sama), penulis aturan tidak boleh menggunakan library atau pola standar yang mengabaikan Skyframe. Untuk Java, itu berarti menghindari java.io.File serta segala bentuk refleksi, dan library apa pun yang melakukannya. Library yang mendukung injeksi dependensi antarmuka level rendah ini masih perlu disiapkan dengan benar untuk Skyframe.

Sebaiknya jangan tampilkan penulis aturan ke runtime bahasa lengkap terlebih dahulu. Bahaya penggunaan API tersebut secara tidak sengaja terlalu besar - beberapa bug Bazel di masa lalu disebabkan oleh aturan yang menggunakan API yang tidak aman, meskipun aturan tersebut ditulis oleh tim Bazel atau pakar domain lainnya.

Menghindari waktu kuadrat dan konsumsi memori sulit dilakukan

Lebih buruk lagi, terlepas dari persyaratan yang diberlakukan oleh Skyframe, batasan historis penggunaan Java, dan usangnya aturan API, secara tidak sengaja memasukkan waktu kuadrat atau konsumsi memori adalah masalah dasar dalam sistem build apa pun berdasarkan library dan aturan biner. Ada dua pola yang sangat umum yang memperkenalkan konsumsi memori kuadrat (sehingga penggunaan waktu kuadrat).

  1. 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 dari aturan ini, seperti classpath runtime Java, atau perintah penaut C++ untuk setiap library. Secara naif, kita mungkin akan mengambil implementasi daftar standar; namun, hal ini sudah memperkenalkan pemakaian memori kuadrat: library pertama berisi satu entri pada classpath, dua entri kedua, tiga entri ketiga, dan seterusnya, dengan total 1+2+3+...+N = O(N^2) entri.

  2. Aturan Biner Tergantung pada Aturan Library yang Sama - Pertimbangkan kasus ketika 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 aturan library lainnya. Sekarang pertimbangkan bahwa setiap biner membuat salinan beberapa properti yang dihitung melalui penutupan transitif aturan library, seperti classpath runtime Java, atau command line linker C++. Misalnya, tindakan 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 sekumpulan class koleksi kustom yang mengompresi informasi secara efektif dalam memori dengan menghindari salinan pada 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 penggunaan depset, bukan apa pun yang sebelumnya digunakan.

Sayangnya, penggunaan depset tidak otomatis menyelesaikan semua masalah; khususnya, bahkan hanya melakukan iterasi pada depset di setiap aturan akan memperkenalkan konsumsi waktu kuadrat. Secara internal, NestedSets juga memiliki beberapa metode helper untuk memfasilitasi interoperabilitas dengan class koleksi normal; sayangnya, meneruskan NestedSet secara tidak sengaja ke salah satu metode ini menyebabkan penyalinan perilaku, dan memperkenalkan kembali pemakaian memori kuadrat.