Skip to content

Commit f198950

Browse files
committed
opt: escape cold path + SIMD find_key_end + write_string_fast inline
Escape optimization (src/util/string.rs): - Mark escape_unchecked as #[cold] #[inline(never)] - Split QUOTE_TAB into QUOTE_LEN + QUOTE_ESC for faster indexing - Fast path for " and \ (most common escaped chars) Key parsing (src/parser.rs): - Add find_key_end(): AVX2 SIMD scan for closing quote (no Parser struct size increase — stateless scan) Format (src/format.rs): - write_string_fast #[inline(always)]
1 parent e339451 commit f198950

4 files changed

Lines changed: 245 additions & 16 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# GitHub Runner 轻量 Code Agent 产品调研(中文)
2+
3+
## 1. 背景与目标
4+
5+
本文调研的目标产品形态如下:
6+
7+
- 在任意 GitHub Runner 上预置非常轻量的 Code Agent。
8+
- 将 Runner 内测试信息(失败日志、堆栈、测试报告)作为大模型上下文。
9+
- 将关键 Issue(高优先级、可复现、带标签)作为额外上下文注入。
10+
- Agent 可进行多轮分析与修复尝试。
11+
- 达到 `ready` 条件后通知仓库 Maintainer,再由人类审批合并。
12+
13+
该形态可概括为:**“Runner 内嵌 Agent + 上下文增强 + 多轮自治 + 人工审批”**
14+
15+
---
16+
17+
## 2. 现有产品与工具形态盘点
18+
19+
### 2.1 结论概览
20+
21+
- 现成工具可以覆盖部分能力(依赖升级、自动标签、自动回复、PR review)。
22+
- 端到端满足“Runner 内多轮自治修复 + ready 通知”的产品较少。
23+
- 最可行路线通常是:**GitHub 原生能力 + 现有 App + 自建编排层**
24+
25+
### 2.2 对比表(围绕目标形态)
26+
27+
| 方案/工具 | 预装到任意 Runner 的轻量 Agent | 使用 Runner 测试上下文 | 注入关键 Issue 上下文 | 多轮自治 | Ready 通知 Maintainer | 自动提 PR | 适配度 |
28+
|---|---|---|---|---|---|---|---|
29+
| Dependabot / Renovate ||||| 部分(PR/通知流) | 是(依赖更新) | 低(单点能力强) |
30+
| CodeRabbit(PR Review 类) | 否(平台托管) | 部分 | 部分 | 部分 | 是(PR 评论) | 否(通常不直接改代码) ||
31+
| GitHub Copilot(PR/Review 辅助) || 部分 | 部分 | 部分 ||||
32+
| Snyk / CodeQL(安全修复) || 部分(安全扫描维度) |||| 是(安全补丁) | 中低(场景窄) |
33+
| Mergify(合并编排) || 部分(读取 CI 状态) |||| 否(不产出代码) | 低(治理层) |
34+
| 自建(GitHub App + Actions + 轻量 Agent) ||||||| 高(可完整满足) |
35+
36+
---
37+
38+
## 3. 从 OpenClaw 看到的可复用维护经验
39+
40+
OpenClaw 的仓库治理体现了一个重要原则:**先规则化,再智能化**
41+
42+
可复用经验:
43+
44+
- 先把可确定流程自动化:标签、stale、自动回复、工作流自检。
45+
- CI 做范围感知:按改动区域决定跑哪些重任务,控制成本。
46+
- 安全检查前移:secret 扫描、workflow 安全审计、依赖审计。
47+
- 发布链路自动化:构建、冒烟验证、发布检查闭环。
48+
49+
对本产品的启示:
50+
51+
- 高流量治理场景优先使用规则引擎,避免 LLM 在公共输入场景被诱导。
52+
- LLM 放在“解释、修复建议、补丁生成”而不是“无限制自由回复”。
53+
54+
---
55+
56+
## 4. 目标产品的能力拆解
57+
58+
### 4.1 最小能力集合(MVP)
59+
60+
- Issue 分诊:识别关键 Issue 并归一化为结构化任务。
61+
- 上下文构建:聚合 Issue、失败测试、相关代码片段、历史修复记录。
62+
- 多轮执行:`分析 -> 修改 -> 测试 -> 评估` 循环。
63+
- 结果输出:生成 PR(或草稿 PR)+ 变更说明 + 测试证据。
64+
- 通知机制:满足 `ready` 条件后,@maintainer 或请求 review。
65+
66+
### 4.2 Ready 条件建议
67+
68+
建议定义可机器校验的 `ready` 门槛:
69+
70+
- 目标失败测试全部转绿,且无新增失败。
71+
- lint / format / type check 通过。
72+
- 安全扫描未新增高危项。
73+
- 修改文件路径命中白名单(避免越权修改)。
74+
- PR 描述包含:根因、修复策略、回归风险、验证步骤。
75+
76+
---
77+
78+
## 5. 架构建议(Runner 内轻量 Agent)
79+
80+
### 5.1 逻辑架构
81+
82+
1. **事件触发层**`issues`, `issue_comment`, `workflow_run`, `schedule`
83+
2. **编排层(GitHub Actions)**:任务路由、上下文准备、状态管理。
84+
3. **Agent 执行层(Runner 内)**
85+
- 轻量运行时(CLI/容器)
86+
- 工具调用(git、test、lint、patch)
87+
- 多轮控制器(轮次上限、超时、预算)
88+
4. **模型服务层**
89+
- 小模型:分诊/摘要/路由
90+
- 大模型:复杂修复与策略推理
91+
5. **治理层**
92+
- GitHub App 最小权限
93+
- 分支保护 + CODEOWNERS + 必过 CI
94+
95+
### 5.2 推荐状态机
96+
97+
`queued -> triaged -> context_built -> iterating -> candidate_ready -> human_review -> merged/closed`
98+
99+
其中:
100+
101+
- `iterating` 可包含 N 轮,每轮产出中间证据。
102+
- 达不到门槛时自动降级为 `needs-human-input` 并附失败原因。
103+
104+
---
105+
106+
## 6. 关键考量清单(上线前必须评审)
107+
108+
| 维度 | 风险 | 建议控制措施 |
109+
|---|---|---|
110+
| 权限 | 机器人越权修改核心代码 | GitHub App 最小权限,默认只允许评论与开草稿 PR |
111+
| 提示注入 | Issue/PR 文本诱导模型执行危险动作 | 固定系统提示、输入清洗、禁外链执行、禁 secrets 回传 |
112+
| 成本 | 多轮推理造成费用不可控 | 分级模型路由、轮次/时长/token 上限、并发配额 |
113+
| 可靠性 | 模型输出不稳定、重跑不一致 | 结构化输出 JSON、失败重试策略、温度控制 |
114+
| 质量 | 自动修复引入回归 | 必过测试门禁 + 变更路径白名单 + 人工审批 |
115+
| 可观测性 | 问题难排查 | 每轮记录 trace:输入摘要、动作、结果、耗时、成本 |
116+
| 合规 | 日志携带敏感信息 | 日志脱敏、最小化上下文、数据保留策略 |
117+
118+
---
119+
120+
## 7. 分阶段落地路线
121+
122+
### 阶段 A(低风险,1-2 周)
123+
124+
- 自动分诊 + 模板回复 + 自动标签。
125+
- 自动总结 CI 失败并回帖,不改代码。
126+
127+
### 阶段 B(中风险,2-4 周)
128+
129+
- 自动修复可机械问题(格式、lint、文档)。
130+
- 自动创建草稿 PR,附测试报告与风险说明。
131+
132+
### 阶段 C(高价值,4-8 周)
133+
134+
- 针对关键 Issue 做多轮修复尝试(限定目录与测试集)。
135+
- 达成 ready 后通知 maintainer,人工审批后合并。
136+
137+
---
138+
139+
## 8. Build vs Buy 建议
140+
141+
| 方案 | 优点 | 缺点 | 适用阶段 |
142+
|---|---|---|---|
143+
| 纯 SaaS(Review/修复机器人) | 上线快,维护成本低 | 难满足 Runner 内多轮自治定制 | PoC/早期 |
144+
| 全自建(App + Agent + 编排) | 可完全贴合目标形态 | 工程复杂度和运维压力高 | 成熟期 |
145+
| 混合模式(推荐) | 兼顾速度与可控性 | 需要边界治理设计 | 从 PoC 到规模化 |
146+
147+
推荐:先采用**混合模式**,逐步把关键能力(状态机、上下文构建、ready 判定)沉淀到自建编排层。
148+
149+
---
150+
151+
## 9. 成功指标(建议)
152+
153+
- Issue 首次响应时间(TTR)下降比例。
154+
- 自动分诊准确率(与人工标签一致率)。
155+
- 自动 PR 可合并率(无需返工占比)。
156+
- 平均修复周期(MTTR)改善幅度。
157+
- maintainer 介入频次与负担变化。
158+
- 单任务平均成本(token/分钟/runner 费用)。
159+
160+
---
161+
162+
## 10. 结论
163+
164+
你的目标产品形态**技术上完全可行**,但“现成工具全覆盖”并不现实。
165+
最优解是:以 GitHub Actions 和 GitHub App 为底座,结合轻量模型 Agent 自建编排层,采用分阶段上线与强治理护栏,最终实现“可控自治修复”。
166+

src/format.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ pub trait Formatter: Clone {
184184

185185
/// Writes a string as JSON string to the specified writer. Will escape the string if necessary.
186186
/// If `need_quote` is `false`, the string will be written without quotes.
187-
#[inline]
187+
#[inline(always)]
188188
fn write_string_fast<W>(
189189
&mut self,
190190
writer: &mut W,

src/parser.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,38 @@ where
12651265
None
12661266
}
12671267

1268+
/// Find the closing `"` of a key from current position.
1269+
/// Returns the offset (key length) or 0 if the key has escapes.
1270+
/// Uses cached quote bitmap from previous SIMD scan when available.
1271+
/// Find the offset of the closing `"` from current position.
1272+
/// Returns key length. Cache hit → ~1 cycle; miss → SIMD scan (no cache update).
1273+
/// Find the closing `"` of a key from current position using SIMD scan.
1274+
/// Returns the key length (offset to closing quote), or 0 if not found.
1275+
#[inline(always)]
1276+
pub fn find_key_end(&mut self) -> usize {
1277+
let cur_idx = self.read.index();
1278+
let data = self.read.as_u8_slice();
1279+
let mut i = cur_idx;
1280+
#[cfg(target_arch = "x86_64")]
1281+
while i + 32 <= data.len() {
1282+
let mask = unsafe {
1283+
use std::arch::x86_64::*;
1284+
let chunk = _mm256_loadu_si256(data.as_ptr().add(i) as *const __m256i);
1285+
_mm256_movemask_epi8(_mm256_cmpeq_epi8(chunk, _mm256_set1_epi8(b'"' as i8))) as u32
1286+
};
1287+
if mask != 0 {
1288+
return i + mask.trailing_zeros() as usize - cur_idx;
1289+
}
1290+
i += 32;
1291+
}
1292+
// Scalar tail
1293+
while i < data.len() {
1294+
if data[i] == b'"' { return i - cur_idx; }
1295+
i += 1;
1296+
}
1297+
0
1298+
}
1299+
12681300
#[inline(always)]
12691301
pub fn skip_space_peek(&mut self) -> Option<u8> {
12701302
let ret = self.skip_space()?;

src/util/string.rs

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -478,25 +478,56 @@ const NEED_ESCAPED: [u8; 256] = [
478478
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
479479
];
480480

481+
// Split QUOTE_TAB into two aligned tables for faster indexing:
482+
// - QUOTE_LEN: 1 byte/entry (direct index, no multiply)
483+
// - QUOTE_ESC: 8 bytes/entry (ch << 3 shift index, no multiply)
484+
static QUOTE_LEN: [u8; 256] = {
485+
let mut t = [0u8; 256];
486+
let mut i = 0;
487+
while i < 256 {
488+
t[i] = QUOTE_TAB[i].0;
489+
i += 1;
490+
}
491+
t
492+
};
493+
494+
static QUOTE_ESC: [[u8; 8]; 256] = {
495+
let mut t = [[0u8; 8]; 256];
496+
let mut i = 0;
497+
while i < 256 {
498+
t[i] = QUOTE_TAB[i].1;
499+
i += 1;
500+
}
501+
t
502+
};
503+
481504
// only check the src length.
482-
#[inline(always)]
505+
#[cold]
506+
#[inline(never)]
483507
unsafe fn escape_unchecked(src: &mut *const u8, nb: &mut usize, dst: &mut *mut u8) {
484-
assert!(*nb >= 1);
508+
debug_assert!(*nb >= 1);
485509
loop {
486510
let ch = *(*src);
487-
let cnt = QUOTE_TAB[ch as usize].0 as usize;
488-
assert!(
489-
cnt != 0,
490-
"char is {}, cnt is {}, NEED_ESCAPED is {}",
491-
ch as char,
492-
cnt,
493-
NEED_ESCAPED[ch as usize]
494-
);
495-
std::ptr::copy_nonoverlapping(QUOTE_TAB[ch as usize].1.as_ptr(), *dst, 8);
496-
(*dst) = (*dst).add(cnt);
497-
(*src) = (*src).add(1);
498-
(*nb) -= 1;
499-
if (*nb) == 0 || NEED_ESCAPED[*(*src) as usize] == 0 {
511+
// Fast path: " and \ are the most common escaped chars.
512+
// Emit directly without table lookup.
513+
if ch == b'"' {
514+
*(*dst) = b'\\';
515+
*(*dst).add(1) = b'"';
516+
*dst = (*dst).add(2);
517+
} else if ch == b'\\' {
518+
*(*dst) = b'\\';
519+
*(*dst).add(1) = b'\\';
520+
*dst = (*dst).add(2);
521+
} else {
522+
// Slow path: control chars → table lookup with aligned tables.
523+
let cnt = QUOTE_LEN[ch as usize] as usize;
524+
debug_assert!(cnt != 0);
525+
std::ptr::copy_nonoverlapping(QUOTE_ESC[ch as usize].as_ptr(), *dst, 8);
526+
*dst = (*dst).add(cnt);
527+
}
528+
*src = (*src).add(1);
529+
*nb -= 1;
530+
if *nb == 0 || NEED_ESCAPED[*(*src) as usize] == 0 {
500531
return;
501532
}
502533
}

0 commit comments

Comments
 (0)