DebuggerHelper - A Handy Debugging Class for IDL Developers
Remember our old friend the IDL PRINT procedure?
In addition to faithfully showing us our derived output, it has also bailed us out of a lot of sticky code development problems over the years when Workbench breakpoints alone didn't suffice. Yes, sometimes bugs end up even in my code, put there by cosmic rays, random keystrokes and bouts of brain freeze.
PRINT does have its drawbacks as a debugging utility, however. Here are four of them:
- You have to type "PRINT,". And then some. That's a lot of typing. (See Implied Print.)
- Unless you develop applications for an audience of one, you generally need to eventually locate and comment out or remove all your debugging PRINT statements. If you choose otherwise, your live demo in front of your customer, manager, or thesis committee may show questionable comments in the IDL Console tab. Don't ask me how I know this.
- When your workbench or command line window is closed as a result of willful misconduct (see: bugs), you'll lose any PRINT output that could have helped you debug the problem causing the needed information to disappear in a puff of illogic.
- If you distribute IDL Runtime applications, as we frequently produce in the NV5 Custom Solutions team, the PRINT statement is of little or no value because there is no console output. You need a way to get useful information from your end user, to supplement the standard description of "IDL crashed!"
Over the course of a number of years a design pattern emerged in my application development. I found I really wanted a number of useful features:
- It has to be easy to use and not obstruct the real work my application must perform
- Debugging information should display in a widget separate from the IDL Console, if desired
- Debugging output should be written to an auto-generated text file with a unique name, if desired
- The command to produce output should be simpler to type than "PRINT,"
- The utility should be a class that I can instantiate or from which I can subclass
- The utility should automatically produce time-stamping and contextual information
- I should be able to enable debugging without modifying my source code
Now I will hook you in with the simplicity of the resulting IDL class, one that provides all this capability but is only about 150 lines long. The code is listed at the bottom of the article. Copy, paste and compile to play along.
Create the DebuggerHelper object and toggle on the debugging
IDL> d = debuggerhelper(/on)
This creates both a log file and a debug text widget. It can't get much easier to instantiate, thereby satisfying my first three requirements.
Generate a line of debugging output.
IDL> d.o, 'Hello bug'
(DEBUGGERHELPER) $MAIN$:Hello bug
Requirement 4? Check.
I'm only showing the output in the IDL Console here, but I'm also writing information to a stand-alone text widget AND a text file. The displayed data shows the class from which the output is produced, which may be a subclass of DebuggerHelper, along with the context of the call. In this case we're at the IDL command line so this is $MAIN$.
If it were called from within an IDL function or procedure, that routine's name would replace $MAIN$, as shown later.
I can turn on TIC and TOC management to help test performance. This wasn't an original requirement, but I added it to my class while writing this article since it was incredibly simple to do so.
IDL> d.tic, 'Timer 1'
IDL> d.o, 'How long did it take me to type this?', toc = 'Timer 1'
(DEBUGGERHELPER) $MAIN$, Performance timer 1 dt=21.950000:How long did it take me to type this?
IDL> d.o, 'That was not very fast.'
(DEBUGGERHELPER) $MAIN$:That was not very fast.
I can have multiple timers defined by name to manage different code sections, if I desire.
Obviously, TIC and TOC aren't going to be as precise for high-performance testing when embedded in debugging statements since the act of watching and logging perturbs the system. However, they're still useful for cases where the operations under test take significantly more time than the debug I/O.
I can programmatically toggle my debugging on and off, which means I don't need to edit my source code, checking another item off my requirements.
IDL> d.off
IDL> d.o, 'Do not criticize my typing speed'
IDL> d.on
IDL> d.o, 'Sorry about that'
(DEBUGGERHELPER) $MAIN$:Sorry about that
The debug log file is automatically generated with a unique name, satisfying another requirement. Let's see where it was put.
IDL> d.file
C:\mylib\logs\DEBUGGERHELPER1403199697.log
In this implementation, the log file is created relative to the location of the source .pro file (or SAVE file) by utilizing ROUTINE_FILEPATH, but you can follow your bliss when modifying the source code, or when subclassing from the original implementation.
Even after the object is destroyed or IDL exits, I can still view my debug information unless I proactively delete the file.
IDL> file = d.file
IDL> obj_destroy, d
IDL> b = strarr(file_lines(file))
IDL> openr,lun,file,/get_lun & readf, lun, b & free_lun, lun
IDL> b
[ 11.310, 1403199708.570] (DEBUGGERHELPER) $MAIN$:Hello bug
[ 66.141, 1403199763.401] (DEBUGGERHELPER) $MAIN$, Timer 1 dt=21.950000:How long did it take me to type this?
[ 84.052, 1403199781.312] (DEBUGGERHELPER) $MAIN$:That was not very fast.
[ 119.896, 1403199817.156] (DEBUGGERHELPER) $MAIN$:Sorry about that
Notice that the log file contains additional time stamp information that was not printed to the IDL Console.
I like to show two times. The first is the elapsed time in seconds since the object was created. It's easier for me to do elapsed-time math in my head with numbers formatted this way. The second time is SYSTIME(1) output. If I have multiple debug objects in an application, I can later merge the log files and sort messages by time. For example I may want to sequence device communication protocols used in different parts of my application, or even among individual, stand-alone applications or IDL_IDLBridge processes.
I may want to ask a customer running my application to turn debugging on so that internal statements I'd built into the code will come to life. How do we fully address the final requirement, that I should not need to change the source code to enable debugging from a Runtime IDL application?
GETENV or COMMAND_LINE_ARGS offer two alternatives. For example, I may ask a client to create an environment variable in their operating system, whose name I have hardwired into the code, before starting my IDL Runtime application. During execution, I always create the debug object but I toggle the debugging statements on or off based on the environment variable setting, for example
toDebug = getenv('DEBUG_IDL_APP') eq 'YES'
d = debuggerhelper(on = toDebug, no_file = ~toDebug)
Here, if the user has not created an environment variable named DEBUG_IDL_APP or they have not populated that environment variable with the string "YES", then the debug helper object will be created, but it will be silent.
Even the presence or absence of a file placed in a particular directory can be used for this purpose, along with a corresponding FILE_TEST call in your code.
Adding the debug object to another class, or subclassing from it, will provide additional feedback. Here, we will create a DebuggerHelper object as a member variable within a new "MyObj" class.
IDL> .run
- function myobj::init
- return, 1
- end
% Compiled module: MYOBJ::INIT.
IDL> .run
- function myobj::init
- self.d = debuggerhelper(/on)
- return, 1
- end
IDL> .run
- pro myobj::yelp
- self.d.o, 'Within myobj'
- end
% Compiled module: MYOBJ::YELP.
Create an object of this class and call the method that generates debugging output
IDL> m = myobj()
IDL> m.yelp
(DEBUGGERHELPER) MYOBJ::YELP:Within myobj
Notice that the class and method names from which the debug output was generated are now displayed in my output as well.
Here's the source IDL for DebuggerHelper. I think it's straightforward, but leave a comment if you have questions. I'd enjoy the feedback, and I'd get a better sense if only web crawlers read our blogs.
Pro DebuggerHelper::_ConstructDebugWidget, Group_Leader
Compile_Opt IDL2
self._DebugTLB = Widget_Base(/Column, $
Title = Obj_Class(self) + ' Debug', $
Group_Leader = Group_Leader)
DebugText = Widget_Text(self._DebugTLB, Value = '', $
UName = 'DebugText', $
XSize = 140, YSize = 24, /Scroll)
Widget_Control, self._DebugTLB, /Realize
End
Pro DebuggerHelper::_CreateDebugFile
Compile_Opt IDL2
LogDir = FilePath('', $
Root = File_DirName(Routine_Filepath()), $
SubDir = ['logs'])
If (~File_Test(LogDir)) then Begin
File_MkDir, LogDir
File_ChMod, LogDir, /A_Read, /A_Write
EndIf
self._DebugFile = FilePath(Obj_Class(self) + $
StrTrim(ULong(SysTime(1)), 2) + '.log', $
Root = LogDir)
If (File_Test(self._DebugFile)) then Begin
File_Delete, self._DebugFile
EndIf
OpenW, DebugLUN, self._DebugFile, /Get_LUN
self._DebugLUN = DebugLUN
File_ChMod, self._DebugFile, /A_Read, /A_Write
End
Pro DebuggerHelper::Cleanup
Compile_Opt IDL2
If (self._DebugLUN ne 0) then $
Free_LUN, self._DebugLUN
If (Widget_Info(self._DebugTLB, /Valid_ID)) then $
Widget_Control, self._DebugTLB, /Destroy
End
Pro DebuggerHelper::DebugOutput, Output, $
No_Print = NoPrint, $
Up = Up, $
Toc = Toc
Compile_Opt IDL2
If (~self._DebugOn) then Begin
Return
EndIf
Elapsed = ''
If (Toc ne !null) then Begin
If (Size(Toc, /TName) eq 'STRING') then Begin
Elapsed = ', ' + Toc + ' dt=' + $
StrTrim(Toc(self._DebugClocks[Toc]), 2)
EndIf Else If (Keyword_Set(Toc)) then begin
Elapsed = ', dt=' + StrTrim(Toc(), 2)
EndIf
EndIf
ThisClass = Obj_Class(self)
Routines = (Scope_Traceback(/Structure)).Routine
Result = '(' + ThisClass + ') ' + $
Routines[N_elements(Routines) - $
(2 + Keyword_Set(Up))] + $
Elapsed + ':' + Output
If (~Keyword_Set(NoPrint)) then Begin
Print, Result, Format = '(a)'
EndIf
If (~Widget_Info(self._DebugTLB, /Valid_ID)) then Begin
Return
EndIf
Now = SysTime(1)
DT = '[' + $
String(Now - self._DebugCreationTime, $
Format = '(f14.3)') + ', ' + $
String(Now, Format = '(f14.3)') + $
'] '
DebugText = Widget_Info(self._DebugTLB, $
Find_by_UName = 'DebugText')
DebugYSize = (Widget_Info(DebugText, /Geometry)).YSize
Widget_Control, DebugText, Get_UValue = NLines
If (N_elements(NLines) eq 0) then Begin
NLines = 0L
EndIf
Line = DT + Result
If (self._DebugLUN ne 0) then Begin
PrintF, self._DebugLUN, Line
Flush, self._DebugLUN
EndIf Else Begin
Print, Line, Format = '(a)'
EndElse
If (NLines gt self._DebugWindowMaxLines) then Begin
Widget_Control, DebugText, Get_Value = Old
Lines = Old[self._DebugWindowMaxLines/2:*]
Old = 0
NLines = N_elements(Lines)
Widget_Control, DebugText, $
Set_Value = [Temporary(Lines), Line]
Widget_Control, DebugText, $
Set_UValue = NLines + 1L
EndIf Else Begin
Widget_Control, DebugText, $
Set_Value = Line, /Append
Widget_Control, DebugText, $
Set_UValue = ++NLines
EndElse
Widget_Control, DebugText, Set_Text_Top_Line = $
NLines - DebugYSize + 3 > 0
End
Pro DebuggerHelper::O, Output, _Extra = Extra
Compile_Opt IDL2
self->DebugOutput, Output, /Up, _Extra = Extra
End
Pro DebuggerHelper::Off
Compile_Opt IDL2
self._DebugOn = 0
End
Pro DebuggerHelper::On
Compile_Opt IDL2
self._DebugOn = 1
End
Pro DebuggerHelper::Tic, Name
Compile_Opt IDL2
self._DebugClocks += Hash(Name, Tic(Name))
End
Pro DebuggerHelper::GetProperty, $
On = On, $
File = File
Compile_Opt IDL2
If (Arg_Present(On)) then On = self._DebugOn
If (Arg_Present(File)) then $
File = self._DebugFile
End
Function DebuggerHelper::Init, $
On = On, $
Group_Leader = Group_Leader, $
No_File = No_File
Compile_Opt IDL2
self._DebugCreationTime = SysTime(1)
self._DebugWindowMaxLines = 500
self._DebugOn = Keyword_Set(On)
self._DebugClocks = Hash()
self->_ConstructDebugWidget, Group_Leader
If (~Keyword_Set(No_File)) then $
self->_CreateDebugFile
Return, 1
End
Pro DebuggerHelper__Define
!null = {DebuggerHelper, Inherits IDL_Object, $
_DebugOn : 0B, $
_DebugLUN : 0L, $
_DebugFile : '', $
_DebugTLB : 0L, $
_DebugCreationTime : 0D, $
_DebugClocks : Hash(), $
_DebugWindowMaxLines : 0L}
End