package jscriptx_test import ( "context" "fmt" "testing" "git.fsdpf.net/go/jscriptx" ) // 脚本碰 Go 对象要付多少钱。 // // 每个用例在 JS 里循环 1000 次,所以报出来的 ns/op 和 allocs/op 是**1000 次访问** // 的总量,除以 1000 才是单次。用同一个循环次数是为了让几种写法可以直接相比。 // // 这组数字是 internal/jscript/README.md 里"绑定的 Go 方法要多少钱"那节的来源, // 改了绑定实现(比如 engine.go 的 freeze / lazyGlobal)之后要重新跑。 // benchRow 冒充一行数据库记录:有导出字段,也有方法。 type benchRow struct { Name string Content string } func (r *benchRow) Get(key string) string { if key == "name" { return r.Name } return r.Content } func benchScript(b *testing.B, body string, globals map[string]any) { b.Helper() e, err := jscriptx.New(jscriptx.WithLogger(nil)) if err != nil { b.Fatal(err) } defer e.Close() s, err := e.Compile("bench.ts", fmt.Sprintf(` export default class C { Run() { let sink = 0 for (let i = 0; i < 1000; i++) { %s } return sink } }`, body)) if err != nil { b.Fatal(err) } ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeGlobals(globals)) obj, err := s.New(ctx) if err != nil { b.Fatal(err) } defer obj.Close() // 先跑一次把 VM 预热,别把首次编译算进去 if _, err := obj.Call(ctx, "Run"); err != nil { b.Fatal(err) } b.ReportAllocs() b.ResetTimer() for b.Loop() { if _, err := obj.Call(ctx, "Run"); err != nil { b.Fatal(err) } } } // 基准线:纯 JS 对象的属性访问。 func Benchmark绑定_纯JS属性(b *testing.B) { benchScript(b, `sink += row.name.length`, map[string]any{ "row": map[string]any{"name": "第一行", "content": "内容"}, }) } // Go 结构体的导出字段。每次访问都要经过 reflect 取值并包成 goja 值。 func Benchmark绑定_Go结构体字段(b *testing.B) { benchScript(b, `sink += row.Name.length`, map[string]any{ "row": &benchRow{Name: "第一行", Content: "内容"}, }) } // Go 方法调用。除了取值,每次还要现造一个函数包装。 func Benchmark绑定_Go方法调用(b *testing.B) { benchScript(b, `sink += row.Get("name").length`, map[string]any{ "row": &benchRow{Name: "第一行", Content: "内容"}, }) } // 同样是调 Go 方法,但循环外先取出来存进局部变量。 // // 省掉的是**每次重新造函数包装**那部分——这是写业务脚本时最容易拿到的优化。 func Benchmark绑定_Go方法提前取出(b *testing.B) { benchScript(b, `if (i === 0) { var get = row.Get.bind(row) } sink += get("name").length`, map[string]any{ "row": &benchRow{Name: "第一行", Content: "内容"}, }) } // 绑定的是普通 Go 函数(扩展的 Bindings 就是这种),不是对象上的方法。 func Benchmark绑定_Go函数(b *testing.B) { benchScript(b, `sink += get("name").length`, map[string]any{ "get": func(key string) string { if key == "name" { return "第一行" } return "内容" }, }) } // 对照:什么都不碰,只跑循环本身。上面几个都要减掉这一项才是净开销。 func Benchmark绑定_空循环(b *testing.B) { benchScript(b, `sink += i`, map[string]any{}) }