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.
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/inappinspector/NetworkInspectorModule.java +34 -0
- package/dist/commonjs/components/ErrorBoundary.d.ts +1 -1
- package/dist/commonjs/components/ErrorBoundary.js +530 -100
- package/dist/commonjs/components/Inspector/AnalyticsTab.js +176 -126
- package/dist/commonjs/components/Inspector/BundleTab.d.ts +1 -0
- package/dist/commonjs/components/Inspector/BundleTab.js +145 -21
- package/dist/commonjs/components/Inspector/CrashTab.js +0 -8
- package/dist/commonjs/components/Inspector/NetworkDetail.js +41 -0
- package/dist/commonjs/components/Inspector/PerformanceTab.d.ts +5 -0
- package/dist/commonjs/components/Inspector/PerformanceTab.js +588 -155
- package/dist/commonjs/components/Inspector/SettingsPanel.js +57 -22
- package/dist/commonjs/components/Inspector/TabBar.js +13 -1
- package/dist/commonjs/components/LogCard.js +75 -34
- package/dist/commonjs/components/NetworkIcons.d.ts +112 -102
- package/dist/commonjs/constants/version.d.ts +1 -1
- package/dist/commonjs/constants/version.js +1 -1
- package/dist/commonjs/customHooks/analyticsLogger.d.ts +1 -11
- package/dist/commonjs/customHooks/analyticsLogger.js +8 -1
- package/dist/commonjs/customHooks/bundleAnalyzer.js +121 -124
- package/dist/commonjs/customHooks/networkLogger.d.ts +1 -0
- package/dist/commonjs/customHooks/networkLogger.js +16 -0
- package/dist/commonjs/customHooks/performanceTracker.d.ts +74 -0
- package/dist/commonjs/customHooks/performanceTracker.js +546 -392
- package/dist/commonjs/customHooks/reduxLogger.d.ts +1 -0
- package/dist/commonjs/customHooks/reduxLogger.js +3 -1
- package/dist/commonjs/helpers/index.js +27 -3
- package/dist/commonjs/i18n/locales/en.json +225 -4
- package/dist/commonjs/index.d.ts +1 -1
- package/dist/commonjs/index.js +36 -2
- package/dist/commonjs/styles/index.d.ts +3 -1
- package/dist/commonjs/styles/index.js +8 -6
- package/dist/commonjs/types/interfaces.d.ts +3 -0
- package/dist/esm/components/ErrorBoundary.d.ts +1 -1
- package/dist/esm/components/ErrorBoundary.js +498 -101
- package/dist/esm/components/Inspector/AnalyticsTab.js +177 -127
- package/dist/esm/components/Inspector/BundleTab.d.ts +1 -0
- package/dist/esm/components/Inspector/BundleTab.js +145 -23
- package/dist/esm/components/Inspector/CrashTab.js +0 -8
- package/dist/esm/components/Inspector/NetworkDetail.js +41 -0
- package/dist/esm/components/Inspector/PerformanceTab.d.ts +5 -0
- package/dist/esm/components/Inspector/PerformanceTab.js +591 -158
- package/dist/esm/components/Inspector/SettingsPanel.js +57 -22
- package/dist/esm/components/Inspector/TabBar.js +13 -1
- package/dist/esm/components/LogCard.js +75 -34
- package/dist/esm/components/NetworkIcons.d.ts +112 -102
- package/dist/esm/constants/version.d.ts +1 -1
- package/dist/esm/constants/version.js +1 -1
- package/dist/esm/customHooks/analyticsLogger.d.ts +1 -11
- package/dist/esm/customHooks/analyticsLogger.js +6 -0
- package/dist/esm/customHooks/bundleAnalyzer.js +121 -124
- package/dist/esm/customHooks/networkLogger.d.ts +1 -0
- package/dist/esm/customHooks/networkLogger.js +16 -0
- package/dist/esm/customHooks/performanceTracker.d.ts +74 -0
- package/dist/esm/customHooks/performanceTracker.js +531 -391
- package/dist/esm/customHooks/reduxLogger.d.ts +1 -0
- package/dist/esm/customHooks/reduxLogger.js +1 -0
- package/dist/esm/helpers/index.js +27 -3
- package/dist/esm/i18n/locales/en.json +225 -4
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +24 -3
- package/dist/esm/styles/index.d.ts +3 -1
- package/dist/esm/styles/index.js +8 -6
- package/dist/esm/types/interfaces.d.ts +3 -0
- package/ios/NetworkInspectorModule.h +2 -0
- package/ios/NetworkInspectorModule.m +10 -0
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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: '
|
|
24
|
-
explanation: '
|
|
25
|
-
codeSnippet:
|
|
26
|
-
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: '
|
|
32
|
-
explanation: '
|
|
33
|
-
codeSnippet:
|
|
34
|
-
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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: '
|
|
68
|
-
explanation: '
|
|
69
|
-
codeSnippet:
|
|
70
|
-
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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: '
|
|
103
|
-
explanation: '
|
|
104
|
-
codeSnippet:
|
|
105
|
-
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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: '
|
|
164
|
-
explanation: '
|
|
165
|
-
codeSnippet:
|
|
166
|
-
impact: '
|
|
167
|
-
impactColor: AppColors.
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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(
|
|
440
|
+
const [minFps, setMinFps] = useState(60);
|
|
334
441
|
const [maxFps, setMaxFps] = useState(60);
|
|
335
|
-
const [avgFps, setAvgFps] = useState(
|
|
336
|
-
const [totalFrames, setTotalFrames] = useState(
|
|
337
|
-
const [jankyFrameCount, setJankyFrameCount] = useState(
|
|
338
|
-
const [jsLagMs, setJsLagMs] = useState(
|
|
339
|
-
const [fpsHistory, setFpsHistory] = useState(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
]);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
|
402
|
-
|
|
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:
|
|
408
|
-
|
|
409
|
-
|
|
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((
|
|
413
|
-
uiTimeMs: Number((
|
|
527
|
+
jsTimeMs: Number((frameTime * 0.65).toFixed(1)),
|
|
528
|
+
uiTimeMs: Number((frameTime * 0.35).toFixed(1)),
|
|
414
529
|
},
|
|
415
|
-
advice: '
|
|
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
|
|
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:
|
|
438
|
-
fcpMs:
|
|
439
|
-
inpMs:
|
|
440
|
-
jankPercentage:
|
|
441
|
-
grade:
|
|
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
|
|
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
|
-
|
|
585
|
+
clearPerformanceEvents();
|
|
460
586
|
};
|
|
461
587
|
const resetRenderCounters = useCallback(() => {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
486
|
-
|
|
614
|
+
severity: nextCount > 30
|
|
615
|
+
? 'critical'
|
|
616
|
+
: nextCount > 15
|
|
617
|
+
? 'warning'
|
|
618
|
+
: 'optimal',
|
|
619
|
+
});
|
|
487
620
|
}
|
|
488
|
-
|
|
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(
|
|
635
|
+
heapUsedMb: Math.max(18.0, Number((prev.heapUsedMb - reclaimedMb).toFixed(1))),
|
|
495
636
|
gcCount: prev.gcCount + 1,
|
|
496
|
-
gcPauseMs: Number((1.
|
|
637
|
+
gcPauseMs: Number((1.2 + Math.random() * 0.9).toFixed(1)),
|
|
497
638
|
}));
|
|
498
|
-
|
|
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:
|
|
505
|
-
label: '
|
|
506
|
-
detail: '
|
|
507
|
-
source: 'Hermes
|
|
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: -
|
|
510
|
-
advice: '
|
|
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 ||
|
|
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);
|