docs: 把能力面摆出来,更新性能数字

起因是「不知道有什么功能」——公开 API 有 14 个类型、17 个 With* 选项,全部平铺
在同一层文档里,看不出哪些是必须懂的。

doc.go 和 README 顶部都加了分层的能力清单:核心五个概念(Engine / Script /
Instance / Scope / Extension,覆盖九成用法),加上「用不到就不用看的」四类。
同时写清楚明确**不做**的三件事:不代管实例生命周期、没有事件循环、不提供沙箱隔离
(脚本能拿到你放行的 Go 对象,它们的方法是真能调的)。

性能数字重测了一遍。InstanceCall 454 → 300 ns——删掉 lastUsed 省下的两次
time.Now() 兑现了。

顺带加了一句测量方法的提醒:这些数字取的是多轮**最小值**。我这轮一度以为
Script.Call 退化了 27%,逐个 commit 二分下去发现跳变落在一个只删死代码和改注释
的 commit 上——那不可能影响调用路径。加大样本后两边最小值持平,是机器噪声。
中位数会被离群值带偏,微基准在有别的负载时能飘 30%。
This commit is contained in:
2026-09-10 15:44:19 +08:00
parent 7c9a70712e
commit 98eb734caa
2 changed files with 67 additions and 11 deletions
+40 -11
View File
@@ -15,6 +15,32 @@ resource.GetDBTable(user, req.WithPermission(req.ResAll))
.Select("name", "cost_price")
```
## 这个库能做什么
核心就五个概念,覆盖九成用法:
| | | 入口 |
| --- | --- | --- |
| **Engine** | 引擎。管编译缓存和配置,一个进程一个 | `jscriptx.New(...)` |
| **Script** | 一份编译好的脚本 + 它的 VM 池,热更新时整体顶替 | `e.Script(name)` |
| **Instance** | 独占一个 VM 的实例,脚本里 `this` 上的状态跨调用保持 | `s.New(ctx, args...)` |
| **Scope** | 作用域。让同一个 ctx 下的多个脚本共享 Go 侧对象 | `jscriptx.WithScope(ctx, ...)` |
| **Extension** | 扩展。给脚本添一个全局对象,并附上 TS 类型 | 实现 `Extension` 接口 |
用不到就不用看的:
| | | 入口 |
| --- | --- | --- |
| 脚本从别处来 | 默认从目录加载。要从数据库、配置中心取就实现 `Loader`;多来源叠加、后面盖前面用 `Overlay` | `Loader` / `Tag` / `Overlay` |
| 自己打包 | 只有一段源码、不走目录时用 | `Bundle` / `FinalizeBundle` |
| 自定义调用约定 | 本库不预设脚本回调该长什么样。要按形参个数分派、要传 Go 闭包当 next,用它 | `WithCall` / `Caller` |
| 观测与排错 | VM 池统计;错误带齐「哪个脚本、哪个函数、脚本里哪一行」 | `Stats` / `Error` |
| 装第三方库 | 拉依赖树、摊平成单文件,**目标机器不需要 node** | `esm.Install` / `esm.Vendor` |
明确**不做**的:不代管实例生命周期(没有按 key 复用、没有空闲回收)、没有事件循环
(定时器、网络、真正的异步都没有)、不提供沙箱隔离(脚本能拿到你放行的 Go 对象,
它们的方法是真能调的)。
## 文档
| | |
@@ -685,22 +711,25 @@ Apple M4 Pro`go test -run XXX -bench . -benchmem`。每行都标了对应的
| 场景 | 基准 | 耗时 | 分配 |
| --- | --- | --- | --- |
| 纯 Go 基准线 | `BenchmarkNative` | 96 ns | 1 |
| `Instance.Call`(状态在 JS 实例里) | `BenchmarkInstanceCall` | 454 ns | 10 |
| `Script.Call`(同脚本同参数,架构对照) | `BenchmarkCall_同脚本对照` | 417 ns | 10 |
| `Instance.Call` + 作用域扩展 | `BenchmarkStoreExtension` | 1.05 μs | 29 |
| `Script.Call` + 传 Go 对象当参数 | `BenchmarkCall` | 1.76 μs | 57 |
| 同上,但传可取消的 context | `BenchmarkCall_带可取消context` | 4.33 μs | 63 |
| `Script.New`(建一个实例) | `BenchmarkInstanceNew` | 3.49 μs | 110 |
| 重新编译 + 建全新 VM | `BenchmarkVM新建` | 224 μs | 1687 |
| 纯 Go 基准线 | `BenchmarkNative` | 39 ns | 1 |
| `Instance.Call`(状态在 JS 实例里) | `BenchmarkInstanceCall` | 300 ns | 11 |
| `Script.Call`(同脚本同参数,架构对照) | `BenchmarkCall_同脚本对照` | 342 ns | 11 |
| `Instance.Call` + 作用域扩展 | `BenchmarkStoreExtension` | 935 ns | 29 |
| `Script.Call` + 传 Go 对象当参数 | `BenchmarkCall` | 1.91 μs | 57 |
| 同上,但传可取消的 context | `BenchmarkCall_带可取消context` | 3.47 μs | 63 |
| `Script.New`(建一个实例) | `BenchmarkInstanceNew` | 3.00 μs | 110 |
| 重新编译 + 建全新 VM | `BenchmarkVM新建` | 213 μs | 1687 |
> 这些是多轮取**最小值**。微基准在有别的负载时能飘 30%,中位数会被离群值带偏——
> 拿它对比改动前后时务必同机器同口径跑两遍。
几点说明:
- **池化和独占 VM 的架构开销几乎一样**(417 vs 454 ns)。差别大的是状态放哪:放 JS 实例
只要 0.45 μs,放作用域扩展 1.05 μs,靠参数把 Go 对象反射包装过去要 1.76 μs——
- **池化和独占 VM 的架构开销几乎一样**(342 vs 300 ns)。差别大的是状态放哪:放 JS 实例
只要 0.3 μs,放作用域扩展 0.94 μs,靠参数把 Go 对象反射包装过去要 1.9 μs——
那个反射往返才是大头。
- 打包和编译只在**加载和热更新**时发生,不在调用路径上。
- 传可取消的 `context` 明显变贵(1.764.33 μs):哨兵要起 goroutine 盯 `ctx.Done()`
- 传可取消的 `context` 明显变贵(1.913.47 μs):哨兵要起 goroutine 盯 `ctx.Done()`
只有超时限制时走定时器快路径。MQTT 那种高频路径建议传 `context.Background()`
`WithTimeout` 兜底。