Quản lý bộ nhớ hiệu quả ở quy mô Gmail

John McCutchan
John McCutchan
Loreena Lee
Loreena Lee

Giới thiệu

Mặc dù JavaScript sử dụng tính năng thu gom rác để quản lý bộ nhớ tự động, nhưng tính năng này không thay thế được việc quản lý bộ nhớ hiệu quả trong các ứng dụng. Các ứng dụng JavaScript gặp phải các vấn đề liên quan đến bộ nhớ giống như các ứng dụng gốc, chẳng hạn như rò rỉ bộ nhớ và tình trạng tăng kích thước, nhưng cũng phải xử lý các điểm tạm dừng thu gom rác. Các ứng dụng quy mô lớn như Gmail cũng gặp phải những vấn đề tương tự như các ứng dụng nhỏ hơn. Hãy đọc tiếp để tìm hiểu cách nhóm Gmail sử dụng Công cụ của Chrome cho nhà phát triển để xác định, tách biệt và khắc phục các sự cố về bộ nhớ.

Phiên Google I/O 2013

Chúng tôi đã trình bày tài liệu này tại Google I/O 2013. Hãy xem video dưới đây:

Gmail, chúng ta có vấn đề…

Nhóm Gmail đang gặp phải một vấn đề nghiêm trọng. Chúng tôi thường xuyên nghe thấy những câu chuyện về các thẻ Gmail tiêu thụ nhiều gigabyte bộ nhớ trên máy tính xách tay và máy tính để bàn bị hạn chế tài nguyên, thường dẫn đến việc toàn bộ trình duyệt bị sập. Những câu chuyện về CPU bị ghim ở mức 100%, ứng dụng không phản hồi và các thẻ Chrome buồn bã ("He's dead, Jim."). Nhóm này không biết phải bắt đầu chẩn đoán vấn đề như thế nào, chứ chưa nói đến việc khắc phục. Họ không biết vấn đề này phổ biến đến mức nào và các công cụ hiện có không mở rộng được cho các ứng dụng lớn. Nhóm này đã hợp tác với các nhóm Chrome để cùng nhau phát triển các kỹ thuật mới nhằm phân loại các vấn đề về bộ nhớ, cải thiện các công cụ hiện có và cho phép thu thập dữ liệu bộ nhớ từ thực địa. Tuy nhiên, trước khi tìm hiểu các công cụ, hãy cùng tìm hiểu kiến thức cơ bản về cách quản lý bộ nhớ JavaScript.

Kiến thức cơ bản về quản lý bộ nhớ

Trước khi có thể quản lý bộ nhớ hiệu quả trong JavaScript, bạn phải hiểu rõ các kiến thức cơ bản. Phần này sẽ đề cập đến các loại dữ liệu gốc, biểu đồ đối tượng và cung cấp định nghĩa chung về tình trạng tăng cường bộ nhớ và rò rỉ bộ nhớ trong JavaScript. Bộ nhớ trong JavaScript có thể được khái niệm hoá dưới dạng biểu đồ và do đó, Lý thuyết đồ thị đóng vai trò trong việc quản lý bộ nhớ JavaScript và Trình phân tích vùng nhớ khối xếp.

Kiểu dữ liệu gốc

JavaScript có ba kiểu dữ liệu gốc:

  1. Số (ví dụ: 4, 3,14159)
  2. Boolean (đúng hoặc sai)
  3. Chuỗi ("Hello World")

Các loại dữ liệu gốc này không thể tham chiếu đến bất kỳ giá trị nào khác. Trong biểu đồ đối tượng, các giá trị này luôn là nút lá hoặc nút kết thúc, nghĩa là chúng không bao giờ có cạnh đi ra.

Chỉ có một loại vùng chứa: Đối tượng. Trong JavaScript, Đối tượng là một mảng liên kết. Đối tượng không trống là một nút bên trong có các cạnh đi ra đến các giá trị (nút) khác.

Mảng thì sao?

Mảng trong JavaScript thực sự là một Đối tượng có khoá dạng số. Đây là cách đơn giản hoá vì môi trường thời gian chạy JavaScript sẽ tối ưu hoá các Đối tượng giống Mảng và biểu thị các đối tượng đó dưới dạng mảng.

Thuật ngữ

  1. Giá trị – Một thực thể của loại dữ liệu nguyên gốc, Đối tượng, Mảng, v.v.
  2. Biến – Tên tham chiếu đến một giá trị.
  3. Thuộc tính – Tên trong Đối tượng tham chiếu đến một giá trị.

Biểu đồ đối tượng

Tất cả giá trị trong JavaScript đều là một phần của biểu đồ đối tượng. Biểu đồ bắt đầu bằng các phần tử gốc, ví dụ: đối tượng cửa sổ. Bạn không thể kiểm soát thời gian hoạt động của các thư mục gốc GC vì các thư mục này do trình duyệt tạo và bị huỷ khi trang không tải. Biến toàn cục thực sự là các thuộc tính trên cửa sổ.

Biểu đồ đối tượng

Khi nào một giá trị trở thành rác?

Một giá trị trở thành rác khi không có đường dẫn từ gốc đến giá trị đó. Nói cách khác, bắt đầu từ gốc và tìm kiếm tất cả các thuộc tính và biến của Đối tượng đang hoạt động trong khung ngăn xếp, không thể truy cập vào một giá trị, giá trị đó đã trở thành rác.

Biểu đồ rác

Lỗi rò rỉ bộ nhớ trong JavaScript là gì?

Sự cố rò rỉ bộ nhớ trong JavaScript thường xảy ra khi có các nút DOM không thể truy cập được từ cây DOM của trang, nhưng vẫn được đối tượng JavaScript tham chiếu. Mặc dù các trình duyệt hiện đại ngày càng khó bị rò rỉ do vô tình, nhưng việc này vẫn dễ dàng hơn bạn nghĩ. Giả sử bạn thêm một phần tử vào cây DOM như sau:

email.message = document.createElement("div");
displayList.appendChild(email.message);

Sau đó, bạn xoá phần tử này khỏi danh sách hiển thị:

displayList.removeAllChildren();

Miễn là email tồn tại, phần tử DOM được tham chiếu bởi thông báo sẽ không bị xoá, mặc dù phần tử này hiện đã tách khỏi cây DOM của trang.

Tình trạng tăng kích thước là gì?

Trang của bạn phù phiếm khi bạn sử dụng nhiều bộ nhớ hơn mức cần thiết để có tốc độ trang tối ưu. Tình trạng rò rỉ bộ nhớ cũng gián tiếp gây ra tình trạng tăng kích thước ứng dụng nhưng đó không phải là do thiết kế. Bộ nhớ đệm ứng dụng không có giới hạn kích thước là một nguồn thường gặp gây ra tình trạng tăng kích thước bộ nhớ. Ngoài ra, trang của bạn có thể bị đầy dữ liệu lưu trữ, chẳng hạn như dữ liệu pixel được tải từ hình ảnh.

Thu gom rác là gì?

Thu gom rác là cách lấy lại bộ nhớ trong JavaScript. Trình duyệt sẽ quyết định thời điểm thực hiện việc này. Trong quá trình thu thập, tất cả hoạt động thực thi tập lệnh trên trang của bạn sẽ bị tạm ngưng trong khi các giá trị trực tiếp được phát hiện bằng cách duyệt qua biểu đồ đối tượng bắt đầu từ gốc GC. Tất cả các giá trị không có thể truy cập đều được phân loại là rác. Trình quản lý bộ nhớ sẽ thu hồi bộ nhớ cho các giá trị rác.

Thông tin chi tiết về trình thu gom rác V8

Để hiểu rõ hơn về cách thu gom rác, hãy cùng tìm hiểu chi tiết về trình thu gom rác V8. V8 sử dụng trình thu thập rác thế hệ. Bộ nhớ được chia thành hai thế hệ: mới và cũ. Quá trình phân bổ và thu gom trong thế hệ mới diễn ra nhanh chóng và thường xuyên. Quá trình phân bổ và thu thập trong thế hệ cũ diễn ra chậm hơn và ít thường xuyên hơn.

Bộ thu thập thế hệ

V8 sử dụng trình thu thập rác hai thế hệ. Độ tuổi của một giá trị được xác định là số byte được phân bổ kể từ khi được phân bổ. Trong thực tế, tuổi của một giá trị thường được ước tính bằng số lượng bộ sưu tập thế hệ mới mà giá trị đó vẫn tồn tại. Sau khi một giá trị đủ cũ, giá trị đó sẽ được chuyển sang thế hệ cũ.

Trong thực tế, các giá trị được phân bổ mới không tồn tại lâu. Một nghiên cứu về các chương trình Smalltalk cho thấy chỉ 7% giá trị tồn tại sau khi thu thập một thế hệ mới. Các nghiên cứu tương tự trên các thời gian chạy cho thấy rằng trung bình từ 90% đến 70% giá trị được phân bổ mới không bao giờ được chuyển sang thế hệ cũ.

Thế hệ trẻ

Vùng nhớ khối xếp thế hệ mới trong V8 được chia thành hai không gian, được đặt tên là from và to. Bộ nhớ được phân bổ từ không gian đến. Quá trình phân bổ diễn ra rất nhanh cho đến khi không gian to đầy, tại thời điểm đó, một hoạt động thu thập thế hệ mới sẽ được kích hoạt. Trước tiên, bộ sưu tập thế hệ mới hoán đổi không gian từ và đến, không gian đến cũ (hiện là không gian từ) được quét và tất cả các giá trị trực tiếp được sao chép vào không gian đến hoặc được chuyển vào thế hệ cũ. Một bộ sưu tập thế hệ mới thông thường sẽ mất khoảng 10 mili giây (ms).

Theo trực giác, bạn nên hiểu rằng mỗi lần phân bổ mà ứng dụng thực hiện sẽ khiến bạn gần hết không gian hơn và phải tạm dừng GC. Các nhà phát triển trò chơi, hãy lưu ý: để đảm bảo thời gian khung hình là 16 mili giây (bắt buộc để đạt được 60 khung hình/giây), ứng dụng của bạn phải không phân bổ, vì một bộ sưu tập thế hệ mới sẽ chiếm phần lớn thời gian khung hình.

Vùng nhớ khối xếp thế hệ mới

Thế hệ cũ

Vùng nhớ khối xếp thế hệ cũ trong V8 sử dụng thuật toán đánh dấu-rút gọn để thu thập. Việc phân bổ thế hệ cũ xảy ra bất cứ khi nào một giá trị được chuyển từ thế hệ mới sang thế hệ cũ. Bất cứ khi nào một quá trình thu gom rác thế hệ cũ xảy ra, quá trình thu gom rác thế hệ mới cũng được thực hiện. Ứng dụng của bạn sẽ tạm dừng theo thứ tự giây. Trong thực tế, điều này là chấp nhận được vì các bộ sưu tập thế hệ cũ không thường xuyên xuất hiện.

Tóm tắt về GC V8

Tính năng quản lý bộ nhớ tự động bằng tính năng thu gom rác rất hữu ích cho năng suất của nhà phát triển, nhưng mỗi khi phân bổ một giá trị, bạn lại tiến gần hơn đến việc tạm dừng thu gom rác. Việc tạm dừng thu gom rác có thể làm hỏng trải nghiệm người dùng ứng dụng bằng cách tạo ra hiện tượng giật. Giờ đây, khi đã hiểu cách JavaScript quản lý bộ nhớ, bạn có thể đưa ra lựa chọn phù hợp cho ứng dụng của mình.

Khắc phục sự cố với Gmail

Trong năm qua, nhiều tính năng và bản sửa lỗi đã được đưa vào Công cụ của Chrome cho nhà phát triển, giúp các tính năng này trở nên mạnh mẽ hơn bao giờ hết. Ngoài ra, chính trình duyệt cũng đã thực hiện một thay đổi quan trọng đối với API performance.memory, cho phép Gmail và mọi ứng dụng khác thu thập số liệu thống kê về bộ nhớ từ trường này. Nhờ có những công cụ tuyệt vời này, việc từng có vẻ như không thể thực hiện được đã nhanh chóng trở thành một trò chơi thú vị để truy tìm thủ phạm.

Công cụ và kỹ thuật

Dữ liệu trường và API performance.memory

Kể từ Chrome 22, performance.memory API được bật theo mặc định. Đối với các ứng dụng chạy trong thời gian dài như Gmail, dữ liệu từ người dùng thực là vô giá. Thông tin này cho phép chúng tôi phân biệt giữa người dùng thường xuyên (những người dành 8 đến 16 giờ mỗi ngày cho Gmail, nhận hàng trăm thư mỗi ngày) với người dùng thông thường (những người dành vài phút mỗi ngày cho Gmail, nhận khoảng một chục thư mỗi tuần).

API này trả về 3 phần dữ liệu:

  1. jsHeapSizeLimit – Dung lượng bộ nhớ (tính bằng byte) mà vùng nhớ khối xếp JavaScript được giới hạn.
  2. totalJSHeapSize – Dung lượng bộ nhớ (tính bằng byte) mà vùng nhớ khối xếp JavaScript đã phân bổ, bao gồm cả không gian trống.
  3. usedJSHeapSize – Dung lượng bộ nhớ (tính bằng byte) hiện đang được sử dụng.

Một điều cần lưu ý là API này trả về các giá trị bộ nhớ cho toàn bộ quá trình của Chrome. Mặc dù không phải là chế độ mặc định, nhưng trong một số trường hợp, Chrome có thể mở nhiều thẻ trong cùng một quy trình kết xuất. Điều này có nghĩa là các giá trị do performance.memory trả về có thể chứa mức sử dụng bộ nhớ của các thẻ trình duyệt khác ngoài thẻ chứa ứng dụng của bạn.

Đo lường bộ nhớ trên quy mô lớn

Gmail đã đo lường JavaScript của họ để sử dụng API performance.memory nhằm thu thập thông tin về bộ nhớ khoảng 30 phút một lần. Vì nhiều người dùng Gmail để ứng dụng mở trong nhiều ngày, nên nhóm nghiên cứu có thể theo dõi mức tăng bộ nhớ theo thời gian cũng như số liệu thống kê tổng thể về mức sử dụng bộ nhớ. Trong vòng vài ngày sau khi đo lường Gmail để thu thập thông tin về bộ nhớ từ một mẫu người dùng ngẫu nhiên, nhóm đã có đủ dữ liệu để hiểu mức độ phổ biến của các vấn đề về bộ nhớ đối với người dùng trung bình. Họ đặt một đường cơ sở và sử dụng luồng dữ liệu đến để theo dõi tiến trình hướng tới mục tiêu giảm mức sử dụng bộ nhớ. Cuối cùng, dữ liệu này cũng sẽ được dùng để phát hiện mọi sự hồi quy về bộ nhớ.

Ngoài mục đích theo dõi, các phép đo tại trường cũng cung cấp thông tin chi tiết sâu sắc về mối tương quan giữa mức sử dụng bộ nhớ và hiệu suất của ứng dụng. Trái với quan niệm phổ biến rằng "bộ nhớ càng lớn thì hiệu suất càng cao", nhóm Gmail nhận thấy rằng dung lượng bộ nhớ càng lớn thì độ trễ càng lâu đối với các thao tác phổ biến trên Gmail. Nhờ thông tin này, họ có động lực hơn bao giờ hết để kiểm soát mức sử dụng bộ nhớ.

Đo lường bộ nhớ trên quy mô lớn

Xác định vấn đề về bộ nhớ bằng Dòng thời gian của Công cụ cho nhà phát triển

Bước đầu tiên để giải quyết mọi vấn đề về hiệu suất là chứng minh rằng vấn đề tồn tại, tạo một chương trình kiểm thử có thể tái tạo và đo lường cơ sở của vấn đề. Nếu không có chương trình có thể tái tạo, bạn không thể đo lường vấn đề một cách đáng tin cậy. Nếu không có số liệu đo lường cơ sở, bạn sẽ không biết hiệu suất của mình đã cải thiện bao nhiêu.

Bảng Timeline (Tiến trình) của DevTools là một ứng cử viên lý tưởng để chứng minh rằng vấn đề tồn tại. Báo cáo này cung cấp thông tin tổng quan đầy đủ về thời gian người dùng dành cho việc tải và tương tác với ứng dụng hoặc trang web của bạn. Tất cả sự kiện, từ việc tải tài nguyên đến phân tích cú pháp JavaScript, tính toán kiểu, tạm dừng thu gom rác và vẽ lại đều được lập biểu đồ trên dòng thời gian. Để điều tra các vấn đề về bộ nhớ, bảng điều khiển Dòng thời gian cũng có Chế độ bộ nhớ theo dõi tổng bộ nhớ được phân bổ, số lượng nút DOM, số lượng đối tượng cửa sổ và số lượng trình nghe sự kiện được phân bổ.

Chứng minh sự cố tồn tại

Bắt đầu bằng cách xác định một chuỗi hành động mà bạn nghi ngờ là rò rỉ bộ nhớ. Bắt đầu ghi dòng thời gian và thực hiện trình tự các hành động. Sử dụng nút thùng rác ở dưới cùng để buộc thu gom rác toàn bộ. Nếu sau một vài lần lặp lại, bạn thấy biểu đồ có hình dạng răng cưa, thì bạn đang phân bổ nhiều đối tượng có thời gian tồn tại ngắn. Tuy nhiên, nếu trình tự các hành động không dự kiến sẽ dẫn đến bất kỳ bộ nhớ nào được giữ lại và số lượng nút DOM không giảm xuống đường cơ sở mà bạn bắt đầu, thì bạn có lý do chính đáng để nghi ngờ có rò rỉ.

Biểu đồ hình răng cưa

Sau khi xác nhận rằng có vấn đề, bạn có thể yêu cầu trợ giúp để xác định nguồn gốc của vấn đề thông qua Trình phân tích mảng xếp trong DevTools.

Tìm lỗi rò rỉ bộ nhớ bằng Trình phân tích vùng nhớ khối xếp của DevTools

Bảng điều khiển Trình phân tích tài nguyên cung cấp cả trình phân tích CPU và trình phân tích vùng nhớ khối xếp. Phân tích tài nguyên vùng nhớ khối xếp hoạt động bằng cách chụp nhanh biểu đồ đối tượng. Trước khi chụp ảnh nhanh, cả thế hệ mới và cũ đều được thu gom rác. Nói cách khác, bạn sẽ chỉ thấy các giá trị còn hiệu lực tại thời điểm chụp ảnh nhanh.

Có quá nhiều chức năng trong trình phân tích tài nguyên vùng nhớ khối xếp để có thể trình bày đầy đủ trong bài viết này, nhưng bạn có thể xem tài liệu chi tiết trên trang web dành cho nhà phát triển Chrome. Chúng ta sẽ tập trung vào trình phân tích tài nguyên Phân bổ vùng nhớ khối xếp.

Sử dụng Trình phân tích tài nguyên phân bổ vùng nhớ khối xếp

Trình phân tích tài nguyên phân bổ vùng nhớ khối xếp kết hợp thông tin tổng quan chi tiết của Trình phân tích tài nguyên vùng nhớ khối xếp với tính năng cập nhật và theo dõi gia tăng của bảng điều khiển Dòng thời gian. Mở bảng điều khiển Hồ sơ, bắt đầu hồ sơ Record Heap Allocations (Ghi lại quá trình phân bổ vùng nhớ khối xếp), thực hiện một chuỗi hành động, sau đó dừng ghi để phân tích. Trình phân tích tài nguyên phân bổ sẽ chụp nhanh vùng nhớ khối xếp theo định kỳ trong suốt quá trình ghi (thường xuyên là 50 mili giây một lần!) và một ảnh chụp nhanh cuối cùng khi kết thúc quá trình ghi.

Công cụ phân tích phân bổ vùng nhớ khối xếp

Các thanh ở trên cùng cho biết thời điểm tìm thấy đối tượng mới trong vùng nhớ khối xếp. Chiều cao của mỗi thanh tương ứng với kích thước của các đối tượng được phân bổ gần đây và màu của các thanh cho biết liệu các đối tượng đó có còn hoạt động trong ảnh chụp nhanh vùng nhớ khối xếp cuối cùng hay không: thanh màu xanh dương cho biết các đối tượng vẫn hoạt động ở cuối dòng thời gian, thanh màu xám cho biết các đối tượng được phân bổ trong dòng thời gian nhưng đã được thu gom rác.

Trong ví dụ trên, một hành động đã được thực hiện 10 lần. Chương trình mẫu lưu 5 đối tượng vào bộ nhớ đệm, vì vậy, 5 thanh màu xanh dương cuối cùng sẽ xuất hiện. Tuy nhiên, thanh màu xanh dương ở ngoài cùng bên trái cho biết có thể có vấn đề. Sau đó, bạn có thể sử dụng thanh trượt trong dòng thời gian ở trên để phóng to ảnh chụp nhanh cụ thể đó và xem các đối tượng được phân bổ gần đây tại thời điểm đó. Khi nhấp vào một đối tượng cụ thể trong vùng nhớ khối xếp, cây giữ lại của đối tượng đó sẽ xuất hiện ở phần dưới cùng của ảnh chụp nhanh vùng nhớ khối xếp. Việc kiểm tra đường dẫn giữ lại đối tượng sẽ cung cấp cho bạn đủ thông tin để hiểu lý do đối tượng không được thu thập. Bạn có thể thực hiện các thay đổi mã cần thiết để xoá tệp tham chiếu không cần thiết.

Giải quyết tình trạng thiếu bộ nhớ của Gmail

Bằng cách sử dụng các công cụ và kỹ thuật đã thảo luận ở trên, nhóm Gmail đã có thể xác định một số loại lỗi: bộ nhớ đệm không giới hạn, các mảng lệnh gọi lại tăng lên vô hạn đang chờ một sự kiện xảy ra nhưng thực sự không bao giờ xảy ra và trình nghe sự kiện vô tình giữ lại mục tiêu của chúng. Nhờ khắc phục những vấn đề này, mức sử dụng bộ nhớ tổng thể của Gmail đã giảm đáng kể. Người dùng trong nhóm 99% đã sử dụng ít bộ nhớ hơn 80% so với trước và mức sử dụng bộ nhớ của người dùng trung bình đã giảm gần 50%.

Mức sử dụng bộ nhớ của Gmail

Vì Gmail sử dụng ít bộ nhớ hơn nên độ trễ tạm dừng GC đã giảm, giúp cải thiện trải nghiệm tổng thể của người dùng.

Ngoài ra, nhờ thu thập số liệu thống kê về mức sử dụng bộ nhớ, nhóm Gmail đã phát hiện được các trường hợp hồi quy thu thập rác bên trong Chrome. Cụ thể, hai lỗi phân mảnh đã được phát hiện khi dữ liệu bộ nhớ của Gmail bắt đầu cho thấy sự gia tăng đáng kể về khoảng cách giữa tổng bộ nhớ được phân bổ và bộ nhớ đang hoạt động.

Kêu gọi hành động

Hãy tự hỏi mình những câu hỏi sau:

  1. Ứng dụng của tôi đang sử dụng bao nhiêu bộ nhớ? Có thể bạn đang sử dụng quá nhiều bộ nhớ, điều này trái ngược với suy nghĩ phổ biến là có tác động tiêu cực đến hiệu suất tổng thể của ứng dụng. Rất khó để biết chính xác con số phù hợp, nhưng hãy nhớ xác minh rằng mọi nội dung lưu vào bộ nhớ đệm bổ sung mà trang của bạn đang sử dụng đều có tác động đáng kể đến hiệu suất.
  2. Trang của tôi có bị rò rỉ thông tin không? Nếu trang của bạn bị rò rỉ bộ nhớ, thì điều này không chỉ ảnh hưởng đến hiệu suất của trang mà còn ảnh hưởng đến các thẻ khác. Sử dụng trình theo dõi đối tượng để giúp thu hẹp mọi rò rỉ.
  3. Trang của tôi có thường xuyên thực hiện GC không? Bạn có thể xem mọi lần tạm dừng GC bằng bảng điều khiển Dòng thời gian trong Công cụ dành cho nhà phát triển Chrome. Nếu trang của bạn thường xuyên thực hiện GC, thì có thể bạn đang phân bổ quá thường xuyên, làm hao tổn bộ nhớ thế hệ mới.

Kết luận

Chúng tôi bắt đầu trong một cuộc khủng hoảng. Cung cấp kiến thức cơ bản về cách quản lý bộ nhớ, đặc biệt là trong JavaScript và V8. Bạn đã tìm hiểu cách sử dụng các công cụ, bao gồm cả tính năng trình theo dõi đối tượng mới có trong các bản dựng Chrome mới nhất. Nhờ có kiến thức này, nhóm Gmail đã giải quyết được vấn đề về mức sử dụng bộ nhớ và cải thiện hiệu suất. Bạn cũng có thể làm tương tự với ứng dụng web!