├── .assets ├── counters.png ├── detail.png ├── summary.png └── warnings.png ├── .editorconfig ├── .eslintrc.js ├── .github └── CODEOWNERS ├── .gitignore ├── .npmignore ├── .nvmrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── devtools ├── common │ ├── index.ts │ └── settings.ts ├── cpuProfileDataModel │ ├── cpuProfileNode.ts │ └── index.ts ├── loader.ts ├── profileTreeModel │ ├── index.ts │ └── profileNode.ts ├── runtime │ ├── experimentsSupport.ts │ └── index.ts ├── timelineModel │ ├── counterGraph │ │ ├── calculator.ts │ │ ├── counter.ts │ │ └── index.ts │ ├── index.ts │ ├── invalidationTracker.ts │ ├── invalidationTrackingEvent.ts │ ├── networkRequest.ts │ ├── pageFrame.ts │ ├── performanceModel.ts │ ├── timelineAsyncEventTracker.ts │ ├── timelineData.ts │ ├── timelineFrame │ │ ├── layerPaintEvent.ts │ │ ├── pendingFrame.ts │ │ ├── timelineFrame.ts │ │ └── tracingFrameLayerTree.ts │ ├── timelineFrameModel.ts │ ├── timelineJSProfileProcessor.ts │ ├── timelineModelFilter │ │ ├── index.ts │ │ ├── timelineCategory.ts │ │ ├── timelineRecordStyle.ts │ │ ├── timelineSelection.ts │ │ └── timelineVisibleEventsFilter.ts │ ├── timelineUIUtils.ts │ └── track.ts ├── tracingModel │ ├── asyncEvent.ts │ ├── event.ts │ ├── index.ts │ ├── namedObject.ts │ ├── objectSnapshot.ts │ ├── process.ts │ ├── profileEventsGroup.ts │ └── thread.ts ├── types.ts └── utils.ts ├── jest.config.js ├── package.json ├── prettier.config.js ├── src ├── index.ts └── utils.ts ├── tests ├── .eslintrc.js ├── __fixtures__ │ └── jankTraceLog.json ├── __snapshots__ │ └── index.test.ts.snap └── index.test.ts └── tsconfig.json /.assets/counters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs/tracelib/4363de05c90fe90dc7bd5ce6a8d7b2c1e7c689ab/.assets/counters.png -------------------------------------------------------------------------------- /.assets/detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs/tracelib/4363de05c90fe90dc7bd5ce6a8d7b2c1e7c689ab/.assets/detail.png -------------------------------------------------------------------------------- /.assets/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs/tracelib/4363de05c90fe90dc7bd5ce6a8d7b2c1e7c689ab/.assets/summary.png -------------------------------------------------------------------------------- /.assets/warnings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs/tracelib/4363de05c90fe90dc7bd5ce6a8d7b2c1e7c689ab/.assets/warnings.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 4 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | 17 | [{.babelrc,.eslintrc,.codeclimate.yml,.travis.yml,*.json,.eslintrc.js}] 18 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'plugin:@typescript-eslint/recommended', 4 | 'prettier/@typescript-eslint' 5 | ], 6 | env: { 7 | node: true, 8 | es6: true 9 | }, 10 | parser: '@typescript-eslint/parser', 11 | parserOptions: { 12 | sourceType: 'module', 13 | project: "./tsconfig.json" 14 | }, 15 | plugins: ['@typescript-eslint'], 16 | rules: { 17 | semi: ['error', 'never'], 18 | quotes: ['error', 'single'], 19 | indent: [2, 4], 20 | curly: 2, 21 | 22 | 'no-multiple-empty-lines': [2, {'max': 1, 'maxEOF': 1}], 23 | 'array-bracket-spacing': ['error', 'never'], 24 | 'brace-style': ['error', '1tbs', { allowSingleLine: true }], 25 | camelcase: ['error', { properties: 'never' }], 26 | 'comma-spacing': ['error', { before: false, after: true }], 27 | 'no-lonely-if': 'error', 28 | 'no-tabs': 'error', 29 | 'no-trailing-spaces': ['error', { 30 | skipBlankLines: false, 31 | ignoreComments: false 32 | }], 33 | quotes: ['error', 'single', { avoidEscape: true }], 34 | 'unicode-bom': ['error', 'never'], 35 | 'object-curly-spacing': ['error', 'always'], 36 | 'require-atomic-updates': 0, 37 | '@typescript-eslint/explicit-function-return-type': 2, 38 | '@typescript-eslint/indent': 0, 39 | '@typescript-eslint/no-use-before-define': 0 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in the repo. 2 | @yfangsl @christian-bromann @farhan-sauce 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | !tests/typings 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # lerna changelog cachedir 62 | .changelog 63 | 64 | **/build 65 | /*.js 66 | !babel.config.js 67 | !.eslintrc.js 68 | !jest.config.js 69 | !prettier.config.js 70 | package-lock.json 71 | .DS_Store 72 | 73 | # Editor files 74 | .idea 75 | .vscode 76 | !.vscode/extensions.json 77 | .*.swp 78 | .history 79 | .gradle 80 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .assets 2 | .github 3 | coverage 4 | /src 5 | /devtools 6 | tests 7 | 8 | /*.js 9 | *.editorconfig 10 | *.gitignore 11 | *.eslintignore 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10.16.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '10' 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `tracelib` 2 | 3 | **Thank you for your interest in `tracelib`. Your contributions are highly welcome.** 4 | 5 | There are multiple ways of getting involved: 6 | 7 | - [Report a bug](#report-a-bug) 8 | - [Suggest a feature](#suggest-a-feature) 9 | - [Contribute code](#contribute-code) 10 | 11 | Below are a few guidelines we would like you to follow. 12 | If you need help, please reach out to us by opening an issue. 13 | 14 | ## Report a bug 15 | Reporting bugs is one of the best ways to contribute. Before creating a bug report, please check that an [issue](/issues) reporting the same problem does not already exist. If there is such an issue, you may add your information as a comment. 16 | 17 | To report a new bug you should open an issue that summarizes the bug and set the label to "bug". 18 | 19 | If you want to provide a fix along with your bug report: That is great! In this case please send us a pull request as described in section [Contribute Code](#contribute-code). 20 | 21 | ## Suggest a feature 22 | To request a new feature you should open an [issue](../../issues/new) and summarize the desired functionality and its use case. Set the issue label to "feature". 23 | 24 | ## Contribute code 25 | This is an outline of what the workflow for code contributions looks like 26 | 27 | - Check the list of open [issues](../../issues). Either assign an existing issue to yourself, or 28 | create a new one that you would like work on and discuss your ideas and use cases. 29 | 30 | It is always best to discuss your plans beforehand, to ensure that your contribution is in line with our goals. 31 | 32 | - Fork the repository on GitHub 33 | - Create a topic branch from where you want to base your work. This is usually `main`. 34 | - Open a new pull request, label it `work in progress` and outline what you will be contributing 35 | - Make commits of logical units. 36 | - Make sure you sign-off on your commits `git commit -s -m "adding X to change Y"` 37 | - Write good commit messages (see below). 38 | - Push your changes to a topic branch in your fork of the repository. 39 | - As you push your changes, update the pull request with new infomation and tasks as you complete them 40 | - Project maintainers might comment on your work as you progress 41 | - When you are done, remove the `work in progess` label and ping the maintainers for a review 42 | - Your pull request must receive a :thumbsup: from two [maintainers](MAINTAINERS) 43 | 44 | ### Prerequisites 45 | 46 | To build and work on this project you need to install: 47 | 48 | - [Node.js](https://nodejs.org/en/) (LTS) 49 | 50 | ### Check out code 51 | 52 | To get the code base, have [git](https://git-scm.com/downloads) installed and run: 53 | 54 | ```sh 55 | $ git clone git@github.com:saucelabs/tracelib.git 56 | ``` 57 | 58 | then ensure to install all project dependencies: 59 | 60 | ```sh 61 | $ cd tracelib 62 | $ npm install 63 | ``` 64 | 65 | You can find a fixture tracelog to test API changes in the [test directory](https://github.com/saucelabs/tracelib/blob/main/tests/__fixtures__/jankTraceLog.json). 66 | 67 | ### Build Project 68 | 69 | To compile all TypeScript files, run: 70 | 71 | ```sh 72 | $ npm run build 73 | ``` 74 | 75 | In order to automatically re-compile the files when files change, you can use the watch command: 76 | 77 | ```sh 78 | $ npm run watch 79 | ``` 80 | 81 | ### Test Project 82 | 83 | To test the project, run: 84 | 85 | ```sh 86 | $ npm run test 87 | ``` 88 | 89 | ### Commit messages 90 | Your commit messages ideally can answer two questions: what changed and why. The subject line should feature the “what” and the body of the commit should describe the “why”. 91 | 92 | When creating a pull request, its description should reference the corresponding issue id. 93 | 94 | ### Sign your work / Developer certificate of origin 95 | All contributions (including pull requests) must agree to the Developer Certificate of Origin (DCO) version 1.1. This is exactly the same one created and used by the Linux kernel developers and posted on http://developercertificate.org/. This is a developer's certification that he or she has the right to submit the patch for inclusion into the project. Simply submitting a contribution implies this agreement, however, please include a "Signed-off-by" tag in every patch (this tag is a conventional way to confirm that you agree to the DCO) - you can automate this with a [Git hook](https://stackoverflow.com/questions/15015894/git-add-signed-off-by-line-using-format-signoff-not-working) 96 | 97 | ``` 98 | git commit -s -m "adding X to change Y" 99 | ``` 100 | 101 | ## Release Project 102 | 103 | Contributor with release rights can release the project by calling one of the following commands: 104 | 105 | ```sh 106 | # for patch releases 107 | $ npm run release:patch 108 | # for minor releases 109 | $ npm run release:minor 110 | # for major releases 111 | $ npm run release:major 112 | ``` 113 | 114 | Ensure you release by following the [semantic versioning](https://semver.org/) principle. 115 | 116 | --- 117 | 118 | **Have fun, and happy hacking!** 119 | 120 | Thanks for your contributions! 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TraceLib 2 | ======== 3 | 4 | This library provides a set of models from the [`devtools-frontend`](https://github.com/ChromeDevTools/devtools-frontend) code base in order to parse trace log files. 5 | 6 | __Note:__ This is early development and not ready to be consumed yet! 7 | 8 | # Installation 9 | 10 | To install the package to your project, run: 11 | 12 | ```sh 13 | $ npm install --save tracelib 14 | ``` 15 | 16 | # Usage 17 | 18 | ## `getSummary` 19 | 20 | Fetch total time-durations of scripting, rendering, painting from tracelogs. 21 | 22 | ![Summary Data](./.assets/summary.png "Summary Data") 23 | 24 | ```js 25 | import Tracelib from 'tracelib' 26 | import JANK_TRACE_LOG from './jankTraceLog.json' 27 | 28 | const tasks = new Tracelib(JANK_TRACE_LOG) 29 | const summary = tasks.getSummary() 30 | console.log(summary) 31 | 32 | /** 33 | * output: 34 | * { 35 | * rendering: 847.373997092247, 36 | * painting: 69.94999980926514, 37 | * other: 9.896000564098358, 38 | * scripting: 394.4800021648407, 39 | * idle: 52.38300037384033, 40 | * startTime: 289959855.634, 41 | * endTime: 289961229.717 42 | * } 43 | ``` 44 | 45 | ## `getWarningCounts` 46 | 47 | Fetch amount of forced synchronous layouts and styles as well as long recurring handlers. 48 | 49 | ![Warning Counts](./.assets/warnings.png "Warning Counts") 50 | 51 | ```js 52 | import Tracelib from 'tracelib' 53 | import JANK_TRACE_LOG from './jankTraceLog.json' 54 | 55 | const tasks = new Tracelib(JANK_TRACE_LOG) 56 | const counts = tasks.getWarningCounts() 57 | console.log(counts) 58 | 59 | /** 60 | * { 61 | * LongRecurringHandler: 13, 62 | * ForcedStyle: 4684, 63 | * ForcedLayout: 4683 64 | * } 65 | */ 66 | ``` 67 | 68 | ## `getFPS` 69 | 70 | Fetch frames per second. 71 | 72 | ```js 73 | import Tracelib from 'tracelib' 74 | import JANK_TRACE_LOG from './jankTraceLog.json' 75 | 76 | const tasks = new Tracelib(JANK_TRACE_LOG) 77 | const fps = tasks.getFPS() 78 | console.log(fps) 79 | 80 | /** 81 | * { 82 | * times: [ 83 | * 289959949.734, 84 | * 289959955.22, 85 | * 289960052.234, 86 | * 289960142.388, 87 | * 289960233.175, 88 | * 289960324.01, 89 | * 289960416.646, 90 | * 289960508.145, 91 | * 289960600.602, 92 | * 289960695.329, 93 | * 289960790.75, 94 | * 289960882.274, 95 | * 289960977.634, 96 | * 289961069.395 97 | * ], 98 | * values: [ 99 | * 182.2821727559685, 100 | * 10.307790628308753, 101 | * 11.092131244032895, 102 | * 11.014792866762287, 103 | * 11.00897231503525, 104 | * 10.794939328106791, 105 | * 10.929081197725838, 106 | * 10.815838712204958, 107 | * 10.556652274643293, 108 | * 10.47987340271033, 109 | * 10.926095888726774, 110 | * 10.486577179634944, 111 | * 10.897876006628481, 112 | * 10.839990888916617 113 | * ] 114 | * } 115 | */ 116 | ``` 117 | 118 | ## `getMemoryCounters` 119 | 120 | Fetch data for JS Heap, Documents, Nodes, Listeners and GPU Memory from tracelogs. 121 | 122 | ![Memory Counters](./.assets/counters.png "Memory Counters") 123 | 124 | ```js 125 | import Tracelib from 'tracelib' 126 | import JANK_TRACE_LOG from './jankTraceLog.json' 127 | 128 | const tasks = new Tracelib(JANK_TRACE_LOG) 129 | const memoryInfo = tasks.getMemoryCounters() 130 | console.log(memoryInfo) 131 | 132 | /** 133 | * output: 134 | * { jsHeapSizeUsed: 135 | * { times: 136 | * [ 49970556.092, 137 | * 49970557.846, 138 | * 49970579.075, 139 | * ... 193 more items ], 140 | * values: 141 | * [ 15307712, 142 | * 15315152, 143 | * 15318984, 144 | * ... 193 more items ] }, 145 | * documents: 146 | * { times: 147 | * [ 49970556.092, 148 | * 49970987.298, 149 | * 49970997.59, 150 | * 49971005.521, 151 | * 49971062.064, 152 | * 49971147.013, 153 | * 49971156.296, 154 | * 49971196.957, 155 | * 49971259.352, 156 | * 49972763.552, 157 | * 49972764.108 ], 158 | * values: [ 6, 7, 8, 9, 8, 6, 7, 6, 5, 6, 7 ] }, 159 | * nodes: 160 | * { times: 161 | * [ 49970556.092, 162 | * 49970570.227, 163 | * 49970946.325, 164 | * ... 165 | * 49973091.931 ], 166 | * values: 167 | * [ 4339, 168 | * 5192, 169 | * 6727, 170 | * ... 171 | * 4528 ] }, 172 | * jsEventListeners: 173 | * { times: 174 | * [ 49970556.092, 175 | * 49970958.971, 176 | * 49970987.298, 177 | * ... 178 | * 49972763.552 ], 179 | * values: 180 | * [ 101, 181 | * 102, 182 | * 103, 183 | * ... 184 | * 111 ] }, 185 | * gpuMemoryUsedKB: { times: [], values: [] } } 186 | */ 187 | ``` 188 | 189 | ## `getDetailStats` 190 | 191 | Fetch data (timestamp and values) of scripting, rendering, painting from tracelogs. 192 | 193 | ![Detail Data](./.assets/detail.png "Detail Data") 194 | 195 | ```js 196 | import Tracelib from 'tracelib' 197 | import JANK_TRACE_LOG from './jankTraceLog.json' 198 | 199 | const tasks = new Tracelib(JANK_TRACE_LOG) 200 | const detail = tasks.getDetailStats() 201 | console.log(detail) 202 | 203 | /** 204 | * output: 205 | * { 206 | * rendering: { 207 | * times: [ 49970556.092, ..., 49972763.552 ], 208 | * values: [1, ..., 5] 209 | * }, 210 | * painting: { 211 | * times: [ 49970556.092, ..., 49972763.552 ], 212 | * values: [1, ..., 5] 213 | * }, 214 | * other: { 215 | * times: [ 49970556.092, ..., 49972763.552 ], 216 | * values: [1, ..., 5] 217 | * }, 218 | * scripting: { 219 | * times: [ 49970556.092, ..., 49972763.552 ], 220 | * values: [1, ..., 5] 221 | * }, 222 | * range: { 223 | * times: [ 289959855.634, 289961229.717 ], 224 | * values: [ 289959855.634, 289961229.717 ] 225 | * } 226 | * } 227 | ``` 228 | 229 | # Test 230 | 231 | To test this package, run: 232 | 233 | ```sh 234 | $ npm run test 235 | ``` 236 | -------------------------------------------------------------------------------- /devtools/common/index.ts: -------------------------------------------------------------------------------- 1 | import Settings from './settings' 2 | 3 | export default class Common { 4 | public static moduleSetting(settingName: string): Settings { 5 | return Settings.moduleSetting(settingName) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /devtools/common/settings.ts: -------------------------------------------------------------------------------- 1 | export default class Settings { 2 | private static _moduleSettings: Map = new Map() 3 | 4 | /** 5 | * @param {string} settingName 6 | * @return {!Common.Setting} 7 | */ 8 | public static moduleSetting(settingName: string): Settings { 9 | const setting = this._moduleSettings.get(settingName) 10 | if (!setting) { 11 | return { 12 | get: (): null => null 13 | } 14 | } 15 | return setting 16 | } 17 | 18 | public get (): boolean { 19 | return true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /devtools/cpuProfileDataModel/cpuProfileNode.ts: -------------------------------------------------------------------------------- 1 | import ProfileNode from '../profileTreeModel/profileNode' 2 | import { CallFrame } from '../types' 3 | 4 | export default class CPUProfileNode extends ProfileNode { 5 | /** 6 | * @param {!Protocol.Profiler.ProfileNode} node 7 | * @param {number} sampleTime 8 | */ 9 | public constructor (node: ProfileNode, sampleTime: number) { 10 | /** 11 | * Backward compatibility for old SamplingHeapProfileNode format. 12 | */ 13 | const nodeCallFrame: CallFrame = { 14 | functionName: node.functionName, 15 | scriptId: node.scriptId, 16 | url: node.url, 17 | lineNumber: node.lineNumber - 1, 18 | columnNumber: node.columnNumber - 1 19 | } 20 | 21 | const callFrame = node.callFrame || nodeCallFrame 22 | super(callFrame) 23 | 24 | this.id = node.id 25 | this.self = node.hitCount * sampleTime 26 | this.positionTicks = node.positionTicks 27 | 28 | /** 29 | * Compatibility: legacy backends could provide "no reason" for optimized functions. 30 | */ 31 | this.deoptReason = node.deoptReason && node.deoptReason !== 'no reason' ? node.deoptReason : null 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /devtools/cpuProfileDataModel/index.ts: -------------------------------------------------------------------------------- 1 | import CPUProfileNode from './cpuProfileNode' 2 | import ProfileNode from '../profileTreeModel/profileNode' 3 | import ProfileTreeModel from '../profileTreeModel' 4 | import { lowerBound, stableSort } from '../utils' 5 | import { Profile } from '../types' 6 | 7 | export default class CPUProfileDataModel extends ProfileTreeModel { 8 | public profileStartTime: number 9 | public profileEndTime: number 10 | public timestamps: number[] 11 | public samples: number[] 12 | public lines: number[] 13 | public totalHitCount: number 14 | public profileHead: ProfileNode 15 | public gcNode: ProfileNode 16 | public programNode?: ProfileNode 17 | public idleNode?: ProfileNode 18 | 19 | private _stackStartTimes?: Float64Array 20 | private _stackChildrenDuration?: Float64Array 21 | private _idToNode: Map 22 | 23 | /** 24 | * @param {!Protocol.Profiler.Profile} profile 25 | */ 26 | public constructor (profile: Profile) { 27 | super() 28 | const isLegacyFormat = !!profile['head'] 29 | 30 | if (isLegacyFormat) { 31 | // Legacy format contains raw timestamps and start/stop times are in seconds. 32 | this.profileStartTime = profile.startTime * 1000 33 | this.profileEndTime = profile.endTime * 1000 34 | this.timestamps = profile.timestamps 35 | this._compatibilityConversionHeadToNodes(profile) 36 | } else { 37 | // Current format encodes timestamps as deltas. Start/stop times are in microseconds. 38 | this.profileStartTime = profile.startTime / 1000 39 | this.profileEndTime = profile.endTime / 1000 40 | this.timestamps = this._convertTimeDeltas(profile) 41 | } 42 | 43 | this.samples = profile.samples 44 | this.lines = profile.lines 45 | this.totalHitCount = 0 46 | this.profileHead = this._translateProfileTree(profile.nodes) 47 | this.initialize(this.profileHead) 48 | this._extractMetaNodes() 49 | 50 | if (this.samples) { 51 | this._buildIdToNodeMap() 52 | this._sortSamples() 53 | this._normalizeTimestamps() 54 | this._fixMissingSamples() 55 | } 56 | } 57 | 58 | /** 59 | * @param {!Protocol.Profiler.Profile} profile 60 | */ 61 | private _compatibilityConversionHeadToNodes (profile: Profile): void { 62 | /** @type {!Array} */ 63 | const nodes: ProfileNode[] = [] 64 | 65 | /** 66 | * @param {!Protocol.Profiler.ProfileNode} node 67 | * @return {number} 68 | */ 69 | function convertNodesTree (node: ProfileNode): number { 70 | nodes.push(node); 71 | // TODO(Christian) fix typings 72 | (node.children as unknown as number[]) = node.children.map(convertNodesTree) 73 | return node.id 74 | } 75 | 76 | if (!profile.head || profile.nodes) { 77 | return 78 | } 79 | 80 | convertNodesTree(profile.head) 81 | profile.nodes = nodes 82 | delete profile.head 83 | } 84 | 85 | /** 86 | * @param {!Protocol.Profiler.Profile} profile 87 | * @return {?Array} 88 | */ 89 | private _convertTimeDeltas (profile: Profile): number[] { 90 | if (!profile.timeDeltas) { 91 | return null 92 | } 93 | 94 | let lastTimeUsec = profile.startTime 95 | const timestamps = new Array(profile.timeDeltas.length) 96 | 97 | for (let i = 0; i < profile.timeDeltas.length; ++i) { 98 | lastTimeUsec += profile.timeDeltas[i] 99 | timestamps[i] = lastTimeUsec 100 | } 101 | 102 | return timestamps 103 | } 104 | 105 | /** 106 | * @param {!Array} nodes 107 | * @return {!CPUProfileNode} 108 | */ 109 | private _translateProfileTree (nodes: ProfileNode[]): CPUProfileNode { 110 | /** @type {!Map} */ 111 | const nodeByIdMap: Map = new Map() 112 | 113 | /** 114 | * @param {!Array} nodes 115 | */ 116 | function buildChildrenFromParents (nodes: ProfileNode[]): void { 117 | if (nodes[0].children) { 118 | return 119 | } 120 | 121 | nodes[0].children = [] 122 | for (let i = 1; i < nodes.length; ++i) { 123 | const node = nodes[i] 124 | // TODO(Christian) fix typings 125 | const parentNode = nodeByIdMap.get((node.parent as unknown as number)) 126 | // TODO(Christian) fix typings 127 | if (parentNode.children) { 128 | (parentNode.children as unknown as number[]).push(node.id) 129 | } else { 130 | (parentNode.children as unknown as number[]) = [node.id] 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * @param {!Array} nodes 137 | * @param {!Array|undefined} samples 138 | */ 139 | function buildHitCountFromSamples (nodes: ProfileNode[], samples?: number[]): void { 140 | /** 141 | * hitCount not defined in ProfileNode model 142 | */ 143 | if (typeof(nodes[0].hitCount) === 'number') { 144 | return 145 | } 146 | 147 | console.assert(Boolean(samples), 'Error: Neither hitCount nor samples are present in profile.') 148 | for (let i = 0; i < nodes.length; ++i) { 149 | nodes[i].hitCount = 0 150 | } 151 | 152 | for (let i = 0; i < samples.length; ++i) { 153 | ++nodeByIdMap.get(samples[i]).hitCount 154 | } 155 | } 156 | 157 | for (let i = 0; i < nodes.length; ++i) { 158 | const node: ProfileNode = nodes[i] 159 | nodeByIdMap.set(node.id, node) 160 | } 161 | 162 | buildHitCountFromSamples(nodes, this.samples) 163 | buildChildrenFromParents(nodes) 164 | this.totalHitCount = nodes.reduce((acc, node): number => acc + node.hitCount, 0) 165 | const sampleTime = (this.profileEndTime - this.profileStartTime) / this.totalHitCount 166 | const root = nodes[0] 167 | /** @type {!Map} */ 168 | const idMap = new Map([[root.id, root.id]]) 169 | const resultRoot = new CPUProfileNode(root, sampleTime) 170 | const parentNodeStack = root.children.map((): CPUProfileNode => resultRoot) 171 | 172 | // TODO(Christian) fix typings 173 | const sourceNodeStack = root.children.map((id): ProfileNode => nodeByIdMap.get(((id as unknown as number)))) 174 | while (sourceNodeStack.length) { 175 | let parentNode = parentNodeStack.pop() 176 | const sourceNode = sourceNodeStack.pop() 177 | if (!sourceNode.children) { 178 | sourceNode.children = [] 179 | } 180 | 181 | const targetNode = new CPUProfileNode(sourceNode, sampleTime) 182 | parentNode.children.push(targetNode) 183 | parentNode = targetNode 184 | idMap.set(sourceNode.id, parentNode.id) 185 | parentNodeStack.push.apply(parentNodeStack, sourceNode.children.map((): CPUProfileNode => parentNode)) 186 | /** 187 | * type defect 188 | */ 189 | // sourceNodeStack.push.apply(sourceNodeStack, sourceNode.children.map((id): ProfileNode => nodeByIdMap.get(id))) 190 | } 191 | 192 | if (this.samples) { 193 | this.samples = this.samples.map((id): number => idMap.get(id)) 194 | } 195 | 196 | return resultRoot 197 | } 198 | 199 | private _sortSamples (): void { 200 | const timestamps = this.timestamps 201 | if (!timestamps) { 202 | return 203 | } 204 | 205 | const samples = this.samples 206 | const indices = [...timestamps.keys()] 207 | stableSort(indices, (a, b): number => timestamps[a] - timestamps[b]) 208 | for (let i = 0; i < timestamps.length; ++i) { 209 | let index = indices[i] 210 | if (index === i) { 211 | continue 212 | } 213 | 214 | // Move items in a cycle. 215 | const savedTimestamp = timestamps[i] 216 | const savedSample = samples[i] 217 | let currentIndex = i 218 | while (index !== i) { 219 | samples[currentIndex] = samples[index] 220 | timestamps[currentIndex] = timestamps[index] 221 | currentIndex = index 222 | index = indices[index] 223 | indices[currentIndex] = currentIndex 224 | } 225 | 226 | samples[currentIndex] = savedSample 227 | timestamps[currentIndex] = savedTimestamp 228 | } 229 | } 230 | 231 | private _normalizeTimestamps (): void { 232 | let timestamps = this.timestamps 233 | if (!timestamps) { 234 | // Support loading old CPU profiles that are missing timestamps. 235 | // Derive timestamps from profile start and stop times. 236 | const profileStartTime = this.profileStartTime 237 | const interval = (this.profileEndTime - profileStartTime) / this.samples.length 238 | timestamps = [...Array(this.samples.length + 1)].map((): number => 0) 239 | 240 | for (let i = 0; i < timestamps.length; ++i) { 241 | timestamps[i] = profileStartTime + i * interval 242 | } 243 | 244 | this.timestamps = timestamps 245 | return 246 | } 247 | 248 | /** 249 | * Convert samples from usec to msec 250 | */ 251 | for (let i = 0; i < timestamps.length; ++i) { 252 | timestamps[i] /= 1000 253 | } 254 | 255 | /** 256 | * Support for a legacy format where were no timeDeltas. 257 | * Add an extra timestamp used to calculate the last sample duration. 258 | */ 259 | if (this.samples.length === timestamps.length) { 260 | const averageSample = (timestamps[timestamps.length - 1] - timestamps[0]) / (timestamps.length - 1) 261 | this.timestamps.push(timestamps[timestamps.length - 1] + averageSample) 262 | } 263 | 264 | this.profileStartTime = timestamps[0] 265 | this.profileEndTime = timestamps[timestamps.length - 1] 266 | } 267 | 268 | private _buildIdToNodeMap (): void { 269 | /** @type {!Map} */ 270 | this._idToNode = new Map() 271 | const idToNode = this._idToNode 272 | const stack = [this.profileHead] 273 | 274 | while (stack.length) { 275 | const node = stack.pop() 276 | idToNode.set(node.id, node) 277 | stack.push.apply(stack, node.children) 278 | } 279 | } 280 | 281 | private _extractMetaNodes (): void { 282 | const topLevelNodes = this.profileHead.children 283 | for (let i = 0; i < topLevelNodes.length && !(this.gcNode && this.programNode && this.idleNode); i++) { 284 | const node = topLevelNodes[i] 285 | if (node.functionName === '(garbage collector)') { 286 | this.gcNode = node 287 | } else if (node.functionName === '(program)') { 288 | this.programNode = node 289 | } else if (node.functionName === '(idle)') { 290 | this.idleNode = node 291 | } 292 | } 293 | } 294 | 295 | /** 296 | * Sometimes sampler is not able to parse the JS stack and returns 297 | * a (program) sample instead. The issue leads to call frames belong 298 | * to the same function invocation being split apart. 299 | * Here's a workaround for that. When there's a single (program) sample 300 | * between two call stacks sharing the same bottom node, it is replaced 301 | * with the preceeding sample. 302 | */ 303 | private _fixMissingSamples (): void { 304 | const samples = this.samples 305 | const samplesCount = samples.length 306 | const idToNode = this._idToNode 307 | const programNodeId = this.programNode.id 308 | const gcNodeId = this.gcNode ? this.gcNode.id : -1 309 | const idleNodeId = this.idleNode ? this.idleNode.id : -1 310 | let prevNodeId = samples[0] 311 | let nodeId = samples[1] 312 | let count = 0 313 | 314 | /** 315 | * @param {!SDK.ProfileNode} node 316 | * @return {!SDK.ProfileNode} 317 | */ 318 | function bottomNode (node: ProfileNode): ProfileNode { 319 | while (node && node.parent && node.parent.parent) { 320 | node = node.parent 321 | } 322 | 323 | return node 324 | } 325 | 326 | /** 327 | * @param {number} nodeId 328 | * @return {boolean} 329 | */ 330 | function isSystemNode (nodeId: number): boolean { 331 | return nodeId === programNodeId || nodeId === gcNodeId || nodeId === idleNodeId 332 | } 333 | 334 | if (!this.programNode || samplesCount < 3) { 335 | return 336 | } 337 | 338 | for (let sampleIndex = 1; sampleIndex < samplesCount - 1; sampleIndex++) { 339 | const nextNodeId = samples[sampleIndex + 1] 340 | if ( 341 | nodeId === programNodeId && 342 | !isSystemNode(prevNodeId) && 343 | !isSystemNode(nextNodeId) && 344 | bottomNode(idToNode.get(prevNodeId)) === bottomNode(idToNode.get(nextNodeId)) 345 | ) { 346 | ++count 347 | samples[sampleIndex] = prevNodeId 348 | } 349 | 350 | prevNodeId = nodeId 351 | nodeId = nextNodeId 352 | } 353 | 354 | if (count) { 355 | console.warn(`DevTools: CPU profile parser is fixing ${count} missing samples.`) 356 | } 357 | } 358 | 359 | /** 360 | * @param {function(number, !CPUProfileNode, number)} openFrameCallback 361 | * @param {function(number, !CPUProfileNode, number, number, number)} closeFrameCallback 362 | * @param {number=} startTime 363 | * @param {number=} stopTime 364 | */ 365 | public forEachFrame ( 366 | openFrameCallback: (depth: number, node: CPUProfileNode, startTime: number) => void, 367 | closeFrameCallback: (depth: number, node: CPUProfileNode, startTime: number, duration: number, selfTime: number) => void, 368 | startTime?: number, 369 | stopTime?: number 370 | ): void { 371 | if (!this.profileHead || !this.samples) { 372 | return 373 | } 374 | 375 | startTime = startTime || 0 376 | stopTime = stopTime || Infinity 377 | const samples = this.samples 378 | const timestamps = this.timestamps 379 | const idToNode = this._idToNode 380 | const gcNode = this.gcNode 381 | const samplesCount = samples.length 382 | const startIndex = lowerBound(timestamps, startTime) 383 | const stackNodes: CPUProfileNode[] = [] 384 | 385 | let stackTop = 0 386 | let prevId = this.profileHead.id 387 | let sampleTime: number 388 | let gcParentNode: CPUProfileNode = null 389 | 390 | // Extra slots for gc being put on top, 391 | // and one at the bottom to allow safe stackTop-1 access. 392 | const stackDepth = this.maxDepth + 3 393 | if (!this._stackStartTimes) { 394 | this._stackStartTimes = new Float64Array(stackDepth) 395 | } 396 | 397 | const stackStartTimes = this._stackStartTimes 398 | if (!this._stackChildrenDuration) { 399 | this._stackChildrenDuration = new Float64Array(stackDepth) 400 | } 401 | 402 | const stackChildrenDuration = this._stackChildrenDuration 403 | 404 | let node: CPUProfileNode 405 | let sampleIndex: number 406 | for (sampleIndex = startIndex; sampleIndex < samplesCount; sampleIndex++) { 407 | sampleTime = timestamps[sampleIndex] 408 | if (sampleTime >= stopTime) { 409 | break 410 | } 411 | 412 | const id = samples[sampleIndex] 413 | if (id === prevId) { 414 | continue 415 | } 416 | 417 | node = idToNode.get(id) 418 | let prevNode = idToNode.get(prevId) 419 | 420 | /** 421 | * GC samples have no stack, so we just put GC node on top 422 | * of the last recorded sample. 423 | */ 424 | if (node === gcNode) { 425 | gcParentNode = prevNode 426 | openFrameCallback(gcParentNode.depth + 1, gcNode, sampleTime) 427 | stackStartTimes[++stackTop] = sampleTime 428 | stackChildrenDuration[stackTop] = 0 429 | prevId = id 430 | continue 431 | } 432 | 433 | /** 434 | * end of GC frame 435 | */ 436 | if (prevNode === gcNode) { 437 | const start = stackStartTimes[stackTop] 438 | const duration = sampleTime - start 439 | stackChildrenDuration[stackTop - 1] += duration 440 | closeFrameCallback( 441 | gcParentNode.depth + 1, 442 | gcNode, 443 | start, 444 | duration, 445 | duration - stackChildrenDuration[stackTop] 446 | ) 447 | --stackTop 448 | prevNode = gcParentNode 449 | prevId = prevNode.id 450 | gcParentNode = null 451 | } 452 | 453 | while (node.depth > prevNode.depth) { 454 | stackNodes.push(node) 455 | node = node.parent 456 | } 457 | 458 | /** 459 | * Go down to the LCA and close current intervals. 460 | */ 461 | while (prevNode !== node) { 462 | const start = stackStartTimes[stackTop] 463 | const duration = sampleTime - start 464 | stackChildrenDuration[stackTop - 1] += duration 465 | closeFrameCallback( 466 | prevNode.depth, 467 | prevNode, 468 | start, 469 | duration, 470 | duration - stackChildrenDuration[stackTop] 471 | ) 472 | --stackTop 473 | if (node.depth === prevNode.depth) { 474 | stackNodes.push(node) 475 | node = node.parent 476 | } 477 | prevNode = prevNode.parent 478 | } 479 | 480 | /** 481 | * Go up the nodes stack and open new intervals. 482 | */ 483 | while (stackNodes.length) { 484 | node = stackNodes.pop() 485 | openFrameCallback(node.depth, node, sampleTime) 486 | stackStartTimes[++stackTop] = sampleTime 487 | stackChildrenDuration[stackTop] = 0 488 | } 489 | 490 | prevId = id 491 | } 492 | 493 | sampleTime = timestamps[sampleIndex] || this.profileEndTime 494 | if (idToNode.get(prevId) === gcNode) { 495 | const start = stackStartTimes[stackTop] 496 | const duration = sampleTime - start 497 | stackChildrenDuration[stackTop - 1] += duration 498 | closeFrameCallback(gcParentNode.depth + 1, node, start, duration, duration - stackChildrenDuration[stackTop]) 499 | --stackTop 500 | prevId = gcParentNode.id 501 | } 502 | 503 | for (let node = idToNode.get(prevId); node.parent; node = node.parent) { 504 | const start = stackStartTimes[stackTop] 505 | const duration = sampleTime - start 506 | stackChildrenDuration[stackTop - 1] += duration 507 | closeFrameCallback( 508 | node.depth, 509 | node, 510 | start, 511 | duration, 512 | duration - stackChildrenDuration[stackTop] 513 | ) 514 | --stackTop 515 | } 516 | } 517 | 518 | /** 519 | * @param {number} index 520 | * @return {?CPUProfileNode} 521 | */ 522 | public nodeByIndex (index: number): CPUProfileNode { 523 | return this._idToNode.get(this.samples[index]) || null 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /devtools/loader.ts: -------------------------------------------------------------------------------- 1 | import TracingModel from './tracingModel/index' 2 | import PerformanceModel from './timelineModel/performanceModel' 3 | 4 | export default class TimelineLoader { 5 | private _tracingModel: TracingModel 6 | public performanceModel: PerformanceModel 7 | private _traceLog: any 8 | 9 | public constructor(traceLog: any) { 10 | this._tracingModel = new TracingModel() 11 | this._traceLog = traceLog 12 | } 13 | 14 | /** 15 | * @param {string} data 16 | */ 17 | public init(): void { 18 | try { 19 | this._tracingModel.addEvents(this._traceLog) 20 | } catch (e) { 21 | console.error('Malformed timeline data: %s', e.toString()) 22 | return 23 | } 24 | this._tracingModel.tracingComplete() 25 | this.performanceModel = new PerformanceModel() 26 | this.performanceModel.setTracingModel(this._tracingModel) 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /devtools/profileTreeModel/index.ts: -------------------------------------------------------------------------------- 1 | import ProfileNode from './profileNode' 2 | 3 | export default class ProfileTreeModel { 4 | public root: ProfileNode 5 | public maxDepth: number 6 | public total: number 7 | 8 | /** 9 | * @param {!SDK.ProfileNode} root 10 | * @protected 11 | */ 12 | public initialize(root: ProfileNode): void { 13 | this.root = root 14 | this._assignDepthsAndParents() 15 | this.total = this._calculateTotals(this.root) 16 | } 17 | 18 | private _assignDepthsAndParents (): void { 19 | const root = this.root 20 | root.depth = -1 21 | root.parent = null 22 | this.maxDepth = 0 23 | 24 | const nodesToTraverse: ProfileNode[] = [root] 25 | while (nodesToTraverse.length) { 26 | const parent = nodesToTraverse.pop() 27 | const depth = parent.depth + 1 28 | if (depth > this.maxDepth) { 29 | this.maxDepth = depth 30 | } 31 | 32 | const children = parent.children 33 | const length = children.length 34 | for (let i = 0; i < length; ++i) { 35 | const child = children[i] 36 | child.depth = depth 37 | child.parent = parent 38 | if (child.children.length) { 39 | nodesToTraverse.push(child) 40 | } 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * @param {!SDK.ProfileNode} root 47 | * @return {number} 48 | */ 49 | private _calculateTotals (root: ProfileNode): number { 50 | const nodesToTraverse: ProfileNode[] = [root] 51 | const dfsList: ProfileNode[] = [] 52 | 53 | while (nodesToTraverse.length) { 54 | const node = nodesToTraverse.pop() 55 | node.total = node.self 56 | dfsList.push(node) 57 | nodesToTraverse.push(...node.children) 58 | } 59 | 60 | while (dfsList.length > 1) { 61 | const node = dfsList.pop() 62 | node.parent.total += node.total 63 | } 64 | 65 | return root.total 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /devtools/profileTreeModel/profileNode.ts: -------------------------------------------------------------------------------- 1 | import { CallFrame } from '../types' 2 | 3 | export default class ProfileNode { 4 | public callFrame: CallFrame 5 | public callUID: string 6 | public self: number 7 | public total: number 8 | public id: number 9 | public parent?: ProfileNode 10 | public children: ProfileNode[] 11 | public hitCount: number 12 | public positionTicks: number[] 13 | public deoptReason: string 14 | public depth?: number 15 | 16 | /** 17 | * @param {!Protocol.Runtime.CallFrame} callFrame 18 | */ 19 | public constructor (callFrame: CallFrame) { 20 | this.callFrame = callFrame 21 | this.callUID = `${callFrame.functionName}@${callFrame.scriptId}:${callFrame.lineNumber}:${callFrame.columnNumber}` 22 | this.self = 0 23 | this.total = 0 24 | this.id = 0 25 | this.parent = null 26 | this.children = [] 27 | } 28 | 29 | /** 30 | * @return {string} 31 | */ 32 | public get functionName (): string { 33 | return this.callFrame.functionName 34 | } 35 | 36 | /** 37 | * @return {string} 38 | */ 39 | public get scriptId (): string { 40 | return this.callFrame.scriptId 41 | } 42 | 43 | /** 44 | * @return {string} 45 | */ 46 | public get url (): string { 47 | return this.callFrame.url 48 | } 49 | 50 | /** 51 | * @return {number} 52 | */ 53 | public get lineNumber (): number { 54 | return this.callFrame.lineNumber 55 | } 56 | 57 | /** 58 | * @return {number} 59 | */ 60 | public get columnNumber (): number { 61 | return this.callFrame.columnNumber 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /devtools/runtime/experimentsSupport.ts: -------------------------------------------------------------------------------- 1 | export default class ExperimentsSupport { 2 | public isEnabled(experimentName: string): boolean { 3 | return !!experimentName 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /devtools/runtime/index.ts: -------------------------------------------------------------------------------- 1 | import ExperimentsSupport from './experimentsSupport' 2 | 3 | export default class Runtime { 4 | public static experiments: ExperimentsSupport = new ExperimentsSupport() 5 | } 6 | -------------------------------------------------------------------------------- /devtools/timelineModel/counterGraph/calculator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @implements {PerfUI.TimelineGrid.Calculator} 3 | * @unrestricted 4 | */ 5 | export default class Calculator { 6 | private _zeroTime: number 7 | private _minimumBoundary: number 8 | private _maximumBoundary: number 9 | private _workingArea: number 10 | 11 | /** 12 | * @param {number} time 13 | */ 14 | public setZeroTime(time: number): void { 15 | this._zeroTime = time 16 | } 17 | 18 | /** 19 | * @override 20 | * @param {number} time 21 | * @return {number} 22 | */ 23 | public computePosition(time: number): number { 24 | return ((time - this._minimumBoundary) / this.boundarySpan()) * this._workingArea 25 | } 26 | 27 | public setWindow(minimumBoundary: number, maximumBoundary: number): void { 28 | this._minimumBoundary = minimumBoundary 29 | this._maximumBoundary = maximumBoundary 30 | } 31 | 32 | /** 33 | * @param {number} clientWidth 34 | */ 35 | public setDisplayWidth(clientWidth: number): void { 36 | this._workingArea = clientWidth 37 | } 38 | 39 | /** 40 | * @override 41 | * @return {number} 42 | */ 43 | public maximumBoundary(): number { 44 | return this._maximumBoundary 45 | } 46 | 47 | /** 48 | * @override 49 | * @return {number} 50 | */ 51 | public minimumBoundary(): number { 52 | return this._minimumBoundary 53 | } 54 | 55 | /** 56 | * @override 57 | * @return {number} 58 | */ 59 | public zeroTime(): number { 60 | return this._zeroTime 61 | } 62 | 63 | /** 64 | * @override 65 | * @return {number} 66 | */ 67 | public boundarySpan(): number { 68 | return this._maximumBoundary - this._minimumBoundary 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /devtools/timelineModel/counterGraph/counter.ts: -------------------------------------------------------------------------------- 1 | import Calculator from './calculator' 2 | import { constrain, upperBound, lowerBound } from '../../utils' 3 | 4 | /** 5 | * @unrestricted 6 | */ 7 | export default class Counter { 8 | public times: number[] 9 | public values: number[] 10 | private _limitValue: number 11 | private _minimumIndex: number 12 | private _maximumIndex: number 13 | private _minTime: number 14 | private _maxTime: number 15 | public x: number[] 16 | 17 | public constructor() { 18 | this.times = [] 19 | this.values = [] 20 | } 21 | 22 | /** 23 | * @param {number} time 24 | * @param {number} value 25 | */ 26 | public appendSample(time: number, value: number): void { 27 | if (this.values[this.values.length - 1] === value) { 28 | return 29 | } 30 | this.times.push(time) 31 | this.values.push(value) 32 | } 33 | 34 | public reset(): void { 35 | this.times = [] 36 | this.values = [] 37 | } 38 | 39 | /** 40 | * @param {number} value 41 | */ 42 | public setLimit(value: number): void { 43 | this._limitValue = value 44 | } 45 | 46 | /** 47 | * @return {!{min: number, max: number}} 48 | */ 49 | private _calculateBounds(): { min: number; max: number } { 50 | let maxValue 51 | let minValue 52 | for (let i = this._minimumIndex; i <= this._maximumIndex; i++) { 53 | const value = this.values[i] 54 | if (minValue === undefined || value < minValue) { 55 | minValue = value 56 | } 57 | if (maxValue === undefined || value > maxValue) { 58 | maxValue = value 59 | } 60 | } 61 | minValue = minValue || 0 62 | maxValue = maxValue || 1 63 | if (this._limitValue) { 64 | if (maxValue > this._limitValue * 0.5) { 65 | maxValue = Math.max(maxValue, this._limitValue) 66 | } 67 | minValue = Math.min(minValue, this._limitValue) 68 | } 69 | return { 70 | min: minValue, 71 | max: maxValue, 72 | } 73 | } 74 | 75 | /** 76 | * @param {!Timeline.CountersGraph.Calculator} calculator 77 | */ 78 | private _calculateVisibleIndexes(calculator: Calculator): void { 79 | const start = calculator.minimumBoundary() 80 | const end = calculator.maximumBoundary() 81 | 82 | // Maximum index of element whose time <= start. 83 | this._minimumIndex = constrain(upperBound(this.times, start) - 1, 0, this.times.length - 1) 84 | 85 | // Minimum index of element whose time >= end. 86 | this._maximumIndex = constrain(lowerBound(this.times, end), 0, this.times.length - 1) 87 | 88 | // Current window bounds. 89 | this._minTime = start 90 | this._maxTime = end 91 | } 92 | 93 | /** 94 | * @param {number} width 95 | */ 96 | private _calculateXValues(width: number): void { 97 | if (!this.values.length) { 98 | return 99 | } 100 | 101 | const xFactor = width / (this._maxTime - this._minTime) 102 | 103 | this.x = new Array(this.values.length) 104 | for (let i = this._minimumIndex + 1; i <= this._maximumIndex; i++) { 105 | this.x[i] = xFactor * (this.times[i] - this._minTime) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /devtools/timelineModel/counterGraph/index.ts: -------------------------------------------------------------------------------- 1 | import Calculator from './calculator' 2 | import Counter from './counter' 3 | import { CountersObject, RecordType, EventData } from '../../types' 4 | import PerformanceModel from '../performanceModel' 5 | import Track from '../track' 6 | 7 | /** 8 | * UI class, therefor only a portion of the original logic is ported 9 | * 10 | * @unrestricted 11 | * @custom 12 | */ 13 | export default class CountersGraph { 14 | private _calculator: Calculator 15 | private _counters: Counter[] 16 | private _countersByName: CountersObject 17 | private _gpuMemoryCounter: Counter 18 | private _model: PerformanceModel 19 | private _track: Track 20 | 21 | public constructor() { 22 | this._calculator = new Calculator() 23 | this._counters = [] 24 | this._countersByName = {} 25 | this._countersByName['jsHeapSizeUsed'] = this._createCounter('JS Heap') 26 | this._countersByName['documents'] = this._createCounter('Documents') 27 | this._countersByName['nodes'] = this._createCounter('Nodes') 28 | this._countersByName['jsEventListeners'] = this._createCounter('Listeners') 29 | this._gpuMemoryCounter = this._createCounter('GPU Memory') 30 | this._countersByName['gpuMemoryUsedKB'] = this._gpuMemoryCounter 31 | } 32 | 33 | /** 34 | * @param {?Timeline.PerformanceModel} model 35 | * @param {?TimelineModel.TimelineModel.Track} track 36 | */ 37 | public setModel(model: PerformanceModel, track: Track): CountersObject { 38 | this._calculator.setZeroTime(model ? model.timelineModel().minimumRecordTime() : 0) 39 | for (let i = 0; i < this._counters.length; ++i) { 40 | this._counters[i].reset() 41 | } 42 | this._track = track 43 | if (!track) { 44 | return 45 | } 46 | const events = track.syncEvents() 47 | for (let i = 0; i < events.length; ++i) { 48 | const event = events[i] 49 | if (event.name !== RecordType.UpdateCounters) { 50 | continue 51 | } 52 | 53 | const counters: EventData = event.args.data 54 | if (!counters) { 55 | return 56 | } 57 | for (const name in counters) { 58 | const counter = this._countersByName[name] 59 | if (counter) { 60 | counter.appendSample(event.startTime, (counters as any)[name]) 61 | } 62 | } 63 | 64 | const gpuMemoryLimitCounterName = 'gpuMemoryLimitKB' 65 | if (gpuMemoryLimitCounterName in counters) { 66 | this._gpuMemoryCounter.setLimit((counters as any)[gpuMemoryLimitCounterName]) 67 | } 68 | } 69 | return this._countersByName 70 | } 71 | 72 | /** 73 | * @param {string} uiName 74 | * @return {!Timeline.CountersGraph.Counter} 75 | */ 76 | private _createCounter(uiName: string): Counter { 77 | const counter = new Counter() 78 | this._counters.push(counter) 79 | return counter 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /devtools/timelineModel/invalidationTracker.ts: -------------------------------------------------------------------------------- 1 | import Event from '../tracingModel/event' 2 | import InvalidationTrackingEvent from './invalidationTrackingEvent' 3 | import { InvalidationMap, RecordType } from '../types' 4 | 5 | export default class InvalidationTracker { 6 | public static readonly invalidationTrackingEventsSymbol: unique symbol = Symbol('invalidationTrackingEvents') 7 | private _lastRecalcStyle: Event 8 | private _lastPaintWithLayer: Event 9 | private _didPaint: boolean 10 | private _invalidations: Record 11 | private _invalidationsByNodeId: Record 12 | 13 | public constructor () { 14 | this._lastRecalcStyle = null 15 | this._lastPaintWithLayer = null 16 | this._didPaint = false 17 | this._initializePerFrameState() 18 | } 19 | 20 | /** 21 | * @param {!SDK.TracingModel.Event} event 22 | * @return {?Array} 23 | */ 24 | public static invalidationEventsFor ( 25 | event: Event 26 | ): InvalidationTrackingEvent[] | null { 27 | return event[InvalidationTracker.invalidationTrackingEventsSymbol] || null 28 | } 29 | 30 | /** 31 | * @param {!TimelineModel.InvalidationTrackingEvent} invalidation 32 | */ 33 | public addInvalidation (invalidation: InvalidationTrackingEvent): void { 34 | this._startNewFrameIfNeeded() 35 | 36 | if (!invalidation.nodeId) { 37 | console.error('Invalidation lacks node information.') 38 | console.error(invalidation) 39 | return 40 | } 41 | 42 | // Suppress StyleInvalidator StyleRecalcInvalidationTracking invalidations because they 43 | // will be handled by StyleInvalidatorInvalidationTracking. 44 | // FIXME: Investigate if we can remove StyleInvalidator invalidations entirely. 45 | if ( 46 | invalidation.type === RecordType.StyleRecalcInvalidationTracking && 47 | invalidation.cause.reason === 'StyleInvalidator' 48 | ) { 49 | return 50 | } 51 | 52 | // Style invalidation events can occur before and during recalc style. didRecalcStyle 53 | // handles style invalidations that occur before the recalc style event but we need to 54 | // handle style recalc invalidations during recalc style here. 55 | const styleRecalcInvalidation = ( 56 | invalidation.type === RecordType.ScheduleStyleInvalidationTracking || 57 | invalidation.type === RecordType.StyleInvalidatorInvalidationTracking || 58 | invalidation.type === RecordType.StyleRecalcInvalidationTracking 59 | ) 60 | 61 | if (styleRecalcInvalidation) { 62 | const duringRecalcStyle = ( 63 | invalidation.startTime && 64 | this._lastRecalcStyle && 65 | invalidation.startTime >= this._lastRecalcStyle.startTime && 66 | invalidation.startTime <= this._lastRecalcStyle.endTime 67 | ) 68 | 69 | if (duringRecalcStyle) { 70 | this._associateWithLastRecalcStyleEvent(invalidation) 71 | } 72 | } 73 | 74 | /** 75 | * Record the invalidation so later events can look it up. 76 | */ 77 | // TODO(Christian) fix typings 78 | if (this._invalidations[invalidation.type as any]) { 79 | // TODO(Christian) fix typings 80 | this._invalidations[invalidation.type as any].push(invalidation) 81 | } else { 82 | // TODO(Christian) fix typings 83 | this._invalidations[invalidation.type as any] = [invalidation] 84 | } 85 | 86 | if (invalidation.nodeId) { 87 | if (this._invalidationsByNodeId[invalidation.nodeId]) { 88 | // TODO(Christian) fix typings 89 | this._invalidationsByNodeId[invalidation.nodeId].push(invalidation as any) 90 | } else { 91 | // TODO(Christian) fix typings 92 | this._invalidationsByNodeId[invalidation.nodeId] = [invalidation as any] 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * @param {!SDK.TracingModel.Event} recalcStyleEvent 99 | */ 100 | public didRecalcStyle (recalcStyleEvent: Event): void { 101 | this._lastRecalcStyle = recalcStyleEvent 102 | const types = [ 103 | RecordType.ScheduleStyleInvalidationTracking, 104 | RecordType.StyleInvalidatorInvalidationTracking, 105 | RecordType.StyleRecalcInvalidationTracking 106 | ] 107 | for (const invalidation of this._invalidationsOfTypes(types)) { 108 | this._associateWithLastRecalcStyleEvent(invalidation) 109 | } 110 | } 111 | 112 | /** 113 | * @param {!TimelineModel.InvalidationTrackingEvent} invalidation 114 | */ 115 | private _associateWithLastRecalcStyleEvent (invalidation: InvalidationTrackingEvent): void { 116 | if (invalidation.linkedRecalcStyleEvent) { 117 | return 118 | } 119 | 120 | const recalcStyleFrameId = this._lastRecalcStyle.args.beginData.frame 121 | 122 | if (invalidation.type === RecordType.StyleInvalidatorInvalidationTracking) { 123 | /** 124 | * Instead of calling _addInvalidationToEvent directly, we create synthetic 125 | * StyleRecalcInvalidationTracking events which will be added in _addInvalidationToEvent. 126 | */ 127 | this._addSyntheticStyleRecalcInvalidations(this._lastRecalcStyle, recalcStyleFrameId, invalidation) 128 | } else if (invalidation.type === RecordType.ScheduleStyleInvalidationTracking) { 129 | /** 130 | * ScheduleStyleInvalidationTracking events are only used for adding information to 131 | * StyleInvalidatorInvalidationTracking events. See: _addSyntheticStyleRecalcInvalidations. 132 | */ 133 | } else { 134 | this._addInvalidationToEvent(this._lastRecalcStyle, recalcStyleFrameId, invalidation) 135 | } 136 | 137 | invalidation.linkedRecalcStyleEvent = true 138 | } 139 | 140 | /** 141 | * @param {!SDK.TracingModel.Event} event 142 | * @param {number} frameId 143 | * @param {!TimelineModel.InvalidationTrackingEvent} styleInvalidatorInvalidation 144 | */ 145 | // TODO(Christian) fix typings 146 | private _addSyntheticStyleRecalcInvalidations (event: Event, frameId: string, styleInvalidatorInvalidation: InvalidationTrackingEvent): void { 147 | if (!styleInvalidatorInvalidation.invalidationList) { 148 | this._addSyntheticStyleRecalcInvalidation( 149 | styleInvalidatorInvalidation.tracingEvent, 150 | styleInvalidatorInvalidation 151 | ) 152 | return 153 | } 154 | 155 | if (!styleInvalidatorInvalidation.nodeId) { 156 | console.error('Invalidation lacks node information.') 157 | console.error(styleInvalidatorInvalidation) 158 | return 159 | } 160 | 161 | for (let i = 0; i < styleInvalidatorInvalidation.invalidationList.length; i++) { 162 | const setId = styleInvalidatorInvalidation.invalidationList[i]['id'] 163 | let lastScheduleStyleRecalculation 164 | const emptyList: InvalidationMap[] = [] 165 | const nodeInvalidations = this._invalidationsByNodeId[styleInvalidatorInvalidation.nodeId] || emptyList 166 | for (let j = 0; j < nodeInvalidations.length; j++) { 167 | const invalidation = nodeInvalidations[j] 168 | if ( 169 | invalidation.frame !== frameId || invalidation.invalidationSet !== setId || 170 | invalidation.type !== RecordType.ScheduleStyleInvalidationTracking 171 | ) { 172 | continue 173 | } 174 | lastScheduleStyleRecalculation = invalidation 175 | } 176 | 177 | if (!lastScheduleStyleRecalculation) { 178 | console.error('Failed to lookup the event that scheduled a style invalidator invalidation.') 179 | continue 180 | } 181 | 182 | this._addSyntheticStyleRecalcInvalidation( 183 | lastScheduleStyleRecalculation.tracingEvent as any, 184 | styleInvalidatorInvalidation 185 | ) 186 | } 187 | } 188 | 189 | /** 190 | * @param {!SDK.TracingModel.Event} baseEvent 191 | * @param {!TimelineModel.InvalidationTrackingEvent} styleInvalidatorInvalidation 192 | */ 193 | private _addSyntheticStyleRecalcInvalidation (baseEvent: Event, styleInvalidatorInvalidation: InvalidationTrackingEvent): void { 194 | const invalidation = new InvalidationTrackingEvent(baseEvent) 195 | invalidation.type = RecordType.StyleRecalcInvalidationTracking 196 | if (styleInvalidatorInvalidation.cause.reason) { 197 | invalidation.cause.reason = styleInvalidatorInvalidation.cause.reason 198 | } 199 | 200 | if (styleInvalidatorInvalidation.selectorPart) { 201 | invalidation.selectorPart = styleInvalidatorInvalidation.selectorPart 202 | } 203 | 204 | this.addInvalidation(invalidation) 205 | if (!invalidation.linkedRecalcStyleEvent) { 206 | this._associateWithLastRecalcStyleEvent(invalidation) 207 | } 208 | } 209 | 210 | /** 211 | * @param {!SDK.TracingModel.Event} layoutEvent 212 | */ 213 | public didLayout (layoutEvent: Event): void { 214 | if (!layoutEvent.args.beginData) { 215 | return 216 | } 217 | const layoutFrameId = layoutEvent.args.beginData.frame 218 | for (const invalidation of this._invalidationsOfTypes([RecordType.LayoutInvalidationTracking])) { 219 | if (invalidation.linkedLayoutEvent) { 220 | continue 221 | } 222 | this._addInvalidationToEvent(layoutEvent, layoutFrameId, invalidation) 223 | invalidation.linkedLayoutEvent = true 224 | } 225 | } 226 | 227 | /** 228 | * @param {!SDK.TracingModel.Event} paintEvent 229 | */ 230 | public didPaint (): void { 231 | this._didPaint = true 232 | } 233 | 234 | /** 235 | * @param {!SDK.TracingModel.Event} event 236 | * @param {number} eventFrameId 237 | * @param {!TimelineModel.InvalidationTrackingEvent} invalidation 238 | */ 239 | private _addInvalidationToEvent (event: Event, eventFrameId: number | string, invalidation: InvalidationTrackingEvent): void { 240 | if (eventFrameId !== invalidation.frame) { 241 | return 242 | } 243 | 244 | if (!event[InvalidationTracker.invalidationTrackingEventsSymbol]) { 245 | event[InvalidationTracker.invalidationTrackingEventsSymbol] = [invalidation] 246 | return 247 | } 248 | 249 | event[InvalidationTracker.invalidationTrackingEventsSymbol].push(invalidation) 250 | } 251 | 252 | /** 253 | * @param {!Array.=} types 254 | * @return {!Iterator.} 255 | */ 256 | private _invalidationsOfTypes (types: string[]): IterableIterator { 257 | const invalidations = this._invalidations 258 | if (!types) { 259 | types = Object.keys(invalidations) 260 | } 261 | 262 | // eslint-disable-next-line 263 | function* generator () { 264 | for (let i = 0; i < types.length; ++i) { 265 | // TODO(Christian) fix typings 266 | const invalidationList = invalidations[(types as any)[i]] || [] 267 | for (let j = 0; j < invalidationList.length; ++j) { 268 | yield invalidationList[j] 269 | } 270 | } 271 | } 272 | 273 | return generator() 274 | } 275 | 276 | private _startNewFrameIfNeeded (): void { 277 | if (!this._didPaint) { 278 | return 279 | } 280 | 281 | this._initializePerFrameState() 282 | } 283 | 284 | private _initializePerFrameState (): void { 285 | /** @type {!Object.>} */ 286 | this._invalidations = {} 287 | /** @type {!Object.>} */ 288 | this._invalidationsByNodeId = {} 289 | 290 | this._lastRecalcStyle = null 291 | this._lastPaintWithLayer = null 292 | this._didPaint = false 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /devtools/timelineModel/invalidationTrackingEvent.ts: -------------------------------------------------------------------------------- 1 | import Event from '../tracingModel/event' 2 | import { InvalidationCause, InvalidationMap, EventData, RecordType } from '../types' 3 | 4 | export default class InvalidationTrackingEvent { 5 | public type: string 6 | public startTime: number 7 | public tracingEvent: Event 8 | 9 | /** @type {number} */ 10 | public frame: number & string 11 | /** @type {?number} */ 12 | public nodeId?: number 13 | /** @type {?string} */ 14 | public nodeName?: string 15 | /** @type {?number} */ 16 | public invalidationSet?: number 17 | /** @type {?string} */ 18 | public invalidatedSelectorId?: string 19 | /** @type {?string} */ 20 | public changedId?: string 21 | /** @type {?string} */ 22 | public changedClass?: string 23 | /** @type {?string} */ 24 | public changedAttribute?: string 25 | /** @type {?string} */ 26 | public changedPseudo?: string 27 | /** @type {?string} */ 28 | public selectorPart?: string 29 | /** @type {?string} */ 30 | public extraData?: string 31 | /** @type {?Array.>} */ 32 | public invalidationList?: InvalidationMap[] 33 | /** @type {!TimelineModel.InvalidationCause} */ 34 | public cause: InvalidationCause 35 | public linkedRecalcStyleEvent: boolean 36 | public linkedLayoutEvent: boolean 37 | 38 | public constructor(event: Event) { 39 | this.type = event.name 40 | this.startTime = event.startTime 41 | this.tracingEvent = event 42 | 43 | const eventData: EventData = event.args['data'] 44 | // this.frame = eventData['frame'] 45 | this.nodeId = eventData['nodeId'] 46 | this.nodeName = eventData['nodeName'] 47 | this.invalidationSet = eventData['invalidationSet'] 48 | this.invalidatedSelectorId = eventData['invalidatedSelectorId'] 49 | this.changedId = eventData['changedId'] 50 | this.changedClass = eventData['changedClass'] 51 | this.changedAttribute = eventData['changedAttribute'] 52 | this.changedPseudo = eventData['changedPseudo'] 53 | this.selectorPart = eventData['selectorPart'] 54 | this.extraData = eventData['extraData'] 55 | this.invalidationList = eventData['invalidationList'] 56 | this.cause = { 57 | reason: eventData['reason'], 58 | stackTrace: eventData['stackTrace'], 59 | } 60 | 61 | if ( 62 | !this.cause.reason && 63 | this.cause.stackTrace && 64 | this.type === RecordType.LayoutInvalidationTracking 65 | ) { 66 | this.cause.reason = 'Layout forced' 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /devtools/timelineModel/networkRequest.ts: -------------------------------------------------------------------------------- 1 | import Event from '../tracingModel/event' 2 | import { Timing, ResourcePriority, RecordType } from '../types' 3 | 4 | export default class NetworkRequest { 5 | public startTime: number 6 | public endTime: number 7 | public encodedDataLength: number 8 | public decodedBodyLength: number 9 | public children: Event[] 10 | public timing?: Timing 11 | public mimeType: string 12 | public url: string 13 | public requestMethod: string 14 | public priority: ResourcePriority 15 | public finishTime: number 16 | public responseTime: number 17 | public fromCache: boolean 18 | public fromServiceWorker: boolean 19 | 20 | /** 21 | * @param {!SDK.TracingModel.Event} event 22 | */ 23 | public constructor (event: Event) { 24 | this.startTime = 25 | event.name === RecordType.ResourceSendRequest ? event.startTime : 0 26 | this.endTime = Infinity 27 | this.encodedDataLength = 0 28 | this.decodedBodyLength = 0 29 | /** @type {!Array} */ 30 | this.children = [] 31 | /** @type {?Object} */ 32 | this.timing 33 | /** @type {string} */ 34 | this.mimeType 35 | /** @type {string} */ 36 | this.url 37 | /** @type {string} */ 38 | this.requestMethod 39 | this.addEvent(event) 40 | } 41 | 42 | /** 43 | * @param {!SDK.TracingModel.Event} event 44 | */ 45 | public addEvent(event: Event): void { 46 | this.children.push(event) 47 | const recordType = RecordType 48 | this.startTime = Math.min(this.startTime, event.startTime) 49 | const eventData = event.args['data'] 50 | 51 | if (eventData['mimeType']) { 52 | this.mimeType = eventData.mimeType 53 | } 54 | 55 | if ('priority' in eventData) { 56 | this.priority = eventData.priority 57 | } 58 | 59 | if (event.name === recordType.ResourceFinish) { 60 | this.endTime = event.startTime 61 | } 62 | 63 | if (eventData['finishTime']) { 64 | this.finishTime = eventData['finishTime'] * 1000 65 | } 66 | 67 | if ( 68 | !this.responseTime && 69 | ( 70 | event.name === recordType.ResourceReceiveResponse || 71 | event.name === recordType.ResourceReceivedData 72 | ) 73 | ) { 74 | this.responseTime = event.startTime 75 | } 76 | 77 | const encodedDataLength = eventData['encodedDataLength'] || 0 78 | if (event.name === recordType.ResourceReceiveResponse) { 79 | if (eventData['fromCache']) { 80 | this.fromCache = true 81 | } 82 | if (eventData['fromServiceWorker']) { 83 | this.fromServiceWorker = true 84 | } 85 | this.encodedDataLength = encodedDataLength 86 | } 87 | 88 | if (event.name === recordType.ResourceReceivedData) { 89 | this.encodedDataLength += encodedDataLength 90 | } 91 | 92 | if (event.name === recordType.ResourceFinish && encodedDataLength) { 93 | this.encodedDataLength = encodedDataLength 94 | } 95 | 96 | const decodedBodyLength = eventData['decodedBodyLength'] 97 | if (event.name === recordType.ResourceFinish && decodedBodyLength) { 98 | this.decodedBodyLength = decodedBodyLength 99 | } 100 | 101 | if (!this.url) { 102 | this.url = eventData.url 103 | } 104 | 105 | if (!this.requestMethod) { 106 | this.requestMethod = eventData['requestMethod'] 107 | } 108 | 109 | if (!this.timing) { 110 | this.timing = eventData['timing'] 111 | } 112 | 113 | if (eventData.fromServiceWorker) { 114 | this.fromServiceWorker = true 115 | } 116 | } 117 | 118 | /** 119 | * @return {number} 120 | */ 121 | public beginTime(): number { 122 | return Math.min( 123 | this.startTime, 124 | (this.timing && this.timing.requestTime * 1000) || Infinity, 125 | (this.timing && this.timing.pushStart * 1000) || Infinity 126 | ) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /devtools/timelineModel/pageFrame.ts: -------------------------------------------------------------------------------- 1 | import { EventData } from '../types' 2 | 3 | export interface PageFrameProcess { 4 | time: number; 5 | processId: number; 6 | processPseudoId?: string; 7 | url: string; 8 | } 9 | 10 | export default class PageFrame { 11 | public frameId: string 12 | public url: string 13 | public name: string 14 | public children: PageFrame[] 15 | public parent: PageFrame 16 | public processes: PageFrameProcess[] 17 | public deletedTime: number 18 | // public ownerNode: any 19 | 20 | /** 21 | * @param {!Object} payload 22 | */ 23 | public constructor (payload: EventData) { 24 | this.frameId = payload.frame 25 | this.url = payload.url || '' 26 | this.name = payload.name 27 | this.children = [] 28 | this.parent = null 29 | this.processes = [] 30 | this.deletedTime = null 31 | // TODO(dgozman): figure this out. 32 | // this.ownerNode = target && payload['nodeId'] ? new SDK.DeferredDOMNode(target, payload['nodeId']) : null 33 | // this.ownerNode = null 34 | } 35 | 36 | /** 37 | * @param {number} time 38 | * @param {!Object} payload 39 | */ 40 | public update (time: number, payload: EventData): void { 41 | this.url = payload['url'] || '' 42 | this.name = payload['name'] 43 | this.processes.push({ 44 | time, 45 | processId: payload.processId ? payload.processId : -1, 46 | processPseudoId: payload.processId ? '' : payload.processPseudoId, 47 | url: payload.url || '' 48 | }) 49 | } 50 | 51 | /** 52 | * @param {string} processPseudoId 53 | * @param {number} processId 54 | */ 55 | public processReady (processPseudoId: string, processId: number): void { 56 | for (const process of this.processes) { 57 | if (process.processPseudoId === processPseudoId) { 58 | process.processPseudoId = '' 59 | process.processId = processId 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * @param {!TimelineModel.TimelineModel.PageFrame} child 66 | */ 67 | public addChild (child: PageFrame): void { 68 | this.children.push(child) 69 | child.parent = this 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /devtools/timelineModel/performanceModel.ts: -------------------------------------------------------------------------------- 1 | import TracingModel from '../tracingModel' 2 | import TimelineModel from '.' 3 | import TimelineFrameModel from './timelineFrameModel' 4 | import Track, { TrackType } from './track' 5 | import TimelineFrame from './timelineFrame/timelineFrame' 6 | import { ThreadData, WarningType, StatsObject } from '../types' 7 | import Event from '../tracingModel/event' 8 | import TimelineData from './timelineData' 9 | 10 | interface ExtensionTracingModel { 11 | title: string 12 | model: TracingModel 13 | timeOffset: number 14 | } 15 | 16 | export default class PerformanceModel { 17 | private _mainTarget: any 18 | private _tracingModel: TracingModel 19 | private _timelineModel: TimelineModel 20 | private _frameModel: TimelineFrameModel 21 | private _extensionTracingModels: ExtensionTracingModel[] 22 | private _recordStartTime: number 23 | public startTime: number 24 | public endTime: number 25 | 26 | public constructor () { 27 | /** @type {?SDK.Target} */ 28 | this._mainTarget = null 29 | /** @type {?SDK.TracingModel} */ 30 | this._tracingModel = null 31 | this._timelineModel = new TimelineModel() 32 | /** @type {!Array} */ 33 | this._extensionTracingModels = [] 34 | /** @type {number|undefined} */ 35 | this._recordStartTime = undefined 36 | this._frameModel = new TimelineFrameModel() 37 | } 38 | 39 | /** 40 | * @param {number} time 41 | */ 42 | public setRecordStartTime(time: number): void { 43 | this._recordStartTime = time 44 | } 45 | 46 | /** 47 | * @return {number|undefined} 48 | */ 49 | public recordStartTime(): number { 50 | return this._recordStartTime 51 | } 52 | 53 | /** 54 | * @param {!SDK.TracingModel} model 55 | */ 56 | public setTracingModel(model: TracingModel): void { 57 | this._tracingModel = model 58 | this._timelineModel.setEvents(model) 59 | 60 | let inputEvents = null 61 | let animationEvents = null 62 | for (const track of this._timelineModel.tracks()) { 63 | if (track.type === TrackType.Input) { 64 | inputEvents = track.asyncEvents 65 | } 66 | if (track.type === TrackType.Animation) { 67 | animationEvents = track.asyncEvents 68 | } 69 | } 70 | 71 | const mainTracks = this._timelineModel 72 | .tracks() 73 | .filter((track): any => track.type === TrackType.MainThread && track.forMainFrame && track.events.length) 74 | const threadData = mainTracks.map((track): ThreadData => { 75 | const event = track.events[0] 76 | return { thread: event.thread, time: event.startTime } 77 | }) 78 | this._frameModel.addTraceEvents(this._mainTarget, this._timelineModel.inspectedTargetEvents(), threadData) 79 | 80 | for (const entry of this._extensionTracingModels) { 81 | entry.model.adjustTime( 82 | this._tracingModel.minimumRecordTime() + entry.timeOffset / 1000 - this._recordStartTime 83 | ) 84 | } 85 | this._autoWindowTimes() 86 | } 87 | 88 | /** 89 | * @param {string} title 90 | * @param {!SDK.TracingModel} model 91 | * @param {number} timeOffset 92 | */ 93 | public addExtensionEvents(title: string, model: TracingModel, timeOffset: number): void { 94 | this._extensionTracingModels.push({ model: model, title: title, timeOffset: timeOffset }) 95 | if (!this._tracingModel) { 96 | return 97 | } 98 | model.adjustTime(this._tracingModel.minimumRecordTime() + timeOffset / 1000 - this._recordStartTime) 99 | } 100 | 101 | /** 102 | * @return {!SDK.TracingModel} 103 | */ 104 | public tracingModel(): TracingModel { 105 | if (!this._tracingModel) { 106 | throw new Error('call setTracingModel before accessing PerformanceModel') 107 | } 108 | return this._tracingModel 109 | } 110 | 111 | /** 112 | * @return {!TimelineModel.TimelineModel} 113 | */ 114 | public timelineModel(): TimelineModel { 115 | return this._timelineModel 116 | } 117 | 118 | /** 119 | * @return {!Array} frames 120 | */ 121 | public frames(): TimelineFrame[] { 122 | return this._frameModel.frames() 123 | } 124 | 125 | /** 126 | * @return {!TimelineModel.TimelineFrameModel} frames 127 | */ 128 | public frameModel(): TimelineFrameModel { 129 | return this._frameModel 130 | } 131 | 132 | /** 133 | * @param {!Timeline.PerformanceModel.Window} window 134 | * @param {boolean=} animate 135 | */ 136 | public setWindow(option: {left: number, right: number}) : void { 137 | this.startTime = option.left 138 | this.endTime = option.right 139 | } 140 | 141 | private _autoWindowTimes(): void { 142 | const timelineModel = this._timelineModel 143 | let tasks: Event[] = [] 144 | for (const track of timelineModel.tracks()) { 145 | // Deliberately pick up last main frame's track. 146 | if (track.type === TrackType.MainThread && track.forMainFrame) { 147 | tasks = track.tasks 148 | } 149 | } 150 | if (!tasks.length) { 151 | this.setWindow({ left: timelineModel.minimumRecordTime(), right: timelineModel.maximumRecordTime() }) 152 | return 153 | } 154 | 155 | /** 156 | * @param {number} startIndex 157 | * @param {number} stopIndex 158 | * @return {number} 159 | */ 160 | function findLowUtilizationRegion(startIndex: number, stopIndex: number): number { 161 | const /** @const */ threshold = 0.1 162 | let cutIndex = startIndex 163 | let cutTime = (tasks[cutIndex].startTime + tasks[cutIndex].endTime) / 2 164 | let usedTime = 0 165 | const step = Math.sign(stopIndex - startIndex) 166 | for (let i = startIndex; i !== stopIndex; i += step) { 167 | const task = tasks[i] 168 | const taskTime = (task.startTime + task.endTime) / 2 169 | const interval = Math.abs(cutTime - taskTime) 170 | if (usedTime < threshold * interval) { 171 | cutIndex = i 172 | cutTime = taskTime 173 | usedTime = 0 174 | } 175 | usedTime += task.duration 176 | } 177 | return cutIndex 178 | } 179 | 180 | const rightIndex = findLowUtilizationRegion(tasks.length - 1, 0) 181 | const leftIndex = findLowUtilizationRegion(0, rightIndex) 182 | let leftTime = tasks[leftIndex].startTime 183 | let rightTime = tasks[rightIndex].endTime 184 | const span = rightTime - leftTime 185 | const totalSpan = timelineModel.maximumRecordTime() - timelineModel.minimumRecordTime() 186 | 187 | if (span < totalSpan * 0.1) { 188 | leftTime = timelineModel.minimumRecordTime() 189 | rightTime = timelineModel.maximumRecordTime() 190 | } else { 191 | leftTime = Math.max(leftTime - 0.05 * span, timelineModel.minimumRecordTime()) 192 | rightTime = Math.min(rightTime + 0.05 * span, timelineModel.maximumRecordTime()) 193 | } 194 | 195 | this.setWindow({ left: leftTime, right: rightTime }) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /devtools/timelineModel/timelineAsyncEventTracker.ts: -------------------------------------------------------------------------------- 1 | import TimelineModel from './index' 2 | import TimelineData from './timelineData' 3 | import Event from '../tracingModel/event' 4 | import { RecordType } from '../types' 5 | 6 | type Initiatior = Map 10 | 11 | export default class TimelineAsyncEventTracker { 12 | private static _asyncEvents: Initiatior 13 | private _initiatorByType: Map> // todo 14 | private static _typeToInitiator: Map 15 | 16 | public constructor () { 17 | TimelineAsyncEventTracker._initialize() 18 | /** @type {!Map>} */ 19 | this._initiatorByType = new Map() 20 | for (const initiator of TimelineAsyncEventTracker._asyncEvents.keys()) { 21 | this._initiatorByType.set(initiator, new Map()) 22 | } 23 | } 24 | 25 | private static _initialize(): void { 26 | if (TimelineAsyncEventTracker._asyncEvents) { 27 | return 28 | } 29 | 30 | /** 31 | * ToDo: type events 32 | */ 33 | const events:Initiatior = new Map() 34 | events.set(RecordType.TimerInstall, { 35 | causes: [RecordType.TimerFire], 36 | joinBy: 'timerId', 37 | }) 38 | events.set(RecordType.ResourceSendRequest, { 39 | causes: [ 40 | RecordType.ResourceReceiveResponse, 41 | RecordType.ResourceReceivedData, 42 | RecordType.ResourceFinish, 43 | ], 44 | joinBy: 'requestId', 45 | }) 46 | events.set(RecordType.RequestAnimationFrame, { 47 | causes: [RecordType.FireAnimationFrame], 48 | joinBy: 'id', 49 | }) 50 | events.set(RecordType.RequestIdleCallback, { 51 | causes: [RecordType.FireIdleCallback], 52 | joinBy: 'id', 53 | }) 54 | events.set(RecordType.WebSocketCreate, { 55 | causes: [ 56 | RecordType.WebSocketSendHandshakeRequest, 57 | RecordType.WebSocketReceiveHandshakeResponse, 58 | RecordType.WebSocketDestroy, 59 | ], 60 | joinBy: 'identifier', 61 | }) 62 | 63 | TimelineAsyncEventTracker._asyncEvents = events 64 | /** @type {!Map} */ 65 | this._typeToInitiator = new Map() 66 | for (const entry of events) { 67 | const types = entry[1].causes 68 | for (let causeType of types) { 69 | // TODO(Christian) fix typings 70 | this._typeToInitiator.set(causeType, entry[0] as any) 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * @param {!SDK.TracingModel.Event} event 77 | */ 78 | public processEvent(event: Event): void { 79 | /** @type {!TimelineModel.TimelineModel.RecordType} */ 80 | let initiatorType: RecordType | string = TimelineAsyncEventTracker._typeToInitiator.get(event.name) 81 | const isInitiator = !initiatorType 82 | 83 | if (!initiatorType) { 84 | /** @type {!TimelineModel.TimelineModel.RecordType} */ 85 | initiatorType = event.name 86 | } 87 | 88 | const initiatorInfo = TimelineAsyncEventTracker._asyncEvents.get(initiatorType) 89 | if (!initiatorInfo) { 90 | return 91 | } 92 | 93 | const id = TimelineModel.globalEventId(event, initiatorInfo.joinBy) 94 | if (!id) { 95 | return 96 | } 97 | 98 | /** @type {!Map|undefined} */ 99 | const initiatorMap: Map = this._initiatorByType.get(initiatorType) 100 | if (isInitiator) { 101 | initiatorMap.set(id, event) 102 | return 103 | } 104 | 105 | const initiator: Event | null = initiatorMap.get(id) || null 106 | const timelineData = TimelineData.forEvent(event) 107 | timelineData.setInitiator(initiator) 108 | if (!timelineData.frameId && initiator) { 109 | timelineData.frameId = TimelineModel.eventFrameId(initiator) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /devtools/timelineModel/timelineData.ts: -------------------------------------------------------------------------------- 1 | import ObjectSnapshot from '../tracingModel/objectSnapshot' 2 | import Event from '../tracingModel/event' 3 | import { CallFrame, WarningType } from '../types' 4 | 5 | export default class TimelineData { 6 | public static readonly timelineDataSymbol: unique symbol = Symbol('timelineData') 7 | public warning: WarningType | null 8 | public previewElement: Element 9 | public url: string 10 | public backendNodeId: number 11 | public stackTrace: any // todo 12 | public picture: ObjectSnapshot 13 | public frameId: string 14 | public timeWaitingForMainThread: number | undefined 15 | private _initiator: Event 16 | 17 | public constructor () { 18 | this.warning = null 19 | this.previewElement = null 20 | this.url = null 21 | this.backendNodeId = 0 22 | this.stackTrace = null 23 | this.picture = null 24 | this._initiator = null 25 | this.frameId = '' 26 | this.timeWaitingForMainThread 27 | } 28 | 29 | /** 30 | * @param {!SDK.TracingModel.Event} initiator 31 | */ 32 | public setInitiator(initiator: Event): void { 33 | this._initiator = initiator 34 | if (!initiator || this.url) { 35 | return 36 | } 37 | const initiatorURL = TimelineData.forEvent(initiator).url 38 | if (initiatorURL) { 39 | this.url = initiatorURL 40 | } 41 | } 42 | 43 | /** 44 | * @return {?SDK.TracingModel.Event} 45 | */ 46 | public initiator(): Event { 47 | return this._initiator 48 | } 49 | 50 | /** 51 | * @return {?Protocol.Runtime.CallFrame} 52 | */ 53 | public topFrame(): CallFrame | null { 54 | const stackTrace = this.stackTraceForSelfOrInitiator() 55 | return (stackTrace && stackTrace[0]) || null 56 | } 57 | 58 | /** 59 | * @return {?Array} 60 | */ 61 | public stackTraceForSelfOrInitiator(): CallFrame[] | null { 62 | return ( 63 | this.stackTrace || 64 | ( 65 | this._initiator && 66 | TimelineData.forEvent(this._initiator).stackTrace 67 | ) 68 | ) 69 | } 70 | 71 | /** 72 | * @param {!SDK.TracingModel.Event} event 73 | * @return {!TimelineModel.TimelineData} 74 | */ 75 | public static forEvent(event: any): TimelineData { 76 | let data = event[TimelineData.timelineDataSymbol] 77 | 78 | if (!data) { 79 | data = new TimelineData() 80 | event[TimelineData.timelineDataSymbol] = data 81 | } 82 | 83 | return data 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /devtools/timelineModel/timelineFrame/layerPaintEvent.ts: -------------------------------------------------------------------------------- 1 | import Event from '../../tracingModel/event' 2 | import TimelineData from '../timelineData' 3 | import { PicturePromise } from '../../types' 4 | 5 | export default class LayerPaintEvent { 6 | private _event: Event 7 | 8 | /** 9 | * @param {!SDK.TracingModel.Event} event 10 | */ 11 | public constructor(event: Event) { 12 | this._event = event 13 | } 14 | 15 | /** 16 | * @return {string} 17 | */ 18 | public layerId(): string { 19 | return this._event.args['data']['layerId'] 20 | } 21 | 22 | /** 23 | * @return {!SDK.TracingModel.Event} 24 | */ 25 | public event(): Event { 26 | return this._event 27 | } 28 | 29 | /** 30 | * @return {!Promise, serializedPicture: string}>} 31 | */ 32 | public picturePromise(): Promise { 33 | const picture = TimelineData.forEvent(this._event).picture 34 | return picture.objectPromise().then((result): PicturePromise | null => { 35 | if (!result) { 36 | return null 37 | } 38 | 39 | const rect = result['params'] && result['params']['layer_rect'] 40 | const picture = result['skp64'] 41 | return rect && picture ? { rect: rect, serializedPicture: picture } : null 42 | }) 43 | } 44 | 45 | /** 46 | * @return !Promise, snapshot: !SDK.PaintProfilerSnapshot}>} 47 | */ 48 | public snapshotPromise(): Promise { 49 | return this.picturePromise().then((picture): null | void => { 50 | if (!picture) { 51 | return null 52 | } 53 | }) 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /devtools/timelineModel/timelineFrame/pendingFrame.ts: -------------------------------------------------------------------------------- 1 | import { TimeByCategory } from '../../types' 2 | import LayerPaintEvent from './layerPaintEvent' 3 | 4 | export default class PendingFrame { 5 | public timeByCategory: TimeByCategory; 6 | public paints: LayerPaintEvent [] 7 | public mainFrameId: number | undefined 8 | public triggerTime: number 9 | 10 | /** 11 | * @param {number} triggerTime 12 | * @param {!Object.} timeByCategory 13 | */ 14 | public constructor(triggerTime: number, timeByCategory: TimeByCategory) { 15 | /** @type {!Object.} */ 16 | this.timeByCategory = timeByCategory 17 | /** @type {!Array.} */ 18 | this.paints = [] 19 | /** @type {number|undefined} */ 20 | this.mainFrameId = undefined 21 | this.triggerTime = triggerTime 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /devtools/timelineModel/timelineFrame/timelineFrame.ts: -------------------------------------------------------------------------------- 1 | import TracingFrameLayerTree from './tracingFrameLayerTree' 2 | import LayerPaintEvent from './layerPaintEvent' 3 | 4 | export interface TimeByCategory { 5 | [key: string]: any 6 | } 7 | 8 | export default class TimelineFrame { 9 | public startTime: number 10 | public startTimeOffset: number 11 | public endTime: number 12 | public duration: number 13 | public timeByCategory: TimeByCategory 14 | public cpuTime: number 15 | public idle: boolean 16 | public layerTree: TracingFrameLayerTree 17 | public paints: LayerPaintEvent[] 18 | public mainFrameId: number | undefined 19 | 20 | /** 21 | * @param {number} startTime 22 | * @param {number} startTimeOffset 23 | */ 24 | public constructor(startTime: number, startTimeOffset: number) { 25 | this.startTime = startTime 26 | this.startTimeOffset = startTimeOffset 27 | this.endTime = this.startTime 28 | this.duration = 0 29 | this.timeByCategory = {} 30 | this.cpuTime = 0 31 | this.idle = false 32 | /** @type {?TimelineModel.TracingFrameLayerTree} */ 33 | this.layerTree = null 34 | /** @type {!Array.} */ 35 | this.paints = [] 36 | /** @type {number|undefined} */ 37 | this.mainFrameId = undefined 38 | } 39 | 40 | /** 41 | * @return {boolean} 42 | */ 43 | public hasWarnings(): boolean { 44 | return false 45 | } 46 | 47 | /** 48 | * @param {number} endTime 49 | */ 50 | public setEndTime(endTime: number): void { 51 | this.endTime = endTime 52 | this.duration = this.endTime - this.startTime 53 | } 54 | 55 | /** 56 | * @param {?TimelineModel.TracingFrameLayerTree} layerTree 57 | */ 58 | public setLayerTree(layerTree: TracingFrameLayerTree): void { 59 | this.layerTree = layerTree 60 | } 61 | 62 | /** 63 | * @param {!Object} timeByCategory 64 | */ 65 | public addTimeForCategories(timeByCategory: TimeByCategory): void { 66 | for (const category in timeByCategory) { 67 | this.addTimeForCategory(category, timeByCategory[category]) 68 | } 69 | } 70 | 71 | /** 72 | * @param {string} category 73 | * @param {number} time 74 | */ 75 | public addTimeForCategory(category: string, time: number): void { 76 | this.timeByCategory[category] = (this.timeByCategory[category] || 0) + time 77 | this.cpuTime += time 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /devtools/timelineModel/timelineFrame/tracingFrameLayerTree.ts: -------------------------------------------------------------------------------- 1 | import ObjectSnapshot from '../../tracingModel/objectSnapshot' 2 | import LayerPaintEvent from './layerPaintEvent' 3 | 4 | export default class TracingFrameLayerTree { 5 | private _snapshot: ObjectSnapshot 6 | private _paints: LayerPaintEvent[] 7 | 8 | /** 9 | * @param {!SDK.Target} target 10 | * @param {!SDK.TracingModel.ObjectSnapshot} snapshot 11 | */ 12 | public constructor(target: any, snapshot: ObjectSnapshot) { 13 | this._snapshot = snapshot 14 | /** @type {!Array|undefined} */ 15 | this._paints 16 | } 17 | 18 | /** 19 | * @return {!Array} 20 | */ 21 | public paints(): LayerPaintEvent[] { 22 | return this._paints || [] 23 | } 24 | 25 | /** 26 | * @param {!Array} paints 27 | */ 28 | public setPaints(paints: LayerPaintEvent[]): void { 29 | this._paints = paints 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /devtools/timelineModel/timelineFrameModel.ts: -------------------------------------------------------------------------------- 1 | import TimelineFrame from './timelineFrame/timelineFrame' 2 | import { lowerBound } from '../utils' 3 | import Event from '../tracingModel/event' 4 | import { FrameById, TimeByCategory, RecordType, EventData, ThreadData } from '../types' 5 | import TracingFrameLayerTree from './timelineFrame/tracingFrameLayerTree' 6 | import PendingFrame from './timelineFrame/pendingFrame' 7 | import Thread from '../tracingModel/thread' 8 | import TracingModel, { Phase } from '../tracingModel' 9 | import LayerPaintEvent from './timelineFrame/layerPaintEvent' 10 | import TimelineData from './timelineData' 11 | 12 | type categoryMapperFunc = (any: Event) => string 13 | 14 | export default class TimelineFrameModel { 15 | private _categoryMapper: any 16 | private _frames: TimelineFrame[] 17 | private _frameById: FrameById 18 | private _minimumRecordTime: number 19 | private _lastFrame: TimelineFrame 20 | private _lastLayerTree: TracingFrameLayerTree 21 | private _mainFrameCommitted: boolean 22 | private _mainFrameRequested: boolean 23 | private _framePendingCommit: PendingFrame 24 | private _lastBeginFrame: number 25 | private _lastNeedsBeginFrame: number 26 | private _framePendingActivation: PendingFrame 27 | private _lastTaskBeginTime: number 28 | private _target: any 29 | private _layerTreeId: any // todo fix type 30 | private _currentTaskTimeByCategory: TimeByCategory 31 | private _currentProcessMainThread: Thread 32 | private _mainFrameMarkers: string[] 33 | 34 | /** 35 | * @param {function(!SDK.TracingModel.Event):string} categoryMapper 36 | */ 37 | public constructor(categoryMapper?: categoryMapperFunc) { 38 | this._categoryMapper = categoryMapper 39 | this._mainFrameMarkers = [ 40 | RecordType.ScheduleStyleRecalculation, 41 | RecordType.InvalidateLayout, 42 | RecordType.BeginMainThreadFrame, 43 | RecordType.ScrollLayer, 44 | ] 45 | this.reset() 46 | } 47 | 48 | /** 49 | * @param {number=} startTime 50 | * @param {number=} endTime 51 | * @return {!Array} 52 | */ 53 | public frames(startTime?: number, endTime?: number): TimelineFrame[] { 54 | if (!startTime && !endTime) { 55 | return this._frames 56 | } 57 | 58 | const firstFrame = lowerBound( 59 | this._frames, 60 | startTime || 0, 61 | (time, frame): number => time - frame.endTime 62 | ) 63 | const lastFrame = lowerBound( 64 | this._frames, 65 | endTime || Infinity, 66 | (time, frame): number => time - frame.startTime 67 | ) 68 | return this._frames.slice(firstFrame, lastFrame) 69 | } 70 | 71 | /** 72 | * @param {!SDK.TracingModel.Event} rasterTask 73 | * @return {boolean} 74 | */ 75 | public hasRasterTile(rasterTask: Event): boolean { 76 | const data = rasterTask.args['tileData'] 77 | if (!data) { 78 | return false 79 | } 80 | const frameId = data['sourceFrameNumber'] 81 | const frame = frameId && this._frameById[frameId] 82 | if (!frame || !frame.layerTree) { 83 | return false 84 | } 85 | return true 86 | } 87 | 88 | public reset(): void { 89 | this._minimumRecordTime = Infinity 90 | this._frames = [] 91 | this._frameById = {} 92 | this._lastFrame = null 93 | this._lastLayerTree = null 94 | this._mainFrameCommitted = false 95 | this._mainFrameRequested = false 96 | this._framePendingCommit = null 97 | this._lastBeginFrame = null 98 | this._lastNeedsBeginFrame = null 99 | this._framePendingActivation = null 100 | this._lastTaskBeginTime = null 101 | this._target = null 102 | this._layerTreeId = null 103 | this._currentTaskTimeByCategory = {} 104 | } 105 | 106 | /** 107 | * @param {number} startTime 108 | */ 109 | public handleBeginFrame(startTime: number): void { 110 | if (!this._lastFrame) { 111 | this._startFrame(startTime) 112 | } 113 | this._lastBeginFrame = startTime 114 | } 115 | 116 | /** 117 | * @param {number} startTime 118 | */ 119 | public handleDrawFrame(startTime: number): void { 120 | if (!this._lastFrame) { 121 | this._startFrame(startTime) 122 | return 123 | } 124 | 125 | // - if it wasn't drawn, it didn't happen! 126 | // - only show frames that either did not wait for the main thread frame or had one committed. 127 | if (this._mainFrameCommitted || !this._mainFrameRequested) { 128 | if (this._lastNeedsBeginFrame) { 129 | const idleTimeEnd = this._framePendingActivation 130 | ? this._framePendingActivation.triggerTime 131 | : this._lastBeginFrame || this._lastNeedsBeginFrame 132 | if (idleTimeEnd > this._lastFrame.startTime) { 133 | this._lastFrame.idle = true 134 | this._startFrame(idleTimeEnd) 135 | if (this._framePendingActivation) { 136 | this._commitPendingFrame() 137 | } 138 | this._lastBeginFrame = null 139 | } 140 | this._lastNeedsBeginFrame = null 141 | } 142 | this._startFrame(startTime) 143 | } 144 | this._mainFrameCommitted = false 145 | } 146 | 147 | public handleActivateLayerTree(): void { 148 | if (!this._lastFrame) { 149 | return 150 | } 151 | if (this._framePendingActivation && !this._lastNeedsBeginFrame) { 152 | this._commitPendingFrame() 153 | } 154 | } 155 | 156 | public handleRequestMainThreadFrame(): void { 157 | if (!this._lastFrame) { 158 | return 159 | } 160 | this._mainFrameRequested = true 161 | } 162 | 163 | public handleCompositeLayers(): void { 164 | if (!this._framePendingCommit) { 165 | return 166 | } 167 | this._framePendingActivation = this._framePendingCommit 168 | this._framePendingCommit = null 169 | this._mainFrameRequested = false 170 | this._mainFrameCommitted = true 171 | } 172 | 173 | /** 174 | * @param {!TimelineModel.TracingFrameLayerTree} layerTree 175 | */ 176 | public handleLayerTreeSnapshot(layerTree: TracingFrameLayerTree): void { 177 | this._lastLayerTree = layerTree 178 | } 179 | 180 | /** 181 | * @param {number} startTime 182 | * @param {boolean} needsBeginFrame 183 | */ 184 | public handleNeedFrameChanged(startTime: number, needsBeginFrame: boolean): void { 185 | if (needsBeginFrame) { 186 | this._lastNeedsBeginFrame = startTime 187 | } 188 | } 189 | 190 | /** 191 | * @param {number} startTime 192 | */ 193 | public _startFrame(startTime: number): void { 194 | if (this._lastFrame) { 195 | this._flushFrame(this._lastFrame, startTime) 196 | } 197 | this._lastFrame = new TimelineFrame(startTime, startTime - this._minimumRecordTime) 198 | } 199 | 200 | /** 201 | * @param {!TimelineModel.TimelineFrame} frame 202 | * @param {number} endTime 203 | */ 204 | private _flushFrame(frame: TimelineFrame, endTime: number): void { 205 | frame.setLayerTree(this._lastLayerTree) 206 | frame.setEndTime(endTime) 207 | if (this._lastLayerTree) { 208 | this._lastLayerTree.setPaints(frame.paints) 209 | } 210 | if ( 211 | this._frames.length && 212 | (frame.startTime !== this._frames[this._frames.length - 1].endTime || 213 | frame.startTime > frame.endTime) 214 | ) { 215 | console.assert( 216 | false, 217 | `Inconsistent frame time for frame ${this._frames.length} (${frame.startTime} - ${frame.endTime})` 218 | ) 219 | } 220 | this._frames.push(frame) 221 | if (typeof frame.mainFrameId === 'number') { 222 | this._frameById[frame.mainFrameId] = frame 223 | } 224 | } 225 | 226 | private _commitPendingFrame(): void { 227 | this._lastFrame.addTimeForCategories(this._framePendingActivation.timeByCategory) 228 | this._lastFrame.paints = this._framePendingActivation.paints 229 | this._lastFrame.mainFrameId = this._framePendingActivation.mainFrameId 230 | this._framePendingActivation = null 231 | } 232 | 233 | /** 234 | * @param {?SDK.Target} target 235 | * @param {!Array.} events 236 | * @param {!Array} threadData 237 | */ 238 | public addTraceEvents(target: any, events: Event[], threadData: ThreadData[]): void { 239 | this._target = target 240 | let j = 0 241 | this._currentProcessMainThread = (threadData.length && threadData[0].thread) || null 242 | for (let i = 0; i < events.length; ++i) { 243 | while (j + 1 < threadData.length && threadData[j + 1].time <= events[i].startTime) { 244 | this._currentProcessMainThread = threadData[++j].thread 245 | } 246 | this._addTraceEvent(events[i]) 247 | } 248 | this._currentProcessMainThread = null 249 | } 250 | 251 | /** 252 | * @param {!SDK.TracingModel.Event} event 253 | */ 254 | private _addTraceEvent(event: Event): void { 255 | const eventNames = RecordType 256 | if (event.startTime && event.startTime < this._minimumRecordTime) { 257 | this._minimumRecordTime = event.startTime 258 | } 259 | 260 | if (event.name === eventNames.SetLayerTreeId) { 261 | this._layerTreeId = event.args['layerTreeId'] || event.args['data']['layerTreeId'] 262 | } else if ( 263 | event.phase === Phase.SnapshotObject && 264 | event.name === eventNames.LayerTreeHostImplSnapshot && 265 | parseInt(event.id, 0) === this._layerTreeId 266 | ) { 267 | // todo fix type here 268 | const snapshot: any = /** @type {!SDK.TracingModel.ObjectSnapshot} */ event 269 | this.handleLayerTreeSnapshot(new TracingFrameLayerTree(this._target, snapshot)) 270 | } else { 271 | this._processCompositorEvents(event) 272 | if (event.thread === this._currentProcessMainThread) { 273 | this._addMainThreadTraceEvent(event) 274 | } 275 | 276 | // else if (this._lastFrame && event.selfTime && !TracingModel.isTopLevelEvent(event)) 277 | // this._lastFrame.addTimeForCategory(this._categoryMapper(event), event.selfTime); 278 | } 279 | } 280 | 281 | /** 282 | * @param {!SDK.TracingModel.Event} event 283 | */ 284 | private _processCompositorEvents(event: Event): void { 285 | const eventNames = RecordType 286 | 287 | if (event.args['layerTreeId'] !== this._layerTreeId) { 288 | return 289 | } 290 | 291 | const timestamp = event.startTime 292 | if (event.name === eventNames.BeginFrame) { 293 | this.handleBeginFrame(timestamp) 294 | } else if (event.name === eventNames.DrawFrame) { 295 | this.handleDrawFrame(timestamp) 296 | } else if (event.name === eventNames.ActivateLayerTree) { 297 | this.handleActivateLayerTree() 298 | } else if (event.name === eventNames.RequestMainThreadFrame) { 299 | this.handleRequestMainThreadFrame() 300 | } else if (event.name === eventNames.NeedsBeginFrameChanged) { 301 | this.handleNeedFrameChanged( 302 | timestamp, 303 | event.args['data'] && event.args['data']['needsBeginFrame'] 304 | ) 305 | } 306 | } 307 | 308 | /** 309 | * @param {!SDK.TracingModel.Event} event 310 | */ 311 | private _addMainThreadTraceEvent(event: Event): void { 312 | const eventNames = RecordType 313 | 314 | if (TracingModel.isTopLevelEvent(event)) { 315 | this._currentTaskTimeByCategory = {} 316 | this._lastTaskBeginTime = event.startTime 317 | } 318 | if (!this._framePendingCommit && this._mainFrameMarkers.indexOf(event.name) >= 0) { 319 | this._framePendingCommit = new PendingFrame( 320 | this._lastTaskBeginTime || event.startTime, 321 | this._currentTaskTimeByCategory 322 | ) 323 | } 324 | if (!this._framePendingCommit) { 325 | this._addTimeForCategory(this._currentTaskTimeByCategory, event) 326 | return 327 | } 328 | this._addTimeForCategory(this._framePendingCommit.timeByCategory, event) 329 | 330 | if ( 331 | event.name === eventNames.BeginMainThreadFrame && 332 | event.args['data'] && 333 | event.args['data']['frameId'] 334 | ) { 335 | this._framePendingCommit.mainFrameId = event.args['data']['frameId'] 336 | } 337 | 338 | if ( 339 | event.name === eventNames.Paint && 340 | event.args['data']['layerId'] && 341 | TimelineData.forEvent(event).picture && 342 | this._target 343 | ) { 344 | this._framePendingCommit.paints.push(new LayerPaintEvent(event)) 345 | } 346 | 347 | if ( 348 | event.name === eventNames.CompositeLayers && 349 | event.args['layerTreeId'] === this._layerTreeId 350 | ) { 351 | this.handleCompositeLayers() 352 | } 353 | } 354 | 355 | /** 356 | * @param {!Object.} timeByCategory 357 | * @param {!SDK.TracingModel.Event} event 358 | */ 359 | private _addTimeForCategory(timeByCategory: TimeByCategory, event: Event): void { 360 | if (!event.selfTime) { 361 | return 362 | } 363 | // const categoryName = this._categoryMapper(event); 364 | // timeByCategory[categoryName] = (timeByCategory[categoryName] || 0) + event.selfTime; 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /devtools/timelineModel/timelineJSProfileProcessor.ts: -------------------------------------------------------------------------------- 1 | import CPUProfileDataModel from '../cpuProfileDataModel/index' 2 | import Thread from '../tracingModel/thread' 3 | import TimelineModel from './index' 4 | import Event from '../tracingModel/event' 5 | import TracingModel, { DevToolsTimelineEventCategory, Phase, MetadataEvent } from '../tracingModel/index' 6 | import Runtime from '../runtime' 7 | import { TraceEvent, RecordType, EventData } from '../types' 8 | import Common from '../common/index' 9 | 10 | export enum NativeGroups { 11 | 'Compile' = 'Compile', 12 | 'Parse' = 'Parse', 13 | } 14 | 15 | export default class TimelineJSProfileProcessor { 16 | /** 17 | * @param {!SDK.CPUProfileDataModel} jsProfileModel 18 | * @param {!SDK.TracingModel.Thread} thread 19 | * @return {!Array} 20 | */ 21 | 22 | public static generateTracingEventsFromCpuProfile(jsProfileModel: CPUProfileDataModel, thread: Thread): Event[] { 23 | const idleNode = jsProfileModel.idleNode 24 | const programNode = jsProfileModel.programNode 25 | const gcNode = jsProfileModel.gcNode 26 | const samples = jsProfileModel.samples 27 | const timestamps = jsProfileModel.timestamps 28 | const jsEvents = [] 29 | /** @type {!Map>} */ 30 | const nodeToStackMap = new Map() 31 | nodeToStackMap.set(programNode, []) 32 | for (let i = 0; i < samples.length; ++i) { 33 | let node = jsProfileModel.nodeByIndex(i) 34 | if (!node) { 35 | console.error(`Node with unknown id ${samples[i]} at index ${i}`) 36 | continue 37 | } 38 | if (node === gcNode || node === idleNode) { 39 | continue 40 | } 41 | 42 | let callFrames = nodeToStackMap.get(node) 43 | if (!callFrames) { 44 | callFrames = /** @type {!Array} */ (new Array(node.depth + 1)) 45 | nodeToStackMap.set(node, callFrames) 46 | for (let j = 0; node.parent; node = node.parent) { 47 | callFrames[j++] = /** @type {!Protocol.Runtime.CallFrame} */ (node) 48 | } 49 | 50 | } 51 | const jsSampleEvent = new Event( 52 | DevToolsTimelineEventCategory, 53 | RecordType.JSSample, 54 | Phase.Instant, 55 | timestamps[i], 56 | thread 57 | ) 58 | jsSampleEvent.args.data = { stackTrace: callFrames } 59 | jsEvents.push(jsSampleEvent) 60 | } 61 | return jsEvents 62 | } 63 | 64 | /** 65 | * @param {!Array} events 66 | * @return {!Array} 67 | */ 68 | public static generateJSFrameEvents(events: Event[]): Event[] { 69 | const jsFrameEvents: Event[] = [] 70 | const jsFramesStack: any[] = [] 71 | const lockedJsStackDepth: any[] = [] 72 | let ordinal = 0 73 | const showAllEvents = Runtime.experiments.isEnabled('timelineShowAllEvents') 74 | const showRuntimeCallStats = Runtime.experiments.isEnabled('timelineV8RuntimeCallStats') 75 | const showNativeFunctions = Common.moduleSetting('showNativeFunctionsInJSProfile').get() 76 | 77 | /** 78 | * @param {!Protocol.Runtime.CallFrame} frame1 79 | * @param {!Protocol.Runtime.CallFrame} frame2 80 | * @return {boolean} 81 | */ 82 | function equalFrames(frame1: any, frame2: any): boolean { 83 | return ( 84 | frame1.scriptId === frame2.scriptId && 85 | frame1.functionName === frame2.functionName && 86 | frame1.lineNumber === frame2.lineNumber 87 | ) 88 | } 89 | 90 | /** 91 | * @param {number} depth 92 | * @param {number} time 93 | */ 94 | function truncateJSStack(depth: number, time: number): void { 95 | if (lockedJsStackDepth.length) { 96 | const lockedDepth = lockedJsStackDepth[lockedJsStackDepth.length - 1] 97 | if (depth < lockedDepth) { 98 | console.error( 99 | `Child stack is shallower (${depth}) than the parent stack (${lockedDepth}) at ${time}` 100 | ) 101 | depth = lockedDepth 102 | } 103 | } 104 | if (jsFramesStack.length < depth) { 105 | console.error(`Trying to truncate higher than the current stack size at ${time}`) 106 | depth = jsFramesStack.length 107 | } 108 | for (let k = 0; k < jsFramesStack.length; ++k) { 109 | jsFramesStack[k].setEndTime(time) 110 | } 111 | 112 | jsFramesStack.length = depth 113 | } 114 | 115 | /** 116 | * @param {string} name 117 | * @return {boolean} 118 | */ 119 | function showNativeName(name: string): boolean { 120 | return showRuntimeCallStats && !!TimelineJSProfileProcessor.nativeGroup(name) 121 | } 122 | 123 | /** 124 | * @param {!Array} stack 125 | */ 126 | function filterStackFrames(stack: any[]): void { 127 | if (showAllEvents) { 128 | return 129 | } 130 | 131 | let previousNativeFrameName = null 132 | let j = 0 133 | for (let i = 0; i < stack.length; ++i) { 134 | const frame = stack[i] 135 | const url = frame.url 136 | const isNativeFrame = url && url.startsWith('native ') 137 | if (!showNativeFunctions && isNativeFrame) { 138 | continue 139 | } 140 | 141 | const isNativeRuntimeFrame = TimelineJSProfileProcessor.isNativeRuntimeFrame(frame) 142 | if (isNativeRuntimeFrame && !showNativeName(frame.functionName)) { 143 | continue 144 | } 145 | 146 | const nativeFrameName = isNativeRuntimeFrame 147 | ? TimelineJSProfileProcessor.nativeGroup(frame.functionName) 148 | : null 149 | if (previousNativeFrameName && previousNativeFrameName === nativeFrameName) { 150 | continue 151 | } 152 | 153 | previousNativeFrameName = nativeFrameName 154 | stack[j++] = frame 155 | } 156 | stack.length = j 157 | } 158 | 159 | /** 160 | * @param {!SDK.TracingModel.Event} e 161 | */ 162 | function extractStackTrace(e: Event): void { 163 | const recordTypes = RecordType 164 | /** @type {!Array} */ 165 | const callFrames = e.name === recordTypes.JSSample 166 | ? e.args['data']['stackTrace'].slice().reverse() 167 | : jsFramesStack.map((frameEvent): any => frameEvent.args['data']) 168 | 169 | filterStackFrames(callFrames) 170 | const endTime = e.endTime || e.startTime 171 | const minFrames = Math.min(callFrames.length, jsFramesStack.length) 172 | 173 | let i 174 | for (i = lockedJsStackDepth[lockedJsStackDepth.length - 1] || 0; i < minFrames; ++i) { 175 | const newFrame = callFrames[i] 176 | const oldFrame = jsFramesStack[i].args['data'] 177 | if (!equalFrames(newFrame, oldFrame)) { 178 | break 179 | } 180 | jsFramesStack[i].setEndTime(Math.max(jsFramesStack[i].endTime, endTime)) 181 | } 182 | truncateJSStack(i, e.startTime) 183 | for (; i < callFrames.length; ++i) { 184 | const frame = callFrames[i] 185 | const jsFrameEvent = new Event( 186 | DevToolsTimelineEventCategory, 187 | recordTypes.JSFrame, 188 | Phase.Complete, 189 | e.startTime, 190 | e.thread 191 | ) 192 | jsFrameEvent.ordinal = e.ordinal 193 | jsFrameEvent.addArgs({ data: frame } as any as EventData) 194 | jsFrameEvent.setEndTime(endTime) 195 | jsFramesStack.push(jsFrameEvent) 196 | jsFrameEvents.push(jsFrameEvent) 197 | } 198 | } 199 | 200 | /** 201 | * @param {!SDK.TracingModel.Event} e 202 | * @return {boolean} 203 | */ 204 | function isJSInvocationEvent(e: Event): boolean { 205 | switch (e.name) { 206 | case RecordType.RunMicrotasks: 207 | case RecordType.FunctionCall: 208 | case RecordType.EvaluateScript: 209 | case RecordType.EvaluateModule: 210 | case RecordType.EventDispatch: 211 | case RecordType.V8Execute: 212 | return true 213 | } 214 | return false 215 | } 216 | 217 | /** 218 | * @param {!SDK.TracingModel.Event} e 219 | */ 220 | function onStartEvent(e: Event): void { 221 | e.ordinal = ++ordinal 222 | extractStackTrace(e) 223 | // For the duration of the event we cannot go beyond the stack associated with it. 224 | lockedJsStackDepth.push(jsFramesStack.length) 225 | } 226 | 227 | /** 228 | * @param {!SDK.TracingModel.Event} e 229 | * @param {?SDK.TracingModel.Event} parent 230 | */ 231 | function onInstantEvent(e: Event, parent: Event): void { 232 | e.ordinal = ++ordinal 233 | if (parent && isJSInvocationEvent(parent)) { 234 | extractStackTrace(e) 235 | } 236 | 237 | } 238 | 239 | /** 240 | * @param {!SDK.TracingModel.Event} e 241 | */ 242 | function onEndEvent(e: Event): void { 243 | truncateJSStack(lockedJsStackDepth.pop(), e.endTime) 244 | } 245 | 246 | const firstTopLevelEvent = events.find(TracingModel.isTopLevelEvent) 247 | const startTime = firstTopLevelEvent ? firstTopLevelEvent.startTime : 0 248 | TimelineModel.forEachEvent(events, onStartEvent, onEndEvent, onInstantEvent, startTime) 249 | return jsFrameEvents 250 | } 251 | 252 | /** 253 | * @param {!Protocol.Runtime.CallFrame} frame 254 | * @return {boolean} 255 | */ 256 | public static isNativeRuntimeFrame(frame: any): boolean { 257 | return frame.url === 'native V8Runtime' 258 | } 259 | 260 | /** 261 | * @param {string} nativeName 262 | * @return {?TimelineModel.TimelineJSProfileProcessor.NativeGroups} 263 | */ 264 | public static nativeGroup(nativeName: string): NativeGroups | null { 265 | if (nativeName.startsWith('Parse')) { 266 | return NativeGroups.Parse 267 | } 268 | 269 | if (nativeName.startsWith('Compile') || nativeName.startsWith('Recompile')) { 270 | return NativeGroups.Compile 271 | } 272 | 273 | return null 274 | } 275 | 276 | /** 277 | * @param {*} profile 278 | * @param {number} tid 279 | * @param {boolean} injectPageEvent 280 | * @param {?string=} name 281 | * @return {!Array} 282 | */ 283 | public static buildTraceProfileFromCpuProfile( 284 | profile: any, 285 | tid: number, 286 | injectPageEvent: boolean, 287 | name?: string 288 | ): TraceEvent[] { 289 | const events: TraceEvent[] = [] 290 | if (injectPageEvent) { 291 | appendEvent('TracingStartedInPage', { data: { sessionId: '1' } }, 0, 0, 'M') 292 | } 293 | 294 | if (!name) { 295 | name = `Thread ${tid}` // todo: original: name = ls`Thread ${tid}` 296 | } 297 | 298 | appendEvent(MetadataEvent.ThreadName, { name }, 0, 0, 'M', '__metadata') 299 | if (!profile) { 300 | return events 301 | } 302 | 303 | const idToNode = new Map() 304 | const nodes = profile['nodes'] 305 | for (let i = 0; i < nodes.length; ++i) { 306 | idToNode.set(nodes[i].id, nodes[i]) 307 | } 308 | 309 | let programEvent: any = null 310 | let functionEvent: any = null 311 | let nextTime = profile.startTime 312 | let currentTime: number 313 | const samples = profile['samples'] 314 | const timeDeltas = profile['timeDeltas'] 315 | for (let i = 0; i < samples.length; ++i) { 316 | currentTime = nextTime 317 | nextTime += timeDeltas[i] 318 | const node = idToNode.get(samples[i]) 319 | const name = node.callFrame.functionName 320 | 321 | if (name === '(idle)') { 322 | closeEvents() 323 | continue 324 | } 325 | 326 | if (!programEvent) { 327 | programEvent = appendEvent('MessageLoop::RunTask', {}, currentTime, 0, 'X', 'toplevel') 328 | } 329 | 330 | if (name === '(program)') { 331 | if (functionEvent) { 332 | functionEvent.dur = currentTime - functionEvent.ts 333 | functionEvent = null 334 | } 335 | } else if (!functionEvent) { 336 | functionEvent = appendEvent('FunctionCall', { data: { sessionId: '1' } }, currentTime) 337 | } 338 | } 339 | closeEvents() 340 | appendEvent('CpuProfile', { data: { cpuProfile: profile } }, profile.endTime, 0, 'I') 341 | return events 342 | 343 | function closeEvents (): void { 344 | if (programEvent) { 345 | programEvent.dur = currentTime - programEvent.ts 346 | } 347 | if (functionEvent) { 348 | functionEvent.dur = currentTime - functionEvent.ts 349 | } 350 | programEvent = null 351 | functionEvent = null 352 | } 353 | 354 | /** 355 | * @param {string} name 356 | * @param {*} args 357 | * @param {number} ts 358 | * @param {number=} dur 359 | * @param {string=} ph 360 | * @param {string=} cat 361 | * @return {!SDK.TracingManager.TraceEvent} 362 | */ 363 | function appendEvent(name: string, args: any, ts: number, dur?: number, ph?: string, cat?: string): TraceEvent { 364 | const event: TraceEvent = /** @type {!SDK.TracingManager.TraceEvent} */ ({ 365 | cat: cat || 'disabled-by-default-devtools.timeline', 366 | name, 367 | ph: ph || 'X', 368 | pid: 1, 369 | tid, 370 | ts, 371 | args, 372 | }) 373 | if (dur) { 374 | event.dur = dur 375 | } 376 | events.push(event) 377 | return event 378 | } 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /devtools/timelineModel/timelineModelFilter/index.ts: -------------------------------------------------------------------------------- 1 | import Event from '../../tracingModel/event' 2 | 3 | export default class TimelineModelFilter { 4 | /** 5 | * @param {!SDK.TracingModel.Event} event 6 | * @return {boolean} 7 | */ 8 | public accept(event: Event): boolean { 9 | return true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /devtools/timelineModel/timelineModelFilter/timelineCategory.ts: -------------------------------------------------------------------------------- 1 | export default class TimelineCategory { 2 | public name: string 3 | public title: string 4 | public visible: boolean 5 | public childColor: string 6 | public color: string 7 | private _hidden: boolean 8 | 9 | /** 10 | * @param {string} name 11 | * @param {string} title 12 | * @param {boolean} visible 13 | * @param {string} childColor 14 | * @param {string} color 15 | */ 16 | public constructor(name: string, title: string, visible: boolean, childColor: string, color: string) { 17 | this.name = name 18 | this.title = title 19 | this.visible = visible 20 | this.childColor = childColor 21 | this.color = color 22 | this.hidden = false 23 | } 24 | 25 | /** 26 | * @return {boolean} 27 | */ 28 | public get hidden(): boolean { 29 | return this._hidden 30 | } 31 | 32 | /** 33 | * @param {boolean} hidden 34 | */ 35 | public set hidden(hidden) { 36 | this._hidden = hidden 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /devtools/timelineModel/timelineModelFilter/timelineRecordStyle.ts: -------------------------------------------------------------------------------- 1 | import TimelineCategory from './timelineCategory' 2 | 3 | export default class TimelineRecordStyle { 4 | public title: string 5 | public category: TimelineCategory 6 | public hidden: boolean 7 | /** 8 | * @param {string} title 9 | * @param {!Timeline.TimelineCategory} category 10 | * @param {boolean=} hidden 11 | */ 12 | 13 | public constructor(title: string, category: TimelineCategory, hidden?: boolean) { 14 | this.title = title 15 | this.category = category 16 | this.hidden = !!hidden 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /devtools/timelineModel/timelineModelFilter/timelineSelection.ts: -------------------------------------------------------------------------------- 1 | import { TimelineSelectionType } from '../../types' 2 | import TimelineFrame from '../timelineFrame/timelineFrame' 3 | import NetworkRequest from '../networkRequest' 4 | import Event from '../../tracingModel/event' 5 | 6 | export default class TimelineSelection { 7 | private _type: TimelineSelectionType 8 | private _startTime: number 9 | private _endTime: number 10 | private _object: object 11 | 12 | /** 13 | * @param {!Timeline.TimelineSelection.Type} type 14 | * @param {number} startTime 15 | * @param {number} endTime 16 | * @param {!Object=} object 17 | */ 18 | public constructor(type: TimelineSelectionType, startTime: number, endTime: number, object?: object) { 19 | this._type = type 20 | this._startTime = startTime 21 | this._endTime = endTime 22 | this._object = object || null 23 | } 24 | 25 | /** 26 | * @param {!TimelineModel.TimelineFrame} frame 27 | * @return {!Timeline.TimelineSelection} 28 | */ 29 | public static fromFrame(frame: TimelineFrame): TimelineSelection { 30 | return new TimelineSelection(TimelineSelectionType.Frame, frame.startTime, frame.endTime, frame) 31 | } 32 | 33 | /** 34 | * @param {!TimelineModel.TimelineModel.NetworkRequest} request 35 | * @return {!Timeline.TimelineSelection} 36 | */ 37 | public static fromNetworkRequest(request: NetworkRequest): TimelineSelection { 38 | return new TimelineSelection( 39 | TimelineSelectionType.NetworkRequest, 40 | request.startTime, 41 | request.endTime || request.startTime, 42 | request 43 | ) 44 | } 45 | 46 | /** 47 | * @param {!SDK.TracingModel.Event} event 48 | * @return {!Timeline.TimelineSelection} 49 | */ 50 | public static fromTraceEvent(event: Event): TimelineSelection { 51 | return new TimelineSelection( 52 | TimelineSelectionType.TraceEvent, 53 | event.startTime, 54 | event.endTime || event.startTime + 1, 55 | event 56 | ) 57 | } 58 | 59 | /** 60 | * @param {number} startTime 61 | * @param {number} endTime 62 | * @return {!Timeline.TimelineSelection} 63 | */ 64 | public static fromRange(startTime: number, endTime: number): TimelineSelection { 65 | return new TimelineSelection(TimelineSelectionType.Range, startTime, endTime) 66 | } 67 | 68 | /** 69 | * @return {!Timeline.TimelineSelection.Type} 70 | */ 71 | public type(): TimelineSelectionType { 72 | return this._type 73 | } 74 | 75 | /** 76 | * @return {?Object} 77 | */ 78 | public object(): object { 79 | return this._object 80 | } 81 | 82 | /** 83 | * @return {number} 84 | */ 85 | public startTime(): number { 86 | return this._startTime 87 | } 88 | 89 | /** 90 | * @return {number} 91 | */ 92 | public endTime(): number { 93 | return this._endTime 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /devtools/timelineModel/timelineModelFilter/timelineVisibleEventsFilter.ts: -------------------------------------------------------------------------------- 1 | import TimelineModelFilter from './' 2 | import Event from '../../tracingModel/event' 3 | import { RecordType, Category } from '../../types' 4 | 5 | export default class TimelineVisibleEventsFilter extends TimelineModelFilter { 6 | private _visibleTypes: Set 7 | 8 | /** 9 | * @param {!Array} visibleTypes 10 | */ 11 | public constructor(visibleTypes: string[]) { 12 | super() 13 | this._visibleTypes = new Set(visibleTypes) 14 | } 15 | 16 | /** 17 | * @override 18 | * @param {!SDK.TracingModel.Event} event 19 | * @return {boolean} 20 | */ 21 | public accept(event: Event): boolean { 22 | return this._visibleTypes.has(TimelineVisibleEventsFilter._eventType(event)) 23 | } 24 | 25 | /** 26 | * @return {!TimelineModel.TimelineModel.RecordType} 27 | */ 28 | public static _eventType(event: Event): RecordType | string { 29 | if (event.hasCategory(Category.Console)) { 30 | return RecordType.ConsoleTime 31 | } 32 | if (event.hasCategory(Category.UserTiming)) { 33 | return RecordType.UserTiming 34 | } 35 | if (event.hasCategory(Category.LatencyInfo)) { 36 | return RecordType.LatencyInfo 37 | } 38 | return /** @type !TimelineModel.TimelineModel.RecordType */ event.name 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /devtools/timelineModel/track.ts: -------------------------------------------------------------------------------- 1 | import Event from '../tracingModel/event' 2 | import Thread from '../tracingModel/thread' 3 | import { Phase } from '../tracingModel' 4 | 5 | export enum TrackType { 6 | MainThread, 7 | Worker, 8 | Input, 9 | Animation, 10 | Timings, 11 | Console, 12 | Raster, 13 | GPU, 14 | Other 15 | } 16 | 17 | export default class Track { 18 | public name: string 19 | public type: TrackType 20 | public forMainFrame: boolean 21 | public url: string 22 | public events: Event[] 23 | public asyncEvents: Event[] 24 | public tasks: Event[] 25 | public thread: Thread 26 | 27 | private _syncEvents: Event[] 28 | 29 | public constructor () { 30 | this.name = '' 31 | this.url = '' 32 | this.type = TrackType.Other 33 | this.asyncEvents = [] 34 | this.tasks = [] 35 | this._syncEvents = null 36 | this.thread = null 37 | 38 | // TODO(dgozman): replace forMainFrame with a list of frames, urls and time ranges. 39 | this.forMainFrame = false 40 | 41 | // TODO(dgozman): do not distinguish between sync and async events. 42 | this.events = [] 43 | } 44 | 45 | /** 46 | * @return {!Array} 47 | */ 48 | public syncEvents (): Event[] { 49 | if (this.events.length) { 50 | return this.events 51 | } 52 | 53 | if (this._syncEvents) { 54 | return this._syncEvents 55 | } 56 | 57 | const stack = [] 58 | this._syncEvents = [] 59 | for (const event of this.asyncEvents) { 60 | const startTime = event.startTime 61 | const endTime = event.endTime 62 | while (stack.length && startTime >= stack[stack.length - 1].endTime) { 63 | stack.pop() 64 | } 65 | 66 | if (stack.length && endTime > stack[stack.length - 1].endTime) { 67 | this._syncEvents = [] 68 | break 69 | } 70 | 71 | const syncEvent = new Event( 72 | event.categoriesString, 73 | event.name, 74 | Phase.Complete, 75 | startTime, 76 | event.thread 77 | ) 78 | 79 | syncEvent.setEndTime(endTime) 80 | syncEvent.addArgs(event.args) 81 | this._syncEvents.push(syncEvent) 82 | stack.push(syncEvent) 83 | } 84 | 85 | return this._syncEvents 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /devtools/tracingModel/asyncEvent.ts: -------------------------------------------------------------------------------- 1 | import { Phase } from './' 2 | import Event from './event' 3 | 4 | export default class AsyncEvent extends Event { 5 | public steps: Event[] 6 | public causedFrame: boolean 7 | 8 | /** 9 | * @param {!TracingModel.Event} startEvent 10 | */ 11 | public constructor(startEvent: Event) { 12 | super(startEvent.categoriesString, startEvent.name, startEvent.phase, startEvent.startTime, startEvent.thread) 13 | this.addArgs(startEvent.args) 14 | this.steps = [startEvent] 15 | } 16 | 17 | /** 18 | * @param {!TracingModel.Event} event 19 | */ 20 | public addStep (event: Event): void { 21 | this.steps.push(event) 22 | 23 | if (event.phase === Phase.AsyncEnd || event.phase === Phase.NestableAsyncEnd) { 24 | this.setEndTime(event.startTime) 25 | // FIXME: ideally, we shouldn't do this, but this makes the logic of converting 26 | // async console events to sync ones much simpler. 27 | this.steps[0].setEndTime(event.startTime) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /devtools/tracingModel/event.ts: -------------------------------------------------------------------------------- 1 | import TracingModel from './index' 2 | import Thread from './thread' 3 | import InvalidationTracker from '../timelineModel/invalidationTracker' 4 | import InvalidationTrackingEvent from '../timelineModel/invalidationTrackingEvent' 5 | import { TraceEvent, EventData } from '../types' 6 | 7 | export default class Event { 8 | private _parsedCategories: Set 9 | 10 | public id?: string 11 | public categoriesString: string 12 | public name: string 13 | public phase: string 14 | public startTime: number 15 | public endTime?: number 16 | public duration?: number 17 | public thread: Thread 18 | public args: Record 19 | public selfTime: number 20 | // eslint-disable-next-line 21 | public bind_id?: string 22 | public ordinal: number 23 | public [InvalidationTracker.invalidationTrackingEventsSymbol]: InvalidationTrackingEvent[] 24 | 25 | /** 26 | * @param {number} startTime * @param {string|undefined} categories 27 | * @param {string} name 28 | * @param {!Phase} phase 29 | * @param {!Thread} thread 30 | */ 31 | public constructor (categories: string | undefined, name: string, phase: string, startTime: number, thread: Thread) { 32 | this.categoriesString = categories || '' 33 | this._parsedCategories = thread.model.parsedCategoriesForString(this.categoriesString) 34 | this.name = name 35 | this.phase = phase 36 | this.startTime = startTime 37 | this.thread = thread 38 | this.args = {} 39 | 40 | this.selfTime = 0 41 | } 42 | 43 | /** 44 | * @param {!TracingManager.EventPayload} payload 45 | * @param {!Thread} thread 46 | * @return {!Event} 47 | */ 48 | public static fromPayload (payload: TraceEvent, thread: Thread): Event { 49 | const event = new Event( 50 | payload.cat, 51 | payload.name, 52 | payload.ph, 53 | payload.ts / 1000, 54 | thread 55 | ) 56 | 57 | if (payload.args) { 58 | event.addArgs(payload.args) 59 | } 60 | 61 | if (typeof payload.dur === 'number') { 62 | event.setEndTime((payload.ts + payload.dur) / 1000) 63 | } 64 | 65 | const id = TracingModel.extractId(payload) 66 | if (typeof id !== 'undefined') { 67 | event.id = id 68 | } 69 | 70 | if (payload.bind_id) { 71 | // eslint-disable-next-line 72 | event.bind_id = payload.bind_id 73 | } 74 | 75 | return event 76 | } 77 | 78 | /** 79 | * @param {!Event} a 80 | * @param {!Event} b 81 | * @return {number} 82 | */ 83 | public static compareStartTime (a: Event, b: Event): number { 84 | return a.startTime - b.startTime 85 | } 86 | 87 | /** 88 | * @param {!Event} a 89 | * @param {!Event} b 90 | * @return {number} 91 | */ 92 | public static orderedCompareStartTime (a: Event, b: Event): number { 93 | // Array.mergeOrdered coalesces objects if comparator returns 0. 94 | // To change this behavior this comparator return -1 in the case events 95 | // startTime's are equal, so both events got placed into the result array. 96 | return a.startTime - b.startTime || -1 97 | } 98 | 99 | /** 100 | * @param {string} categoryName 101 | * @return {boolean} 102 | */ 103 | public hasCategory (categoryName: string): boolean { 104 | return this._parsedCategories.has(categoryName) 105 | } 106 | 107 | /** 108 | * @param {number} endTime 109 | */ 110 | public setEndTime (endTime: number): void { 111 | if (endTime < this.startTime) { 112 | console.assert(false, 'Event out of order: ' + this.name) 113 | return 114 | } 115 | this.endTime = endTime 116 | this.duration = endTime - this.startTime 117 | } 118 | 119 | /** 120 | * @param {!Object} args 121 | */ 122 | public addArgs (args: EventData): void { 123 | /** 124 | * Shallow copy args to avoid modifying original payload which may be saved to file. 125 | */ 126 | for (const name in args) { 127 | if (name in this.args) { 128 | console.error(`Same argument name (${name}) is used for begin and end phases of ${this.name}`) 129 | } 130 | 131 | this.args[name] = (args as any)[name] 132 | } 133 | } 134 | 135 | /** 136 | * @param {!Event} endEvent 137 | */ 138 | private _complete (endEvent: Event): void { 139 | if (endEvent.args) { 140 | this.addArgs(endEvent.args) 141 | } else { 142 | console.error(`Missing mandatory event argument 'args' at ${endEvent.startTime}`) 143 | } 144 | 145 | this.setEndTime(endEvent.startTime) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /devtools/tracingModel/namedObject.ts: -------------------------------------------------------------------------------- 1 | import TracingModel from './index' 2 | 3 | export default class NamedObject { 4 | protected _model: TracingModel 5 | protected _id: number 6 | private _name: string 7 | private _sortIndex: number 8 | 9 | /** 10 | * @param {!TracingModel} model 11 | * @param {number} id 12 | */ 13 | public constructor (model: TracingModel, id: number) { 14 | this._model = model 15 | this._id = id 16 | this._name = '' 17 | this._sortIndex = 0 18 | } 19 | 20 | public get model (): TracingModel { 21 | return this._model 22 | } 23 | 24 | /** 25 | * @param {!Array.} array 26 | */ 27 | public static sort (array: T[]): T[] { 28 | /** 29 | * @param {!TracingModel.NamedObject} a 30 | * @param {!TracingModel.NamedObject} b 31 | */ 32 | function comparator (a: T, b: T): number { 33 | return a._sortIndex !== b._sortIndex ? a._sortIndex - b._sortIndex : a.name().localeCompare(b.name()) 34 | } 35 | return array.sort(comparator) 36 | } 37 | 38 | /** 39 | * @param {string} name 40 | */ 41 | protected _setName (name: string): void { 42 | this._name = name 43 | } 44 | 45 | /** 46 | * @return {string} 47 | */ 48 | public name (): string { 49 | return this._name 50 | } 51 | 52 | /** 53 | * @param {number} sortIndex 54 | */ 55 | public setSortIndex (sortIndex: number): void { 56 | this._sortIndex = sortIndex 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /devtools/tracingModel/objectSnapshot.ts: -------------------------------------------------------------------------------- 1 | import Event from './event' 2 | import Thread from './thread' 3 | import TracingModel, { Phase } from './' 4 | import { TraceEvent } from '../types' 5 | 6 | export default class ObjectSnapshot extends Event { 7 | public id?: string 8 | private _objectPromise?: Promise 9 | 10 | /** 11 | * @param {string|undefined} category 12 | * @param {string} name 13 | * @param {number} startTime 14 | * @param {!SDK.TracingModel.Thread} thread 15 | */ 16 | public constructor(category?: string, name?: string, startTime?: number, thread?: Thread) { 17 | super(category, name, Phase.SnapshotObject, startTime, thread) 18 | /** @type {?function():!Promise} */ 19 | /** @type {string} */ 20 | this.id 21 | /** @type {?Promise} */ 22 | this._objectPromise = null 23 | } 24 | 25 | /** 26 | * @param {!SDK.TracingManager.EventPayload} payload 27 | * @param {!SDK.TracingModel.Thread} thread 28 | * @return {!SDK.TracingModel.ObjectSnapshot} 29 | */ 30 | public static fromPayload(payload: TraceEvent, thread: Thread): ObjectSnapshot { 31 | const snapshot = new ObjectSnapshot(payload.cat, payload.name, payload.ts / 1000, thread) 32 | const id = TracingModel.extractId(payload) 33 | if (typeof id !== 'undefined') { 34 | snapshot.id = id 35 | } 36 | 37 | if (!payload.args || !payload.args['snapshot']) { 38 | console.error(`Missing mandatory 'snapshot' argument at ${payload.ts / 1000}`) 39 | return snapshot 40 | } 41 | if (payload.args) { 42 | snapshot.addArgs(payload.args) 43 | } 44 | 45 | return snapshot 46 | } 47 | 48 | /** 49 | * @param {function(?)} callback 50 | */ 51 | // todo fix callback type 52 | public requestObject?(callback: any): void { 53 | const snapshot = this.args['snapshot'] 54 | if (snapshot) { 55 | callback(snapshot) 56 | return 57 | } 58 | /** 59 | * @param {?string} result 60 | */ 61 | function onRead(result: string): void { 62 | if (!result) { 63 | callback(null) 64 | return 65 | } 66 | try { 67 | const payload = JSON.parse(result) 68 | callback(payload['args']['snapshot']) 69 | } catch (e) { 70 | console.error('Malformed event data in backing storage') 71 | callback(null) 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * @return {!Promise} 78 | */ 79 | public objectPromise?(): Promise { 80 | if (!this._objectPromise) { 81 | this._objectPromise = new Promise(this.requestObject.bind(this)) 82 | } 83 | return this._objectPromise 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /devtools/tracingModel/process.ts: -------------------------------------------------------------------------------- 1 | import Event from './event' 2 | import Thread from './thread' 3 | import TracingModel from './index' 4 | import NamedObject from './namedObject' 5 | import { TraceEvent } from '../types' 6 | 7 | export default class Process extends NamedObject { 8 | private _threads: Map 9 | private _threadByName: Map 10 | 11 | /** 12 | * @param {!TracingModel} model 13 | * @param {number} id 14 | */ 15 | public constructor (model: TracingModel, id: number) { 16 | super(model, id) 17 | this._threads = new Map() 18 | this._threadByName = new Map() 19 | } 20 | 21 | public get threads (): Map { 22 | return this._threads 23 | } 24 | 25 | /** 26 | * @return {number} 27 | */ 28 | public id (): number { 29 | return this._id 30 | } 31 | 32 | /** 33 | * @override 34 | * @param {string} name 35 | */ 36 | public setName (name: string): void { 37 | super._setName(name) 38 | } 39 | 40 | /** 41 | * @param {number} id 42 | * @return {!TracingModel.Thread} 43 | */ 44 | public threadById (id: number): Thread { 45 | let thread = this._threads.get(id) 46 | if (!thread) { 47 | thread = new Thread(this, id) 48 | this._threads.set(id, thread) 49 | } 50 | return thread 51 | } 52 | 53 | /** 54 | * @param {string} name 55 | * @return {?TracingModel.Thread} 56 | */ 57 | public threadByName (name: string): Thread | null { 58 | return this._threadByName.get(name) || null 59 | } 60 | 61 | /** 62 | * @param {string} name 63 | * @param {!TracingModel.Thread} thread 64 | */ 65 | public setThreadByName (name: string, thread: Thread): void { 66 | this._threadByName.set(name, thread) 67 | } 68 | 69 | /** 70 | * @param {!TracingManager.EventPayload} payload 71 | * @return {?TracingModel.Event} event 72 | */ 73 | public addEvent (payload: TraceEvent): Event | null { 74 | return this.threadById(payload.tid).addEvent(payload) 75 | } 76 | 77 | /** 78 | * @return {!Array.} 79 | */ 80 | public sortedThreads (): Thread[] { 81 | return Thread.sort([...this._threads.values()]) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /devtools/tracingModel/profileEventsGroup.ts: -------------------------------------------------------------------------------- 1 | import Event from './event' 2 | 3 | export default class ProfileEventsGroup { 4 | public children: Event[] 5 | 6 | /** 7 | * @param {!TracingModel.Event} event 8 | */ 9 | public constructor (event: Event) { 10 | /** @type {!Array} */ 11 | this.children = [event] 12 | } 13 | 14 | /** 15 | * @param {!TracingModel.Event} event 16 | */ 17 | public addChild (event: Event): void { 18 | this.children.push(event) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /devtools/tracingModel/thread.ts: -------------------------------------------------------------------------------- 1 | import AsyncEvent from './asyncEvent' 2 | import Event from './event' 3 | import Process from './process' 4 | import NamedObject from './namedObject' 5 | import ObjectSnapshot from './objectSnapshot' 6 | import TracingModel, { Phase } from './' 7 | import { TraceEvent } from '../types' 8 | import { stableSort, remove } from '../utils' 9 | 10 | export default class Thread extends NamedObject { 11 | public ordinal: number 12 | 13 | private _process: Process 14 | private _events: Event[] 15 | private _asyncEvents: AsyncEvent[] 16 | private _lastTopLevelEvent: Event | AsyncEvent | null 17 | 18 | /** 19 | * @param {!Process} process 20 | * @param {number} id 21 | */ 22 | public constructor (process: Process, id: number) { 23 | super(process.model, id) 24 | this._process = process 25 | this._events = [] 26 | this._asyncEvents = [] 27 | this._lastTopLevelEvent = null 28 | } 29 | 30 | public tracingComplete(): void { 31 | stableSort(this._asyncEvents, Event.compareStartTime) 32 | stableSort(this._events, Event.compareStartTime) 33 | const phases = Phase 34 | const stack = [] 35 | for (let i = 0; i < this._events.length; ++i) { 36 | const e = this._events[i] 37 | e.ordinal = i 38 | switch (e.phase) { 39 | case phases.End: 40 | this._events[i] = null // Mark for removal. 41 | // Quietly ignore unbalanced close events, they're legit (we could have missed start one). 42 | if (!stack.length) { 43 | continue 44 | } 45 | const top: any = stack.pop() 46 | if (top.name !== e.name || top.categoriesString !== e.categoriesString) { 47 | console.error( 48 | 'B/E events mismatch at ' + top.startTime + ' (' + top.name + ') vs. ' + e.startTime + ' (' + e.name + 49 | ')') 50 | } else { 51 | top._complete(e) 52 | } 53 | break 54 | case phases.Begin: 55 | stack.push(e) 56 | break 57 | } 58 | } 59 | while (stack.length) { 60 | stack.pop().setEndTime(this._model.maximumRecordTime()) 61 | } 62 | remove(this._events, null, false) 63 | } 64 | 65 | /** 66 | * @param {!TracingManager.EventPayload} payload 67 | * @return {?Event} event 68 | */ 69 | public addEvent (payload: TraceEvent): Event | null { 70 | const event = payload.ph === Phase.SnapshotObject 71 | ? ObjectSnapshot.fromPayload(payload, this) 72 | : Event.fromPayload(payload, this) 73 | 74 | if (TracingModel.isTopLevelEvent(event)) { 75 | // Discard nested "top-level" events. 76 | if (this._lastTopLevelEvent && this._lastTopLevelEvent.endTime > event.startTime) { 77 | return null 78 | } 79 | this._lastTopLevelEvent = event 80 | } 81 | 82 | this._events.push(event) 83 | return event 84 | } 85 | 86 | /** 87 | * @param {!AsyncEvent} asyncEvent 88 | */ 89 | public addAsyncEvent (asyncEvent: AsyncEvent): void { 90 | this._asyncEvents.push(asyncEvent) 91 | } 92 | 93 | /** 94 | * @override 95 | * @param {string} name 96 | */ 97 | public setName (name: string): void { 98 | super._setName(name) 99 | this._process.setThreadByName(name, this) 100 | } 101 | 102 | /** 103 | * @return {number} 104 | */ 105 | public id (): number { 106 | return this._id 107 | } 108 | 109 | /** 110 | * @return {!Process} 111 | */ 112 | public process (): Process { 113 | return this._process 114 | } 115 | 116 | /** 117 | * @return {!Array.} 118 | */ 119 | public events (): Event[] { 120 | return this._events 121 | } 122 | 123 | /** 124 | * @return {!Array.} 125 | */ 126 | public asyncEvents (): AsyncEvent[] { 127 | return this._asyncEvents 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /devtools/types.ts: -------------------------------------------------------------------------------- 1 | import InvalidationTrackingEvent from './timelineModel/invalidationTrackingEvent' 2 | import PageFrame, { PageFrameProcess } from './timelineModel/pageFrame' 3 | import ProfileNode from './profileTreeModel/profileNode' 4 | import Event from './tracingModel/event' 5 | import TimelineFrame from './timelineModel/timelineFrame/timelineFrame' 6 | import Thread from './tracingModel/thread' 7 | import TimelineRecordStyle from './timelineModel/timelineModelFilter/timelineRecordStyle' 8 | import TimelineCategory from './timelineModel/timelineModelFilter/timelineCategory' 9 | import Counter from './timelineModel/counterGraph/counter' 10 | 11 | export interface TracelogArgs { 12 | name?: string; 13 | // eslint-disable-next-line 14 | sort_index?: number; 15 | snapshot?: string; 16 | stackTrace?: any; 17 | data?: { 18 | args: Record 19 | }; 20 | } 21 | 22 | export interface PageFramePayload { 23 | frameId: string; 24 | url: string; 25 | name: string; 26 | children: PageFrame[]; 27 | parent: string; 28 | processes: PageFrameProcess[]; 29 | deletedTime: number; 30 | } 31 | 32 | export interface EventData { 33 | startTime?: number; 34 | endTime?: number; 35 | finishTime?: number; 36 | cpuProfile?: Profile; 37 | lines?: number[]; 38 | timeDeltas?: number[]; 39 | stackTrace?: string[]; 40 | url?: string; 41 | frame?: string; 42 | nodeId?: number; 43 | name?: string; 44 | nodeName?: string; 45 | invalidationSet?: number; 46 | invalidatedSelectorId?: string; 47 | layerId?: string; 48 | layerTreeId?: number; 49 | changedId?: string; 50 | workerThreadId?: number; 51 | processId?: number; 52 | workerId?: string; 53 | processPseudoId?: string; 54 | frameTreeNodeId?: number; 55 | persistentIds?: number[]; 56 | changedClass?: string; 57 | changedAttribute?: string; 58 | changedPseudo?: string; 59 | selectorPart?: string; 60 | extraData?: string; 61 | invalidationList?: InvalidationMap[]; 62 | reason?: string; 63 | mimeType?: string; 64 | scriptLine?: number; 65 | scriptName?: string; 66 | lineNumber?: number; 67 | decodedBodyLength?: number; 68 | encodedDataLength?: number; 69 | requestMethod?: string; 70 | timing?: Timing; 71 | fromServiceWorker?: boolean; 72 | fromCache?: boolean; 73 | priority?: ResourcePriority; 74 | isMainFrame?: boolean; 75 | allottedMilliseconds?: number; 76 | sessionId?: string; 77 | page?: boolean; 78 | INPUT_EVENT_LATENCY_RENDERER_SWAP_COMPONENT?: string; 79 | INPUT_EVENT_LATENCY_RENDERER_MAIN_COMPONENT?: { time: number }; 80 | frames?: PageFramePayload[]; 81 | // eslint-disable-next-line 82 | sort_index?: number; 83 | snapshot?: string; 84 | sourceFrameNumber?: number; 85 | needsBeginFrame?: any; // todo fix type here 86 | frameId?: number | undefined; 87 | parent?: string; 88 | } 89 | 90 | export interface Profile { 91 | startTime?: number; 92 | endTime?: number; 93 | timestamps?: number[]; 94 | samples?: number[]; 95 | lines?: number[]; 96 | nodes?: ProfileNode[]; 97 | head?: ProfileNode; 98 | timeDeltas?: number[]; 99 | } 100 | 101 | export interface TraceEvent { 102 | cat?: string; 103 | pid: number; 104 | tid: number; 105 | ts: number; 106 | ph: string; 107 | name: string; 108 | args: EventData; 109 | dur?: number; 110 | id?: string; 111 | id2?: { 112 | global?: string; 113 | local?: string; 114 | } | void; 115 | scope?: string; 116 | // eslint-disable-next-line 117 | bind_id?: string; 118 | s?: string; 119 | } 120 | 121 | export interface InvalidationCause { 122 | reason: string 123 | stackTrace: any 124 | } 125 | 126 | export interface InvalidationMap { 127 | [key: string]: InvalidationTrackingEvent | string | number 128 | } 129 | 130 | export interface Timing { 131 | blocked: number; 132 | dns: number; 133 | ssl: number; 134 | connect: number; 135 | send: number; 136 | wait: number; 137 | receive: number; 138 | // eslint-disable-next-line 139 | _blocked_queueing: number; 140 | // eslint-disable-next-line 141 | _blocked_proxy: (number|undefined); 142 | pushStart: number; 143 | requestTime: number; 144 | } 145 | 146 | export enum ResourcePriority { 147 | VeryLow, Low, Medium, High, VeryHigh 148 | } 149 | 150 | export interface CallFrame { 151 | functionName: string; 152 | scriptId: string; 153 | url: string; 154 | lineNumber: number; 155 | columnNumber: number; 156 | } 157 | 158 | export enum RecordType { 159 | Task = 'RunTask', 160 | Program = 'Program', 161 | EventDispatch = 'EventDispatch', 162 | 163 | GPUTask = 'GPUTask', 164 | 165 | Animation = 'Animation', 166 | RequestMainThreadFrame = 'RequestMainThreadFrame', 167 | BeginFrame = 'BeginFrame', 168 | NeedsBeginFrameChanged = 'NeedsBeginFrameChanged', 169 | BeginMainThreadFrame = 'BeginMainThreadFrame', 170 | ActivateLayerTree = 'ActivateLayerTree', 171 | DrawFrame = 'DrawFrame', 172 | HitTest = 'HitTest', 173 | ScheduleStyleRecalculation = 'ScheduleStyleRecalculation', 174 | RecalculateStyles = 'RecalculateStyles', // For backwards compatibility only, now replaced by UpdateLayoutTree. 175 | UpdateLayoutTree = 'UpdateLayoutTree', 176 | InvalidateLayout = 'InvalidateLayout', 177 | Layout = 'Layout', 178 | UpdateLayer = 'UpdateLayer', 179 | UpdateLayerTree = 'UpdateLayerTree', 180 | PaintSetup = 'PaintSetup', 181 | Paint = 'Paint', 182 | PaintImage = 'PaintImage', 183 | Rasterize = 'Rasterize', 184 | RasterTask = 'RasterTask', 185 | ScrollLayer = 'ScrollLayer', 186 | CompositeLayers = 'CompositeLayers', 187 | 188 | ScheduleStyleInvalidationTracking = 'ScheduleStyleInvalidationTracking', 189 | StyleRecalcInvalidationTracking = 'StyleRecalcInvalidationTracking', 190 | StyleInvalidatorInvalidationTracking = 'StyleInvalidatorInvalidationTracking', 191 | LayoutInvalidationTracking = 'LayoutInvalidationTracking', 192 | 193 | ParseHTML = 'ParseHTML', 194 | ParseAuthorStyleSheet = 'ParseAuthorStyleSheet', 195 | 196 | TimerInstall = 'TimerInstall', 197 | TimerRemove = 'TimerRemove', 198 | TimerFire = 'TimerFire', 199 | 200 | XHRReadyStateChange = 'XHRReadyStateChange', 201 | XHRLoad = 'XHRLoad', 202 | CompileScript = 'v8.compile', 203 | EvaluateScript = 'EvaluateScript', 204 | CompileModule = 'v8.compileModule', 205 | EvaluateModule = 'v8.evaluateModule', 206 | WasmStreamFromResponseCallback = 'v8.wasm.streamFromResponseCallback', 207 | WasmCompiledModule = 'v8.wasm.compiledModule', 208 | WasmCachedModule = 'v8.wasm.cachedModule', 209 | WasmModuleCacheHit = 'v8.wasm.moduleCacheHit', 210 | WasmModuleCacheInvalid = 'v8.wasm.moduleCacheInvalid', 211 | 212 | FrameStartedLoading = 'FrameStartedLoading', 213 | CommitLoad = 'CommitLoad', 214 | MarkLoad = 'MarkLoad', 215 | MarkDOMContent = 'MarkDOMContent', 216 | MarkFirstPaint = 'firstPaint', 217 | MarkFCP = 'firstContentfulPaint', 218 | MarkFMP = 'firstMeaningfulPaint', 219 | 220 | TimeStamp = 'TimeStamp', 221 | ConsoleTime = 'ConsoleTime', 222 | UserTiming = 'UserTiming', 223 | 224 | ResourceSendRequest = 'ResourceSendRequest', 225 | ResourceReceiveResponse = 'ResourceReceiveResponse', 226 | ResourceReceivedData = 'ResourceReceivedData', 227 | ResourceFinish = 'ResourceFinish', 228 | 229 | RunMicrotasks = 'RunMicrotasks', 230 | FunctionCall = 'FunctionCall', 231 | GCEvent = 'GCEvent', // For backwards compatibility only, now replaced by MinorGC/MajorGC. 232 | MajorGC = 'MajorGC', 233 | MinorGC = 'MinorGC', 234 | JSFrame = 'JSFrame', 235 | JSSample = 'JSSample', 236 | // V8Sample events are coming from tracing and contain raw stacks with function addresses. 237 | // After being processed with help of JitCodeAdded and JitCodeMoved events they 238 | // get translated into function infos and stored as stacks in JSSample events. 239 | V8Sample = 'V8Sample', 240 | JitCodeAdded = 'JitCodeAdded', 241 | JitCodeMoved = 'JitCodeMoved', 242 | ParseScriptOnBackground = 'v8.parseOnBackground', 243 | V8Execute = 'V8.Execute', 244 | 245 | UpdateCounters = 'UpdateCounters', 246 | 247 | RequestAnimationFrame = 'RequestAnimationFrame', 248 | CancelAnimationFrame = 'CancelAnimationFrame', 249 | FireAnimationFrame = 'FireAnimationFrame', 250 | 251 | RequestIdleCallback = 'RequestIdleCallback', 252 | CancelIdleCallback = 'CancelIdleCallback', 253 | FireIdleCallback = 'FireIdleCallback', 254 | 255 | WebSocketCreate = 'WebSocketCreate', 256 | WebSocketSendHandshakeRequest = 'WebSocketSendHandshakeRequest', 257 | WebSocketReceiveHandshakeResponse = 'WebSocketReceiveHandshakeResponse', 258 | WebSocketDestroy = 'WebSocketDestroy', 259 | 260 | EmbedderCallback = 'EmbedderCallback', 261 | 262 | SetLayerTreeId = 'SetLayerTreeId', 263 | TracingStartedInPage = 'TracingStartedInPage', 264 | TracingSessionIdForWorker = 'TracingSessionIdForWorker', 265 | 266 | DecodeImage = 'Decode Image', 267 | ResizeImage = 'Resize Image', 268 | DrawLazyPixelRef = 'Draw LazyPixelRef', 269 | DecodeLazyPixelRef = 'Decode LazyPixelRef', 270 | 271 | LazyPixelRef = 'LazyPixelRef', 272 | LayerTreeHostImplSnapshot = 'cc::LayerTreeHostImpl', 273 | PictureSnapshot = 'cc::Picture', 274 | DisplayItemListSnapshot = 'cc::DisplayItemList', 275 | LatencyInfo = 'LatencyInfo', 276 | LatencyInfoFlow = 'LatencyInfo.Flow', 277 | InputLatencyMouseMove = 'InputLatency::MouseMove', 278 | InputLatencyMouseWheel = 'InputLatency::MouseWheel', 279 | ImplSideFling = 'InputHandlerProxy::HandleGestureFling::started', 280 | GCCollectGarbage = 'BlinkGC.AtomicPhase', 281 | 282 | CryptoDoEncrypt = 'DoEncrypt', 283 | CryptoDoEncryptReply = 'DoEncryptReply', 284 | CryptoDoDecrypt = 'DoDecrypt', 285 | CryptoDoDecryptReply = 'DoDecryptReply', 286 | CryptoDoDigest = 'DoDigest', 287 | CryptoDoDigestReply = 'DoDigestReply', 288 | CryptoDoSign = 'DoSign', 289 | CryptoDoSignReply = 'DoSignReply', 290 | CryptoDoVerify = 'DoVerify', 291 | CryptoDoVerifyReply = 'DoVerifyReply', 292 | 293 | // CpuProfile is a virtual event created on frontend to support 294 | // serialization of CPU Profiles within tracing timeline data. 295 | CpuProfile = 'CpuProfile', 296 | Profile = 'Profile', 297 | 298 | AsyncTask = 'AsyncTask', 299 | } 300 | 301 | export enum Category { 302 | Console = 'blink.console', 303 | UserTiming = 'blink.user_timing', 304 | LatencyInfo = 'latencyInfo', 305 | } 306 | 307 | export enum WarningType { 308 | LongTask = 'LongTask', 309 | ForcedStyle = 'ForcedStyle', 310 | ForcedLayout = 'ForcedLayout', 311 | IdleDeadlineExceeded = 'IdleDeadlineExceeded', 312 | LongHandler = 'LongHandler', 313 | LongRecurringHandler = 'LongRecurringHandler', 314 | V8Deopt = 'V8Deopt', 315 | } 316 | 317 | export enum DevToolsMetadataEvent { 318 | TracingStartedInBrowser = 'TracingStartedInBrowser', 319 | TracingStartedInPage = 'TracingStartedInPage', 320 | TracingSessionIdForWorker = 'TracingSessionIdForWorker', 321 | FrameCommittedInBrowser = 'FrameCommittedInBrowser', 322 | ProcessReadyInBrowser = 'ProcessReadyInBrowser', 323 | FrameDeletedInBrowser = 'FrameDeletedInBrowser', 324 | } 325 | 326 | export enum Thresholds { 327 | LongTask = 200, 328 | Handler = 150, 329 | RecurringHandler = 50, 330 | ForcedLayout = 30, 331 | IdleCallbackAddon = 5, 332 | } 333 | 334 | export interface MetadataEvents { 335 | page: Event[] 336 | workers: Event[] 337 | } 338 | 339 | export interface Range { 340 | from: number 341 | to: number 342 | } 343 | 344 | export interface LayoutInvalidationMap { 345 | [key: string]: Event 346 | } 347 | 348 | export interface TimeByCategory { 349 | [key: string]: number 350 | } 351 | 352 | export interface BrowserFrames { 353 | from: number 354 | to: number 355 | main: boolean 356 | url: string 357 | } 358 | 359 | export interface Summary { 360 | scripting: number 361 | rendering: number 362 | painting: number 363 | system: number 364 | idle: number 365 | } 366 | 367 | export interface FPS { 368 | fps: number 369 | } 370 | 371 | export interface PicturePromise { 372 | rect: number[], 373 | serializedPicture: string 374 | } 375 | 376 | export interface FrameById { 377 | [key: number]: TimelineFrame 378 | } 379 | 380 | export interface ThreadData { 381 | thread: Thread, 382 | time: number 383 | } 384 | 385 | export enum TimelineSelectionType { 386 | Frame, 387 | NetworkRequest, 388 | TraceEvent, 389 | Range 390 | }; 391 | 392 | export interface StatsObject { 393 | [key: string]: number 394 | } 395 | 396 | export interface TimelineRecordObject { 397 | [key: string]: TimelineRecordStyle 398 | } 399 | 400 | export interface TimelineCategoryObject { 401 | [key: string]: TimelineCategory 402 | } 403 | 404 | export enum NetworkCategory { 405 | HTML, 406 | Script, 407 | Style, 408 | Media, 409 | Other 410 | }; 411 | 412 | export interface StatsArray { 413 | [key: string]: { 414 | time: number[], 415 | value: number[] 416 | } 417 | } 418 | 419 | export interface CountersObject { 420 | [key: string]: Counter 421 | } 422 | 423 | export interface CountersValuesTimestamp { 424 | times: number[], 425 | values: number[] 426 | } 427 | 428 | export interface CountersData { 429 | [key: string]: CountersValuesTimestamp 430 | } 431 | -------------------------------------------------------------------------------- /devtools/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | /** 4 | * Return index of the leftmost element that is greater 5 | * than the specimen object. If there's no such element (i.e. all 6 | * elements are smaller or equal to the specimen) returns right bound. 7 | * The function works for sorted array. 8 | * When specified, |left| (inclusive) and |right| (exclusive) indices 9 | * define the search window. 10 | * 11 | * @param {!T} object 12 | * @param {function(!T,!S):number=} comparator 13 | * @param {number=} left 14 | * @param {number=} right 15 | * @return {number} 16 | * @this {Array.} 17 | * @template T,S 18 | */ 19 | export function upperBound( 20 | self: any[], 21 | object: T, 22 | comparator?: (object: any, arg1: any) => number, 23 | left?: number, 24 | right?: number 25 | ): any { 26 | function defaultComparator(a: T, b: S): number { 27 | return a < b ? -1 : a > b ? 1 : 0 28 | } 29 | comparator = comparator || defaultComparator 30 | let l = left || 0 31 | let r = right !== undefined ? right : self.length 32 | while (l < r) { 33 | const m = (l + r) >> 1 34 | if (comparator(object, self[m]) >= 0) { 35 | l = m + 1 36 | } else { 37 | r = m 38 | } 39 | } 40 | return r 41 | } 42 | 43 | /** 44 | * Return index of the leftmost element that is equal or greater 45 | * than the specimen object. If there's no such element (i.e. all 46 | * elements are smaller than the specimen) returns right bound. 47 | * The function works for sorted array. 48 | * When specified, |left| (inclusive) and |right| (exclusive) indices 49 | * define the search window. 50 | * 51 | * @param {!T} object 52 | * @param {function(!T,!S):number=} comparator 53 | * @param {number=} left 54 | * @param {number=} right 55 | * @return {number} 56 | * @this {Array.} 57 | * @template T,S 58 | */ 59 | export function lowerBound( 60 | self: any[], 61 | object: T, 62 | comparator?: (object: any, arg1: any) => number, 63 | left?: number, 64 | right?: number 65 | ): any { 66 | function defaultComparator(a: T, b: S): number { 67 | return a < b ? -1 : a > b ? 1 : 0 68 | } 69 | comparator = comparator || defaultComparator 70 | let l = left || 0 71 | let r = right !== undefined ? right : self.length 72 | while (l < r) { 73 | const m = (l + r) >> 1 74 | if (comparator(object, self[m]) > 0) { 75 | l = m + 1 76 | } else { 77 | r = m 78 | } 79 | } 80 | return r 81 | } 82 | 83 | /** 84 | * @param {function(?T, ?T): number=} comparator 85 | * @return {!Array.} 86 | * @this {Array.} 87 | * @template T 88 | */ 89 | export function stableSort( 90 | that: any[], 91 | comparator: (r: any, l: any) => number 92 | ): any { 93 | function defaultComparator(a: L, b: R): number { 94 | return a < b ? -1 : a > b ? 1 : 0 95 | } 96 | comparator = comparator || defaultComparator 97 | 98 | const indices = new Array(that.length) 99 | for (let i = 0; i < that.length; ++i) { 100 | indices[i] = i 101 | } 102 | 103 | const self = that 104 | 105 | /** 106 | * @param {number} a 107 | * @param {number} b 108 | * @return {number} 109 | */ 110 | function indexComparator(a: number, b: number): number { 111 | const result = comparator(self[a], self[b]) 112 | return result ? result : a - b 113 | } 114 | 115 | indices.sort(indexComparator) 116 | 117 | for (let i = 0; i < that.length; ++i) { 118 | if (indices[i] < 0 || i === indices[i]) { 119 | continue 120 | } 121 | 122 | let cyclical = i 123 | const saved = that[i] 124 | while (true) { 125 | const next = indices[cyclical] 126 | indices[cyclical] = -1 127 | if (next === i) { 128 | that[cyclical] = saved 129 | break 130 | } else { 131 | that[cyclical] = that[next] 132 | cyclical = next 133 | } 134 | } 135 | } 136 | 137 | return that 138 | } 139 | 140 | export function pushAll(self: T[], newData: T[]): T[] { 141 | for (let i = 0; i < newData.length; ++i) { 142 | self.push(newData[i]) 143 | } 144 | return newData 145 | } 146 | 147 | /** 148 | * @param {!Array.} array1 149 | * @param {!Array.} array2 150 | * @param {function(T,T):number} comparator 151 | * @param {boolean} mergeNotIntersect 152 | * @return {!Array.} 153 | * @template T 154 | */ 155 | export function mergeOrIntersect( 156 | array1: T[], 157 | array2: T[], 158 | comparator: (val1: T, val2: T) => number, 159 | mergeNotIntersect: boolean 160 | ): T[] { 161 | const result = [] 162 | let i = 0 163 | let j = 0 164 | while (i < array1.length && j < array2.length) { 165 | const compareValue = comparator(array1[i], array2[j]) 166 | if (mergeNotIntersect || !compareValue) { 167 | result.push(compareValue <= 0 ? array1[i] : array2[j]) 168 | } 169 | 170 | if (compareValue <= 0) { 171 | i++ 172 | } 173 | 174 | if (compareValue >= 0) { 175 | j++ 176 | } 177 | } 178 | 179 | if (mergeNotIntersect) { 180 | while (i < array1.length) { 181 | result.push(array1[i++]) 182 | } 183 | 184 | while (j < array2.length) { 185 | result.push(array2[j++]) 186 | } 187 | } 188 | return result 189 | } 190 | 191 | /** 192 | * @param {!number} frameDuration 193 | * @return {!number} 194 | */ 195 | export function calcFPS(frameDuration: number): number { 196 | return 1000 / frameDuration 197 | } 198 | 199 | /** 200 | * @param {!T} value 201 | * @param {function(!T,!S):number} comparator 202 | * @return {number} 203 | * @this {Array.} 204 | * @template T,S 205 | */ 206 | export function binaryIndexOf(array1: T[], value: number, comparator: (startTime: any, e: any) => number): number { 207 | const index = lowerBound(array1, value, comparator) 208 | return index < array1.length && comparator(value, array1[index]) === 0 ? index : -1 209 | } 210 | 211 | /** 212 | * @param {!T} value 213 | * @param {boolean=} firstOnly 214 | * @return {boolean} 215 | * @this {Array.} 216 | * @template T 217 | */ 218 | export function remove(array1: T[], value: any, firstOnly: boolean): boolean { 219 | let index = array1.indexOf(value) 220 | 221 | if (index === -1) { 222 | return false 223 | } 224 | 225 | if (firstOnly) { 226 | array1.splice(index, 1) 227 | return true 228 | } 229 | 230 | for (let i = index + 1, n = array1.length; i < n; ++i) { 231 | if (array1[i] !== value) { 232 | array1[index++] = array1[i] 233 | } 234 | } 235 | 236 | array1.length = index 237 | return true 238 | } 239 | 240 | /** 241 | * @param {number} num 242 | * @param {number} min 243 | * @param {number} max 244 | * @return {number} 245 | */ 246 | export function constrain(num: number, min: number, max: number): number { 247 | if (num < min) { 248 | num = min 249 | } else if (num > max) { 250 | num = max 251 | } 252 | return num 253 | }; 254 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverageFrom: ['./src/**'], 5 | coverageDirectory: './coverage/', 6 | coverageThreshold: { 7 | global: { 8 | branches: 90, 9 | functions: 90, 10 | lines: 90, 11 | statements: 90 12 | } 13 | }, 14 | testMatch: [ 15 | '**/tests/**/*.test.ts' 16 | ], 17 | moduleFileExtensions: ['js', 'ts'], 18 | transform: { '^.+\\.tsx?$': 'ts-jest' } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tracelib", 3 | "version": "1.0.1", 4 | "description": "A Node.js library that provides Chrome DevTools trace models to parse arbitrary trace logs.", 5 | "main": "./build/src/index.js", 6 | "scripts": { 7 | "build": "run-s clean compile", 8 | "clean": "rm -rf build coverage", 9 | "compile": "NODE_ENV=production tsc", 10 | "prepublishOnly": "npm prune && run-s build", 11 | "release": "npm run release:patch", 12 | "release:patch": "np patch", 13 | "release:minor": "np minor", 14 | "release:major": "np major", 15 | "test": "run-s test:*", 16 | "test:lint": "run-p test:lint:*", 17 | "test:lint:eslint": "eslint --ext ts src devtools tests", 18 | "test:lint:tsc": "tsc", 19 | "test:unit": "jest --coverage", 20 | "watch": "tsc --watch" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/saucelabs/tracelib.git" 25 | }, 26 | "keywords": [ 27 | "chrome", 28 | "devtools" 29 | ], 30 | "author": "Christian Bromann ", 31 | "license": "Apache-2.0", 32 | "bugs": { 33 | "url": "https://github.com/saucelabs/tracelib/issues" 34 | }, 35 | "homepage": "https://github.com/saucelabs/tracelib#readme", 36 | "devDependencies": { 37 | "@types/jest": "^24.0.16", 38 | "@typescript-eslint/eslint-plugin": "^1.13.0", 39 | "@typescript-eslint/parser": "^1.13.0", 40 | "eslint": "^6.1.0", 41 | "eslint-config-prettier": "^6.0.0", 42 | "eslint-plugin-prettier": "^3.1.0", 43 | "jest": "^24.8.0", 44 | "np": "^5.0.3", 45 | "npm-run-all": "^4.1.5", 46 | "prettier": "^1.18.2", 47 | "ts-jest": "^24.0.2", 48 | "typescript": "^3.5.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 4, 4 | semi: false, 5 | singleQuote: true, 6 | 'printWidth': 100 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Range, StatsObject, CountersData, CountersValuesTimestamp } from '../devtools/types' 2 | import TimelineLoader from '../devtools/loader' 3 | import { calcFPS } from '../devtools/utils' 4 | import Track, { TrackType } from '../devtools/timelineModel/track' 5 | import TimelineUIUtils from '../devtools/timelineModel/timelineUIUtils' 6 | import PerformanceModel from '../devtools/timelineModel/performanceModel' 7 | import TimelineData from '../devtools/timelineModel/timelineData' 8 | import Event from '../devtools/tracingModel/event' 9 | import CountersGraph from '../devtools/timelineModel/counterGraph' 10 | import CustomUtils from './utils' 11 | 12 | export default class Tracelib { 13 | public tracelog: object 14 | private _timelineLoader: TimelineLoader 15 | private _performanceModel: PerformanceModel 16 | 17 | public constructor (tracelog: object, range?: Range) { 18 | this.tracelog = tracelog 19 | this._timelineLoader = new TimelineLoader(this.tracelog) 20 | this._timelineLoader.init() 21 | this._performanceModel = this._timelineLoader.performanceModel 22 | } 23 | 24 | private _findMainTrack(): Track { 25 | const threads: Track[] = this._performanceModel 26 | .timelineModel() 27 | .tracks() 28 | 29 | const mainTrack = threads.find((track: Track): boolean => Boolean( 30 | track.type === TrackType.MainThread && track.forMainFrame && track.events.length 31 | )) 32 | 33 | /** 34 | * If no main thread could be found, pick the thread with most events 35 | * captured in it and assume this is the main track. 36 | */ 37 | if (!mainTrack) { 38 | return threads.slice(1).reduce( 39 | (curr: Track, com: Track): Track => curr.events.length > com.events.length ? curr : com, 40 | threads[0]) 41 | } 42 | 43 | return mainTrack 44 | } 45 | 46 | public getMainTrackEvents(): Event[] { 47 | const mainTrack = this._findMainTrack() 48 | return mainTrack.events 49 | } 50 | 51 | public getFPS(): CountersValuesTimestamp { 52 | const fpsData: CountersValuesTimestamp = { 53 | times: [], 54 | values: [] 55 | } 56 | this._timelineLoader.performanceModel.frames().forEach(({ duration, startTime }): void => { 57 | fpsData.values.push(calcFPS(duration)) 58 | fpsData.times.push(startTime) 59 | }) 60 | return fpsData 61 | } 62 | 63 | public getSummary(from?: number, to?: number): StatsObject { 64 | const timelineUtils = new TimelineUIUtils() 65 | const startTime = from || this._performanceModel.timelineModel().minimumRecordTime() 66 | const endTime = to || this._performanceModel.timelineModel().maximumRecordTime() 67 | const mainTrack = this._findMainTrack() 68 | 69 | // We are facing data mutaion issue in devtools, to avoid it cloning syncEvents 70 | const syncEvents = mainTrack.syncEvents().slice() 71 | 72 | return { 73 | ...timelineUtils.statsForTimeRange( 74 | syncEvents, startTime, endTime 75 | ), 76 | startTime, 77 | endTime, 78 | } 79 | } 80 | 81 | public getWarningCounts(): StatsObject { 82 | const mainTrack = this._findMainTrack() 83 | return mainTrack.events.reduce((counter: StatsObject, event: Event): StatsObject => { 84 | const timelineData = TimelineData.forEvent(event) 85 | const warning = timelineData.warning 86 | if (warning) { 87 | counter[warning] = counter[warning] ? counter[warning] + 1 : 1 88 | } 89 | return counter 90 | }, {}) 91 | } 92 | 93 | public getMemoryCounters(): CountersData { 94 | const counterGraph = new CountersGraph() 95 | const counters = counterGraph.setModel(this._performanceModel, this._findMainTrack()) 96 | return Object.keys(counters).reduce((acc, counter): CountersData => ({ 97 | ...acc, 98 | [counter]: { 99 | times: counters[counter].times, 100 | values: counters[counter].values, 101 | } 102 | }), {}) 103 | } 104 | 105 | public getDetailStats(from?: number, to?: number): CountersData { 106 | const timelineUtils = new CustomUtils() 107 | const startTime = from || this._performanceModel.timelineModel().minimumRecordTime() 108 | const endTime = to || this._performanceModel.timelineModel().maximumRecordTime() 109 | const mainTrack = this._findMainTrack() 110 | 111 | // We are facing data mutaion issue in devtools, to avoid it cloning syncEvents 112 | const syncEvents = mainTrack.syncEvents().slice() 113 | 114 | return { 115 | ...timelineUtils.detailStatsForTimeRange( 116 | syncEvents, startTime, endTime 117 | ), 118 | range: { 119 | times: [startTime, endTime], 120 | values: [startTime, endTime] 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import TimelineUIUtils from './../devtools/timelineModel/timelineUIUtils' 2 | import { CountersData } from './../devtools/types' 3 | import TimelineModel from './../devtools/timelineModel' 4 | import Event from './../devtools/tracingModel/event' 5 | import TracingModel from './../devtools/tracingModel' 6 | 7 | export default class CustomUtils extends TimelineUIUtils { 8 | /** 9 | * @param {!Array} events 10 | * @param {number} startTime 11 | * @param {number} endTime 12 | * @return {!Object} 13 | */ 14 | public detailStatsForTimeRange(events: Event[], startTime: number, endTime: number): CountersData { 15 | const eventStyle = this.eventStyle.bind(this) 16 | const visibleEventsFilterFunc = this.visibleEventsFilter.bind(this) 17 | 18 | if (!events.length) { 19 | return { 20 | idle: { 21 | 'times': [endTime - startTime], 22 | 'values': [endTime - startTime] 23 | } 24 | } 25 | } 26 | 27 | // aggeregatedStats is a map by categories. For each category there's an array 28 | // containing sorted time points which records accumulated value of the category. 29 | const aggregatedStats: CountersData = {} 30 | const categoryStack: string[] = [] 31 | let lastTime = 0 32 | TimelineModel.forEachEvent( 33 | events, 34 | onStartEvent, 35 | onEndEvent, 36 | undefined, 37 | undefined, 38 | undefined, 39 | filterForStats() 40 | ) 41 | 42 | /** 43 | * @return {function(!SDK.TracingModel.Event):boolean} 44 | */ 45 | function filterForStats(): any { 46 | const visibleEventsFilter = visibleEventsFilterFunc() 47 | return (event: Event): any => visibleEventsFilter.accept(event) || TracingModel.isTopLevelEvent(event) 48 | } 49 | 50 | /** 51 | * @param {string} category 52 | * @param {number} time 53 | */ 54 | function updateCategory(category: string, time: number): void { 55 | let statsArrays = aggregatedStats[category] 56 | if (!statsArrays) { 57 | statsArrays = { times: [], values: [] } 58 | aggregatedStats[category] = statsArrays 59 | } 60 | if (statsArrays.times.length && statsArrays.times[statsArrays.times.length - 1] === time) { 61 | return 62 | } 63 | statsArrays.values.push(time - lastTime) 64 | statsArrays.times.push(time) 65 | } 66 | 67 | /** 68 | * @param {?string} from 69 | * @param {?string} to 70 | * @param {number} time 71 | */ 72 | function categoryChange(from?: string, to?: string, time?: number): void { 73 | if (from) { 74 | updateCategory(from, time) 75 | } 76 | 77 | lastTime = time 78 | 79 | if (to) { 80 | updateCategory(to, time) 81 | } 82 | } 83 | 84 | /** 85 | * @param {!SDK.TracingModel.Event} e 86 | */ 87 | function onStartEvent(e: Event): void { 88 | const category = eventStyle(e).category.name 89 | const parentCategory = categoryStack.length ? categoryStack[categoryStack.length - 1] : null 90 | if (category !== parentCategory) { 91 | categoryChange(parentCategory, category, e.startTime) 92 | } 93 | categoryStack.push(category) 94 | } 95 | 96 | /** 97 | * @param {!SDK.TracingModel.Event} e 98 | */ 99 | function onEndEvent(e: Event): void { 100 | const category = categoryStack.pop() 101 | const parentCategory = categoryStack.length ? categoryStack[categoryStack.length - 1] : null 102 | if (category !== parentCategory) { 103 | categoryChange(category, parentCategory, e.endTime) 104 | } 105 | } 106 | return aggregatedStats 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | '@typescript-eslint/explicit-function-return-type': 0 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`getDetailStats should get detail stats 1`] = ` 4 | Array [ 5 | "rendering", 6 | "painting", 7 | "other", 8 | "scripting", 9 | "range", 10 | ] 11 | `; 12 | 13 | exports[`getDetailStats should get summary data between passed range 1`] = ` 14 | Array [ 15 | "rendering", 16 | "painting", 17 | "other", 18 | "scripting", 19 | "range", 20 | ] 21 | `; 22 | 23 | exports[`getDetailStats should not throw error on second call of getDetailStats 1`] = ` 24 | Array [ 25 | "rendering", 26 | "painting", 27 | "other", 28 | "scripting", 29 | "range", 30 | ] 31 | `; 32 | 33 | exports[`getSummary should get summary data 1`] = ` 34 | Object { 35 | "endTime": 289961229.717, 36 | "idle": 52.38300037384033, 37 | "other": 9.896000564098358, 38 | "painting": 69.94999980926514, 39 | "rendering": 847.373997092247, 40 | "scripting": 394.4800021648407, 41 | "startTime": 289959855.634, 42 | } 43 | `; 44 | 45 | exports[`getSummary should get summary data between passed range 1`] = ` 46 | Object { 47 | "endTime": 289960729.717, 48 | "idle": 0.6339998841285706, 49 | "other": 4.653000295162201, 50 | "painting": 34.8999999165535, 51 | "rendering": 425.89399832487106, 52 | "scripting": 208.0020015835762, 53 | "startTime": 289960055.634, 54 | } 55 | `; 56 | 57 | exports[`getSummary should not throw error on second call of getSummary 1`] = ` 58 | Object { 59 | "endTime": 289961229.717, 60 | "idle": 52.38300037384033, 61 | "other": 9.896000564098358, 62 | "painting": 69.94999980926514, 63 | "rendering": 847.373997092247, 64 | "scripting": 394.4800021648407, 65 | "startTime": 289959855.634, 66 | } 67 | `; 68 | 69 | exports[`getWarningCounts should get warning counts 1`] = ` 70 | Object { 71 | "ForcedLayout": 4683, 72 | "ForcedStyle": 4684, 73 | "LongRecurringHandler": 13, 74 | } 75 | `; 76 | 77 | exports[`should get FPS 1`] = ` 78 | Object { 79 | "times": Array [ 80 | 289959949.734, 81 | 289959955.22, 82 | 289960052.234, 83 | 289960142.388, 84 | 289960233.175, 85 | 289960324.01, 86 | 289960416.646, 87 | 289960508.145, 88 | 289960600.602, 89 | 289960695.329, 90 | 289960790.75, 91 | 289960882.274, 92 | 289960977.634, 93 | 289961069.395, 94 | ], 95 | "values": Array [ 96 | 182.2821727559685, 97 | 10.307790628308753, 98 | 11.092131244032895, 99 | 11.014792866762287, 100 | 11.00897231503525, 101 | 10.794939328106791, 102 | 10.929081197725838, 103 | 10.815838712204958, 104 | 10.556652274643293, 105 | 10.47987340271033, 106 | 10.926095888726774, 107 | 10.486577179634944, 108 | 10.897876006628481, 109 | 10.839990888916617, 110 | ], 111 | } 112 | `; 113 | 114 | exports[`should get memory counters 1`] = ` 115 | Object { 116 | "documents": Object { 117 | "times": Array [ 118 | 289959946.271, 119 | ], 120 | "values": Array [ 121 | 5, 122 | ], 123 | }, 124 | "gpuMemoryUsedKB": Object { 125 | "times": Array [], 126 | "values": Array [], 127 | }, 128 | "jsEventListeners": Object { 129 | "times": Array [ 130 | 289959946.271, 131 | ], 132 | "values": Array [ 133 | 38, 134 | ], 135 | }, 136 | "jsHeapSizeUsed": Object { 137 | "times": Array [ 138 | 289959946.271, 139 | 289959983.488, 140 | 289960044.53, 141 | 289960134.836, 142 | 289960210.797, 143 | 289960225.218, 144 | 289960316.444, 145 | 289960408.738, 146 | 289960459.387, 147 | 289960499.987, 148 | 289960592.295, 149 | 289960687.485, 150 | 289960694.396, 151 | 289960713.344, 152 | 289960782.934, 153 | 289960874.227, 154 | 289960965.767, 155 | 289960969.893, 156 | 289961061.729, 157 | 289961153.41, 158 | 289961219.984, 159 | ], 160 | "values": Array [ 161 | 22549008, 162 | 20896736, 163 | 21471160, 164 | 22241560, 165 | 20976176, 166 | 21113456, 167 | 21899112, 168 | 22662104, 169 | 21028744, 170 | 21407840, 171 | 22185296, 172 | 22947872, 173 | 22949192, 174 | 21045640, 175 | 21666816, 176 | 22435800, 177 | 21096152, 178 | 21133192, 179 | 21922608, 180 | 22685512, 181 | 21179632, 182 | ], 183 | }, 184 | "nodes": Object { 185 | "times": Array [ 186 | 289959946.271, 187 | ], 188 | "values": Array [ 189 | 837, 190 | ], 191 | }, 192 | } 193 | `; 194 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import Tracelib from '../src/index' 2 | import JANK_TRACE_LOG from './__fixtures__/jankTraceLog.json' 3 | 4 | let trace: Tracelib 5 | beforeAll(() => { 6 | trace = new Tracelib(JANK_TRACE_LOG) 7 | }) 8 | 9 | test('should contain traceLog', () => { 10 | const sampleTrace = new Tracelib({ foo: 'bar' }) 11 | expect(sampleTrace.tracelog).toEqual({ foo: 'bar' }) 12 | }) 13 | 14 | test('should get FPS', () => { 15 | const result = trace.getFPS() 16 | expect(result).toMatchSnapshot() 17 | }) 18 | 19 | describe('getSummary', () => { 20 | it('should get summary data', () => { 21 | const result = trace.getSummary() 22 | expect(result).toMatchSnapshot() 23 | }) 24 | 25 | it('should not throw error on second call of getSummary', () => { 26 | const result = trace.getSummary() 27 | expect(result).toMatchSnapshot() 28 | }) 29 | 30 | it('should get summary data between passed range', () => { 31 | const result = trace.getSummary(289960055.634, 289960729.717) 32 | expect(result).toMatchSnapshot() 33 | }) 34 | }) 35 | 36 | describe('getWarningCounts', () => { 37 | it('should get warning counts', () => { 38 | const result = trace.getWarningCounts() 39 | expect(result).toMatchSnapshot() 40 | }) 41 | }) 42 | 43 | test('should get memory counters', () => { 44 | const result = trace.getMemoryCounters() 45 | expect(result).toMatchSnapshot() 46 | }) 47 | 48 | describe('mainTrackEvents', () => { 49 | it('should get events', () => { 50 | const result = trace.getMainTrackEvents() 51 | expect(result.length).toEqual(56244) 52 | }) 53 | 54 | it('should throws error if main track is missing', () => { 55 | /** 56 | * use tracelog with CrRenderer thread name metadata missing 57 | */ 58 | const borkedTrace = JANK_TRACE_LOG.slice(0, -1) 59 | 60 | const tracelib = new Tracelib(borkedTrace) 61 | const result = tracelib.getMainTrackEvents() 62 | expect(result.length).toEqual(56244) 63 | }) 64 | }) 65 | 66 | describe('getDetailStats', () => { 67 | it('should get detail stats', () => { 68 | const result = trace.getDetailStats() 69 | expect(Object.keys(result)).toMatchSnapshot() 70 | }) 71 | 72 | it('should not throw error on second call of getDetailStats', () => { 73 | const result = trace.getDetailStats() 74 | expect(Object.keys(result)).toMatchSnapshot() 75 | }) 76 | 77 | it('should get summary data between passed range', () => { 78 | const result = trace.getDetailStats(289960055.634, 289960729.717) 79 | expect(Object.keys(result)).toMatchSnapshot() 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "ES2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./build/", /* Redirect output structure to the directory. */ 16 | "rootDirs": ["./src", "./devtools"], /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | "strictNullChecks": false, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | "resolveJsonModule": true 64 | }, 65 | "exclude": [ 66 | "node_modules", 67 | "build", 68 | "tests" 69 | ] 70 | } 71 | --------------------------------------------------------------------------------