A web application that visualizes C++ data structures with real-time animations. It compiles and executes real C++ code via a backend g++ compiler, traces actual memory operations at runtime using GDB MI, and renders step-by-step animated visualizations.
C++ Code --> g++ Compile --> GDB MI Session --> Line-by-line Snapshots --> gdbMapper --> TraceStep[] --> Visualization
(-g -O0) (--interpreter=mi2) (locals + struct fields) (ALLOC/SET_PTR/ (React + Framer Motion)
SET_FIELD/LOCAL_VAR)
- User writes C++ code with custom structs / pointers
- Backend compiles code with debug symbols (
g++ -g -O0) - GDB runs the binary line-by-line in MI mode, capturing local variables and struct field values at every step
gdbMapperconverts raw GDB snapshots intoTraceStep[]events (ALLOC, SET_PTR, SET_FIELD, LOCAL_VAR)- Runtime pattern analysis builds a pointer graph and detects data structure types by analyzing actual graph topology (out-degree, cycles, depth)
- Frontend replays commands as animated visualizations
There is no second tracing path. The program is compiled, run, and its output
returned; steps is empty and the visualizer draws nothing. The response
carries a notice saying so.
An earlier version rewrote the user's source to inject trace calls. It was removed because it could emit C++ that would not compile — for a program that compiled perfectly well on its own.
- Real C++ Execution: Compiles and runs actual C++ code via g++ backend. No simulation or pseudo-code parsing.
- GDB-Powered Tracing: Uses GDB Machine Interface (MI) to step through code line by line and capture exact memory state — no source code transformation needed.
- Smart Auto-Detection: Two-layer detection system:
- Static analysis: Method names (
push/pop= Stack) and self-type pointer counts (2+ = Tree, 1 = Linked List) - Runtime analysis: Builds actual pointer graph from execution traces and reclassifies based on graph topology (branching, cycles, depth)
- Static analysis: Method names (
- Memory Graph Visualization: Traces pointers (
->), allocations (new), and deallocations (delete) to draw memory relationships. - Step-by-Step Execution: Run / Pause / Step / Prev controls with precise state tracking.
- Adjustable Speed: Playback speed control from 0.25x to 4x.
- Resizable Panels: Editor, visualizer, and terminal panels are all draggable.
- Interactive Views: Drag-to-pan and Ctrl+Scroll zoom in visualization boxes.
- C++ Autocomplete: IDE-style code completion for C++ keywords, types, and STL containers.
- Terminal Output: Real-time
coutoutput display with stdin input support.
| Data Structure | Color | Detection Method | Visualization |
|---|---|---|---|
| Stack | Purple | push() + pop() methods, or std::stack/vector |
Vertical plate stacking with TOP indicator |
| Queue | Cyan | enqueue() + dequeue() methods, or std::queue/deque/priority_queue |
Horizontal conveyor belt with FRONT/BACK labels |
| Tree (BST / N-ary) | Green | 2+ self-type pointers OR runtime branching pattern (parent pointers tolerated) | Hierarchical bucket layout with ROOT badge, drag-to-pan |
| Linked List | Purple | 1 self-type pointer, linear chain | Graph view with arrow connections |
| Doubly Linked List | Cyan / Amber | Linear chain with bidirectional next/prev pairs |
Horizontal chain — solid cyan next arrows + dashed amber prev arrows |
| Circular Linked List | Amber | Runtime cycle detection (single outgoing pointer with loop) | Polygon layout with HEAD badge, amber dashed cycle-back edge |
| General Graph | Rose | Branching + cycle that survives back-edge stripping | Graph view with all edges |
| Memory (Heap) | Purple | Fallback for pointer structures | Graph view with memory addresses |
Static Hint (compile-time) Runtime Analysis (post-execution)
push/pop methods → Stack (already classified)
enqueue/dequeue → Queue (already classified)
std::stack/vector → Stack
std::queue/deque/priority_queue → Queue
┌─ strip bidirectional pairs from one field ─┐
│ (the "back-edge" / prev / parent field) │
└─────────────────┬──────────────────────────┘
▼
primary graph cyclic + branching → Graph
primary graph cyclic + max-out-deg ≤ 1 → Circular Linked List
primary graph acyclic + branching → Tree
primary graph acyclic + back-edges → Doubly Linked List
primary graph acyclic + chain → Linked List
Field names don't matter: left/right, a/b, child1/child2, ptr1/ptr2, next/prev, child/parent — the system detects structure from actual runtime pointer topology.
Supported C++ patterns: struct, class, private/public/protected, friend class, constructors with initializer lists, const/static qualifiers, array fields, multi-variable declarations, delete[].
Supported Value Types: int, double, string, bool, char
Supported STL Containers:
std::stack, std::queue, std::priority_queue, std::vector, std::deque. The
tracer evaluates .size() / .top() / .back() per snapshot and synthesises
PUSH / POP commands from the differences.
#include <iostream>
using namespace std;
struct Stack {
int data[100];
int top;
Stack() : top(-1) {}
void push(int val) { data[++top] = val; }
void pop() { if (top >= 0) top--; }
};
int main() {
Stack s;
s.push(10);
s.push(20);
s.push(30);
s.pop();
s.push(40);
return 0;
}#include <iostream>
using namespace std;
class Node {
private:
int data;
Node* next;
Node(int d) : data(d), next(nullptr) {}
friend class LLQueue;
};
class LLQueue {
private:
Node* front;
Node* rear;
public:
LLQueue() : front(nullptr), rear(nullptr) {}
void enqueue(int val) {
Node* node = new Node(val);
if (!rear) { front = rear = node; return; }
rear->next = node;
rear = node;
}
void dequeue() {
if (!front) return;
Node* temp = front;
front = front->next;
if (!front) rear = nullptr;
delete temp;
}
};
int main() {
LLQueue q;
q.enqueue(100);
q.enqueue(200);
q.enqueue(300);
q.dequeue();
q.enqueue(400);
return 0;
}#include <iostream>
using namespace std;
struct BBB {
int val;
BBB* a;
BBB* b;
BBB(int v) : val(v), a(nullptr), b(nullptr) {}
};
int main() {
BBB* root = new BBB(42);
root->a = new BBB(10);
root->b = new BBB(99);
root->a->a = new BBB(5);
root->a->b = new BBB(25);
return 0;
}#include <iostream>
using namespace std;
struct Node {
int data;
Node* next;
Node(int v) : data(v), next(nullptr) {}
};
int main() {
Node* head = new Node(1);
head->next = new Node(2);
head->next->next = new Node(3);
head->next->next->next = new Node(4);
// Delete middle node
Node* temp = head->next;
head->next = temp->next;
delete temp;
return 0;
}#include <iostream>
using namespace std;
struct Node {
int data;
Node* next;
Node(int v) : data(v), next(nullptr) {}
};
int main() {
Node* head = new Node(1);
head->next = new Node(2);
head->next->next = new Node(3);
head->next->next->next = head; // cycle back to head
return 0;
}#include <iostream>
using namespace std;
struct Node {
int data;
Node* next;
Node* prev;
Node(int v) : data(v), next(nullptr), prev(nullptr) {}
};
int main() {
Node* a = new Node(1);
Node* b = new Node(2);
Node* c = new Node(3);
a->next = b; b->prev = a;
b->next = c; c->prev = b;
return 0;
}#include <stack>
#include <queue>
using namespace std;
int main() {
stack<int> s;
s.push(10); s.push(20); s.push(30);
s.pop();
queue<int> q;
q.push(1); q.push(2); q.push(3);
q.pop();
return 0;
}- Node.js 22 (the version in
.nvmrc, which CI also uses) - npm 9+
- g++ (MSYS2 on Windows, or system g++ on Linux/Mac)
- GDB (MSYS2 on Windows:
pacman -S mingw-w64-ucrt-x86_64-gdb, Linux/Mac:apt install gdb/brew install gdb)
GDB is required. It is the only thing that produces a trace, so without it there is nothing to visualize. If GDB is missing the server still compiles and runs the program and returns its output, but
stepscomes back empty and the visualizer draws nothing. There is no longer a second tracing path to fall back to — the instrumenter was removed.
# Frontend
npm install
npm run dev
# Backend (separate terminal)
cd server
npm install
npm run devOpen http://localhost:5173 in your browser.
npm run test:all # frontend and server suites
npm run typecheck # both TypeScript projects
npm run lint # covers server/src as wellnpm test on its own runs only the frontend suite — the root vitest config is
scoped to src/**. Use test:all for both. CI
(.github/workflows/ci.yml) runs lint, typecheck, both suites and both builds
on Node 22.
Numeric values are validated at startup. Anything that is not a plain positive whole number —
256M,8s,-1,1e6— throws and the server does not start. Write byte counts out in full:RLIMIT_AS_BYTES=268435456, never256M. Unset or empty uses the default. This is deliberate: the old parser read256Mas256, which gave every traced program a 256-byte address space and reported it as an unrelated GDB failure.
| Variable | Default | Description |
|---|---|---|
VITE_COMPILER_API_URL |
http://localhost:3001 |
Backend API URL (frontend) |
PORT |
3001 |
Server port (backend) |
GPP_PATH |
Auto-detected | Path to g++ compiler |
GDB_PATH |
Auto-detected | Path to GDB debugger |
USE_GDB |
true |
Set to false to run programs without a trace — output only, steps is empty and nothing is visualized |
TEMP_BASE |
Probed | Directory that compiled binaries are written to and run from. Defaults to /dev/shm when it is exec-capable, otherwise the OS temp dir. Set explicitly if neither works |
FRONTEND_URL |
— | Allowed CORS origin for production |
MAX_STDIN_BYTES |
65536 |
Largest stdin payload accepted per request |
MAX_OUTPUT_BYTES |
1048576 |
Cap on captured program output |
GDB_SESSION_BUDGET_MS |
45000 |
Wall-clock budget for one trace session |
COMPILE_TIMEOUT_MS |
15000 |
How long the debug build gets before it is killed |
MAX_CONCURRENT_JOBS |
2 |
Compile+trace jobs running at once. Each is a compiler, a GDB and the traced program |
MAX_QUEUED_JOBS |
8 |
Jobs allowed to wait for a slot. Past this a request is refused with 503 rather than joining a pile that will time out |
QUEUE_WAIT_MS |
20000 |
How long a queued job waits before giving up |
PISTON_URL |
— | Delegate execution to a Piston server instead of local g++. No tracing on this path |
VERBOSE_STEP_LOG |
— | true logs every generated trace step |
VERBOSE_MI_LOG |
— | true echoes every GDB/MI line. Very noisy, and it prints user source and variable values |
RATE_LIMIT_COMPILE |
20 |
Compile requests per minute per IP |
RATE_LIMIT_GENERAL |
120 |
Other /api requests per minute per IP |
RATE_LIMIT_WINDOW_MS |
60000 |
Rate-limit window in milliseconds |
TRUST_PROXY |
— | Express trust-proxy setting (set to 1 or a CIDR list when behind a load balancer so rate limit keys on the real client IP) |
RLIMIT_CPU_SEC |
8 |
Per-program CPU-time cap in seconds (Linux, applied via prlimit) |
RLIMIT_AS_BYTES |
268435456 |
Per-program virtual-memory cap (256 MiB default) |
RLIMIT_STACK_BYTES |
16777216 |
Per-program stack cap (16 MiB default) |
RLIMIT_FSIZE_BYTES |
8388608 |
Max file size a program may write (8 MiB) |
RLIMIT_NOFILE |
64 |
Max open file descriptors |
RLIMIT_NPROC |
— | No longer used. RLIMIT_NPROC counts processes per UID system-wide, not per container, so a neighbour could exhaust it and stop the compiler forking cc1plus. Bound the container instead: pids_limit in compose, or --pids-limit on docker run |
PRLIMIT_PATH |
/usr/bin/prlimit |
Path to prlimit. On Linux the server refuses to start if it is missing — install util-linux, correct this path, or set DISABLE_RLIMIT=true deliberately |
DISABLE_RLIMIT |
— | true starts the server with no CPU, memory, file-size or process caps on user code. This is the documented escape hatch for a host that is already constrained (cgroups, a VM); anywhere else it turns this service into an unbounded remote-execution endpoint |
- Connect GitHub repo to Vercel
- Set
VITE_COMPILER_API_URLto your backend URL - Deploy
The backend needs two things a managed PaaS often will not give you:
- ptrace, because GDB is the only thing that produces a trace.
- An exec-capable temp directory. Compiled binaries are written and then
run. Docker mounts
/dev/shmnoexecby default, so the binary is written successfully and then refuses to execute. The server probes for this at startup and falls back to the OS temp dir;TEMP_BASEoverrides it outright.
Without ptrace you get a backend that compiles and runs the submitted program
and returns its output, with steps empty and nothing drawn. That is a
run-only service, not a visualizer. There is no instrumenter fallback any
more — if a platform denies ptrace, the answer is a different platform, not a
different setting.
The repo has no render.yaml or Procfile; anything deployed to a dashboard-
configured PaaS has its build command, start command and env vars living
outside version control.
server/docker-compose.yml is the only invocation that sets every flag this
image needs — the exec tmpfs mounts above included:
cd server
docker compose up compilerIt applies read_only, no-new-privileges, the bundled seccomp profile,
mem_limit, cpus and pids_limit, and mounts /tmp and /dev/shm as
exec-capable tmpfs. Set FRONTEND_URL to your frontend origin before exposing
it. The equivalent raw docker run is in the comment at the end of
server/Dockerfile — note the seccomp path there is a host path, resolved
by the Docker CLI before the container exists.
The server compiles and runs arbitrary C++ submitted over HTTP, so these flags
are load-bearing rather than decorative. prlimit caps each program's CPU,
memory, file size and process count; the container flags bound everything
prlimit cannot. /api/compile is additionally rate-limited (20 req/min/IP by
default); tune with the RATE_LIMIT_* vars above.
Known gaps in the bundled profile, so you can decide rather than discover:
the seccomp profile permits socket/connect, so traced code can reach the
network — add network_mode: none to the compose service if that matters. It
also permits ptrace unconditionally, which GDB needs; on a host with
kernel.yama.ptrace_scope=0 that also lets user code attach to the Node process,
since both run as the same uid. The piston profile in the compose file starts a
privileged container and is off by default; leave it off unless you have
isolated its network.
src/
├── types.ts # Type definitions (Command, State, etc.)
├── App.tsx # Main layout with resizable panels
├── api/
│ └── compilerApi.ts # Frontend -> Backend API client
├── engine/
│ └── stepMapper.ts # TraceStep[] -> Commands + runtime pattern analysis
├── hooks/
│ └── useVisualizer.ts # State management (useReducer)
├── utils/
│ └── ids.ts # ID generation utility
└── components/
├── CodeInput.tsx # Monaco C++ editor with autocomplete
├── Controls.tsx # Execution controls & speed slider
├── Terminal.tsx # Output / Input / Command Log tabs
├── Visualizer.tsx # Routes structures to visualization components
└── DataStructures/
├── StackPlate.tsx # Stack: vertical plate stacking
├── QueueBlock.tsx # Queue: horizontal conveyor belt
├── GraphView.tsx # Memory / Linked List / General Graph: graph with arrows
├── TreeChart.tsx # Tree: hierarchical bucket layout (N-ary)
├── CircularListView.tsx # Circular Linked List: polygon layout
└── DoublyListView.tsx # Doubly Linked List: forward/back arrows
server/
├── Dockerfile # Docker image for deployment (Node.js + g++ + gdb + util-linux)
├── docker-compose.yml # The supported invocation — applies every hardening flag
├── seccomp-profile.json # Conservative syscall whitelist for `--security-opt seccomp=...`
└── src/
├── index.ts # Express server with CORS + rate-limit
├── env.ts # Strict parsing of numeric settings; throws at startup on a bad value
├── routes/compile.ts # POST /api/compile endpoint
└── services/
├── compiler.ts # g++ compilation + prlimit wrapper (Linux)
├── childEnv.ts # Env allowlist — user code never sees the server's own variables
├── tempBase.ts # Picks an exec-capable directory for compiled binaries
├── gdbDriver.ts # GDB MI driver: spawns GDB, steps line-by-line, captures snapshots, exec-wrapper rlimit
└── gdbMapper.ts # Converts GDB snapshots → TraceStep[] events (incl. STL containers)
This project is built with the following open-source libraries:
| Library | License | Description |
|---|---|---|
| React | MIT | UI component library |
| React DOM | MIT | React rendering for web |
| TypeScript | Apache-2.0 | Typed superset of JavaScript |
| Vite | MIT | Next-generation frontend build tool |
| Tailwind CSS | MIT | Utility-first CSS framework |
| Framer Motion | MIT | Animation library for React |
| Monaco Editor (React) | MIT | VS Code editor component for React |
| ESLint | MIT | JavaScript/TypeScript linter |
| Library | License | Description |
|---|---|---|
| Express | MIT | Web framework for Node.js |
| CORS | MIT | Cross-Origin Resource Sharing middleware |
| Node.js | MIT | JavaScript runtime |
| tsx | MIT | TypeScript execution for Node.js |
| express-rate-limit | MIT | Per-IP rate limiting for /api |
| dotenv | BSD-2-Clause | Loads server/.env into the environment |
| Tool | License | Description |
|---|---|---|
| g++ (GCC) | GPL-3.0 | C++ compiler (runtime dependency, not bundled) |
| GDB | GPL-3.0 | GNU Debugger for line-by-line tracing (runtime dependency, not bundled) |
MIT License - see LICENSE for details.