Skip to content
A Julia Debugger that works with mixed compiled and interpretted mode for performance
Julia TeX
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Type Name Latest commit message Commit time
Failed to load latest commit information.


Stable Dev Build Status Build Status Codecov Coveralls

Build Status

What is MixedModeDebugger?

MixedModeDebugger is a debugger that runs in a mixture of modes: both compiled and interpretted. It aims to achieve the best of both worlds. We can constrast this to MagneticReadHead which is purely compiled; and against JuliaInterpreter / JuliaDebugger, which is purely interpretted.

How is it different from MagneticReadHead ?

MagneticReadHead is fully compiled. The whole code being debugged is actually rewritten (at the IR level) to include the debugging functionality. This allows it to be pretty fast, at runtime. However, because of how extensive the code tranformation is, it can take an immense amount of time to compile. Infact, it might take orders of magnitude comparable to the head death of the universe to compile.

MixedModeDebugger uses a far lighter transform during its compiled-mode, just enough to check if a method being called is one with a breakpoint set on it. So it doesn't introduce anywhere near as much overhead.

How is it different from Debugger.jl ?

Debugger.jl is fully interpretted, when debugging. What Debugger.jl calls "Compiled mode" is actually disabling all debugger functionality until one returns out. In contrast MixedModeDebugger's Compiled-mode disables most debugger functionality, except one crucial thing: the ability to hit a breakpoint, and thus then enable full debugger functionality.

Because Debugger.jl is always interpretted, it is much slower all the time. Where as MixedModeDebugger.jl can run at compiled speeds, until it hits a breakpoint.

Under-the-hood: when MixedModeDebugger.jl switches to interpretted-mode it literally calls Debugger.@run.

How it works:

  • Compiled-mode execution is done in a Cassette context
  • This context overdubs any function with a breakpoint set on/within them to tell it to switch to interpretted mode.
    • This happens purely on the Method, not on any run-time information, so the switch will also be made for conditional breakpoints.
  • Hooks on JuliaInterpreter breakpoint commands cause these overdubs to be created and deleted.
  • When in interpretted mode JuliaDebugger.jl is used to provide the actual functioning debugger.

Installation and Usage:


This package is still experimental, and so is not registered. It may even be upstreamed directly into JuliaDebugger at some point. Currently, it requires the ox/hooks branch of JuliaInterpreter

Installation is thus via:

pkg> add

Note: this package requires Julia 1.3+ as it relies on the fix to the #265 issue for Cassette.


Usage is as per JuliaDebugger, so just refer to those docs

With the following notes/exceptions:

  • breakon(:error) and breakon(:throw) do not function in compiled mode.
    • They will work fine if in interpretted mode (i.e. deaper in the callstack than a breakpoint)
  • Rather than using @run f(x), uses @run_mixedmode f(x)
  • There is no matching @enter f(x) as setting a breakpoint right at the start would switch to interpretted-mode straight away.
  • Entering compiled mode via entering C at the debug prompt will switch you to Debugger.jl's idea of compiled mode so breakpoints will not then be hit. [Issue #1]


**Figure:** Run-time performance of JuliaDebugger vs MagneticReadHead, vs Native on the `summer` benchmark. With no breakpoints set. Note that on this task MixedModeDebugger performs exactly the same as Native, as there are no breakpoints set.

Full performance benchmarks can be found here. The take aways of those benchmarks are:

  • The compile-time overhead of MixedModeDebugger is the same order of magnitude as JuliaDebugger/JuliaInterpreter.
  • In normal use worst-case runtime performance is the same as JuliaDebugger/JuliaInterpreter, and the best is basically the same as running natively.
  • The exception to this is the pathelogical case where it is constantly flipping between compiled and interpretted modes.

More on that pathelogical case and conditional breakpoints.

The key case where this breaks down is for conditional breakpoints in tight inner loops. There is some overhead for switching from compiled-mode to interpretted-mode. Normally this doesn't matter for two reasons:

  1. The switch happens on a breakpoint, and thus execution normally stops anyway
  2. The time to switch, is much lower than the time it takes to run the breakpointed function in interpretted-mode.

However, if the breakpoint is conditional and the condition is not met then the first point doesn't apply. If it also is within a function on a function that is very fast (tight), then the second doesn't apply either. And if it (the call the the function is inside an This case is fortunately fairly rare.

If you find youself in it, then there is a fairly easy work around: place a breakpoint (even one with a condition that is always false) further up the callstack: such as in the function with the loop that calls the function the conditional breakpoint. This extra breakpoint (even if not triggered) will cause the function to switch to interpretted mode before the problematic breakpoint, and so you will not see the mode-switch overhead.

You can’t perform that action at this time.