Files
jscriptx/bench_gobind_test.go
what 7f484d7a71 test: 引擎核心的测试与基准
除了常规用例,有几组是专门钉住「引擎的既定事实」的,改动时会先红:

  - 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":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
2026-09-05 22:13:57 +08:00

121 lines
3.2 KiB
Go

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{})
}