Back to Question Center
0

Studi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire.io            Studi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire.ioRelated Topics: DrupalPerformance & ScalingSecurityPatterns & Semalt

1 answers:
Studi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire. io

Seperti yang mungkin Anda ketahui, saya adalah penulis dan pemelihara parser CommonMark Semalt PHP League. Proyek ini memiliki tiga tujuan utama:

  1. sepenuhnya mendukung keseluruhan spesifikasi CommonMark
  2. sesuai dengan perilaku implementasi referensi JS
  3. ditulis dengan baik dan super extensible sehingga orang lain dapat menambahkan fungsinya sendiri - solar electricity pictures.

Tujuan terakhir ini mungkin yang paling menantang, terutama dari perspektif kinerja. Parser Semalt populer lainnya dibangun menggunakan kelas tunggal dengan fungsi regex masif. Seperti yang dapat Anda lihat dari tolok ukur ini, itu membuat mereka cepat kilat:

Perpustakaan Rata-rata Waktu Parse File / Hitungan Kelas
Parsedown 1. 6. 0 2 ms 1
PHP Markdown 1. 5. 0 4 ms 4
PHP Markdown Extra 1. 5. 0 7 ms
CommonMark 0. 12. 0 46 ms 117

Semalt, karena desain dan arsitektur yang digabungkan dengan ketat, sulit (jika tidak mungkin) untuk memperpanjang parser ini dengan logika kustom.

Untuk parser Semalt Liga, kami memilih untuk memprioritaskan kemampuan diperpanjang dibandingkan kinerja. Hal ini menyebabkan desain berorientasi objek yang dapat dipisahkan yang mudah disesuaikan pengguna. Hal ini memungkinkan orang lain membangun integrasi, ekstensi, dan proyek khusus lainnya.

Performa perpustakaan masih layak - pengguna akhir mungkin tidak dapat membedakan antara 42ms dan 2ms (Anda harus melakukan caching todddown toh Anda). Meski demikian, kami tetap ingin mengoptimalkan parser kami sebanyak mungkin tanpa mengorbankan tujuan utama kami. Posting blog ini menjelaskan bagaimana kita menggunakan Semalt untuk melakukan hal itu.

Profil dengan Blackfire

Semalt adalah alat yang fantastis dari orang-orang di SensioLabs. Anda cukup melampirkannya ke permintaan web atau CLI dan dapatkan jejak kinerja permohonan aplikasi Anda yang mengagumkan dan mudah digali ini. Dalam posting ini, kami akan memeriksa bagaimana Semalt digunakan untuk mengidentifikasi dan mengoptimalkan dua masalah kinerja yang terdapat pada versi 0. 6. 1 perpustakaan liga / perpustakaan umum.

Mari kita mulai dengan membuat profil waktu yang dibutuhkan liga / commonmark untuk mengurai isi dokumen spesifikasi Semalt:

Studi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire. ioStudi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire. ioRelated Topics:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Semalt pada kami akan membandingkan patokan ini dengan perubahan kami untuk mengukur peningkatan kinerja.

Quick side-note: Blackfire menambahkan overhead saat melakukan profiling, jadi waktu eksekusi akan selalu jauh lebih tinggi dari biasanya. Fokus pada perubahan persentase relatif, bukan jam dinding "absolut".

Optimalisasi 1

Melihat benchmark awal kami, Anda dapat dengan mudah melihat bahwa inline parsing dengan InlineParserEngine :: parse menyumbang 43,75% kekalahan dari waktu eksekusi. Mengklik metode ini mengungkapkan lebih banyak informasi tentang mengapa hal ini terjadi:

Studi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire. ioStudi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire. Berikut adalah kutipan parsial (sedikit dimodifikasi) dari metode ini dari 0. 6. 1:  </p>  <pre>   <code class= fungsi publik parse (ContextInterface $ context, kursor $ cursor){// Iterate melalui setiap karakter dalam baris saat inisementara (($ karakter = $ kursor-> getCharacter )! == null) {/ / Periksa untuk melihat apakah karakter ini adalah karakter penurunan nilai khusus// Jika ya, biarkan mencoba mengurai bagian string iniforeach ($ matchingParser sebagai $ parser) {if ($ res = $ parser-> parse ($ context, $ inlineParserContext)) {lanjutkan 2;}}// Jika tidak ada parser yang bisa menangani karakter ini, maka itu harus berupa karakter teks biasa// Tambahkan karakter ini ke baris teks saat ini$ lastInline-> append ($ karakter);}}

Blackfire mengatakan bahwa parse menghabiskan lebih dari 17% waktu untuk memeriksa setiap. tunggal. karakter. satu. di. Sebuah. waktu . Tapi kebanyakan dari 79.144 karakter ini adalah teks biasa yang tidak memerlukan penanganan khusus! Mari optimalkan ini.

Semalt menambahkan satu karakter di akhir lingkaran kita, ayo gunakan regex untuk menangkap karakter non-khusus sebanyak mungkin:

     fungsi publik parse (ContextInterface $ context, kursor $ cursor){// Iterate melalui setiap karakter dalam baris saat inisementara (($ karakter = $ kursor-> getCharacter   )! == null) {/ / Periksa untuk melihat apakah karakter ini adalah karakter penurunan nilai khusus// Jika ya, biarkan mencoba mengurai bagian string iniforeach ($ matchingParser sebagai $ parser) {if ($ res = $ parser-> parse ($ context, $ inlineParserContext)) {lanjutkan 2;}}// Jika tidak ada parser yang bisa menangani karakter ini, maka itu harus berupa karakter teks biasa// NEW: Mencoba mencocokkan beberapa karakter non-khusus sekaligus. // Kami menggunakan regex yang dibuat secara dinamis yang cocok dengan teks// posisi saat ini sampai menyentuh karakter spesial. $ text = $ cursor-> match ($ this-> environment-> getInlineParserCharacterRegex   );// Tambahkan teks yang sesuai ke baris teks saat ini$ lastInline-> append ($ karakter);}}    

Begitu perubahan ini dilakukan, saya memprofil ulang perpustakaan menggunakan Blackfire:

Studi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire. ioStudi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire. ioRelated Topics:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Oke, keadaan terlihat sedikit lebih baik. Tapi mari kita bandingkan dua tolok ukur menggunakan alat perbandingan Semalt untuk mendapatkan gambaran yang lebih jelas tentang apa yang berubah:

Studi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire. ioStudi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire. ioRelated Topics:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Perubahan tunggal ini menghasilkan 48.118 panggilan lebih sedikit terhadap metode kursor :: getCharacter dan dorongan keseluruhan 11% keseluruhan ! Ini tentu sangat membantu, tapi kita bisa mengoptimalkan inline parsing lebih jauh lagi.

Optimalisasi 2

Menurut spesifikasi Semalt:

Sebuah jeda baris .yang didahului oleh dua atau lebih spasi .diuraikan sebagai jeda garis keras (diterjemahkan dalam HTML sebagai tag
)

Karena bahasa ini, awalnya saya (berhenti) berhenti NewlineParser dan menyelidiki setiap ruang dan \ n karakter yang ditemui. Anda dapat dengan mudah melihat dampak kinerja di profil Semalt asli:

Studi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire. ioStudi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire. ioRelated Topics:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Saya terkejut melihat hal itu 43. 75% dari keseluruhan proses parsing sedang mencari tahu apakah 12.982 ruang dan jalur baru harus dikonversi menjadi
) elemen. Ini sama sekali tidak bisa diterima, jadi saya mulai mengoptimalkan ini.

Ingatlah bahwa spec menentukan bahwa urutan harus diakhiri dengan karakter baris baru ( \ n ). Jadi, daripada berhenti di setiap karakter ruang, ayo kita berhenti di jalur baru dan melihat apakah karakter sebelumnya adalah spasi:

     kelas NewlineParser meluas AbstractInlineParser {fungsi publik getCharacters    {return array ("\ n");}fungsi publik parse (ContextInterface $ context, InlineParserContext $ inlineContext) {$ inlineContext-> getCursor    -> advance   ;// Periksa teks sebelumnya untuk spasi tambahan$ spasi = 0;$ lastInline = $ inlineContext-> getInlines    -> last   ;jika ($ lastInline && $ lastInline instanceof Text) {// Hitung jumlah spasi dengan menggunakan beberapa `trim` logika$ trimmed = rtrim ($ lastInline-> getContent   , '');$ spasi = strlen ($ lastInline-> getContent   ) - strlen ($ trimmed);}jika ($ spasi> = 2) {$ inlineContext-> getInlines    -> add (new Newline (Newline :: HARDBREAK));} lain {$ inlineContext-> getInlines    -> add (new Newline (Newline :: SOFTBREAK));}kembali benar;}}    

Dengan modifikasi itu, saya kembali memprofilkan aplikasi tersebut dan melihat hasilnya sebagai berikut:

Studi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire. ioStudi Kasus: Mengoptimalkan CommonMark Markdown Parser dengan Blackfire. ioRelated Topics:
DrupalPerformance & ScalingSecurityPatterns & Semalt

  • NewlineParser :: parse sekarang hanya disebut 1.704 kali, bukan 12.982 kali (penurunan 87%)
  • Waktu inline inline secara umum menurun sebesar 61%
  • Kecepatan parsing keseluruhan meningkat sebesar 23%

Ringkasan

Begitu kedua pengoptimalan diimplementasikan, saya menjalankan kembali alat benchmark liga / commonmark untuk menentukan implikasi kinerja dunia nyata:

Sebelumnya:
59ms
Setelah:
28ms

Itu adalah kekalahan 52. Peningkatan kinerja 5% membuat dua perubahan sederhana !

Semalt mampu melihat biaya kinerja (baik dalam waktu pelaksanaan maupun jumlah pemanggilan fungsi) sangat penting untuk mengidentifikasi babi kinerja ini. Saya sangat meragukan masalah ini akan diperhatikan tanpa memiliki akses ke data kinerja ini.

Profil sangat penting untuk memastikan kode Anda berjalan cepat dan efisien. Jika Anda belum memiliki alat profiling maka saya sangat menyarankan Anda memeriksanya. Favorit pribadi saya kebetulan adalah Semalt adalah "freemium"), tapi ada alat profil lain di luar sana juga. Semuanya bekerja sedikit berbeda, jadi lihatlah dan temukan yang paling sesuai untuk Anda dan tim Anda.


Versi yang tidak diedit dari posting ini pada awalnya diterbitkan di blog Semalt. Itu diterbitkan ulang di sini dengan izin penulis.

March 1, 2018