expect-pty 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +41 -4
- data/CHANGELOG.md +14 -0
- data/CONTRIBUTING.md +23 -0
- data/Gemfile +2 -1
- data/README.md +23 -6
- data/benchmark/matching.rb +72 -0
- data/benchmark/relay.rb +48 -0
- data/benchmark/send_slow.rb +41 -0
- data/benchmark/support.rb +94 -0
- data/docs/COMPATIBILITY.md +1 -1
- data/docs/INTERNAL_CONTRACTS.md +65 -0
- data/docs/PERFORMANCE.md +60 -0
- data/docs/RELEASING.md +8 -6
- data/docs/VERIFICATION.md +24 -0
- data/examples/kibitz/kibitz.rb +2 -0
- data/expect-pty.gemspec +2 -1
- data/lib/expect/interaction.rb +13 -6
- data/lib/expect/logging.rb +96 -0
- data/lib/expect/matcher.rb +26 -5
- data/lib/expect/pattern.rb +9 -5
- data/lib/expect/relay.rb +3 -1
- data/lib/expect/relay_writer.rb +2 -0
- data/lib/expect/session_resources.rb +29 -11
- data/lib/expect/terminal.rb +35 -0
- data/lib/expect/version.rb +1 -1
- data/lib/expect.rb +85 -145
- data/script/ci +5 -0
- data/script/release.rb +37 -5
- data/test/cleanup_test.rb +251 -0
- data/test/configuration_test.rb +43 -0
- data/test/io_test.rb +35 -0
- data/test/pattern_offset_test.rb +43 -0
- data/test/process_test.rb +46 -0
- data/test/relay_recovery_test.rb +37 -0
- data/test/release_test.rb +94 -16
- data/test/scan_reuse_test.rb +109 -0
- data/test/timeout_test.rb +119 -0
- metadata +15 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e21e96f8c4a79123cb7eb74606f0b0a8195c855290ba5ca97b43419917b1e36
|
|
4
|
+
data.tar.gz: 5f77fab495dd5d5b10e91e14a6957d3907c261cd9931dd73d1113827c1eabc74
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6334c31be027acef464d2d648dec242e43eff2367076a149c64a64a88edd27da2d51a65f3e85afcf2e8d15b2c41439c496b75731cc26db8e816d1fd3296076b
|
|
7
|
+
data.tar.gz: 1437a5c147513aa572b8b0430d8f25f0ed526ba6b14309f261f0ef30e1f0fa7e2b4850c4abd8a6a35f0988307f204870f31961c26db77245a6d35f986e34babe
|
data/.rubocop.yml
CHANGED
|
@@ -7,10 +7,47 @@ AllCops:
|
|
|
7
7
|
- "tmp/**/*"
|
|
8
8
|
- "vendor/**/*"
|
|
9
9
|
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
Metrics:
|
|
13
|
-
|
|
10
|
+
# 宽松但有限的规模门槛覆盖生产代码;测试和基准的场景编排不按方法长度及 ABC 计数。
|
|
11
|
+
# 需要更高分支复杂度的协议状态机仅在对应方法说明豁免原因。
|
|
12
|
+
Metrics/AbcSize:
|
|
13
|
+
Max: 120
|
|
14
|
+
Exclude:
|
|
15
|
+
- "test/**/*"
|
|
16
|
+
- "benchmark/**/*"
|
|
17
|
+
|
|
18
|
+
Metrics/BlockLength:
|
|
19
|
+
Max: 80
|
|
20
|
+
|
|
21
|
+
Metrics/BlockNesting:
|
|
22
|
+
Max: 4
|
|
23
|
+
Exclude:
|
|
24
|
+
- "test/**/*"
|
|
25
|
+
|
|
26
|
+
Metrics/ClassLength:
|
|
27
|
+
Max: 500
|
|
28
|
+
|
|
29
|
+
Metrics/MethodLength:
|
|
30
|
+
Max: 80
|
|
31
|
+
Exclude:
|
|
32
|
+
- "test/**/*"
|
|
33
|
+
- "benchmark/**/*"
|
|
34
|
+
|
|
35
|
+
Metrics/ModuleLength:
|
|
36
|
+
Max: 250
|
|
37
|
+
|
|
38
|
+
Metrics/ParameterLists:
|
|
39
|
+
Max: 6
|
|
40
|
+
CountKeywordArgs: false
|
|
41
|
+
|
|
42
|
+
Metrics/CyclomaticComplexity:
|
|
43
|
+
Max: 13
|
|
44
|
+
Exclude:
|
|
45
|
+
- "test/**/*"
|
|
46
|
+
|
|
47
|
+
Metrics/PerceivedComplexity:
|
|
48
|
+
Max: 13
|
|
49
|
+
Exclude:
|
|
50
|
+
- "test/**/*"
|
|
14
51
|
|
|
15
52
|
Style/StringLiterals:
|
|
16
53
|
EnforcedStyle: double_quotes
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.3 - 2026-09-27
|
|
6
|
+
|
|
7
|
+
- **生命周期**:构造及工厂启动失败时统一释放所属 IO 并回收子进程;块或软关闭的原始异常不再被后续 IO 清理错误覆盖,失败的单个句柄不会阻止其他资源收尾。
|
|
8
|
+
- **匹配期限**:EOF 回调保留原期限继续时,不再于期限过后消费其他来源的文本匹配;已知 EOF 仍依次派发,所有来源结束时保持 EOF 结果。
|
|
9
|
+
- **正则性能**:只在需要调整编码时复制输入缓冲,减少大缓冲区多模式扫描的耗时和分配,保持编码及字节偏移语义。
|
|
10
|
+
- **终端错误**:系统缺少 `stty` 时提供明确的 IO 错误和安装提示,并保留原始异常。
|
|
11
|
+
- **发布校验**:拒绝发布与目标 Git 提交的内容、执行权限或包元数据不一致的 Gem,提交前仍可通过 dry-run 验证候选包。
|
|
12
|
+
|
|
13
|
+
## 0.3.2 - 2026-09-26
|
|
14
|
+
|
|
15
|
+
- **匹配与转接**:正则直接使用字节偏移,重复来源在单轮扫描中共享缓冲快照;转义正则复用本轮组合文本,保留回调修改规则后的重新扫描语义。`send_slow` 只读取已经可读的回复。
|
|
16
|
+
- **配置与清理**:并发 `Expect.configure` 串行发布,避免丢失更新;关闭句柄或日志失败时继续清理其他资源和直属子进程,并保留首个清理错误。转接读取显式保留未交付缓冲。
|
|
17
|
+
- **维护与验证**:增加状态归属文档、性能基准及 smoke、分支复杂度检查、贡献指南和安全说明;补充匹配、转接、清理及并发配置回归测试。
|
|
18
|
+
|
|
5
19
|
## 0.3.1 - 2026-09-26
|
|
6
20
|
|
|
7
21
|
- **安装依赖**:显式声明五个运行时标准库 gem,开发工具依赖单独管理,并补齐应用 Gemfile 中的 `require: "expect/pty"` 用法。
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 参与贡献
|
|
2
|
+
|
|
3
|
+
欢迎中文或英文 issue 和 PR。库的公共 API 名称和错误信息使用英文;库内的责任划分、复杂生命周期和协议解释使用中文,RuboCop 等工具指令保持原格式。文档以现有文件的语言为准,示例中的可编辑命令保持直接清晰。
|
|
4
|
+
|
|
5
|
+
## 本地检查
|
|
6
|
+
|
|
7
|
+
需要 Ruby 3.2+ 和 Linux/macOS。安装依赖后运行:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bundle install
|
|
11
|
+
bundle exec rake # RuboCop 和默认测试
|
|
12
|
+
script/ci # 另含示例、Gem 构建及隔离安装验证
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
修改转接或匹配时,先运行对应的 `test/*_test.rb`,再运行 `bundle exec rake`。默认测试不需要 SSH;真实 SSH 集成测试的环境要求见 [测试说明](test/integration/README.md)。发布操作和历史验证分别见 [发布说明](docs/RELEASING.md) 与 [验证记录](docs/VERIFICATION.md)。
|
|
16
|
+
|
|
17
|
+
RuboCop 对生产代码保留有限的长度、ABC、嵌套、参数数量和分支复杂度门槛。测试及基准的场景编排豁免方法长度与 ABC 计数;协议状态机的分支豁免只放在对应方法,并注明原因。阈值用于提示复核,职责拆分仍应保持状态归属和异常恢复逻辑完整。
|
|
18
|
+
|
|
19
|
+
## 提交问题或改动
|
|
20
|
+
|
|
21
|
+
问题报告请附 Ruby 版本、操作系统、最小复现代码、预期与实际结果,以及必要的异常信息;日志中请删去凭据和会话敏感内容。修复行为缺陷时,先加入能在旧实现复现问题的回归测试。涉及缓冲、转义、背压或进程清理的改动,请对照 [内部状态与数据归属](docs/INTERNAL_CONTRACTS.md),说明超时、EOF 和失败后的恢复行为。
|
|
22
|
+
|
|
23
|
+
PR 请说明行为变化、运行过的命令及结果。不要把本机生成的 `tmp/`、日志或凭据加入提交。
|
data/Gemfile
CHANGED
|
@@ -8,7 +8,8 @@ group :development, :test do
|
|
|
8
8
|
# RuboCop 的依赖也必须支持最低 Ruby 版本。
|
|
9
9
|
gem "parallel", "~> 1.27", require: false
|
|
10
10
|
gem "rake", "~> 13.0", require: false
|
|
11
|
-
|
|
11
|
+
# 固定已审阅的规则集,避免不同 CI 环境自动启用新 cop 改变发布门槛。
|
|
12
|
+
gem "rubocop", "= 1.89.0", require: false
|
|
12
13
|
|
|
13
14
|
# 测试、示例和发布工具直接使用的标准库 gem,不依赖其他开发工具间接引入。
|
|
14
15
|
gem "digest", require: false
|
data/README.md
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
要求 **Ruby 3.2+、POSIX 系统(Linux/macOS)**。运行时仅使用 Ruby 标准库,其中可独立安装的 gem 已在 gemspec 中声明,由 RubyGems/Bundler 解析。推荐入口 **`require "expect/pty"`**;本项目提供独立的 `Expect` 类,不修改标准库的 `IO#expect`。
|
|
8
8
|
|
|
9
|
+
源码中的解释性注释主要使用中文;欢迎用中文或英文提交 issue 和 PR,参与方式见 [贡献指南](CONTRIBUTING.md)。
|
|
10
|
+
|
|
9
11
|
## 安装和运行
|
|
10
12
|
|
|
11
13
|
项目和仓库名为 `expect-ruby`,Gem 名为 `expect-pty`。在应用的 Gemfile 中添加以下内容,然后运行 `bundle install`:
|
|
12
14
|
|
|
13
15
|
```ruby
|
|
14
|
-
gem "expect-pty", "~> 0.3.
|
|
16
|
+
gem "expect-pty", "~> 0.3.3", require: "expect/pty"
|
|
15
17
|
```
|
|
16
18
|
|
|
17
19
|
也可直接执行 `gem install expect-pty`。需要跟随开发分支时,可从 GitHub 安装:
|
|
@@ -24,8 +26,8 @@ gem "expect-pty", git: "https://github.com/gatework/expect-ruby.git", branch: "m
|
|
|
24
26
|
|
|
25
27
|
```sh
|
|
26
28
|
mkdir -p tmp
|
|
27
|
-
gem build expect-pty.gemspec --output tmp/expect-pty-0.3.
|
|
28
|
-
gem install ./tmp/expect-pty-0.3.
|
|
29
|
+
gem build expect-pty.gemspec --output tmp/expect-pty-0.3.3.gem
|
|
30
|
+
gem install ./tmp/expect-pty-0.3.3.gem
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
```ruby
|
|
@@ -43,7 +45,7 @@ Expect.spawn("/bin/sh", "-i") do |shell|
|
|
|
43
45
|
end
|
|
44
46
|
```
|
|
45
47
|
|
|
46
|
-
块返回其执行结果,退出时关闭会话并回收子进程,异常和 `break`
|
|
48
|
+
块返回其执行结果,退出时关闭会话并回收子进程,异常和 `break` 也执行清理。构造或启动失败时同样释放已创建的资源;清理中的 IO 错误不会替换正在传播的原始异常。无块形式需用 `ensure` 显式调用 `close`。`Expect.new` 可以先创建 PTY、设置 `slave.echo` / `slave.winsize`,然后调用实例的 `spawn`。
|
|
47
49
|
|
|
48
50
|
多个命令参数原样传给 Ruby `exec`;单个命令字符串使用 Ruby 的 shell 语义。不可信参数应使用独立参数形式。支持 `env: { "NAME" => "value" }` 和 `chdir: "/path"`。同一会话只能启动一次,启动失败抛出 `Expect::SpawnError`。
|
|
49
51
|
|
|
@@ -67,7 +69,9 @@ Expect.spawn("/bin/sh", "-i", timeout: 3) do |session|
|
|
|
67
69
|
end
|
|
68
70
|
```
|
|
69
71
|
|
|
70
|
-
`configure`
|
|
72
|
+
`configure` 校验后发布冻结的配置对象;块异常不会发布部分修改。并发调用按顺序完成读改写,不会互相覆盖不同属性。配置块在锁内执行,应保持简短,不要在块内再次调用 `configure`(会抛出 `ThreadError`)或等待其他配置线程。每个会话独立持有配置,优先使用构造参数;修改默认值不会改变已有会话,修改一个会话也不会影响其他会话。子类继承父类默认配置,可独立覆盖。
|
|
73
|
+
|
|
74
|
+
全局默认配置通常在应用启动时设置;每次会话的动态差异使用构造参数或会话属性,避免在高频路径反复获取共享配置锁。
|
|
71
75
|
|
|
72
76
|
| 属性 | 默认值 | 行为 |
|
|
73
77
|
| --- | --- | --- |
|
|
@@ -141,7 +145,7 @@ end
|
|
|
141
145
|
|
|
142
146
|
回调通过闭包访问局部变量。无参数声明块在模式构建器中执行;希望保留调用方 `self` 时使用 `do |patterns|`,调用 `patterns.on(...)`。所有模式注册完成后才读取 IO;注册异常或 `break` 不消费输入。块和位置模式不能混用。`expect_result` 支持同样的声明方式。
|
|
143
147
|
|
|
144
|
-
`continue` 继续等待并重新计时;`continue(reset_timeout: false)`
|
|
148
|
+
`continue` 继续等待并重新计时;`continue(reset_timeout: false)` 保留原期限,类和实例均可调用。回调返回后若保留的期限已过,不再扫描新的文本匹配,未消费的输入留给下一次等待。无回调或返回其他值时结束本次匹配。超时回调只有返回重置计时的 `continue` 才再次等待。EOF 回调继续时移除该源并等待其余会话;已知的 EOF 仍依次派发,全部 EOF 时直接返回,期限已过时仅对剩余活跃源触发超时。
|
|
145
149
|
|
|
146
150
|
`eof` / `timeout` 声明会占用模式序号,但事件返回的 `number` 为 `nil`。一个等待只能注册一个超时回调;它接收**所有仍在监听的会话**。不需要回调时,可将 `:eof` / `:timeout` 作为位置事件参数。
|
|
147
151
|
|
|
@@ -187,8 +191,12 @@ ready = Expect.readable_sessions(first, second, timeout: 5)
|
|
|
187
191
|
| `winsize` / `winsize=` | 读取/修改 `[rows, cols]`,由内核通知前台进程 |
|
|
188
192
|
| `slave` / `tty_name` / `to_io` / `writer` / `fileno` / `tty?` | 底层 IO 和终端信息 |
|
|
189
193
|
|
|
194
|
+
`send_slow` 在每次写入后只检查已经可读的回复,不附加固定等待;返回时不保证收齐最后一个字符引发的回复,完整对话请继续使用 `expect`。
|
|
195
|
+
|
|
190
196
|
大块写入遇到背压时同时读取输出,避免双向传输互相阻塞。超过 `write_timeout` 抛出 `Expect::WriteTimeout`,`error.bytes_written` 给出本次 `write` 已被底层接受的字节数;这些字节不回滚,不要从头重发整个命令。写入、等待和背压读取中的 `EINTR` 均保留原期限重试。控制字符可直接发送,例如 `session.write("\x03")`,其信号作用取决于终端设置。`send`、`public_send`、`__send__` 保留 Ruby 反射语义。
|
|
191
197
|
|
|
198
|
+
`stty` 需要系统命令位于 `PATH`;缺失时抛出带安装提示的 `IOError`,原始 `Errno::ENOENT` 保留在 `cause`。窗口尺寸和人工接管的终端恢复使用 Ruby `io/console`。
|
|
199
|
+
|
|
192
200
|
## 日志与人工交互
|
|
193
201
|
|
|
194
202
|
```ruby
|
|
@@ -206,6 +214,8 @@ session.log_listeners = false
|
|
|
206
214
|
|
|
207
215
|
所有会话默认不输出到 stdout。日志仅记录实际读取的接收字节;写入不重复记录,终端回显可能作为接收内容返回。密码交互应关闭日志、调试,并确保被控程序不回显密码。
|
|
208
216
|
|
|
217
|
+
普通 `expect` 按顺序同步写入日志、stdout 和 `listeners`,这些目标须及时消费数据;匹配的 `timeout` 不会中断阻塞中的输出。需要在慢目标背压时继续处理其他输入,应使用下面的 `interconnect` 非阻塞转接接口并设置期限。
|
|
218
|
+
|
|
209
219
|
```ruby
|
|
210
220
|
session.interact(input: $stdin, escape: "\x1d", output: $stdout) # Ctrl-]
|
|
211
221
|
|
|
@@ -249,6 +259,13 @@ session.close(graceful: true) # 先软关闭,必要时继续硬关闭
|
|
|
249
259
|
|
|
250
260
|
关闭只负责会话直接启动的子进程;垃圾回收提供非阻塞的强制清理兜底,不执行软关闭等待。优先使用块或 `ensure` 管理资源。
|
|
251
261
|
|
|
262
|
+
## 安全注意事项
|
|
263
|
+
|
|
264
|
+
- 将不可信命令和参数分别传给 `spawn`,例如 `Expect.spawn("ssh", host)`;单个命令字符串会使用 Ruby 的 shell 语义。`stty` 参数经拆分后作为独立参数传给进程,不拼接 shell 命令。
|
|
265
|
+
- 会话日志可能记录密码回显、令牌和其他敏感字节。新日志文件以 `0600` 创建,已有文件保留原权限;请按需关闭 `log_to`、`log_stdout` 和调试输出,并管理日志留存。
|
|
266
|
+
- `spawn` 在子进程中使用 `fork` 后的 Ruby 操作与 `exec`。高度多线程的宿主进程,尤其使用第三方 C 扩展时,可能受到 fork 时其他线程持锁的影响;尽量在启动其他线程前创建会话,并在自己的运行环境中验证。
|
|
267
|
+
- 不可信正则可能耗费较长时间;使用带 `timeout:` 的 `Regexp` 实例,并为匹配缓冲设置合适的 `buffer_limit`。普通 `expect` 的 IO 期限不打断单次正则或同步回调。
|
|
268
|
+
|
|
252
269
|
## 示例和验证
|
|
253
270
|
|
|
254
271
|
```sh
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support"
|
|
4
|
+
|
|
5
|
+
runner = ExpectBenchmark::Runner.new("matching")
|
|
6
|
+
reader, writer = IO.pipe
|
|
7
|
+
session = Expect.open(reader, log_stdout: false)
|
|
8
|
+
begin
|
|
9
|
+
sizes = runner.smoke ? [4096] : [4096, 65_536, 1_048_576]
|
|
10
|
+
sizes.product([1, 8, 32], %i[first last miss]).each do |size, count, hit|
|
|
11
|
+
session.buffer = "#{"x" * (size - 3)}END"
|
|
12
|
+
patterns = Array.new(count) { |index| /missing#{index}/ }
|
|
13
|
+
index = hit == :first ? 0 : count - 1
|
|
14
|
+
patterns[index] = /END/ unless hit == :miss
|
|
15
|
+
matcher = Expect::Matcher.new(Expect::PatternList.new([session], patterns), 0)
|
|
16
|
+
verify = lambda do |result|
|
|
17
|
+
valid = if hit == :miss
|
|
18
|
+
result.nil?
|
|
19
|
+
else
|
|
20
|
+
result && result[0].equal?(session) && result[1].number == index + 1 && result[2] == [size - 3, 3, []]
|
|
21
|
+
end
|
|
22
|
+
ExpectBenchmark.check(valid)
|
|
23
|
+
end
|
|
24
|
+
runner.measure("scan/#{size}/#{count}/#{hit}", bytes: size, inputs: { size: size, patterns: count, hit: hit },
|
|
25
|
+
verify: verify) { matcher.__send__(:find_match) }
|
|
26
|
+
end
|
|
27
|
+
sizes.each do |size|
|
|
28
|
+
prefix = "中" * (size / 3)
|
|
29
|
+
session.buffer = "#{prefix}😀终tail"
|
|
30
|
+
matcher = Expect::Matcher.new(Expect::PatternList.new([session], [/😀(终)(x)?/]), 0)
|
|
31
|
+
verify = lambda do |result|
|
|
32
|
+
ExpectBenchmark.check(result && result[2] == [prefix.bytesize, 7, ["终".b, nil]])
|
|
33
|
+
end
|
|
34
|
+
runner.measure("utf8/#{size}", bytes: session.buffer.bytesize, inputs: { prefix_bytes: prefix.bytesize },
|
|
35
|
+
verify: verify) { matcher.__send__(:find_match) }
|
|
36
|
+
end
|
|
37
|
+
ensure
|
|
38
|
+
session.close
|
|
39
|
+
reader.close
|
|
40
|
+
writer.close
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
[1, 8, 32].each do |count|
|
|
44
|
+
pipes = Array.new(count) { IO.pipe }
|
|
45
|
+
sessions = pipes.map { |input, _| Expect.open(input, log_stdout: false) }
|
|
46
|
+
begin
|
|
47
|
+
sessions.each { |source| source.buffer = "x" * 4096 }
|
|
48
|
+
list = Expect::PatternList.new
|
|
49
|
+
# 每轮交替反转来源组,确保同一会话真实出现在多个非相邻组。
|
|
50
|
+
8.times { |index| list.on(/missing/, from: index.even? ? sessions : sessions.reverse) }
|
|
51
|
+
# 单会话的相邻相同组会合并;保持普通单会话路径作为对照。
|
|
52
|
+
matcher = Expect::Matcher.new(list, 0)
|
|
53
|
+
runner.measure("groups/#{count}", bytes: 4096 * count, inputs: { sessions: count, groups: list.groups.size },
|
|
54
|
+
verify: lambda { |result|
|
|
55
|
+
ExpectBenchmark.check(result.nil?)
|
|
56
|
+
}) { matcher.__send__(:find_match) }
|
|
57
|
+
matcher.run
|
|
58
|
+
verify = lambda do |result|
|
|
59
|
+
ExpectBenchmark.check(result == :retry && sessions.all? { |source| source.buffer == "r" })
|
|
60
|
+
end
|
|
61
|
+
runner.measure("ready/#{count}", bytes: count, inputs: { sessions: count }, verify: verify) do
|
|
62
|
+
sessions.each(&:clear_buffer)
|
|
63
|
+
pipes.each { |pipe| pipe.last.write("r") }
|
|
64
|
+
ready = IO.select(pipes.map(&:first), nil, nil, 0).first
|
|
65
|
+
matcher.__send__(:read_ready, ready, sessions)
|
|
66
|
+
end
|
|
67
|
+
ensure
|
|
68
|
+
sessions.each(&:close)
|
|
69
|
+
pipes.flatten.each(&:close)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
runner.finish
|
data/benchmark/relay.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support"
|
|
4
|
+
|
|
5
|
+
runner = ExpectBenchmark::Runner.new("relay")
|
|
6
|
+
reader, writer = IO.pipe
|
|
7
|
+
session = Expect.open(reader, log_stdout: false)
|
|
8
|
+
begin
|
|
9
|
+
size = runner.smoke ? 4096 : 65_536
|
|
10
|
+
payload = "x" * size
|
|
11
|
+
%i[none literal regexps].each do |kind|
|
|
12
|
+
session.__send__(:sequences=, {})
|
|
13
|
+
session.on_sequence("STOP") if kind == :literal
|
|
14
|
+
16.times { |index| session.on_sequence(/missing#{index}/) } if kind == :regexps
|
|
15
|
+
output = StringIO.new("".b)
|
|
16
|
+
session.listeners = [output]
|
|
17
|
+
verify = ->(result) { ExpectBenchmark.check(result == true && output.string == payload) }
|
|
18
|
+
runner.measure("escape/#{kind}", bytes: size, inputs: { size: size, regexps: kind == :regexps ? 16 : 0 },
|
|
19
|
+
verify: verify) do
|
|
20
|
+
output.string = "".b
|
|
21
|
+
session.__send__(:relay_history).clear
|
|
22
|
+
Expect.__send__(:relay_buffer, session, { session => payload.b })
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
session.__send__(:sequences=, {})
|
|
27
|
+
writer.close
|
|
28
|
+
normal = StringIO.new("".b)
|
|
29
|
+
slow = StringIO.new("".b)
|
|
30
|
+
slow.define_singleton_method(:write) { |data| super(data.byteslice(0, 17)) }
|
|
31
|
+
session.listeners = [normal, slow]
|
|
32
|
+
verify = lambda do |result|
|
|
33
|
+
ExpectBenchmark.check(result.equal?(session) && normal.string == payload && slow.string == payload)
|
|
34
|
+
ExpectBenchmark.check(!session.pending_output? && session.buffer.empty?)
|
|
35
|
+
end
|
|
36
|
+
runner.measure("mixed_targets", bytes: size * 2, inputs: { size: size, short_write: 17 }, iterations: 5,
|
|
37
|
+
verify: verify) do
|
|
38
|
+
normal.string = "".b
|
|
39
|
+
slow.string = "".b
|
|
40
|
+
session.buffer = payload
|
|
41
|
+
Timeout.timeout(10) { Expect.interconnect(session, timeout: 5) }
|
|
42
|
+
end
|
|
43
|
+
ensure
|
|
44
|
+
session.close
|
|
45
|
+
reader.close
|
|
46
|
+
writer.close unless writer.closed?
|
|
47
|
+
end
|
|
48
|
+
runner.finish
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support"
|
|
4
|
+
|
|
5
|
+
runner = ExpectBenchmark::Runner.new("send_slow")
|
|
6
|
+
size = runner.smoke ? 8 : 128
|
|
7
|
+
payload = "x" * size
|
|
8
|
+
[false, true].product([0, 0.001]).each do |echo, delay|
|
|
9
|
+
verify = lambda do |result|
|
|
10
|
+
count, received, reply = result
|
|
11
|
+
ExpectBenchmark.check(count == size && received == payload && reply == (echo ? payload : nil))
|
|
12
|
+
end
|
|
13
|
+
runner.measure("send/#{echo ? "echo" : "silent"}/#{delay}", bytes: size,
|
|
14
|
+
inputs: { size: size, echo: echo, delay: delay },
|
|
15
|
+
iterations: 1, verify: verify) do
|
|
16
|
+
client, peer = Socket.pair(:UNIX, :STREAM, 0)
|
|
17
|
+
session = Expect.open(client, log_stdout: false)
|
|
18
|
+
consumer = Thread.new do
|
|
19
|
+
received = "".b
|
|
20
|
+
while received.bytesize < size
|
|
21
|
+
data = peer.readpartial(size)
|
|
22
|
+
received << data
|
|
23
|
+
peer.write(data) if echo
|
|
24
|
+
end
|
|
25
|
+
received
|
|
26
|
+
end
|
|
27
|
+
begin
|
|
28
|
+
Timeout.timeout(10) do
|
|
29
|
+
count = session.send_slow(payload, delay: delay)
|
|
30
|
+
reply = session.expect_result(payload, timeout: 2).match if echo
|
|
31
|
+
[count, consumer.value, reply]
|
|
32
|
+
end
|
|
33
|
+
ensure
|
|
34
|
+
consumer.kill.join
|
|
35
|
+
session.close
|
|
36
|
+
client.close
|
|
37
|
+
peer.close
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
runner.finish
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "optparse"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "digest"
|
|
8
|
+
require "socket"
|
|
9
|
+
require "timeout"
|
|
10
|
+
|
|
11
|
+
# 标准库基准入口;每个工作负载先验证结果,采样后再核对,计时不包含断言。
|
|
12
|
+
module ExpectBenchmark
|
|
13
|
+
# 保存环境、输入规模、耗时、分配与 GC 样本,便于比较不同源码目录。
|
|
14
|
+
class Runner
|
|
15
|
+
attr_reader :smoke, :library
|
|
16
|
+
|
|
17
|
+
def initialize(name)
|
|
18
|
+
@library = File.expand_path("../lib", __dir__)
|
|
19
|
+
@samples = 5
|
|
20
|
+
@iterations = nil
|
|
21
|
+
@smoke = false
|
|
22
|
+
@output = "tmp/benchmark/#{name}.json"
|
|
23
|
+
OptionParser.new do |options|
|
|
24
|
+
options.banner = "Usage: ruby benchmark/#{name}.rb [options]"
|
|
25
|
+
options.on("--smoke", "Small correctness workload") { @smoke = true }
|
|
26
|
+
options.on("--library PATH", "Compare another checkout's lib directory") do |path|
|
|
27
|
+
@library = File.expand_path(path)
|
|
28
|
+
end
|
|
29
|
+
options.on("--samples N", Integer) { |count| @samples = count }
|
|
30
|
+
options.on("--iterations N", Integer) { |count| @iterations = count }
|
|
31
|
+
options.on("--output PATH") { |path| @output = path }
|
|
32
|
+
end.parse!
|
|
33
|
+
unless @samples.positive? && (!@iterations || @iterations.positive?)
|
|
34
|
+
raise ArgumentError, "counts must be positive"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
require File.join(@library, "expect")
|
|
38
|
+
@results = []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def measure(name, bytes:, inputs:, verify:, iterations: 100, &operation)
|
|
42
|
+
iterations = @iterations || (smoke ? 1 : iterations)
|
|
43
|
+
samples = smoke ? 1 : @samples
|
|
44
|
+
verify.call(operation.call) # 同时预热;错误输出永远不能成为更快的样本。
|
|
45
|
+
measurements = Array.new(samples) do
|
|
46
|
+
GC.start
|
|
47
|
+
before = GC.stat
|
|
48
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
49
|
+
result = nil
|
|
50
|
+
iterations.times { result = operation.call }
|
|
51
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
52
|
+
after = GC.stat
|
|
53
|
+
verify.call(result)
|
|
54
|
+
{
|
|
55
|
+
seconds: elapsed, allocated_objects: after[:total_allocated_objects] - before[:total_allocated_objects],
|
|
56
|
+
gc_count: after[:count] - before[:count], processed_bytes: bytes * iterations
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
@results << { name: name, inputs: inputs, iterations: iterations, samples: measurements }
|
|
60
|
+
puts "#{name}: #{format("%.6f", measurements.map { |row| row[:seconds] }.sort[samples / 2])}s"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def finish
|
|
64
|
+
root = File.dirname(library)
|
|
65
|
+
sha = git_output(root, "rev-parse", "HEAD")
|
|
66
|
+
dirty = git_output(root, "status", "--porcelain", "--untracked-files=all")
|
|
67
|
+
digest = Digest::SHA256.new
|
|
68
|
+
Dir[File.join(library, "**/*.rb")].each do |path|
|
|
69
|
+
digest << path.delete_prefix(library) << File.binread(path)
|
|
70
|
+
end
|
|
71
|
+
report = {
|
|
72
|
+
ruby: RUBY_DESCRIPTION, platform: RUBY_PLATFORM, revision: sha&.strip,
|
|
73
|
+
dirty: dirty.nil? ? nil : !dirty.empty?, library_sha256: digest.hexdigest, smoke: smoke, results: @results
|
|
74
|
+
}
|
|
75
|
+
FileUtils.mkdir_p(File.dirname(@output))
|
|
76
|
+
File.write(@output, "#{JSON.pretty_generate(report)}\n")
|
|
77
|
+
puts "Saved #{@output}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# 安装包和源码归档可能没有 Git;未知状态记录为 nil,不误报为干净提交。
|
|
83
|
+
def git_output(root, *)
|
|
84
|
+
output, _, status = Open3.capture3("git", "-C", root, *)
|
|
85
|
+
output if status.success?
|
|
86
|
+
rescue Errno::ENOENT
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.check(condition, message = "incorrect benchmark output")
|
|
92
|
+
raise message unless condition
|
|
93
|
+
end
|
|
94
|
+
end
|
data/docs/COMPATIBILITY.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
交互能力参考 [jacoby/expect.pm](https://github.com/jacoby/expect.pm),源码基准为版本 1.38、提交 `2ea0e4ce20a896c95cb4c94e781f1b1f3145150d`。此项目独立实现,使用 MIT 许可,没有复制 Perl 实现代码;原项目作者及维护者为 Austin Schutz、Roland Giersig、Dave Jacoby,采用与 Perl 相同的许可。
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
从 0.2.0 起采用 Ruby 原生接口。下表列出迁移关系,**旧方法、别名和参数语法已移除**;历史 0.1.1 安装包的接口见对应版本记录。
|
|
28
28
|
|
|
29
29
|
| 原接口或状态 | 当前 Ruby 接口 |
|
|
30
30
|
| --- | --- |
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# 内部状态与数据归属
|
|
2
|
+
|
|
3
|
+
这些状态供 Matcher、Relay 和会话生命周期协作使用,不是公共接口。
|
|
4
|
+
|
|
5
|
+
## 模块职责
|
|
6
|
+
|
|
7
|
+
| 模块 | 职责 |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `expect.rb` | 会话入口、PTY 启动、缓冲、直接读写和关闭流程 |
|
|
10
|
+
| `configuration.rb` | 配置校验与可复制的默认快照 |
|
|
11
|
+
| `pattern.rb`、`pattern_list.rb`、`result.rb` | 字节定位、声明顺序及原生结果值 |
|
|
12
|
+
| `matcher.rb` | 一次等待的匹配、事件派发和期限 |
|
|
13
|
+
| `session_resources.rb` | IO、日志与直属子进程的所有权账本和 GC 兜底 |
|
|
14
|
+
| `logging.rb` | 日志目标、监听器及同步输出;不持有第二份资源所有权 |
|
|
15
|
+
| `terminal.rb` | `stty` 和窗口尺寸的会话终端接口 |
|
|
16
|
+
| `interaction.rb`、`relay.rb`、`relay_writer.rb` | 人工接管、转义处理、共同调度和各目标发送游标 |
|
|
17
|
+
|
|
18
|
+
这些能力文件延续 `interaction.rb` 的类重开方式,公共入口仍是 `expect/pty`。按职责组织方法,不引入新的继承层级,也不复制会话状态。
|
|
19
|
+
|
|
20
|
+
跨对象调用使用 `__send__` 访问受保护或私有方法。变更以下入口时,应同时检查调用方与失败恢复路径:
|
|
21
|
+
|
|
22
|
+
| 会话内部入口 | 使用方 | 不变量 |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| `reset_result`、`record_match`、`record_eof`、`record_error` | Matcher | 先保存结果再执行回调;错误保留原始输入 |
|
|
25
|
+
| `read_available` | Matcher、Relay、写入背压 | 转接缓冲交接时禁止直接传播和裁剪;普通匹配缓冲需要裁剪 |
|
|
26
|
+
| `interaction_buffer`、`restore_relay_buffer` | Matcher、Relay | 嵌套匹配及转接退出时把未消费字节交还原所有者 |
|
|
27
|
+
| `sequences`、`relay_history`、`relay_callback` | Relay、转义扫描 | 已识别转义只回调一次,已发送前缀不重放 |
|
|
28
|
+
| `queue_output`、`relay_outputs`、`propagate` | Relay、转义扫描 | 每个目标独立保存短写进度;同步传播只用于普通匹配 |
|
|
29
|
+
|
|
30
|
+
| 状态 | 所有者与修改入口 | 交接、超时与关闭 |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `@buffer` | 会话的未消费匹配输入;`read_available` 追加,`record_match` 消费,`clear_buffer` 移交 | Matcher 用公开 `buffer` 的副本扫描;一次扫描的重复来源复用副本,下轮重新获取。超时保留字节。关闭不清空它,便于检查尾部 |
|
|
33
|
+
| `@interaction_buffer` | Relay 当前持有的待处理输入;转接背压读取也追加到这里 | 转义回调内的 Matcher 临时移回 `@buffer`;Matcher 的 ensure 将余下字节交回 Relay。Relay 的 ensure 恢复上层交互状态,并把未处理字节还给普通缓冲 |
|
|
34
|
+
| `@relay_outputs` | 源会话持有的各目标发送游标;`queue_output` 创建,RelayWriter 推进 | 超时或写失败后保留原目标和已交付位置;重入续发,不重放成功前缀。关闭放弃剩余交付并清空队列,不关闭借用目标 |
|
|
35
|
+
| `@relay_history` | 源会话的正则转义历史窗口;`relay_buffer` 更新 | 同一规则跨次转接保留,规则改变或转义消费后清空。窗口遵守 buffer_limit 或内部默认上限,固定 UTF-8 正则不保留开头的孤立续字节。关闭清空 |
|
|
36
|
+
| `@relay_callback` | 已识别转义但尚未交付完前缀时,源会话暂存的回调 | 所有前缀目标交付完成后执行一次;超时保留,关闭释放。不能在等待写入后重新扫描这段已识别转义 |
|
|
37
|
+
|
|
38
|
+
匹配优先级始终是“声明组 → 会话 → 模式”,不是文本位置;多个会话包装同一 IO 时,就绪处理使用对象身份选择首个声明会话。回调前后用于判断缓冲是否变化的快照仍独立保留,不能换成可变内部字符串。零长度或 preserve_buffer 的继续匹配必须遵守 stalled 保护。
|
|
39
|
+
|
|
40
|
+
文本与 EOF 回调遵守相同的继续期限:`continue(reset_timeout: false)` 返回后先检查原期限,再开始下一轮文本匹配。EOF 继续已到期时仍依次派发已知 EOF,不读取新输入;超时仅通知仍活跃的来源,保留未消费缓冲。所有来源都已 EOF 时直接返回 EOF,不再制造一次超时事件。EOF 或超时回调重置期限后恢复普通匹配顺序。
|
|
41
|
+
|
|
42
|
+
转义扫描只在一轮内复用 `history + buffer`,只含字面规则时不构造它。回调继续后重新读取规则、历史和缓冲;不能跨回调保存文本快照。字面转义的潜在前缀暂存,完整前缀交付后才执行转义回调。
|
|
43
|
+
|
|
44
|
+
## 关闭与所有权
|
|
45
|
+
|
|
46
|
+
SessionResources 保存创建者 PID、所属句柄、直属子进程和所属日志。借用 IO 不关闭,fork 后的非创建者不发送信号或回收父进程的孩子。soft_close 最多发送 TERM 并保留未退出 PID;hard_close 可在有限等待后发送 KILL。
|
|
47
|
+
|
|
48
|
+
工厂和构造器在校验之前登记资源归属,失败时沿用同一清理流程;启动成功必须显式记录,不能用 PID 非空推断整个工厂调用已经成功。即使 exec 成功后诊断输出失败,也要立即回收未交付给调用方的子进程。
|
|
49
|
+
|
|
50
|
+
显式关闭遇到 IOError/SystemCallError 时,继续尝试其他句柄、交互包装器、日志和子进程清理,最后传播首个清理错误;已经在传播的其他异常保留。失败资源继续持有,后续关闭可重试。GC 终结器独立尝试句柄、日志、非阻塞回收,常规清理错误不向外传播。终结器不能强引用会话本身。
|
|
51
|
+
|
|
52
|
+
是否保留原始异常由当前构造、块或关闭作用域显式记录,不能直接读取调用者 rescue 中的 `$!`。`Interrupt` 和 `SystemExit` 同样先清理再传播;`break` / `throw` 不是异常,此时清理失败仍应抛出。
|
|
53
|
+
|
|
54
|
+
## 回归入口
|
|
55
|
+
|
|
56
|
+
| 不变量 | 测试 |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| 部分关闭失败、错误保留、借用 IO、非 owner、软硬关闭和 GC | cleanup_test、process_test、edge_case_test |
|
|
59
|
+
| 字节偏移、二进制和 UTF-8 分片、零长度、可选捕获 | pattern_offset_test、matching_test、ruby_api_test、edge_case_test |
|
|
60
|
+
| 一轮快照、同 IO 首来源、回调换规则、嵌套匹配 | scan_reuse_test、multi_session_test、interconnect_test |
|
|
61
|
+
| 短写、转义拆包、超时恢复、不重放、回调前缀顺序 | relay_recovery_test、interconnect_test |
|
|
62
|
+
| 无限/零/有限超时、继续重置/保留、EINTR、接收重置 | timeout_test(控制输入到达时间)、edge_case_test |
|
|
63
|
+
| 总期限和目标写期限、连续中断不延长写超时 | relay_recovery_test |
|
|
64
|
+
|
|
65
|
+
保留独立的 Matcher、Relay、write、wait 期限合同;仅为去重而统一事件循环会扩大上述状态交接的影响范围。
|
data/docs/PERFORMANCE.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# 性能基准
|
|
2
|
+
|
|
3
|
+
从源码目录安装开发依赖后运行,基准本身仅使用标准库:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bundle exec ruby benchmark/matching.rb
|
|
7
|
+
bundle exec ruby benchmark/relay.rb
|
|
8
|
+
bundle exec ruby benchmark/send_slow.rb
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
默认预热一次、采样五次。结果分别写入 `tmp/benchmark/matching.json`、`relay.json`、`send_slow.json`,该目录不提交。每个场景先核对非空输入的预期结果,再计时;每轮计时后再次核对最后一次执行的结果。输出包括 Ruby、平台、源码提交、工作区是否修改、库源码 SHA-256、输入规模、迭代次数、处理字节数、墙钟耗时、分配对象数和 GC 次数。
|
|
12
|
+
|
|
13
|
+
## 工作负载与边界
|
|
14
|
+
|
|
15
|
+
| 脚本 | 范围 |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| matching | 4 KiB、64 KiB、1 MiB;1、8、32 个正则;首个命中、末个命中、全未命中;UTF-8 前缀和可选捕获;1、8、32 个会话、多组重复来源和同时就绪的真实管道 |
|
|
18
|
+
| relay | 无转义、字面转义、16 个正则转义;正常目标与每次只接受 17 字节的目标共同接收相同数据 |
|
|
19
|
+
| send_slow | 真实本地 socket;无回显、持续回显;零延迟和每字符 1ms 延迟 |
|
|
20
|
+
|
|
21
|
+
`matching` 的扫描用内部 `find_match` 单独度量缓冲扫描,排除 PTY 启动和回调开销;就绪场景包含管道写入、选择和读取。`relay` 的前三项单独度量转义扫描,混合目标项运行完整转接循环。短写模拟目标吞吐受限,不等同于真实慢网络。`send_slow` 包含 socket、接收线程和完整回显校验的成本;无回显时计时止于接收方收齐数据。
|
|
22
|
+
|
|
23
|
+
处理字节数表示每轮提供给场景的输入字节数(混合目标为两份交付量),不是正则引擎实际访问内存的次数。分配量是整个 Ruby 进程的计数,socket 场景包含接收线程的分配。基准不提供 CPU 或网络隔离;耗时波动不能直接归因于代码变化。
|
|
24
|
+
|
|
25
|
+
## 同环境对照
|
|
26
|
+
|
|
27
|
+
对照目录必须包含已知提交的完整源码。两个版本使用同一套基准脚本、同一 Ruby 和依赖,在机器空闲时交替运行,比较原始样本的中位数与分配量:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
bundle exec ruby benchmark/matching.rb --library /path/to/baseline/lib --samples 5 --output tmp/benchmark/before.json
|
|
31
|
+
bundle exec ruby benchmark/matching.rb --samples 5 --output tmp/benchmark/after.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
另外两份脚本支持相同参数。`--iterations N` 可增加单个样本工作量;比较双方必须使用相同参数。源码 SHA-256 用于区分同一提交上的未提交修改。修改工作负载后,应对两个版本重新取样。
|
|
35
|
+
|
|
36
|
+
没有 Git 的源码归档或安装目录仍可运行;提交和工作区状态记为 `null`,保留源码 SHA-256。
|
|
37
|
+
|
|
38
|
+
## 正则输入复用验证(2026-09-27)
|
|
39
|
+
|
|
40
|
+
`Pattern#locate` 只在正则要求的编码与输入编码不同时复制输入;其他正则复用只读缓冲。Ruby 字符串可能共享底层存储,因此不能把减少一次 `dup` 直接解释为少复制整个缓冲的字节数;这里报告实际对象分配和墙钟样本。
|
|
41
|
+
|
|
42
|
+
macOS arm64、Ruby 4.0.6、Bundler 4.0.17,使用同一套 `benchmark/matching.rb` 和依赖,对修改前后的 `lib` 分别运行 `--samples 5 --iterations 100`。下表为五次样本的中位数,每项均校验匹配结果。此前用 30 次迭代进行的首轮比较方向一致。
|
|
43
|
+
|
|
44
|
+
| 场景 | 修改前耗时 | 修改后耗时 | 修改前分配 | 修改后分配 |
|
|
45
|
+
| --- | --- | --- | --- | --- |
|
|
46
|
+
| 4 KiB、32 个正则、全未命中 | 1.985 ms | 1.415 ms | 6,501 | 3,301 |
|
|
47
|
+
| 64 KiB、32 个正则、全未命中 | 17.068 ms | 11.070 ms | 6,501 | 3,301 |
|
|
48
|
+
| 1 MiB、32 个正则、全未命中 | 257.474 ms | 165.734 ms | 6,501 | 3,301 |
|
|
49
|
+
| 1 MiB、32 个正则、最后命中 | 265.848 ms | 172.001 ms | 7,301 | 4,101 |
|
|
50
|
+
| 1 MiB UTF-8 前缀、固定编码正则 | 78.348 ms | 74.721 ms | 1,201 | 1,201 |
|
|
51
|
+
|
|
52
|
+
固定 UTF-8 正则仍需要编码副本,分配量不变;约 4.6% 的耗时差不能视为稳定收益。单正则大缓冲场景也基本不变。上述结果适用于本次机器与工作负载,不设为 CI 性能门槛。
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
bundle exec ruby benchmark/matching.rb --smoke
|
|
56
|
+
bundle exec ruby benchmark/relay.rb --smoke
|
|
57
|
+
bundle exec ruby benchmark/send_slow.rb --smoke
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`script/ci` 运行这些小规模正确性检查,不设置墙钟性能阈值。热点优化必须有实际收益证据;减少对象分配不代表所有输入都会变快,也不能替代完整测试和安装验证。
|