Audio-Visual thread debugging or "Place phone to the PC, please"

Comment on this article

When debugging programs holding a number of threads, it can be real difficult to get an intuitive feeling of how these threads are progressing. One particular problem is knowing if all threads are still ticking along, since an exception in a thread can easily kill that one thread only. The human sense that is most tuned to detecting relative speeds and really small variations in relationships and tempo is hearing, so I got the idea to make the threads each play their tune, then simply lean back and listen to them. Not so amazingly, it turns out to be really useful. Freakish, but useful.

For instance, you've got this customer really far away who claims that your app has "locked up again". Now you can have him hit the secret hotkey, a few letter keys, then ask him to "please hold the phone close to the PC so I can listen to it". Now, ain't that cool. Plus, as a matter of fact, it will give you very useful information over the blower. (Don't go there. Where you were almost going now... down, down!)

So, how did I do it?

The way I do it is to have a "secret" key combination call up a window with the list of commands to listen to (see screenshot). The user can now toggle each sound on and off by using the indicated alphabetic key. At the same time as a beep sounds, the little slash to the left wiggles back and forth, reinforcing the experience and at the same time making it easier to correlate the sound with the list of thread events. Also, some machines don't have sound, and then the wiggly slash is the only indication to the user.

So, how do we implement this in Delphi 6? Well, as follows....

The main form

In the main form, we need to catch the "secret" hotkey to open and close the AV window. All other keystrokes need also be forwarded, so the user won't need to focus on the AV window just to have the keyboard keys react right. In my particular application, there is no keyboard input in the main form, so I can unashamedly forward all keystrokes to the AV window, but if you're not in that situation, you will probably not forward as I do. In short, in the main form, add a OnKeyDown event handler as:

procedure TfrmPmi2ClientExe.sgKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); 
begin
BeepMsgForm.KeyDown(Sender, Key, Shift);
end;

The threads that beep

Each unit that wants to send beeps to the beeping form (that is, the "AV debug window") just calls the BeepOnKey() procedure like so:

BeepOnKey(cOfflineCmd);

...where the cOfflineCmd is a constant telling the BeepOnKey() procedure where it is coming from and what letter it corresponds to in the list.

The form that beeps

In the AV window, these constants are defined as follows:

const
  cNothing0           = 'A';
  cMainThreadTimer    = 'B';
  cMainThreadCmd      = 'C';
  cMainThreadNotify   = 'D';
  cOfflineTimer       = 'E';
  cOfflineCmd         = 'F';
  cOnlineTimer        = 'G';
  cOnlineCmd          = 'H';
  cUpdatesCmd         = 'I';
  cUpdatesTimer       = 'J';
  cClientSessionTimer = 'K';
  cClientSessionCmd   = 'L';
  cNetStatusTimer     = 'M';
  cNetStatusCmd       = 'N';
  cScheduleTimer      = 'O';
  cScheduleCmd        = 'P';

  asKey : array ['A'..'P'] of string = (
    'A. nothing',
    'B. MainThread timer tick',
    'C. MainThread command',
    'D. MainThread notifications',
    'E. Offline thread timer',
    'F. offline thread command',
    'G. online thread timer',
    'H. online thread command',
    'I. updates thread timer',
    'J. updates thread command',
    'K. client session timer',
    'L. client session command',
    'M. net status timer',
    'N. net status command',
    'O. schedule timer',
    'P. schedule command'
  );

How they beep

There are more constants definining how all this must sound:

const
  cfCMD = 400;
  cfNFY = 800;
  cfTMR = 1600;
  cdCMD = 100;
  cdNFY = 50;
  cdTMR = 10;
  cmMainThread = 1;
  cmOffline = 1.2;
  cmOnline = 1.4;
  cmUpdate = 1.6;
  cmClientSess = 1.8;
  cmNetStatus = 0.8;
  cmSchedule = 0.6;

  adFreq : array[Low(asKey)..High(asKey)] of double = (
    50,
    cmMainThread * cfTMR,
    cmMainThread * cfCMD,
    cmMainThread * cfNFY,
    cmOffline * cfTMR,
    cmOffline * cfCMD,
    cmOnline * cfTMR,
    cmOnline * cfCMD,
    cmUpdate * cfTMR,
    cmUpdate * cfCMD,
    cmClientSess * cfTMR,
    cmClientSess * cfCMD,
    cmNetStatus * cfTMR,
    cmNetStatus * cfCMD,
    cmSchedule * cfTMR,
    cmSchedule * cfCMD
  );

  aiDuration : array[Low(asKey)..High(asKey)] of integer = (
    1000,
    cdTMR,
    cdCMD,
    cdNFY,
    cdTMR,
    cdCMD,
    cdTMR,
    cdCMD,
    cdTMR,
    cdCMD,
    cdTMR,
    cdCMD,
    cdTMR,
    cdCMD,
    cdTMR,
    cdCMD
  );

All the above constants do nothing but define how each signal should sound, both in pitch and duration. You can play endlessly with these constants while driving your nearest and dearest to the brink and beyond.

The thing as a whole

What you need more is a stringgrid on your form. I usually use my own stringgrid object that is derived from TStringGrid and is just that little bit nicer to work with. Other colors and stuff. But I put in the standard VCL TStringGrid in the code. Further, as you see, there are eventhandlers for FormCreate, FormClose, and sgKeyDown. The public procedure KeyDown() forwards all keystrokes to the eventhandler for OnKeyDown, so any keys hit while the main form has focus should be equivalent to keystrokes while the AV window itself has focus. Except for the key combination that opens and closes the AV window, those are only handled if they come from the main window. (I do think the closing hotkey should also be handled when it comes from the AV window, but I tired of it. You do it.) The only other public procedure is BeepOnKey() which is called in all places you potentially want to make a noise. Most of the rest of the code is well nigh trivial, so here it comes in all its glory:

unit BeepMsgForm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics,
  Controls, Forms, Dialogs, StdCtrls, Grids;

type
  TfrmBeepMsg = class(TForm)
    sg: TStringGrid;
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure sgKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState); 
  private
    function GetRow(c: char): integer;
    procedure RotateCell(iRow: integer);
    procedure BeepOnKey(cKey: char);
    function IsKeyDown(key: Word): boolean;
    function InRange(key: Word): boolean;
  end;

const

  cfCMD = 400;
  cfNFY = 800;
  cfTMR = 1600;
  cdCMD = 100;
  cdNFY = 50;
  cdTMR = 10;
  cmMainThread = 1;
  cmOffline = 1.2;
  cmOnline = 1.4;
  cmUpdate = 1.6;
  cmClientSess = 1.8;
  cmNetStatus = 0.8;
  cmSchedule = 0.6;

  adFreq : array[Low(asKey)..High(asKey)] of double = (
    50,
    cmMainThread * cfTMR,
    cmMainThread * cfCMD,
    cmMainThread * cfNFY,
    cmOffline * cfTMR,
    cmOffline * cfCMD,
    cmOnline * cfTMR,
    cmOnline * cfCMD,
    cmUpdate * cfTMR,
    cmUpdate * cfCMD,
    cmClientSess * cfTMR,
    cmClientSess * cfCMD,
    cmNetStatus * cfTMR,
    cmNetStatus * cfCMD,
    cmSchedule * cfTMR,
    cmSchedule * cfCMD
  );

  aiDuration : array[Low(asKey)..High(asKey)] of integer = (
    1000,
    cdTMR,
    cdCMD,
    cdNFY,
    cdTMR,
    cdCMD,
    cdTMR,
    cdCMD,
    cdTMR,
    cdCMD,
    cdTMR,
    cdCMD,
    cdTMR,
    cdCMD,
    cdTMR,
    cdCMD
  );

procedure KeyDown(Sender: TObject; key: Word; Shift: TShiftState);
procedure BeepOnKey(cKey: char);

//==================================================================

implementation
{$R *.dfm}

uses Math, strutils, strfuncs;

const
  eCOL_FLAG = 0;
  eCOL_DESC = 1;

// ------------------------------------------------------------------

type
  abool = array[Low(asKey)..High(asKey)] of boolean;

var
  g_sb : abool;
  g_frm : TfrmBeepMsg;

// ------------------------------------------------------------------

procedure OpenScreen();
var
  mfm : TForm;
begin
  if not assigned(g_frm) then begin
    mfm := Application.MainForm;
    g_frm := TfrmBeepMsg.Create(mfm);
    g_frm.Caption := 'You are listening to...';
    g_frm.Left := mfm.Left + mfm.Width;
    g_frm.Top := mfm.Top;
    g_frm.Height := mfm.Height;
    g_frm.Width := 350;
    g_frm.Show;
    mfm.SetFocus;
  end;
end;

// ------------------------------------------------------------------

procedure CloseScreen();
begin
  g_frm.Close;
  FreeAndNil(g_frm);
end;

// ------------------------------------------------------------------

procedure BeepOnKey(cKey: char);
begin
  if assigned(g_frm) then
    g_frm.BeepOnKey(cKey);
end;

// ------------------------------------------------------------------

procedure KeyDown(Sender: TObject; key: Word; Shift: TShiftState);
var
  iRow : integer;
begin
  if (key = VK_SPACE) and (ssCtrl in Shift) then begin
    if assigned(g_frm) then
      CloseScreen
    else
      OpenScreen;
  end
  else if assigned(g_frm) then begin
    g_frm.sgKeyDown(Sender, Key, Shift);
  end;
end;

// ==================================================================

function TfrmBeepMsg.InRange(key: Word): boolean;
begin
  Result := (char(key) >= Low(asKey)) and (char(key) <= High(asKey));
end;

// ------------------------------------------------------------------

function TfrmBeepMsg.IsKeyDown(key: Word): boolean;
begin
  if InRange(key) then
    Result := g_sb[char(key)];
end;

// ------------------------------------------------------------------

procedure TfrmBeepMsg.BeepOnKey(cKey: char);
var
  Key : cardinal;
begin
  key := ord(cKey);
  if InRange(key) and g_sb[char(key)] then begin
    Windows.Beep(Floor(adFreq[char(key)]), aiDuration[char(key)]);
    RotateCell(Ord(char(key)) - ord('A') + 1);
  end;
end;

// ------------------------------------------------------------------

procedure TfrmBeepMsg.RotateCell(iRow: integer);
var
  c : char;
begin
  assert(iRow < g_frm.sg.RowCount);
  c := g_frm.sg.Cells[eCOL_FLAG, iRow][1];
  case c of
    '\' : c := '/';
    '/' : c := '\';
  end;
  g_frm.sg.Cells[eCOL_FLAG, iRow] := c;
end;

// ------------------------------------------------------------------

procedure TfrmBeepMsg.FormCreate(Sender: TObject);
var
  c : char;
  i : integer;
begin
  sg.ClearData;
  sg.RowCount := Ord(High(asKey)) - Ord(Low(asKey)) + 2;
  sg.ColCount := 2;
  sg.ColWidths[eCOL_FLAG] := 20;
  sg.ColWidths[eCOL_DESC] := 250;
  i := 1;
  for c := Low(asKey) to High(asKey) do begin
    sg.Cells[eCOL_DESC, i] := asKey[c];
    Inc(i);
  end;
end;

// ------------------------------------------------------------------

procedure TfrmBeepMsg.FormClose(Sender: TObject; var Action: TCloseAction);
var
  c : char;
begin
  for c := Low(g_sb) to High(g_sb) do begin
    g_sb[c] := False;
  end;
  Action := caNone;
end;

// ------------------------------------------------------------------

procedure TfrmBeepMsg.sgKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin 
  if InRange(Key) then begin
    g_sb[char(Key)] := not g_sb[char(Key)];
    sg.Cells[eCOL_FLAG, GetRow(char(Key))] := IfThen(g_sb[char(Key)], '\', ' ');
  end;
end;

// ------------------------------------------------------------------

function TfrmBeepMsg.GetRow(c: char): integer;
begin
  assert(c >= Low(asKey));
  assert(c <= High(asKey));
  Result := 1 + Ord(c) - Ord(Low(asKey));
end;

// ------------------------------------------------------------------

end.

Comment on this article

TOP