@@ -4,18 +4,21 @@ use crate::discovery::DiscoveryReport;
44use crate :: go_lint_command:: selected_sources_for_candidate;
55use crate :: grammar_probe_command:: read_plain_source;
66use crate :: grammar_route:: route_source;
7+ use crate :: syntax_worker_candidate_observation:: SyntaxWorkerCandidateObservation ;
78use crate :: syntax_worker_runner:: run_syntax_worker_candidate;
89use codeguard_adapters:: bundled_grammar_metadata;
910use serde_json:: { Value , json} ;
1011use sha2:: { Digest , Sha256 } ;
1112use std:: collections:: { BTreeMap , BTreeSet } ;
1213use std:: path:: Path ;
14+ use std:: sync:: Arc ;
1315use std:: sync:: atomic:: AtomicBool ;
1416use std:: time:: { Duration , Instant } ;
1517
1618const MAX_FILES : usize = 64 ;
1719const MAX_FRAGMENTS : usize = 64 ;
1820const MAX_VISIBLE_RECOVERIES : usize = 8 ;
21+ const MAX_CONCURRENT_WORKERS : usize = 2 ;
1922
2023/// 本轮已执行的原生语法相关结果及显式工具;仅用于判断同一源码是否可避免重复解析。
2124pub 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+ /// 参数为项目根、静态发现、语言选择、全局并发上限、 共同截止时间及范围稳定状态;返回不具门禁权威的报告。
2947pub 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+
224331fn native_go_covers ( files : & BTreeMap < String , String > , relative : & str , source : & [ u8 ] ) -> bool {
225332 relative. ends_with ( ".go" )
226333 && files
0 commit comments