除了常规用例,有几组是专门钉住「引擎的既定事实」的,改动时会先红:
- 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":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
121 lines
3.2 KiB
Go
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{})
|
|
}
|