1use std::collections::{BTreeMap, BTreeSet};
9
10use serde::{Deserialize, Serialize};
11
12use crate::{ArtifactDataSegment, ArtifactFingerprint, ArtifactIr, ArtifactSymbol};
13
14pub const DEFAULT_MIN_DUPLICATE_DATA_BYTES: u64 = 16;
20
21const MAX_SHARED_DEPENDENCY_ROOTS: usize = 1024;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(transparent)]
34pub struct EstimatedRefactorSavingsBytes(pub i64);
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(transparent)]
42pub struct VerifiedSavingsBytes(pub i64);
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct DuplicateReport {
47 pub exact: Vec<DuplicateGroup>,
49 pub normalized: Vec<DuplicateGroup>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct SizeClassification {
60 pub observed_bytes: u64,
62 pub duplicated_bytes: u64,
64 pub retained_bytes: Option<u64>,
66 pub shared_dependency_bytes: Option<u64>,
68 pub duplicated_data_bytes: Option<u64>,
71 pub upper_bound_savings_bytes: Option<u64>,
76 pub estimated_refactor_savings_bytes: Option<EstimatedRefactorSavingsBytes>,
78 pub verified_savings_bytes: Option<VerifiedSavingsBytes>,
80 pub clone_confidence: EvidenceConfidence,
84 pub savings_confidence: EvidenceConfidence,
87 pub assumptions: Vec<String>,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "kebab-case")]
94pub enum EvidenceConfidence {
95 High,
97 Medium,
99 Low,
101 Unavailable,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct DeadCodeReport {
108 pub symbols: Vec<ArtifactFingerprint>,
110 pub definitive: bool,
112 pub assumptions: Vec<String>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct RetainedSize {
119 pub symbol: ArtifactFingerprint,
121 pub retained_bytes: u64,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct DuplicateGroup {
128 pub fingerprint: ArtifactFingerprint,
130 pub duplicated_bytes: u64,
134 pub members: Vec<DuplicateMember>,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141pub struct DuplicateMember {
142 pub symbol: ArtifactFingerprint,
144 pub offset: u64,
146 pub size: u64,
148}
149
150#[must_use]
152pub fn find_duplicates(artifact: &ArtifactIr) -> DuplicateReport {
153 let exact = groups(&artifact.symbols, |symbol| {
154 Some(("exact", symbol.code.as_slice()))
155 });
156 let normalized = if artifact.capabilities.normalized_duplicates {
157 groups(&artifact.symbols, |symbol| {
158 symbol.normalized.as_ref().map(|normalized| {
159 (normalized.version.as_str(), normalized.bytes.as_slice())
162 })
163 })
164 } else {
165 Vec::new()
166 };
167 DuplicateReport { exact, normalized }
168}
169
170#[must_use]
175pub fn find_duplicate_data(artifact: &ArtifactIr, min_bytes: u64) -> Vec<DuplicateGroup> {
176 if !artifact.capabilities.independent_data_segments {
177 return Vec::new();
178 }
179 groups_data(&artifact.data_segments, min_bytes)
180}
181
182#[must_use]
184pub fn classify_sizes(artifact: &ArtifactIr) -> SizeClassification {
185 let duplicates = find_duplicates(artifact);
186 let duplicate_data = find_duplicate_data(artifact, DEFAULT_MIN_DUPLICATE_DATA_BYTES);
187 classify_sizes_from_duplicates(artifact, &duplicates, &duplicate_data)
188}
189
190#[must_use]
193pub fn classify_sizes_from_duplicates(
194 artifact: &ArtifactIr,
195 duplicates: &DuplicateReport,
196 duplicate_data: &[DuplicateGroup],
197) -> SizeClassification {
198 let duplicated_bytes = duplicates
199 .exact
200 .iter()
201 .map(|group| group.duplicated_bytes)
202 .sum();
203 let duplicated_data_bytes = artifact.capabilities.independent_data_segments.then(|| {
204 duplicate_data
205 .iter()
206 .map(|group| group.duplicated_bytes)
207 .sum()
208 });
209 let mut assumptions = vec![
210 "upper_bound_savings_bytes is not a guaranteed reduction".to_owned(),
211 "estimated_refactor_savings_bytes needs source-artifact mapping".to_owned(),
212 ];
213 if duplicated_data_bytes.is_none() {
214 assumptions
215 .push("duplicated_data_bytes needs independently established data regions".to_owned());
216 }
217 let graph_sizes = resolved_graph(artifact);
218 if graph_sizes.is_none() {
219 assumptions
220 .push("retained and shared dependency sizes need a resolved call graph".to_owned());
221 }
222 let (retained_bytes, shared_dependency_bytes) = graph_sizes.map_or((None, None), |graph| {
223 let retained_bytes = graph
224 .reachable
225 .iter()
226 .map(|symbol| graph.sizes[symbol])
227 .sum();
228 let mut root_reach_counts: BTreeMap<ArtifactFingerprint, u64> = BTreeMap::new();
229 for root in &graph.roots {
230 for symbol in reachable_from(BTreeSet::from([*root]), &graph.successors) {
231 *root_reach_counts.entry(symbol).or_default() += 1;
232 }
233 }
234 let shared_dependency_bytes = root_reach_counts
235 .into_iter()
236 .filter(|(_, count)| *count > 1)
237 .map(|(symbol, _)| graph.sizes[&symbol])
238 .sum();
239 (Some(retained_bytes), Some(shared_dependency_bytes))
240 });
241 SizeClassification {
242 observed_bytes: artifact.observed_bytes,
243 duplicated_bytes,
244 retained_bytes,
245 shared_dependency_bytes,
246 duplicated_data_bytes,
247 upper_bound_savings_bytes: Some(duplicated_bytes),
248 estimated_refactor_savings_bytes: None,
249 verified_savings_bytes: None,
250 clone_confidence: EvidenceConfidence::High,
251 savings_confidence: EvidenceConfidence::Unavailable,
252 assumptions,
253 }
254}
255
256#[must_use]
262pub fn dead_code_candidates(artifact: &ArtifactIr) -> Option<DeadCodeReport> {
263 if !artifact.capabilities.call_graph {
264 return None;
265 }
266 let mut reachable: BTreeSet<ArtifactFingerprint> = artifact
267 .symbols
268 .iter()
269 .filter(|symbol| symbol.exported)
270 .map(|symbol| symbol.fingerprint)
271 .collect();
272 reachable.extend(artifact.entry_points.iter().copied());
273 reachable.extend(artifact.indirect_references.iter().copied());
274 if reachable.is_empty() {
275 return None;
276 }
277 loop {
278 let before = reachable.len();
279 for call in &artifact.calls {
280 if reachable.contains(&call.caller) {
281 if let Some(target) = call.target {
282 reachable.insert(target);
283 }
284 }
285 }
286 if reachable.len() == before {
287 break;
288 }
289 }
290 let unresolved = artifact.calls.iter().any(|call| call.unresolved.is_some());
291 let mut symbols: Vec<_> = artifact
292 .symbols
293 .iter()
294 .map(|symbol| symbol.fingerprint)
295 .filter(|fingerprint| !reachable.contains(fingerprint))
296 .collect();
297 symbols.sort();
298 symbols.dedup();
299 Some(DeadCodeReport {
300 symbols,
301 definitive: !unresolved,
302 assumptions: if unresolved {
303 vec!["unresolved dispatch prevents proving unreachable symbols are dead".to_owned()]
304 } else {
305 vec!["all recorded call edges were resolved locally".to_owned()]
306 },
307 })
308}
309
310#[must_use]
322pub fn retained_sizes(artifact: &ArtifactIr) -> Option<Vec<RetainedSize>> {
323 let graph = resolved_graph(artifact)?;
324 let symbols: Vec<_> = graph.reachable.iter().copied().collect();
325 let index: BTreeMap<_, _> = symbols
326 .iter()
327 .enumerate()
328 .map(|(position, symbol)| (*symbol, position + 1))
329 .collect();
330 let mut successors = vec![Vec::new(); symbols.len() + 1];
331 successors[0] = graph.roots.iter().map(|root| index[root]).collect();
332 for (caller, targets) in &graph.successors {
333 if !graph.reachable.contains(caller) {
334 continue;
335 }
336 for target in targets {
337 if graph.reachable.contains(target) {
338 successors[index[caller]].push(index[target]);
339 }
340 }
341 }
342
343 let (dfs_vertices, parents) = depth_first_tree(&successors);
344 let mut dfs_index = vec![None; successors.len()];
345 for (position, vertex) in dfs_vertices.iter().copied().enumerate() {
346 dfs_index[vertex] = Some(position);
347 }
348 let mut predecessors = vec![Vec::new(); dfs_vertices.len()];
349 for (vertex, edges) in successors.iter().enumerate() {
350 let Some(from) = dfs_index[vertex] else {
351 continue;
352 };
353 for target in edges {
354 if let Some(to) = dfs_index[*target] {
355 predecessors[to].push(from);
356 }
357 }
358 }
359 let immediate = lengauer_tarjan(&predecessors, &parents);
360 let mut retained = dfs_vertices
361 .iter()
362 .map(|vertex| {
363 if *vertex == 0 {
364 0
365 } else {
366 graph.sizes[&symbols[*vertex - 1]]
367 }
368 })
369 .collect::<Vec<_>>();
370 for node in (1..retained.len()).rev() {
371 if let Some(parent) = immediate[node] {
372 retained[parent] = retained[parent].saturating_add(retained[node]);
373 }
374 }
375 let mut result: Vec<_> = dfs_vertices
376 .iter()
377 .enumerate()
378 .skip(1)
379 .map(|(position, vertex)| RetainedSize {
380 symbol: symbols[*vertex - 1],
381 retained_bytes: retained[position],
382 })
383 .collect();
384 result.sort_by(|left, right| {
385 right
386 .retained_bytes
387 .cmp(&left.retained_bytes)
388 .then_with(|| left.symbol.cmp(&right.symbol))
389 });
390 Some(result)
391}
392
393fn depth_first_tree(successors: &[Vec<usize>]) -> (Vec<usize>, Vec<Option<usize>>) {
395 let mut vertices = vec![0];
396 let mut parents = vec![None];
397 let mut index = vec![None; successors.len()];
398 index[0] = Some(0);
399 let mut stack = vec![(0usize, 0usize)];
400 while let Some((vertex, next_edge)) = stack.last_mut() {
401 if *next_edge == successors[*vertex].len() {
402 stack.pop();
403 continue;
404 }
405 let target = successors[*vertex][*next_edge];
406 *next_edge += 1;
407 if index[target].is_some() {
408 continue;
409 }
410 let Some(parent) = index[*vertex] else {
411 continue;
412 };
413 index[target] = Some(vertices.len());
414 vertices.push(target);
415 parents.push(Some(parent));
416 stack.push((target, 0));
417 }
418 (vertices, parents)
419}
420
421fn lengauer_tarjan(predecessors: &[Vec<usize>], parents: &[Option<usize>]) -> Vec<Option<usize>> {
423 let nodes = predecessors.len();
424 let mut semi: Vec<_> = (0..nodes).collect();
425 let mut labels: Vec<_> = (0..nodes).collect();
426 let mut ancestors = vec![None; nodes];
427 let mut buckets = vec![Vec::new(); nodes];
428 let mut immediate = vec![None; nodes];
429
430 for node in (1..nodes).rev() {
431 for predecessor in &predecessors[node] {
432 let candidate = lt_eval(*predecessor, &mut ancestors, &mut labels, &semi);
433 semi[node] = semi[node].min(semi[candidate]);
434 }
435 buckets[semi[node]].push(node);
436 let Some(parent) = parents[node] else {
437 continue;
438 };
439 ancestors[node] = Some(parent);
440 for member in std::mem::take(&mut buckets[parent]) {
441 let candidate = lt_eval(member, &mut ancestors, &mut labels, &semi);
442 immediate[member] = Some(if semi[candidate] < semi[member] {
443 candidate
444 } else {
445 parent
446 });
447 }
448 }
449 for node in 1..nodes {
450 let Some(parent) = immediate[node] else {
451 continue;
452 };
453 if parent != semi[node] {
454 immediate[node] = immediate[parent];
455 }
456 }
457 immediate
458}
459
460fn lt_eval(
462 node: usize,
463 ancestors: &mut [Option<usize>],
464 labels: &mut [usize],
465 semi: &[usize],
466) -> usize {
467 if ancestors[node].is_none() {
468 return node;
469 }
470 lt_compress(node, ancestors, labels, semi);
471 labels[node]
472}
473
474fn lt_compress(node: usize, ancestors: &mut [Option<usize>], labels: &mut [usize], semi: &[usize]) {
476 let mut path = Vec::new();
477 let mut current = node;
478 while let Some(parent) = ancestors[current] {
479 if ancestors[parent].is_none() {
480 break;
481 }
482 path.push(current);
483 current = parent;
484 }
485 for current in path.into_iter().rev() {
486 let Some(parent) = ancestors[current] else {
487 continue;
488 };
489 if semi[labels[parent]] < semi[labels[current]] {
490 labels[current] = labels[parent];
491 }
492 ancestors[current] = ancestors[parent];
493 }
494}
495
496struct ResolvedGraph {
498 sizes: BTreeMap<ArtifactFingerprint, u64>,
499 roots: BTreeSet<ArtifactFingerprint>,
500 reachable: BTreeSet<ArtifactFingerprint>,
501 successors: BTreeMap<ArtifactFingerprint, Vec<ArtifactFingerprint>>,
502}
503
504fn resolved_graph(artifact: &ArtifactIr) -> Option<ResolvedGraph> {
505 if !artifact.capabilities.call_graph
506 || artifact.calls.iter().any(|call| call.unresolved.is_some())
507 {
508 return None;
509 }
510 let mut sizes = BTreeMap::new();
511 for symbol in &artifact.symbols {
512 if sizes.insert(symbol.fingerprint, symbol.size).is_some() {
513 return None;
514 }
515 }
516 let roots: BTreeSet<_> = artifact
517 .symbols
518 .iter()
519 .filter(|symbol| symbol.exported)
520 .map(|symbol| symbol.fingerprint)
521 .chain(artifact.entry_points.iter().copied())
522 .chain(artifact.indirect_references.iter().copied())
523 .collect();
524 if roots.is_empty()
525 || roots.len() > MAX_SHARED_DEPENDENCY_ROOTS
526 || !roots.iter().all(|root| sizes.contains_key(root))
527 {
528 return None;
529 }
530 if artifact.calls.iter().any(|call| {
531 !sizes.contains_key(&call.caller)
532 || !call
533 .target
534 .is_some_and(|target| sizes.contains_key(&target))
535 }) {
536 return None;
537 }
538 let mut successors: BTreeMap<_, Vec<_>> = sizes
539 .keys()
540 .copied()
541 .map(|symbol| (symbol, Vec::new()))
542 .collect();
543 for call in &artifact.calls {
544 if let Some(target) = call.target {
545 successors.entry(call.caller).or_default().push(target);
546 }
547 }
548 for targets in successors.values_mut() {
549 targets.sort_unstable();
550 targets.dedup();
551 }
552 let reachable = reachable_from(roots.clone(), &successors);
553 Some(ResolvedGraph {
554 sizes,
555 roots,
556 reachable,
557 successors,
558 })
559}
560
561fn reachable_from(
562 mut reachable: BTreeSet<ArtifactFingerprint>,
563 successors: &BTreeMap<ArtifactFingerprint, Vec<ArtifactFingerprint>>,
564) -> BTreeSet<ArtifactFingerprint> {
565 let mut pending: Vec<_> = reachable.iter().copied().collect();
566 while let Some(symbol) = pending.pop() {
567 if let Some(targets) = successors.get(&symbol) {
568 for target in targets {
569 if reachable.insert(*target) {
570 pending.push(*target);
571 }
572 }
573 }
574 }
575 reachable
576}
577
578fn groups<'a>(
579 symbols: &'a [ArtifactSymbol],
580 key: impl Fn(&'a ArtifactSymbol) -> Option<(&'a str, &'a [u8])>,
581) -> Vec<DuplicateGroup> {
582 let mut buckets: BTreeMap<(&str, &[u8]), Vec<&ArtifactSymbol>> = BTreeMap::new();
583 for symbol in symbols {
584 let Some((version, content)) = key(symbol) else {
585 continue;
586 };
587 buckets.entry((version, content)).or_default().push(symbol);
588 }
589 let mut result: Vec<DuplicateGroup> = buckets
590 .into_iter()
591 .filter(|(_, members)| members.len() > 1)
592 .map(|((version, content), members)| group(version, content, members))
593 .collect();
594 result.sort_by(|left, right| {
595 right
596 .duplicated_bytes
597 .cmp(&left.duplicated_bytes)
598 .then_with(|| left.fingerprint.cmp(&right.fingerprint))
599 });
600 result
601}
602
603fn group(version: &str, content: &[u8], symbols: Vec<&ArtifactSymbol>) -> DuplicateGroup {
604 let mut members: Vec<DuplicateMember> = symbols
605 .into_iter()
606 .map(|symbol| DuplicateMember {
607 symbol: symbol.fingerprint,
608 offset: symbol.offset,
609 size: symbol.size,
610 })
611 .collect();
612 members.sort_by_key(|member| (member.offset, member.symbol));
613 let total = members.iter().map(|member| member.size).sum::<u64>();
614 let canonical = members.iter().map(|member| member.size).max().unwrap_or(0);
615 DuplicateGroup {
616 fingerprint: group_fingerprint(version, content),
617 duplicated_bytes: total.saturating_sub(canonical),
618 members,
619 }
620}
621
622fn groups_data(segments: &[ArtifactDataSegment], min_bytes: u64) -> Vec<DuplicateGroup> {
623 let mut buckets: BTreeMap<&[u8], Vec<&ArtifactDataSegment>> = BTreeMap::new();
624 for segment in segments {
625 if segment.bytes.len() as u64 >= min_bytes {
626 buckets
627 .entry(segment.bytes.as_slice())
628 .or_default()
629 .push(segment);
630 }
631 }
632 let mut result: Vec<DuplicateGroup> = buckets
633 .into_iter()
634 .filter(|(_, members)| members.len() > 1)
635 .map(|(bytes, segments)| {
636 let mut members: Vec<DuplicateMember> = segments
637 .into_iter()
638 .map(|segment| DuplicateMember {
639 symbol: segment.fingerprint,
640 offset: segment.offset,
641 size: segment.bytes.len() as u64,
642 })
643 .collect();
644 members.sort_by_key(|member| (member.offset, member.symbol));
645 let total = members.iter().map(|member| member.size).sum::<u64>();
646 let canonical = members.iter().map(|member| member.size).max().unwrap_or(0);
647 DuplicateGroup {
648 fingerprint: group_fingerprint("data-exact", bytes),
649 duplicated_bytes: total.saturating_sub(canonical),
650 members,
651 }
652 })
653 .collect();
654 result.sort_by(|left, right| {
655 right
656 .duplicated_bytes
657 .cmp(&left.duplicated_bytes)
658 .then_with(|| left.fingerprint.cmp(&right.fingerprint))
659 });
660 result
661}
662
663fn group_fingerprint(version: &str, content: &[u8]) -> ArtifactFingerprint {
664 let mut identity = Vec::new();
665 identity.extend((version.len() as u64).to_le_bytes());
666 identity.extend(version.as_bytes());
667 identity.extend(content);
668 ArtifactFingerprint::from_content("artifact-duplicate-group", &identity)
669}
670
671#[cfg(test)]
672#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
673mod tests {
674 use super::*;
675 use crate::{ArtifactDataSegment, ArtifactFormat, NormalizedInstructions};
676 use proptest::prelude::*;
677
678 fn symbol(offset: u64, code: &[u8], normalized: Option<&[u8]>) -> ArtifactSymbol {
679 ArtifactSymbol {
680 fingerprint: ArtifactFingerprint::from_content("test-symbol", &offset.to_le_bytes()),
681 name: None,
682 exported: false,
683 section: Some(1),
684 offset,
685 size: code.len() as u64,
686 size_inferred: false,
687 code: code.to_vec(),
688 normalized: normalized.map(|bytes| NormalizedInstructions {
689 version: "test-normal-v1".to_owned(),
690 bytes: bytes.to_vec(),
691 }),
692 inline_stack: Vec::new(),
693 }
694 }
695
696 #[test]
697 fn exact_and_normalized_groups_are_reported_separately_and_deterministically() {
698 let mut artifact = ArtifactIr::empty(ArtifactFormat::Wasm, b"input");
699 artifact.capabilities.normalized_duplicates = true;
700 artifact.symbols = vec![
701 symbol(30, &[1, 2], Some(&[9])),
702 symbol(10, &[1, 2], Some(&[9])),
703 symbol(20, &[1, 3], Some(&[9])),
704 symbol(40, &[5], None),
705 ];
706 let duplicates = find_duplicates(&artifact);
707 assert_eq!(duplicates.exact.len(), 1);
708 assert_eq!(duplicates.exact[0].members.len(), 2);
709 assert_eq!(duplicates.exact[0].duplicated_bytes, 2);
710 assert_eq!(
711 duplicates.exact[0]
712 .members
713 .iter()
714 .map(|member| member.offset)
715 .collect::<Vec<_>>(),
716 vec![10, 30]
717 );
718 assert_eq!(duplicates.normalized.len(), 1);
719 assert_eq!(duplicates.normalized[0].members.len(), 3);
720 assert_eq!(duplicates.normalized[0].duplicated_bytes, 4);
721 assert_eq!(find_duplicates(&artifact), duplicates);
722 }
723
724 #[test]
725 fn normalized_groups_are_unavailable_without_a_supported_normalizer() {
726 let mut artifact = ArtifactIr::empty(ArtifactFormat::Elf, b"input");
727 artifact.symbols = vec![
728 symbol(10, &[1, 2], Some(&[9])),
729 symbol(20, &[3, 4], Some(&[9])),
730 ];
731
732 let duplicates = find_duplicates(&artifact);
733
734 assert!(duplicates.exact.is_empty());
735 assert!(duplicates.normalized.is_empty());
736 }
737
738 #[test]
739 fn size_categories_separate_observed_data_and_unavailable_estimates() {
740 let mut artifact = ArtifactIr::empty(ArtifactFormat::Wasm, b"input bytes");
741 artifact.capabilities.independent_data_segments = true;
742 artifact.symbols = vec![symbol(10, &[1, 2, 3], None), symbol(20, &[1, 2, 3], None)];
743 let bytes = vec![7; 16];
744 artifact.data_segments = vec![
745 ArtifactDataSegment {
746 fingerprint: ArtifactFingerprint::from_content("data", b"one"),
747 section: None,
748 offset: 100,
749 bytes: bytes.clone(),
750 },
751 ArtifactDataSegment {
752 fingerprint: ArtifactFingerprint::from_content("data", b"two"),
753 section: None,
754 offset: 200,
755 bytes,
756 },
757 ];
758 let sizes = classify_sizes(&artifact);
759 assert_eq!(sizes.observed_bytes, 11);
760 assert_eq!(sizes.duplicated_bytes, 3);
761 assert_eq!(sizes.duplicated_data_bytes, Some(16));
762 assert_eq!(sizes.upper_bound_savings_bytes, Some(3));
763 assert!(sizes.estimated_refactor_savings_bytes.is_none());
764 assert!(sizes.verified_savings_bytes.is_none());
765 assert_eq!(sizes.clone_confidence, EvidenceConfidence::High);
766 assert_eq!(sizes.savings_confidence, EvidenceConfidence::Unavailable);
767 assert!(sizes.duplicated_bytes >= sizes.upper_bound_savings_bytes.unwrap_or(u64::MAX));
768 }
769
770 proptest! {
771 #[test]
772 fn size_categories_keep_exact_duplicate_bounds_for_disjoint_regions(
773 lengths in prop::collection::vec(16_usize..128, 0..24),
774 ) {
775 let mut artifact = ArtifactIr::empty(ArtifactFormat::Wasm, b"");
776 artifact.capabilities.independent_data_segments = true;
777 let mut offset = 0_u64;
778 for (index, length) in lengths.iter().copied().enumerate() {
779 let bytes = vec![u8::try_from(index).unwrap_or(u8::MAX); length];
780 artifact.symbols.push(symbol(offset, &bytes, None));
781 offset += length as u64;
782 artifact.symbols.push(symbol(offset, &bytes, None));
783 offset += length as u64;
784 artifact.data_segments.push(ArtifactDataSegment {
785 fingerprint: ArtifactFingerprint::from_content("property-data", &bytes),
786 section: Some(11),
787 offset,
788 bytes: bytes.clone(),
789 });
790 offset += length as u64;
791 artifact.data_segments.push(ArtifactDataSegment {
792 fingerprint: ArtifactFingerprint::from_content("property-data", &bytes),
793 section: Some(11),
794 offset,
795 bytes,
796 });
797 offset += length as u64;
798 }
799 artifact.observed_bytes = offset;
800 let sizes = classify_sizes(&artifact);
801 prop_assert!(sizes.duplicated_bytes <= sizes.observed_bytes);
802 prop_assert!(sizes.duplicated_data_bytes.is_some_and(|value| value <= sizes.observed_bytes));
803 prop_assert_eq!(
804 sizes.upper_bound_savings_bytes,
805 Some(sizes.duplicated_bytes)
806 );
807 prop_assert!(
808 sizes.estimated_refactor_savings_bytes.is_none()
809 && sizes.verified_savings_bytes.is_none()
810 );
811 }
812 }
813
814 #[test]
815 fn unresolved_dispatch_downgrades_unreachable_symbols_to_candidates() {
816 let mut artifact = ArtifactIr::empty(ArtifactFormat::Wasm, b"input");
817 let entry = symbol(1, &[1], None);
818 let live = symbol(2, &[2], None);
819 let dead = symbol(3, &[3], None);
820 artifact.symbols = vec![entry.clone(), live.clone(), dead.clone()];
821 artifact.symbols[0].exported = true;
822 artifact.capabilities.call_graph = true;
823 artifact.calls = vec![crate::ArtifactCall {
824 caller: entry.fingerprint,
825 target: Some(live.fingerprint),
826 unresolved: None,
827 }];
828 let report = dead_code_candidates(&artifact).unwrap();
829 assert!(report.definitive);
830 assert_eq!(report.symbols, vec![dead.fingerprint]);
831 artifact.calls.push(crate::ArtifactCall {
832 caller: live.fingerprint,
833 target: None,
834 unresolved: Some(crate::UnresolvedCall::IndirectTable),
835 });
836 assert!(!dead_code_candidates(&artifact).unwrap().definitive);
837 }
838
839 #[test]
840 fn retained_size_uses_dominator_regions_without_summing_their_overlap() {
841 let mut artifact = ArtifactIr::empty(ArtifactFormat::Wasm, b"input");
842 let entry = symbol(1, &[1], None);
843 let middle = symbol(2, &[2, 2], None);
844 let leaf = symbol(3, &[3, 3, 3], None);
845 artifact.symbols = vec![entry.clone(), middle.clone(), leaf.clone()];
846 artifact.symbols[0].exported = true;
847 artifact.capabilities.call_graph = true;
848 artifact.calls = vec![
849 crate::ArtifactCall {
850 caller: entry.fingerprint,
851 target: Some(middle.fingerprint),
852 unresolved: None,
853 },
854 crate::ArtifactCall {
855 caller: middle.fingerprint,
856 target: Some(leaf.fingerprint),
857 unresolved: None,
858 },
859 ];
860 let retained = retained_sizes(&artifact).unwrap();
861 let value = |fingerprint| {
862 retained
863 .iter()
864 .find(|item| item.symbol == fingerprint)
865 .unwrap()
866 .retained_bytes
867 };
868 assert_eq!(value(entry.fingerprint), 6);
869 assert_eq!(value(middle.fingerprint), 5);
870 assert_eq!(value(leaf.fingerprint), 3);
871 let sizes = classify_sizes(&artifact);
872 assert_eq!(sizes.retained_bytes, Some(6));
873 assert_eq!(sizes.shared_dependency_bytes, Some(0));
874 artifact.calls[1].unresolved = Some(crate::UnresolvedCall::IndirectTable);
875 assert!(retained_sizes(&artifact).is_none());
876 }
877
878 #[test]
879 fn path_compression_handles_a_deep_ancestor_chain_iteratively() {
880 let nodes = 100_000_usize;
881 let mut ancestors = (0..nodes)
882 .map(|node| node.checked_sub(1))
883 .collect::<Vec<_>>();
884 let mut labels = (0..nodes).collect::<Vec<_>>();
885 let semi = (0..nodes).collect::<Vec<_>>();
886
887 lt_compress(nodes - 1, &mut ancestors, &mut labels, &semi);
888
889 assert!(ancestors[0].is_none());
890 assert_eq!(ancestors[1], Some(0));
891 assert!(ancestors[2..].iter().all(|ancestor| *ancestor == Some(0)));
892 assert!(labels[1..].iter().all(|label| *label == 1));
893 }
894
895 #[test]
896 fn retained_size_converges_for_a_cycle() {
897 let mut artifact = ArtifactIr::empty(ArtifactFormat::Wasm, b"input");
898 let entry = symbol(1, &[1], None);
899 let left = symbol(2, &[2, 2], None);
900 let right = symbol(3, &[3, 3, 3], None);
901 artifact.symbols = vec![entry.clone(), left.clone(), right.clone()];
902 artifact.symbols[0].exported = true;
903 artifact.capabilities.call_graph = true;
904 artifact.calls = vec![
905 crate::ArtifactCall {
906 caller: entry.fingerprint,
907 target: Some(left.fingerprint),
908 unresolved: None,
909 },
910 crate::ArtifactCall {
911 caller: left.fingerprint,
912 target: Some(right.fingerprint),
913 unresolved: None,
914 },
915 crate::ArtifactCall {
916 caller: right.fingerprint,
917 target: Some(left.fingerprint),
918 unresolved: None,
919 },
920 ];
921 let retained = retained_sizes(&artifact).unwrap();
922 let value = |fingerprint| {
923 retained
924 .iter()
925 .find(|item| item.symbol == fingerprint)
926 .unwrap()
927 .retained_bytes
928 };
929 assert_eq!(value(entry.fingerprint), 6);
930 assert_eq!(value(left.fingerprint), 5);
931 assert_eq!(value(right.fingerprint), 3);
932 }
933
934 #[test]
935 fn retained_size_handles_a_deep_call_chain_without_quadratic_state() {
936 const DEPTH: usize = 10_000;
937 let mut artifact = ArtifactIr::empty(ArtifactFormat::Wasm, b"input");
938 artifact.symbols = (0..DEPTH)
939 .map(|offset| symbol(u64::try_from(offset).unwrap(), &[1], None))
940 .collect();
941 artifact.symbols[0].exported = true;
942 artifact.capabilities.call_graph = true;
943 artifact.calls = artifact
944 .symbols
945 .windows(2)
946 .map(|pair| crate::ArtifactCall {
947 caller: pair[0].fingerprint,
948 target: Some(pair[1].fingerprint),
949 unresolved: None,
950 })
951 .collect();
952
953 let retained = retained_sizes(&artifact).unwrap();
954 assert_eq!(retained.len(), DEPTH);
955 let value = |fingerprint| {
956 retained
957 .iter()
958 .find(|item| item.symbol == fingerprint)
959 .unwrap()
960 .retained_bytes
961 };
962 assert_eq!(
963 value(artifact.symbols[0].fingerprint),
964 u64::try_from(DEPTH).unwrap()
965 );
966 assert_eq!(value(artifact.symbols[DEPTH - 1].fingerprint), 1);
967 }
968
969 #[test]
970 fn size_categories_keep_shared_dependencies_separate() {
971 let mut artifact = ArtifactIr::empty(ArtifactFormat::Wasm, b"input");
972 let left_root = symbol(1, &[1], None);
973 let right_root = symbol(2, &[2, 2], None);
974 let shared = symbol(3, &[3, 3, 3], None);
975 artifact.symbols = vec![left_root.clone(), right_root.clone(), shared.clone()];
976 artifact.symbols[0].exported = true;
977 artifact.symbols[1].exported = true;
978 artifact.capabilities.call_graph = true;
979 artifact.calls = vec![
980 crate::ArtifactCall {
981 caller: left_root.fingerprint,
982 target: Some(shared.fingerprint),
983 unresolved: None,
984 },
985 crate::ArtifactCall {
986 caller: right_root.fingerprint,
987 target: Some(shared.fingerprint),
988 unresolved: None,
989 },
990 ];
991 let sizes = classify_sizes(&artifact);
992 assert_eq!(sizes.retained_bytes, Some(6));
993 assert_eq!(sizes.shared_dependency_bytes, Some(3));
994 }
995
996 #[test]
997 fn excessive_root_count_makes_shared_dependency_sizes_unavailable() {
998 let mut artifact = ArtifactIr::empty(ArtifactFormat::Wasm, b"input");
999 artifact.symbols = (0..=MAX_SHARED_DEPENDENCY_ROOTS)
1000 .map(|offset| symbol(u64::try_from(offset).unwrap(), &[1], None))
1001 .collect();
1002 artifact
1003 .symbols
1004 .iter_mut()
1005 .for_each(|symbol| symbol.exported = true);
1006 artifact.capabilities.call_graph = true;
1007
1008 let sizes = classify_sizes(&artifact);
1009
1010 assert_eq!(sizes.retained_bytes, None);
1011 assert_eq!(sizes.shared_dependency_bytes, None);
1012 assert!(retained_sizes(&artifact).is_none());
1013 }
1014}