Skip to main content

next_api/
analyze.rs

1mod groups;
2mod membership;
3mod output_file;
4
5use std::io::Write;
6
7use anyhow::Result;
8use bincode::{Decode, Encode};
9use byteorder::{BE, WriteBytesExt};
10use next_core::app_structure::FileSystemPathVec;
11use output_file::{AnalyzedChunkPart, analyze_output_file, analyze_traced_file};
12use rustc_hash::{FxHashMap, FxHashSet};
13use serde::Serialize;
14use turbo_rcstr::RcStr;
15use turbo_tasks::{
16    FxIndexSet, JoinIterExt, NonLocalValue, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt,
17    ValueToString, ValueToStringRef, Vc,
18};
19use turbo_tasks_fs::{
20    File, FileContent, FileSystemPath,
21    rope::{Rope, RopeBuilder},
22};
23use turbo_tasks_hash::hash_xxh3_hash64;
24use turbopack_core::{
25    asset::{Asset, AssetContent},
26    chunk::{ChunkingType, TracedMode},
27    module::Module,
28    module_graph::{GraphTraversalAction, ModuleGraph},
29    output::{OutputAsset, OutputAssets, OutputAssetsReference},
30    reference::all_assets_from_entries,
31};
32use turbopack_ecmascript::references::service_worker::ServiceWorkerEntryModule;
33
34use crate::route::AnalyzeChunkGroups;
35
36const ANALYZE_SCHEMA_VERSION: u32 = 1;
37
38#[turbo_tasks::value]
39#[derive(Clone, Copy, Serialize)]
40pub struct BundleTotals {
41    pub size: u64,
42    pub compressed_size: u64,
43}
44
45#[turbo_tasks::value]
46#[derive(Clone, Copy, Serialize)]
47pub struct RouteBundleSummary {
48    pub size: u64,
49    pub compressed_size: u64,
50    pub client: BundleTotals,
51}
52
53#[derive(Serialize)]
54pub struct AnalyzeSource {
55    pub parent_source_index: Option<u32>,
56    /// Path. When there is a parent, this is concatenated to the parent's path.
57    /// Folders end with a slash. Might have multiple path segments when folders contain only a
58    /// single child.
59    pub path: RcStr,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, NonLocalValue, Encode, Decode, Serialize)]
63pub struct AnalyzeModule {
64    pub ident: RcStr,
65    pub path: RcStr,
66}
67
68#[derive(Serialize)]
69pub struct AnalyzeChunkPart {
70    pub source_index: u32,
71    pub output_file_index: u32,
72    pub size: u32,
73    pub compressed_size: u32,
74}
75
76#[derive(Serialize)]
77pub struct AnalyzeOutputFile {
78    pub filename: RcStr,
79}
80
81/// Exact endpoint graph root. Client roles and references are build-time
82/// provenance; neither establishes that a browser requested a chunk.
83#[turbo_tasks::value(shared)]
84#[derive(Clone, Debug, Serialize)]
85pub struct AnalyzeRouteEntry {
86    pub route_entry_id: RcStr,
87    pub module_ident: RcStr,
88    pub module_path: RcStr,
89    pub role: RcStr,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub entry_kind: Option<RcStr>,
92    #[serde(skip_serializing_if = "Vec::is_empty")]
93    pub client_references: Vec<AnalyzeClientReferenceEntry>,
94}
95
96#[derive(Clone, Debug, PartialEq, Eq, NonLocalValue, Encode, Decode, Serialize)]
97pub struct AnalyzeClientReferenceEntry {
98    pub module_ident: RcStr,
99    pub module_path: RcStr,
100    pub reference_kind: RcStr,
101}
102
103#[turbo_tasks::value(transparent)]
104pub struct AnalyzeRouteEntries(Vec<AnalyzeRouteEntry>);
105
106pub type AnalyzeGraphModule = ResolvedVc<Box<dyn Module>>;
107pub type AnalyzeSyncDependents = FxHashMap<AnalyzeGraphModule, Vec<AnalyzeGraphModule>>;
108pub type AnalyzeWorkerEntry = (AnalyzeGraphModule, ResolvedVc<ServiceWorkerEntryModule>);
109
110/// Snapshot-scoped module order shared by both artifact writers. Never regenerate
111/// indices independently from a route graph or use them across analyzer snapshots.
112#[turbo_tasks::value(shared)]
113pub struct AnalyzeModuleIndex {
114    pub modules: Vec<AnalyzeModule>,
115    pub by_ident: FxHashMap<RcStr, u32>,
116    /// Fingerprint of the exact ordered module identities serialized in modules.data.
117    pub module_index_hash: RcStr,
118    /// Cached once for the whole application, not rebuilt for each route.
119    pub sync_dependents: AnalyzeSyncDependents,
120    pub worker_entries: Vec<AnalyzeWorkerEntry>,
121}
122
123#[turbo_tasks::function]
124pub async fn analyze_module_index(module_graph: Vc<ModuleGraph>) -> Result<Vc<AnalyzeModuleIndex>> {
125    let mut all_modules = FxIndexSet::default();
126    let mut sync_dependents: AnalyzeSyncDependents = FxHashMap::default();
127    let mut registrations = FxIndexSet::default();
128    let graph = module_graph.await?;
129    graph.traverse_edges_dfs(
130        graph.all_entry_modules(),
131        &mut (),
132        |parent, node, _| {
133            all_modules.insert(node);
134            if let Some((importer, reference)) = parent {
135                if !matches!(
136                    reference.chunking_type,
137                    ChunkingType::Async | ChunkingType::Traced { .. }
138                ) {
139                    sync_dependents.entry(node).or_default().push(importer);
140                }
141                if let Some(marker) =
142                    ResolvedVc::try_downcast_type::<ServiceWorkerEntryModule>(node)
143                {
144                    registrations.insert((importer, marker));
145                }
146            }
147            Ok(GraphTraversalAction::Continue)
148        },
149        |_, _, _| Ok(()),
150        true,
151        false,
152    )?;
153    let mut modules = Vec::with_capacity(all_modules.len());
154    let mut by_ident = FxHashMap::default();
155    let idents_and_paths = all_modules
156        .iter()
157        .copied()
158        .map(async |module| {
159            let ident = module.ident();
160            let ident_str = ident.to_string().owned().await?;
161            let path = ident.await?.path.to_string_ref().await?;
162            anyhow::Ok((ident_str, path))
163        })
164        .join()
165        .await;
166    for pair in idents_and_paths {
167        let (ident, path) = pair?;
168        if by_ident.contains_key(&ident) {
169            continue;
170        }
171        by_ident.insert(ident.clone(), modules.len() as u32);
172        modules.push(AnalyzeModule { ident, path });
173    }
174    let module_index_hash = format!(
175        "{:016x}",
176        hash_xxh3_hash64(
177            modules
178                .iter()
179                .map(|module| (module.ident.as_str(), module.path.as_str()))
180                .collect::<Vec<_>>()
181        )
182    )
183    .into();
184    Ok(AnalyzeModuleIndex {
185        modules,
186        by_ident,
187        module_index_hash,
188        sync_dependents,
189        worker_entries: registrations.into_iter().collect(),
190    }
191    .cell())
192}
193
194pub fn analyze_route_entry_id(
195    route: &str,
196    role: &str,
197    sub_name: &str,
198    module_ident: &str,
199) -> RcStr {
200    format!("{route}|{role}|{sub_name}|{module_ident}").into()
201}
202
203#[derive(Serialize)]
204struct EdgesDataReference {
205    pub offset: u32,
206    pub length: u32,
207}
208
209#[derive(Serialize)]
210struct AnalyzeDataHeader {
211    /// The header and modules.data must use the same supported schema version.
212    pub schema_version: u32,
213    pub module_index_hash: RcStr,
214    pub sources: Vec<AnalyzeSource>,
215    pub chunk_parts: Vec<AnalyzeChunkPart>,
216    pub output_files: Vec<AnalyzeOutputFile>,
217    /// Exact indices into this snapshot's modules.data.modules, one row per output file.
218    pub output_file_modules: EdgesDataReference,
219    pub output_file_async_loaders: EdgesDataReference,
220    pub output_file_module_coverage: Vec<AnalyzeOutputFileCoverage>,
221    pub chunk_groups: Vec<AnalyzeChunkGroupData>,
222    /// Exact endpoint roots; nested client references do not become roots.
223    pub route_entries: Vec<AnalyzeRouteEntry>,
224    /// Edges from chunks to chunk parts
225    pub output_file_chunk_parts: EdgesDataReference,
226    /// Edges from sources to chunk parts
227    pub source_chunk_parts: EdgesDataReference,
228    /// Edges from sources to their children sources
229    pub source_children: EdgesDataReference,
230    /// Root level sources, walking their children will reach all sources
231    pub source_roots: Vec<u32>,
232}
233
234#[derive(Serialize)]
235struct ModulesDataHeader {
236    /// The header and modules.data must use the same supported schema version.
237    pub schema_version: u32,
238    pub module_index_hash: RcStr,
239    pub modules: Vec<AnalyzeModule>,
240    /// Edges from modules to modules
241    pub module_dependents: EdgesDataReference,
242    /// Edges from modules to modules
243    pub async_module_dependents: EdgesDataReference,
244    /// Edges from modules to modules
245    pub traced_module_dependents: EdgesDataReference,
246    /// Edges from modules to modules
247    pub module_dependencies: EdgesDataReference,
248    /// Edges from modules to modules
249    pub async_module_dependencies: EdgesDataReference,
250    /// Edges from modules to modules
251    pub traced_module_dependencies: EdgesDataReference,
252}
253
254#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, NonLocalValue, Encode, Decode, Serialize)]
255#[serde(rename_all = "snake_case")]
256enum AnalyzeOutputFileCoverage {
257    Exact,
258    Unsupported,
259    #[default]
260    NotAChunk,
261}
262
263#[derive(Serialize)]
264struct AnalyzeChunkGroupData {
265    id: u32,
266    kind: RcStr,
267    /// Async groups identify the dynamically imported target, not a loader stub.
268    /// Other group kinds retain their explicitly selected trigger.
269    #[serde(skip_serializing_if = "Option::is_none")]
270    trigger_module_index: Option<u32>,
271    /// Direct emitted output_file indices; group membership is not a claim that
272    /// an individual reference contributes every module in a cumulative group.
273    output_file_indices: Vec<u32>,
274}
275
276struct ChunkLoadCandidate {
277    source: u32,
278    target: ResolvedVc<Box<dyn OutputAsset>>,
279    /// The dynamically imported target, shared by loaders and manifests.
280    trigger_module_index: u32,
281}
282
283struct AnalyzeOutputFileBuilder {
284    output_file: AnalyzeOutputFile,
285    chunk_part_indices: Vec<u32>,
286    module_indices: Vec<u32>,
287    async_loader_indices: Vec<u32>,
288    module_coverage: AnalyzeOutputFileCoverage,
289}
290
291struct AnalyzeSourceBuilder {
292    source: AnalyzeSource,
293    child_source_indices: Vec<u32>,
294    chunk_part_indices: Vec<u32>,
295}
296
297struct AnalyzeModuleBuilder {
298    module: AnalyzeModule,
299    dependencies: FxIndexSet<u32>,
300    async_dependencies: FxIndexSet<u32>,
301    traced_dependencies: FxIndexSet<u32>,
302    dependents: FxIndexSet<u32>,
303    async_dependents: FxIndexSet<u32>,
304    traced_dependents: FxIndexSet<u32>,
305}
306
307struct AnalyzeDataBuilder {
308    sources: Vec<AnalyzeSourceBuilder>,
309    source_index_map: FxHashMap<RcStr, u32>,
310    chunk_parts: Vec<AnalyzeChunkPart>,
311    output_files: Vec<AnalyzeOutputFileBuilder>,
312    output_file_index_map: FxHashMap<RcStr, u32>,
313    route_entries: Vec<AnalyzeRouteEntry>,
314    module_index_hash: RcStr,
315    chunk_groups: Vec<AnalyzeChunkGroupData>,
316}
317
318struct ModulesDataBuilder {
319    modules: Vec<AnalyzeModuleBuilder>,
320    module_index_map: FxHashMap<RcStr, u32>,
321    module_index_hash: RcStr,
322}
323
324/// Shared row-offset encoding used by both analyzer artifacts.
325pub struct EdgesData {
326    pub offsets: Vec<u32>,
327    pub data: Vec<u32>,
328}
329
330impl EdgesData {
331    fn from_iterator<'a>(iterable: impl IntoIterator<Item = &'a Vec<u32>> + Clone) -> Self {
332        let mut current_offset = 0;
333        let sum: usize = iterable.clone().into_iter().map(|v| v.len()).sum();
334        let mut data = Vec::with_capacity(sum);
335        let offsets = iterable
336            .into_iter()
337            .map(|edges| {
338                current_offset += edges.len() as u32;
339                data.extend(edges);
340                current_offset
341            })
342            .collect();
343        Self { offsets, data }
344    }
345
346    fn write(&self, writer: &mut impl Write) -> Result<()> {
347        writer.write_u32::<BE>(self.offsets.len() as u32)?;
348        for &offset in &self.offsets {
349            writer.write_u32::<BE>(offset)?;
350        }
351        for &data in &self.data {
352            writer.write_u32::<BE>(data)?;
353        }
354        Ok(())
355    }
356}
357
358struct EdgesDataSectionBuilder {
359    data: Vec<u8>,
360}
361
362impl EdgesDataSectionBuilder {
363    fn new() -> Self {
364        Self { data: vec![] }
365    }
366
367    fn add_edges(&mut self, edges: &EdgesData) -> EdgesDataReference {
368        let offset = self.data.len().try_into().unwrap();
369        edges.write(&mut self.data).unwrap();
370        let length = (self.data.len() - offset as usize).try_into().unwrap();
371        EdgesDataReference { offset, length }
372    }
373}
374
375impl AnalyzeDataBuilder {
376    fn new(route_entries: Vec<AnalyzeRouteEntry>, module_index_hash: RcStr) -> Self {
377        Self {
378            module_index_hash,
379            sources: vec![],
380            source_index_map: FxHashMap::default(),
381            chunk_parts: vec![],
382            output_files: vec![],
383            output_file_index_map: FxHashMap::default(),
384            route_entries,
385            chunk_groups: vec![],
386        }
387    }
388
389    fn ensure_source(&mut self, path: &str) -> (&mut AnalyzeSourceBuilder, u32) {
390        if let Some(&index) = self.source_index_map.get(path) {
391            return (&mut self.sources[index as usize], index);
392        }
393        let index = self.sources.len() as u32;
394        let path = RcStr::from(path);
395        self.source_index_map.insert(path.clone(), index);
396        self.sources.push(AnalyzeSourceBuilder {
397            source: AnalyzeSource {
398                parent_source_index: None,
399                path,
400            },
401            child_source_indices: vec![],
402            chunk_part_indices: vec![],
403        });
404        (&mut self.sources[index as usize], index)
405    }
406
407    fn add_chunk_part(&mut self, chunk_part: AnalyzeChunkPart) -> u32 {
408        let i = self.chunk_parts.len() as u32;
409        self.chunk_parts.push(chunk_part);
410        i
411    }
412
413    fn add_output_file(&mut self, output_file: AnalyzeOutputFile) -> u32 {
414        if let Some(&index) = self.output_file_index_map.get(&output_file.filename) {
415            return index;
416        }
417        let i = self.output_files.len() as u32;
418        self.output_file_index_map
419            .insert(output_file.filename.clone(), i);
420        self.output_files.push(AnalyzeOutputFileBuilder {
421            output_file,
422            chunk_part_indices: vec![],
423            module_indices: vec![],
424            async_loader_indices: vec![],
425            module_coverage: AnalyzeOutputFileCoverage::NotAChunk,
426        });
427        i
428    }
429
430    fn add_chunk_part_to_output_file(&mut self, output_file_index: u32, chunk_part_index: u32) {
431        self.output_files[output_file_index as usize]
432            .chunk_part_indices
433            .push(chunk_part_index);
434    }
435
436    fn add_chunk_part_to_source(&mut self, source_index: u32, chunk_part_index: u32) {
437        self.sources[source_index as usize]
438            .chunk_part_indices
439            .push(chunk_part_index);
440    }
441
442    /// Link the attributed sources into their directory hierarchy before
443    /// serializing the route artifact.
444    fn finish_sources(&mut self) {
445        let mut i: u32 = 0;
446        while i < self.sources.len().try_into().unwrap() {
447            let source = &self.sources[i as usize];
448            let path = source.source.path.as_str();
449            if !path.is_empty() {
450                let (parent_path, path) = if let Some(pos) = path.trim_end_matches('/').rfind('/') {
451                    (&path[..pos + 1], &path[pos + 1..])
452                } else {
453                    ("", path)
454                };
455                let parent_path = parent_path.to_string();
456                let path = path.into();
457                let (parent_source, parent_index) = self.ensure_source(&parent_path);
458                parent_source.child_source_indices.push(i);
459                self.sources[i as usize].source.parent_source_index = Some(parent_index);
460                self.sources[i as usize].source.path = path;
461            }
462            i += 1;
463        }
464    }
465
466    fn build(self) -> Rope {
467        let source_roots = self
468            .sources
469            .iter()
470            .enumerate()
471            .filter_map(|(i, s)| {
472                if s.source.parent_source_index.is_none() {
473                    Some(i as u32)
474                } else {
475                    None
476                }
477            })
478            .collect();
479
480        let source_children =
481            EdgesData::from_iterator(self.sources.iter().map(|s| &s.child_source_indices));
482
483        let source_chunk_parts =
484            EdgesData::from_iterator(self.sources.iter().map(|s| &s.chunk_part_indices));
485
486        let output_file_chunk_parts =
487            EdgesData::from_iterator(self.output_files.iter().map(|of| &of.chunk_part_indices));
488        let output_file_modules =
489            EdgesData::from_iterator(self.output_files.iter().map(|of| &of.module_indices));
490
491        let output_file_async_loaders =
492            EdgesData::from_iterator(self.output_files.iter().map(|of| &of.async_loader_indices));
493        let mut binary_section = EdgesDataSectionBuilder::new();
494
495        let header = AnalyzeDataHeader {
496            schema_version: ANALYZE_SCHEMA_VERSION,
497            module_index_hash: self.module_index_hash,
498            sources: self.sources.into_iter().map(|s| s.source).collect(),
499            chunk_parts: self.chunk_parts,
500            output_file_module_coverage: self
501                .output_files
502                .iter()
503                .map(|of| of.module_coverage)
504                .collect(),
505            output_file_modules: binary_section.add_edges(&output_file_modules),
506            output_file_async_loaders: binary_section.add_edges(&output_file_async_loaders),
507            output_files: self
508                .output_files
509                .into_iter()
510                .map(|of| of.output_file)
511                .collect(),
512            chunk_groups: self.chunk_groups,
513            route_entries: self.route_entries,
514            output_file_chunk_parts: binary_section.add_edges(&output_file_chunk_parts),
515            source_chunk_parts: binary_section.add_edges(&source_chunk_parts),
516            source_children: binary_section.add_edges(&source_children),
517            source_roots,
518        };
519
520        let header_json = serde_json::to_vec(&header).unwrap();
521
522        let mut rope = RopeBuilder::default();
523        rope.push_bytes(&(header_json.len() as u32).to_be_bytes());
524        rope.reserve_bytes(header_json.len() + binary_section.data.len());
525        rope.push_bytes(&header_json);
526        rope.push_bytes(&binary_section.data);
527        rope.build()
528    }
529}
530
531impl ModulesDataBuilder {
532    fn new(module_index_hash: RcStr) -> Self {
533        Self {
534            module_index_hash,
535            modules: vec![],
536            module_index_map: FxHashMap::default(),
537        }
538    }
539
540    fn get_module(&mut self, ident: &str) -> (&mut AnalyzeModuleBuilder, u32) {
541        if let Some(&index) = self.module_index_map.get(ident) {
542            return (&mut self.modules[index as usize], index);
543        }
544        panic!("Module with ident `{}` not found", ident);
545    }
546
547    fn ensure_module(&mut self, ident: &str, path: &str) -> (&mut AnalyzeModuleBuilder, u32) {
548        if let Some(&index) = self.module_index_map.get(ident) {
549            return (&mut self.modules[index as usize], index);
550        }
551        let index = self.modules.len() as u32;
552        let ident = RcStr::from(ident);
553        let path = RcStr::from(path);
554        self.module_index_map.insert(ident.clone(), index);
555        self.modules.push(AnalyzeModuleBuilder {
556            module: AnalyzeModule { ident, path },
557            dependencies: FxIndexSet::default(),
558            async_dependencies: FxIndexSet::default(),
559            traced_dependencies: FxIndexSet::default(),
560            dependents: FxIndexSet::default(),
561            async_dependents: FxIndexSet::default(),
562            traced_dependents: FxIndexSet::default(),
563        });
564        (&mut self.modules[index as usize], index)
565    }
566
567    fn build(self) -> Rope {
568        let module_dependencies_vecs: Vec<Vec<u32>> = self
569            .modules
570            .iter()
571            .map(|s| s.dependencies.iter().copied().collect())
572            .collect();
573        let async_module_dependencies_vecs: Vec<Vec<u32>> = self
574            .modules
575            .iter()
576            .map(|s| s.async_dependencies.iter().copied().collect())
577            .collect();
578        let traced_module_dependencies_vecs: Vec<Vec<u32>> = self
579            .modules
580            .iter()
581            .map(|s| s.traced_dependencies.iter().copied().collect())
582            .collect();
583        let module_dependents_vecs: Vec<Vec<u32>> = self
584            .modules
585            .iter()
586            .map(|s| s.dependents.iter().copied().collect())
587            .collect();
588        let async_module_dependents_vecs: Vec<Vec<u32>> = self
589            .modules
590            .iter()
591            .map(|s| s.async_dependents.iter().copied().collect())
592            .collect();
593        let traced_module_dependents_vecs: Vec<Vec<u32>> = self
594            .modules
595            .iter()
596            .map(|s| s.traced_dependents.iter().copied().collect())
597            .collect();
598
599        let module_dependencies = EdgesData::from_iterator(&module_dependencies_vecs);
600        let async_module_dependencies = EdgesData::from_iterator(&async_module_dependencies_vecs);
601        let traced_module_dependencies = EdgesData::from_iterator(&traced_module_dependencies_vecs);
602        let module_dependents = EdgesData::from_iterator(&module_dependents_vecs);
603        let async_module_dependents = EdgesData::from_iterator(&async_module_dependents_vecs);
604        let traced_module_dependents = EdgesData::from_iterator(&traced_module_dependents_vecs);
605
606        let mut binary_section = EdgesDataSectionBuilder::new();
607
608        let header = ModulesDataHeader {
609            schema_version: ANALYZE_SCHEMA_VERSION,
610            module_index_hash: self.module_index_hash,
611            modules: self.modules.into_iter().map(|s| s.module).collect(),
612            module_dependents: binary_section.add_edges(&module_dependents),
613            async_module_dependents: binary_section.add_edges(&async_module_dependents),
614            traced_module_dependents: binary_section.add_edges(&traced_module_dependents),
615            module_dependencies: binary_section.add_edges(&module_dependencies),
616            async_module_dependencies: binary_section.add_edges(&async_module_dependencies),
617            traced_module_dependencies: binary_section.add_edges(&traced_module_dependencies),
618        };
619
620        let header_json = serde_json::to_vec(&header).unwrap();
621
622        let mut rope = RopeBuilder::default();
623        rope.push_bytes(&(header_json.len() as u32).to_be_bytes());
624        rope.reserve_bytes(header_json.len() + binary_section.data.len());
625        rope.push_bytes(&header_json);
626        rope.push_bytes(&binary_section.data);
627        rope.build()
628    }
629}
630
631/// Merges two sets of output assets into one. Used to combine per-route output
632/// assets with shared assets (e.g. `_app`, `_document`) at report generation time.
633#[turbo_tasks::function]
634pub async fn combine_output_assets(
635    primary: Vc<OutputAssets>,
636    extra: Vc<OutputAssets>,
637) -> Result<Vc<OutputAssets>> {
638    let mut combined: Vec<ResolvedVc<Box<dyn OutputAsset>>> =
639        primary.await?.iter().copied().collect();
640    combined.extend(extra.await?.iter().copied());
641    Ok(Vc::cell(combined))
642}
643
644/// Merges two sets of traced modules into one. Used to combine per-route traced
645/// modules with shared modules (e.g. `_app`, `_document`) at report generation time.
646#[turbo_tasks::function]
647pub async fn combine_traced_files(
648    primary: Vc<FileSystemPathVec>,
649    extra: Vc<FileSystemPathVec>,
650) -> Result<Vc<FileSystemPathVec>> {
651    let mut combined: Vec<FileSystemPath> = primary.await?.iter().cloned().collect();
652    combined.extend(extra.await?.iter().cloned());
653    Ok(Vc::cell(combined))
654}
655
656#[turbo_tasks::value]
657pub struct AnalyzedRoute {
658    pub content: ResolvedVc<FileContent>,
659    pub summary: RouteBundleSummary,
660}
661
662#[turbo_tasks::value_impl]
663impl AnalyzedRoute {
664    #[turbo_tasks::function]
665    fn file_content(&self) -> Vc<FileContent> {
666        *self.content
667    }
668}
669
670#[turbo_tasks::function]
671pub async fn analyze_output_assets(
672    output_assets: Vc<OutputAssets>,
673    traced_files: Vc<FileSystemPathVec>,
674    route_entries: Vc<AnalyzeRouteEntries>,
675    chunk_groups: Vc<AnalyzeChunkGroups>,
676    module_graph: Vc<ModuleGraph>,
677) -> Result<Vc<AnalyzedRoute>> {
678    let output_assets = all_assets_from_entries(output_assets);
679    let mut summary = RouteBundleSummary {
680        size: 0,
681        compressed_size: 0,
682        client: BundleTotals {
683            size: 0,
684            compressed_size: 0,
685        },
686    };
687    let route_entries = route_entries.await?.iter().cloned().collect();
688    let module_index_vc = analyze_module_index(module_graph);
689    let module_index = module_index_vc.await?;
690
691    let mut builder =
692        AnalyzeDataBuilder::new(route_entries, module_index.module_index_hash.clone());
693    let mut asset_indices: FxHashMap<ResolvedVc<Box<dyn OutputAsset>>, Vec<u32>> =
694        FxHashMap::default();
695    let mut candidates = Vec::new();
696    let mut browser_chunks = FxHashSet::default();
697
698    let output_assets = output_assets.await?;
699    let traced_files = traced_files.await?;
700    // One call per file: per-route fan-out over shared assets dominates analysis
701    // through task-graph bookkeeping, not the work behind each call.
702    let files = output_assets
703        .iter()
704        .map(|&asset| analyze_output_file(*asset, module_index_vc))
705        .chain(
706            traced_files
707                .iter()
708                .map(|path| analyze_traced_file(path.clone())),
709        )
710        .try_join()
711        .await?;
712
713    for file in files.iter().filter_map(|file| (**file).as_ref()) {
714        let is_client = file.filename.starts_with("[client-fs]/");
715        let output_file_index = builder.add_output_file(AnalyzeOutputFile {
716            filename: file.filename.clone(),
717        });
718        if let Some(emitted) = &file.emitted {
719            let membership = &emitted.membership;
720            asset_indices
721                .entry(emitted.asset)
722                .or_default()
723                .push(output_file_index);
724            if emitted.is_browser_chunk {
725                browser_chunks.insert(output_file_index);
726            }
727            candidates.extend(
728                membership
729                    .load_targets
730                    .iter()
731                    .map(|load| ChunkLoadCandidate {
732                        source: output_file_index,
733                        target: load.target,
734                        trigger_module_index: load.trigger_module_index,
735                    }),
736            );
737            let output = &mut builder.output_files[output_file_index as usize];
738            output.module_indices.extend(&membership.module_indices);
739            output.module_indices.sort_unstable();
740            output.module_indices.dedup();
741            output
742                .async_loader_indices
743                .extend(&membership.async_loader_indices);
744            output.async_loader_indices.sort_unstable();
745            output.async_loader_indices.dedup();
746            output.module_coverage = membership.coverage;
747        }
748        for &AnalyzedChunkPart {
749            ref source,
750            size,
751            compressed_size,
752        } in &file.chunk_parts
753        {
754            let source_index = builder.ensure_source(source).1;
755            summary.size += u64::from(size);
756            summary.compressed_size += u64::from(compressed_size);
757            if is_client {
758                summary.client.size += u64::from(size);
759                summary.client.compressed_size += u64::from(compressed_size);
760            }
761            let chunk_part_index = builder.add_chunk_part(AnalyzeChunkPart {
762                source_index,
763                output_file_index,
764                size,
765                compressed_size,
766            });
767            builder.add_chunk_part_to_output_file(output_file_index, chunk_part_index);
768            builder.add_chunk_part_to_source(source_index, chunk_part_index);
769        }
770    }
771
772    groups::collect_groups(
773        &mut builder,
774        candidates,
775        &asset_indices,
776        chunk_groups,
777        &module_index,
778        &browser_chunks,
779    )
780    .await?;
781
782    builder.finish_sources();
783
784    let rope = builder.build();
785    Ok(AnalyzedRoute {
786        content: FileContent::Content(File::from(rope))
787            .cell()
788            .to_resolved()
789            .await?,
790        summary,
791    }
792    .cell())
793}
794
795#[turbo_tasks::function]
796pub async fn analyze_module_graphs(module_graph: Vc<ModuleGraph>) -> Result<Vc<FileContent>> {
797    let module_index = analyze_module_index(module_graph).await?;
798    let mut builder = ModulesDataBuilder::new(module_index.module_index_hash.clone());
799
800    let mut all_edges = FxIndexSet::default();
801    let mut all_async_edges = FxIndexSet::default();
802    let mut all_traced_edges = FxIndexSet::default();
803    let mut traced_modules = FxHashSet::default();
804
805    let module_graph = module_graph.await?;
806    module_graph.traverse_edges_dfs(
807        module_graph.all_entry_modules(),
808        &mut (),
809        |parent, node, _| {
810            let Some((parent_node, reference)) = parent else {
811                return Ok(GraphTraversalAction::Continue);
812            };
813
814            // ChunkingType::Traced{TracedMode::Entry}     => target is always traced
815            // ChunkingType::Traced{TracedMode::Transitive}=> target only traced if parent is traced
816            // ChunkingType::*                             => target only traced if parent is traced
817            if matches!(
818                reference.chunking_type,
819                ChunkingType::Traced {
820                    mode: TracedMode::Entry
821                }
822            ) || traced_modules.contains(&parent_node)
823            {
824                traced_modules.insert(node);
825                all_traced_edges.insert((parent_node, node));
826                return Ok(GraphTraversalAction::Continue);
827            };
828
829            match reference.chunking_type {
830                ChunkingType::Async => {
831                    all_async_edges.insert((parent_node, node));
832                }
833                _ => {
834                    all_edges.insert((parent_node, node));
835                }
836            }
837            Ok(GraphTraversalAction::Continue)
838        },
839        |_, _, _| Ok(()),
840        true,
841        false,
842    )?;
843
844    type ModulePair = (ResolvedVc<Box<dyn Module>>, ResolvedVc<Box<dyn Module>>);
845    async fn mapper((from, to): ModulePair) -> Result<Option<(RcStr, RcStr)>> {
846        if from == to {
847            return Ok(None);
848        }
849        let from_ident = from.ident().to_string().owned().await?;
850        let to_ident = to.ident().to_string().owned().await?;
851        Ok(Some((from_ident, to_ident)))
852    }
853
854    for module in &module_index.modules {
855        builder.ensure_module(&module.ident, &module.path);
856    }
857
858    let all_edges = all_edges
859        .iter()
860        .copied()
861        .map(mapper)
862        .try_flat_join()
863        .await?;
864    let all_async_edges = all_async_edges
865        .iter()
866        .copied()
867        .map(mapper)
868        .try_flat_join()
869        .await?;
870    let all_traced_edges = all_traced_edges
871        .iter()
872        .copied()
873        .map(mapper)
874        .try_flat_join()
875        .await?;
876    for (from_ident, to_ident) in all_edges {
877        let from_index = builder.get_module(&from_ident).1;
878        let to_index = builder.get_module(&to_ident).1;
879        if from_index == to_index {
880            continue;
881        }
882        builder.modules[from_index as usize]
883            .dependencies
884            .insert(to_index);
885        builder.modules[to_index as usize]
886            .dependents
887            .insert(from_index);
888    }
889    for (from_ident, to_ident) in all_async_edges {
890        let from_index = builder.get_module(&from_ident).1;
891        let to_index = builder.get_module(&to_ident).1;
892        if from_index == to_index {
893            continue;
894        }
895        builder.modules[from_index as usize]
896            .async_dependencies
897            .insert(to_index);
898        builder.modules[to_index as usize]
899            .async_dependents
900            .insert(from_index);
901    }
902    for (from_ident, to_ident) in all_traced_edges {
903        let from_index = builder.get_module(&from_ident).1;
904        let to_index = builder.get_module(&to_ident).1;
905        if from_index == to_index {
906            continue;
907        }
908        builder.modules[from_index as usize]
909            .traced_dependencies
910            .insert(to_index);
911        builder.modules[to_index as usize]
912            .traced_dependents
913            .insert(from_index);
914    }
915
916    let rope = builder.build();
917    Ok(FileContent::Content(File::from(rope)).cell())
918}
919
920#[turbo_tasks::value]
921pub struct AnalyzeDataOutputAsset {
922    pub path: FileSystemPath,
923    pub output_assets: ResolvedVc<OutputAssets>,
924    pub traced_files: ResolvedVc<FileSystemPathVec>,
925    pub route_entries: ResolvedVc<AnalyzeRouteEntries>,
926    pub chunk_groups: ResolvedVc<AnalyzeChunkGroups>,
927    pub module_graph: ResolvedVc<ModuleGraph>,
928}
929
930#[turbo_tasks::value_impl]
931impl AnalyzeDataOutputAsset {
932    #[turbo_tasks::function]
933    pub async fn new(
934        path: FileSystemPath,
935        output_assets: ResolvedVc<OutputAssets>,
936        traced_files: ResolvedVc<FileSystemPathVec>,
937        route_entries: ResolvedVc<AnalyzeRouteEntries>,
938        chunk_groups: ResolvedVc<AnalyzeChunkGroups>,
939        module_graph: ResolvedVc<ModuleGraph>,
940    ) -> Result<Vc<Self>> {
941        Ok(Self {
942            path,
943            output_assets,
944            traced_files,
945            route_entries,
946            chunk_groups,
947            module_graph,
948        }
949        .cell())
950    }
951}
952
953#[turbo_tasks::value_impl]
954impl Asset for AnalyzeDataOutputAsset {
955    #[turbo_tasks::function]
956    fn content(&self) -> Vc<AssetContent> {
957        let file_content = analyze_output_assets(
958            *self.output_assets,
959            *self.traced_files,
960            *self.route_entries,
961            *self.chunk_groups,
962            *self.module_graph,
963        )
964        .file_content();
965        AssetContent::file(file_content)
966    }
967}
968
969#[turbo_tasks::value_impl]
970impl OutputAssetsReference for AnalyzeDataOutputAsset {}
971
972#[turbo_tasks::value_impl]
973impl OutputAsset for AnalyzeDataOutputAsset {
974    #[turbo_tasks::function]
975    fn path(&self) -> Vc<FileSystemPath> {
976        self.path.clone().cell()
977    }
978}
979
980#[turbo_tasks::value]
981pub struct ModulesDataOutputAsset {
982    pub path: FileSystemPath,
983    pub module_graph: ResolvedVc<ModuleGraph>,
984}
985
986#[turbo_tasks::value_impl]
987impl ModulesDataOutputAsset {
988    #[turbo_tasks::function]
989    pub async fn new(
990        path: FileSystemPath,
991        module_graph: ResolvedVc<ModuleGraph>,
992    ) -> Result<Vc<Self>> {
993        Ok(Self { path, module_graph }.cell())
994    }
995}
996
997#[turbo_tasks::value_impl]
998impl Asset for ModulesDataOutputAsset {
999    #[turbo_tasks::function]
1000    fn content(&self) -> Vc<AssetContent> {
1001        let file_content = analyze_module_graphs(*self.module_graph);
1002        AssetContent::file(file_content)
1003    }
1004}
1005
1006#[turbo_tasks::value_impl]
1007impl OutputAssetsReference for ModulesDataOutputAsset {}
1008
1009#[turbo_tasks::value_impl]
1010impl OutputAsset for ModulesDataOutputAsset {
1011    #[turbo_tasks::function]
1012    fn path(&self) -> Vc<FileSystemPath> {
1013        self.path.clone().cell()
1014    }
1015}