1use std::{path::PathBuf, thread};
2
3use crate::domain::{
4 CodeIndexBatch, CodeIndexResourceBudget, CodeIndexSession, CodeRepositoryRegistration,
5 CodeRepositorySelector, code_snapshot_scope_id,
6};
7
8use super::{
9 CodeIndexError,
10 changes::{GitTreeEntry, tracked_entries},
11 git::{git_batch_blobs, resolve_ref, resolve_tree},
12 identity, parse_indexed_file,
13 scope::{load_ignore_rules_from_commit, selection_exclusion_reason},
14 snapshot::SnapshotBuild,
15};
16
17const GIT_BLOB_FETCH_GROUP: usize = CodeIndexResourceBudget::DEFAULT_MAX_FILES_PER_BATCH;
18
19#[derive(Debug, Clone)]
21pub struct CodeIndexPlan {
22 registration: CodeRepositoryRegistration,
23 selector: CodeRepositorySelector,
24 root: PathBuf,
25 commit: String,
26 tree_hash: String,
27 source_scope: String,
28 path_filters: Vec<String>,
29 language_filters: Vec<String>,
30 paths: Vec<GitTreeEntry>,
31 cursor: usize,
32 next_batch_index: usize,
33 resource_budget: CodeIndexResourceBudget,
34}
35
36impl CodeIndexPlan {
37 pub fn session(&self) -> CodeIndexSession {
39 CodeIndexSession {
40 repository_id: self.registration.repository_id.clone(),
41 source_scope: self.source_scope.clone(),
42 base_resolved_commit_sha: None,
43 resolved_commit_sha: self.commit.clone(),
44 tree_hash: self.tree_hash.clone(),
45 path_filters: self.path_filters.clone(),
46 language_filters: self.language_filters.clone(),
47 full_replace: true,
48 total_path_count: self.paths.len(),
49 changed_path_count: self.paths.len(),
50 skipped_unchanged_count: 0,
51 deleted_paths: Vec::new(),
52 tombstones: Vec::new(),
53 resource_budget: self.resource_budget,
54 }
55 }
56
57 pub fn parse_next_batch(mut self) -> Result<(Self, Option<CodeIndexBatch>), CodeIndexError> {
59 if self.cursor >= self.paths.len() {
60 return Ok((self, None));
61 }
62
63 let mut build = SnapshotBuild::new_with_selector(
64 &self.registration,
65 &self.selector,
66 self.commit.clone(),
67 self.tree_hash.clone(),
68 true,
69 self.paths.len(),
70 0,
71 );
72 let mut parsed_bytes = 0usize;
73 while self.cursor < self.paths.len() {
74 let fetch_end = next_fetch_end(&self, build.files.len(), parsed_bytes);
75 if fetch_end == self.cursor {
76 break;
77 }
78 let fetched_paths = self.paths[self.cursor..fetch_end]
79 .iter()
80 .map(|entry| entry.path.clone())
81 .collect::<Vec<_>>();
82 let blobs = git_batch_blobs(&self.root, &self.commit, &fetched_paths)?;
83 let parsed_files = parse_fetched_files(&self, &fetched_paths, &blobs)?;
84 for (bytes, parsed_file) in blobs.iter().zip(parsed_files) {
85 parsed_bytes = parsed_bytes.saturating_add(bytes.len());
86 build.append_file_records(parsed_file);
87 self.cursor += 1;
88
89 if !build.files.is_empty()
90 && (build.files.len() >= self.resource_budget.max_files_per_batch
91 || parsed_bytes >= self.resource_budget.max_bytes_per_batch
92 || batch_row_count(&build) >= self.resource_budget.max_rows_per_batch)
93 {
94 break;
95 }
96 }
97 if !build.files.is_empty()
98 && (build.files.len() >= self.resource_budget.max_files_per_batch
99 || parsed_bytes >= self.resource_budget.max_bytes_per_batch
100 || batch_row_count(&build) >= self.resource_budget.max_rows_per_batch)
101 {
102 break;
103 }
104 }
105 identity::enrich_symbol_identities(&build.repository_id, &mut build.symbols);
106
107 let batch = CodeIndexBatch {
108 repository_id: build.repository_id,
109 source_scope: build.source_scope,
110 batch_index: self.next_batch_index,
111 parsed_byte_count: parsed_bytes,
112 files: build.files,
113 symbols: build.symbols,
114 references: build.references,
115 imports: build.imports,
116 chunks: build.chunks,
117 diagnostics: build.diagnostics,
118 };
119 self.next_batch_index += 1;
120
121 Ok((self, Some(batch)))
122 }
123}
124
125fn parse_fetched_files(
126 plan: &CodeIndexPlan,
127 paths: &[String],
128 blobs: &[Vec<u8>],
129) -> Result<Vec<SnapshotBuild>, CodeIndexError> {
130 let worker_count = worker_count(paths.len());
131 if paths.len() <= 1 || worker_count <= 1 {
132 return paths
133 .iter()
134 .zip(blobs.iter())
135 .map(|(path, bytes)| parse_one_file(plan, path, bytes))
136 .collect();
137 }
138
139 let mut parsed = thread::scope(|scope| {
140 let handles = (0..worker_count)
141 .map(|worker_index| {
142 scope.spawn(move || {
143 parse_worker_stride(plan, paths, blobs, worker_index, worker_count)
144 })
145 })
146 .collect::<Vec<_>>();
147 let mut parsed = Vec::with_capacity(paths.len());
148 for handle in handles {
149 let worker_output = handle.join().map_err(|_| {
150 CodeIndexError::InvalidInput("code parser worker panicked".to_owned())
151 })??;
152 parsed.extend(worker_output);
153 }
154
155 Ok::<_, CodeIndexError>(parsed)
156 })?;
157 parsed.sort_by_key(|(index, _)| *index);
158
159 Ok(parsed.into_iter().map(|(_, build)| build).collect())
160}
161
162fn parse_one_file(
163 plan: &CodeIndexPlan,
164 path: &str,
165 bytes: &[u8],
166) -> Result<SnapshotBuild, CodeIndexError> {
167 let mut build = SnapshotBuild::new_with_selector(
168 &plan.registration,
169 &plan.selector,
170 plan.commit.clone(),
171 plan.tree_hash.clone(),
172 true,
173 plan.paths.len(),
174 0,
175 );
176 parse_indexed_file(&mut build, path, bytes)?;
177
178 Ok(build)
179}
180
181fn parse_worker_stride(
182 plan: &CodeIndexPlan,
183 paths: &[String],
184 blobs: &[Vec<u8>],
185 worker_index: usize,
186 worker_count: usize,
187) -> Result<Vec<(usize, SnapshotBuild)>, CodeIndexError> {
188 let mut parsed = Vec::new();
189 let mut index = worker_index;
190 while index < paths.len() {
191 parsed.push((index, parse_one_file(plan, &paths[index], &blobs[index])?));
192 index += worker_count;
193 }
194
195 Ok(parsed)
196}
197
198fn worker_count(item_count: usize) -> usize {
199 thread::available_parallelism()
200 .map(usize::from)
201 .unwrap_or(1)
202 .min(item_count)
203}
204
205pub fn prepare_full_index_plan(
207 registration: CodeRepositoryRegistration,
208 selector: CodeRepositorySelector,
209 resource_budget: CodeIndexResourceBudget,
210) -> Result<CodeIndexPlan, CodeIndexError> {
211 let root = PathBuf::from(®istration.root_path);
212 let commit = resolve_ref(&root, &selector.ref_selector)?;
213 let tree_hash = resolve_tree(&root, &commit)?;
214 let ignore_rules = load_ignore_rules_from_commit(&root, &commit)?;
215 let paths = tracked_entries(&root, &commit)?
216 .into_iter()
217 .filter(|entry| {
218 selection_exclusion_reason(&entry.path, ®istration, &selector, &ignore_rules)
219 .is_none()
220 })
221 .collect::<Vec<_>>();
222 let path_filters = merged_filters(®istration.path_filters, &selector.path_filters);
223 let language_filters =
224 merged_filters(®istration.language_filters, &selector.language_filters);
225 let source_scope = code_snapshot_scope_id(
226 ®istration.repository_id,
227 &tree_hash,
228 &path_filters,
229 &language_filters,
230 );
231
232 Ok(CodeIndexPlan {
233 registration,
234 selector,
235 root,
236 commit,
237 tree_hash,
238 source_scope,
239 path_filters,
240 language_filters,
241 paths,
242 cursor: 0,
243 next_batch_index: 1,
244 resource_budget,
245 })
246}
247
248fn next_fetch_end(plan: &CodeIndexPlan, batch_file_count: usize, parsed_bytes: usize) -> usize {
249 let remaining_files = plan
250 .resource_budget
251 .max_files_per_batch
252 .saturating_sub(batch_file_count)
253 .max(1);
254 let file_limited_end = plan.paths.len().min(
255 plan.cursor
256 .saturating_add(GIT_BLOB_FETCH_GROUP.min(remaining_files)),
257 );
258 let remaining_bytes = plan
259 .resource_budget
260 .max_bytes_per_batch
261 .saturating_sub(parsed_bytes);
262 let mut byte_count = 0usize;
263 let mut end = plan.cursor;
264 while end < file_limited_end {
265 let entry_bytes = plan.paths[end].byte_count;
266 if end > plan.cursor && byte_count.saturating_add(entry_bytes) > remaining_bytes {
267 break;
268 }
269 byte_count = byte_count.saturating_add(entry_bytes);
270 end += 1;
271 }
272
273 if end == plan.cursor && batch_file_count == 0 {
274 return (plan.cursor + 1).min(plan.paths.len());
275 }
276
277 end
278}
279
280fn batch_row_count(build: &SnapshotBuild) -> usize {
281 build
282 .files
283 .len()
284 .saturating_add(build.symbols.len())
285 .saturating_add(build.references.len())
286 .saturating_add(build.imports.len())
287 .saturating_add(build.chunks.len())
288 .saturating_add(build.diagnostics.len())
289}
290
291fn merged_filters(left: &[String], right: &[String]) -> Vec<String> {
292 let mut merged = Vec::new();
293 for value in left.iter().chain(right.iter()) {
294 if !merged.contains(value) {
295 merged.push(value.clone());
296 }
297 }
298
299 merged
300}