مثال عملي على تصحيح أخطاء الأداء: أخطاء ANR

يوضّح هذا القسم كيفية تصحيح خطأ التطبيق لا يستجيب (ANR) باستخدام ProfilingManager مع مثال على تتبُّع تسلسل استدعاء الدوال البرمجية.

إعداد التطبيق لجمع أخطاء ANR

ابدأ بإعداد مشغّل خطأ ANR في تطبيقك:

public void addANRTrigger() {
  ProfilingManager profilingManager = getApplicationContext().getSystemService(
      ProfilingManager.class);
  List<ProfilingTrigger> triggers = new ArrayList<>();
  ProfilingTrigger.Builder triggerBuilder = new ProfilingTrigger.Builder(
      ProfilingTrigger.TRIGGER_TYPE_ANR);
  triggers.add(triggerBuilder.build());
  Executor mainExecutor = Executors.newSingleThreadExecutor();
  Consumer<ProfilingResult> resultCallback =
      profilingResult -> {
        // Handle uploading trace to your back-end
      };
  profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback);
  profilingManager.addProfilingTriggers(triggers);
}

بعد تسجيل تتبُّع خطأ ANR وتحميله، افتحه في واجهة مستخدم Perfetto.

تحليل عملية التتبُّع

بما أنّ خطأ ANR أدّى إلى تشغيل التتبُّع، ستعرف أنّ التتبُّع انتهى عندما رصد النظام عدم استجابة في سلسلة التعليمات الرئيسية لتطبيقك. يوضّح الشكل 1 كيفية الانتقال إلى سلسلة التعليمات الرئيسية لتطبيقك التي تم وضع علامة عليها وفقًا لذلك ضمن واجهة المستخدم.

التنقّل في واجهة مستخدم Perfetto إلى سلسلة التعليمات الرئيسية للتطبيق
الشكل 1. التنقّل إلى سلسلة التعليمات الرئيسية للتطبيق

تتطابق نهاية التتبُّع مع الطابع الزمني لخطأ ANR، كما هو موضّح في الشكل 2.

واجهة مستخدم Perfetto تعرض نهاية عملية تتبُّع، مع تمييز الموقع الجغرافي الذي تم فيه تشغيل خطأ ANR.
الشكل 2. الموقع الجغرافي الذي تم فيه تسجيل خطأ ANR

تعرض عملية التتبُّع أيضًا العمليات التي كان التطبيق ينفّذها عند حدوث خطأ ANR. على وجه التحديد، نفَّذ التطبيق الرمز في شريحة التتبُّع handleNetworkResponse. كانت شريحة البيانات هذه داخل شريحة البيانات MyApp:SubmitButton. استغرقت 1.48 ثانية من وقت وحدة المعالجة المركزية (الشكل 3).

تعرض واجهة مستخدم Perfetto وقت وحدة المعالجة المركزية (CPU) الذي استغرقه تنفيذ handleNetworkResponse
 في وقت حدوث خطأ ANR.
الشكل 3. التنفيذ في وقت حدوث خطأ ANR

إذا كنت تعتمد حاليًا على عمليات تتبُّع تسلسل استدعاء الدوال البرمجية في لحظة حدوث خطأ ANR لتصحيح الأخطاء، قد تنسب خطأ ANR بشكل خاطئ بالكامل إلى الرمز البرمجي الذي يتم تنفيذه ضمن شريحة التتبُّع handleNetworkResponse التي لم تنتهِ عند انتهاء تسجيل الملف الشخصي. ومع ذلك، لا يكفي وقت التنفيذ البالغ 1.48 ثانية لتفعيل خطأ ANR وحده، على الرغم من أنّه عملية مكلفة. عليك الرجوع إلى وقت أبعد لفهم ما أوقف مؤقتًا سلسلة التعليمات الرئيسية قبل تنفيذ هذه الطريقة.

للحصول على نقطة بداية للبحث عن سبب خطأ ANR، نبدأ البحث بعد آخر إطار تم إنشاؤه بواسطة سلسلة التعليمات الخاصة بواجهة المستخدم والذي يتوافق مع شريحة Choreographer#doFrame 551275، ولا توجد مصادر كبيرة للتأخير قبل بدء شريحة MyApp:SubmitButton التي انتهت بخطأ ANR (الشكل 4).

تعرض واجهة مستخدم Perfetto آخر لقطة عرضتها سلسلة واجهة المستخدم قبل حدوث خطأ ANR.
الشكل 4. آخر إطار للتطبيق تم إنشاؤه قبل حدوث خطأ ANR

لفهم سبب الحظر، يمكنك التصغير لفحص شريحة MyApp:SubmitButton البيانات الكاملة. ستلاحظ تفصيلاً مهمًا في حالات سلسلة المحادثات، كما هو موضّح في الشكل 4: استغرقت سلسلة المحادثات 75% من الوقت (6.7 ثانية) في الحالة Sleeping و24% فقط من الوقت في الحالة Running.

تعرض واجهة مستخدم Perfetto حالات سلاسل المحادثات أثناء عملية ما، مع إبراز أنّ 75% من الوقت كان في وضع السكون و24% من الوقت كان وقت تشغيل.
الشكل 5. حالات سلسلة المحادثات أثناء عملية `MyApp:SubmitButton`.

يشير ذلك إلى أنّ السبب الرئيسي لخطأ ANR هو الانتظار، وليس الاحتساب. افحص حالات النوم الفردية للعثور على نمط.

تعرض واجهة مستخدم Perfetto الفاصل الزمني الأول لوضع السكون ضمن شريحة تتبُّع MyAppSubmitButton.
الشكل 6. وقت النوم الأول ضمن `MyAppSubmitButton`.
تعرض واجهة مستخدم Perfetto الفاصل الزمني الثاني لوضع السكون ضمن شريحة التتبُّع MyAppSubmitButton.
الشكل 7. وقت النوم الثاني ضمن `MyAppSubmitButton`.
تعرض واجهة مستخدم Perfetto الفاصل الزمني الثالث لوضع السكون ضمن شريحة التتبُّع MyAppSubmitButton.
الشكل 8. وقت النوم الثالث ضمن `MyAppSubmitButton`.
تعرض واجهة مستخدم Perfetto الفاصل الزمني الرابع في وضع السكون ضمن شريحة تتبُّع MyAppSubmitButton.
الشكل 9. وقت النوم الرابع ضمن `MyAppSubmitButton`.

فترات النوم الثلاث الأولى (الأشكال 6-8) متطابقة تقريبًا، حيث تبلغ كل منها ثانيتَين تقريبًا. يبلغ معدّل النوم الرابع الشاذ (الشكل 9) 0.7 ثانية. من النادر أن تكون مدة 2 ثانية بالضبط مجرد صدفة في بيئة حوسبة. يشير ذلك بقوة إلى انتهاء مهلة مبرمَجة بدلاً من تعارض عشوائي في الموارد. قد يكون سبب السكون الأخير هو انتهاء انتظار سلسلة التعليمات لأنّ العملية التي كانت تنتظرها قد نجحت.

تتمثّل هذه الفرضية في أنّ التطبيق كان يتجاوز مهلة محدّدة من قِبل المستخدم تبلغ ثانيتَين عدة مرات، ثم ينجح في النهاية، ما يؤدي إلى حدوث تأخير كافٍ لتفعيل خطأ ANR.

تعرض واجهة مستخدم Perfetto ملخّصًا للتأخيرات خلال شريحة التتبُّع MyApp:SubmitButton<0x0A>، ما يشير إلى فواصل نوم متعدّدة لمدة ثانيتَين.
الشكل 10. ملخّص التأخيرات خلال شريحة الرمز `MyApp:SubmitButton`.

للتأكّد من ذلك، افحص الرمز المرتبط بقسم MyApp:SubmitButton لتتبُّع الأخطاء:

private static final int NETWORK_TIMEOUT_MILLISECS = 2000;
public void setupButtonCallback() {
  findViewById(R.id.submit).setOnClickListener(submitButtonView -> {
    Trace.beginSection("MyApp:SubmitButton");
    onClickSubmit();
    Trace.endSection();
  });
}

public void onClickSubmit() {
  prepareNetworkRequest();

  boolean networkRequestSuccess = false;
  int maxAttempts = 10;
  while (!networkRequestSuccess && maxAttempts > 0) {
    networkRequestSuccess = performNetworkRequest(NETWORK_TIMEOUT_MILLISECS);
    maxAttempts--;
  }

  if (networkRequestSuccess) {
    handleNetworkResponse();
  }
}

boolean performNetworkRequest(int timeoutMiliseconds) {
  // ...
}

  // ...
}

public void handleNetworkResponse() {
  Trace.beginSection("handleNetworkResponse");
  // ...
  Trace.endSection();
}

يؤكّد الرمز هذه الفرضية. تنفّذ الطريقة onClickSubmit طلب شبكة على سلسلة واجهة المستخدم مع قيمة NETWORK_TIMEOUT_MILLISECS مبرمَجة مسبقًا تبلغ 2000 ملي ثانية. والأهم من ذلك، يتم تشغيله داخل حلقة while تعيد المحاولة حتى 10 مرات.

في عملية التتبُّع المحدّدة هذه، من المحتمل أنّ اتصال المستخدم بالشبكة كان ضعيفًا. فشلت المحاولات الثلاث الأولى، ما أدّى إلى حدوث ثلاث مهلات مدة كل منها ثانيتان (إجمالي 6 ثوانٍ). نجحت المحاولة الرابعة بعد 0.7 ثانية، ما سمح للرمز البرمجي بالانتقال إلى handleNetworkResponse. ومع ذلك، أدّى وقت الانتظار المتراكم إلى حدوث خطأ ANR.

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