Flow-storm Debugger

repository·master·Indexed 21 days ago

https://github.com/flow-storm/flow-storm-debugger

An omniscient time travel debugger for Clojure and ClojureScript that provides deep visibility into execution history. It allows developers to inspect and navigate through execution timelines using either automatic instrumentation via ClojureStorm/ClojureScriptStorm or manual instrumentation via reader tags like #trace. The system consists of an instrumentation engine, a runtime that records traces into registries, and a JavaFX-based GUI debugger.

Tokens
11.6K
Snippets
21
Records
58
Agent score
73%

What's inside flow-storm-debugger

  1. Overview of Flow-storm debugger

    master

    Flow-storm is an omniscient time travel debugger designed for Clojure and ClojureScript. It allows developers to inspect and navigate through execution history. There are two primary ways to use it:

    1. With ClojureStorm (Recommended): Swap your standard Clojure compiler for ClojureStorm during development. This automatically instruments your code so you don't have to manually manage instrumentation.
    2. Vanilla FlowStorm: Add FlowStorm to your development classpath and instrument code by re-evaluating forms manually.
  2. Understand the FlowStorm Runtime and Registries

    master

    The Runtime subsystem runs inside the debuggee process and records traces as long as flow-storm.tracer/recording is true. It stores data in two main registries:

    • flow-storm.runtime.indexes.api/forms-registry: Stores all instrumented forms, indexed by a form-id (a hash of the form).
    • flow-storm.runtime.indexes.api/flow-thread-registry: Stores timelines. It maintains one timeline per thread, plus a multi-thread timeline when recording is active.

    Key Implementation Details:

    • Timeline: Implemented as flow-storm.runtime.indexes.timeline-index/ExecutionTimelineTree. It uses a mutable list for performance and to minimize garbage generation in the debuggee.
    • Data Types: Traces are represented by types in flow-storm.runtime.types.* rather than maps to reduce memory footprint.
    • Accessing Data: The runtime exposes functionality via flow-storm.runtime.debuggers-api. Unlike the standard indexes.api, the debuggers-api returns ValueRefs (pointers) instead of actual values to improve performance and handle values that cannot leave the debuggee (like infinite sequences).
  3. How FlowStorm instruments and records code

    master

    FlowStorm works by instrumenting code, running it, and recording all execution events on each thread into timelines. There are two primary ways to instrument your code:

    1. ClojureStorm (Recommended for Clojure): This method swaps your official Clojure compiler with a patched version (for development only) that emits extra JVM bytecode during compilation. This provides automatic instrumentation everywhere. Note that you can un-instrument code when you need to perform accurate performance measurements.

    2. The Vanilla Way (Required for ClojureScript): This method grabs specific Clojure source expressions, walks the AST, instruments them, and re-evaluates the instrumented version through the REPL. You can trigger this using:

      • Reader tags: e.g., #trace (defn foo [...] ...)
      • FlowStorm browser tab: To instrument entire namespaces.

    Because Clojure is expression-based and data is mostly immutable, recording is efficient: FlowStorm simply retains JVM references along with the source coordinates of each expression.

  4. Limit function call recording to prevent heap exhaustion

    master

    High-frequency functions (like mouse movement handlers) can generate excessive traces and exhaust heap memory. You can limit function calls per thread using the flowstorm.threadFnCallLimits JVM property.

    Usage: Set the property in the format namespace/function:limit. When a function reaches its limit, FlowStorm stops recording it and all functions below it in the callstack.

    Example JVM Option:

    -Dflowstorm.threadFnCallLimits=org.my-app/fn1:2,org.my-app/fn2:4

    Dynamic Modification via REPL: You can manage these limits at runtime using the following functions from flow-storm.runtime.indexes.api/:

    • add-fn-call-limit
    • rm-fn-call-limit
    • get-fn-call-limits
  5. How ClojureStorm and ClojureScriptStorm work

    master

    ClojureStorm and ClojureScriptStorm are specialized development compilers. They are forks of the official Clojure and ClojureScript compilers, respectively, enhanced with automatic instrumentation.

    Usage Pattern: Swap the official compiler for the Storm version during development (using deps CLI aliases or lein profiles). This ensures that instrumentation is applied automatically to your code as you work, while allowing you to use the standard, uninstrumented compiler for production and testing environments.

  6. How events communicate between Runtime and Debugger

    master

    Events allow the Runtime to communicate information to the Debugger. All possible events are defined in flow-storm.runtime.events.

    Event Lifecycle:

    1. Buffering: If no debugger is subscribed, events accumulate in flow-storm.runtime.events/pending-events. They are dispatched as soon as a subscription occurs.
    2. Queueing: On the debugger side, events accumulate in flow-storm.debugger.events-queue.
    3. Dispatching: A specialized thread dispatches events. Most are processed by flow-storm.debugger.events-processor/process-event.
    4. Custom Listeners: Any part of the debugger can listen to runtime events by adding a callback via flow-storm.debugger.events-queue/add-dispatch-fn.
  7. Compare FlowStorm with other Clojure debuggers

    master

    FlowStorm differs from traditional debuggers (like Cider, VSCode, or Cursive) in several key ways:

    FeatureFlowStormTraditional Steppers (Cider/VSCode/Cursive)
    Primary FocusExpression and value orientedLine and memory poking oriented
    Execution ModelOmniscient (records whole execution)Blocking (step-by-step forward)
    Language SupportClojure and ClojureScriptPrimarily Clojure
    ScopeProgram execution as a wholeSmall pieces of execution (breakpoints)
    SteppingNon-blocking, multiple directionsBlocking, forward-only

    When to use which:

    • Use Cursive if you need to step over Java code in a mixed-language codebase.
    • Use FlowStorm when you want to understand an entire codebase execution, trace values through complex transformations (like map, filter, reduce), or when you aren't sure where a bug is located.
  8. How FlowStorm is designed

    master

    FlowStorm is composed of three primary subsystems that work together to provide a debugging experience:

    1. Instrumentation: Responsible for interleaving extra code into your program to trace its execution. It is independent of FlowStorm but requires a chosen instrumentation engine to function.
    2. Runtime: Runs inside the debuggee process. It records traces emitted by the instrumentation system into registries (forms and threads) and manages the execution timeline.
    3. Debugger: The user-facing component that provides a GUI (implemented in JavaFX) to explore recordings. It communicates with the runtime to fetch data and receive events.

    For a visual representation of how these parts interact, refer to the high-level diagram.

  9. How values are handled via ValueRef

    master

    To prevent expensive serialization and handle values that cannot leave the debuggee process (like infinite sequences), the flow-storm.runtime.debuggers-api does not return actual values. Instead, it returns flow-storm.types/ValueRef objects, which act as reified pointers to the values stored in the runtime registry.

    To interact with these references, use:

    • val-pprint: Prints a value into a string representation using the provided print-level and print-length.
  10. Choose an instrumentation system

    master

    To use FlowStorm, you must select one of the following instrumentation engines. FlowStorm sets up callbacks that these engines use when generating instrumentation:

    • Hansel: A library that adds instrumentation by re-writing forms at macroexpansion time.
    • ClojureStorm: A Clojure dev compiler that instruments by emitting extra bytecode.
    • ClojurescriptStorm: A ClojureScript dev compiler that instruments by emitting extra javascript.

    FlowStorm hooks into these via:

    • flow-storm.tracer/[hansel-config | hook-clojure-storm | hook-clojurescript-storm]

    Once instrumented, the code will trigger the following flow-storm.tracer functions during execution:

    • flow-storm.tracer/trace-fn-call
    • flow-storm.tracer/trace-fn-return
    • flow-storm.tracer/trace-fn-unwind
    • flow-storm.tracer/trace-expr-exec
    • flow-storm.tracer/trace-bind
  11. How the FlowStorm Debugger works

    master

    The Debugger is the GUI-based tool used to explore recordings.

    • Entry Point: flow-storm.debugger.main/start-debugger.
    • UI: Implemented as a JavaFX application using namespaces in flow-storm.debugger.ui.*.
    • State Management: Uses a custom component system defined in flow-storm.state-management (similar to mount).
    • Subsystems: Includes state, runtime-api, ui.main, events-queue, websocket-server, and repl.core.

    In remote debugging mode, the websocket-server and repl client are active to facilitate communication with a remote runtime.

  12. How tasks handle long-running operations

    master

    To prevent blocking the UI during heavy operations (like searching or collecting data from large timelines), FlowStorm uses an asynchronous Task system. Tasks can report progress and be interrupted.

    Key Functions:

    • From the Debugger: Use flow-storm.debugger.ui.tasks/submit-task to call a task function.
    • On the Runtime: Use these utilities to implement interruptible loops:
      • flow-storm.runtime.debuggers-api/submit-batched-collect-interruptible-task: For collecting functionality that traverses the entire timeline.
      • flow-storm.runtime.debuggers-api/submit-find-interruptible-task: For looping through until the first match is found.