Fix Jotai state leak in frontend tests causing unbounded memory growth (#18249)

## Summary

The global `jotaiStore` singleton was shared across all tests without
reset, leaking state between test suites and causing unbounded heap
growth within each Jest worker.

- `getJestMetadataAndApolloMocksWrapper` now creates a **fresh Jotai
store per wrapper** (equivalent of RecoilRoot isolation), so each test
gets clean state
- Added `workerIdleMemoryLimit: '512MB'` to recycle workers that still
accumulate memory
- Set `maxWorkers: 3` for CI

## Performance

Measured on 48 test suites (command-menu + views + object-record hooks),
single worker:

| Metric | Before | After |
|---|---|---|
| Peak heap | **1519 MB** | **~530 MB** (-65%) |
| Final heap | **1519 MB** | **437 MB** (-71%) |
| Growth pattern | Monotonic (never freed) | Bounded sawtooth (recycled
at 512MB) |
This commit is contained in:
Charles Bochet
2026-02-26 01:20:25 +01:00
committed by GitHub
parent a9649b06d5
commit f325509d96
5 changed files with 27 additions and 49 deletions
+2 -1
View File
@@ -86,7 +86,8 @@ const jestConfig = {
'display/icon/index.ts',
],
coverageDirectory: './coverage',
maxWorkers: '50%',
maxWorkers: 3,
workerIdleMemoryLimit: '512MB',
errorOnDeprecated: true,
};
@@ -4,10 +4,7 @@ import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import {
jotaiStore,
resetJotaiStore,
} from '@/ui/utilities/state/jotai/jotaiStore';
import { resetJotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups } from '@/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
@@ -42,10 +39,6 @@ describe('useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups', () => {
resetJotaiStore();
});
afterEach(() => {
jotaiStore.set(coreViewsState.atom, []);
});
const mockViewFilterGroup: ViewFilterGroup = {
__typename: 'ViewFilterGroup',
id: 'filter-group-1',
@@ -79,8 +72,6 @@ describe('useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups', () => {
} satisfies CoreViewWithRelations;
it('should apply view filter groups from current view', () => {
jotaiStore.set(coreViewsState.atom, [mockCoreView]);
const { result } = renderHook(
() => {
const { applyCurrentViewFilterGroupsToCurrentRecordFilterGroups } =
@@ -103,6 +94,9 @@ describe('useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups', () => {
contextStoreCurrentViewId: mockView.id,
contextStoreCurrentObjectMetadataNameSingular:
mockObjectMetadataItemNameSingular,
onInitializeJotaiStore: (store) => {
store.set(coreViewsState.atom, [mockCoreView]);
},
}),
},
);
@@ -130,8 +124,6 @@ describe('useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups', () => {
});
it('should not apply view filter groups when current view is not found', () => {
jotaiStore.set(coreViewsState.atom, []);
const { result } = renderHook(
() => {
const { applyCurrentViewFilterGroupsToCurrentRecordFilterGroups } =
@@ -173,10 +165,6 @@ describe('useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups', () => {
});
it('should handle view with empty view filter groups', () => {
jotaiStore.set(coreViewsState.atom, [
{ ...mockCoreView, viewFilterGroups: [] },
]);
const { result } = renderHook(
() => {
const { applyCurrentViewFilterGroupsToCurrentRecordFilterGroups } =
@@ -199,6 +187,9 @@ describe('useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups', () => {
contextStoreCurrentObjectMetadataNameSingular:
mockObjectMetadataItemNameSingular,
onInitializeJotaiStore: (store) => {
store.set(coreViewsState.atom, [
{ ...mockCoreView, viewFilterGroups: [] },
]);
store.set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: 'instanceId',
@@ -4,10 +4,7 @@ import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import {
jotaiStore,
resetJotaiStore,
} from '@/ui/utilities/state/jotai/jotaiStore';
import { resetJotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { type View } from '@/views/types/View';
@@ -44,10 +41,6 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => {
resetJotaiStore();
});
afterEach(() => {
jotaiStore.set(coreViewsState.atom, []);
});
const allCompaniesView = mockedViewsData[0];
const allCompaniesCoreView = mockedCoreViewsData[0];
@@ -90,8 +83,6 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => {
} satisfies CoreViewWithRelations;
it('should apply filters from current view', () => {
jotaiStore.set(coreViewsState.atom, [mockCoreView]);
const { result } = renderHook(
() => {
const { applyCurrentViewFiltersToCurrentRecordFilters } =
@@ -114,6 +105,9 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => {
contextStoreCurrentObjectMetadataNameSingular:
mockObjectMetadataItemNameSingular,
contextStoreCurrentViewId: mockView.id,
onInitializeJotaiStore: (store) => {
store.set(coreViewsState.atom, [mockCoreView]);
},
}),
},
);
@@ -139,8 +133,6 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => {
});
it('should not apply filters when current view is not found', () => {
jotaiStore.set(coreViewsState.atom, []);
const { result } = renderHook(
() => {
const { applyCurrentViewFiltersToCurrentRecordFilters } =
@@ -182,8 +174,6 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => {
});
it('should handle view with empty filters', () => {
jotaiStore.set(coreViewsState.atom, [{ ...mockCoreView, viewFilters: [] }]);
const { result } = renderHook(
() => {
const { applyCurrentViewFiltersToCurrentRecordFilters } =
@@ -206,6 +196,9 @@ describe('useApplyCurrentViewFiltersToCurrentRecordFilters', () => {
contextStoreCurrentObjectMetadataNameSingular:
mockObjectMetadataItemNameSingular,
onInitializeJotaiStore: (store) => {
store.set(coreViewsState.atom, [
{ ...mockCoreView, viewFilters: [] },
]);
store.set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: 'instanceId',
@@ -6,10 +6,6 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import {
jotaiStore,
resetJotaiStore,
} from '@/ui/utilities/state/jotai/jotaiStore';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewSortEssential } from '@/views/types/CoreViewSortEssential';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
@@ -37,10 +33,6 @@ describe('useApplyCurrentViewSortsToCurrentRecordSorts', () => {
);
}
afterEach(() => {
resetJotaiStore();
});
const mockFieldMetadataItem = mockObjectMetadataItem.fields.find(
(field) => field.name === 'name',
);
@@ -77,8 +69,6 @@ describe('useApplyCurrentViewSortsToCurrentRecordSorts', () => {
} satisfies CoreViewWithRelations;
it('should apply sorts from current view', () => {
jotaiStore.set(coreViewsState.atom, [mockCoreView]);
const { result } = renderHook(
() => {
const { applyCurrentViewSortsToCurrentRecordSorts } =
@@ -101,6 +91,9 @@ describe('useApplyCurrentViewSortsToCurrentRecordSorts', () => {
contextStoreCurrentObjectMetadataNameSingular:
mockObjectMetadataItemNameSingular,
contextStoreCurrentViewId: mockView.id,
onInitializeJotaiStore: (store) => {
store.set(coreViewsState.atom, [mockCoreView]);
},
}),
},
);
@@ -119,8 +112,6 @@ describe('useApplyCurrentViewSortsToCurrentRecordSorts', () => {
});
it('should not apply sorts when current view is not found', () => {
jotaiStore.set(coreViewsState.atom, []);
const { result } = renderHook(
() => {
const { applyCurrentViewSortsToCurrentRecordSorts } =
@@ -167,8 +158,6 @@ describe('useApplyCurrentViewSortsToCurrentRecordSorts', () => {
viewSorts: [],
};
jotaiStore.set(coreViewsState.atom, [viewWithNoSorts]);
const { result } = renderHook(
() => {
const { applyCurrentViewSortsToCurrentRecordSorts } =
@@ -191,6 +180,7 @@ describe('useApplyCurrentViewSortsToCurrentRecordSorts', () => {
contextStoreCurrentObjectMetadataNameSingular:
mockObjectMetadataItemNameSingular,
onInitializeJotaiStore: (store) => {
store.set(coreViewsState.atom, [viewWithNoSorts]);
store.set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: 'instanceId',
@@ -7,9 +7,10 @@ import { ContextStoreComponentInstanceContext } from '@/context-store/states/con
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { resetJotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { type InMemoryCache } from '@apollo/client';
import type { Store } from 'jotai/vanilla/store';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
@@ -23,13 +24,15 @@ export const getJestMetadataAndApolloMocksWrapper = ({
apolloMocks?:
| readonly MockedResponse<Record<string, any>, Record<string, any>>[]
| undefined;
onInitializeJotaiStore?: (store: typeof jotaiStore) => void;
onInitializeJotaiStore?: (store: Store) => void;
objectMetadataItems?: ObjectMetadataItem[];
}) => {
onInitializeJotaiStore?.(jotaiStore);
const store = resetJotaiStore();
onInitializeJotaiStore?.(store);
return ({ children }: { children: ReactNode }) => (
<JotaiProvider store={jotaiStore}>
<JotaiProvider store={store}>
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'snack-bar-manager' }}
>