יצירת פרופילים מבוססת-טריגר

ב-ProfilingManager אפשר לתעד פרופילים על סמך טריגרים של המערכת. המערכת מנהלת את תהליך ההקלטה ומספקת לאפליקציה את הפרופיל שנוצר.

הטריגרים קשורים לאירועים קריטיים לביצועים. הפרופילים שהמערכת מתעדת מספקים מידע מפורט על תוצאות ניפוי הבאגים של חוויות השימוש ההכרחיות (CUJ) שמשויכות לטריגרים האלה.

תיעוד נתונים היסטוריים

כדי להפעיל הרבה מהטריגרים, צריך לנתח את הנתונים ההיסטוריים שמובילים לאירוע. הטריגר עצמו הוא בדרך כלל תוצאה של בעיה, ולא שורש הבעיה. אם תתחילו פרופיל רק אחרי שהטריגר יתרחש, יכול להיות ששורש הבעיה כבר יאבד.

לדוגמה, פעולה ממושכת בשרשור ה-UI גורמת לשגיאה האפליקציה לא מגיבה (ANR). עד שהמערכת מזהה את שגיאת ה-ANR ומאותתת לאפליקציה, יכול להיות שהפעולה כבר הסתיימה. אם מתחילים פרופיל ברגע הזה, מפספסים את פעולת החסימה בפועל.

אי אפשר לחזות בדיוק מתי יתרחשו טריגרים מסוימים, ולכן אי אפשר להפעיל פרופיל באופן ידני מראש.

למה כדאי להשתמש בצילום מבוסס-טריגר?

הסיבה העיקרית לשימוש בטריגרים לפרופיילינג היא לתעד נתונים של אירועים בלתי צפויים, שבהם האפליקציה לא יכולה להתחיל להקליט באופן ידני לפני שהאירועים האלה מתרחשים. אפשר להשתמש בטריגרים של פרופילים כדי:

  • ניפוי באגים בבעיות בביצועים: אבחון של מקרי ANR, דליפות זיכרון ובעיות יציבות אחרות.
  • אופטימיזציה של חוויות שימוש הכרחיות (CUJ): ניתוח ושיפור של תהליכים, למשל הפעלת האפליקציה.
  • הבנת התנהגות המשתמשים: קבלת תובנות לגבי אירועים, למשל, יציאות של משתמשים מהאפליקציה.

הגדרת טריגר

הקוד הבא מדגים איך להירשם לטריגר TRIGGER_TYPE_APP_FULLY_DRAWN ולהחיל עליו הגבלת קצב של יצירת בקשות.

Kotlin

fun recordWithTrigger() {
    val profilingManager = applicationContext.getSystemService(ProfilingManager::class.java)

    val triggers = ArrayList<ProfilingTrigger>()

    val triggerBuilder = ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_APP_FULLY_DRAWN)
        .setRateLimitingPeriodHours(1)

    triggers.add(triggerBuilder.build())

    val mainExecutor: Executor = Executors.newSingleThreadExecutor()

    val resultCallback = Consumer<ProfilingResult> { profilingResult ->
        if (profilingResult.errorCode == ProfilingResult.ERROR_NONE) {
            Log.d(
                "ProfileTest",
                "Received profiling result file=" + profilingResult.resultFilePath
            )
            setupProfileUploadWorker(profilingResult.resultFilePath)
        } else {
            Log.e(
                "ProfileTest",
                "Profiling failed errorcode=" + profilingResult.errorCode + " errormsg=" + profilingResult.errorMessage
            )
        }
    }

    profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback)
    profilingManager.addProfilingTriggers(triggers)

}

Java

public void recordWithTrigger() {
  ProfilingManager profilingManager = getApplicationContext().getSystemService(
      ProfilingManager.class);
  List<ProfilingTrigger> triggers = new ArrayList<>();
  ProfilingTrigger.Builder triggerBuilder = new ProfilingTrigger.Builder(
      ProfilingTrigger.TRIGGER_TYPE_APP_FULLY_DRAWN);
  triggerBuilder.setRateLimitingPeriodHours(1);
  triggers.add(triggerBuilder.build());

  Executor mainExecutor = Executors.newSingleThreadExecutor();
  Consumer<ProfilingResult> resultCallback =
      new Consumer<ProfilingResult>() {
        @Override
        public void accept(ProfilingResult profilingResult) {
          if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE) {
            Log.d(
                "ProfileTest",
                "Received profiling result file=" + profilingResult.getResultFilePath());
            setupProfileUploadWorker(profilingResult.getResultFilePath());
          } else {
            Log.e(
                "ProfileTest",
                "Profiling failed errorcode="
                    + profilingResult.getErrorCode()
                    + " errormsg="
                    + profilingResult.getErrorMessage());
          }
        }
      };
  profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback);
  profilingManager.addProfilingTriggers(triggers);

}

הקוד מבצע את השלבים הבאים:

  1. Get the manager: אחזור של שירות ProfilingManager.
  2. ‫Define a trigger: יצירת ProfilingTrigger ל-TRIGGER_TYPE_APP_FULLY_DRAWN. האירוע הזה מתרחש כשהאפליקציה מדווחת שהיא סיימה את ההפעלה והיא אינטראקטיבית.
  3. הגדרת מגבלות קצב: מגדירה מגבלת קצב של שעה לטריגר הספציפי הזה (setRateLimitingPeriodHours(1)). כך האפליקציה לא יכולה לתעד יותר מפרופיל הפעלה אחד בשעה.
  4. רישום מאזין: קורא ל-registerForAllProfilingResults כדי להגדיר את הקריאה החוזרת (callback) שמטפלת בתוצאה. הקריאה החוזרת הזו מקבלת את הנתיב של הפרופיל השמור דרך getResultFilePath().
  5. הוספת טריגרים: רישום רשימת הטריגרים ב-ProfilingManager באמצעות addProfilingTriggers.
  6. הפעלת אירוע: קריאה ל-reportFullyDrawn(), ששולחת את האירוע TRIGGER_TYPE_APP_FULLY_DRAWN למערכת ומפעילה איסוף פרופילים, בהנחה שמעקב המערכת ברקע פועל ויש מכסה זמינה להגבלת קצב של יצירת בקשות. השלב האופציונלי הזה מדגים תהליך מקצה לקצה, כי האפליקציה צריכה לקרוא ל-reportFullyDrawn() בשביל הטריגר הזה.

אחזור נתוני המעקב

המערכת שומרת פרופילים שמבוססים על טריגרים באותה ספרייה שבה נשמרים פרופילים אחרים. שם הקובץ של מעקבים שהופעלו הוא בפורמט הבא:

profile_trigger_<profile_type_code>_<datetime>.<profile-type-name>

אתם יכולים לשלוף את הקובץ באמצעות ADB. לדוגמה, כדי לשלוף את עקבות המערכת שתועדו באמצעות הקוד לדוגמה באמצעות ADB, אפשר להשתמש בקוד הבא:

adb pull /data/user/0/com.example.sampleapp/files/profiling/profile_trigger_1_2025-05-06-14-12-40.perfetto-trace

לפרטים על הצגת המעקבים האלה, ראו אחזור וניתוח של נתוני פרופיילינג.

איך פועל מעקב ברקע

כדי לתעד נתונים מלפני הטריגר, מערכת ההפעלה מתחילה מעקב ברקע מדי פעם. אם מתרחש טריגר בזמן שהמעקב ברקע פעיל והאפליקציה רשומה אליו, המערכת שומרת את פרופיל המעקב בספרייה של האפליקציה. הפרופיל יכלול מידע שהוביל לטריגר.

אחרי שהפרופיל נשמר, המערכת שולחת התראה לאפליקציה באמצעות פונקציית ה-callback שסופקה ל-registerForAllProfilingResults. הקריאה החוזרת הזו מספקת את הנתיב לפרופיל שנוצר, שאפשר לגשת אליו באמצעות קריאה ל-ProfilingResult#getResultFilePath().

תרשים שמראה איך פועלים קובצי ה-snapshot של המעקב ברקע, עם מאגר נתונים זמני (ring buffer) שמתעד נתונים לפני אירוע טריגר.
איור 1: איך פועלים קובצי ה-snapshot של המעקב ברקע.

כדי לצמצם את ההשפעה על ביצועי המכשיר ועל חיי הסוללה, המערכת לא מפעילה מעקבים ברקע באופן רציף. במקום זאת, הוא משתמש בשיטת דגימה. המערכת מתחילה באופן אקראי מעקב ברקע בתוך מסגרת זמן מוגדרת (עם משך מינימלי ומקסימלי). הוספת מרווחים אקראיים בין העקבות האלה משפרת את הכיסוי של הטריגרים.

לפרופילים שמופעלים על ידי המערכת יש גודל מקסימלי שמוגדר על ידי המערכת, ולכן הם משתמשים במאגר נתונים זמני מעגלי. כשהמאגר מלא, נתוני מעקב חדשים מחליפים את הנתונים הישנים ביותר. כמו שמוצג באיור 1, יכול להיות שהמעקב שתועד לא יכסה את כל משך ההקלטה ברקע אם המאגר מתמלא. במקום זאת, הוא מייצג את הפעילות האחרונה שהובילה לטריגר.

הטמעה של הגבלת קצב של יצירת בקשות ספציפית לטריגר

טריגרים בתדירות גבוהה יכולים לנצל במהירות את המכסה של הגבלת קצב של יצירת בקשות של האפליקציה. כדי להבין טוב יותר את מגביל הקצב, מומלץ לעיין במאמר איך פועל מגביל הקצב. כדי למנוע מצב שבו סוג אחד של טריגר ינצל את כל המכסה, אתם יכולים להטמיע הגבלת קצב של יצירת בקשות לטריגרים ספציפיים.

‫ProfilingManager תומך בהגבלת קצב של יצירת בקשות ספציפיות לטריגרים שמוגדרים באפליקציה. כך אפשר להוסיף עוד שכבה של ויסות נתונים מבוסס-זמן, בנוסף למגביל הקצב הקיים. משתמשים ב-API‏ setRateLimitingPeriodHours כדי להגדיר זמן השהיה ספציפי לטריגר. אחרי שתקופת הצינון תסתיים, תוכלו להפעיל את התכונה שוב.

ניפוי באגים של טריגרים באופן מקומי

מכיוון שמעקבים ברקע פועלים בזמנים אקראיים, קשה לנפות באגים בטריגרים באופן מקומי. כדי לאלץ מעקב ברקע לצורך בדיקה, משתמשים בפקודת ה-ADB הבאה:

adb shell device_config put profiling_testing system_triggered_profiling.testing_package_name <com.example.myapp>

הפקודה הזו מאלצת את המערכת להתחיל מעקב רציף ברקע של החבילה שצוינה, ומאפשרת לכל טריגר לאסוף פרופיל אם מגביל הקצב מאפשר זאת.

אפשר גם להפעיל אפשרויות אחרות לניפוי באגים, למשל להשבית את הגבלת קצב של יצירת בקשות כשמנפים באגים באופן מקומי. מידע נוסף זמין במאמר פקודות ניפוי באגים ליצירת פרופילים מקומיים.