Bảng điều khiển Hiệu suất nhanh hơn 400% nhờ tính năng nhận biết

Andrés Olivares
Andrés Olivares
Nancy Li
Nancy Li

Bất kể bạn đang phát triển loại ứng dụng nào, việc tối ưu hoá hiệu suất và đảm bảo ứng dụng tải nhanh cũng như cung cấp các tương tác mượt mà là điều rất quan trọng đối với trải nghiệm người dùng và sự thành công của ứng dụng. Có một cách để thực hiện việc này là kiểm tra hoạt động của một ứng dụng bằng cách dùng các công cụ phân tích tài nguyên để xem những gì đang xảy ra khi ứng dụng chạy trong một khoảng thời gian. Bảng điều khiển Hiệu suất trong Công cụ cho nhà phát triển là một công cụ phân tích tài nguyên tuyệt vời để phân tích và tối ưu hoá hiệu suất của các ứng dụng web. Nếu ứng dụng của bạn đang chạy trong Chrome, ứng dụng sẽ cung cấp cho bạn thông tin tổng quan chi tiết về những gì trình duyệt đang thực hiện khi ứng dụng của bạn đang được thực thi. Khi hiểu được hoạt động này, bạn có thể xác định những quy luật, điểm tắc nghẽn và điểm nóng về hiệu suất mà bạn có thể xử lý để cải thiện hiệu suất.

Trong ví dụ sau đây, bạn sẽ thấy bảng điều khiển Performance (Hiệu suất).

Thiết lập và tạo lại tình huống lập hồ sơ

Gần đây, chúng tôi đặt mục tiêu giúp bảng điều khiển Hiệu suất hoạt động hiệu quả hơn. Cụ thể, chúng tôi muốn nền tảng này tải lượng lớn dữ liệu hiệu suất nhanh hơn. Trường hợp này xảy ra, chẳng hạn như khi phân tích các quy trình chạy trong thời gian dài hoặc phức tạp hoặc thu thập dữ liệu có độ chi tiết cao. Để làm được điều này, trước tiên, bạn cần phải nắm được cách hoạt động của ứng dụng và lý do ứng dụng hoạt động theo cách đó. Công cụ phân tích tài nguyên có thể giúp bạn đạt được điều này.

Như bạn có thể đã biết, Công cụ cho nhà phát triển là một ứng dụng web. Do đó, bạn có thể phân tích tài nguyên bằng bảng Hiệu suất. Để phân tích tài nguyên của bảng điều khiển này, bạn có thể mở Công cụ cho nhà phát triển, sau đó mở một phiên bản Công cụ cho nhà phát triển khác được đính kèm vào bảng điều khiển đó. Tại Google, cách thiết lập này được gọi là DevTools-on-DevTools.

Khi thiết lập đã sẵn sàng, bạn phải tạo lại và ghi lại tình huống được phân tích tài nguyên. Để tránh nhầm lẫn, cửa sổ Công cụ cho nhà phát triển ban đầu sẽ được gọi là "phiên bản Công cụ cho nhà phát triển đầu tiên" và cửa sổ kiểm tra công cụ cho nhà phát triển đầu tiên sẽ được gọi là "phiên bản Công cụ cho nhà phát triển thứ hai".

Ảnh chụp màn hình về một thực thể của Công cụ cho nhà phát triển đang kiểm tra các phần tử trong chính Công cụ cho nhà phát triển.
Công cụ cho nhà phát triển trên công cụ cho nhà phát triển: kiểm tra Công cụ cho nhà phát triển bằng Công cụ cho nhà phát triển.

Trên phiên bản thứ hai của Công cụ cho nhà phát triển, bảng điều khiển Hiệu suất (sẽ được gọi là bảng hoạt động từ đây trở đi) sẽ quan sát phiên bản Công cụ cho nhà phát triển đầu tiên để tạo lại tình huống rồi tải một cấu hình.

Ở phiên bản thứ hai của Công cụ cho nhà phát triển, quá trình ghi trực tiếp được bắt đầu, còn ở phiên bản đầu tiên, hồ sơ được tải từ một tệp trên ổ đĩa. Một tệp lớn được tải để lập hồ sơ chính xác hiệu suất của việc xử lý dữ liệu đầu vào có kích thước lớn. Khi cả hai trường hợp tải xong, dữ liệu phân tích hiệu suất (thường gọi là trace) sẽ được hiển thị trong phiên bản Công cụ cho nhà phát triển thứ hai của bảng điều khiển hiệu suất đang tải một hồ sơ.

Trạng thái ban đầu: xác định cơ hội cải thiện

Sau khi quá trình tải hoàn tất, bạn có thể thấy những nội dung sau đây trên thực thể bảng điều khiển hiệu suất thứ hai trong ảnh chụp màn hình tiếp theo. Tập trung vào hoạt động của luồng chính, hiển thị trong kênh có nhãn Main. Bạn có thể thấy rằng có năm nhóm hoạt động lớn trong biểu đồ hình ngọn lửa. Các nhiệm vụ này bao gồm những nhiệm vụ mà quá trình tải mất nhiều thời gian nhất. Tổng thời gian của những việc này là khoảng 10 giây. Trong ảnh chụp màn hình sau đây, bảng hiệu suất được dùng để tập trung vào từng nhóm hoạt động này và xem bạn có thể tìm thấy những gì.

Ảnh chụp màn hình về bảng hiệu suất trong Công cụ cho nhà phát triển khi kiểm tra việc tải dấu vết hiệu suất trong bảng điều khiển hiệu suất của một phiên bản Công cụ cho nhà phát triển khác. Quá trình tải hồ sơ mất khoảng 10 giây. Thời gian này chủ yếu được chia thành 5 nhóm hoạt động chính.

Nhóm hoạt động đầu tiên: công việc không cần thiết

Rõ ràng nhóm hoạt động đầu tiên là mã cũ vẫn chạy nhưng không thực sự cần thiết. Về cơ bản, mọi thứ trong khối màu xanh lục có nhãn processThreadEvents là lãng phí công sức. Bạn đã giành chiến thắng nhanh chóng. Việc xoá lệnh gọi hàm đó tiết kiệm được khoảng 1,5 giây. Tuyệt!

Nhóm hoạt động thứ hai

Trong nhóm hoạt động thứ hai, giải pháp không đơn giản như với nhóm đầu tiên. buildProfileCalls mất khoảng 0, 5 giây và tác vụ đó không thể tránh được.

Ảnh chụp màn hình về bảng điều khiển hiệu suất trong Công cụ cho nhà phát triển đang kiểm tra một phiên bản bảng điều khiển hiệu suất khác. Một tác vụ được liên kết với hàm buildProfilecall mất khoảng 0,5 giây.

Vì tò mò, chúng tôi đã bật tuỳ chọn Memory (Bộ nhớ) trong bảng điều khiển perf để điều tra thêm và nhận thấy hoạt động buildProfileCalls cũng đang sử dụng nhiều bộ nhớ. Tại đây, bạn có thể thấy cách biểu đồ đường màu xanh dương đột ngột chuyển sang thời điểm buildProfileCalls đang chạy, điều này cho thấy khả năng rò rỉ bộ nhớ.

Ảnh chụp màn hình trình phân tích bộ nhớ trong Công cụ cho nhà phát triển đánh giá mức sử dụng bộ nhớ của bảng điều khiển hiệu suất. Trình kiểm tra đề xuất rằng hàm buildProfilecall phải chịu trách nhiệm về sự cố rò rỉ bộ nhớ.

Để theo dõi nghi ngờ này, chúng tôi đã sử dụng bảng điều khiển Bộ nhớ (một bảng điều khiển khác trong Công cụ cho nhà phát triển, khác với ngăn Bộ nhớ trong bảng điều khiển hiệu suất) để điều tra. Trong bảng điều khiển Memory (Bộ nhớ), lựa chọn loại lập hồ sơ "Allocationsampling" (Lấy mẫu phân bổ) được chọn, ghi lại ảnh chụp nhanh của vùng nhớ khối xếp cho bảng điều khiển perf đang tải hồ sơ CPU.

Ảnh chụp màn hình trạng thái ban đầu của trình phân tích bộ nhớ. Tuỳ chọn 'allocationsampling' (lấy mẫu phân bổ) được làm nổi bật bằng hộp màu đỏ và cho biết đây là tuỳ chọn tốt nhất cho việc phân tích bộ nhớ JavaScript.

Ảnh chụp màn hình sau đây cho thấy ảnh chụp nhanh của vùng nhớ khối xếp đã được thu thập.

Ảnh chụp màn hình trình phân tích bộ nhớ, trong đó thao tác Đặt dựa trên bộ nhớ đã được chọn.

Trên ảnh chụp nhanh của vùng nhớ khối xếp này, chúng tôi quan sát thấy lớp Set đang sử dụng nhiều bộ nhớ. Khi kiểm tra các điểm gọi, chúng ta thấy rằng chúng ta đã không cần thiết phải chỉ định thuộc tính loại Set cho các đối tượng được tạo trong khối lượng lớn. Chi phí này ngày càng tăng và tiêu tốn nhiều bộ nhớ, đến mức ứng dụng thường gặp sự cố khi nhập dữ liệu đầu vào lớn.

Tập hợp rất hữu ích cho việc lưu trữ các mục duy nhất và cung cấp các thao tác sử dụng tính duy nhất của nội dung, như loại bỏ tập dữ liệu trùng lặp và cung cấp hoạt động tra cứu hiệu quả hơn. Tuy nhiên, những tính năng đó không cần thiết vì dữ liệu được lưu trữ được đảm bảo là duy nhất từ nguồn. Do đó, tập hợp là không cần thiết ngay từ đầu. Để cải thiện khả năng phân bổ bộ nhớ, loại thuộc tính đã được thay đổi từ Set thành một mảng thuần tuý. Sau khi áp dụng thay đổi này, hệ thống đã chụp một ảnh chụp nhanh của vùng nhớ khối xếp khác và quan sát thấy mức phân bổ bộ nhớ giảm. Mặc dù không đạt được những cải thiện đáng kể về tốc độ nhờ thay đổi này, nhưng lợi ích thứ hai là ứng dụng gặp sự cố ít thường xuyên hơn.

Ảnh chụp màn hình trình phân tích bộ nhớ. Thao tác Đặt dựa trên bộ nhớ cần nhiều bộ nhớ trước đây đã được thay đổi để sử dụng một mảng thuần tuý, giúp giảm đáng kể chi phí bộ nhớ.

Nhóm hoạt động thứ ba: cân nhắc đánh đổi cấu trúc dữ liệu

Phần thứ ba rất đặc biệt: bạn có thể thấy trong biểu đồ hình ngọn lửa bao gồm các cột hẹp nhưng cao, biểu thị các lệnh gọi hàm sâu và các đệ quy sâu trong trường hợp này. Tổng cộng, phần này kéo dài khoảng 1,4 giây. Khi xem ở cuối phần này, rõ ràng chiều rộng của các cột này đã được xác định theo thời lượng của một hàm: appendEventAtLevel. Điều này cho thấy đây có thể là một điểm tắc nghẽn

Trong việc triển khai hàm appendEventAtLevel, có một điều nổi bật. Đối với mỗi mục dữ liệu trong mục nhập (được gọi trong mã là "sự kiện"), một mục đã được thêm vào bản đồ theo dõi vị trí dọc của các mục nhập dòng thời gian. Đây là vấn đề vì số lượng mục đã được lưu trữ rất lớn. Maps hoạt động nhanh chóng đối với việc tra cứu dựa trên khoá, nhưng lợi thế này không phải là miễn phí. Khi một bản đồ phát triển lớn hơn, việc thêm dữ liệu vào bản đồ có thể trở nên tốn kém hơn do việc băm lại. Chi phí này sẽ tăng lên đáng kể khi một lượng lớn các mục liên tiếp được thêm vào bản đồ.

/**
 * Adds an event to the flame chart data at a defined vertical level.
 */
function appendEventAtLevel (event, level) {
  // ...

  const index = data.length;
  data.push(event);
  this.indexForEventMap.set(event, index);

  // ...
}

Chúng tôi đã thử nghiệm một phương pháp khác mà không yêu cầu thêm một mục vào bản đồ cho mỗi mục nhập trong biểu đồ hình ngọn lửa. Cải tiến rất đáng kể, xác nhận rằng điểm tắc nghẽn thực sự có liên quan đến chi phí phát sinh từ việc thêm tất cả dữ liệu vào bản đồ. Thời gian của nhóm hoạt động giảm từ khoảng 1,4 giây xuống còn khoảng 200 mili giây.

Trước:

Ảnh chụp màn hình về bảng điều khiển hiệu suất trước khi thực hiện tối ưu hoá cho hàm conversionEventAtLevel. Tổng thời gian để chạy hàm này là 1.372,51 mili giây.

Sau:

Ảnh chụp màn hình bảng điều khiển hiệu suất sau khi thực hiện tối ưu hoá cho hàm ExtensionEventAtLevel. Tổng thời gian để chạy hàm này là 207,2 mili giây.

Nhóm hoạt động thứ tư: trì hoãn công việc không quan trọng và lưu dữ liệu vào bộ nhớ đệm để ngăn công việc trùng lặp

Khi phóng to trên cửa sổ này, bạn có thể thấy có 2 khối lệnh gọi hàm gần như giống hệt nhau. Bằng cách nhìn vào tên của hàm được gọi, bạn có thể suy ra rằng các khối này bao gồm mã xây dựng cây (ví dụ: với các tên như refreshTree hoặc buildChildren). Trên thực tế, mã liên quan là mã tạo thành phần hiển thị dạng cây trong ngăn dưới cùng của bảng điều khiển. Điều thú vị là các chế độ xem dạng cây này không xuất hiện ngay sau khi tải. Thay vào đó, người dùng cần chọn chế độ xem dạng cây (các thẻ "Dưới cùng", "Cây cuộc gọi" và "Nhật ký sự kiện" trong ngăn) để hiển thị cây. Hơn nữa, như bạn có thể thấy trong ảnh chụp màn hình, quy trình xây dựng cây đã được thực thi hai lần.

Ảnh chụp màn hình bảng hiệu suất cho thấy một số công việc lặp lại, được thực hiện ngay cả khi không cần thiết. Những tác vụ này có thể bị trì hoãn để thực thi theo yêu cầu, thay vì trước đó.

Chúng tôi đã phát hiện thấy hai vấn đề với bức ảnh này:

  1. Một nhiệm vụ không quan trọng đã cản trở hiệu suất của thời gian tải. Không phải lúc nào người dùng cũng cần kết quả của tệp. Do đó, nhiệm vụ này không quan trọng đối với việc tải hồ sơ.
  2. Kết quả của những nhiệm vụ này không được lưu vào bộ nhớ đệm. Đó là lý do các cây được tính toán hai lần, mặc dù dữ liệu không thay đổi.

Chúng tôi bắt đầu bằng việc trì hoãn việc tính toán dạng cây cho thời điểm người dùng mở chế độ xem dạng cây theo cách thủ công. Chỉ khi đó mới đáng trả tiền để tạo ra những cái cây này. Tổng thời gian chạy hai lần này là khoảng 3,4 giây, vì vậy việc trì hoãn nó đã tạo ra sự khác biệt đáng kể trong thời gian tải. Chúng tôi cũng đang xem xét việc lưu các loại tác vụ này vào bộ nhớ đệm.

Nhóm hoạt động thứ năm: tránh hệ phân cấp cuộc gọi phức tạp khi có thể

Khi xem xét kỹ nhóm này, chúng ta thấy rõ ràng là một chuỗi cuộc gọi cụ thể đã được gọi nhiều lần. Cùng một hoa văn xuất hiện 6 lần ở những nơi khác nhau trong biểu đồ ngọn lửa, và tổng thời lượng của cửa sổ này là khoảng 2,4 giây!

Ảnh chụp màn hình bảng điều khiển hiệu suất cho thấy 6 lệnh gọi hàm riêng biệt để tạo cùng một bản đồ thu nhỏ theo dõi, mỗi lệnh gọi có ngăn xếp lệnh gọi sâu.

Đoạn mã có liên quan được gọi nhiều lần là phần xử lý dữ liệu cần hiển thị trên "minimap" (tổng quan về hoạt động theo dòng thời gian ở đầu bảng điều khiển). Chúng tôi đã không rõ lý do tại sao việc này xảy ra nhiều lần, nhưng chắc chắn không nhất thiết phải xảy ra đến 6 lần! Trên thực tế, kết quả của mã vẫn sẽ duy trì nếu không có hồ sơ nào khác được tải. Theo lý thuyết, mã chỉ nên chạy một lần.

Sau khi điều tra, chúng tôi nhận thấy mã liên quan đã được gọi do nhiều phần trong quy trình tải đã gọi trực tiếp hoặc gián tiếp hàm tính toán bản đồ thu nhỏ. Nguyên nhân là do tính phức tạp của biểu đồ lệnh gọi của chương trình đã thay đổi theo thời gian và nhiều phần phụ thuộc hơn vào mã này đã được thêm vào mà không biết. Không có cách khắc phục nhanh cho vấn đề này. Cách giải quyết vấn đề này tuỳ thuộc vào cấu trúc của cơ sở mã đang được đề cập. Trong trường hợp này, chúng ta phải giảm độ phức tạp của hệ phân cấp lệnh gọi một chút và thêm một bước kiểm tra để ngăn thực thi mã nếu dữ liệu đầu vào vẫn không thay đổi. Sau khi triển khai, chúng tôi có triển vọng về tiến trình như sau:

Ảnh chụp màn hình bảng điều khiển hiệu suất cho thấy 6 lệnh gọi hàm riêng biệt để tạo cùng một bản đồ thu nhỏ dấu vết (được giảm xuống chỉ còn 2 lần).

Xin lưu ý rằng quá trình kết xuất bản đồ thu nhỏ sẽ diễn ra hai lần chứ không phải một lần. Điều này là do có hai bản đồ thu nhỏ được vẽ cho mỗi hồ sơ: một cho phần tổng quan ở đầu bảng điều khiển và một cho trình đơn thả xuống chọn hồ sơ hiện đang hiển thị từ lịch sử (mỗi mục trong trình đơn này chứa một thông tin tổng quan về hồ sơ được chọn). Tuy nhiên, hai đoạn mã này có cùng nội dung nên có thể sử dụng lại cho cái còn lại.

Vì các bản đồ nhỏ này đều là hình ảnh được vẽ trên canvas, nên bạn cần sử dụng tiện ích canvas drawImage, sau đó chỉ chạy mã một lần để tiết kiệm thêm thời gian. Kết quả của nỗ lực này là thời lượng của nhóm đã giảm từ 2, 4 giây xuống còn 140 mili giây.

Kết luận

Sau khi áp dụng tất cả các bản sửa lỗi này (và một số bản sửa lỗi nhỏ khác ở vài nơi), sự thay đổi tiến trình tải hồ sơ như sau:

Trước:

Ảnh chụp màn hình bảng hiệu suất cho thấy quá trình tải dấu vết trước khi tối ưu hoá. Quá trình này mất khoảng 10 giây.

Sau:

Ảnh chụp màn hình bảng hiệu suất cho thấy quá trình tải dấu vết sau khi tối ưu hoá. Quá trình này hiện chỉ mất khoảng 2 giây.

Thời gian tải sau khi cải tiến là 2 giây, nghĩa là mức độ cải thiện khoảng 80% đã đạt được với nỗ lực tương đối thấp, vì hầu hết những gì được thực hiện đều bao gồm các bản sửa lỗi nhanh. Tất nhiên, việc xác định đúng việc cần làm ban đầu là yếu tố then chốt, và bảng điều khiển hiệu suất chính là công cụ phù hợp để làm việc này.

Ngoài ra, xin lưu ý rằng các số liệu này dành riêng cho hồ sơ đang được dùng làm đối tượng nghiên cứu. Chúng tôi thấy hồ sơ đó thú vị vì hồ sơ đó đặc biệt lớn. Tuy nhiên, vì quy trình xử lý cho mọi hồ sơ đều giống nhau, nên điểm cải tiến đáng kể đạt được sẽ áp dụng cho mọi hồ sơ được tải trong bảng điều khiển hiệu suất.

Cướp lại bóng

Có một số bài học rút ra từ những kết quả này về việc tối ưu hoá hiệu suất của ứng dụng:

1. Sử dụng các công cụ phân tích để xác định các mẫu hiệu suất trong thời gian chạy

Các công cụ phân tích tài nguyên cực kỳ hữu ích trong việc nắm bắt những gì đang xảy ra trong khi ứng dụng đang chạy, đặc biệt là trong việc xác định các cơ hội cải thiện hiệu suất. Bảng điều khiển hiệu suất trong Công cụ của Chrome cho nhà phát triển là một lựa chọn tuyệt vời cho các ứng dụng web vì đây là công cụ phân tích web gốc trong trình duyệt và được liên tục duy trì để luôn cập nhật các tính năng mới nhất của nền tảng web. Ngoài ra, giờ đây công cụ này đã nhanh hơn đáng kể! 😉

Hãy dùng các mẫu có thể dùng làm tải công việc tiêu biểu và xem bạn có thể tìm thấy gì!

2. Tránh hệ phân cấp lệnh gọi phức tạp

Khi có thể, tránh tạo biểu đồ cuộc gọi quá phức tạp. Với hệ phân cấp lệnh gọi phức tạp, việc triển khai hồi quy hiệu suất trở nên dễ dàng và khó hiểu lý do mã của bạn đang chạy như vậy, điều này khiến bạn khó đạt được mục tiêu cải thiện.

3. Xác định công việc không cần thiết

Các cơ sở mã cũ thường chứa các mã không còn cần thiết nữa. Trong trường hợp của chúng tôi, mã cũ và không cần thiết đang chiếm một phần đáng kể trong tổng thời gian tải. Việc loại bỏ nó là mối quan hệ ít ỏi nhất.

4. Sử dụng cấu trúc dữ liệu hợp lý

Hãy dùng cấu trúc dữ liệu để tối ưu hoá hiệu suất, đồng thời hiểu rõ chi phí và ưu nhược điểm mà mỗi loại cấu trúc dữ liệu mang lại khi quyết định nên sử dụng cấu trúc nào. Điều này không chỉ phức tạp về không gian của chính cấu trúc dữ liệu mà còn phức tạp về thời gian của các thao tác áp dụng.

5. Lưu kết quả vào bộ nhớ đệm để tránh công việc trùng lặp cho các thao tác phức tạp hoặc lặp lại

Nếu việc thực thi thao tác này tốn kém, bạn nên lưu trữ kết quả của thao tác đó cho lần tiếp theo cần đến. Việc này cũng hợp lý nếu thao tác được thực hiện nhiều lần – ngay cả khi từng lần riêng lẻ không quá tốn kém.

6. Trì hoãn công việc không quan trọng

Nếu đầu ra của một tác vụ chưa cần ngay lập tức và quá trình thực thi tác vụ đang mở rộng đường dẫn quan trọng, hãy cân nhắc trì hoãn nó bằng cách gọi từng phần (lazy) khi thực sự cần thiết.

7. Sử dụng thuật toán hiệu quả đối với dữ liệu đầu vào lớn

Đối với các dữ liệu đầu vào lớn, các thuật toán có độ phức tạp thời gian tối ưu trở nên rất quan trọng. Chúng tôi chưa xem xét danh mục này trong ví dụ này, nhưng hầu như không thể bỏ qua tầm quan trọng của danh mục này.

8. Phần bổ sung: đo điểm chuẩn cho quy trình của bạn

Để đảm bảo mã đang phát triển của bạn luôn nhanh chóng, bạn nên theo dõi hành vi và so sánh mã đó với các tiêu chuẩn. Bằng cách này, bạn chủ động xác định sự hồi quy và cải thiện độ tin cậy tổng thể, giúp bạn chuẩn bị để đạt được thành công lâu dài.