memtrace
PlaygroundBetaLearn memoryFollow on

Getting started

  • Introduction

Unsafe Rust

  • Introduction
  • Raw pointers
  • Undefined behavior

Real world

  • Cloudflare: 100 TB saved in 1.1.1.1's DNS cache

Playground

  • How the Playground works
  • What the Playground can't do yet

Playground

How the Playground works

Your Rust program, compiled and run inside your browser, turned into the same step-by-step memory walkthrough as the visualizers

What it does

The Playground takes a Rust program, runs it, and stops after every line to look at memory: each stack frame and its variables, every heap block, where each pointer points, and what the program has printed. Each stop becomes one step, drawn the same way as the built-in visualizers

  • Write or paste a program, or start from the one in the editor: a small store with static items, string literals, structs that own Strings, and a &mut borrow. Reset brings it back, and the copy button in the editor's corner copies whatever is in it
  • It runs from fn main, like any Rust program, so a program without one gets a note instead of a walkthrough. A lecture file of separate functions needs a main that calls the one you want to see
  • Or open one from elsewhere on the site: Open in Playground on a visualizer, or Visualize in Playground under a program in these docs. It arrives in the editor and generates straight away
  • Press Generate visualization, or ⌘/Ctrl + Enter. While it works, the right side shows each stage: loading the engine, compiling and running, drawing the steps
  • Step through it with the arrows, the step numbers or auto-play, like any visualizer. The last step is always Program ends: main's frame is popped and the stack is empty
  • Generating the same code again is instant: the Playground remembers your last 20 programs, so the button says Show visualization when nothing has changed

You need a laptop or desktop

The Playground puts your code and its memory side by side, so it needs a screen at least 1024 px wide. On a phone or tablet it shows a note instead, with a link you can copy to open it later. The visualizers and these docs work on any screen

Nothing leaves your browser

There's no server. The Rust compiler runs inside the page as WebAssembly, so your code is never uploaded anywhere

The engine

The engine is Aquascope, a research tool from Brown University. It's the real rustc front end plus Miri, Rust's interpreter for finding undefined behavior, compiled to WebAssembly. Miri runs the program one MIR statement at a time, and Aquascope records what memory looks like at each pause

Because Miri checks every memory access, the Playground also catches real undefined behavior: a use-after-free, or a read past the end of a buffer, stops the walkthrough with a step that explains what went wrong

The first run downloads the engine

The compiler and Rust's standard library are about 200 MB, roughly 50 MB compressed. They download once, the first time you generate your own program, and the browser keeps them. After that a program takes well under a second. The visualizer pages never need it

Reading the diagram

Aquascope's raw output is a list of memory snapshots. The Playground turns each one into the visualizers' picture, and works out what changed since the step before to write its title and explanation. They come from fixed templates, so the text never claims anything the diagram doesn't show

The stack:

  • One frame per call, the running one on top. Recursion stacks a frame per call, and a run of returns reads as one step: "3 calls to factorial return"
  • A struct gets a slot per field, so you see how its fields sit side by side. A variable declared but not set yet reads [???]
  • A moved variable fades out, and comes back once it's given a new value. A shadowed one (let b = …; let b = …;) stays, drawn as b₁
  • Cell and RefCell show the value they hold; RefCell's borrow counter isn't drawn. A HashMap or HashSet is one slot with its length
  • A line that changes nothing still says what it did: an if or while reads "Check n <= 1", a passing assert_eq! reads "assert_eq! holds", and a print shows the text it wrote

The heap:

  • String, Vec and Box become .ptr, .len and .cap slots with an arrow to their block. Spare capacity shows as empty cells, and a push reads as a write into it
  • Each block keeps one made-up address for as long as it lives, and a freed address is never reused in the drawing. x = Box::new(10) frees the old Box and allocates a new one; *x = 10 writes into the same Box; mem::swap swaps the pointers, not the data
  • A Vec or String that outgrows its buffer moves to a bigger block, and the old one is shown freed. shrink_to_fit moves it to a smaller one
  • clone() makes a new block with a copy of the contents. Rc::clone and Arc::clone share the one block instead, and its strong count goes up
  • A block nothing points at any more that was never freed is shown leaked, outlined in amber, and stays to the end, since it's still allocated. Program ends lists what leaked
  • A block whose values are too long to sit side by side, like a Vec of structs, gets a row per element, with its index

Static memory and pointers:

  • String literals and static items with a literal value are drawn in static memory from the first step, since they're part of the program itself
  • A reference or raw pointer is an arrow. A struct holding a &str borrows its text; one holding a raw pointer, like a hand-written smart pointer, owns its block
  • A write through a pointer names it: *raw = 99 reads as a write through raw into the variable or block it points at. A pointer into freed memory is marked dangling; a null one reads null
  • A pointer stored inside a block, like a linked-list node's next, shows the address it leads to
  • When several pointers lead to the same block, their arrows fan in side by side instead of crossing

Unsafe code:

  • Each statement in an unsafe { … } block is a step of its own, so *p = 10; and *p += 1; are two writes, and a Box rebuilt with Box::from_raw is freed at the block's closing brace
  • Raw pointers into the stack or the heap, ptr::read, ptr::write and ptr::swap, slice::from_raw_parts, Vec::from_raw_parts and ManuallyDrop are all drawn as they happen. A ptr::write into a Vec's spare capacity shows once set_len counts it
  • Undefined behavior ends the walkthrough with a step saying which kind it was: a use after free, a double free, a read past the end of a block (with the offset and the block's size), or a pointer into a function that had already returned
  • A panic gets a step of its own on the line that panicked, with Rust's message (attempt to subtract with overflow). Each function the panic passes through unwinds, dropping what it owns, and the program ends with exit code 101
  • A program that starts a thread is shown up to the first thread::spawn, and its last step says why it stops there
1fn main() {
2 let kept = Box::new(String::from("never freed"));
3 let raw = Box::into_raw(kept);
4 println!("{}", unsafe { &*raw });
5}
$never freedVisualize in Playground
Nothing turns raw back into a Box, so when it goes out of scope the Box and its String leak. The walkthrough keeps both, outlined in amber

A big program stays readable: the diagram fills the width of the panel and scrolls down, and each step is only as tall as what it shows. The zoom button in the toolbar (Fit the whole diagram) shows all of it at once, and again goes back

What it adds to your program

Aquascope has a few gaps, so the Playground quietly changes a copy of your program before running it, and undoes those changes in the drawing. Your code in the editor isn't touched, and no line moves

  • Printed output. Miri throws away what a program prints, so println! and friends are swapped for versions that pass the text along. That's how the terminal fills in
  • String literals. A local holding a &'static str crashes Aquascope, so let s = "hi";, or a literal passed to a function like create_item("Apple", 100), gets a hidden owner and is drawn back in static memory
  • Derived Debug. Printing a #[derive(Debug)] type with {:?} would crash it too, so the same text is built with format! instead, for the type and for the Vec, Option, Result or tuple it sits in. The output is byte-for-byte what rustc prints
  • Panics. The engine ends a panicking run as if nothing went wrong, so main runs under a hidden catch_unwind that passes the message on. That's how the walkthrough can show the line that panicked, and what it said
  • Struct layout. The engine works out which field a reference points into by adding up field sizes in the order they're declared, while rustc moves fields around and pads them. So a struct's fields are declared largest first and marked #[repr(C)]: same size, but laid out as declared. The diagram still lists them in the order you wrote them
  • Blocks. The engine pauses only at a function's own statements, so an unsafe { … } block would be one step. Each statement inside becomes a statement of the function, with its own unsafe, and at the closing brace the block's variables are dropped, latest first, just as the block would. A block that needs to stay whole for the program to mean the same thing stays whole
1fn test_stack() {
2 let on_stack = 6;
3 println!("{} at {:p}", on_stack, &on_stack);
4}
5
6fn main() {
7 test_stack();
8}
Visualize in Playground
A lecture function, run from main. The terminal shows the real output, including the address {:p} prints

How it's tested

Every change to the Playground runs over a hundred programs through the real engine: the Rektoff lectures, everyday Rust (clones, borrows, slices, structs and enums that own heap data, Rc<RefCell>, HashMap, recursion, Drop order, closures), unsafe Rust with real undefined behavior, and low-level code of the kind Solana programs are made of (byte serialization, account handles, zero-copy casts, overflow, missing owner and signer checks). Each must print exactly what a native rustc build prints, and its walkthrough must keep the heap consistent: a freed block never comes back, two live blocks never share an address, a block keeps its address unless it grows, and every arrow points at something drawn

They're also run through full Miri, which checks Rust's aliasing rules too. The Playground and full Miri must agree on every program: undefined behavior in one exactly when in the other, and a panic in one exactly when in the other

Resources

  • AquascopeThe engine: Rust's compiler and Miri, run in the browser
  • MiriThe interpreter that runs each step and catches undefined behavior
PreviousCloudflare: 100 TB saved in 1.1.1.1's DNS cacheNextWhat the Playground can't do yet

On this page

  1. What it does
  2. The engine
  3. Reading the diagram
  4. What it adds to your program
  5. How it's tested
  6. Resources