nexo/apps/mattermost/app/utils/performance
2026-05-28 05:59:35 +09:00
..
observable_profiler.test.ts refactor: rename apps/client to apps/flutter-test and add mattermost/push-proxy apps 2026-05-28 05:59:35 +09:00
observable_profiler.ts refactor: rename apps/client to apps/flutter-test and add mattermost/push-proxy apps 2026-05-28 05:59:35 +09:00
post_list_performance.test.ts refactor: rename apps/client to apps/flutter-test and add mattermost/push-proxy apps 2026-05-28 05:59:35 +09:00
post_list_performance.ts refactor: rename apps/client to apps/flutter-test and add mattermost/push-proxy apps 2026-05-28 05:59:35 +09:00
react_profiler.test.ts refactor: rename apps/client to apps/flutter-test and add mattermost/push-proxy apps 2026-05-28 05:59:35 +09:00
react_profiler.ts refactor: rename apps/client to apps/flutter-test and add mattermost/push-proxy apps 2026-05-28 05:59:35 +09:00
README.md refactor: rename apps/client to apps/flutter-test and add mattermost/push-proxy apps 2026-05-28 05:59:35 +09:00

Performance Profiling Utilities

This directory contains three specialized performance profiling utilities for measuring and analyzing performance in the Mattermost Mobile app. These tools are designed to help identify bottlenecks and optimize rendering performance.

Table of Contents


Configuration

All profilers are disabled by default and only work in development mode (__DEV__). Enable them in assets/base/config.json:

{
  "EnableObservableProfiling": true,
  "EnablePostListPerformance": true
}

Observable Profiler

Profiles WatermelonDB observable resolution timing to understand which observables are slow and whether they resolve in parallel or serially.

Import

import observableProfiler from '@utils/performance/observable_profiler';

Usage

Wrap Observables

Wrap observables returned by WatermelonDB queries to track their timing:

import {withObservables} from '@nozbe/watermelondb/react';
import observableProfiler from '@utils/performance/observable_profiler';

const enhance = withObservables(['postId'], ({postId, database}) => {
    const post = database.get('Post').findAndObserve(postId);
    const user = post.observe().pipe(
        switchMap((p) => p.author.observe())
    );

    return {
        post: observableProfiler.profile(post, 'post', 'Post', postId),
        user: observableProfiler.profile(user, 'user', 'Post', postId),
    };
});

Analyze Component Performance

After rendering, analyze how observables resolved:

// Analyze a specific component instance
observableProfiler.analyzeComponent('Post', postId);

// Analyze all components of a specific type
observableProfiler.analyzeComponentsByPrefix('Post');
observableProfiler.analyzeComponentsByPrefix('Channel');
observableProfiler.analyzeComponentsByPrefix('User');

// Print emission counts
observableProfiler.printEmissionSummary('Post', postId);

Manual Control

// Enable/disable profiling
observableProfiler.setEnabled(true);

// Check if enabled
const isEnabled = observableProfiler.isEnabled();

// Clear collected data
observableProfiler.clear();

// Get timing data
const timings = observableProfiler.getTimings('Post', postId);
const allTimings = observableProfiler.getAllTimings();

// Conditionally profile (checks __DEV__ and enabled state)
observableProfiler.profileIfEnabled(observable$, 'name', 'Component', 'id');

Output

The profiler identifies three patterns:

  1. WATERFALL: Observables resolving serially (avgGap > 5ms)

    • Indicates blocking database queries
    • Consider batching queries or optimizing schema
  2. BATCH PATTERN: Mostly parallel with occasional blocks (avgGap ≤ 5ms, maxGap > 20ms)

    • Some observables blocked, then released together
    • Check for database locks or synchronous operations
  3. PARALLEL: Observables resolving concurrently (avgGap ≤ 5ms, maxGap ≤ 20ms)

    • Optimal performance
    • Queries executing efficiently

Example Output

═══════════════════════════════════════════════════════
  OBSERVABLE ANALYSIS: Post:abc123
═══════════════════════════════════════════════════════
  Total Observables: 5
  Emitted: 5 | Pending: 0
  Time Span: 45.32ms (from first subscribe to last emit)
───────────────────────────────────────────────────────
  EMISSION TIMELINE (chronological):
      10.23ms: post
      12.45ms: user (+2.22ms gap)
      15.67ms: channel (+3.22ms gap)
      18.90ms: reactions (+3.23ms gap)
      45.32ms: customEmojis (+26.42ms gap)
───────────────────────────────────────────────────────
  SLOWEST OBSERVABLES:
      45.32ms: customEmojis
      18.90ms: reactions
      15.67ms: channel
───────────────────────────────────────────────────────
  RESOLUTION PATTERN:
    Average gap between emissions: 8.77ms
    Max gap between emissions: 26.42ms
    ⚠️  WATERFALL DETECTED: Observables resolving serially
═══════════════════════════════════════════════════════

When analyzing by prefix:

═══════════════════════════════════════════════════════
  POST OBSERVABLE ANALYSIS (50 components)
═══════════════════════════════════════════════════════
  OBSERVABLE PERFORMANCE SUMMARY:
  (Averaged across all Post components)

  Observable Name                    Count   Avg       P50       Max
  ──────────────────────────────────────────────────────────────────
  customEmojis                       50      42.3ms    38.5ms    89.2ms
  reactions                          50      18.4ms    16.2ms    34.5ms
  channel                            48      15.2ms    14.1ms    28.9ms
  user                               50      12.7ms    11.3ms    25.1ms
  post                               50      10.5ms    9.8ms     22.3ms

  🎯 TOP 3 OPTIMIZATION TARGETS:
     1. customEmojis: 42.3ms avg (2115ms total impact)
     2. reactions: 18.4ms avg (920ms total impact)
     3. channel: 15.2ms avg (730ms total impact)
═══════════════════════════════════════════════════════

Post List Performance Tracker

Dedicated performance monitoring for post list optimization. Tracks initial render timing and markdown parsing performance.

Import

import PostListPerformance from '@utils/performance/post_list_performance';

Usage

Track Initial Render

// In your post list component
useEffect(() => {
    // Start tracking when mounting
    PostListPerformance.startInitialRender(channelId, threadId, posts.length);

    return () => {
        // End tracking when first render completes
        PostListPerformance.endInitialRender(
            channelId,
            threadId,
            renderedCount,
            viewableCount
        );
    };
}, []);

Track Markdown Parsing

// In your markdown component
const parseMarkdown = (text: string, postId: string) => {
    const startTime = performance.now();
    const parsed = markdownParser.parse(text);
    const duration = performance.now() - startTime;

    PostListPerformance.trackMarkdownParse(
        channelId,
        threadId,
        postId,
        text.length,
        duration
    );

    return parsed;
};

View Results

// Print comprehensive summary
PostListPerformance.printSummary(channelId, threadId);

// Clear metrics
PostListPerformance.clearMetrics(channelId, threadId);

// Manual control
PostListPerformance.setEnabled(true);
PostListPerformance.isEnabled();

Example Output

═══════════════════════════════════════════════════════
  POST LIST PERFORMANCE REPORT
═══════════════════════════════════════════════════════
  Channel/Thread: channel123
  Architecture:   new
  Platform:       ios
  Post Count:     50
───────────────────────────────────────────────────────
  INITIAL RENDER
    Duration:      245.67ms
    TTI:           0.246s
    Total Posts:   50
    Rendered:      15
    Viewable:      8
───────────────────────────────────────────────────────
  MARKDOWN PARSING
    Parse Count:  50
    Total Time:   123.45ms
    Avg per Post: 2.47ms
    P50:          1.89ms
    P95:          5.23ms
    P99:          8.91ms
═══════════════════════════════════════════════════════

React Profiler

Tracks React component render timing to identify rendering bottlenecks and memoization opportunities.

Import

import ReactProfiler from '@utils/performance/react_profiler';

Usage

Wrap Components with React.Profiler

import {Profiler} from 'react';
import ReactProfiler from '@utils/performance/react_profiler';

const PostList = ({posts}) => {
    return (
        <Profiler id="PostList" onRender={ReactProfiler.onRender}>
            {posts.map((post) => (
                <Profiler key={post.id} id={`Post-${post.id}`} onRender={ReactProfiler.onRender}>
                    <Post post={post} />
                </Profiler>
            ))}
        </Profiler>
    );
};

Analyze Results

// Get metrics for specific component
const metrics = ReactProfiler.getMetrics('PostList');

// Get all metrics
const allMetrics = ReactProfiler.getAllMetrics();

// Print summary report
ReactProfiler.printSummary();

// Clear metrics
ReactProfiler.clearMetrics(); // Clear all
ReactProfiler.clearMetrics('PostList'); // Clear specific component

// Manual control
ReactProfiler.setEnabled(true);
ReactProfiler.isEnabled();

Example Output

═══════════════════════════════════════════════════════
  REACT PROFILER REPORT
═══════════════════════════════════════════════════════
  TOP SLOWEST COMPONENTS:
───────────────────────────────────────────────────────
  1. PostList
     Total Time:    523.45ms
     Avg per Render: 87.24ms
     Renders:       6 (1 mount, 5 update)
     Base Duration: 445.32ms
     ⚠️  Memoization opportunity: 18% slower than optimized

  2. MarkdownText
     Total Time:    234.56ms
     Avg per Render: 11.73ms
     Renders:       20 (20 mount, 0 update)
     Base Duration: 220.15ms

  3. PostHeader
     Total Time:    156.78ms
     Avg per Render: 7.84ms
     Renders:       20 (20 mount, 0 update)
     Base Duration: 148.90ms
───────────────────────────────────────────────────────
  TOTAL PROFILED TIME: 914.79ms
═══════════════════════════════════════════════════════

Understanding the Metrics

  • actualDuration: Actual time spent rendering (includes effects)
  • baseDuration: Estimated time without memoization
  • mount: Initial render of component
  • update: Re-render due to state/props change
  • nested-update: Update triggered by parent update

Memoization Warning: Shows when actualDuration is significantly higher than baseDuration, indicating potential optimization opportunities with React.memo, useMemo, or useCallback.


Best Practices

When to Use Each Profiler

  • Observable Profiler: Use when debugging slow database queries or understanding data flow patterns
  • Post List Performance: Use for baseline measurements and optimization tracking
  • React Profiler: Use when components re-render frequently or render times are slow

Performance Tips

  1. Always disable in production: These profilers add overhead
  2. Profile in realistic conditions: Use production-like data volumes
  3. Profile on actual devices: Simulators/emulators don't reflect real performance
  4. Clear metrics between tests: Avoid contaminating measurements
  5. Focus on patterns, not individual measurements: Look for trends across multiple renders

Common Issues

Observable Profiler shows WATERFALL pattern:

  • Check for sequential queries that could be batched
  • Review database schema for missing indexes
  • Consider using Q.experimentalJoinTables for related data

Post List shows high markdown parse times:

  • Consider memoizing parsed markdown
  • Evaluate if simpler markdown subset is sufficient
  • Profile on actual message content (links, code blocks, etc.)

React Profiler shows frequent re-renders:

  • Wrap expensive components with React.memo
  • Memoize callbacks with useCallback
  • Memoize computed values with useMemo
  • Check if props are being recreated unnecessarily

Example Workflow

// 1. Enable profilers
observableProfiler.setEnabled(true);
PostListPerformance.setEnabled(true);
ReactProfiler.setEnabled(true);

// 2. Navigate to post list
// ... user navigates to channel ...

// 3. Wait for initial render
await new Promise(resolve => setTimeout(resolve, 1000));

// 4. Analyze results
PostListPerformance.printSummary(channelId);
observableProfiler.analyzeComponentsByPrefix('Post');
ReactProfiler.printSummary();

// 5. Clear and repeat
observableProfiler.clear();
PostListPerformance.clearMetrics(channelId);
ReactProfiler.clearMetrics();

Architecture Detection

The Post List Performance Tracker automatically detects whether the app is running on the New or Old React Native architecture:

  • New Architecture: TurboModules enabled (React Native 0.76+)
  • Old Architecture: Legacy bridge

This is useful for comparing performance between architectures during migration.


Development Notes

  • All profilers check __DEV__ to ensure they don't run in production
  • Config flags provide additional control beyond __DEV__
  • Profilers use react-native-performance for accurate timing
  • Observable Profiler uses RxJS operators to intercept emissions
  • React Profiler uses React's built-in Profiler API

See Also