Skip to content

Commit 4cf2f79

Browse files
authored
Merge pull request #28 from retejs/feature/performance-testing
feat: add performance testing
2 parents 9e0b31c + 6610ca4 commit 4cf2f79

10 files changed

Lines changed: 876 additions & 1 deletion

File tree

src/index.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import build from './build'
66
import { setVerbose } from './build/utils'
77
import doc from './doc'
88
import lint from './lint'
9+
import perf from './performance'
910
import test from './test'
1011

1112
const program = createCommand()
@@ -52,13 +53,37 @@ program
5253

5354
program
5455
.command('test')
55-
.description('Run tests')
5656
.description('Run tests using Jest')
5757
.option('-w --watch')
5858
.action(async (options: { watch?: boolean }) => {
5959
await test(options.watch)
6060
})
6161

62+
program
63+
.command('perf')
64+
.description('⚠️ EXPERIMENTAL Run performance tests and benchmarks (interface may change)')
65+
.option('--pattern <pattern>', 'Test file pattern (default: **/*.perf.ts)')
66+
.option('--iterations <number>', 'Number of iterations', '1')
67+
.addOption(new Option('--output <format>', 'Output format')
68+
.choices(['console', 'json', 'both'])
69+
.default('console'))
70+
.option('--output-file <file>', 'Output file path', 'performance-results.json')
71+
.action(async (options: {
72+
pattern?: string
73+
iterations?: string
74+
output?: 'json' | 'console' | 'both'
75+
outputFile?: string
76+
}) => {
77+
await perf({
78+
testPattern: options.pattern,
79+
iterations: options.iterations
80+
? parseInt(options.iterations, 10)
81+
: undefined,
82+
outputFormat: options.output,
83+
outputFile: options.outputFile
84+
})
85+
})
86+
6287
program.parse(process.argv)
6388

6489
export { }

src/performance/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Performance testing module for rete-cli
3+
*
4+
* This module provides comprehensive performance testing capabilities including:
5+
* - Memory usage tracking per test
6+
* - Duration measurement with fallback strategies
7+
* - Jest-style output formatting
8+
* - JSON result export
9+
* - Garbage collection handling
10+
*/
11+
12+
export { MemoryUtils } from './memory-utils'
13+
export { OutputFormatter } from './output-formatter'
14+
export { PerformanceReporter } from './reporter'
15+
export { ResultSaver } from './result-saver'
16+
export { default } from './runner'
17+
export { TestEstimator } from './test-estimator'
18+
export type {
19+
FileResult,
20+
JestTestResult,
21+
MemorySnapshot,
22+
PerformanceConfig,
23+
PerformanceMetrics,
24+
Test,
25+
TestResult,
26+
TestStatus
27+
} from './types'

src/performance/memory-utils.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* Memory measurement utilities for performance testing
3+
*/
4+
5+
export class MemoryUtils {
6+
private gcDetectionThreshold = 10 * 1024 * 1024 // 10MB threshold for GC detection
7+
8+
/**
9+
* Force garbage collection if available
10+
*/
11+
forceGarbageCollection(): void {
12+
if (global.gc) {
13+
try {
14+
global.gc()
15+
// Wait for GC to complete
16+
this.waitForMemoryStabilization()
17+
} catch (error) {
18+
// GC might not be available or might fail, log and continue
19+
const errorMessage = error instanceof Error
20+
? error.message
21+
: String(error)
22+
23+
console.warn('Warning: Could not force garbage collection:', errorMessage)
24+
}
25+
}
26+
}
27+
28+
/**
29+
* Establish memory baseline by taking multiple readings
30+
*/
31+
establishMemoryBaseline(): number {
32+
// Force GC if available to establish a clean baseline
33+
this.forceGarbageCollection()
34+
35+
// Take multiple readings to establish stable baseline
36+
const readings: number[] = []
37+
38+
for (let i = 0; i < 3; i++) {
39+
readings.push(process.memoryUsage().heapUsed)
40+
// Small delay between readings
41+
const start = Date.now()
42+
43+
while (Date.now() - start < 10) {
44+
// Busy wait for 10ms
45+
}
46+
}
47+
48+
return Math.min(...readings)
49+
}
50+
51+
/**
52+
* Get stabilized memory reading with GC interference detection
53+
*/
54+
getStabilizedMemoryReading(): number {
55+
const readings: number[] = []
56+
57+
// Take multiple memory readings to detect GC interference
58+
for (let i = 0; i < 5; i++) {
59+
readings.push(process.memoryUsage().heapUsed)
60+
61+
// Small delay between readings
62+
const start = Date.now()
63+
64+
while (Date.now() - start < 5) {
65+
// Busy wait for 5ms
66+
}
67+
}
68+
69+
// Check for GC interference (large drops in memory)
70+
const hasGcInterference = this.detectGarbageCollection(readings)
71+
72+
if (hasGcInterference) {
73+
// If GC occurred, wait a bit and take fresh readings
74+
this.waitForMemoryStabilization()
75+
return this.getCleanMemoryReading()
76+
}
77+
78+
// Return the median reading to avoid outliers
79+
return this.getMedianValue(readings)
80+
}
81+
82+
/**
83+
* Detect if garbage collection occurred during readings
84+
*/
85+
private detectGarbageCollection(readings: number[]): boolean {
86+
for (let i = 1; i < readings.length; i++) {
87+
const memoryDrop = readings[i - 1] - readings[i]
88+
89+
// If memory dropped significantly, likely GC occurred
90+
if (memoryDrop > this.gcDetectionThreshold) {
91+
return true
92+
}
93+
}
94+
95+
return false
96+
}
97+
98+
/**
99+
* Wait for memory to stabilize after GC
100+
*/
101+
private waitForMemoryStabilization(): void {
102+
const stabilizationTime = 50 // 50ms
103+
const start = Date.now()
104+
105+
while (Date.now() - start < stabilizationTime) {
106+
// Busy wait
107+
}
108+
}
109+
110+
/**
111+
* Get a single clean memory reading after stabilization
112+
*/
113+
private getCleanMemoryReading(): number {
114+
return process.memoryUsage().heapUsed
115+
}
116+
117+
/**
118+
* Calculate median value from array of numbers
119+
*/
120+
private getMedianValue(values: number[]): number {
121+
const sorted = [...values].sort((a, b) => a - b)
122+
const mid = Math.floor(sorted.length / 2)
123+
124+
if (sorted.length % 2 === 0) {
125+
return (sorted[mid - 1] + sorted[mid]) / 2
126+
}
127+
128+
return sorted[mid]
129+
}
130+
}

0 commit comments

Comments
 (0)