TclOO trace filter

Using the 'filter' subcommand to trace class method calls

dzach: For this code I've used Tclkit 8.6b1 (from Pat Thoyts's bleeding-edge builds [L1 ]) that comes with TclOO in the core. Additions and corrections are welcome.


# When used inside a proc, 'calledby' returns the name of the caller of that proc. 
# This auxiliary proc is used to trace callers originating outside of class methods:
#
proc ::calledby {} {
        set level [expr {[info level] - 2}]
        if { $level > 0 } {
                return [lindex [info level $level ] 0] 
        } else {
                if { [info script] ne {} } {
                        return [info script]
                } else {
                        return [info nameofexecutable]
                }
        }        
}
#
# The OO portion
#
namespace import ::oo::*
catch {Trace destroy}
class create Trace {
        self export varname
        filter Trace

        # var_ is a class variable
        variable var_

        constructor args {

                # link instance var_ to the class variable var_ for global trace. Comment out for class-instance-wide trace
                my eval upvar [[self class] varname var_ ] var_

                # Initialize default values, but only when the first instance gets created. 
                # Subsequent instances will use the existing values
                if {! [info exists var_ ] } {

                        set var_(defaults) { trace on history {} length 20 methods {} mode exclude print off}
                        array set var_ $var_(defaults)
                }
                next {*}$args
        }
        method Trace args {
                # avoid tracing of system methods
                if {[lindex [self target] 0] eq "::oo::object"} { return [next {*}$args] }
                if {! $var_(trace)} { return [next {*}$args] }
                set caller {}

                # [self caller] returns error when used outside a class, but we need to trace method calls
                #   that originate outside the class, from a normal proc or a console
                if {[catch {
                        set caller [lrange [self caller] 1 end]
                }]} {
                        # outside caller
                        set caller [calledby]
                }
                set meth [lindex [self target] 1]

                # form a trace data item
                set i [list $caller [self] $meth $args]

                # method filter
                if {[string match "in*" $var_(mode)] &&  $meth ni $var_(methods) || \
                   ![string match "in*" $var_(mode)] && ($meth in $var_(methods) || $meth eq "trace" || $meth eq "print")
                } {
                        return [next {*}$args]
                }

                # print trace item along with execution
                if {$var_(print)} { my print $i }

                # save trace item in history and limit history's length
                set var_(history) [lrange [lappend var_(history) $i] end-$var_(length) end]
                next {*}$args
        }
        method trace args {
                while {[llength $args]} {
                        set opt  [lindex $args 0]

                        # parse options

                        if {[string index $opt 0] ne "-"} break
                        set i 1
                        switch -glob -- $opt {
                                -on - -of* { set var_(trace) [string range $opt 1 end] }
                                -le* {
                                        set var_(length) [lindex $args $i]
                                        incr var_(length) -1
                                        set var_(history) [lrange $var_(history) end-$var_(length) end]
                                        incr i
                                     }
                                -me* { set var_(methods) [lindex $args $i] ; incr i }
                                -mo* { set var_(mode) [lindex $args $i]    ; incr i }
                                -cl* { set var_(history) {} }
                                -re* { array set var_ $var_(defaults) }
                                -pr* {set var_(print) [lindex $args $i]    ;incr i }
                                default { error "unknown or ambiguous option $opt : may be any of -on, -off, -methods, -mode, -clear, reset or -print" }
                        }
                        incr i $i
                        set args [lrange $args $i end]
                }
                if {! $var_(trace)} {
                        return "History is turned off. Try \"[self] trace on\" to turn it on"
                }
                foreach i $var_(history) { my print $i }
        }
        method print args {
                if {![llength $args]} return
                lassign {*}$args caller inst meth arg
                if {$arg ne {}} { set arg "\n\targs = $arg" }

                # print a trace item
                puts [join "$caller -> $inst $meth"]$arg
        }
}

Usage:

Include mixin Trace in the definition of a class, to trace calls to class methods. Trace is global, i.e. it traces method calls originating from any class using it, so that interactions between objects can be monitored in the same output. By changing class variable var_ to an instance variable, e.g. by commenting out my eval upvar [self class] varname var_ ] var_, the behaviour can change to class-instance-wide trace, i.e. each instance will have its own trace history.

Output is sent to stdout using 'puts'. Use any of the following commands:

  • obj trace ? -on ? ? -off ? : turns tracing on/off. Default is off.
  • obj trace ? -clear ? : clears the history buffer.
  • obj trace ? -length integer ? : limits the number of history items to integer. Default history length is 20.
  • obj trace ? -methods list ? : list of methods to include/exclude, depending on -mode. The default is an empty list.
  • obj trace ? -mode include | exclude ? : include mode traces only methods incuded in -methods list while exclude mode traces all methods except those in the -methods list. Default mode is exclude. Methods trace and print of class Trace are not traced.
  • obj trace ? -print ? on ? ? -off ? : turns trace output during program execution on/off. Default is off.
  • obj trace ? -reset : resets tracing to the default values.

Demonstration:

#Create a test class:
% class create c {
        mixin Trace
        constructor {} {my variable cnt; set cnt 0}
        method get {} {my variable cnt;puts $cnt}
        method incr {} {my variable cnt;incr cnt}
}
::c
# create an instance of the class
% c create t
::t

# produce some output
% t incr
1
% t incr
2
% t get
2
# see the trace
% t trace
bin/tclkit8.6 -> ::t incr
bin/tclkit8.6 -> ::t incr
bin/tclkit8.6 -> ::t get

# the default mode is 'exclude', so by setting '-methods incr' we exclude 'incr' from being traced
% t trace -methods incr
...
% t incr
3
% t get
3
% t trace
bin/tclkit8.6 -> ::t incr
bin/tclkit8.6 -> ::t get
bin/tclkit8.6 -> ::t get  <-- 'incr' was not traced, but the 'get' following it was