كيف يعمل المحللون في Ruby و Python؟

تم إعداد ترجمة المقال استعدادًا لبدء الدورة المتقدمة "مطور Python" .



يمكن قراءة المقال الأصلي هنا .










مرحبا! كمقبل للشهية لملف تعريف روبي ، أردت أن أتحدث عن كيفية عمل أدوات التعريف الحالية لروبي وبايثون. كما أنه سيساعد في الإجابة على السؤال الذي يسألني كثير من الناس: "كيف تكتب المحلل؟"



في هذه المقالة ، سنركز على ملفات تعريف المعالج (وليس ، دعنا نقول ، ملفات تعريف الذاكرة / الكومة). سوف أقوم بتغطية بعض الأساليب الأساسية لكتابة ملف التعريف ، وتقديم أمثلة التعليمات البرمجية وإظهار العديد من الأمثلة على المحترفين المشهورين في Ruby و Python ، كما سأوضح لك كيفية عملهم تحت الغطاء.



ربما تكون هناك أخطاء في المقالة (استعدادًا لكتابتها ، لقد بحثت جزئيًا في كود 14 مكتبة للتنميط ، ولم أكن على دراية بالعديد منها حتى الآن) ، لذا يرجى إعلامي إذا عثرت عليها ...



2 أنواع من بروفيلات



هناك نوعان رئيسيان من المحللون معالج - أخذ العينات و تتبع المحللون.



تسجل محولات التتبع كل استدعاء دالة في برنامجك ، وتقدم تقريرًا في النهاية. يأخذ المحللون في أخذ العينات نهجًا إحصائيًا ، ويكتبون المكدس كل بضعة أجزاء من الثانية ، وينشئون تقريرًا بناءً على هذه البيانات.



السبب الرئيسي لاستخدام ملف تعريف العينات بدلاً من ملف تعريف التتبع هو أنه خفيف الوزن. يمكنك التقاط 20 أو 200 صورة في الثانية - لا يستغرق الأمر الكثير من الوقت. ستكون أدوات التعريف هذه فعالة للغاية إذا كانت لديك مشكلة خطيرة في الأداء (يتم إنفاق 80٪ من الوقت في استدعاء وظيفة واحدة بطيئة) ، نظرًا لأن 200 لقطة في الثانية ستكون كافية لتحديد وظيفة المشكلة!



المحللون



بعد ذلك ، سأقدم ملخصًا عامًا للملفات التعريفية التي تمت مناقشتها في هذه المقالة ( من هنا ). سأشرح المصطلحات المستخدمة في هذه المقالة ( setitimer ، rb_add_event_hook ، ptrace ) بعد قليل. ومن المثير للاهتمام ، أن جميع أدوات التعريف يتم تنفيذها باستخدام مجموعة صغيرة من الميزات الأساسية.



بروفيلز بايثون







لا تعتبر "Gdb hacks" أداة تعريف لغة Python - فهي ترتبط بموقع ويب يشرح كيفية تنفيذ أداة تعريف القرصنة كملف نصي للقذيفة حول gdb . يتعلق هذا تحديدًا ببايثون ، حيث أن الإصدارات الأحدث من gbd ستنشر فعليًا مكدس Python نيابةً عنك. شيء مثل اللهب للفقراء.



المحللون روبي







تعيش جميع أدوات التعريف هذه تقريبًا داخل العملية الخاصة بك



قبل أن ندخل في تفاصيل هذه المحولات ، هناك شيء واحد مهم للغاية - كل هذه المحولات ، باستثناء pyflame ، تعمل داخل عملية Python / Ruby. إذا كنت داخل برنامج Python / Ruby ، ​​فعادة ما يكون لديك وصول سهل إلى المكدس. على سبيل المثال ، إليك برنامج Python بسيط يطبع محتويات حزمة كل خيط قيد التشغيل:



import sys
import traceback

def bar():
    foo()

def foo():
    for _, frame in sys._current_frames().items():
        for line in traceback.extract_stack(frame):
            print line

bar()


هنا هو إخراج وحدة التحكم. يمكنك أن ترى أنه يحتوي على أسماء وظائف من المكدس وأرقام الأسطر وأسماء الملفات - كل ما قد تحتاجه إذا كنت تقوم بإنشاء ملفات تعريف.



('test2.py', 12, '<module>', 'bar()')
('test2.py', 5, 'bar', 'foo()')
('test2.py', 9, 'foo', 'for line in traceback.extract_stack(frame):')


إنه أبسط في Ruby: يمكنك استخدام puts caller للحصول على المكدس.



معظم أدوات التعريف هذه هي امتدادات أداء لـ C ، لذا فهي تختلف قليلاً ، لكن مثل هذه الامتدادات لبرامج Ruby / Python لها أيضًا وصول سهل إلى مكدس المكالمات.



كيف تتبع المحللون العمل



لقد قمت بإدراج جميع ملفات تعريف تتبع Ruby و Python في الجداول أعلاه: rblineprof و ruby-prof و line_profiler و cProfile . انهم جميعا يعملون بطريقة مماثلة. يسجلون كل استدعاء للوظيفة ويكونون ملحقات C لتقليل النفقات العامة.



كيف يعملون؟ في كل من Ruby و Python ، يمكنك تحديد رد نداء يتم تشغيله عند حدوث أحداث مختلفة للمترجم (على سبيل المثال ، "استدعاء دالة" أو "سطر من تنفيذ التعليمات البرمجية"). عندما يتم استدعاء رد الاتصال ، فإنه يكتب المكدس لتحليله لاحقًا.



من المفيد أن ترى بالضبط مكان عمليات الاسترجاعات هذه في الكود ، لذلك سأربط سطور التعليمات البرمجية ذات الصلة على github.



في Python ، يمكنك تخصيص رد الاتصال باستخدام PyEval_SetTraceأو PyEval_SetProfile. هذا موصوف في قسم الوثائقالتنميط والتعقب في بايثون. تقول ، "على PyEval_SetTraceغرار PyEval_SetProfileما عدا أن دالة التتبع تتلقى أحداث رقم السطر."



الرمز:



  • line_profilerينشئ رد الاتصال باستخدام PyEval_SetTrace: انظر line_profiler.pyxالسطر 157
  • cProfileلإعداد رد الاتصال الخاص به باستخدام PyEval_SetProfile: انظر _lsprof.c السطر 693 (تم تنفيذ cProfile باستخدام lsprof )


في Ruby ، ​​يمكنك تخصيص رد الاتصال الخاص بك باستخدام rb_add_event_hook. لم أتمكن من العثور على أي وثائق حول هذا الموضوع ، ولكن هكذا يبدو الأمر.



rb_add_event_hook(prof_event_hook,
      RUBY_EVENT_CALL | RUBY_EVENT_RETURN |
      RUBY_EVENT_C_CALL | RUBY_EVENT_C_RETURN |
      RUBY_EVENT_LINE, self);


التوقيع prof_event_hook:



static void
prof_event_hook(rb_event_flag_t event, VALUE data, VALUE self, ID mid, VALUE klass)




شيء من هذا القبيل PyEval_SetTraceفي Python ، ولكن بشكل أكثر مرونة - يمكنك اختيار الأحداث التي تريد أن يتم إعلامك بها (على سبيل المثال ، "استدعاءات الوظائف فقط").



الرمز:



  • ruby-prof rb_add_event_hook : ruby-prof.c 329
  • rblineprof rb_add_event_hook : rblineprof.c 649


tracing-



العيب الرئيسي لملفات التعريف التي يتم تنفيذها بهذه الطريقة هو أنها تضيف مقدارًا ثابتًا من التعليمات البرمجية لكل وظيفة / استدعاء خط يتم تنفيذه. يمكن أن يجعلك تتخذ قرارات خاطئة! على سبيل المثال ، إذا كان لديك تطبيقان لشيء ما - أحدهما يحتوي على الكثير من استدعاءات الوظائف والآخر بدون ، والذي يستغرق عادةً نفس القدر من الوقت ، فسيظهر التطبيق الأول ، الذي يحتوي على الكثير من استدعاءات الوظائف ، بشكل أبطأ عند التنميط.



للتوضيح ، قمت بإنشاء ملف صغير اسمه test.pyبالمحتوى التالي وقارنت وقت التنفيذ python -mcProfile test.pyو python test.py. python. test.pyاكتمل في حوالي 0.6 ثانية ، وفي python -mcProfile test.pyحوالي ثانية واحدة. لذلك في هذا المثال بالذات cProfileأضفت زيادة بنسبة 60٪ إضافية.

cProfileتقول الوثائق :

تضيف طبيعة Python المفسرة الكثير من وقت التشغيل الزائد بحيث يميل التنميط الحتمي إلى إضافة القليل من تكاليف المعالجة في التطبيقات العادية.


يبدو هذا كأنه بيان معقول جدًا - المثال السابق (الذي يجعل 3.5 مليون استدعاء وظيفي ولا شيء آخر) من الواضح أنه ليس برنامج Python عاديًا ، وتقريبًا أي برنامج آخر سيكون له عبء أقل.

لم ruby-profأتحقق من ملف تعريف التتبع الخاص بـ Ruby ، ​​لكن README الخاص به يقول ما يلي:

ستعمل معظم البرامج ببطء نصفها تقريبًا ، في حين أن البرامج عالية التكرار (مثل اختبار سلسلة فيبوناتشي) ستعمل بشكل أبطأ بثلاث مرات .


كيف تعمل نماذج أخذ العينات بشكل عام: setitimer



حان الوقت للحديث عن النوع الثاني من المحلل: نماذج عينات!

يتم تنفيذ معظم أدوات تحديد العينات في Ruby و Python باستخدام استدعاء النظام setitimer. ما هذا؟



لنفترض أنك تريد التقاط لقطة من مكدس البرنامج 50 مرة في الثانية. ويمكن القيام بذلك على النحو التالي:



  • اطلب من Linux kernel أن يرسل لك إشارة كل 20 مللي ثانية (باستخدام مكالمة نظام setitimer) ؛
  • تسجيل معالج إشارة للحصول على لقطة مكدس عند استقبال إشارة ؛
  • عند اكتمال التنميط ، اطلب من Linux التوقف عن إرسال إشارة إليك وتقديم النتيجة!


إذا كنت تريد أن ترى حالة استخدام عملية setitimerلتنفيذ ملف تعريف عينات ، أعتقد أن stacksampler.pyأفضل مثال هو ملف تعريف مفيد وعملي ، وهو عبارة عن 100 سطر في Python. وهذا رائع جدا!



السبب في أنه stacksampler.pyلا يتطلب سوى 100 سطر في Python هو أنه عندما تقوم بتسجيل وظيفة Python كمعالج إشارة ، يتم تمرير الوظيفة إلى الحزمة الحالية من برنامجك. لذلك ، من stacksampler.pyالسهل جدًا تسجيل معالج إشارة :



def _sample(self, signum, frame):
   stack = []
   while frame is not None:
       stack.append(self._format_frame(frame))
       frame = frame.f_back

   stack = ';'.join(reversed(stack))
   self._stack_counts[stack] += 1


إنه ينبثق فقط كومة من إطار ويزيد عدد المرات التي تم فيها عرض مكدس معين. بسيط جدا! لطيف جدا!



دعنا نلقي نظرة على جميع بيانات التعريف الأخرى التي يستخدمونها setitimerومعرفة أين يسمون في الكود setitimer:



  • stackprof (Ruby): stackprof.c 118
  • perftools.rb (Ruby): , , , , gem (?)
  • stacksampler (Python): stacksampler.py 51
  • statprof (Python): statprof.py 239
  • vmprof (Python): vmprof_unix.c 294


الشيء المهم في setitimer- عليك أن تقرر كيفية حساب الوقت. هل تريد 20 مللي ثانية في الوقت الحقيقي؟ 20ms المستخدم وحدة المعالجة المركزية الوقت؟ 20ms مستخدم + وقت وحدة المعالجة المركزية للنظام؟ إذا نظرت عن كثب إلى الروابط أعلاه ، ستلاحظ أن هؤلاء المحترفين يستخدمون بالفعل أشياء مختلفة setitimer- أحيانًا يكون السلوك قابلاً للتخصيص وأحيانًا لا. صفحة setitimerالدليل قصيرة وتستحق القراءة لجميع التكوينات الممكنة. أشار إلى حالة استخدام مثيرة للاهتمام



@mgedminعلى Twittersetitimer . هذا الموضوع وهذه القضية يكشفان المزيد من التفاصيل.



عيب واحد مثير للاهتمام لمحات على أساسsetitimer- ما توقيت الإشارات! تقوم الإشارات أحيانًا بمقاطعة مكالمات النظام! تستغرق مكالمات النظام أحيانًا بضع ميلي ثانية! إذا كنت تلتقط لقطات في كثير من الأحيان ، يمكنك جعل البرنامج ينفذ مكالمات النظام إلى أجل غير مسمى!



نماذج عينات لا تستخدم setitimer



هناك العديد من أدوات تحديد العينات التي لا تستخدم setitimer:



  • pyinstrumentيستخدم PyEval_SetProfile(لذلك فهو نوع من ملف تعريف التتبع) ، لكنه لا يجمع دائمًا لقطات المكدس عندما يتم استدعاء رد نداء التتبع. هذا هو الكود الذي يختار توقيت لقطة تتبع المكدس . اقرأ المزيد عن هذا الحل في هذه المدونة . (بشكل أساسي: setitimerيسمح لك بإنشاء ملف تعريف للخيط الرئيسي فقط في Python)
  • pyflameملفات تعريف Python البرمجية خارج العملية باستخدام استدعاء النظام ptrace. إنه يستخدم حلقة حيث يلتقط الصور وينام لفترة معينة من الوقت ويفعل الشيء نفسه مرة أخرى. ها هي مكالمة للانتظار.
  • python-flamegraphيتبع نهجًا مشابهًا حيث يبدأ خيطًا جديدًا في عملية Python الخاصة بك ويعيد تتبع المكدس والنوم والحلقات مرة أخرى. ها هي مكالمة للانتظار .


كل 3 من هؤلاء المحترفين يأخذون لقطات في الوقت الحقيقي.



مشاركات مدونة Pyflame



لقد أمضيت كل وقتي تقريبًا في هذا المنشور على ملفات التعريف بخلاف ذلك pyflame، لكنه في الواقع أكثر ما يثير اهتمامي لأنه يقوم بتوصيف برنامج Python الخاص بك من عملية منفصلة ، ولهذا السبب أريد أن يعمل ملف تعريف Ruby الخاص بي بشكل مشابه.



بالطبع ، كل شيء أكثر تعقيدًا قليلاً مما وصفته. لن أخوض في التفاصيل ، لكن إيفان كليتزكي كتب العديد من المقالات الجيدة حول هذا في مدونته:





يمكن العثور على مزيد من المعلومات على eklitzke.org . هذه كلها أشياء مثيرة للاهتمام وسأقرأ عنها عن كثب - ربما ptraceيتضح أنها أفضل من process_vm_readvتنفيذ ملف تعريف روبي! إنه يحتوي على process_vm_readvقدر أقل من النفقات العامة لأنه لا يوقف العملية ، ولكنه يمكن أن يمنحك أيضًا لقطة غير صحيحة لأنه لا يوقف العملية :). في تجاربي ، لم يكن الحصول على صور متضاربة مشكلة كبيرة ، لكنني أعتقد أنني هنا سأجري سلسلة من التجارب.



هذا كل شيء لهذا اليوم!



هناك العديد من التفاصيل الدقيقة المهمة التي لم أخوض فيها في هذا المنشور - على سبيل المثال ، قلت ذلك بشكل أساسي vmprofو stacksampler- متشابهة (ليست كذلك -vmprofيدعم تنميط السلاسل وتنميط وظائف Python المكتوبة بلغة C ، والتي أعتقد أنها تجعل ملف التعريف أكثر تعقيدًا). لكن لديهم بعض المبادئ الأساسية نفسها ، ولذا أعتقد أن مراجعة اليوم ستكون نقطة انطلاق جيدة.






TDD مع وبدون pytest. ندوة مجانية على الويب






اقرأ أكثر:






All Articles