Saturday, February 14, 2015

A Critique of the Ruby 2.1's TracePoint API

In updating Ruby 1.9.1 patches to work on Ruby 2.1.5, I have gotten familiar with the TracePoint API, and more generally, with the changes that facilitate run-time introspection error reporting and debugging.

For example, caller_locations() is like the old caller() routine but gives structured data. This is a welcome addition that I recall asking for in 2010. However, in the debugger I've been recently working on, I don't use it. More on this in another blog entry.

Here, I will mostly focus on the TracePoint API.

Side comment: the name TracePoint seems really weird to me. I don't get the point of "Point" in the name. Perhaps this is another word for "Object" or "thingy"?

But whatever its name, TracePoint is a big improvement over Kernel#set_trace_func(). And here's why: set_trace_func() has global effect. Calling that function removes any previous callback.

As far back as I can remember in Ruby 1.9, the thread tracing structures coded in C were a linked list that allowed for several hooks to be serviced, but Ruby 1.9 didn't have any API to get access to that or the event bitmask. Therefore, in patches I wrote for 1.9 I added access to add and remove entries to this list. I also wrote a Ruby gem, rb_tracer, to give access to this and to act as a multiplexer when there were several hooks.

And here is why you might want to have multiple hooks. Suppose you are interested in gathering statistics for calls and returns. For example you might want to record elapsed time in some functions. So you could write a trace hook that triggers only on calls and returns. And at some point you might want to debug other portions of the code. Or maybe you want to do something with regard to exceptions that get raised. So, one trace function would gather the statistics while the debugger might temporarily use another hook, and another hook might do something for exception events.

Well, the TracePoint API now handles registering multiple hooks and each can register what event they want to trigger on. So those patches and the rb_tracer gem are happily obsolete.

Unfortunately, there are limitations.

Once you set the events you want to trigger on, you can't change them. Nor can you see what mask has been set. In the context of a debugger, I allow the user to set and change events. And of course I want to be able to see what events we've registered. So in my patches to 2.1.5, I allow you to set and change the event masks.

Also good is the fact that the TracePoint API is Object Oriented. Again compare this to the older set_trace_func() which is a method. As a result, you no longer have that long list of 6 parameters in the callback. Instead there is just the one object. From that, you can get whatever other information you need, like the event triggered, source position, access to a binding object to be able to run eval() in the context of where you were stopped, and so on.

But ...

In terms of OO design it feels like there are two objects combined into one. Consider this example from ruby-doc:

trace = TracePoint.new(:raise) do |tp|
   p [tp.lineno, tp.event, tp.raised_exception]
end

trace and tp are both of type TracePoint, but they really function differently. tp has methods which are specific to the callback (lineno(), event(), raised_exception()).  But some methods aren't specific to the callback such as enabling and disabling the tracepoint. Setting the event mask isn't specific to the callbeck. In the example above, the event mask is :raise.

Finally, there is a weird artifact in the implementation. When the tracepoint is enabled, you well get a "c return" event callback from the enable() method.

Here is how I eliminate that unwanted callback in my patches. In order to facilitate "step in", "step over" and "step out", I have the ability to disable tracing on a per-frame basis. So inside the enable() method, I disable frame tracing for that frame.

No comments:

Post a Comment