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