├── .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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Visualize Facebook's [Hermes JavaScript runtime](https://github.com/facebook/hermes) profile traces in Chrome Developer Tools.
14 |
15 | 
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 | 
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 |
--------------------------------------------------------------------------------