Just a whole lot of crap
This commit is contained in:
482
tests/tui/performance/memoryLeakDetection.test.js
Normal file
482
tests/tui/performance/memoryLeakDetection.test.js
Normal file
@@ -0,0 +1,482 @@
|
||||
const React = require("react");
|
||||
const { render } = require("ink-testing-library");
|
||||
const {
|
||||
MemoryLeakDetector,
|
||||
getGlobalDetector,
|
||||
useMemoryLeakDetection,
|
||||
MemoryLeakUtils,
|
||||
} = require("../../../src/tui/utils/memoryLeakDetector.js");
|
||||
|
||||
/**
|
||||
* Memory leak detection tests
|
||||
* Requirements: 4.2, 4.5
|
||||
*/
|
||||
|
||||
describe("MemoryLeakDetector", () => {
|
||||
let detector;
|
||||
|
||||
beforeEach(() => {
|
||||
detector = new MemoryLeakDetector({
|
||||
checkInterval: 100, // Fast interval for testing
|
||||
sampleSize: 5,
|
||||
growthThreshold: 1024 * 1024, // 1MB
|
||||
verbose: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
detector.stop();
|
||||
});
|
||||
|
||||
describe("Basic Functionality", () => {
|
||||
test("should start and stop monitoring", () => {
|
||||
expect(detector.isMonitoring).toBe(false);
|
||||
|
||||
detector.start();
|
||||
expect(detector.isMonitoring).toBe(true);
|
||||
|
||||
detector.stop();
|
||||
expect(detector.isMonitoring).toBe(false);
|
||||
});
|
||||
|
||||
test("should take memory samples", () => {
|
||||
// Mock process.memoryUsage
|
||||
const mockMemoryUsage = jest.fn(() => ({
|
||||
heapUsed: 50 * 1024 * 1024,
|
||||
heapTotal: 60 * 1024 * 1024,
|
||||
external: 5 * 1024 * 1024,
|
||||
rss: 70 * 1024 * 1024,
|
||||
}));
|
||||
|
||||
const originalMemoryUsage = process.memoryUsage;
|
||||
process.memoryUsage = mockMemoryUsage;
|
||||
|
||||
detector.takeSample();
|
||||
expect(detector.samples).toHaveLength(1);
|
||||
expect(detector.samples[0]).toMatchObject({
|
||||
heapUsed: 50 * 1024 * 1024,
|
||||
heapTotal: 60 * 1024 * 1024,
|
||||
});
|
||||
|
||||
process.memoryUsage = originalMemoryUsage;
|
||||
});
|
||||
|
||||
test("should limit sample size", () => {
|
||||
const mockMemoryUsage = jest.fn(() => ({
|
||||
heapUsed: Math.random() * 100 * 1024 * 1024,
|
||||
heapTotal: 100 * 1024 * 1024,
|
||||
external: 5 * 1024 * 1024,
|
||||
rss: 120 * 1024 * 1024,
|
||||
}));
|
||||
|
||||
const originalMemoryUsage = process.memoryUsage;
|
||||
process.memoryUsage = mockMemoryUsage;
|
||||
|
||||
// Take more samples than the limit
|
||||
for (let i = 0; i < 10; i++) {
|
||||
detector.takeSample();
|
||||
}
|
||||
|
||||
expect(detector.samples).toHaveLength(5); // Should be limited to sampleSize
|
||||
process.memoryUsage = originalMemoryUsage;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Component Registration", () => {
|
||||
test("should register and unregister components", () => {
|
||||
detector.registerComponent("TestComponent", 1);
|
||||
expect(detector.componentRegistry.has("TestComponent")).toBe(true);
|
||||
expect(detector.componentRegistry.get("TestComponent").instances).toBe(1);
|
||||
|
||||
detector.registerComponent("TestComponent", 1);
|
||||
expect(detector.componentRegistry.get("TestComponent").instances).toBe(2);
|
||||
|
||||
detector.unregisterComponent("TestComponent");
|
||||
expect(detector.componentRegistry.get("TestComponent").instances).toBe(1);
|
||||
|
||||
detector.unregisterComponent("TestComponent");
|
||||
expect(detector.componentRegistry.has("TestComponent")).toBe(false);
|
||||
});
|
||||
|
||||
test("should detect suspicious components", () => {
|
||||
detector.registerComponent("LeakyComponent", 1);
|
||||
|
||||
// Register many instances
|
||||
for (let i = 0; i < 10; i++) {
|
||||
detector.registerComponent("LeakyComponent", 1);
|
||||
}
|
||||
|
||||
const suspicious = detector.getSuspiciousComponents();
|
||||
expect(suspicious).toHaveLength(1);
|
||||
expect(suspicious[0].name).toBe("LeakyComponent");
|
||||
expect(suspicious[0].instances).toBe(11);
|
||||
expect(suspicious[0].ratio).toBeGreaterThan(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Leak Detection", () => {
|
||||
test("should detect steady memory growth", () => {
|
||||
const mockMemoryUsage = jest.fn();
|
||||
const originalMemoryUsage = process.memoryUsage;
|
||||
process.memoryUsage = mockMemoryUsage;
|
||||
|
||||
// Simulate steady growth
|
||||
const baseMemory = 50 * 1024 * 1024;
|
||||
const growthPerSample = 5 * 1024 * 1024;
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
mockMemoryUsage.mockReturnValue({
|
||||
heapUsed: baseMemory + i * growthPerSample,
|
||||
heapTotal: 100 * 1024 * 1024,
|
||||
external: 5 * 1024 * 1024,
|
||||
rss: 120 * 1024 * 1024,
|
||||
});
|
||||
detector.takeSample();
|
||||
}
|
||||
|
||||
const analysis = detector.analyzeLeaks();
|
||||
expect(analysis.hasLeak).toBe(true);
|
||||
expect(analysis.analysis.steadyGrowth).toBe(true);
|
||||
|
||||
process.memoryUsage = originalMemoryUsage;
|
||||
});
|
||||
|
||||
test("should detect rapid memory growth", () => {
|
||||
const mockMemoryUsage = jest.fn();
|
||||
const originalMemoryUsage = process.memoryUsage;
|
||||
process.memoryUsage = mockMemoryUsage;
|
||||
|
||||
// Simulate rapid growth
|
||||
mockMemoryUsage.mockReturnValueOnce({
|
||||
heapUsed: 50 * 1024 * 1024,
|
||||
heapTotal: 100 * 1024 * 1024,
|
||||
external: 5 * 1024 * 1024,
|
||||
rss: 120 * 1024 * 1024,
|
||||
});
|
||||
detector.takeSample();
|
||||
|
||||
// Wait a bit then add large growth
|
||||
setTimeout(() => {
|
||||
mockMemoryUsage.mockReturnValueOnce({
|
||||
heapUsed: 60 * 1024 * 1024, // 10MB growth
|
||||
heapTotal: 100 * 1024 * 1024,
|
||||
external: 5 * 1024 * 1024,
|
||||
rss: 130 * 1024 * 1024,
|
||||
});
|
||||
detector.takeSample();
|
||||
|
||||
mockMemoryUsage.mockReturnValueOnce({
|
||||
heapUsed: 70 * 1024 * 1024, // Another 10MB growth
|
||||
heapTotal: 100 * 1024 * 1024,
|
||||
external: 5 * 1024 * 1024,
|
||||
rss: 140 * 1024 * 1024,
|
||||
});
|
||||
detector.takeSample();
|
||||
|
||||
const analysis = detector.analyzeLeaks();
|
||||
expect(analysis.hasLeak).toBe(true);
|
||||
expect(analysis.analysis.rapidGrowth).toBe(true);
|
||||
|
||||
process.memoryUsage = originalMemoryUsage;
|
||||
}, 10);
|
||||
});
|
||||
|
||||
test("should generate appropriate recommendations", () => {
|
||||
const mockMemoryUsage = jest.fn();
|
||||
const originalMemoryUsage = process.memoryUsage;
|
||||
process.memoryUsage = mockMemoryUsage;
|
||||
|
||||
// Simulate component leak
|
||||
detector.registerComponent("LeakyComponent", 1);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
detector.registerComponent("LeakyComponent", 1);
|
||||
}
|
||||
|
||||
// Simulate memory growth
|
||||
for (let i = 0; i < 3; i++) {
|
||||
mockMemoryUsage.mockReturnValue({
|
||||
heapUsed: 50 * 1024 * 1024 + i * 2 * 1024 * 1024,
|
||||
heapTotal: 100 * 1024 * 1024,
|
||||
external: 5 * 1024 * 1024,
|
||||
rss: 120 * 1024 * 1024,
|
||||
});
|
||||
detector.takeSample();
|
||||
}
|
||||
|
||||
const analysis = detector.analyzeLeaks();
|
||||
expect(analysis.analysis.recommendations).toBeDefined();
|
||||
expect(analysis.analysis.recommendations.length).toBeGreaterThan(0);
|
||||
|
||||
const hasComponentRecommendation = analysis.analysis.recommendations.some(
|
||||
(rec) => rec.type === "component-leak"
|
||||
);
|
||||
expect(hasComponentRecommendation).toBe(true);
|
||||
|
||||
process.memoryUsage = originalMemoryUsage;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Event Listeners", () => {
|
||||
test("should notify listeners of events", () => {
|
||||
const listener = jest.fn();
|
||||
detector.addListener(listener);
|
||||
|
||||
const mockMemoryUsage = jest.fn(() => ({
|
||||
heapUsed: 50 * 1024 * 1024,
|
||||
heapTotal: 100 * 1024 * 1024,
|
||||
external: 5 * 1024 * 1024,
|
||||
rss: 120 * 1024 * 1024,
|
||||
}));
|
||||
|
||||
const originalMemoryUsage = process.memoryUsage;
|
||||
process.memoryUsage = mockMemoryUsage;
|
||||
|
||||
detector.takeSample();
|
||||
expect(listener).toHaveBeenCalledWith("sample", expect.any(Object));
|
||||
|
||||
detector.removeListener(listener);
|
||||
detector.takeSample();
|
||||
expect(listener).toHaveBeenCalledTimes(1); // Should not be called again
|
||||
|
||||
process.memoryUsage = originalMemoryUsage;
|
||||
});
|
||||
|
||||
test("should handle listener errors gracefully", () => {
|
||||
const consoleSpy = jest
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation(() => {});
|
||||
const badListener = jest.fn(() => {
|
||||
throw new Error("Listener error");
|
||||
});
|
||||
const goodListener = jest.fn();
|
||||
|
||||
detector.addListener(badListener);
|
||||
detector.addListener(goodListener);
|
||||
|
||||
const mockMemoryUsage = jest.fn(() => ({
|
||||
heapUsed: 50 * 1024 * 1024,
|
||||
heapTotal: 100 * 1024 * 1024,
|
||||
external: 5 * 1024 * 1024,
|
||||
rss: 120 * 1024 * 1024,
|
||||
}));
|
||||
|
||||
const originalMemoryUsage = process.memoryUsage;
|
||||
process.memoryUsage = mockMemoryUsage;
|
||||
|
||||
detector.takeSample();
|
||||
|
||||
expect(badListener).toHaveBeenCalled();
|
||||
expect(goodListener).toHaveBeenCalled();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"[MemoryLeakDetector] Error in listener:",
|
||||
expect.any(Error)
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
process.memoryUsage = originalMemoryUsage;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Statistics and Reporting", () => {
|
||||
test("should provide memory statistics", () => {
|
||||
const mockMemoryUsage = jest.fn();
|
||||
const originalMemoryUsage = process.memoryUsage;
|
||||
process.memoryUsage = mockMemoryUsage;
|
||||
|
||||
// Take a few samples
|
||||
for (let i = 0; i < 3; i++) {
|
||||
mockMemoryUsage.mockReturnValue({
|
||||
heapUsed: 50 * 1024 * 1024 + i * 1024 * 1024,
|
||||
heapTotal: 100 * 1024 * 1024,
|
||||
external: 5 * 1024 * 1024,
|
||||
rss: 120 * 1024 * 1024,
|
||||
});
|
||||
detector.takeSample();
|
||||
}
|
||||
|
||||
const stats = detector.getStatistics();
|
||||
expect(stats).toBeDefined();
|
||||
expect(stats.current).toBeDefined();
|
||||
expect(stats.growth).toBeDefined();
|
||||
expect(stats.trend).toBeDefined();
|
||||
expect(stats.samples).toBe(3);
|
||||
|
||||
process.memoryUsage = originalMemoryUsage;
|
||||
});
|
||||
|
||||
test("should generate comprehensive report", () => {
|
||||
detector.registerComponent("TestComponent", 1);
|
||||
|
||||
const mockMemoryUsage = jest.fn(() => ({
|
||||
heapUsed: 50 * 1024 * 1024,
|
||||
heapTotal: 100 * 1024 * 1024,
|
||||
external: 5 * 1024 * 1024,
|
||||
rss: 120 * 1024 * 1024,
|
||||
}));
|
||||
|
||||
const originalMemoryUsage = process.memoryUsage;
|
||||
process.memoryUsage = mockMemoryUsage;
|
||||
|
||||
detector.takeSample();
|
||||
const report = detector.generateReport();
|
||||
|
||||
expect(report).toMatchObject({
|
||||
timestamp: expect.any(Number),
|
||||
monitoring: expect.any(Boolean),
|
||||
statistics: expect.any(Object),
|
||||
components: expect.any(Array),
|
||||
recommendations: expect.any(Array),
|
||||
});
|
||||
|
||||
expect(report.components).toHaveLength(1);
|
||||
expect(report.components[0].name).toBe("TestComponent");
|
||||
|
||||
process.memoryUsage = originalMemoryUsage;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("useMemoryLeakDetection Hook", () => {
|
||||
test("should register and unregister component on mount/unmount", () => {
|
||||
const TestComponent = () => {
|
||||
useMemoryLeakDetection("TestComponent");
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
const detector = getGlobalDetector();
|
||||
expect(detector.componentRegistry.has("TestComponent")).toBe(false);
|
||||
|
||||
const { unmount } = render(React.createElement(TestComponent));
|
||||
expect(detector.componentRegistry.has("TestComponent")).toBe(true);
|
||||
|
||||
unmount();
|
||||
expect(detector.componentRegistry.has("TestComponent")).toBe(false);
|
||||
});
|
||||
|
||||
test("should provide detector utilities", () => {
|
||||
let detectorUtils = {};
|
||||
|
||||
const TestComponent = () => {
|
||||
detectorUtils = useMemoryLeakDetection("TestComponent");
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
render(React.createElement(TestComponent));
|
||||
|
||||
expect(typeof detectorUtils.detector).toBe("object");
|
||||
expect(typeof detectorUtils.forceGC).toBe("function");
|
||||
expect(typeof detectorUtils.getReport).toBe("function");
|
||||
expect(typeof detectorUtils.getStats).toBe("function");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryLeakUtils", () => {
|
||||
describe("checkObjectForLeaks", () => {
|
||||
test("should detect circular references", () => {
|
||||
const obj = { name: "test" };
|
||||
obj.self = obj; // Create circular reference
|
||||
|
||||
const issues = MemoryLeakUtils.checkObjectForLeaks(obj, "testObj");
|
||||
const circularIssue = issues.find(
|
||||
(issue) => issue.type === "circular-reference"
|
||||
);
|
||||
|
||||
expect(circularIssue).toBeDefined();
|
||||
expect(circularIssue.message).toContain("Circular reference");
|
||||
});
|
||||
|
||||
test("should detect large arrays", () => {
|
||||
const largeArray = new Array(15000).fill("item");
|
||||
const issues = MemoryLeakUtils.checkObjectForLeaks(
|
||||
largeArray,
|
||||
"largeArray"
|
||||
);
|
||||
|
||||
const arrayIssue = issues.find((issue) => issue.type === "large-array");
|
||||
expect(arrayIssue).toBeDefined();
|
||||
expect(arrayIssue.length).toBe(15000);
|
||||
});
|
||||
|
||||
test("should detect objects with many properties", () => {
|
||||
const obj = {};
|
||||
for (let i = 0; i < 1500; i++) {
|
||||
obj[`prop${i}`] = i;
|
||||
}
|
||||
|
||||
const issues = MemoryLeakUtils.checkObjectForLeaks(obj, "manyPropsObj");
|
||||
const propsIssue = issues.find(
|
||||
(issue) => issue.type === "many-properties"
|
||||
);
|
||||
|
||||
expect(propsIssue).toBeDefined();
|
||||
expect(propsIssue.count).toBe(1500);
|
||||
});
|
||||
|
||||
test("should handle null and undefined objects", () => {
|
||||
expect(MemoryLeakUtils.checkObjectForLeaks(null)).toEqual([]);
|
||||
expect(MemoryLeakUtils.checkObjectForLeaks(undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkDOMNodeForLeaks", () => {
|
||||
test("should detect excessive event listeners", () => {
|
||||
const mockNode = {
|
||||
_events: {},
|
||||
};
|
||||
|
||||
// Add many event listeners
|
||||
for (let i = 0; i < 60; i++) {
|
||||
mockNode._events[`event${i}`] = () => {};
|
||||
}
|
||||
|
||||
const issues = MemoryLeakUtils.checkDOMNodeForLeaks(mockNode);
|
||||
const listenerIssue = issues.find(
|
||||
(issue) => issue.type === "excessive-listeners"
|
||||
);
|
||||
|
||||
expect(listenerIssue).toBeDefined();
|
||||
expect(listenerIssue.count).toBe(60);
|
||||
});
|
||||
|
||||
test("should detect detached DOM nodes", () => {
|
||||
const mockNode = {
|
||||
parentNode: null,
|
||||
};
|
||||
|
||||
const issues = MemoryLeakUtils.checkDOMNodeForLeaks(mockNode);
|
||||
const detachedIssue = issues.find(
|
||||
(issue) => issue.type === "detached-node"
|
||||
);
|
||||
|
||||
expect(detachedIssue).toBeDefined();
|
||||
});
|
||||
|
||||
test("should handle invalid nodes", () => {
|
||||
expect(MemoryLeakUtils.checkDOMNodeForLeaks(null)).toEqual([]);
|
||||
expect(MemoryLeakUtils.checkDOMNodeForLeaks("not an object")).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Global Detector", () => {
|
||||
test("should return the same instance", () => {
|
||||
const detector1 = getGlobalDetector();
|
||||
const detector2 = getGlobalDetector();
|
||||
|
||||
expect(detector1).toBe(detector2);
|
||||
});
|
||||
|
||||
test("should accept options on first call", () => {
|
||||
// Reset global detector
|
||||
const MemoryLeakDetectorModule = require("../../../src/tui/utils/memoryLeakDetector.js");
|
||||
MemoryLeakDetectorModule.globalDetector = null;
|
||||
|
||||
const detector = getGlobalDetector({
|
||||
checkInterval: 5000,
|
||||
verbose: true,
|
||||
});
|
||||
|
||||
expect(detector.options.checkInterval).toBe(5000);
|
||||
expect(detector.options.verbose).toBe(true);
|
||||
});
|
||||
});
|
||||
526
tests/tui/performance/memoryManagement.test.js
Normal file
526
tests/tui/performance/memoryManagement.test.js
Normal file
@@ -0,0 +1,526 @@
|
||||
const React = require("react");
|
||||
const { render } = require("ink-testing-library");
|
||||
const {
|
||||
useEventListener,
|
||||
useInterval,
|
||||
useTimeout,
|
||||
useAsyncOperation,
|
||||
useMemoryMonitor,
|
||||
useWeakRef,
|
||||
useCleanup,
|
||||
useResourcePool,
|
||||
} = require("../../../src/tui/hooks/useMemoryManagement.js");
|
||||
const {
|
||||
withMemoryManagement,
|
||||
MemoryOptimizedContainer,
|
||||
MemoryEfficientList,
|
||||
AutoCleanupComponent,
|
||||
} = require("../../../src/tui/components/common/MemoryOptimizedComponent.jsx");
|
||||
|
||||
/**
|
||||
* Memory management tests for TUI components
|
||||
* Requirements: 4.2, 4.5
|
||||
*/
|
||||
|
||||
describe("Memory Management Hooks", () => {
|
||||
describe("useCleanup", () => {
|
||||
test("should execute cleanup functions on unmount", () => {
|
||||
const cleanupFn1 = jest.fn();
|
||||
const cleanupFn2 = jest.fn();
|
||||
|
||||
const TestComponent = () => {
|
||||
const { addCleanup } = useCleanup();
|
||||
|
||||
React.useEffect(() => {
|
||||
addCleanup(cleanupFn1);
|
||||
addCleanup(cleanupFn2);
|
||||
}, [addCleanup]);
|
||||
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
const { unmount } = render(React.createElement(TestComponent));
|
||||
|
||||
expect(cleanupFn1).not.toHaveBeenCalled();
|
||||
expect(cleanupFn2).not.toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
|
||||
expect(cleanupFn1).toHaveBeenCalledTimes(1);
|
||||
expect(cleanupFn2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should handle cleanup function errors gracefully", () => {
|
||||
const consoleSpy = jest
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation(() => {});
|
||||
const goodCleanup = jest.fn();
|
||||
const badCleanup = jest.fn(() => {
|
||||
throw new Error("Cleanup error");
|
||||
});
|
||||
|
||||
const TestComponent = () => {
|
||||
const { addCleanup } = useCleanup();
|
||||
|
||||
React.useEffect(() => {
|
||||
addCleanup(badCleanup);
|
||||
addCleanup(goodCleanup);
|
||||
}, [addCleanup]);
|
||||
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
const { unmount } = render(React.createElement(TestComponent));
|
||||
unmount();
|
||||
|
||||
expect(badCleanup).toHaveBeenCalled();
|
||||
expect(goodCleanup).toHaveBeenCalled();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"Error during cleanup:",
|
||||
expect.any(Error)
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useAsyncOperation", () => {
|
||||
test("should cancel operations on unmount", async () => {
|
||||
let operationCancelled = false;
|
||||
const asyncOperation = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
if (operationCancelled) {
|
||||
reject(new Error("Operation cancelled"));
|
||||
} else {
|
||||
resolve("success");
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
const TestComponent = () => {
|
||||
const { executeAsync, cancelAllOperations } = useAsyncOperation();
|
||||
|
||||
React.useEffect(() => {
|
||||
executeAsync(asyncOperation).catch((error) => {
|
||||
if (error.message === "Operation cancelled") {
|
||||
operationCancelled = true;
|
||||
}
|
||||
});
|
||||
}, [executeAsync]);
|
||||
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
const { unmount } = render(React.createElement(TestComponent));
|
||||
|
||||
// Unmount before operation completes
|
||||
setTimeout(() => unmount(), 50);
|
||||
|
||||
// Wait for operation to complete or be cancelled
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
|
||||
expect(operationCancelled).toBe(true);
|
||||
});
|
||||
|
||||
test("should not execute callbacks after unmount", async () => {
|
||||
const onSuccess = jest.fn();
|
||||
const onError = jest.fn();
|
||||
|
||||
const TestComponent = () => {
|
||||
const { executeAsync } = useAsyncOperation();
|
||||
|
||||
React.useEffect(() => {
|
||||
const asyncOp = () => Promise.resolve("success");
|
||||
executeAsync(asyncOp, onSuccess, onError);
|
||||
}, [executeAsync]);
|
||||
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
const { unmount } = render(React.createElement(TestComponent));
|
||||
|
||||
// Unmount immediately
|
||||
unmount();
|
||||
|
||||
// Wait for async operation
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(onSuccess).not.toHaveBeenCalled();
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useInterval", () => {
|
||||
test("should clear interval on unmount", () => {
|
||||
jest.useFakeTimers();
|
||||
const callback = jest.fn();
|
||||
|
||||
const TestComponent = () => {
|
||||
useInterval(callback, 1000);
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
const { unmount } = render(React.createElement(TestComponent));
|
||||
|
||||
// Fast-forward time
|
||||
jest.advanceTimersByTime(2000);
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
|
||||
unmount();
|
||||
|
||||
// Fast-forward more time after unmount
|
||||
jest.advanceTimersByTime(2000);
|
||||
expect(callback).toHaveBeenCalledTimes(2); // Should not increase
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test("should provide manual control over interval", () => {
|
||||
jest.useFakeTimers();
|
||||
const callback = jest.fn();
|
||||
|
||||
const TestComponent = () => {
|
||||
const { start, stop, restart } = useInterval(callback, 1000);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Test manual control
|
||||
setTimeout(() => stop(), 1500);
|
||||
setTimeout(() => start(), 2500);
|
||||
setTimeout(() => restart(), 3500);
|
||||
}, [start, stop, restart]);
|
||||
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
render(React.createElement(TestComponent));
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
|
||||
jest.advanceTimersByTime(1000); // 2000ms total, stopped at 1500ms
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
|
||||
jest.advanceTimersByTime(1000); // 3000ms total, restarted at 2500ms
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useTimeout", () => {
|
||||
test("should clear timeout on unmount", () => {
|
||||
jest.useFakeTimers();
|
||||
const callback = jest.fn();
|
||||
|
||||
const TestComponent = () => {
|
||||
useTimeout(callback, 1000);
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
const { unmount } = render(React.createElement(TestComponent));
|
||||
|
||||
// Unmount before timeout
|
||||
unmount();
|
||||
|
||||
// Fast-forward past timeout
|
||||
jest.advanceTimersByTime(1500);
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe("useMemoryMonitor", () => {
|
||||
test("should track render count", () => {
|
||||
let renderCount = 0;
|
||||
|
||||
const TestComponent = ({ value }) => {
|
||||
const { renderCount: currentRenderCount } =
|
||||
useMemoryMonitor("TestComponent");
|
||||
renderCount = currentRenderCount;
|
||||
return React.createElement("div", null, value);
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
React.createElement(TestComponent, { value: 1 })
|
||||
);
|
||||
expect(renderCount).toBe(1);
|
||||
|
||||
rerender(React.createElement(TestComponent, { value: 2 }));
|
||||
expect(renderCount).toBe(2);
|
||||
|
||||
rerender(React.createElement(TestComponent, { value: 3 }));
|
||||
expect(renderCount).toBe(3);
|
||||
});
|
||||
|
||||
test("should provide memory statistics", () => {
|
||||
let memoryStats = null;
|
||||
|
||||
const TestComponent = () => {
|
||||
const { getMemoryStats } = useMemoryMonitor("TestComponent");
|
||||
|
||||
React.useEffect(() => {
|
||||
// Simulate some memory usage
|
||||
setTimeout(() => {
|
||||
memoryStats = getMemoryStats();
|
||||
}, 100);
|
||||
}, [getMemoryStats]);
|
||||
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
render(React.createElement(TestComponent));
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
// Memory stats might be null in test environment
|
||||
// but the function should exist
|
||||
expect(typeof memoryStats).toBeDefined();
|
||||
resolve();
|
||||
}, 150);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("useWeakRef", () => {
|
||||
test("should store and retrieve values using weak references", () => {
|
||||
let getValue, setValue;
|
||||
|
||||
const TestComponent = () => {
|
||||
[getValue, setValue] = useWeakRef();
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
render(React.createElement(TestComponent));
|
||||
|
||||
const testObject = { data: "test" };
|
||||
setValue(testObject);
|
||||
|
||||
expect(getValue()).toBe(testObject);
|
||||
|
||||
setValue(null);
|
||||
expect(getValue()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("useResourcePool", () => {
|
||||
test("should manage resource pool efficiently", () => {
|
||||
let resourcePool;
|
||||
const createResource = jest.fn(() => ({ id: Math.random() }));
|
||||
const resetResource = jest.fn();
|
||||
|
||||
const TestComponent = () => {
|
||||
resourcePool = useResourcePool(createResource, resetResource, 3);
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
render(React.createElement(TestComponent));
|
||||
|
||||
// Acquire resources
|
||||
const resource1 = resourcePool.acquire();
|
||||
const resource2 = resourcePool.acquire();
|
||||
|
||||
expect(createResource).toHaveBeenCalledTimes(2);
|
||||
expect(resourcePool.activeCount).toBe(2);
|
||||
expect(resourcePool.poolSize).toBe(0);
|
||||
|
||||
// Release resources
|
||||
resourcePool.release(resource1);
|
||||
expect(resourcePool.activeCount).toBe(1);
|
||||
expect(resourcePool.poolSize).toBe(1);
|
||||
|
||||
// Acquire again (should reuse)
|
||||
const resource3 = resourcePool.acquire();
|
||||
expect(createResource).toHaveBeenCalledTimes(2); // No new creation
|
||||
expect(resetResource).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Memory Optimized Components", () => {
|
||||
describe("withMemoryManagement HOC", () => {
|
||||
test("should provide memory management props to wrapped component", () => {
|
||||
let receivedProps = {};
|
||||
|
||||
const TestComponent = (props) => {
|
||||
receivedProps = props;
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
const MemoryManagedComponent = withMemoryManagement(TestComponent, {
|
||||
componentName: "TestComponent",
|
||||
});
|
||||
|
||||
render(
|
||||
React.createElement(MemoryManagedComponent, { testProp: "value" })
|
||||
);
|
||||
|
||||
expect(receivedProps.testProp).toBe("value");
|
||||
expect(typeof receivedProps.addCleanup).toBe("function");
|
||||
expect(typeof receivedProps.executeAsync).toBe("function");
|
||||
expect(typeof receivedProps.getMemoryStats).toBe("function");
|
||||
expect(typeof receivedProps.renderCount).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryOptimizedContainer", () => {
|
||||
test("should display memory warnings when threshold is exceeded", () => {
|
||||
// Mock process.memoryUsage to return high memory usage
|
||||
const originalMemoryUsage = process.memoryUsage;
|
||||
process.memoryUsage = jest.fn(() => ({
|
||||
heapUsed: 200 * 1024 * 1024, // 200MB
|
||||
heapTotal: 250 * 1024 * 1024,
|
||||
external: 10 * 1024 * 1024,
|
||||
rss: 300 * 1024 * 1024,
|
||||
}));
|
||||
|
||||
const onMemoryWarning = jest.fn();
|
||||
|
||||
const { lastFrame } = render(
|
||||
React.createElement(
|
||||
MemoryOptimizedContainer,
|
||||
{
|
||||
memoryThreshold: 100 * 1024 * 1024, // 100MB threshold
|
||||
memoryCheckInterval: 100, // Fast check for testing
|
||||
onMemoryWarning,
|
||||
},
|
||||
"Test content"
|
||||
)
|
||||
);
|
||||
|
||||
// Wait for memory check
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain("Memory Warning");
|
||||
process.memoryUsage = originalMemoryUsage;
|
||||
resolve();
|
||||
}, 150);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryEfficientList", () => {
|
||||
test("should limit cached items to prevent memory bloat", () => {
|
||||
const items = Array.from({ length: 200 }, (_, i) => `Item ${i}`);
|
||||
const renderItem = (item, index) =>
|
||||
React.createElement("div", { key: index }, item);
|
||||
|
||||
const { lastFrame } = render(
|
||||
React.createElement(MemoryEfficientList, {
|
||||
items,
|
||||
renderItem,
|
||||
maxCachedItems: 50,
|
||||
})
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain("Cached:");
|
||||
expect(output).toContain("50"); // Should show cache limit
|
||||
});
|
||||
});
|
||||
|
||||
describe("AutoCleanupComponent", () => {
|
||||
test("should cleanup old resources automatically", () => {
|
||||
jest.useFakeTimers();
|
||||
let resourceUtils = {};
|
||||
|
||||
const TestComponent = (utils) => {
|
||||
resourceUtils = utils;
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
render(
|
||||
React.createElement(
|
||||
AutoCleanupComponent,
|
||||
{
|
||||
cleanupInterval: 1000,
|
||||
maxAge: 2000,
|
||||
},
|
||||
TestComponent
|
||||
)
|
||||
);
|
||||
|
||||
// Add a resource
|
||||
const mockResource = {
|
||||
cleanup: jest.fn(),
|
||||
};
|
||||
resourceUtils.addResource("test", mockResource);
|
||||
expect(resourceUtils.resourceCount).toBe(1);
|
||||
|
||||
// Fast-forward past maxAge
|
||||
jest.advanceTimersByTime(3000);
|
||||
|
||||
expect(mockResource.cleanup).toHaveBeenCalled();
|
||||
expect(resourceUtils.resourceCount).toBe(0);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Memory Leak Detection", () => {
|
||||
test("should detect potential memory leaks in component lifecycle", async () => {
|
||||
const components = [];
|
||||
|
||||
// Create multiple components with potential leaks
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const TestComponent = () => {
|
||||
const [data, setData] = React.useState([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Simulate memory leak by accumulating data
|
||||
const interval = setInterval(() => {
|
||||
setData((prev) => [...prev, new Array(1000).fill(i)]);
|
||||
}, 10);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return React.createElement("div", null, `Component ${i}`);
|
||||
};
|
||||
|
||||
const { unmount } = render(React.createElement(TestComponent));
|
||||
components.push(unmount);
|
||||
}
|
||||
|
||||
// Let components run for a bit
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Unmount all components
|
||||
components.forEach((unmount) => unmount());
|
||||
|
||||
// Wait for cleanup
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// In a real scenario, we would check memory usage here
|
||||
// For testing, we just verify that components were created and destroyed
|
||||
expect(components).toHaveLength(10);
|
||||
});
|
||||
|
||||
test("should properly cleanup event listeners", () => {
|
||||
const mockAddEventListener = jest.fn();
|
||||
const mockRemoveEventListener = jest.fn();
|
||||
|
||||
// Mock DOM element
|
||||
const mockElement = {
|
||||
addEventListener: mockAddEventListener,
|
||||
removeEventListener: mockRemoveEventListener,
|
||||
};
|
||||
|
||||
const TestComponent = () => {
|
||||
useEventListener("click", () => {}, mockElement);
|
||||
return React.createElement("div", null, "Test");
|
||||
};
|
||||
|
||||
const { unmount } = render(React.createElement(TestComponent));
|
||||
|
||||
expect(mockAddEventListener).toHaveBeenCalledTimes(1);
|
||||
expect(mockRemoveEventListener).not.toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
|
||||
expect(mockRemoveEventListener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
446
tests/tui/performance/renderingPerformance.test.js
Normal file
446
tests/tui/performance/renderingPerformance.test.js
Normal file
@@ -0,0 +1,446 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user