24/10/2026 · VietTechies · Biên tập

Năm 2010, kỹ sư phần mềm Evan Martin bắt đầu phát triển Ninja để giảm thời gian chờ trước khi hệ thống build (tạo phần mềm từ mã nguồn) của Google Chrome bắt đầu biên dịch. Một hệ thống trước đó trên Linux dùng SCons có thể mất ba mươi giây chỉ để xác định những tệp nào đã thay đổi. Thử nghiệm của Martin nhắm vào thời gian chờ lúc khởi động ấy, thay vì thời gian biên dịch code.
Google Chrome đang phát triển nhanh chóng, với số tệp mã nguồn lên tới hàng chục nghìn khi các kỹ sư đưa trình duyệt từ Windows sang Linux và macOS. Martin nhớ lại những lần phải chờ hết lượt này đến lượt khác sau khi sửa một tệp, rồi sửa tiếp một dấu chấm phẩy còn thiếu. Thiết kế của ông hướng tới việc chuyển khâu cấu hình sang một chương trình sinh tệp build (generator), để những lần build sau có thể nạp nhanh kết quả đã tạo. Martin muốn thử một giả thuyết đơn giản: nếu công cụ build để chương trình sinh tệp đưa ra gần như mọi quyết định, và hoạt động ít giống một trình biên dịch mà giống một trình hợp dịch hơn, thì sao?
Độ trễ khi chỉ build lại phần thay đổi
Sau khi Chrome cho Windows ra mắt năm 2008, nhóm dùng GYP (Generate Your Projects), công cụ sinh tệp build, để tạo các tệp này cho Windows và những nền tảng khác. Trên Linux, thiết lập ban đầu tạo ra các tệp build cho SCons. Nhưng SCons có thể mất khoảng ba mươi giây chỉ để kiểm tra cây thư mục và xác định những gì đã thay đổi trước khi bắt đầu biên dịch.
Martin thử giải quyết vấn đề bằng cách cho GYP xuất ra các Makefile thông thường, vận dụng những kỹ thuật tổ chức từ nhân Linux. Ban đầu, các Makefile cho cảm giác chạy nhanh. Nhưng khi phiên bản Linux đưa thêm nhiều tệp vào quá trình build, Make cũng chậm dần. Martin nhớ rằng sau những chỉnh sửa nhỏ, ông phải đợi lâu đến mức quên mất mình đang làm gì. Ông chỉ ra điều trớ trêu: nhóm Chrome nỗ lực giảm độ trễ khởi động của trình duyệt, trong khi chính các kỹ sư lại mất mạch suy nghĩ vì chờ những script build của mình.
Tách quy tắc ra quyết định khỏi khâu thực thi
Phần lớn công cụ build phổ biến cố làm cùng lúc hai công việc rất khác nhau. Chúng phân tích các ngôn ngữ cấu hình dễ đọc với con người, có biến, điều kiện, bước kiểm tra nền tảng và những quy tắc tìm kiếm phức tạp. Sau đó, chúng xác định quan hệ phụ thuộc giữa các tệp và lên lịch chạy lệnh.
Martin quyết định tách hẳn hai trách nhiệm ấy. Những quyết định cấu hình cấp cao hoàn toàn thuộc về chương trình sinh tệp: tạo bản nhị phân để gỡ lỗi hay phát hành, đặt những cờ tùy chọn nào cho trình biên dịch, đưa vào những tính năng nào của hệ điều hành và lưu tệp đối tượng ở đâu. Chương trình đó có thể là GYP, rồi sau này là GN, hệ thống tạo cấu hình build cho Chromium, hoặc CMake. Chương trình sinh tệp sẽ chốt các quyết định cấu hình từ trước và ghi ra chỉ dẫn cụ thể để Ninja tái sử dụng trong những lần build tiếp theo.
Bản thân Ninja gần như không làm gì khác. Định dạng đầu vào của Ninja không có câu điều kiện, cách chọn tệp bằng ký tự đại diện hay quy tắc dựa trên phần mở rộng của tệp. Một tệp .ninja mô tả rõ đầu vào, lệnh và đầu ra, với các quy tắc cùng biến có thể tái sử dụng. Định dạng đơn giản giúp giảm công việc diễn giải, dù khâu phân tích cú pháp vẫn cần tối ưu khi Chrome lớn dần. Khi bắt đầu một lần build lại phần thay đổi, Ninja nạp đồ thị quan hệ phụ thuộc, kiểm tra dấu thời gian của tệp cùng các giá trị băm của lệnh đã ghi trong nhật ký, rồi lập tức chạy những tác vụ cần cập nhật.
Tối ưu từng chi tiết và thiết kế đồ thị
Để duyệt đồ thị nhanh nhất có thể, Ninja ánh xạ mỗi đường dẫn tệp tới một con trỏ nút duy nhất trong bộ nhớ, nhờ đó loại bỏ những phép so sánh chuỗi tốn kém khi phát hiện chu trình và duyệt các quan hệ phụ thuộc. Để theo dõi thay đổi của lệnh mà không làm nhật ký phình to, cộng tác viên Nico Weber thay các chuỗi lệnh đầy đủ trong nhật ký build bằng giá trị băm của lệnh. Thay đổi này thu nhỏ nhật ký build của Chrome trên macOS từ 200 megabyte xuống dưới hai megabyte.
Với quan hệ phụ thuộc vào tệp tiêu đề, Ninja tránh dùng những công cụ quét tệp tiêu đề chậm chạp trước khi build. Các quy tắc có thể yêu cầu trình biên dịch như GCC (GNU Compiler Collection, bộ công cụ biên dịch GNU) và Clang xuất danh sách phụ thuộc theo định dạng Makefile ngay trong lúc biên dịch. Ban đầu, Ninja nạp những tệp này ở lần build tiếp theo; một cải tiến sau đó, có từ Ninja 1.3, xử lý chúng ngay sau khi biên dịch và lưu dữ liệu phụ thuộc gọn hơn trong .ninja_deps. Khi Martin thiết lập phép đo hiệu năng làm mốc — chạy lại Ninja trên Chrome ngay sau một lần build hoàn tất để xác nhận không có gì cần làm lại — toàn bộ bước kiểm tra chỉ mất chưa đầy một giây.
Giới hạn của việc làm ít hơn
Tuy nhiên, thiết kế tối giản cũng đặt ra những giới hạn rõ ràng cho Ninja. Ninja được thiết kế để làm việc cùng một chương trình sinh tệp, thay vì trở thành một hệ thống build độc lập, tiện dụng. Người dùng có thể tự viết tệp .ninja, nhưng cung cấp cú pháp thuận tiện cho việc viết tay là điều Ninja chủ đích không hướng tới. Tài liệu chính thức của Ninja cũng lưu ý rằng với những dự án nhỏ vốn đã có chu kỳ chỉnh sửa rồi biên dịch nhanh, tốc độ của Ninja có thể không quan trọng mấy. Các dự án lớn có thể hưởng lợi từ việc giảm thời gian xử lý phụ trợ này, dù tài liệu cũng ghi nhận rằng đôi khi cả dự án nhỏ cũng chạy nhanh hơn nhờ các quy tắc build đơn giản hơn.
GN sinh tệp build Ninja cho Chrome và các dự án liên quan, còn bộ sinh Ninja của CMake giúp những dự án như LLVM và ReactOS sử dụng Ninja. Với các thao tác lặp đi lặp lại, Ninja gợi ra một câu hỏi thực tế: có thể chuyển những quyết định cấu hình nào ra khỏi khâu thực thi? Với các lập trình viên Chrome, kết quả là: khi không có gì cần build lại, bước kiểm tra hoàn tất trong chưa đầy một giây.
Bài liên quan
Đọc bằng ngôn ngữ khác: English
Nguồn
- Evan Martin, “Ninja,” trong The Performance of Open Source Software (AOSA)
- The Ninja Build System Manual, v1.13.1, tháng 7 năm 2025
- GN Reference Manual, Google Source
- CMake Documentation: Ninja Generator
- Chromium Projects: GN Build Configuration
- Ảnh đại diện: Robert Kloosterhuis; Wikimedia Commons; CC BY 2.0 (đã chỉnh sửa từ ảnh gốc)
