├── .npmrc ├── src ├── index.js ├── JSPerfProfiler.js └── JSPerfProfiler.test.js ├── .gitignore ├── README.md ├── package.json ├── scripts └── release.js ├── how-it-works.md └── .npmignore /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './JSPerfProfiler' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .idea/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactNative JS Profiler 2 | 3 | This projects tries to measure the performance impact on JS thread by different app modules. 4 | It attempts to do so by measuring execution time in of a given function and also measuring callbacks 5 | of asynchronous operations starting inside. 6 | 7 | How it works: 8 | ------------- 9 | 10 | See [how-it-works.md](how-it-works.md) 11 | 12 | 13 | Usage: 14 | ------ 15 | 16 | ```js 17 | import { 18 | attach, 19 | timeAndLog, 20 | } from 'react-native-js-profiler'; 21 | 22 | attach(); 23 | 24 | timeAndLog(initModuleA, 'MyMessage', 'ModuleAContext', 'GeneralScope'); 25 | 26 | ``` 27 | 28 | TODO 29 | ---- 30 | * More generic API: don't depend on detox-instruments-react-native-utils 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-js-profiler", 3 | "version": "1.2.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "release": "node ./scripts/release.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/wix-incubator/react-native-js-profiler.git" 13 | }, 14 | "keywords": [ 15 | "react-native", 16 | "profiler", 17 | "devtools", 18 | "performance" 19 | ], 20 | "author": "Andy Kogut ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/wix-incubator/react-native-js-profiler/issues" 24 | }, 25 | "homepage": "https://github.com/wix-incubator/react-native-js-profiler#readme", 26 | "peerDependencies": { 27 | "react-native": "*" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.12.9", 31 | "babel-preset-react-native": "^4.0.1", 32 | "jest": "^26.6.3", 33 | "react": "^17.0.1", 34 | "react-native": "^0.64.x", 35 | "semver": "5.x.x", 36 | "shell-utils": "1.x.x", 37 | "lodash": ">=4.17.5" 38 | }, 39 | "babel": { 40 | "env": { 41 | "test": { 42 | "presets": [ 43 | "react-native" 44 | ], 45 | "retainLines": true 46 | } 47 | } 48 | }, 49 | "jest": { 50 | "transform": { 51 | "^.+\\.(js|tsx|ts)$": "/node_modules/react-native/jest/preprocessor.js" 52 | }, 53 | "transformIgnorePatterns": [ 54 | "node_modules/(?!(@react-native|react-native|@react-native-community/async-storage|@sentry/react-native)/)" 55 | ], 56 | "preset": "react-native", 57 | "roots": [ 58 | "node_modules", 59 | "src" 60 | ], 61 | "resetMocks": true, 62 | "resetModules": true, 63 | "testPathIgnorePatterns": [ 64 | "/node_modules/" 65 | ], 66 | "coverageThreshold": { 67 | "global": { 68 | "statements": 100, 69 | "branches": 100, 70 | "functions": 100, 71 | "lines": 100 72 | } 73 | } 74 | }, 75 | "dependencies": { 76 | "detox-instruments-react-native-utils": "3.x.x" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const exec = require('shell-utils').exec; 3 | const semver = require('semver'); 4 | const fs = require('fs'); 5 | const _ = require('lodash'); 6 | 7 | const ONLY_ON_BRANCH = 'origin/master'; 8 | const VERSION_TAG = 'latest'; 9 | const VERSION_INC = 'patch'; 10 | 11 | function run() { 12 | if (!validateEnv()) { 13 | return; 14 | } 15 | setupGit(); 16 | createNpmRc(); 17 | versionTagAndPublish(); 18 | } 19 | 20 | function validateEnv() { 21 | if (!process.env.JENKINS_CI) { 22 | throw new Error(`releasing is only available from CI`); 23 | } 24 | 25 | if (!process.env.JENKINS_MASTER) { 26 | console.log(`not publishing on a different build`); 27 | return false; 28 | } 29 | 30 | if (process.env.GIT_BRANCH !== ONLY_ON_BRANCH) { 31 | console.log(`not publishing on branch ${process.env.GIT_BRANCH}`); 32 | return false; 33 | } 34 | 35 | return true; 36 | } 37 | 38 | function setupGit() { 39 | exec.execSyncSilent(`git config --global push.default simple`); 40 | exec.execSyncSilent(`git config --global user.email "${process.env.GIT_EMAIL}"`); 41 | exec.execSyncSilent(`git config --global user.name "${process.env.GIT_USER}"`); 42 | const remoteUrl = new RegExp(`https?://(\\S+)`).exec(exec.execSyncRead(`git remote -v`))[1]; 43 | exec.execSyncSilent(`git remote add deploy "https://${process.env.GIT_USER}:${process.env.GIT_TOKEN}@${remoteUrl}"`); 44 | exec.execSync(`git checkout ${ONLY_ON_BRANCH}`); 45 | } 46 | 47 | function createNpmRc() { 48 | exec.execSync(`rm -f package-lock.json`); 49 | const content = ` 50 | email=\${NPM_EMAIL} 51 | //registry.npmjs.org/:_authToken=\${NPM_TOKEN} 52 | `; 53 | fs.writeFileSync(`.npmrc`, content); 54 | } 55 | 56 | function versionTagAndPublish() { 57 | const packageVersion = semver.clean(process.env.npm_package_version); 58 | console.log(`package version: ${packageVersion}`); 59 | 60 | const currentPublished = findCurrentPublishedVersion(); 61 | console.log(`current published version: ${currentPublished}`); 62 | 63 | const version = semver.gt(packageVersion, currentPublished) ? packageVersion : semver.inc(currentPublished, VERSION_INC); 64 | tryPublishAndTag(version); 65 | } 66 | 67 | function findCurrentPublishedVersion() { 68 | return exec.execSyncRead(`npm view ${process.env.npm_package_name} dist-tags.${VERSION_TAG}`); 69 | } 70 | 71 | function tryPublishAndTag(version) { 72 | let theCandidate = version; 73 | for (let retry = 0; retry < 5; retry++) { 74 | try { 75 | tagAndPublish(theCandidate); 76 | console.log(`Released ${theCandidate}`); 77 | return; 78 | } catch (err) { 79 | const alreadyPublished = _.includes(err.toString(), 'You cannot publish over the previously published version'); 80 | if (!alreadyPublished) { 81 | throw err; 82 | } 83 | console.log(`previously published. retrying with increased ${VERSION_INC}...`); 84 | theCandidate = semver.inc(theCandidate, VERSION_INC); 85 | } 86 | } 87 | } 88 | 89 | function tagAndPublish(newVersion) { 90 | console.log(`trying to publish ${newVersion}...`); 91 | exec.execSync(`npm --no-git-tag-version --allow-same-version version ${newVersion}`); 92 | exec.execSyncRead(`npm publish --tag ${VERSION_TAG}`); 93 | exec.execSync(`git tag -a ${newVersion} -m "${newVersion}"`); 94 | exec.execSyncSilent(`git push deploy ${newVersion} || true`); 95 | } 96 | 97 | run(); 98 | -------------------------------------------------------------------------------- /how-it-works.md: -------------------------------------------------------------------------------- 1 | # How it works 2 | 3 | Consider this example: 4 | 5 | ```js 6 | function wait(ms) { 7 | let s = Date.now(); 8 | while(Date.now() - s < ms) { /* do nothing */ } 9 | } 10 | 11 | // Imagine this is the entry point for one of the modules 12 | function myFunc() { 13 | wait(500); 14 | setTimeout(() => wait(500), 10); 15 | Promise.resolve().then(() => wait(500)); 16 | fetch('some').then(() => wait(500)); 17 | } 18 | ``` 19 | 20 | How can the performance impact of `myFunc` can be measured? 21 | 22 | The **first naive approach** is to simply measure the execution time of that function: 23 | 24 | ```js 25 | var totalTime = 0; 26 | 27 | function track(fn) { 28 | let s = Date.now(); 29 | fn(); 30 | const delta = Date.now() - s; 31 | totalTime += delta; 32 | } 33 | 34 | track(() => myFunc()); 35 | 36 | 37 | console.log(totalTime); // 500ms 38 | ``` 39 | 40 | 41 | ## Problem 42 | 43 | Async callbacks are not measured. 44 | 45 | ## Solution 46 | 47 | **1. Patch react-native timers** 48 | 49 | ```js 50 | var oritinalSetTimeout = setTimeout; 51 | setTimeout = (cb, ms) => 52 | originalSetTimeout(() => track(cb), ms); 53 | 54 | track(() => myFunc()); 55 | 56 | 57 | console.log(totalTime); // 1000ms 58 | ``` 59 | 60 | Now `setTimeout` callback is tracked, but `Promise.resolve` is not, 61 | because it uses `setImmediate` under the hood. 62 | Let’s patch other timers: 63 | 64 | ```js 65 | patchTimer('setTimeout'); 66 | patchTimer('setInterval'); 67 | patchTimer('setImmediate'); 68 | patchTimer('requestAnimationFrame'); 69 | patchTimer('requestIdleCallback'); 70 | track(() => myFunc()); 71 | 72 | 73 | console.log(totalTime); // 1500ms 74 | ``` 75 | 76 | Good, all the jobs, scheduled for later are taken into account. 77 | 78 | **2. Patch callbacks to native** 79 | When using native APIs we provide some callbacks and wait for native side to invoke them. 80 | Most of them are done via `BatchedBridge.enqueueNativeCall` and `NativeEventEmitter#addListener`. 81 | 82 | ```js 83 | const otiginalEnqueueNativeCall = BatchedBridge.enqueueNativeCall.bind(BatchedBridge); 84 | 85 | BatchedBridge.enqueueNativeCall = (moduleID, methodID, args, resolve, reject) => { 86 | return otiginalEnqueueNativeCall( 87 | moduleID, 88 | methodID, 89 | args, 90 | () => track(resolve), 91 | () => track(reject), 92 | ); 93 | }; 94 | 95 | track(() => myFunc()); 96 | 97 | 98 | console.log(totalTime); // 2000ms 99 | ``` 100 | 101 | Perfect! 102 | 103 | **3. What about React components, registered by that method?** 104 | Our module can register some React components that will be used later elsewhere, 105 | it’s good to track everything it’s performing as part of the measured function. 106 | This is where things get trickier, but I looks like patching `AppRegistry.registerComponent`, 107 | `AppRegistry.registerRunnable` & &`AppRegistry.runApplication` can help to achieve this. 108 | 109 | 110 | ## FAQ 111 | 112 | **Does it track rendering? Should ReactComponent#render() be patched?** 113 | All the subsequent invocations any JS methods should be tracked by this solution, `render()` is no different in that sense. But it’s needed to mention, that sometimes React delays updating the components tree to let other jobs performed. Then a signal to continue updating is sent from native. Those delayed jobs are not tracked for now. As well as some callbacks related to animations. 114 | 115 | **Does this solution guarantee to track everything?** 116 | No. I can imaging there are still some APIs that are not taken into consideration and can break this mechanism. Please tell me if you have found one. 117 | 118 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | scripts/ 3 | remx-usage-example/ 4 | 5 | ################# 6 | # from .gitignore: 7 | ################ 8 | 9 | ############ 10 | # Node 11 | ############ 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (http://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules 42 | jspm_packages 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | 50 | ################ 51 | # JetBrains 52 | ################ 53 | .idea 54 | 55 | ## File-based project format: 56 | *.iws 57 | 58 | ## Plugin-specific files: 59 | 60 | # IntelliJ 61 | /out/ 62 | 63 | # mpeltonen/sbt-idea plugin 64 | .idea_modules/ 65 | 66 | # JIRA plugin 67 | atlassian-ide-plugin.xml 68 | 69 | # Crashlytics plugin (for Android Studio and IntelliJ) 70 | com_crashlytics_export_strings.xml 71 | crashlytics.properties 72 | crashlytics-build.properties 73 | fabric.properties 74 | 75 | 76 | ############ 77 | # iOS 78 | ############ 79 | # Xcode 80 | # 81 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 82 | 83 | ## Build generated 84 | ios/build/ 85 | ios/DerivedData/ 86 | 87 | ## Various settings 88 | *.pbxuser 89 | !default.pbxuser 90 | *.mode1v3 91 | !default.mode1v3 92 | *.mode2v3 93 | !default.mode2v3 94 | *.perspectivev3 95 | !default.perspectivev3 96 | ios/xcuserdata/ 97 | 98 | ## Other 99 | *.moved-aside 100 | *.xcuserstate 101 | 102 | ## Obj-C/Swift specific 103 | *.hmap 104 | *.ipa 105 | *.dSYM.zip 106 | *.dSYM 107 | 108 | # CocoaPods 109 | # 110 | # We recommend against adding the Pods directory to your .gitignore. However 111 | # you should judge for yourself, the pros and cons are mentioned at: 112 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 113 | # 114 | ios/Pods/ 115 | 116 | # Carthage 117 | # 118 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 119 | # Carthage/Checkouts 120 | 121 | Carthage/Build 122 | 123 | # fastlane 124 | # 125 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 126 | # screenshots whenever they are needed. 127 | # For more information about the recommended setup visit: 128 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 129 | 130 | fastlane/report.xml 131 | fastlane/screenshots 132 | 133 | 134 | ############ 135 | # Android 136 | ############ 137 | # Built application files 138 | *.apk 139 | *.ap_ 140 | 141 | # Files for the Dalvik VM 142 | *.dex 143 | 144 | # Java class files 145 | *.class 146 | 147 | # Generated files 148 | android/bin/ 149 | android/gen/ 150 | android/out/ 151 | 152 | # Gradle files 153 | .gradle/ 154 | android/.gradle/ 155 | android/build/ 156 | android/*/build/ 157 | 158 | # Local configuration file (sdk path, etc) 159 | local.properties 160 | 161 | # Proguard folder generated by Eclipse 162 | android/proguard/ 163 | 164 | # Log Files 165 | *.log 166 | 167 | # Android Studio Navigation editor temp files 168 | android/.navigation/ 169 | 170 | # Android Studio captures folder 171 | android/captures/ 172 | 173 | # Intellij 174 | *.iml 175 | 176 | # Keystore files 177 | *.jks 178 | 179 | ################## 180 | # React-Native 181 | ################## 182 | # OSX 183 | # 184 | .DS_Store 185 | 186 | # Xcode 187 | # 188 | build/ 189 | *.pbxuser 190 | !default.pbxuser 191 | *.mode1v3 192 | !default.mode1v3 193 | *.mode2v3 194 | !default.mode2v3 195 | *.perspectivev3 196 | !default.perspectivev3 197 | xcuserdata 198 | *.xccheckout 199 | *.moved-aside 200 | DerivedData 201 | *.hmap 202 | *.ipa 203 | *.xcuserstate 204 | project.xcworkspace 205 | 206 | # Android/IJ 207 | # 208 | .idea 209 | android/.idea 210 | android/.gradle 211 | android/local.properties 212 | 213 | # node.js 214 | # 215 | node_modules/ 216 | npm-debug.log 217 | 218 | # BUCK 219 | buck-out/ 220 | \.buckd/ 221 | android/app/libs 222 | android/keystores/debug.keystore 223 | -------------------------------------------------------------------------------- /src/JSPerfProfiler.js: -------------------------------------------------------------------------------- 1 | import {Event} from 'detox-instruments-react-native-utils'; 2 | 3 | let __ENABLED__ = false; 4 | 5 | const contextStack = []; 6 | 7 | const accTimeByContext = {}; 8 | 9 | let timer; 10 | let previousMessage; 11 | 12 | export const executeInContext = (context, message, fn, ...args) => { 13 | if (!accTimeByContext[context]) { 14 | accTimeByContext[context] = {}; 15 | } 16 | const prevContext = getContext(); 17 | if (prevContext) { 18 | if (!accTimeByContext[prevContext][previousMessage]){ 19 | accTimeByContext[prevContext][previousMessage] = 0 20 | } 21 | accTimeByContext[prevContext][previousMessage] += (now() - timer); 22 | } 23 | previousMessage = message 24 | timer = now(); 25 | contextStack.push(context); 26 | const result = fn(...args); 27 | contextStack.pop(); 28 | if (!accTimeByContext[context][message]){ 29 | accTimeByContext[context][message] = 0 30 | } 31 | accTimeByContext[context][message] += (now() - timer); 32 | timer = now(); 33 | previousMessage = message 34 | return result; 35 | }; 36 | 37 | const bindContext = (context,message, fn) => (...args) => executeInContext(context, message, fn, ...args); 38 | 39 | export const getContext = () => contextStack[contextStack.length - 1]; 40 | export const getPerfInfo = () => accTimeByContext; 41 | export const clearPerfInfo = () => { 42 | Object.keys(accTimeByContext).forEach((k) => accTimeByContext[k] = 0); 43 | } 44 | 45 | export const timeAndLog = (fn, message, context, scope = 'General') => { 46 | /* istanbul ignore else */ 47 | if (__ENABLED__) { 48 | const event = new Event(scope, message); 49 | event.beginInterval(`${message} [${context}]`); 50 | const result = executeInContext(context,message, fn); 51 | event.endInterval(Event.EventStatus.completed); 52 | return result; 53 | } else { 54 | return fn(); 55 | } 56 | } 57 | 58 | export const attach = () => { 59 | if (__ENABLED__) { 60 | return; 61 | } 62 | 63 | attachRequire(); 64 | patchTimer('setTimeout'); 65 | patchTimer('setInterval'); 66 | patchTimer('setImmediate'); 67 | patchTimer('requestAnimationFrame'); 68 | patchTimer('requestIdleCallback'); 69 | patchBridge(); 70 | patchEventEmitter(); 71 | patchAppRegistry(); 72 | patchAnimated(); 73 | 74 | __ENABLED__ = true; 75 | }; 76 | 77 | export const attachRequire = () => { 78 | const eventsStack = []; 79 | /* istanbul ignore else */ 80 | if (require.Systrace && Event) { 81 | require.Systrace.beginEvent = (message) => { 82 | const context = contextStack.join('->'); 83 | const skip = !message || message.indexOf('JS_require_') !== 0; 84 | message = message && message.substr('JS_require_'.length); 85 | const event = !skip && new Event('Systrace', `require()`); 86 | const finalDescription = `${message} ([${context}])`; 87 | eventsStack.push({event, skip}); 88 | if (!skip) { 89 | event.beginInterval(finalDescription); 90 | } 91 | }; 92 | require.Systrace.endEvent = () => { 93 | const event = eventsStack.pop(); 94 | if (event && !event.skip) { 95 | event.event.endInterval(); 96 | } 97 | }; 98 | } 99 | } 100 | 101 | const patchTimer = (timerName) => { 102 | const JSTimers = require('react-native/Libraries/Core/Timers/JSTimers'); 103 | const originalTimer = JSTimers[timerName]; 104 | const patchedTimer = (fn, ...args) => { 105 | let context = getContext(); 106 | // if (!context) { debugger; }; 107 | return originalTimer((...a) => executeInContext( 108 | context, 109 | timerName, 110 | () => fn(...a), 111 | ), ...args); 112 | }; 113 | JSTimers[timerName] = patchedTimer; 114 | defineProperty(global, timerName, patchedTimer); 115 | }; 116 | 117 | const patchAnimated = () => { 118 | const NativeAnimatedHelper = require('react-native/Libraries/Animated/NativeAnimatedHelper'); 119 | const orig = NativeAnimatedHelper.API.startAnimatingNode; 120 | NativeAnimatedHelper.API.startAnimatingNode = (node, nodeTag, config, endCallback) => { 121 | let context = getContext(); 122 | if (!context) { 123 | context = 'untrackableAnimation'; 124 | }; 125 | return orig(node, nodeTag, config, bindContext(context, "NativeAnimatedHelper" , endCallback)); 126 | } 127 | } 128 | 129 | const patchBridge = () => { 130 | const BatchedBridge = require('react-native/Libraries/BatchedBridge/BatchedBridge'); 131 | const orig = BatchedBridge.enqueueNativeCall.bind(BatchedBridge); 132 | if (!BatchedBridge._remoteModuleTable || !BatchedBridge._remoteMethodTable) { 133 | return; 134 | } 135 | BatchedBridge.enqueueNativeCall = (moduleID, methodID, args, resolve, reject) => { 136 | let context = getContext(); 137 | const moduleName = BatchedBridge._remoteModuleTable[moduleID] 138 | const methodName = BatchedBridge._remoteMethodTable[moduleID][methodID] 139 | // if ((resolve || reject) && !context) { debugger; }; 140 | return orig( 141 | moduleID, 142 | methodID, 143 | args, 144 | resolve && bindContext(context, `${moduleName}: ${methodName}`, resolve), 145 | reject && bindContext(context, `${moduleName}: ${methodName}` , reject), 146 | ); 147 | }; 148 | }; 149 | 150 | const patchEventEmitter = () => { 151 | const NativeEventEmitter = require('react-native/Libraries/EventEmitter/NativeEventEmitter').default; 152 | const orig = NativeEventEmitter.prototype.addListener; 153 | 154 | NativeEventEmitter.prototype.addListener = function (eventType, listener, ...a) { 155 | let context = getContext(); 156 | // if (!context) { debugger; }; 157 | return orig.call( 158 | this, 159 | eventType, 160 | bindContext(context, "NativeEventEmitter", listener), 161 | ...a 162 | ); 163 | }; 164 | }; 165 | 166 | const patchAppRegistry = () => { 167 | const {AppRegistry} = require('react-native'); 168 | 169 | const contextsByAppKeys = {}; 170 | 171 | ['registerComponent', 'registerRunnable'].forEach((regName) => { 172 | const orig = AppRegistry[regName]; 173 | AppRegistry[regName] = (appKey, ...props) => { 174 | let context = getContext(); 175 | // if (!context) { debugger; }; 176 | contextsByAppKeys[appKey] = context; 177 | return orig(appKey, ...props); 178 | }; 179 | }); 180 | 181 | const origRun = AppRegistry.runApplication; 182 | AppRegistry.runApplication = (appKey, ...args) => { 183 | let context = contextsByAppKeys[appKey]; 184 | // if (!context) { debugger; }; 185 | return executeInContext(context, "patchAppRegistry", origRun, appKey, ...args); 186 | }; 187 | }; 188 | 189 | const defineProperty = (object, name, value) => { 190 | const descriptor = Object.getOwnPropertyDescriptor(object, name); 191 | if (descriptor) { 192 | const backupName = `originalRN${name[0].toUpperCase()}${name.substr(1)}`; 193 | Object.defineProperty(object, backupName, descriptor); 194 | } 195 | 196 | const {enumerable, writable, configurable} = descriptor || {}; 197 | /* istanbul ignore next */ 198 | if (descriptor && !configurable) { 199 | console.error('Failed to attach profiler. ' + name + ' is not configurable.'); 200 | return; 201 | } 202 | 203 | Object.defineProperty(object, name, { 204 | value, 205 | enumerable: enumerable !== false, 206 | writable: writable !== false, 207 | configurable: true, 208 | }); 209 | }; 210 | 211 | const now = (() => { 212 | if (typeof performance === 'object' && performance.now) { 213 | return performance.now.bind(performance); 214 | } 215 | return Date.now.bind(Date); 216 | })() 217 | 218 | // Used in unit tests 219 | export const $require = require; 220 | -------------------------------------------------------------------------------- /src/JSPerfProfiler.test.js: -------------------------------------------------------------------------------- 1 | const mockEventConstructor = jest.fn(); 2 | const mockBeginInterval = jest.fn(); 3 | const mockEndInterval = jest.fn(); 4 | function mockEvent(...args) { 5 | mockEventConstructor(...args); 6 | this.beginInterval = mockBeginInterval; 7 | this.endInterval = mockEndInterval; 8 | } 9 | mockEvent.EventStatus = {}; 10 | const mockTimeout = setTimeout; 11 | 12 | jest.mock('detox-instruments-react-native-utils', () => ({ 13 | Event: mockEvent, 14 | })); 15 | 16 | jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => ({ 17 | __esModule: true, 18 | default: require('events') 19 | })); 20 | 21 | jest.mock('react-native', () => { 22 | const apps = {}; 23 | return { 24 | AppRegistry: { 25 | registerComponent: (appKey, fn) => apps[appKey] = fn, 26 | runApplication: (appKey) => apps[appKey](), 27 | } 28 | }; 29 | }); 30 | 31 | jest.mock('react-native/Libraries/BatchedBridge/BatchedBridge', () => { 32 | let cb; 33 | return { 34 | enqueueNativeCall: (name, $$, $$$, onSuccess, onFail) => { 35 | cb = onSuccess; 36 | }, 37 | __mockInvokeCallback: () => cb(), 38 | _remoteModuleTable:{"0" : "moduleName"}, 39 | _remoteMethodTable:{"0" : ["methodName"]} 40 | }; 41 | }); 42 | 43 | jest.mock('react-native/Libraries/Core/Timers/JSTimers', () => ({ 44 | setTimeout: mockTimeout, 45 | })); 46 | 47 | jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper', () => { 48 | let cb; 49 | return { 50 | API: { 51 | startAnimatingNode: (node, nodeTag, config, endCallback) => { 52 | cb = endCallback; 53 | }, 54 | }, 55 | __mockInvokeCallback: () => cb(), 56 | } 57 | }); 58 | 59 | describe('JSPerfProfiler', () => { 60 | let JSPerfProfiler; 61 | 62 | beforeEach(() => { 63 | jest.resetModules(); 64 | JSPerfProfiler = require('./JSPerfProfiler'); 65 | JSPerfProfiler.$require.Systrace = {}; 66 | JSPerfProfiler.attach(); 67 | }); 68 | 69 | it('Should ignore unmatching end events', () => { 70 | JSPerfProfiler.attach(); 71 | JSPerfProfiler.$require.Systrace.endEvent(); 72 | JSPerfProfiler.$require.Systrace.endEvent(); 73 | JSPerfProfiler.$require.Systrace.endEvent(); 74 | expect(mockBeginInterval).not.toHaveBeenCalled(); 75 | expect(mockEndInterval).not.toHaveBeenCalled(); 76 | }); 77 | 78 | it('Should create events for DetoxInstruments', () => { 79 | JSPerfProfiler.attach(); 80 | JSPerfProfiler.$require.Systrace.beginEvent('JS_require_'); 81 | expect(mockBeginInterval).toHaveBeenCalledWith(' ([])'); 82 | JSPerfProfiler.$require.Systrace.endEvent(); 83 | expect(mockEndInterval).toHaveBeenCalledTimes(1); 84 | expect(mockEventConstructor.mock.calls).toEqual([ 85 | ['Systrace', 'require()'] 86 | ]); 87 | }); 88 | 89 | it('should filter events', () => { 90 | JSPerfProfiler.attach(); 91 | JSPerfProfiler.$require.Systrace.beginEvent('Some'); 92 | expect(mockBeginInterval).toHaveBeenCalledTimes(0); 93 | JSPerfProfiler.$require.Systrace.beginEvent('JS_require_'); 94 | expect(mockBeginInterval).toHaveBeenCalledTimes(1); 95 | JSPerfProfiler.$require.Systrace.beginEvent('Any'); 96 | expect(mockBeginInterval).toHaveBeenCalledTimes(1); 97 | JSPerfProfiler.$require.Systrace.beginEvent('JS_require_'); 98 | expect(mockBeginInterval).toHaveBeenCalledTimes(2); 99 | JSPerfProfiler.$require.Systrace.beginEvent('Other'); 100 | expect(mockBeginInterval).toHaveBeenCalledTimes(2); 101 | JSPerfProfiler.$require.Systrace.endEvent(); 102 | expect(mockEndInterval).toHaveBeenCalledTimes(0); 103 | JSPerfProfiler.$require.Systrace.endEvent(); 104 | expect(mockEndInterval).toHaveBeenCalledTimes(1); 105 | JSPerfProfiler.$require.Systrace.endEvent(); 106 | expect(mockEndInterval).toHaveBeenCalledTimes(1); 107 | JSPerfProfiler.$require.Systrace.endEvent(); 108 | expect(mockEndInterval).toHaveBeenCalledTimes(2); 109 | JSPerfProfiler.$require.Systrace.endEvent(); 110 | JSPerfProfiler.$require.Systrace.endEvent(); 111 | JSPerfProfiler.$require.Systrace.endEvent(); 112 | expect(mockEndInterval).toHaveBeenCalledTimes(2); 113 | expect(mockBeginInterval).toHaveBeenCalledTimes(2); 114 | }); 115 | 116 | it('should time and log events', () => { 117 | JSPerfProfiler.timeAndLog(() => {}, 'testMessage', 'testModule'); 118 | expect(mockEventConstructor.mock.calls).toEqual([ 119 | ['General', 'testMessage'], 120 | ]); 121 | expect(mockBeginInterval).toHaveBeenCalledWith('testMessage [testModule]'); 122 | expect(mockEndInterval).toHaveBeenCalledWith(undefined); 123 | }); 124 | 125 | 126 | it('should return results from time and log events', () => { 127 | const result = JSPerfProfiler.timeAndLog(() => 'RESULT', 'testMessage', 'testModule'); 128 | expect(result).toEqual('RESULT'); 129 | }); 130 | 131 | it('Should track context for timers', (done) => { 132 | const JSTimers = require('react-native/Libraries/Core/Timers/JSTimers'); 133 | JSPerfProfiler.executeInContext('testContext', "message", () => { 134 | JSTimers.setTimeout(() => { 135 | expect(JSPerfProfiler.getContext()).toBe('testContext'); 136 | expect(JSPerfProfiler.getPerfInfo()["testContext"]).toHaveProperty('message'); 137 | done(); 138 | }, 1); 139 | }); 140 | }); 141 | 142 | it('Should track context for Bridge', (done) => { 143 | const BatchedBridge = require('react-native/Libraries/BatchedBridge/BatchedBridge'); 144 | // BatchedBridge.createDebugLookup(1, 'test', ['a']) 145 | JSPerfProfiler.executeInContext('testContext',"message", () => { 146 | BatchedBridge.enqueueNativeCall( 147 | 0, 148 | 0, 149 | [], 150 | () => { 151 | expect(JSPerfProfiler.getContext()).toBe('testContext'); 152 | expect(JSPerfProfiler.getPerfInfo()["testContext"]).toHaveProperty('message'); 153 | done(); 154 | }, 155 | () => { 156 | expect(JSPerfProfiler.getContext()).toBe('testContext'); 157 | expect(JSPerfProfiler.getPerfInfo()["testContext"]).toHaveProperty('message'); 158 | done(); 159 | }, 160 | ); 161 | }); 162 | BatchedBridge.__mockInvokeCallback(); 163 | }); 164 | 165 | it('Should track context for EventEmitter', (done) => { 166 | const NativeEventEmitter = require('react-native/Libraries/EventEmitter/NativeEventEmitter').default; 167 | const emitter = new NativeEventEmitter(); 168 | JSPerfProfiler.executeInContext('testContext',"message", () => { 169 | emitter.addListener('tev', () => { 170 | expect(JSPerfProfiler.getContext()).toBe('testContext'); 171 | expect(JSPerfProfiler.getPerfInfo()["testContext"]).toHaveProperty('message'); 172 | done(); 173 | }, 1); 174 | }); 175 | emitter.emit('tev'); 176 | }); 177 | 178 | it('Should track context for AppRegistry', (done) => { 179 | const {AppRegistry} = require('react-native'); 180 | JSPerfProfiler.executeInContext('testContext',"message", () => { 181 | AppRegistry.registerComponent('TestApp', () => { 182 | expect(JSPerfProfiler.getContext()).toBe('testContext'); 183 | expect(JSPerfProfiler.getPerfInfo()["testContext"]).toHaveProperty('message'); 184 | done(); 185 | }); 186 | }); 187 | AppRegistry.runApplication('TestApp'); 188 | }); 189 | 190 | it('Should track context for Animated', (done) => { 191 | const NativeAnimatedHelper = require('react-native/Libraries/Animated/NativeAnimatedHelper'); 192 | JSPerfProfiler.executeInContext('testContext',"message", () => { 193 | NativeAnimatedHelper.API.startAnimatingNode('', '', '', () => { 194 | expect(JSPerfProfiler.getContext()).toBe('testContext'); 195 | expect(JSPerfProfiler.getPerfInfo()["testContext"]).toHaveProperty('message'); 196 | done(); 197 | }); 198 | }); 199 | NativeAnimatedHelper.__mockInvokeCallback(); 200 | }); 201 | }); 202 | 203 | --------------------------------------------------------------------------------