Tantangan Menulis Aturan

Laporkan masalah Lihat sumber Per malam · 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 Sulit antara Pemuatan, Analisis, dan Eksekusi adalah Usang, 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 ketepatan, 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 pohon sumber tertentu, {i>output<i} dari build yang sama harus selalu sama, terlepas dari tampilan hierarki output suka. Pada perkiraan pertama, ini berarti Bazel perlu mengetahui setiap yang masuk ke dalam langkah build tertentu, sehingga ia dapat menjalankan kembali langkah tersebut jika ada 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.

Tujuan kedua dari sistem build adalah memiliki throughput yang tinggi; kami secara permanen mendobrak batas-batas apa yang dapat dilakukan dalam untuk layanan eksekusi jarak jauh. Jika eksekusi jarak jauh layanan menjadi kelebihan beban, tidak ada yang bisa menyelesaikan pekerjaan.

Yang berikutnya adalah kemudahan penggunaan. Dari beberapa pendekatan yang benar dengan serupa) dari layanan eksekusi jarak jauh, kita memilih salah satu 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 dari throughput layanan eksekusi jarak jauh, sebagaimana ketepatan yang relevan untuk kemudahan penggunaan.

Repositori skala besar

Sistem pembangunan perlu beroperasi pada skala repositori yang besar di mana {i>scale<i} berarti bahwa itu tidak muat pada satu {i>hard drive<i}, sehingga mustahil untuk melakukan {i>checkout<i} penuh pada hampir semua komputer pengembang. Build berukuran sedang perlu membaca dan menguraikan puluhan ribu file BUILD, serta mengevaluasi ratusan ribu glob. Meskipun secara teori mungkin untuk membaca semua BUILD di satu komputer, kami belum dapat melakukannya dalam dalam jumlah waktu dan memori yang wajar. Oleh karena itu, file BUILD harus dapat dimuat dan diuraikan secara terpisah.

Bahasa deskripsi yang mirip BANGUN

Dalam konteks ini, kita mengasumsikan bahasa konfigurasi yang kurang lebih mirip dengan file BUILD dalam deklarasi library dan aturan biner dan interdependensi mereka. BUILD file dapat dibaca dan diuraikan secara terpisah, dan kita bahkan menghindari melihat file sumber kapan pun kita bisa (kecuali ).

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, walaupun inti Bazel tidak lagi memerlukannya (detail selengkapnya ada 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. Sebagai contoh, aturan java_library bernama 'foo' secara implisit menghasilkan {i> output<i} bernama 'libfoo.jar', yang bisa direferensikan dari aturan lain dalam grafik build.

Selain itu, analisis aturan tidak dapat membaca file sumber apa pun atau memeriksa {i>output<i} dari suatu tindakan; sebagai gantinya, model perlu menghasilkan model grafik yang menunjukkan langkah-langkah pembuatan dan nama file output yang hanya ditentukan dari aturan tersebut 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 jarak jauh dan caching sulit

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, pada skala yang perlu dilakukan sungguh menakjubkan: layanan eksekusi jarak jauh dirancang untuk menangani sejumlah besar permintaan per kedua, dan protokol dengan hati-hati menghindari perjalanan bolak-balik 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 memberlakukan pembatasan pada aturan, yang harus mendeklarasikan semua file input sebelumnya.

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 yang masuk ke dalam langkah build untuk mendeteksi apakah langkah build itu masih diperbarui. Hal yang sama berlaku untuk pemuatan paket dan analisis aturan, dan kita telah merancang 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). Memiliki grafik ini secara eksplisit dalam memori memungkinkan sistem pembangunan untuk mengidentifikasi dengan tepat {i>node<i} mana yang terpengaruh oleh mengubah file input (termasuk pembuatan atau penghapusan file input), melakukan upaya minimal untuk memulihkan hierarki output ke status yang diinginkan.

Sebagai bagian dari 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 simpul Skyframe, yang tidak mudah dilakukan dengan Java saat ini teknologi (dan karena alasan historis, saat ini kita terikat pada penggunaan Java, jadi tanpa thread ringan dan tanpa kelanjutan).

Sebagai gantinya, Bazel menggunakan kumpulan thread berukuran tetap. Namun, itu berarti bahwa jika sebuah {i>node<i} mendeklarasikan dependensi yang belum tersedia, kita mungkin harus membatalkannya evaluasi dan memulai ulang (mungkin di thread lain), saat dependensi yang 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 API aturan; sebagai gantinya, API aturan masih didefinisikan menggunakan konsep lama pemuatan, analisis, dan eksekusi. Namun, batasan mendasar adalah semua akses ke {i>node<i} lain harus melewati kerangka kerja sehingga 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, itu berarti menghindari java.io.File serta segala bentuk refleksi, dan perpustakaan apa pun yang melakukan keduanya. Library yang mendukung dependensi injeksi antarmuka tingkat rendah ini masih perlu diatur dengan benar untuk dengan Skyframe.

Hal ini sangat menyarankan untuk menghindari mengekspos penulis aturan ke runtime bahasa lengkap sejak awal. Risiko penggunaan API semacam itu secara tidak sengaja terlalu besar - beberapa {i>bug<i} Bazel di masa lalu disebabkan oleh aturan yang menggunakan API yang tidak aman, bahkan 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 menimbulkan konsumsi memori kuadrat (dan karenanya konsumsi waktu kuadrat).

  1. Jaringan Aturan Koleksi - Pertimbangkan kasus rantai aturan library A bergantung pada B, bergantung pada C, dan 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}. Naif, kita mungkin mengambil implementasi daftar standar; namun, model ini sudah memperkenalkan konsumsi memori kuadrat: library pertama berisi satu entri di classpath, dua entri kedua, tiga entri ketiga, aktif, dengan total 1+2+3+...+N = O(N^2) entri.

  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. Katakanlah dari aturan N, setengah dari aturan itu adalah aturan biner, dan setengah aturan perpustakaan lainnya. Sekarang perhatikan bahwa setiap biner membuat salinan beberapa properti dihitung selama penutupan transitif aturan perpustakaan, classpath runtime Java, atau command line penaut C++. Sebagai contoh, dapat memperluas representasi string command line dari tindakan link C++. T/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 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 kelas koleksi biasa; sayangnya, secara tidak sengaja meneruskan NestedSet ke salah satu metode ini akan menyebabkan penyalinan perilaku model, dan memperkenalkan kembali konsumsi memori kuadrat.