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
JavaScript
// 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;