Hi there,
While investigating why parts of our application felt sluggish (combo boxes taking noticeable fractions of a second to repopulate, page refreshes visibly lagging), we traced the cause to AdvGrid.pas: its initialization section calls HookAdvStringGrid, which installs a WH_CALLWNDPROC hook on the main thread. This happens the moment the unit is linked โ no TAdvStringGrid needs to be created, and it applies to every form and control in the application for the process lifetime.
On current Windows builds, having a WH_CALLWNDPROC hook installed forces kernel mode-transitions on every SendMessage in the thread, adding ~15โ40 ยตs each (the hook proc body is irrelevant โ ours does nothing and costs the same). That sounds small, but ordinary VCL/Win32 UI work is made of thousands of messages: reading one combo item is 2โ3 messages, populating a 200-item combo is ~500, and a routine screen refresh can be tens of thousands. In our application the measured, user-visible effect was:
- populating two date-range combos: ~165 ms instead of ~8 ms โ on every page refresh, so ~330 ms of added latency per navigation;
- reading combo items back: 450 ยตs/item instead of 3 ยตs โ turning simple UI-state checks into 100 ms operations;
- the tax applies app-wide, permanently, including screens that contain no TMS grid at all.
After removing the hook (locally patching the HookAdvStringGrid call out), the same operations returned to single-digit milliseconds and the sluggishness disappeared. The hook only serves the grid FocusHelper feature, which we don't use.
The attached single-file project (AdvGridHookCostRepro.dpr (3.0 KB)) demonstrates this: run it as-is to see a clean baseline and the effect of one synthetic do-nothing WH_CALLWNDPROC hook; then enable {$DEFINE USE_ADVGRID} so AdvGrid is merely linked (nothing called) and observe the baseline degrade to the same profile.
Request: install the hook lazily โ only while a grid actually has FocusHelper active โ or provide a compile-time define to opt out.
Repro:
program AdvGridHookCostRepro;
// Demonstrates the cost of the WH_CALLWNDPROC hook that AdvGrid.pas installs
// unconditionally in its unit initialization (HookAdvStringGrid), even when no
// TAdvStringGrid is ever created and the FocusHelper feature is never used.
//
// How to use:
// 1. Run as-is: prints the baseline costs, then the same measurements with a
// synthetic do-nothing WH_CALLWNDPROC hook installed (the mechanism).
// 2. Remove the '.' from {.$DEFINE USE_ADVGRID} below (or build with
// -DUSE_ADVGRID) so that AdvGrid is merely linked - nothing from it is
// called. The baseline section now shows the same degraded numbers as the
// synthetic-hook section, caused by AdvGrid's initialization alone.
//
// Ask: install the hook lazily (only while a grid actually uses FocusHelper),
// or provide a compile-time define to opt out.
{.$DEFINE USE_ADVGRID}
{$APPTYPE CONSOLE}
uses
System.Diagnostics,
System.SysUtils,
Winapi.Messages,
Winapi.Windows,
Vcl.Controls,
Vcl.Forms,
Vcl.StdCtrls
{$IFDEF USE_ADVGRID}
, AdvGrid // <-- linking this is all it takes
{$ENDIF}
;
var
GHook: HHOOK = 0;
function NoOpHookProc(ACode: Integer; AWParam: WPARAM; ALParam: LPARAM): LRESULT; stdcall;
begin
Result := CallNextHookEx(GHook, ACode, AWParam, ALParam);
end;
procedure RunMeasurements(const ATitle: string);
var
AForm: TForm;
ACombo: TComboBox;
AWatch: TStopwatch;
ATotalLen: Integer;
begin
Writeln('--- ', ATitle, ' ---');
AForm := TForm.Create(nil);
try
ACombo := TComboBox.Create(AForm);
ACombo.Parent := AForm;
AWatch := TStopwatch.StartNew;
for var i := 1 to 10000 do
SendMessage(ACombo.Handle, WM_NULL, 0, 0);
Writeln(Format(' WM_NULL x10000: %8.2f ms (%6.2f us/msg)',
[AWatch.Elapsed.TotalMilliseconds, AWatch.Elapsed.TotalMilliseconds * 1000 / 10000]));
AWatch := TStopwatch.StartNew;
for var i := 1 to 1000 do
ACombo.Items.Add('Item number ' + IntToStr(i));
Writeln(Format(' Items.Add x1000 : %8.2f ms (%6.2f us/item)',
[AWatch.Elapsed.TotalMilliseconds, AWatch.Elapsed.TotalMilliseconds * 1000 / 1000]));
AWatch := TStopwatch.StartNew;
ATotalLen := 0;
for var i := 0 to ACombo.Items.Count - 1 do
Inc(ATotalLen, Length(ACombo.Items[i]));
Writeln(Format(' Items[] read x1000 : %8.2f ms (%6.2f us/item, total len %d)',
[AWatch.Elapsed.TotalMilliseconds, AWatch.Elapsed.TotalMilliseconds * 1000 / 1000, ATotalLen]));
finally
AForm.Free;
end;
end;
begin
{$IFDEF USE_ADVGRID}
Writeln('AdvGrid is linked (nothing from it is called).');
{$ELSE}
Writeln('AdvGrid is not linked.');
{$ENDIF}
Writeln;
RunMeasurements('baseline (no hook installed by this program)');
Writeln;
GHook := SetWindowsHookEx(WH_CALLWNDPROC, @NoOpHookProc, 0, GetCurrentThreadId);
RunMeasurements('with one synthetic do-nothing WH_CALLWNDPROC hook');
UnhookWindowsHookEx(GHook);
Writeln;
Writeln('Done. Press Enter to exit.');
ReadLn;
end.
We have applied the improvement to only install the hook when FocusHelper is enabled.
This improvement will be in the next update.