Files
klink/PERFORMANCE_IMPROVEMENTS.md
T
gary db6d64e9c9
CI / test (push) Successful in 1m7s
CI / cross-build (amd64, darwin) (push) Successful in 38s
CI / cross-build (amd64, linux) (push) Successful in 33s
CI / cross-build (amd64, windows) (push) Successful in 38s
CI / cross-build (arm64, darwin) (push) Successful in 39s
CI / cross-build (arm64, linux) (push) Successful in 39s
CI / cross-build (arm64, windows) (push) Successful in 38s
perf: optimize SFTP sessions and configuration imports
2026-09-13 18:19:56 +08:00

8.1 KiB
Raw Blame History

性能与算法优化方向

本文记录当前审查得出的性能和算法优化方向,供评审和任务拆分使用。优先级按预期用户收益排序,不按实现工作量排序。

P0:恢复 SFTP 并发传输路径

现状

internal/sftp/transport.go 已启用 SFTP 并发读写,但 internal/sftp/service.go 为进度和取消逻辑包裹了 io.Copy 的两端。包装类型隐藏了 pkg/sftp.File 的 WriteTo 和 ReadFrom 方法,使依赖库无法走并发传输路径。

在高延迟连接中,当前约 32 KiB 的串行分块可能接近每个往返只传输一个分块。以 80 ms RTT 估算,忽略协议开销时上限约为 0.4 MiB/s。

优化方向

  • 下载时通过远端 SFTP 文件的 WriteTo 路径传输,同时保留带进度统计的本地 writer。
  • 上传时通过远端 SFTP 文件的 ReadFrom 或 ReadFromWithConcurrency 路径传输,并提供支持 context、可报告大小的本地 reader。
  • 保留现有的取消机制:等待已有宽限期后关闭 SFTP 会话。
  • 保持临时文件原子完成和断点续传语义。

实现约束

  • 并发远端写入在失败后可能留下超出首个失败偏移的数据。失败时必须截断或丢弃远端临时文件,不能将其发布为最终目标。
  • 保持现有的覆盖确认和 partial 文件校验保证。
  • 仅在真实测量表明默认值不适合常见服务器时,再将并发度设为可配置项。

验收标准

  • 使用注入延迟的基准测试,证明大于两个 SFTP 包的文件吞吐有明显提升。
  • 取消、覆盖、中断上传、中断下载和续传测试继续通过。
  • 重复取消时不存在 goroutine 或文件描述符泄漏。

P1:消除目录传输的重复遍历

现状

目录传输先计算文件数和总字节数,再次遍历后实际传输。远端下载会执行两次递归 ReadDir,并且在已经读取到目录项后,仍为每个文件单独发起一次 Stat。

优化方向

  • 在首次遍历远端目录时构建传输清单,包含远端路径、本地目标路径和复制所需的 FileInfo。
  • 用同一份清单计算总量和执行传输;将已知文件元数据传给 downloadFile,避免额外 Stat。
  • 对超大清单设置合理内存上限;超过上限时退化为不显示总量的整体进度,但保留单文件进度。
  • 在远端下载路径完成后,再考虑将本地上传也改成清单模式。本地双遍历的成本低于额外远端往返。

验收标准

  • 下载目录树时,每个远端目录只在准备阶段读取一次,不再出现可避免的逐文件 Stat。
  • 文件数、总字节、符号链接拒绝和错误信息保持现有行为。
  • 使用带请求计数的远端文件系统测试,断言请求次数。

P1:复用交互式 SFTP 会话

现状

每次未命中缓存的远端列表、变更、上传和下载都会启动新的 ssh -s ... sftp 子进程,并在操作结束时关闭。目录列表缓存能帮助重复访问,但首次连续进入目录树仍会反复认证和握手。

优化方向

  • 引入有上限的会话管理器,按节点 ID 和解析后的地址索引;每个活跃操作租用一个 SFTP 客户端。
  • 在短暂空闲后关闭会话;遇到传输或认证错误时使会话失效。
  • 备选方案是评估 OpenSSH ControlMaster/ControlPersist 复用。它可减少应用内连接池逻辑,但需要谨慎处理控制 socket 生命周期和跨平台差异。

验收标准

  • 同一节点连续浏览未缓存目录时,在安全情况下复用一次已认证连接。
  • 失效或过期的会话会在下一次操作时透明替换。
  • 应用退出和节点配置变更时,受影响的会话会关闭或失效。

P1:减少 TUI 空闲工作和搜索分配

现状

marquee 定时器即使没有任何文本需要滚动,也会每 250 ms 运行一次。一次渲染可能多次过滤节点;过滤会为每个节点构造小写搜索字符串并分配新的结果切片。

优化方向

  • 仅在当前可见文本实际溢出时启动 marquee 定时器,离开该状态后停止。
  • 以查询值和单调递增的配置版本缓存过滤后的节点 ID 或索引。
  • 为节点保存规范化搜索字段,或与过滤结果一起缓存;编辑、导入、重载和删除后使缓存失效。
  • 过滤时返回指向 m.cfg.Nodes 的引用或索引,而不是复制 Node 值。

验收标准

  • 空闲界面在没有动画时不再调度 250 ms 重绘。
  • 相同查询的重复渲染不再扫描全部节点。
  • 为 100、1,000、10,000 节点增加基准测试,并保持当前光标和搜索行为。

P2:批量观察隧道状态

现状

TUI 每秒刷新一次隧道状态。每条活跃 Unix 隧道会进行状态文件 I/O、存活检查,并启动独立 ps 子进程验证 worker 所有权;所有 profile 串行处理。

优化方向

  • 高频轮询活跃或状态转换中的隧道;缓存终态,降低其刷新频率,或仅在用户操作后刷新。
  • 增加批量状态 API,集中加载状态,并在平台允许时批量验证进程所有权。
  • 每次发信号前仍保留 PID/token 所有权验证。性能优化不得削弱 PID 复用安全边界。

验收标准

  • 大量已停止隧道不会各自触发周期性的进程检查。
  • 停止/重启安全测试,包括 PID 复用保护,继续通过。

P3:为导入和批量配置操作建立索引

现状

SyncKTUI 和 Merge 会反复扫描已有节点和隧道来检查 ID、名称和地址匹配。大型导入时算法复杂度为二次。

优化方向

  • 导入前建立节点 ID、规范化节点名称、受管理节点身份、隧道 ID 和规范化隧道名称的 map。
  • 每次接受新记录后同步更新 map,确保同一次导入内部的冲突也能被检测。
  • 保持现有的冲突输出顺序和 ktui 所有权语义。

验收标准

  • 除必要的输出排序外,大型导入的复杂度为已有记录数加新增记录数的线性复杂度。
  • 现有导入冲突和幂等性测试不变,或补充为更明确的断言。

P3:节流传输进度发布

现状

每个复制分块都会发布进度。TUI 侧每次更新都会锁定任务并读取时钟,但速度显示本身已使用 200 ms 采样窗口。

优化方向

  • 在传输路径中原子累加已传输字节数。
  • 每 100-200 ms 发布一次 UI 进度;文件完成和传输完成时额外发布。
  • 保持进度单调递增,并确保完成消息前发出最终进度。

验收标准

  • 高速本地或局域网传输不再每个分块都发生 UI 加锁和时钟读取。
  • 进度条、最终字节数、取消和任务队列行为不变。

测量与交付顺序

  1. 修改 P0 前先增加可复现基准:模拟 5 ms、40 ms、100 ms RTT 的单文件上传/下载、小文件目录树、续传和 TUI 过滤。
  2. 先实现并测量 P0。它预期吞吐收益最大,且依赖库已提供明确的优化路径。
  3. 接着实现远端传输清单,再评估会话复用。
  4. 在修改渲染调度器前,对空闲和大规模配置的 TUI 进行 profile。
  5. 增加普通 push/pull-request CI,执行 go test ./...、go vet ./... 和重点 race 测试。现有工作流只在发布 tag 时验证。

实施状态(2026-07-24)

已完成 P0 和 P1:

  • P0:非续传上传重新使用 ReaderFrom,下载重新使用 WriterTo,从而让 pkg/sftp 的并发读写选项真正参与数据复制;续传仍使用顺序写入,避免并发写入失败时留下稀疏文件。
  • P1(目录传输):上传和下载均先建立一次传输清单;下载不再对每个远端文件额外 Stat,也不再为统计与实际传输重复遍历目录。
  • P1(会话):按节点和地址缓存空闲 SFTP 会话,支持空闲过期、容量限制、节点失效和应用退出时关闭;配置重载时会使会话失效。
  • P1(TUI):缓存节点搜索结果;仅在页面存在溢出文本时调度跑马灯刷新,空闲页面不再持续产生 250 ms 更新。

新增覆盖上述路径的单元测试和基准。验证已执行:go test ./...、go vet ./...、go test -race ./...、go build ./cmd/klink。