1use std::collections::{BTreeMap, BTreeSet};
11use std::path::Path;
12
13use fallow_graph::graph::{DominatingImport, ModuleGraph, is_declaration_file_path};
14use fallow_output::{
15 DominatingImportOutput, EagerPackageOutput, EntryWeightListing, EntryWeightOutput,
16 EntryWeightUnit,
17};
18use fallow_types::discover::{DiscoveredFile, EntryPoint, FileId};
19use rustc_hash::FxHashMap;
20
21use crate::session::AnalysisSession;
22
23pub const DOMINATING_IMPORT_LIMIT: usize = 10;
25
26const STYLESHEET_EXTENSIONS: &[&str] = &["css", "scss", "sass", "less"];
28
29const UNKNOWN_ENTRY_SOURCE: &str = "entry point";
31
32pub fn compute_entry_weight(
42 session: &AnalysisSession,
43 entry_points: &[EntryPoint],
44) -> crate::EngineResult<EntryWeightListing> {
45 let artifacts = session.analyze_dead_code_with_shared_artifacts(false, true)?;
46 let graph = artifacts
47 .graph
48 .as_ref()
49 .ok_or_else(|| crate::EngineError::new("entry weight requires a retained module graph"))?;
50 Ok(entry_weight_listing(
51 graph.as_graph(),
52 session.files(),
53 entry_points,
54 session.root(),
55 ))
56}
57
58#[must_use]
60pub fn entry_weight_listing(
61 graph: &ModuleGraph,
62 files: &[DiscoveredFile],
63 entry_points: &[EntryPoint],
64 root: &Path,
65) -> EntryWeightListing {
66 let sizes = FileSizes::new(files);
67 let sources: FxHashMap<&Path, String> = entry_points
68 .iter()
69 .map(|entry| (entry.path.as_path(), entry.source.to_string()))
70 .collect();
71 let mut entry_ids: Vec<FileId> = graph.runtime_entry_points.iter().copied().collect();
72 entry_ids.sort_unstable_by_key(|id| id.0);
73
74 let mut line_offsets = LineOffsetCache::default();
75 let mut entries: Vec<EntryWeightOutput> = entry_ids
76 .into_iter()
77 .filter_map(|entry| {
78 let module = graph.modules.get(entry.0 as usize)?;
79 if is_declaration_file_path(&module.path) {
81 return None;
82 }
83 let source = sources
84 .get(module.path.as_path())
85 .cloned()
86 .unwrap_or_else(|| UNKNOWN_ENTRY_SOURCE.to_string());
87 Some(entry_weight(
88 graph,
89 EntryRow {
90 entry,
91 source,
92 root,
93 sizes: &sizes,
94 },
95 &mut line_offsets,
96 ))
97 })
98 .collect();
99 entries.sort_by(|a, b| {
100 b.eager_bytes
101 .cmp(&a.eager_bytes)
102 .then_with(|| a.path.cmp(&b.path))
103 });
104
105 EntryWeightListing {
106 unit: EntryWeightUnit::SourceBytes,
107 entry_count: entries.len(),
108 entries,
109 regression: None,
110 }
111}
112
113struct EntryRow<'a> {
114 entry: FileId,
115 source: String,
116 root: &'a Path,
117 sizes: &'a FileSizes,
118}
119
120fn entry_weight(
121 graph: &ModuleGraph,
122 row: EntryRow<'_>,
123 line_offsets: &mut LineOffsetCache,
124) -> EntryWeightOutput {
125 let EntryRow {
126 entry,
127 source,
128 root,
129 sizes,
130 } = row;
131 let closure = graph.entry_load_closure(entry);
132 let eager_css_bytes = closure
133 .eager
134 .iter()
135 .filter(|&&id| {
136 graph
137 .modules
138 .get(id.0 as usize)
139 .is_some_and(|m| is_stylesheet(&m.path))
140 })
141 .map(|&id| sizes.get(id))
142 .sum();
143 let eager_packages = eager_packages(graph, &closure.eager);
144 let dominating_imports = graph
145 .eager_dominating_imports(entry, &closure.eager, |id| sizes.get(id))
146 .into_iter()
147 .take(DOMINATING_IMPORT_LIMIT)
148 .map(|import| dominating_import_output(graph, root, &import, line_offsets))
149 .collect();
150
151 EntryWeightOutput {
152 path: relative_path(graph, entry, root),
153 source,
154 eager_modules: closure.eager.len(),
155 eager_bytes: sizes.sum(&closure.eager),
156 eager_css_bytes,
157 deferred_modules: closure.deferred.len(),
158 deferred_bytes: sizes.sum(&closure.deferred),
159 out_of_thread_modules: closure.out_of_thread.len(),
160 out_of_thread_bytes: sizes.sum(&closure.out_of_thread),
161 eager_package_count: eager_packages.len(),
162 eager_packages,
163 dominating_imports,
164 }
165}
166
167fn eager_packages(graph: &ModuleGraph, eager: &[FileId]) -> Vec<EagerPackageOutput> {
168 let mut packages: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
169 for id in eager {
170 let Some(imports) = graph.eager_package_imports.get(id) else {
171 continue;
172 };
173 for import in imports
174 .iter()
175 .filter(|import| !crate::core_backend::is_builtin_module(&import.package))
176 {
177 packages
178 .entry(import.package.as_str())
179 .or_default()
180 .insert(import.specifier.as_str());
181 }
182 }
183 packages
184 .into_iter()
185 .map(|(name, specifiers)| EagerPackageOutput {
186 name: name.to_string(),
187 specifiers: specifiers.into_iter().map(str::to_string).collect(),
188 })
189 .collect()
190}
191
192fn dominating_import_output(
193 graph: &ModuleGraph,
194 root: &Path,
195 import: &DominatingImport,
196 line_offsets: &mut LineOffsetCache,
197) -> DominatingImportOutput {
198 let line = import
199 .import_span_start
200 .and_then(|start| line_offsets.line(graph, import.importer, start));
201 DominatingImportOutput {
202 importer: relative_path(graph, import.importer, root),
203 line,
204 target: relative_path(graph, import.target, root),
205 exclusive_bytes: import.exclusive_weight,
206 exclusive_modules: import.exclusive_modules,
207 }
208}
209
210fn relative_path(graph: &ModuleGraph, id: FileId, root: &Path) -> String {
211 graph
212 .modules
213 .get(id.0 as usize)
214 .map(|module| crate::trace::trace_impl::relativize(&module.path, root))
215 .unwrap_or_default()
216}
217
218fn is_stylesheet(path: &Path) -> bool {
219 path.extension()
220 .and_then(|ext| ext.to_str())
221 .is_some_and(|ext| {
222 STYLESHEET_EXTENSIONS
223 .iter()
224 .any(|known| ext.eq_ignore_ascii_case(known))
225 })
226}
227
228struct FileSizes(Vec<u64>);
230
231impl FileSizes {
232 fn new(files: &[DiscoveredFile]) -> Self {
233 let len = files.iter().map(|f| f.id.0 as usize + 1).max().unwrap_or(0);
234 let mut sizes = vec![0; len];
235 for file in files {
236 sizes[file.id.0 as usize] = file.size_bytes;
237 }
238 Self(sizes)
239 }
240
241 fn get(&self, id: FileId) -> u64 {
242 self.0.get(id.0 as usize).copied().unwrap_or(0)
243 }
244
245 fn sum(&self, ids: &[FileId]) -> u64 {
246 ids.iter().map(|&id| self.get(id)).sum()
247 }
248}
249
250#[derive(Default)]
252struct LineOffsetCache(FxHashMap<FileId, Option<Vec<u32>>>);
253
254impl LineOffsetCache {
255 fn line(&mut self, graph: &ModuleGraph, file: FileId, byte_offset: u32) -> Option<u32> {
256 let offsets = self.0.entry(file).or_insert_with(|| {
257 let module = graph.modules.get(file.0 as usize)?;
258 std::fs::read_to_string(&module.path)
259 .ok()
260 .map(|source| fallow_types::extract::compute_line_offsets(&source))
261 });
262 offsets
263 .as_ref()
264 .map(|offsets| fallow_types::extract::byte_offset_to_line_col(offsets, byte_offset).0)
265 }
266}