react-native-inapp-inspector 1.1.26 → 1.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/android/build.gradle +1 -0
  2. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.java +34 -0
  3. package/dist/commonjs/components/ErrorBoundary.d.ts +1 -1
  4. package/dist/commonjs/components/ErrorBoundary.js +530 -100
  5. package/dist/commonjs/components/Inspector/AnalyticsTab.js +176 -126
  6. package/dist/commonjs/components/Inspector/BundleTab.d.ts +1 -0
  7. package/dist/commonjs/components/Inspector/BundleTab.js +145 -21
  8. package/dist/commonjs/components/Inspector/CrashTab.js +0 -8
  9. package/dist/commonjs/components/Inspector/NetworkDetail.js +41 -0
  10. package/dist/commonjs/components/Inspector/PerformanceTab.d.ts +5 -0
  11. package/dist/commonjs/components/Inspector/PerformanceTab.js +588 -155
  12. package/dist/commonjs/components/Inspector/SettingsPanel.js +57 -22
  13. package/dist/commonjs/components/Inspector/TabBar.js +13 -1
  14. package/dist/commonjs/components/LogCard.js +75 -34
  15. package/dist/commonjs/components/NetworkIcons.d.ts +112 -102
  16. package/dist/commonjs/constants/version.d.ts +1 -1
  17. package/dist/commonjs/constants/version.js +1 -1
  18. package/dist/commonjs/customHooks/analyticsLogger.d.ts +1 -11
  19. package/dist/commonjs/customHooks/analyticsLogger.js +8 -1
  20. package/dist/commonjs/customHooks/bundleAnalyzer.js +121 -124
  21. package/dist/commonjs/customHooks/networkLogger.d.ts +1 -0
  22. package/dist/commonjs/customHooks/networkLogger.js +16 -0
  23. package/dist/commonjs/customHooks/performanceTracker.d.ts +74 -0
  24. package/dist/commonjs/customHooks/performanceTracker.js +546 -392
  25. package/dist/commonjs/customHooks/reduxLogger.d.ts +1 -0
  26. package/dist/commonjs/customHooks/reduxLogger.js +3 -1
  27. package/dist/commonjs/helpers/index.js +27 -3
  28. package/dist/commonjs/i18n/locales/en.json +225 -4
  29. package/dist/commonjs/index.d.ts +1 -1
  30. package/dist/commonjs/index.js +36 -2
  31. package/dist/commonjs/styles/index.d.ts +3 -1
  32. package/dist/commonjs/styles/index.js +8 -6
  33. package/dist/commonjs/types/interfaces.d.ts +3 -0
  34. package/dist/esm/components/ErrorBoundary.d.ts +1 -1
  35. package/dist/esm/components/ErrorBoundary.js +498 -101
  36. package/dist/esm/components/Inspector/AnalyticsTab.js +177 -127
  37. package/dist/esm/components/Inspector/BundleTab.d.ts +1 -0
  38. package/dist/esm/components/Inspector/BundleTab.js +145 -23
  39. package/dist/esm/components/Inspector/CrashTab.js +0 -8
  40. package/dist/esm/components/Inspector/NetworkDetail.js +41 -0
  41. package/dist/esm/components/Inspector/PerformanceTab.d.ts +5 -0
  42. package/dist/esm/components/Inspector/PerformanceTab.js +591 -158
  43. package/dist/esm/components/Inspector/SettingsPanel.js +57 -22
  44. package/dist/esm/components/Inspector/TabBar.js +13 -1
  45. package/dist/esm/components/LogCard.js +75 -34
  46. package/dist/esm/components/NetworkIcons.d.ts +112 -102
  47. package/dist/esm/constants/version.d.ts +1 -1
  48. package/dist/esm/constants/version.js +1 -1
  49. package/dist/esm/customHooks/analyticsLogger.d.ts +1 -11
  50. package/dist/esm/customHooks/analyticsLogger.js +6 -0
  51. package/dist/esm/customHooks/bundleAnalyzer.js +121 -124
  52. package/dist/esm/customHooks/networkLogger.d.ts +1 -0
  53. package/dist/esm/customHooks/networkLogger.js +16 -0
  54. package/dist/esm/customHooks/performanceTracker.d.ts +74 -0
  55. package/dist/esm/customHooks/performanceTracker.js +531 -391
  56. package/dist/esm/customHooks/reduxLogger.d.ts +1 -0
  57. package/dist/esm/customHooks/reduxLogger.js +1 -0
  58. package/dist/esm/helpers/index.js +27 -3
  59. package/dist/esm/i18n/locales/en.json +225 -4
  60. package/dist/esm/index.d.ts +1 -1
  61. package/dist/esm/index.js +24 -3
  62. package/dist/esm/styles/index.d.ts +3 -1
  63. package/dist/esm/styles/index.js +8 -6
  64. package/dist/esm/types/interfaces.d.ts +3 -0
  65. package/ios/NetworkInspectorModule.h +2 -0
  66. package/ios/NetworkInspectorModule.m +10 -0
  67. package/package.json +1 -1
@@ -1,359 +1,484 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getPerformanceEvents = exports.subscribePerformanceEvents = exports.clearPerformanceEvents = exports.logPerformanceEvent = exports.usePerformanceTracker = exports.INITIAL_EVENTS = void 0;
3
+ exports.getPerformanceEvents = exports.subscribePerformanceEvents = exports.clearPerformanceEvents = exports.logPerformanceEvent = exports.usePerformanceTracker = exports.useComponentProfiler = exports.measureAsync = exports.trackHeavyTask = exports.useNavigationProfiler = exports.trackNavigationTransition = exports.getHermesMemoryStats = exports.trackComponentRender = exports.registerComponentProfile = exports.getRenderProfiles = exports.subscribeRenderProfiles = exports.INITIAL_EVENTS = exports.getInitialPerformanceEvents = exports.getInitialRenderProfiles = exports.generateFixSnippet = void 0;
4
+ exports.getCallerSourceFile = getCallerSourceFile;
4
5
  const AppColors_1 = require("../styles/AppColors");
6
+ const i18n_1 = require("../i18n");
5
7
  const react_1 = require("react");
6
- const INITIAL_RENDER_PROFILES = [
7
- {
8
- id: 'render-1',
9
- name: 'ProductDetailScreen',
10
- type: 'screen',
11
- sourceFile: 'src/screens/ProductDetailScreen.tsx',
12
- renderCount: 48,
13
- wastefulCount: 34,
14
- wastefulPercentage: 70.8,
15
- avgRenderTimeMs: 14.2,
16
- totalRenderTimeMs: 681.6,
17
- lastRenderedAt: Date.now() - 2500,
18
- reasons: [
19
- 'Inline arrow function props passed to children (onAddToCart={() => ...})',
20
- 'Unmemoized Redux selector creating new object reference on every dispatch',
21
- 'Dynamic style object created in render body ({ marginTop: insets.top + 10 })',
22
- ],
23
- fixKeys: [
24
- {
8
+ /**
9
+ * Dynamically generates tailored Before/After performance optimization code snippets
10
+ * based on the target component's real identifier, props, and detected anti-pattern.
11
+ */
12
+ const generateFixSnippet = (fixKey, componentName = 'Component', context) => {
13
+ const comp = componentName || 'Component';
14
+ const prop = context?.propName || 'onPress';
15
+ const action = context?.actionName || 'handlePress';
16
+ const itemHeight = context?.itemHeight || 72;
17
+ switch (fixKey) {
18
+ case 'useCallback':
19
+ return {
25
20
  keyName: 'useCallback',
26
- title: 'Wrap Event Handlers in useCallback',
27
- explanation: 'Inline functions recreate a new memory reference on every render, invalidating React.memo on child components.',
28
- codeSnippet: `// Before:\n<AddToCartButton onPress={() => handleAddToCart(item.id)} />\n\n// ✅ After:\nconst onAddToCart = useCallback(() => {\n handleAddToCart(item.id);\n}, [item.id]);\n<AddToCartButton onPress={onAddToCart} />`,
29
- impact: 'High Impact',
21
+ title: (0, i18n_1.t)('performance.fixUseCallbackTitle'),
22
+ explanation: (0, i18n_1.t)('performance.fixUseCallbackDesc'),
23
+ codeSnippet: `${(0, i18n_1.t)('performance.beforeUseCallback', { comp })}\n<Button ${prop}={() => ${action}(item.id)} />\n\n${(0, i18n_1.t)('performance.afterUseCallback')}\nconst ${action}Memo = useCallback(() => {\n ${action}(item.id);\n}, [item.id]);\n<Button ${prop}={${action}Memo} />`,
24
+ impact: (0, i18n_1.t)('performance.highImpact'),
30
25
  impactColor: AppColors_1.AppColors.pink500,
31
- },
32
- {
26
+ };
27
+ case 'createSelector':
28
+ case 'useMemo':
29
+ return {
33
30
  keyName: 'createSelector',
34
- title: 'Memoize Redux / Zustand Selectors with shallowEqual',
35
- explanation: 'Returning new object or array references inside useSelector forces an automatic re-render on every state dispatch.',
36
- codeSnippet: `// Before:\nconst { items, total } = useSelector(state => ({ items: state.cart.items, total: state.cart.total }));\n\n// ✅ After:\nimport { shallowEqual } from 'react-redux';\nconst { items, total } = useSelector(\n state => ({ items: state.cart.items, total: state.cart.total }),\n shallowEqual\n);`,
37
- impact: 'High Impact',
31
+ title: (0, i18n_1.t)('performance.fixCreateSelectorTitle'),
32
+ explanation: (0, i18n_1.t)('performance.fixCreateSelectorDesc'),
33
+ codeSnippet: `${(0, i18n_1.t)('performance.beforeCreateSelector', { comp })}\nconst { items, total } = useSelector(state => ({\n items: state.cart.items,\n total: state.cart.total,\n}));\n\n${(0, i18n_1.t)('performance.afterCreateSelector')}\nimport { shallowEqual } from 'react-redux';\nconst { items, total } = useSelector(\n state => ({ items: state.cart.items, total: state.cart.total }),\n shallowEqual\n);`,
34
+ impact: (0, i18n_1.t)('performance.highImpact'),
38
35
  impactColor: AppColors_1.AppColors.pink500,
39
- },
40
- {
41
- keyName: 'useMemoStyles',
42
- title: 'Hoist Styles or Use useMemo for Dynamic Dimensions',
43
- explanation: 'Inline style objects create new object identities on every frame pass, causing Yoga Flexbox reconciliation diffs.',
44
- codeSnippet: `// ❌ Before:\n<View style={{ paddingTop: insets.top, backgroundColor: AppColors.white }} />\n\n// ✅ After:\nconst containerStyle = useMemo(() => ({\n paddingTop: insets.top,\n backgroundColor: AppColors.white,\n}), [insets.top]);\n<View style={containerStyle} />`,
45
- impact: 'Medium Impact',
46
- impactColor: AppColors_1.AppColors.purple500,
47
- },
48
- ],
49
- severity: 'critical',
50
- },
51
- {
52
- id: 'render-2',
53
- name: 'HomeFeedFlatList',
54
- type: 'screen',
55
- sourceFile: 'src/screens/HomeScreen.tsx',
56
- renderCount: 36,
57
- wastefulCount: 22,
58
- wastefulPercentage: 61.1,
59
- avgRenderTimeMs: 18.6,
60
- totalRenderTimeMs: 669.6,
61
- lastRenderedAt: Date.now() - 8000,
62
- reasons: [
63
- 'FlatList missing getItemLayout causing async layout measuring passes',
64
- 'renderItem function defined anonymously inside JSX body',
65
- 'List item components not wrapped with React.memo',
66
- ],
67
- fixKeys: [
68
- {
36
+ };
37
+ case 'memo':
38
+ case 'React.memo':
39
+ return {
40
+ keyName: 'memo',
41
+ title: (0, i18n_1.t)('performance.fixReactMemoTitle'),
42
+ explanation: (0, i18n_1.t)('performance.fixReactMemoDesc'),
43
+ codeSnippet: `${(0, i18n_1.t)('performance.beforeMemo', { comp })}\nexport const ${comp} = (props: ${comp}Props) => {\n return <View>...</View>;\n};\n\n${(0, i18n_1.t)('performance.afterMemo')}\nexport const ${comp} = React.memo((props: ${comp}Props) => {\n return <View>...</View>;\n}, (prev, next) => prev.id === next.id && prev.updatedAt === next.updatedAt);`,
44
+ impact: (0, i18n_1.t)('performance.highImpact'),
45
+ impactColor: AppColors_1.AppColors.pink500,
46
+ };
47
+ case 'getItemLayout':
48
+ return {
69
49
  keyName: 'getItemLayout',
70
- title: 'Implement getItemLayout for Fixed-Height Items',
71
- explanation: 'Supplying getItemLayout allows FlatList to immediately compute scroll offsets and virtual windows without measuring views asynchronously.',
72
- codeSnippet: `const ITEM_HEIGHT = 80;\nconst getItemLayout = useCallback((data, index) => ({\n length: ITEM_HEIGHT,\n offset: ITEM_HEIGHT * index,\n index,\n}), []);\n\n<FlatList\n data={items}\n getItemLayout={getItemLayout}\n renderItem={renderItem}\n keyExtractor={item => item.id}\n/>`,
73
- impact: 'High Impact',
50
+ title: (0, i18n_1.t)('performance.fixGetItemLayoutTitle'),
51
+ explanation: (0, i18n_1.t)('performance.fixGetItemLayoutDesc'),
52
+ codeSnippet: `${(0, i18n_1.t)('performance.flatListOptimization', { comp })}\nconst ITEM_HEIGHT = ${itemHeight};\nconst getItemLayout = useCallback((_data: any, index: number) => ({\n length: ITEM_HEIGHT,\n offset: ITEM_HEIGHT * index,\n index,\n}), []);\n\n<FlatList\n data={items}\n getItemLayout={getItemLayout}\n renderItem={({item}) => <${comp} item={item} />}\n keyExtractor={item => String(item.id)}\n/>`,
53
+ impact: (0, i18n_1.t)('performance.highImpact'),
74
54
  impactColor: AppColors_1.AppColors.pink500,
75
- },
76
- {
77
- keyName: 'React.memo',
78
- title: 'Wrap List Items in React.memo',
79
- explanation: 'Prevents all 50+ visible list items from re-rendering when parent list state (e.g. scroll position or pagination) updates.',
80
- codeSnippet: `// FeedItem.tsx\nexport const FeedItem = React.memo(({ item, onSelect }: FeedItemProps) => {\n return <View>...</View>;\n}, (prev, next) => prev.item.id === next.item.id && prev.item.updatedAt === next.item.updatedAt);`,
81
- impact: 'High Impact',
55
+ };
56
+ case 'customComparator':
57
+ return {
58
+ keyName: 'customComparator',
59
+ title: (0, i18n_1.t)('performance.fixCustomComparatorTitle'),
60
+ explanation: (0, i18n_1.t)('performance.fixCustomComparatorDesc'),
61
+ codeSnippet: `${(0, i18n_1.t)('performance.customComparator', { comp })}\nexport const ${comp} = React.memo(\n ${comp}Component,\n (prevProps, nextProps) => {\n return prevProps.id === nextProps.id && prevProps.status === nextProps.status;\n }\n);`,
62
+ impact: (0, i18n_1.t)('performance.mediumImpact'),
63
+ impactColor: AppColors_1.AppColors.amber500,
64
+ };
65
+ case 'DebouncedInput':
66
+ return {
67
+ keyName: 'DebouncedInput',
68
+ title: (0, i18n_1.t)('performance.fixDebouncedInputTitle'),
69
+ explanation: (0, i18n_1.t)('performance.fixDebouncedInputDesc'),
70
+ codeSnippet: `${(0, i18n_1.t)('performance.debounceInput', { comp })}\nconst [localText, setLocalText] = useState('');\nconst debouncedSearch = useMemo(\n () => debounce((query: string) => onSearch(query), 250),\n [onSearch]\n);`,
71
+ impact: (0, i18n_1.t)('performance.highImpact'),
82
72
  impactColor: AppColors_1.AppColors.pink500,
83
- },
84
- ],
85
- severity: 'critical',
86
- },
87
- {
88
- id: 'render-3',
89
- name: 'CartSummarySheet',
90
- type: 'modal',
91
- sourceFile: 'src/components/CartSummarySheet.tsx',
92
- renderCount: 24,
93
- wastefulCount: 14,
94
- wastefulPercentage: 58.3,
95
- avgRenderTimeMs: 8.4,
96
- totalRenderTimeMs: 201.6,
97
- lastRenderedAt: Date.now() - 14000,
98
- reasons: [
99
- 'Parent screen re-rendered on keyboard show/hide event',
100
- 'Unstable callback reference passed into checkout button',
101
- ],
102
- fixKeys: [
103
- {
73
+ };
74
+ case 'ComponentSplitting':
75
+ return {
104
76
  keyName: 'ComponentSplitting',
105
- title: 'Isolate Fast-Changing State in Leaf Components',
106
- explanation: 'Move keyboard listeners and modal animation state into self-contained subcomponents so the parent does not re-render.',
107
- codeSnippet: `// ❌ Before: Parent holds keyboardHeight state, re-rendering entire screen\n// After: Use KeyboardStickyView component that encapsulates layout animation`,
108
- impact: 'Medium Impact',
77
+ title: (0, i18n_1.t)('performance.fixComponentSplittingTitle'),
78
+ explanation: (0, i18n_1.t)('performance.fixComponentSplittingDesc'),
79
+ codeSnippet: `${(0, i18n_1.t)('performance.isolateState', { comp })}\nexport const StickyKeyboard = React.memo(KeyboardStickyView);`,
80
+ impact: (0, i18n_1.t)('performance.mediumImpact'),
109
81
  impactColor: AppColors_1.AppColors.purple500,
110
- },
111
- ],
112
- severity: 'warning',
113
- },
114
- {
115
- id: 'render-4',
116
- name: 'SearchFilterHeader',
117
- type: 'component',
118
- sourceFile: 'src/components/SearchFilterHeader.tsx',
119
- renderCount: 29,
120
- wastefulCount: 16,
121
- wastefulPercentage: 55.2,
122
- avgRenderTimeMs: 6.2,
123
- totalRenderTimeMs: 179.8,
124
- lastRenderedAt: Date.now() - 19000,
125
- reasons: [
126
- 'TextInput value state triggers parent re-render on every keystroke without debouncing',
127
- 'Passing unmemoized filter object ({ category, minPrice }) down to child chips',
128
- ],
129
- fixKeys: [
130
- {
131
- keyName: 'DebouncedInput',
132
- title: 'Debounce Search Input or Use Local Controlled State',
133
- explanation: 'Do not propagate keystroke state into global store immediately. Use a 250ms debounce or uncontrolled ref.',
134
- codeSnippet: `const [localText, setLocalText] = useState('');\nconst debouncedSearch = useMemo(\n () => debounce(query => onSearch(query), 250),\n [onSearch]\n);`,
135
- impact: 'High Impact',
82
+ };
83
+ case 'InteractionManager':
84
+ return {
85
+ keyName: 'InteractionManager',
86
+ title: (0, i18n_1.t)('performance.fixInteractionManagerTitle'),
87
+ explanation: (0, i18n_1.t)('performance.fixInteractionManagerDesc'),
88
+ codeSnippet: `${(0, i18n_1.t)('performance.beforeInteractionManager', { comp })}\n\n${(0, i18n_1.t)('performance.afterInteractionManager')}`,
89
+ impact: (0, i18n_1.t)('performance.highImpact'),
136
90
  impactColor: AppColors_1.AppColors.pink500,
137
- },
138
- {
139
- keyName: 'PrimitiveProps',
140
- title: 'Pass Primitive Props Instead of Large Objects',
141
- explanation: 'Passing only categoryId string instead of whole category object prevents re-renders when other category metadata updates.',
142
- codeSnippet: `// ❌ Before:\n<CategoryChip category={category} />\n\n// ✅ After:\n<CategoryChip id={category.id} name={category.name} isSelected={selectedId === category.id} />`,
143
- impact: 'Medium Impact',
91
+ };
92
+ case 'FlatListWindowing':
93
+ return {
94
+ keyName: 'FlatListWindowing',
95
+ title: (0, i18n_1.t)('performance.fixFlatListWindowingTitle'),
96
+ explanation: (0, i18n_1.t)('performance.fixFlatListWindowingDesc'),
97
+ codeSnippet: `${(0, i18n_1.t)('performance.beforeFlatListWindowing', { comp })}`,
98
+ impact: (0, i18n_1.t)('performance.mediumImpact'),
99
+ impactColor: AppColors_1.AppColors.purple500,
100
+ };
101
+ case 'ImageCaching':
102
+ return {
103
+ keyName: 'ImageCaching',
104
+ title: (0, i18n_1.t)('performance.fixImageCachingTitle'),
105
+ explanation: (0, i18n_1.t)('performance.fixImageCachingDesc'),
106
+ codeSnippet: `${(0, i18n_1.t)('performance.beforeImageCaching', { comp })}`,
107
+ impact: (0, i18n_1.t)('performance.mediumImpact'),
144
108
  impactColor: AppColors_1.AppColors.purple500,
145
- },
146
- ],
147
- severity: 'warning',
148
- },
149
- {
150
- id: 'render-5',
151
- name: 'NavbarUserProfile',
152
- type: 'component',
153
- sourceFile: 'src/components/NavbarUserProfile.tsx',
154
- renderCount: 12,
155
- wastefulCount: 2,
156
- wastefulPercentage: 16.7,
157
- avgRenderTimeMs: 3.1,
158
- totalRenderTimeMs: 37.2,
159
- lastRenderedAt: Date.now() - 32000,
160
- reasons: [
161
- 'Avatar image cache re-validation on auth session refresh',
162
- ],
163
- fixKeys: [
164
- {
109
+ };
110
+ case 'ContextSplitting':
111
+ return {
112
+ keyName: 'ContextSplitting',
113
+ title: (0, i18n_1.t)('performance.fixContextSplittingTitle'),
114
+ explanation: (0, i18n_1.t)('performance.fixContextSplittingDesc'),
115
+ codeSnippet: `${(0, i18n_1.t)('performance.beforeContextSplitting', { comp })}\n\n${(0, i18n_1.t)('performance.afterContextSplitting')}`,
116
+ impact: (0, i18n_1.t)('performance.highImpact'),
117
+ impactColor: AppColors_1.AppColors.pink500,
118
+ };
119
+ case 'InlineStylesHoist':
120
+ return {
121
+ keyName: 'InlineStylesHoist',
122
+ title: (0, i18n_1.t)('performance.fixInlineStylesHoistTitle'),
123
+ explanation: (0, i18n_1.t)('performance.fixInlineStylesHoistDesc'),
124
+ codeSnippet: `${(0, i18n_1.t)('performance.beforeInlineStyles', { comp })}\n\n${(0, i18n_1.t)('performance.afterInlineStyles')}`,
125
+ impact: (0, i18n_1.t)('performance.bestPractice'),
126
+ impactColor: AppColors_1.AppColors.pink500,
127
+ };
128
+ case 'useRef':
129
+ case 'useRefForTracking':
130
+ default:
131
+ return {
165
132
  keyName: 'useRefForTracking',
166
- title: 'Use useRef for Non-Visual Tracking Values',
167
- explanation: 'Do not store analytics timers, scroll offsets, or tracking IDs in useState if they do not directly alter the JSX tree.',
168
- codeSnippet: `// Before:\nconst [sessionCount, setSessionCount] = useState(0);\n\n// ✅ After:\nconst sessionCountRef = useRef(0);`,
169
- impact: 'Best Practice',
170
- impactColor: AppColors_1.AppColors.sky500,
171
- },
172
- ],
173
- severity: 'optimal',
174
- },
175
- ];
176
- exports.INITIAL_EVENTS = [
177
- {
178
- id: 'perf-1',
179
- timestamp: Date.now() - 48000,
180
- type: 'fps_drop',
181
- category: 'navigation',
182
- fps: 38,
183
- durationMs: 26.3,
184
- label: 'Main Thread Spike during Navigation',
185
- detail: 'Screen transition triggered heavy layout reconciliation and simultaneous component mounts.',
186
- source: 'src/navigation/RootNavigator.tsx',
187
- breakdown: { jsTimeMs: 18.2, uiTimeMs: 8.1, bridgeLatencyMs: 1.2 },
188
- heapDeltaKb: 640,
189
- advice: 'Defer non-critical offscreen hooks with InteractionManager.runAfterInteractions to preserve 60 FPS.',
190
- severity: 'warning',
191
- },
192
- {
193
- id: 'perf-2',
194
- timestamp: Date.now() - 41000,
195
- type: 'slow_render',
196
- category: 'render',
197
- fps: 42,
198
- durationMs: 23.8,
199
- label: 'FlatList Virtualization Re-render Pass',
200
- detail: 'FlatList rendered 25 items simultaneously on orientation change without memoized row component.',
201
- source: 'src/components/Inspector/NetworkTab.tsx',
202
- breakdown: { jsTimeMs: 16.4, uiTimeMs: 7.4 },
203
- heapDeltaKb: 380,
204
- advice: 'Implement getItemLayout and React.memo(LogCard) to skip redundant diffing passes.',
205
- severity: 'warning',
206
- },
207
- {
208
- id: 'perf-3',
209
- timestamp: Date.now() - 35000,
210
- type: 'transition',
211
- category: 'navigation',
212
- fps: 59,
213
- durationMs: 16.9,
214
- label: 'Native Modal Slide-Up Transition',
215
- detail: 'Hardware accelerated native driver animated transform running smoothly at sustained 60 FPS.',
216
- source: 'src/components/Inspector/MainScreen.tsx',
217
- breakdown: { jsTimeMs: 2.1, uiTimeMs: 14.8 },
218
- heapDeltaKb: 120,
219
- advice: 'Using nativeDriver: true successfully prevents JS thread blocking during animations.',
220
- severity: 'optimal',
221
- },
222
- {
223
- id: 'perf-4',
224
- timestamp: Date.now() - 28000,
225
- type: 'memory',
226
- category: 'memory',
227
- fps: 60,
228
- durationMs: 16.6,
229
- label: 'Hermes Generational Garbage Collection',
230
- detail: 'Minor generational GC cycle scavenged 4.2 MB ephemeral heap objects with sub-millisecond thread pause.',
231
- source: 'Hermes VM Garbage Collector',
232
- breakdown: { jsTimeMs: 3.1, uiTimeMs: 0.2 },
233
- heapDeltaKb: -4280,
234
- advice: 'Hermes generational garbage collector is operating within optimal sub-5ms limits.',
235
- severity: 'optimal',
236
- },
237
- {
238
- id: 'perf-5',
239
- timestamp: Date.now() - 22000,
240
- type: 'network',
241
- category: 'io',
242
- fps: 48,
243
- durationMs: 20.8,
244
- label: 'Large JSON Payload Deserialization',
245
- detail: '50-item API response parse overhead in network adapter (185 KB JSON raw string).',
246
- source: 'src/customHooks/networkLogger.ts',
247
- breakdown: { jsTimeMs: 15.6, uiTimeMs: 5.2, bridgeLatencyMs: 2.1 },
248
- heapDeltaKb: 890,
249
- advice: 'Consider paginating API payloads or streaming responses if payload size exceeds 250 KB.',
250
- severity: 'warning',
251
- },
252
- {
253
- id: 'perf-6',
254
- timestamp: Date.now() - 17000,
255
- type: 'slow_render',
256
- category: 'render',
257
- fps: 52,
258
- durationMs: 19.2,
259
- label: 'Image Bitmap Decode & Rasterization',
260
- detail: 'Retina raster decode for banner_dark.png (1200×630px raster buffer allocation).',
261
- source: 'src/components/Inspector/BundleTab.tsx',
262
- breakdown: { jsTimeMs: 3.4, uiTimeMs: 15.8 },
263
- heapDeltaKb: 1450,
264
- advice: 'Downscale asset dimensions or convert to WebP to reduce decode latency by ~65%.',
265
- severity: 'warning',
266
- },
267
- {
268
- id: 'perf-7',
269
- timestamp: Date.now() - 12000,
270
- type: 'bridge',
271
- category: 'bridge',
272
- fps: 60,
273
- durationMs: 16.6,
274
- label: 'Native TurboModule JSI Invocation',
275
- detail: 'AsyncStorage / MMKV preferences transaction read across 32 configuration keys.',
276
- source: 'src/helpers/settingsStore.ts',
277
- breakdown: { jsTimeMs: 1.8, uiTimeMs: 0.8, bridgeLatencyMs: 0.4 },
278
- heapDeltaKb: 45,
279
- advice: 'Direct C++ JSI Turbomodule bindings completely bypass legacy JSON bridge serialization overhead.',
280
- severity: 'optimal',
281
- },
282
- {
283
- id: 'perf-8',
284
- timestamp: Date.now() - 8000,
285
- type: 'slow_render',
286
- category: 'render',
287
- fps: 60,
288
- durationMs: 16.6,
289
- label: 'Redux Action State Tree Diffing',
290
- detail: 'Redux dispatch pass evaluated 6 reducer slices and emitted state notification in 4.8ms.',
291
- source: 'src/components/Inspector/ReduxTab.tsx',
292
- breakdown: { jsTimeMs: 4.8, uiTimeMs: 1.2 },
293
- heapDeltaKb: 180,
294
- advice: 'State tree immutability preserved. Memoized selectors prevented redundant component renders.',
295
- severity: 'optimal',
296
- },
297
- {
298
- id: 'perf-9',
299
- timestamp: Date.now() - 4000,
300
- type: 'touch',
301
- category: 'render',
302
- fps: 60,
303
- durationMs: 16.6,
304
- label: 'Touch-to-Render Event Latency',
305
- detail: 'Gesture responder dispatched tap event to TabBar button with immediate 60 FPS response.',
306
- source: 'src/components/Inspector/TabBar.tsx',
307
- breakdown: { jsTimeMs: 4.2, uiTimeMs: 2.1 },
308
- heapDeltaKb: 30,
309
- advice: 'Touch responder latency is well within standard 16.67ms frame budget.',
310
- severity: 'optimal',
311
- },
312
- {
313
- id: 'perf-10',
314
- timestamp: Date.now() - 1500,
315
- type: 'transition',
316
- category: 'render',
317
- fps: 60,
318
- durationMs: 16.6,
319
- label: 'C++ Yoga Flexbox Layout Pass',
320
- detail: 'Inspector UI multi-tab card layout recalculation and font metrics pass in C++ Yoga engine.',
321
- source: 'Yoga Flexbox Layout Engine',
322
- breakdown: { jsTimeMs: 2.8, uiTimeMs: 3.4 },
323
- heapDeltaKb: 65,
324
- advice: 'Flexbox layout constraints are cached and computed efficiently with zero reflow penalties.',
325
- severity: 'optimal',
326
- },
327
- ];
328
- // Global in-memory render registry
133
+ title: (0, i18n_1.t)('performance.fixUseRefForTrackingTitle'),
134
+ explanation: (0, i18n_1.t)('performance.fixUseRefForTrackingDesc'),
135
+ codeSnippet: `${(0, i18n_1.t)('performance.beforeUseRef', { comp })}\nconst [eventCount, setEventCount] = useState(0);\n\n${(0, i18n_1.t)('performance.afterUseRef')}\nconst eventCountRef = useRef(0);`,
136
+ impact: (0, i18n_1.t)('performance.bestPractice'),
137
+ impactColor: AppColors_1.AppColors.pink500,
138
+ };
139
+ }
140
+ };
141
+ exports.generateFixSnippet = generateFixSnippet;
142
+ /**
143
+ * Dynamically resolves caller file path or runtime source from the active JavaScript stack trace.
144
+ * Does not assume fixed directory hierarchies (such as src/ or components/) and extracts
145
+ * whatever relative path or symbol the host project structure uses.
146
+ */
147
+ function getCallerSourceFile(fallbackName) {
148
+ try {
149
+ const stack = new Error().stack;
150
+ if (!stack)
151
+ return fallbackName || 'React Native Runtime';
152
+ const lines = stack.split('\n');
153
+ for (let i = 0; i < lines.length; i++) {
154
+ const line = lines[i];
155
+ if (line.includes('performanceTracker') ||
156
+ line.includes('node_modules') ||
157
+ line.includes('Error') ||
158
+ line.includes('regeneratorRuntime')) {
159
+ continue;
160
+ }
161
+ const match = line.match(/(?:at\s+)?(?:.*?\((.*?)(?::\d+)?(?::\d+)?\)|(\S+:\d+:\d+))/);
162
+ if (match) {
163
+ const file = (match[1] || match[2] || '').trim().replace(/^at\s+/, '');
164
+ if (file && !file.includes('performanceTracker'))
165
+ return file;
166
+ }
167
+ const cleaned = line.trim().replace(/^at\s+/, '');
168
+ if (cleaned && !cleaned.includes('performanceTracker'))
169
+ return cleaned;
170
+ }
171
+ }
172
+ catch (e) { }
173
+ return fallbackName || 'React Native Runtime';
174
+ }
175
+ const getInitialRenderProfiles = () => [];
176
+ exports.getInitialRenderProfiles = getInitialRenderProfiles;
177
+ const getInitialPerformanceEvents = () => [];
178
+ exports.getInitialPerformanceEvents = getInitialPerformanceEvents;
179
+ exports.INITIAL_EVENTS = [];
329
180
  const globalRenderRegistry = new Map();
330
- INITIAL_RENDER_PROFILES.forEach(profile => {
181
+ const renderListeners = new Set();
182
+ const notifyRenderListeners = () => {
183
+ const list = Array.from(globalRenderRegistry.values());
184
+ renderListeners.forEach(listener => {
185
+ try {
186
+ listener(list);
187
+ }
188
+ catch { }
189
+ });
190
+ };
191
+ const subscribeRenderProfiles = (listener) => {
192
+ renderListeners.add(listener);
193
+ listener(Array.from(globalRenderRegistry.values()));
194
+ return () => {
195
+ renderListeners.delete(listener);
196
+ };
197
+ };
198
+ exports.subscribeRenderProfiles = subscribeRenderProfiles;
199
+ const getRenderProfiles = () => Array.from(globalRenderRegistry.values());
200
+ exports.getRenderProfiles = getRenderProfiles;
201
+ const registerComponentProfile = (profile) => {
331
202
  globalRenderRegistry.set(profile.name, profile);
332
- });
203
+ notifyRenderListeners();
204
+ };
205
+ exports.registerComponentProfile = registerComponentProfile;
206
+ const trackComponentRender = (params) => {
207
+ const existing = globalRenderRegistry.get(params.name);
208
+ const now = Date.now();
209
+ const renderTimeMs = params.renderTimeMs ?? 1.5;
210
+ const isWasteful = params.isWasteful ?? false;
211
+ if (existing) {
212
+ const nextCount = existing.renderCount + 1;
213
+ const nextWasteful = existing.wastefulCount + (isWasteful ? 1 : 0);
214
+ const nextPct = Number(((nextWasteful / nextCount) * 100).toFixed(1));
215
+ const nextTotalTime = Number((existing.totalRenderTimeMs + renderTimeMs).toFixed(1));
216
+ const nextAvgTime = Number((nextTotalTime / nextCount).toFixed(1));
217
+ const reasons = params.reason
218
+ ? Array.from(new Set([...existing.reasons, params.reason]))
219
+ : existing.reasons;
220
+ const updated = {
221
+ ...existing,
222
+ renderCount: nextCount,
223
+ wastefulCount: nextWasteful,
224
+ wastefulPercentage: nextPct,
225
+ avgRenderTimeMs: nextAvgTime,
226
+ totalRenderTimeMs: nextTotalTime,
227
+ lastRenderedAt: now,
228
+ reasons,
229
+ severity: nextPct > 70 && nextCount > 20
230
+ ? 'critical'
231
+ : nextPct > 40 && nextCount > 10
232
+ ? 'warning'
233
+ : 'optimal',
234
+ };
235
+ globalRenderRegistry.set(params.name, updated);
236
+ }
237
+ else {
238
+ const profile = {
239
+ id: `render-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`,
240
+ name: params.name,
241
+ type: params.type || 'component',
242
+ sourceFile: params.sourceFile || getCallerSourceFile(params.name),
243
+ renderCount: 1,
244
+ wastefulCount: isWasteful ? 1 : 0,
245
+ wastefulPercentage: isWasteful ? 100 : 0,
246
+ avgRenderTimeMs: renderTimeMs,
247
+ totalRenderTimeMs: renderTimeMs,
248
+ lastRenderedAt: now,
249
+ reasons: params.reason ? [params.reason] : [],
250
+ fixKeys: params.fixKeys || [
251
+ (0, exports.generateFixSnippet)('useCallback', params.name),
252
+ (0, exports.generateFixSnippet)('memo', params.name),
253
+ ],
254
+ severity: 'optimal',
255
+ };
256
+ globalRenderRegistry.set(params.name, profile);
257
+ }
258
+ notifyRenderListeners();
259
+ };
260
+ exports.trackComponentRender = trackComponentRender;
261
+ /**
262
+ * Dynamically queries Hermes or JavaScript runtime memory statistics.
263
+ */
264
+ const getHermesMemoryStats = () => {
265
+ try {
266
+ const hermes = globalThis.HermesInternal;
267
+ if (hermes && typeof hermes.getInstrumentedStats === 'function') {
268
+ const stats = hermes.getInstrumentedStats();
269
+ const heapUsedMb = Number(((stats.js_heap_size || stats.heap_size || 34800000) / 1048576).toFixed(1));
270
+ const heapTotalMb = Number(((stats.js_heap_capacity || stats.allocated_bytes || 64000000) /
271
+ 1048576).toFixed(1));
272
+ const gcCount = Number(stats.num_gcs || 0);
273
+ const gcPauseMs = Number(((stats.gc_time_ms || 0) / Math.max(1, gcCount)).toFixed(1));
274
+ return {
275
+ heapUsedMb,
276
+ heapTotalMb,
277
+ gcCount,
278
+ gcPauseMs,
279
+ allocationRateMbPerSec: 1.8,
280
+ };
281
+ }
282
+ }
283
+ catch { }
284
+ return {
285
+ heapUsedMb: 32.5,
286
+ heapTotalMb: 64.0,
287
+ gcCount: 0,
288
+ gcPauseMs: 1.5,
289
+ allocationRateMbPerSec: 1.2,
290
+ };
291
+ };
292
+ exports.getHermesMemoryStats = getHermesMemoryStats;
293
+ /**
294
+ * Dynamically records and evaluates screen navigation transition performance.
295
+ */
296
+ const trackNavigationTransition = (screenName, durationMs, options) => {
297
+ const duration = Math.max(1, Number(durationMs.toFixed(1)));
298
+ const tti = options?.ttiMs ? Number(options.ttiMs.toFixed(1)) : duration;
299
+ const droppedFrames = options?.droppedFrames ??
300
+ (duration > 32 ? Math.round((duration - 16.67) / 16.67) : 0);
301
+ const fps = Math.min(60, Math.max(15, Math.round(1000 / Math.max(16.67, duration))));
302
+ return (0, exports.logPerformanceEvent)({
303
+ type: 'transition',
304
+ category: 'navigation',
305
+ fps,
306
+ durationMs: duration,
307
+ droppedFrames,
308
+ frameTimeMs: duration,
309
+ frameBudgetPct: Number(((duration / 16.67) * 100).toFixed(0)),
310
+ bottleneckThread: duration > 32 ? 'JS Thread' : 'Balanced',
311
+ screenName,
312
+ label: (0, i18n_1.t)('performance.transitionEventLabel', { screen: screenName }),
313
+ detail: (0, i18n_1.t)('performance.transitionEventDetail', { duration, tti }),
314
+ source: options?.source || getCallerSourceFile(screenName),
315
+ advice: (0, i18n_1.t)('performance.transitionEventAdvice'),
316
+ severity: duration > 50 ? 'critical' : duration > 25 ? 'warning' : 'optimal',
317
+ });
318
+ };
319
+ exports.trackNavigationTransition = trackNavigationTransition;
320
+ /**
321
+ * React Hook that automatically profiles screen mounts and navigation transitions.
322
+ */
323
+ const useNavigationProfiler = (screenName, options) => {
324
+ const mountTimeRef = (0, react_1.useRef)(Date.now());
325
+ (0, react_1.useEffect)(() => {
326
+ const elapsed = Date.now() - mountTimeRef.current;
327
+ (0, exports.trackNavigationTransition)(screenName, elapsed);
328
+ if (options?.onTransitionEnd) {
329
+ options.onTransitionEnd(elapsed);
330
+ }
331
+ }, [screenName]);
332
+ };
333
+ exports.useNavigationProfiler = useNavigationProfiler;
334
+ /**
335
+ * Measures synchronous execution duration and alerts on frame budget spikes (>16ms).
336
+ */
337
+ const trackHeavyTask = (taskName, syncFn, options) => {
338
+ const start = Date.now();
339
+ try {
340
+ return syncFn();
341
+ }
342
+ finally {
343
+ const duration = Date.now() - start;
344
+ const threshold = options?.thresholdMs ?? 16;
345
+ if (duration >= threshold) {
346
+ (0, exports.logPerformanceEvent)({
347
+ type: 'slow_render',
348
+ category: 'render',
349
+ fps: Math.min(60, Math.max(10, Math.round(1000 / duration))),
350
+ durationMs: duration,
351
+ frameTimeMs: duration,
352
+ frameBudgetPct: Number(((duration / 16.67) * 100).toFixed(0)),
353
+ bottleneckThread: 'JS Thread',
354
+ screenName: taskName,
355
+ label: (0, i18n_1.t)('performance.heavyTaskEventLabel', { taskName }),
356
+ detail: (0, i18n_1.t)('performance.heavyTaskEventDetail', { duration }),
357
+ source: options?.source || getCallerSourceFile(taskName),
358
+ advice: (0, i18n_1.t)('performance.heavyTaskEventAdvice'),
359
+ severity: duration > 50 ? 'critical' : 'warning',
360
+ });
361
+ }
362
+ }
363
+ };
364
+ exports.trackHeavyTask = trackHeavyTask;
365
+ /**
366
+ * Measures async operation / Promise latency and records slow network or disk tasks.
367
+ */
368
+ const measureAsync = async (taskName, asyncFn, options) => {
369
+ const start = Date.now();
370
+ try {
371
+ return await asyncFn();
372
+ }
373
+ finally {
374
+ const duration = Date.now() - start;
375
+ const threshold = options?.thresholdMs ?? 100;
376
+ if (duration >= threshold) {
377
+ (0, exports.logPerformanceEvent)({
378
+ type: 'network',
379
+ category: 'io',
380
+ fps: 60,
381
+ durationMs: duration,
382
+ bottleneckThread: 'Balanced',
383
+ screenName: taskName,
384
+ label: (0, i18n_1.t)('performance.asyncOpEventLabel', { opName: taskName }),
385
+ detail: (0, i18n_1.t)('performance.asyncOpEventDetail', { duration }),
386
+ source: options?.source || getCallerSourceFile(taskName),
387
+ severity: duration > 1000 ? 'critical' : duration > 300 ? 'warning' : 'optimal',
388
+ });
389
+ }
390
+ }
391
+ };
392
+ exports.measureAsync = measureAsync;
393
+ /**
394
+ * Custom React Hook to profile any component or screen renders dynamically in runtime.
395
+ */
396
+ const useComponentProfiler = (componentName, options) => {
397
+ const renderCountRef = (0, react_1.useRef)(0);
398
+ const prevPropsRef = (0, react_1.useRef)(options?.propsToCompare);
399
+ const startTimeRef = (0, react_1.useRef)(Date.now());
400
+ const renderTimestampsRef = (0, react_1.useRef)([]);
401
+ startTimeRef.current = Date.now();
402
+ (0, react_1.useEffect)(() => {
403
+ const duration = Date.now() - startTimeRef.current;
404
+ const now = Date.now();
405
+ renderCountRef.current += 1;
406
+ // Detect rapid re-render loop spikes (>= 10 renders within 500ms)
407
+ renderTimestampsRef.current = [
408
+ ...renderTimestampsRef.current.filter(ts => now - ts <= 500),
409
+ now,
410
+ ];
411
+ if (renderTimestampsRef.current.length >= 10) {
412
+ (0, exports.logPerformanceEvent)({
413
+ type: 'slow_render',
414
+ category: 'render',
415
+ fps: 20,
416
+ durationMs: duration,
417
+ bottleneckThread: 'JS Thread',
418
+ screenName: componentName,
419
+ label: (0, i18n_1.t)('performance.renderLoopWarningLabel', { comp: componentName }),
420
+ detail: (0, i18n_1.t)('performance.renderLoopWarningDetail', {
421
+ comp: componentName,
422
+ count: renderTimestampsRef.current.length,
423
+ }),
424
+ source: options?.sourceFile || getCallerSourceFile(componentName),
425
+ advice: (0, i18n_1.t)('performance.renderLoopWarningAdvice'),
426
+ severity: 'critical',
427
+ });
428
+ }
429
+ let isWasteful = false;
430
+ let wastefulReason = undefined;
431
+ if (renderCountRef.current > 1 && options?.propsToCompare) {
432
+ const prev = prevPropsRef.current;
433
+ const next = options.propsToCompare;
434
+ const keys = Object.keys(next);
435
+ const changedKeys = keys.filter(k => prev[k] !== next[k]);
436
+ if (changedKeys.length === 0) {
437
+ isWasteful = true;
438
+ wastefulReason =
439
+ 'Re-rendered with identical props (memoization candidate)';
440
+ }
441
+ prevPropsRef.current = next;
442
+ }
443
+ (0, exports.trackComponentRender)({
444
+ name: componentName,
445
+ type: options?.type || 'component',
446
+ sourceFile: options?.sourceFile || getCallerSourceFile(componentName),
447
+ renderTimeMs: Math.max(0.5, duration),
448
+ isWasteful,
449
+ reason: wastefulReason,
450
+ });
451
+ });
452
+ };
453
+ exports.useComponentProfiler = useComponentProfiler;
333
454
  const usePerformanceTracker = () => {
334
455
  const [isRecording, setIsRecording] = (0, react_1.useState)(true);
335
456
  const [currentFps, setCurrentFps] = (0, react_1.useState)(60);
336
- const [minFps, setMinFps] = (0, react_1.useState)(57);
457
+ const [minFps, setMinFps] = (0, react_1.useState)(60);
337
458
  const [maxFps, setMaxFps] = (0, react_1.useState)(60);
338
- const [avgFps, setAvgFps] = (0, react_1.useState)(59);
339
- const [totalFrames, setTotalFrames] = (0, react_1.useState)(4820);
340
- const [jankyFrameCount, setJankyFrameCount] = (0, react_1.useState)(4);
341
- const [jsLagMs, setJsLagMs] = (0, react_1.useState)(1.2);
342
- const [fpsHistory, setFpsHistory] = (0, react_1.useState)([
343
- 60, 59, 60, 60, 58, 60, 59, 60, 60, 60, 57, 60, 59, 60, 60, 58, 60, 60, 59, 60,
344
- 60, 60, 59, 60, 58, 60, 60, 60, 59, 60,
345
- ]);
346
- const [memoryStats, setMemoryStats] = (0, react_1.useState)({
347
- heapUsedMb: 34.8,
348
- heapTotalMb: 64.0,
349
- gcCount: 14,
350
- gcPauseMs: 2.1,
351
- allocationRateMbPerSec: 1.8,
352
- });
353
- const [renderProfiles, setRenderProfiles] = (0, react_1.useState)(INITIAL_RENDER_PROFILES);
354
- const [events, setEvents] = (0, react_1.useState)(exports.INITIAL_EVENTS);
459
+ const [avgFps, setAvgFps] = (0, react_1.useState)(60);
460
+ const [totalFrames, setTotalFrames] = (0, react_1.useState)(0);
461
+ const [jankyFrameCount, setJankyFrameCount] = (0, react_1.useState)(0);
462
+ const [jsLagMs, setJsLagMs] = (0, react_1.useState)(0);
463
+ const [fpsHistory, setFpsHistory] = (0, react_1.useState)(() => Array(30).fill(60));
464
+ const [memoryStats, setMemoryStats] = (0, react_1.useState)(() => (0, exports.getHermesMemoryStats)());
465
+ const [renderProfiles, setRenderProfiles] = (0, react_1.useState)(() => (0, exports.getRenderProfiles)());
466
+ const [events, setEvents] = (0, react_1.useState)(() => (0, exports.getPerformanceEvents)());
467
+ (0, react_1.useEffect)(() => {
468
+ const unsubRender = (0, exports.subscribeRenderProfiles)(profiles => {
469
+ setRenderProfiles(profiles);
470
+ });
471
+ const unsubEvents = (0, exports.subscribePerformanceEvents)(liveEvents => {
472
+ setEvents(liveEvents);
473
+ });
474
+ return () => {
475
+ unsubRender();
476
+ unsubEvents();
477
+ };
478
+ }, []);
355
479
  const lastFrameTimeRef = (0, react_1.useRef)(Date.now());
356
480
  const rafIdRef = (0, react_1.useRef)(null);
481
+ const appStartTimestampRef = (0, react_1.useRef)(Date.now());
357
482
  // Live Frame Measurement Loop
358
483
  (0, react_1.useEffect)(() => {
359
484
  if (!isRecording)
@@ -390,35 +515,38 @@ const usePerformanceTracker = () => {
390
515
  setAvgFps(Math.round(sum / next.length));
391
516
  return next;
392
517
  });
393
- // Simulate subtle real-world memory fluctuation
394
- setMemoryStats(prev => {
395
- const delta = (Math.random() * 0.4 - 0.18);
396
- const nextUsed = Math.min(prev.heapTotalMb * 0.9, Math.max(20.0, Number((prev.heapUsedMb + delta).toFixed(1))));
397
- return {
398
- ...prev,
399
- heapUsedMb: nextUsed,
400
- allocationRateMbPerSec: Number((1.2 + Math.random() * 1.4).toFixed(1)),
401
- };
402
- });
518
+ // Update live memory telemetry from Hermes / engine
519
+ const liveMem = (0, exports.getHermesMemoryStats)();
520
+ setMemoryStats(prev => ({
521
+ ...prev,
522
+ heapUsedMb: liveMem.heapUsedMb,
523
+ heapTotalMb: liveMem.heapTotalMb,
524
+ gcCount: liveMem.gcCount,
525
+ gcPauseMs: liveMem.gcPauseMs,
526
+ allocationRateMbPerSec: liveMem.allocationRateMbPerSec,
527
+ }));
403
528
  if (measuredFps < 50) {
404
- const newEvent = {
405
- id: `drop-${Date.now()}`,
406
- timestamp: Date.now(),
529
+ const frameTime = Number((1000 / measuredFps).toFixed(1));
530
+ (0, exports.logPerformanceEvent)({
407
531
  type: 'fps_drop',
408
532
  category: 'render',
409
533
  fps: measuredFps,
410
- durationMs: Number((1000 / measuredFps).toFixed(1)),
411
- label: `Live Frame Rate Dip (${measuredFps} FPS)`,
412
- detail: `Main thread frame duration extended to ${(1000 / measuredFps).toFixed(1)}ms during view update.`,
534
+ durationMs: frameTime,
535
+ droppedFrames: Math.max(0, 60 - measuredFps),
536
+ frameTimeMs: frameTime,
537
+ frameBudgetPct: Number(((frameTime / 16.67) * 100).toFixed(0)),
538
+ bottleneckThread: maxLagInSecond > 8 ? 'JS Thread' : 'UI Thread',
539
+ screenName: 'Live Application Render',
540
+ label: (0, i18n_1.t)('performance.liveFpsDropLabel', { fps: measuredFps }),
541
+ detail: (0, i18n_1.t)('performance.liveFpsDropDetail', { duration: frameTime }),
413
542
  source: 'React Native UI Thread',
414
543
  breakdown: {
415
- jsTimeMs: Number(((1000 / measuredFps) * 0.65).toFixed(1)),
416
- uiTimeMs: Number(((1000 / measuredFps) * 0.35).toFixed(1)),
544
+ jsTimeMs: Number((frameTime * 0.65).toFixed(1)),
545
+ uiTimeMs: Number((frameTime * 0.35).toFixed(1)),
417
546
  },
418
- advice: 'Heavy JavaScript execution during frame pass delayed display presentation.',
547
+ advice: (0, i18n_1.t)('performance.liveFpsDropAdvice'),
419
548
  severity: measuredFps < 30 ? 'critical' : 'warning',
420
- };
421
- setEvents(prev => [newEvent, ...prev.slice(0, 49)]);
549
+ });
422
550
  }
423
551
  frameCount = 0;
424
552
  maxLagInSecond = 0;
@@ -435,20 +563,32 @@ const usePerformanceTracker = () => {
435
563
  };
436
564
  }, [isRecording]);
437
565
  const mobileVitals = (0, react_1.useMemo)(() => {
438
- const jankPct = totalFrames > 0 ? Number(((jankyFrameCount / Math.max(1, totalFrames / 60)) * 100).toFixed(1)) : 0.8;
566
+ const measuredJankPct = totalFrames > 0
567
+ ? Number(((jankyFrameCount / Math.max(1, totalFrames / 60)) * 100).toFixed(1))
568
+ : 0;
569
+ const slowRenderCount = events.filter(e => e.type === 'slow_render').length;
570
+ const dynamicInp = Math.min(100, Math.max(8.0, Number((jsLagMs + 12.5).toFixed(1))));
571
+ const dynamicFcp = Math.max(120, Math.min(500, Math.round(150 + slowRenderCount * 25)));
572
+ const dynamicTti = Math.max(280, Math.min(1200, Math.round(dynamicFcp + 180 + slowRenderCount * 40)));
439
573
  return {
440
- ttiMs: 412,
441
- fcpMs: 180,
442
- inpMs: 14.2,
443
- jankPercentage: jankPct,
444
- grade: jankPct <= 2.0 ? 'Optimal' : jankPct <= 5.0 ? 'Fair' : 'Poor',
574
+ ttiMs: dynamicTti,
575
+ fcpMs: dynamicFcp,
576
+ inpMs: dynamicInp,
577
+ jankPercentage: measuredJankPct,
578
+ grade: measuredJankPct <= 2.0
579
+ ? 'Optimal'
580
+ : measuredJankPct <= 5.0
581
+ ? 'Fair'
582
+ : 'Poor',
445
583
  };
446
- }, [totalFrames, jankyFrameCount]);
584
+ }, [totalFrames, jankyFrameCount, jsLagMs, events]);
447
585
  // Aggregate re-render stats
448
586
  const reRenderSummary = (0, react_1.useMemo)(() => {
449
587
  const totalRenders = renderProfiles.reduce((sum, p) => sum + p.renderCount, 0);
450
588
  const totalWasteful = renderProfiles.reduce((sum, p) => sum + p.wastefulCount, 0);
451
- const overallWastefulPct = totalRenders > 0 ? Number(((totalWasteful / totalRenders) * 100).toFixed(1)) : 0;
589
+ const overallWastefulPct = totalRenders > 0
590
+ ? Number(((totalWasteful / totalRenders) * 100).toFixed(1))
591
+ : 0;
452
592
  const topOffender = [...renderProfiles].sort((a, b) => b.renderCount - a.renderCount)[0];
453
593
  return {
454
594
  totalRenders,
@@ -459,61 +599,73 @@ const usePerformanceTracker = () => {
459
599
  };
460
600
  }, [renderProfiles]);
461
601
  const clearEvents = () => {
462
- setEvents([]);
602
+ (0, exports.clearPerformanceEvents)();
463
603
  };
464
604
  const resetRenderCounters = (0, react_1.useCallback)(() => {
465
- setRenderProfiles(prev => prev.map(p => ({
466
- ...p,
467
- renderCount: 1,
468
- wastefulCount: 0,
469
- wastefulPercentage: 0,
470
- totalRenderTimeMs: p.avgRenderTimeMs,
471
- lastRenderedAt: Date.now(),
472
- severity: 'optimal',
473
- })));
605
+ globalRenderRegistry.forEach((p, key) => {
606
+ globalRenderRegistry.set(key, {
607
+ ...p,
608
+ renderCount: 1,
609
+ wastefulCount: 0,
610
+ wastefulPercentage: 0,
611
+ totalRenderTimeMs: p.avgRenderTimeMs,
612
+ lastRenderedAt: Date.now(),
613
+ severity: 'optimal',
614
+ });
615
+ });
616
+ notifyRenderListeners();
474
617
  }, []);
475
618
  const simulateComponentRender = (0, react_1.useCallback)((componentId) => {
476
- setRenderProfiles(prev => prev.map(p => {
477
- if (p.id === componentId) {
619
+ globalRenderRegistry.forEach((p, key) => {
620
+ if (p.id === componentId || p.name === componentId) {
478
621
  const nextCount = p.renderCount + 1;
479
622
  const nextWasteful = p.wastefulCount + 1;
480
623
  const nextPct = Number(((nextWasteful / nextCount) * 100).toFixed(1));
481
- return {
624
+ globalRenderRegistry.set(key, {
482
625
  ...p,
483
626
  renderCount: nextCount,
484
627
  wastefulCount: nextWasteful,
485
628
  wastefulPercentage: nextPct,
486
629
  totalRenderTimeMs: Number((p.totalRenderTimeMs + p.avgRenderTimeMs).toFixed(1)),
487
630
  lastRenderedAt: Date.now(),
488
- severity: nextCount > 30 ? 'critical' : nextCount > 15 ? 'warning' : 'optimal',
489
- };
631
+ severity: nextCount > 30
632
+ ? 'critical'
633
+ : nextCount > 15
634
+ ? 'warning'
635
+ : 'optimal',
636
+ });
490
637
  }
491
- return p;
492
- }));
638
+ });
639
+ notifyRenderListeners();
493
640
  }, []);
494
641
  const triggerGc = () => {
642
+ try {
643
+ if (typeof globalThis.gc === 'function') {
644
+ globalThis.gc();
645
+ }
646
+ }
647
+ catch { }
648
+ const reclaimedMb = Number((4.5 + Math.random() * 3.5).toFixed(1));
649
+ const reclaimedKb = Math.round(reclaimedMb * 1024);
495
650
  setMemoryStats(prev => ({
496
651
  ...prev,
497
- heapUsedMb: Math.max(22.4, Number((prev.heapUsedMb - 6.8).toFixed(1))),
652
+ heapUsedMb: Math.max(18.0, Number((prev.heapUsedMb - reclaimedMb).toFixed(1))),
498
653
  gcCount: prev.gcCount + 1,
499
- gcPauseMs: Number((1.4 + Math.random() * 0.8).toFixed(1)),
654
+ gcPauseMs: Number((1.2 + Math.random() * 0.9).toFixed(1)),
500
655
  }));
501
- const gcEvent = {
502
- id: `gc-${Date.now()}`,
503
- timestamp: Date.now(),
656
+ (0, exports.logPerformanceEvent)({
504
657
  type: 'memory',
505
658
  category: 'memory',
506
659
  fps: 60,
507
- durationMs: 2.1,
508
- label: 'Manual Hermes GC Cycle Invoked',
509
- detail: 'Reclaimed ~6.8 MB unreferenced objects and compacted nursery spaces.',
510
- source: 'Hermes Memory Scavenger',
660
+ durationMs: Number((1.6 + Math.random() * 0.8).toFixed(1)),
661
+ label: (0, i18n_1.t)('performance.gcCycleLabel'),
662
+ detail: (0, i18n_1.t)('performance.gcCycleDetail', { amount: reclaimedMb }),
663
+ source: 'Hermes JavaScript VM Scavenger',
511
664
  breakdown: { jsTimeMs: 1.8, uiTimeMs: 0.3 },
512
- heapDeltaKb: -6960,
513
- advice: 'Heap usage optimized. Generational nursery cleared.',
665
+ heapDeltaKb: -reclaimedKb,
666
+ advice: (0, i18n_1.t)('performance.gcCycleAdvice'),
514
667
  severity: 'optimal',
515
- };
516
- setEvents(prev => [gcEvent, ...prev.slice(0, 49)]);
668
+ });
517
669
  };
518
670
  return {
519
671
  isRecording,
@@ -543,8 +695,10 @@ const globalPerformanceEvents = [...exports.INITIAL_EVENTS];
543
695
  const performanceListeners = new Set();
544
696
  const logPerformanceEvent = (event) => {
545
697
  const fullEvent = {
546
- id: event.id || `perf-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`,
698
+ id: event.id ||
699
+ `perf-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`,
547
700
  timestamp: event.timestamp || Date.now(),
701
+ source: event.source || getCallerSourceFile(),
548
702
  ...event,
549
703
  };
550
704
  globalPerformanceEvents.unshift(fullEvent);