Ví dụ thực tế về gỡ lỗi hiệu suất: ANR

Phần này trình bày cách gỡ lỗi Ứng dụng không phản hồi (ANR) bằng ProfilingManager kèm theo một dấu vết ví dụ.

Thiết lập ứng dụng để thu thập lỗi ANR

Bắt đầu bằng cách thiết lập một điều kiện kích hoạt lỗi ANR trong ứng dụng:

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);
}

Sau khi ghi lại và tải dấu vết ANR lên, hãy mở dấu vết đó trong Giao diện người dùng Perfetto.

Phân tích dấu vết

Vì lỗi ANR đã kích hoạt dấu vết, nên bạn biết dấu vết kết thúc khi hệ thống phát hiện thấy luồng chính của ứng dụng không phản hồi. Hình 1 cho thấy cách chuyển đến luồng chính của ứng dụng được gắn thẻ tương ứng trong giao diện người dùng.

Điều hướng giao diện người dùng Perfetto đến luồng chính của ứng dụng.
Hình 1. Điều hướng đến luồng chính của ứng dụng.

Thời gian kết thúc dấu vết trùng với dấu thời gian của lỗi ANR, như trong Hình 2.

Giao diện người dùng Perfetto cho thấy phần cuối của một dấu vết, làm nổi bật vị trí kích hoạt ANR.
Hình 2. Vị trí kích hoạt lỗi ANR.

Dấu vết này cũng cho thấy các thao tác mà ứng dụng đang chạy khi xảy ra lỗi ANR. Cụ thể, ứng dụng đã chạy mã trong lát theo dõi handleNetworkResponse. Lát này nằm trong lát MyApp:SubmitButton. Thao tác này mất 1,48 giây thời gian CPU (Hình 3).

Giao diện người dùng Perfetto cho biết thời gian CPU mà quá trình thực thi handleNetworkResponse tiêu thụ tại thời điểm xảy ra lỗi ANR.
Hình 3. Thực thi tại thời điểm xảy ra lỗi ANR.

Nếu chỉ dựa vào dấu vết ngăn xếp tại thời điểm xảy ra lỗi ANR để gỡ lỗi, bạn có thể nhầm lẫn khi cho rằng lỗi ANR hoàn toàn là do mã đang chạy trong lát cắt dấu vết handleNetworkResponse chưa kết thúc khi hồ sơ hoàn tất quá trình ghi. Tuy nhiên, 1,48 giây là không đủ để tự kích hoạt lỗi ANR, mặc dù đây là một thao tác tốn nhiều tài nguyên. Bạn cần xem xét khoảng thời gian trước đó để hiểu rõ điều gì đã chặn luồng chính trước phương thức này.

Để nắm bắt điểm bắt đầu tìm nguyên nhân gây ra lỗi ANR, chúng ta bắt đầu tìm kiếm sau khung hình cuối cùng do luồng giao diện người dùng tạo ra, tương ứng với lát Choreographer#doFrame 551275 và không có nguồn gây ra độ trễ lớn nào trước khi bắt đầu lát MyApp:SubmitButton kết thúc bằng lỗi ANR (Hình 4).

Giao diện người dùng Perfetto cho thấy khung hình cuối cùng do luồng giao diện người dùng hiển thị trước khi xảy ra lỗi ANR.
Hình 4. Khung ứng dụng gần đây nhất được tạo trước lỗi ANR.

Để hiểu rõ hơn về tình trạng tắc nghẽn, hãy thu nhỏ để kiểm tra toàn bộ lát cắt MyApp:SubmitButton. Bạn sẽ nhận thấy một chi tiết quan trọng trong trạng thái của luồng, như minh hoạ trong Hình 4: luồng đã dành 75% thời gian (6,7 giây) ở trạng thái Sleeping và chỉ 24% thời gian ở trạng thái Running.

Giao diện người dùng Perfetto cho thấy trạng thái luồng trong một thao tác, làm nổi bật 75% thời gian ngủ và 24% thời gian chạy.
Hình 5. Trạng thái của luồng trong quá trình hoạt động `MyApp:SubmitButton`.

Điều này cho thấy nguyên nhân chính gây ra lỗi ANR là do chờ đợi, chứ không phải do tính toán. Xem xét từng lần ngủ để tìm ra một quy luật.

Giao diện người dùng Perfetto cho thấy khoảng thời gian ngủ đầu tiên trong lát dấu vết MyAppSubmitButton.
Hình 6. Thời gian ngủ đầu tiên trong `MyAppSubmitButton`.
Giao diện người dùng Perfetto cho thấy khoảng thời gian ngủ thứ hai trong lát cắt dấu vết MyAppSubmitButton.
Hình 7. Thời gian ngủ thứ hai trong `MyAppSubmitButton`.
Giao diện người dùng Perfetto cho thấy khoảng thời gian ngủ thứ ba trong lát dấu vết MyAppSubmitButton.
Hình 8. Thời gian ngủ thứ ba trong `MyAppSubmitButton`.
Giao diện người dùng Perfetto cho thấy khoảng thời gian ngủ thứ tư trong lát dấu vết MyAppSubmitButton.
Hình 9. Thời gian ngủ thứ tư trong `MyAppSubmitButton`.

3 khoảng thời gian ngủ đầu tiên (Hình 6 – 8) gần như giống hệt nhau, mỗi khoảng thời gian khoảng 2 giây. Giấc ngủ thứ tư bất thường (Hình 9) là 0,7 giây. Thời lượng chính xác là 2 giây hiếm khi là một sự trùng hợp ngẫu nhiên trong môi trường điện toán. Điều này cho thấy rõ ràng là đã có thời gian chờ được lập trình thay vì xảy ra tình trạng tranh chấp tài nguyên ngẫu nhiên. Lần ngủ gần đây nhất có thể là do luồng đã hoàn tất quá trình chờ vì thao tác mà luồng đang chờ đã thành công.

Giả thuyết này cho rằng ứng dụng đã đạt đến thời gian chờ do người dùng xác định là 2 giây nhiều lần và cuối cùng thành công, gây ra đủ độ trễ để kích hoạt lỗi ANR.

Giao diện người dùng Perfetto cho thấy thông tin tóm tắt về độ trễ trong lát cắt dấu vết MyApp:SubmitButton, cho biết nhiều khoảng thời gian ngủ 2 giây.
Hình 10. Tóm tắt độ trễ trong lát cắt `MyApp:SubmitButton`.

Để xác minh điều này, hãy kiểm tra mã được liên kết với phần dấu vết 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();
}

Mã này xác nhận giả thuyết này. Phương thức onClickSubmit thực thi một yêu cầu mạng trên luồng giao diện người dùng với NETWORK_TIMEOUT_MILLISECS được mã hoá cứng là 2000 mili giây. Điều quan trọng là vòng lặp này chạy trong vòng lặp while và thử lại tối đa 10 lần.

Trong dấu vết cụ thể này, người dùng có thể có kết nối mạng kém. Ba lần thử đầu tiên không thành công, dẫn đến 3 lần hết thời gian chờ 2 giây (tổng cộng là 6 giây). Lần thử thứ tư thành công sau 0,7 giây, cho phép mã chuyển sang handleNetworkResponse. Tuy nhiên, thời gian chờ tích luỹ đã kích hoạt lỗi ANR.

Tránh loại lỗi ANR này bằng cách đưa các thao tác liên quan đến mạng có độ trễ khác nhau vào một luồng nền thay vì thực thi chúng trong luồng chính. Điều này giúp giao diện người dùng vẫn phản hồi ngay cả khi kết nối kém, hoàn toàn loại bỏ lớp ANR này.