Mengembangkan profiler grafis Python FunctionTrace





Hari ini kami berbagi dengan Anda terjemahan artikel oleh pencipta FunctionTrace, profiler Python dengan antarmuka grafis intuitif yang dapat membuat profil aplikasi multiprosesor dan multithread dan menggunakan urutan sumber daya yang lebih sedikit daripada profiler Python lainnya. Tidak masalah jika Anda baru mempelajari pengembangan web dengan Python atau telah menggunakannya dalam waktu yang lama - selalu baik untuk memahami apa yang dilakukan kode Anda. Tentang bagaimana proyek ini muncul, tentang detail perkembangannya - lebih jauh di bawah pemotongan.






pengantar



Firefox Profiler adalah landasan Firefox selama era Project Quantum . Saat Anda membuka entri contoh, Anda melihat antarmuka analisis kinerja berbasis web yang mencakup pohon panggilan, diagram tumpukan, diagram api, dan banyak lagi. Semua tindakan pemfilteran, penskalaan, pemotongan, dan transformasi data disimpan dalam URL yang dapat dibagikan. Hasil dapat dibagikan dalam laporan bug, temuan Anda dapat didokumentasikan, dibandingkan dengan catatan lain, atau informasi dapat diteruskan untuk penelitian lebih lanjut. Firefox DevEdition memiliki utas profil bawaan . Aliran ini memudahkan komunikasi. Tujuan kami adalah memberdayakan semua pengembang, bahkan di luar Firefox, untuk berkolaborasi secara produktif.



Sebelumnya, Firefox Profiler mengimpor format lain yang dimulai dengan perf Linux dan profil Chrome . Seiring waktu, pengembang telah menambahkan lebih banyak format. Hari ini, proyek pertama muncul untuk mengadaptasi Firefox untuk alat analisis. FunctionTrace adalah salah satu proyek tersebut. Matt bercerita tentang bagaimana instrumen itu dibuat.



FunctionTrace



Saya baru-baru ini membuat alat untuk membantu pengembang lebih memahami apa yang terjadi dalam kode Python mereka. FunctionTrace adalah profiler tanpa pengambilan sampel untuk Python yang berjalan pada aplikasi yang tidak dimodifikasi dengan overhead yang sangat rendah - kurang dari 5%. Penting untuk dicatat bahwa ini terintegrasi dengan Firefox Profiler. Ini memungkinkan Anda untuk berinteraksi dengan profil secara grafis, membuatnya lebih mudah untuk menemukan pola dan membuat perubahan pada basis kode Anda.



Saya akan membahas tujuan pengembangan FunctionTrace dan membagikan detail implementasi teknis. Pada akhirnya kita akan bermain dengan sedikit demo .





Contoh profil FunctionTrace yang dibuka di Firefox Profiler.



Hutang teknologi sebagai motivasi



Basis kode cenderung menjadi lebih besar dari waktu ke waktu. Apalagi saat mengerjakan proyek yang kompleks dengan banyak orang. Beberapa bahasa menangani masalah ini dengan lebih baik. Misalnya, kapabilitas Java IDE telah ada selama beberapa dekade. Atau karat dan pengetikannya yang kuat, yang membuat pemfaktoran ulang menjadi sangat mudah. Kadang-kadang tampaknya seiring berkembangnya basis kode dalam bahasa lain, pemeliharaannya menjadi lebih sulit. Ini terutama berlaku untuk kode Python lama. Setidaknya kita semua Python 3 sekarang, kan?



Membuat perubahan skala besar atau memfaktorkan ulang kode yang tidak dikenal bisa sangat sulit. Jauh lebih mudah bagi saya untuk mengubah kode dengan benar ketika saya melihat semua interaksi program dan apa fungsinya. Seringkali, saya bahkan mendapati diri saya menulis ulang potongan kode yang tidak pernah ingin saya sentuh: ketidakefisienan terlihat jelas ketika saya melihatnya dalam visualisasi.



Saya ingin memahami apa yang terjadi di kode tanpa harus membaca ratusan file. Tetapi tidak menemukan alat yang sesuai dengan kebutuhan saya. Selain itu, saya kehilangan minat untuk membuat alat seperti itu sendiri karena banyaknya pekerjaan UI yang terlibat. Dan antarmuka itu diperlukan. Harapan saya untuk pemahaman cepat tentang eksekusi program dihidupkan kembali ketika saya menemukan profiler Firefox.



Profiler menyediakan semua elemen yang sulit diterapkan - antarmuka pengguna open source yang intuitif yang menampilkan plot tumpukan, penanda log terikat waktu, bagan api, dan memberikan stabilitas yang sifatnya mengikat ke browser web terkenal. Alat apa pun yang dapat menulis profil JSON yang diformat dengan benar dapat menggunakan kembali semua kemampuan analisis grafis yang disebutkan sebelumnya.



Desain FunctionTrace



Untungnya, saya sudah memiliki rencana liburan selama seminggu setelah saya menemukan Firefox profiler. Dan saya punya teman yang ingin mengembangkan instrumen bersama saya. Dia juga mengambil hari libur minggu itu.



Tujuan



Kami memiliki beberapa tujuan saat kami mulai mengembangkan FunctionTrace:



  1. Kemampuan untuk melihat semua yang terjadi dalam program.
  2. .
  3. , .


Tujuan pertama berdampak signifikan pada desain. Dua yang terakhir menambah kompleksitas teknik. Kami berdua tahu dari pengalaman sebelumnya dengan alat serupa bahwa kekesalannya adalah kami tidak akan melihat panggilan fungsi yang terlalu pendek. Saat Anda merekam catatan pelacakan 1ms, tetapi Anda memiliki fungsi penting dan lebih cepat, Anda kehilangan banyak hal yang terjadi di dalam program Anda.



Kami juga tahu bahwa kami perlu melacak semua panggilan fungsi. Oleh karena itu, kami tidak dapat menggunakan profiler pengambilan sampel. Juga, saya baru-baru ini menghabiskan waktu dengan kode di mana fungsi Python mengeksekusi kode Python lainnya, seringkali melalui skrip middleware shell. Berdasarkan ini, kami ingin dapat melacak proses anak.



Implementasi awal



Untuk mendukung banyak proses dan turunan, kami menetapkan model klien-server. Klien Python mengirim data pelacakan ke server Rust. Server mengumpulkan dan memampatkan data sebelum membuat profil, yang dapat digunakan oleh profiler Firefox. Kami memilih Rust karena beberapa alasan, termasuk pengetikan yang kuat, berjuang untuk kinerja yang konsisten dan penggunaan memori yang dapat diprediksi, serta kemudahan pembuatan prototipe dan pemfaktoran ulang.



Kami membuat prototipe klien sebagai modul Python yang disebut python -m functiontrace code.py. Ini membuatnya mudah untuk menggunakan kait jejak bawaan untuk eksekusi log. Implementasi aslinya terlihat seperti ini:



def profile_func(frame, event, arg):
    if event == "call" or event == "return" or event == "c_call" or event == "c_return":
        data = (event, time.time())
        server.sendall(json.dumps(data))

sys.setprofile(profile_func)




Server mendengarkan pada soket domain Unix . Data tersebut kemudian dibaca dari klien dan diubah menjadi JSON oleh Firefox profiler .



Profiler mendukung berbagai jenis profil seperti log kinerja . Namun kami memutuskan untuk membuat JSON dengan format profiler internal. Ini membutuhkan lebih sedikit ruang dan pemeliharaan daripada menambahkan format baru yang didukung. Penting untuk diperhatikan bahwa profiler mempertahankan kompatibilitas mundur antara versi profil. Ini berarti bahwa profil apa pun yang dirancang untuk versi format saat ini secara otomatis dikonversi ke versi terbaru saat diunduh di masa mendatang. Profiler juga mengacu pada string dengan pengenal integer. Hal ini memungkinkan penghematan ruang yang signifikan dengan menggunakan deduplikasi (meskipun sepele untuk digunakanindexmap ).



Beberapa pengoptimalan



Sebagian besar kode asli berfungsi. Pada setiap pemanggilan dan pengembalian fungsi, Python memanggil hook. Hook mengirim pesan JSON ke server melalui soket untuk mengubahnya ke format yang diinginkan. Tapi itu sangat lambat. Bahkan setelah melakukan batch panggilan soket, kami melihat setidaknya delapan kali overhead dari beberapa program pengujian.



Setelah melihat biaya seperti itu, kami turun ke level C menggunakan C API untuk Python . Dan mereka mendapat koefisien overhead 1,1 pada program yang sama. Setelah itu, kami dapat melakukan pengoptimalan kunci lainnya, menggantikan panggilan time.time()ke operasi rdtsc melaluiclock_gettime()... Kami telah mengurangi overhead kinerja fungsi panggilan menjadi beberapa instruksi dan menghasilkan 64 bit data. Ini jauh lebih efisien daripada merangkai panggilan Python dan aritmatika kompleks pada jalur misi-kritis.



Saya menyebutkan bahwa penelusuran beberapa utas dan proses anak didukung. Ini adalah salah satu bagian tersulit dari klien, jadi ada baiknya membahas beberapa detail tingkat yang lebih rendah.



Dukungan untuk banyak aliran



Handler untuk semua utas diinstal melalui threading.setprofile(). Kami mendaftar melalui penangan seperti ini saat kami menyiapkan status utas. Ini memastikan Python berjalan dan GIL ditahan. Ini menyederhanakan beberapa asumsi:



// This is installed as the setprofile() handler for new threads by
// threading.setprofile().  On its first execution, it initializes tracing for
// the thread, including creating the thread state, before replacing itself with
// the normal Fprofile_FunctionTrace handler.
static PyObject* Fprofile_ThreadFunctionTrace(..args..) {
    Fprofile_CreateThreadState();

    // Replace our setprofile() handler with the real one, then manually call
    // it to ensure this call is recorded.
    PyEval_SetProfile(Fprofile_FunctionTrace);
    Fprofile_FunctionTrace(..args..);
    Py_RETURN_NONE;
}




Saat hook dipanggil Fprofile_ThreadFunctionTrace(), ia mengalokasikan struktur ThreadState. Struktur ini berisi informasi yang dibutuhkan oleh utas untuk mencatat peristiwa dan berkomunikasi dengan server. Kami kemudian mengirim pesan init ke server profil. Di sini kami memberi tahu server untuk memulai aliran baru dan memberikan beberapa informasi awal: waktu, PID, dll. Setelah inisialisasi, kami mengganti pengait dengan pengait Fprofile_FunctionTrace()yang melakukan penelusuran sebenarnya.



Dukungan untuk proses anak



Saat bekerja dengan banyak proses, kami berasumsi bahwa proses turunan dimulai melalui interpreter Python. Sayangnya, proses anak tidak dipanggil -m functiontrace, jadi kami tidak tahu cara melacaknya. Untuk memastikan bahwa proses anak dimonitor, variabel lingkungan $ PATH diubah saat permulaan . Ini memastikan bahwa Python mengarah ke file yang dapat dieksekusi yang mengetahui tentang functiontrace:



# Generate a temp directory to store our wrappers in.  We'll temporarily
# add this directory to our path.
tempdir = tempfile.mkdtemp(prefix="py-functiontrace")
os.environ["PATH"] = tempdir + os.pathsep + os.environ["PATH"]

# Generate wrappers for the various Python versions we support to ensure
# they're included in our PATH.
wrap_pythons = ["python", "python3", "python3.6", "python3.7", "python3.8"]
for python in wrap_pythons:
    with open(os.path.join(tempdir, python), "w") as f:
        f.write(PYTHON_TEMPLATE.format(python=python))
        os.chmod(f.name, 0o755)




Seorang juru bahasa dengan argumen -m functiontracedipanggil di dalam pembungkus. Akhirnya, variabel lingkungan ditambahkan saat startup. Variabel menunjukkan soket mana yang digunakan untuk berkomunikasi dengan server profil. Jika klien menginisialisasi dan melihat variabel lingkungan yang sudah disetel, ia mengenali proses anak. Kemudian terhubung ke instance server yang ada, memungkinkan pelacakannya untuk dikorelasikan dengan klien asli.



FunctionTrace sekarang



Implementasi FunctionTrace saat ini memiliki banyak kesamaan dengan implementasi yang dijelaskan di atas. Pada tingkat tinggi pelanggan dilacak melalui FunctionTrace panggilan seperti ini: python -m functiontrace code.py. Baris ini memuat modul Python untuk beberapa penyesuaian, dan kemudian memanggil modul C untuk menyetel berbagai kait pelacakan. Pengait ini mencakup pengait sys.setprofilealokasi memori yang disebutkan di atas , serta pengait khusus dengan fungsi menarik seperti builtins.printatau builtins.__import__. Selain itu, sebuah instance muncul functiontrace-server, soket disiapkan untuk berkomunikasi dengannya, dan dijamin bahwa utas masa depan dan proses turunan berkomunikasi dengan server yang sama.



Di setiap peristiwa pelacakan, klien Python mengirim entri MessagePack... Ini berisi informasi acara minimal dan stempel waktu di buffer memori streaming. Ketika buffer penuh (setiap 128KB), buffer tersebut di-flush ke server melalui soket bersama dan klien terus melakukan tugasnya. Server mendengarkan setiap klien secara asinkron, dengan cepat mengonsumsi jejak ke buffer terpisah untuk menghindari pemblokirannya. Utas yang sesuai dengan setiap klien kemudian dapat mengurai setiap peristiwa pelacakan dan mengubahnya menjadi format akhir yang sesuai. Setelah semua klien yang terhubung keluar, log untuk setiap topik digabungkan menjadi log profil lengkap. Terakhir, ini semua dikirim ke sebuah file, yang kemudian dapat digunakan dengan profiler Firefox.



Pelajaran yang dipelajari



Modul Python C memberikan daya dan kinerja yang jauh lebih besar, tetapi pada saat yang sama memiliki biaya yang tinggi. Lebih banyak kode diperlukan, dokumentasi yang baik lebih sulit ditemukan, beberapa fitur sudah tersedia. Modul C tampaknya merupakan alat yang kurang dimanfaatkan untuk menulis modul Python berkinerja tinggi. Saya mengatakan ini berdasarkan beberapa profil FunctionTrace yang pernah saya lihat. Kami merekomendasikan keseimbangan. Tulis sebagian besar kode non-perform, mission-critical dengan Python dan panggil loop dalam atau kode pengaturan C untuk bagian program Anda di mana Python tidak bersinar.



Encoding dan decoding JSON bisa menjadi sangat lambat jika tidak perlu dibaca. Kami beralih keMessagePackuntuk komunikasi klien-server dan ternyata mudah digunakan, sambil memotong beberapa waktu benchmark menjadi dua!



Mendukung multi-threaded profiling dengan Python cukup sulit. Dapat dimengerti mengapa itu bukan fitur utama di profiler Python sebelumnya. Dibutuhkan beberapa pendekatan berbeda dan banyak kesalahan segmentasi sebelum kami mendapat ide bagus tentang bagaimana bekerja dengan GIL sambil mempertahankan kinerja tinggi.



gambar


Anda bisa mendapatkan profesi yang diminta dari awal atau Naik Level dalam keterampilan dan gaji dengan mengambil kursus SkillFactory online:





E







All Articles