package jscriptx_test import ( "context" "testing" "time" "git.fsdpf.net/go/jscriptx" "git.fsdpf.net/go/jscriptx/internal/testext" ) func benchEngine(b *testing.B, opts ...jscriptx.Option) *jscriptx.Engine { b.Helper() opts = append([]jscriptx.Option{jscriptx.WithLogger(nil)}, opts...) e, err := jscriptx.New(opts...) if err != nil { b.Fatal(err) } b.Cleanup(e.Close) return e } // 纯 Go 基准线:跟下面脚本版做同样的事。 func BenchmarkNative(b *testing.B) { state := testext.New() onMessage := func(s *testext.Store, payload string) { s.Set("last", payload) s.Incr("count", 1) } b.ResetTimer() for i := 0; i < b.N; i++ { onMessage(state, "payload") } } // 单次脚本调用的开销(VM 池命中,对应生产上的常态)。 func BenchmarkCall(b *testing.B) { e := benchEngine(b) s, err := e.Compile("bench", ` export function onMessage(state, payload) { state.Set("last", payload) state.Incr("count", 1) } `) if err != nil { b.Fatal(err) } state := testext.New() ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { if _, err := s.Call(ctx, "onMessage", state, "payload"); err != nil { b.Fatal(err) } } } // 关掉超时哨兵的开销对比:哨兵每次调用要起一个 goroutine,这里量化它值多少。 func BenchmarkCall_无超时哨兵(b *testing.B) { e := benchEngine(b, jscriptx.WithTimeout(0)) s, err := e.Compile("bench", ` export function onMessage(state, payload) { state.Set("last", payload) state.Incr("count", 1) } `) if err != nil { b.Fatal(err) } state := testext.New() ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { if _, err := s.Call(ctx, "onMessage", state, "payload"); err != nil { b.Fatal(err) } } } // 并发调用,验证 VM 池在多核下的表现。 func BenchmarkCall_并发(b *testing.B) { e := benchEngine(b) s, err := e.Compile("bench", ` export function onMessage(state, payload) { state.Set("last", payload) state.Incr("count", 1) } `) if err != nil { b.Fatal(err) } ctx := context.Background() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { state := testext.New() for pb.Next() { if _, err := s.Call(ctx, "onMessage", state, "payload"); err != nil { b.Fatal(err) } } }) } // 池化的价值:每次都新建 VM 要多花多少。用容量 1 的池加并发制造"取不到就新建"的场景太绕, // 这里直接对比"池命中"和"强制重建"两种路径。 func BenchmarkVM新建(b *testing.B) { e := benchEngine(b, jscriptx.WithMaxVMs(1), jscriptx.WithTimeout(time.Minute)) src := ` export function onMessage(state, payload) { state.Set("last", payload) state.Incr("count", 1) } ` state := testext.New() ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { s, err := e.Compile("bench", src) // 每次重新编译 + 全新 VM 池 if err != nil { b.Fatal(err) } if _, err := s.Call(ctx, "onMessage", state, "payload"); err != nil { b.Fatal(err) } } } // 调用方传了可取消的 context 时,哨兵要起 goroutine 盯着,比只有超时的快路径贵一些。 // HTTP 请求级的回调走的是这条路,这点开销相对请求本身可以忽略。 func BenchmarkCall_带可取消context(b *testing.B) { e := benchEngine(b) s, err := e.Compile("bench", ` export function onMessage(state, payload) { state.Set("last", payload) state.Incr("count", 1) } `) if err != nil { b.Fatal(err) } state := testext.New() ctx, cancel := context.WithCancel(context.Background()) defer cancel() b.ResetTimer() for i := 0; i < b.N; i++ { if _, err := s.Call(ctx, "onMessage", state, "payload"); err != nil { b.Fatal(err) } } } // new 一个实例要多少钱(决定了能开多少个对象) func BenchmarkInstanceNew(b *testing.B) { e := benchEngine(b) s, err := e.Compile("bench", ` export default class H { constructor(id) { this.id = id; this.count = 0 } onMessage(payload) { this.count++ } } `) if err != nil { b.Fatal(err) } ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { obj, err := s.New(ctx, "dev") if err != nil { b.Fatal(err) } obj.Close() } } // 作用域里的 store vs 参数传 State:同样的业务,两种写法的开销对比 func BenchmarkStoreExtension(b *testing.B) { e, err := jscriptx.New(jscriptx.WithLogger(nil)) if err != nil { b.Fatal(err) } defer e.Close() s, err := e.Compile("bench.ts", ` export default class H { onMessage(payload) { store.Set("last", payload) store.Incr("count", 1) } } `) if err != nil { b.Fatal(err) } ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(testext.New())) obj, err := s.New(ctx) if err != nil { b.Fatal(err) } defer obj.Close() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { if _, err := obj.Call(ctx, "onMessage", "payload"); err != nil { b.Fatal(err) } } } // 跟 BenchmarkInstanceCall 用完全一样的脚本和参数,唯一差别是走 VM 池而不是独占实例。 // 用来隔离出"池借还 vs 实例加锁"的纯架构开销(这个脚本在池化下状态会丢,只作性能对照)。 func BenchmarkCall_同脚本对照(b *testing.B) { e := benchEngine(b) s, err := e.Compile("bench", ` export default class H { constructor() { this.count = 0 } onMessage(payload) { this.count++; return this.count } } `) if err != nil { b.Fatal(err) } ctx := context.Background() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { if _, err := s.Call(ctx, "onMessage", "payload"); err != nil { b.Fatal(err) } } } // 独占实例的单次调用开销:状态在 JS 实例里,不用每次把 Go 对象反射包装过去。 func BenchmarkInstanceCall(b *testing.B) { e := benchEngine(b) s, err := e.Compile("bench", ` export default class H { constructor() { this.count = 0 } onMessage(payload) { this.count++; return this.count } } `) if err != nil { b.Fatal(err) } ctx := context.Background() obj, err := s.New(ctx) if err != nil { b.Fatal(err) } defer obj.Close() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { if _, err := obj.Call(ctx, "onMessage", "payload"); err != nil { b.Fatal(err) } } }