WatchPoints - rmu75/linuxcnc-wiki GitHub Wiki
date: '2012-04-09T17:22:39' title: WatchPoints
Interpreter Watchpoints, and Run-from-Line
I recently laid out some thoughts about line number handling in the interpreter, which - among many other uses - relates to the Run-From-Line feature (see http://wiki.linuxcnc.org/cgi-bin/wiki.pl?LineNumbers). While I have not implemented the proposal outlined there, I've made some progress on a different approach to the Run-From-Line theme.
The mechanism - interpreter watchpoints - can be used for RFL, but is much more generally applicable than that.
Restating the problem
The current RFL implementation works roughly like this (I'm skipping some details here like boundary tests):
- task records the fact that RFL for line > 1 was issued
- task starts the interpreter on the current file
- the interpreter generates canon commands and puts them onto the interplist, along with the current line number in each interplist node.
- task inspects the nodes in turn, and sees whether the linenumber in the current node is greater equal to the RFL line.
- if the RFL condition holds, start execuing the queued commands, else skip them.
Viewed a bit differently, task is looking for a start condition, and does so by inspecting the line numbers. Consequently, the line number
- with all its deficiencies - is the only start condition which we have.
Moving evalution of conditions to the interpreter: introducing watchpoints
Task clearly isnt a great place for ex-post analysis of a condition which the interpreter could have determined much better in the first place, and it's kind of too late at that point to get clever about it.
Let's steal an idea from a different field: Debuggers like gdb have an interesting feature: conditional breakpoints (see for example: http://blog.vinceliu.com/2009/07/gdbs-conditional-breakpoints.html). What this does is: the program is stepped, and after each step, the condition is evaluated, with some action attached to it.
We can do the same with the RS274NGC interpreter, in particular since the embedded Python interpreter gives pretty much full access to internal state.
The overall idea is as follows:
- provide methods to define a set of watchpoints, which are Python expressions
- execute them before each block is executed (the "stepping" in gdb)
- record the watchpoint results and pass them along with the canon commands generated.
An example Interpreter method could look like so:
interp.set_watchpoint(0, "this.sequence_number >= 7")
Without going through the details: the expression would be true after line 7 of the NGC program has been reached.
Now, if task could in turn inspect the results of this watchpoint, this would not only give the equivalent for Run-from-line, but the capability to associate arbitrary expressions as a start condition.
proof-of-concept
The branch at http://git.mah.priv.at/gitweb/emc2-dev.git/shortlog/refs/heads/watchpoints-run-from-line shows how it can be done. What is does is:
- introduce some new interpreter methods to add and manipulate watchpoints
- set_watchpoint(int num, const char *expression)
- print_watchpoints()
- execute_watchpoints()
- add a canon method to convey the current state of watchpoints (currently up to 32): WATCHPOINT_UPDATE(unsigned condition)
- add methods to interpl.hh/cc to accept and convey the condition mask
- add rudimentary support in task to inspect the current command's condition mask as a run-from-line replacement.
This emulates the current RFL feature by constructing a trivial watchpoing ("this.sequence_number >= startline") and watching the conditon.
To see how it works, build this branch, turn on debugging to 0xFFFFFFFF and do a run-from-line of an arbitrary NGC program. You should see the WATCHPOINT_UPDATE changes and the moves only starting after the given line.
The watchpoint mask might change often; the Run-From-Line condition is derived as ' execute commands after watchpoint 0 has been found true for the first time'.
Python bindings
The new methods are exposed to Python internally and can be used through the ';py,...python code...' pseudocomment feature, see tests/interp/watchpoints .
Example:
; define two watchpoints (default to active)
;py,this.set_watchpoint(0,"this.sequence_number > 7")
;py,this.set_watchpoint(1,"this.sequence_number < 10")
; this is a watch statement - it has no value, but might execute some Python code:
;py,this.set_watchpoint(1,"if this.sequence_number > 10: print 'l>10:',this.sequence_number")
;py,print "line=",this.sequence_number
;py,this.execute_watchpoints()
;py,this.print_watchpoints()
;py,this.set_watchpoint(1,0) # disable wp 1
Other uses
Watchpoints are not limited to emulating run-from-line. Some other uses could be:
- more complex start conditions (up to the outright whacky), like: "run from line X in procedure Y but only if parameter Z has value foo", or "start after third recursion of procedure X"
- assertions, for instance arbitrary axis/joint boundary tests: "assert that the controlled point fulfills condition X", where X is some arbitrary test of a geometric property defined in Python.
- stop conditions: RFL could easily be supplemented by a Run-To-line.
- breakpoints: doable, but pending better support of multiple interpreter contexts in Linuxcnc
I havent fully thought through the implications for stepping, and motion for that matter. I think some use can be made, but for that I have to first understand the current stepping code better.
The current code also permits non-expression watchpoints like so:
pythonprocedure()
or
if <expression>: action()
to call arbitrary Python code (eg a boundary test). However, there is currently no action associated with such a statement (like aborting the interpreter).
Update: the easiest method to deal with it: in the action, raise InterpreterException,"message". In the calling code, detect that and abort the interpreter with that message.
Example for a complex start condition
The watchpoint in the following example reads as:
-
start executing when in Oword subroutine 'mysub' AND
-
the named parameter '#<val>' exists AND
-
#<val> is < 2
;load and point on the next line in Axis, then RFL - this will override the default run-from-line watchpoint ;py,this.set_watchpoint(0,"this.sub_context[this.call_level].subname == 'mysub' and this.params.exists('val') and this.params['val'] < 3")
o<mysub> sub
o200 if [#1 GT 0] (debug, before val assignment) #<val> = #1 (debug,val=#<val>) o<mysub> call [#1 - 1] o200 endif o<mysub> endsub
o<mysub> call [5] m2
Performance
"Oh my god, he's executing Python code on every block". To gauge the problem, I did some ballpark measurements. The upshot is:
- no watchpoints: no performance penalty
- a simple watchpoint (line number test) takes about 2.5uSec on my MacbookPro.
Executing more complex Python code, like an arbitrary boundary test, will naturally take longer. The implementation uses compiled Python code objects for better performance.
TBD
All this doesnt solve the line number problem outlined here:http://wiki.linuxcnc.org/cgi-bin/wiki.pl?LineNumbers, but it improves upon the kind of conditions considerably.
Stepping: not clear yet how this fits in.
Motion stepping: Looking at src/emc/motion I find:
/motion_debug.h:96: int idForStep;
./command.c:1043: emcmotDebug->idForStep = emcmotStatus->id;
./control.c:1955: if (emcmotDebug->stepping && emcmotDebug->idForStep != emcmotStatus->id) {
which I read as: when stepping, motion is looking for a change in idForStep (which is the line_number). Since only inequality is used, I conjecture it would be enough to have a single bit which changes on each line. A different bit in the condition mask could fulfill this purpose, and a watchpoint which changes this bit on every new line encountered. Sounds right ?
Update: this doesnt work - there seems to be an assumption about increasing motion id's, maybe in the tp.
Motion: Passing the watchpoint condition mask down to motion along with the linenumber is easy. Not sure about the use scenario.
Task: there's no reason why task couldnt have such watchpoints as well, although I dont see a useful scenario at this point (safety?). The footwork is mostly done - much of task and interpreter internals are exposed to Python, and doing it should be easy - if one comes up with a good use case.