Skip to content

Commit ba139fc

Browse files
committed
test: add memory leak detection test for navigation cycles and improve formatMemory function
- Added a new performance test to detect memory leaks during repeated navigation cycles between pages. - The test measures heap memory usage before, during, and after navigation, ensuring memory growth stays within acceptable limits. - Enhanced the formatMemory utility to correctly handle negative byte values and added JSDoc comments for clarity. - Removed an obsolete shallowly equal props performance test to streamline the test suite.
1 parent 2ecaabd commit ba139fc

File tree

1 file changed

+145
-20
lines changed

1 file changed

+145
-20
lines changed

tests/performance.test.ts

Lines changed: 145 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,20 @@ expect.addSnapshotSerializer(createSerializer())
3737

3838
jest.useRealTimers()
3939

40-
function formatMemory(bytes: number): string {
40+
/**
41+
* Formats a number of bytes into a human-readable string.
42+
* @param bytes The number of bytes to format.
43+
* @returns A string representing the formatted memory size.
44+
*/
45+
export function formatMemory(bytes: number): string {
4146
if (bytes === 0) return '0 Bytes'
4247
const k = 1024
4348
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
44-
const i = Math.floor(Math.log(bytes) / Math.log(k))
45-
return i === 0 ? `${bytes} ${sizes[i]}` : `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
49+
const isNegative = bytes < 0
50+
const absBytes = Math.abs(bytes)
51+
const i = Math.floor(Math.log(absBytes) / Math.log(k))
52+
const formattedValue = i === 0 ? `${absBytes} ${sizes[i]}` : `${parseFloat((absBytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
53+
return isNegative ? `-${formattedValue}` : formattedValue
4654
}
4755

4856
afterEach(cleanup)
@@ -814,6 +822,140 @@ describe('Performance Testing', () => {
814822

815823
expect(memoryGrowthMB).toBeLessThan(20)
816824
}, 30000)
825+
826+
it('should not leak memory during navigation cycles', async () => {
827+
const group = 'Memory Management Performance'
828+
const groupDescription = 'Tests for memory usage, garbage collection, and resource management'
829+
const testName = 'Navigation Memory Leak Check'
830+
const testDescription = 'Measures memory usage after simulating navigation between two different pages multiple times to detect leaks.'
831+
832+
const forceGC = async () => {
833+
if (global.gc) {
834+
global.gc()
835+
await new Promise(resolve => setTimeout(resolve, 50))
836+
}
837+
}
838+
839+
const createPage = (pageName: string, numElements: number) => {
840+
const elements = Array.from({ length: numElements }, (_, i) =>
841+
Div({
842+
key: `${pageName}-element-${i}`,
843+
css: {
844+
width: '10px',
845+
height: '10px',
846+
backgroundColor: i % 2 === 0 ? 'blue' : 'green',
847+
margin: '2px',
848+
},
849+
children: `${pageName} Item ${i}`,
850+
}),
851+
)
852+
return Container({
853+
children: [
854+
H1(`${pageName} Page`, { 'data-testid': `${pageName}-header` }),
855+
...elements,
856+
Button(`Go to Other Page from ${pageName}`, {
857+
'data-testid': `${pageName}-navigate-button`,
858+
onClick: () => {
859+
/* Handled by parent */
860+
},
861+
}),
862+
],
863+
}).render()
864+
}
865+
866+
const NUM_NAV_CYCLES = 10
867+
868+
const RootComponent: React.FC = () => {
869+
const [currentPage, setCurrentPage] = React.useState<'page1' | 'page2'>('page1')
870+
871+
const handleNavigate = () => {
872+
setCurrentPage(prev => (prev === 'page1' ? 'page2' : 'page1'))
873+
}
874+
875+
const page1 = createPage('Page1', 1000)
876+
const page2 = createPage('Page2', 1500)
877+
878+
return ThemeProvider({
879+
theme,
880+
children: Div({
881+
children: [
882+
currentPage === 'page1'
883+
? Div({ key: 'page1', children: page1, onClick: handleNavigate })
884+
: Div({ key: 'page2', children: page2, onClick: handleNavigate }),
885+
],
886+
}).render(),
887+
}).render()
888+
}
889+
890+
// Initial memory measurement
891+
await forceGC()
892+
const initialMemory = process.memoryUsage().heapUsed
893+
894+
const { getByTestId, unmount } = render(Node(RootComponent).render())
895+
896+
let currentMemory = 0
897+
const memorySnapshots: { cycle: number; page: string; memory: number }[] = []
898+
899+
for (let i = 0; i < NUM_NAV_CYCLES; i++) {
900+
await act(async () => {
901+
const currentPageId = i % 2 === 0 ? 'Page1' : 'Page2'
902+
fireEvent.click(getByTestId(`${currentPageId}-navigate-button`))
903+
})
904+
905+
await forceGC()
906+
currentMemory = process.memoryUsage().heapUsed
907+
memorySnapshots.push({ cycle: i + 1, page: i % 2 === 0 ? 'page2' : 'page1', memory: currentMemory })
908+
909+
// Add a small delay to allow potential async operations to complete
910+
await new Promise(resolve => setTimeout(resolve, 20))
911+
}
912+
913+
unmount()
914+
await forceGC()
915+
const finalMemoryAfterUnmount = process.memoryUsage().heapUsed
916+
917+
// Calculate peak memory usage during navigation
918+
const peakMemory = Math.max(...memorySnapshots.map(s => s.memory))
919+
920+
// Analyze memory trends
921+
// We expect the memory usage to stabilize, not constantly grow.
922+
// A simple check is to compare the initial memory to the final memory after unmounting,
923+
// and also ensure that the peak during navigation isn't excessively high compared to initial.
924+
const memoryGrowthAfterNavigations = peakMemory - initialMemory
925+
const memoryGrowthAfterUnmount = finalMemoryAfterUnmount - initialMemory
926+
927+
recordGroupMetric(group, groupDescription, testName, testDescription, 'Initial Heap Size', formatMemory(initialMemory))
928+
recordGroupMetric(group, groupDescription, testName, testDescription, 'Peak Heap Size During Navigation', formatMemory(peakMemory))
929+
recordGroupMetric(group, groupDescription, testName, testDescription, 'Final Heap Size After Unmount', formatMemory(finalMemoryAfterUnmount))
930+
recordGroupMetric(group, groupDescription, testName, testDescription, 'Memory Growth (Peak - Initial)', formatMemory(memoryGrowthAfterNavigations))
931+
recordGroupMetric(group, groupDescription, testName, testDescription, 'Memory Growth (Final Unmount - Initial)', formatMemory(memoryGrowthAfterUnmount))
932+
933+
// Allow for some fluctuation, but not a continuous leak.
934+
// For example, allow up to 50 MB growth during operations, and expect it to return closer to initial after unmount.
935+
const PEAK_MEMORY_TOLERANCE_MB = 50
936+
const UNMOUNT_MEMORY_TOLERANCE_MB = 20
937+
938+
expect(memoryGrowthAfterNavigations / 1024 / 1024).toBeLessThan(PEAK_MEMORY_TOLERANCE_MB)
939+
expect(memoryGrowthAfterUnmount / 1024 / 1024).toBeLessThan(UNMOUNT_MEMORY_TOLERANCE_MB)
940+
941+
// Additionally, check that memory tends to return to a baseline after unmount for each cycle
942+
// This is a more robust check for continuous leaks than just initial vs final.
943+
// For simplicity, we'll check that the last snapshot is not drastically higher than the first.
944+
if (memorySnapshots.length > 1) {
945+
const firstNavMemory = memorySnapshots[0].memory
946+
const lastNavMemory = memorySnapshots[memorySnapshots.length - 1].memory
947+
const memoryChangeDuringCycles = lastNavMemory - firstNavMemory
948+
recordGroupMetric(
949+
group,
950+
groupDescription,
951+
testName,
952+
testDescription,
953+
'Memory Change Across Cycles (Last Nav - First Nav)',
954+
formatMemory(memoryChangeDuringCycles),
955+
)
956+
expect(memoryChangeDuringCycles / 1024 / 1024).toBeLessThan(10) // Small growth allowed
957+
}
958+
}, 100000)
817959
})
818960

819961
// Prop Processing Group
@@ -839,23 +981,6 @@ describe('Performance Testing', () => {
839981
expect(duration).toBeLessThan(100)
840982
})
841983

842-
it('should generate stableKeys efficiently for nodes with shallowly equal props', () => {
843-
const testName = 'Shallowly Equal Props'
844-
const testDescription = 'Measures performance of node instantiation with shallowly equal props to test stableKey generation efficiency'
845-
const NUM_NODES = 5000
846-
847-
const t0 = performance.now()
848-
for (let i = 0; i < NUM_NODES; i++) {
849-
Column({ color: 'theme.primary', padding: '10px' })
850-
}
851-
const t1 = performance.now()
852-
const duration = t1 - t0
853-
854-
recordGroupMetric(group, groupDescription, testName, testDescription, `Duration`, `${duration.toFixed(2)} ms`)
855-
recordGroupMetric(group, groupDescription, testName, testDescription, 'Nodes Processed', NUM_NODES)
856-
expect(duration).toBeLessThan(250)
857-
})
858-
859984
it('should generate stableKeys for nodes with unique props', () => {
860985
const testName = 'Unique Props'
861986
const testDescription = 'Measures performance of node instantiation with unique props to test stableKey generation efficiency'

0 commit comments

Comments
 (0)