Tantangan Menulis Aturan

Halaman ini memberikan ringkasan tingkat tinggi tentang masalah dan tantangan khusus dalam menulis aturan Bazel yang efisien.

Persyaratan Ringkasan

  • Asumsi: Berupayalah untuk mencapai Akurasi, Throughput, Kemudahan Penggunaan & Latensi
  • Asumsi: Repositori Skala Besar
  • Asumsi: Bahasa Deskripsi seperti BUILD
  • Historis: Pemisahan Ketat antara Pemuatan, Analisis, dan Eksekusi sudah Ketinggalan Zaman, 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 Pengodean yang Tidak Biasa
  • Intrinsik: Menghindari Konsumsi Waktu dan Memori Kuadratik itu Sulit

Asumsi

Berikut 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 secara efektif.

Targetkan kebenaran, throughput, kemudahan penggunaan, dan latensi

Kami mengasumsikan bahwa sistem build harus benar terlebih dahulu sehubungan dengan build inkremental. Untuk hierarki sumber tertentu, output dari build yang sama harus selalu sama, terlepas dari tampilan hierarki output. Dalam 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 seberapa akurat Bazel, karena Bazel membocorkan beberapa informasi seperti tanggal / waktu build, dan mengabaikan jenis perubahan tertentu seperti perubahan pada atribut file. Sandbox membantu memastikan kebenaran 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 keduanya merupakan masalah sulit. Kami berupaya jangka panjang untuk memperbaikinya.

Tujuan kedua sistem build adalah memiliki throughput tinggi; kami terus-menerus mendorong batas kemampuan 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 menjadi prioritas berikutnya. Dari beberapa pendekatan yang benar dengan jejak layanan eksekusi jarak jauh yang sama (atau serupa), 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 tujuan ini sering kali tumpang-tindih; latensi sama pentingnya dengan throughput layanan eksekusi jarak jauh seperti halnya kebenaran yang relevan untuk kemudahan penggunaan.

Repositori skala besar

Sistem build harus beroperasi dalam skala repositori besar, yang berarti tidak muat dalam satu hard drive, sehingga tidak mungkin melakukan checkout penuh di hampir semua mesin developer. Build berukuran sedang perlu membaca dan mengurai puluhan ribu file BUILD, serta mengevaluasi ratusan ribu blob. Meskipun secara teoretis semua file BUILD dapat dibaca 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 kurang lebih mirip dengan file BUILD dalam deklarasi aturan library dan biner serta interdependensinya. File BUILD dapat dibaca dan diuraikan secara independen, dan kami menghindari melihat file sumber jika memungkinkan (kecuali untuk keberadaan).

Bersejarah

Ada perbedaan antara versi Bazel yang menimbulkan 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, basis kode Bazel asli memiliki pemisahan yang ketat dalam memuat paket, lalu menganalisis aturan menggunakan konfigurasi (pada dasarnya, tanda command line), dan baru kemudian menjalankan tindakan apa pun. Perbedaan ini masih menjadi bagian dari Rules API hingga saat ini, meskipun inti Bazel tidak lagi memerlukannya (detail selengkapnya di bawah).

Artinya, Rules API memerlukan deskripsi deklaratif antarmuka aturan (atribut yang dimilikinya, 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 dirujuk dari aturan lain dalam grafik build.

Selain itu, analisis aturan tidak dapat membaca file sumber atau memeriksa output tindakan; sebagai gantinya, analisis harus membuat grafik bipartit berarah 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 properti yang paling umum dijelaskan di bagian berikut.

Eksekusi dan penyimpanan dalam cache jarak jauh sulit dilakukan

Eksekusi dan penyimpanan dalam cache jarak jauh meningkatkan waktu build di repositori besar dengan sekitar dua kali lipat dibandingkan dengan menjalankan build di satu mesin. Namun, skala yang diperlukan untuk melakukannya sangat besar: Layanan eksekusi jarak jauh Google dirancang untuk menangani sejumlah besar permintaan per detik, dan protokolnya dengan cermat menghindari perjalanan pulang pergi yang tidak perlu serta pekerjaan yang tidak perlu di sisi layanan.

Saat ini, protokol mengharuskan sistem build mengetahui semua input ke tindakan tertentu sebelumnya; sistem build kemudian menghitung sidik jari tindakan unik, dan meminta hit cache dari penjadwal. Jika hit cache ditemukan, penjadwal akan membalas dengan ringkasan file output; file itu sendiri ditangani berdasarkan ringkasan nanti. Namun, hal ini membatasi aturan Bazel, yang harus mendeklarasikan semua file input terlebih dahulu.

Penggunaan informasi perubahan untuk build inkremental yang benar dan cepat memerlukan pola coding yang tidak biasa

Di atas, kami berpendapat bahwa agar benar, Bazel perlu mengetahui semua file input yang masuk ke langkah build untuk mendeteksi apakah langkah build tersebut masih terbaru. Hal yang sama berlaku untuk pemuatan paket dan analisis aturan, dan kami telah mendesain Skyframe untuk menangani hal ini secara umum. Skyframe adalah framework evaluasi dan pustaka grafik yang menggunakan node tujuan (seperti 'build //foo dengan opsi ini'), dan menguraikannya menjadi bagian-bagiannya, yang kemudian dievaluasi dan digabungkan untuk menghasilkan hasil ini. Sebagai bagian dari proses ini, Skyframe membaca paket, menganalisis aturan, dan mengeksekusi tindakan.

Di setiap node, Skyframe melacak secara persis node mana yang digunakan oleh node tertentu untuk menghitung outputnya sendiri, mulai dari node tujuan hingga file input (yang juga merupakan node Skyframe). Dengan grafik ini yang direpresentasikan secara eksplisit dalam memori, sistem build dapat mengidentifikasi secara tepat node mana yang terpengaruh oleh perubahan tertentu pada file input (termasuk pembuatan atau penghapusan file input), sehingga melakukan jumlah pekerjaan minimal untuk memulihkan pohon output ke keadaan yang diinginkan.

Sebagai bagian dari proses 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 ribu node Skyframe, yang tidak mungkin dilakukan dengan teknologi Java saat ini (dan karena alasan historis, saat ini kami terikat untuk menggunakan Java, sehingga tidak ada thread ringan dan tidak ada kelanjutan).

Sebagai gantinya, Bazel menggunakan kumpulan thread berukuran tetap. Namun, itu berarti jika node mendeklarasikan dependensi yang belum tersedia, kita mungkin harus membatalkan evaluasi tersebut dan memulainya kembali (mungkin di thread lain), saat dependensi tersedia. Oleh karena itu, node tidak boleh melakukan hal ini secara berlebihan; node yang mendeklarasikan N dependensi secara serial berpotensi dimulai ulang N kali, sehingga memerlukan waktu O(N^2). Sebagai gantinya, kami berupaya melakukan deklarasi massal di awal untuk dependensi, yang terkadang memerlukan penataan ulang kode, atau bahkan membagi 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 mendasar 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 melewati Skyframe. Untuk Java, hal ini berarti menghindari java.io.File serta segala bentuk refleksi, dan library apa pun yang melakukan salah satu hal tersebut. Library yang mendukung injeksi dependensi antarmuka tingkat rendah ini masih perlu disiapkan dengan benar untuk Skyframe.

Hal ini sangat menyarankan untuk menghindari pemaparan penulis aturan ke runtime bahasa lengkap sejak awal. Bahaya penggunaan API semacam itu secara tidak sengaja terlalu besar - beberapa bug Bazel di masa lalu disebabkan oleh aturan yang menggunakan API tidak aman, meskipun aturan tersebut ditulis oleh tim Bazel atau pakar domain lainnya.

Menghindari konsumsi memori dan waktu kuadratik sulit dilakukan

Lebih buruk lagi, selain persyaratan yang diberlakukan oleh Skyframe, batasan historis penggunaan Java, dan API aturan yang sudah usang, pengenalan waktu kuadratik atau konsumsi memori secara tidak sengaja adalah masalah mendasar dalam sistem build apa pun yang didasarkan pada aturan library dan biner. Ada dua pola yang sangat umum yang menyebabkan konsumsi memori kuadratik (dan oleh karena itu, konsumsi waktu kuadratik).

  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 linker C++ untuk setiap library. Secara sederhana, kita dapat menerapkan daftar standar; namun, hal ini sudah menimbulkan konsumsi memori kuadratik: library pertama berisi satu entri di classpath, yang kedua berisi dua, yang ketiga berisi tiga, dan seterusnya, dengan total 1+2+3+...+N = O(N^2) entri.

  2. Aturan Biner yang Bergantung 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, setengahnya adalah aturan biner, dan setengahnya lagi 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 linker C++. Misalnya, tindakan ini dapat memperluas representasi string command line dari tindakan penautan C++. N/2 salinan N/2 elemen adalah memori O(N^2).

Class koleksi kustom untuk menghindari kompleksitas kuadrat

Bazel sangat terpengaruh oleh kedua skenario ini, jadi kami memperkenalkan serangkaian class koleksi kustom yang secara efektif memadatkan informasi dalam memori dengan menghindari penyalinan di setiap langkah. Hampir semua struktur data ini memiliki semantik set, 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 depsets, bukan apa pun yang digunakan sebelumnya.

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