Skip to content

feature: Change Plush to VM Bytecode Compiler #223

Description

@Mido-sys

Description

Plush VM Benchmark Description

Overview

This benchmark compares two execution strategies for the same Plush template:

  • Tree-walker evaluator path
  • VM path

The template includes:

  • Variable interpolation
  • Conditional rendering
  • Loop rendering

Benchmark Template

const testTemplate = `Hello <%= name %>!
<% if (admin) { %><b>Admin</b><% } %>
<%= for (i, v) in items { %><%= v %> <% } %>`

// ── current tree-walker path ──────────────────────────────────────────────────

func BenchmarkTreeWalker(b *testing.B) {
	// --- parse once (mirrors vm.Compile) ---
	t, err := plush.NewTemplate(testTemplate)
	if err != nil {
		b.Fatal(err)
	}

	ctx := plush.NewContext()
	ctx.Set("name", "World")
	ctx.Set("admin", true)
	ctx.Set("items", []string{"a", "b", "c"})

	cachedAST := t.Program
	if cachedAST == nil {
		b.Fatal("parsed AST is nil")
	}
	if err := t.Parse(); err != nil {
		b.Fatal(err)
	}
	if t.Program != cachedAST {
		b.Fatal("AST was reparsed; expected cached program")
	}

	b.ResetTimer()
	// --- tree-walk every request ---
	for i := 0; i < b.N; i++ {
		_, _, err := t.Exec(ctx)
		if err != nil {
			b.Fatal(err)
		}
	}
}

// ── new threaded-interpreter path ─────────────────────────────────────────────
//
// Compile once (simulating what would be stored in the cache),
// then only Execute on each request.

func BenchmarkThreadedInterpreter(b *testing.B) {
	// --- compile once ---
	program, err := parser.Parse(testTemplate)
	if err != nil {
		b.Fatal(err)
	}
	compiled, err := vm.Compile(program)
	if err != nil {
		b.Fatal(err)
	}

	ctx := plush.NewContext()
	ctx.Set("name", "World")
	ctx.Set("admin", true)
	ctx.Set("items", []string{"a", "b", "c"})

	b.ResetTimer()
	// --- execute many times ---
	for i := 0; i < b.N; i++ {
		_, _, err := vm.Execute(compiled, ctx)
		if err != nil {
			b.Fatal(err)
		}
	}
}

What Each Benchmark Measures

BenchmarkTreeWalker

  • Parses template once via NewTemplate
  • Builds a shared context (name, admin, items)
  • Verifies AST reuse by checking Program pointer stability
  • Executes evaluator tree-walk on each iteration with Exec(ctx)

Why this matters:

  • Confirms parsing is cached and not repeated in the hot loop
  • Isolates runtime cost of evaluator execution

BenchmarkThreadedInterpreter

  • Parses source once into AST
  • Compiles AST once into VM instruction program
  • Builds equivalent context (name, admin, items)
  • Executes precompiled instruction stream each iteration

Why this matters:

  • Measures steady-state runtime when compile cost is paid once
  • Reflects cache-friendly production behavior for repeated renders

Fairness Notes

  • Both paths use the same template and context data
  • Both paths do one-time setup before timing
  • Timer starts only after parse/compile setup is complete
  • Hot loop measures render execution only

Why VM Is Faster (Summary)

  • Flat instruction dispatch avoids repeated AST traversal
  • Fewer per-node runtime checks during render
  • Lower allocation pressure in hot path
  • Better branch and cache locality in execution loop

Expected Outcome

The VM benchmark should show:

  • Lower ns/op
  • Fewer allocs/op
  • Lower B/op

##Results:

Plush VM Benchmark Summary

We benchmarked BenchmarkTreeWalker vs BenchmarkThreadedInterpreter using:

go test ./vm -run '^$' -bench 'Benchmark(TreeWalker|ThreadedInterpreter)$' -benchmem -count=6

Environment:

  • OS: Linux
  • Arch: amd64
  • Package: github.com/gobuffalo/plush/v5/vm

Average results across 6 runs:

Metric TreeWalker ThreadedInterpreter Improvement
ns/op 3397.17 1175.50 2.89x faster
B/op 1297 376 71.01% less
allocs/op 44 14 68.18% less

Key takeaway:

The VM Compiler is significantly faster and more memory efficient than the current tree-walker evaluator for this template workload.

Because both benchmarks exclude parse and compile time from the hot loop, the result demonstrates the benefit of a compile-once, execute-many rendering model. In this benchmark, the VM path reduces execution time by roughly 65% and substantially reduces allocation pressure.

This provides strong proof of concept that Plush can benefit meaningfully from a VM-based execution path, especially for cached templates that are rendered repeatedly in production.

Additional Information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions