Skip to main content

wamex_cli/
incremental.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    fmt::Display,
4};
5
6use anyhow::Result;
7use wamex_types::{BumpVersion, ModuleId, map_vec::MiniSet};
8
9use crate::{
10    ModuleIdentifier, SplitModuleIdentifier, SplitPointExtractor,
11    analysis::{
12        self, StaticModuleInfo,
13        split_point::{OutputModuleInfo, SharedModuleIdentifier},
14        symbols::DiffEntry,
15    },
16    index::{IdMap, SymbolId},
17};
18
19pub type ModuleDeps = BTreeMap<ModuleId, Vec<ModuleId>>;
20pub struct ModuleUpdate {
21    pub module_id: ModuleId,
22    pub force_restart: bool,
23}
24pub enum IncrementalSplitResult {
25    Unchanged,
26    UpdatedModules(Vec<ModuleUpdate>),
27    FullResplit,
28}
29
30pub struct SplitResult {
31    pub deps: ModuleDeps,
32    pub incremental_result: IncrementalSplitResult,
33    _non_exhaustive: (),
34}
35
36#[derive(Clone)]
37pub struct IncrementalSplitState {
38    // Symbols map for the latest source module.
39    last_module_info: StaticModuleInfo,
40    last_module_structure: Vec<(SplitModuleIdentifier, OutputModuleInfo)>,
41    modules_versions: BTreeMap<SplitModuleIdentifier, BumpVersion>,
42    bump_version: BumpVersion,
43    // last structure of split modules?.
44}
45impl IncrementalSplitState {
46    pub fn new() -> Self {
47        Self {
48            last_module_info: StaticModuleInfo::empty(),
49            last_module_structure: Vec::new(),
50            modules_versions: BTreeMap::new(),
51            bump_version: BumpVersion::new(),
52        }
53    }
54    pub fn is_empty(&self) -> bool {
55        self.last_module_structure.is_empty()
56    }
57
58    pub fn split_incremental(
59        &mut self,
60        input_wasm: &[u8],
61        verbose: bool,
62        precise_modification: bool,
63        split_point_extractor: SplitPointExtractor,
64        mut emit_module_fn: impl FnMut(ModuleId, &[u8]) -> Result<()>,
65    ) -> Result<SplitResult> {
66        // 1. Analyze new module.
67        let module = crate::InputModule::parse(input_wasm)?;
68        let info = analysis::ModuleInfo::from_raw_module(module)?;
69        let dep_graph = analysis::dep_graph::get_dependencies(&info)?;
70        let split_points = analysis::split_point::find_split_points(&info, split_point_extractor)?;
71        let wbg_closures = analysis::split_point::SplitProgramInfo::wbg_closures(&info, &dep_graph);
72        let mut split_program_info = crate::SplitProgramInfo::compute_split_modules(
73            &info,
74            &dep_graph,
75            &split_points,
76            &wbg_closures,
77        )?;
78
79        // one of the possible mode is to merge all shared with main chunks into main module.
80        // The other way can be used in incremental build, when main is not changed but we emit "mini-main".
81        crate::emit::merge_main_shared(&mut split_program_info);
82
83        if verbose {
84            println!("Split points: {split_points:?}");
85            println!("Split program info: {split_program_info:?}");
86            println!("Dependency graph: {dep_graph:?}");
87            println!("Module symbols:");
88            info.symbols.print_debug();
89
90            println!("Module split details:");
91            for (name, split_deps) in split_program_info.output_modules.iter() {
92                split_deps.print(format!("{:?}", name).as_str(), &info, &dep_graph);
93            }
94        }
95
96        // TODO: use (module_id, exports, imports) list as well to detect changes.
97        let module_structure = split_program_info.output_modules.clone();
98        let structure_diff = StructureDiffResult::new(
99            &self.last_module_structure,
100            &self.last_module_info,
101            &module_structure,
102            &info,
103        );
104        log::info!("Incremental split analysis result: {}", structure_diff);
105        if structure_diff.not_changed() {
106            log::info!("No changes detected in split modules.");
107            return Ok(SplitResult {
108                deps: self.build_deps_map(),
109                incremental_result: IncrementalSplitResult::Unchanged,
110                _non_exhaustive: (),
111            });
112        }
113        // 4. Update state.
114        self.last_module_structure = module_structure;
115        self.last_module_info = StaticModuleInfo::new(&info);
116        self.bump_version.bump();
117        let whitelist = structure_diff.whitelist();
118
119        let latest_version = self.bump_version.clone();
120        // 5.  Re-split only changed modules.
121        let emit_fn = |identifier: &SplitModuleIdentifier, data: &[u8]| -> Result<()> {
122            self.modules_versions
123                .insert(identifier.clone(), latest_version);
124
125            let module_id = self.last_module_id(identifier);
126            emit_module_fn(module_id, data)
127        };
128
129        crate::emit::emit_modules(
130            &info,
131            verbose,
132            &split_program_info,
133            &wbg_closures,
134            precise_modification,
135            whitelist.as_ref(),
136            latest_version,
137            emit_fn,
138        )?;
139
140        if let Some(changed_modules) = whitelist {
141            Ok(SplitResult {
142                deps: self.build_deps_map(),
143                incremental_result: IncrementalSplitResult::UpdatedModules(
144                    self.build_update_list(changed_modules, structure_diff),
145                ),
146                _non_exhaustive: (),
147            })
148        } else {
149            Ok(SplitResult {
150                deps: self.build_deps_map(),
151                incremental_result: IncrementalSplitResult::FullResplit,
152                _non_exhaustive: (),
153            })
154        }
155    }
156
157    // Build a set of changed modules.
158    // It could be directly changed modules
159    fn build_update_list(
160        &self,
161        changed_list: BTreeSet<SplitModuleIdentifier>,
162        structure_diff: StructureDiffResult,
163    ) -> Vec<ModuleUpdate> {
164        changed_list
165            .into_iter()
166            .map(|id| ModuleUpdate {
167                module_id: self.last_module_id(&id),
168                force_restart: !structure_diff.only_struct_dep_change(&id),
169            })
170            .collect()
171    }
172
173    fn last_module_id(&self, module: &SplitModuleIdentifier) -> ModuleId {
174        ModuleId::new_from_components(
175            module.to_string(),
176            Some(
177                self.modules_versions
178                    .get(module)
179                    .expect("Module version must be present for last emitted module")
180                    .clone(),
181            ),
182            None,
183        )
184    }
185
186    fn build_deps_map(&self) -> ModuleDeps {
187        let shared_modules = self
188            .last_module_structure
189            .iter()
190            .filter_map(|(id, _)| id.as_shared().cloned())
191            .collect::<Vec<_>>();
192
193        let mut deps = BTreeMap::new();
194        for (id, _) in &self.last_module_structure {
195            if id.is_shared() {
196                continue;
197            }
198
199            let module_id = self.last_module_id(id);
200            let module_deps = id.collect_deps(&shared_modules);
201            let prev = deps.insert(
202                module_id,
203                module_deps
204                    .into_iter()
205                    .map(|m| self.last_module_id(&SplitModuleIdentifier::Shared(m)))
206                    .collect(),
207            );
208            debug_assert!(prev.is_none());
209        }
210        deps
211    }
212}
213
214#[derive(Debug)]
215struct StructureDiffResult {
216    // Modules with changed symbols
217    with_changed_symbols: BTreeSet<SplitModuleIdentifier>,
218    // Modules with changed exports (only shared).
219    with_changed_exports: BTreeSet<SharedModuleIdentifier>,
220
221    // Modules that has no symbol changes, but depends on changed modules.
222    changed_deps: BTreeMap<SplitModuleIdentifier, bool /* child modified */>,
223    // Structure of split modules changed
224    structure_changed: bool,
225}
226impl StructureDiffResult {
227    fn main_changed(&self) -> bool {
228        self.with_changed_symbols
229            .contains(&SplitModuleIdentifier::Single(ModuleIdentifier::Main))
230    }
231
232    pub fn not_changed(&self) -> bool {
233        !self.structure_changed
234            && self.with_changed_symbols.is_empty()
235            && self.with_changed_exports.is_empty()
236            && self.changed_deps.is_empty()
237    }
238    pub fn need_full_rebuild(&self) -> bool {
239        self.structure_changed || self.main_changed()
240    }
241    fn entrypoint_list(
242        module_structure: &[(SplitModuleIdentifier, OutputModuleInfo)],
243    ) -> Vec<SplitModuleIdentifier> {
244        module_structure
245            .iter()
246            .filter(|(m, _)| !m.is_shared())
247            .map(|(m, _)| m.clone())
248            .collect()
249    }
250
251    // There is no direct change of module, and dep structure changed only.
252    pub fn only_struct_dep_change(&self, module: &SplitModuleIdentifier) -> bool {
253        self.changed_deps.get(module).copied().unwrap_or(false)
254    }
255    /// List of modules that need to be reemitted.
256    pub fn whitelist(&self) -> Option<BTreeSet<SplitModuleIdentifier>> {
257        if self.need_full_rebuild() {
258            return None;
259        }
260        let mut whitelist = self.with_changed_symbols.clone();
261        whitelist.extend(
262            self.with_changed_exports
263                .iter()
264                .map(|m| SplitModuleIdentifier::Shared(m.clone())),
265        );
266        whitelist.extend(self.changed_deps.keys().cloned());
267
268        Some(whitelist)
269    }
270
271    // Find shared modules with changed exports.
272    fn find_changed_sig<SymMapper>(
273        old_modules: &[(SplitModuleIdentifier, OutputModuleInfo)],
274        new_modules: &[(SplitModuleIdentifier, OutputModuleInfo)],
275        sym_mapper: SymMapper,
276    ) -> BTreeSet<SharedModuleIdentifier>
277    where
278        SymMapper: Fn(&SymbolId) -> Option<SymbolId>,
279    {
280        let old_set: BTreeMap<SplitModuleIdentifier, &OutputModuleInfo> = old_modules
281            .iter()
282            .map(|(m, info)| (m.clone(), info))
283            .collect();
284        let mut new_chunks = BTreeSet::new();
285        for (module, info) in new_modules {
286            let Some(shared) = module.as_shared() else {
287                continue;
288            };
289            if let Some(old_info) = old_set.get(module) {
290                let exports_list = old_info
291                    .exports
292                    .iter()
293                    .map(|s| sym_mapper(s))
294                    .collect::<Option<MiniSet<SymbolId>>>();
295                if let Some(exports_list) = exports_list {
296                    if exports_list == info.exports {
297                        continue;
298                    }
299                }
300            }
301            // If
302            // - Module is new ||
303            // - no mapping for some exported symbol (deleted) ||
304            // - export_list changed
305            // mark module as changed.
306            new_chunks.insert(shared.clone());
307        }
308        new_chunks
309    }
310
311    // Find modules, that uses shared module.
312    // TODO: 1. this is brute-force, 2. we add more deps that needed (not all deps are using shared module).
313    fn shared_module_users(
314        shared: &SharedModuleIdentifier,
315        list: &[(SplitModuleIdentifier, OutputModuleInfo)],
316    ) -> Vec<SplitModuleIdentifier> {
317        list.iter()
318            .filter(|(m, _)| shared.includes(m) && shared != m)
319            .map(|(m, _)| m.clone())
320            .collect()
321    }
322
323    // Find module from module list that uses some of changed shared modules.
324    fn collect_changed_users<'a>(
325        changed_shared: impl Iterator<Item = &'a SharedModuleIdentifier>,
326        new_module_structure: &[(SplitModuleIdentifier, OutputModuleInfo)],
327    ) -> BTreeSet<SplitModuleIdentifier> {
328        let mut changed_deps = BTreeSet::new();
329        for shared in changed_shared {
330            let users = Self::shared_module_users(shared, new_module_structure);
331            for user in users {
332                changed_deps.insert(user);
333            }
334        }
335        changed_deps
336    }
337
338    pub fn new(
339        old_module_structure: &[(SplitModuleIdentifier, OutputModuleInfo)],
340        old_module_info: &StaticModuleInfo,
341        new_module_structure: &[(SplitModuleIdentifier, OutputModuleInfo)],
342        new_module_info: &analysis::ModuleInfo<'_>,
343    ) -> Self {
344        let old_entrypoits = Self::entrypoint_list(old_module_structure);
345        let new_entrypoints = Self::entrypoint_list(new_module_structure);
346        // List of entrypoitns is defined in indirect function table and must be stable between runs.
347        // TODO: we should not only check module list - but also check their entrypoint functions.
348        let rebuild_full = old_entrypoits != new_entrypoints;
349
350        let mut debug_module_changed_info = BTreeMap::new();
351
352        // Diff with last_module_info.
353        if rebuild_full {
354            log::info!("Split module structure changed, performing full rebuild.");
355            return Self {
356                with_changed_symbols: BTreeSet::new(),
357                with_changed_exports: BTreeSet::new(),
358                changed_deps: BTreeMap::new(),
359                structure_changed: true,
360            };
361        }
362
363        // Build symbol -> module map.
364        let mut symbol_map = IdMap::<SymbolId, SplitModuleIdentifier>::new();
365        for (module_id, split_module) in new_module_structure.iter() {
366            for symbol in &split_module.defined_symbols {
367                symbol_map.insert(*symbol, module_id.clone());
368            }
369        }
370
371        // iterate over changed symbols - and mark modules as changed.
372        let differ = analysis::symbols::Differ::new(old_module_info, new_module_info);
373        let sym_map = differ.symbol_map();
374        let diff_result = differ.build_diff(&sym_map);
375        let changed_syms = diff_result
376            .all_changes()
377            .filter_map(|entry| match entry {
378                DiffEntry::Added { right } | DiffEntry::Replaced { right, .. } => Some(right),
379                _ => None,
380            })
381            .copied()
382            .collect::<Vec<_>>();
383
384        // Identify changed split points.
385        let mut with_changed_symbols = BTreeSet::new();
386        for sym in changed_syms {
387            if let Some(module_id) = symbol_map.get(sym) {
388                with_changed_symbols.insert(module_id.clone());
389                debug_module_changed_info
390                    .entry(module_id.clone())
391                    .or_insert_with(Vec::new)
392                    .push(sym);
393            }
394        }
395
396        let mut with_changed_exports =
397            Self::find_changed_sig(old_module_structure, new_module_structure, |sym| {
398                sym_map.map(*sym)
399            });
400
401        let changed_symbols_users = Self::collect_changed_users(
402            with_changed_symbols.iter().filter_map(|m| match m {
403                SplitModuleIdentifier::Shared(shared) => Some(shared),
404                _ => None,
405            }),
406            new_module_structure,
407        )
408        .into_iter()
409        .map(|m| (m, true)); // child has changed symbols
410
411        let changed_exports_users =
412            Self::collect_changed_users(with_changed_exports.iter(), new_module_structure)
413                .into_iter()
414                .map(|m| (m, false)); // child is just restructured
415
416        let mut changed_deps = changed_symbols_users
417            .chain(changed_exports_users)
418            .collect::<BTreeMap<_, _>>();
419
420        // Remove duplicates
421        with_changed_exports
422            .retain(|m| !with_changed_symbols.contains(&SplitModuleIdentifier::Shared(m.clone())));
423
424        changed_deps.retain(|m, _| {
425            !with_changed_symbols.contains(m)
426                && m.as_shared()
427                    .map(|s| !with_changed_exports.contains(s))
428                    .unwrap_or(true)
429        });
430
431        // TODO: assert list of imports remain unchanged (for unchanged modules).
432        // TODO: if imports changed - mark module as changed.
433        let this = Self {
434            with_changed_symbols,
435            with_changed_exports,
436            structure_changed: rebuild_full,
437            changed_deps,
438        };
439        // If main changed - perform full rebuild.
440        if this.main_changed() {
441            log::info!("Main module changed, performing full rebuild.");
442        }
443
444        if !debug_module_changed_info.is_empty() {
445            log::info!("Changed modules and symbols:");
446            for (module, symbols) in debug_module_changed_info.iter() {
447                log::info!("  Module {:?} changed symbols:", module);
448                for sym in symbols {
449                    let symbol = new_module_info.symbols.get(*sym).unwrap();
450                    log::info!(
451                        "    Symbol {:?} <{}>",
452                        sym,
453                        crate::helpers::demangle_full(&symbol.name)
454                    );
455                }
456            }
457        }
458
459        this
460    }
461}
462
463impl Display for StructureDiffResult {
464    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
465        if self.not_changed() {
466            return write!(f, "No changes detected");
467        }
468        // print in format [module1, module2, ...]
469        if !self.with_changed_symbols.is_empty() {
470            let with_changed_symbols = self
471                .with_changed_symbols
472                .iter()
473                .map(|m| m.to_string())
474                .collect::<Vec<_>>()
475                .join(", ");
476            write!(f, " Modules with changed symbols: {}", with_changed_symbols)?;
477        }
478        if !self.with_changed_exports.is_empty() {
479            let with_changed_exports = self
480                .with_changed_exports
481                .iter()
482                .map(|m| m.to_string())
483                .collect::<Vec<_>>()
484                .join(", ");
485            write!(f, " Modules with changed exports: {}", with_changed_exports)?;
486        }
487
488        if !self.changed_deps.is_empty() {
489            let changed_deps = self
490                .changed_deps
491                .iter()
492                .map(|(m, v)| format!("{m}, soft_reload: {v}"))
493                .collect::<Vec<_>>()
494                .join(", ");
495            write!(f, " Changed dependent modules: {}", changed_deps)?;
496        }
497        Ok(())
498    }
499}