Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 51 additions & 7 deletions src/react_compiler/validation/validate_exhaustive_dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,39 +492,54 @@ fn collect_dependencies(
for (_block_id, block) in &func.body.blocks {
// Process phis
for phi in &block.phis {
// TS tests `deps.length`, then stores `new Set(deps)`.
let mut deps: Vec<InferredDependency> = Vec::new();
let mut deps_len: usize = 0;
for (_pred_id, operand) in &phi.operands {
if let Some(dep) = temporaries.get(operand.identifier) {
match dep {
Temporary::Aggregate {
dependencies: agg, ..
} => {
deps.extend(agg.iter().cloned());
deps_len += agg.len();
for dep in agg {
if !contains_same_dependency(&deps, dep) {
deps.push(dep.clone());
}
}
}
Temporary::Local {
identifier,
path,
context,
loc,
} => {
deps.push(InferredDependency::Local {
deps_len += 1;
let dep = InferredDependency::Local {
identifier: *identifier,
path: path.clone(),
context: *context,
loc: *loc,
});
};
if !contains_same_dependency(&deps, &dep) {
deps.push(dep);
}
}
Temporary::Global { binding } => {
deps.push(InferredDependency::Global {
deps_len += 1;
let dep = InferredDependency::Global {
binding: binding.clone(),
});
};
if !contains_same_dependency(&deps, &dep) {
deps.push(dep);
}
}
}
}
}
if deps.is_empty() {
if deps_len == 0 {
continue;
} else if deps.len() == 1 {
} else if deps_len == 1 {
let dep = &deps[0];
match dep {
InferredDependency::Local {
Expand Down Expand Up @@ -1587,6 +1602,35 @@ fn is_optional_dependency_inferred(
// Equality check for temporaries
// =============================================================================

/// Stands in for object identity in the TS `Set<InferredDependency>` of a phi.
fn is_same_dependency(a: &InferredDependency, b: &InferredDependency) -> bool {
match (a, b) {
(
InferredDependency::Global { binding: ab },
InferredDependency::Global { binding: bb },
) => ab.name() == bb.name(),
(
InferredDependency::Local {
identifier: a_id,
path: a_path,
context: a_context,
loc: a_loc,
},
InferredDependency::Local {
identifier: b_id,
path: b_path,
context: b_context,
loc: b_loc,
},
) => a_id == b_id && a_context == b_context && a_loc == b_loc && a_path == b_path,
_ => false,
}
}

fn contains_same_dependency(deps: &[InferredDependency], dep: &InferredDependency) -> bool {
deps.iter().any(|d| is_same_dependency(d, dep))
}

fn is_equal_temporary(a: &InferredDependency, b: &InferredDependency) -> bool {
match (a, b) {
(
Expand Down
39 changes: 39 additions & 0 deletions test/bundler/transpiler/react-compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2095,6 +2095,45 @@ test("react-compiler memory does not grow with the square of the size of a compo
expect(pattern - empty).toBeLessThan(bound);
});

// ValidateExhaustiveDependencies gives each phi the dependencies of its
// operands. TS keeps them in a `Set`. The port appended clones to a `Vec`, so a
// phi held one copy of a dependency for each path that reaches it. Each `if`
// below merges `v` twice, which doubled the list: 20 of them took 300 MB, 22
// took 1 GB and 24 took 4 GB.
test("react-compiler keeps one copy of each dependency of a phi", async () => {
// A debug build takes 7 seconds for 20 and 25 seconds for 22.
const statements = isDebug || isASAN ? 20 : 22;
using dir = tempDir("react-compiler-phi-dependencies", {
"empty.jsx": `export default function App() { return null; }`,
"ladder.jsx": `
export default function App(p) {
let v = "init";
${Array.from({ length: statements }, (_, i) => `if (p.a${i}) { log(${i}); } else if (p.b${i}) { v = "s${i}"; }`).join("\n")}
return <div>{v}</div>;
}
`,
});

const build = async (entry: string) => {
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--react-compiler", "--target=browser", "--external=*", entry],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
return { memoized: /\b_c\(\d+\)/.test(stdout), peakMB: proc.resourceUsage()!.maxRSS / 1024 / 1024 };
};

const [empty, ladder] = await Promise.all([build("empty.jsx"), build("ladder.jsx")]);
expect(ladder.memoized).toBe(true);
// Above the empty build: 300 MB to 1 GB without the fix, under 20 MB with it.
expect(ladder.peakMB - empty.peakMB).toBeLessThan(100);
});

// validate_locals_not_reassigned_after_render (src/react_compiler/validation)
// records the locals a component's closures capture while walking the
// component body, and reports a nested function that assigns to one of them,
Expand Down
Loading