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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user