除了常规用例,有几组是专门钉住「引擎的既定事实」的,改动时会先红:
- vmstate_test goja 的硬限制:非原始值不是 goroutine 安全的、对象跨不了
Runtime。其中一条是哨兵——goja 哪天允许对象跨 Runtime 了,
它会失败,提醒我们可以简化设计。
- govalue_test Go 的切片/map 进到脚本里长什么样。这不是本库的行为而是
goja 的,但脚本作者照着它写代码,变了会静默走错分支。
- safety_test 并发下坏 VM 不会被别的 goroutine 捡到。
- bench_gobind 脚本碰 Go 对象的单次开销,README 性能一节的数据来源。
missing_global_test 里有一条断言 typeof setTimeout === "undefined":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
266 lines
6.2 KiB
Go
266 lines
6.2 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|