Blog

All Blog Posts  |  Next Post  |  Previous Post

TMS MemInsight, Delphi runtime memory inspection: statistics and filters

Tuesday, February 22, 2022

TMS Software Delphi  Components

Using TMS MemInsight is easy: drop a component on your form, set some properties and there you go. Altough the component and UI of TMS MemInsight are currently based on the VCL you can already use it in your FireMonkey (on Windows and without UI), Windows service or console applications or any other headless application, such as a TMX XData server. But in this blog, we dive deeper:

TMS MemInsight has a wide API and offers all of its options and features by code.

Let's start by looking at some of the properties of the memory profiler first. To access it, as well as other parts of TMS MemInsight, use the units TMS.MI.Access and TMS.MI.Memory from within your code. These units contain some functions which serve as entry point to gain access to the interfaces needed to control tracing, filters as well as more advanced options like collecting memory statistics as it's done within the UI. Another handy unit is TMS.MI.Core which offers some ready to use utility functions.

You can enable/disable the profiler by setting it's Active property. You can Suspend/Resume profiling for the current thread by calling the methods of the same name or exclude them completely to control and tune what is being traced. For this, TMS MemInsight also offers filters which allow for even more fine tuning. Filters can also be customized and attached and removed dynamically. The folder "Dev" contains a sample for such a filter which allows tracing of objects contained in certain units only. We will use it in our example below. Another useful method is SuspendMethod which disables tracing for the current thread until the current scope is exited. Usually this is, as the name suggests, the current method, although this has slightly changed in Delphi 11 Alexandria with the introduction of inline variables where the scope might be limited to the current begin/end block.

The memory profiler includes three different filters. The Default filter allows to configure, which threads are being traced as well as setting the TraceTypes: mtString, mtObject, mtArray, mtRecord and mtUnknown. Beside of that you can, if your applications is build using run-time packages, select which package classes are shown. The IncludeFilter and ExcludeFilter allow fine grained tracing considering the class name of objects and supports pattern matching. This way you can limit the tracing to certain class names as well as class names matching something like "TYourPrefix*" or exclude class names matching similar patterns. All filters can be activated/deactivated by setting their Active property. You can store and reload these configurations for latter use.

Let's jump right into our first example: collecting memory statistics and dumping them into a file. Working with these statistics is, in almost all cases, my starting point when trying to understand why, where and what the application is consuming. As this is happening during run-time I can use the application, repeat certain functions and observe its behavior to isolate and concentrate on concrete spots. Limiting the tracing on these identified classes by using the above mentioned filters is also helpful.

For the sake of this example we create five utility functions: InitializeProfiling, StartProfiling, StopProfiling, ResetStatistics and SaveToFile. InitializeProfiling will set up some default options and attach and configure the filter as well as the memory statistics. We also disable stack traces as we don't need them here. Therefore we also don't need to generate the MAP file. StartProfiling and StopProfiling will, as the name suggests, activate and deactivate the profiling while SaveToFile will export the current statistics. ResetStatistics will reveal how many objects of a certain class have been created and destroyed since the last time you did reset their statistics. Please ensure that the units we are using are in your projects search path.

Our sample unit (compatible with Delphi 2010 and up) looks like this:

unit TMS.MI.StatisticsSample;

interface
  procedure InitializeProfiling;
  procedure StartProfiling;
  procedure StopProfiling;
  procedure ResetStatistics;
  procedure SaveToFile(const AFileName: String);

implementation

uses
  Classes, SysUtils, TMS.MI.Access, TMS.MI.Memory, TMS.MI.UnitNameFilter, TMS.MI.Core;

var
  GStatistics: TTMSMIMemoryStatistics;
  GFilter: TTMSMIUnitNameFilter;

procedure InitializeProfiling;
begin
  // initialize once (it's an example)
  if GStatistics = nil then begin
    // create statistics. attaches to the profiler by itself.
    GStatistics := TTMSMIMemoryStatistics.Create;
    // create a filter, configure and attach it
    GFilter := TTMSMIUnitNameFilter.Create;
    GFilter.Add('*StdCtrls');
    GFilter.Active := True;
    MemoryProfiler.Attach(GFilter);
    // set some options
    MemoryProfiler.Callstacks := [];
    MemoryProfiler.DefaultFilter.TraceTypes := [mtObject];
  end;
end;

procedure StartProfiling;
begin
  MemoryProfiler.Active := True;
end;

procedure StopProfiling;
begin
  MemoryProfiler.Active := False;
end;

procedure ResetStatistics;
begin
  if GStatistics <> nil then
    GStatistics.ResetItems;
end;

procedure SaveToFile(const AFileName: String);
var
  LStream: TStreamWriter;
  i: Integer;
  LItem: PTMSMIMemoryStatisticItem;
  LLine: String;
begin
  if Assigned(GStatistics) then begin
    // disable profiling for this method
    MemoryProfiler.SuspendMethod;
    // stream will be freed when done
    TObjectGuard.Guard(LStream, TStreamWriter.Create(AFileName));
    // dump the statistics
    for i := 0 to GStatistics.Count -1 do begin
      LItem := GStatistics.Items[i];
      LLine := LItem.TypeName + ' Live: ' + IntToStr(LItem.LiveCount)
        + ' +: ' + IntToStr(LItem.Created) + ' -: ' + IntToStr(LItem.Destroyed) + #13#10;
      LStream.Write(LLine);
    end;
  end;
end;

initialization

finalization
  FreeAndNil(GStatistics);
  FreeAndNil(GFilter);

end.

Within your application call InitializeProfiling once. If possible at a very early stage but initializing at some later point during run-time is not a problem. Also activating/deactivating the profiler several times during run-time is absolutely fine and allows profiling right on the spot. Call StartProfiling from within your application whenever you need to and StopProfiling if you are done. The latter is not strictly necessary but allows accurate control when profiling takes place. Call SaveToFile whenever you need to. Adjust the filter (*StdCtrls) as well as the output to your needs.

I hope you enjoyed my first blog post. One of the next updates of TMS MemInsight will contain some utility classes to make these exports - statistics, memory items, callstacks etc. - even more easy and customizable.

If you have any questions don't hesitate to leave a comment or go to the support forum at https://support.tmssoftware.com/c/developer-tools/tms-meminsight/111

Offering more options, like accessing TMS MemInsight remotely are on our long list of ideas, sketches, enhancements and extensions. Stay tuned!

Stefan Meisner, architect TMS MemInsight



Bruno Fierens




This blog post has received 2 comments.


1. Tuesday, February 22, 2022 at 5:36:27 PM

Very nice. Great job guys.
I see it states "you can already use it in your Firemonkey".
At first we were told that only VCL was supported.
In the releasenotes I didn''t see any mentions of FMX.
So the question is: Is FMX (Windows?) already supported?
Thx.

Jorissen John


2. Tuesday, February 22, 2022 at 6:00:17 PM

The techniques presented here work in a FireMonkey app on Windows (i.e. without the MemInsight UI)

Bruno Fierens




Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post