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.