Skip to content

Commit 40f9990

Browse files
authored
Fix memory leak on Script and Module source handling (boa-dev#5050)
Yep, the old implementation of early dropping the Script's source had a data leak, thanks to a `Gc` inside an `#[unsafe_ignore_trace]` field. This also cleans up the Module equivalent of this, since we can use the state of the module to carry the source, which saves a couple of bytes (448 -> 392).
1 parent f5ae9f7 commit 40f9990

2 files changed

Lines changed: 99 additions & 63 deletions

File tree

core/engine/src/module/source.rs

Lines changed: 92 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,18 @@ pub(super) struct DfsInfo {
6262
/// but with a state machine-like design for better correctness.
6363
///
6464
/// [cyclic]: https://tc39.es/ecma262/#table-cyclic-module-fields
65-
#[derive(Debug, Trace, Finalize, Default)]
65+
#[derive(Debug, Trace, Finalize)]
6666
#[boa_gc(unsafe_no_drop)]
6767
enum ModuleStatus {
68-
#[default]
69-
Unlinked,
68+
Unlinked {
69+
#[unsafe_ignore_trace]
70+
source: boa_ast::Module,
71+
source_text: SourceText,
72+
},
7073
Linking {
74+
#[unsafe_ignore_trace]
75+
source: boa_ast::Module,
76+
source_text: SourceText,
7177
info: DfsInfo,
7278
},
7379
PreLinked {
@@ -111,15 +117,21 @@ impl ModuleStatus {
111117
where
112118
F: FnOnce(Self) -> Self,
113119
{
114-
*self = f(std::mem::take(self));
120+
*self = f(std::mem::replace(
121+
self,
122+
ModuleStatus::Unlinked {
123+
source: boa_ast::Module::default(),
124+
source_text: SourceText::default(),
125+
},
126+
));
115127
}
116128

117129
/// Gets the current index info of the module within the dependency graph, or `None` if the
118130
/// module is not in a state executing the dfs algorithm.
119131
const fn dfs_info(&self) -> Option<&DfsInfo> {
120132
match self {
121-
Self::Unlinked | Self::EvaluatingAsync { .. } | Self::Evaluated { .. } => None,
122-
Self::Linking { info }
133+
Self::Unlinked { .. } | Self::EvaluatingAsync { .. } | Self::Evaluated { .. } => None,
134+
Self::Linking { info, .. }
123135
| Self::PreLinked { info, .. }
124136
| Self::Linked { info, .. }
125137
| Self::Evaluating { info, .. } => Some(info),
@@ -130,8 +142,8 @@ impl ModuleStatus {
130142
/// or `None` if the module is not in a state executing the dfs algorithm.
131143
fn dfs_info_mut(&mut self) -> Option<&mut DfsInfo> {
132144
match self {
133-
Self::Unlinked | Self::EvaluatingAsync { .. } | Self::Evaluated { .. } => None,
134-
Self::Linking { info }
145+
Self::Unlinked { .. } | Self::EvaluatingAsync { .. } | Self::Evaluated { .. } => None,
146+
Self::Linking { info, .. }
135147
| Self::PreLinked { info, .. }
136148
| Self::Linked { info, .. }
137149
| Self::Evaluating { info, .. } => Some(info),
@@ -142,7 +154,7 @@ impl ModuleStatus {
142154
/// level capability.
143155
const fn top_level_capability(&self) -> Option<&PromiseCapability> {
144156
match &self {
145-
Self::Unlinked
157+
Self::Unlinked { .. }
146158
| Self::Linking { .. }
147159
| Self::PreLinked { .. }
148160
| Self::Linked { .. } => None,
@@ -182,14 +194,30 @@ impl ModuleStatus {
182194
/// Gets the declarative environment from the module status.
183195
fn environment(&self) -> Option<Gc<DeclarativeEnvironment>> {
184196
match self {
185-
ModuleStatus::Unlinked | ModuleStatus::Linking { .. } => None,
197+
ModuleStatus::Unlinked { .. } | ModuleStatus::Linking { .. } => None,
186198
ModuleStatus::PreLinked { environment, .. }
187199
| ModuleStatus::Linked { environment, .. }
188200
| ModuleStatus::Evaluating { environment, .. }
189201
| ModuleStatus::EvaluatingAsync { environment, .. }
190202
| ModuleStatus::Evaluated { environment, .. } => Some(environment.clone()),
191203
}
192204
}
205+
206+
/// If this module is in the unlinked or linking states, gets its source.
207+
fn source(&self) -> Option<(&boa_ast::Module, &SourceText)> {
208+
match self {
209+
ModuleStatus::Unlinked {
210+
source,
211+
source_text,
212+
}
213+
| ModuleStatus::Linking {
214+
source,
215+
source_text,
216+
..
217+
} => Some((source, source_text)),
218+
_ => None,
219+
}
220+
}
193221
}
194222

195223
/// The execution context of a [`SourceTextModule`].
@@ -242,8 +270,6 @@ impl std::fmt::Debug for SourceTextModule {
242270
struct ModuleCode {
243271
has_tla: bool,
244272
requested_modules: IndexSet<super::ModuleRequest, BuildHasherDefault<FxHasher>>,
245-
source: RefCell<Option<boa_ast::Module>>,
246-
source_text: RefCell<Option<SourceText>>,
247273
path: Option<PathBuf>,
248274
import_entries: Vec<ImportEntry>,
249275
local_export_entries: Vec<LocalExportEntry>,
@@ -307,7 +333,7 @@ impl SourceTextModule {
307333
///
308334
/// [parse]: https://tc39.es/ecma262/#sec-parsemodule
309335
pub(super) fn new(
310-
code: boa_ast::Module,
336+
source: boa_ast::Module,
311337
interner: &Interner,
312338
source_text: SourceText,
313339
path: Option<PathBuf>,
@@ -320,11 +346,11 @@ impl SourceTextModule {
320346
interner,
321347
requests: IndexSet::default(),
322348
};
323-
let _ = visitor.visit_module(&code);
349+
let _ = visitor.visit_module(&source);
324350
visitor.requests
325351
};
326352
// 4. Let importEntries be ImportEntries of body.
327-
let import_entries = code.items().import_entries();
353+
let import_entries = source.items().import_entries();
328354

329355
// 5. Let importedBoundNames be ImportedLocalNames(importEntries).
330356
// Can be ignored because this is just a simple `Iter::map`
@@ -337,7 +363,7 @@ impl SourceTextModule {
337363
let mut star_export_entries = Vec::new();
338364

339365
// 10. For each ExportEntry Record ee of exportEntries, do
340-
for ee in code.items().export_entries() {
366+
for ee in source.items().export_entries() {
341367
match ee {
342368
// a. If ee.[[ModuleRequest]] is null, then
343369
ExportEntry::Ordinary(entry) => {
@@ -393,7 +419,7 @@ impl SourceTextModule {
393419
}
394420

395421
// 11. Let async be body Contains await.
396-
let has_tla = contains(&code, ContainsSymbol::AwaitExpression);
422+
let has_tla = contains(&source, ContainsSymbol::AwaitExpression);
397423

398424
// 12. Return Source Text Module Record {
399425
// [[Realm]]: realm, [[Environment]]: empty, [[Namespace]]: empty, [[CycleRoot]]: empty,
@@ -409,16 +435,17 @@ impl SourceTextModule {
409435
// }.
410436
// Most of this can be ignored, since `Status` takes care of the remaining state.
411437
Self {
412-
status: GcRefCell::default(),
438+
status: GcRefCell::new(ModuleStatus::Unlinked {
439+
source,
440+
source_text,
441+
}),
413442
loaded_modules: GcRefCell::default(),
414443
async_parent_modules: GcRefCell::default(),
415444
import_meta: GcRefCell::default(),
416445
code: ModuleCode {
417-
source: RefCell::new(Some(code)),
418-
source_text: RefCell::new(Some(source_text)),
419-
path,
420-
requested_modules,
421446
has_tla,
447+
requested_modules,
448+
path,
422449
import_entries,
423450
local_export_entries,
424451
indirect_export_entries,
@@ -523,7 +550,7 @@ impl SourceTextModule {
523550
// 2. If module is a Cyclic Module Record, module.[[Status]] is new, and state.[[Visited]] does not contain
524551
// module, then
525552
// a. Append module to state.[[Visited]].
526-
if matches!(&*self.status.borrow(), ModuleStatus::Unlinked)
553+
if matches!(&*self.status.borrow(), ModuleStatus::Unlinked { .. })
527554
&& state.visited.borrow_mut().insert(module_self.clone())
528555
{
529556
// b. Let requestedModulesCount be the number of elements in module.[[RequestedModules]].
@@ -765,7 +792,7 @@ impl SourceTextModule {
765792
// 1. Assert: module.[[Status]] is one of unlinked, linked, evaluating-async, or evaluated.
766793
debug_assert!(matches!(
767794
&*self.status.borrow(),
768-
ModuleStatus::Unlinked
795+
ModuleStatus::Unlinked { .. }
769796
| ModuleStatus::Linked { .. }
770797
| ModuleStatus::EvaluatingAsync { .. }
771798
| ModuleStatus::Evaluated { .. }
@@ -780,12 +807,24 @@ impl SourceTextModule {
780807
// a. For each Cyclic Module Record m of stack, do
781808
for m in stack.iter().filter_map(|cmr| cmr.kind().as_source_text()) {
782809
// i. Assert: m.[[Status]] is linking.
783-
debug_assert!(matches!(&*m.status.borrow(), ModuleStatus::Linking { .. }));
784810
// ii. Set m.[[Status]] to unlinked.
785-
*m.status.borrow_mut() = ModuleStatus::Unlinked;
811+
m.status.borrow_mut().transition(|status| match status {
812+
ModuleStatus::Linking {
813+
source,
814+
source_text,
815+
..
816+
} => ModuleStatus::Unlinked {
817+
source,
818+
source_text,
819+
},
820+
_ => unreachable!("i. Assert: m.[[Status]] is linking."),
821+
});
786822
}
787823
// b. Assert: module.[[Status]] is unlinked.
788-
debug_assert!(matches!(&*self.status.borrow(), ModuleStatus::Unlinked));
824+
debug_assert!(matches!(
825+
&*self.status.borrow(),
826+
ModuleStatus::Unlinked { .. }
827+
));
789828
// c. Return ? result.
790829
return Err(err);
791830
}
@@ -827,18 +866,23 @@ impl SourceTextModule {
827866
return Ok(index);
828867
}
829868

830-
// 3. Assert: module.[[Status]] is unlinked.
831-
debug_assert!(matches!(&*self.status.borrow(), ModuleStatus::Unlinked));
832-
833869
// 4. Set module.[[Status]] to linking.
834870
// 5. Set module.[[DFSIndex]] to index.
835871
// 6. Set module.[[DFSAncestorIndex]] to index.
836-
*self.status.borrow_mut() = ModuleStatus::Linking {
837-
info: DfsInfo {
838-
dfs_index: index,
839-
dfs_ancestor_index: index,
872+
self.status.borrow_mut().transition(|status| match status {
873+
ModuleStatus::Unlinked {
874+
source,
875+
source_text,
876+
} => ModuleStatus::Linking {
877+
source,
878+
source_text,
879+
info: DfsInfo {
880+
dfs_index: index,
881+
dfs_ancestor_index: index,
882+
},
840883
},
841-
};
884+
_ => unreachable!("3. Assert: module.[[Status]] is unlinked."),
885+
});
842886

843887
// 7. Set index to index + 1.
844888
index += 1;
@@ -873,10 +917,11 @@ impl SourceTextModule {
873917
DfsInfo {
874918
dfs_ancestor_index, ..
875919
},
920+
..
876921
} = &*required_module_src.status.borrow()
877922
{
878-
// 1. Set module.[[DFSAncestorIndex]] to min(module.[[DFSAncestorIndex]],
879-
// requiredModule.[[DFSAncestorIndex]]).
923+
// 1. Set module.[[DFSAncestorIndex]] to
924+
// min(module.[[DFSAncestorIndex]], requiredModule.[[DFSAncestorIndex]]).
880925

881926
Some(*dfs_ancestor_index)
882927
} else {
@@ -976,7 +1021,7 @@ impl SourceTextModule {
9761021
// 1. Assert: This call to Evaluate is not happening at the same time as another call to Evaluate within the surrounding agent.
9771022
let (module, promise) = {
9781023
match &*self.status.borrow() {
979-
ModuleStatus::Unlinked
1024+
ModuleStatus::Unlinked { .. }
9801025
| ModuleStatus::Linking { .. }
9811026
| ModuleStatus::PreLinked { .. }
9821027
| ModuleStatus::Evaluating { .. } => {
@@ -1559,24 +1604,16 @@ impl SourceTextModule {
15591604
// 4. Assert: realm is not undefined.
15601605
let realm = module_self.realm().clone();
15611606

1562-
// Take the AST and source text — they are only needed during compilation.
1563-
// Dropping them here frees the parse tree after linking is complete.
1564-
let source = self
1565-
.code
1566-
.source
1567-
.take()
1568-
.expect("module source consumed before initialize_environment");
1569-
let source_text = self
1570-
.code
1571-
.source_text
1572-
.take()
1573-
.expect("module source_text consumed before initialize_environment");
1607+
let status = self.status.borrow();
1608+
let (source, source_text) = status
1609+
.source()
1610+
.expect("module can only initialize its environment in the linking phase");
15741611

15751612
// 5. Let env be NewModuleEnvironment(realm.[[GlobalEnv]]).
15761613
// 6. Set module.[[Environment]] to env.
15771614
let env = source.scope().clone();
15781615

1579-
let spanned_source_text = SpannedSourceText::new_source_only(source_text);
1616+
let spanned_source_text = SpannedSourceText::new_source_only(source_text.clone());
15801617
let mut compiler = ByteCompiler::new(
15811618
js_string!("<main>"),
15821619
true,
@@ -1664,7 +1701,7 @@ impl SourceTextModule {
16641701

16651702
// 18. Let code be module.[[ECMAScriptCode]].
16661703
// 19. Let varDeclarations be the VarScopedDeclarations of code.
1667-
let var_declarations = var_scoped_declarations(&source);
1704+
let var_declarations = var_scoped_declarations(source);
16681705
// 20. Let declaredVarNames be a new empty List.
16691706
let mut declared_var_names = Vec::new();
16701707
// 21. For each element d of varDeclarations, do
@@ -1695,7 +1732,7 @@ impl SourceTextModule {
16951732

16961733
// 22. Let lexDeclarations be the LexicallyScopedDeclarations of code.
16971734
// 23. Let privateEnv be null.
1698-
let lex_declarations = lexically_scoped_declarations(&source);
1735+
let lex_declarations = lexically_scoped_declarations(source);
16991736
let mut functions = Vec::new();
17001737
// 24. For each element d of lexDeclarations, do
17011738
for declaration in lex_declarations {
@@ -1759,6 +1796,7 @@ impl SourceTextModule {
17591796
// 8. Let moduleContext be a new ECMAScript code execution context.
17601797
let mut envs = EnvironmentStack::new();
17611798
envs.push_module(source.scope().clone());
1799+
drop(status);
17621800

17631801
// 9. Set the Function of moduleContext to null.
17641802
// 10. Assert: module.[[Realm]] is not undefined.
@@ -1862,7 +1900,7 @@ impl SourceTextModule {
18621900

18631901
// 16. Set module.[[Context]] to moduleContext.
18641902
self.status.borrow_mut().transition(|state| match state {
1865-
ModuleStatus::Linking { info } => ModuleStatus::PreLinked {
1903+
ModuleStatus::Linking { info, .. } => ModuleStatus::PreLinked {
18661904
environment: env,
18671905
info,
18681906
context: SourceTextContext {

core/engine/src/script.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@
88
//! [spec]: https://tc39.es/ecma262/#sec-scripts
99
//! [script]: https://tc39.es/ecma262/#sec-script-records
1010
11-
use std::{
12-
cell::RefCell,
13-
path::{Path, PathBuf},
14-
};
11+
use std::path::{Path, PathBuf};
1512

1613
use rustc_hash::FxHashMap;
1714

@@ -45,16 +42,17 @@ impl std::fmt::Debug for Script {
4542
.finish()
4643
}
4744
}
48-
#[derive(Debug, Finalize)]
45+
46+
#[derive(Trace, Debug, Finalize)]
4947
enum ScriptPhase {
50-
Ast(boa_ast::Script),
48+
Ast(#[unsafe_ignore_trace] boa_ast::Script),
5149
Codeblock(Gc<CodeBlock>),
5250
}
51+
5352
#[derive(Trace, Finalize)]
5453
struct Inner {
5554
realm: Realm,
56-
#[unsafe_ignore_trace]
57-
phase: RefCell<ScriptPhase>,
55+
phase: GcRefCell<ScriptPhase>,
5856
source_text: SourceText,
5957
loaded_modules: GcRefCell<FxHashMap<JsString, Module>>,
6058
host_defined: HostDefined,
@@ -108,7 +106,7 @@ impl Script {
108106
Ok(Self {
109107
inner: Gc::new(Inner {
110108
realm: realm.unwrap_or_else(|| context.realm().clone()),
111-
phase: RefCell::new(ScriptPhase::Ast(code)),
109+
phase: GcRefCell::new(ScriptPhase::Ast(code)),
112110
source_text,
113111
loaded_modules: GcRefCell::default(),
114112
host_defined: HostDefined::default(),

0 commit comments

Comments
 (0)