Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ go-polyscript democratizes different scripting engines by abstracting the loadin

## Engines Implemented

- **Risor**: A Python-like scripting language designed for embedding in Go applications
- **Risor**: A fast scripting language designed for embedding in Go applications
- **Starlark**: Google's deterministic configuration language (used in Bazel, and others)
- **Extism**: Pure Go runtime and plugin system for executing WASM

Expand Down Expand Up @@ -51,15 +51,15 @@ func main() {

script := `
// The ctx object from the Go inputData map
name := ctx.get("name")
let name = ctx.get("name")

p := "."
if ctx.get("excited") {
let p = "."
if (ctx.get("excited")) {
p = "!"
}
message := "Hello, " + name + p

let message = "Hello, " + name + p

// Return a map with our result
{
"greeting": message,
Expand Down
4 changes: 2 additions & 2 deletions engines/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ data := map[string]any{
}

// Risor script access
name := ctx["name"] // "World"
debug := ctx["config"]["debug"] // true
let name = ctx["name"] // "World"
let debug = ctx["config"]["debug"] // true
```

### Starlark Engine: `ctx` Context Wrapper
Expand Down
42 changes: 32 additions & 10 deletions engines/benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,29 @@ This directory contains benchmarking tools and historical benchmark records for

## Performance Optimization Patterns

The numbers below are from `BenchmarkEvaluationPatterns`, `BenchmarkDataProviders`, and `BenchmarkEngineComparison` on an Apple M5 Max (darwin/arm64) using the trivial greeting script in [`benchmark_test.go`](./benchmark_test.go). Your absolute numbers will differ, but the relative deltas are the signal. See [`results/latest.txt`](./results/latest.txt) for the authoritative most-recent run.

### 1. Compile Once, Run Many Times

The benchmarks show that pre-loading and parsing scripts is ~35% faster than recompiling for each execution:
Reusing a compiled evaluator is **~2.25x faster** than recompiling the script on every call, and allocates **~44% less memory** / **~65% fewer objects**:

| Pattern | ns/op | B/op | allocs/op |
|----------------------|--------:|--------:|----------:|
| SingleExecution | 48,323 | 120,751 | 713 |
| CompileOnceRunMany | 21,435 | 67,682 | 252 |

```go
// COMPILATION PHASE (expensive, do once)
evaluator, err := polyscript.FromRisorString(script, options...)

// EXECUTION PHASE (inexpensive, do many times)
// EXECUTION PHASE (cheap, do many times)
result1, _ := evaluator.Eval(ctx1)
result2, _ := evaluator.Eval(ctx2)
result3, _ := evaluator.Eval(ctx3)
```

The single-execution path pays parser + compiler + globals-validation cost per call. If you're running the same script more than once, create the evaluator once and hold onto it.

### 2. Data Preparation Separation

For distributed architectures, separate data preparation from evaluation to improve system architecture design:
Expand All @@ -32,19 +41,32 @@ result, _ := evaluator.Eval(enrichedCtx)

### 3. Provider Performance Comparison

The benchmarks show performance differences between `data.Provider` implementations:
On `BenchmarkDataProviders` (Risor engine), all three `data.Provider` implementations land within ~3.5% of each other — raw provider throughput is **not** a meaningful selection criterion. Choose based on data flow shape, not speed:

- **StaticProvider**: Fastest overall (~5-10% faster than other providers) - use when input data is static
- **ContextProvider**: Needed for request-specific data that varies per execution
- **CompositeProvider**: Small overhead but enables both static configuration and dynamic request data
| Provider | ns/op | B/op | allocs/op |
|--------------------|--------:|-------:|----------:|
| StaticProvider | 22,354 | 67,382 | 251 |
| ContextProvider | 21,604 | 66,283 | 243 |
| CompositeProvider | 21,966 | 67,382 | 253 |

- **StaticProvider** — data is fixed at evaluator creation (config, constants, feature flags).
- **ContextProvider** — data varies per-call; carried via `context.Context`.
- **CompositeProvider** — the backing store for the 2-step pattern (static config + dynamic per-request data).

### 4. Script Engine Performance Characteristics

Performance characteristics vary significantly by implementation:
On the same greeting script, `BenchmarkEngineComparison` shows Starlark is **~5.4x faster** than Risor for raw per-call overhead on small scripts, with **~8.6x less memory** and **~3.5x fewer allocations**:

| Engine | ns/op | B/op | allocs/op |
|-----------|--------:|-------:|----------:|
| Risor | 22,121 | 67,382 | 251 |
| Starlark | 4,119 | 7,806 | 71 |

Caveats: the benchmark script is trivial (two variables + a map literal), so this measures per-call VM setup more than real work. Interpret by use case, not by the numbers alone:

- **Risor**: Generally fastest for general-purpose scripting with good Go interoperability
- **Starlark**: Optimized for configuration processing with Python-like syntax
- **Extism/WASM**: Best for security isolation with pre-compiled modules
- **Starlark** — lowest per-call overhead; deterministic, Python-like, designed for configuration. Limited language capabilities (no loops as iteration, no stdlib I/O). Best when you have many fast, simple evaluations.
- **Risor** — richer stdlib (`math`, `rand`, `regexp` in v2), TypeScript-aligned syntax (arrow functions, `try/catch`, optional chaining), friendlier for general scripting and data munging. Pays a higher fixed per-call cost.
- **Extism/WASM** — language-agnostic isolation via pre-compiled modules. Choose when you need to run untrusted code, support multiple languages, or get true sandbox isolation.

## Running Benchmarks

Expand Down
22 changes: 11 additions & 11 deletions engines/benchmarks/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ var quietHandler = slog.NewJSONHandler(io.Discard, &slog.HandlerOptions{Level: s
func BenchmarkEvaluationPatterns(b *testing.B) {
// Simple script for benchmarking
scriptContent := `
name := ctx["name"]
message := "Hello, " + name + "!"
let name = ctx["name"]
let message = "Hello, " + name + "!"

{
"greeting": message,
"length": len(message)
Expand Down Expand Up @@ -110,9 +110,9 @@ func BenchmarkEvaluationPatterns(b *testing.B) {
func BenchmarkDataProviders(b *testing.B) {
// Simple script for benchmarking
scriptContent := `
name := ctx["name"]
message := "Hello, " + name + "!"
let name = ctx["name"]
let message = "Hello, " + name + "!"

{
"greeting": message,
"length": len(message)
Expand Down Expand Up @@ -168,8 +168,8 @@ func BenchmarkDataProviders(b *testing.B) {
// For CompositeProvider use case, we can prepare the context separately
// We access the name directly from the context
compositeScript := `
name := ctx["name"]
message := "Hello, " + name + "!"
let name = ctx["name"]
let message = "Hello, " + name + "!"
{
"greeting": message,
"length": len(message)
Expand Down Expand Up @@ -214,9 +214,9 @@ func BenchmarkEngineComparison(b *testing.B) {

// Risor script
risorScript := `
name := ctx["name"]
message := "Hello, " + name + "!"
let name = ctx["name"]
let message = "Hello, " + name + "!"

{
"greeting": message,
"length": len(message)
Expand Down
45 changes: 45 additions & 0 deletions engines/benchmarks/results/benchmark_2026-04-11_15-27-36.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{"Time":"2026-04-11T15:27:47.928167-04:00","Action":"start","Package":"github.com/robbyt/go-polyscript/engines/benchmarks"}
{"Time":"2026-04-11T15:27:47.93643-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Output":"goos: darwin\n"}
{"Time":"2026-04-11T15:27:47.93651-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Output":"goarch: arm64\n"}
{"Time":"2026-04-11T15:27:47.936515-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Output":"pkg: github.com/robbyt/go-polyscript/engines/benchmarks\n"}
{"Time":"2026-04-11T15:27:47.936522-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Output":"cpu: Apple M5 Max\n"}
{"Time":"2026-04-11T15:27:47.936529-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEvaluationPatterns"}
{"Time":"2026-04-11T15:27:47.936533-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEvaluationPatterns","Output":"=== RUN BenchmarkEvaluationPatterns\n"}
{"Time":"2026-04-11T15:27:47.936537-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEvaluationPatterns","Output":"BenchmarkEvaluationPatterns\n"}
{"Time":"2026-04-11T15:27:47.936661-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEvaluationPatterns/SingleExecution"}
{"Time":"2026-04-11T15:27:47.936678-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEvaluationPatterns/SingleExecution","Output":"=== RUN BenchmarkEvaluationPatterns/SingleExecution\n"}
{"Time":"2026-04-11T15:27:47.936684-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEvaluationPatterns/SingleExecution","Output":"BenchmarkEvaluationPatterns/SingleExecution\n"}
{"Time":"2026-04-11T15:27:49.62027-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEvaluationPatterns/SingleExecution","Output":"BenchmarkEvaluationPatterns/SingleExecution-18 \t 24763\t 47300 ns/op\t 120751 B/op\t 713 allocs/op\n"}
{"Time":"2026-04-11T15:27:49.620349-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEvaluationPatterns/CompileOnceRunMany"}
{"Time":"2026-04-11T15:27:49.620367-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEvaluationPatterns/CompileOnceRunMany","Output":"=== RUN BenchmarkEvaluationPatterns/CompileOnceRunMany\n"}
{"Time":"2026-04-11T15:27:49.620371-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEvaluationPatterns/CompileOnceRunMany","Output":"BenchmarkEvaluationPatterns/CompileOnceRunMany\n"}
{"Time":"2026-04-11T15:27:51.18472-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEvaluationPatterns/CompileOnceRunMany","Output":"BenchmarkEvaluationPatterns/CompileOnceRunMany-18 \t 59836\t 21618 ns/op\t 67682 B/op\t 252 allocs/op\n"}
{"Time":"2026-04-11T15:27:51.184786-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders"}
{"Time":"2026-04-11T15:27:51.1848-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders","Output":"=== RUN BenchmarkDataProviders\n"}
{"Time":"2026-04-11T15:27:51.184817-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders","Output":"BenchmarkDataProviders\n"}
{"Time":"2026-04-11T15:27:51.25881-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders/StaticProvider"}
{"Time":"2026-04-11T15:27:51.258855-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders/StaticProvider","Output":"=== RUN BenchmarkDataProviders/StaticProvider\n"}
{"Time":"2026-04-11T15:27:51.258882-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders/StaticProvider","Output":"BenchmarkDataProviders/StaticProvider\n"}
{"Time":"2026-04-11T15:27:52.772376-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders/StaticProvider","Output":"BenchmarkDataProviders/StaticProvider-18 \t 57315\t 21799 ns/op\t 67382 B/op\t 251 allocs/op\n"}
{"Time":"2026-04-11T15:27:52.772435-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders/ContextProvider"}
{"Time":"2026-04-11T15:27:52.77244-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders/ContextProvider","Output":"=== RUN BenchmarkDataProviders/ContextProvider\n"}
{"Time":"2026-04-11T15:27:52.772445-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders/ContextProvider","Output":"BenchmarkDataProviders/ContextProvider\n"}
{"Time":"2026-04-11T15:27:54.349074-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders/ContextProvider","Output":"BenchmarkDataProviders/ContextProvider-18 \t 59300\t 21286 ns/op\t 66278 B/op\t 243 allocs/op\n"}
{"Time":"2026-04-11T15:27:54.349117-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders/CompositeProvider"}
{"Time":"2026-04-11T15:27:54.349122-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders/CompositeProvider","Output":"=== RUN BenchmarkDataProviders/CompositeProvider\n"}
{"Time":"2026-04-11T15:27:54.349126-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders/CompositeProvider","Output":"BenchmarkDataProviders/CompositeProvider\n"}
{"Time":"2026-04-11T15:27:55.948097-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkDataProviders/CompositeProvider","Output":"BenchmarkDataProviders/CompositeProvider-18 \t 58492\t 21914 ns/op\t 67382 B/op\t 253 allocs/op\n"}
{"Time":"2026-04-11T15:27:55.948161-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEngineComparison"}
{"Time":"2026-04-11T15:27:55.948169-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEngineComparison","Output":"=== RUN BenchmarkEngineComparison\n"}
{"Time":"2026-04-11T15:27:55.948198-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEngineComparison","Output":"BenchmarkEngineComparison\n"}
{"Time":"2026-04-11T15:27:56.028991-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEngineComparison/RisorEngine"}
{"Time":"2026-04-11T15:27:56.029037-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEngineComparison/RisorEngine","Output":"=== RUN BenchmarkEngineComparison/RisorEngine\n"}
{"Time":"2026-04-11T15:27:56.029043-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEngineComparison/RisorEngine","Output":"BenchmarkEngineComparison/RisorEngine\n"}
{"Time":"2026-04-11T15:27:57.521117-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEngineComparison/RisorEngine","Output":"BenchmarkEngineComparison/RisorEngine-18 \t 55202\t 22296 ns/op\t 67382 B/op\t 251 allocs/op\n"}
{"Time":"2026-04-11T15:27:57.521171-04:00","Action":"run","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEngineComparison/StarlarkEngine"}
{"Time":"2026-04-11T15:27:57.521237-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEngineComparison/StarlarkEngine","Output":"=== RUN BenchmarkEngineComparison/StarlarkEngine\n"}
{"Time":"2026-04-11T15:27:57.521247-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEngineComparison/StarlarkEngine","Output":"BenchmarkEngineComparison/StarlarkEngine\n"}
{"Time":"2026-04-11T15:27:59.194638-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Test":"BenchmarkEngineComparison/StarlarkEngine","Output":"BenchmarkEngineComparison/StarlarkEngine-18 \t 351516\t 4261 ns/op\t 7808 B/op\t 71 allocs/op\n"}
{"Time":"2026-04-11T15:27:59.194849-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Output":"PASS\n"}
{"Time":"2026-04-11T15:27:59.225755-04:00","Action":"output","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Output":"ok \tgithub.com/robbyt/go-polyscript/engines/benchmarks\t11.297s\n"}
{"Time":"2026-04-11T15:27:59.225792-04:00","Action":"pass","Package":"github.com/robbyt/go-polyscript/engines/benchmarks","Elapsed":11.298}
13 changes: 13 additions & 0 deletions engines/benchmarks/results/benchmark_2026-04-11_15-27-36.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
goos: darwin
goarch: arm64
pkg: github.com/robbyt/go-polyscript/engines/benchmarks
cpu: Apple M5 Max
BenchmarkEvaluationPatterns/SingleExecution-18 24620 48323 ns/op 120751 B/op 713 allocs/op
BenchmarkEvaluationPatterns/CompileOnceRunMany-18 59073 21435 ns/op 67682 B/op 252 allocs/op
BenchmarkDataProviders/StaticProvider-18 56025 22354 ns/op 67382 B/op 251 allocs/op
BenchmarkDataProviders/ContextProvider-18 59650 21604 ns/op 66283 B/op 243 allocs/op
BenchmarkDataProviders/CompositeProvider-18 58615 21966 ns/op 67382 B/op 253 allocs/op
BenchmarkEngineComparison/RisorEngine-18 55406 22121 ns/op 67382 B/op 251 allocs/op
BenchmarkEngineComparison/StarlarkEngine-18 338401 4119 ns/op 7806 B/op 71 allocs/op
PASS
ok github.com/robbyt/go-polyscript/engines/benchmarks 11.202s
2 changes: 1 addition & 1 deletion engines/benchmarks/results/latest.txt
Loading
Loading