nexo/apps/mattermost/app/utils/performance/react_profiler.test.ts

313 lines
13 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import ReactProfiler from '@utils/performance/react_profiler';
describe('ReactProfilerManager', () => {
beforeEach(() => {
jest.clearAllMocks();
ReactProfiler.clearMetrics();
ReactProfiler.setEnabled(true);
});
describe('when profiling is disabled', () => {
beforeEach(() => {
(global as any).__DEV__ = false;
ReactProfiler.setEnabled(false);
});
it('should return false for isEnabled', () => {
expect(ReactProfiler.isEnabled()).toBe(false);
});
it('should not track metrics when disabled', () => {
ReactProfiler.onRender('TestComponent', 'mount', 100, 90, 1000, 1100);
const metrics = ReactProfiler.getMetrics('TestComponent');
expect(metrics).toHaveLength(0);
});
it('should not print summary when disabled', () => {
ReactProfiler.onRender('TestComponent', 'mount', 100, 90, 1000, 1100);
// Should not throw
ReactProfiler.printSummary();
});
});
describe('when profiling is enabled', () => {
beforeEach(() => {
(global as any).__DEV__ = true;
ReactProfiler.setEnabled(true);
});
it('should return true for isEnabled', () => {
expect(ReactProfiler.isEnabled()).toBe(true);
});
describe('onRender', () => {
it('should track mount phase metrics', () => {
ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050);
const metrics = ReactProfiler.getMetrics('Post');
expect(metrics).toHaveLength(1);
expect(metrics[0]).toEqual({
componentName: 'Post',
phase: 'mount',
actualDuration: 50,
baseDuration: 45,
startTime: 1000,
commitTime: 1050,
});
});
it('should track update phase metrics', () => {
ReactProfiler.onRender('Channel', 'update', 25, 20, 2000, 2025);
const metrics = ReactProfiler.getMetrics('Channel');
expect(metrics).toHaveLength(1);
expect(metrics[0]).toEqual({
componentName: 'Channel',
phase: 'update',
actualDuration: 25,
baseDuration: 20,
startTime: 2000,
commitTime: 2025,
});
});
it('should track nested-update phase metrics', () => {
ReactProfiler.onRender('User', 'nested-update', 10, 8, 3000, 3010);
const metrics = ReactProfiler.getMetrics('User');
expect(metrics).toHaveLength(1);
expect(metrics[0].phase).toBe('nested-update');
});
it('should track multiple renders for same component', () => {
ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050);
ReactProfiler.onRender('Post', 'update', 25, 20, 2000, 2025);
ReactProfiler.onRender('Post', 'update', 15, 12, 3000, 3015);
const metrics = ReactProfiler.getMetrics('Post');
expect(metrics).toHaveLength(3);
expect(metrics[0].phase).toBe('mount');
expect(metrics[1].phase).toBe('update');
expect(metrics[2].phase).toBe('update');
});
it('should track renders for multiple components', () => {
ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050);
ReactProfiler.onRender('Channel', 'mount', 30, 28, 1100, 1130);
ReactProfiler.onRender('User', 'mount', 10, 8, 1200, 1210);
expect(ReactProfiler.getMetrics('Post')).toHaveLength(1);
expect(ReactProfiler.getMetrics('Channel')).toHaveLength(1);
expect(ReactProfiler.getMetrics('User')).toHaveLength(1);
});
it('should not log renders under 50ms', () => {
ReactProfiler.onRender('FastComponent', 'mount', 30, 25, 1000, 1030);
const metrics = ReactProfiler.getMetrics('FastComponent');
expect(metrics).toHaveLength(1);
});
it('should log renders over 50ms', () => {
ReactProfiler.onRender('SlowComponent', 'mount', 75, 70, 1000, 1075);
const metrics = ReactProfiler.getMetrics('SlowComponent');
expect(metrics).toHaveLength(1);
expect(metrics[0].actualDuration).toBe(75);
});
});
describe('getMetrics', () => {
it('should return empty array for component with no metrics', () => {
const metrics = ReactProfiler.getMetrics('NonexistentComponent');
expect(metrics).toEqual([]);
});
it('should return all metrics for a component', () => {
ReactProfiler.onRender('TestComponent', 'mount', 50, 45, 1000, 1050);
ReactProfiler.onRender('TestComponent', 'update', 25, 20, 2000, 2025);
const metrics = ReactProfiler.getMetrics('TestComponent');
expect(metrics).toHaveLength(2);
});
});
describe('getAllMetrics', () => {
it('should return empty map when no metrics tracked', () => {
const allMetrics = ReactProfiler.getAllMetrics();
expect(allMetrics.size).toBe(0);
});
it('should return all tracked metrics', () => {
ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050);
ReactProfiler.onRender('Channel', 'mount', 30, 28, 1100, 1130);
const allMetrics = ReactProfiler.getAllMetrics();
expect(allMetrics.size).toBe(2);
expect(allMetrics.has('Post')).toBe(true);
expect(allMetrics.has('Channel')).toBe(true);
});
});
describe('printSummary', () => {
it('should handle no metrics collected', () => {
// Should not throw
ReactProfiler.printSummary();
});
it('should print summary for single component', () => {
ReactProfiler.onRender('Post', 'mount', 100, 90, 1000, 1100);
ReactProfiler.onRender('Post', 'update', 50, 45, 2000, 2050);
ReactProfiler.printSummary();
});
it('should print summary for multiple components', () => {
ReactProfiler.onRender('Post', 'mount', 100, 90, 1000, 1100);
ReactProfiler.onRender('Channel', 'mount', 80, 75, 1100, 1180);
ReactProfiler.onRender('User', 'mount', 60, 55, 1200, 1260);
ReactProfiler.printSummary();
});
it('should sort components by total actual time', () => {
ReactProfiler.onRender('Fast', 'mount', 10, 8, 1000, 1010);
ReactProfiler.onRender('Slow', 'mount', 100, 90, 1100, 1200);
ReactProfiler.onRender('Medium', 'mount', 50, 45, 1200, 1250);
ReactProfiler.printSummary();
});
it('should limit output to top 10 components', () => {
// Add 15 components
for (let i = 0; i < 15; i++) {
ReactProfiler.onRender(`Component${i}`, 'mount', 10 + i, 8 + i, 1000 + (i * 100), 1010 + (i * 100));
}
ReactProfiler.printSummary();
});
it('should calculate correct statistics', () => {
// Add multiple renders to test statistics
ReactProfiler.onRender('TestComponent', 'mount', 100, 90, 1000, 1100);
ReactProfiler.onRender('TestComponent', 'update', 50, 45, 2000, 2050);
ReactProfiler.onRender('TestComponent', 'update', 25, 20, 3000, 3025);
ReactProfiler.printSummary();
});
it('should identify memoization opportunities', () => {
// Component with actual duration much higher than base duration
ReactProfiler.onRender('UnoptimizedComponent', 'mount', 200, 100, 1000, 1200);
ReactProfiler.printSummary();
});
it('should not show memoization warning for well-optimized components', () => {
// Component with actual duration close to base duration
ReactProfiler.onRender('OptimizedComponent', 'mount', 105, 100, 1000, 1105);
ReactProfiler.printSummary();
});
});
describe('clearMetrics', () => {
it('should clear all metrics when no component specified', () => {
ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050);
ReactProfiler.onRender('Channel', 'mount', 30, 28, 1100, 1130);
ReactProfiler.clearMetrics();
expect(ReactProfiler.getMetrics('Post')).toEqual([]);
expect(ReactProfiler.getMetrics('Channel')).toEqual([]);
expect(ReactProfiler.getAllMetrics().size).toBe(0);
});
it('should clear metrics for specific component', () => {
ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050);
ReactProfiler.onRender('Channel', 'mount', 30, 28, 1100, 1130);
ReactProfiler.clearMetrics('Post');
expect(ReactProfiler.getMetrics('Post')).toEqual([]);
expect(ReactProfiler.getMetrics('Channel')).toHaveLength(1);
});
it('should handle clearing nonexistent component', () => {
ReactProfiler.clearMetrics('NonexistentComponent');
// Should not throw
expect(ReactProfiler.getMetrics('NonexistentComponent')).toEqual([]);
});
});
describe('setEnabled', () => {
it('should disable profiling when set to false', () => {
ReactProfiler.setEnabled(false);
ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050);
expect(ReactProfiler.getMetrics('Post')).toEqual([]);
});
it('should enable profiling when set to true', () => {
ReactProfiler.setEnabled(false);
ReactProfiler.setEnabled(true);
ReactProfiler.onRender('Post', 'mount', 50, 45, 1000, 1050);
expect(ReactProfiler.getMetrics('Post')).toHaveLength(1);
});
});
describe('phase tracking', () => {
it('should correctly count mount renders', () => {
ReactProfiler.onRender('Component', 'mount', 50, 45, 1000, 1050);
ReactProfiler.onRender('Component', 'mount', 55, 50, 2000, 2055);
const metrics = ReactProfiler.getMetrics('Component');
const mountCount = metrics.filter((m) => m.phase === 'mount').length;
expect(mountCount).toBe(2);
});
it('should correctly count update renders', () => {
ReactProfiler.onRender('Component', 'mount', 50, 45, 1000, 1050);
ReactProfiler.onRender('Component', 'update', 25, 20, 2000, 2025);
ReactProfiler.onRender('Component', 'update', 30, 25, 3000, 3030);
const metrics = ReactProfiler.getMetrics('Component');
const updateCount = metrics.filter((m) => m.phase === 'update').length;
expect(updateCount).toBe(2);
});
});
describe('timing data', () => {
it('should track actual duration correctly', () => {
ReactProfiler.onRender('Post', 'mount', 75.5, 70.2, 1000, 1075.5);
const metrics = ReactProfiler.getMetrics('Post');
expect(metrics[0].actualDuration).toBe(75.5);
});
it('should track base duration correctly', () => {
ReactProfiler.onRender('Post', 'mount', 75.5, 70.2, 1000, 1075.5);
const metrics = ReactProfiler.getMetrics('Post');
expect(metrics[0].baseDuration).toBe(70.2);
});
it('should track start and commit times correctly', () => {
ReactProfiler.onRender('Post', 'mount', 50, 45, 12345, 12395);
const metrics = ReactProfiler.getMetrics('Post');
expect(metrics[0].startTime).toBe(12345);
expect(metrics[0].commitTime).toBe(12395);
});
});
});
});