EurekaLog
Menu

The EurekaLog Developer Guide

22. Leak detection, configured and read

A leak entry tells you where an allocation began, not where ownership was lost. Start with the operation that grows memory - opening and closing a document, serving a request, rebuilding a cache - and establish when its allocations should end. Then use the allocation stack to find the owner.

EurekaLog normally reports leaks during shutdown. That checkpoint is not proof that every possible deallocation has already happened: some RTL cleanup runs later, and finalization-order defects can produce both leak entries and shutdown failures. Read survivors in the context of their intended lifetime, the compiler/RTL build and the order in which modules close.

This chapter takes you from enabling the right detector to verifying a fix. Chapter 12 covers EMemLeaks calls; chapter 19 covers EResLeaks; chapter 20 explains report navigation.

22.1 Five kinds of allocation, two features

The documentation sorts the ways an application obtains memory into five patterns, and the sort decides which of the two features is watching:

Pattern EurekaLog calls it
Allocation through the Delphi memory manager or its wrappers - GetMem, AllocMem and the things built on them memory leak
Direct allocation from the operating system with VirtualAlloc resource leak
Heap memory obtained with HeapAlloc resource leak
Kernel handles from Kernel32 calls such as CreateFile, CreateEvent or CreateThread, which hold kernel memory on behalf of the application resource leak
GDI and USER handles from User32 and Gdi32 - each process has a quota of ten thousand handles by default resource leak

This table classifies ownership mechanisms; it is not a guarantee that every listed API is hooked. EMemLeaks monitors allocations through Delphi's memory-manager filter. EResLeaks monitors supported imported API calls in the selected resource categories (chapter 19). Both units are included in a EurekaLog-enabled project even when leak checking is disabled; unit presence alone proves neither registration nor reporting is active.

Keep normal shutdown clean where your application controls ownership. Intentionally abandoning every global object may let the operating system reclaim memory, but it fills reports with noise that can hide request-by-request leaks. Conversely, a process-lifetime cache that stays flat is not the same problem as a cache that grows on every request.

Do not diagnose from one Task Manager memory column. Working set, committed memory, reserved address space and requested heap bytes answer different questions. Use the filter counters for tracked heap allocations (chapter 12, section 12.3), resource counts for the relevant handle/API family, and the out-of-memory discriminators in section 22.8 for process-wide pressure.

22.2 The two switches that enable the checks

Configuration for the common case is one sentence: "You can enable/disable leaks control in EurekaLog options" - memory leaks on the Memory problems page, resource leaks on the Resource leaks page.

Two options on the Memory problems page do the work. "Enable extended memory manager" installs EurekaLog's filter around the memory manager, which is what allows EurekaLog "to perform additional checks"; it "must be turned on, if you want to use debugging memory features", and on its own it runs none of the checks below, because at least one of those sub-options has to be selected as well

  • though the filter still does its work, which is why the unit's own description asks for this option to stay on even when every other memory option is off: it associates an exception object with its exception information. The page recommends keeping it on, with one documented exception - a project that must communicate with a module that has neither EurekaLog's memory manager nor FastMM - and warns separately against turning it off on RAD Studio 2007 and earlier. It tracks the life-time of exception objects, several other features need it, including catch-handled-exceptions and chained exceptions, and it makes the application allocate more memory.

"Catch memory leaks" records the allocation information used by the shutdown leak check. With no reportable leaks, no leak dialog is expected; this does not mean the checks were free. With reportable leaks, the project uses its configured shutdown dialog and delivery options. The option also enriches other memory-error reports; a double-free investigation can use the stored allocation history. "Catch memory problems" is the separate misuse check (section 22.3): turning leak reporting off does not itself remove the filter or every memory check.

On the Resource leaks page the equivalent is "Catch resource leaks", with a prerequisite that is easy to miss: you "have to select at least one API category to monitor", and if no category is selected the catch option "will be automatically disabled" - without the list of APIs there is nothing to watch.

Choosing when the check is active

Both pages carry the same pair of activation options.

"Active only when running under debugger" restricts the check - memory or resource - to runs under a debugger such as the IDE, so that end users are not annoyed with "useless error reports". Both pages recommend keeping it on, with the same exception - a server or service application, where a leak accumulates for weeks and the checks are wanted in the deployed build.

"Allow manual activation control" makes the check controllable from the command line:

Switch Effect
/EL_EnableMemoryLeaks enables memory leak detection
/EL_DisableMemoryLeaks disables memory leak detection
/EL_DisableMemoryFilter disables the extended memory manager (the filter itself)
/EL_EnableResourceLeaks enables resource leak detection
/EL_DisableResourceLeaks disables resource leak detection

Two properties are stated. With the option unchecked the switches "are ignored"; with it checked they "will turn on or off" the corresponding detection and they "override any other check for memory leak detection activation". The option can be combined with "Active only when running under debugger" - the configuration the documentation describes for a shipped application: checks off for ordinary users, and a user reporting memory growth asked to start the program with the enable switch. One limit comes with it: "it's not possible to enable extended memory manager via command-line switches" - the filter can be switched off from the command line, but not on, so it has to be enabled in the project's options first.

Prove the deployed path is being checked

Before a long reproduction, make one controlled diagnostic build and run it with the same profile, bitness, packages and launch arguments as the failing application.

  1. For Delphi-heap leaks, enable the extended memory manager and Catch memory leaks. For direct Windows allocations or handles, enable Catch resource leaks and the relevant API category.
  2. Decide deliberately whether the debugger-only gate should apply. For a run outside the IDE, clear that gate or allow manual activation and pass the appropriate enable switch above. An enable switch cannot install a filter omitted at startup.
  3. In ordinary application code, inspect IsMemLeaksInstalled and IsMemLeaksEnabled from EMemLeaks. The first answers whether the filter is installed; the second answers whether new allocations are being registered. Neither certifies resource-hook coverage.
  4. In a disposable local run, deliberately leave one named allocation alive, then close normally. Confirm its entry and allocation stack arrive. Remove the probe before returning the build to use. For a resource investigation, similarly exercise a known monitored create/release pair and, separately, a deliberate omission in an isolated diagnostic run.
  5. Repeat without the deliberate omission. Only then begin the real workload. A process killed before normal shutdown has not exercised the shutdown detector.

This small helper belongs in an ordinary diagnostic unit with EMemLeaks and SysUtils in its implementation uses. Call it once, after application initialization, only in the disposable run. It intentionally leaks 1024 bytes; it is not production code.

procedure CreateLeakProbe;
var
  P: Pointer;
begin
  if not IsMemLeaksInstalled then
    raise Exception.Create('Memory filter is not installed');
  if not IsMemLeaksEnabled then
    raise Exception.Create('Leak registration is disabled');
  GetMem(P, 1024);
  MemLeaksName(P, 'Local leak-check probe');
  // Intentionally no FreeMem: verify the shutdown report, then remove this helper.
end;

Check report filters too: a size threshold or entry limit can hide the probe. If it is absent, investigate activation, registration timing, grouping and filtering before concluding that a real workload is leak-free. Do not leave this probe enabled in a service.

22.3 The rest of the memory options page

The same page also carries "Catch memory problems", which checks memory misuse rather than leaks

  • double free, cross-module operations (freeing memory that belongs to another module without a shared memory manager), heap buffer overflow, calling virtual or interface methods of an already deleted object, and out-of-memory errors. Without it, misuse can continue unnoticed. It can operate without leak-stack collection; failure behavior also depends on the error and handler configuration (chapter 12, sections 12.4 and 12.7), so do not assume every detected allocator error is recoverable. The rest of the page belongs to memory debugging rather than to leaks: reserving - not committing - the lower 2 GB (32-bit) or 4 GB (64-bit) of the address space so that allocations land at higher addresses and integer and pointer bugs surface, where a 32-bit application must carry the IMAGE_FILE_LARGE_ADDRESS_AWARE flag or it will have "no memory to use" and the option is not for 32-bit production builds (the same reservation from code is ReserveLowerMemory, chapter 18); filling a released block with zeros or with the $DEADBEEF debug marker, which "may be replaced with" $FEEEFEEE, $FDFDFDFD, $FADEDEAD, $CCCCCCCC or $00009999, so that a later access produces a recognisable address - while "EurekaLog does not support checks for detecting memory writes after free"; wiping the unused stack when the application goes idle, which needs an Application.OnIdle handler and so applies to VCL, CLX and FMX applications using the (Q)Forms unit (MemWipeStack, chapter 18); and the shared memory manager, which is installed by "Share memory manager" and merely adopted by "Use existing shared memory manager" - adoption ignores the module's own manager settings. Sharing is what allows strings and dynamic arrays to be passed to and from DLL functions, provided every participating module was compiled with EurekaLog and the option. Two requirements come with it: a single-threaded executable sharing with a multithreaded DLL needs IsMultiThread set to True, or it "will crash when a thread contention occurs"; and because statically linked DLLs are initialised before the executable, the adopt-only setting can leave some of them outside the sharing.

Initialization order is essential in an unusual project: EMemLeaks must initialize first, with no active memory allocations before its initialization, so it comes first in the project's uses clause. A handful of units are allowed ahead of it by name - memory managers such as FastMM4, SafeMMInstall or ShareMem, plus helpers like FastMove, FastCode and FastSys - and more through the PreInitUnits option under "Advanced" / "Custom / Manual".

22.4 What the shape of the project changes

For a typical Delphi VCL, FMX or console executable without packages or shared memory, start with the options above and the activation probe. Keep EMemLeaks initialization early as described in section 22.3; adding it after units with live allocations is not an equivalent setup.

For C++Builder, selecting the check is not enough. The build needs the EMemLeaksBCB.cpp bridge and the applicable linker/initialization setup. Dynamic RTL (_RTLDLL) disables the memory features. Follow chapter 2 for the supported compiler/platform, USEOBJ ordering before USEFORM, EurekaLogInstallCpp() requirements and full rebuild; do not copy one line between compiler families.

For DLLs, packages and shared memory managers, also use the module setup in chapters 16 and 2. Match memory-manager settings in all sharing modules, establish who owns each allocation and which module releases it, and check load/unload order. A DLL profile does not exempt its allocations from ownership rules. An unloaded allocating DLL can prevent readable allocation stacks, and adopt-only sharing can miss a statically loaded DLL that initialized before the EXE.

Leak checking can be enabled without activating the rest of EurekaLog: disable Activate EurekaLog and enable the required memory/leak options. This is useful for compatible memory-manager participation, but still requires verification in the actual executable and DLL combination.

22.5 What arrives, and when

Reportable leaks are gathered into one leak report and use the configured reporting destination. The normal trigger is the shutdown checkpoint, not the moment the last useful reference disappears. A long-running service may accumulate a leak for days before that report is attempted.

Leak reports omit CPU and Assembler sections and do not execute standard reporting events. Their dependencies may already be finalized. Prepare required configuration earlier; do not assume ordinary exception callbacks can add data or filter entries at this stage.

Two historical examples explain why "still allocated" requires interpretation:

  • In a C++Builder 10.3 Rio shutdown case with EurekaLog 7.7.8.2, staff traced stream-lock allocations that remained at EurekaLog's checkpoint and were released by subsequent C++ RTL cleanup.
  • In a Delphi Berlin Update 1 FMX case with EurekaLog 7.5.0.0, staff attributed remaining weak-reference allocations and a later failure to System finalization running after EurekaLog. That conversation did not establish a successful fix.

These are specific ordering explanations, not a blanket exemption for RTL-looking stacks. Chapter 26 examines the cases. In your build, distinguish an understood late finalizer from a lost application owner, and distinguish both from a free attempted after the memory filter has been finalized.

22.6 Reading a leak entry

A leak report is a list of blocks. The documentation shows an entry first for an unnamed block allocated by a click handler:

$06961F90 x 1 DATA [Data at $06961F90] 1024 bytes (total of 1024 bytes)

(00318491){Project70.exe} [00718491] Unit62.TForm62.Button1Click (Line 33, "Unit62.pas") + $5
(00247C57){Project70.exe} [00647C57] Vcl.Controls.TControl.Click (Line 7442, "Vcl.Controls.pas") + $8

Field by field, the line says: the block's address ($06961F90), the number of blocks merged behind this entry (x 1), the kind of allocation (DATA), then the block's description and its size, followed by the total this entry accounts for, children included once they have been grouped into it. The description in square brackets is the anonymous one the documentation calls hard to interpret, and the reason the naming routines exist. Underneath the row is the allocation call stack, in the report's usual line format; chapter 20, section 20.2 names its fields one by one. The first relevant application frame leads you back to the allocation path, not necessarily the faulty line.

Names replace that description in place - the same 1024-byte block after MemLeaksName:

$06F91F90 x 1 DATA [[P variable] Data at $06F91F90] 1024 bytes (total of 1024 bytes)

The naming calls themselves - MemLeaksName and its string and dynamic-array variants, and the rule that a pointer must point at the beginning of the block - are chapter 12, section 12.5. What matters here is what the documentation uses them for: a name is assigned to a block allocated through the memory manager while leak checking is active, it can be changed at any time, and it is the documented answer when you "have problems identifying resource". Any dynamically allocated type can be passed as a pointer, with strings and dynamic arrays the documented exceptions - their headers sit at negative offsets. The article on ignoring a leak lists the ways a leak can be identified - by exact variable, leak type, location, size and whatever else the block carries - and naming the block covers the first of those.

What no entry contains is the release: "memory leak report does not point to the problem". It holds the resource and the call stack of its allocation, "but where is our problem? An actual problem is sitting at 'release/free' moment!" The tool cannot know where you planned to release the resource, and that is what section 22.9 builds on.

22.7 Making the report readable: the filters

A leak list is a list of what your code did, not of what is wrong with it, and a large application can produce a large one. The Memory problems page therefore carries options whose only purpose is to remove, merge or limit entries.

Grouping children under their parents

"Group child memory leaks with its parent" hides child leaks from reports. The pair to have in mind is the documented one: a leaked object is a parent leak, a leaked field of that object is a child leak. Its illustration is a TComplexLeakObject whose constructor calls AllocMem(10240) into a field that its destructor frees: when the object itself is never freed, two blocks are left behind. With the option unchecked "you'll get report about 2 different leaks"; with it checked, and the heuristic below also checked, "you'll get report only about 1 leak", the field's size added to the object's. The grouping option "will only group explicitly parented memory blocks"; it should be turned on "to minimize large reports" and off "to get detailed reports", and turning it off means being prepared to "greatly increase" the limit of "Maximum leak number to report", two options below. Declaring a parent explicitly, when there is no constructor to infer it from, is the MemLeaksOwn / MemLeaksSetParentBlock family of chapter 12, section 12.5.

"Use call stack heuristic for grouping" makes that automatic: for a block that is an object, EurekaLog searches for other blocks that share its call stack, and the search "will return all blocks that were allocated in parent's constructor". Four properties are documented: it is ignored when the grouping option is off; it is heuristic; it only works with objects; and it can only group leaks created inside the parent's constructor - assign an object's fields outside the constructor and the call stacks differ, so those leaks will not be grouped. Enabling it "will greatly increase time to build leak report", so do not use it in an application with an enormous number of leaks. Turning it off, like turning the grouping option off, means being prepared to "greatly increase" the "Maximum leak number to report" limit.

The four options that filter and bound

"Hide string leaks" hides leaks of String type. Strings have automatic life-time management, so a string in a leak report "typically" does not indicate a string leak - it leaks because its owner was not deleted, and "the presence of information about the leaked string owner in the leak report is sufficient" while the string itself only adds noise.

"Hide RTL/VCL leaks" suppresses recognized RTL/VCL cases, including known compiler-library bugs and allocations kept by design. It is a visibility rule, not proof that every hidden allocation is harmless in your scenario. Compare with hiding disabled in a controlled investigation when needed.

"Minimal memory leak size to report" filters by size: 0 reports all leaks, a greater value reports only when "total leaked memory's size is greater than (or equal to) the specified value".

"Maximum leak number to report" limits the count: 0 reports all leaks, "use with caution", and a greater value reports only the first N. The purpose is report size and shutdown time - it avoids "creation of huge bug reports" and the "temporary hung at shutdown, when EurekaLog will busy creating report". One caveat matters when you are counting: EurekaLog "may report less leaks than this option is set to", because some leaks are grouped as the same leak or merged into a parent.

RAW tracing, and the resource side

"RAW stack tracing" switches the call stacks built for memory problems from the default frame-based method to RAW tracing, which "can show more complete call stacks, but it's significantly slower" (chapter 11 covers the two methods). Keep it off until you meet a problem you cannot solve without a more detailed stack, and never deploy a release build with it enabled. Running with RAW tracing under the debugger may "occasionally raise access violation exceptions (from IsValidCallSite called from TracerProcLeaksRAW)" - the page calls this normal, and states that the exception is handled inside the tracing code and never reaches the application's own handlers.

The Resource leaks page carries the same RAW option, the same "Hide RTL/VCL leaks" option and the "APIs to monitor" list; it has no equivalents of the grouping, string hiding, size and count options.

22.8 What the checks cannot see

The detector observes a subset of allocations, at a particular time:

  • Delphi-heap monitoring does not cover direct HeapAlloc or VirtualAlloc allocations. Resource monitoring is a separate imported-function hook mechanism and sees only supported, enabled API paths; chapter 19 lists the coverage limits and custom registration rules.
  • Recorded leak stacks are documented as limited to 35 entries. Missing or unloaded module symbols and filtering can shorten what you can use. The unloaded-DLL limitation for memory reports applies to shared-memory-manager applications; resource reports carry the same limitation without that qualifier.
  • Shared-memory participants need compatible settings in every module. C++Builder Dynamic RTL disables memory features. Neither a quiet run nor an empty report proves these prerequisites.

Budget for the configuration, not a percentage

Historical estimates give roughly 5% operation overhead and 200/400-byte x86-32/x86-64 per-allocation cost; these are not guaranteed bounds. KnowHow 92 describes much larger allocation-heavy slowdowns. KnowHow 98 illustrates 32 bytes without leak stacks and 272 bytes with leak checks, but does not establish an identical build/bitness configuration for comparison with the other estimates. Do not combine them into a universal formula.

Measure your real workload with the intended settings. Tiny blocks amplify metadata cost; RAW tracing and constructor grouping add work of their own. Disabling registration does not uninstall the filter (chapter 12). If a temporary filter-disabled rebuild isolates a slowdown, first consider reducing allocation churn or leak-stack collection; retaining the filter preserves other diagnostics.

When growth becomes an out-of-memory report

Use the report's values together. They are observations taken around a failure, not an atomic accounting identity; memory can change before the report is assembled.

Observation Next investigation
Total allocated (virtual) approaches the process address-space limit Address-space exhaustion, including reserved regions and loaded modules. A normal 32-bit process can hit its 2 GB limit while the machine still has ample RAM. Large-address-aware limits depend on the executable and OS configuration.
Address space remains, but Total available (page file) is low System commit pressure; inspect the paging-file configuration and other processes, not just this heap.
The second Total allocated (RTL) value greatly exceeds the first Filter overhead is significant: the pair represents tracked bytes without and with EurekaLog overhead. Inspect the number and size of small allocations.
Virtual usage greatly exceeds tracked RTL bytes Investigate non-RTL allocations, modules and reservations. It is not automatically a Delphi-object leak.
Largest available is small compared with the failed request, despite substantial aggregate space Fragmentation is a candidate. The value is the largest free virtual-address region, not a promise that the allocator can satisfy a request. Allocators may need a larger backing chunk than the requested payload.
A small request fails with ample address space and system capacity Investigate allocator state and corruption rather than assuming a lack of physical RAM. The values alone do not prove corruption.

Fix the identified constraint. For fragmentation, separate short- and long-lived allocations and reduce churn; for overhead, reduce many tiny allocations or intentionally narrow diagnostic collection; for a true leak, fix ownership. Turning the extended memory manager off indiscriminately can remove the evidence needed to find the cause. In local diagnostic builds, corruption investigations can benefit from both memory checks and leak allocation history.

22.9 From a leak entry to a line of your code

Work one reproducible lifetime at a time:

  1. Record the baseline. Use a fixed build, data set and activation settings. Warm up caches, then perform one operation and its intended cleanup. Repeat with more operations in a fresh run.
  2. Read the allocation stack. Identify the type, size, count and first relevant application frame. Grouped child blocks may all be consequences of one missing parent release. Disable grouping locally if you need to inspect the individual allocations.
  3. Write down the owner. Who acquired the block or handle? Was ownership transferred? Which destructor, scope exit, finally, reference-count transition or API call should release it?
  4. Follow every exit. Look for exceptions between allocation and cleanup, overwritten pointers, early returns, unfinished asynchronous work and reference cycles. The allocation can be entirely correct while the release path is wrong.
  5. Classify shutdown-only survivors. Check compiler/RTL version and finalization order before labelling them lost ownership. Reproduce outside the IDE with checks explicitly enabled; this removes one environmental variable, not every possible false positive.
  6. Verify the fix against the same workload. After each operation's cleanup, current tracked counts should return to the intended baseline or a documented bounded cache level; peaks need not fall. Check the shutdown report as well, without newly hiding the original entry.

For a process that grows while running but releases everything at exit, use current counters and allocation dumps rather than expecting a shutdown leak report to expose the entire lifetime problem. DumpAllocationsToFile is local-debugging only: it blocks memory operations and its contract warns that the heap may be invalid afterwards. End each disposable run after a dump; compare separate runs with different operation counts. Do not add periodic dumps to a production service (chapter 12, section 12.3).

Only after establishing the owner and lifetime should you suppress an entry. Expected-leak registration excludes an already allocated block; it neither frees it nor makes subsequent growth safe. Use the exact known block and the correct pointer variant, register late while it is still valid, preserve third-party ownership, and record why the exclusion exists. Strings and arrays need their dedicated variants, and WideString is not supported by the string registration routine. Chapter 12, section 12.6, gives the registration/unregistration signatures and limits. Ordinary exception filters are too late in their own lifetime to filter shutdown leaks.

Habits that keep the list short

To keep the number of leaks down, use managed data types with reference counting and smart-pointer wrappers for unmanaged ones, while accounting for managed-type leak patterns such as circular references between objects; a single exit path per function, with function-scope allocations freed in one block at the end; try/finally so that finalisation and disposal are guaranteed; care with the RTL's type-less functions, since any code working with an untyped argument must be analysed for the possibility of a leak; and naming blocks for easy identification (section 22.6).