Skip to content

Commit ff60184

Browse files
committed
并行调度有界语法候选以覆盖单项目全部资产
1 parent 65ee909 commit ff60184

6 files changed

Lines changed: 244 additions & 71 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ jobs:
3636
run: |
3737
cargo test -p codeguard-runtime --features wasm-precheck --test process_contract
3838
cargo test -p codeguard-runtime --features wasm-precheck --test wasm_grammar_load --test dart_upstream_corpus
39-
cargo test -p codeguard-cli --features wasm-precheck --test syntax_worker_candidate --test grammar_probe_cli --test grammar_route --test check_all_grammar_candidates --test zig_lint_cli --test typescript_syntax_fallback_candidate --test java_syntax_fallback_candidate
39+
cargo test -p codeguard-cli --features wasm-precheck --test syntax_worker_candidate --test grammar_probe_cli --test grammar_route --test zig_lint_cli --test typescript_syntax_fallback_candidate --test java_syntax_fallback_candidate
40+
# 32 份 WASM 的批次和单项目回归各自独占候选阶段预算,避免测试进程互相争抢内存与 CPU。
41+
cargo test -p codeguard-cli --features wasm-precheck --test check_all_grammar_candidates -- --test-threads=1
4042
4143
- name: Verify the local npm package contains a running WASM worker
4244
run: |

‎crates/codeguard-cli/src/check_command.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,7 @@ pub fn run(args: &[String]) -> ExitCode {
14611461
go_tool: parsed.go_tool.as_deref(),
14621462
},
14631463
parsed.selection == Selection::Java,
1464+
parsed.jobs_limit,
14641465
deadline,
14651466
if request_cancelled {
14661467
Some("request_cancelled")

‎crates/codeguard-cli/src/check_syntax_candidates.rs‎

Lines changed: 165 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@ use crate::discovery::DiscoveryReport;
44
use crate::go_lint_command::selected_sources_for_candidate;
55
use crate::grammar_probe_command::read_plain_source;
66
use crate::grammar_route::route_source;
7+
use crate::syntax_worker_candidate_observation::SyntaxWorkerCandidateObservation;
78
use crate::syntax_worker_runner::run_syntax_worker_candidate;
89
use codeguard_adapters::bundled_grammar_metadata;
910
use serde_json::{Value, json};
1011
use sha2::{Digest, Sha256};
1112
use std::collections::{BTreeMap, BTreeSet};
1213
use std::path::Path;
14+
use std::sync::Arc;
1315
use std::sync::atomic::AtomicBool;
1416
use std::time::{Duration, Instant};
1517

1618
const MAX_FILES: usize = 64;
1719
const MAX_FRAGMENTS: usize = 64;
1820
const MAX_VISIBLE_RECOVERIES: usize = 8;
21+
const MAX_CONCURRENT_WORKERS: usize = 2;
1922

2023
/// 本轮已执行的原生语法相关结果及显式工具;仅用于判断同一源码是否可避免重复解析。
2124
pub struct NativeCoverage<'a> {
@@ -24,13 +27,29 @@ pub struct NativeCoverage<'a> {
2427
pub go_tool: Option<&'a Path>,
2528
}
2629

30+
struct CandidateJob {
31+
relative: String,
32+
source: Arc<Vec<u8>>,
33+
language: &'static str,
34+
scope: &'static str,
35+
byte_offset: usize,
36+
end_byte: usize,
37+
known_limitations: Vec<String>,
38+
}
39+
40+
enum PlannedObservation {
41+
Immediate(Value),
42+
Worker(CandidateJob),
43+
}
44+
2745
/// 对已发现源码按方言选择固定资产,受文件、片段和剩余时间限制。
28-
/// 参数为项目根、静态发现、语言选择、共同截止时间及范围稳定状态;返回不具门禁权威的报告。
46+
/// 参数为项目根、静态发现、语言选择、全局并发上限、共同截止时间及范围稳定状态;返回不具门禁权威的报告。
2947
pub fn observe(
3048
root: &Path,
3149
discovery: &DiscoveryReport,
3250
native: NativeCoverage<'_>,
3351
java_only: bool,
52+
jobs_limit: usize,
3453
deadline: Instant,
3554
skip_reason: Option<&str>,
3655
) -> Value {
@@ -44,7 +63,7 @@ pub fn observe(
4463
paths.extend(discovery.ambiguous_source_files.iter());
4564
}
4665
let source_file_count = paths.len();
47-
let mut observations = Vec::new();
66+
let observations = Vec::new();
4867
let mut skipped_count = 0;
4968
let mut unrouted_count = 0;
5069
let mut native_preferred_count = 0;
@@ -101,9 +120,10 @@ pub fn observe(
101120
)
102121
.unwrap_or_default()
103122
};
123+
let mut planned = Vec::new();
104124
for (index, relative) in paths.into_iter().enumerate() {
105125
if index >= MAX_FILES
106-
|| observations.len() >= MAX_FRAGMENTS
126+
|| planned.len() >= MAX_FRAGMENTS
107127
|| Instant::now() >= candidate_deadline
108128
|| codeguard_runtime::sigint_cancellation_requested()
109129
{
@@ -113,12 +133,12 @@ pub fn observe(
113133
let source = match read_plain_source(&root.join(relative)) {
114134
Ok(source) => source,
115135
Err(reason) => {
116-
observations.push(json!({
136+
planned.push(PlannedObservation::Immediate(json!({
117137
"path":relative,"language":null,"scope":"whole_file","byte_offset":0,
118138
"status":"candidate_unavailable","reason":reason,"grammar_qualified":false,
119139
"source_sha256":null,"grammar_sha256":null,"recovery_count":0,"recoveries":[],
120140
"known_limitations":[]
121-
}));
141+
})));
122142
continue;
123143
}
124144
};
@@ -128,67 +148,99 @@ pub fn observe(
128148
native_preferred_count += 1;
129149
continue;
130150
}
131-
let routes = route_source(relative, &source);
151+
let routes = route_source(relative, &source)
152+
.into_iter()
153+
.map(|route| {
154+
(
155+
route.language,
156+
route.scope,
157+
route.byte_offset,
158+
route.byte_offset + route.source.len(),
159+
)
160+
})
161+
.collect::<Vec<_>>();
132162
if routes.is_empty() {
133163
unrouted_count += 1;
134164
}
135-
for route in routes {
136-
let known_limitations = manifest
137-
.assets
138-
.iter()
139-
.find(|asset| asset.language == route.language)
140-
.map(|asset| asset.known_limitations.as_slice())
141-
.unwrap_or(&[]);
142-
if observations.len() >= MAX_FRAGMENTS || Instant::now() >= candidate_deadline {
165+
let source = Arc::new(source);
166+
for (language, scope, byte_offset, end_byte) in routes {
167+
if planned.len() >= MAX_FRAGMENTS || Instant::now() >= candidate_deadline {
143168
skipped_count += 1;
144169
continue;
145170
}
146-
match run_syntax_worker_candidate(
147-
&executable, route.language, relative, route.source, candidate_deadline, &cancelled,
148-
) {
149-
Ok(observation) => {
150-
if read_plain_source(&root.join(relative)).as_deref() != Ok(source.as_slice()) {
151-
observations.push(json!({
152-
"path":relative,"language":route.language,"scope":route.scope,"byte_offset":route.byte_offset,
153-
"status":"candidate_unavailable","reason":"source_changed_during_precheck","grammar_qualified":false,
154-
"source_sha256":null,"grammar_sha256":null,"recovery_count":0,"recoveries":[],
155-
"known_limitations":known_limitations
156-
}));
157-
continue;
158-
}
159-
// 嵌入片段的 worker 坐标以片段为起点,公开报告还原为文件坐标。
160-
let base_row = source[..route.byte_offset].iter().filter(|byte| **byte == b'\n').count();
161-
let base_column = source[..route.byte_offset].rsplit(|byte| *byte == b'\n').next().map_or(0, <[u8]>::len);
162-
let recovery_count = observation.recoveries.len();
163-
let recoveries: Vec<Value> = observation.recoveries.iter().take(MAX_VISIBLE_RECOVERIES).map(|recovery| {
164-
json!({
165-
"kind":recovery.kind,
166-
"syntax_kind":recovery.syntax_kind,
167-
"start_byte":route.byte_offset + recovery.start_byte,
168-
"end_byte":route.byte_offset + recovery.end_byte,
169-
"start_row":base_row + recovery.start_row,
170-
"start_column_byte":if recovery.start_row == 0 { base_column + recovery.start_column_byte } else { recovery.start_column_byte },
171-
"end_row":base_row + recovery.end_row,
172-
"end_column_byte":if recovery.end_row == 0 { base_column + recovery.end_column_byte } else { recovery.end_column_byte },
173-
})
174-
}).collect();
175-
observations.push(json!({
176-
"path":relative,"language":route.language,"scope":route.scope,"byte_offset":route.byte_offset,
177-
"status":"candidate_observed","reason":null,"grammar_qualified":false,
178-
"source_sha256":observation.source_sha256,"grammar_sha256":observation.grammar_sha256,
179-
"recovery_count":recovery_count,"recoveries":recoveries,
180-
"known_limitations":known_limitations
181-
}));
182-
}
183-
Err(reason) => observations.push(json!({
184-
"path":relative,"language":route.language,"scope":route.scope,"byte_offset":route.byte_offset,
185-
"status":"candidate_unavailable","reason":reason,"grammar_qualified":false,
186-
"source_sha256":null,"grammar_sha256":null,"recovery_count":0,"recoveries":[],
187-
"known_limitations":known_limitations
188-
})),
189-
}
171+
let known_limitations = manifest
172+
.assets
173+
.iter()
174+
.find(|asset| asset.language == language)
175+
.map(|asset| asset.known_limitations.clone())
176+
.unwrap_or_default();
177+
planned.push(PlannedObservation::Worker(CandidateJob {
178+
relative: relative.clone(),
179+
source: Arc::clone(&source),
180+
language,
181+
scope,
182+
byte_offset,
183+
end_byte,
184+
known_limitations,
185+
}));
186+
}
187+
}
188+
let mut completed: Vec<Option<Value>> = vec![None; planned.len()];
189+
let worker_indices = planned
190+
.iter()
191+
.enumerate()
192+
.filter_map(|(index, item)| matches!(item, PlannedObservation::Worker(_)).then_some(index))
193+
.collect::<Vec<_>>();
194+
for batch in worker_indices.chunks(jobs_limit.clamp(1, MAX_CONCURRENT_WORKERS)) {
195+
if Instant::now() >= candidate_deadline
196+
|| codeguard_runtime::sigint_cancellation_requested()
197+
{
198+
skipped_count += batch.len();
199+
continue;
200+
}
201+
let results = std::thread::scope(|scope| {
202+
let handles = batch
203+
.iter()
204+
.map(|index| {
205+
let PlannedObservation::Worker(job) = &planned[*index] else {
206+
unreachable!("worker index must refer to a prepared job")
207+
};
208+
scope.spawn(|| {
209+
run_syntax_worker_candidate(
210+
&executable,
211+
job.language,
212+
&job.relative,
213+
&job.source[job.byte_offset..job.end_byte],
214+
candidate_deadline,
215+
&cancelled,
216+
)
217+
})
218+
})
219+
.collect::<Vec<_>>();
220+
handles
221+
.into_iter()
222+
.map(|handle| {
223+
handle
224+
.join()
225+
.unwrap_or_else(|_| Err("syntax_worker_parent_panic".into()))
226+
})
227+
.collect::<Vec<_>>()
228+
});
229+
for (index, result) in batch.iter().copied().zip(results) {
230+
let PlannedObservation::Worker(job) = &planned[index] else {
231+
unreachable!("worker result must match a prepared job")
232+
};
233+
completed[index] = Some(candidate_result(root, job, result));
190234
}
191235
}
236+
let observations = planned
237+
.into_iter()
238+
.zip(completed)
239+
.filter_map(|(item, result)| match item {
240+
PlannedObservation::Immediate(value) => Some(value),
241+
PlannedObservation::Worker(_) => result,
242+
})
243+
.collect::<Vec<_>>();
192244
let observed = observations
193245
.iter()
194246
.any(|item| item["status"] == "candidate_observed");
@@ -221,6 +273,61 @@ pub fn observe(
221273
)
222274
}
223275

276+
fn candidate_result(
277+
root: &Path,
278+
job: &CandidateJob,
279+
result: Result<SyntaxWorkerCandidateObservation, String>,
280+
) -> Value {
281+
match result {
282+
Ok(observation) => {
283+
if read_plain_source(&root.join(&job.relative)).as_deref() != Ok(job.source.as_slice())
284+
{
285+
return json!({
286+
"path":job.relative,"language":job.language,"scope":job.scope,"byte_offset":job.byte_offset,
287+
"status":"candidate_unavailable","reason":"source_changed_during_precheck","grammar_qualified":false,
288+
"source_sha256":null,"grammar_sha256":null,"recovery_count":0,"recoveries":[],
289+
"known_limitations":job.known_limitations
290+
});
291+
}
292+
// 嵌入片段的 worker 坐标以片段为起点,公开报告还原为文件坐标。
293+
let base_row = job.source[..job.byte_offset]
294+
.iter()
295+
.filter(|byte| **byte == b'\n')
296+
.count();
297+
let base_column = job.source[..job.byte_offset]
298+
.rsplit(|byte| *byte == b'\n')
299+
.next()
300+
.map_or(0, <[u8]>::len);
301+
let recovery_count = observation.recoveries.len();
302+
let recoveries: Vec<Value> = observation.recoveries.iter().take(MAX_VISIBLE_RECOVERIES).map(|recovery| {
303+
json!({
304+
"kind":recovery.kind,
305+
"syntax_kind":recovery.syntax_kind,
306+
"start_byte":job.byte_offset + recovery.start_byte,
307+
"end_byte":job.byte_offset + recovery.end_byte,
308+
"start_row":base_row + recovery.start_row,
309+
"start_column_byte":if recovery.start_row == 0 { base_column + recovery.start_column_byte } else { recovery.start_column_byte },
310+
"end_row":base_row + recovery.end_row,
311+
"end_column_byte":if recovery.end_row == 0 { base_column + recovery.end_column_byte } else { recovery.end_column_byte },
312+
})
313+
}).collect();
314+
json!({
315+
"path":job.relative,"language":job.language,"scope":job.scope,"byte_offset":job.byte_offset,
316+
"status":"candidate_observed","reason":null,"grammar_qualified":false,
317+
"source_sha256":observation.source_sha256,"grammar_sha256":observation.grammar_sha256,
318+
"recovery_count":recovery_count,"recoveries":recoveries,
319+
"known_limitations":job.known_limitations
320+
})
321+
}
322+
Err(reason) => json!({
323+
"path":job.relative,"language":job.language,"scope":job.scope,"byte_offset":job.byte_offset,
324+
"status":"candidate_unavailable","reason":reason,"grammar_qualified":false,
325+
"source_sha256":null,"grammar_sha256":null,"recovery_count":0,"recoveries":[],
326+
"known_limitations":job.known_limitations
327+
}),
328+
}
329+
}
330+
224331
fn native_go_covers(files: &BTreeMap<String, String>, relative: &str, source: &[u8]) -> bool {
225332
relative.ends_with(".go")
226333
&& files

0 commit comments

Comments
 (0)