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 "useMainThreadID".AMainWnd- the handle of the main application form. 0 (the default) means autodetection.AThreadHandle- the handle of the thread identified byAThreadID.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 selectsThe application seems to be frozen..AAntiFreezeWCTMsg- deadlock exception text. An empty argument selectsA 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
PauseFreezeCheckmust be paired with a call toResumeFreezeCheck. - 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 "useMainThreadID".AThreadHandle- the handle of the thread identified byAThreadID.AAntiFreezeMsg- exception message. An empty argument selectsThe 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
DoneCheckFreezeand start withInitCheckFreeze. - 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
InitCheckFreezefunction". 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
Executeand insert the code that should run when the thread runs. Executeis responsible for checkingTerminatedto decide whether the thread must exit.Executeis responsible for checkingChecksPausedto decide whether freeze checks are disabled.Important!- "Do not forget to callinherited Executeimplementation in your own overridden implementations ofExecute."- The reporting call is
RaiseFreezeException. The class page's own remark puts it plainly: implement your own detection logic, and "callRaiseFreezeExceptiononce 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:
- Override
Executeand callinherited Execute. - Repeatedly check
Terminated; your wait strategy must allow shutdown to finish. - Check
ChecksPausedbefore testing or reporting; skip detection while it is true. - Observe the agreed progress signal without holding application locks across reporting.
- Call
RaiseFreezeExceptionwith the stalled thread's ID and handle when the condition is met. - 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.