Saya telah bekerja untuk perusahaan yang mengembangkan IDE untuk interaksi database selama lebih dari lima tahun. Sebelum mulai menulis artikel ini, saya tidak tahu berapa banyak dongeng yang akan terbentang di depan.
Tim saya mengembangkan dan mendukung fitur bahasa IDE, dan pelengkapan otomatis kode adalah yang utama. Saya menghadapi banyak hal menarik yang terjadi. Beberapa hal yang kami lakukan dengan baik sejak percobaan pertama, dan beberapa lainnya gagal bahkan setelah beberapa tembakan.
Penguraian SQL dan dialek
SQL adalah upaya untuk terlihat seperti bahasa alami, dan upaya tersebut cukup berhasil, menurut saya. Tergantung pada dialeknya, ada beberapa ribu kata kunci. Untuk membedakan satu pernyataan dari yang lain, Anda sering perlu mencari satu atau dua kata (token) di depan. Pendekatan ini disebut lookahead .
Ada klasifikasi parser tergantung pada seberapa jauh mereka dapat melihat ke depan:LA(1), LA(2), atau LA(*), yang berarti bahwa parser dapat melihat sejauh yang diperlukan untuk menentukan garpu yang tepat.
Terkadang, akhiran klausa opsional cocok dengan awal klausa opsional lainnya. Situasi ini membuat parsing lebih sulit untuk dijalankan. T-SQL tidak membuat segalanya lebih mudah. Selain itu, beberapa pernyataan SQL mungkin memiliki, tetapi tidak harus, akhiran yang dapat bertentangan dengan awal pernyataan sebelumnya.
Apakah kamu tidak percaya? Ada cara untuk mendeskripsikan bahasa formal melalui Grammar. Anda dapat membuat parser darinya menggunakan alat ini atau itu. Alat dan bahasa yang paling menonjol yang menjelaskan tata bahasa adalah YACC dan ANTLR.
YACC parser yang dihasilkan digunakan di mesin MySQL, MariaDB, dan PostgreSQL. Kami dapat mencoba dan mengambilnya langsung dari kode sumber dan mengembangkan penyelesaian kode dan fungsi lainnya berdasarkan analisis SQL menggunakan parser ini. Selain itu, produk ini akan menerima pembaruan pengembangan gratis, dan pengurai akan berperilaku seperti yang dilakukan oleh mesin sumber.
Jadi mengapa kita masih menggunakan ANTLR ? Ini dengan kuat mendukung C#/.NET, memiliki toolkit yang layak, sintaksnya jauh lebih mudah untuk dibaca dan ditulis. Sintaks ANTLR menjadi sangat berguna sehingga Microsoft sekarang menggunakannya dalam dokumentasi C# resminya.
Tapi mari kita kembali ke kompleksitas SQL dalam hal penguraian. Saya ingin membandingkan ukuran tata bahasa dari bahasa yang tersedia untuk umum. Di dbForge, kami menggunakan potongan tata bahasa kami. Mereka lebih lengkap dari yang lain. Sayangnya, mereka kelebihan beban dengan sisipan kode C# untuk mendukung fungsi yang berbeda.
Ukuran tata bahasa untuk bahasa yang berbeda adalah sebagai berikut :
JS – 475 baris parser + 273 lexer =748 baris
Java – 615 baris parser + 211 lexers =826 baris
C# – 1159 baris parser + 433 lexers =1592 baris
++ – 1933 baris
MySQL – 2515 baris parser + 1189 lexers =3704 baris
T-SQL – 4035 baris parser + 896 lexers =4931 baris
PL SQL – 6719 baris parser + 2366 lexers =9085 baris
Akhiran beberapa lexer menampilkan daftar karakter Unicode yang tersedia dalam bahasa tersebut. Daftar tersebut tidak berguna mengenai evaluasi kompleksitas bahasa. Jadi, jumlah baris yang saya ambil selalu berakhir sebelum daftar ini.
Mengevaluasi kompleksitas parsing bahasa berdasarkan jumlah baris dalam tata bahasa masih bisa diperdebatkan. Namun, saya yakin penting untuk menunjukkan angka yang menunjukkan perbedaan besar.
Itu tidak semua. Karena kami sedang mengembangkan IDE, kami harus menangani skrip yang tidak lengkap atau tidak valid. Kami harus menemukan banyak trik, tetapi pelanggan masih mengirim banyak skenario kerja dengan skrip yang belum selesai. Kita harus menyelesaikan ini.
Perang predikat
Selama penguraian kode, kata terkadang tidak memberi tahu Anda yang mana dari dua alternatif yang harus dipilih. Mekanisme yang memecahkan jenis ketidakakuratan ini adalah lookahead di ANTLR. Metode parser adalah rantai yang disisipkan dari if's , dan masing-masing tampak selangkah lebih maju. Lihat contoh tata bahasa yang menghasilkan ketidakpastian seperti ini:
rule1:
'a' rule2 | rule3
;
rule2:
'b' 'c' 'd'
;
rule3:
'b' 'c' 'e'
;
Di tengah aturan1, ketika token 'a' sudah berlalu, parser akan melihat dua langkah ke depan untuk memilih aturan yang akan diikuti. Pemeriksaan ini akan dilakukan sekali lagi, tetapi tata bahasa ini dapat ditulis ulang untuk mengecualikan lookahead . Kelemahannya adalah pengoptimalan seperti itu merusak struktur, sedangkan peningkatan kinerja agak kecil.
Ada cara yang lebih kompleks untuk memecahkan ketidakpastian semacam ini. Misalnya, Predikat sintaks (SynPred) mekanisme di ANTLR3 . Ini membantu ketika akhir opsional klausa melintasi awal klausa opsional berikutnya.
Dalam hal ANTLR3, predikat adalah metode yang dihasilkan yang melakukan entri teks virtual sesuai dengan salah satu alternatif . Ketika berhasil, ia mengembalikan true nilai, dan penyelesaian predikat berhasil. Jika ini adalah entri virtual, ini disebut mundur entri modus. Jika predikat berhasil, entri yang sebenarnya terjadi.
Itu hanya masalah ketika sebuah predikat dimulai di dalam predikat lain. Maka satu jarak mungkin dilewati ratusan atau ribuan kali.
Mari kita tinjau contoh yang disederhanakan. Ada tiga titik ketidakpastian:(A, B, C).
- Pengurai memasuki A, mengingat posisinya dalam teks, memulai entri virtual level-1.
- Pengurai memasuki B, mengingat posisinya dalam teks, memulai entri virtual level-2.
- Pengurai memasuki C, mengingat posisinya dalam teks, memulai entri virtual level-3.
- Pengurai menyelesaikan entri virtual level-3, kembali ke level-2, dan melewati C sekali lagi.
- Pengurai menyelesaikan entri virtual level-2, kembali ke level-1, dan melewati B dan C sekali lagi.
- Pengurai menyelesaikan entri virtual, mengembalikan, dan melakukan entri nyata melalui A, B, dan C.
Akibatnya, semua pemeriksaan dalam C akan dilakukan 4 kali, dalam B – 3 kali, dalam A – 2 kali.
Tetapi bagaimana jika alternatif yang cocok ada di urutan kedua atau ketiga dalam daftar? Maka salah satu tahap predikat akan gagal. Posisinya dalam teks akan mundur, dan predikat lain akan mulai berjalan.
Saat menganalisis alasan pembekuan aplikasi, kami sering menemukan jejak SynPred dieksekusi beberapa ribu kali. SynPred s sangat bermasalah dalam aturan rekursif. Sayangnya, SQL bersifat rekursif. Kemampuan untuk menggunakan subquery hampir di mana-mana memiliki harganya sendiri. Namun, dimungkinkan untuk memanipulasi aturan untuk menghilangkan predikat.
SynPred merusak kinerja. Pada titik tertentu, jumlah mereka berada di bawah kendali yang ketat. Tetapi masalahnya adalah ketika Anda menulis kode tata bahasa, SynPred mungkin tampak tidak jelas bagi Anda. Lebih dari itu, mengubah satu aturan dapat menyebabkan SynPred muncul di aturan lain, dan itu membuat kontrol atas aturan tersebut hampir tidak mungkin.
Kami membuat ekspresi reguler sederhana alat untuk mengontrol jumlah predikat yang dijalankan oleh Tugas MSBuild khusus . Jika jumlah predikat tidak cocok dengan jumlah yang ditentukan dalam file, tugas akan segera gagal membangun dan memperingatkan tentang kesalahan.
Saat melihat kesalahan, pengembang harus menulis ulang kode aturan beberapa kali untuk menghapus predikat yang berlebihan. Jika seseorang tidak dapat menghindari predikat, pengembang akan menambahkannya ke file khusus yang menarik perhatian ekstra untuk peninjauan.
Pada kesempatan langka, kami bahkan menulis predikat kami menggunakan C# hanya untuk menghindari predikat yang dihasilkan ANTLR. Untungnya, metode ini juga ada.
Warisan tata bahasa
Ketika ada perubahan pada DBMS kami yang didukung, kami harus menemukannya di alat kami. Dukungan untuk konstruksi sintaksis tata bahasa selalu menjadi titik awal.
Kami membuat tata bahasa khusus untuk setiap dialek SQL. Ini memungkinkan beberapa pengulangan kode, tetapi lebih mudah daripada mencoba menemukan kesamaan mereka.
Kami mulai menulis preprosesor tata bahasa ANTLR kami sendiri yang melakukan pewarisan tata bahasa.
Juga menjadi jelas bahwa kami membutuhkan mekanisme untuk polimorfisme – kemampuan untuk tidak hanya mendefinisikan ulang aturan di turunan tetapi juga memanggil aturan dasar. Kami juga ingin mengontrol posisi saat memanggil aturan dasar.
Alat adalah nilai tambah yang pasti ketika kami membandingkan ANTLR dengan alat pengenalan bahasa lainnya, Visual Studio, dan ANTLRWorks. Dan Anda tidak ingin kehilangan keuntungan ini saat menerapkan warisan. Solusinya adalah menentukan tata bahasa dasar dalam tata bahasa yang diwariskan dalam format komentar ANTLR. Untuk alat ANTLR, ini hanya komentar, tetapi kami dapat mengekstrak semua informasi yang diperlukan darinya.
Kami menulis Tugas MsBuild yang disematkan ke dalam sistem whole-build sebagai pre-build-action. Tugasnya adalah melakukan pekerjaan preprosesor untuk tata bahasa ANTLR dengan menghasilkan tata bahasa yang dihasilkan dari basisnya dan mewarisi rekan-rekan. Tata bahasa yang dihasilkan diproses oleh ANTLR sendiri.
Pemrosesan pasca ANTLR
Dalam banyak bahasa pemrograman, Kata Kunci tidak dapat digunakan sebagai nama subjek. Ada bisa dari 800 hingga 3000 kata kunci dalam SQL tergantung pada dialeknya. Kebanyakan dari mereka terikat dengan konteks di dalam database. Dengan demikian, melarang mereka sebagai nama objek akan membuat pengguna frustrasi. Itu sebabnya SQL memiliki kata kunci yang dicadangkan dan tidak dicadangkan.
Anda tidak dapat memberi nama objek Anda sebagai kata yang dicadangkan (PILIH, DARI, dll.) tanpa mengutipnya, tetapi Anda dapat melakukannya untuk kata yang tidak dilindungi (CONVERSATION, AVAILABILITY, dll.). Interaksi ini membuat pengembangan parser menjadi lebih sulit.
Selama analisis leksikal, konteksnya tidak diketahui, tetapi pengurai sudah membutuhkan nomor yang berbeda untuk pengidentifikasi dan kata kunci. Itu sebabnya kami menambahkan postprocessing lain ke parser ANTLR. Itu menggantikan semua pemeriksaan pengenal yang jelas dengan memanggil metode khusus.
Metode ini memiliki pemeriksaan yang lebih detail. Jika entri memanggil pengidentifikasi, dan kami berharap pengidentifikasi terpenuhi dan seterusnya, maka semuanya baik-baik saja. Tetapi jika kata yang tidak dicadangkan adalah entri, kita harus memeriksanya kembali. Pemeriksaan ekstra ini meninjau pencarian cabang dalam konteks saat ini di mana kata kunci tanpa syarat ini dapat menjadi kata kunci. Jika tidak ada cabang seperti itu, itu dapat digunakan sebagai pengenal.
Secara teknis, masalah ini dapat diselesaikan dengan ANTLR tetapi keputusan ini tidak optimal. Cara ANTLR adalah dengan membuat aturan yang mencantumkan semua kata kunci tanpa syarat dan pengenal leksem. Selanjutnya, aturan khusus akan berfungsi sebagai ganti pengenal leksem. Solusi ini membuat pengembang tidak lupa untuk menambahkan kata kunci yang digunakan dan dalam aturan khusus. Selain itu, ini mengoptimalkan waktu yang dihabiskan.
Kesalahan dalam analisis sintaks tanpa pohon
Pohon sintaks biasanya merupakan hasil kerja parser. Ini adalah struktur data yang mencerminkan teks program melalui tata bahasa formal. Jika Anda ingin menerapkan editor kode dengan pelengkapan otomatis bahasa, kemungkinan besar Anda akan mendapatkan algoritme berikut:
- Mengurai teks dalam editor. Kemudian Anda mendapatkan pohon sintaks.
- Temukan simpul di bawah carriage dan cocokkan dengan tata bahasa.
- Cari tahu kata kunci dan jenis objek mana yang akan tersedia di Point.
Dalam hal ini, tata bahasa mudah dibayangkan sebagai Grafik atau Mesin Negara.
Sayangnya, hanya ANTLR versi ketiga yang tersedia ketika dbForge IDE telah memulai pengembangannya. Namun, itu tidak gesit dan meskipun Anda dapat memberi tahu ANTLR cara membuat pohon, penggunaannya tidak mulus.
Selain itu, banyak artikel tentang topik ini menyarankan penggunaan mekanisme 'tindakan' untuk menjalankan kode saat parser melewati aturan. Mekanisme ini sangat berguna, tetapi telah menyebabkan masalah arsitektur dan membuat fungsi pendukung baru menjadi lebih kompleks.
Masalahnya, satu file tata bahasa mulai mengumpulkan 'tindakan' karena banyaknya fungsi yang seharusnya didistribusikan ke build yang berbeda. Kami berhasil mendistribusikan pengendali tindakan ke berbagai build dan membuat variasi pola pemberi notifikasi yang licik untuk ukuran tersebut.
ANTLR3 bekerja 6 kali lebih cepat dari ANTLR4 menurut pengukuran kami. Selain itu, pohon sintaks untuk skrip besar dapat memakan terlalu banyak RAM, yang bukan merupakan kabar baik, jadi kami perlu beroperasi dalam ruang alamat 32-bit Visual Studio dan SQL Management Studio.
Pemrosesan parser ANTLR
Saat bekerja dengan string, salah satu momen paling kritis adalah tahap analisis leksikal di mana kita membagi naskah menjadi kata-kata terpisah.
ANTLR mengambil tata bahasa sebagai input yang menentukan bahasa dan mengeluarkan parser dalam salah satu bahasa yang tersedia. Pada titik tertentu, parser yang dihasilkan tumbuh sedemikian rupa sehingga kami takut untuk men-debug-nya. Jika Anda menekan F11 (melangkah ke) saat men-debug dan membuka file parser, Visual Studio hanya akan mogok.
Ternyata gagal karena pengecualian OutOfMemory saat menganalisis file parser. File ini berisi lebih dari 200.000 baris kode.
Tetapi men-debug parser adalah bagian penting dari proses kerja, dan Anda tidak dapat mengabaikannya. Dengan bantuan kelas parsial C#, kami menganalisis parser yang dihasilkan menggunakan ekspresi reguler dan membaginya menjadi beberapa file. Visual Studio bekerja dengan sempurna dengannya.
Analisis leksikal tanpa substring sebelum Span API
Tugas utama analisis leksikal adalah klasifikasi – mendefinisikan batas-batas kata dan memeriksanya terhadap kamus. Jika kata ditemukan, lexer akan mengembalikan indeksnya. Jika tidak, kata tersebut dianggap sebagai pengenal objek. Ini adalah deskripsi algoritme yang disederhanakan.
Lexing latar belakang selama pembukaan file
Penyorotan sintaks didasarkan pada analisis leksikal. Operasi ini biasanya memakan waktu lebih lama dibandingkan dengan membaca teks dari disk. Apa tangkapannya? Dalam satu utas, teks sedang dibaca dari file, sedangkan analisis leksikal dilakukan di utas yang berbeda.
Lexer membaca teks baris demi baris. Jika meminta baris yang tidak ada, itu akan berhenti dan menunggu.
BlockingCollection
- Membaca dari file adalah Produser, sedangkan lexer adalah Konsumen.
- Lexer sudah menjadi Produser, dan editor teksnya adalah Konsumen.
Kumpulan trik ini memungkinkan kami untuk secara signifikan mempersingkat waktu yang dihabiskan untuk membuka file besar. Halaman pertama dokumen ditampilkan dengan sangat cepat, namun, dokumen dapat membeku jika pengguna mencoba pindah ke akhir file dalam beberapa detik pertama. Hal ini terjadi karena pembaca latar belakang dan lexer harus mencapai akhir dokumen. Namun, jika pengguna bekerja bergerak dari awal dokumen menuju akhir secara perlahan, tidak akan ada jeda yang terlihat.
Pengoptimalan ambigu:analisis leksikal parsial
Analisis sintaksis biasanya dibagi menjadi dua tingkatan:
- aliran karakter masukan diproses untuk mendapatkan leksem (token) berdasarkan kaidah bahasa – ini disebut analisis leksikal
- pengurai menggunakan aliran token yang memeriksanya sesuai dengan aturan tata bahasa formal dan sering membuat pohon sintaksis.
Pemrosesan string adalah operasi yang mahal. Untuk mengoptimalkannya, kami memutuskan untuk tidak melakukan analisis leksikal teks secara penuh setiap saat, tetapi hanya menganalisis kembali bagian yang diubah. Tapi bagaimana menangani konstruksi multiline seperti komentar blok atau baris? Kami menyimpan status akhir baris untuk setiap baris:“tidak ada token multiline” =0, “awal komentar blok” =1, “awal literal string multibaris” =2. Analisis leksikal dimulai dari bagian yang diubah dan berakhir ketika status akhir baris sama dengan status yang disimpan.
Ada satu masalah dengan solusi ini:sangat merepotkan untuk memantau nomor baris dalam struktur seperti itu sementara nomor baris adalah atribut yang diperlukan dari token ANTLR karena ketika sebuah baris dimasukkan atau dihapus, nomor baris berikutnya harus diperbarui sesuai dengan itu. Kami menyelesaikannya dengan menetapkan nomor baris segera, sebelum menyerahkan token ke parser. Pengujian yang kami lakukan kemudian menunjukkan bahwa kinerja meningkat sebesar 15-25%. Peningkatan yang sebenarnya bahkan lebih besar.
Jumlah RAM yang dibutuhkan untuk semua ini ternyata jauh lebih banyak dari yang kami harapkan. Token ANTLR terdiri dari:titik awal – 8 byte, titik akhir – 8 byte, tautan ke teks kata – 4 atau 8 byte (tidak menyebutkan string itu sendiri), tautan ke teks dokumen – 4 atau 8 byte, dan jenis token – 4 byte.
Jadi apa yang bisa kita simpulkan? Kami fokus pada kinerja dan mendapatkan konsumsi RAM yang berlebihan di tempat yang tidak kami duga. Kami tidak menganggap ini akan terjadi karena kami mencoba menggunakan struktur ringan alih-alih kelas. Dengan menggantinya dengan benda berat, kami secara sadar mengeluarkan biaya memori tambahan untuk mendapatkan kinerja yang lebih baik. Untungnya, ini memberi kami pelajaran penting, jadi sekarang setiap pengoptimalan kinerja diakhiri dengan pembuatan profil konsumsi memori dan sebaliknya.
Ini adalah cerita dengan moral. Beberapa fitur mulai bekerja hampir seketika dan yang lainnya hanya sedikit lebih cepat. Lagi pula, tidak mungkin melakukan trik analisis leksikal latar belakang jika tidak ada objek di mana salah satu utas dapat menyimpan token.
Semua masalah lebih lanjut terungkap dalam konteks pengembangan desktop di tumpukan .NET.
Masalah 32-bit
Beberapa pengguna memilih untuk menggunakan versi mandiri dari produk kami. Lainnya tetap bekerja di dalam Visual Studio dan SQL Server Management Studio. Banyak ekstensi dikembangkan untuk mereka. Salah satu ekstensi ini adalah SQL Complete. Untuk memperjelas, ini memberikan lebih banyak kekuatan dan fitur daripada SSMS dan VS Penyelesaian Kode standar untuk SQL.
Parsing SQL adalah proses yang sangat mahal, baik dalam hal sumber daya CPU dan RAM. Untuk meminta daftar objek dalam skrip pengguna, tanpa panggilan yang tidak perlu ke server, kami menyimpan cache objek di RAM. Seringkali, ini tidak memakan banyak ruang, tetapi beberapa pengguna kami memiliki database yang berisi hingga seperempat juta objek.
Bekerja dengan SQL sangat berbeda dari bekerja dengan bahasa lain. Di C#, praktis tidak ada file bahkan dengan ribuan baris kode. Sementara itu, dalam SQL seorang pengembang dapat bekerja dengan database dump yang terdiri dari beberapa juta baris kode. Tidak ada yang aneh dengan itu.
DLL-Neraka di dalam VS
Ada alat yang berguna untuk mengembangkan plugin di .NET Framework, ini adalah domain aplikasi. Semuanya dilakukan dengan cara yang terisolasi. Hal ini dimungkinkan untuk membongkar. Sebagian besar, implementasi ekstensi mungkin merupakan alasan utama mengapa domain aplikasi diperkenalkan.
Juga, ada Kerangka MAF, yang dirancang oleh MS untuk memecahkan masalah pembuatan add-on untuk program. Ini mengisolasi pengaya ini sedemikian rupa sehingga dapat mengirimnya ke proses terpisah dan mengambil alih semua komunikasi. Terus terang, solusi ini terlalu rumit dan belum mendapatkan banyak popularitas.
Sayangnya, Microsoft Visual Studio dan SQL Server Management Studio yang dibangun di atasnya, menerapkan sistem ekstensi secara berbeda. Ini menyederhanakan akses aplikasi hosting untuk plugin, tetapi memaksa mereka untuk cocok bersama dalam satu proses dan domain dengan yang lain.
Sama seperti aplikasi lain di abad ke-21, aplikasi kami memiliki banyak ketergantungan. Sebagian besar dari mereka adalah perpustakaan yang terkenal, terbukti waktu, dan populer di dunia .NET.
Menarik pesan ke dalam kunci
Tidak diketahui secara luas bahwa .NET Framework akan memompa Windows Message Queue di dalam setiap WaitHandle. Untuk meletakkannya di dalam setiap kunci, setiap pengendali dari setiap peristiwa dalam aplikasi dapat dipanggil jika kunci ini memiliki waktu untuk beralih ke mode kernel, dan tidak dilepaskan selama fase spin-wait.
Hal ini dapat mengakibatkan masuk kembali di beberapa tempat yang sangat tidak terduga. Beberapa kali hal itu menyebabkan masalah seperti "Koleksi diubah selama enumerasi" dan berbagai ArgumentOutOfRangeException.
Menambahkan rakitan ke solusi menggunakan SQL
Ketika proyek berkembang, tugas menambahkan rakitan, yang awalnya sederhana, berkembang menjadi selusin langkah rumit. Suatu kali, kami harus menambahkan selusin rakitan berbeda ke solusi, kami melakukan refactoring besar. Hampir 80 solusi, termasuk produk dan pengujian, dibuat berdasarkan sekitar 300 proyek .NET.
Berdasarkan solusi produk, kami menulis file Inno Setup. Mereka menyertakan daftar rakitan yang dikemas dalam instalasi yang diunduh pengguna. Algoritme penambahan proyek adalah sebagai berikut:
- Buat proyek baru.
- Tambahkan sertifikat ke dalamnya. Siapkan tag build.
- Tambahkan file versi.
- Konfigurasikan ulang jalur tujuan proyek.
- Ganti nama folder agar sesuai dengan spesifikasi internal.
- Tambahkan proyek ke solusi sekali lagi.
- Tambahkan beberapa rakitan yang perlu ditautkan ke semua proyek.
- Tambahkan build ke semua solusi yang diperlukan:pengujian dan produk.
- Untuk semua solusi produk, tambahkan rakitan ke instalasi.
9 langkah ini harus diulang sekitar 10 kali. Langkah 8 dan 9 tidak terlalu sepele, dan mudah lupa untuk menambahkan build di mana-mana.
Dihadapkan dengan tugas yang begitu besar dan rutin, setiap programmer normal pasti ingin mengotomatiskannya. Itulah yang ingin kami lakukan. Tetapi bagaimana kita menunjukkan solusi dan instalasi mana yang tepat untuk ditambahkan ke proyek yang baru dibuat? Ada begitu banyak skenario dan terlebih lagi, sulit untuk memprediksi beberapa di antaranya.
Kami datang dengan ide gila. Solusi terhubung dengan proyek seperti banyak-ke-banyak, proyek dengan instalasi dengan cara yang sama, dan SQL dapat menyelesaikan jenis tugas yang kita miliki.
Kami membuat .Net Core Console App yang memindai semua file .sln di folder sumber, mengambil daftar proyek dari mereka dengan bantuan DotNet CLI, dan meletakkannya di database SQLite. Program ini memiliki beberapa mode:
- Baru – membuat proyek dan semua folder yang diperlukan, menambahkan sertifikat, menyiapkan tag, menambahkan versi, rakitan esensial minimum.
- Tambah-Proyek – menambahkan proyek ke semua solusi yang memenuhi kueri SQL yang akan diberikan sebagai salah satu parameter. Untuk menambahkan proyek ke solusi, program di dalamnya menggunakan DotNet CLI.
- Add-ISS – menambahkan proyek ke semua instalasi, yang memenuhi kueri SQL.
Meskipun ide untuk menunjukkan daftar solusi melalui kueri SQL mungkin tampak rumit, itu benar-benar menutup semua kasus yang ada dan kemungkinan besar kasus yang mungkin terjadi di masa mendatang.
Biarkan saya mendemonstrasikan skenarionya. Buat proyek “A” dan tambahkan ke semua solusi di mana proyek “B” digunakan:
dbforgeasm add-project Folder1\Folder2\A "SELECT s.Id FROM Projects p JOIN Solutions s ON p.SolutionId = s.Id WHERE p.Name = 'B'"
Masalah dengan LiteDB
Beberapa tahun yang lalu, kami mendapat tugas untuk mengembangkan fungsi latar belakang untuk menyimpan dokumen pengguna. Itu memiliki dua alur aplikasi utama:kemampuan untuk menutup IDE secara instan dan keluar, dan setelah kembali untuk memulai dari tempat Anda tinggalkan, dan kemampuan untuk memulihkan dalam situasi mendesak seperti pemadaman atau crash program.
Untuk mengimplementasikan tugas ini, perlu menyimpan konten file di suatu tempat di samping, dan melakukannya dengan sering dan cepat. Selain konten, beberapa metadata perlu disimpan, yang membuat penyimpanan langsung di sistem file tidak nyaman.
Pada saat itu, kami menemukan perpustakaan LiteDB, yang membuat kami terkesan dengan kesederhanaan dan kinerjanya. LiteDB adalah basis data tertanam yang cepat dan ringan, yang seluruhnya ditulis dalam C#. Kecepatan dan kesederhanaan keseluruhan membuat kami menang.
Selama proses pengembangan, seluruh tim puas bekerja dengan LiteDB. Masalah utama, bagaimanapun, dimulai setelah rilis.
Dokumentasi resmi menjamin bahwa database memastikan pekerjaan yang tepat dengan akses bersamaan dari banyak utas serta beberapa proses. Pengujian sintetis yang agresif menunjukkan bahwa database tidak bekerja dengan benar di lingkungan multithread.
Untuk memperbaiki masalah dengan cepat, kami menyinkronkan proses dengan bantuan ReadWriteLock antarproses yang ditulis sendiri. Sekarang, setelah hampir tiga tahun, LiteDB bekerja jauh lebih baik.
StreamStringList
Masalah ini adalah kebalikan dari kasus dengan analisis leksikal parsial. Saat kita bekerja dengan teks, akan lebih mudah untuk bekerja dengannya sebagai daftar string. String dapat diminta dalam urutan acak, tetapi kepadatan akses memori tertentu masih ada. Pada titik tertentu, perlu menjalankan beberapa tugas untuk memproses file yang sangat besar tanpa beban memori penuh. Idenya adalah sebagai berikut:
- Untuk membaca file baris demi baris. Ingat offset dalam file.
- Atas permintaan, terbitkan baris berikutnya, setel offset yang diperlukan, dan kembalikan datanya.
Tugas utama selesai. Struktur ini tidak memakan banyak ruang dibandingkan dengan ukuran file. Pada tahap pengujian, kami benar-benar memeriksa jejak memori untuk file besar dan sangat besar. File besar diproses untuk waktu yang lama, dan file kecil akan segera diproses.
Tidak ada referensi untuk memeriksa waktu eksekusi . RAM disebut Random Access Memory – ini adalah keunggulan kompetitifnya dibandingkan SSD dan terutama di atas HDD. Driver ini mulai bekerja dengan buruk untuk akses acak. Ternyata pendekatan ini memperlambat pekerjaan hampir 40 kali lipat, dibandingkan dengan memuat file sepenuhnya ke dalam memori. Selain itu, kami membaca file 2,5 -10 kali penuh tergantung konteksnya.
Solusinya sederhana, dan peningkatannya cukup sehingga operasi hanya akan memakan waktu sedikit lebih lama daripada saat file dimuat penuh ke dalam memori.
Demikian juga, konsumsi RAM juga tidak signifikan. Kami menemukan inspirasi dalam prinsip memuat data dari RAM ke dalam prosesor cache. Saat Anda mengakses elemen array, prosesor menyalin lusinan elemen tetangga ke cache-nya karena elemen yang diperlukan sering kali berada di dekat Anda.
Banyak struktur data menggunakan pengoptimalan prosesor ini untuk mendapatkan kinerja terbaik. Karena kekhasan inilah akses acak ke elemen array jauh lebih lambat daripada akses sekuensial. Kami menerapkan mekanisme serupa:kami membaca satu set seribu string dan mengingat offsetnya. Ketika kita mengakses string ke-1001, kita melepaskan 500 string pertama dan memuat 500 string berikutnya. Jika kita membutuhkan salah satu dari 500 baris pertama, maka kita melakukannya secara terpisah, karena kita sudah memiliki offsetnya.
Pemrogram tidak perlu dengan hati-hati merumuskan dan memeriksa persyaratan non-fungsional. Akibatnya, kami ingat untuk kasus di masa mendatang, bahwa kami perlu bekerja secara berurutan dengan memori persisten.
Menganalisis pengecualian
Anda dapat mengumpulkan data aktivitas pengguna dengan mudah di web. Namun, tidak demikian halnya dengan menganalisis aplikasi desktop. Tidak ada alat seperti itu yang mampu memberikan serangkaian metrik dan alat visualisasi yang luar biasa seperti Google Analytics. Mengapa? Berikut asumsi saya adalah:
- Sepanjang sebagian besar sejarah pengembangan aplikasi desktop, mereka tidak memiliki akses yang stabil dan permanen ke Web.
- Ada banyak alat pengembangan untuk aplikasi desktop. Oleh karena itu, mustahil untuk membangun alat pengumpulan data pengguna multiguna untuk semua kerangka kerja dan teknologi UI.
Aspek kunci dari pengumpulan data adalah untuk melacak pengecualian. Misalnya, kami mengumpulkan data tentang kerusakan. Sebelumnya, pengguna kami harus menulis sendiri ke email dukungan pelanggan, menambahkan Jejak Tumpukan kesalahan, yang disalin dari jendela aplikasi khusus. Beberapa pengguna mengikuti semua langkah ini. Data yang dikumpulkan benar-benar dianonimkan, yang membuat kami kehilangan kesempatan untuk mengetahui langkah-langkah reproduksi atau informasi lain apa pun dari pengguna.
Di sisi lain, data kesalahan ada di database Postgres, dan ini membuka jalan untuk pemeriksaan instan dari lusinan hipotesis. Anda bisa langsung mendapatkan jawabannya hanya dengan membuat query SQL ke database. Seringkali tidak jelas dari hanya satu tumpukan atau jenis pengecualian bagaimana pengecualian terjadi, itulah sebabnya semua informasi ini sangat penting untuk mempelajari masalah.
Selain itu, Anda memiliki kesempatan untuk menganalisis semua data yang dikumpulkan dan menemukan modul dan kelas yang paling bermasalah. Dengan mengandalkan hasil analisis, Anda dapat merencanakan pemfaktoran ulang atau pengujian tambahan untuk mencakup bagian-bagian program ini.
Layanan dekode tumpukan
Build .NET berisi kode IL, yang dapat dengan mudah diubah kembali menjadi kode C#, akurat untuk operator, menggunakan beberapa program khusus. Salah satu cara untuk melindungi kode program adalah pengaburannya. Program dapat diganti namanya; metode, variabel, dan kelas dapat diganti; kode dapat diganti dengan padanannya, tetapi itu benar-benar tidak dapat dipahami.
Kebutuhan untuk mengaburkan kode sumber muncul ketika Anda mendistribusikan produk Anda dengan cara yang menunjukkan bahwa pengguna mendapatkan build aplikasi Anda. Aplikasi desktop adalah kasusnya. Semua build, termasuk build menengah untuk penguji, disamarkan dengan cermat.
Unit Jaminan Kualitas kami menggunakan alat tumpukan dekode dari pengembang obfuscator. Untuk memulai decoding, mereka perlu menjalankan aplikasi, menemukan peta deobfuscation yang diterbitkan oleh CI untuk build tertentu, dan memasukkan tumpukan pengecualian ke dalam kolom input.
Versi dan editor yang berbeda dikaburkan dengan cara yang berbeda, yang menyulitkan pengembang untuk mempelajari masalah atau bahkan dapat menempatkannya di jalur yang salah. Jelas bahwa proses ini harus diotomatisasi.
Format peta deobfuscation ternyata cukup mudah. Kami dengan mudah menguraikannya dan menulis program decoding tumpukan. Sesaat sebelum itu, UI web dikembangkan untuk merender pengecualian menurut versi produk dan mengelompokkannya menurut tumpukan. Itu adalah situs web .NET Core dengan database di SQLite.
SQLite adalah alat yang rapi untuk solusi kecil. Kami mencoba untuk menempatkan peta deobfuscation di sana juga. Setiap build menghasilkan sekitar 500 ribu pasangan enkripsi &dekripsi. SQLite tidak dapat menangani tingkat penyisipan yang agresif seperti itu.
Sementara data pada satu build dimasukkan ke dalam database, dua lagi ditambahkan ke antrian. Tidak lama sebelum masalah itu, saya mendengarkan laporan tentang Clickhouse dan sangat ingin mencobanya. Ini terbukti sangat baik, tingkat penyisipan dipercepat lebih dari 200 kali.
Meskipun demikian, decoding tumpukan (membaca dari database) melambat hampir 50 kali lipat, tetapi karena setiap tumpukan membutuhkan waktu kurang dari 1 md, menghabiskan waktu mempelajari masalah ini menjadi tidak hemat biaya.
ML.NET for classification of exceptions
On the subject of the automatic processing of exceptions, we made a few more enhancements.
We already had the Web-UI for a convenient review of exceptions grouped by stacks. We had a Grafana for high-level analysis of product stability at the level of versions and product lines. But a programmer’s eye, constantly craving optimization, caught another aspect of this process.
Historically, dbForge line development was divided among 4 teams. Each team had its own functionality to work on, though the borderline was not always obvious. Our technical support team, relying on their experience, read the stack and assigned it to this or that team. They managed it quite well, yet, in some cases, mistakes occurred. The information on errors from analytics came to Jira on its own, but the support team still needed to classify tasks by team.
In the meantime, Microsoft introduced a new library – ML.NET. And we still had this classification task. A library of that kind was exactly what we needed. It extracted stacks of all resolved exceptions from Redmine, a project management system that we used earlier, and Jira, which we use at present.
We obtained a data array that contained some 5 thousand pairs of Exception StackTrace and command. We put 100 exceptions aside and used the rest of the exceptions to teach a model. The accuracy was about 75%. Again, we had 4 teams, hence, random and round-robin would only attain 25%. It sufficiently saved up their time.
To my way of thinking, if we considerably clean up incoming data array, make a thorough examination of the ML.NET library, and theoretical foundation in machine learning, on the whole, we can improve these results. At the same time, I was impressed with the simplicity of this library:with no special knowledge in AI and ML, we managed to gain real cost-benefits in less than an hour.
Conclusion
Hopefully, some of the readers happen to be users of the products I describe in this article, and some lines shed light on the reasons why this or that function was implemented this way.
And now, let me conclude:
We should make decisions based on data and not assumptions. It is about behavior analytics and insights that we can obtain from it.
We ought to constantly invest in tools. There is nothing wrong if we need to develop something for it. In the next few months, it will save us a lot of time and rid us of routine. Routine on top of time expenditure can be very demotivating.
When we develop some internal tools, we get a super chance to try out new technologies, which can be applied in production solutions later on.
There are infinitely many tools for data analysis. Still, we managed to extract some crucial information using SQL tools. This is the most powerful tool to formulate a question to data and receive an answer in a structured form.