Linking AdvGrid.pas installs a WH_CALLWNDPROC hook that slows every SendMessage app-wide (10-20x)

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.

Amazing. Thanks!