1+ // Credit to ChatGPT
2+
3+ import { executeExampleStream } from './run_test.ts'
4+
5+ type ProcessMessage = {
6+ value : number ,
7+ change : number ,
8+ }
9+
10+ export type MemoryUsageReport = {
11+ overAllMemoryTrendPerSample : number ,
12+ firstHalfAverage : number ,
13+ secondHalfAverage : number ,
14+ lastQuarterTrendPerSample : number ,
15+ lastQuarterStdDev : number ,
16+ memoryIncreaseFromFirstToSecondHalf : number ,
17+ memoryIncreaseFromFirstToSecondHalfPercent : number ,
18+ }
19+
20+ export async function executeMemoryTest ( testName : string ,
21+ args : string [ ] = [ ] ,
22+ env : Record < string , string > = { } ) : Promise < MemoryUsageReport > {
23+ const { done, stdout } = await executeExampleStream ( testName , args , env )
24+
25+ const samples : number [ ] = [ ] ;
26+ const WARMUP_SAMPLES = 5 ; // Skip initial samples during warmup
27+
28+ for await ( const recordStr of stdout ) {
29+ const record : ProcessMessage = JSON . parse ( recordStr + '\n' )
30+ samples . push ( record . value ) ;
31+ // console.log(`Memory: ${record.value}KB (${record.change >= 0 ? '+' : ''}${record.change}KB)`);
32+ }
33+
34+ await done
35+
36+ // Analysis after process completes
37+ if ( samples . length < 15 ) {
38+ throw new Error ( 'Not enough samples collected for reliable leak detection (need at least 15)' ) ;
39+ }
40+
41+ // Skip warmup phase and analyze the rest
42+ const steadyStateSamples = samples . slice ( WARMUP_SAMPLES ) ;
43+
44+ // Strategy 1: Check if memory trend is consistently upward
45+ const overallTrend = calculateTrend ( steadyStateSamples ) ;
46+ // console.log(`\nOverall memory trend: ${overallTrend > 0 ? '+' : ''}${overallTrend.toFixed(2)}KB per sample`);
47+
48+ // Strategy 2: Compare first half vs second half
49+ const midpoint = Math . floor ( steadyStateSamples . length / 2 ) ;
50+ const firstHalf = steadyStateSamples . slice ( 0 , midpoint ) ;
51+ const secondHalf = steadyStateSamples . slice ( midpoint ) ;
52+
53+ const firstHalfAvg = average ( firstHalf ) ;
54+ const secondHalfAvg = average ( secondHalf ) ;
55+ const halfDiff = secondHalfAvg - firstHalfAvg ;
56+
57+ // console.log(`First half average: ${firstHalfAvg.toFixed(2)}KB`);
58+ // console.log(`Second half average: ${secondHalfAvg.toFixed(2)}KB`);
59+ // console.log(`Difference: ${halfDiff >= 0 ? '+' : ''}${halfDiff.toFixed(2)}KB`);
60+
61+ // Strategy 3: Check for stabilization
62+ const lastQuarter = steadyStateSamples . slice ( - Math . floor ( steadyStateSamples . length / 4 ) ) ;
63+ const lastQuarterTrend = calculateTrend ( lastQuarter ) ;
64+ const lastQuarterStdDev = standardDeviation ( lastQuarter ) ;
65+
66+ // console.log(`Last quarter trend: ${lastQuarterTrend > 0 ? '+' : ''}${lastQuarterTrend.toFixed(2)}KB per sample`);
67+ // console.log(`Last quarter std dev: ${lastQuarterStdDev.toFixed(2)}KB`);
68+
69+ // Detect leak based on multiple signals
70+ // const leakSignals: string[] = [];
71+
72+ // Signal 1: Strong upward trend overall (>512KB per sample)
73+ // if (overallTrend > 512) {
74+ // leakSignals.push(`Strong upward trend: ${overallTrend.toFixed(2)}KB per sample`);
75+ // }
76+
77+ // Signal 2: Second half significantly higher than first (>20MB or >20% increase)
78+ const percentIncrease = ( halfDiff / firstHalfAvg ) * 100 ;
79+ // if (halfDiff > 20480 || percentIncrease > 20) {
80+ // leakSignals.push(`Memory increased ${halfDiff.toFixed(2)}KB (${percentIncrease.toFixed(1)}%) from first to second half`);
81+ // }
82+
83+ // Signal 3: Memory still climbing at the end (trend in last quarter >307KB)
84+ // if (lastQuarterTrend > 307) {
85+ // leakSignals.push(`Memory still climbing at end: ${lastQuarterTrend.toFixed(2)}KB per sample in last quarter`);
86+ // }
87+
88+ // Signal 4: High variance in last quarter suggests unstable memory (may indicate leak)
89+ // if (lastQuarterStdDev > 15360 && lastQuarterTrend > 205) {
90+ // leakSignals.push(`High memory variance with upward trend: ${lastQuarterStdDev.toFixed(2)}KB std dev`);
91+ // }
92+
93+ // if (leakSignals.length >= 2) {
94+ // throw new Error(
95+ // `Memory leak detected (${leakSignals.length} signals):\n` +
96+ // leakSignals.map(s => ` - ${s}`).join('\n')
97+ // );
98+ // }
99+
100+ // if (leakSignals.length === 1) {
101+ // console.warn(`\n⚠ Potential leak indicator: ${leakSignals[0]}`);
102+ // }
103+
104+ // console.log('\n✓ No significant memory leak detected');
105+
106+ return {
107+ overAllMemoryTrendPerSample : overallTrend ,
108+ firstHalfAverage : firstHalfAvg ,
109+ secondHalfAverage : secondHalfAvg ,
110+ lastQuarterTrendPerSample : lastQuarterTrend ,
111+ lastQuarterStdDev : lastQuarterStdDev ,
112+ memoryIncreaseFromFirstToSecondHalf : halfDiff ,
113+ memoryIncreaseFromFirstToSecondHalfPercent : percentIncrease ,
114+ }
115+ } ;
116+
117+ function average ( samples : number [ ] ) : number {
118+ return samples . reduce ( ( a , b ) => a + b , 0 ) / samples . length ;
119+ }
120+
121+ function standardDeviation ( samples : number [ ] ) : number {
122+ const avg = average ( samples ) ;
123+ const squareDiffs = samples . map ( value => Math . pow ( value - avg , 2 ) ) ;
124+ return Math . sqrt ( average ( squareDiffs ) ) ;
125+ }
126+
127+ function calculateTrend ( samples : number [ ] ) : number {
128+ const n = samples . length ;
129+ const indices = Array . from ( { length : n } , ( _ , i ) => i ) ;
130+
131+ const sumX = indices . reduce ( ( a , b ) => a + b , 0 ) ;
132+ const sumY = samples . reduce ( ( a , b ) => a + b , 0 ) ;
133+ const sumXY = indices . reduce ( ( sum , x , i ) => sum + x * samples [ i ] , 0 ) ;
134+ const sumX2 = indices . reduce ( ( sum , x ) => sum + x * x , 0 ) ;
135+
136+ const slope = ( n * sumXY - sumX * sumY ) / ( n * sumX2 - sumX * sumX ) ;
137+ return slope ;
138+ }
0 commit comments