EurekaLog
Menu

The EurekaLog Developer Guide

14. EFreeze: freeze detection from code

A window stops responding, but no exception is raised. First decide what you need to detect: an unresponsive UI, a deadlock, or a worker that has exceeded an application-specific deadline. These are different conditions, and no single detector proves all of them.

EFreeze runs detection outside the watched thread. Its UI detector checks message responsiveness; its Wait Chain Traversal detector looks for deadlocks between EurekaLog-enabled threads on Vista and later. A worker can keep executing without making useful progress: WCT does not detect such live locks. For a job-specific deadline, supply your own observation and call RaiseFreezeException.

A detection produces EFrozenApplication and enters normal exception processing. Report creation, dialogs and sending still depend on the application's settings; detection does not guarantee delivery (chapter 3). Start investigation with the named thread's stack and its dependencies, not with an assumption that a timeout proves a lock cycle.

For installed/enabled queries and the main-thread-only FreezeNotifyStillAlive notification, see chapter 6, section 6.5. Chapter 28 covers ambiguous freeze reports, including the conflicting historical descriptions of the polling message; do not build application logic around a particular message number.

14.1 What the API lets you do, and to what

You want to Call
Start the detection thread, or re-create it after changing its settings InitCheckFreeze
Stop it completely DoneCheckFreeze
Silence checks for a while, without stopping the thread PauseFreezeCheck / ResumeFreezeCheck
Report a freeze your own code has detected RaiseFreezeException
Choose which class the detection thread is built from FreezeThreadClass (TFreezeThreadClass)
Write that class TFreezeThread and its descendants

Normally configuration starts detection. Enable hang detection in the project's EurekaLog options and set its timeout for the application. Initialization starts the detector automatically unless the options disable it. Use the API below to change a running configuration, deliberately stop detection, or install a different detector.

The configured application timeout and an API parameter default are not the same default. The hang-detection guidance describes a 60-second application default; the declaration of InitCheckFreeze below uses 4 seconds when its timeout argument is omitted. Inspect the actual project setting and pass an explicit timeout when reinitializing.

The detection thread is a real class, and it is replaceable. InitCheckFreeze does not hard-code the thread it creates: it reads the global variable FreezeThreadClass and instantiates whatever class is stored there. Two built-in classes come with the unit - TMainThreadFreezeDetectionThread and TWCTFreezeDetectionThread - and a third option is to write a descendant of TFreezeThread yourself. Sections 14.5 and 14.6 cover that path.

14.2 Starting and stopping the detection thread

InitCheckFreeze

procedure InitCheckFreeze(
  const AThreadID: Cardinal = 0;
  const AMainWnd: HWND = 0;
  const AThreadHandle: THandle = 0;
  const ATimeout: Cardinal = 4;
  const AAntiFreezeMsg: String = '';
  const AAntiFreezeWCTMsg: String = ''
);   // EFreeze

Creates the background detector selected by FreezeThreadClass, subject to the current EurekaLog options. A call is not an unconditional request to override those options.

The six parameters divide into what to watch and what the report will say:

  • AThreadID - the thread to detect hangs inside. Normally the main thread's id: 0 (the default) means "use MainThreadID".
  • AMainWnd - the handle of the main application form. 0 (the default) means autodetection.
  • AThreadHandle - the handle of the thread identified by AThreadID.
  • ATimeout - timeout in seconds, with an API default of 4. Pass the intended value explicitly; preserving configured values also includes preserving any customized messages.
  • AAntiFreezeMsg - freeze exception text. An empty argument selects The application seems to be frozen..
  • AAntiFreezeWCTMsg - deadlock exception text. An empty argument selects A deadlock was detected in thread %d., substituting the thread number.

Two of those parameters come with a caveat that tells you something about how the detection actually works: AThreadID and AMainWnd "can be ignored by some checks". The documentation gives the deadlock case as the example - Wait Chain Traversal detects deadlocks in any EurekaLog-enabled thread, so it needs neither a thread id nor a window handle. Passing them is not wrong; expecting every check to honour them is.

The remarks carry four points that decide whether the call does anything at all:

  • It should be called from the main thread. The documentation marks this as Important!
  • It is normally automatic. Called during initialization of EurekaLog; the documentation invites you to call it manually to re-create or re-initialize the checks - "for example, if you want to change timeout at run-time".
  • It does nothing if a detection thread is already running. This is the documentation's Warning, and it is the reason a re-initialization is a pair: stop with DoneCheckFreeze, then start again.
  • It does nothing if your current settings block freeze detection. The example the documentation gives is settings that say freeze detection must not be active during debugging: run the application under the debugger and the call quietly does nothing. No error is raised; the checks simply never start.

DoneCheckFreeze

procedure DoneCheckFreeze;   // EFreeze

Stops the background freeze detection thread launched by InitCheckFreeze, and does nothing when no such thread is running. The documentation's phrasing is that it should be used "to completely stop freeze detection".

Its Important! note is the mirror image of InitCheckFreeze's: this function should be called from main thread. And its tip is a reminder that stopping is not the only way out - PauseFreezeCheck turns detection off temporarily, "without full stop/re-initialization".

Re-initializing: the documented way to change the timeout at run time

Run this procedure on the main thread, after EurekaLog initialization. It needs only EFreeze and intentionally uses the standard messages and automatically selected main-thread target:

uses
  EFreeze;

procedure SetTenSecondFreezeTimeout;
begin
  DoneCheckFreeze;
  InitCheckFreeze(0, 0, 0, 10); // seconds, not milliseconds
end;

Calling InitCheckFreeze alone while a detector exists does not change its timeout. Stopping and restarting creates a detection gap, so do it at a controlled point rather than repeatedly.

Afterward, inspect IsFreezeCheckInstalled and IsFreezeCheckEnabled. An installed but paused detector can return False for the latter. These queries are momentary observations, not locks or proof that the next operation is covered. If no detector starts, check the project's hang-detection and debugger restrictions. Confirm the intended behavior with a controlled blocking operation in a development build under the same debugger conditions as the target run, not by hanging production.

14.3 Pausing and resuming checks

Pausing creates a blind interval: checks stop while the background detector remains alive. Use it only for a bounded interval in which you have deliberately decided not to detect freezes. It does not repair a hang. If the operation never returns, its finally block never runs either, so a genuine hang inside that interval remains hidden.

If you still need to detect a true hang in the operation, do not pause around it. Move the blocking work off the UI thread, choose a justified timeout, or monitor actual progress from a watchdog. The absence of reports during a pause proves suppression, not responsiveness.

PauseFreezeCheck

procedure PauseFreezeCheck;   // EFreeze

Temporarily disables checks without shutting down the background detection thread.

Three notes come with it, and they combine into a rule:

  • Warning: each call to PauseFreezeCheck must be paired with a call to ResumeFreezeCheck.
  • Tip: it can be called multiple times by the same thread.
  • Note: it can be called from any thread.

Pauses nest: two pauses require two resumes before checks can be active again. This differs from the nonnested leak-registration switch DisableMemLeaksRegistering / EnableMemLeaksRegistering in chapter 12. Do not transfer balancing assumptions from one API to the other.

ResumeFreezeCheck

procedure ResumeFreezeCheck;   // EFreeze

Undoes one pause. A single call does not necessarily enable checks: outstanding pauses still count. It can be called from any thread. Keep each pair in the same lexical scope so normal completion and exception unwinding both restore its contribution. In a unit using EFreeze, wrap the application's existing DoSomethingThatBlocksForAWhile call as follows:

PauseFreezeCheck;
try
  DoSomethingThatBlocksForAWhile;
finally
  ResumeFreezeCheck;   // keeps the pause/resume counts balanced even on an exception
end;

Pausing or notifying

FreezeNotifyStillAlive acknowledges progress for a single check and may be called only by the main thread. One notification does not exempt the whole operation: if notifications stop, a later check can detect a freeze. Notify at genuine progress points in main-thread work, not from an unrelated timer or worker that can remain healthy while the watched work is stuck. See chapter 6, section 6.5 for the declaration.

An uninterrupted blocking call cannot send progress notifications. A pause suppresses its checks, but only use that option if losing hang detection during the call is acceptable.

14.4 Raising a freeze exception yourself: RaiseFreezeException

Some applications can detect their own hangs - a worker thread that has not answered within a sane amount of time, a lock that has not been released, a request that has overrun its budget. Where the condition is the application's own - a deadline it set, progress it expected and did not see - the built-in detectors have nothing to compare against, but the processing behind their report is available as a call.

procedure RaiseFreezeException(
  const AThreadID: Cardinal = 0;
  const AThreadHandle: THandle = 0;
  const AAntiFreezeMsg: String = ''
);   // EFreeze

Constructs EFrozenApplication, builds the target thread's call stack, and invokes standard EurekaLog processing. It reports the condition; it does not release locks, cancel the job or terminate the target thread.

The parameters:

  • AThreadID - the thread id to raise the exception for; 0 (the default) means "use MainThreadID".
  • AThreadHandle - the handle of the thread identified by AThreadID.
  • AAntiFreezeMsg - exception message. An empty argument selects The application seems to be frozen.; pass custom text when your deadline has a more specific meaning.

Two notes from the page: this is the call the detection thread itself uses when it decides a freeze has happened, and it may be called from any thread.

The exception class matters if you route exceptions: the call constructs an EFrozenApplication and then hands it to standard EurekaLog processing, which is what makes a freeze report look like any other report. And the call stack in the report is built for the thread named in AThreadID - not for the caller - so the two arguments decide which thread the report describes.

The helper below belongs in a unit using Windows, SysUtils, Classes and EFreeze. Call it from a watchdog/background thread, never the GUI thread. The caller must retain a concrete, already-started worker with FreeOnTerminate = False and keep both its object and handle alive for the entire call. TimeoutMS is a finite deadline in milliseconds, unlike InitCheckFreeze's seconds.

uses
  Windows, SysUtils, Classes, EFreeze;

function WorkerFinishedBeforeDeadline(const Worker: TThread;
  const TimeoutMS: Cardinal): Boolean;
var
  WaitResult: DWORD;
begin
  WaitResult := WaitForSingleObject(Worker.Handle, TimeoutMS);
  case WaitResult of
    WAIT_OBJECT_0:
      Result := True;
    WAIT_TIMEOUT:
      begin
        Result := False;
        RaiseFreezeException(Worker.ThreadID, Worker.Handle,
          'Worker exceeded its execution deadline.');
      end;
    WAIT_FAILED:
      RaiseLastOSError;
  else
    raise Exception.Create('Unexpected thread wait result');
  end;
end;

True means the thread has terminated, not that its work succeeded: inspect its FatalException and return value as chapter 21 explains. False means a timeout was observed and reporting was requested. The worker may finish immediately afterward. This one-shot helper neither owns nor frees it; the owning code must arrange cooperative completion and cleanup, even after a timeout. Do not free a running worker simply because the deadline expired or use TerminateThread to make the timeout path convenient.

14.5 Choosing the class behind the detection thread

FreezeThreadClass

FreezeThreadClass: TFreezeThreadClass;   // EFreeze, global variable

Determines the class to be used as the background freeze detection thread. Its "value" is documented as the class to create by InitCheckFreeze - which is the whole of its job: InitCheckFreeze reads this variable to instantiate the detection thread. You may store one of the built-in classes (TMainThreadFreezeDetectionThread, TWCTFreezeDetectionThread) or a class of your own; a custom class "has to be descendant of TFreezeThread and it must override Execute method".

Three notes govern when you may change it:

  • If freeze checks are running, the change needs a re-initialization. Stop with DoneCheckFreeze and start with InitCheckFreeze.
  • If you change it before EurekaLog's initialization, no re-initialization is needed. Assigning the variable early is enough.
  • The recommended order, in the documentation's words, is "disabling freeze detection in EurekaLog options, then assigning your own freeze detection class and enabling freeze detection via a call to InitCheckFreeze function". Detection starts from your assignment instead of starting twice.
// before EurekaLog initializes: the assignment alone decides the class
FreezeThreadClass := TMyFreezeDetectionThread;

TFreezeThreadClass

TFreezeThreadClass = class of TFreezeThread;   // EFreeze

The type of that variable: it "accepts TFreezeThread class or any of its descendants", and the documentation states where it is used - the FreezeThreadClass global variable.

It also carries a caution that is easy to miss when reading a class-reference type: TFreezeThread is abstract, you should not use it. The three documented options are TMainThreadFreezeDetectionThread, TWCTFreezeDetectionThread, or a class you implement.

14.6 Writing your own detection thread: TFreezeThread

TFreezeThread is the base class for freeze detection threads - the common ancestor the two built-in classes share, and the class a custom detector must descend from. Its own declaration names the class it builds on, which is where the thread machinery comes from:

TFreezeThread = class(TThreadEx);   // EFreeze

The class exposes the six members described below.

Execute

protected
procedure Execute; override;   // TFreezeThread

Implements the actual freeze detection code. The method to override when the built-in detection does not fit your application.

The remarks are a set of responsibilities, not a description of behaviour:

  • Override Execute and insert the code that should run when the thread runs.
  • Execute is responsible for checking Terminated to decide whether the thread must exit.
  • Execute is responsible for checking ChecksPaused to decide whether freeze checks are disabled.
  • Important! - "Do not forget to call inherited Execute implementation in your own overridden implementations of Execute."
  • The reporting call is RaiseFreezeException. The class page's own remark puts it plainly: implement your own detection logic, and "call RaiseFreezeException once you detect freeze/hang/deadlock".

Call inherited Execute even though the reference does not explain its internal work.

ChecksPaused

protected
function ChecksPaused: Boolean;   // TFreezeThread

Indicates whether freeze checks are disabled: True means the checks are disabled, False that they are enabled. The thread reads it; PauseFreezeCheck is what sets it - the documentation states plainly that the PauseFreezeCheck procedure sets ChecksPaused to True.

The remark tells you how to use it inside an override: "The thread's Execute method and any methods that Execute calls should check ChecksPaused periodically and do nothing when it's True." A detector that ignores this keeps reporting freezes while the application has explicitly asked it to stay quiet - which is what a pause is for.

The four values that come from InitCheckFreeze

Four properties of the class carry values supplied by InitCheckFreeze:

Member Documented as
FreezeTimeout Freeze timeout from InitCheckFreeze
TargetThreadHandle Thread handle from InitCheckFreeze
TargetThreadID Thread ID from InitCheckFreeze
TargetWindow Window handle from InitCheckFreeze

These properties carry the corresponding InitCheckFreeze values; in particular the timeout originates in its seconds argument. No full public property declarations are specified; the table describes the values, not their Delphi types or access modifiers.

Implementing a detector without creating another hang

A custom detector needs an application-specific progress signal and a finite wait between checks. There is no useful generic implementation of that signal. Before registering a descendant:

  1. Override Execute and call inherited Execute.
  2. Repeatedly check Terminated; your wait strategy must allow shutdown to finish.
  3. Check ChecksPaused before testing or reporting; skip detection while it is true.
  4. Observe the agreed progress signal without holding application locks across reporting.
  5. Call RaiseFreezeException with the stalled thread's ID and handle when the condition is met.
  6. Decide what follows a report, so one stalled job does not unintentionally generate an endless stream of identical reports.

Install the class through FreezeThreadClass; use DoneCheckFreeze / InitCheckFreeze on the main thread when a detector is already running. Do not instantiate the abstract base class directly.

The two classes EurekaLog ships

Both are direct descendants of TFreezeThread, and they differ in the mechanism and in the systems they can be used on:

TMainThreadFreezeDetectionThread = class(TFreezeThread);   // EFreeze

Detects hangs in the UI thread. It uses SendMessageTimeout "to detect if main thread is responding on messages". It can be used on any system, and EurekaLog uses it by default on pre-Vista systems (Windows XP and earlier).

TWCTFreezeDetectionThread = class(TFreezeThread);   // EFreeze

Detects deadlocks between threads. It uses Wait Chain Traversal to detect deadlocks "between two or more EurekaLog-enabled threads" - which is also why, as section 14.2 noted, its check ignores the thread id and window handle passed to InitCheckFreeze. It can be used on Vista+ systems only, and EurekaLog uses it by default on Vista and later.

Choosing between them starts from what you need detected, not from the platform: one watches the UI thread, the other looks for deadlocks between threads, and on Vista and later both are available - only the first can be used on Windows XP and earlier. Use a class of your own when neither of them asks the question your application needs answered.

14.7 Reading a freeze result

Observation Interpretation and next step
UI report during a long synchronous action Inspect whether the main thread was processing messages. A legitimate workload can still make the UI unresponsive; move or split the work instead of merely hiding the report.
Deadlock report Inspect the participating enabled threads and their lock/wait dependencies. Do not treat the timeout alone as proof of a cycle.
No report Check installation, active state, pause balance, debugger restrictions and whether the selected detector observes this kind of failure. WCT is not a live-lock detector.
Reports disappear after adding a pause Expected suppression during a blind interval, not evidence that the hang is fixed.
Worker timeout followed by completion A deadline was exceeded; the snapshot and completion describe different moments. Check the deadline and work distribution before calling it a permanent deadlock.

Detector state queries can change immediately after they return. The target thread can also move after a stack is captured. Keep the operation's timing, thread identity and pause interval alongside the report so these observations are not confused.