Skip to content
Open
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
5 changes: 5 additions & 0 deletions apps/manifest-demo/3010-rspack-provider/rspack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,16 @@ module.exports = (_env, argv = {}) => {
},
type: 'javascript/auto',
},
{
test: /\.module\.css$/,
type: 'css/module',
},
],
},
experiments: {
css: true,
},
lazyCompilation: false,
plugins: [
new HtmlRspackPlugin({
template: path.resolve(__dirname, 'src/index.html'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ module.exports = (_env, argv = {}) => {
},
],
},
lazyCompilation: false,
plugins: [
new HtmlRspackPlugin({
template: path.resolve(__dirname, 'src/index.html'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ module.exports = (_env, argv = {}) => {
},
],
},
lazyCompilation: false,
plugins: [
new HtmlRspackPlugin({
template: path.resolve(__dirname, 'src/index.html'),
Expand Down
174 changes: 174 additions & 0 deletions apps/modernjs-ssr/host/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Modern SSR removeRemote cache repro

This app contains a manual repro for checking whether a same-name remote can be
removed from SSR memory and loaded again from a different URL.

## Services

Start the three apps from the repository root:

```bash
pnpm --dir apps/modernjs-ssr/remote dev
pnpm --dir apps/modernjs-ssr/remote-new-version dev
pnpm --dir apps/modernjs-ssr/host dev
```

The host `dev` script starts Node with `--expose-gc`, so the repro page can
trigger GC before collecting memory snapshots.

Ports used by this repro:

- Host: `http://localhost:3050`
- Remote v1: `http://127.0.0.1:3051/static/mf-manifest.json`
- Remote v2: `http://127.0.0.1:3055/mf-manifest.json`

## Manual Repro

Open:

```text
http://localhost:3050/remove-remote-cache
```

The page runs the full flow once:

1. Register remote v1.
2. Snapshot memory before loading the remote.
3. Load `remote/Heavy` from v1.
4. Snapshot memory after loading v1.
5. Call `removeRemote('remote')`.
6. Snapshot memory after removal.
7. Trigger GC and snapshot memory again.
8. Wait 10s, 20s, and 30s, triggering GC before each delayed snapshot.
9. Register remote v2.
10. Load `remote/Heavy` from v2.
11. Snapshot memory after loading v2.

## What To Check

Check the table on the page. The most useful column is `heap MB`.

Expected remote result:

- `heavy version` is `v1`
- `reloaded heavy version` is `v2`
- `initial remote` points to `127.0.0.1:3051/static/mf-manifest.json`
- `reloaded remote` points to `127.0.0.1:3055/mf-manifest.json`

Expected memory shape:

- `after load` is higher than `before load`, because v1 loads a large payload.
- `after gc` or one of the delayed GC snapshots should be lower than
`after removeRemote`.
- `after reload` is higher again, because v2 loads another large payload.

The raw JSON is also printed in `#remove-remote-cache-result` at the bottom of
the page.

## Split Step Repro

Use these routes when heap snapshots make the full route too heavy. Visit them
in order from the same host process:

```text
http://localhost:3050/remove-remote-cache/load-remote
http://localhost:3050/remove-remote-cache/remove-remote
http://localhost:3050/remove-remote-cache/register-new-remote
```

Each route returns one memory row and writes one heap snapshot:

- `load-remote`: registers remote v1, loads `remote/Heavy`, then snapshots
memory.
- `remove-remote`: calls `removeRemote('remote')`, triggers GC, then snapshots
memory. It does not register remote v2.
- `register-new-remote`: registers remote v2, loads `remote/Heavy`, then
snapshots memory.

## Heap Snapshot Debugging

Heap snapshots are disabled by default because each snapshot pauses the process
and can use a large amount of memory. Enable them only when you need to inspect
what is retained after `removeRemote`.

Start the host with heap snapshots enabled:

```bash
MF_SSR_HEAP_SNAPSHOT=all \
MF_SSR_HEAP_SNAPSHOT_DIR=/tmp/mf-ssr-cache-probe \
pnpm --dir apps/modernjs-ssr/host dev
```

Then open:

```text
http://localhost:3050/remove-remote-cache
```

Each memory row will include a `heap snapshot` file path. The files are also
written to `MF_SSR_HEAP_SNAPSHOT_DIR`.

To reduce the number of files, pass a comma-separated label list instead of
`all`:

```bash
MF_SSR_HEAP_SNAPSHOT='before load,after gc,after delayed gc 30s' \
MF_SSR_HEAP_SNAPSHOT_DIR=/tmp/mf-ssr-cache-probe \
pnpm --dir apps/modernjs-ssr/host dev
```

Recommended snapshots to compare:

- `before load`: baseline before loading `remote/Heavy`
- `after load`: memory after loading remote v1
- `after gc`: memory after `removeRemote` and immediate GC
- `after delayed gc 30s`: memory after delayed GC
- `after reload`: memory after loading remote v2

Open the `.heapsnapshot` files in Chrome DevTools:

1. Open DevTools.
2. Go to the `Memory` tab.
3. Use `Load` to load the snapshot files.
4. Select the later snapshot and switch to `Comparison`.
5. Compare it against `before load`.
6. Search for `remote-heavy`, `remote-heavy-v2`, `Heavy`, or `remote/Heavy`.
7. Select retained objects and inspect `Retainers` to see what still references
them.

When reporting retained memory, include:

- the two snapshot files being compared
- the top retained object names
- retained size
- the `Retainers` path for suspicious objects
- the table from `/remove-remote-cache`

## Fast Route

For repeated checks without the 10s/20s/30s wait, use:

```text
http://localhost:3050/remove-remote-cache-fast
```

This route runs the same load, remove, GC, and reload flow, but skips delayed
snapshots.

## Automated Checks

Run the Cypress check:

```bash
node tools/scripts/run-modern-e2e.mjs --mode=manifest
```

Run the local repeated-memory probe:

```bash
pnpm run probe:modern:ssr-gc -- --iterations=20 --max-growth-mb=20
```

The repeated probe starts one host process, runs the fast repro route multiple
times, and fails if heap usage keeps growing beyond the configured limit after
warmup.
61 changes: 61 additions & 0 deletions apps/modernjs-ssr/host/cypress/e2e/remove-remote-cache.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
describe('/remove-remote-cache', () => {
it('reloads a same-name remote from a different URL after removeRemote', () => {
cy.visit('/remove-remote-cache', {
timeout: 120_000,
});

cy.get('#remove-remote-cache-result', { timeout: 60_000 })
.invoke('text')
.then((text) => {
const result = JSON.parse(text);

expect(result.initialRemoteEntry).to.contain(
'127.0.0.1:3051/static/mf-manifest.json',
);
expect(result.reloadedRemoteEntry).to.contain(
'127.0.0.1:3055/mf-manifest.json',
);
expect(result.gcAvailable).to.equal(true);
expect(result.heavyStats.version).to.equal('v1');
expect(result.reloadedHeavyStats.version).to.equal('v2');
expect(result.heavyStats.items).to.equal(200000);
expect(result.reloadedHeavyStats.items).to.equal(200000);
expect(result.heavyStats.first).to.not.equal(
result.reloadedHeavyStats.first,
);
expect(result.removeRemoteError).to.equal(undefined);
expect(result.clearCacheCalls).to.have.length(1);
expect(result.clearCacheCalls[0].result).to.equal('resolved');
expect(result.snapshots.map((item) => item.label)).to.deep.equal([
'before load',
'after load',
'after removeRemote',
'after gc',
'after delayed gc 10s',
'after delayed gc 20s',
'after delayed gc 30s',
'after reload',
]);

const snapshotsByLabel = Object.fromEntries(
result.snapshots.map((item) => [item.label, item]),
);
const afterRemoveHeap =
snapshotsByLabel['after removeRemote'].heapUsedMb;
const delayedGcSnapshots = [
snapshotsByLabel['after delayed gc 10s'],
snapshotsByLabel['after delayed gc 20s'],
snapshotsByLabel['after delayed gc 30s'],
];

expect(
delayedGcSnapshots.some(
(snapshot) => snapshot.heapUsedMb < afterRemoveHeap,
),
`heap should decrease after removeRemote within 30s, afterRemove=${afterRemoveHeap}, delayed=${delayedGcSnapshots
.map((snapshot) => snapshot.heapUsedMb)
.join(',')}`,
).to.equal(true);
});
});
});
4 changes: 2 additions & 2 deletions apps/modernjs-ssr/host/module-federation.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { createModuleFederationConfig } from '@module-federation/modern-js-v3';
export default createModuleFederationConfig({
name: 'host',
remotes: {
remote: 'remote@http://localhost:3051/static/mf-manifest.json',
nested_remote: 'nested_remote@http://localhost:3052/mf-manifest.json',
remote: 'remote@http://127.0.0.1:3051/static/mf-manifest.json',
nested_remote: 'nested_remote@http://127.0.0.1:3052/mf-manifest.json',
},
shared: {
react: { singleton: true },
Expand Down
4 changes: 2 additions & 2 deletions apps/modernjs-ssr/host/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"version": "0.1.34",
"scripts": {
"reset": "npx rimraf ./**/node_modules",
"dev": "modern dev",
"dev": "node --expose-gc ./node_modules/@modern-js/app-tools/bin/modern.js dev",
"build": "modern build",
"start": "modern start",
"serve": "modern serve",
"serve": "node --expose-gc ./node_modules/@modern-js/app-tools/bin/modern.js serve",
"new": "modern new",
"lint": "modern lint",
"upgrade": "modern upgrade",
Expand Down
13 changes: 13 additions & 0 deletions apps/modernjs-ssr/host/src/remotes.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
declare module 'remote/Image' {
import type { ComponentType } from 'react';

const Comp: ComponentType<any>;
export default Comp;
}

declare module 'nested_remote/Content' {
import type { ComponentType } from 'react';

const Comp: ComponentType<any>;
export default Comp;
}
6 changes: 4 additions & 2 deletions apps/modernjs-ssr/host/src/routes/all/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
registerRemotes,
} from '@module-federation/modern-js-v3/runtime';

type LazyRemoteModule = { default: React.ComponentType<any> };

registerRemotes([
{
name: 'dynamic_nested_remote',
Expand All @@ -17,13 +19,13 @@ registerRemotes([

const DynamicNestedRemote = React.lazy(() =>
loadRemote('dynamic_nested_remote/Content').then((m) => {
return m;
return m as LazyRemoteModule;
}),
);

const DynamicRemote = React.lazy(() =>
loadRemote('dynamic_remote').then((m) => {
return m;
return m as LazyRemoteModule;
}),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
registerRemotes,
} from '@module-federation/modern-js-v3/runtime';

type LazyRemoteModule = { default: React.ComponentType<any> };

registerRemotes([
{
name: 'dynamic_nested_remote',
Expand All @@ -13,7 +15,7 @@ registerRemotes([

const DynamicNestedRemote = React.lazy(() =>
loadRemote('dynamic_nested_remote/Content').then((m) => {
return m;
return m as LazyRemoteModule;
}),
);

Expand Down
6 changes: 4 additions & 2 deletions apps/modernjs-ssr/host/src/routes/dynamic-remote/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
registerRemotes,
} from '@module-federation/modern-js-v3/runtime';

type LazyRemoteModule = { default: React.ComponentType<any> };

registerRemotes([
{
name: 'dynamic_remote',
Expand All @@ -21,12 +23,12 @@ const RemoteSSRComponent = getInstance()!.createLazyComponent({
}
return <div>fallback</div>;
},
});
}) as React.ComponentType<{ text: string }>;

const NewRemoteCom = React.lazy(() =>
loadRemote('dynamic_remote').then((m) => {
console.log('加载');
return m;
return m as LazyRemoteModule;
}),
);
const Index = (): JSX.Element => {
Expand Down
4 changes: 3 additions & 1 deletion apps/modernjs-ssr/host/src/routes/remote/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import {
loadRemote,
} from '@module-federation/modern-js-v3/runtime';

type LazyRemoteModule = { default: React.ComponentType<any> };

const NewRemoteCom = React.lazy(() =>
loadRemote('remote/Image').then((m) => {
console.log('加载');
return m;
return m as LazyRemoteModule;
}),
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { runProbe } from '../remove-remote-cache/probe';

export const loader = () =>
runProbe({
delayedGcSeconds: [],
});

export type { ProbeResult } from '../remove-remote-cache/probe';
Loading
Loading