A lightweight and interactive back-in-time debugger for Squeak to trace and retrace past method invocations and state changes. Powered by SimulationStudio.
Implement a retracing simulator with vector semantics for simultaneous evaluation against all memory states of interest. Allow the client to specify a relevant time window. Provide a search mode with different strategies (forward, backward, binary) with streaming of results.
when an unsupported primitive is hit, write a note to the Transcript
Implementing an optional primitive means to accelerate its execution time.
TDBRetracingSimulator
We have a general issue here with primitives that directly access the state of any object which was changed at a later point in time. A solution to this problem would be temporal rematerialization (#23) of the state in question, maybe in a copy of the original object.
Known affected primitives:
BitBltPlugin/primitiveCopyBits - which variables next to sourceForm are affected?
tbc
Both simulators
Groups of primitives:
changeClass, flushCache
Here is an example that will currently crash your image with a VM failure:
scrolling/tree display: TDBCursor>>#traceFor: (via TDBCursor>>#childContextsFor:, 68%) - forego generators in TDBTrace>>#withAllDescendants, use binary depth-first search
stepping: TDBTrace>>#traceAtTime: (13%) - inline binary search? don't use #associations.
Further optimization potential:
We are still rebuilding the tree after every step (#wrapRoots), which helps us fulfill all invariants, but is terribly slow. Updating the trace iteratively would be hard with respect to the current limitations of the PluggableTreeMorph API, automatic expansion of relevant nodes, and coroutines (#14).- [ ] use non-linear search algorithms for stepping operations (i.e., binary search with unknown size)
All stepping operations could be optimized by rolling out an exponential search mechanism. For this, we would need to define an absolute criterion for "whether a timeIndex belongs to the right step".
optimize inspector updates (could be possible with the help of #21)
not yet optimized: stepBack
Reduce truncationLimit in snapshot inspectors (requires upstream contribution)
Currently, coroutines cannot be traced and break tracing because the architecture relies on a constant sender per context. Optimization is also an issue here. We could implement the missing look-ups in a second, slower run. See #coroutines.
Further notes:
correct context update when stepping to new sender - it needs to be registered as trace
Also discuss how we could still display parts of the trace that are currently not in the active stack.
array :=ArraystreamContents: [:s|
aTdbProxyForAnArray do: [:ea | s nextPut: ea]].
array first someState.
One might expect that the element arrays would be further TDBProxys after executing this snippet, or that the entire handling of the array would be continued in the retracing simulator. However, none of this is currently the case and the final expression accesses the current version of the item's state instead of its historic state.
Hypothetically related if we wanted to go the second route: LinqLover/SimulationStudio#51
At the moment, a workaround is to wrap the entire expression with a message send to the proxy, e.g.:
aTdbProxyForAnArray in: [:theTdbProxyForAnArray |
array :=ArraystreamContents: [:s|
theTdbProxyForAnArray do: [:ea | s nextPut: ea]].
array first someState].
Maybe we should at least document this pattern somewhere.
See TDBObjectExplorerWrapper>>#contents for a real motivating example.
Currently, search is scoped to the tree below the selected context (and I think this is the way that makes most sense). However, search suggestions should only be collected for all actually reachable hits.
Also pre-select the current value when bringing up the dialog.
Only linear stepping possible, no back-in-time display of prior construction/rendering states, no binary search to trace back certain changes to certain stack frames.
Irrelevant stack frames distract understanding of the relevant process (e.g., Canvas clipping, Morph iteration).
Intermediate result is hardly accessible: Need to reevaluate aMorph imageForm for every (relevant) step or to navigate to aCanvas form > "screenshot" and update the inspector manually (bug in Form >> #=).
Possible solutions:
:
display the entire trace at once
step into details later in the context of the original event state
filter trace by method category/class/package
similar to printbugger:
overview of single drawing steps of #imageForm (maybe sandboxed) from which the user can jump to the responsible stack frame
slider for efficient time-traveling (maybe fish-eye slider)
Problems: Narrowing down when the property #doLayoutAgain is added to the morph requires stepping into all details or performing a binary search by starting a new debugger whenever it was stepped too far.
Possible solutions:
step into details later in the context of the original morph state
find all instructions in the trace that add the property #doLayoutAgain
In theory, it should be even possible to debug ProtoObject new yourself without that the UI crashes. This, however, is currently neither possible in the normal debugger nor in the TraceDebugger because there are several places that inexorably send messages to the receiver of the selected context. This issue is for making sure that no debugger pops up while stepping through ProtoObject new yourself etc. Also, print-its in the inspector should work.
Build an API to reevaluate a trace from an earlier point in time in a forked memory (#27).
In the context of the UI, this would be relevant for the following operations:
accept contents to inspector field (currently changes the present state, unclear semantics)
debug it from code pane/inspector field (could spawn another trace debugger)
return value from context (not yet implemented)
skip context (not yet implemented)
Technically, TDBRetracingSimulator could detect state changes and offer the client to reevaluate the trace automatically. See "retrace side effects" in the design process document.
Could either be solved via rematerialization (#23) into a copy or by melting a tracing simulator and a retracing simulator properly.
Add a simple "step until" button to the debugger that steps until a given expression by the user evaluates at the current time to true. Would benefit from #20.
Also implement the "run to here" item in the code pane menu.
Additional ideas:
User can enter a non-boolean expression that is checked for changes
Discuss dealing with side-effects
What about dedicated "step over until", "step through until", and "step back until" buttons?
Roles: System learner (understand the entire process), system expert (already knows most static artifacts, wants to fix a specific bug)
Goal: Explore the relevant event handlers for a MorphicEvent that is propagated through the world (including composed handlers, submorphs, event filters, rejecting handlers, etc.).
Currently, this requires stepping into every possibly relevant detail and either viewing all methods of interest in temporal order or noting them down manually.
Finding an entry point for debugging event handling is hard.
Traditional workarounds for this problem include:
Some expressions/processes have a large computational foot stamp because they execute expensive methods such as ClassDescription>>#packageInfo that, at the same time, do not include any relevant side effects to the debugged system. As a "last resort" optimization performed by the user of the tool, it might be helpful to declare an exclusion list of methods that should be ignored during tracing. The tracer would be responsible for running all invocations of these methods outside of the simulator. See LinqLover/SimulationStudio#54.
Discuss a proper UI:
analogously to filters, select a context to exclude it by example
When the user presses Cmd + . while a step is being executed, interrupt the tracer gently. At the moment, one has to complete the current step message inside the cursor manually from the interrupt debugger before abandoning it. For an elegant solution, this would require an UserInterruptException provided by the Trunk (see https://github.com/hpi-swa-lab/squeak/issues/131).
support "add field"/"edit field getter" items for custom fields
For the required inspector hooks (dispatch compilation cue construction to inspector), see inspector-compilercue.cs, which only needs to be contributed upstream.
For the remaining todos on the TDB side, see the comment in TDBInspector>>#fieldListMenu:shifted:.
InteractivePrintIt & Co. need to dispatch to the requesting model (upstream).
Alternatively, this feature could be turned off by changing the preference temporarily or overriding printIt:result: in the models.
At the moment, the TraceDebugger will eliminate any other custom context customizations in the code to be traced. For instance, the following does not run the code sandboxed when you trace it and step over the assignment:
| x |
x :=0.
Sandbox2debug: [x :=1].
The cause for this behavior lies in TDBTrace>>#enableSimulatorDuring: where the customizations of all contexts are via simulator customize: context. We should preserve other custom context simulators and combine all simulators instead via the COR mechanism.
Currently, custom title strings for exceptions/the debugger invocation are not displayed in the title. On the other hand, we do not want to leave the timeIndex display. Think about a proper solution. This problem might also relate to the snapshot inspectors.
TDBTrace>>#traceAtTime:ifAbsent: (18.4%) - single index for all traces?
TDBTrace>>#enableSimulatorDuring:: Currently, we are customizing all contexts there, which makes every forward-step in a large tree an expensive operation. Would it be fine only to customize the top context instead? Or could we at least exclude dead contexts (this would still be an optimization from O(n) to O(√n))?
Ideas for reducing space consumption:
Use dense table memory layout for certain objects/slots (i.e., Context/pc)?
Reduce default size of TDBTrace.children? Currently, over 90% of all trace instances do have 2 children or less:
analogously to #testTraceSimple, add acceptance test for recorded states somewhere?
test #isContextAlive:
test stepToHome: with foreign contexts (see examples in stepToHome:)
test stepThrough: from sender
Kernel tests
These tests currently do not include the building of a trace (only side-effects are traced).
We should discuss whether this reveals a bad design of the tracing simulator (the tracing simulator is not responsible for building the trace; the cursor/trace does that) or whether we should talk to the cursor directly in some of the kernel tests.
Smart smoke assumptions for kernel tests: For instance, reevaluate each test twice and compare the second execution to the trace of the first one. Reevaluation #24 would give us further possibilities here.
Compare trace with normal execution? This would not work for details of SimulationContext.
UI
test spawn & process termination
General concerns
Improve primitive tests in the simulated code (such as WriteBarrierTest)
Run all tests without useProxiesAlways (CI config?)
Detect state changes inside the retracing simulator and design a RetraceSideEffect exception that can be used to cancel or permit the state change or to apply it in a forked memory (#27).