Skip to main content

Getting Started

ยท 15 min read
Billy Chan

In this blog post, we will cover the basic usage of FireDBG VS Code Extension ("the Extension") and FireDBG CLI ("the CLI"). By the end of this tutorial, you will learn:

  • How to install the Extension and the CLI
  • How to setup a Rust workspace for FireDBG debugger
  • How to debug Rust binary, example, unit test and integration test with FireDBG debugger
  • How to interpret and inspect visualized call tree, variables and timeline in the Extension
  • How to interpret and inspect breakpoint events of multi-threaded program in the Extension
  • How to trace any variable / expression of interest with the fire::dbg! trace macro
  • How to selectively enable / disable tracing of a local package
  • How to use the CLI to operate FireDBG debugger and then interpret and inspect breakpoint events via SQLite

If you're curious about the background and inner working of FireDBG

Installationโ€‹

Before we start, make sure you have VS Code and Rust installed.

  • VS Code (version 1.80.0 or later)
  • Rust (version 1.74.0 or later)

Rust & Cargoโ€‹

# install rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# install stable
rustup install stable

FireDBG VS Code Extensionโ€‹

The Extension provides seamless integration with FireDBG to enhance debugging experience and developer productivity. Search and install the FireDBG extension.

Windows Notesโ€‹

We only support Windows under WSL 2 right now, so please follow these steps on Windows:

  1. Install Ubuntu 22.04
  2. Install the WSL extension
  3. Click >< on the bottom left, and select "Connect to WSL"
  4. Install the FireDBG extension

FireDBG Binariesโ€‹

To keep the .vsix package small in size, we dont't ship the platform-specific binaries with the Extension. Instead, we have a dedicated installer script for the FireDBG binaries. We provide prebuilt binaries for the following CPU and OS combination:

Linux1macOS2Windows (WSL 2)3
x64โœ…โœ… Intelโœ…
arm64โ›”๏ธโœ… Appleโ›”๏ธ

Once the the Extension has been installed, you should see a prompt hinting that FireDBG binaries are missing. Click on the "install" button to run the installer.

Or, you can run the installer manually.

curl https://raw.githubusercontent.com/SeaQL/FireDBG.for.Rust/main/install.sh -sSf | sh

FireDBG binaries will be installed in ~/.cargo/bin and a debugger self test will be conducted to verify the installation. Expect to see:

info: completed FireDBG self tests

In case you got error messages when performing self test, read Troubleshooting Guide for the solution of common errors.

GUI Tourโ€‹

Download the zipped source code, or cloning Rust Testbench for FireDBG to your local machine, then follow the tour below to learn the basic usage of the Extension.

git clone git@github.com:SeaQL/FireDBG.Rust.Testbench.git

Note that the root directory of testbench isn't a Cargo workspace. In the next section, please open each sub-folder (e.g. getting-started) in VS Code. For convenience, each example already included a sample run.

Debug Targets and Runsโ€‹

Where can I see the list of all debuggable Rust targets, how can I debug it and how to inspect previous runs?

  1. Click on the "Run and Debug" panel on your primary sidebar, you should see two new panels on the bottom
  2. Click on the "Activate FireDBG" to enable FireDBG debugger on this workspace
  3. The FIREDBG panel should display all binaries, examples, integration tests and unit tests in your current Rust workspace. Click on the list item to reveal the Rust source code. To debug it, hover the list item and click on the play icon โ–ถ๏ธ on the list item. A new debugger view will be shown and tail the progress in real time.
  4. All previous debug runs can be found in the FIREDBG RUNS panel, simply click on it to open.

Alternatively, you can list all debuggable Rust targets with the CLI:

$ firedbg list-target

Available binaries are:
roll_dice

And, list all previous runs with the CLI:

$ firedbg list-run

Available `firedbg` runs are:
1) roll_dice-1701678002235.firedbg.ss

Visual Debuggerโ€‹

How to interpret and inspect visualized call tree, variables, timeline and threads in the Extension?

We can the open the debugger view by clicking the items in the FIREDBG RUNS panel, or with the open command:

firedbg open
  1. Each node represent a function call; the depth of each node indicates the depth of the call stack; each node has a unique frame ID; there are two types of edge:
    • Function call with return value: -<->-
    • Function call only: -->--
  2. If the program exited with a panic, the panicking function will be highlighted in red with an exclamation mark.
  3. Click on the function name on the call tree node to reveal the Rust source code.
  4. Function Arguments: the name of the argument is shown as the label. The faded text on the bounding box denote the type name, where hovering on it will reveal the fully-qualified name. The actual value is enclosed in the bounding box.
  5. Function Return Value: the return value will be shown on the far right with the label return.
  6. Timeline: toggle the timline by checking the timeline checkbox on the bottom. There are two kinds of node:
    • Circle: function call
    • Square: function return
  7. Thread selector: If the program has more than one thread, a dropdown will be shown on the bottom. You can switch to inspect the execution of other threads. Bring up the timeline to view the execution of all threads in a single view.

Controlsโ€‹

How to navigate the program execution flow?

  1. Use the control buttons on the timebar to jump to the beginning or the end of program execution. Use J K on your keyboard or stepping buttons < > to step one frame backward or forward. The unit of time on the timebar is frame ID of the selected thread. Clicking on the timebar would jump to the exact function call.
  2. The visualization will be updated as you traverse the call tree. Use W A S D keys on your keyboard or your left mouse click to pan; Click the +|- buttons on the bottom right or - = on your keyboard or use your mouse scroll wheel to zoom.

To resize a panel, hover the mouse on the panel gutter then drag to resize. Tip: double clicking the gutter reverts the panel to a pre-defined size.

FireDBG Sidebarโ€‹

How to retrieve detailed debug info?

The FireDBG sidebar contains all debug info. It will be updated as you traverse the call tree.

  1. Debugger Info: FireDBG debugger info, program executable info and runtime info
  2. Frame Info: frame metadata of the inspected function call
  3. Parameters: Rust-like representation of the inspected function call's arguments
  4. Return Value: Rust-like representation of the inspected function's return value
  5. Call Stack (Ancestors): ancestors of the inspected function; up until root
  6. Callee (Children): immediate children of the inspected function

FireDBG CLIโ€‹

There are two ways to tell firedbg where is the root directory of a cargo workspace:

  1. By default, the current directory will be the root directory of a cargo workspace
  2. Or, overriding it with --workspace-root option, i.e. firedbg --workspace-root <WORKSPACE-ROOT>

Some common sub-commands include:

  • cache: Parse all .rs source files in the current workspace
  • clean: Cleanup the firedbg/ folder
  • list-target: List all runnable targets
  • run: Run a binary target with debugging enabled
  • example: Run an example with debugging enabled
  • test: Run an integrated test with debugging enabled
  • unit-test: Run a unit test with debugging enabled
  • index: Run indexer on the latest run and save it as a .sqlite db file
  • list-run: List all firedbg runs
  • open: Open debugger view in VS Code
  • help: Print help message or the help of the given subcommand(s)

You can get the help messages by appending the --help flag.

FireDBG Workspaceโ€‹

Cargo workspace is a set of crates sharing the same Cargo.lock and target directory. FireDBG rely on Cargo to locate source files and targets for debugging.

Now you have a basic understanding on the usage of FireDBG. Let's create a Cargo workspace and practice debugging with FireDBG!

Full source code is available on our testbench.

Start by creating a getting-started workspace.

$ mkdir getting-started
$ cd getting-started

For now, we only have a single quicksort package in this workspace. We will add one more crate later.

Cargo.toml
[workspace]
members = ["quicksort"]

To create the quicksort library, we can use the convenient cargo command.

$ cargo new --lib quicksort
$ cd quicksort

Debugging Unit Testsโ€‹

Replace the content of lib.rs with our "faulty" quick sort library code.

quicksort/src/lib.rs
pub fn run<T: PartialOrd>(arr: &mut [T]) {
let len = arr.len();
quick_sort(arr, 0, (len - 1) as isize);
}

fn quick_sort<T: PartialOrd>(arr: &mut [T], low: isize, high: isize) {
if low < high {
let p = partition(arr, low, high);
quick_sort(arr, low, p - 1);
quick_sort(arr, p + 1, high);
}
}

fn partition<T: PartialOrd>(arr: &mut [T], low: isize, high: isize) -> isize {
let pivot = high as usize;
let mut store_index = low; // Shouldn't this be `low - 1`?
let mut last_index = high;

loop {
store_index += 1;
while arr[store_index as usize] < arr[pivot] {
store_index += 1;
}
last_index -= 1;
while last_index >= 0 && arr[last_index as usize] > arr[pivot] {
last_index -= 1;
}
if store_index >= last_index {
break;
} else {
arr.swap(store_index as usize, last_index as usize);
}
}
arr.swap(store_index as usize, pivot as usize);
store_index
}

Then add some unit tests to the end of lib.rs file.

quicksort/src/lib.rs
#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_quicksort_1() {
let mut numbers = [4, 65, 2, -31, 0, 99, 2, 83, 782, 1];
run(&mut numbers);
assert_eq!(numbers, [-31, 0, 1, 2, 2, 4, 65, 83, 99, 782]);
}

#[test]
fn test_quicksort_2() {
let mut numbers = [1, 2, 2];
run(&mut numbers);
assert_eq!(numbers, [1, 2, 2]);
}
}

Click on the debug icon on the left to start debugging the unit test. Or with the CLI:

firedbg unit-test quicksort test::test_quicksort_1

Oooops... assertion failure!

We found that it's quite hard to inspect what elements are swapped in each partition. To help debugging, we can add a swap function and rewrite the original code:

fn partition<T: PartialOrd>(arr: &mut [T], low: isize, high: isize) -> isize {
let pivot = high as usize;
let mut store_index = low - 1;
let mut last_index = high;

loop {
store_index += 1;
while arr[store_index as usize] < arr[pivot] {
store_index += 1;
}
last_index -= 1;
while last_index >= 0 && arr[last_index as usize] > arr[pivot] {
last_index -= 1;
}
if store_index >= last_index {
break;
} else {
- arr.swap(store_index as usize, last_index as usize);
+ swap(&mut arr[store_index as usize..=last_index as usize]);
}
}
- arr.swap(store_index as usize, pivot as usize);
+ swap(&mut arr[store_index as usize..=pivot as usize]);
store_index
}

+ fn swap<T: PartialOrd>(arr: &mut [T]) {
+ arr.swap(0, arr.len() - 1);
+ }

Tip: you can add #[cfg_attr(not(debug_assertions), inline)] to the swap function to inline it in release build, so that it will not incur any overhead.

Now, we can clearly see what was swapped and how many times swap was called. Upon a closer inspection we will see a pattern, i.e. the first element is always untouched in all partition operations. That's the bug ๐Ÿ›!

fire::dbg! Trace Macroโ€‹

Let's try to debug the same program with a different approach. An non-invasive approach, this time we only trace the swap without modifying the program structure.

FireDBG provide a fire::dbg! macro similar to std::dbg! to capture the variable of interest.

We can trace the swap actions with the help of fire::dbg!. The main advantage compared to std::dbg!, is that the trace data is associated with the stack frame of the calling function.

Undo the previous change and go back to the original implementation.

Cargo.toml
[dependencies]
+ firedbg-lib = "0.1"
+ use firedbg_lib::fire;

fn partition<T: PartialOrd>(arr: &mut [T], low: isize, high: isize) -> isize {
let pivot = high as usize;
let mut store_index = low - 1;
let mut last_index = high;

loop {
store_index += 1;
while arr[store_index as usize] < arr[pivot] {
store_index += 1;
}
last_index -= 1;
while last_index >= 0 && arr[last_index as usize] > arr[pivot] {
last_index -= 1;
}
if store_index >= last_index {
break;
} else {
+ fire::dbg!("swap", &arr[store_index as usize..=last_index as usize]);
arr.swap(store_index as usize, last_index as usize);
}
}
+ fire::dbg!("swap", &arr[store_index as usize..=pivot as usize]);
arr.swap(store_index as usize, pivot as usize);
store_index
}

This time the swap is shown in the variables of the partition function. As expected, we see 3 swaps. This should help us to observe the pattern, locate and fix the bug ๐Ÿ”Ž!

Debugging Integration Testsโ€‹

Similar to unit test, we can debug integration test with FireDBG. Let's add an integration test file:

quicksort/tests/bookshelf.rs
#[test]
fn test_quicksort_1() {
let mut books = [
"The Rust Programming Language",
"Beginning Rust: From Novice to Professional",
"Rust in Action",
"Programming Rust: Fast, Safe Systems Development",
"Rust Programming Language for Beginners",
];
quicksort::run(&mut books);
assert_eq!(
books,
[
"Beginning Rust: From Novice to Professional",
"Programming Rust: Fast, Safe Systems Development",
"Rust Programming Language for Beginners",
"Rust in Action",
"The Rust Programming Language",
]
);
}

Alternatively, you can debug integration test with the CLI:

firedbg test bookshelf test_quicksort_1

Debugging Binary Targetsโ€‹

Let's create an executable program. We need to add some dependencies first.

Cargo.toml
[dependencies]
firedbg-lib = "0.1"
+ fastrand = "2"
+ structopt = "0.3"
quicksort/src/main.rs
use firedbg_lib::fire;
use std::iter::repeat_with;
use structopt::StructOpt;

#[derive(StructOpt, Debug)]
struct Opt {
/// Random seed
#[structopt(long, default_value = "2525")]
seed: u64,
/// Number of random numbers to be sorted
#[structopt(default_value = "10")]
n: usize,
}

fn main() {
let Opt { seed, n } = Opt::from_args();

fire::dbg!(&seed);
fire::dbg!(&n);

fastrand::seed(seed);

let max = if n <= 10 { 100 } else { 1000 };

println!("Sort {n} numbers in ascending order");
let mut numbers: Vec<_> = repeat_with(|| fastrand::i32(1..max)).take(n).collect();

println!("Input: {:?}", numbers);
quicksort::run(&mut numbers);
println!("Sorted: {:?}", numbers);

let mut c = 0;
for n in numbers {
assert!(n >= c);
c = n;
}
}

We can add a [[targets]] entry in firedbg.toml to create additional profiles:

[[targets]]
name = "quicksort_100"
target.type = "binary"
target.name = "quicksort"
argv = ["100", "--seed", "1212"]

See the full example.

Or use the FireDBG CLI to pass additional parameters to the Rust binary:

firedbg run quicksort -- 100 --seed 1212

Debugging Examplesโ€‹

Examples work the same as binary targets, just that they are located under the examples/ directory.

We can also debug example with the CLI:

firedbg example random100

firedbg/ Output Folderโ€‹

A firedbg folder will be created for storing the symbols, debug runs and other supporting files. You should ignore this folder from your source control, i.e. add firedbg/ to .gitignore.

firedbg.toml Configโ€‹

Let's try and add one more crate to the workspace.

$ cargo new --lib book-store
$ cd book-store

Update the Cargo.toml at workspace root, adding our new book-store package.

Cargo.toml
[workspace]
members = ["quicksort", "book-store"]

Below we have a simple function to list the inventory in alphabetical order.

book-store/src/lib.rs
use anyhow::Result;
use std::fs::File;
use std::io::{BufRead, BufReader};

pub fn inventory(path: &str) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);

let mut books = Vec::new();
for line in reader.lines() {
let book = line?.trim().to_owned();
books.push(book);
}
quicksort::run(&mut books);
Ok(books)
}

To put it in action we can add a books.txt file to the package root and then write a unit test to invoke the inventory function.

book-store/books.txt
The Rust Programming Language
Rust Programming Language for Beginners
Programming Rust: Fast, Safe Systems Development
Beginning Rust: From Novice to Professional
Rust in Action
book-store/src/lib.rs
#[cfg(test)]
mod test {
use super::*;
use anyhow::Result;

#[test]
fn test_inventory_1() -> Result<()> {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/books.txt");
let books = inventory(path)?;
assert_eq!(
books,
[
"Beginning Rust: From Novice to Professional",
"Programming Rust: Fast, Safe Systems Development",
"Rust Programming Language for Beginners",
"Rust in Action",
"The Rust Programming Language",
]
);
Ok(())
}
}

Umm... we see that function calls to the quicksort crate are missing in the call tree.

By default FireDBG will only trace the functions of the debug target. If you want to trace other crates in your local workspace, you will need to create a firedbg.toml config file in your workspace root.

firedbg.toml
[workspace.members]
quicksort = { trace = "full" }
# Syntax: <PACKAGE> = { trace = "<full | none>" }

Now, we can see the function calls of the quicksort crate!

The Event Indexโ€‹

When you open a .firedbg.ss file, FireDBG indexer will create a .sqlite file to store the analyzed debug info. You can also run the indexer manually with the firedbg index sub-command. You can now write SQL queries to your heart's content!


  1. Supported Windows (WSL 2) distributions: Ubuntu 22.04, Ubuntu 20.04โ†ฉ
  2. Supported macOS versions: macOS 13 (Ventura), macOS 14 (Sonoma)โ†ฉ
  3. Supported Linux distributions: Ubuntu 22.04, Ubuntu 20.04, Debian 12, Fedora 39โ†ฉ