Skip to content

rustviz/rustviz2

Repository files navigation

RustViz 2

RustViz generates interactive timeline visualizations of ownership and borrowing for short Rust programs. It is meant as a teaching aid: paste a snippet, see exactly when each binding becomes the resource owner, when references go in and out of scope, and which lines those events correspond to.

RustViz 2 is the compiler-integrated rewrite of the project. Earlier RustViz read hand-annotated source; RustViz 2 plugs into rustc directly and walks HIR/MIR, so the diagram reflects the real borrow checker's view of your program rather than a hand-curated approximation.

RustViz is a project of the Future of Programming Lab at the University of Michigan.

Try it live: https://rustviz.github.io/playground/ (compile API at https://rustviz-playground.fly.dev/)

screenshot placeholder


Architecture at a glance

              GET /  (CDN, instant)
   browser ─────────────────────▶  GitHub Pages
                                   rustviz.github.io/playground/
                                   (Vite SPA, ex-assets)

              POST /submit-code (cold start ~10s
                                  after Fly auto-stop,
                                  cached afterward)
   browser ─────────────────────▶  rv-serve (Actix-web on Fly)
                                          │
                                          │ docker run --network=none --read-only …
                                          ▼
                                   ┌────────────────────────┐
                                   │  rustviz-runner image  │  ephemeral container per request
                                   │  (nightly + plugin)    │  tmpfs /work, capped CPU/RAM/PIDs
                                   └──────────┬─────────────┘
                                              │ cargo rv-plugin
                                              ▼
                                   ┌────────────────────────┐
                                   │   rustviz2-plugin      │  rustc plugin: walks HIR/MIR,
                                   │   (rustc_private)      │  emits two SVGs on stdout
                                   └────────────────────────┘

The frontend is a static Vite bundle, hosted on GitHub Pages, so the page loads instantly even when no one has visited recently. The compile API on Fly is allowed to auto-stop and cold-start; that latency only shows up after the user clicks "Generate Visualization", where a couple-second delay is expected. CORS in rv-serve/src/main.rs allows the Pages origin to call /submit-code.

The same rv-serve binary still serves the SPA + API from a single origin in the all-in-one Fly deploy (and in local development), so neither hosting mode is special-cased in the application code.

Workspace members:

Crate Role
rustviz2-plugin The rustc plugin. Built on Will Crichton's rustc_plugin/rustc_utils crates (same family as Flowistry/Aquascope). Provides cargo rv-plugin.
rustviz2 Thin user-facing library. Rustviz::new(code) runs the plugin against code (in a sandboxed Docker container by default) and returns the rendered code-panel and timeline-panel SVGs.
mdbook-rustviz mdbook preprocessor that turns ```rv ``` fenced blocks into embedded SVGs.
rv-serve Actix-web playground: serves the React/CodeMirror SPA and exposes POST /submit-code.

Quick start (local)

Requirements: rustup, node 20+, and (for the sandboxed backend) docker or Colima. The pinned nightly toolchain is auto-installed by rustup from rust-toolchain.toml.

git clone https://github.com/rustviz/rustviz2
cd rustviz2
./setup.sh                    # toolchain, plugin install, frontend build, runner image
cd rv-serve && cargo run --release
open http://127.0.0.1:8080/

Iterating on the frontend with hot reload:

cd rv-serve/frontend && npm run dev   # serves at http://127.0.0.1:3000/
# (proxies /submit-code + /ex-assets to rv-serve at :8080, so leave
#  `cargo run` running in another terminal)

If you don't have Docker installed and just want to poke at the server, set RV_RUNNER=local. Never do this on a public deployment — see SECURITY.md.


Deploy

Production runs in two pieces:

  • Static SPA on GitHub Pages, at https://rustviz.github.io/playground/. Built from rv-serve/frontend/ by .github/workflows/pages.yml and pushed to the rustviz/playground repo on every change. Loads instantly even when no one has visited recently.
  • Compile API on Fly.io, at https://rustviz-playground.fly.dev/. Ten Machines provisioned, all auto-stopping when idle; the edge proxy routes traffic to whichever ones are awake and starts more from stopped state when concurrency thresholds (fly.toml::http_service.concurrency) are crossed. dockerd uses the fuse-overlayfs storage driver so Machines don't need a per-Machine ext4 volume for /var/lib/docker (see fly.toml and deploy/entrypoint.sh for context). Idle cost ~$2–3/mo total; an HN-spike day adds ~$5–10 of Machine compute. Allowed origins for cross-origin requests are listed in rv-serve/src/main.rs::cors.

The same rv-serve binary also still works as an all-in-one server (SPA + API on a single origin); the GitHub Pages split is just a latency optimization for the static page-load.

First-time setup (Fly compile API)

fly auth login                                  # browser OAuth
fly launch --copy-config --no-deploy            # creates the app

# Trigger the runner-image workflow manually for the first publication.
# It also auto-fires on every push to main that touches runner/** or
# rustviz2-plugin/**, but the very first time it has to be kicked off
# by hand because there's nothing in GHCR yet for the deploy to pull.
gh workflow run runner-image.yml --ref main
gh run watch                                    # blocks until the run finishes
                                                # (~30 min first time, ~5 min later)

# Mark the new GHCR package public so Fly Machines can pull without auth:
#   GitHub → Org → Packages → rustviz-runner →
#     Package settings → Change visibility → Public.
# This step has to happen before the next command, otherwise the deploy's
# first-boot `docker pull` fails.

./deploy/deploy.sh                              # two-phase Fly deploy

The first boot of each Fly Machine pulls the rustviz/rustviz-runner image from GHCR (~30 s for ~600 MB). It's then cached on the Machine's local filesystem; subsequent cold starts after auto-stop take ~10 s.

./deploy/deploy.sh also ensures the fleet stays at 10 Machines (override with RV_FLY_MACHINES=N). With auto-stop on, idle Machines are free; the extra capacity exists so the edge proxy has somewhere to spill load when one Machine gets saturated. No need to manually scale up before posting the URL somewhere.

Routine deploys

./deploy/deploy.sh

When you change runner/** or rustviz2-plugin/**, .github/workflows/runner-image.yml automatically republishes the sandbox image to GHCR; the next ./deploy/deploy.sh picks it up on each Machine's first boot.

Every push to main triggers .github/workflows/build.yml. The workflow runs the build + tests first (also on every PR), then on main pushes only a downstream deploy job runs ./deploy/deploy.sh on a hosted runner (requires a FLY_API_TOKEN repo secret). Because deploy declares needs: build, a failing test suite blocks the deploy. The deploy job opens a deploy-failure-labelled issue on failure, in addition to GitHub's default email-on-failure notification.

First-time setup (GitHub Pages SPA)

# 1. Create the receiving repo
gh repo create rustviz/playground --public \
  --description "Static front-end for the RustViz playground"

# 2. Enable Pages on rustviz/playground via Settings → Pages →
#    Source: Deploy from a branch → main / root.

# 3. Generate a deploy keypair
ssh-keygen -t ed25519 -f /tmp/playground_deploy_key -N "" -C playground-deploy

# 4. Add the *public* key as a write-enabled deploy key on rustviz/playground
gh api -X POST repos/rustviz/playground/keys \
  -f title=playground-deploy -F read_only=false \
  -f key="$(cat /tmp/playground_deploy_key.pub)"

# 5. Add the *private* key as a secret on rustviz/rustviz2
gh secret set PAGES_DEPLOY_KEY --repo rustviz/rustviz2 < /tmp/playground_deploy_key

# 6. Clean up
rm /tmp/playground_deploy_key /tmp/playground_deploy_key.pub

After that, every push to main (when the change touches rv-serve/frontend/**) triggers .github/workflows/pages.yml, which builds the SPA in pages mode and pushes the dist/ tree to rustviz/playground for serving at https://rustviz.github.io/playground/.

Adding a new SPA origin

If you ever stand up the SPA at another URL (custom domain, mirror), add that origin to the CORS allowlist in rv-serve/src/main.rs and redeploy the API. The allowlist is the gate — without it the new origin's browsers will refuse to call /submit-code.

Why a script instead of fly deploy directly

fly.toml ships with auto_stop_machines = 'off' because Fly's autoscaler stops a Machine after ~40 s of "no incoming traffic", and our entrypoint takes 5–15 min on a fresh Machine to extract the ~1 GiB runner image through fuse-overlayfs. With 'stop' set at deploy time, every fresh Machine would be killed mid-bootstrap before rv-serve ever binds :8080fly deploy would deadlock. So 'off' is the only value that lets a fresh deploy actually finish.

The cost-saving auto-stop behavior is applied per-Machine after deploy. deploy/deploy.sh does a destroy-and-recreate every time:

  1. Destroys every existing Machine (parallel fly machine destroy --force).
  2. Runs fly scale count and fly deploy --strategy immediate, then polls fly status --json until every freshly-created Machine's HTTP check passes.
  3. Runs fly machine update --autostop=stop --skip-health-checks against every Machine in parallel, flipping the per-Machine service config via the Machines API. Verifies the config landed; doesn't poll for health afterwards (auto-stop kicks in immediately, so a poll-for-all-healthy loop is unwinnable).

We destroy and recreate rather than incrementally updating the existing fleet because every iteration of the in-place approach hit a different stale-state edge case (stopped Machines that don't auto-start during deploy, drift between Machines created under different fly.toml settings, autoscaler racing the post-update bounce, etc.). Nuking the fleet sidesteps all of it for the cost of a few minutes of downtime per deploy — acceptable for a research tool with sparse traffic. End state: every Machine on the new release with auto_stop = 'stop', fleet idles cheaply (~$2–3/mo for the IP and Machine baseline). In steady state between deploys the auto-stop / auto-start cycle is fast (~10 s cold start) thanks to persist_rootfs = 'always' keeping the runner image cached on each Machine's rootfs.

Pass --keep-warm to skip step 3 if you want Machines to never auto-stop (~$24/mo per always-running Machine).

Security

Read SECURITY.md before exposing the playground to the public internet. The runner image's sandbox flags are not optional.


Limitations

RustViz 2 is a research tool. It supports a meaningful subset of Rust but not all of it. Currently unsupported (or known to misbehave):

  • For-loops
  • Conditional let bindings
  • Borrows that occur inside conditionals
  • Chained method calls (x.get().get_mut())
  • Lifetime annotations
  • Borrows over struct members

The plugin has a TODO list with more detail in rustviz2-plugin/README.md.


Security

The playground compiles untrusted Rust source. Proc-macro expansion in user code is arbitrary code execution, so the plugin always runs inside a sandboxed container. The full threat model and the operator checklist are in SECURITY.md. Report findings to comar@umich.edu.


Contributing

Issues and PRs welcome. The project follows standard GitHub flow; keep each PR focused on a single concern, and run ./setup.sh plus cargo build --workspace --locked before opening one.


License

MIT.

Citing

If you use RustViz in academic work, please cite the VL/HCC 2022 paper.

About

Interactive visualizer for ownership and borrowing in simple Rust code. Version 2 adds automatic visualization.

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors