|
| 1 | +# Debugger Visualizers |
| 2 | + |
| 3 | +These are typically the last step before the debugger displays the information, but the results may |
| 4 | +be piped through a debug adapter such as an IDE's debugger API. |
| 5 | + |
| 6 | +The term "Visualizer" is a bit of a misnomer. The real goal isn't just to prettify the output, but |
| 7 | +to provide an interface for the user to interact with that is as useful as possible. In many cases |
| 8 | +this means reconstructing the original type as closely as possible to its Rust representation, but |
| 9 | +not always. |
| 10 | + |
| 11 | +The visualizer interface allows generating "synthetic children" - fields that don't exist in the |
| 12 | +debug info, but can be derived from invariants about the language and the type itself. A simple |
| 13 | +example is allowing one to interact with the elements of a `Vec<T>` instead of just it's `*mut u8` |
| 14 | +heap pointer, length, and capacity. |
| 15 | + |
| 16 | +## `rust-lldb`, `rust-gdb`, and `rust-windbg.cmd` |
| 17 | + |
| 18 | +These support scripts are distributed with Rust toolchains. They locate the appropriate debugger and |
| 19 | +the toolchain's visualizer scripts, then launch the debugger with the appropriate arguments to load |
| 20 | +the visualizer scripts before a debugee is launched/attached to. |
| 21 | + |
| 22 | +## `#![debugger_visualizer]` |
| 23 | + |
| 24 | +[This attribute][dbg_vis_attr] allows Rust library authors to include pretty printers for their |
| 25 | +types within the library itself. These pretty printers are of the same format as typical |
| 26 | +visualizers, but are embedded directly into the compiled binary. These scripts are loaded |
| 27 | +automatically by the debugger, allowing a seamless experience for users. This attribute currently |
| 28 | +works for GDB and natvis scripts. |
| 29 | + |
| 30 | +[dbg_vis_attr]: https://doc.rust-lang.org/reference/attributes/debugger.html#the-debugger_visualizer-attribute |
| 31 | + |
| 32 | +GDB python scripts are embedded in the `.debug_gdb_scripts` section of the binary. More information |
| 33 | +can be found [here](https://sourceware.org/gdb/current/onlinedocs/gdb.html/dotdebug_005fgdb_005fscripts-section.html). Rustc accomplishes this in [`rustc_codegen_llvm/src/debuginfo/gdb.rs`][gdb_rs] |
| 34 | + |
| 35 | +[gdb_rs]: https://github.com/rust-lang/rust/blob/main/compiler/rustc_codegen_llvm/src/debuginfo/gdb.rs |
| 36 | + |
| 37 | +Natvis files can be embedded in the PDB debug info using the [`/NATVIS` linker option][linker_opt], |
| 38 | +and have the [highest priority][priority] when a type is resolving which visualizer to use. The |
| 39 | +files specified by the attribute are collected into |
| 40 | +[`CrateInfo::natvis_debugger_visualizers`][natvis] which are then added as linker arguments in |
| 41 | +[`rustc_codegen_ssa/src/back/linker.rs`][linker_rs] |
| 42 | + |
| 43 | +[linker_opt]: https://learn.microsoft.com/en-us/cpp/build/reference/natvis-add-natvis-to-pdb?view=msvc-170 |
| 44 | +[priority]: https://learn.microsoft.com/en-us/visualstudio/debugger/create-custom-views-of-native-objects?view=visualstudio#BKMK_natvis_location |
| 45 | +[natvis]: https://github.com/rust-lang/rust/blob/e0e204f3e97ad5f79524b9c259dc38df606ed82c/compiler/rustc_codegen_ssa/src/lib.rs#L212 |
| 46 | +[linker_rs]: https://github.com/rust-lang/rust/blob/main/compiler/rustc_codegen_ssa/src/back/linker.rs#L1106 |
| 47 | + |
| 48 | +LLDB is not currently supported, but there are a few methods that could potentially allow support in |
| 49 | +the future. Officially, the intended method is via a [formatter bytecode][bytecode]. This was |
| 50 | +created to offer a comparable experience to GDB's, but without the safety concerns associated with |
| 51 | +embedding an entire python script. The opcodes are limited, but it works with `SBValue` and `SBType` |
| 52 | +in roughly the same way as python visualizer scripts. Implementing this would require writing some |
| 53 | +sort of DSL/mini compiler. |
| 54 | + |
| 55 | +[bytecode]: https://lldb.llvm.org/resources/formatterbytecode.html |
| 56 | + |
| 57 | +Alternatively, it might be possible to copy GDB's strategy entirely: create a bespoke section in the |
| 58 | +binary and embed a python script in it. LLDB will not load it automatically, but the python API does |
| 59 | +allow one to access the [raw sections of the debug info][SBSection]. With this, it may be possible |
| 60 | +to extract the python script from our bespoke section and then load it in during the startup of |
| 61 | +Rust's visualizer scripts. |
| 62 | + |
| 63 | +[SBSection]: https://lldb.llvm.org/python_api/lldb.SBSection.html#sbsection |
| 64 | + |
| 65 | +## Performance |
| 66 | + |
| 67 | +Before tackling the visualizers themselves, it's important to note that these are part of a |
| 68 | +performance-sensitive system. Please excuse the break in formality, but: if I have to spend |
| 69 | +significant time debugging, I'm annoyed. If I have to *wait on my debugger*, I'm pissed. |
| 70 | + |
| 71 | +Every millisecond spent in these visualizers is a millisecond longer for the user to see output. |
| 72 | +This can be especially painful for large stackframes that contain many/large container types. |
| 73 | +Debugger GUI's such as VSCode will request the whole stack frame at once, and this can result in |
| 74 | +delays of tens of seconds (or even minutes) before being able to interact with any variables in the |
| 75 | +frame. |
| 76 | + |
| 77 | +There is a tendancy to balk at the idea of optimizing Python code, but it really can have a |
| 78 | +substantial impact. Remember, there is no compiler to help keep the code fast. Even simple |
| 79 | +transformations are not done for you. It can be difficult to find Python performance tips through |
| 80 | +all the noise of people suggesting you don't bother optimizing Python, so here are some things to |
| 81 | +keep in mind that are relevant to these scripts: |
| 82 | + |
| 83 | +* Everything allocates, even `int` |
| 84 | +* Use tuples when possible. `list` is effectively `Vec<Box<[Any]>>`, whereas tuples are equivalent |
| 85 | +to `Box<[Any]>`. They have one less layer of indirection, don't carry extra capacity and can't |
| 86 | +grow/shrink which can be advantageous in many cases. An additional benefit is that Python caches and |
| 87 | +recycles the underlying allocations of all tuples up to size 20. |
| 88 | +* Regexes are slow and should be avoided when simple string manipulation will do |
| 89 | +* Strings are immutable, thus many string operations implictly copy the contents. |
| 90 | +* When concatenating large lists of strings, `"".join(iterable_of_strings)` is typically the fastest |
| 91 | +way to do it. |
| 92 | +* f-strings are generally the fastest way to do small, simple string transformations such as |
| 93 | +surrounding a string with parentheses. |
| 94 | +* The act of calling a function is somewhat slow (even if the function is completely empty). If the |
| 95 | +code section is very hot, consider inlining the function manually. |
| 96 | +* Local variable access is significantly faster than global and built-in function access |
| 97 | +* Member/method access via the `.` operator is also slow, consider reassigning deeply nested values |
| 98 | +to local variables to avoid this cost (e.g. `h = a.b.c.d.e.f.g.h`). |
| 99 | +* Accessing inherited methods and fields is about 2x slower than base-class methods and fields. |
| 100 | +Avoid inheritance whenever possible. |
| 101 | +* Use [`__slots__`](https://wiki.python.org/moin/UsingSlots) wherever possible. `__slots__` is a way |
| 102 | +to indicate to Python that your class's fields won't change and speeds up field access by a |
| 103 | +noticable amount. This does require you to name your fields in advance and initialize them in |
| 104 | +`__init__`, but it's a small price to pay for the benefits. |
| 105 | +* Match statements/if..elif..else are not optimized in any way. The conditions are checked in order, |
| 106 | +1 by 1. If possible, use an alternative such as dictionary dispatch or a table of values |
| 107 | +* Compute lazily when possible |
| 108 | +* List comprehensions are typically faster than loops, generator comprehensions are a bit slower |
| 109 | +than list comprehensions, but use less memory. You can think of comprehensions as equivalent to |
| 110 | +Rust's `iter.map()`. List comprehensions effectively call `collect::<Vec<_>>` at the end, whereas |
| 111 | +generator comprehensions do not. |
0 commit comments