Skip to content

Commit 50bb451

Browse files
motiz88facebook-github-bot
authored andcommitted
Fix crash when GCing modules reachable via multiple paths
Summary: Changelog: * **[Fix]**: Fix incremental build crashing when garbage collecting modules reachable via multiple paths in the graph. Under the GC algorithm implemented in D36403390 (9065257), the same white ( = marked for deletion) node may be reached multiple times during the "CollectRoots" phase - specifically, if it is the child of two or more white nodes. We had an overly strict null check that caused a crash in this case. In the test case added to `traverseDependencies-test`, the node that can be reached multiple times is `/baz`: ``` ┌─────────────────────────┐ │ ▼ ┌─────────┐ / ┌──────┐ ┌──────┐ ┌──────┐ │ /bundle │ ┈/▷ │ /foo │ ──▶ │ /bar │ ──▶ │ /baz │ └─────────┘ / └──────┘ └──────┘ └──────┘ ▲ │ └────────────┘ ``` A real-world instance of this crash can be seen by launching the default React Native starter app (created with `npx react-native init MyApp`) and deleting the contents of `index.js`. This causes a crash when Metro attempts to collect the `node_modules/babel/runtime/helpers/interopRequireDefault.js` dependency multiple times. Reviewed By: huntie Differential Revision: D39416657 fbshipit-source-id: 084b3a0f6363ffc5f51f8cdabc1fc7e81616f4db
1 parent e9ee037 commit 50bb451

2 files changed

Lines changed: 41 additions & 4 deletions

File tree

packages/metro/src/DeltaBundler/__tests__/traverseDependencies-test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2188,6 +2188,42 @@ describe('edge cases', () => {
21882188
expect(mockTransform).toHaveBeenCalledWith('/bar', undefined);
21892189
expect(mockTransform).toHaveBeenCalledWith('/foo', undefined);
21902190
});
2191+
2192+
it('removing a cycle with multiple outgoing edges to the same module', async () => {
2193+
/*
2194+
┌─────────────────────────┐
2195+
│ ▼
2196+
┌─────────┐ ┌──────┐ ┌──────┐ ┌──────┐
2197+
│ /bundle │ ──▶ │ /foo │ ──▶ │ /bar │ ──▶ │ /baz │
2198+
└─────────┘ └──────┘ └──────┘ └──────┘
2199+
▲ │
2200+
└────────────┘
2201+
*/
2202+
Actions.addDependency('/bar', '/foo');
2203+
Actions.addDependency('/bar', '/baz');
2204+
files.clear();
2205+
2206+
await initialTraverseDependencies(graph, options);
2207+
2208+
/*
2209+
┌─────────────────────────┐
2210+
│ ▼
2211+
┌─────────┐ / ┌──────┐ ┌──────┐ ┌──────┐
2212+
│ /bundle │ ┈/▷ │ /foo │ ──▶ │ /bar │ ──▶ │ /baz │
2213+
└─────────┘ / └──────┘ └──────┘ └──────┘
2214+
▲ │
2215+
└────────────┘
2216+
*/
2217+
Actions.removeDependency('/bundle', '/foo');
2218+
2219+
expect(
2220+
getPaths(await traverseDependencies([...files], graph, options)),
2221+
).toEqual({
2222+
added: new Set(),
2223+
modified: new Set(['/bundle']),
2224+
deleted: new Set(['/foo', '/bar', '/baz']),
2225+
});
2226+
});
21912227
});
21922228

21932229
describe('require.context', () => {

packages/metro/src/DeltaBundler/graphOperations.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -812,10 +812,11 @@ function collectWhite<T>(module: Module<T>, graph: Graph<T>, delta: Delta) {
812812
) {
813813
graph.privateState.gc.color.set(module.path, 'black');
814814
for (const dependency of module.dependencies.values()) {
815-
const childModule = nullthrows(
816-
graph.dependencies.get(dependency.absolutePath),
817-
);
818-
collectWhite(childModule, graph, delta);
815+
const childModule = graph.dependencies.get(dependency.absolutePath);
816+
// The child may already have been collected.
817+
if (childModule) {
818+
collectWhite(childModule, graph, delta);
819+
}
819820
}
820821
freeModule(module, graph, delta);
821822
}

0 commit comments

Comments
 (0)