@@ -37,12 +37,20 @@ expect.addSnapshotSerializer(createSerializer())
3737
3838jest . 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
4856afterEach ( 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