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 = Vec::with_capacity(paths.len());
140 for (path_chunk, blob_chunk) in paths.chunks(worker_count).zip(blobs.chunks(worker_count)) {
141 let chunk = thread::scope(|scope| {
142 let handles = path_chunk
143 .iter()
144 .zip(blob_chunk.iter())
145 .map(|(path, bytes)| scope.spawn(move || parse_one_file(plan, path, bytes)))
146 .collect::<Vec<_>>();
147 handles
148 .into_iter()
149 .map(|handle| {
150 handle.join().map_err(|_| {
151 CodeIndexError::InvalidInput("code parser worker panicked".to_owned())
152 })?
153 })
154 .collect::<Result<Vec<_>, _>>()
155 })?;
156 parsed.extend(chunk);
157 }
158
159 Ok(parsed)
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 worker_count(item_count: usize) -> usize {
182 thread::available_parallelism()
183 .map(usize::from)
184 .unwrap_or(1)
185 .min(item_count)
186}
187
188pub fn prepare_full_index_plan(
190 registration: CodeRepositoryRegistration,
191 selector: CodeRepositorySelector,
192 resource_budget: CodeIndexResourceBudget,
193) -> Result<CodeIndexPlan, CodeIndexError> {
194 let root = PathBuf::from(®istration.root_path);
195 let commit = resolve_ref(&root, &selector.ref_selector)?;
196 let tree_hash = resolve_tree(&root, &commit)?;
197 let ignore_rules = load_ignore_rules_from_commit(&root, &commit)?;
198 let paths = tracked_entries(&root, &commit)?
199 .into_iter()
200 .filter(|entry| {
201 selection_exclusion_reason(&entry.path, ®istration, &selector, &ignore_rules)
202 .is_none()
203 })
204 .collect::<Vec<_>>();
205 let path_filters = merged_filters(®istration.path_filters, &selector.path_filters);
206 let language_filters =
207 merged_filters(®istration.language_filters, &selector.language_filters);
208 let source_scope = code_snapshot_scope_id(
209 ®istration.repository_id,
210 &tree_hash,
211 &path_filters,
212 &language_filters,
213 );
214
215 Ok(CodeIndexPlan {
216 registration,
217 selector,
218 root,
219 commit,
220 tree_hash,
221 source_scope,
222 path_filters,
223 language_filters,
224 paths,
225 cursor: 0,
226 next_batch_index: 1,
227 resource_budget,
228 })
229}
230
231fn next_fetch_end(plan: &CodeIndexPlan, batch_file_count: usize, parsed_bytes: usize) -> usize {
232 let remaining_files = plan
233 .resource_budget
234 .max_files_per_batch
235 .saturating_sub(batch_file_count)
236 .max(1);
237 let file_limited_end = plan.paths.len().min(
238 plan.cursor
239 .saturating_add(GIT_BLOB_FETCH_GROUP.min(remaining_files)),
240 );
241 let remaining_bytes = plan
242 .resource_budget
243 .max_bytes_per_batch
244 .saturating_sub(parsed_bytes);
245 let mut byte_count = 0usize;
246 let mut end = plan.cursor;
247 while end < file_limited_end {
248 let entry_bytes = plan.paths[end].byte_count;
249 if end > plan.cursor && byte_count.saturating_add(entry_bytes) > remaining_bytes {
250 break;
251 }
252 byte_count = byte_count.saturating_add(entry_bytes);
253 end += 1;
254 }
255
256 if end == plan.cursor && batch_file_count == 0 {
257 return (plan.cursor + 1).min(plan.paths.len());
258 }
259
260 end
261}
262
263fn batch_row_count(build: &SnapshotBuild) -> usize {
264 build
265 .files
266 .len()
267 .saturating_add(build.symbols.len())
268 .saturating_add(build.references.len())
269 .saturating_add(build.imports.len())
270 .saturating_add(build.chunks.len())
271 .saturating_add(build.diagnostics.len())
272}
273
274fn merged_filters(left: &[String], right: &[String]) -> Vec<String> {
275 let mut merged = Vec::new();
276 for value in left.iter().chain(right.iter()) {
277 if !merged.contains(value) {
278 merged.push(value.clone());
279 }
280 }
281
282 merged
283}