Vấn đề đo thời gian chạy ứng dụng Node.js của bạn

Mar 07 2023
Đo lường thời gian chạy của ứng dụng Node.js là một khía cạnh quan trọng của việc tối ưu hóa hiệu suất.
Ảnh của Chris Gonzalez từ Burst Copied

Đo lường thời gian chạy của ứng dụng Node.js là một khía cạnh quan trọng của việc tối ưu hóa hiệu suất. Bằng cách đo thời gian thực thi các phần khác nhau trong ứng dụng của bạn, bạn có thể xác định các nút thắt cổ chai và tối ưu hóa mã của mình để cải thiện hiệu suất.

Làm thế nào chúng ta có thể đo lường thời gian thực hiện của một chức năng?

Có nhiều cách để đo thời gian trong node.js, cách có độ phân giải cao nhất để thực hiện là sử dụng process.hrtime() hoặc Perf_hooks , cả hai cách này đều tốt hơn so với sử dụng lớp Date vì chúng không phụ thuộc vào đồng hồ hệ thống của bạn, nhưng vẫn có một số khác biệt giữa chúng:

process.hrtime() cung cấp độ phân giải cấp nano giây, trong khi perf_hooks cung cấp độ phân giải cấp micro giây. Điều này có nghĩa là process.hrtime() thường chính xác hơn nhưng cũng đắt hơn khi gọi.

Bây giờ hãy lấy một ví dụ đo thời gian chạy của logic sau. Chúng tôi có logic không đồng bộ (chúng tôi có thể cho rằng đây là một số lệnh gọi HTTP) sẽ giải quyết sau 1000 mili giây. Chúng tôi sẽ sử dụng perf_hooks để đo thời gian cần thiết để thực hiện lời hứa này.

const {performance} = require('perf_hooks');
const SOME_BIG_NUMBER = 4000000000;

async function asyncLogic(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const start = performance.now();
asyncLogic(1000).then(() => {
    const time = performance.now() - start;
    console.log(`Time taken: ${time}ms`);
});

let x = 0;
for (let i = 0; i < SOME_BIG_NUMBER; i++) {
    x += Math.sqrt(SOME_BIG_NUMBER);
}

Time taken: 3948ms

Vòng lặp sự kiện và hàng đợi sự kiện

Mô hình thời gian chạy JavaScript dựa trên vòng lặp sự kiện, đây là phần chịu trách nhiệm thực thi mã của chúng tôi và cho phép chúng tôi sử dụng logic không đồng bộ.

Khi chúng tôi thực thi mã JavaScript, điều sau đây sẽ xảy ra:

  1. JavaScript thực thi tất cả mã đồng bộ theo thứ tự xuất hiện trong chương trình.
  2. Nếu có bất kỳ mã không đồng bộ nào (chẳng hạn như hàm asyncLogic của chúng tôi), JavaScript sẽ thêm mã đó vào hàng đợi sự kiện thay vì thực thi mã đó ngay lập tức.
  3. Vòng lặp sự kiện liên tục kiểm tra hàng đợi sự kiện để xem có sự kiện nào đang chờ thực hiện hay không.
  4. Khi một sự kiện đã sẵn sàng để được thực thi, vòng lặp sự kiện sẽ di chuyển sự kiện đó từ hàng đợi sự kiện sang ngăn xếp cuộc gọi , nơi nó có thể được thực thi.
  5. Sau khi sự kiện được thực thi, mọi mã đồng bộ bổ sung sẽ được thực thi trước khi vòng lặp sự kiện quay lại để kiểm tra hàng đợi sự kiện để biết thêm sự kiện.

Đó là lý do tại sao trong ví dụ của chúng tôi, phép đo thời gian là 4000 mili giây,

  1. Chúng tôi thực thi chức năng asyncLogic, chức năng này đã được thêm vào hàng đợi sự kiện.
  2. Sau đó, chúng tôi thực thi logic đồng bộ, mất khoảng 4000 mili giây để chạy.
  3. Vòng lặp sự kiện di chuyển lệnh gọi asyncLogic() của chúng ta từ hàng đợi sự kiện sang ngăn xếp cuộc gọi
  4. Sau đó, chúng tôi thực hiện chức năng gọi lại và tính toán thời gian kết thúc

Độ trễ vòng lặp sự kiện

Do đó, khi theo dõi hiệu suất ứng dụng của chúng tôi, có một số liệu quan trọng mà chúng tôi nên chú ý đến, độ trễ của vòng lặp sự kiện, thời gian dành cho vòng lặp sự kiện giữa việc thêm một cuộc gọi lại mới vào hàng đợi sự kiện cho đến khi thực hiện cuộc gọi lại này.

Chúng tôi có thể dễ dàng tạo một vòng lặp cho phép chúng tôi đo độ trễ của vòng lặp sự kiện bằng mã sau:

function measureEventLoopLag() {
    let lastLoopTime = performance.now();
    setTimeout(() => {
        const delay = performance.now() - lastLoopTime - 1000;
        lastLoopTime = performance.now();
        console.log(`Event Loop lag is: ${delay} ms`)
        measureEventLoopLag();
    }, 1000)
}

cải thiện thời gian chạy

Giả sử rằng mã của chúng ta có lệnh gọi HTTP mất 4000 mili giây và chúng ta cũng có một vòng lặp chậm cũng mất khoảng 4000 mili giây để chạy. Như chúng ta đã thấy trong ví dụ trước, khi đo thời gian chạy này, chúng tôi sẽ phát hiện ra rằng chúng tôi mất 4000 mili giây để chạy toàn bộ mã.

Sau khi tái cấu trúc mã của chúng tôi, chúng tôi giảm 50% thời gian chạy vòng lặp chậm. Khi chúng tôi chạy lại mã, thời gian chạy tổng thể theo phép đo của chúng tôi không thực sự thay đổi, chúng tôi vẫn đợi khoảng 4000 mili giây để hoàn thành luồng tổng thể vì chúng tôi đã đợi luồng không đồng bộ của mình hoàn thành. Có vẻ như công cụ tái cấu trúc của chúng tôi không thực sự hữu ích.

Không thực sự, nếu chúng ta xem xét về CPU, chúng ta đã tạo ra một sự khác biệt rất lớn. Các cuộc gọi HTTP của chúng tôi không tiêu tốn nhiều CPU, chúng tôi chủ yếu chỉ chờ phản hồi. Vì vậy, hầu hết thời gian, CPU của chúng tôi sử dụng để thực thi vòng lặp chậm, khi chúng tôi cấu trúc lại mã và giảm 50%, nó không thay đổi tổng thời gian nhưng nó có ảnh hưởng đáng kể đến CPU của chúng tôi.

Phần kết luận

Trong ví dụ của chúng tôi, chúng tôi có rất ít logic để theo dõi, mọi thứ trở nên thực sự phức tạp khi bạn có một hệ thống phức tạp với nhiều logic để chạy, trong trường hợp đó, bạn nên cố gắng đo lường các phần logic nhỏ của mình để theo dõi sự suy giảm hiệu suất tốt hơn.

Có hai điều quan trọng cần lưu ý khi theo dõi thời gian chạy ứng dụng của bạn:

  • Việc đo thời gian của chức năng không đồng bộ có thể bị ảnh hưởng bởi mã đồng bộ (độ trễ của vòng lặp sự kiện)
  • Các chức năng đo lường có cả logic không đồng bộ và đồng bộ có thể che giấu sự cải thiện (hoặc suy giảm) thời gian chạy.
  • Độ trễ vòng lặp sự kiện cao hơn ~30 mili giây thực sự không tốt cho ứng dụng của bạn.