Giám sát bộ nhớ với kbmMW

Các phiên bản mới hơn của kbmMW cung cấp nhiều tính năng mạnh mẽ cho việc ghi log, kiểm toán, theo dõi stack khi xảy ra ngoại lệ, và gần đây nhất là gỡ lỗi sử dụng bộ nhớ. Những tính năng này có thể áp dụng cho bất kỳ ứng dụng nào, không chỉ giới hạn trong các hệ thống đa tầng dựa trên kbmMW.

Tôi đã viết một số bài về hệ thống ghi log và kiểm toán trong kbmMW, bao gồm cách theo dõi stack khi xử lý ngoại lệ. Tính năng mới bổ sung khả năng theo dõi từng cấp phát bộ nhớ theo thời gian thực. Bạn có thể thắc mắc: FastMM đã có khả năng phát hiện rò rỉ bộ nhớ, vậy tại sao cần dùng trình gỡ lỗi bộ nhớ của kbmMW?

FastMM chỉ theo dõi các cấp phát qua GetMem thông thường, không thể phát hiện các cấp phát qua các hàm Virtual/Heap/Global/Local Allocation của Windows. Trình gỡ lỗi của kbmMW vẫn hoạt động ngay cả khi tắt tính năng phát hiện rò rỉ bộ nhớ của FastMM, và cho phép ghi lại việc sử dụng cũng như cấp phát bộ nhớ tại bất kỳ thời điểm nào trong ứng dụng.

Bắt đầu

Để sử dụng, bạn cần thêm unit kbmMWDebugMemory vào mệnh đề uses của ứng dụng, và đảm bảo các định nghĩa sau đã được thiết lập trong file kbmMWConfig.inc:

{$DEFINE KBMMW_SUPPORT_DEBUGMEMORY}   
{$DEFINE KBMMW_INSTALL_DEBUGMEMORY_HANDLERS}

Nếu không, mọi chức năng gỡ lỗi bộ nhớ sẽ bị vô hiệu hóa. Nếu thiếu KBMMW_SUPPORT_DEBUGMEMORY, toàn bộ các hàm/phương thức liên quan đến gỡ lỗi bộ nhớ sẽ không khả dụng. Nếu thiếu KBMMW_INSTALL_DEBUGMEMORY_HANDLERS, hệ thống sẽ không tự động cài đặt hook và handler, nhưng các hàm vẫn có thể gọi thủ công mà không thực hiện theo dõi bộ nhớ.

Sau khi thiết lập hai định nghĩa trên và thêm unit kbmMWDebugMemory vào uses, các hook và chức năng gỡ lỗi bộ nhớ của kbmMW sẽ tự động được kích hoạt.

Để tận dụng tối đa, ứng dụng nên được biên dịch ở chế độ debug với Stack frames bật (Project/Options/Compiler/Stack frames = True), đồng thời tạo file *.tds hoặc Detailed Map file. Nếu chạy ứng dụng bên ngoài IDE và cần gỡ lỗi bộ nhớ, file *.tds hoặc *.map phải nằm cùng thư mục với file thực thi.

Hình ảnh dưới đây minh họa cách thiết lập tham số biên dịch:

Khái niệm

kbmMW tự động hook các phương thức cấp phát bộ nhớ kiểu Borland (GetMem, New, v.v.) và các phương thức Microsoft Windows (VirtualAlloc, GlobalAlloc, LocalAlloc, HeapAlloc). Nó tương thích tốt với các trình quản lý bộ nhớ khác, bao gồm cả FastMM.

Mỗi lần cấp phát hoặc cấp phát lại bộ nhớ, kbmMW gán cho nó một số 64-bit duy nhất và tăng dần (uint64). Số này có thể dùng để theo dõi tất cả các cấp phát giữa hai thời điểm, gọi là các điểm kiểm tra (checkpoint). Về cơ bản, mỗi điểm kiểm tra là một số uint64. Nhờ đó, bạn có thể kiểm tra chính xác những cấp phát bộ nhớ nào đã xảy ra trong một đoạn mã, thậm chí lấy được thông tin stack trace nơi cấp phát.

Thống kê

kbmMW duy trì các thống kê về số lượng và kích thước bộ nhớ đã cấp phát còn sống (chưa giải phóng).

Bạn có thể truy cập các thống kê này bất kỳ lúc nào như sau:

lLiveAllocationsCount.Caption := IntToStr(TkbmMWDebugMemory.LiveAllocationCount) +
      ' (' + IntToStr(TkbmMWDebugMemory.LiveAllocationCountPerSec) + '/sec)';
lLiveAllocSize.Caption := IntToStr(TkbmMWDebugMemory.LiveAllocationSize) +
      ' (' + IntToStr(TkbmMWDebugMemory.LiveAllocationSizePerSec) + '/sec)';
lMaxAllocationCount.Caption := IntToStr(TkbmMWDebugMemory.MaxAllocationCount);
lMaxAllocationSize.Caption := IntToStr(TkbmMWDebugMemory.MaxAllocationSize);
lMaxCapacity.Caption := IntToStr(TkbmMWDebugMemory.CurrentAllocationCountCapacity);

LiveAllocationCount là số lượng cấp phát đang hoạt động. Nó tính tất cả các loại cấp phát (Borland – object/string/memory, Local, Global, Virtual, Heap). Tuy nhiên, con số này có thể không chính xác tuyệt đối do các cơ chế như FastMM cấp phát khối lớn qua VirtualAlloc và chia nhỏ qua GetMem, khiến một số cấp phát bị đếm hai lần.

Do đó, các giá trị thời gian thực chỉ nên dùng để so sánh tương đối (ví dụ: phát hiện tăng đột biến sử dụng bộ nhớ có thể chỉ ra rò rỉ), không nên dựa vào giá trị tuyệt đối.

Phát hiện rò rỉ khi đóng ứng dụng

Rò rỉ bộ nhớ là khi tài nguyên được cấp phát nhưng không bao giờ được giải phóng. Một số rò rỉ là vô hại (xảy ra một lần trong quá trình khởi động), trong khi những rò rỉ khác nguy hiểm (cấp phát lặp đi lặp lại không giải phóng, dẫn đến cạn kiệt bộ nhớ).

Điểm duy nhất có thể phát hiện rò rỉ một cách đáng tin cậy là khi ứng dụng đóng, vì lúc đó hầu hết các cấp phát đã được giải phóng.

kbmMW giúp việc này trở nên dễ dàng. Trong sự kiện khởi tạo sớm của ứng dụng (ví dụ Form.OnCreate), thêm mã sau:

TkbmMWDebugMemory.ReportDestination('c:\temp\leaks.txt');
TkbmMWDebugMemory.ReportLeaksOnShutdown := True;
TkbmMWDebugMemory.StartLeakChecking;

Đoạn mã trên chỉ định nơi lưu báo cáo rò rỉ, yêu cầu tạo báo cáo khi đóng ứng dụng, và kích hoạt kiểm tra rò rỉ ngay lập tức. Hàm StartLeakChecking tải file .tds hoặc .map (để lấy stack trace) và đăng ký một điểm kiểm tra cơ sở (baseline) – mọi cấp phát trước thời điểm này sẽ bị bỏ qua (bao gồm cả các cấp phát của VCL/RTL và các object đã được ánh xạ).

Bạn có thể xem giá trị baseline qua:

ShowMessage('Baseline=' + IntToStr(TkbmMWDebugMemory.Baseline));

Nếu muốn kiểm tra tất cả cấp phát từ khi cài hook, đặt baseline về 0:

TkbmMWDebugMemory.Baseline := 0;

Điểm kiểm tra cuối có thể lấy như sau:

var
  cp: TkbmMWDebugMemoryAllocationKey;
begin
  cp := TkbmMWDebugMemory.Checkpoint;
  ShowMessage('Checkpoint=' + IntToStr(cp));
end;

Khi chạy và đóng ứng dụng, kbmMW sẽ tạo báo cáo rò rỉ. Mặc định, nó hiển thị thông tin tóm tắt trên màn hình và chi tiết trong file đã chỉ định. Báo cáo cho biết object, string, hay kiểu bộ nhớ nào bị rò rỉ, kèm số lượng. Ví dụ: một instance của TkbmMWInnerThread (do lịch trình sự kiện nền của kbmMW).

Stack trace có độ chi tiết phụ thuộc vào thông tin debug (cần bật Stack frames) và file .tds/.map. Bạn có thể tắt thu thập stack trace để tiết kiệm tài nguyên, bằng cách thêm dòng sau trước khi gọi StartLeakChecking:

TkbmMWDebugMemory.CollectStacks := False;

Theo dõi cấp phát trong các tình huống cụ thể

Bạn có thể báo cáo tất cả cấp phát trong một khoảng thời gian hoặc giữa hai vị trí trong code bằng cách sử dụng điểm kiểm tra. Ví dụ:

var
  MyCP1, MyCP2: TkbmMWDebugMemoryAllocationKey;
begin
  MyCP1 := TkbmMWDebugMemory.Checkpoint;
  // Đoạn mã cần kiểm tra
  MyCP2 := TkbmMWDebugMemory.Checkpoint;
  MyReport(MyCP1, MyCP2);
end;

procedure MyReport(ACP1, ACP2: TkbmMWDebugMemoryAllocationKey);
var
  sr: TkbmMWDebugMemoryScanResult;
begin
  sr := TkbmMWDebugMemoryScanResult.Create;
  try
    TkbmMWDebugMemory.Scan(sr, ACP1, ACP2, [mwdmstObject]);
    sr.Log(mwltDebug, mwllDetailed, [mwsrpoNoStack]);
  finally
    sr.Free;
  end;
end;

Ví dụ trên chỉ báo cáo các cấp phát object, ghi log qua hệ thống log của kbmMW, và bỏ qua stack trace.

Trên đây là giới thiệu tổng quan về trình gỡ lỗi bộ nhớ của kbmMW. Hy vọng bạn có thể áp dụng để nhanh chóng phát hiện và giải quyết các vấn đề rò rỉ bộ nhớ.

Thẻ: kbmMW memory debug Delphi memory leak stack trace

Đăng vào ngày 26 tháng 7 lúc 16:41