Debugging and Tracing — How Stackless differs

Debugging tools, like those used for tracing, are implemented through calls to the sys.settrace() function. Now, in standard Python®, when this has been called any code that runs within the operating system thread is covered by it. In Stackless however, this function only covers the current tasklet. Stackless provides the tasklet attributes trace_function and profile_function to get and set the trace/profile function of a particular tasklet.

The debugging related modules, whether in the standard library or not, do not take this difference into account. They are not likely to work, and if they do, are not likely to work in the way you expect. In an ideal world, Stackless-Python might include modified versions of these modules, and patches adding them would be most welcome.

If you want working debugging for Stackless-Python, at this time your best option is to use the WingWare Python IDE or the Eclipse IDE with the PyDev-Plugin. Both have gone out of their way to add and support Stackless-Python development.

Note

In the past, the possibility of ditching the per-tasklet behaviour for the standard per-thread behaviour has been broached on the mailing list. Given the lack of movement on usability for this part of Stackless, it is not unlikely that this suggested change will be revisited.

Tracing tasklets

In order to get debugging support working on a per-tasklet basis, you need to ensure you enable tracing for all tasklets. This can be archived by the schedule callback. This callback sees every task switch. Here is a complete example:

from __future__ import absolute_import, print_function

import sys
import stackless
import traceback


class NamedTasklet(stackless.tasklet):
    __slots__ = ("name",)

    def __init__(self, func, name=None):
        stackless.tasklet.__init__(self, func)
        if name is None:
            name = "at %08x" % (id(self))
        self.name = name

    def __repr__(self):
        return "<tasklet %s>" % (self.name)


class Mutex(object):

    def __init__(self, capacity=1):
        self.queue = stackless.channel()
        self.capacity = capacity

    def isLocked(self):
        '''return non-zero if locked'''
        return self.capacity == 0

    def lock(self):
        '''acquire the lock'''
        currentTasklet = stackless.getcurrent()
        atomic = currentTasklet.set_atomic(True)
        try:
            if self.capacity:
                self.capacity -= 1
            else:
                self.queue.receive()
        finally:
            currentTasklet.set_atomic(atomic)

    def unlock(self):
        '''release the lock'''
        currentTasklet = stackless.getcurrent()
        atomic = currentTasklet.set_atomic(True)
        try:
            if self.queue.balance < 0:
                self.queue.send(None)
            else:
                self.capacity += 1
        finally:
            currentTasklet.set_atomic(atomic)

m = Mutex()


def task():
    name = stackless.getcurrent().name
    print(name, "acquiring")
    m.lock()
    print(name, "switching")
    stackless.schedule()
    print(name, "releasing")
    m.unlock()


def trace_function(frame, event, arg):
    if frame.f_code.co_name in ('schedule_cb', 'channel_cb'):
        return None
    print("         trace_function: %s %s in %s, line %s" %
          (stackless.current, event, frame.f_code.co_name, frame.f_lineno))
    if event in ('call', 'line', 'exception'):
        return trace_function
    return None


def channel_cb(channel, tasklet, sending, willblock):
    tf = tasklet.trace_function
    try:
        tasklet.trace_function = None
        print("Channel CB, tasklet %r, %s%s" %
              (tasklet, ("recv", "send")[sending], ("", " will block")[willblock]))
    finally:
        tasklet.trace_function = tf


def schedule_cb(prev, next):
    # During a tasklet switch (during the execution of this function) the
    # the result of stackless.getcurrent() is implementation defined.
    # Therefore this function avoids any assumptions about the current tasklet.
    current_tf = sys.gettrace()
    try:
        sys.settrace(None)  # don't trace this callback
        current_frame = sys._getframe()
        if current_tf is None:
            # also look at the previous frame, in case this callback is exempt
            # from tracing
            f_back = current_frame.f_back
            if f_back is not None:
                current_tf = f_back.f_trace

        current_info = "Schedule CB "
        if not prev:
            print("%sstarting %r" % (current_info, next))
        elif not next:
            print("%sending %r" % (current_info, prev))
        else:
            print("%sjumping from %s to %s" % (current_info, prev, next))

        # Inform about the installed trace functions
        prev_tf = current_tf if prev.frame is current_frame else prev.trace_function
        next_tf = current_tf if next.frame is current_frame else next.trace_function
        print("    Current trace functions: prev: %r, next: %r" % (prev_tf, next_tf))

        # Eventually set a trace function
        if next is not None:
            if not next.is_main:
                tf = trace_function
            else:
                tf = None
            print("    Setting trace function for next: %r" % (tf,))
            # Set the "global" trace function for the tasklet
            next.trace_function = tf
            # Set the "local" trace function for each frame
            # This is required, if the tasklet is already running
            frame = next.frame
            if frame is current_frame:
                frame = frame.f_back
            while frame is not None:
                frame.f_trace = tf
                frame = frame.f_back
    except:
        traceback.print_exc()
    finally:
        sys.settrace(current_tf)

if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == 'hard':
        stackless.enable_softswitch(False)

    stackless.set_channel_callback(channel_cb)
    stackless.set_schedule_callback(schedule_cb)

    NamedTasklet(task, "tick")()
    NamedTasklet(task, "trick")()
    NamedTasklet(task, "track")()

    stackless.run()

    stackless.set_channel_callback(None)
    stackless.set_schedule_callback(None)

settrace and tasklets

Note

This section is out dated and only of historical interest. Since the implementation of trace_function and profile_function a debugger can enable tracing or profiling within the schedule callback without monkey patching.

In order to get debugging support working on a per-tasklet basis, you need to ensure you call sys.settrace() for all tasklets. Vilhelm Saevarsson has an email giving code and a description of the steps required including potentially unforeseen circumstances, in the Stackless mailing list archives.

Vilhelm’s code:

import sys
import stackless

def contextDispatch( prev, next ):
    if not prev: #Creating next
        # I never see this print out
        print("Creating ", next)
    elif not next: #Destroying prev
        # I never see this print out either
        print("Destroying ", prev)
    else:
        # Prev is being suspended
        # Next is resuming
        # When worker tasklets are resuming and have
        # not been set to trace, we make sure that
        # they are tracing before they run again
        if not next.frame.f_trace:
            # We might already be tracing so ...
            sys.call_tracing(next.settrace, (traceDispatch, ))

stackless.set_schedule_callback(contextDispatch)

def __call__(self, *args, **kwargs):
     f = self.tempval
     def new_f(old_f, args, kwargs):
         sys.settrace(traceDispatch)
         old_f(*args, **kwargs)
         sys.settrace(None)
     self.tempval = new_f
     stackless.tasklet.setup(self, f, args, kwargs)

def settrace( self, tb ):
    self.frame.f_trace = tb
    sys.settrace(tb)

stackless.tasklet.__call__ = __call__
stackless.tasklet.settrace = settrace

The key actions taken by this code:

  • Wrap the creation of tasklets, so that the debugging hook is installed when the tasklet is first run.
  • Intercept scheduling events, so that tasklets that were created before debugging was engaged, have the debugging hook installed before they are run again.