1use std::collections::{BTreeMap, BTreeSet};
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::time::Instant;
5
6use rusqlite::{Connection, OptionalExtension};
7
8use crate::alias::{head_tree_entries, AliasStore, GitMode};
9use crate::blob_store::{
10 BlobPlane, BlobStore, CallgraphKey, FullKey, PutOutcome, CALLGRAPH_PRODUCER_VERSION,
11};
12use crate::callgraph_store::join::CallgraphBlob;
13use crate::parser::detect_language;
14use crate::path_status::PathStatusStore;
15use crate::pins::AssemblyPin;
16
17use super::{
18 ArtifactPlane, ByteString, ClosureRequirements, Manifest, ManifestEntry, PublicationArtifacts,
19 PublicationClosure, PublicationRequest, PublishOutcome, RegularPlanes, RelPath, Result,
20 ViewError, ViewStore,
21};
22
23#[derive(Clone, Debug)]
24pub struct AssemblyRequest {
25 pub storage: PathBuf,
26 pub project_root: PathBuf,
27 pub family: String,
28 pub scope: String,
29 pub desired_head: String,
30 pub changed_paths: BTreeSet<Vec<u8>>,
31 pub semantic_keys: BTreeMap<Vec<u8>, String>,
32 pub require_semantic: bool,
33 pub allow_blob_put: bool,
34}
35
36#[derive(Clone, Debug)]
37pub struct AssemblyReport {
38 pub generation: Option<String>,
39 pub manifest: Option<Manifest>,
40 pub blob_puts: usize,
41 pub pending_paths: BTreeSet<Vec<u8>>,
42 pub published: bool,
43}
44
45struct Candidate {
46 path: RelPath,
47 entry: ManifestEntry,
48 key: Option<FullKey>,
49 payload: Option<Vec<u8>>,
50 tracked: Option<crate::alias::TrackedPath>,
51 source: Option<Vec<u8>>,
52}
53
54pub fn head_tree_fingerprint(entries: &[crate::alias::TrackedPath]) -> String {
55 let mut hasher = blake3::Hasher::new();
56 for entry in entries {
57 hasher.update(&(entry.rel_path.len() as u64).to_le_bytes());
58 hasher.update(&entry.rel_path);
59 hasher.update(entry.mode.as_bytes());
60 hasher.update(entry.git_oid.as_bytes());
61 }
62 hasher.finalize().to_hex().to_string()
63}
64
65pub fn publish_checkout(request: &AssemblyRequest) -> Result<AssemblyReport> {
66 let mut prepared = prepare_checkout(request, &mut |_| Ok(()))?;
67 prepared.commit()
68}
69
70pub struct PreparedAssembly {
73 report: AssemblyReport,
74 publication: Option<super::PreparedPublication>,
75 files: Option<(ViewStore, String)>,
76 derived_checkpoint: Option<(PathBuf, Connection)>,
77 pin: Option<AssemblyPin>,
78 _base_pin: Option<crate::pins::QueryPin>,
79 profile: PublicationProfile,
80}
81
82impl PreparedAssembly {
83 pub fn report(&self) -> &AssemblyReport {
84 &self.report
85 }
86
87 pub fn commit(&mut self) -> Result<AssemblyReport> {
88 if let Some(publication) = self.publication.take() {
89 let retires_legacy_plane = publication.base_generation.is_none();
90 let pointer_started = Instant::now();
91 let outcome = publication.commit();
92 self.profile.pointer_ms = pointer_started.elapsed().as_millis();
93 match outcome? {
94 PublishOutcome::Published => {
95 self.report.published = true;
96 self.files = None;
97 if retires_legacy_plane {
98 log::info!(
99 "views: root={} legacy plane retired at generation={}",
100 self.profile.root.display(),
101 self.report
102 .generation
103 .as_deref()
104 .expect("prepared publication has a generation")
105 );
106 }
107 if let Some((path, connection)) = self.derived_checkpoint.take() {
108 self.profile.finish();
109 super::generation::schedule_derived_checkpoint(
110 path,
111 connection,
112 self.profile.root.clone(),
113 );
114 }
115 self.profile.outcome = "published";
116 }
117 PublishOutcome::Conflict { current_generation } => {
118 self.profile.outcome = "conflict";
119 return Err(ViewError::InvalidManifest(format!(
120 "publication base changed to {current_generation:?}"
121 )));
122 }
123 }
124 }
125 self.profile.finish();
126 Ok(AssemblyReport {
127 generation: self.report.generation.take(),
128 manifest: self.report.manifest.take(),
129 blob_puts: self.report.blob_puts,
130 pending_paths: std::mem::take(&mut self.report.pending_paths),
131 published: self.report.published,
132 })
133 }
134}
135
136impl Drop for PreparedAssembly {
137 fn drop(&mut self) {
138 if let Some((view, generation)) = &self.files {
139 if view
140 .current_generation()
141 .is_ok_and(|current| current.as_deref() != Some(generation))
142 {
143 view.remove_generation_files(generation);
144 }
145 }
146 self.pin.take();
148 }
149}
150
151pub fn prepare_checkout(
152 request: &AssemblyRequest,
153 phase: &mut impl FnMut(&str) -> Result<()>,
154) -> Result<PreparedAssembly> {
155 let mut timing = super::profile::PublicationTiming::new(&request.project_root);
156 super::cache_head_fingerprint(request.project_root.clone(), request.desired_head.clone());
161 let mut profile = PublicationProfile::new(&request.project_root);
162 profile.enter(0, phase)?;
163 let view = ViewStore::open(&request.storage, &request.scope)?;
164 let current_generation = view.current_generation()?;
165 let base_pin = current_generation
166 .as_deref()
167 .map(|generation| crate::pins::QueryPin::acquire(view.view_dir(), generation))
168 .transpose()
169 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
170 let previous = current_generation
171 .as_deref()
172 .map(|generation| view.load_manifest(generation))
173 .transpose()?;
174 timing.phase("previous_manifest");
175 let head_started = Instant::now();
176 let head = head_tree_entries(&request.project_root)
177 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
178 profile.head_ms = head_started.elapsed().as_millis();
179 timing.phase("head_tree");
180 let mut callgraph = BlobStore::open(
181 &request.storage,
182 request.family.clone(),
183 BlobPlane::Callgraph,
184 )
185 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
186 let semantic = BlobStore::open(
187 &request.storage,
188 request.family.clone(),
189 BlobPlane::Semantic,
190 )
191 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
192 let mut aliases = AliasStore::open(&request.storage, &request.family)
193 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
194
195 timing.phase("store_opens");
196 let previous_entries = previous
197 .as_ref()
198 .map(|manifest| {
199 manifest
200 .entries()
201 .map(|(path, entry)| (path.as_bytes().to_vec(), entry.clone()))
202 .collect::<BTreeMap<_, _>>()
203 })
204 .unwrap_or_default();
205 let rebuild_all = request.changed_paths.is_empty();
206 let assembly_started = Instant::now();
207 let mut blocking_paths = BTreeSet::new();
208 let mut semantic_pending_paths = BTreeSet::new();
209 let mut candidates = Vec::with_capacity(head.len());
210
211 for tracked in head {
212 let rel_path = RelPath::new(tracked.rel_path.clone())?;
213 if !rebuild_all && !request.changed_paths.contains(&tracked.rel_path) {
214 if let Some(entry) = previous_entries.get(&tracked.rel_path) {
215 candidates.push(Candidate {
216 path: rel_path,
217 entry: entry.clone(),
218 key: None,
219 payload: None,
220 tracked: None,
221 source: None,
222 });
223 continue;
224 }
225 }
226
227 match tracked.mode {
228 GitMode::Gitlink => candidates.push(Candidate {
229 path: rel_path,
230 entry: ManifestEntry::Gitlink {
231 oid: tracked.git_oid.to_hex(),
232 },
233 key: None,
234 payload: None,
235 tracked: None,
236 source: None,
237 }),
238 GitMode::Symlink => {
239 let target = read_symlink_bytes(
240 &request
241 .project_root
242 .join(path_from_bytes(&tracked.rel_path)),
243 )?;
244 candidates.push(Candidate {
245 path: rel_path,
246 entry: ManifestEntry::Symlink {
247 target_bytes: ByteString::new(target),
248 },
249 key: None,
250 payload: None,
251 tracked: None,
252 source: None,
253 });
254 }
255 GitMode::Regular { executable } => {
256 let absolute = request
257 .project_root
258 .join(path_from_bytes(&tracked.rel_path));
259 let source = fs::read(&absolute)?;
260 let resolution_input = is_resolution_input(&tracked.rel_path);
261 let language = if resolution_input {
262 Some("config".to_string())
263 } else {
264 detect_language(&absolute)
265 .map(|language| format!("{language:?}").to_lowercase())
266 };
267 let (key, payload) = language
268 .as_deref()
269 .map(|language| {
270 let key = CallgraphKey::for_current(&source, language).full_key();
271 let key_hex = key.to_hex();
272 let callgraph_is_current = previous_entries
273 .get(&tracked.rel_path)
274 .is_some_and(|entry| {
275 manifest_entry_callgraph_key(entry) == Some(key_hex.as_str())
276 });
277 let payload = if callgraph_is_current {
278 None
279 } else {
280 missing_callgraph_payload(
281 &callgraph,
282 &key,
283 &source,
284 language,
285 resolution_input,
286 )?
287 };
288 Ok::<_, ViewError>((key, payload))
289 })
290 .transpose()?
291 .map_or((None, None), |(key, payload)| (Some(key), payload));
292 let callgraph_key = key.as_ref().map(FullKey::to_hex);
293 candidates.push(Candidate {
294 path: rel_path,
295 entry: ManifestEntry::Regular {
296 mode: if executable { 0o100755 } else { 0o100644 },
297 planes: RegularPlanes {
298 semantic: request.semantic_keys.get(&tracked.rel_path).cloned(),
299 callgraph: callgraph_key,
300 },
301 resolution_input,
302 },
303 key,
304 payload,
305 tracked: Some(tracked),
306 source: Some(source),
307 });
308 }
309 GitMode::Other(_) => {
310 blocking_paths.insert(tracked.rel_path);
311 }
312 }
313 }
314
315 timing.phase("candidate_assembly");
316 profile.candidates = candidates.len();
317 profile.assembly_ms = assembly_started.elapsed().as_millis();
318
319 if request.require_semantic {
320 for candidate in &candidates {
321 let missing = matches!(
322 &candidate.entry,
323 ManifestEntry::Regular { planes, .. }
324 if planes.semantic.is_none()
325 && crate::semantic_index::is_semantic_indexed_extension(
326 &request
327 .project_root
328 .join(path_from_bytes(candidate.path.as_bytes()))
329 )
330 );
331 if missing {
332 semantic_pending_paths.insert(candidate.path.as_bytes().to_vec());
333 }
334 }
335 }
336
337 let keys = candidates
338 .iter()
339 .filter_map(|candidate| candidate.key.clone())
340 .collect::<Vec<_>>();
341 let next_generation = next_generation(current_generation.as_deref(), &request.desired_head);
342 profile.generation = next_generation.clone();
343 let pin = AssemblyPin::create(
344 view.view_dir(),
345 request.family.clone(),
346 request.scope.clone(),
347 next_generation.clone(),
348 &keys,
349 )
350 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
351 let mut prepared = PreparedAssembly {
352 report: AssemblyReport {
353 generation: current_generation.clone(),
354 manifest: previous.clone(),
355 blob_puts: 0,
356 pending_paths: BTreeSet::new(),
357 published: false,
358 },
359 publication: None,
360 files: Some((view.clone(), next_generation.clone())),
361 derived_checkpoint: None,
362 pin: Some(pin),
363 _base_pin: base_pin,
364 profile,
365 };
366 prepared.profile.enter(1, phase)?;
367 timing.phase("assembly_pin");
368 let mut blob_puts = 0;
369 for candidate in &candidates {
370 let (Some(key), Some(payload)) = (&candidate.key, &candidate.payload) else {
371 continue;
372 };
373 if request.allow_blob_put {
374 prepared
375 .pin
376 .as_mut()
377 .expect("assembly pin")
378 .renew_if_due()
379 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
380 let put = callgraph
381 .put(key, payload)
382 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
383 blob_puts += usize::from(matches!(put.outcome, PutOutcome::Inserted));
384 if let (Some(tracked), Some(source)) = (&candidate.tracked, &candidate.source) {
385 aliases
386 .seed_proven_alias(tracked, source)
387 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
388 }
389 } else if callgraph
390 .get(key)
391 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?
392 .is_none()
393 {
394 blocking_paths.insert(candidate.path.as_bytes().to_vec());
395 }
396 }
397
398 timing.phase("blob_and_alias_puts");
399 prepared.profile.blob_puts = blob_puts;
400 prepared.profile.pending_paths = blocking_paths.len() + semantic_pending_paths.len();
401 let mut status = PathStatusStore::open(view.view_dir())
402 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
403 for path in blocking_paths.iter().chain(&semantic_pending_paths) {
404 status
405 .mark_pending(
406 path,
407 if blocking_paths.contains(path) {
408 "shared callgraph blob unavailable"
409 } else {
410 "shared semantic blob unavailable"
411 },
412 generation_number(&next_generation),
413 )
414 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
415 }
416 if !blocking_paths.is_empty() {
417 prepared.profile.outcome = "pending";
418 prepared.report.blob_puts = blob_puts;
419 prepared.report.pending_paths = blocking_paths
420 .union(&semantic_pending_paths)
421 .cloned()
422 .collect();
423 return Ok(prepared);
424 }
425 for candidate in &candidates {
426 if !semantic_pending_paths.contains(candidate.path.as_bytes()) {
427 status
428 .clear(candidate.path.as_bytes())
429 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
430 }
431 }
432 timing.phase("path_status");
433 prepared.report.pending_paths = semantic_pending_paths;
434
435 let manifest = Manifest::new(
436 candidates
437 .into_iter()
438 .map(|candidate| (candidate.path, candidate.entry)),
439 )?;
440 prepared.profile.semantic_fill = previous.as_ref().is_some_and(|base| {
441 base.entries
442 .iter()
443 .map(|(path, entry)| (path, manifest_entry_callgraph_key(entry)))
444 .eq(manifest
445 .entries
446 .iter()
447 .map(|(path, entry)| (path, manifest_entry_callgraph_key(entry))))
448 && base != &manifest
449 });
450 if current_generation
456 .as_deref()
457 .is_some_and(|generation| generation.ends_with(&request.desired_head))
458 && previous.as_ref() == Some(&manifest)
459 {
460 prepared.profile.outcome = "no_op";
461 prepared.report.manifest = None;
462 prepared.report.blob_puts = blob_puts;
463 return Ok(prepared);
464 }
465 prepared.profile.enter(2, phase)?;
466 let reused_derived = previous
467 .as_ref()
468 .is_some_and(|base| super::materialization::manifest_callgraph_equivalent(base, &manifest))
469 && current_generation
470 .as_deref()
471 .is_some_and(|base| view.derived_path(base).is_ok_and(|path| path.is_file()));
472 if reused_derived {
473 view.reuse_derived(
474 &next_generation,
475 current_generation.as_deref().expect("reused base"),
476 )?;
477 }
478 prepared.profile.io.enter(super::io::Phase::Clone);
479 let derived = view.derived_path(&next_generation)?;
480 let mut cloned_base = false;
481 if !reused_derived {
482 let clone_started = Instant::now();
483 if let Some(base) = current_generation.as_deref() {
484 let base_path = view.derived_path(base)?;
485 if base_path.is_file() {
486 super::generation::clone_derived(&base_path, &derived)?;
487 cloned_base = true;
488 }
489 }
490 prepared.profile.derived_clone_ms = clone_started.elapsed().as_millis();
491 let derived_keeper = Connection::open(&derived)?;
494 derived_keeper.busy_timeout(std::time::Duration::from_secs(5))?;
495 derived_keeper.pragma_update(None, "journal_mode", "WAL")?;
499 derived_keeper.query_row("SELECT COUNT(*) FROM sqlite_schema", [], |row| {
500 row.get::<_, i64>(0)
501 })?;
502 prepared.derived_checkpoint = Some((derived.clone(), derived_keeper));
503 prepared.profile.io.enter(super::io::Phase::Materialize);
504 let materialization_started = Instant::now();
505 let derived_manifest = current_generation
506 .as_deref()
507 .filter(|_| cloned_base)
508 .map(|base| {
509 view.derived_owner(base)
510 .and_then(|owner| view.load_manifest(&owner))
511 })
512 .transpose()?;
513 if let Some(base_manifest) = derived_manifest.as_ref() {
514 let (stats, timings) = super::materialization::apply_manifest_diff_profiled(
515 &derived,
516 base_manifest,
517 &manifest,
518 callgraph.path(),
519 )
520 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
521 prepared.profile.materialization = timings;
522 log::info!(
523 "view manifest diff: generation={} stats={stats:?}",
524 next_generation
525 );
526 } else {
527 crate::callgraph_store::materialize_manifest_view_database(
528 &derived,
529 callgraph.path(),
530 &manifest,
531 )
532 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
533 }
534 prepared.profile.materialization_call_ms = materialization_started.elapsed().as_millis();
535 }
536 prepared.profile.io.enter(super::io::Phase::DerivedOther);
537 let trigram = view.trigram_path(&next_generation)?;
538 fs::write(&trigram, [])?;
539 let artifacts = PublicationArtifacts {
540 blob_databases: if reused_derived {
541 vec![semantic.path().to_path_buf()]
542 } else {
543 vec![
544 semantic.path().to_path_buf(),
545 callgraph.path().to_path_buf(),
546 ]
547 },
548 derived_database: derived.clone(),
549 trigram_artifact: trigram.clone(),
550 alias_database: aliases.path().to_path_buf(),
551 };
552 let closure = SqliteClosure {
553 semantic: semantic.path().to_path_buf(),
554 callgraph: callgraph.path().to_path_buf(),
555 trigram,
556 connections: Default::default(),
557 };
558 prepared.profile.io.enter(super::io::Phase::Closure);
559 let closure_started = Instant::now();
560 let publication = view.prepare_with_reused_derived(
561 &PublicationRequest {
562 generation: &next_generation,
563 base_generation: current_generation.as_deref(),
564 manifest: &manifest,
565 artifacts,
566 closure_requirements: ClosureRequirements::default(),
567 },
568 &closure,
569 if reused_derived {
570 previous.as_ref()
571 } else {
572 None
573 },
574 )?;
575 prepared.profile.closure_ms = closure_started.elapsed().as_millis();
576 prepared.profile.derived_bytes = fs::metadata(&derived)
577 .map(|metadata| metadata.len())
578 .unwrap_or(0);
579 prepared.profile.enter(3, phase)?;
580 prepared.publication = Some(publication);
581 prepared.report = AssemblyReport {
582 generation: Some(next_generation),
583 manifest: Some(manifest),
584 blob_puts,
585 pending_paths: prepared.report.pending_paths.clone(),
586 published: false,
587 };
588 Ok(prepared)
589}
590
591struct PublicationProfile {
595 root: PathBuf,
596 io: super::io::PublicationIo,
597 overlap: super::io::Overlap,
598 concurrent_publications: u64,
599 semantic_fill: bool,
600 generation: String,
601 outcome: &'static str,
602 candidates: usize,
603 blob_puts: usize,
604 pending_paths: usize,
605 head_ms: u128,
606 assembly_ms: u128,
607 pointer_ms: u128,
608 phase_ms: [u128; 4],
609 active_phase: Option<usize>,
610 phase_started: Instant,
611 started: Instant,
612 total_ms: u128,
613 derived_bytes: u64,
614 derived_clone_ms: u128,
615 materialization_call_ms: u128,
616 closure_ms: u128,
617 materialization: super::materialization::profile::PhaseTimings,
618}
619
620impl PublicationProfile {
621 fn new(root: &Path) -> Self {
622 let now = Instant::now();
623 Self {
624 root: root.to_owned(),
625 io: super::io::PublicationIo::new(),
626 overlap: super::io::Overlap::new(true),
627 concurrent_publications: 0,
628 semantic_fill: false,
629 generation: "none".into(),
630 outcome: "cancelled_or_failed",
631 candidates: 0,
632 blob_puts: 0,
633 pending_paths: 0,
634 head_ms: 0,
635 assembly_ms: 0,
636 pointer_ms: 0,
637 phase_ms: [0; 4],
638 active_phase: Some(0),
639 phase_started: now,
640 started: now,
641 total_ms: 0,
642 derived_bytes: 0,
643 derived_clone_ms: 0,
644 materialization_call_ms: 0,
645 closure_ms: 0,
646 materialization: super::materialization::profile::PhaseTimings::default(),
647 }
648 }
649
650 fn enter(&mut self, phase: usize, callback: &mut impl FnMut(&str) -> Result<()>) -> Result<()> {
651 self.checkpoint();
652 self.io.enter(
653 [
654 super::io::Phase::Manifest,
655 super::io::Phase::Blobs,
656 super::io::Phase::DerivedOther,
657 super::io::Phase::Cas,
658 ][phase],
659 );
660 self.active_phase = Some(phase);
661 self.phase_started = Instant::now();
662 callback(["manifest", "blobs", "derived", "cas"][phase])
663 }
664
665 fn checkpoint(&mut self) {
666 if let Some(phase) = self.active_phase.take() {
667 self.phase_ms[phase] += self.phase_started.elapsed().as_millis();
668 }
669 }
670
671 fn finish(&mut self) {
672 if self.active_phase.is_none() {
673 return;
674 }
675 self.io.finish();
676 self.concurrent_publications = self.overlap.finish();
677 self.checkpoint();
678 self.total_ms = self.started.elapsed().as_millis();
679 }
680}
681
682impl Drop for PublicationProfile {
683 fn drop(&mut self) {
684 if self.active_phase.is_some() {
685 self.finish();
686 }
687 log_publication_profile(self);
688 }
689}
690
691fn log_publication_profile(profile: &PublicationProfile) {
692 crate::slog_info!("{}", publication_profile_line(profile));
693}
694
695fn publication_profile_line(profile: &PublicationProfile) -> String {
696 format!(
700 "index_event kind=view_publication plane=views root={} outcome={} candidates={} blob_puts={} pending_paths={} manifest_ms={} blobs_ms={} derived_ms={} cas_ms={} head_ms={} assembly_ms={} blob_ms={} materialize_ms={} derived_clone_ms={} materialization_call_ms={} closure_ms={} materialize_load_bindings_select_ms={} materialize_delete_rows_ms={} materialize_owned_blob_decode_insert_ms={} materialize_join_load_payloads_ms={} materialize_join_decode_bind_index_entries_ms={} materialize_join_index_surface_replay_ms={} materialize_join_decode_resolved_callers_ms={} materialize_join_resolve_record_ms={} materialize_join_dependency_union_ms={} materialize_selected_join_ms={} materialize_write_bindings_ms={} materialize_emit_refs_edges_ms={} materialize_commit_ms={} materialize_cleanup_memory_ms={} materialize_cleanup_connections_ms={} pointer_ms={} total_ms={} derived_bytes={} semantic_fill={} generation={} concurrent_publications={} {}",
701 profile.root.display(), profile.outcome, profile.candidates, profile.blob_puts,
702 profile.pending_paths, profile.phase_ms[0], profile.phase_ms[1],
703 profile.phase_ms[2], profile.phase_ms[3], profile.head_ms, profile.assembly_ms,
704 profile.phase_ms[1], profile.materialization_call_ms,
705 profile.derived_clone_ms,
706 profile.materialization_call_ms,
707 profile.closure_ms,
708 profile.materialization.load_bindings_select_ms,
709 profile.materialization.delete_rows_ms,
710 profile.materialization.owned_blob_decode_and_insert_ms,
711 profile.materialization.join_load_payloads_ms,
712 profile.materialization.join_decode_bind_index_entries_ms,
713 profile.materialization.join_index_and_surface_replay_ms,
714 profile.materialization.join_decode_resolved_callers_ms,
715 profile.materialization.join_resolve_and_record_ms,
716 profile.materialization.join_dependency_union_ms,
717 profile.materialization.selected_join_ms,
718 profile.materialization.write_bindings_ms,
719 profile.materialization.emit_refs_edges_ms,
720 profile.materialization.commit_ms,
721 profile.materialization.cleanup_memory_ms,
722 profile.materialization.cleanup_connections_ms,
723 profile.pointer_ms, profile.total_ms, profile.derived_bytes,
724 profile.semantic_fill, profile.generation, profile.concurrent_publications, profile.io.fields(),
725 )
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731
732 #[test]
733 fn publication_profile_line_attributes_the_canonical_root() {
734 let mut profile = PublicationProfile::new(Path::new("/checkout"));
735 profile.outcome = "published";
736 profile.phase_ms = [5, 6, 7, 8];
737 profile.derived_clone_ms = 9;
738 profile.materialization_call_ms = 10;
739 profile.closure_ms = 11;
740 profile.materialization = crate::views::materialization::profile::PhaseTimings {
741 load_bindings_select_ms: 11,
742 delete_rows_ms: 12,
743 owned_blob_decode_and_insert_ms: 13,
744 join_load_payloads_ms: 14,
745 join_decode_bind_index_entries_ms: 15,
746 join_index_and_surface_replay_ms: 16,
747 join_decode_resolved_callers_ms: 17,
748 join_resolve_and_record_ms: 18,
749 join_dependency_union_ms: 19,
750 selected_join_ms: 20,
751 write_bindings_ms: 21,
752 emit_refs_edges_ms: 22,
753 commit_ms: 23,
754 cleanup_memory_ms: 24,
755 cleanup_connections_ms: 25,
756 };
757 let line = publication_profile_line(&profile);
758 assert!(line.contains("plane=views root=/checkout outcome=published"));
759 assert!(line.contains("manifest_ms=5 blobs_ms=6 derived_ms=7 cas_ms=8"));
760 assert!(line.contains("derived_clone_ms=9 materialization_call_ms=10 closure_ms=11"));
761 assert!(line.contains("blob_ms=6 materialize_ms=10"));
762 assert!(line.contains(
763 "materialize_load_bindings_select_ms=11 materialize_delete_rows_ms=12 \
764 materialize_owned_blob_decode_insert_ms=13 materialize_join_load_payloads_ms=14 \
765 materialize_join_decode_bind_index_entries_ms=15 \
766 materialize_join_index_surface_replay_ms=16 \
767 materialize_join_decode_resolved_callers_ms=17 \
768 materialize_join_resolve_record_ms=18 materialize_join_dependency_union_ms=19 \
769 materialize_selected_join_ms=20 materialize_write_bindings_ms=21 \
770 materialize_emit_refs_edges_ms=22 materialize_commit_ms=23 \
771 materialize_cleanup_memory_ms=24 materialize_cleanup_connections_ms=25"
772 ));
773 assert!(line.contains("io_scope=process io_available="));
774 assert!(line.contains("manifest_physical_bytes_written="));
775 assert!(line.contains("closure_logical_bytes_written="));
776 assert!(line.contains("total_bytes_read="));
777 assert!(line.contains("semantic_fill=false generation=none concurrent_publications=0"));
778 assert_eq!(line.matches("index_event kind=view_publication").count(), 1);
779 }
780}
781
782fn manifest_entry_callgraph_key(entry: &ManifestEntry) -> Option<&str> {
783 match entry {
784 ManifestEntry::Regular { planes, .. } => planes.callgraph.as_deref(),
785 ManifestEntry::Synthetic { planes, .. } => Some(&planes.callgraph),
786 ManifestEntry::Symlink { .. } | ManifestEntry::Gitlink { .. } => None,
787 }
788}
789
790pub(super) fn is_resolution_input(path: &[u8]) -> bool {
791 let name = path.rsplit(|byte| *byte == b'/').next().unwrap_or(path);
792 name == b"package.json"
793 || name == b"Cargo.toml"
794 || name == b".gitignore"
795 || name.starts_with(b"tsconfig") && name.ends_with(b".json")
796}
797
798fn next_generation(current: Option<&str>, desired_head: &str) -> String {
799 let generation = current
800 .map(generation_number)
801 .unwrap_or(0)
802 .saturating_add(1);
803 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
804 let serial = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
805 let nanos = std::time::SystemTime::now()
806 .duration_since(std::time::UNIX_EPOCH)
807 .unwrap_or_default()
808 .as_nanos();
809 format!(
810 "{generation}-{}-{nanos}-{serial}-{desired_head}",
811 std::process::id()
812 )
813}
814
815fn generation_number(generation: &str) -> u64 {
816 generation
817 .split_once('-')
818 .and_then(|(number, _)| number.parse().ok())
819 .unwrap_or(0)
820}
821
822struct SqliteClosure {
823 semantic: PathBuf,
824 callgraph: PathBuf,
825 trigram: PathBuf,
826 connections: std::cell::RefCell<BTreeMap<bool, crate::db::lifecycle::TrackedConnection>>,
829}
830
831impl PublicationClosure for SqliteClosure {
832 fn contains_blob(&self, plane: ArtifactPlane, full_key: &str) -> Result<bool> {
833 let Some(key) = decode_hex(full_key) else {
834 return Ok(false);
835 };
836 let path = match plane {
837 ArtifactPlane::Semantic => &self.semantic,
838 ArtifactPlane::Callgraph => &self.callgraph,
839 };
840 let mut connections = self.connections.borrow_mut();
841 let semantic = plane == ArtifactPlane::Semantic;
842 if let std::collections::btree_map::Entry::Vacant(entry) = connections.entry(semantic) {
843 entry.insert(crate::db::lifecycle::TrackedConnection::open(
844 path,
845 crate::db::lifecycle::SqliteStore::BlobStore,
846 )?);
847 }
848 let present = connections[&semantic]
849 .prepare_cached(
850 "SELECT 1 FROM blob_payloads INDEXED BY blob_membership WHERE full_key = ?1",
851 )?
852 .query_row([key], |_| Ok(()))
853 .optional()?
854 .is_some();
855 Ok(present)
856 }
857
858 fn probe_blobs(&self, keys: &[(ArtifactPlane, &str)]) -> Result<()> {
859 let mut present = BTreeSet::new();
860 for (plane_id, plane) in [ArtifactPlane::Semantic, ArtifactPlane::Callgraph]
861 .into_iter()
862 .enumerate()
863 {
864 let wanted = keys
865 .iter()
866 .filter(|(p, _)| *p == plane)
867 .map(|(_, key)| *key)
868 .collect::<BTreeSet<_>>()
869 .into_iter()
870 .collect::<Vec<_>>();
871 for chunk in wanted.chunks(500) {
872 let Some(first_valid) = chunk.iter().find(|key| decode_hex(key).is_some()) else {
875 continue;
876 };
877 self.contains_blob(plane, first_valid)?;
878 let connections = self.connections.borrow();
879 let Some(connection) = connections.get(&(plane == ArtifactPlane::Semantic)) else {
880 continue;
881 };
882 let decoded = chunk
883 .iter()
884 .filter_map(|key| decode_hex(key))
885 .collect::<Vec<_>>();
886 if decoded.is_empty() {
887 continue;
888 }
889 let sql = membership_query(decoded.len());
890 let mut statement = connection.prepare_cached(&sql)?;
891 for key in statement.query_map(rusqlite::params_from_iter(&decoded), |row| {
892 row.get::<_, Vec<u8>>(0)
893 })? {
894 present.insert((plane_id, key?));
895 }
896 }
897 }
898 for &(plane, key) in keys {
901 let plane_id = usize::from(plane == ArtifactPlane::Callgraph);
902 if decode_hex(key).is_none_or(|key| !present.contains(&(plane_id, key))) {
903 return Err(ViewError::MissingBlob {
904 plane,
905 key: key.to_owned(),
906 });
907 }
908 }
909 Ok(())
910 }
911
912 fn trigram_is_present(&self) -> Result<bool> {
913 Ok(self.trigram.is_file())
914 }
915
916 fn contains_alias(&self, _git_oid: &str) -> Result<bool> {
917 Ok(true)
918 }
919}
920
921fn missing_callgraph_payload(
922 store: &BlobStore,
923 key: &FullKey,
924 source: &[u8],
925 language: &str,
926 resolution_input: bool,
927) -> Result<Option<Vec<u8>>> {
928 if store
932 .get(key)
933 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?
934 .is_some()
935 {
936 return Ok(None);
937 }
938 let blob = if resolution_input {
939 CallgraphBlob::config(source.to_vec(), CALLGRAPH_PRODUCER_VERSION)
940 } else {
941 CallgraphBlob::extract(
942 std::str::from_utf8(source)
943 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?,
944 language,
945 CALLGRAPH_PRODUCER_VERSION,
946 )
947 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?
948 };
949 Ok(Some(blob.to_bytes().map_err(|error| {
950 ViewError::InvalidManifest(error.to_string())
951 })?))
952}
953
954#[cfg(test)]
955mod reuse_tests {
956 use super::*;
957
958 #[test]
959 fn views_corrupt_cached_payload_is_not_reused() {
960 let dir = tempfile::tempdir().unwrap();
961 let mut store = BlobStore::open(dir.path(), "corrupt", BlobPlane::Callgraph).unwrap();
962 let source = b"export function target() {}";
963 let key = CallgraphKey::for_current(source, "typescript").full_key();
964 let payload = missing_callgraph_payload(&store, &key, source, "typescript", false)
965 .unwrap()
966 .unwrap();
967 store.put(&key, &payload).unwrap();
968 Connection::open(store.path())
969 .unwrap()
970 .execute("UPDATE blob_payloads SET payload_digest = zeroblob(32)", [])
971 .unwrap();
972 assert!(
973 missing_callgraph_payload(&store, &key, source, "typescript", false)
974 .unwrap()
975 .is_some()
976 );
977 }
978
979 #[test]
980 fn views_cached_callgraph_payload_does_not_extract_again() {
981 let dir = tempfile::tempdir().unwrap();
982 let mut store = BlobStore::open(dir.path(), "reuse", BlobPlane::Callgraph).unwrap();
983 let source = b"export function target() {}";
984 let key = CallgraphKey::for_current(source, "typescript").full_key();
985 let payload = missing_callgraph_payload(&store, &key, source, "typescript", false)
986 .unwrap()
987 .unwrap();
988 store.put(&key, &payload).unwrap();
989 assert!(
990 missing_callgraph_payload(&store, &key, source, "typescript", false)
991 .unwrap()
992 .is_none(),
993 "cached content must not be extracted or put again"
994 );
995 }
996}
997
998fn membership_query(count: usize) -> String {
999 format!(
1000 "SELECT full_key FROM blob_payloads INDEXED BY blob_membership WHERE full_key IN ({})",
1001 vec!["?"; count].join(",")
1002 )
1003}
1004
1005fn decode_hex(value: &str) -> Option<Vec<u8>> {
1006 if value.len() != 64 {
1007 return None;
1008 }
1009 (0..value.len())
1010 .step_by(2)
1011 .map(|index| u8::from_str_radix(&value[index..index + 2], 16).ok())
1012 .collect()
1013}
1014
1015fn path_from_bytes(bytes: &[u8]) -> PathBuf {
1016 #[cfg(unix)]
1017 {
1018 use std::os::unix::ffi::OsStringExt as _;
1019 PathBuf::from(std::ffi::OsString::from_vec(bytes.to_vec()))
1020 }
1021 #[cfg(not(unix))]
1022 {
1023 PathBuf::from(String::from_utf8_lossy(bytes).into_owned())
1024 }
1025}
1026
1027fn read_symlink_bytes(path: &Path) -> Result<Vec<u8>> {
1028 let target = fs::read_link(path)?;
1029 #[cfg(unix)]
1030 {
1031 use std::os::unix::ffi::OsStrExt as _;
1032 Ok(target.as_os_str().as_bytes().to_vec())
1033 }
1034 #[cfg(not(unix))]
1035 {
1036 Ok(target.to_string_lossy().as_bytes().to_vec())
1037 }
1038}
1039
1040#[cfg(test)]
1041#[path = "closure_connection_tests.rs"]
1042mod closure_connection_tests;
1043
1044#[cfg(test)]
1045#[path = "semantic_fill_tests.rs"]
1046mod semantic_fill_tests;