ALDH
ALDH

Hiệu năng scene R3F, phần 1: đo trước khi tối ưu

Bảng đo r3f-monitor

Những nguyên nhân thường làm scene React Three Fiber chậm đi, và cách dùng r3f-monitor để biết nguyên nhân nào đang xảy ra trước khi bắt tay vào sửa.

Một khung hình tốn những gì

Màn hình 60 Hz vẽ lại 60 lần mỗi giây, nghĩa là mỗi khung hình có 16,7 ms để hoàn thành. Màn hình 120 Hz chỉ còn 8,3 ms. Trong khoảng đó, một scene React Three Fiber phải làm xong hai việc.

Việc đầu tiên nằm ở CPU. JavaScript chạy các callback useFrame, cập nhật vị trí và ma trận của từng object, rồi three.js duyệt qua scene để gửi lệnh vẽ cho card đồ hoạ. Mỗi lệnh như vậy là một draw call.

Việc thứ hai nằm ở GPU. Card đồ hoạ biến đổi từng đỉnh, rồi tô màu từng điểm ảnh mà hình học phủ lên.

Chỉ cần một trong hai bên vượt quá ngân sách thời gian là khung hình bị trễ, và người dùng thấy scene giật. Điều khó chịu là hai bên chậm vì những lý do rất khác nhau, trong khi triệu chứng thì giống hệt.

Những nguyên nhân thường gặp

Quá nhiều draw call

Mỗi mesh có material riêng thường là một draw call. Trước mỗi lệnh vẽ, CPU phải chuẩn bị trạng thái: gắn shader, gắn texture, nạp uniform. Một nghìn hộp giống hệt nhau, mỗi hộp là một <mesh>, sẽ thành một nghìn lần chuẩn bị, dù GPU vẽ từng hộp gần như tức thì.

Quá nhiều tam giác

Model xuất thẳng từ phần mềm dựng hình thường mang nhiều chi tiết hơn mức màn hình thể hiện được. Một vật ở xa chỉ chiếm vài chục điểm ảnh vẫn có thể được vẽ bằng hàng trăm nghìn tam giác.

Tô quá nhiều điểm ảnh

Chi phí tô màu tăng theo số điểm ảnh, không theo số object. Ba thứ thường đẩy con số này lên:

  • dpr cao. Điện thoại có tỉ lệ điểm ảnh 3 phải tô gấp 9 lần so với dpr bằng 1.
  • Vật liệu trong suốt chồng lên nhau, khiến cùng một điểm ảnh bị tô nhiều lần.
  • Hiệu ứng hậu kỳ. Mỗi hiệu ứng là thêm ít nhất một lần tô toàn màn hình.

Đèn và bóng đổ

Mỗi đèn có đổ bóng buộc scene phải được vẽ thêm một lần vào shadow map trước khi vẽ khung hình chính. Riêng đèn điểm cần tới sáu lần, mỗi lần cho một hướng. Vài chiếc đèn có bóng là đủ nhân đôi, nhân ba lượng việc của cả CPU lẫn GPU.

Texture lớn

Dung lượng file ảnh không nói lên chi phí thật. Một ảnh JPG 4096 × 4096 có thể chỉ nặng vài MB trên đĩa, nhưng khi lên GPU nó được giải nén thành khoảng 64 MB, và thêm mipmap thì khoảng 85 MB. Thiết bị di động hết bộ nhớ đồ hoạ sẽ giật mạnh hoặc mất luôn context.

Biên dịch shader

Mỗi biến thể material là một chương trình shader phải biên dịch khi nó xuất hiện lần đầu. Việc này chặn luồng chính, và là lý do scene hay khựng một nhịp lúc vừa mở hoặc lúc một vật mới hiện ra.

Việc thừa trong vòng lặp

useFrame chạy ở mọi khung hình, nên một sai sót nhỏ trong đó bị nhân lên 60 lần mỗi giây. Hai lỗi hay gặp nhất là gọi setState trong useFrame, làm React render lại liên tục, và tạo object mới ở mỗi khung hình, khiến bộ dọn rác phải chạy thường xuyên hơn.

Quên giải phóng tài nguyên

R3F tự giải phóng những gì bạn khai báo bằng JSX khi component bị gỡ. Tài nguyên bạn tự tạo bằng new, hoặc tải qua loader và được giữ trong cache, thì không. Nếu không gọi dispose() cho chúng, bộ nhớ GPU tăng dần sau mỗi lần đổi model hay chuyển trang, và scene chậm dần theo thời gian dùng.

Vì sao cần công cụ quan sát

Tám nguyên nhân trên cho ra cùng một triệu chứng: FPS giảm. Đoán sai nguyên nhân thì sửa cũng sai. Hạ dpr không giúp gì khi nút thắt là một nghìn draw call, và gộp mesh cũng vô ích khi GPU đang nghẽn vì hiệu ứng hậu kỳ.

Công cụ của trình duyệt cho thấy JavaScript chạy bao lâu, nhưng không cho biết scene có bao nhiêu draw call, bao nhiêu tam giác, hay GPU mất bao nhiêu mili giây cho một khung hình. Muốn biết thì phải hỏi chính renderer, ngay lúc scene đang chạy.

r3f-monitor

r3f-monitor là thư viện tôi viết để làm việc đó: một bảng số liệu nằm ngay trên scene, cập nhật theo thời gian thực.

npm install r3f-monitor

Đặt <PerfMonitor /> vào bất kỳ đâu bên trong <Canvas>:

import { Canvas } from "@react-three/fiber";
import { PerfMonitor } from "r3f-monitor";

export default function App() {
  return (
    <Canvas>
      <PerfMonitor />
      {/* scene của bạn */}
    </Canvas>
  );
}

Bảng đo hiện ở góc trên bên phải. Bạn có thể thử ngay trên CodeSandbox.

Vài tuỳ chọn hay dùng:

<PerfMonitor
  position="bottom-left" // top-right (mặc định), top-left, bottom-right, bottom-left
  displayType="classic" // "tab" (mặc định) hoặc "classic"
  minimal={false} // bản rút gọn, chỉ còn các số chính
/>

tab chia số liệu thành nhiều tab, hợp khi cần soi kỹ. classic gom tất cả vào một bảng nhỏ luôn hiện trên màn hình.

Đọc các con số

Thời gian của một khung hình

  • FPS: số khung hình mỗi giây, tính trên đúng một giây gần nhất nên ra số lẻ như 59,8 thay vì nhảy qua lại giữa 59 và 60.
  • CPU: thời gian JavaScript của một khung hình, tính bằng ms. Nó gồm mọi callback useFrame và lời gọi render của three.js.
  • GPU: thời gian card đồ hoạ thật sự dùng để vẽ khung hình, tính bằng ms.

Số GPU cần một lưu ý. Trình duyệt chỉ trả kết quả đo sau khi GPU làm xong, nên con số này luôn trễ vài khung hình so với số CPU đứng cạnh. Một số trình duyệt không cho đo, khi đó GPU hiện 0 và các số khác vẫn chạy bình thường.

Lượng việc gửi cho GPU

  • DRAWS: số draw call của khung hình vừa vẽ.
  • TRIANGLES, LINES, POINTS: lượng hình học thực sự được vẽ. Những gì nằm ngoài tầm nhìn của camera không được tính.
  • SHADERS: số chương trình shader đã biên dịch.

Bộ nhớ

  • MEM: bộ nhớ heap JavaScript đang dùng, tính bằng MB. Số này chỉ có trên trình duyệt nhân Chromium.
  • VRAM: bộ nhớ GPU dành cho texture và geometry, tính bằng MB.
  • GEOMETRIES, TEXTURES: số tài nguyên đang nằm trên GPU.

Trên WebGL, VRAM là con số ước lượng, vì trình duyệt không cho hỏi GPU đang dùng bao nhiêu bộ nhớ. Thư viện cộng kích thước các mảng dữ liệu của geometry, và tính texture theo công thức rộng × cao × 4 byte, nhân 1,33 nếu có mipmap. Con số này bỏ qua render target, nên hãy dùng nó để so sánh trước và sau một thay đổi thay vì xem nó là dung lượng tuyệt đối.

Từ con số tới nguyên nhân

Bước đầu tiên luôn là so CPU với GPU, vì nó cho biết nên tìm ở nửa nào của danh sách nguyên nhân.

  • CPU cao, GPU thấp: nút thắt nằm ở JavaScript. Nhìn sang DRAWS. Nếu nó lên tới hàng nghìn thì đó là câu trả lời, còn nếu không thì xem lại những gì đang chạy trong useFrame.
  • GPU cao, CPU thấp: máy đang nghẽn ở khâu vẽ. TRIANGLES cao nghĩa là hình học quá nặng. TRIANGLES bình thường nghĩa là vấn đề nằm ở số điểm ảnh phải tô: dpr, vật liệu trong suốt, hiệu ứng hậu kỳ.
  • DRAWS và TRIANGLES cao gấp nhiều lần dự tính: scene đang được vẽ lại nhiều lần trong một khung hình, thường là do bóng đổ hoặc hiệu ứng hậu kỳ.
  • Khựng lúc mới vào scene rồi mượt lại: xem SHADERS có tăng đúng lúc đó không.
  • GEOMETRIES, TEXTURES hoặc VRAM tăng dần khi dùng: có tài nguyên chưa được giải phóng.
  • VRAM lớn bất thường so với độ phức tạp của scene: gần như chắc chắn là texture.

Khi đã biết hướng mà chưa biết thủ phạm, bật deepAnalyze:

<PerfMonitor deepAnalyze />

Bảng đo sẽ có thêm danh sách các chương trình shader, mỗi mục kèm những mesh đang dùng nó và phần tam giác nó chiếm trong khung hình. Bấm vào biểu tượng con mắt để ẩn nhóm mesh đó đi. Ẩn một nhóm mà FPS bật lên tức là bạn đã tìm đúng chỗ. Tính năng này làm mỗi material biên dịch lại shader một lần, nên chỉ bật lúc đang điều tra.

Phần tiếp theo

Bài này dừng ở việc nhìn ra vấn đề. Phần 2 sẽ đi vào cách xử lý từng nguyên nhân: giảm draw call, giảm hình học, kiểm soát số điểm ảnh phải tô và quản lý bộ nhớ.

Trong lúc chờ, bạn có thể xem mã nguồn, gói trên npm và trang tài liệu của r3f-monitor.