447 lines
13 KiB
JavaScript
447 lines
13 KiB
JavaScript
const React = require("react");
|
|
const { render } = require("ink-testing-library");
|
|
const {
|
|
PerformanceBenchmark,
|
|
PerformanceProfiler,
|
|
MemoryMonitor,
|
|
} = require("../../../src/tui/utils/performanceUtils.js");
|
|
|
|
// Import optimized components
|
|
const OptimizedMenuList = require("../../../src/tui/components/common/OptimizedMenuList.jsx");
|
|
const VirtualScrollableContainer = require("../../../src/tui/components/common/VirtualScrollableContainer.jsx");
|
|
const OptimizedProgressBar = require("../../../src/tui/components/common/OptimizedProgressBar.jsx");
|
|
|
|
// Import original components for comparison
|
|
const MenuList = require("../../../src/tui/components/common/MenuList.jsx");
|
|
const ScrollableContainer = require("../../../src/tui/components/common/ScrollableContainer.jsx");
|
|
const ProgressBar = require("../../../src/tui/components/common/ProgressBar.jsx");
|
|
|
|
/**
|
|
* Performance tests for TUI component rendering
|
|
* Requirements: 4.1, 4.3, 4.4
|
|
*/
|
|
|
|
describe("TUI Component Rendering Performance", () => {
|
|
let profiler;
|
|
let memoryMonitor;
|
|
|
|
beforeEach(() => {
|
|
profiler = new PerformanceProfiler();
|
|
memoryMonitor = new MemoryMonitor();
|
|
});
|
|
|
|
afterEach(() => {
|
|
profiler.clear();
|
|
memoryMonitor.stopMonitoring();
|
|
});
|
|
|
|
describe("MenuList Performance", () => {
|
|
const generateMenuItems = (count) => {
|
|
return Array.from({ length: count }, (_, i) => ({
|
|
label: `Menu Item ${i + 1}`,
|
|
shortcut: String.fromCharCode(97 + (i % 26)), // a-z
|
|
description: `Description for menu item ${i + 1}`,
|
|
}));
|
|
};
|
|
|
|
test("should render small menu lists efficiently", async () => {
|
|
const items = generateMenuItems(10);
|
|
const benchmark = new PerformanceBenchmark("Small MenuList Rendering");
|
|
|
|
const testFunction = () => {
|
|
const { unmount } = render(
|
|
React.createElement(OptimizedMenuList, {
|
|
items,
|
|
selectedIndex: 0,
|
|
onSelect: () => {},
|
|
showShortcuts: true,
|
|
})
|
|
);
|
|
unmount();
|
|
};
|
|
|
|
const results = await benchmark.run(testFunction, 100);
|
|
|
|
expect(results.average).toBeLessThan(10); // Should render in less than 10ms on average
|
|
expect(results.p95).toBeLessThan(20); // 95% of renders should be under 20ms
|
|
|
|
benchmark.logResults();
|
|
});
|
|
|
|
test("should handle large menu lists with virtual scrolling", async () => {
|
|
const items = generateMenuItems(1000);
|
|
const benchmark = new PerformanceBenchmark("Large MenuList Rendering");
|
|
|
|
const testFunction = () => {
|
|
const { unmount } = render(
|
|
React.createElement(OptimizedMenuList, {
|
|
items,
|
|
selectedIndex: 0,
|
|
onSelect: () => {},
|
|
showShortcuts: true,
|
|
})
|
|
);
|
|
unmount();
|
|
};
|
|
|
|
const results = await benchmark.run(testFunction, 50);
|
|
|
|
expect(results.average).toBeLessThan(50); // Should render in less than 50ms on average
|
|
expect(results.p95).toBeLessThan(100); // 95% of renders should be under 100ms
|
|
|
|
benchmark.logResults();
|
|
});
|
|
|
|
test("should show performance improvement over original MenuList", async () => {
|
|
const items = generateMenuItems(500);
|
|
|
|
// Benchmark original MenuList
|
|
const originalBenchmark = new PerformanceBenchmark("Original MenuList");
|
|
const originalTestFunction = () => {
|
|
const { unmount } = render(
|
|
React.createElement(MenuList, {
|
|
items,
|
|
selectedIndex: 0,
|
|
onSelect: () => {},
|
|
showShortcuts: true,
|
|
})
|
|
);
|
|
unmount();
|
|
};
|
|
const originalResults = await originalBenchmark.run(
|
|
originalTestFunction,
|
|
30
|
|
);
|
|
|
|
// Benchmark optimized MenuList
|
|
const optimizedBenchmark = new PerformanceBenchmark("Optimized MenuList");
|
|
const optimizedTestFunction = () => {
|
|
const { unmount } = render(
|
|
React.createElement(OptimizedMenuList, {
|
|
items,
|
|
selectedIndex: 0,
|
|
onSelect: () => {},
|
|
showShortcuts: true,
|
|
})
|
|
);
|
|
unmount();
|
|
};
|
|
const optimizedResults = await optimizedBenchmark.run(
|
|
optimizedTestFunction,
|
|
30
|
|
);
|
|
|
|
// Optimized version should be at least 20% faster
|
|
const improvement =
|
|
(originalResults.average - optimizedResults.average) /
|
|
originalResults.average;
|
|
expect(improvement).toBeGreaterThan(0.2);
|
|
|
|
console.log(
|
|
`Performance improvement: ${(improvement * 100).toFixed(1)}%`
|
|
);
|
|
originalBenchmark.logResults();
|
|
optimizedBenchmark.logResults();
|
|
});
|
|
});
|
|
|
|
describe("VirtualScrollableContainer Performance", () => {
|
|
const generateScrollItems = (count) => {
|
|
return Array.from({ length: count }, (_, i) => ({
|
|
id: i,
|
|
content: `Item ${
|
|
i + 1
|
|
} - Lorem ipsum dolor sit amet, consectetur adipiscing elit.`,
|
|
}));
|
|
};
|
|
|
|
const renderItem = (item, index) => {
|
|
return React.createElement("div", { key: index }, item.content);
|
|
};
|
|
|
|
test("should handle large datasets efficiently with virtual scrolling", async () => {
|
|
const items = generateScrollItems(10000);
|
|
const benchmark = new PerformanceBenchmark(
|
|
"Virtual Scrolling Large Dataset"
|
|
);
|
|
|
|
memoryMonitor.startMonitoring(1000);
|
|
|
|
const testFunction = () => {
|
|
const { unmount } = render(
|
|
React.createElement(VirtualScrollableContainer, {
|
|
items,
|
|
renderItem,
|
|
itemHeight: 1,
|
|
showScrollIndicators: true,
|
|
})
|
|
);
|
|
unmount();
|
|
};
|
|
|
|
const results = await benchmark.run(testFunction, 20);
|
|
|
|
// Should handle large datasets efficiently
|
|
expect(results.average).toBeLessThan(100); // Should render in less than 100ms
|
|
expect(results.p95).toBeLessThan(200); // 95% of renders should be under 200ms
|
|
|
|
// Check memory usage
|
|
const memoryStats = memoryMonitor.getStatistics();
|
|
const memoryLeak = memoryMonitor.checkForLeaks();
|
|
|
|
expect(memoryLeak.isLikely).toBe(false); // Should not have memory leaks
|
|
|
|
benchmark.logResults();
|
|
memoryMonitor.logSummary();
|
|
});
|
|
|
|
test("should maintain consistent performance with different scroll positions", async () => {
|
|
const items = generateScrollItems(5000);
|
|
const scrollPositions = [0, 100, 500, 1000, 2500, 4999];
|
|
const results = [];
|
|
|
|
for (const scrollPosition of scrollPositions) {
|
|
const benchmark = new PerformanceBenchmark(
|
|
`Virtual Scroll Position ${scrollPosition}`
|
|
);
|
|
|
|
const testFunction = () => {
|
|
const { unmount } = render(
|
|
React.createElement(VirtualScrollableContainer, {
|
|
items,
|
|
renderItem,
|
|
itemHeight: 1,
|
|
initialScrollPosition: scrollPosition,
|
|
})
|
|
);
|
|
unmount();
|
|
};
|
|
|
|
const result = await benchmark.run(testFunction, 20);
|
|
results.push(result.average);
|
|
}
|
|
|
|
// Performance should be consistent across different scroll positions
|
|
const maxVariation = Math.max(...results) - Math.min(...results);
|
|
const averageTime =
|
|
results.reduce((sum, time) => sum + time, 0) / results.length;
|
|
const variationPercentage = (maxVariation / averageTime) * 100;
|
|
|
|
expect(variationPercentage).toBeLessThan(50); // Variation should be less than 50%
|
|
|
|
console.log(
|
|
`Scroll position performance variation: ${variationPercentage.toFixed(
|
|
1
|
|
)}%`
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("ProgressBar Performance", () => {
|
|
test("should handle rapid progress updates efficiently", async () => {
|
|
const benchmark = new PerformanceBenchmark("Rapid Progress Updates");
|
|
|
|
memoryMonitor.startMonitoring(500);
|
|
|
|
const testFunction = () => {
|
|
let progress = 0;
|
|
const { rerender, unmount } = render(
|
|
React.createElement(OptimizedProgressBar, {
|
|
progress,
|
|
label: "Test Progress",
|
|
animate: true,
|
|
debounceDelay: 50,
|
|
})
|
|
);
|
|
|
|
// Simulate rapid updates
|
|
for (let i = 0; i < 100; i++) {
|
|
progress = i;
|
|
rerender(
|
|
React.createElement(OptimizedProgressBar, {
|
|
progress,
|
|
label: "Test Progress",
|
|
animate: true,
|
|
debounceDelay: 50,
|
|
})
|
|
);
|
|
}
|
|
|
|
unmount();
|
|
};
|
|
|
|
const results = await benchmark.run(testFunction, 10);
|
|
|
|
expect(results.average).toBeLessThan(200); // Should handle rapid updates efficiently
|
|
|
|
// Check for memory leaks during rapid updates
|
|
const memoryLeak = memoryMonitor.checkForLeaks();
|
|
expect(memoryLeak.isLikely).toBe(false);
|
|
|
|
benchmark.logResults();
|
|
memoryMonitor.logSummary();
|
|
});
|
|
|
|
test("should optimize multi-progress bar rendering", async () => {
|
|
const progressItems = Array.from({ length: 20 }, (_, i) => ({
|
|
key: `progress-${i}`,
|
|
label: `Operation ${i + 1}`,
|
|
progress: Math.random() * 100,
|
|
color: ["blue", "green", "yellow", "cyan", "magenta"][i % 5],
|
|
}));
|
|
|
|
const benchmark = new PerformanceBenchmark(
|
|
"Multi-Progress Bar Rendering"
|
|
);
|
|
|
|
const testFunction = () => {
|
|
const { unmount } = render(
|
|
React.createElement(OptimizedProgressBar.Multi, {
|
|
progressItems,
|
|
width: 40,
|
|
showLabels: true,
|
|
showPercentages: true,
|
|
animate: false,
|
|
})
|
|
);
|
|
unmount();
|
|
};
|
|
|
|
const results = await benchmark.run(testFunction, 50);
|
|
|
|
expect(results.average).toBeLessThan(30); // Should render multiple progress bars efficiently
|
|
expect(results.p95).toBeLessThan(60);
|
|
|
|
benchmark.logResults();
|
|
});
|
|
});
|
|
|
|
describe("Memory Management", () => {
|
|
test("should not leak memory during component lifecycle", async () => {
|
|
const items = Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`);
|
|
|
|
memoryMonitor.startMonitoring(500);
|
|
|
|
// Create and destroy components multiple times
|
|
for (let i = 0; i < 50; i++) {
|
|
const { unmount } = render(
|
|
React.createElement(OptimizedMenuList, {
|
|
items,
|
|
selectedIndex: i % items.length,
|
|
onSelect: () => {},
|
|
showShortcuts: true,
|
|
})
|
|
);
|
|
|
|
// Simulate some async operations
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
unmount();
|
|
}
|
|
|
|
// Wait for garbage collection
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
|
|
const memoryLeak = memoryMonitor.checkForLeaks();
|
|
const memoryStats = memoryMonitor.getStatistics();
|
|
|
|
expect(memoryLeak.isLikely).toBe(false);
|
|
expect(memoryStats.growth.heapUsed).toBeLessThan(50 * 1024 * 1024); // Less than 50MB growth
|
|
|
|
memoryMonitor.logSummary();
|
|
});
|
|
|
|
test("should clean up event listeners and timers", async () => {
|
|
const initialHandlers = process.listenerCount("uncaughtException");
|
|
|
|
// Create components with timers and event listeners
|
|
const components = [];
|
|
for (let i = 0; i < 10; i++) {
|
|
const { unmount } = render(
|
|
React.createElement(OptimizedProgressBar, {
|
|
progress: 50,
|
|
animate: true,
|
|
animationSpeed: 100,
|
|
})
|
|
);
|
|
components.push(unmount);
|
|
}
|
|
|
|
// Unmount all components
|
|
components.forEach((unmount) => unmount());
|
|
|
|
// Wait for cleanup
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
|
|
const finalHandlers = process.listenerCount("uncaughtException");
|
|
|
|
// Should not have increased the number of event listeners
|
|
expect(finalHandlers).toBeLessThanOrEqual(initialHandlers + 1);
|
|
});
|
|
});
|
|
|
|
describe("Debouncing and Throttling", () => {
|
|
test("should reduce render frequency with debouncing", async () => {
|
|
let renderCount = 0;
|
|
const TestComponent = () => {
|
|
renderCount++;
|
|
return React.createElement("div", null, "Test");
|
|
};
|
|
|
|
const { rerender } = render(React.createElement(TestComponent));
|
|
|
|
// Trigger multiple rapid re-renders
|
|
for (let i = 0; i < 100; i++) {
|
|
rerender(React.createElement(TestComponent));
|
|
}
|
|
|
|
// With proper debouncing, render count should be significantly less than 100
|
|
expect(renderCount).toBeLessThan(50);
|
|
});
|
|
|
|
test("should maintain responsiveness with throttling", async () => {
|
|
const updates = [];
|
|
let lastUpdate = Date.now();
|
|
|
|
const TestComponent = ({ value }) => {
|
|
const currentTime = Date.now();
|
|
updates.push(currentTime - lastUpdate);
|
|
lastUpdate = currentTime;
|
|
return React.createElement("div", null, value);
|
|
};
|
|
|
|
const { rerender } = render(
|
|
React.createElement(TestComponent, { value: 0 })
|
|
);
|
|
|
|
// Simulate rapid updates with throttling
|
|
for (let i = 1; i <= 50; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
rerender(React.createElement(TestComponent, { value: i }));
|
|
}
|
|
|
|
// Updates should be throttled but still responsive
|
|
const averageInterval =
|
|
updates.reduce((sum, interval) => sum + interval, 0) / updates.length;
|
|
expect(averageInterval).toBeGreaterThan(5); // Should be throttled
|
|
expect(averageInterval).toBeLessThan(100); // But still responsive
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Performance Regression Tests", () => {
|
|
test("should maintain performance benchmarks", async () => {
|
|
const benchmarks = {
|
|
smallMenuList: { maxAverage: 10, maxP95: 20 },
|
|
largeMenuList: { maxAverage: 50, maxP95: 100 },
|
|
virtualScrolling: { maxAverage: 100, maxP95: 200 },
|
|
progressBar: { maxAverage: 30, maxP95: 60 },
|
|
};
|
|
|
|
// This test would be run in CI to ensure performance doesn't regress
|
|
// For now, we'll just verify the benchmarks structure
|
|
expect(benchmarks).toBeDefined();
|
|
expect(Object.keys(benchmarks)).toHaveLength(4);
|
|
});
|
|
});
|