Scripting APIs
If you are not happy with what the present tooling of the TraceDebugger offers, of course you are invited to open an issue in the repository! Apart from that, here is short summary of relevant interfaces that you might want to use to assemble some information about a program trace yourself. Hand me a workspace with all code snippets.
Recording a program trace
cursor := TDBCursor traceForBlock: ['\w+' asRegex].
Accessing the context tree (like the TraceDebugger does)
root := cursor context tdbFullStack first. "[] in UndefinedObject>>DoIt"
children := cursor childContextsFor: root.
firstChild := children first. "ByteString(String)>>asRegex"
someContext := cursor findContextSuchThat: [:ctx | ctx method == (PositionableStream class>>#on:)]. "ReadStream class(PositionableStream class)>>on:"
someContext sender. "RxParser>>parse:"
stream := cursor returnValueFor: someContext ifPresent: #yourself. "a ReadStream"
Do not manually access the trace instances. They are an implementation detail of the cursor and subjected to change. See the class comment for more information.
Stepping through a program trace (like the TraceDebugger does)
See the stepping protocol and TDBCursor exampleStepping.
For more information on the cursor interface, do this:
cursor exploreProtocol.
Accessing historic values (like the snapshot inspectors do)
All historic values are stored in the cursor's memory and can be conveniently accessed through transparent proxies that just behave like the requested object at a certain time. Proxies can be sent further messages to access their state, all relevant answers will be wrapped in further proxy instances.
(cursor object: stream atTime: 0) position. "nil"
(cursor object: stream atTime: (cursor maxTimeIndexFor: someContext)) position. "0"
(cursor object: stream atTime: (cursor maxTimeIndexFor: root)) position. "3"
Note that all side effects during a proxy access will be isolated, so for instance, sending a proxy the message #printOn: will not actually modify the passed stream instance.
(cursor object: stream atTime: (cursor maxTimeIndexFor: someContext)) next. "$\"
For more details, refer to the comment in TDBProxy. If you ever need to escape from proxy hell, send aProxy copy tdbproxyYourself. See flag #proxyHacks.
Accessing historic ranges (like the history explorer does)
cursor object: stream collect: #position. "a TDBMemorySlice(... (0 to: 28) -> nil (29 to: 55) -> 0 (56 to: 174) -> 1 (175 to: 299) -> 2 (300 to: 2338) -> 3) ...)"
cursor memory object: stream atTimes: ((cursor minTimeIndexFor: someContext) to: (cursor maxTimeIndexFor: someContext)) collect: #position. "a TDBMemorySlice(... (18 to: 28) -> nil (29 to: 36) -> 0) ...)"
The following example illustrates that historic range queries typically need to perform a lot of error handling and need to copy objects for later reuse (#proxyHacks):
cursor object: stream collect: [:ea | [(ea originalContents first: ea position) copy] on: Error do: #yourself]. "a TDBMemorySlice((0 to: 23) -> MessageNotUnderstood: UndefinedObject>>first: (24 to: 28) -> MessageNotUnderstood: UndefinedObject>>- (29 to: 55) -> '' (56 to: 174) -> '\' (175 to: 299) -> '\w' (300 to: 2338) -> '\w+')"
The resulting memory slices can be accessed and transformed. Noticeably, when you operate on the discretized values from a memory slice, your code will likely operate on transparent range proxies that represent a value in an entire time range. A range proxy will behave similar to a normal proxy, but if it is sent further messages that reveal any state that has changed within the time range, the proxy will signal a fray out error. For more details, please refer to the class comments in TDBMemorySlice and TDBRangeProxy.