Every algorithm visualization tool I've used has the same problem — you have to rewrite your code to use their API. You're not learning the algorithm anymore, you're learning their framework.
I wanted something different. Write normal Java. See it visualized. No SDK, no tracing calls, nothing.
So I built AlgoFlow (the engine behind AlgoPad). It supports both Java and Python — this post focuses on the Java side.
// This is all you write. Seriously.
int[] arr = {5, 2, 8, 1};
arr[0] = 10; // ← automatically visualized
@Tree TreeNode root = new TreeNode(1);
root.left = new TreeNode(2); // ← tree updates in real time
No tracer.patch(0, 10). No visualize(arr). Just code.
Why bytecode?
The obvious approach for Java would be AST transformation — parse the source, inject tracing calls, compile the modified source. That's what most tools do, and it works fine for simple cases.
But I wanted to intercept everything. Every array read. Every array write. Every field mutation on a tree node. Every list.add(). Every map.put(). And I wanted it to work on code the user writes naturally, without them thinking about visualization at all.
Java bytecode gives you that. The JVM has specific opcodes for every operation I care about:
-
IASTORE/IALOAD— array element write / read -
PUTFIELD/GETFIELD— object field mutation / access -
PUTSTATIC— static field assignment -
INVOKEVIRTUAL— method calls on collections
When your Java code does arr[3] = 42, the compiler emits an IASTORE instruction. I intercept that instruction before the class even loads, inject my visualization callback around it, and the original code runs exactly as written. The user's code never knows it's being watched.
AST transformation can't do this as cleanly. And the reason comes down to something fundamental: Java source semantics are huge, but bytecode semantics are tiny.
Think about all the ways you can write an array store in Java source:
arr[i] = 5;
arr[i + 1] = arr[j - 1] + arr[k] * 2;
arr[getIndex()] = computeValue();
arr[map.get(key)] = list.get(i) > list.get(j) ? x : y;
With AST transformation, every one of these is a different tree shape. You'd need to pattern-match all of them, handle nested expressions, ternaries, method calls as indices, method calls as values — the combinatorial space is massive. Miss a pattern and it silently doesn't visualize. Add a new Java language feature and you have new patterns to handle.
But at the bytecode level? They're all the same thing. No matter how complex the source expression is, the compiler reduces it to: push array ref, push index, push value, IASTORE. One opcode. One interception point. Done.
This is true across the board. Field access in source can be this.left, node.left, getNode().left, nodes[i].left — in bytecode it's always GETFIELD. Collection operations go through INVOKEVIRTUAL regardless of how the receiver or arguments were computed.
The compiler already did the hard work of flattening Java's rich syntax into a small, fixed set of operations. Bytecode manipulation lets me piggyback on that work instead of reimplementing it.
I'm intercepting a handful of opcodes to cover a practically infinite surface of user code. That's the leverage bytecode gives you.
So how does this work in practice?
Under the hood
The engine is a Java agent — it hooks into the JVM before the user's class loads and rewrites bytecode using ByteBuddy and raw ASM visitors.
Here's the pipeline:
User writes code
↓
JVM loads the class
↓
Agent intercepts class loading (premain)
↓
ByteBuddy + ASM rewrite bytecode
↓
Transformed class runs normally
↓
Intercepted operations emit visualization commands
↓
Frontend renders step-by-step animation
Intercepting array access
This was the first thing I built. The tricky part isn't arrays specifically — it's the raw ASM stack manipulation underneath. When the JVM hits an IASTORE (integer array store), the stack looks like this:
Stack: [array_ref, index, value]
I need to capture all three, call my visualization callback, then let the original store happen. The problem is you can't just "peek" at the JVM stack — you have to pop values off, save them to local variable slots, do your work, then push them back.
// Simplified version of what ArrayAccessWrapper does:
// 1. Pop value and index into temp slots
super.visitVarInsn(Opcodes.ISTORE, valueSlot);
super.visitVarInsn(Opcodes.ISTORE, indexSlot);
super.visitVarInsn(Opcodes.ASTORE, arraySlot);
// 2. Call VisualizerRegistry.onArraySet(array, [index, value, lineNumber])
// ... pack args into Object[], invoke static method ...
// 3. Restore stack and execute original IASTORE
super.visitVarInsn(Opcodes.ALOAD, arraySlot);
super.visitVarInsn(Opcodes.ILOAD, indexSlot);
super.visitVarInsn(Opcodes.ILOAD, valueSlot);
super.visitInsn(Opcodes.IASTORE); // original operation
This runs for every single array access in the user's code. int[], boolean[], double[] — all of them. The user writes arr[i] = arr[j] and both the read (IALOAD) and the write (IASTORE) fire visualization events.
Intercepting field mutations (trees and linked lists)
When someone writes node.left = new TreeNode(5), that compiles to a PUTFIELD instruction. I intercept it the same way — capture the owner object, the field name, and the line number, then call VisualizerRegistry.onFieldSet().
The registry figures out that node belongs to a tree visualizer, and the tree visualizer knows how to turn that field mutation into a "add node, add edge" command for the frontend.
The user just annotates the root with @Tree and writes normal tree code. The engine auto-detects the node class structure — finds the two self-referential fields (children) and the value field. Any class that looks like a binary tree node works.
@Tree TreeNode root = new TreeNode(1);
root.left = new TreeNode(2); // PUTFIELD intercepted → "add node 2, add edge 1→2"
root.right = new TreeNode(3); // PUTFIELD intercepted → "add node 3, add edge 1→3"
Try it
AlgoFlow is live at algopad.dev. Write a sorting algorithm, build a graph, implement a tree traversal — and watch it execute step by step.
The code is open source: github.com/vish-chan/AlgoFlow
In the next post, I'll cover the gnarlier problems — how I got bytecode interception working on JDK bootstrap classes like ArrayList and HashMap, the auto-detection system that eliminates manual registration, and the war stories from debugging JVM stack manipulation. Stay tuned.

Top comments (0)