├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── convertedProfile.png └── loading.png ├── package.json ├── src ├── __test__ │ ├── CpuProfilerModel.test.ts │ ├── EventInterfaces.test.ts │ └── Phases.test.ts ├── index.ts ├── profiler │ ├── applySourceMapsToEvents.ts │ └── cpuProfilerModel.ts ├── types │ ├── CPUProfile.ts │ ├── EventInterfaces.ts │ ├── HermesProfile.ts │ ├── Phases.ts │ └── SourceMap.ts └── utils │ └── fileSystem.ts ├── tsconfig.json └── yarn.lock /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | 7 | steps: 8 | - name: Begin CI... 9 | uses: actions/checkout@v2 10 | 11 | - name: Use Node 8 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: 8.x 15 | 16 | - name: Use cached node_modules 17 | uses: actions/cache@v1 18 | with: 19 | path: node_modules 20 | key: nodeModules-${{ hashFiles('**/yarn.lock') }} 21 | restore-keys: | 22 | nodeModules- 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile --ignore-engines 26 | env: 27 | CI: true 28 | 29 | - name: Lint 30 | run: yarn lint 31 | env: 32 | CI: true 33 | 34 | - name: Test 35 | run: yarn test --ci --coverage --maxWorkers=2 36 | env: 37 | CI: true 38 | 39 | - name: Build 40 | run: yarn build 41 | env: 42 | CI: true 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Generated Profiles 19 | *.cpuprofile 20 | 21 | # Files specifically for debugging 22 | *.debug.ts 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | 84 | # Next.js build output 85 | .next 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | 112 | # Random Files 113 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Saphal Patro and Jessie Anh Nguyen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Hermes Profile Transformer 3 |

4 | 5 |

6 | npm 7 | node-current 8 | npm bundle size 9 | NPM 10 | npm type definitions 11 |

12 | 13 | Visualize Facebook's [Hermes JavaScript runtime](https://github.com/facebook/hermes) profile traces in Chrome Developer Tools. 14 | 15 | ![Demo Profile](https://raw.githubusercontent.com/react-native-community/hermes-profile-transformer/master/assets/convertedProfile.png) 16 | 17 | ## Overview 18 | 19 | The Hermes runtime, used by React Native for Android, is able to output [Chrome Trace Events](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview) in JSON Object Format. 20 | 21 | This TypeScript package converts Hermes CPU profiles to Chrome Developer Tools compatible JSON Array Format, and enriches it with line and column numbers and event categories from JavaScript source maps. 22 | 23 | ## Usage 24 | 25 | If you're using `hermes-profile-transformer` to debug React Native Android applications, you can use the [React Native CLI](https://github.com/react-native-community/cli) `react-native profile-hermes` command, which uses this package to convert the downloaded Hermes profiles automatically. 26 | 27 | ### As a standalone package 28 | 29 | ```js 30 | const transformer = require('hermes-profile-transformer').default; 31 | const { writeFileSync } = require('fs'); 32 | 33 | const hermesCpuProfilePath = './testprofile.cpuprofile'; 34 | const sourceMapPath = './index.map'; 35 | const sourceMapBundleFileName = 'index.bundle.js'; 36 | 37 | transformer( 38 | // profile path is required 39 | hermesCpuProfilePath, 40 | // source maps are optional 41 | sourceMap, 42 | sourceMapBundleFileName 43 | ) 44 | .then(events => { 45 | // write converted trace to a file 46 | return writeFileSync( 47 | './chrome-supported.json', 48 | JSON.stringify(events, null, 2), 49 | 'utf-8' 50 | ); 51 | }) 52 | .catch(err => { 53 | console.log(err); 54 | }); 55 | ``` 56 | 57 | ## Creating Hermes CPU Profiles 58 | 59 | ## Opening converted profiles in Chrome Developer Tools 60 | 61 | Open Developer Tools in Chrome, navigate to the **Performance** tab, and use the **Load profile...** feature. 62 | 63 | ![Loading the Profile](https://raw.githubusercontent.com/react-native-community/hermes-profile-transformer/master/assets/loading.png) 64 | 65 | ## API 66 | 67 | ### transformer(profilePath: string, sourceMapPath?: string, bundleFileName?: string) 68 | 69 | #### Parameters 70 | 71 | | Parameter | Type | Required | Description | 72 | | -------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 73 | | profilePath | string | Yes | Path to a JSON-formatted `.cpuprofile` file created by the Hermes runtime | 74 | | sourceMapPath | string | No | Path to a [source-map](https://www.npmjs.com/package/source-map) compatible Source Map file | 75 | | bundleFileName | string | No | If `sourceMapPath` is provided, you need to also provide the name of the JavaScript bundle file that the source map applies to. This file does not need to exist on disk. | 76 | 77 | #### Returns 78 | 79 | `Promise`, where `DurationEvent` is as defined in [EventInterfaces.ts](src/types/EventInterfaces.ts). 80 | 81 | ## Resources 82 | 83 | - [Using Hermes with React Native](https://reactnative.dev/docs/hermes). 84 | - [Chrome Trace Event Format](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview). Hermes uses the JSON Object format. 85 | - [Measuring JavaScript performance in Chrome](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference) 86 | 87 | ## LICENSE 88 | 89 | [MIT](LICENSE) 90 | -------------------------------------------------------------------------------- /assets/convertedProfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/hermes-profile-transformer/8349bca4f147b45c437dfcd035e39b76fc99272d/assets/convertedProfile.png -------------------------------------------------------------------------------- /assets/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/hermes-profile-transformer/8349bca4f147b45c437dfcd035e39b76fc99272d/assets/loading.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.9", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "engines": { 11 | "node": ">=8" 12 | }, 13 | "scripts": { 14 | "start": "tsdx watch", 15 | "build": "tsdx build", 16 | "test": "tsdx test", 17 | "lint": "tsdx lint", 18 | "prepare": "tsdx build" 19 | }, 20 | "peerDependencies": {}, 21 | "husky": { 22 | "hooks": { 23 | "pre-commit": "tsdx lint" 24 | } 25 | }, 26 | "prettier": { 27 | "printWidth": 80, 28 | "semi": true, 29 | "singleQuote": true, 30 | "trailingComma": "es5" 31 | }, 32 | "name": "hermes-profile-transformer", 33 | "author": { 34 | "name": "Saphal Patro", 35 | "email": "saphal1998@gmail.com", 36 | "url": "http://github.com/saphal1998" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/react-native-community/hermes-profile-transformer" 41 | }, 42 | "module": "dist/hermes-tracing-profile-transformer.esm.js", 43 | "devDependencies": { 44 | "husky": "^4.2.5", 45 | "tsdx": "^0.13.2", 46 | "tslib": "^2.0.0", 47 | "typescript": "^3.9.5" 48 | }, 49 | "dependencies": { 50 | "source-map": "^0.7.3" 51 | }, 52 | "keywords": [ 53 | "profiling", 54 | "hermes", 55 | "transformation", 56 | "transformers", 57 | "dev-tools", 58 | "react-native", 59 | "react-native-community", 60 | "react-native-cli" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /src/__test__/CpuProfilerModel.test.ts: -------------------------------------------------------------------------------- 1 | import { CpuProfilerModel } from '../profiler/cpuProfilerModel'; 2 | import { HermesCPUProfile } from '../types/HermesProfile'; 3 | import { EventsPhase } from '../types/Phases'; 4 | import { CPUProfileChunk } from 'types/CPUProfile'; 5 | 6 | const uniq = (arr: T[]) => { 7 | return Array.from(new Set(arr)); 8 | }; 9 | 10 | // prettier-ignore 11 | const sampleProfile: HermesCPUProfile = { 12 | traceEvents: [], 13 | samples: [ 14 | { cpu: '-1', name: '', ts: '1', pid: 6052, tid: '6105', weight: '1', sf: 1, }, 15 | { cpu: '-1', name: '', ts: '2', pid: 6052, tid: '6105', weight: '1', sf: 3, }, 16 | { cpu: '-1', name: '', ts: '3', pid: 6052, tid: '6105', weight: '1', sf: 4, }, 17 | { cpu: '-1', name: '', ts: '4', pid: 6052, tid: '6105', weight: '1', sf: 5, }, 18 | { cpu: '-1', name: '', ts: '5', pid: 6052, tid: '6105', weight: '1', sf: 2, }, 19 | { cpu: '-1', name: '', ts: '6', pid: 6052, tid: '6105', weight: '1', sf: 1, }, 20 | { cpu: '-1', name: '', ts: '7', pid: 6052, tid: '6105', weight: '1', sf: 3, }, 21 | ], 22 | stackFrames: { 23 | '1': { line: '1', column: '1', funcLine: '1', funcColumn: '1', name: 'Func 1', category: 'JavaScript', }, 24 | '2': { line: '2', column: '2', funcLine: '2', funcColumn: '2', name: 'Func 2', category: 'Typescript', parent: 1, }, 25 | '3': { line: '3', column: '3', funcLine: '3', funcColumn: '3', name: 'Func 3', category: 'Python', parent: 2, }, 26 | '4': { line: '4', column: '4', funcLine: '4', funcColumn: '4', name: 'Func 4', category: 'C++', parent: 3, }, 27 | '5': { line: '5', column: '5', funcLine: '5', funcColumn: '5', name: 'Func 5', category: 'Swift', parent: 4, }, 28 | }, 29 | }; 30 | 31 | describe(CpuProfilerModel, () => { 32 | describe(CpuProfilerModel.prototype.createStartEndEvents, () => { 33 | /** 34 | * Visual Representation 35 | * ---------1----------- 36 | * -----2--------- 37 | * ----3---- 38 | * --4--- 39 | * -5- 40 | */ 41 | 42 | it('should create start and end events in order', () => { 43 | const profileChunk: CPUProfileChunk = CpuProfilerModel.collectProfileEvents( 44 | sampleProfile 45 | ); 46 | const profiler = new CpuProfilerModel(profileChunk); 47 | const chromeEvents = profiler.createStartEndEvents(); 48 | expect(chromeEvents).toMatchObject([ 49 | { ts: 1, ph: EventsPhase.DURATION_EVENTS_BEGIN }, 50 | { ts: 2, ph: EventsPhase.DURATION_EVENTS_BEGIN }, 51 | { ts: 2, ph: EventsPhase.DURATION_EVENTS_BEGIN }, 52 | { ts: 3, ph: EventsPhase.DURATION_EVENTS_BEGIN }, 53 | { ts: 4, ph: EventsPhase.DURATION_EVENTS_BEGIN }, 54 | { ts: 5, ph: EventsPhase.DURATION_EVENTS_END }, 55 | { ts: 5, ph: EventsPhase.DURATION_EVENTS_END }, 56 | { ts: 5, ph: EventsPhase.DURATION_EVENTS_END }, 57 | { ts: 6, ph: EventsPhase.DURATION_EVENTS_END }, 58 | { ts: 7, ph: EventsPhase.DURATION_EVENTS_BEGIN }, 59 | { ts: 7, ph: EventsPhase.DURATION_EVENTS_BEGIN }, 60 | { ts: 7, ph: EventsPhase.DURATION_EVENTS_END }, 61 | { ts: 7, ph: EventsPhase.DURATION_EVENTS_END }, 62 | { ts: 7, ph: EventsPhase.DURATION_EVENTS_END }, 63 | ]); 64 | }); 65 | 66 | it('should have a single process and thread Id', () => { 67 | const profileChunk = CpuProfilerModel.collectProfileEvents(sampleProfile); 68 | const profiler = new CpuProfilerModel(profileChunk); 69 | const chromeEvents = profiler.createStartEndEvents(); 70 | const uniqueProcessIds = uniq(chromeEvents.map(event => event.pid)); 71 | const uniqueThreadIds = uniq(chromeEvents.map(event => event.tid)); 72 | // Hermes stuff 73 | expect(uniqueProcessIds).toHaveLength(1); 74 | // Hermes stuff 75 | expect(uniqueThreadIds).toHaveLength(1); 76 | }); 77 | 78 | // Currently the implementation supports Duration Events, and the repo has interfaces defined for other event types as well. 79 | // Ideally, we would want to test for all event types, but for our current use cases Duration Events shall suffice. 80 | it('should only have events of type Duration Event', () => { 81 | const profileChunk = CpuProfilerModel.collectProfileEvents(sampleProfile); 82 | const profiler = new CpuProfilerModel(profileChunk); 83 | const chromeEvents = profiler.createStartEndEvents(); 84 | const uniquePhases = uniq(chromeEvents.map(event => event.ph)); 85 | expect(uniquePhases).toEqual( 86 | expect.arrayContaining([ 87 | EventsPhase.DURATION_EVENTS_BEGIN, 88 | EventsPhase.DURATION_EVENTS_END, 89 | ]) 90 | ); 91 | expect(uniquePhases).toHaveLength(2); 92 | }); 93 | }); 94 | 95 | describe(CpuProfilerModel.collectProfileEvents, () => { 96 | it('should create accurate Profile Chunks', () => { 97 | const profileChunk = CpuProfilerModel.collectProfileEvents(sampleProfile); 98 | expect(profileChunk).toMatchObject({ 99 | id: '0x1', 100 | pid: 6052, 101 | tid: '6105', 102 | startTime: 1, 103 | // prettier-ignore 104 | nodes: [ 105 | { id: 1, callFrame: { line: '1', column: '1', funcLine: '1', funcColumn: '1', name: 'Func 1', category: 'JavaScript', url: 'Func 1', }, }, 106 | { id: 2, callFrame: { line: '2', column: '2', funcLine: '2', funcColumn: '2', name: 'Func 2', category: 'Typescript', url: 'Func 2', }, parent: 1, }, 107 | { id: 3, callFrame: { line: '3', column: '3', funcLine: '3', funcColumn: '3', name: 'Func 3', category: 'Python', url: 'Func 3', }, parent: 2, }, 108 | { id: 4, callFrame: { line: '4', column: '4', funcLine: '4', funcColumn: '4', name: 'Func 4', category: 'C++', url: 'Func 4', }, parent: 3, }, 109 | { id: 5, callFrame: { line: '5', column: '5', funcLine: '5', funcColumn: '5', name: 'Func 5', category: 'Swift', url: 'Func 5', }, parent: 4, }, 110 | ], 111 | samples: [1, 3, 4, 5, 2, 1, 3], 112 | timeDeltas: [0, 1, 1, 1, 1, 1, 1], 113 | }); 114 | 115 | expect(profileChunk.samples.length).toEqual( 116 | profileChunk.timeDeltas.length 117 | ); 118 | 119 | // Would want at least start and end events at two different timestamps 120 | expect(profileChunk.samples.length).toBeGreaterThanOrEqual(2); 121 | 122 | // Events displayed in flamechart have timestamps relative to the profile 123 | // event's startTime. Source: https://github.com/v8/v8/blob/44bd8fd7/src/inspector/js_protocol.json#L1486 124 | expect(profileChunk.timeDeltas[0]).toEqual(0); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /src/__test__/EventInterfaces.test.ts: -------------------------------------------------------------------------------- 1 | import { Event, DurationEvent, FlowEvent } from '../types/EventInterfaces'; 2 | import { EventsPhase } from '../types/Phases'; 3 | 4 | /** 5 | * These tests are 50% about testing that the types are implemented correctly, 6 | * and 50% documenting how to handle mapping between Event types and subtypes 7 | * with and without EventsPhas literal. 8 | * 9 | * The tests rely on the @ts-expect-error pragma, which will pass the type check 10 | * if the following line has a type error, and will error if the following line is fine. 11 | */ 12 | describe('Event', () => { 13 | it('should allow constructing event objects using EventsPhase enum values', () => { 14 | // create a new flow event 15 | const event: DurationEvent = { 16 | ts: 1, 17 | ph: EventsPhase.DURATION_EVENTS_BEGIN, 18 | }; 19 | 20 | // check that value is correct in runtime 21 | expect(event).toEqual({ ts: 1, ph: 'B' }); 22 | }); 23 | it('should not allow constructing event objects using wrong enum values', () => { 24 | // try to create a new flow event, should fail with TypeScript error 25 | // @ts-expect-error 26 | const event: DurationEvent = { ts: 1, ph: EventsPhase.INSTANT_EVENTS }; 27 | 28 | // at runtime object is still created, but we should never be here 29 | expect(event).toEqual({ ts: 1, ph: 'I' }); 30 | }); 31 | 32 | it('should not allow constructing event objects with phase literal at type level', () => { 33 | // try to create a new flow event, should fail with TypeScript error 34 | // @ts-expect-error 35 | const event: DurationEvent = { ts: 'ts', ph: 's' }; 36 | 37 | // check that value is correct in runtime 38 | expect(event).toEqual({ ts: 'ts', ph: 's' }); 39 | }); 40 | 41 | it('should not allow coercing event objects with incorrect phase literal', () => { 42 | // try to create a new flow event, should fail with TypeScript error 43 | // @ts-expect-error 44 | const event: DurationEvent = { ts: 'ts', ph: 'NOT_s' } as DurationEvent; 45 | 46 | // check that value is correct in runtime 47 | expect(event).toEqual({ ts: 'ts', ph: 'NOT_s' }); 48 | }); 49 | 50 | it('should allow polymorphic lists of different event types', () => { 51 | const flow: FlowEvent = { ts: 1, ph: EventsPhase.FLOW_EVENTS_END }; 52 | const duration: DurationEvent = { 53 | ts: 1, 54 | ph: EventsPhase.DURATION_EVENTS_END, 55 | }; 56 | 57 | // should not type error 58 | const events: Event[] = [flow, duration]; 59 | 60 | expect(events).toEqual([ 61 | { ts: 1, ph: 'f' }, 62 | { ts: 1, ph: 'E' }, 63 | ]); 64 | }); 65 | 66 | it('should not allow polymorphic lists where any value is not a valid event type', () => { 67 | const durationEnd: DurationEvent = { 68 | ts: 1, 69 | ph: EventsPhase.DURATION_EVENTS_END, 70 | }; 71 | const durationBegin: DurationEvent = { 72 | ts: 1, 73 | ph: EventsPhase.DURATION_EVENTS_BEGIN, 74 | }; 75 | const invalid = { 76 | ts: 'ts', 77 | ph: 'invalid', 78 | }; 79 | 80 | // @ts-expect-error 81 | const events: Event[] = [durationEnd, durationBegin, invalid]; 82 | 83 | expect(events).toEqual([ 84 | { ts: 1, ph: 'E' }, 85 | { ts: 1, ph: 'B' }, 86 | { ts: 'ts', ph: 'invalid' }, 87 | ]); 88 | }); 89 | 90 | it('should support type guards', () => { 91 | // If we want to ensure that a type is *actually* of the type 92 | // we want it to be instead of relying on type coercion/casting, 93 | // we can use a type guard 94 | // 95 | // See: https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards 96 | function isDurationEvent(event: any): event is DurationEvent { 97 | return ( 98 | event.ph === EventsPhase.DURATION_EVENTS_BEGIN || 99 | event.ph === EventsPhase.DURATION_EVENTS_END 100 | ); 101 | } 102 | 103 | // This function expects a duration event 104 | function expectsDurationEvent(event: DurationEvent): any { 105 | return event.ph; 106 | } 107 | 108 | // This 109 | const durationEventLike = { ts: 1, ph: 'B' }; 110 | 111 | // This fails, because string `B` is not coerced to EventsPhase 112 | // @ts-expect-error 113 | expectsDurationEvent(durationEventLike); 114 | 115 | // But if we use our type guard first... 116 | if (isDurationEvent(durationEventLike)) { 117 | // This will pass, because isDurationEvent type guard refines the type 118 | // by checking that the value matches the expected type 119 | expectsDurationEvent(durationEventLike); 120 | } else { 121 | // This will fail, because the value didn't match 122 | // @ts-expect-error 123 | expectsDurationEvent(durationEventLike); 124 | } 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /src/__test__/Phases.test.ts: -------------------------------------------------------------------------------- 1 | import { EventsPhase } from '../types/Phases'; 2 | 3 | describe('EventPhase', () => { 4 | it('should map to corresponding string value correctly at type-level and runtime', () => { 5 | // If you added @ts-expect-error below, the type check should 6 | // error because the comparison always returns false 7 | 8 | expect(EventsPhase.DURATION_EVENTS_BEGIN === 'B').toBe(true); 9 | }); 10 | 11 | it('should cause a type error and fail runtime check when compared to incorrect literal', () => { 12 | // If you remove @ts-expect-error below, the type check should 13 | // error because the comparison always returns false 14 | 15 | // @ts-expect-error 16 | expect(EventsPhase.DURATION_EVENTS_BEGIN === 'NOT_B').toBe(false); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { CpuProfilerModel } from './profiler/cpuProfilerModel'; 2 | import { DurationEvent } from './types/EventInterfaces'; 3 | import { readFileAsync } from './utils/fileSystem'; 4 | import { HermesCPUProfile } from './types/HermesProfile'; 5 | import applySourceMapsToEvents from './profiler/applySourceMapsToEvents'; 6 | import { SourceMap } from './types/SourceMap'; 7 | 8 | /** 9 | * This transformer can take in the path of the profile, the source map (optional) and the bundle file name (optional) 10 | * and return a promise which resolves to Chrome Dev Tools compatible events 11 | * @param profilePath string 12 | * @param sourceMapPath string 13 | * @param bundleFileName string 14 | * @return Promise 15 | */ 16 | const transformer = async ( 17 | profilePath: string, 18 | sourceMapPath: string | undefined, 19 | bundleFileName: string | undefined 20 | ): Promise => { 21 | const hermesProfile: HermesCPUProfile = await readFileAsync(profilePath); 22 | const profileChunk = CpuProfilerModel.collectProfileEvents(hermesProfile); 23 | const profiler = new CpuProfilerModel(profileChunk); 24 | const chromeEvents = profiler.createStartEndEvents(); 25 | if (sourceMapPath) { 26 | const sourceMap: SourceMap = await readFileAsync(sourceMapPath); 27 | const events = applySourceMapsToEvents( 28 | sourceMap, 29 | chromeEvents, 30 | bundleFileName 31 | ); 32 | return events; 33 | } 34 | return chromeEvents; 35 | }; 36 | 37 | export default transformer; 38 | export { SourceMap } from './types/SourceMap'; 39 | -------------------------------------------------------------------------------- /src/profiler/applySourceMapsToEvents.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { SourceMapConsumer, RawSourceMap } from 'source-map'; 3 | import { DurationEvent } from '../types/EventInterfaces'; 4 | import { SourceMap } from '../types/SourceMap'; 5 | 6 | /** 7 | * This function is a helper to the applySourceMapsToEvents. The node_module identification logic is implemented here based on the sourcemap url (if available). Incase a node_module could not be found, this defaults to the category of the event 8 | * @param defaultCategory The category the event is of by default without the use of Source maps 9 | * @param url The URL which can be parsed to interpret the new category of the event (depends on node_modules) 10 | */ 11 | const findNodeModuleNameIfExists = ( 12 | defaultCategory: string, 13 | url: string | null 14 | ): string => { 15 | const obtainCategory = (url: string): string => { 16 | const dirs = url 17 | .substring(url.lastIndexOf(`${path.sep}node_modules${path.sep}`)) 18 | .split(path.sep); 19 | return dirs.length > 2 && dirs[1] === 'node_modules' 20 | ? dirs[2] 21 | : defaultCategory; 22 | }; 23 | return url ? obtainCategory(url) : defaultCategory; 24 | }; 25 | 26 | /** 27 | * The unification of categories is important as we want identify the specific reasons why the application slows down, namely via unoptimised native/JS code, or react-native renders or third party modules. The common colours for node_modules can help idenitfy problems instantly 28 | * @param nodeModuleName The node module name associated with the event obtained via sourcemap, this nodeModule name is simply the output of @see findNodeModuleNameIfExists 29 | */ 30 | const improveCategories = ( 31 | nodeModuleName: string, 32 | defaultCategory: string 33 | ): string => { 34 | // The nodeModuleName obtained from `findNodeModuleNameIfExists` by default is the original category name in the generated Hermes Profile. If we cannot isolate a nodeModule name, we simply return with the default category 35 | if (nodeModuleName === defaultCategory) { 36 | return defaultCategory; 37 | } 38 | // The events from these modules will fall under the umbrella of react-native events and hence be represented by the same colour 39 | const reactNativeModuleNames = ['react-native', 'react', 'metro']; 40 | if (reactNativeModuleNames.includes(nodeModuleName)) { 41 | return 'react-native-internals'; 42 | } else { 43 | return 'other_node_modules'; 44 | } 45 | }; 46 | 47 | /** 48 | * Enhances the function line, column and params information and event categories 49 | * based on JavaScript source maps to make it easier to associate trace events with 50 | * the application code 51 | * 52 | * Throws error if args not set up in ChromeEvents 53 | * @param {SourceMap} sourceMap 54 | * @param {DurationEvent[]} chromeEvents 55 | * @param {string} indexBundleFileName 56 | * @throws If `args` for events are not populated 57 | * @returns {DurationEvent[]} 58 | */ 59 | const applySourceMapsToEvents = async ( 60 | sourceMap: SourceMap, 61 | chromeEvents: DurationEvent[], 62 | indexBundleFileName: string | undefined 63 | ): Promise => { 64 | // SEE: Should file here be an optional parameter, so take indexBundleFileName as a parameter and use 65 | // a default name of `index.bundle` 66 | const rawSourceMap: RawSourceMap = { 67 | version: Number(sourceMap.version), 68 | file: indexBundleFileName || 'index.bundle', 69 | sources: sourceMap.sources, 70 | mappings: sourceMap.mappings, 71 | names: sourceMap.names, 72 | }; 73 | 74 | const consumer = await new SourceMapConsumer(rawSourceMap); 75 | const events = chromeEvents.map((event: DurationEvent) => { 76 | if (event.args) { 77 | const sm = consumer.originalPositionFor({ 78 | line: Number(event.args.line), 79 | column: Number(event.args.column), 80 | }); 81 | /** 82 | * The categories can help us better visualise the profile if we modify the categories. 83 | * We change these categories only in the root level and not deeper inside the args, just so we have our 84 | * original categories as well as these modified categories (as the modified categories simply help with visualisation) 85 | */ 86 | const nodeModuleNameIfAvailable = findNodeModuleNameIfExists( 87 | event.cat!, 88 | sm.source 89 | ); 90 | event.cat = improveCategories(nodeModuleNameIfAvailable, event.cat!); 91 | event.args = { 92 | ...event.args, 93 | url: sm.source, 94 | line: sm.line, 95 | column: sm.column, 96 | params: sm.name, 97 | allocatedCategory: event.cat, 98 | allocatedName: event.name, 99 | node_module: nodeModuleNameIfAvailable, 100 | }; 101 | } else { 102 | throw new Error( 103 | `Source maps could not be derived for an event at ${event.ts} and with stackFrame ID ${event.sf}` 104 | ); 105 | } 106 | return event; 107 | }); 108 | consumer.destroy(); 109 | return events; 110 | }; 111 | 112 | export default applySourceMapsToEvents; 113 | -------------------------------------------------------------------------------- /src/profiler/cpuProfilerModel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2020 The Lighthouse Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | * 6 | * MODIFICATION NOTICE: 7 | * This file is derived from `https://github.com/GoogleChrome/lighthouse/blob/0422daa9b1b8528dd8436860b153134bd0f959f1/lighthouse-core/lib/tracehouse/cpu-profile-model.js` 8 | * and has been modified by Saphal Patro (email: saphal1998@gmail.com) 9 | * The following changes have been made to the original file: 10 | * 1. Converted code to Typescript and defined necessary types 11 | * 2. Wrote a method @see collectProfileEvents to convert the Hermes Samples to Profile Chunks supported by Lighthouse Parser 12 | * 3. Modified @see constructNodes to work with the Hermes Samples and StackFrames 13 | */ 14 | 15 | /** 16 | * @fileoverview 17 | * 18 | * This model converts the `Profile` and `ProfileChunk` mega trace events from the `disabled-by-default-v8.cpu_profiler` 19 | * category into B/E-style trace events that main-thread-tasks.js already knows how to parse into a task tree. 20 | * 21 | * The CPU profiler measures where time is being spent by sampling the stack (See https://www.jetbrains.com/help/profiler/Profiling_Guidelines__Choosing_the_Right_Profiling_Mode.html 22 | * for a generic description of the differences between tracing and sampling). 23 | * 24 | * A `Profile` event is a record of the stack that was being executed at different sample points in time. 25 | * It has a structure like this: 26 | * 27 | * nodes: [function A, function B, function C] 28 | * samples: [node with id 2, node with id 1, ...] 29 | * timeDeltas: [4125μs since last sample, 121μs since last sample, ...] 30 | * 31 | * Helpful prior art: 32 | * @see https://cs.chromium.org/chromium/src/third_party/devtools-frontend/src/front_end/sdk/CPUProfileDataModel.js?sq=package:chromium&g=0&l=42 33 | * @see https://github.com/v8/v8/blob/99ca333b0efba3236954b823101315aefeac51ab/tools/profile.js 34 | * @see https://github.com/jlfwong/speedscope/blob/9ed1eb192cb7e9dac43a5f25bd101af169dc654a/src/import/chrome.ts#L200 35 | */ 36 | 37 | import { 38 | CPUProfileChunk, 39 | CPUProfileChunkNode, 40 | CPUProfileChunker, 41 | } from '../types/CPUProfile'; 42 | import { DurationEvent } from '../types/EventInterfaces'; 43 | import { 44 | HermesCPUProfile, 45 | HermesSample, 46 | HermesStackFrame, 47 | } from '../types/HermesProfile'; 48 | import { EventsPhase } from '../types/Phases'; 49 | 50 | export class CpuProfilerModel { 51 | _profile: CPUProfileChunk; 52 | _nodesById: Map; 53 | _activeNodeArraysById: Map; 54 | 55 | constructor(profile: CPUProfileChunk) { 56 | this._profile = profile; 57 | this._nodesById = this._createNodeMap(); 58 | this._activeNodeArraysById = this._createActiveNodeArrays(); 59 | } 60 | 61 | /** 62 | * Initialization function to enable O(1) access to nodes by node ID. 63 | * @return {Map { 66 | /** @type {Map} */ 67 | const map: Map = new Map< 68 | number, 69 | CPUProfileChunkNode 70 | >(); 71 | for (const node of this._profile.nodes) { 72 | map.set(node.id, node); 73 | } 74 | 75 | return map; 76 | } 77 | 78 | /** 79 | * Initialization function to enable O(1) access to the set of active nodes in the stack by node ID. 80 | * @return Map 81 | */ 82 | _createActiveNodeArrays(): Map { 83 | const map: Map = new Map(); 84 | 85 | /** 86 | * Given a nodeId, `getActiveNodes` gets all the parent nodes in reversed call order 87 | * @param {number} id 88 | */ 89 | const getActiveNodes = (id: number): number[] => { 90 | if (map.has(id)) return map.get(id) || []; 91 | 92 | const node = this._nodesById.get(id); 93 | if (!node) throw new Error(`No such node ${id}`); 94 | if (node.parent) { 95 | const array = getActiveNodes(node.parent).concat([id]); 96 | map.set(id, array); 97 | return array; 98 | } else { 99 | return [id]; 100 | } 101 | }; 102 | 103 | for (const node of this._profile.nodes) { 104 | map.set(node.id, getActiveNodes(node.id)); 105 | } 106 | return map; 107 | } 108 | 109 | /** 110 | * Returns all the node IDs in a stack when a specific nodeId is at the top of the stack 111 | * (i.e. a stack's node ID and the node ID of all of its parents). 112 | */ 113 | _getActiveNodeIds(nodeId: number): number[] { 114 | const activeNodeIds = this._activeNodeArraysById.get(nodeId); 115 | if (!activeNodeIds) throw new Error(`No such node ID ${nodeId}`); 116 | return activeNodeIds; 117 | } 118 | 119 | /** 120 | * Generates the necessary B/E-style trace events for a single transition from stack A to stack B 121 | * at the given timestamp. 122 | * 123 | * Example: 124 | * 125 | * timestamp 1234 126 | * previousNodeIds 1,2,3 127 | * currentNodeIds 1,2,4 128 | * 129 | * yields [end 3 at ts 1234, begin 4 at ts 1234] 130 | * 131 | * @param {number} timestamp 132 | * @param {Array} previousNodeIds 133 | * @param {Array} currentNodeIds 134 | * @returns {Array} 135 | */ 136 | _createStartEndEventsForTransition( 137 | timestamp: number, 138 | previousNodeIds: number[], 139 | currentNodeIds: number[] 140 | ): DurationEvent[] { 141 | // Start nodes are the nodes which are present only in the currentNodeIds and not in PreviousNodeIds 142 | const startNodes: CPUProfileChunkNode[] = currentNodeIds 143 | .filter(id => !previousNodeIds.includes(id)) 144 | .map(id => this._nodesById.get(id)!); 145 | // End nodes are the nodes which are present only in the PreviousNodeIds and not in CurrentNodeIds 146 | const endNodes: CPUProfileChunkNode[] = previousNodeIds 147 | .filter(id => !currentNodeIds.includes(id)) 148 | .map(id => this._nodesById.get(id)!); 149 | 150 | /** 151 | * The name needs to be modified if `http://` is present as this directs us to bundle files which does not add any information for the end user 152 | * @param name 153 | */ 154 | const removeLinksIfExist = (name: string): string => { 155 | // If the name includes `http://`, we can filter the name 156 | if (name.includes('http://')) { 157 | name = name.substring(0, name.lastIndexOf('(')); 158 | } 159 | return name || 'anonymous'; 160 | }; 161 | 162 | /** 163 | * Create a Duration Event from CPUProfileChunkNodes. 164 | * @param {CPUProfileChunkNode} node 165 | * @return {DurationEvent} */ 166 | const createEvent = (node: CPUProfileChunkNode): DurationEvent => ({ 167 | ts: timestamp, 168 | pid: this._profile.pid, 169 | tid: Number(this._profile.tid), 170 | ph: EventsPhase.DURATION_EVENTS_BEGIN, 171 | name: removeLinksIfExist(node.callFrame.name), 172 | cat: node.callFrame.category, 173 | args: { ...node.callFrame }, 174 | }); 175 | 176 | const startEvents: DurationEvent[] = startNodes 177 | .map(createEvent) 178 | .map(evt => ({ ...evt, ph: EventsPhase.DURATION_EVENTS_BEGIN })); 179 | const endEvents: DurationEvent[] = endNodes 180 | .map(createEvent) 181 | .map(evt => ({ ...evt, ph: EventsPhase.DURATION_EVENTS_END })); 182 | return [...endEvents.reverse(), ...startEvents]; 183 | } 184 | 185 | /** 186 | * Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()` 187 | * @return {DurationEvent} 188 | * @throws If the length of timeDeltas array or the samples array does not match with the length of samples in Hermes Profile 189 | */ 190 | createStartEndEvents(): DurationEvent[] { 191 | const profile = this._profile; 192 | const length = profile.samples.length; 193 | if ( 194 | profile.timeDeltas.length !== length || 195 | profile.samples.length !== length 196 | ) 197 | throw new Error(`Invalid CPU profile length`); 198 | 199 | const events: DurationEvent[] = []; 200 | 201 | let timestamp = profile.startTime; 202 | let lastActiveNodeIds: number[] = []; 203 | for (let i = 0; i < profile.samples.length; i++) { 204 | const nodeId = profile.samples[i]; 205 | const timeDelta = Math.max(profile.timeDeltas[i], 0); 206 | const node = this._nodesById.get(nodeId); 207 | if (!node) throw new Error(`Missing node ${nodeId}`); 208 | 209 | timestamp += timeDelta; 210 | const activeNodeIds = this._getActiveNodeIds(nodeId); 211 | events.push( 212 | ...this._createStartEndEventsForTransition( 213 | timestamp, 214 | lastActiveNodeIds, 215 | activeNodeIds 216 | ) 217 | ); 218 | lastActiveNodeIds = activeNodeIds; 219 | } 220 | 221 | events.push( 222 | ...this._createStartEndEventsForTransition( 223 | timestamp, 224 | lastActiveNodeIds, 225 | [] 226 | ) 227 | ); 228 | 229 | return events; 230 | } 231 | 232 | /** 233 | * Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()` 234 | * @param {CPUProfileChunk} profile 235 | */ 236 | static createStartEndEvents(profile: CPUProfileChunk) { 237 | const model = new CpuProfilerModel(profile); 238 | return model.createStartEndEvents(); 239 | } 240 | 241 | /** 242 | * Converts the Hermes Sample into a single CpuProfileChunk object for consumption 243 | * by `createStartEndEvents()`. 244 | * 245 | * @param {HermesCPUProfile} profile 246 | * @throws Profile must have at least one sample 247 | * @return {CPUProfileChunk} 248 | */ 249 | static collectProfileEvents(profile: HermesCPUProfile): CPUProfileChunk { 250 | if (profile.samples.length > 0) { 251 | const { samples, stackFrames } = profile; 252 | // Assumption: The sample will have a single process 253 | const pid: number = samples[0].pid; 254 | // Assumption: Javascript is single threaded, so there should only be one thread throughout 255 | const tid: string = samples[0].tid; 256 | // TODO: What role does id play in string parsing 257 | const id: string = '0x1'; 258 | const startTime: number = Number(samples[0].ts); 259 | const { nodes, sampleNumbers, timeDeltas } = this.constructNodes( 260 | samples, 261 | stackFrames 262 | ); 263 | return { 264 | id, 265 | pid, 266 | tid, 267 | startTime, 268 | nodes, 269 | samples: sampleNumbers, 270 | timeDeltas, 271 | }; 272 | } else { 273 | throw new Error('The hermes profile has zero samples'); 274 | } 275 | } 276 | 277 | /** 278 | * Constructs CPUProfileChunk Nodes and the resultant samples and time deltas to be inputted into the 279 | * CPUProfileChunk object which will be processed to give createStartEndEvents() 280 | * 281 | * @param {HermesSample} samples 282 | * @param {} stackFrames 283 | * @return {CPUProfileChunker} 284 | */ 285 | static constructNodes( 286 | samples: HermesSample[], 287 | stackFrames: { [key in string]: HermesStackFrame } 288 | ): CPUProfileChunker { 289 | samples = samples.map((sample: HermesSample) => { 290 | sample.stackFrameData = stackFrames[sample.sf]; 291 | return sample; 292 | }); 293 | const stackFrameIds: string[] = Object.keys(stackFrames); 294 | const profileNodes: CPUProfileChunkNode[] = stackFrameIds.map( 295 | (stackFrameId: string) => { 296 | const stackFrame = stackFrames[stackFrameId]; 297 | return { 298 | id: Number(stackFrameId), 299 | callFrame: { 300 | ...stackFrame, 301 | url: stackFrame.name, 302 | }, 303 | parent: stackFrames[stackFrameId].parent, 304 | }; 305 | } 306 | ); 307 | const returnedSamples: number[] = []; 308 | const timeDeltas: number[] = []; 309 | let lastTimeStamp = Number(samples[0].ts); 310 | samples.forEach((sample: HermesSample, idx: number) => { 311 | returnedSamples.push(sample.sf); 312 | if (idx === 0) { 313 | timeDeltas.push(0); 314 | } else { 315 | const timeDiff = Number(sample.ts) - lastTimeStamp; 316 | lastTimeStamp = Number(sample.ts); 317 | timeDeltas.push(timeDiff); 318 | } 319 | }); 320 | 321 | return { 322 | nodes: profileNodes, 323 | sampleNumbers: returnedSamples, 324 | timeDeltas, 325 | }; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/types/CPUProfile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The CPUProfileChunk is the intermediate file that Lighthouse can interpret and 3 | * hence subsequently convert to events supported by Chrome Dev Tools 4 | */ 5 | export interface CPUProfileChunk { 6 | id: string; 7 | pid: number; 8 | tid: string; 9 | startTime: number; 10 | nodes: CPUProfileChunkNode[]; 11 | samples: number[]; 12 | timeDeltas: number[]; 13 | } 14 | 15 | /** 16 | * The CPUProfileChunkNode is an individual element of the nodes[] property in the CPUProfileChunk 17 | * @see CPUProfileChunk 18 | */ 19 | export interface CPUProfileChunkNode { 20 | id: number; 21 | callFrame: { 22 | line: string; 23 | column: string; 24 | funcLine: string; 25 | funcColumn: string; 26 | name: string; 27 | url?: string; 28 | category: string; 29 | }; 30 | parent?: number; 31 | } 32 | 33 | /** 34 | * The process of conversion of Hermes Profile Events to Lighthouse supported events are primarily focussed 35 | * around generating the correct values of the properties in CPUProfileChunker. 36 | */ 37 | export type CPUProfileChunker = { 38 | nodes: CPUProfileChunkNode[]; 39 | sampleNumbers: number[]; 40 | timeDeltas: number[]; 41 | }; 42 | -------------------------------------------------------------------------------- /src/types/EventInterfaces.ts: -------------------------------------------------------------------------------- 1 | import { EventsPhase } from './Phases'; 2 | 3 | // All of the event types in this module are not currently 4 | // being used, but they are included here for completeness 5 | // for future implementers 6 | 7 | export interface SharedEventProperties { 8 | /** 9 | * name of the event 10 | */ 11 | name?: string; 12 | /** 13 | * event category 14 | */ 15 | cat?: string; 16 | /** 17 | * tracing clock timestamp 18 | */ 19 | ts?: number; 20 | /** 21 | * process ID 22 | */ 23 | pid?: number; 24 | /** 25 | * thread ID 26 | */ 27 | tid?: number; 28 | /** 29 | * event type (phase) 30 | */ 31 | ph: EventsPhase; 32 | /** 33 | * id for a stackFrame object 34 | */ 35 | sf?: number; 36 | /** 37 | * thread clock timestamp 38 | */ 39 | tts?: number; 40 | /** 41 | * a fixed color name 42 | */ 43 | cname?: string; 44 | /** 45 | * event arguments 46 | */ 47 | args?: { 48 | [key in string]: any; 49 | }; 50 | } 51 | 52 | interface DurationEventBegin extends SharedEventProperties { 53 | ph: EventsPhase.DURATION_EVENTS_BEGIN; 54 | } 55 | 56 | interface DurationEventEnd extends SharedEventProperties { 57 | ph: EventsPhase.DURATION_EVENTS_END; 58 | } 59 | 60 | export type DurationEvent = DurationEventBegin | DurationEventEnd; 61 | 62 | export interface CompleteEvent extends SharedEventProperties { 63 | ph: EventsPhase.COMPLETE_EVENTS; 64 | dur: number; 65 | } 66 | 67 | export interface MetadataEvent extends SharedEventProperties { 68 | ph: EventsPhase.METADATA_EVENTS; 69 | } 70 | 71 | export interface SampleEvent extends SharedEventProperties { 72 | ph: EventsPhase.SAMPLE_EVENTS; 73 | } 74 | 75 | interface ObjectEventCreated extends SharedEventProperties { 76 | ph: EventsPhase.OBJECT_EVENTS_CREATED; 77 | scope?: string; 78 | } 79 | 80 | interface ObjectEventSnapshot extends SharedEventProperties { 81 | ph: EventsPhase.OBJECT_EVENTS_SNAPSHOT; 82 | scope?: string; 83 | } 84 | 85 | interface ObjectEventDestroyed extends SharedEventProperties { 86 | ph: EventsPhase.OBJECT_EVENTS_DESTROYED; 87 | scope?: string; 88 | } 89 | 90 | export type ObjectEvent = 91 | | ObjectEventCreated 92 | | ObjectEventSnapshot 93 | | ObjectEventDestroyed; 94 | 95 | export interface ClockSyncEvent extends SharedEventProperties { 96 | ph: EventsPhase.CLOCK_SYNC_EVENTS; 97 | args: { 98 | sync_id: string; 99 | issue_ts?: number; 100 | }; 101 | } 102 | 103 | interface ContextEventEnter extends SharedEventProperties { 104 | ph: EventsPhase.CONTEXT_EVENTS_ENTER; 105 | } 106 | 107 | interface ContextEventLeave extends SharedEventProperties { 108 | ph: EventsPhase.CONTEXT_EVENTS_LEAVE; 109 | } 110 | 111 | export type ContextEvent = ContextEventEnter | ContextEventLeave; 112 | 113 | interface AsyncEventStart extends SharedEventProperties { 114 | ph: EventsPhase.ASYNC_EVENTS_NESTABLE_START; 115 | id: number; 116 | scope?: string; 117 | } 118 | 119 | interface AsyncEventInstant extends SharedEventProperties { 120 | ph: EventsPhase.ASYNC_EVENTS_NESTABLE_INSTANT; 121 | id: number; 122 | scope?: string; 123 | } 124 | 125 | interface AsyncEventEnd extends SharedEventProperties { 126 | ph: EventsPhase.ASYNC_EVENTS_NESTABLE_END; 127 | id: number; 128 | scope?: string; 129 | } 130 | 131 | export type AsyncEvent = AsyncEventStart | AsyncEventInstant | AsyncEventEnd; 132 | 133 | export interface InstantEvent extends SharedEventProperties { 134 | ph: EventsPhase.INSTANT_EVENTS; 135 | s: string; 136 | } 137 | 138 | export interface CounterEvent extends SharedEventProperties { 139 | ph: EventsPhase.COUNTER_EVENTS; 140 | } 141 | 142 | interface FlowEventStart extends SharedEventProperties { 143 | ph: EventsPhase.FLOW_EVENTS_START; 144 | } 145 | 146 | interface FlowEventStep extends SharedEventProperties { 147 | ph: EventsPhase.FLOW_EVENTS_STEP; 148 | } 149 | interface FlowEventEnd extends SharedEventProperties { 150 | ph: EventsPhase.FLOW_EVENTS_END; 151 | } 152 | 153 | export type FlowEvent = FlowEventStart | FlowEventStep | FlowEventEnd; 154 | 155 | interface MemoryDumpGlobal extends SharedEventProperties { 156 | ph: EventsPhase.MEMORY_DUMP_EVENTS_GLOBAL; 157 | id: string; 158 | } 159 | 160 | interface MemoryDumpProcess extends SharedEventProperties { 161 | ph: EventsPhase.MEMORY_DUMP_EVENTS_PROCESS; 162 | id: string; 163 | } 164 | export type MemoryDumpEvent = MemoryDumpGlobal | MemoryDumpProcess; 165 | 166 | export interface MarkEvent extends SharedEventProperties { 167 | ph: EventsPhase.MARK_EVENTS; 168 | } 169 | 170 | export interface LinkedIDEvent extends SharedEventProperties { 171 | ph: EventsPhase.LINKED_ID_EVENTS; 172 | id: number; 173 | args: { 174 | linked_id: number; 175 | }; 176 | } 177 | 178 | export type Event = 179 | | DurationEvent 180 | | CompleteEvent 181 | | MetadataEvent 182 | | SampleEvent 183 | | ObjectEvent 184 | | ClockSyncEvent 185 | | ContextEvent 186 | | AsyncEvent 187 | | InstantEvent 188 | | CounterEvent 189 | | FlowEvent 190 | | MemoryDumpEvent 191 | | MarkEvent 192 | | LinkedIDEvent; 193 | -------------------------------------------------------------------------------- /src/types/HermesProfile.ts: -------------------------------------------------------------------------------- 1 | import { SharedEventProperties } from './EventInterfaces'; 2 | 3 | /** 4 | * Each item in the stackFrames object of the hermes profile 5 | */ 6 | export interface HermesStackFrame { 7 | line: string; 8 | column: string; 9 | funcLine: string; 10 | funcColumn: string; 11 | name: string; 12 | category: string; 13 | /** 14 | * A parent function may or may not exist 15 | */ 16 | parent?: number; 17 | } 18 | /** 19 | * Each item in the samples array of the hermes profile 20 | */ 21 | export interface HermesSample { 22 | cpu: string; 23 | name: string; 24 | ts: string; 25 | pid: number; 26 | tid: string; 27 | weight: string; 28 | /** 29 | * Will refer to an element in the stackFrames object of the Hermes Profile 30 | */ 31 | sf: number; 32 | stackFrameData?: HermesStackFrame; 33 | } 34 | 35 | /** 36 | * Hermes Profile Interface 37 | */ 38 | export interface HermesCPUProfile { 39 | traceEvents: SharedEventProperties[]; 40 | samples: HermesSample[]; 41 | stackFrames: { [key in string]: HermesStackFrame }; 42 | } 43 | -------------------------------------------------------------------------------- /src/types/Phases.ts: -------------------------------------------------------------------------------- 1 | export enum EventsPhase { 2 | DURATION_EVENTS_BEGIN = 'B', 3 | DURATION_EVENTS_END = 'E', 4 | COMPLETE_EVENTS = 'X', 5 | INSTANT_EVENTS = 'I', 6 | COUNTER_EVENTS = 'C', 7 | ASYNC_EVENTS_NESTABLE_START = 'b', 8 | ASYNC_EVENTS_NESTABLE_INSTANT = 'n', 9 | ASYNC_EVENTS_NESTABLE_END = 'e', 10 | FLOW_EVENTS_START = 's', 11 | FLOW_EVENTS_STEP = 't', 12 | FLOW_EVENTS_END = 'f', 13 | SAMPLE_EVENTS = 'P', 14 | OBJECT_EVENTS_CREATED = 'N', 15 | OBJECT_EVENTS_SNAPSHOT = 'O', 16 | OBJECT_EVENTS_DESTROYED = 'D', 17 | METADATA_EVENTS = 'M', 18 | MEMORY_DUMP_EVENTS_GLOBAL = 'V', 19 | MEMORY_DUMP_EVENTS_PROCESS = 'v', 20 | MARK_EVENTS = 'R', 21 | CLOCK_SYNC_EVENTS = 'c', 22 | CONTEXT_EVENTS_ENTER = '(', 23 | CONTEXT_EVENTS_LEAVE = ')', 24 | // Deprecated 25 | ASYNC_EVENTS_START = 'S', 26 | ASYNC_EVENTS_STEP_INTO = 'T', 27 | ASYNC_EVENTS_STEP_PAST = 'p', 28 | ASYNC_EVENTS_END = 'F', 29 | LINKED_ID_EVENTS = '=', 30 | } 31 | -------------------------------------------------------------------------------- /src/types/SourceMap.ts: -------------------------------------------------------------------------------- 1 | export interface SourceMap { 2 | version: string; 3 | sources: string[]; 4 | sourceContent: string[]; 5 | x_facebook_sources: { names: string[]; mappings: string }[] | null; 6 | names: string[]; 7 | mappings: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/fileSystem.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs'; 2 | import { promisify } from 'util'; 3 | 4 | export const readFileAsync = async (path: string): Promise => { 5 | try { 6 | const readFileAsync = promisify(readFile); 7 | const fileString: string = (await readFileAsync(path, 'utf-8')) as string; 8 | if (fileString.length === 0) { 9 | throw new Error(`${path} is an empty file`); 10 | } 11 | const obj = JSON.parse(fileString); 12 | return obj; 13 | } catch (err) { 14 | throw err; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src" 4 | ], 5 | "compilerOptions": { 6 | "module": "esnext", 7 | "lib": ["dom", "esnext"], 8 | "importHelpers": true, 9 | "declaration": true, 10 | "sourceMap": true, 11 | "rootDir": "./src", 12 | "strict": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "moduleResolution": "node", 18 | "baseUrl": "./", 19 | "paths": { 20 | "*": ["src/*", "node_modules/*"] 21 | }, 22 | "jsx": "react", 23 | "esModuleInterop": true 24 | } 25 | } 26 | --------------------------------------------------------------------------------