Tantangan Menulis Aturan

Laporkan masalah Lihat sumber Per Malam · 7,4 kami. 7,3 · 7,2 · 7,1 · 7,0 · 6,5

Halaman ini memberikan gambaran umum tentang masalah dan tantangan tertentu penulisan aturan Bazel yang efisien.

Persyaratan Ringkasan

  • Asumsi: Bertujuan untuk Ketepatan, Throughput, Kemudahan Penggunaan & Latensi
  • Asumsi: Repositori Skala Besar
  • Asumsi: Bahasa Deskripsi mirip BANGUN
  • Historis: Pemisahan Keras antara Pemuatan, Analisis, dan Eksekusi sudah Tidak berlaku, tetapi masih memengaruhi API
  • Intrinsik: Eksekusi Jarak Jauh dan Cache Sulit
  • Intrinsic: Menggunakan Informasi Perubahan untuk Build Inkremental yang Benar dan Cepat memerlukan Pola Coding yang Tidak Biasa
  • Intrinsik: Menghindari Waktu Kuadrat dan Konsumsi Memori itu Sulit

Asumsi

Berikut adalah beberapa asumsi yang dibuat tentang sistem build, seperti kebutuhan akan kebenaran, kemudahan penggunaan, throughput, dan repositori skala besar. Tujuan bagian berikut membahas asumsi tersebut dan memberikan panduan untuk memastikan aturan tertulis secara efektif.

Targetkan ketepatan, throughput, kemudahan penggunaan, & latensi

Kami berasumsi bahwa sistem pembangunan harus benar dan pertama, terkait dengan 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 cara mendapatkan Bazel yang benar, karena kebocoran beberapa informasi seperti tanggal / waktu pembangunan, dan mengabaikan jenis perubahan 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 kebenaran yang diketahui, yang sebagian besar terkait dengan Fileset atau aturan C++, yang keduanya menyelesaikan semua jenis permasalahan. Kami memiliki upaya jangka panjang untuk memperbaikinya.

Sasaran kedua sistem build adalah memiliki throughput tinggi; kita secara permanen mendorong batas apa yang dapat dilakukan dalam alokasi mesin saat ini untuk layanan eksekusi jarak jauh. Jika eksekusi jarak jauh layanan menjadi kelebihan beban, tidak ada yang bisa 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 mulai dari memulai build hingga mencapai tujuan yang diinginkan hasil, apakah itu log pengujian dari pengujian yang lulus atau gagal, atau error pesan bahwa file BUILD memiliki kesalahan ketik.

Perhatikan bahwa sasaran ini sering kali tumpang-tindih; latensi adalah fungsi throughput layanan eksekusi jarak jauh dan juga relevan dengan ketepatan 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 perlu membaca dan menguraikan puluhan ribu file BUILD, serta mengevaluasi ratusan ribu glob. Meskipun secara teoritis dapat membaca semua file BUILD di satu komputer, 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 library dan aturan biner dan 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 usang, tetapi masih memengaruhi API

Secara teknis, cukup bagi sebuah aturan untuk mengetahui file {i>input <i}dan {i>output <i}dari tindakan sebelum tindakan dikirim ke eksekusi jarak jauh. Namun, basis kode Bazel asli memiliki pemisahan paket pemuatan yang ketat, maka menganalisis aturan menggunakan konfigurasi (pada dasarnya flag command line), 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, API aturan memerlukan deskripsi deklaratif dari aturan antarmuka (atribut yang dimilikinya, jenis atribut). Ada beberapa pengecualian saat API mengizinkan kode kustom berjalan selama fase pemuatan untuk menghitung nama implisit file {i>output<i} 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 caching meningkatkan waktu build di repositori besar dengan kira-kira dua kali lipat dibandingkan dengan menjalankan build pada satu mesin Linux dan Windows. Namun, skala yang diperlukan untuk menjalankannya sangat besar: layanan eksekusi jarak jauh Google dirancang untuk menangani permintaan dalam jumlah besar per detik, dan protokol ini dengan cermat menghindari roundtrip yang tidak perlu serta pekerjaan yang tidak perlu di sisi layanan.

Saat ini, protokol mengharuskan sistem pembangunan mengetahui semua input ke diberikan tindakan sebelumnya; sistem build kemudian menghitung tindakan unik sidik jari, dan meminta penjadwal untuk mendapatkan cache ditemukan. Jika ditemukan cache, penjadwal membalas dengan ringkasan file {i>output<i}; file tersebut ditangani oleh digest. Namun, hal ini akan memberlakukan batasan pada aturan Bazel, yang perlu mendeklarasikan semua file input terlebih dahulu.

Penggunaan 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 pustaka grafik dan kerangka kerja evaluasi yang membutuhkan node sasaran (seperti 'build //foo with these options'), dan memecahnya menjadi bagian konstituennya, yang kemudian dievaluasi dan digabungkan untuk menghasilkan hasil pengujian tersebut. Sebagai bagian dari proses ini, Skyframe membaca paket, menganalisis aturan, dan menjalankan tindakan.

Pada setiap {i>node<i}, Skyframe melacak dengan tepat {i>node<i} mana yang digunakan untuk menghitung outputnya 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 jumlah pekerjaan minimal untuk memulihkan hierarki output ke status yang diinginkan.

Sebagai bagian dari hal ini, setiap node melakukan proses penemuan dependensi. Masing-masing node dapat mendeklarasikan dependensi, lalu menggunakan konten dependensi tersebut untuk mendeklarasikan dependensi yang lebih jauh. Pada prinsipnya, ini memetakan 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. Artinya, node seharusnya tidak melakukan ini secara berlebihan; suatu yang mendeklarasikan dependensi N secara berurutan bisa dimulai ulang sebanyak N kali, yang menghabiskan waktu O(N^2). Sebagai gantinya, kami menargetkan untuk melakukan deklarasi secara massal dependensi, yang terkadang membutuhkan pengaturan ulang kode, atau bahkan pemisahan node menjadi beberapa {i>node<i} untuk membatasi jumlah {i>restart<i}.

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. Apa pun bahasa yang digunakan sistem build, diterapkan atau di mana aturan ditulis (tidak harus berupa sama), penulis aturan tidak boleh menggunakan pustaka standar atau pola yang mengabaikan dengan 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 mengekspos penulis aturan ke runtime bahasa lengkap sejak 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 waktu kuadrat dan konsumsi memori adalah hal yang sulit

Untuk memperburuk masalah, terlepas dari persyaratan yang diberlakukan oleh Skyframe, kendala historis penggunaan Java, dan usangnya API aturan, memasukkan waktu kuadrat atau konsumsi memori secara tidak sengaja adalah di sistem build apa pun berdasarkan perpustakaan dan aturan biner. Ada dua pola yang sangat umum yang menyebabkan konsumsi memori kuadrat (dan karenanya konsumsi 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 selama penutupan transitif dari aturan ini, seperti classpath runtime Java, atau perintah penaut C++ untuk setiap {i>library<i}. 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).

  2. Aturan Biner yang Bergantung pada Aturan Library yang Sama - Pertimbangkan kasus saat kumpulan biner yang bergantung pada library yang sama seperti jika Anda memiliki sejumlah aturan pengujian yang menguji kode library. 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++. Salinan N/2 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 serangkaian yang secara efektif mengompresi informasi dalam memori dengan menghindari salinan di setiap langkah. Hampir semua struktur data ini telah menetapkan semantik, jadi kita menyebutnya depset (juga dikenal sebagai NestedSet dalam implementasi internal). Mayoritas perubahan untuk mengurangi konsumsi memori Bazel selama beberapa tahun terakhir perubahan untuk menggunakan {i>depset <i}alih-alih apa pun yang telah digunakan sebelumnya.

Sayangnya, penggunaan depset tidak otomatis menyelesaikan semua masalah; secara khusus, bahkan hanya mengulangi {i>depset<i} di setiap aturan yang 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.