The debugging facilities can be roughly grouped into 3 basic mechanisms:
self halt
",
which need a recompilation of the inspected method,
these (method oriented) wrappers do not modify the source code
or recompile any existing method. Thus machine-code (primitive) methods remain
untouched and are not replaced by possibly slower interpreted bytecode.
"changes"
file
and removing breakpoints is much easier than searching for leftover debug-halts
(simply evaluate "MessageTracer cleanup").
Wrapping break- and tracepoints are possible from both on individual instances, or per method.
The inspector can install traces/traps on the inspected object via its popup menu.
These facilities are implemented by changing the class of the debugged instance to an anonymous subclass of the creating a wrapper method which is installed in that classes method dictionary.
Thus no recompilation or runtime checks are needed, which would slow down the system noticably. However, the object's class identity changes while this wrapping is in effect, which may affect code depending on it (i.e. code which does "foo class == SomeClass" or which uses the class as key in an identity-based collection, such as an IdentityDictionary).
MessageTracer trap:anObject selector:aSelector
arranges that the debugger is entered, whenever a aSelector
message is sent to anObject.
You can also specify a list of selectors,
as in:
or arrange for all messages (which are understood) with:
MessageTracer trap:anObject selectors:aCollectionOfSelectors
MessageTracer trapAll:anObject
In the debugger, use "step", "send" or
"continue" to resume execution in the breakpointed method.
A breakpoint on an individual selector is removed by:
MessageTracer untrap:anObject selector:aSelector
to remove all traps on anObject evaluate:
Of course, in order to refer to anObject, you need access to it,
so the above is done by evaluating expressions either in an inspector
(refering to the object as "self" in its workspace view),
or by keeping a reference in a workspace variable (see
"Workspace documentation").
MessageTracer untrap:anObject
It may also be useful to arrange for the above traps programmatically, for examlpe in a test case. in this case, you would write an exception handler around the code, to catch the breakpoint in your program. (those breakpoints raise a BreakPointInterrupt exception, which is a subexception under ControlInterrupt.)
MessageTracer trace:anObject selector:aSelector
this arranges that a trace message is printed to the standard error output
(Stderr
),
both upon entry and exit to/from the method.
In analogy to traps, there are also:
to install tracers for more selectors, and:
MessageTracer trace:anObject selectors:aCollectionOfSelectors
to install tracers on all implemented selectors.
MessageTracer traceAll:anObject
The trace is removed with:
or:
MessageTracer untrace:anObject selector:aSelector
to remove all traces of anObject.
MessageTracer untrace:anObject
MessageTracer traceSender:anObject selector:aSelector
arranges that the sender is output the standard error, whenever some message is sent.
Use untrace:selector:
or untrace
(as described above), to remove the trace.
|arr|
arr := #(1 2 3 4 5).
MessageTracer traceSender:arr selector:#at:.
arr collect:[:e | ]
in contrast to:
|arr|
arr := #(1 2 3 4 5).
MessageTracer trace:arr selector:#at:.
arr collect:[:e | ]
MessageTracer
wrap:anObject
selector:aSelector
onEntry:entryBlock
onExit:exitBlock
where anObject and aSelector are as above,
entryBlock is a one-argument block to be evaluated on entry into the method.
It will get the current context passed as argument.
This allows conditional breakpoints or conditional tracing.
It can also be used
to implement pre- and post conditions a la Eiffel while debugging.
For example, you want to trace any sent to an object with a specific argument,
use something like:
(in the above example, the first argument is checked for being greater than 5;
if so, the debugger is entered)
|p|
p := Point new.
MessageTracer wrap:p selector:#x:
onEntry:[:con |
(con args at:1) > 5 ifTrue:[
Debugger
enter:con
withMessage:'hit breakPoint; arg > 5'
]
]
onExit:nil.
p x:4. "nothing happens"
p x:-1. "nothing happens."
p x:10. "bummm"
Postcondition checking can be implemented with this mechanism.
For example if you want to
check the range of some instance variable after every send of some selector,
use:
(in the above, replace getInstVar by an appropriate access selector
for the instance variable to be checked)
MessageTracer
wrap:someObject
selector:someSelector
onEntry:nil
onExit:[:con :retVal |
(con receiver getInstVar between:min and:max) ifFalse:[
Debugger
enter:con
withMessage:'postcondition violated'
]
]
MessageTracer
trapModificationsIn:anObject
and arranges that the debugger is entered, whenever any instance variable is changed in
anObject.
You can also specify a filter block, which gets the old value and new value of the object as arguments:
This kind of breakpoint is installed by:
arranges for a debugger to be entered if some instance variable is changed in anObject
AND the filterblock returns true.
Example:
MessageTracer
trapModificationsIn:anObject
filter:[:old :new | conditionForBreak]
Example (only trap modifications of x):
|p1 p2|
'creating points' printNL.
p1 := Point x:1 y:1. p2 := Point x:2 y:2.
'setting breakpoints' printNL.
MessageTracer trapModificationsIn:p1.
"p2 has no breakpoints set: - nothing happens"
'p2 x:' printNL. p2 x:22.
'p2 x' printNL. p2 x.
"now let it trap ..."
'p1 x:' printNL. p1 x:5.
'p1 x' printNL. p1 x.
'remove breakpoints' printNL.
MessageTracer untrap:p1.
"nothing happens now ..."
'p1 x:' printNL. p1 x:6.
|p|
'creating point' printNL.
p := Point x:1 y:1.
'setting breakpoints' printNL.
MessageTracer trapModificationsIn:p filter:[:old :new | old x ~~ new x].
"nothing happens for y"
'p y:' printNL. p y:22.
"now let it trap ..."
'p x:' printNL. p x:5.
'remove breakpoint' printNL.
MessageTracer untrap:p.
"nothing happens now ..."
'p x:' printNL. p x:6.
Since the breakpoint/trace is placed on a particular method, you may have to place multiple debugging points if super-sends are involved (which was not the case with the above instance debugging).
The implementation creates a wrapper method and installs it in the classes method dictionary. Thus, it does not affect class identity. Also, the original method is kept in a save place and reinstalled when the wrap is deinstalled later. As the case in the above mechanism, no recompilation is needed, therefore you can wrap even methods with primitive code. Also, the change file/change set is not affected.
The system browser provides menu entries in tehe debugging menu (and in the method list#s popup menu), for the most common wraps. Below, the underlying programmatic interface is described.
MessageTracer trapMethod:aMethod
this arranges that the debugger is entered, whenever aMethod
is about to be executed
(no matter, if for instances of the implementing class, or of any subclass).
The browser has convenient menu and toolbar functions to set this kind of breakpoint, see below or the SystemBrowser documentation.
MessageTracer traceMethod:aMethod
Example:
MessageTracer traceMethod:(Integer compiledMethodAt:#factorial).
5 factorial.
MessageTracer untraceMethod:(Integer compiledMethodAt:#factorial)
MessageTracer traceMethodSender:aMethod
Example:
MessageTracer traceMethodSender:(Integer compiledMethodAt:#factorial).
5 factorial.
MessageTracer untraceMethod:(Integer compiledMethodAt:#factorial)
MessageTracer
wrapMethod:aMethod
onEntry:entryBlock
onExit:exitBlock
Example 1: catching a specific factorial invocation:
cleanup with:
MessageTracer wrapMethod:(Integer compiledMethodAt:#factorial)
onEntry:[:con |
con receiver == 3 ifTrue:[
Debugger
enter:con
withMessage:'3 factorial encountered'
]
]
onExit:[:con :val |
'leaving ' errorPrint.
con errorPrint. ' -> ' errorPrint.
val errorPrintNL.
].
5 factorial.
Example 2: tracing file-open of specific files:
MessageTracer unwrapMethod:(Integer compiledMethodAt:#factorial)
cleanup with:
MessageTracer wrapMethod:(FileStream compiledMethodAt:#openWithMode:)
onEntry:[:con |
(con receiver pathName endsWith:'.st') ifTrue:[
'opening ' errorPrint.
con receiver pathName errorPrintNL
]
]
onExit:nil.
"now, play around with system browser (look at methods source-code)
or look at files with the filebrowser ...
And watch the output in the xterm window."
Example 3: traping access to a specific file:
MessageTracer unwrapMethod:(FileStream compiledMethodAt:#openWithMode:)
cleanup with:
MessageTracer wrapMethod:(FileStream compiledMethodAt:#openWithMode:)
onEntry:[:con |
(con receiver name = 'test') ifTrue:[
Debugger
enter:con
withMessage:'opening file named ''test'''
]
]
onExit:nil.
"now, create a file named 'test' with the filebrowser ..."
MessageTracer unwrapMethod:(FileStream compiledMethodAt:#openWithMode:)
Once enabled, an additional side panel is shown in codeviews (actually: CodeView2) at the left, which displays the line number, and also reacts to clicks by adding/removing statement break- and tracepoints.
Notice, that these breakpoints are compiled into the code; therefore, a new method is created and installed in the class. Thus, they cannot be placed into primitive code. Methods which are already being executed and blocks which have been created before are not affected. Also, the method's identity changes due to the recompilation, which may affect identity based collections. Actually, one of those collections is the browser's current method list itself, so many tools had to be changed to deal with this gracefully. (there may still be situations, where a tool looses track of this, and shows the original method, or it cannot figure out, where a method belongs to).
When the line reached, a debugger is opened, where single stepping, proceeding or aborting is possible as usual. The debugger can also be instructed to ignore this breakpoint (via the menu) or to remove this breakpoint (by clicking on the breakpoint in the left side panel). It is also possible to add additional breakpoints to the stopped method and proceed, and thus skip forward within the debugged method or block.
The breakpoint browser also allows for selective enabling/disabling or filtering the list of shown breakpoints.
The launcher offers a convenient menu item (in the "tools"-"debugging" submenu) to plant a breakpoint on whenever a particular message is sent to the Transcript. Once in the debugger, it is trivial locate the code which was responsible for that message.
MessageTracer cleanup
to remove all trace points in the system,
MessageTracer untraceAllMethods
to remove all method traces.
Also, the Debuggers popupMenu (in the walkback list) offers an
untrace all entry, which does the above cleanup.
You may need this in case you get trapped by a persistent trap ;-)
The search menu contains useful filters to find methods with breakpoints (although, similar functions are found in the launcher and especially the breakpoint browser). Use the one, which is nearest your mouse pointer!
Methods being traced or which have a breakpoint are shown with a stop bullet icon before their name (in the top-right method list).
example:
see whats sent to a StandardSystemView during its
startup ...
|v|
v := StandardSystemView new.
v inspect.
... now, set a trap ...
...
... then evaluate: 'self open'
... in the inspector
Also, the debugger can be instructed to ignore breakpoints either for some time, for a number of invocations, or when some other condition is met. This is *very* useful if a breakpoint was placed on code which is executed elsewhere, for example in parts of the GUI. See the debugger's menu for details.
Finally, the Launcher has a "Remove all Breakpoints" menu item in its "Tools" - "Debugging" menu.
Consider the case where you want to count the number a particular
method is invoced.
To do so, we place a wrapper on the method
which increments a (global) counter:
For example, lets count the invocations of Float +
:
Try a few adds applied to a float, and have a look at the globals
count.
Smalltalk at:#MyCount put:0 .
MessageTracer
wrapMethod:(Float compiledMethodAt:#+)
onEntry:[:con | MyCount := MyCount + 1]
onExit:[:con :ret | ].
To remove the wrapper, execute:
or, simply:
MessageTracer
unwrapMethod:(Float compiledMethodAt:#+)
and don't forget to remove the global counter:
MessageTracer
unwrapAllMethods
In the above example, you will notice that a lot of float
additions happen without your explicit calls (for example, in
the window handling code). Thus, you may prefer to try it
with some other method (for example,
Smalltalk removeKey:#MyCount
sin
).
For example, placing a trace on Context » sender
will lead to this situation,
because that method is indirectly called by the trace-code.
Thus the process will run into recursion overflow problems, and start up
a debugger; this debugger itself will run into the same problem, since during
its startup, it reads the context chain using Context » sender
.
Finally, ST/X's runtime system will kill the (lightweight) process.
If you are lucky, the system is in a condition to enter the
MiniDebugger.
In this case, try to repair things with:
Notice: the Minidebugger of the newest release has a special command for this:
MiniDebugger> I <- I-command; gets you into a
line-by-line expression evaluator
MessageTracer unwrapAllMethods <- remove all breakpoints
<- empty line to leave the
expression evaluator
MiniDebugger> a <- abort
MiniDebugger> U <- U-command; unwrap all methods
Example:
ATTENTION: save your work before evaluating this
MessageTracer traceMethodSender:(Context compiledMethodAt:#sender).
thisContext sender.
MessageTracer untraceMethod:(Context compiledMethodAt:#sender).
after the above, you should repair things, by opening a workspace,
and evaluating:
MessageTracer unwrapAllmethods
(this will remove the tracers, and allow normal use of the debugger again).
Critical methods are all context methods, string concatenation and printing
methods.
To trace these critical methods, use the instance debugging facilities
described above, instead of method debugging - if possible
(it is not possible for context-related methods though).
It is difficult to offer a satisfying solution to this problem - the obvious fix (to simply check for recursion in the wrapper method) would prevent the tracing or breakpointing of recursive methods, such as the factorial-example above.
Future versions may provide better solutions - for now, be warned.
Late note: we have worked hard to detect the problems as described above in most
cases; the debugger tries hard to detect recursive errors, halts and breakpoints,
and ignores them. However, there may still be some rare situations, where this detection
may fail (if the fail-detection code is itself affected).
In situations where the debugger itself (or code called by it) is to be debugged, you can enable the "Halts in Debugger" feature via the debugger's menu.
Copyright © 2013 Claus Gittinger, all rights reserved
<cg at exept.de>