code-moniker-workspace 0.7.1

Workspace model, ports, snapshots, linkage, and change analysis for code-moniker.
Documentation
use std::time::{Duration, Instant};

use rayon::prelude::*;

use crate::linkage::binding::LinkageMemoryMetrics;
use crate::linkage::binding::ReferenceLinkageDecision;
use crate::linkage::binding::{
	LinkageStore, LinkageStoreRefresh, insert_reference_ordinals, reference_indexes,
};
use crate::linkage::catalog::CandidateCatalog;
use crate::linkage::catalog::ReferenceLocations;
use crate::linkage::catalog::{ReferenceOrdinal, ReferenceSet};
use crate::linkage::change::{BindingReadModel, EditedGraph, RebindScope};
use crate::linkage::change::{LinkageRefreshImpact, SymbolDelta, changes_c_include_topology};
use crate::linkage::resolve::BindingForwards;
use crate::linkage::resolve::LinkagePolicies;
use crate::linkage::resolve::LinkageRefiner;
use crate::linkage::resolve::ManifestPolicy;
use crate::linkage::resolve::MethodIndexer;
use crate::linkage::resolve::ReferenceResolver;
use crate::linkage::resolve::WorkspacePackageIndex;
use crate::linkage::resolve::run_full_linkage_with_timings;
use crate::linkage::source_groups::SourceGroupPolicy;
use crate::linkage::{LinkageRefreshTimings, LocalLinkage, TimedLinkageRefresh};
use crate::snapshot::{
	CodeIndex, LinkageSnapshot, RecordTable, ReferenceId, ReferenceRecord, ResourceGeneration,
	WorkspaceResult,
};
use crate::source::CodeIndexMaterial;

pub(in crate::linkage) fn run_refresh_linkage_with_timings(
	linkage: &mut LocalLinkage,
	previous: &LinkageSnapshot,
	code_index: &CodeIndex,
	refresh_impact: LinkageRefreshImpact,
) -> WorkspaceResult<TimedLinkageRefresh> {
	let total_timer = Instant::now();
	if refresh_impact.is_empty() {
		let memory = linkage.memory;
		return Ok(refresh_empty_linkage(
			&mut linkage.store,
			previous,
			code_index,
			memory,
			total_timer,
		));
	}
	let material = linkage.linkage_material(code_index)?;
	if changes_c_include_topology(&refresh_impact, &material) {
		let full = run_full_linkage_with_timings(linkage, code_index)?;
		return Ok(TimedLinkageRefresh {
			snapshot: full.snapshot,
			timings: LinkageRefreshTimings {
				candidate_index: full.timings.candidate_index,
				plan_invalidation: full.timings.manifest_policy,
				resolve_references: full.timings.resolve_references,
				semantic_refinement: full.timings.semantic_refinement,
				rebuild_indexes: full.timings.store_index,
				project_snapshot: full.timings.project_snapshot,
				total: full.timings.total,
				stale_refs: code_index.references.len(),
				changed_refs: code_index.references.len(),
				..LinkageRefreshTimings::default()
			},
			memory: full.memory,
		});
	}
	let generation = linkage.cache.next_generation();
	let candidate_timer = Instant::now();
	let candidates = match linkage.candidates.as_mut() {
		Some(candidates) => {
			candidates.refresh_files(
				&material,
				std::sync::Arc::clone(code_index.inventory.catalog()),
			);
			candidates
		}
		None => linkage.candidates.get_or_insert_with(|| {
			CandidateCatalog::new(
				&material,
				std::sync::Arc::clone(code_index.inventory.catalog()),
			)
		}),
	};
	let candidates = &*candidates;
	let mut candidate_index = candidate_timer.elapsed();
	if linkage.store.is_none() {
		linkage.store = Some(LinkageStore::from_snapshot(
			previous,
			&code_index.references,
			&material,
			candidates,
		));
	}
	let Some(store) = linkage.store.as_mut() else {
		panic!("linkage store is initialized before refresh");
	};
	let method_timer = Instant::now();
	let indexer = linkage
		.method_indexer
		.get_or_insert_with(|| MethodIndexer::new(&material, candidates));
	candidate_index += method_timer.elapsed();
	let input = IncrementalLinkageInput {
		index: code_index,
		impact: refresh_impact,
		material: &material,
		generation,
	};
	let refresh = run_incremental_refresh(
		RefreshExecution {
			store,
			indexer,
			candidates,
			previous,
		},
		&input,
		candidate_index,
		total_timer,
	);
	linkage.memory = refresh.memory;
	Ok(refresh)
}

fn refresh_empty_linkage(
	store: &mut Option<LinkageStore>,
	previous: &LinkageSnapshot,
	code_index: &CodeIndex,
	memory: LinkageMemoryMetrics,
	total_timer: Instant,
) -> TimedLinkageRefresh {
	if let Some(store) = store {
		store.advance_index_generation(code_index.generation);
	}
	let project_timer = Instant::now();
	let mut snapshot = previous.clone();
	snapshot.index_generation = code_index.generation;
	TimedLinkageRefresh {
		snapshot,
		timings: LinkageRefreshTimings {
			project_snapshot: project_timer.elapsed(),
			total: total_timer.elapsed(),
			..LinkageRefreshTimings::default()
		},
		memory,
	}
}

struct IncrementalLinkageInput<'a> {
	index: &'a CodeIndex,
	impact: LinkageRefreshImpact,
	material: &'a CodeIndexMaterial,
	generation: ResourceGeneration,
}

struct RefreshExecution<'a> {
	store: &'a mut LinkageStore,
	indexer: &'a mut MethodIndexer,
	candidates: &'a CandidateCatalog,
	previous: &'a LinkageSnapshot,
}

fn run_incremental_refresh(
	execution: RefreshExecution<'_>,
	input: &IncrementalLinkageInput<'_>,
	candidate_index_elapsed: Duration,
	total_timer: Instant,
) -> TimedLinkageRefresh {
	let RefreshExecution {
		store,
		indexer,
		candidates,
		previous,
	} = execution;
	let mut timings = LinkageRefreshTimings {
		candidate_index: candidate_index_elapsed,
		..LinkageRefreshTimings::default()
	};
	let decisions_unchanged =
		refresh_incremental_linkage(store, indexer, input, candidates, &mut timings);
	let project_timer = Instant::now();
	let snapshot = if decisions_unchanged {
		let mut snapshot = previous.clone();
		snapshot.generation = input.generation;
		snapshot.index_generation = input.index.generation;
		snapshot
	} else {
		store.project_snapshot(
			&input.index.references,
			&input.material.identity,
			candidates.symbols(),
		)
	};
	let memory = store.memory_metrics(candidates.symbols());
	timings.project_snapshot = project_timer.elapsed();
	timings.total = total_timer.elapsed();
	TimedLinkageRefresh {
		snapshot,
		timings,
		memory,
	}
}

fn refresh_incremental_linkage(
	store: &mut LinkageStore,
	indexer: &mut MethodIndexer,
	input: &IncrementalLinkageInput<'_>,
	candidates: &CandidateCatalog,
	timings: &mut LinkageRefreshTimings,
) -> bool {
	let plan_timer = Instant::now();
	let positions_stable = input.impact.references().id_remaps().is_empty()
		&& input.impact.references().removed_ids().is_empty()
		&& store.indexes.reference_indexes.len() == input.index.references.len();
	if positions_stable {
		insert_reference_ordinals(
			store,
			input.impact.references().changed_ids(),
			&input.index.references,
			input.material,
		);
	} else {
		store.rebase_reference_ordinals(
			reference_indexes(&input.index.references),
			input.impact.references().id_remaps(),
			input.impact.references().removed_ids(),
		);
	}
	store.ensure_resolved_target_index(input.material, candidates.symbols());
	let RebindScope {
		stale_references,
		target_index_references,
		changed_files,
	} = RebindScope::plan(
		BindingReadModel {
			store,
			inventory: &input.index.inventory,
			reference_indexes: &store.indexes.reference_indexes,
		},
		EditedGraph {
			references: &input.index.references,
			material: input.material,
			candidates,
		},
		&input.impact,
	);
	timings.plan_invalidation = plan_timer.elapsed();
	timings.stale_refs = stale_references.len() as usize;
	let changed_reference_indexes = stale_reference_indexes(&stale_references);
	timings.changed_refs = changed_reference_indexes.len();
	let locations = (!changed_reference_indexes.is_empty())
		.then(|| ReferenceLocations::from_material(input.material));
	let refresh_policies = locations
		.as_ref()
		.map(|_| RefreshPolicies::build(input.material));
	let resolve_timer = Instant::now();
	let changed = match (&locations, &refresh_policies) {
		(Some(locations), Some(policies)) => resolve_reference_decisions(
			input,
			&changed_reference_indexes,
			candidates,
			locations,
			policies,
		),
		(None, None) => Vec::new(),
		_ => unreachable!("locations and policies are built together"),
	};
	timings.resolve_references = resolve_timer.elapsed();
	let apply_timer = Instant::now();
	store.apply_refresh(LinkageStoreRefresh {
		generation: input.generation,
		index_generation: input.index.generation,
		stale_references: &stale_references,
		changed_decisions: changed,
		references: &input.index.references,
		material: input.material,
	});
	timings.apply_store = apply_timer.elapsed();
	if changed_reference_indexes.is_empty() {
		let symbol_ids_stable = matches!(
			input.impact.definitions(),
			SymbolDelta::Unchanged | SymbolDelta::AdditiveOnly { .. }
		);
		return positions_stable && symbol_ids_stable && stale_references.is_empty();
	}
	let method_timer = Instant::now();
	let methods = indexer.reindex(input.material, candidates, &changed_files);
	timings.candidate_index += method_timer.elapsed();
	let refinement_timer = Instant::now();
	let stale_reference_ids = reference_ids_for_set(&stale_references, &input.index.references);
	let locations = locations.unwrap_or_else(|| ReferenceLocations::from_material(input.material));
	let Some(refresh_policies) = refresh_policies.as_ref() else {
		unreachable!("changed references always build refresh policies");
	};
	LinkageRefiner::new(
		input.material,
		methods,
		candidates,
		&locations,
		crate::linkage::resolve::RefinementPolicies::new(
			&refresh_policies.source_groups,
			&refresh_policies.packages,
			&refresh_policies.manifests,
		),
	)
	.refine_changed(
		store.decisions_mut(),
		&input.index.references,
		&stale_reference_ids,
	);
	timings.semantic_refinement = refinement_timer.elapsed();
	let rebuild_timer = Instant::now();
	store.refresh_resolved_target_index(
		&target_index_references,
		input.material,
		candidates.symbols(),
	);
	timings.rebuild_indexes = rebuild_timer.elapsed();
	false
}

struct RefreshPolicies {
	manifests: ManifestPolicy,
	source_groups: SourceGroupPolicy,
	packages: WorkspacePackageIndex,
}

impl RefreshPolicies {
	fn build(material: &CodeIndexMaterial) -> Self {
		Self {
			manifests: ManifestPolicy::build(material),
			source_groups: SourceGroupPolicy::build(material),
			packages: WorkspacePackageIndex::build(material),
		}
	}
}

fn resolve_reference_decisions(
	input: &IncrementalLinkageInput<'_>,
	reference_indexes: &[usize],
	candidates: &CandidateCatalog,
	locations: &ReferenceLocations,
	refresh_policies: &RefreshPolicies,
) -> Vec<ReferenceLinkageDecision> {
	let forwards = BindingForwards::build(input.material, &refresh_policies.manifests);
	let java_on_demand = crate::linkage::resolve::JavaOnDemandImports::build(input.material);
	let policies = LinkagePolicies {
		candidates,
		manifests: &refresh_policies.manifests,
		source_groups: &refresh_policies.source_groups,
		packages: &refresh_policies.packages,
		forwards: &forwards,
		java_on_demand: &java_on_demand,
	};
	let resolver = ReferenceResolver::new(input.material, &policies);
	indexes_to_references(input.index, reference_indexes)
		.par_iter()
		.map(|(reference_idx, reference)| {
			resolver.resolve_reference(*reference_idx, reference, locations.get(*reference_idx))
		})
		.collect::<Vec<_>>()
}

fn stale_reference_indexes(stale_references: &ReferenceSet) -> Vec<usize> {
	stale_references
		.iter()
		.map(ReferenceOrdinal::index)
		.collect()
}

fn reference_ids_for_set(
	references: &ReferenceSet,
	records: &RecordTable<ReferenceRecord>,
) -> rustc_hash::FxHashSet<ReferenceId> {
	references
		.iter()
		.filter_map(|reference| records.get(reference.index()))
		.map(|reference| reference.id)
		.collect()
}

fn indexes_to_references<'a>(
	index: &'a CodeIndex,
	reference_indexes: &[usize],
) -> Vec<(usize, &'a ReferenceRecord)> {
	reference_indexes
		.iter()
		.filter_map(|reference_idx| {
			index
				.references
				.get(*reference_idx)
				.map(|reference| (*reference_idx, reference))
		})
		.collect()
}