Tantangan Menulis Aturan

Laporkan masalah Lihat sumber

Halaman ini memberikan ringkasan umum tentang masalah dan tantangan spesifik dalam menulis aturan Bazel yang efisien.

Persyaratan Ringkasan

  • Asumsi: Bertujuan untuk Kebenaran, Throughput, Kemudahan Penggunaan & Latensi
  • Asumsi: Repositori Skala Besar
  • Asumsi: Bahasa Deskripsi mirip BANGUN
  • Historis: Pemisahan Keras antara Pemuatan, Analisis, dan Eksekusi Usang, tetapi masih memengaruhi API
  • Intrinsik: Eksekusi Jarak Jauh dan Caching Sangat Sulit
  • Intrinsik: Penggunaan Informasi Perubahan untuk Build Inkremental yang Cepat dan Benar 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 perlunya ketepatan, kemudahan penggunaan, throughput, dan repositori berskala besar. Bagian berikut membahas asumsi tersebut dan menawarkan panduan untuk memastikan aturan ditulis secara efektif.

Targetkan ketepatan, throughput, kemudahan penggunaan, dan latensi

Kami berasumsi bahwa sistem build harus yang pertama dan terpenting benar terkait dengan build inkremental. Untuk hierarki sumber tertentu, output build yang sama harus selalu sama, terlepas dari tampilan hierarki outputnya. Dalam perkiraan pertama, ini berarti Bazel perlu mengetahui setiap input yang masuk ke langkah build tertentu, sehingga dapat menjalankan kembali langkah tersebut jika ada perubahan input. Ada batasan terkait seberapa benar Bazel dapat memperoleh 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 ketepatan umum, yang sebagian besar terkait dengan Fileset atau aturan C++, yang keduanya merupakan masalah sulit. Kami memiliki upaya jangka panjang untuk memperbaikinya.

Tujuan kedua dari sistem build adalah memiliki throughput yang tinggi; kami mendorong batas-batas yang dapat dilakukan secara permanen dalam alokasi mesin saat ini untuk layanan eksekusi jarak jauh. Jika layanan eksekusi jarak jauh kelebihan beban, tidak ada yang dapat menyelesaikan pekerjaan.

Berikutnya adalah kemudahan penggunaan. Dari beberapa pendekatan yang benar dengan jejak yang sama (atau serupa) dengan layanan eksekusi jarak jauh, kami memilih pendekatan yang lebih mudah digunakan.

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

Perlu diperhatikan bahwa sasaran ini sering kali tumpang tindih; latensi sama pentingnya dengan throughput layanan eksekusi jarak jauh, sebagaimana ketepatan yang relevan untuk kemudahan penggunaan.

Repositori berskala besar

Sistem build perlu beroperasi pada skala repositori besar, yang berskala besar berarti tidak muat di satu hard drive, sehingga tidak mungkin untuk melakukan checkout penuh di hampir semua mesin developer. Build berukuran sedang perlu membaca dan mengurai puluhan ribu file BUILD, serta mengevaluasi ratusan ribu glob. Meskipun secara teoritis mungkin untuk membaca semua file BUILD pada satu mesin, kami belum dapat melakukannya dalam waktu dan memori yang wajar. Oleh karena itu, file BUILD harus dapat dimuat dan diurai secara independen.

Bahasa deskripsi mirip BANGUN

Dalam konteks ini, kami asumsikan bahasa konfigurasi yang kurang lebih mirip dengan file BUILD di deklarasi aturan library dan biner serta interdependensinya. File BUILD dapat dibaca dan diurai secara independen, dan kita bahkan menghindari melihat file sumber kapan pun kita bisa (kecuali keberadaannya).

Bersejarah

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

Pemisahan keras antara pemuatan, analisis, dan eksekusi sudah usang, tetapi masih memengaruhi API

Secara teknis, aturan cukup untuk mengetahui file input dan output dari tindakan tepat sebelum tindakan tersebut dikirim ke eksekusi jarak jauh. Namun, code base Bazel yang asli memiliki pemisahan yang ketat antara memuat paket, lalu menganalisis aturan menggunakan konfigurasi (pada dasarnya flag command line), dan hanya 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 antarmuka aturan (atribut apa yang dimilikinya, jenis atribut). Ada beberapa pengecualian saat API memungkinkan 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 direferensikan dari aturan lain dalam grafik build.

Selain itu, analisis aturan tidak dapat membaca file sumber apa pun atau memeriksa output suatu tindakan. Sebagai gantinya, aturan harus menghasilkan grafik bipartit terarah parsial yang mengarahkan 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 jarak jauh dan cache sulit dilakukan

Eksekusi dan cache jarak jauh meningkatkan waktu build dalam repositori besar dengan sekitar dua kali lipat dibandingkan dengan menjalankan build pada satu mesin. Namun, skala yang diperlukan untuk menjalankan performa ini sungguh mengejutkan: Layanan eksekusi jarak jauh Google dirancang untuk menangani banyak permintaan per detik, dan protokol dengan hati-hati menghindari perjalanan bolak-balik yang tidak perlu serta pekerjaan yang tidak perlu di sisi layanan.

Untuk saat ini, protokol mengharuskan sistem build mengetahui semua input ke tindakan tertentu terlebih dahulu; sistem build kemudian menghitung sidik jari tindakan unik, dan meminta penjadwal untuk menemukan cache ditemukan. Jika cache ditemukan, penjadwal akan membalas dengan ringkasan file output; file itu sendiri akan ditangani oleh ringkasan nanti. Namun, hal ini akan menerapkan 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 dalam langkah build untuk mendeteksi apakah langkah build tersebut masih terbaru atau tidak. 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 mengambil node tujuan (seperti 'build //foo with these options'), dan menguraikannya menjadi bagian-bagian konstituennya, yang kemudian dievaluasi dan digabungkan untuk memberikan hasil ini. Sebagai bagian dari proses ini, Skyframe membaca paket, menganalisis aturan, dan menjalankan tindakan.

Di setiap node, Skyframe melacak dengan tepat node mana yang digunakan untuk menghitung output-nya sendiri, mulai dari node sasaran hingga file input (yang juga merupakan node Skyframe). Grafik ini ditampilkan secara eksplisit dalam memori memungkinkan sistem build mengidentifikasi secara tepat node yang terpengaruh oleh perubahan tertentu pada file input (termasuk pembuatan atau penghapusan file input), sehingga melakukan pekerjaan minimal untuk memulihkan hierarki output ke status yang diinginkan.

Sebagai bagian dari 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 mudah dilakukan dengan teknologi Java saat ini (dan karena alasan historis, saat ini kami terikat pada penggunaan Java, sehingga tidak ada thread ringan dan tidak ada kelanjutan).

Sebagai gantinya, Bazel menggunakan kumpulan thread ukuran tetap. Namun, itu berarti jika node mendeklarasikan dependensi yang belum tersedia, kita mungkin harus membatalkan evaluasi tersebut dan memulai ulangnya (mungkin di thread lain), saat dependensi tersedia. Pada akhirnya, hal ini berarti node tidak boleh melakukan hal ini secara berlebihan; node yang mendeklarasikan dependensi N secara serial dapat berpotensi dimulai ulang N kali, yang menghabiskan waktu O(N^2). Sebagai gantinya, kami menargetkan deklarasi dependensi massal di awal, yang terkadang memerlukan pengaturan ulang kode, atau bahkan membagi node menjadi beberapa node untuk membatasi jumlah mulai ulang.

Perhatikan bahwa teknologi ini saat ini tidak tersedia di API aturan; sebagai gantinya, API aturan masih ditentukan menggunakan konsep lama 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 sistem build atau yang digunakan untuk menulis 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 harus disiapkan dengan benar untuk Skyframe.

Hal ini sangat disarankan untuk menghindari mengekspos penulis aturan ke runtime bahasa lengkap sejak awal. Bahaya penggunaan API yang tidak disengaja 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 penggunaan waktu kuadrat dan memori sulit

Lebih buruk lagi, terlepas dari persyaratan yang diberlakukan oleh Skyframe, batasan historis penggunaan Java, dan ketinggalan zaman API aturan, menghadirkan waktu kuadrat atau konsumsi memori secara tidak sengaja merupakan masalah mendasar dalam setiap sistem build yang didasarkan pada aturan library dan biner. Ada dua pola yang sangat umum yang memperkenalkan konsumsi memori kuadrat (dan, karenanya konsumsi waktu kuadrat).

  1. Rantai Aturan Library - Perhatikan kasus rantai aturan library A yang 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 linker C++ untuk setiap library. Secara alami, kita mungkin mengambil implementasi daftar standar; akan tetapi, ini sudah memperkenalkan konsumsi memori kuadrat: library pertama berisi satu entri pada classpath, dua kedua, tiga ketiga, dan seterusnya, dengan total 1+2+3+...+N = O(N^2).

  2. Aturan Biner 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 aturan N, setengah dari aturan adalah aturan biner, dan separuh aturan library lainnya adalah aturan. 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, fungsi ini dapat memperluas representasi string command line untuk 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 dalam memori secara efektif dengan menghindari penyalinan di setiap langkah. Hampir semua struktur data ini telah menetapkan semantik, jadi kami menyebutnya dependensi (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 dependensi tidak otomatis menyelesaikan semua masalah; khususnya, bahkan hanya melakukan iterasi melalui dependensi 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, meneruskan NestedSet yang tidak sengaja ke salah satu metode ini menyebabkan penyalinan perilaku dan memperkenalkan kembali penggunaan memori kuadrat.