DebuggerHelper - IDL 开发者的便捷调试类
21313 为此文章评分:
无评分
DebuggerHelper - IDL 开发者的便捷调试类
Jim Pendleton 2014年6月26日,星期四
还记得我们IDL中的老朋友——PRINT 过程吗?
除了忠实地展示我们导出的输出结果外,多年来它也帮我们解决了许多棘手的代码开发问题,尤其是在仅凭Workbench断点不够用时。是的,有时候连我的代码里也会出现错误,这得归功于宇宙射线、随机按键以及大脑突然卡壳。
然而,PRINT作为调试工具也有其缺点。以下是其中四个:
- 你必须输入"PRINT,",而且还有更多内容。这需要大量输入。(参见 Implied Print。)
- 除非你开发的应用程序只有一个用户,否则通常最终需要定位并注释掉或移除所有用于调试的PRINT语句。如果你不这样做,在客户、经理或论文委员会面前的现场演示中,IDL控制台选项卡上可能会出现可疑的注释。别问我是怎么知道的。
- 当你的Workbench或命令行窗口由于故意的不当行为(参见:bug)而关闭时,你会丢失任何本可以帮助你调试问题的PRINT输出,这些信息会在莫名其妙的逻辑中消失。
- 如果你分发IDL运行时应用程序(正如我们NV5定制解决方案团队经常做的那样),PRINT语句几乎没有什么价值,因为没有控制台输出。你需要一种方法从最终用户那里获取有用的信息,来补充"IDL崩溃了!"这种标准描述。
在多年的应用程序开发过程中,我逐渐形成了一种设计模式。我发现我真的需要一些有用的功能:
- 它必须易于使用,并且不能妨碍我的应用程序必须执行的真正工作
- 调试信息最好能显示在独立于IDL控制台的小部件中(如果需要)
- 调试输出最好能写入自动生成的、具有唯一名称的文本文件中(如果需要)
- 产生输出的命令输入起来应该比"PRINT,"更简单
- 这个工具应该是一个类,我可以实例化它,或者继承它来创建子类
- 该工具应该能自动生成时间戳和上下文信息
- 我应该能够在不修改源代码的情况下启用调试
现在,我将向你展示最终这个IDL类的简洁性,它提供了所有这些功能,却只有大约150行代码。代码在文章底部列出。你可以复制、粘贴并编译来跟着操作。
创建DebuggerHelper对象并开启调试
IDL> d = debuggerhelper(/on)
这会同时创建一个日志文件和一个调试文本小部件。实例化不能再简单了,这样就满足了我的前三个需求。
生成一行调试输出。
IDL> d.o, 'Hello bug'
(DEBUGGERHELPER) $MAIN$:Hello bug
需求4?满足。
我这里只展示了IDL控制台中的输出,但我同时也将信息写入了一个独立的文本小部件以及一个文本文件。显示的数据表明了输出是由哪个类产生的(可能是DebuggerHelper的子类),以及调用的上下文。在这个例子中,我们处于IDL命令行环境,所以显示为$MAIN$。
如果它是在一个IDL函数或过程内部被调用的,那么该例程的名称将替换$MAIN$,如下文所示。
我可以开启TIC 和 TOC 管理来帮助测试性能。这不是最初的需求,但在写这篇文章时我将其添加到了我的类中,因为实现起来非常简单。
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.
如果需要,我可以定义多个按名称区分的计时器来管理不同的代码段。
显然,对于内嵌在调试语句中的高性能测试,TIC和TOC不会那么精确,因为观察和记录的行为会干扰系统。然而,对于被测试操作所需时间显著超过调试I/O时间的情况,它们仍然有用。
我可以编程方式切换调试的开启和关闭,这意味着我不需要编辑源代码,这又满足了我的一个需求。
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
调试日志文件会自动生成并具有唯一名称,满足了另一个需求。让我们看看它被放在哪里了。
IDL> d.file
C:\mylib\logs\DEBUGGERHELPER1403199697.log
在这个实现中,日志文件是利用ROUTINE_FILEPATH 相对于源代码.pro文件(或SAVE文件)的位置创建的,但是在修改源代码或者从原始实现创建子类时,你可以根据自己的喜好进行调整。
即使在对象被销毁或IDL退出后,除非我主动删除文件,否则我仍然可以查看我的调试信息。
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
请注意,日志文件包含了额外的、未打印到IDL控制台的时间戳信息。
我喜欢显示两个时间。第一个是自对象创建以来经过的秒数(相对时间)。对我来说,用这种方式格式化的数字做经过时间的脑力计算更容易。第二个时间是SYSTIME(1) 的输出。如果我的应用程序中有多个调试对象,以后我可以合并日志文件并按时间排序消息。例如,我可能想要对我应用程序不同部分使用的设备通信协议进行排序,甚至是在单独的独立应用程序或IDL_IDLBridge进程之间进行排序。
我可能想要求运行我应用程序的客户开启调试,以便我在代码中构建的内部语句能发挥作用。我们如何完全满足最后一个需求,即我不需要更改源代码就能在运行时IDL应用程序中启用调试呢?
GETENV 或 COMMAND_LINE_ARGS 提供了两种选择。例如,我可能会要求客户在启动我的IDL运行时应用程序之前,在他们的操作系统中创建一个环境变量,其名称是我在代码中硬编码的。在执行期间,我总是创建调试对象,但我会根据环境变量的设置来切换调试语句的开关,例如:
toDebug = getenv('DEBUG_IDL_APP') eq 'YES'
d = debuggerhelper(on = toDebug, no_file = ~toDebug)
在这里,如果用户没有创建一个名为DEBUG_IDL_APP的环境变量,或者他们没有用字符串"YES"来填充该环境变量,那么调试助手对象将被创建,但会保持静默。
甚至,在特定目录中放置或未放置一个文件,结合代码中相应的FILE_TEST调用,也可以用于此目的。
将调试对象添加到另一个类中,或从其继承创建子类,将提供额外的反馈。这里,我们将在新的"MyObj"类中创建一个DebuggerHelper对象作为成员变量。
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.
创建该类的一个对象并调用生成调试输出的方法:
IDL> m = myobj()
IDL> m.yelp
(DEBUGGERHELPER) MYOBJ::YELP:Within myobj
请注意,产生调试输出的类名和方法名现在也显示在我的输出中了。
以下是DebuggerHelper的源代码IDL。我认为它很直接,但如果你有任何问题,请留言评论。我很乐意收到反馈,并且如果不仅仅是网络爬虫阅读我们的博客,我会对此有更好的感知。
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