X
18644 Rate this article:
No rating

DebuggerHelper - A Handy Debugging Class for IDL Developers

Anonym

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:

  1. You have to type "PRINT,". And then some. That's a lot of typing.  (See Implied Print.)
  2. 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.
  3. 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.
  4. 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:

  1. It has to be easy to use and not obstruct the real work my application must perform
  2. Debugging information should display in a widget separate from the IDL Console, if desired
  3. Debugging output should be written to an auto-generated text file with a unique name, if desired
  4. The command to produce output should be simpler to type than "PRINT,"
  5. The utility should be a class that I can instantiate or from which I can subclass
  6. The utility should automatically produce time-stamping and contextual information
  7. 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