Tối ưu hóa hiệu suất các phương thức xử lý đường dẫn trong Ruby
Bài viết phân tích quá trình cải thiện hiệu suất của các phương thức xử lý đường dẫn trong Ruby, đặc biệt là `File.join`, nhằm giảm thời gian thiết lập CI. Thông qua việc tối ưu hóa logic mã hóa, tìm kiếm ngược và quản lý bộ nhớ, tốc độ thực thi đã được tăng lên gấp nhiều lần so với bản gốc.

Tối ưu hóa hiệu suất các phương thức xử lý đường dẫn trong Ruby
Trong quá trình làm việc tại Intercom, một trong những dự án đầu tiên tôi tham gia là cải thiện hiệu suất CI cho hệ thống monolith khổng lồ của họ. Một yếu tố then chốt ảnh hưởng trực tiếp đến trải nghiệm người dùng và chi phí vận hành CI là tốc độ khởi động quy trình Ruby để chạy thử nghiệm (test).
Khi chạy song song hàng trăm, thậm chí hàng nghìn worker, thời gian thiết lập (setup time) trở thành một chi phí cố định không thể bỏ qua. Chỉ cần tối ưu hóa một giây trong giai đoạn này, với quy mô 1350 worker song song, chúng ta có thể tiết kiệm được hơn 20 phút tính toán cho mỗi lần build. Do đó, việc "cạo từng giây" trong thời gian khởi động ứng dụng trở nên cực kỳ quan trọng.
Vai trò của Bootsnap và vấn đề về Load Path
Nếu bạn là lập trình viên Ruby, có lẽ bạn đã nghe qua Bootsnap. Đây là gem mặc định trong Rails gần một thập kỷ qua, giúp tăng tốc độ tải mã nguồn. Một trong những tính năng chính của Bootsnap là Load Path Caching.
Thay vì để Ruby thực hiện tìm kiếm tuyến tính tốn kém trong $LOAD_PATH mỗi khi gọi require, Bootsnap quét trước tất cả các thư mục để xây dựng một bản đồ lớn (hash map), cho phép tra cứu đường dẫn tuyệt đối với độ phức tạp O(1).
Tuy nhiên, vấn đề nằm ở chỗ: Bootsnap cần quét lại các thư mục để xác thực cache (cache invalidation). Trên hệ thống CI, việc này thường phải thực hiện lại từ đầu do git không bảo toàn mtime (thời gian sửa đổi) của file. Tại kho chứa (repo) monolith của Intercom, việc quét load path này tiêu tốn gần một giây. Mặc dù nghe có vẻ ít, nhưng trong bối cảnh CI, đây là một khoảng thời gian đáng kể cần cải thiện.
Hồ sơ hiệu suất của File.join
Phân tích và tối ưu hóa File.join
Nhìn vào hồ sơ hiệu suất (profile), tôi nhận thấy File.join và một số phương thức xử lý đường dẫn khác đang tiêu tốn nhiều tài nguyên. Dưới đây là các vấn đề tôi đã phát hiện và khắc phục:
Hỗ trợ mã hóa đa byte (Multi-byte Encoding)
Ruby hỗ trợ hơn một hundred loại mã hóa ký tự. Trước đây, File.join phải xử lý rất cẩn thận các mã hóa đa byte như Shift JIS, nơi một byte có thể là dấu gạch chéo ngược \ hoặc là một phần của ký tự đa byte. Việc kiểm tra này sử dụng hàm rb_enc_mbclen, vốn rất tốn kém.
Tuy nhiên, đại đa số các đường dẫn hiện nay đều sử dụng UTF-8 hoặc ASCII thuần túy. Tôi đã triển khai một "fast path" cho các mã hóa này, cho phép sử dụng các thuật toán thao tác byte đơn giản và nhanh chóng thay vì các hàm xử lý mã hóa phức tạp.
Câu hỏi về mã hóa trong File.join
Logic tìm kiếm ngược (Reverse Search)
Một vấn đề khác nằm trong hàm chompdirsep, dùng để loại bỏ các dấu phân cách đường dẫn thừa ở cuối chuỗi. Logic cũ quét toàn bộ chuỗi từ đầu đến cuối để tìm dấu phân cách cuối cùng. Điều này khiến các đường dẫn dài trở nên chậm hơn một cách không cần thiết.
Tôi đã sửa đổi logic để quét ngược từ cuối chuỗi, giúp tìm kiếm nhanh hơn rất nhiều.
Câu trả lời chi tiết về vấn đề mã hóa
Kiểm tra chuỗi C và Đối số biến thiên
Hồ sơ cho thấy 6.7% thời gian bị tiêu tốn trong rb_string_value_cstr. Hàm này đảm bảo chuỗi kết thúc bằng NULL và không chứa byte NULL. Tuy nhiên, File.join không thực sự cần chuỗi kết thúc NULL vì nó chỉ đang nối chuỗi, không truyền dữ liệu xuống API cấp C nào. Tôi đã thay thế nó bằng rb_str_null_check đơn giản hơn.
Ngoài ra, File.join trước đây tạo ra một mảng Array tạm thời để chứa các đối số đầu vào. Tôi đã thay đổi signature của hàm để nhận trực tiếp con trỏ stack và số lượng đối số, loại bỏ việc cấp phát bộ nhớ không cần thiết cho các trường hợp gọi hàm đơn giản.
Kết quả đạt được
Sau khi áp dụng tất cả các cải tiến trên, hiệu suất của File.join đã tăng lên vượt bậc:
- Với hai chuỗi đơn giản: Tăng 7.80 lần.
- Với nhiều chuỗi: Tăng 18.81 lần.
Đáng chú ý nhất, trên Ruby 4.1.0dev, việc sử dụng File.join để nối hai đường dẫn bây giờ thậm chí còn nhanh hơn cả việc sử dụng nội suy chuỗi (string interpolation).
Mở rộng tối ưu hóa
Sau khi thành công với File.join, tôi đã áp dụng các kỹ thuật tương tự để tối ưu hóa các phương thức xử lý đường dẫn khác bao gồm: File.basename, File.dirname, File.extname và File.expand_path. Mặc dù các phương thức này không phải là điểm nghẽn lớn (hotspot) chính, nhưng việc tối ưu hóa chúng giúp mang lại lợi ích chung cho hiệu suất của hệ sinh thái Ruby.


