UNPKG

react-native-debug-toolkit

Version:

A simple yet powerful debugging toolkit for React Native with a convenient floating UI for development

390 lines (323 loc) 11.4 kB
// import SubViewPerformance from '../views/SubViewPerformance'; import performance, { setResourceLoggingEnabled, PerformanceObserver } from 'react-native-performance'; class PerformanceFeature { static instance = null; static MAX_ENTRIES = 100; // Native mark names based on vanilla example static NATIVE_MARKS = { NATIVE_LAUNCH_START: 'nativeLaunchStart', NATIVE_LAUNCH_END: 'nativeLaunchEnd', RUN_JS_BUNDLE_START: 'runJsBundleStart', RUN_JS_BUNDLE_END: 'runJsBundleEnd', CONTENT_APPEARED: 'contentAppeared' }; constructor() { if (PerformanceFeature.instance) { return PerformanceFeature.instance; } this.name = 'performance'; this.label = 'Performance'; this.performanceData = { measures: [], metrics: [], resources: [], nativeMarks: [] }; this.observers = []; this.isSetup = false; this.thresholds = new Map(); // Performance thresholds for alerts this.timeOrigin = performance.timeOrigin; this.resourceLoggingEnabled = false; PerformanceFeature.instance = this; } setup(options = {}) { if (this.isSetup) { return this; } const { enableResourceLogging = true } = options; // Enable resource logging if specified if (enableResourceLogging) { setResourceLoggingEnabled(true); this.resourceLoggingEnabled = true; } // Mark when the feature is initialized performance.mark('DebugToolkit.Init'); // Setup observers for different entry types this._setupObservers(); // Measure app launch metrics this._setupAppLaunchMeasurements(); this.isSetup = true; return this; } _setupObservers() { // Use the same pattern as in the vanilla example App.tsx const setupObserver = (type, dataKey) => { try { const observer = new PerformanceObserver((list) => { // Get entries and sort them by startTime as in the example const entries = list.getEntries().sort((a, b) => a.startTime - b.startTime); // Store entries this._addEntries(dataKey, entries); // Check thresholds for measures if (type === 'measure') { this._checkThresholds(entries); } }); // Observe with buffered set to true to get existing entries observer.observe({ type, buffered: true }); this.observers.push(observer); } catch (e) { console.error(`Failed to setup PerformanceObserver for type "${type}":`, e); } }; // Setup observers for all entry types setupObserver('mark', 'marks'); setupObserver('measure', 'measures'); setupObserver('metric', 'metrics'); setupObserver('resource', 'resources'); setupObserver('react-native-mark', 'nativeMarks'); } _setupAppLaunchMeasurements() { // Create a one-time observer for app launch measurements try { const observer = new PerformanceObserver((list, obs) => { const entries = list.getEntries(); if (entries.some(entry => entry.name === PerformanceFeature.NATIVE_MARKS.RUN_JS_BUNDLE_END)) { this._createLaunchMeasurements(); obs.disconnect(); } }); observer.observe({ type: 'react-native-mark', buffered: true }); } catch (e) { console.error('Failed to setup app launch measurements:', e); } } _createLaunchMeasurements() { const { NATIVE_LAUNCH_START, NATIVE_LAUNCH_END, RUN_JS_BUNDLE_START, RUN_JS_BUNDLE_END, CONTENT_APPEARED } = PerformanceFeature.NATIVE_MARKS; // Measure app launch time (native initialization) if (this.hasMark(NATIVE_LAUNCH_START) && this.hasMark(NATIVE_LAUNCH_END)) { this.measure('nativeLaunch', NATIVE_LAUNCH_START, NATIVE_LAUNCH_END); } // Measure JS bundle execution time if (this.hasMark(RUN_JS_BUNDLE_START) && this.hasMark(RUN_JS_BUNDLE_END)) { this.measure('runJsBundle', RUN_JS_BUNDLE_START, RUN_JS_BUNDLE_END); } // Measure time to first render if content appeared mark exists if (this.hasMark(NATIVE_LAUNCH_START) && this.hasMark(CONTENT_APPEARED)) { this.measure('timeToContent', NATIVE_LAUNCH_START, CONTENT_APPEARED); } } _addEntries(type, entries) { if (!entries || entries.length === 0) return; // Keep only the last MAX_ENTRIES entries this.performanceData[type] = [ ...entries, ...this.performanceData[type] ].slice(0, PerformanceFeature.MAX_ENTRIES); } _checkThresholds(entries) { if (!entries || entries.length === 0 || this.thresholds.size === 0) return; entries.forEach(entry => { const threshold = this.thresholds.get(entry.name); if (threshold && entry.duration > threshold.value) { console.warn(`Performance threshold exceeded: ${entry.name} took ${entry.duration}ms (threshold: ${threshold.value}ms)`); if (threshold.callback) { threshold.callback(entry); } } }); } hasMark(name) { try { return performance.getEntriesByName(name, 'mark').length > 0 || performance.getEntriesByName(name, 'react-native-mark').length > 0; } catch (e) { return false; } } // Convert performance timestamp to unix epoch timestamp // Based on example: Date.now() - performance.timeOrigin + entry.startTime getUnixTimestamp(entry) { if (!entry || typeof entry.startTime !== 'number') return null; return Date.now() - this.timeOrigin + entry.startTime; } // Enable or disable resource logging at runtime setResourceLoggingEnabled(enabled) { if (this.resourceLoggingEnabled !== enabled) { setResourceLoggingEnabled(enabled); this.resourceLoggingEnabled = enabled; } return this; } // Threshold management setThreshold(name, value, callback) { this.thresholds.set(name, { value, callback }); return this; } removeThreshold(name) { this.thresholds.delete(name); return this; } clearThresholds() { this.thresholds.clear(); return this; } // Mark events (following pattern from vanilla example) mark(name, detail = {}) { if (!this.isSetup) return this; if (typeof detail !== 'object') { detail = { value: detail }; } performance.mark(name, { detail }); return this; } measure(name, startMarkOrOptions, endMark, detail = {}) { if (!this.isSetup) return this; try { if (typeof startMarkOrOptions === 'string' && endMark) { performance.measure(name, startMarkOrOptions, endMark, { detail }); } else if (typeof startMarkOrOptions === 'object') { performance.measure(name, startMarkOrOptions); } else { performance.measure(name, { detail }); } } catch (e) { } return this; } // Record custom metrics (following pattern from vanilla example) metric(name, value, detail = {}) { if (!this.isSetup) return this; if (typeof detail !== 'object') { detail = { info: detail }; } performance.metric(name, value, { detail }); return this; } // Measure execution time of a function measureFunction(name, fn, ...args) { if (!this.isSetup) { return fn(...args); } const markName = `${name}_start`; this.mark(markName); try { const result = fn(...args); // Handle promises if (result instanceof Promise) { return result.finally(() => { this.measure(name, markName); }); } // Handle synchronous functions this.measure(name, markName); return result; } catch (e) { this.measure(`${name}_error`, markName, undefined, { error: e.message }); throw e; } } // Get formatted performance data getData() { return this.performanceData; } getTimingData(options = {}) { const { includeMarks = true, includeMeasures = true, filterByName } = options; let data = []; if (includeMarks) { data = [...data, ...this.performanceData.marks, ...this.performanceData.nativeMarks]; } if (includeMeasures) { data = [...data, ...this.performanceData.measures]; } if (filterByName) { if (filterByName instanceof RegExp) { data = data.filter(entry => filterByName.test(entry.name)); } else if (typeof filterByName === 'string') { data = data.filter(entry => entry.name.includes(filterByName)); } } return data.sort((a, b) => a.startTime - b.startTime); } // Get network resource data (similar to the demo pattern) getResourceData() { return this.performanceData.resources.sort((a, b) => a.startTime - b.startTime); } // Trigger a test network request (for demo purposes) generateTestNetworkRequest() { if (!this.isSetup || !this.resourceLoggingEnabled) return this; // Similar to the fetch in the vanilla example fetch('https://jsonplaceholder.typicode.com/todos/1', { cache: 'no-cache' }) .catch(err => console.error('Test network request failed:', err)); return this; } // Clear specific type of entries clear(type) { if (this.performanceData[type]) { this.performanceData[type] = []; } return this; } // Clear all performance data clearAll() { Object.keys(this.performanceData).forEach(key => { this.performanceData[key] = []; }); return this; } cleanup() { // Disconnect all observers this.observers.forEach(observer => { observer.disconnect(); }); this.observers = []; // Disable resource logging if it was enabled if (this.resourceLoggingEnabled) { setResourceLoggingEnabled(false); this.resourceLoggingEnabled = false; } // Clear stored data this.clearAll(); this.clearThresholds(); this.isSetup = false; return this; } } export const createPerformanceFeature = () => { const feature = new PerformanceFeature(); return { name: feature.name, label: feature.label, setup: (options) => feature.setup(options), getData: () => feature.getData(), getTimingData: (options) => feature.getTimingData(options), getResourceData: () => feature.getResourceData(), getUnixTimestamp: (entry) => feature.getUnixTimestamp(entry), hasMark: (name) => feature.hasMark(name), setResourceLoggingEnabled: (enabled) => feature.setResourceLoggingEnabled(enabled), generateTestNetworkRequest: () => feature.generateTestNetworkRequest(), cleanup: () => feature.cleanup(), mark: (name, detail) => feature.mark(name, detail), measure: (name, startMarkOrOptions, endMark, detail) => feature.measure(name, startMarkOrOptions, endMark, detail), metric: (name, value, detail) => feature.metric(name, value, detail), measureFunction: (name, fn, ...args) => feature.measureFunction(name, fn, ...args), clear: (type) => feature.clear(type), clearAll: () => feature.clearAll(), setThreshold: (name, value, callback) => feature.setThreshold(name, value, callback), removeThreshold: (name) => feature.removeThreshold(name), clearThresholds: () => feature.clearThresholds(), }; }; // For backward compatibility const instance = new PerformanceFeature(); export default instance;