// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import performance from 'react-native-performance'; import {Subject} from 'rxjs'; import LocalConfig from '@assets/config.json'; import * as log from '@utils/log'; import observableProfiler from '@utils/performance/observable_profiler'; // Mock dependencies jest.mock('react-native-performance', () => ({ now: jest.fn(), })); jest.mock('@assets/config.json', () => ({ EnableObserbvableProfiling: false, })); const mockPerformance = performance as jest.Mocked; const mockLogDebug = log.logDebug as jest.Mock; const mockLogInfo = log.logInfo as jest.Mock; describe('ObservableProfiler', () => { beforeEach(() => { jest.clearAllMocks(); mockPerformance.now.mockReturnValue(0); observableProfiler.clear(); observableProfiler.setEnabled(false); }); describe('when profiling is disabled', () => { beforeEach(() => { LocalConfig.EnableObservableProfiling = false; observableProfiler.setEnabled(false); }); it('should not wrap observables when disabled', (done) => { const source$ = new Subject(); const wrapped$ = observableProfiler.profile(source$, 'testObs', 'TestComponent', 'comp123'); // Should return the same observable without wrapping expect(wrapped$).toBe(source$); wrapped$.subscribe({ next: (value) => { expect(value).toBe(42); expect(mockLogInfo).not.toHaveBeenCalled(); done(); }, }); source$.next(42); }); it('should return false for isEnabled', () => { expect(observableProfiler.isEnabled()).toBe(false); }); it('should not track timing data', () => { const source$ = new Subject(); observableProfiler.profile(source$, 'testObs', 'TestComponent', 'comp123'); source$.next(42); const timings = observableProfiler.getTimings('TestComponent', 'comp123'); expect(timings).toBeUndefined(); }); }); describe('when profiling is enabled', () => { beforeEach(() => { LocalConfig.EnableObservableProfiling = true; (global as any).__DEV__ = true; observableProfiler.setEnabled(true); }); afterEach(() => { observableProfiler.clear(); }); it('should wrap observables and track subscription time', (done) => { mockPerformance.now.mockReturnValue(100); const source$ = new Subject(); const wrapped$ = observableProfiler.profile(source$, 'user', 'Post', 'post123'); wrapped$.subscribe({ next: () => { const timings = observableProfiler.getTimings('Post', 'post123'); expect(timings).toBeDefined(); expect(timings).toHaveLength(1); expect(timings![0].subscribeTime).toBe(100); expect(timings![0].observableName).toBe('user'); expect(mockLogDebug).toHaveBeenCalledWith( '[ObservableProfiler] Subscribed to user for Post:post123', ); done(); }, }); source$.next('value'); }); it('should track first emission timing', (done) => { mockPerformance.now. mockReturnValueOnce(100). // subscribe time mockReturnValueOnce(150); // first emit time const source$ = new Subject(); const wrapped$ = observableProfiler.profile(source$, 'channel', 'Post', 'post456'); wrapped$.subscribe({ next: () => { const timings = observableProfiler.getTimings('Post', 'post456'); expect(timings![0].firstEmitTime).toBe(150); expect(timings![0].timeToFirstEmit).toBe(50); expect(timings![0].emitCount).toBe(1); expect(mockLogInfo).toHaveBeenCalledWith( '[ObservableProfiler] channel (Post:post456) FIRST EMIT after 50.00ms', ); done(); }, }); source$.next(1); }); it('should track multiple emissions', (done) => { mockPerformance.now. mockReturnValueOnce(100). // subscribe mockReturnValueOnce(150). // first emit mockReturnValueOnce(200); // second emit const source$ = new Subject(); const wrapped$ = observableProfiler.profile(source$, 'post', 'Post', 'post789'); let emitCount = 0; wrapped$.subscribe({ next: () => { emitCount++; if (emitCount === 2) { const timings = observableProfiler.getTimings('Post', 'post789'); expect(timings![0].emitCount).toBe(2); expect(timings![0].lastEmitTime).toBe(200); expect(mockLogInfo).toHaveBeenCalledWith( expect.stringContaining('EMIT #2 at 100.00ms (+50.00ms)'), ); done(); } }, }); source$.next('first'); source$.next('second'); }); it('should track errors', (done) => { const source$ = new Subject(); const wrapped$ = observableProfiler.profile(source$, 'errorObs', 'Component', 'comp1'); wrapped$.subscribe({ error: (err) => { expect(err.message).toBe('test error'); expect(mockLogDebug).toHaveBeenCalledWith( '[ObservableProfiler] errorObs ERROR:', err, ); done(); }, }); source$.error(new Error('test error')); }); it('should track completion', (done) => { const source$ = new Subject(); const wrapped$ = observableProfiler.profile(source$, 'completeObs', 'Component', 'comp2'); wrapped$.subscribe({ complete: () => { expect(mockLogDebug).toHaveBeenCalledWith( '[ObservableProfiler] completeObs COMPLETED', ); done(); }, }); source$.complete(); }); it('should track multiple observables for same component', (done) => { mockPerformance.now.mockReturnValue(100); const source1$ = new Subject(); const source2$ = new Subject(); const wrapped1$ = observableProfiler.profile(source1$, 'obs1', 'Post', 'postABC'); const wrapped2$ = observableProfiler.profile(source2$, 'obs2', 'Post', 'postABC'); let doneCount = 0; const checkDone = () => { doneCount++; if (doneCount === 2) { const timings = observableProfiler.getTimings('Post', 'postABC'); expect(timings).toHaveLength(2); expect(timings![0].observableName).toBe('obs1'); expect(timings![1].observableName).toBe('obs2'); done(); } }; wrapped1$.subscribe({next: checkDone}); wrapped2$.subscribe({next: checkDone}); source1$.next(1); source2$.next('a'); }); it('should return all timings via getAllTimings', (done) => { const source1$ = new Subject(); const source2$ = new Subject(); const wrapped1$ = observableProfiler.profile(source1$, 'obs1', 'Component1', 'id1'); const wrapped2$ = observableProfiler.profile(source2$, 'obs2', 'Component2', 'id2'); let doneCount = 0; const checkDone = () => { doneCount++; if (doneCount === 2) { const allTimings = observableProfiler.getAllTimings(); expect(allTimings.size).toBe(2); expect(allTimings.has('Component1:id1')).toBe(true); expect(allTimings.has('Component2:id2')).toBe(true); done(); } }; wrapped1$.subscribe({next: checkDone}); wrapped2$.subscribe({next: checkDone}); source1$.next(1); source2$.next(2); }); describe('analyzeComponent', () => { it('should handle no timing data', () => { observableProfiler.analyzeComponent('NoComponent', 'noId'); expect(mockLogInfo).toHaveBeenCalledWith( '[ObservableProfiler] No timing data for NoComponent:noId', ); }); it('should handle no emitted observables', (done) => { const source$ = new Subject(); observableProfiler.profile(source$, 'obs', 'TestComp', 'testId'); // Subscribe but don't emit source$.subscribe({ next: () => { // This won't be called }, }); setTimeout(() => { observableProfiler.analyzeComponent('TestComp', 'testId'); expect(mockLogInfo).toHaveBeenCalledWith(' No observables have emitted yet'); done(); }, 10); }); it('should analyze component with emitted observables', (done) => { mockPerformance.now. mockReturnValueOnce(100). // subscribe obs1 mockReturnValueOnce(110). // subscribe obs2 mockReturnValueOnce(120). // emit obs1 mockReturnValueOnce(150); // emit obs2 const source1$ = new Subject(); const source2$ = new Subject(); const wrapped1$ = observableProfiler.profile(source1$, 'fastObs', 'AnalyzeTest', 'analyze1'); const wrapped2$ = observableProfiler.profile(source2$, 'slowObs', 'AnalyzeTest', 'analyze1'); let emitCount = 0; const checkDone = () => { emitCount++; if (emitCount === 2) { observableProfiler.analyzeComponent('AnalyzeTest', 'analyze1'); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringContaining('OBSERVABLE ANALYSIS')); expect(mockLogInfo).toHaveBeenCalledWith(' Total Observables: 2'); expect(mockLogInfo).toHaveBeenCalledWith(' Emitted: 2 | Pending: 0'); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringContaining('EMISSION TIMELINE')); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringContaining('SLOWEST OBSERVABLES')); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringContaining('RESOLUTION PATTERN')); done(); } }; wrapped1$.subscribe({next: checkDone}); wrapped2$.subscribe({next: checkDone}); source1$.next(1); source2$.next(2); }); it('should detect waterfall pattern', (done) => { // Create observables that resolve with large gaps (>5ms avg) mockPerformance.now. mockReturnValueOnce(0). // subscribe obs1 mockReturnValueOnce(0). // subscribe obs2 mockReturnValueOnce(10). // emit obs1 mockReturnValueOnce(20); // emit obs2 (10ms gap) const source1$ = new Subject(); const source2$ = new Subject(); const wrapped1$ = observableProfiler.profile(source1$, 'obs1', 'Waterfall', 'w1'); const wrapped2$ = observableProfiler.profile(source2$, 'obs2', 'Waterfall', 'w1'); let emitCount = 0; const checkDone = () => { emitCount++; if (emitCount === 2) { observableProfiler.analyzeComponent('Waterfall', 'w1'); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringContaining('WATERFALL DETECTED')); done(); } }; wrapped1$.subscribe({next: checkDone}); wrapped2$.subscribe({next: checkDone}); source1$.next(1); source2$.next(2); }); it('should detect batch pattern', (done) => { // Batch pattern: avgGap <= 5ms but maxGap > 20ms // Need many observables with small gaps plus one large gap // gaps: [1, 1, 1, 1, 1, 1, 1, 1, 1, 26] -> avgGap = (9*1 + 26)/10 = 3.5ms < 5ms, maxGap = 26ms > 20ms mockPerformance.now. mockReturnValueOnce(0). // subscribe all mockReturnValueOnce(0). mockReturnValueOnce(0). mockReturnValueOnce(0). mockReturnValueOnce(0). mockReturnValueOnce(0). mockReturnValueOnce(0). mockReturnValueOnce(0). mockReturnValueOnce(0). mockReturnValueOnce(0). mockReturnValueOnce(0). mockReturnValueOnce(10). // emit obs1 mockReturnValueOnce(11). // emit obs2 (1ms gap) mockReturnValueOnce(12). // emit obs3 (1ms gap) mockReturnValueOnce(13). // emit obs4 (1ms gap) mockReturnValueOnce(14). // emit obs5 (1ms gap) mockReturnValueOnce(15). // emit obs6 (1ms gap) mockReturnValueOnce(16). // emit obs7 (1ms gap) mockReturnValueOnce(17). // emit obs8 (1ms gap) mockReturnValueOnce(18). // emit obs9 (1ms gap) mockReturnValueOnce(19). // emit obs10 (1ms gap) mockReturnValueOnce(45); // emit obs11 (26ms gap) const sources$ = Array.from({length: 11}, () => new Subject()); const wrappeds$ = sources$.map((s, i) => observableProfiler.profile(s, `obs${i + 1}`, 'Batch', 'b1')); let emitCount = 0; const checkDone = () => { emitCount++; if (emitCount === 11) { observableProfiler.analyzeComponent('Batch', 'b1'); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringContaining('BATCH PATTERN')); done(); } }; wrappeds$.forEach((w) => w.subscribe({next: checkDone})); sources$.forEach((s, i) => s.next(i + 1)); }); it('should detect parallel pattern', (done) => { // Create observables with small gaps (<5ms avg) mockPerformance.now. mockReturnValueOnce(0). // subscribe obs1 mockReturnValueOnce(0). // subscribe obs2 mockReturnValueOnce(10). // emit obs1 mockReturnValueOnce(12); // emit obs2 (2ms gap) const source1$ = new Subject(); const source2$ = new Subject(); const wrapped1$ = observableProfiler.profile(source1$, 'obs1', 'Parallel', 'p1'); const wrapped2$ = observableProfiler.profile(source2$, 'obs2', 'Parallel', 'p1'); let emitCount = 0; const checkDone = () => { emitCount++; if (emitCount === 2) { observableProfiler.analyzeComponent('Parallel', 'p1'); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringContaining('PARALLEL')); done(); } }; wrapped1$.subscribe({next: checkDone}); wrapped2$.subscribe({next: checkDone}); source1$.next(1); source2$.next(2); }); }); describe('analyzeComponentsByPrefix', () => { it('should handle no component data', () => { observableProfiler.analyzeComponentsByPrefix('Post'); expect(mockLogInfo).toHaveBeenCalledWith( '[ObservableProfiler] No Post timing data available', ); }); it('should analyze all components and aggregate statistics', (done) => { mockPerformance.now. mockReturnValueOnce(0). // post1 obs1 subscribe mockReturnValueOnce(0). // post2 obs1 subscribe mockReturnValueOnce(10). // post1 obs1 emit (10ms) mockReturnValueOnce(20); // post2 obs1 emit (20ms) const source1$ = new Subject(); const source2$ = new Subject(); const wrapped1$ = observableProfiler.profile(source1$, 'userObs', 'Post', 'post1'); const wrapped2$ = observableProfiler.profile(source2$, 'userObs', 'Post', 'post2'); let emitCount = 0; const checkDone = () => { emitCount++; if (emitCount === 2) { observableProfiler.analyzeComponentsByPrefix('Post'); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringContaining('POST OBSERVABLE ANALYSIS (2 components)')); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringContaining('OBSERVABLE PERFORMANCE SUMMARY')); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringContaining('TOP 3 OPTIMIZATION TARGETS')); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringMatching(/userObs.*15\.0ms avg/)); done(); } }; wrapped1$.subscribe({next: checkDone}); wrapped2$.subscribe({next: checkDone}); source1$.next(1); source2$.next(2); }); it('should calculate percentiles correctly and work with different prefixes', (done) => { mockPerformance.now. mockReturnValueOnce(0). // channel1 subscribe mockReturnValueOnce(0). // channel2 subscribe mockReturnValueOnce(0). // channel3 subscribe mockReturnValueOnce(10). // channel1 emit (10ms) mockReturnValueOnce(20). // channel2 emit (20ms) mockReturnValueOnce(30); // channel3 emit (30ms) const source1$ = new Subject(); const source2$ = new Subject(); const source3$ = new Subject(); const wrapped1$ = observableProfiler.profile(source1$, 'posts', 'Channel', 'c1'); const wrapped2$ = observableProfiler.profile(source2$, 'posts', 'Channel', 'c2'); const wrapped3$ = observableProfiler.profile(source3$, 'posts', 'Channel', 'c3'); let emitCount = 0; const checkDone = () => { emitCount++; if (emitCount === 3) { observableProfiler.analyzeComponentsByPrefix('Channel'); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringContaining('CHANNEL OBSERVABLE ANALYSIS')); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringMatching(/posts.*20\.0ms.*20\.0ms/)); done(); } }; wrapped1$.subscribe({next: checkDone}); wrapped2$.subscribe({next: checkDone}); wrapped3$.subscribe({next: checkDone}); source1$.next(1); source2$.next(2); source3$.next(3); }); }); describe('printEmissionSummary', () => { it('should handle no timing data', () => { observableProfiler.printEmissionSummary('NoComponent', 'noId'); expect(mockLogInfo).toHaveBeenCalledWith( '[ObservableProfiler] No timing data for NoComponent:noId', ); }); it('should print emission summary sorted by count', (done) => { mockPerformance.now. mockReturnValueOnce(0). // subscribe obs1 mockReturnValueOnce(0). // subscribe obs2 mockReturnValueOnce(10). // emit obs1 #1 mockReturnValueOnce(15). // emit obs1 #2 mockReturnValueOnce(20); // emit obs2 #1 const source1$ = new Subject(); const source2$ = new Subject(); const wrapped1$ = observableProfiler.profile(source1$, 'frequentObs', 'Summary', 's1'); const wrapped2$ = observableProfiler.profile(source2$, 'rareObs', 'Summary', 's1'); let emitCount = 0; const checkDone = () => { emitCount++; if (emitCount === 3) { observableProfiler.printEmissionSummary('Summary', 's1'); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringContaining('OBSERVABLE EMISSION COUNTS')); expect(mockLogInfo).toHaveBeenCalledWith(expect.stringContaining('TOTAL EMISSIONS: 3')); done(); } }; const sub1 = wrapped1$.subscribe({next: checkDone}); const sub2 = wrapped2$.subscribe({next: checkDone}); source1$.next(1); source1$.next(2); source2$.next(1); sub1.unsubscribe(); sub2.unsubscribe(); }); }); describe('clear', () => { it('should clear all timing data', (done) => { const source$ = new Subject(); const wrapped$ = observableProfiler.profile(source$, 'obs', 'Clear', 'c1'); wrapped$.subscribe({ next: () => { expect(observableProfiler.getTimings('Clear', 'c1')).toBeDefined(); observableProfiler.clear(); expect(observableProfiler.getTimings('Clear', 'c1')).toBeUndefined(); done(); }, }); source$.next(1); }); }); describe('setEnabled', () => { it('should disable profiling when set to false', (done) => { const source$ = new Subject(); observableProfiler.setEnabled(false); const wrapped$ = observableProfiler.profile(source$, 'obs', 'Disabled', 'd1'); expect(wrapped$).toBe(source$); wrapped$.subscribe({ next: () => { expect(observableProfiler.getTimings('Disabled', 'd1')).toBeUndefined(); done(); }, }); source$.next(1); }); it('should enable profiling when set to true', (done) => { observableProfiler.setEnabled(true); const source$ = new Subject(); const wrapped$ = observableProfiler.profile(source$, 'obs', 'Enabled', 'e1'); wrapped$.subscribe({ next: () => { expect(observableProfiler.getTimings('Enabled', 'e1')).toBeDefined(); done(); }, }); source$.next(1); }); }); describe('profileIfEnabled', () => { it('should profile when enabled', (done) => { observableProfiler.setEnabled(true); const source$ = new Subject(); const wrapped$ = observableProfiler.profileIfEnabled(source$, 'obs', 'Test', 't1'); wrapped$.subscribe({ next: () => { expect(observableProfiler.getTimings('Test', 't1')).toBeDefined(); done(); }, }); source$.next(1); }); it('should not profile when disabled', (done) => { observableProfiler.setEnabled(false); const source$ = new Subject(); const wrapped$ = observableProfiler.profileIfEnabled(source$, 'obs', 'Test', 't2'); expect(wrapped$).toBe(source$); wrapped$.subscribe({ next: () => { expect(observableProfiler.getTimings('Test', 't2')).toBeUndefined(); done(); }, }); source$.next(1); }); }); }); });