├── .drone.yml ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples ├── profiles │ ├── heavy.cpuprofile │ └── simple.heapprofile ├── simple.js ├── simple.ts └── tsconfig.json ├── lerna.json ├── package.json ├── packages ├── openprofiling-core │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── common │ │ │ ├── console-logger.ts │ │ │ └── types.ts │ │ ├── exporters │ │ │ ├── base-exporter.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── agent.ts │ │ │ ├── config.ts │ │ │ ├── profile.ts │ │ │ └── types.ts │ │ ├── profilers │ │ │ ├── base-profiler.ts │ │ │ └── types.ts │ │ ├── triggers │ │ │ ├── base-trigger.ts │ │ │ └── types.ts │ │ └── utils │ │ │ ├── clock.ts │ │ │ └── version.ts │ ├── test │ │ ├── base-exporter.spec.ts │ │ ├── core-agent.spec.ts │ │ └── mocks │ │ │ ├── dummyExporter.ts │ │ │ ├── dummyProfiler.ts │ │ │ └── dummyTrigger.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock ├── openprofiling-exporter-file │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── file-exporter.ts │ │ └── index.ts │ ├── test │ │ └── exporter-file.spec.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock ├── openprofiling-exporter-gcs │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── gcloud-storage-exporter.ts │ │ └── index.ts │ ├── test │ │ └── gcloud-storage-exporter.spec.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock ├── openprofiling-exporter-s3 │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── s3-exporter.ts │ ├── test │ │ └── s3-exporter.spec.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock ├── openprofiling-gateway-ws-client │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── gateway-client.ts │ │ └── index.ts │ ├── test │ │ └── gateway-client.spec.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock ├── openprofiling-gateway-ws │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ ├── runner.ts │ │ └── types.ts │ ├── test │ │ └── gateway.spec.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock ├── openprofiling-inspector-cpu-profiler │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── inspector-cpu-profiler.ts │ ├── test │ │ └── inspector-cpu.spec.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock ├── openprofiling-inspector-heap-profiler │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── inspector-heap-profiler.ts │ ├── test │ │ └── inspector-heap.spec.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock ├── openprofiling-inspector-trace-events │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── inspector-trace-events-profiler.ts │ ├── test │ │ └── inspector-trace-events.spec.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock ├── openprofiling-nodejs │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── node-agent.ts │ ├── test │ │ └── agent.spec.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock ├── openprofiling-trigger-http │ ├── package.json │ ├── src │ │ ├── http-trigger.ts │ │ └── index.ts │ ├── test │ │ └── http-trigger.spec.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock └── openprofiling-trigger-signal │ ├── README.md │ ├── package.json │ ├── src │ ├── index.ts │ └── signal-trigger.ts │ ├── test │ └── signal-trigger.spec.ts │ ├── tsconfig.json │ ├── tslint.json │ └── yarn.lock └── yarn.lock /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: node10 3 | 4 | steps: 5 | - name: build 6 | image: node:10 7 | commands: 8 | - node -v 9 | - yarn -v 10 | - uname -r 11 | - yarn install 12 | - export PATH=$PATH:./node_modules/.bin/ 13 | - lerna bootstrap 14 | - lerna run build 15 | - lerna run test 16 | environment: 17 | NODE_ENV: test 18 | S3_HOST: minio 19 | GCS_HOST: gcs:4443 20 | when: 21 | event: 22 | - push 23 | services: 24 | - name: minio 25 | image: minio/minio 26 | environment: 27 | MINIO_ACCESS_KEY: accessKey 28 | MINIO_SECRET_KEY: secretKey 29 | entrypoint: [ 'minio', 'server', '/data' ] 30 | - name: gcs 31 | image: fsouza/fake-gcs-server 32 | 33 | --- 34 | 35 | kind: pipeline 36 | name: node12 37 | 38 | steps: 39 | - name: build 40 | image: node:12 41 | commands: 42 | - node -v 43 | - yarn -v 44 | - uname -r 45 | - yarn install 46 | - export PATH=$PATH:./node_modules/.bin/ 47 | - lerna bootstrap 48 | - lerna run build 49 | - lerna run ci 50 | environment: 51 | NODE_ENV: test 52 | S3_HOST: minio 53 | GCS_HOST: gcs:4443 54 | CODECOV_TOKEN: 55 | from_secret: coverage_token 56 | when: 57 | event: 58 | - push 59 | services: 60 | - name: minio 61 | image: minio/minio 62 | environment: 63 | MINIO_ACCESS_KEY: accessKey 64 | MINIO_SECRET_KEY: secretKey 65 | entrypoint: [ 'minio', 'server', '/data' ] 66 | - name: gcs 67 | image: fsouza/fake-gcs-server 68 | 69 | --- 70 | 71 | kind: pipeline 72 | name: node14 73 | 74 | steps: 75 | - name: build 76 | image: node:14 77 | commands: 78 | - node -v 79 | - yarn -v 80 | - uname -r 81 | - yarn install 82 | - export PATH=$PATH:./node_modules/.bin/ 83 | - lerna bootstrap 84 | - lerna run build 85 | - lerna run test 86 | environment: 87 | NODE_ENV: test 88 | S3_HOST: minio 89 | GCS_HOST: gcs:4443 90 | when: 91 | event: 92 | - push 93 | services: 94 | - name: minio 95 | image: minio/minio 96 | environment: 97 | MINIO_ACCESS_KEY: accessKey 98 | MINIO_SECRET_KEY: secretKey 99 | entrypoint: [ 'minio', 'server', '/data' ] 100 | - name: gcs 101 | image: fsouza/fake-gcs-server 102 | 103 | --- 104 | 105 | kind: pipeline 106 | name: linter 107 | 108 | steps: 109 | - name: build 110 | image: node:12 111 | commands: 112 | - node -v 113 | - yarn -v 114 | - uname -r 115 | - yarn install 116 | - export PATH=$PATH:./node_modules/.bin/ 117 | - lerna bootstrap 118 | - lerna run lint 119 | when: 120 | event: 121 | - push 122 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | Please answer these questions before submitting a bug report. 8 | 9 | ### What version of OpenProfiling are you using? 10 | 11 | 12 | ### What version of Node are you using? 13 | 14 | 15 | ### What did you do? 16 | If possible, provide a recipe for reproducing the error. 17 | 18 | 19 | ### What did you expect to see? 20 | 21 | 22 | ### What did you see instead? 23 | 24 | 25 | ### Additional context 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | 8 | **Is your feature request related to a problem? Please describe.** 9 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 10 | 11 | **Describe the solution you'd like** 12 | A clear and concise description of what you want to happen. 13 | 14 | **Describe alternatives you've considered** 15 | A clear and concise description of any alternative solutions or features you've considered. 16 | 17 | **Additional context** 18 | Add any other context or screenshots about the feature request here. 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ IDEA 2 | .idea 3 | *.iml 4 | 5 | # Eclipse 6 | .classpath 7 | .project 8 | .settings 9 | bin 10 | 11 | # OS X 12 | .DS_Store 13 | 14 | # Emacs 15 | *~ 16 | \#*\# 17 | 18 | # Vim 19 | .swp 20 | 21 | #VScode 22 | .vscode/ 23 | 24 | # nodejs 25 | node_modules/ 26 | !packages/opencensus-nodejs/test/instrumentation/node_modules 27 | npm-debug.log 28 | .nyc_output/ 29 | build/ 30 | 31 | #backup files 32 | *-- 33 | *_backup 34 | 35 | #log files 36 | *.log 37 | 38 | #istanbul files 39 | coverage/ 40 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /coverage 3 | /doc 4 | /test 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | The openprofiling-node is written in TypeScript. 4 | 5 | The command `npm test` tests code the same way that our CI will test it. 6 | This is a convenience command for a number of steps, which can run separately if needed: 7 | 8 | - `npm run compile` compiles the code, checking for type errors. 9 | - `npm run bootstrap` Bootstrap the packages in the current Lerna repo. Installs all of their dependencies and links any cross-dependencies. 10 | 11 | # How to become a contributor and submit your own code 12 | 13 | 1. Submit an issue describing your proposed change to the repo in question. 14 | 1. The repo owner will respond to your issue promptly. 15 | 1. Fork the desired repo, develop and test your code changes. 16 | 1. Submit a pull request. 17 | 1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 Valentin 'vmarchaud' Marchaud 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 | 7 | [![Version](https://img.shields.io/npm/v/@openprofiling/core.svg)](https://img.shields.io/npm/v/@openprofiling/core.svg) 8 | [![Build Status](https://cloud.drone.io/api/badges/vmarchaud/openprofiling-node/status.svg)](https://cloud.drone.io/vmarchaud/openprofiling-node) 9 | [![codecov](https://codecov.io/gh/vmarchaud/openprofiling-node/branch/master/graph/badge.svg)](https://codecov.io/gh/vmarchaud/openprofiling-node) 10 | [![License](https://img.shields.io/npm/l/@opencensus/core.svg)](https://img.shields.io/npm/l/@opencensus/core.svg) 11 | 12 | **NOTE: This project is deprecated, OpenTelemetry [is discusting adding support for profiling](https://github.com/open-telemetry/oteps/issues/139)** 13 | 14 | OpenProfiling is a toolkit for collecting profiling data from production workload safely. 15 | 16 | The project's goal is to empower developers to understand how their applications is behaving in production with minimal performance impact and without vendor lock-in. 17 | 18 | The library is in alpha stage and the API is subject to change. 19 | 20 | I expect that the library will not match everyone use-cases, so I'm asking everyone in this case to open an issue so we can discuss how the toolkit could meet yours. 21 | 22 | The NodeJS implementation is currently tested with all recent NodeJS LTS (10, 12) and the most recent major (14). 23 | 24 | ## Use cases 25 | 26 | ### An application have a memory leak 27 | 28 | The recommended profiler is the [Heap Sampling Profiler](https://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-inspector-heap-profiler) which has the lowest impact in terms of performance, [here are the instructions on how to use it](https://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-inspector-heap-profiler#how-to-use). 29 | After getting the exported file, you can go to [speedscope](https://www.speedscope.app/) to analyse it. 30 | [If we load an example heap profile](https://www.speedscope.app/#profileURL=https%3A%2F%2Frawcdn.githack.com%2Fvmarchaud%2Fopenprofiling-node%2F475c1f31e5635cd9230c9296549dfbf9765a7464%2Fexamples%2Fprofiles%2Fsimple.heapprofile) and head to the `Sandwich` panel, we can see a list of functions sorted by how much memory they allocated. 31 | 32 | At the left of the table, you have two entry: 33 | - `self` memory: is how much the function allocated in the memory to run without counting any function that it may have called. 34 | - `total` memory: the opposite of `self` which means that it count the memory it allocated plus all the memory allocated by the functions it called. 35 | 36 | Note that the top function in the view should not be automatically considered as a leak: for example, when you receive a HTTP request, NodeJS allocates some memory for it but it will be freed after the request finishes. The view will only show where memory is allocated, not where it leaks. 37 | 38 | We highly recommend to [read the documentation](ttps://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-inspector-heap-profiler) about the profiler to understand all the pros and cons of using it. 39 | 40 | ### An application is using too much CPU 41 | 42 | The recommended profiler is the [CPU JS Sampling Profiler](https://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-inspector-cpu-profiler) which is made for production profiling (low overhead), [check the instructions to get it running](https://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-inspector-cpu-profiler#how-to-use). 43 | After getting the exported file, you can go to [speedscope](https://www.speedscope.app/) to analyze it. 44 | [If we load an example CPU profile](https://www.speedscope.app/#profileURL=https%3A%2F%2Frawcdn.githack.com%2Fvmarchaud%2Fopenprofiling-node%2F475c1f31e5635cd9230c9296549dfbf9765a7464%2Fexamples%2Fprofiles%2Fheavy.cpuprofile) and head to the `Sandwich` panel again, we can see a list of functions sorted by how much CPU they used. 45 | 46 | As the heap profiler that there is two concepts to read the table: 47 | - `self` time: is the time the CPU took in the function **itself**, without considering calling other functions. 48 | - `total` time: the opposite of `self`, it represent **both the time used by the function and all functions that it called**. 49 | 50 | You should then look for functions that have a high `self` time, which means that their inner code take a lot of time to execute. 51 | 52 | We highly recommend to [read the documentation](ttps://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-inspector-cpu-profiler) about the profiler to understand all the pros and cons of using it. 53 | 54 | 55 | ## Installation 56 | 57 | Install OpenProfiling for NodeJS with: 58 | 59 | ```bash 60 | yarn add @openprofiling/nodejs 61 | ``` 62 | 63 | or 64 | 65 | ```bash 66 | npm install @openprofiling/nodejs 67 | ``` 68 | 69 | ## Configure 70 | 71 | Before running your application with `@openprofiling/nodejs`, you will need to choose 3 different things: 72 | - What do you want to profile: a `profiler` 73 | - How to start this profiler: a `trigger` 74 | - Where to send the profiling data: an `exporter` 75 | 76 | ### Typescript Example 77 | 78 | ```ts 79 | import { ProfilingAgent } from '@openprofiling/nodejs' 80 | import { FileExporter } from '@openprofiling/exporter-file' 81 | import { InspectorHeapProfiler } from '@openprofiling/inspector-heap-profiler' 82 | import { InspectorCPUProfiler } from '@openprofiling/inspector-cpu-profiler' 83 | import { SignalTrigger } from '@openprofiling/trigger-signal' 84 | 85 | const profilingAgent = new ProfilingAgent() 86 | /** 87 | * Register a profiler for a specific trigger 88 | * ex: we want to collect cpu profile when the application receive a SIGUSR2 signal 89 | */ 90 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR2' }), new InspectorCPUProfiler({})) 91 | /** 92 | * Start the agent (which will tell the trigger to start listening) and 93 | * configure where to output the profiling data 94 | * ex: the file exporter will output on the disk, by default in /tmp 95 | */ 96 | profilingAgent.start({ exporter: new FileExporter() }) 97 | ``` 98 | 99 | ### JavaScript Example 100 | 101 | ```js 102 | const { ProfilingAgent } = require('@openprofiling/nodejs') 103 | const { FileExporter } = require('@openprofiling/exporter-file') 104 | const { InspectorCPUProfiler } = require('@openprofiling/inspector-cpu-profiler') 105 | const { SignalTrigger } = require('@openprofiling/trigger-signal') 106 | 107 | const profilingAgent = new ProfilingAgent() 108 | /** 109 | * Register a profiler for a specific trigger 110 | * ex: we want to collect cpu profile when the application receive a SIGUSR2 signal 111 | */ 112 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR1' }), new InspectorCPUProfiler({})) 113 | /** 114 | * Start the agent (which will tell the trigger to start listening) and 115 | * configure where to output the profiling data 116 | * ex: the file exporter will output on the disk, by default in /tmp 117 | */ 118 | profilingAgent.start({ exporter: new FileExporter(), logLevel: 4 }) 119 | ``` 120 | 121 | ## Triggers 122 | 123 | A trigger is simply a way to start collecting data, you can choose between those: 124 | 125 | - [Signal](https://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-trigger-signal) 126 | - [HTTP](https://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-trigger-http) 127 | 128 | ## Profilers 129 | 130 | Profilers are the implementation that collect profiling data from different sources, current available profilers: 131 | 132 | - [CPU Sampling JS Profiler](https://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-inspector-cpu-profiler) 133 | - [Heap Sampling Profiler](https://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-inspector-heap-profiler) 134 | - [Trace Events](https://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-inspector-trace-events) 135 | 136 | ## Exporters 137 | 138 | OpenProfiling aims to be vendor-neutral and can push profiling data to any backend with different exporter implementations. Currently, it supports: 139 | 140 | - [File exporter](https://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-exporter-file) 141 | - [S3 exporter](https://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-exporter-s3) 142 | - [Google Cloud Storage exporter](https://github.com/vmarchaud/openprofiling-node/tree/master/packages/openprofiling-exporter-gcs) 143 | 144 | ## Versioning 145 | 146 | This library follows [Semantic Versioning](http://semver.org/). 147 | 148 | Note that before the 1.0.0 release, any minor update can have breaking changes. 149 | 150 | ## LICENSE 151 | 152 | Apache License 2.0 153 | 154 | [npm-url]: https://www.npmjs.com/package/@openprofiling/core.svg 155 | [linter-img]: https://img.shields.io/badge/linter-ts--standard-brightgreen.svg 156 | [node-img]: https://img.shields.io/node/v/@openprofiling/core.svg 157 | [license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat 158 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | 2 | const { ProfilingAgent } = require('../packages/openprofiling-nodejs') 3 | const { FileExporter } = require('../packages/openprofiling-exporter-file') 4 | const { InspectorHeapProfiler } = require('../packages/openprofiling-inspector-heap-profiler') 5 | const { InspectorCPUProfiler } = require('../packages/openprofiling-inspector-cpu-profiler') 6 | const { SignalTrigger } = require('../packages/openprofiling-trigger-signal') 7 | const inspector = require('inspector') 8 | 9 | const profilingAgent = new ProfilingAgent() 10 | const session = new inspector.Session() 11 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR2' }), new InspectorHeapProfiler({ session })) 12 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR1' }), new InspectorCPUProfiler({ session })) 13 | profilingAgent.start({ exporter: new FileExporter(), logLevel: 4 }) 14 | 15 | setInterval(_ => { 16 | console.log(process.pid) 17 | }, 1000) 18 | -------------------------------------------------------------------------------- /examples/simple.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ProfilingAgent } from '../packages/openprofiling-nodejs' 3 | import { FileExporter } from '../packages/openprofiling-exporter-file' 4 | import { InspectorHeapProfiler } from '../packages/openprofiling-inspector-heap-profiler' 5 | import { InspectorCPUProfiler } from '../packages/openprofiling-inspector-cpu-profiler' 6 | import { SignalTrigger } from '../packages/openprofiling-trigger-signal' 7 | import * as inspector from 'inspector' 8 | 9 | const profilingAgent = new ProfilingAgent() 10 | const session = new inspector.Session() 11 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR2' }), new InspectorHeapProfiler({ session })) 12 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR1' }), new InspectorCPUProfiler({ session })) 13 | profilingAgent.start({ exporter: new FileExporter(), logLevel: 4 }) 14 | 15 | setInterval(_ => { 16 | console.log(process.pid) 17 | }, 1000) 18 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "inlineSourceMap": true, 8 | "strictNullChecks": true, 9 | "resolveJsonModule": true 10 | }, 11 | "compileOnSave": false 12 | } -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.11.0", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "0.2.2", 7 | "npmClient": "yarn" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openprofiling/base", 3 | "version": "0.0.1", 4 | "description": "OpenProfiling is a toolkit for collecting profiling data from production workload safely..", 5 | "main": "build/src/index.js", 6 | "types": "build/src/index.d.ts", 7 | "repository": "openprofiling/openprofiling-node", 8 | "keywords": [ 9 | "openprofiling", 10 | "nodejs", 11 | "profiling" 12 | ], 13 | "author": "Valentin Marchaud", 14 | "license": "Apache-2.0", 15 | "engines": { 16 | "node": ">=8.0" 17 | }, 18 | "devDependencies": { 19 | "@types/mocha": "^5.2.6", 20 | "@types/node": "^12.0.0", 21 | "lerna": "2.11.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/openprofiling-core/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /packages/openprofiling-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openprofiling/core", 3 | "version": "0.2.2", 4 | "main": "build/index.js", 5 | "types": "build/index.d.ts", 6 | "repository": "openprofiling/openprofiling-node", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.json", 9 | "lint": "tslint --project . src/**/*.ts", 10 | "test": "mocha -r ts-node/register ./test/*.spec.ts", 11 | "test-coverage": "nyc mocha -r ts-node/register test/*.spec.ts", 12 | "report-coverage": "nyc report --reporter=json && codecov -f coverage/*.json -p ../..", 13 | "ci": "yarn test-coverage && yarn report-coverage", 14 | "prepublishOnly": "yarn build" 15 | }, 16 | "keywords": [ 17 | "openprofiling", 18 | "nodejs", 19 | "debugging", 20 | "profiling" 21 | ], 22 | "author": "Valentin Marchaud", 23 | "license": "Apache-2.0", 24 | "engines": { 25 | "node": ">=6.0" 26 | }, 27 | "files": [ 28 | "build/", 29 | "doc", 30 | "CHANGELOG.md", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "extension": [ 39 | ".ts" 40 | ], 41 | "exclude": [ 42 | "build/", 43 | "config/", 44 | "examples/", 45 | "test/" 46 | ], 47 | "cache": true, 48 | "all": true 49 | }, 50 | "devDependencies": { 51 | "codecov": "^3.4.0", 52 | "mocha": "^6.1.4", 53 | "nyc": "^14.1.1", 54 | "ts-node": "^8.2.0", 55 | "tslint": "^5.17.0", 56 | "tslint-config-standard": "^8.0.1", 57 | "typescript": "^3.7.4" 58 | }, 59 | "gitHead": "a8098997633d0ffefed78ec7b4c12df64e47f3d5" 60 | } 61 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/common/console-logger.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as util from 'util' 3 | import * as types from './types' 4 | 5 | /** 6 | * This class implements a console logger. 7 | */ 8 | export class ConsoleLogger implements types.Logger { 9 | 10 | static LEVELS = ['silent', 'error', 'warn', 'info', 'debug'] 11 | public level: string 12 | private namespace: string = 'core' 13 | 14 | /** 15 | * Constructs a new ConsoleLogger instance 16 | * @param options A logger configuration object. 17 | */ 18 | constructor (options?: types.LoggerOptions | string | number) { 19 | let opt: types.LoggerOptions = {} 20 | if (typeof options === 'number') { 21 | if (options < 0) { 22 | options = 0 23 | } else if (options > ConsoleLogger.LEVELS.length) { 24 | options = ConsoleLogger.LEVELS.length - 1 25 | } 26 | opt = { level: ConsoleLogger.LEVELS[options] } 27 | } else if (typeof options === 'string') { 28 | opt = { level: options } 29 | } else { 30 | opt = options || {} 31 | } 32 | this.level = opt.level || 'error' 33 | if (typeof options === 'object' && options.namespace) { 34 | this.namespace = options.namespace 35 | } 36 | } 37 | 38 | /** 39 | * Logger error function. 40 | * @param message menssage erro to log in console 41 | * @param args arguments to log in console 42 | */ 43 | // tslint:disable-next-line:no-any 44 | error (message: any, ...args: any[]): void { 45 | if (ConsoleLogger.LEVELS.indexOf(this.level) < 1) return 46 | console.log(`${new Date().toISOString()} - ${this.namespace} - ERROR - ${util.format(message, ...args)}`) 47 | } 48 | 49 | /** 50 | * Logger warning function. 51 | * @param message menssage warning to log in console 52 | * @param args arguments to log in console 53 | */ 54 | // tslint:disable-next-line:no-any 55 | warn (message: any, ...args: any[]): void { 56 | if (ConsoleLogger.LEVELS.indexOf(this.level) < 2) return 57 | console.log(`${new Date().toISOString()} - ${this.namespace} - WARN - ${util.format(message, ...args)}`) 58 | } 59 | 60 | /** 61 | * Logger info function. 62 | * @param message menssage info to log in console 63 | * @param args arguments to log in console 64 | */ 65 | // tslint:disable-next-line:no-any 66 | info (message: any, ...args: any[]): void { 67 | if (ConsoleLogger.LEVELS.indexOf(this.level) < 3) return 68 | console.log(`${new Date().toISOString()} - ${this.namespace} - INFO - ${util.format(message, ...args)}`) 69 | } 70 | 71 | /** 72 | * Logger debug function. 73 | * @param message menssage debug to log in console 74 | * @param args arguments to log in console 75 | */ 76 | // tslint:disable-next-line:no-any 77 | debug (message: any, ...args: any[]): void { 78 | if (ConsoleLogger.LEVELS.indexOf(this.level) < 4) return 79 | console.log(`${new Date().toISOString()} - ${this.namespace} - DEBUG - ${util.format(message, ...args)}`) 80 | } 81 | } 82 | 83 | /** 84 | * Function logger exported to others classes. Inspired by: 85 | * https://github.com/cainus/logdriver/blob/bba1761737ca72f04d6b445629848538d038484a/index.js#L50 86 | * @param options A logger options or strig to logger in console 87 | */ 88 | const logger = (options?: types.LoggerOptions | string | number): types.Logger => { 89 | return new ConsoleLogger(options) 90 | } 91 | 92 | export { logger } 93 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/common/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018, OpenCensus Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // tslint:disable:no-any 18 | export type LogFunction = (message: any, ...args: any[]) => void 19 | 20 | /** Defines an logger interface. */ 21 | export interface Logger { 22 | /** Logger verbosity level. If omitted, `debug` level is assumed. */ 23 | level?: string 24 | 25 | error: LogFunction 26 | warn: LogFunction 27 | info: LogFunction 28 | debug: LogFunction 29 | } 30 | 31 | /** Defines an logger options interface. */ 32 | export interface LoggerOptions { 33 | level?: string, 34 | namespace?: string 35 | } 36 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/exporters/base-exporter.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Exporter, ExporterOptions } from './types' 3 | import { Logger } from '../common/types' 4 | import { CoreAgent } from '../models/agent' 5 | import { ConsoleLogger } from '../common/console-logger' 6 | import { Profile } from '../models/profile' 7 | 8 | export abstract class BaseExporter implements Exporter { 9 | 10 | protected name: string 11 | protected logger: Logger 12 | protected options: ExporterOptions 13 | protected agent: CoreAgent 14 | 15 | constructor (name: string, options?: ExporterOptions) { 16 | this.name = name 17 | this.options = options || {} 18 | } 19 | 20 | enable (agent: CoreAgent) { 21 | this.logger = new ConsoleLogger({ 22 | level: agent.logger.level, 23 | namespace: `${this.name}-exporter` 24 | }) 25 | this.agent = agent 26 | this.logger.info(`Enabling exporter '${this.name}'`) 27 | } 28 | 29 | disable () { 30 | this.logger.info(`Disabling exporter '${this.name}'`) 31 | } 32 | 33 | abstract onProfileEnd (profile: Profile): Promise 34 | abstract onProfileStart (profile: Profile): Promise 35 | } 36 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/exporters/types.ts: -------------------------------------------------------------------------------- 1 | import { ProfileListener, Agent } from '../models/types' 2 | 3 | /** Defines a exporter interface. */ 4 | export interface Exporter extends ProfileListener { 5 | /** 6 | * Method to enable the exporter 7 | * 8 | * @param agent a agent instance 9 | */ 10 | enable (agent: Agent): void 11 | /** Method to disable the exporter */ 12 | disable (): void 13 | } 14 | 15 | export type ExporterOptions = { 16 | [key: string]: any 17 | } 18 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './exporters/types' 3 | export * from './exporters/base-exporter' 4 | export * from './profilers/types' 5 | export * from './profilers/base-profiler' 6 | export * from './triggers/types' 7 | export * from './triggers/base-trigger' 8 | 9 | export * from './models/config' 10 | export * from './models/profile' 11 | export * from './models/agent' 12 | 13 | export * from './utils/version' 14 | 15 | export { 16 | ProfileType, 17 | ProfileStatus, 18 | Attributes, 19 | ProfileListener 20 | } from './models/types' 21 | 22 | import * as logger from './common/console-logger' 23 | 24 | export { 25 | logger 26 | } 27 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/models/agent.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import * as types from './types' 4 | import { Config, Reaction } from './config' 5 | import * as loggerTypes from '../common/types' 6 | import * as logger from '../common/console-logger' 7 | import { Trigger, TriggerState, TriggerEventListener, TriggerEventOptions } from '../triggers/types' 8 | 9 | export class CoreAgent implements types.Agent, TriggerEventListener { 10 | /** Indicates if the tracer is active */ 11 | private activeLocal: boolean 12 | /** A configuration for starting the tracer */ 13 | private config: Config 14 | /** A list of end span event listeners */ 15 | private listeners: types.ProfileListener[] 16 | /** A list of end span event listeners */ 17 | private reactions: Reaction[] = [] 18 | /** A configuration for starting the tracer */ 19 | logger: loggerTypes.Logger = logger.logger(0) 20 | 21 | /** Constructs a new CoreAgent instance. */ 22 | constructor () { 23 | this.activeLocal = false 24 | } 25 | 26 | /** 27 | * Starts a tracer. 28 | * @param config A tracer configuration object to start a tracer. 29 | */ 30 | start (config: Config): CoreAgent { 31 | this.activeLocal = true 32 | this.config = config 33 | this.reactions = config.reactions 34 | this.logger = this.config.logger || logger.logger(config.logLevel || 1) 35 | this.listeners = [] 36 | return this 37 | } 38 | 39 | stop (): CoreAgent { 40 | this.activeLocal = false 41 | return this 42 | } 43 | 44 | /** Gets the list of event listeners. */ 45 | get profileListeners (): types.ProfileListener[] { 46 | return this.listeners 47 | } 48 | 49 | /** Indicates if the tracer is active or not. */ 50 | get active (): boolean { 51 | return this.activeLocal 52 | } 53 | 54 | onTrigger (state: TriggerState, options: TriggerEventOptions): void { 55 | const reactions = this.reactions.find(reaction => reaction.trigger === options.source) 56 | if (reactions === undefined) return 57 | reactions.profiler.onTrigger(state, options) 58 | } 59 | 60 | registerProfileListener (listener: types.ProfileListener) { 61 | this.listeners.push(listener) 62 | } 63 | 64 | unregisterProfileListener (listener: types.ProfileListener) { 65 | const index = this.listeners.indexOf(listener, 0) 66 | if (index > -1) { 67 | this.listeners.splice(index, 1) 68 | } 69 | return this 70 | } 71 | 72 | notifyStartProfile (profile: types.Profile) { 73 | this.logger.debug(`starting to notify listeners the start of ${profile.kind}`) 74 | if (this.listeners && this.listeners.length > 0) { 75 | for (const listener of this.listeners) { 76 | listener.onProfileStart(profile).then(() => { 77 | this.logger.debug(`succesfully called ${listener}.onProfileStart`) 78 | }).catch(err => { 79 | this.logger.error(`Failed to called onProfileStart on ${listener}`, err) 80 | }) 81 | } 82 | } 83 | } 84 | 85 | notifyEndProfile (profile: types.Profile) { 86 | this.logger.debug(`starting to notify listeners the end of ${profile.kind}`) 87 | if (this.listeners.length === 0) return 88 | 89 | for (const listener of this.listeners) { 90 | listener.onProfileEnd(profile).then(() => { 91 | this.logger.debug(`succesfully called ${listener}.onProfileEnd`) 92 | }).catch(err => { 93 | this.logger.error(`Failed to called onProfileEnd on ${listener}`, err) 94 | }) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/models/config.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Profiler } from '../profilers/types' 3 | import { Logger } from '../common/types' 4 | import { Trigger } from '../triggers/types' 5 | 6 | export interface Reaction { 7 | trigger: Trigger 8 | profiler: Profiler 9 | } 10 | 11 | export interface CoreConfig { 12 | /** level of logger - 0:disable, 1: error, 2: warn, 3: info, 4: debug */ 13 | logLevel?: number 14 | /** An instance of a logger */ 15 | logger?: Logger 16 | /** List of reaction to apply for a given trigger */ 17 | reactions: Reaction[] 18 | } 19 | 20 | export type Config = CoreConfig 21 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/models/profile.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as types from './types' 3 | import { Clock } from '../utils/clock' 4 | 5 | export class Profile implements types.Profile { 6 | 7 | public data: Buffer 8 | public name: string 9 | public kind: types.ProfileType 10 | public status: types.ProfileStatus 11 | public attributes: types.Attributes 12 | /** The clock used to mesure the beginning and ending of a span */ 13 | private clock: Clock 14 | 15 | constructor (name: string, kind: types.ProfileType) { 16 | this.data = Buffer.alloc(0) 17 | this.name = name 18 | this.kind = kind 19 | this.clock = new Clock() 20 | this.attributes = {} 21 | this.status = types.ProfileStatus.UNKNOWN 22 | } 23 | 24 | addAttribute (key: string, value: string | number | boolean) { 25 | if (this.ended) { 26 | throw new Error('You cannot add attributes to ended profile.') 27 | } 28 | this.attributes[key] = value 29 | } 30 | 31 | addProfileData (toAppend: Buffer) { 32 | this.data = Buffer.concat([ this.data, toAppend ]) 33 | } 34 | 35 | end (err?: Error) { 36 | this.clock.end() 37 | if (err instanceof Error) { 38 | this.status = types.ProfileStatus.FAILED 39 | this.addAttribute('error', err.message) 40 | } else { 41 | this.status = types.ProfileStatus.SUCCESS 42 | } 43 | } 44 | 45 | /** Indicates if span was started. */ 46 | get started (): boolean { 47 | return !!this.clock.startTime 48 | } 49 | 50 | /** Indicates if span was ended. */ 51 | get ended (): boolean { 52 | return this.clock.ended 53 | } 54 | 55 | /** 56 | * Gives a timestamp that indicates the span's start time in RFC3339 UTC 57 | * "Zulu" format. 58 | */ 59 | get startTime (): Date { 60 | return this.clock.startTime 61 | } 62 | 63 | /** 64 | * Gives a timestap that indicates the span's end time in RFC3339 UTC 65 | * "Zulu" format. 66 | */ 67 | get endTime (): Date { 68 | return this.clock.endTime 69 | } 70 | 71 | /** 72 | * Gives a timestap that indicates the span's duration in RFC3339 UTC 73 | * "Zulu" format. 74 | */ 75 | get duration (): number { 76 | return this.clock.duration 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/models/types.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Config } from './config' 3 | 4 | export enum ProfileType { 5 | HEAP_PROFILE = 'HEAP_PROFILE', 6 | CPU_PROFILE = 'CPU_PROFILE', 7 | /** 8 | * Performance instrumentation and tracing for Android, Linux and Chrome 9 | * ref: https://www.perfetto.dev/#/ 10 | */ 11 | PERFECTO = 'PERFECTO' 12 | } 13 | 14 | export enum ProfileStatus { 15 | UNKNOWN = 0, 16 | SUCCESS = 1, 17 | FAILED = 2 18 | } 19 | 20 | export interface ProfileListener { 21 | onProfileStart (profile: Profile): Promise 22 | onProfileEnd (profile: Profile): Promise 23 | } 24 | 25 | /** Maps a label to a string, number or boolean. */ 26 | export interface Attributes { 27 | [attributeKey: string]: string | number | boolean 28 | } 29 | 30 | /** Interface for profile */ 31 | export interface Profile { 32 | 33 | /** The resource name of the profile */ 34 | name: string 35 | 36 | /** Kind of profile. */ 37 | kind: ProfileType 38 | 39 | /** A final status for this profile */ 40 | status: ProfileStatus 41 | 42 | /** A set of attributes, each in the format [KEY]:[VALUE] */ 43 | attributes: Attributes 44 | 45 | /** The actual data that the profiler gave us */ 46 | data: Buffer 47 | 48 | /** Indicates if profile was started. */ 49 | readonly started: boolean 50 | 51 | /** Indicates if profile was ended. */ 52 | readonly ended: boolean 53 | 54 | /** 55 | * Gives a timestap that indicates the profile's start time in RFC3339 UTC 56 | * "Zulu" format. 57 | */ 58 | readonly startTime: Date 59 | 60 | /** 61 | * Gives a timestap that indicates the profile's end time in RFC3339 UTC 62 | * "Zulu" format. 63 | */ 64 | readonly endTime: Date 65 | 66 | /** 67 | * Gives a timestap that indicates the profile's duration in RFC3339 UTC 68 | * "Zulu" format. 69 | */ 70 | readonly duration: number 71 | 72 | /** 73 | * Adds an atribute to the profile. 74 | * @param key Describes the value added. 75 | * @param value The result of an operation. 76 | */ 77 | addAttribute (key: string, value: string | number | boolean): void 78 | 79 | /** 80 | * Adds raw data to a profile 81 | * @param data Buffer containing the data that will be attached to the profile 82 | */ 83 | addProfileData (data: Buffer): void 84 | 85 | /** Ends a profile. */ 86 | end (): void 87 | } 88 | 89 | export interface Agent { 90 | /** Gets active status */ 91 | active: boolean 92 | 93 | /** 94 | * Starts agent. 95 | * @param userConfig A configuration object to start the agent. 96 | * @returns The profile agent object. 97 | */ 98 | start (userConfig?: Config): ThisType 99 | 100 | /** Stops agent. */ 101 | stop (): ThisType 102 | 103 | /** 104 | * Registers an end span event listener. 105 | * @param listener The listener to register. 106 | */ 107 | registerProfileListener (listener: ProfileListener): void 108 | 109 | /** 110 | * Unregisters an end span event listener. 111 | * @param listener The listener to unregister. 112 | */ 113 | unregisterProfileListener (listener: ProfileListener): void 114 | 115 | /** 116 | * Notify profile listener that a new profile has been created 117 | * @param profile a profile to broadcast to exporters 118 | */ 119 | notifyStartProfile (profile: Profile): void 120 | 121 | /** 122 | * Notify profile listener that a profile has been completed 123 | * @param profile a profile to broadcast to exporters 124 | */ 125 | notifyEndProfile (profile: Profile): void 126 | } 127 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/profilers/base-profiler.ts: -------------------------------------------------------------------------------- 1 | import { Profiler, ProfilerOptions } from './types' 2 | import { Logger } from '../common/types' 3 | import { CoreAgent } from '../models/agent' 4 | import { TriggerEventOptions, TriggerState } from '../triggers/types' 5 | import { ConsoleLogger } from '../common/console-logger' 6 | 7 | export abstract class BaseProfiler implements Profiler { 8 | 9 | protected name: string 10 | protected logger: Logger 11 | protected options: ProfilerOptions 12 | protected agent: CoreAgent 13 | 14 | constructor (name: string, options?: ProfilerOptions) { 15 | this.name = name 16 | this.options = options || {} 17 | } 18 | 19 | enable (agent: CoreAgent) { 20 | this.agent = agent 21 | this.logger = new ConsoleLogger({ 22 | level: agent.logger.level, 23 | namespace: `${this.name}-profiler` 24 | }) 25 | this.logger.info(`Enabling profiler '${this.name}'`) 26 | this.init() 27 | } 28 | 29 | disable () { 30 | this.logger.info(`Disabling profiler '${this.name}'`) 31 | this.destroy() 32 | } 33 | 34 | abstract init (): void 35 | abstract destroy (): void 36 | 37 | abstract onTrigger (state: TriggerState, options: TriggerEventOptions): Promise 38 | } 39 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/profilers/types.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from '../models/types' 2 | import { TriggerEventListener } from '../triggers/types' 3 | 4 | export interface Profiler extends TriggerEventListener { 5 | /** 6 | * Method to enable the profiler 7 | * 8 | * @param agent a agent instance 9 | */ 10 | enable (agent: Agent): void 11 | /** Method to disable the profiler */ 12 | disable (): void 13 | } 14 | 15 | export type ProfilerOptions = { 16 | [key: string]: any 17 | } 18 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/triggers/base-trigger.ts: -------------------------------------------------------------------------------- 1 | import { Trigger, TriggerOptions } from './types' 2 | import { Logger } from '../common/types' 3 | import { CoreAgent } from '../models/agent' 4 | import { ConsoleLogger } from '../common/console-logger' 5 | 6 | export abstract class BaseTrigger implements Trigger { 7 | 8 | protected name: string 9 | protected logger: Logger 10 | protected options: TriggerOptions 11 | protected agent: CoreAgent 12 | 13 | constructor (name: string, options?: TriggerOptions) { 14 | this.name = name 15 | this.options = options || {} 16 | } 17 | 18 | enable (agent: CoreAgent) { 19 | this.logger = new ConsoleLogger({ 20 | level: agent.logger.level, 21 | namespace: `${this.name}-trigger` 22 | }) 23 | this.agent = agent 24 | this.logger.info(`Enabling trigger '${this.name}'`) 25 | this.init() 26 | } 27 | 28 | disable () { 29 | this.logger.info(`Disabling trigger '${this.name}'`) 30 | this.destroy() 31 | } 32 | 33 | abstract init (): void 34 | abstract destroy (): void 35 | } 36 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/triggers/types.ts: -------------------------------------------------------------------------------- 1 | import { Agent, Attributes } from '../models/types' 2 | 3 | export interface Trigger { 4 | /** 5 | * Method to enable the trigger 6 | * @param agent a agent instance 7 | */ 8 | enable (agent: Agent): void 9 | /** Method to disable the trigger */ 10 | disable (): void 11 | } 12 | 13 | export enum TriggerState { 14 | START = 1, 15 | END = 0 16 | } 17 | 18 | export type TriggerOptions = { 19 | [key: string]: any; 20 | } 21 | 22 | export type TriggerEventOptions = { 23 | source: Trigger 24 | name?: string, 25 | attributes?: Attributes 26 | } 27 | 28 | /** Called when a trigger fires a change in the state */ 29 | export interface TriggerEventListener { 30 | onTrigger (state: TriggerState, options: TriggerEventOptions): void 31 | } 32 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/utils/clock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018, OpenCensus Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * The Clock class is used to record the duration and endTime for spans. 19 | */ 20 | export class Clock { 21 | /** Indicates if the clock is endend. */ 22 | private endedLocal = false 23 | /** Indicates the clock's start time. */ 24 | private startTimeLocal: Date 25 | /** The time in high resolution in a [seconds, nanoseconds]. */ 26 | private hrtimeLocal: [number, number] 27 | /** The duration between start and end of the clock. */ 28 | private diff: [number, number] 29 | 30 | /** Constructs a new SamplerImpl instance. */ 31 | constructor () { 32 | this.startTimeLocal = new Date() 33 | this.hrtimeLocal = process.hrtime() 34 | } 35 | 36 | /** Ends the clock. */ 37 | end (): void { 38 | if (this.endedLocal) return 39 | this.diff = process.hrtime(this.hrtimeLocal) 40 | this.endedLocal = true 41 | } 42 | 43 | /** Gets the duration of the clock. */ 44 | get duration (): number { 45 | if (!this.endedLocal) { 46 | return -1 47 | } 48 | const ns = this.diff[0] * 1e9 + this.diff[1] 49 | return ns / 1e6 50 | } 51 | 52 | /** Starts the clock. */ 53 | get startTime (): Date { 54 | return this.startTimeLocal 55 | } 56 | 57 | /** 58 | * Gets the time so far. 59 | * @returns A Date object with the current duration. 60 | */ 61 | get endTime (): Date { 62 | let result: Date 63 | if (this.ended) { 64 | result = new Date(this.startTime.getTime() + this.duration) 65 | } else { 66 | result = new Date(0) 67 | } 68 | return result 69 | } 70 | 71 | /** Indicates if the clock was ended. */ 72 | get ended (): boolean { 73 | return this.endedLocal 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/openprofiling-core/src/utils/version.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Copyright 2018, OpenCensus Authors 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | type Package = { 19 | version: string; 20 | } 21 | 22 | // Load the package details. Note that the `require` is performed at runtime, 23 | // which means the source files will be in the `/build` directory, so the 24 | // package path is relative to that location. 25 | const pjson: Package = require('../../package.json') 26 | 27 | // Export the core package version 28 | export const version: string = pjson.version 29 | -------------------------------------------------------------------------------- /packages/openprofiling-core/test/base-exporter.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BaseExporter, CoreAgent } from '../src' 3 | import * as assert from 'assert' 4 | import { DummyExporter } from './mocks/dummyExporter' 5 | 6 | describe('Base Exporter test', () => { 7 | 8 | it('should have correct methods', () => { 9 | assert(typeof BaseExporter.prototype.enable === 'function') 10 | assert(typeof BaseExporter.prototype.disable === 'function') 11 | assert(BaseExporter.prototype.onProfileStart === undefined) 12 | assert(BaseExporter.prototype.onProfileEnd === undefined) 13 | }) 14 | 15 | it('should create implementation and disable it', () => { 16 | const agent = new CoreAgent() 17 | agent.start({ 18 | reactions: [], 19 | logLevel: 0 20 | }) 21 | const exporter = new DummyExporter() 22 | assert.doesNotThrow(() => { 23 | exporter.enable(agent) 24 | }) 25 | // @ts-ignore 26 | assert(typeof exporter.logger === 'object') 27 | // @ts-ignore 28 | assert(exporter.agent === agent) 29 | // @ts-ignore 30 | assert(exporter.name === 'dummy') 31 | assert.doesNotThrow(() => { 32 | exporter.disable() 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/openprofiling-core/test/core-agent.spec.ts: -------------------------------------------------------------------------------- 1 | import { CoreAgent, Profile, TriggerState, ProfileStatus } from '../src' 2 | import * as assert from 'assert' 3 | import { DummyTrigger } from './mocks/dummyTrigger' 4 | import { DummyProfiler } from './mocks/dummyProfiler' 5 | 6 | describe('Core Agent implementation', () => { 7 | 8 | let agent: CoreAgent 9 | let trigger = new DummyTrigger() 10 | let profiler = new DummyProfiler() 11 | 12 | it('should instanciate agent', () => { 13 | assert.doesNotThrow(() => { 14 | agent = new CoreAgent() 15 | }) 16 | assert(agent.active === false, 'should not be active') 17 | trigger.enable(agent) 18 | profiler.enable(agent) 19 | }) 20 | 21 | it('should correctly start the agent', () => { 22 | const reactions = [{ 23 | trigger, profiler 24 | }] 25 | assert.doesNotThrow(() => { 26 | const result = agent.start({ reactions }) 27 | assert(result === agent) 28 | assert(agent.active === true, 'should be active') 29 | }) 30 | }) 31 | 32 | it('should trigger via dummy trigger', (done) => { 33 | const handler = { 34 | onProfileStart: async (profile: Profile) => { 35 | assert(profile instanceof Profile) 36 | assert(profile.ended === false) 37 | assert.doesNotThrow(() => { 38 | profile.addAttribute('test', true) 39 | }) 40 | assert(profile.started === true) 41 | assert(profile.data.length === 0) 42 | assert(profile.status === ProfileStatus.UNKNOWN) 43 | agent.unregisterProfileListener(handler) 44 | return done() 45 | }, 46 | onProfileEnd: async (profile: Profile) => { 47 | return 48 | } 49 | } 50 | agent.registerProfileListener(handler) 51 | assert(agent.profileListeners.length === 1) 52 | trigger.trigger(TriggerState.START) 53 | }) 54 | 55 | it('should end profile via dummy trigger', (done) => { 56 | const handler = { 57 | onProfileStart: async (profile: Profile) => { 58 | return 59 | }, 60 | onProfileEnd: async (profile: Profile) => { 61 | assert(profile instanceof Profile) 62 | assert(profile.ended === true) 63 | assert.throws(() => { 64 | profile.addAttribute('test', true) 65 | }) 66 | assert(profile.duration > 0) 67 | assert(profile.data.toString() === 'test') 68 | assert(profile.status === ProfileStatus.SUCCESS) 69 | agent.unregisterProfileListener(handler) 70 | return done() 71 | } 72 | } 73 | agent.registerProfileListener(handler) 74 | assert(agent.profileListeners.length === 1) 75 | trigger.trigger(TriggerState.END) 76 | }) 77 | 78 | it('should stop agent', () => { 79 | assert.doesNotThrow(() => { 80 | const result = agent.stop() 81 | assert(result === agent) 82 | assert(agent.active === false) 83 | }) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /packages/openprofiling-core/test/mocks/dummyExporter.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Profile, BaseExporter } from '../../src' 3 | 4 | export type onProfile = (profile: Profile) => void 5 | 6 | export class DummyExporter extends BaseExporter { 7 | 8 | private onStart: onProfile | undefined 9 | private onEnd: onProfile | undefined 10 | 11 | constructor (onStart?: onProfile, onEnd?: onProfile) { 12 | super('dummy') 13 | this.onEnd = onEnd 14 | this.onStart = onStart 15 | } 16 | 17 | async onProfileStart (profile) { 18 | if (typeof this.onStart === 'function') { 19 | this.onStart(profile) 20 | } 21 | } 22 | 23 | async onProfileEnd (profile) { 24 | if (typeof this.onEnd === 'function') { 25 | this.onEnd(profile) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/openprofiling-core/test/mocks/dummyProfiler.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Profile, BaseProfiler, TriggerState, ProfileType, TriggerEventOptions } from '../../src' 3 | 4 | export class DummyProfiler extends BaseProfiler { 5 | private currentProfile: Profile 6 | 7 | constructor () { 8 | super('dummy-profiler') 9 | } 10 | 11 | destroy () { 12 | return 13 | } 14 | 15 | init () { 16 | return 17 | } 18 | 19 | async onTrigger (state: TriggerState, options: TriggerEventOptions) { 20 | if (state === TriggerState.START) { 21 | this.currentProfile = new Profile('test', ProfileType.CPU_PROFILE) 22 | this.agent.notifyStartProfile(this.currentProfile) 23 | } else { 24 | this.currentProfile.addProfileData(Buffer.from('test')) 25 | this.currentProfile.end() 26 | this.agent.notifyEndProfile(this.currentProfile) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/openprofiling-core/test/mocks/dummyTrigger.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BaseTrigger, TriggerState } from '../../src' 3 | 4 | export class DummyTrigger extends BaseTrigger { 5 | constructor () { 6 | super('dummny-trigger') 7 | } 8 | 9 | init () { 10 | return 11 | } 12 | 13 | destroy () { 14 | return 15 | } 16 | 17 | trigger (state: TriggerState) { 18 | this.agent.onTrigger(state, { source: this }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/openprofiling-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "build", 5 | "rootDirs": ["src"], 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "strictNullChecks": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "test/**/*.ts" 18 | ], 19 | "compileOnSave": false 20 | } -------------------------------------------------------------------------------- /packages/openprofiling-core/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-file/README.md: -------------------------------------------------------------------------------- 1 | # OpenProfiling NodeJS - File Exporter 2 | 3 | This exporter is the simplest one, it will just write the profile to the disk, you can configure the path if needed. 4 | The file name will follow the following format: 5 | 6 | ```js 7 | const name = `${profile.kind}-${profile.startTime.toISOString()}.${profile.extension}` 8 | ``` 9 | 10 | Where the profile kind can be: `HEAP_PROFILE`, `CPU_PROFILE` or `PERFECTO` 11 | And the extension can be either: `heaprofile`, `cpuprofile` or `json` 12 | 13 | ### Advantages 14 | 15 | - Since the exporter just write the disk, it's easy to find if you don't have a lot of servers and specially low chance of failing (since disks are pretty resilient) 16 | - Easy setup, since again you are simply writing on the disk 17 | 18 | ### Drawbacks 19 | 20 | - Hard to locate and retrieve the file if your applications are distributed, it would be better to use the S3 exporter in this case. 21 | - The exporter will not add any metadata to the file, so the profile attributes are generally lost (for example if it has failed, no error will be given) 22 | 23 | ### How to use 24 | 25 | In the following example, when the profile will be done it will be written on disk: 26 | 27 | ```ts 28 | import { ProfilingAgent } from '@openprofiling/nodejs' 29 | import { FileExporter } from '@openprofiling/exporter-file' 30 | import { InspectorCPUProfiler } from '@openprofiling/inspector-cpu-profiler' 31 | import { SignalTrigger } from '@openprofiling/trigger-signal' 32 | 33 | const profilingAgent = new ProfilingAgent() 34 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR2' }), new InspectorCPUProfiler()) 35 | profilingAgent.start({ exporter: new FileExporter() }) 36 | ``` 37 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openprofiling/exporter-file", 3 | "version": "0.2.2", 4 | "main": "build/index.js", 5 | "types": "build/index.d.ts", 6 | "repository": "openprofiling/openprofiling-node", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.json", 9 | "lint": "tslint --project . src/**/*.ts", 10 | "test": "mocha -r ts-node/register ./test/*.spec.ts", 11 | "test-coverage": "nyc mocha -r ts-node/register test/*.spec.ts", 12 | "report-coverage": "nyc report --reporter=json && codecov -f coverage/*.json -p ../..", 13 | "ci": "yarn test-coverage && yarn report-coverage", 14 | "prepublishOnly": "yarn build" 15 | }, 16 | "keywords": [ 17 | "openprofiling", 18 | "nodejs", 19 | "tracing", 20 | "profiling" 21 | ], 22 | "author": "Valentin Marchaud", 23 | "license": "Apache-2.0", 24 | "engines": { 25 | "node": ">=6.0" 26 | }, 27 | "files": [ 28 | "build/", 29 | "doc", 30 | "CHANGELOG.md", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "extension": [ 39 | ".ts" 40 | ], 41 | "exclude": [ 42 | "build/", 43 | "config/", 44 | "examples/", 45 | "test/" 46 | ], 47 | "cache": true, 48 | "all": true 49 | }, 50 | "devDependencies": { 51 | "codecov": "^3.4.0", 52 | "mocha": "^6.1.4", 53 | "nyc": "^14.1.1", 54 | "ts-node": "^8.2.0", 55 | "tslint": "^5.17.0", 56 | "tslint-config-standard": "^8.0.1", 57 | "typescript": "^3.7.4" 58 | }, 59 | "dependencies": { 60 | "@openprofiling/core": "^0.2.2" 61 | }, 62 | "gitHead": "a8098997633d0ffefed78ec7b4c12df64e47f3d5" 63 | } 64 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-file/src/file-exporter.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Profile, ExporterOptions, BaseExporter } from '@openprofiling/core' 3 | import * as fs from 'fs' 4 | import { tmpdir } from 'os' 5 | import { resolve } from 'path' 6 | 7 | export interface FileExporterConfig extends ExporterOptions { 8 | path: string 9 | } 10 | 11 | const defaultFileExporterConfig: FileExporterConfig = { 12 | path: tmpdir() 13 | } 14 | 15 | export const fileExtensions = { 16 | 'HEAP_PROFILE': 'heapprofile', 17 | 'CPU_PROFILE': 'cpuprofile', 18 | 'PERFECTO': 'json' 19 | } 20 | 21 | export class FileExporter extends BaseExporter { 22 | 23 | private config: FileExporterConfig = defaultFileExporterConfig 24 | 25 | constructor (options?: FileExporterConfig) { 26 | super('file', options) 27 | if (typeof options === 'object') { 28 | this.config = options 29 | } 30 | } 31 | 32 | async onProfileStart (profile: Profile) { 33 | return 34 | } 35 | 36 | async onProfileEnd (profile: Profile) { 37 | const extension = fileExtensions[profile.kind] 38 | const filename = `${profile.kind.toLowerCase()}-${profile.startTime.toISOString()}.${extension}` 39 | const targetPath = resolve(this.config.path, filename) 40 | fs.writeFile(targetPath, profile.data, (err) => { 41 | if (err) { 42 | this.logger.error(`Error while writing profile to disk`, err.message) 43 | } else { 44 | this.logger.info(`File written to ${filename}`) 45 | } 46 | }) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-file/src/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export * from './file-exporter' 4 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-file/test/exporter-file.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { FileExporter, fileExtensions } from '../src' 3 | import * as assert from 'assert' 4 | import { Profile, ProfileType, CoreAgent } from '@openprofiling/core' 5 | import { tmpdir } from 'os' 6 | import { resolve } from 'path' 7 | import { readFile, mkdir } from 'fs' 8 | 9 | describe('Exporter File', () => { 10 | let exporter = new FileExporter() 11 | let agent = new CoreAgent() 12 | agent.start({ logLevel: 4, reactions: [] }) 13 | exporter.enable(agent) 14 | 15 | it('should export a profiler implementation', () => { 16 | assert(typeof FileExporter.prototype.onProfileEnd === 'function') 17 | assert(typeof FileExporter.prototype.onProfileStart === 'function') 18 | }) 19 | 20 | it('should correctly save the profile to disk', (done) => { 21 | const profile = new Profile('test', ProfileType.CPU_PROFILE) 22 | profile.addProfileData(Buffer.from('test')) 23 | profile.end() 24 | const expectedPath = resolve(tmpdir(), `${profile.kind.toLowerCase()}-${profile.startTime.toISOString()}.${fileExtensions[profile.kind]}`) 25 | exporter.onProfileEnd(profile).then().catch(done) 26 | // without waiting, it could be race between the reading and the writing 27 | setTimeout(_ => { 28 | readFile(expectedPath, (err, buffer) => { 29 | assert.ifError(err) 30 | assert(buffer.toString() === 'test') 31 | return done(err) 32 | }) 33 | }, 200) 34 | }) 35 | 36 | it('should correctly save the profile to disk with custom path', (done) => { 37 | const customPath = resolve(tmpdir(), 'exporter-file-test') 38 | mkdir(customPath, _err => { 39 | return 40 | }) 41 | exporter = new FileExporter({ 42 | path: customPath 43 | }) 44 | exporter.enable(agent) 45 | const profile = new Profile('test', ProfileType.CPU_PROFILE) 46 | profile.addProfileData(Buffer.from('test')) 47 | profile.end() 48 | const expectedPath = resolve(customPath, `${profile.kind.toLowerCase()}-${profile.startTime.toISOString()}.${fileExtensions[profile.kind]}`) 49 | exporter.onProfileEnd(profile).then().catch(done) 50 | // without waiting, it could be race between the reading and the writing 51 | setTimeout(_ => { 52 | readFile(expectedPath, (err, buffer) => { 53 | assert.ifError(err) 54 | assert(buffer.toString() === 'test') 55 | return done(err) 56 | }) 57 | }, 200) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-file/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "build", 5 | "rootDirs": ["src"], 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "strictNullChecks": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "test/**/*.ts" 18 | ], 19 | "compileOnSave": false 20 | } -------------------------------------------------------------------------------- /packages/openprofiling-exporter-file/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-gcs/README.md: -------------------------------------------------------------------------------- 1 | # OpenProfiling NodeJS - Google Cloud Storage Exporter 2 | 3 | This exporter is advised when profiling distributed applications where retrieving from each container file system can be hard. 4 | It will just write the profile to a remote S3-compatible server, the file name in each bucket will follow the following format: 5 | 6 | ```js 7 | const name = `${profile.kind}-${profile.startTime.toISOString()}.${profile.extension}` 8 | ``` 9 | 10 | Where the profile kind can be: `HEAP_PROFILE`, `CPU_PROFILE` or `PERFECTO` 11 | And the extension can be either: `heaprofile`, `cpuprofile` or `json` 12 | 13 | ### Advantages 14 | 15 | - Centralization of every profile, prefered when using containers 16 | 17 | ### Drawbacks 18 | 19 | - You need to have a S3 compatible running (or AWS S3 itself) and manage it yourself. 20 | 21 | ### How to use 22 | 23 | In the following example, when the profile will be done it will be written to the remote S3 bucket: 24 | 25 | ```ts 26 | import { ProfilingAgent } from '@openprofiling/nodejs' 27 | import { GcloudStorageExporter } from '@openprofiling/exporter-gcs' 28 | import { InspectorCPUProfiler } from '@openprofiling/inspector-cpu-profiler' 29 | import { SignalTrigger } from '@openprofiling/trigger-signal' 30 | 31 | const profilingAgent = new ProfilingAgent() 32 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR2' }), new InspectorCPUProfiler()) 33 | profilingAgent.start({ 34 | exporter: new GcloudStorageExporter({ 35 | // Alternatively, you might pass it via the GOOGLE_APPLICATION_CREDENTIALS env variable 36 | keyFilename: 'some/where/key.json', 37 | // Alternatively, you might pass it via the GCLOUD_PROJECT env variable 38 | projectId: 'my-project', 39 | /* 40 | * name of the bucket to create 41 | */ 42 | bucket: 'test' 43 | } 44 | }) 45 | ``` 46 | 47 | ## Development 48 | 49 | When developing against this package, you might want to run a fake gcs server with Minio to be able to run tests and verify the behavior: 50 | 51 | ```bash 52 | docker run -d --name fake-gcs-server -p 4443:4443 fsouza/fake-gcs-server 53 | ``` -------------------------------------------------------------------------------- /packages/openprofiling-exporter-gcs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openprofiling/exporter-gcs", 3 | "version": "0.2.2", 4 | "main": "build/index.js", 5 | "types": "build/index.d.ts", 6 | "repository": "openprofiling/openprofiling-node", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.json", 9 | "lint": "tslint --project . src/**/*.ts", 10 | "test": "mocha -r ts-node/register ./test/*.spec.ts", 11 | "test-coverage": "nyc mocha -r ts-node/register test/*.spec.ts", 12 | "report-coverage": "nyc report --reporter=json && codecov -f coverage/*.json -p ../..", 13 | "ci": "yarn test-coverage && yarn report-coverage", 14 | "prepublishOnly": "yarn build" 15 | }, 16 | "keywords": [ 17 | "openprofiling", 18 | "nodejs", 19 | "tracing", 20 | "profiling" 21 | ], 22 | "author": "Valentin Marchaud", 23 | "license": "Apache-2.0", 24 | "engines": { 25 | "node": ">=8.9.1" 26 | }, 27 | "files": [ 28 | "build/", 29 | "doc", 30 | "CHANGELOG.md", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "extension": [ 39 | ".ts" 40 | ], 41 | "exclude": [ 42 | "build/", 43 | "config/", 44 | "examples/", 45 | "test/" 46 | ], 47 | "cache": true, 48 | "all": true 49 | }, 50 | "devDependencies": { 51 | "codecov": "^3.4.0", 52 | "mocha": "^6.1.4", 53 | "nyc": "^14.1.1", 54 | "ts-node": "^8.2.0", 55 | "tslint": "^5.17.0", 56 | "tslint-config-standard": "^8.0.1", 57 | "typescript": "^3.7.4" 58 | }, 59 | "dependencies": { 60 | "@google-cloud/storage": "^4.1.3", 61 | "@openprofiling/core": "^0.2.2" 62 | }, 63 | "gitHead": "a8098997633d0ffefed78ec7b4c12df64e47f3d5" 64 | } 65 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-gcs/src/gcloud-storage-exporter.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Profile, ExporterOptions, BaseExporter } from '@openprofiling/core' 3 | import { Storage, StorageOptions, Bucket } from '@google-cloud/storage' 4 | import { Duplex } from 'stream' 5 | 6 | export interface GcloudStorageExporterConfig extends ExporterOptions, StorageOptions { 7 | keyFilename?: string 8 | projectId?: string 9 | bucket: string 10 | } 11 | 12 | export const fileExtensions = { 13 | 'HEAP_PROFILE': 'heapprofile', 14 | 'CPU_PROFILE': 'cpuprofile', 15 | 'PERFECTO': 'json' 16 | } 17 | 18 | export class GcloudStorageExporter extends BaseExporter { 19 | 20 | private config: GcloudStorageExporterConfig 21 | private storage: Storage 22 | 23 | constructor (options?: GcloudStorageExporterConfig) { 24 | super('gcloud-storage', options) 25 | if (typeof options === 'object') { 26 | this.config = options 27 | // tslint:disable-next-line 28 | if (this.config.bucket === undefined) { 29 | throw new Error(`You must pass at least the bucket name`) 30 | } 31 | } else { 32 | throw new Error('You must pass options to the GcloudStorageExporter') 33 | } 34 | this.storage = new Storage(this.config) 35 | } 36 | 37 | async onProfileStart (profile: Profile) { 38 | return 39 | } 40 | 41 | async onProfileEnd (profile: Profile) { 42 | const extension = fileExtensions[profile.kind] 43 | const filename = `${profile.kind.toLowerCase()}-${profile.startTime.toISOString()}.${extension}` 44 | const metadata = Object.assign({ 45 | kind: profile.kind.toString(), 46 | startTime: profile.startTime.toISOString(), 47 | endTime: profile.endTime.toISOString(), 48 | duration: profile.duration.toString(), 49 | status: profile.status.toString() 50 | }, Object.entries(profile.attributes).reduce((agg, [key, value]) => { 51 | agg[key] = value.toString() 52 | return agg 53 | }, {})) 54 | // ensure the bucket exists 55 | const bucket = await this.ensureBucket() 56 | await this.upload(bucket, filename, profile.data, { metadata }) 57 | } 58 | 59 | private async ensureBucket () { 60 | const bucket = this.storage.bucket(this.config.bucket) 61 | const [ exists ] = await bucket.exists() 62 | if (exists === true) return bucket 63 | await bucket.create() 64 | return bucket 65 | } 66 | 67 | private async upload (bucket: Bucket, path: string, data: Buffer, metadata: unknown) { 68 | const file = bucket.file(path) 69 | return new Promise((resolve, reject) => { 70 | const stream = new Duplex() 71 | stream.push(data) 72 | stream.push(null) 73 | stream.pipe(file.createWriteStream({ 74 | predefinedAcl: 'publicRead', 75 | resumable: false, 76 | metadata: Object.assign({}, metadata), 77 | validation: 'crc32c' 78 | })).on('error', reject).on('finish', resolve) 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-gcs/src/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export * from './gcloud-storage-exporter' 4 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-gcs/test/gcloud-storage-exporter.spec.ts: -------------------------------------------------------------------------------- 1 | import { GcloudStorageExporter, fileExtensions } from '../src' 2 | import * as assert from 'assert' 3 | import { Profile, ProfileType, CoreAgent, Exporter } from '@openprofiling/core' 4 | import { Storage } from '@google-cloud/storage' 5 | import { URL } from 'url' 6 | 7 | process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0' 8 | process.on('unhandledRejection', err => { throw err }) 9 | 10 | describe('GCS Exporter', () => { 11 | let exporter: Exporter 12 | let agent = new CoreAgent() 13 | const fakeGCSHost = process.env.GCS_HOST || `localhost:4443` 14 | const options = { 15 | apiEndpoint: fakeGCSHost, 16 | bucket: 'test-gcs-bucket', 17 | projectId: 'test', 18 | useSSL: false 19 | } 20 | let storage = new Storage(options) 21 | const interceptor = { 22 | request: function (reqOpts) { 23 | const url = new URL(reqOpts.uri) 24 | url.host = fakeGCSHost 25 | reqOpts.uri = url.toString() 26 | return reqOpts 27 | } 28 | } 29 | // @ts-ignore Used to modify url to our fake instance 30 | storage.interceptors.push(interceptor) 31 | 32 | const getProfilePath = (profile: Profile) => { 33 | const extension = fileExtensions[profile.kind] 34 | return `${profile.kind.toLowerCase()}-${profile.startTime.toISOString()}.${extension}` 35 | } 36 | 37 | it('should export a exporter implementation', () => { 38 | assert(typeof GcloudStorageExporter.prototype.onProfileEnd === 'function') 39 | assert(typeof GcloudStorageExporter.prototype.onProfileStart === 'function') 40 | }) 41 | 42 | it('should throw if no options is given in the constructor', () => { 43 | assert.throws(() => { 44 | exporter = new GcloudStorageExporter() 45 | }) 46 | assert.throws(() => { 47 | // @ts-ignore 48 | exporter = new GcloudStorageExporter({}) 49 | }) 50 | }) 51 | 52 | it('should throw because of wrong creds', async () => { 53 | exporter = new GcloudStorageExporter({ 54 | apiEndpoint: fakeGCSHost, 55 | bucket: 'test-gcs-bucket' 56 | }) 57 | exporter.enable(agent) 58 | const profile = new Profile('test', ProfileType.CPU_PROFILE) 59 | profile.addProfileData(Buffer.from('test')) 60 | profile.end() 61 | await assert.rejects(() => exporter.onProfileEnd(profile)) 62 | }) 63 | 64 | it('should succesfully upload profile', async () => { 65 | exporter = new GcloudStorageExporter(options) 66 | // @ts-ignore Used to modify url to our fake instance 67 | exporter.storage.interceptors.push(interceptor) 68 | exporter.enable(agent) 69 | const profile = new Profile('test', ProfileType.CPU_PROFILE) 70 | profile.addProfileData(Buffer.from('test')) 71 | profile.end() 72 | await assert.doesNotReject(() => exporter.onProfileEnd(profile)) 73 | const bucket = storage.bucket(options.bucket) 74 | const [ exists ] = await bucket.exists() 75 | assert(exists === true) 76 | const file = bucket.file(getProfilePath(profile)) 77 | const fileExists = await file.exists() 78 | assert(fileExists[0] === true) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-gcs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "build", 5 | "rootDirs": ["src"], 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "strictNullChecks": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "test/**/*.ts" 18 | ], 19 | "compileOnSave": false 20 | } -------------------------------------------------------------------------------- /packages/openprofiling-exporter-gcs/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-s3/README.md: -------------------------------------------------------------------------------- 1 | # OpenProfiling NodeJS - S3 Exporter 2 | 3 | This exporter is advised when profiling distributed applications where retrieving from each container file system can be hard. 4 | It will just write the profile to a remote S3-compatible server, the file name in each bucket will follow the following format: 5 | 6 | ```js 7 | const name = `${profile.kind}-${profile.startTime.toISOString()}.${profile.extension}` 8 | ``` 9 | 10 | Where the profile kind can be: `HEAP_PROFILE`, `CPU_PROFILE` or `PERFECTO` 11 | And the extension can be either: `heaprofile`, `cpuprofile` or `json` 12 | 13 | ### Advantages 14 | 15 | - Centralization of every profile, prefered when using containers 16 | 17 | ### Drawbacks 18 | 19 | - You need to have a S3 compatible running (or AWS S3 itself) and manage it yourself. 20 | 21 | ### How to use 22 | 23 | In the following example, when the profile will be done it will be written to the remote S3 bucket: 24 | 25 | ```ts 26 | import { ProfilingAgent } from '@openprofiling/nodejs' 27 | import { S3Exporter } from '@openprofiling/exporter-s3' 28 | import { InspectorCPUProfiler } from '@openprofiling/inspector-cpu-profiler' 29 | import { SignalTrigger } from '@openprofiling/trigger-signal' 30 | 31 | const profilingAgent = new ProfilingAgent() 32 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR2' }), new InspectorCPUProfiler()) 33 | profilingAgent.start({ 34 | exporter: new S3Exporter({ 35 | /** 36 | * string representing the endpoint of the server to connect to; for AWS S3, set this to s3.amazonaws.com and the library will pick the correct endpoint based on the connection.region argument (default: 'us-east-1') 37 | */ 38 | endPoint: '', 39 | /* 40 | * (optional): string containing the AWS region to use, useful for connecting to AWS S3 41 | */ 42 | region: '', 43 | /* 44 | * string containing the access key (the "public key") 45 | */ 46 | accessKey: '', 47 | /* 48 | * string containing the secret key 49 | */ 50 | secretKey: '', 51 | /* 52 | * (optional): boolean that will force the connection using HTTPS if true (default: true) 53 | */ 54 | useSSL: true, 55 | /* 56 | * (optional): number representing the port to connect to; defaults to 443 if useSSL is true, 80 otherwise 57 | */ 58 | port: 443, 59 | /* 60 | * name of the bucket to create 61 | */ 62 | bucket: 'test' 63 | } 64 | }) 65 | ``` 66 | 67 | ## Development 68 | 69 | When developing against this package, you might want to run a simple s3 server with Minio to be able to run tests and verify the behavior: 70 | 71 | ```bash 72 | docker run -p 9000:9000 -e "MINIO_ACCESS_KEY=accessKey" -e "MINIO_SECRET_KEY=secretKey" minio/minio server /data 73 | ``` -------------------------------------------------------------------------------- /packages/openprofiling-exporter-s3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openprofiling/exporter-s3", 3 | "version": "0.2.2", 4 | "main": "build/index.js", 5 | "types": "build/index.d.ts", 6 | "repository": "openprofiling/openprofiling-node", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.json", 9 | "lint": "tslint --project . src/**/*.ts", 10 | "test": "mocha -r ts-node/register ./test/*.spec.ts", 11 | "test-coverage": "nyc mocha -r ts-node/register test/*.spec.ts", 12 | "report-coverage": "nyc report --reporter=json && codecov -f coverage/*.json -p ../..", 13 | "ci": "yarn test-coverage && yarn report-coverage", 14 | "prepublishOnly": "yarn build" 15 | }, 16 | "keywords": [ 17 | "openprofiling", 18 | "nodejs", 19 | "tracing", 20 | "profiling" 21 | ], 22 | "author": "Valentin Marchaud", 23 | "license": "Apache-2.0", 24 | "engines": { 25 | "node": ">=8.9.1" 26 | }, 27 | "files": [ 28 | "build/", 29 | "doc", 30 | "CHANGELOG.md", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "extension": [ 39 | ".ts" 40 | ], 41 | "exclude": [ 42 | "build/", 43 | "config/", 44 | "examples/", 45 | "test/" 46 | ], 47 | "cache": true, 48 | "all": true 49 | }, 50 | "devDependencies": { 51 | "codecov": "^3.4.0", 52 | "mocha": "^6.1.4", 53 | "nyc": "^14.1.1", 54 | "ts-node": "^8.2.0", 55 | "tslint": "^5.17.0", 56 | "tslint-config-standard": "^8.0.1", 57 | "typescript": "^3.7.4" 58 | }, 59 | "dependencies": { 60 | "@openprofiling/core": "^0.2.2", 61 | "@smcloudstore/generic-s3": "^0.2.0", 62 | "smcloudstore": "^0.2.1" 63 | }, 64 | "gitHead": "a8098997633d0ffefed78ec7b4c12df64e47f3d5" 65 | } 66 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-s3/src/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export * from './s3-exporter' 4 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-s3/src/s3-exporter.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Profile, ExporterOptions, BaseExporter } from '@openprofiling/core' 3 | import * as SMCloudStore from 'smcloudstore' 4 | import { StorageProvider } from '@smcloudstore/core/dist/StorageProvider' 5 | 6 | export interface S3ExporterConfig extends ExporterOptions { 7 | region?: string 8 | accessKey: string 9 | secretKey: string 10 | useSSL?: boolean 11 | port?: number 12 | endPoint: string 13 | bucket: string 14 | } 15 | 16 | export const fileExtensions = { 17 | 'HEAP_PROFILE': 'heapprofile', 18 | 'CPU_PROFILE': 'cpuprofile', 19 | 'PERFECTO': 'json' 20 | } 21 | 22 | export class S3Exporter extends BaseExporter { 23 | 24 | private config: S3ExporterConfig 25 | private storage: StorageProvider 26 | 27 | constructor (options?: S3ExporterConfig) { 28 | super('s3', options) 29 | if (typeof options === 'object') { 30 | this.config = options 31 | } else { 32 | throw new Error('You must pass options to the S3Exporter') 33 | } 34 | this.storage = SMCloudStore.Create('generic-s3', this.config) 35 | } 36 | 37 | async onProfileStart (profile: Profile) { 38 | return 39 | } 40 | 41 | async onProfileEnd (profile: Profile) { 42 | const extension = fileExtensions[profile.kind] 43 | const filename = `${profile.kind.toLowerCase()}-${profile.startTime.toISOString()}.${extension}` 44 | const metadata = Object.assign({ 45 | kind: profile.kind, 46 | startTime: profile.startTime.toISOString(), 47 | endTime: profile.endTime.toISOString(), 48 | duration: profile.duration, 49 | status: profile.status 50 | }, profile.attributes) 51 | await this.storage.ensureContainer(this.config.bucket) 52 | await this.storage.putObject(this.config.bucket, filename, profile.data, { metadata }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-s3/test/s3-exporter.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { S3Exporter, fileExtensions } from '../src' 3 | import * as assert from 'assert' 4 | import { Profile, ProfileType, CoreAgent, Exporter } from '@openprofiling/core' 5 | import * as SMCloudStore from 'smcloudstore' 6 | 7 | describe('Exporter File', () => { 8 | let exporter: Exporter 9 | let agent = new CoreAgent() 10 | const options = { 11 | endPoint: process.env.S3_HOST || 'localhost', 12 | port: process.env.S3_PORT ? parseInt(process.env.S3_PORT, 10) : 9000, 13 | accessKey: process.env.S3_ACCESS_KEY || 'accessKey', 14 | secretKey: process.env.S3_SECRET_KEY || 'secretKey', 15 | bucket: 'test-s3-bucket', 16 | useSSL: false 17 | } 18 | let storage = SMCloudStore.Create('generic-s3', options) 19 | const getProfilePath = (profile: Profile) => { 20 | const extension = fileExtensions[profile.kind] 21 | return `${profile.kind.toLowerCase()}-${profile.startTime.toISOString()}.${extension}` 22 | } 23 | 24 | it('should export a exporter implementation', () => { 25 | assert(typeof S3Exporter.prototype.onProfileEnd === 'function') 26 | assert(typeof S3Exporter.prototype.onProfileStart === 'function') 27 | }) 28 | 29 | it('should throw if no options is given in the constructor', () => { 30 | assert.throws(() => { 31 | exporter = new S3Exporter() 32 | }) 33 | assert.throws(() => { 34 | // @ts-ignore 35 | exporter = new S3Exporter({}) 36 | }) 37 | }) 38 | 39 | it('should throw because of wrong creds', async () => { 40 | exporter = new S3Exporter({ 41 | endPoint: 'localhost', 42 | accessKey: process.env.S3_ACCESS_KEY || 'accessKey', 43 | secretKey: process.env.S3_SECRET_KEY || 'secretKey', 44 | bucket: 'toto' 45 | }) 46 | exporter.enable(agent) 47 | const profile = new Profile('test', ProfileType.CPU_PROFILE) 48 | profile.addProfileData(Buffer.from('test')) 49 | profile.end() 50 | await assert.rejects(() => exporter.onProfileEnd(profile)) 51 | }) 52 | 53 | it('should succesfully upload profile', async () => { 54 | exporter = new S3Exporter(options) 55 | exporter.enable(agent) 56 | const profile = new Profile('test', ProfileType.CPU_PROFILE) 57 | profile.addProfileData(Buffer.from('test')) 58 | profile.end() 59 | await assert.doesNotReject(() => exporter.onProfileEnd(profile)) 60 | await assert.doesNotReject(() => storage.isContainer(options.bucket)) 61 | const storedProfile = await storage.getObjectAsBuffer(options.bucket, getProfilePath(profile)) 62 | assert(storedProfile.toString() === 'test') 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /packages/openprofiling-exporter-s3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "build", 5 | "rootDirs": ["src"], 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "strictNullChecks": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "test/**/*.ts" 18 | ], 19 | "compileOnSave": false 20 | } -------------------------------------------------------------------------------- /packages/openprofiling-exporter-s3/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws-client/README.md: -------------------------------------------------------------------------------- 1 | # OpenProfiling NodeJS - Websocket Gateway Client 2 | 3 | TODO -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openprofiling/gateway-ws-client", 3 | "version": "0.2.2", 4 | "main": "build/index.js", 5 | "types": "build/index.d.ts", 6 | "repository": "openprofiling/openprofiling-node", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.json", 9 | "lint": "tslint --project . src/**/*.ts", 10 | "test": "mocha -r ts-node/register ./test/*.spec.ts", 11 | "test-coverage": "nyc mocha -r ts-node/register test/*.spec.ts", 12 | "report-coverage": "nyc report --reporter=json && codecov -f coverage/*.json -p ../..", 13 | "ci": "yarn test-coverage && yarn report-coverage", 14 | "prepublishOnly": "yarn build" 15 | }, 16 | "keywords": [ 17 | "openprofiling", 18 | "nodejs", 19 | "tracing", 20 | "profiling" 21 | ], 22 | "author": "Valentin Marchaud", 23 | "license": "Apache-2.0", 24 | "engines": { 25 | "node": ">=10" 26 | }, 27 | "files": [ 28 | "build/", 29 | "doc", 30 | "CHANGELOG.md", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "extension": [ 39 | ".ts" 40 | ], 41 | "exclude": [ 42 | "build/", 43 | "config/", 44 | "examples/", 45 | "test/" 46 | ], 47 | "cache": true, 48 | "all": true 49 | }, 50 | "devDependencies": { 51 | "@types/ws": "^6.0.1", 52 | "codecov": "^3.4.0", 53 | "mocha": "^6.1.4", 54 | "nyc": "^14.1.1", 55 | "ts-node": "^8.2.0", 56 | "tslint": "^5.17.0", 57 | "tslint-config-standard": "^8.0.1", 58 | "typescript": "^3.7.4" 59 | }, 60 | "dependencies": { 61 | "@openprofiling/core": "^0.2.2", 62 | "@openprofiling/gateway-ws": "^0.2.2", 63 | "@openprofiling/inspector-cpu-profiler": "^0.2.2", 64 | "@openprofiling/inspector-heap-profiler": "^0.2.2", 65 | "@openprofiling/inspector-trace-events": "^0.2.2", 66 | "ws": "^7.1.0" 67 | }, 68 | "gitHead": "a8098997633d0ffefed78ec7b4c12df64e47f3d5" 69 | } 70 | -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws-client/src/gateway-client.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as WS from 'ws' 3 | 4 | import { CoreAgent, ProfileListener, Profile, ProfileType, BaseProfiler, TriggerState, BaseTrigger } from '@openprofiling/core' 5 | import { AgentMetadata, StopPacket, StartPacket, Packet, PacketType, HelloPacket } from '@openprofiling/gateway-ws' 6 | import { InspectorCPUProfiler } from '@openprofiling/inspector-cpu-profiler' 7 | import { InspectorHeapProfiler } from '@openprofiling/inspector-heap-profiler' 8 | import { TraceEventsProfiler } from '@openprofiling/inspector-trace-events' 9 | 10 | type AgentConfig = { 11 | logLevel?: number, 12 | gatewayURI: string, 13 | metadata: AgentMetadata 14 | } & WS.ClientOptions 15 | 16 | class DummyTrigger extends BaseTrigger { 17 | constructor () { 18 | super('gateway-ws-client') 19 | } 20 | 21 | init () { 22 | return 23 | } 24 | 25 | destroy () { 26 | return 27 | } 28 | } 29 | 30 | export class GatewayProfilingAgent implements ProfileListener { 31 | 32 | private agent: CoreAgent = new CoreAgent() 33 | private _options: AgentConfig 34 | private started = false 35 | private client: WS 36 | private profilers: Map = new Map() 37 | private fakeTrigger = new DummyTrigger() 38 | 39 | start (options: AgentConfig): GatewayProfilingAgent { 40 | this._options = options 41 | 42 | this.client = new WS(options.gatewayURI, options) 43 | this.client.on('error', (err) => { 44 | this.agent.logger.error(`Gateway WS error, closing connection for safety`, err) 45 | this.stop() 46 | }) 47 | this.client.on('open', () => { 48 | this.started = true 49 | this.profilers.set(ProfileType.CPU_PROFILE, new InspectorCPUProfiler()) 50 | this.profilers.set(ProfileType.HEAP_PROFILE, new InspectorHeapProfiler()) 51 | this.profilers.set(ProfileType.PERFECTO, new TraceEventsProfiler()) 52 | for (let profiler of this.profilers.values()) { 53 | profiler.enable(this.agent) 54 | } 55 | this.agent.start(Object.assign(options, { reactions: [] })) 56 | this.agent.registerProfileListener(this) 57 | this.client.on('message', this.onMessage.bind(this)) 58 | const hello: HelloPacket = { 59 | type: PacketType.HELLO, 60 | payload: this._options.metadata 61 | } 62 | // send hello world 63 | this.client.send(JSON.stringify(hello)) 64 | }) 65 | this.client.on('ping', () => this.client.pong()) 66 | this.client.on('close', () => this.stop()) 67 | return this 68 | } 69 | 70 | onMessage (data: WS.Data) { 71 | try { 72 | const parsed = JSON.parse(data.toString()) as Packet 73 | // then process the PacketType 74 | switch (parsed.type) { 75 | case PacketType.START: { 76 | // tslint:disable-next-line 77 | return this.startProfiler(parsed as StartPacket) 78 | } 79 | case PacketType.STOP: { 80 | // tslint:disable-next-line 81 | return this.stopProfiler(parsed as StopPacket) 82 | } 83 | } 84 | } catch (err) { 85 | console.error(`Error with parsing msg`, err) 86 | this.client.close() 87 | } 88 | } 89 | 90 | async onProfileEnd (profile: Profile) { 91 | this.client.send(JSON.stringify({ 92 | type: PacketType.PROFILE, 93 | payload: { 94 | profile 95 | } 96 | })) 97 | } 98 | 99 | async onProfileStart (profile: Profile) { 100 | this.client.send(JSON.stringify({ 101 | type: PacketType.START, 102 | payload: { 103 | result: 'succedeed', 104 | type: profile.kind, 105 | message: `Profiler ${profile.kind} has been started` 106 | } 107 | })) 108 | } 109 | 110 | startProfiler (packet: StartPacket) { 111 | const profiler = this.profilers.get(packet.payload.type) 112 | if (profiler === undefined) { 113 | this.agent.logger.error(`Received start packet for profiler ${packet.payload.type} but wasn't found`) 114 | return 115 | } 116 | profiler.onTrigger(TriggerState.START, { source: this.fakeTrigger }).then(_ => { 117 | return 118 | }).catch(err => { 119 | console.error(`Error while starting profiler ${packet.payload.type}, stopping agent`, err) 120 | this.stop() 121 | }) 122 | } 123 | 124 | stopProfiler (packet: StopPacket) { 125 | const profiler = this.profilers.get(packet.payload.type) 126 | if (profiler === undefined) { 127 | this.agent.logger.error(`Received stop packet for profiler ${packet.payload.type} but wasn't found`) 128 | return 129 | } 130 | profiler.onTrigger(TriggerState.END, { source: this.fakeTrigger }).then(_ => { 131 | return 132 | }).catch(err => { 133 | console.error(`Error while starting profiler ${packet.payload.type}, stopping agent`, err) 134 | this.stop() 135 | }) 136 | } 137 | 138 | stop () { 139 | this.agent.unregisterProfileListener(this) 140 | this.agent.stop() 141 | this.started = false 142 | this.agent.logger.info(`Profiling agent is now stopped.`) 143 | } 144 | 145 | isStarted () { 146 | return this.started 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws-client/src/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export * from './gateway-client' 4 | -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws-client/test/gateway-client.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | GatewayProfilingAgent 4 | } from '../src/index' 5 | import { EventEmitter } from 'events' 6 | import * as assert from 'assert' 7 | import { ProfileType } from '@openprofiling/core' 8 | import { WebsocketGateway, PacketType, Packet, HelloPacket, StartPacket, StopPacket, ProfilePacket } from '@openprofiling/gateway-ws' 9 | import * as WS from 'ws' 10 | 11 | class FakeWSClient extends EventEmitter { 12 | constructor (private sendCallback: Function) { 13 | super() 14 | } 15 | send () { 16 | return this.sendCallback.apply(this, arguments) 17 | } 18 | } 19 | 20 | describe('Agent Websocket Gateway Test', () => { 21 | 22 | let server = new WebsocketGateway({ 23 | port: 8080 24 | }) 25 | let client: GatewayProfilingAgent 26 | let monitor: WS 27 | 28 | after(() => { 29 | server.destroy() 30 | }) 31 | 32 | it('should create monitor client', (done) => { 33 | const monitorPacket = Buffer.from(JSON.stringify({ 34 | type: PacketType.MONITOR, 35 | payload: null 36 | })) 37 | monitor = new WS('http://localhost:8080') 38 | monitor.on('open', () => { 39 | monitor.send(monitorPacket) 40 | return done() 41 | }) 42 | monitor.on('error', done) 43 | }) 44 | 45 | it('should instanciate correctly the client', () => { 46 | client = new GatewayProfilingAgent() 47 | }) 48 | 49 | it('should connect and receive hello packet', done => { 50 | monitor.on('message', (data) => { 51 | const packet = JSON.parse(data.toString()).packet as Packet 52 | if (packet.type === PacketType.CUSTOM) return 53 | assert.strictEqual(packet.type, PacketType.HELLO) 54 | const payload = (packet as HelloPacket).payload 55 | assert.strictEqual(payload.name, 'dummy') 56 | monitor.removeAllListeners() 57 | return done() 58 | }) 59 | client.start({ 60 | gatewayURI: 'http://localhost:8080', 61 | metadata: { 62 | name: 'dummy', 63 | attributes: { 64 | test: 1 65 | } 66 | } 67 | }) 68 | }) 69 | 70 | it('should start cpu profiler', done => { 71 | monitor.on('message', (data) => { 72 | const packet = JSON.parse(data.toString()).packet as Packet 73 | if (packet.type !== PacketType.START) return 74 | assert.strictEqual(packet.type, PacketType.START) 75 | assert.strictEqual((packet as StartPacket).payload.type, ProfileType.CPU_PROFILE) 76 | monitor.removeAllListeners() 77 | return done() 78 | }) 79 | client.startProfiler({ 80 | type: PacketType.START, 81 | payload: { 82 | type: ProfileType.CPU_PROFILE 83 | }, 84 | identifier: 'dummy' 85 | }) 86 | }) 87 | 88 | it('should stop cpu profiler and ', done => { 89 | monitor.on('message', (data) => { 90 | const packet = JSON.parse(data.toString()).packet as Packet 91 | if (packet.type !== PacketType.PROFILE) return 92 | assert.strictEqual(packet.type, PacketType.PROFILE) 93 | const profile = (packet as ProfilePacket).payload.profile 94 | assert.strictEqual(profile.kind, ProfileType.CPU_PROFILE) 95 | assert(profile.data.toString().length > 10) 96 | monitor.removeAllListeners() 97 | return done() 98 | }) 99 | client.stopProfiler({ 100 | type: PacketType.STOP, 101 | payload: { 102 | type: ProfileType.CPU_PROFILE 103 | }, 104 | identifier: 'dummy' 105 | }) 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "build", 5 | "rootDirs": ["src"], 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "strictNullChecks": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "test/**/*.ts" 18 | ], 19 | "compileOnSave": false 20 | } -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws-client/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws/README.md: -------------------------------------------------------------------------------- 1 | # OpenProfiling NodeJS - Websocket Gateway 2 | 3 | This package is intented to be used as a standalone -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openprofiling/gateway-ws", 3 | "version": "0.2.2", 4 | "main": "build/index.js", 5 | "types": "build/index.d.ts", 6 | "repository": "openprofiling/openprofiling-node", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.json", 9 | "lint": "tslint --project . src/**/*.ts", 10 | "test": "mocha -r ts-node/register ./test/*.spec.ts", 11 | "test-coverage": "nyc mocha -r ts-node/register test/*.spec.ts", 12 | "report-coverage": "nyc report --reporter=json && codecov -f coverage/*.json -p ../..", 13 | "ci": "yarn test-coverage && yarn report-coverage", 14 | "prepublishOnly": "yarn build" 15 | }, 16 | "keywords": [ 17 | "openprofiling", 18 | "nodejs", 19 | "tracing", 20 | "profiling" 21 | ], 22 | "author": "Valentin Marchaud", 23 | "license": "Apache-2.0", 24 | "engines": { 25 | "node": ">=10" 26 | }, 27 | "files": [ 28 | "build/", 29 | "doc", 30 | "CHANGELOG.md", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "extension": [ 39 | ".ts" 40 | ], 41 | "exclude": [ 42 | "build/", 43 | "config/", 44 | "examples/", 45 | "test/" 46 | ], 47 | "cache": true, 48 | "all": true 49 | }, 50 | "devDependencies": { 51 | "@types/ws": "^6.0.1", 52 | "codecov": "^3.4.0", 53 | "mocha": "^6.1.4", 54 | "nyc": "^14.1.1", 55 | "ts-node": "^8.2.0", 56 | "tslint": "^5.17.0", 57 | "tslint-config-standard": "^8.0.1", 58 | "typescript": "^3.7.4" 59 | }, 60 | "dependencies": { 61 | "@openprofiling/core": "^0.2.2", 62 | "ws": "^7.1.0" 63 | }, 64 | "gitHead": "a8098997633d0ffefed78ec7b4c12df64e47f3d5" 65 | } 66 | -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws/src/gateway.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as ws from 'ws' 3 | 4 | import * as types from './types' 5 | 6 | type WebsocketGatewayOptions = { 7 | port: number 8 | } 9 | 10 | type Options = WebsocketGatewayOptions & ws.ServerOptions 11 | 12 | export class WebsocketGateway { 13 | 14 | private server: ws.Server 15 | private options: WebsocketGatewayOptions 16 | private agents: types.AgentConnection[] = [] 17 | 18 | constructor (options: Options) { 19 | this.options = options 20 | this.server = new ws.Server(options) 21 | this.server.on('connection', this.onConnection.bind(this)) 22 | console.log(`Listening on port ${this.options.port}`) 23 | } 24 | 25 | onConnection (ws: ws) { 26 | const agentConnection: types.AgentConnection = { 27 | websocket: ws, 28 | identifier: Math.random().toString(36).substring(2, 15), 29 | metadata: { 30 | name: 'unset', 31 | attributes: {} 32 | }, 33 | mode: types.AgentMode.NORMAL 34 | } 35 | this.agents.push(agentConnection) 36 | ws.on('ping', _ => ws.pong()) 37 | ws.on('close', () => { 38 | const index = this.agents.findIndex(agent => agent === agentConnection) 39 | if (index === -1) return 40 | this.agents.splice(index, 1) 41 | // broadcast the disconnect to every monitor agent 42 | this.agents.filter(agent => agent.mode === types.AgentMode.MONITOR).forEach(agent => { 43 | agent.websocket.send(JSON.stringify({ 44 | packet: { 45 | type: types.PacketType.CUSTOM, 46 | payload: 'agent-disconnect' 47 | }, 48 | agent: { 49 | identifier: agent.identifier, 50 | mode: agent.mode, 51 | metadata: agent.metadata 52 | } 53 | })) 54 | }) 55 | }) 56 | ws.on('error', _ => ws.close()) 57 | ws.on('message', (data) => { 58 | try { 59 | const parsed = JSON.parse(data.toString()) as types.Packet 60 | // broadcast every message to agent in monitor mode 61 | this.agents.filter(agent => agent.mode === types.AgentMode.MONITOR).forEach(agent => { 62 | agent.websocket.send(JSON.stringify({ 63 | packet: parsed, 64 | agent: { 65 | identifier: agent.identifier, 66 | mode: agent.mode, 67 | metadata: agent.metadata 68 | } 69 | })) 70 | }) 71 | // then process the PacketType 72 | switch (parsed.type) { 73 | case types.PacketType.LIST: { 74 | return this.list(parsed as types.ListPacket, agentConnection) 75 | } 76 | case types.PacketType.START: { 77 | return this.start(parsed as types.StartPacket, agentConnection) 78 | } 79 | case types.PacketType.STOP: { 80 | return this.stop(parsed as types.StopPacket, agentConnection) 81 | } 82 | case types.PacketType.HELLO: { 83 | return this.hello(parsed as types.HelloPacket, agentConnection) 84 | } 85 | case types.PacketType.MONITOR: { 86 | return this.monitor(parsed as types.MonitorPacket, agentConnection) 87 | } 88 | } 89 | } catch (err) { 90 | console.error(`Error with parsing msg`, err) 91 | ws.close() 92 | } 93 | }) 94 | // broadcast connection of new agent 95 | this.agents.filter(agent => agent.mode === types.AgentMode.MONITOR).forEach(agent => { 96 | agent.websocket.send(JSON.stringify({ 97 | packet: { 98 | type: types.PacketType.CUSTOM, 99 | payload: 'agent-connect' 100 | }, 101 | agent: { 102 | identifier: agent.identifier, 103 | mode: agent.mode, 104 | metadata: agent.metadata 105 | } 106 | })) 107 | }) 108 | } 109 | 110 | list (packet: types.ListPacket, agent: types.AgentConnection) { 111 | agent.websocket.send(JSON.stringify({ 112 | type: types.PacketType.LIST, 113 | payload: this.agents.map(agent => { 114 | return { 115 | identifier: agent.identifier, 116 | mode: agent.mode, 117 | metadata: agent.metadata 118 | } 119 | }) 120 | })) 121 | } 122 | 123 | start (packet: types.StartPacket, agent: types.AgentConnection) { 124 | const target = this.agents.find(agent => agent.identifier === packet.identifier) 125 | if (target === undefined) { 126 | return agent.websocket.send(JSON.stringify({ 127 | type: types.PacketType.START, 128 | payload: { 129 | result: 'failed', 130 | message: `Agent with identifier ${packet.identifier} not found` 131 | } 132 | })) 133 | } 134 | target.websocket.send(JSON.stringify(packet)) 135 | agent.websocket.send(JSON.stringify({ 136 | type: types.PacketType.START, 137 | payload: { 138 | result: 'succedeed', 139 | message: `Packet broadcasted to agent` 140 | } 141 | })) 142 | } 143 | 144 | stop (packet: types.StopPacket, agent: types.AgentConnection) { 145 | const target = this.agents.find(agent => agent.identifier === packet.identifier) 146 | if (target === undefined) { 147 | return agent.websocket.send(JSON.stringify({ 148 | type: types.PacketType.START, 149 | payload: { 150 | result: 'failed', 151 | message: `Agent with identifier ${packet.identifier} not found` 152 | } 153 | })) 154 | } 155 | target.websocket.send(JSON.stringify(packet)) 156 | agent.websocket.send(JSON.stringify({ 157 | type: types.PacketType.START, 158 | payload: { 159 | result: 'succedeed' 160 | } 161 | })) 162 | } 163 | 164 | hello (packet: types.HelloPacket, agent: types.AgentConnection) { 165 | agent.metadata = packet.payload 166 | agent.websocket.send(JSON.stringify({ 167 | type: types.PacketType.HELLO, 168 | payload: { 169 | result: 'succedeed' 170 | } 171 | })) 172 | } 173 | 174 | monitor (packet: types.MonitorPacket, agent: types.AgentConnection) { 175 | agent.mode = types.AgentMode.MONITOR 176 | agent.websocket.send(JSON.stringify({ 177 | type: types.PacketType.MONITOR, 178 | payload: { 179 | result: 'succedeed' 180 | } 181 | })) 182 | } 183 | 184 | destroy () { 185 | this.server.close() 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws/src/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export * from './gateway' 4 | export * from './types' 5 | -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws/src/runner.ts: -------------------------------------------------------------------------------- 1 | 2 | import { WebsocketGateway } from './index' 3 | 4 | const options = { 5 | port: process.env.PORT ? parseInt(process.env.PORT, 10) : 8080 6 | } 7 | 8 | // tslint:disable-next-line 9 | new WebsocketGateway(options) 10 | -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws/src/types.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as ws from 'ws' 3 | import { ProfileType, Profile } from '@openprofiling/core' 4 | 5 | export enum PacketType { 6 | LIST, 7 | START, 8 | STOP, 9 | HELLO, 10 | MONITOR, 11 | PROFILE, 12 | CUSTOM 13 | } 14 | 15 | export type AgentMetadata = { 16 | name: string, 17 | attributes: { 18 | [key: string]: unknown 19 | } 20 | } 21 | 22 | export enum AgentMode { 23 | NORMAL, 24 | MONITOR 25 | } 26 | 27 | export type AgentConnection = { 28 | websocket: ws 29 | metadata: AgentMetadata 30 | identifier: string 31 | mode: AgentMode 32 | } 33 | 34 | export type SerializedAgentConnection = { 35 | metadata: AgentMetadata 36 | identifier: string 37 | mode: AgentMode 38 | } 39 | 40 | export interface Packet { 41 | type: PacketType, 42 | payload: unknown 43 | } 44 | 45 | export interface ListPacket extends Packet { 46 | type: PacketType.LIST, 47 | payload: SerializedAgentConnection[] 48 | } 49 | 50 | export interface HelloPacket extends Packet { 51 | type: PacketType.HELLO, 52 | payload: { 53 | name: string 54 | attributes: { 55 | [key: string]: unknown 56 | } 57 | } 58 | } 59 | 60 | export interface StartPacket extends Packet { 61 | type: PacketType.START, 62 | payload: { 63 | type: ProfileType 64 | }, 65 | identifier: string 66 | } 67 | 68 | export interface ProfilePacket extends Packet { 69 | type: PacketType.PROFILE, 70 | payload: { 71 | profile: Profile 72 | } 73 | } 74 | 75 | export interface StopPacket extends Packet { 76 | type: PacketType.STOP, 77 | payload: { 78 | type: ProfileType 79 | }, 80 | identifier: string 81 | } 82 | 83 | export interface MonitorPacket extends Packet { 84 | type: PacketType.MONITOR 85 | payload: {} 86 | } 87 | 88 | export interface CustomPacket extends Packet { 89 | type: PacketType.CUSTOM, 90 | payload: unknown 91 | } 92 | 93 | export type AnyPacket = StopPacket | StartPacket | ListPacket 94 | -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws/test/gateway.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | WebsocketGateway, 4 | PacketType, 5 | ListPacket, 6 | Packet, 7 | StartPacket, 8 | StopPacket, 9 | HelloPacket, 10 | SerializedAgentConnection, 11 | CustomPacket 12 | } from '../src/index' 13 | import { EventEmitter } from 'events' 14 | import * as assert from 'assert' 15 | import { ProfileType } from '@openprofiling/core' 16 | 17 | class FakeWSClient extends EventEmitter { 18 | constructor (private sendCallback: Function) { 19 | super() 20 | } 21 | send () { 22 | return this.sendCallback.apply(this, arguments) 23 | } 24 | } 25 | 26 | describe('Websocket Gateway Test', () => { 27 | 28 | let server: WebsocketGateway 29 | const listPacket = Buffer.from(JSON.stringify({ 30 | type: PacketType.LIST, 31 | payload: null 32 | })) 33 | const monitorPacket = Buffer.from(JSON.stringify({ 34 | type: PacketType.MONITOR, 35 | payload: null 36 | })) 37 | const getStartPacket = (identifier: string) => Buffer.from(JSON.stringify({ 38 | type: PacketType.START, 39 | payload: { 40 | type: ProfileType.CPU_PROFILE 41 | }, 42 | identifier 43 | })) 44 | const getStopPacket = (identifier: string) => Buffer.from(JSON.stringify({ 45 | type: PacketType.STOP, 46 | payload: {}, 47 | identifier 48 | })) 49 | 50 | after(() => { 51 | server.destroy() 52 | }) 53 | 54 | it('should instanciate correctly the server', () => { 55 | server = new WebsocketGateway({ port: 0 }) 56 | }) 57 | 58 | it('should process a list packet and return the list', (done) => { 59 | const fakeClient = new FakeWSClient((data: string) => { 60 | const msg = JSON.parse(data) as ListPacket 61 | assert(msg.type === PacketType.LIST) 62 | assert(msg.payload instanceof Array) 63 | assert(msg.payload.length > 0) 64 | fakeClient.emit('close') 65 | return done() 66 | }) 67 | // @ts-ignore: used to mock an websocket client 68 | server.onConnection(fakeClient) 69 | fakeClient.emit('message', listPacket) 70 | }) 71 | 72 | it('should process a hello packet and save the metadata', (done) => { 73 | const helloPacket = Buffer.from(JSON.stringify({ 74 | type: PacketType.HELLO, 75 | payload: { 76 | name: 'test', 77 | attributes: { 78 | server: 'test' 79 | } 80 | } 81 | })) 82 | const fakeClient = new FakeWSClient((data: string) => { 83 | const msg = JSON.parse(data) as Packet 84 | if (msg.type !== PacketType.LIST) return 85 | const list = msg as ListPacket 86 | assert(list.payload instanceof Array) 87 | assert(list.payload.length > 0) 88 | assert(list.payload[0].metadata.name === 'test') 89 | assert(list.payload[0].metadata.attributes.server === 'test') 90 | fakeClient.emit('close') 91 | return done() 92 | }) 93 | // @ts-ignore: used to mock an websocket client 94 | server.onConnection(fakeClient) 95 | fakeClient.emit('message', helloPacket) 96 | fakeClient.emit('message', listPacket) 97 | }) 98 | 99 | it('should process and broadcast a start packet', (done) => { 100 | let identifier: string 101 | const firstClient = new FakeWSClient((data: string) => { 102 | const msg = JSON.parse(data) as Packet 103 | if (msg.type !== PacketType.LIST) return 104 | const list = msg as ListPacket 105 | assert(list.payload instanceof Array) 106 | assert(list.payload.length > 0) 107 | identifier = list.payload[0].identifier 108 | firstClient.emit('message', getStartPacket(identifier)) 109 | }) 110 | const secondClient = new FakeWSClient((data: string) => { 111 | const msg = JSON.parse(data) as Packet 112 | if (msg.type !== PacketType.START) return 113 | const start = msg as StartPacket 114 | assert(start.identifier === identifier) 115 | assert(start.payload.type === ProfileType.CPU_PROFILE) 116 | secondClient.emit('close') 117 | firstClient.emit('close') 118 | return done() 119 | }) 120 | // @ts-ignore: used to mock an websocket client 121 | server.onConnection(secondClient) 122 | // @ts-ignore: used to mock an websocket client 123 | server.onConnection(firstClient) 124 | firstClient.emit('message', listPacket) 125 | }) 126 | 127 | it('should process and broadcast a stop packet', (done) => { 128 | let identifier: string 129 | const firstClient = new FakeWSClient((data: string) => { 130 | const msg = JSON.parse(data) as Packet 131 | if (msg.type !== PacketType.LIST) return 132 | const list = msg as ListPacket 133 | assert(list.payload instanceof Array) 134 | assert(list.payload.length > 0) 135 | identifier = list.payload[0].identifier 136 | firstClient.emit('message', getStopPacket(identifier)) 137 | }) 138 | const secondClient = new FakeWSClient((data: string) => { 139 | const msg = JSON.parse(data) as Packet 140 | if (msg.type !== PacketType.STOP) return 141 | const start = msg as StopPacket 142 | assert(start.identifier === identifier) 143 | secondClient.emit('close') 144 | firstClient.emit('close') 145 | return done() 146 | }) 147 | // @ts-ignore: used to mock an websocket client 148 | server.onConnection(secondClient) 149 | // @ts-ignore: used to mock an websocket client 150 | server.onConnection(firstClient) 151 | firstClient.emit('message', listPacket) 152 | }) 153 | 154 | it('should process monitor packet and broadcast every packet to client', (done) => { 155 | const helloPacket = Buffer.from(JSON.stringify({ 156 | type: PacketType.HELLO, 157 | payload: { 158 | name: 'test', 159 | attributes: { 160 | server: 'test' 161 | } 162 | } 163 | })) 164 | const firstClient = new FakeWSClient((data: string) => { 165 | const packet = JSON.parse(data) 166 | // we are only looking for packet from the monitor mode 167 | if (packet.packet === undefined) return 168 | const msg = packet.packet as Packet 169 | const agent = packet.agent as SerializedAgentConnection 170 | if (msg.type !== PacketType.HELLO) return 171 | const hello = msg as HelloPacket 172 | assert(hello.payload.name === 'test') 173 | assert(hello.payload.attributes.server === 'test') 174 | firstClient.emit('close') 175 | secondClient.emit('close') 176 | return done() 177 | }) 178 | const secondClient = new FakeWSClient(() => { 179 | return 180 | }) 181 | // @ts-ignore: used to mock an websocket client 182 | server.onConnection(firstClient) 183 | // @ts-ignore: used to mock an websocket client 184 | server.onConnection(secondClient) 185 | firstClient.emit('message', monitorPacket) 186 | secondClient.emit('message', helloPacket) 187 | }) 188 | 189 | it('should process monitor packet and broadcast connect', (done) => { 190 | const firstClient = new FakeWSClient((data: string) => { 191 | const packet = JSON.parse(data) 192 | // we are only looking for packet from the monitor mode 193 | if (packet.packet === undefined) return 194 | const msg = packet.packet as Packet 195 | const agent = packet.agent as SerializedAgentConnection 196 | if (msg.type !== PacketType.CUSTOM) return 197 | const custom = msg as CustomPacket 198 | assert(custom.payload === 'agent-connect') 199 | firstClient.emit('close') 200 | secondClient.emit('close') 201 | return done() 202 | }) 203 | const secondClient = new FakeWSClient(() => { 204 | return 205 | }) 206 | // @ts-ignore: used to mock an websocket client 207 | server.onConnection(firstClient) 208 | firstClient.emit('message', monitorPacket) 209 | // @ts-ignore: used to mock an websocket client 210 | server.onConnection(secondClient) 211 | }) 212 | 213 | it('should process monitor packet and broadcast disconnect', (done) => { 214 | const firstClient = new FakeWSClient((data: string) => { 215 | const packet = JSON.parse(data) 216 | // we are only looking for packet from the monitor mode 217 | if (packet.packet === undefined) return 218 | const msg = packet.packet as Packet 219 | const agent = packet.agent as SerializedAgentConnection 220 | if (msg.type !== PacketType.CUSTOM) return 221 | const custom = msg as CustomPacket 222 | assert(custom.payload === 'agent-disconnect') 223 | firstClient.emit('close') 224 | return done() 225 | }) 226 | const secondClient = new FakeWSClient(() => { 227 | return 228 | }) 229 | // @ts-ignore: used to mock an websocket client 230 | server.onConnection(secondClient) 231 | // @ts-ignore: used to mock an websocket client 232 | server.onConnection(firstClient) 233 | firstClient.emit('message', monitorPacket) 234 | secondClient.emit('close') 235 | }) 236 | }) 237 | -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "build", 5 | "rootDirs": ["src"], 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "strictNullChecks": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "test/**/*.ts" 18 | ], 19 | "compileOnSave": false 20 | } -------------------------------------------------------------------------------- /packages/openprofiling-gateway-ws/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-cpu-profiler/README.md: -------------------------------------------------------------------------------- 1 | # OpenProfiling NodeJS - Inspector-based Sampling Javascript CPU Profiler 2 | 3 | This profiler is the recomended one to profile the CPU usage of your NodeJS application. It has a almost-zero impact on performance and specially suited for long-lived application. 4 | 5 | ### Advantages 6 | 7 | - This profiler works by forking a new specific thread, which will capture the main thread stacktrace (list of functions) every 1ms (by default), then aggregate the result to have approximatilly the CPU usage for a given period. **This implementation have the advantage that it doesn't run any additional code on the main thread which doesn't slow your application.** 8 | - The fact that is use the core `inspector` module means that it's **available out of the box without installing any dependency**. 9 | 10 | ### Drawbacks 11 | 12 | - The **sampling approach means that you never record every functions**, while it's possible that it doesn't record a function, it's highly impropable that it doesn't record it over one minute, specially if the function is called often. Put simply, **the more a given function use CPU time, the more it had the chance to be recorded**. 13 | - This profiler only record Javascript functions, which is generally enough, you will not be able to profile any C/C++ code running (eiter from V8, libuv, NodeJS or a native addon). 14 | - If you are using Node 8, the `inspector` module can have only one session for a given process, means that if another dependency (generally APM vendors) already use it, you will have errors either in `openprofiling` or the other module. 15 | - Some V8 versions [has specific bug](https://bugs.chromium.org/p/v8/issues/detail?id=6623) that can impact your application, that's why it's not available for all versions in node 8. 16 | - Since it doesn't record every function call, **the profiler isn't suited for short-lived application** (ex: serverless) or to record cpu usage for a given request. 17 | 18 | ### How to use 19 | 20 | In the following example, you will need to send the `SIGUSR2` signal to the process to start the profiling and again when do you want to end it: 21 | 22 | ```ts 23 | import { ProfilingAgent } from '@openprofiling/nodejs' 24 | import { FileExporter } from '@openprofiling/exporter-file' 25 | import { InspectorCPUProfiler } from '@openprofiling/inspector-cpu-profiler' 26 | import { SignalTrigger } from '@openprofiling/trigger-signal' 27 | 28 | const profilingAgent = new ProfilingAgent() 29 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR2' }), new InspectorCPUProfiler()) 30 | profilingAgent.start({ exporter: new FileExporter() }) 31 | ``` 32 | 33 | If you are using Node 8 and multiple profilers (ex: using both heap and cpu profiler), you will need to share a `inspector` session like this: 34 | 35 | ```ts 36 | import { ProfilingAgent } from '@openprofiling/nodejs' 37 | import { FileExporter } from '@openprofiling/exporter-file' 38 | import { InspectorCPUProfiler } from '@openprofiling/inspector-cpu-profiler' 39 | import { InspectorHeapProfiler } from '@openprofiling/inspector-heap-profiler' 40 | import { SignalTrigger } from '@openprofiling/trigger-signal' 41 | import * as inspector from 'inspector' 42 | 43 | const profilingAgent = new ProfilingAgent() 44 | // creation a session 45 | const session = new inspector.Session() 46 | // give it as parameters to the constructor 47 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR1' }), new InspectorHeapProfiler({ session })) 48 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR2' }), new InspectorCPUProfiler({ session })) 49 | profilingAgent.start({ exporter: new FileExporter() }) 50 | ``` 51 | 52 | After starting your process, you just need to send to it the configured signal: 53 | - linux/macos: `kill -s USR2 ` 54 | - kubectl: `kubectl exec -ti /bin/kill -s USR2 1` (assuming your process is the pid 1) 55 | 56 | You can find the pid either by `console.log(process.pid)` when your process start or use `ps aux | grep node` and looking for your process. 57 | The first time you send the signal, it will start the profiler which will start recording memory allocation, you should then wait for your memory leak to happen again. 58 | When you think you collected enough data (either you reproduced the leak or you believe there enought data), you just need to send the same signal as above. 59 | The profiling agent will then write the file to the disk (by default in `/tmp`), it should start with `cpu-profile`. 60 | 61 | 62 | ### Vizualisation 63 | 64 | After retrieving the cpu profile file where it has been exported, it should have a `.cpuprofile` extension. Which is the standard extension for this type of data. 65 | You have multiple ways to read the output, here the list of (known) tools that you can use : 66 | - Chrome Developers Tools: https://developers.google.com/web/updates/2016/12/devtools-javascript-cpu-profile-migration#old 67 | - Speedscope: https://github.com/jlfwong/speedscope 68 | - Flamebearer: https://github.com/mapbox/flamebearer -------------------------------------------------------------------------------- /packages/openprofiling-inspector-cpu-profiler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openprofiling/inspector-cpu-profiler", 3 | "version": "0.2.2", 4 | "main": "build/index.js", 5 | "types": "build/index.d.ts", 6 | "repository": "openprofiling/openprofiling-node", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.json", 9 | "lint": "tslint --project . src/**/*.ts", 10 | "test": "mocha -r ts-node/register ./test/*.spec.ts", 11 | "test-coverage": "nyc mocha -r ts-node/register test/*.spec.ts", 12 | "report-coverage": "nyc report --reporter=json && codecov -f coverage/*.json -p ../..", 13 | "ci": "yarn test-coverage && yarn report-coverage", 14 | "prepublishOnly": "yarn build" 15 | }, 16 | "keywords": [ 17 | "openprofiling", 18 | "nodejs", 19 | "tracing", 20 | "profiling" 21 | ], 22 | "author": "Valentin Marchaud", 23 | "license": "Apache-2.0", 24 | "engines": { 25 | "node": ">=10.0.0" 26 | }, 27 | "files": [ 28 | "build/", 29 | "doc", 30 | "CHANGELOG.md", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "extension": [ 39 | ".ts" 40 | ], 41 | "exclude": [ 42 | "build/", 43 | "config/", 44 | "examples/", 45 | "test/" 46 | ], 47 | "cache": true, 48 | "all": true 49 | }, 50 | "devDependencies": { 51 | "codecov": "^3.4.0", 52 | "mocha": "^6.1.4", 53 | "nyc": "^14.1.1", 54 | "ts-node": "^8.2.0", 55 | "tslint": "^5.17.0", 56 | "tslint-config-standard": "^8.0.1", 57 | "typescript": "^3.7.4" 58 | }, 59 | "dependencies": { 60 | "@openprofiling/core": "^0.2.2" 61 | }, 62 | "gitHead": "a8098997633d0ffefed78ec7b4c12df64e47f3d5" 63 | } 64 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-cpu-profiler/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './inspector-cpu-profiler' 3 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-cpu-profiler/src/inspector-cpu-profiler.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BaseProfiler, Trigger, TriggerState, ProfilerOptions, CoreAgent, Profile, ProfileType, ProfileStatus, TriggerOptions, TriggerEventOptions } from '@openprofiling/core' 3 | import * as inspector from 'inspector' 4 | 5 | export interface InspectorCPUProfilerOptions extends ProfilerOptions { 6 | session?: inspector.Session 7 | } 8 | 9 | export class InspectorCPUProfiler extends BaseProfiler { 10 | 11 | private session: inspector.Session | undefined 12 | private started: boolean = false 13 | private currentProfile: Profile | undefined 14 | 15 | protected options: InspectorCPUProfilerOptions 16 | 17 | constructor (options?: InspectorCPUProfilerOptions) { 18 | super('inspector-cpu', options) 19 | } 20 | 21 | init () { 22 | if (typeof this.options.session === 'object') { 23 | this.session = this.options.session 24 | // try to connect the session if not already the case 25 | try { 26 | this.session.connect() 27 | } catch (err) { 28 | this.logger.debug('failed to connect to given session', err.message) 29 | } 30 | } else { 31 | this.session = new inspector.Session() 32 | try { 33 | this.session.connect() 34 | } catch (err) { 35 | this.logger.error('Could not connect to inspector', err.message) 36 | return 37 | } 38 | } 39 | this.session.post('Profiler.enable') 40 | } 41 | 42 | destroy () { 43 | if (this.session === undefined) return 44 | 45 | if (this.started === true) { 46 | this.stopProfiling() 47 | } 48 | this.session.post('Profiler.disable') 49 | // if we openned a new session internally, we need to close it 50 | // as only one session can be openned in node 8 51 | if (this.options.session === undefined) { 52 | this.session.disconnect() 53 | } 54 | } 55 | 56 | async onTrigger (state: TriggerState, options: TriggerEventOptions) { 57 | if (this.session === undefined) { 58 | throw new Error(`Session wasn't initialized`) 59 | } 60 | if (state === TriggerState.START && this.started === true) { 61 | this.logger.info('Received start trigger but already started, ignoring') 62 | return 63 | } 64 | if (state === TriggerState.END && this.started === false) { 65 | this.logger.error('Received end trigger but wasnt started, ignoring') 66 | return 67 | } 68 | 69 | if (state === TriggerState.START) { 70 | this.logger.info(`Starting profiling`) 71 | this.currentProfile = new Profile(options.name || 'noname', ProfileType.CPU_PROFILE) 72 | if (options.attributes) { 73 | this.currentProfile.attributes = options.attributes 74 | } 75 | this.started = true 76 | // start the idle time reporter to tell V8 when node is idle 77 | // See https://github.com/nodejs/node/issues/19009#issuecomment-403161559. 78 | if (process.hasOwnProperty('_startProfilerIdleNotifier') === true) { 79 | (process as any)._startProfilerIdleNotifier() 80 | } 81 | this.session.post('Profiler.start') 82 | this.agent.notifyStartProfile(this.currentProfile) 83 | return 84 | } 85 | 86 | if (state === TriggerState.END) { 87 | this.logger.info(`Stopping profiling`) 88 | this.stopProfiling() 89 | } 90 | } 91 | 92 | private stopProfiling () { 93 | if (this.session === undefined) { 94 | throw new Error(`Session wasn't initialized`) 95 | } 96 | this.session.post('Profiler.stop', (err, params) => { 97 | // stop the idle time reporter to tell V8 when node is idle 98 | // See https://github.com/nodejs/node/issues/19009#issuecomment-403161559. 99 | if (process.hasOwnProperty('_stopProfilerIdleNotifier') === true) { 100 | (process as any)._stopProfilerIdleNotifier() 101 | } 102 | 103 | if (this.currentProfile === undefined) return 104 | if (err) { 105 | this.logger.error(`Failed to stop cpu profiler`, err.message) 106 | this.currentProfile.end(err) 107 | } else { 108 | const data = JSON.stringify(params.profile) 109 | this.currentProfile.addProfileData(Buffer.from(data)) 110 | this.currentProfile.addAttribute('profiler', this.name) 111 | this.currentProfile.end() 112 | } 113 | this.agent.notifyEndProfile(this.currentProfile) 114 | this.started = false 115 | this.currentProfile = undefined 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-cpu-profiler/test/inspector-cpu.spec.ts: -------------------------------------------------------------------------------- 1 | import { InspectorCPUProfiler } from '../src' 2 | import * as assert from 'assert' 3 | import * as inspector from 'inspector' 4 | import { CoreAgent, BaseProfiler, TriggerState, Profile, BaseTrigger, Exporter, ProfileType, ProfileStatus } from '@openprofiling/core' 5 | 6 | class DummyTrigger extends BaseTrigger { 7 | constructor () { 8 | super('dummny-trigger') 9 | } 10 | 11 | init () { 12 | return 13 | } 14 | 15 | destroy () { 16 | return 17 | } 18 | 19 | trigger (state: TriggerState) { 20 | this.agent.onTrigger(state, { source: this }) 21 | } 22 | } 23 | 24 | type onProfile = (profile: Profile) => void 25 | 26 | class DummyExporter implements Exporter { 27 | 28 | private onStart: onProfile | undefined 29 | private onEnd: onProfile | undefined 30 | 31 | constructor (onStart?: onProfile, onEnd?: onProfile) { 32 | this.onEnd = onEnd 33 | this.onStart = onStart 34 | } 35 | 36 | async onProfileStart (profile) { 37 | if (typeof this.onStart === 'function') { 38 | this.onStart(profile) 39 | } 40 | } 41 | 42 | async onProfileEnd (profile) { 43 | if (typeof this.onEnd === 'function') { 44 | this.onEnd(profile) 45 | } 46 | } 47 | 48 | enable () { 49 | return 50 | } 51 | 52 | disable () { 53 | return 54 | } 55 | } 56 | 57 | describe('Inspector Heap Profiler', () => { 58 | 59 | const agent: CoreAgent = new CoreAgent() 60 | let profiler = new InspectorCPUProfiler({}) 61 | const trigger = new DummyTrigger() 62 | 63 | before(async () => { 64 | profiler.enable(agent) 65 | trigger.enable(agent) 66 | agent.start({ 67 | reactions: [ 68 | { 69 | profiler, 70 | trigger 71 | } 72 | ], 73 | logLevel: 4 74 | }) 75 | }) 76 | 77 | it('should export a profiler implementation', () => { 78 | assert(InspectorCPUProfiler.prototype instanceof BaseProfiler) 79 | }) 80 | 81 | it('should receive profile start from profiler', (done) => { 82 | const listener = new DummyExporter(profile => { 83 | assert(profile.kind === ProfileType.CPU_PROFILE) 84 | assert(profile.status === ProfileStatus.UNKNOWN) 85 | agent.unregisterProfileListener(listener) 86 | return done() 87 | }) 88 | agent.registerProfileListener(listener) 89 | trigger.trigger(TriggerState.START) 90 | }) 91 | 92 | it('should receive profile end from profiler', (done) => { 93 | const listener = new DummyExporter(undefined, profile => { 94 | assert(profile.kind === ProfileType.CPU_PROFILE) 95 | assert(profile.status === ProfileStatus.SUCCESS) 96 | assert(profile.data.length > 10) 97 | agent.unregisterProfileListener(listener) 98 | return done() 99 | }) 100 | agent.registerProfileListener(listener) 101 | trigger.trigger(TriggerState.END) 102 | }) 103 | 104 | it('should stop profile if profiler is destroyed', (done) => { 105 | const listener = new DummyExporter(undefined, profile => { 106 | assert(profile.kind === ProfileType.CPU_PROFILE) 107 | assert(profile.status === ProfileStatus.SUCCESS) 108 | agent.unregisterProfileListener(listener) 109 | return done() 110 | }) 111 | agent.registerProfileListener(listener) 112 | trigger.trigger(TriggerState.START) 113 | setTimeout(_ => { 114 | profiler.disable() 115 | }, 100) 116 | }) 117 | 118 | describe('should work with custom inspector session', () => { 119 | let session: inspector.Session 120 | 121 | it('should setup use of custom session', () => { 122 | agent.stop() 123 | session = new inspector.Session() 124 | session.connect() 125 | profiler = new InspectorCPUProfiler({ session }) 126 | profiler.enable(agent) 127 | agent.start({ 128 | reactions: [ 129 | { 130 | profiler, 131 | trigger 132 | } 133 | ], 134 | logLevel: 4 135 | }) 136 | }) 137 | 138 | it('should take a profile succesfully', (done) => { 139 | const listener = new DummyExporter(undefined, profile => { 140 | assert(profile.kind === ProfileType.CPU_PROFILE) 141 | assert(profile.status === ProfileStatus.SUCCESS) 142 | agent.unregisterProfileListener(listener) 143 | // see https://github.com/nodejs/node/issues/27641 144 | setImmediate(_ => { 145 | session.disconnect() 146 | }) 147 | return done() 148 | }) 149 | agent.registerProfileListener(listener) 150 | trigger.trigger(TriggerState.START) 151 | setTimeout(_ => { 152 | trigger.trigger(TriggerState.END) 153 | }, 100) 154 | }) 155 | }) 156 | 157 | describe('should work with custom inspector session (not opened)', () => { 158 | let session: inspector.Session 159 | 160 | it('should setup use of custom session', () => { 161 | agent.stop() 162 | session = new inspector.Session() 163 | profiler = new InspectorCPUProfiler({ session }) 164 | profiler.enable(agent) 165 | agent.start({ 166 | reactions: [ 167 | { 168 | profiler, 169 | trigger 170 | } 171 | ], 172 | logLevel: 4 173 | }) 174 | }) 175 | 176 | it('should take a profile succesfully', (done) => { 177 | const listener = new DummyExporter(undefined, profile => { 178 | assert(profile.kind === ProfileType.CPU_PROFILE) 179 | assert(profile.status === ProfileStatus.SUCCESS) 180 | agent.unregisterProfileListener(listener) 181 | // see https://github.com/nodejs/node/issues/27641 182 | setImmediate(_ => { 183 | session.disconnect() 184 | }) 185 | return done() 186 | }) 187 | agent.registerProfileListener(listener) 188 | trigger.trigger(TriggerState.START) 189 | setTimeout(_ => { 190 | trigger.trigger(TriggerState.END) 191 | }, 100) 192 | }) 193 | }) 194 | }) 195 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-cpu-profiler/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "build", 5 | "rootDirs": ["src"], 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "strictNullChecks": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "test/**/*.ts" 18 | ], 19 | "compileOnSave": false 20 | } -------------------------------------------------------------------------------- /packages/openprofiling-inspector-cpu-profiler/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-heap-profiler/README.md: -------------------------------------------------------------------------------- 1 | # OpenProfiling NodeJS - Inspector-based Sampling Javascript Heap Profiler 2 | 3 | This profiler is the recomended one to profile the memory usage per function of your NodeJS application. It has a almost-zero impact on performance and specially suited for long-lived application. 4 | 5 | **NOTE**: Do not confuse the Sampling Heap Profiler (which is the same as `Allocation Sampling`) with the `Allocation Instrumentation`. The Instrumentation based is useful for recording *every* allocation but it was a greater impact in performance (and is not available currently in openprofiling). 6 | 7 | ### Advantages 8 | 9 | - The profiler will only record allocation made each X bytes (by default every 8KiB) which it doesn't impact a lot in terms of performance, while it's not possible to give a specific percentage, the V8 team made a module on top of the same API and said that it was fine to use in production ([src]()https://github.com/v8/sampling-heap-profiler) 10 | - The fact that is use the core `inspector` module means that it's **available out of the box without installing any dependency**. 11 | 12 | ### Drawbacks 13 | 14 | - The **sampling approach means that you never record every memory allocations**, while it's possible that it doesn't record an allocation, it's highly impropable that it doesn't record it over one minute, specially if the function is called often. Put simply, **the more a given function allocate memory, the more it had the chance to be recorded**. 15 | - This profiler only record Javascript functions, which is generally enough, you will not be able to profile any C/C++ code running (eiter from V8, libuv, NodeJS or a native addon). 16 | - If you are using Node 8, the `inspector` module can have only one session for a given process, means that if another dependency (generally APM vendors) already use it, you will have errors either in `openprofiling` or the other module. 17 | - Some V8 versions [has specific bug](https://bugs.chromium.org/p/chromium/issues/detail?id=847863) that can impact your application, that's why it's not available for all versions in node 10. 18 | - Since it doesn't record every memory allocation, **the profiler isn't suited for short-lived application** (ex: serverless) or to record memory allocation for a given request. 19 | 20 | ### How to use 21 | 22 | In the following example, you will need to send the `SIGUSR2` signal to the process to start the profiling and again when do you want to end it: 23 | 24 | ```ts 25 | import { ProfilingAgent } from '@openprofiling/nodejs' 26 | import { FileExporter } from '@openprofiling/exporter-file' 27 | import { InspectorHeapProfiler } from '@openprofiling/inspector-heap-profiler' 28 | import { SignalTrigger } from '@openprofiling/trigger-signal' 29 | 30 | const profilingAgent = new ProfilingAgent() 31 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR2' }), new InspectorHeapProfiler()) 32 | profilingAgent.start({ exporter: new FileExporter() }) 33 | ``` 34 | 35 | If you are using Node 8 and multiple profilers (ex: using both heap and cpu profiler), you will need to share a `inspector` session like this: 36 | 37 | ```ts 38 | import { ProfilingAgent } from '@openprofiling/nodejs' 39 | import { FileExporter } from '@openprofiling/exporter-file' 40 | import { InspectorCPUProfiler } from '@openprofiling/inspector-cpu-profiler' 41 | import { InspectorHeapProfiler } from '@openprofiling/inspector-heap-profiler' 42 | import { SignalTrigger } from '@openprofiling/trigger-signal' 43 | import * as inspector from 'inspector' 44 | 45 | const profilingAgent = new ProfilingAgent() 46 | // creation a session 47 | const session = new inspector.Session() 48 | // give it as parameters to the constructor 49 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR2' }), new InspectorHeapProfiler({ session })) 50 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR1' }), new InspectorCPUProfiler({ session })) 51 | profilingAgent.start({ exporter: new FileExporter() }) 52 | ``` 53 | 54 | After starting your process, you just need to send to it the configured signal: 55 | - linux/macos: `kill -s USR2 ` 56 | - kubectl: `kubectl exec -ti /bin/kill -s USR2 1` (assuming your process is the pid 1) 57 | 58 | You can find the pid either by `console.log(process.pid)` when your process start or use `ps aux | grep node` and looking for your process. 59 | The first time you send the signal, it will start the profiler which will start recording memory allocation, you should then wait for your memory leak to happen again. 60 | When you think you collected enough data (either you reproduced the leak or you believe there enought data), you just need to send the same signal as above. 61 | The profiling agent will then write the file to the disk (by default in `/tmp`), it should start with `heap-profile`. 62 | 63 | ### Vizualisation 64 | 65 | After retrieving the cpu profile file where it has been exported, it should have a `.heapprofile` extension. Which is the standard extension for this type of data. 66 | You have multiple ways to read the output, here the list of (known) tools that you can use : 67 | - Chrome Developers Tools (Memory tab) 68 | - Speedscope: https://github.com/jlfwong/speedscope -------------------------------------------------------------------------------- /packages/openprofiling-inspector-heap-profiler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openprofiling/inspector-heap-profiler", 3 | "version": "0.2.2", 4 | "main": "build/index.js", 5 | "types": "build/index.d.ts", 6 | "repository": "openprofiling/openprofiling-node", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.json", 9 | "lint": "tslint --project . src/**/*.ts", 10 | "test": "mocha -r ts-node/register ./test/*.spec.ts", 11 | "test-coverage": "nyc mocha -r ts-node/register test/*.spec.ts", 12 | "report-coverage": "nyc report --reporter=json && codecov -f coverage/*.json -p ../..", 13 | "ci": "yarn test-coverage && yarn report-coverage", 14 | "prepublishOnly": "yarn build" 15 | }, 16 | "keywords": [ 17 | "openprofiling", 18 | "nodejs", 19 | "tracing", 20 | "profiling" 21 | ], 22 | "author": "Valentin Marchaud", 23 | "license": "Apache-2.0", 24 | "engines": { 25 | "node": ">=10.4.1" 26 | }, 27 | "files": [ 28 | "build/", 29 | "doc", 30 | "CHANGELOG.md", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "extension": [ 39 | ".ts" 40 | ], 41 | "exclude": [ 42 | "build/", 43 | "config/", 44 | "examples/", 45 | "test/" 46 | ], 47 | "cache": true, 48 | "all": true 49 | }, 50 | "devDependencies": { 51 | "codecov": "^3.4.0", 52 | "mocha": "^6.1.4", 53 | "nyc": "^14.1.1", 54 | "ts-node": "^8.2.0", 55 | "tslint": "^5.17.0", 56 | "tslint-config-standard": "^8.0.1", 57 | "typescript": "^3.7.4" 58 | }, 59 | "dependencies": { 60 | "@openprofiling/core": "^0.2.2" 61 | }, 62 | "gitHead": "a8098997633d0ffefed78ec7b4c12df64e47f3d5" 63 | } 64 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-heap-profiler/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './inspector-heap-profiler' 3 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-heap-profiler/src/inspector-heap-profiler.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BaseProfiler, Trigger, TriggerState, ProfilerOptions, Profile, ProfileType, ProfileStatus, TriggerEventOptions } from '@openprofiling/core' 3 | import * as inspector from 'inspector' 4 | 5 | export class InspectorHeapProfilerOptions implements ProfilerOptions { 6 | session?: inspector.Session 7 | /** 8 | * The less the interval is, the more the profiler is precise and the cost 9 | * in term of performance is high 10 | */ 11 | samplingInterval?: number 12 | } 13 | // if we openned a new session internally, we need to close it 14 | // as only one session can be openned in node 8 15 | 16 | export class InspectorHeapProfiler extends BaseProfiler { 17 | 18 | private session: inspector.Session | undefined 19 | private started: boolean = false 20 | private currentProfile: Profile | undefined 21 | 22 | protected options: InspectorHeapProfilerOptions 23 | 24 | constructor (options?: InspectorHeapProfilerOptions) { 25 | super('inspector-heap', options) 26 | } 27 | 28 | init () { 29 | if (typeof this.options.session === 'object') { 30 | this.session = this.options.session 31 | // try to connect the session if not already the case 32 | try { 33 | this.session.connect() 34 | } catch (err) { 35 | this.logger.debug('failed to connect to given session', err.message) 36 | } 37 | } else { 38 | this.session = new inspector.Session() 39 | try { 40 | this.session.connect() 41 | } catch (err) { 42 | this.logger.error('Could not connect to inspector', err.message) 43 | return 44 | } 45 | } 46 | this.session.post('HeapProfiler.enable') 47 | } 48 | 49 | destroy () { 50 | if (this.session === undefined) return 51 | 52 | if (this.started === true) { 53 | this.stopProfiling() 54 | } 55 | this.session.post('HeapProfiler.disable') 56 | // if we openned a new session internally, we need to close it 57 | // as only one session can be openned in node 8 58 | if (this.options.session === undefined) { 59 | this.session.disconnect() 60 | } 61 | } 62 | 63 | async onTrigger (state: TriggerState, options: TriggerEventOptions) { 64 | if (this.session === undefined) { 65 | throw new Error(`Session wasn't initialized`) 66 | } 67 | if (state === TriggerState.START && this.started === true) { 68 | this.logger.info('Received start trigger but already started, ignoring') 69 | return 70 | } 71 | if (state === TriggerState.END && this.started === false) { 72 | this.logger.error('Received end trigger but wasnt started, ignoring') 73 | return 74 | } 75 | 76 | if (state === TriggerState.START) { 77 | this.logger.info(`Starting profiling`) 78 | this.currentProfile = new Profile(options.name || 'noname', ProfileType.HEAP_PROFILE) 79 | if (options.attributes) { 80 | this.currentProfile.attributes = options.attributes 81 | } 82 | this.started = true 83 | this.session.post('HeapProfiler.startSampling', { 84 | samplingInterval: typeof this.options.samplingInterval === 'number' ? 85 | this.options.samplingInterval : 8 * 1024 86 | }) 87 | this.agent.notifyStartProfile(this.currentProfile) 88 | return 89 | } 90 | 91 | if (state === TriggerState.END) { 92 | this.logger.info(`Stopping profiling`) 93 | this.stopProfiling() 94 | } 95 | } 96 | 97 | private stopProfiling () { 98 | if (this.session === undefined) { 99 | throw new Error(`Session wasn't initialized`) 100 | } 101 | this.session.post('HeapProfiler.stopSampling', (err, params) => { 102 | if (this.currentProfile === undefined) return 103 | if (err) { 104 | this.logger.error(`Failed to stop Heap profiler`, err.message) 105 | this.currentProfile.end(err) 106 | } else { 107 | const data = JSON.stringify(params.profile) 108 | this.currentProfile.addProfileData(Buffer.from(data)) 109 | this.currentProfile.addAttribute('profiler', this.name) 110 | this.currentProfile.end() 111 | } 112 | 113 | this.agent.notifyEndProfile(this.currentProfile) 114 | this.started = false 115 | this.currentProfile = undefined 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-heap-profiler/test/inspector-heap.spec.ts: -------------------------------------------------------------------------------- 1 | import { InspectorHeapProfiler } from '../src' 2 | import * as assert from 'assert' 3 | import * as inspector from 'inspector' 4 | import { CoreAgent, BaseProfiler, TriggerState, Profile, BaseTrigger, Exporter, ProfileType, ProfileStatus } from '@openprofiling/core' 5 | 6 | class DummyTrigger extends BaseTrigger { 7 | constructor () { 8 | super('dummny-trigger') 9 | } 10 | 11 | init () { 12 | return 13 | } 14 | 15 | destroy () { 16 | return 17 | } 18 | 19 | trigger (state: TriggerState) { 20 | this.agent.onTrigger(state, { source: this }) 21 | } 22 | } 23 | 24 | type onProfile = (profile: Profile) => void 25 | 26 | class DummyExporter implements Exporter { 27 | 28 | private onStart: onProfile | undefined 29 | private onEnd: onProfile | undefined 30 | 31 | constructor (onStart?: onProfile, onEnd?: onProfile) { 32 | this.onEnd = onEnd 33 | this.onStart = onStart 34 | } 35 | 36 | async onProfileStart (profile) { 37 | if (typeof this.onStart === 'function') { 38 | this.onStart(profile) 39 | } 40 | } 41 | 42 | enable () { 43 | return 44 | } 45 | 46 | disable () { 47 | return 48 | } 49 | 50 | async onProfileEnd (profile) { 51 | if (typeof this.onEnd === 'function') { 52 | this.onEnd(profile) 53 | } 54 | } 55 | } 56 | 57 | describe('Inspector Heap Profiler', () => { 58 | 59 | const agent: CoreAgent = new CoreAgent() 60 | let profiler = new InspectorHeapProfiler({}) 61 | const trigger = new DummyTrigger() 62 | 63 | before(async () => { 64 | profiler.enable(agent) 65 | trigger.enable(agent) 66 | agent.start({ 67 | reactions: [ 68 | { 69 | profiler, 70 | trigger 71 | } 72 | ], 73 | logLevel: 4 74 | }) 75 | }) 76 | 77 | it('should export a profiler implementation', () => { 78 | assert(InspectorHeapProfiler.prototype instanceof BaseProfiler) 79 | }) 80 | 81 | it('should receive profile start from profiler', (done) => { 82 | const listener = new DummyExporter((profile: Profile) => { 83 | assert(profile.kind === ProfileType.HEAP_PROFILE) 84 | assert(profile.status === ProfileStatus.UNKNOWN) 85 | assert(profile.ended === false) 86 | agent.unregisterProfileListener(listener) 87 | return done() 88 | }) 89 | agent.registerProfileListener(listener) 90 | trigger.trigger(TriggerState.START) 91 | }) 92 | 93 | it('should receive profile end from profiler', (done) => { 94 | const listener = new DummyExporter(undefined, profile => { 95 | assert(profile.kind === ProfileType.HEAP_PROFILE) 96 | assert(profile.status === ProfileStatus.SUCCESS) 97 | assert(profile.ended === true) 98 | assert(profile.data.length > 10) 99 | agent.unregisterProfileListener(listener) 100 | return done() 101 | }) 102 | agent.registerProfileListener(listener) 103 | trigger.trigger(TriggerState.END) 104 | }) 105 | 106 | it('should stop profile if profiler is destroyed', (done) => { 107 | const listener = new DummyExporter(undefined, profile => { 108 | assert(profile.kind === ProfileType.HEAP_PROFILE) 109 | assert(profile.status === ProfileStatus.SUCCESS) 110 | assert(profile.ended === true) 111 | agent.unregisterProfileListener(listener) 112 | return done() 113 | }) 114 | agent.registerProfileListener(listener) 115 | trigger.trigger(TriggerState.START) 116 | setTimeout(_ => { 117 | profiler.disable() 118 | }, 100) 119 | }) 120 | 121 | describe('should work with custom inspector session', () => { 122 | let session: inspector.Session 123 | 124 | it('should setup use of custom session', () => { 125 | agent.stop() 126 | session = new inspector.Session() 127 | session.connect() 128 | profiler = new InspectorHeapProfiler({ session }) 129 | profiler.enable(agent) 130 | agent.start({ 131 | reactions: [ 132 | { 133 | profiler, 134 | trigger 135 | } 136 | ], 137 | logLevel: 4 138 | }) 139 | }) 140 | 141 | it('should take a profile succesfully', (done) => { 142 | const listener = new DummyExporter(undefined, profile => { 143 | assert(profile.kind === ProfileType.HEAP_PROFILE) 144 | assert(profile.status === ProfileStatus.SUCCESS) 145 | agent.unregisterProfileListener(listener) 146 | // see https://github.com/nodejs/node/issues/27641 147 | setImmediate(_ => { 148 | session.disconnect() 149 | }) 150 | return done() 151 | }) 152 | agent.registerProfileListener(listener) 153 | trigger.trigger(TriggerState.START) 154 | setTimeout(_ => { 155 | trigger.trigger(TriggerState.END) 156 | }, 100) 157 | }) 158 | }) 159 | 160 | describe('should work with custom inspector session (not opened)', () => { 161 | let session: inspector.Session 162 | 163 | it('should setup use of custom session', () => { 164 | agent.stop() 165 | session = new inspector.Session() 166 | profiler = new InspectorHeapProfiler({ session }) 167 | profiler.enable(agent) 168 | agent.start({ 169 | reactions: [ 170 | { 171 | profiler, 172 | trigger 173 | } 174 | ], 175 | logLevel: 4 176 | }) 177 | }) 178 | 179 | it('should take a profile succesfully', (done) => { 180 | const listener = new DummyExporter(undefined, profile => { 181 | assert(profile.kind === ProfileType.HEAP_PROFILE) 182 | assert(profile.status === ProfileStatus.SUCCESS) 183 | agent.unregisterProfileListener(listener) 184 | // see https://github.com/nodejs/node/issues/27641 185 | setImmediate(_ => { 186 | session.disconnect() 187 | }) 188 | return done() 189 | }) 190 | agent.registerProfileListener(listener) 191 | trigger.trigger(TriggerState.START) 192 | setTimeout(_ => { 193 | trigger.trigger(TriggerState.END) 194 | }, 100) 195 | }) 196 | }) 197 | }) 198 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-heap-profiler/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "build", 5 | "rootDirs": ["src"], 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "strictNullChecks": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "test/**/*.ts" 18 | ], 19 | "compileOnSave": false 20 | } -------------------------------------------------------------------------------- /packages/openprofiling-inspector-heap-profiler/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-trace-events/README.md: -------------------------------------------------------------------------------- 1 | # OpenProfiling NodeJS - Inspector-based Trace Events Inspector 2 | 3 | This one isn't really a profier, more like a collector. As defined in the [official documentation](https://nodejs.org/dist/latest-v10.x/docs/api/tracing.html): 4 | > Trace Event provides a mechanism to centralize tracing information generated by V8, Node.js core, and userspace code. 5 | 6 | It allows you to collect specific events emited by the Node or V8, such as detailed GC events or function deoptimization. V8 has [some documentation about it](https://v8.dev/docs/trace) that we advise to read. 7 | 8 | ### Advantages 9 | 10 | - Low performance impact since it's just collecting simple events. 11 | - The fact that is use the core `inspector` module means that it's **available out of the box without installing any dependency**. 12 | 13 | ### Drawbacks 14 | 15 | - Events doesn't have a lot of details, specially ones about deoptimization. You will just know that they are happening. 16 | 17 | ### How to use 18 | 19 | In the following example, you will need to send the `SIGUSR2` signal to the process to start the profiling and again when do you want to end it: 20 | 21 | ```ts 22 | import { ProfilingAgent } from '@openprofiling/nodejs' 23 | import { FileExporter } from '@openprofiling/exporter-file' 24 | import { TraceEventsProfiler } from '@openprofiling/inspector-trace-events' 25 | import { SignalTrigger } from '@openprofiling/trigger-signal' 26 | 27 | const profilingAgent = new ProfilingAgent() 28 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR2' }), new TraceEventsProfiler({ 29 | categories: [ 'v8.gc', 'node.bootstrap', 'v8.jit' ] // those are by default 30 | })) 31 | profilingAgent.start({ exporter: new FileExporter() }) 32 | ``` 33 | 34 | You can choose which categories of events you want to profile, here the list: 35 | - `node.fs.sync`: event emited when doing sync calls to filesystem 36 | - `v8.gc`: events about the v8's gc 37 | - `v8.runtime`: executing function 38 | - `node.bootstrap`: event that are sent by node itself when starting 39 | - `v8.jit`: deoptimization events 40 | - `v8.compile`: parsing scripts 41 | 42 | Those are high levels categories and do not represent low levels one (ex: `disabled-by-default-v8.runtime_stats`) that are used inside V8, you can also add low levels one 43 | 44 | If you are using Node 8 and multiple profilers (ex: using both heap and cpu profiler), you will need to share a `inspector` session like this: 45 | 46 | ```ts 47 | import { ProfilingAgent } from '@openprofiling/nodejs' 48 | import { FileExporter } from '@openprofiling/exporter-file' 49 | import { TraceEventsProfiler } from '@openprofiling/inspector-trace-events' 50 | import { InspectorHeapProfiler } from '@openprofiling/inspector-heap-profiler' 51 | import { SignalTrigger } from '@openprofiling/trigger-signal' 52 | import * as inspector from 'inspector' 53 | 54 | const profilingAgent = new ProfilingAgent() 55 | // creation a session 56 | const session = new inspector.Session() 57 | // give it as parameters to the constructor 58 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR1' }), new TraceEventsProfiler({ session })) 59 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR2' }), new InspectorCPUProfiler({ session })) 60 | profilingAgent.start({ exporter: new FileExporter() }) 61 | ``` 62 | 63 | ### Vizualisation 64 | 65 | After retrieving the trace-events file where it has been exported, it should have a `.json` extension. Which is the standard extension for this type of data. 66 | You have multiple ways to read the output, here the list of (known) tools that you can use : 67 | - `chrome://tracing` -------------------------------------------------------------------------------- /packages/openprofiling-inspector-trace-events/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openprofiling/inspector-trace-events", 3 | "version": "0.2.2", 4 | "main": "build/index.js", 5 | "types": "build/index.d.ts", 6 | "repository": "openprofiling/openprofiling-node", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.json", 9 | "lint": "tslint --project . src/**/*.ts", 10 | "test": "mocha -r ts-node/register ./test/*.spec.ts", 11 | "test-coverage": "nyc mocha -r ts-node/register test/*.spec.ts", 12 | "report-coverage": "nyc report --reporter=json && codecov -f coverage/*.json -p ../..", 13 | "ci": "yarn test-coverage && yarn report-coverage", 14 | "prepublishOnly": "yarn build" 15 | }, 16 | "keywords": [ 17 | "openprofiling", 18 | "nodejs", 19 | "tracing", 20 | "profiling" 21 | ], 22 | "author": "Valentin Marchaud", 23 | "license": "Apache-2.0", 24 | "engines": { 25 | "node": ">=10.10.0" 26 | }, 27 | "files": [ 28 | "build/", 29 | "doc", 30 | "CHANGELOG.md", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "extension": [ 39 | ".ts" 40 | ], 41 | "exclude": [ 42 | "build/", 43 | "config/", 44 | "examples/", 45 | "test/" 46 | ], 47 | "cache": true, 48 | "all": true 49 | }, 50 | "devDependencies": { 51 | "@types/mocha": "^5.2.5", 52 | "codecov": "^3.4.0", 53 | "mocha": "^6.1.4", 54 | "nyc": "^14.1.1", 55 | "semver": "^6.0.0", 56 | "ts-node": "^8.2.0", 57 | "tslint": "^5.17.0", 58 | "tslint-config-standard": "^8.0.1", 59 | "typescript": "^3.7.4" 60 | }, 61 | "dependencies": { 62 | "@openprofiling/core": "^0.2.2" 63 | }, 64 | "gitHead": "a8098997633d0ffefed78ec7b4c12df64e47f3d5" 65 | } 66 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-trace-events/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './inspector-trace-events-profiler' 3 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-trace-events/src/inspector-trace-events-profiler.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BaseProfiler, TriggerState, ProfilerOptions, Profile, ProfileType, ProfileStatus, TriggerEventOptions } from '@openprofiling/core' 3 | import * as inspector from 'inspector' 4 | 5 | export type TraceEventsCategories = 'node.fs.sync' | 'v8.gc' | 'v8.runtime' | 'node.bootstrap' | 'v8.jit' | 'v8.compile' 6 | 7 | export class TraceEventsProfilerOptions implements ProfilerOptions { 8 | session?: inspector.Session 9 | categories?: TraceEventsCategories[] 10 | } 11 | 12 | export type TraceEvent = { 13 | pid: Number 14 | tid: Number 15 | ts: Number 16 | tts: Number 17 | name: String 18 | cat: String 19 | dur: Number 20 | tdur: Number 21 | args: Object 22 | } 23 | 24 | export type TraceEventsCollection = { 25 | value: TraceEvent[] 26 | } 27 | 28 | export type TraceEventsCollected = { 29 | method: String, 30 | params: TraceEventsCollection 31 | } 32 | 33 | export class TraceEventsProfiler extends BaseProfiler { 34 | 35 | private session: inspector.Session | undefined 36 | private started: boolean = false 37 | private currentProfile: Profile | undefined 38 | 39 | protected options: TraceEventsProfilerOptions 40 | private categories: Set = new Set([ 41 | 'node.bootstrap', 'v8.gc', 'v8.jit' 42 | ]) 43 | 44 | constructor (options?: TraceEventsProfilerOptions) { 45 | super('inspector-trace-events', options) 46 | } 47 | 48 | init () { 49 | if (typeof this.options.session === 'object') { 50 | this.session = this.options.session 51 | // try to connect the session if not already the case 52 | try { 53 | this.session.connect() 54 | } catch (err) { 55 | this.logger.debug('failed to connect to given session', err.message) 56 | } 57 | } else { 58 | this.session = new inspector.Session() 59 | try { 60 | this.session.connect() 61 | } catch (err) { 62 | this.logger.error('Could not connect to inspector', err.message) 63 | return 64 | } 65 | } 66 | if (this.options.categories !== undefined && this.options.categories.length > 0) { 67 | this.categories = new Set(this.options.categories) 68 | } 69 | for (let category of this.categories) { 70 | switch (category) { 71 | case 'v8.gc': { 72 | this.categories.add('disabled-by-default-v8.gc_stats') 73 | this.categories.add('disabled-by-default-v8.gc') 74 | break 75 | } 76 | case 'v8.jit': { 77 | this.categories.add('disabled-by-default-v8.ic_stats') 78 | this.categories.add('v8') 79 | this.categories.add('V8.DeoptimizeCode') 80 | break 81 | } 82 | case 'v8.runtime': { 83 | this.categories.add('disabled-by-default-v8.runtime_stats') 84 | this.categories.add('disabled-by-default-v8.runtime') 85 | this.categories.add('v8.execute') 86 | break 87 | } 88 | case 'v8.compile': { 89 | this.categories.add('disabled-by-default-v8.compile') 90 | this.categories.add('v8') 91 | this.categories.add('v8.compile') 92 | break 93 | } 94 | } 95 | } 96 | } 97 | 98 | destroy () { 99 | if (this.session === undefined) return 100 | 101 | if (this.started === true) { 102 | this.stopProfiling() 103 | } 104 | } 105 | 106 | async onTrigger (state: TriggerState, options: TriggerEventOptions) { 107 | if (this.session === undefined) { 108 | throw new Error(`Session wasn't initialized`) 109 | } 110 | if (state === TriggerState.START && this.started === true) { 111 | this.logger.info('Received start trigger but already started, ignoring') 112 | return 113 | } 114 | if (state === TriggerState.END && this.started === false) { 115 | this.logger.error('Received end trigger but wasnt started, ignoring') 116 | return 117 | } 118 | 119 | if (state === TriggerState.START) { 120 | this.logger.info(`Starting profiling`) 121 | this.currentProfile = new Profile(options.name || 'noname', ProfileType.PERFECTO) 122 | this.started = true 123 | this.session.post('NodeTracing.start', { 124 | traceConfig: { 125 | includedCategories: Array.from(this.categories) 126 | } 127 | }) 128 | this.agent.notifyStartProfile(this.currentProfile) 129 | return 130 | } 131 | 132 | if (state === TriggerState.END) { 133 | this.logger.info(`Stopping profiling`) 134 | this.stopProfiling() 135 | } 136 | } 137 | 138 | private stopProfiling () { 139 | if (this.session === undefined) { 140 | throw new Error(`Session wasn't initialized`) 141 | } 142 | const buffer: TraceEvent[] = [] 143 | const onDataCollected = (message: TraceEventsCollected) => { 144 | const data = message.params.value 145 | buffer.push(...data) 146 | } 147 | const onTracingComplete = () => { 148 | if (this.session === undefined) return 149 | // cleanup listeners 150 | this.session.removeListener('NodeTracing.dataCollected', onDataCollected) 151 | this.session.removeListener('NodeTracing.tracingComplete', onTracingComplete) 152 | 153 | if (this.currentProfile === undefined) return 154 | 155 | // serialize buffer 156 | const data = JSON.stringify(buffer) 157 | this.currentProfile.addProfileData(Buffer.from(data)) 158 | this.currentProfile.status = ProfileStatus.SUCCESS 159 | this.currentProfile.addAttribute('profiler', this.name) 160 | this.agent.notifyEndProfile(this.currentProfile) 161 | this.started = false 162 | this.currentProfile = undefined 163 | } 164 | 165 | this.session.on('NodeTracing.dataCollected', onDataCollected) 166 | this.session.on('NodeTracing.tracingComplete', onTracingComplete) 167 | this.session.post('NodeTracing.stop', { 168 | traceConfig: { 169 | includedCategories: Array.from(this.categories) 170 | } 171 | }, (err, args) => { 172 | if (err && this.currentProfile !== undefined) { 173 | this.logger.error(`Failed to stop trace events profiler`, err.message) 174 | this.currentProfile.status = ProfileStatus.FAILED 175 | this.currentProfile.addAttribute('error', err.message) 176 | } 177 | }) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-trace-events/test/inspector-trace-events.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { TraceEventsProfiler, TraceEvent } from '../src/index' 3 | import * as assert from 'assert' 4 | import * as inspector from 'inspector' 5 | import { readFileSync } from 'fs' 6 | import { CoreAgent, BaseProfiler, TriggerState, Profile, BaseTrigger, Exporter, ProfileType, ProfileStatus } from '@openprofiling/core' 7 | 8 | class DummyTrigger extends BaseTrigger { 9 | constructor () { 10 | super('dummny-trigger') 11 | } 12 | 13 | init () { 14 | return 15 | } 16 | 17 | destroy () { 18 | return 19 | } 20 | 21 | trigger (state: TriggerState) { 22 | this.agent.onTrigger(state, { source: this }) 23 | } 24 | } 25 | 26 | type onProfile = (profile: Profile) => void 27 | 28 | class DummyExporter implements Exporter { 29 | 30 | private onStart: onProfile | undefined 31 | private onEnd: onProfile | undefined 32 | 33 | constructor (onStart?: onProfile, onEnd?: onProfile) { 34 | this.onEnd = onEnd 35 | this.onStart = onStart 36 | } 37 | 38 | async onProfileStart (profile) { 39 | if (typeof this.onStart === 'function') { 40 | this.onStart(profile) 41 | } 42 | } 43 | 44 | enable () { 45 | return 46 | } 47 | 48 | disable () { 49 | return 50 | } 51 | 52 | async onProfileEnd (profile) { 53 | if (typeof this.onEnd === 'function') { 54 | this.onEnd(profile) 55 | } 56 | } 57 | } 58 | 59 | describe('Inspector Trace Events Profiler', () => { 60 | 61 | const agent: CoreAgent = new CoreAgent() 62 | let profiler = new TraceEventsProfiler({ 63 | categories: [ 'node.fs.sync' ] 64 | }) 65 | const trigger = new DummyTrigger() 66 | 67 | before(async () => { 68 | profiler.enable(agent) 69 | trigger.enable(agent) 70 | agent.start({ 71 | reactions: [ 72 | { 73 | profiler, 74 | trigger 75 | } 76 | ], 77 | logLevel: 4 78 | }) 79 | }) 80 | 81 | it('should export a profiler implementation', () => { 82 | assert(TraceEventsProfiler.prototype instanceof BaseProfiler) 83 | }) 84 | 85 | it('should receive profile start from profiler', (done) => { 86 | const listener = new DummyExporter(profile => { 87 | assert(profile.kind === ProfileType.PERFECTO) 88 | assert(profile.status === ProfileStatus.UNKNOWN) 89 | agent.unregisterProfileListener(listener) 90 | return done() 91 | }) 92 | agent.registerProfileListener(listener) 93 | trigger.trigger(TriggerState.START) 94 | }) 95 | 96 | it('should receive profile end from profiler', (done) => { 97 | const listener = new DummyExporter(undefined, (profile: Profile) => { 98 | assert(profile.kind === ProfileType.PERFECTO) 99 | assert(profile.status === ProfileStatus.SUCCESS) 100 | assert(profile.data.length > 10) 101 | const data = JSON.parse(profile.data.toString()) 102 | assert(data.filter(event => event.name === 'fs.sync.open').length === 2) 103 | agent.unregisterProfileListener(listener) 104 | return done() 105 | }) 106 | agent.registerProfileListener(listener) 107 | readFileSync(__filename) 108 | trigger.trigger(TriggerState.END) 109 | }) 110 | 111 | it('should stop profile if profiler is destroyed', (done) => { 112 | const listener = new DummyExporter(undefined, profile => { 113 | assert(profile.kind === ProfileType.PERFECTO) 114 | assert(profile.status === ProfileStatus.SUCCESS) 115 | agent.unregisterProfileListener(listener) 116 | return done() 117 | }) 118 | agent.registerProfileListener(listener) 119 | trigger.trigger(TriggerState.START) 120 | setTimeout(_ => { 121 | profiler.disable() 122 | }, 100) 123 | }) 124 | 125 | describe('should work with custom inspector session', () => { 126 | it('should setup use of custom session', () => { 127 | agent.stop() 128 | const session = new inspector.Session() 129 | session.connect() 130 | profiler = new TraceEventsProfiler({ session }) 131 | profiler.enable(agent) 132 | agent.start({ 133 | reactions: [ 134 | { 135 | profiler, 136 | trigger 137 | } 138 | ], 139 | logLevel: 4 140 | }) 141 | }) 142 | 143 | it('should take a profile succesfully', (done) => { 144 | const listener = new DummyExporter(undefined, profile => { 145 | assert(profile.kind === ProfileType.PERFECTO) 146 | assert(profile.status === ProfileStatus.SUCCESS) 147 | agent.unregisterProfileListener(listener) 148 | return done() 149 | }) 150 | agent.registerProfileListener(listener) 151 | trigger.trigger(TriggerState.START) 152 | setTimeout(_ => { 153 | trigger.trigger(TriggerState.END) 154 | }, 100) 155 | }) 156 | }) 157 | 158 | describe('should work with custom inspector session (not opened)', () => { 159 | it('should setup use of custom session', () => { 160 | agent.stop() 161 | const session = new inspector.Session() 162 | profiler = new TraceEventsProfiler({ session }) 163 | profiler.enable(agent) 164 | agent.start({ 165 | reactions: [ 166 | { 167 | profiler, 168 | trigger 169 | } 170 | ], 171 | logLevel: 4 172 | }) 173 | }) 174 | 175 | it('should take a profile succesfully', (done) => { 176 | const listener = new DummyExporter(undefined, profile => { 177 | assert(profile.kind === ProfileType.PERFECTO) 178 | assert(profile.status === ProfileStatus.SUCCESS) 179 | agent.unregisterProfileListener(listener) 180 | return done() 181 | }) 182 | agent.registerProfileListener(listener) 183 | trigger.trigger(TriggerState.START) 184 | setTimeout(_ => { 185 | trigger.trigger(TriggerState.END) 186 | }, 100) 187 | }) 188 | }) 189 | 190 | describe('should pickup gc trace events', () => { 191 | let session: inspector.Session 192 | it('should setup use of custom session', () => { 193 | agent.stop() 194 | session = new inspector.Session() 195 | profiler = new TraceEventsProfiler({ session, categories: ['v8.gc'] }) 196 | profiler.enable(agent) 197 | agent.start({ 198 | reactions: [ 199 | { 200 | profiler, 201 | trigger 202 | } 203 | ], 204 | logLevel: 4 205 | }) 206 | }) 207 | 208 | it('should take a profile succesfully', (done) => { 209 | const listener = new DummyExporter(undefined, (profile: Profile) => { 210 | assert(profile.kind === ProfileType.PERFECTO) 211 | assert(profile.status === ProfileStatus.SUCCESS) 212 | assert(profile.data.length > 10) 213 | const data = JSON.parse(profile.data.toString()) as TraceEvent[] 214 | const gcEvents = data.filter(event => event.cat !== '_metadata') 215 | assert(gcEvents.length > 0) 216 | agent.unregisterProfileListener(listener) 217 | return done() 218 | }) 219 | agent.registerProfileListener(listener) 220 | trigger.trigger(TriggerState.START) 221 | session.post('HeapProfiler.enable') 222 | session.post('HeapProfiler.collectGarbage', (err) => { 223 | assert.ifError(err) 224 | trigger.trigger(TriggerState.END) 225 | }) 226 | }) 227 | }) 228 | 229 | describe('should pickup deoptimization trace events', () => { 230 | let session: inspector.Session 231 | it('should setup use of custom session', () => { 232 | agent.stop() 233 | session = new inspector.Session() 234 | profiler = new TraceEventsProfiler({ session, categories: ['v8.jit'] }) 235 | profiler.enable(agent) 236 | agent.start({ 237 | reactions: [ 238 | { 239 | profiler, 240 | trigger 241 | } 242 | ], 243 | logLevel: 4 244 | }) 245 | }) 246 | 247 | it('should take a profile succesfully', (done) => { 248 | const listener = new DummyExporter(undefined, (profile: Profile) => { 249 | assert(profile.kind === ProfileType.PERFECTO) 250 | assert(profile.status === ProfileStatus.SUCCESS) 251 | assert(profile.data.length > 10) 252 | const data = JSON.parse(profile.data.toString()) as TraceEvent[] 253 | const events = data.filter(event => event.name === 'V8.DeoptimizeCode') 254 | assert(events.length > 0) 255 | agent.unregisterProfileListener(listener) 256 | return done() 257 | }) 258 | agent.registerProfileListener(listener) 259 | trigger.trigger(TriggerState.START) 260 | 261 | let result = null 262 | const willBeDeoptimize = function (a, b) { 263 | return a + b 264 | } 265 | // it should jit the function since we are only using number 266 | for (let i = 0; i < 100000; i++) { 267 | result = willBeDeoptimize(i, 1) 268 | } 269 | // then we use a string, which should deoptimize the function 270 | willBeDeoptimize('a', 'b') 271 | trigger.trigger(TriggerState.END) 272 | }) 273 | }) 274 | }) 275 | -------------------------------------------------------------------------------- /packages/openprofiling-inspector-trace-events/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "build", 5 | "rootDirs": ["src"], 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "strictNullChecks": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "test/**/*.ts" 18 | ], 19 | "compileOnSave": false 20 | } -------------------------------------------------------------------------------- /packages/openprofiling-inspector-trace-events/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /packages/openprofiling-nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openprofiling/nodejs", 3 | "version": "0.2.2", 4 | "main": "build/index.js", 5 | "types": "build/index.d.ts", 6 | "repository": "openprofiling/openprofiling-node", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.json", 9 | "lint": "tslint --project . src/**/*.ts", 10 | "test": "mocha -r ts-node/register ./test/*.spec.ts", 11 | "test-coverage": "nyc mocha -r ts-node/register test/*.spec.ts", 12 | "report-coverage": "nyc report --reporter=json && codecov -f coverage/*.json -p ../..", 13 | "ci": "yarn test-coverage && yarn report-coverage", 14 | "prepublishOnly": "yarn build" 15 | }, 16 | "keywords": [ 17 | "openprofiling", 18 | "nodejs", 19 | "tracing", 20 | "profiling" 21 | ], 22 | "author": "Valentin Marchaud", 23 | "license": "Apache-2.0", 24 | "engines": { 25 | "node": ">=8.0" 26 | }, 27 | "files": [ 28 | "build/", 29 | "doc", 30 | "CHANGELOG.md", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "extension": [ 39 | ".ts" 40 | ], 41 | "exclude": [ 42 | "build/", 43 | "config/", 44 | "examples/", 45 | "test/" 46 | ], 47 | "cache": true, 48 | "all": true 49 | }, 50 | "dependencies": { 51 | "@openprofiling/core": "^0.2.2" 52 | }, 53 | "devDependencies": { 54 | "codecov": "^3.4.0", 55 | "mocha": "^6.1.4", 56 | "nyc": "^14.1.1", 57 | "ts-node": "^8.2.0", 58 | "tslint": "^5.17.0", 59 | "tslint-config-standard": "^8.0.1", 60 | "typescript": "^3.7.4" 61 | }, 62 | "gitHead": "a8098997633d0ffefed78ec7b4c12df64e47f3d5" 63 | } 64 | -------------------------------------------------------------------------------- /packages/openprofiling-nodejs/src/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export * from './node-agent' 4 | -------------------------------------------------------------------------------- /packages/openprofiling-nodejs/src/node-agent.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { CoreAgent, Exporter, Profiler, Reaction, Trigger } from '@openprofiling/core' 4 | 5 | export interface AgentConfig { 6 | exporter: Exporter 7 | logLevel?: number 8 | } 9 | 10 | export class ProfilingAgent { 11 | 12 | private agent: CoreAgent = new CoreAgent() 13 | private reactions: Reaction[] = [] 14 | private _options: AgentConfig 15 | private started = false 16 | 17 | start (options: AgentConfig): ProfilingAgent { 18 | this._options = options 19 | this.started = true 20 | 21 | for (let reaction of this.reactions) { 22 | reaction.trigger.enable(this.agent) 23 | reaction.profiler.enable(this.agent) 24 | } 25 | this.agent.start(Object.assign(options, { reactions: this.reactions })) 26 | this._options.exporter.enable(this.agent) 27 | this.agent.registerProfileListener(options.exporter) 28 | return this 29 | } 30 | 31 | register (trigger: Trigger, profiler: Profiler): ProfilingAgent { 32 | if (this.started === true) { 33 | throw new Error(`You cannot register new link between trigger and profiler when the agent has been started`) 34 | } 35 | this.reactions.push({ trigger, profiler }) 36 | return this 37 | } 38 | 39 | stop () { 40 | for (let reaction of this.reactions) { 41 | reaction.trigger.disable() 42 | reaction.profiler.disable() 43 | } 44 | this.agent.unregisterProfileListener(this._options.exporter) 45 | this._options.exporter.disable() 46 | this.agent.stop() 47 | this.started = false 48 | } 49 | 50 | isStarted () { 51 | return this.started 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /packages/openprofiling-nodejs/test/agent.spec.ts: -------------------------------------------------------------------------------- 1 | import { ProfilingAgent } from '../src' 2 | import { TriggerState, Profile, BaseTrigger, BaseExporter, BaseProfiler, Trigger, ProfileType, TriggerEventOptions } from '@openprofiling/core' 3 | import * as assert from 'assert' 4 | 5 | class DummyTrigger extends BaseTrigger { 6 | constructor () { 7 | super('dummny-trigger') 8 | } 9 | 10 | init () { 11 | return 12 | } 13 | 14 | destroy () { 15 | return 16 | } 17 | 18 | trigger (state: TriggerState) { 19 | this.agent.onTrigger(state, { source: this }) 20 | } 21 | } 22 | 23 | type onProfile = (profile: Profile) => void 24 | 25 | class DummyExporter extends BaseExporter { 26 | 27 | private onStart: onProfile | undefined 28 | private onEnd: onProfile | undefined 29 | 30 | constructor (onStart?: onProfile, onEnd?: onProfile) { 31 | super('dummy') 32 | this.onEnd = onEnd 33 | this.onStart = onStart 34 | } 35 | 36 | async onProfileStart (profile) { 37 | if (typeof this.onStart === 'function') { 38 | this.onStart(profile) 39 | } 40 | } 41 | 42 | async onProfileEnd (profile) { 43 | if (typeof this.onEnd === 'function') { 44 | this.onEnd(profile) 45 | } 46 | } 47 | 48 | enable () { 49 | return 50 | } 51 | 52 | disable () { 53 | return 54 | } 55 | } 56 | 57 | class DummyProfiler extends BaseProfiler { 58 | private currentProfile: Profile 59 | 60 | constructor () { 61 | super('dummy-profiler') 62 | } 63 | 64 | destroy () { 65 | return 66 | } 67 | 68 | init () { 69 | return 70 | } 71 | 72 | async onTrigger (state: TriggerState, options: TriggerEventOptions) { 73 | if (state === TriggerState.START) { 74 | this.currentProfile = new Profile('test', ProfileType.CPU_PROFILE) 75 | this.agent.notifyStartProfile(this.currentProfile) 76 | } else { 77 | this.currentProfile.addProfileData(Buffer.from('test')) 78 | this.agent.notifyEndProfile(this.currentProfile) 79 | } 80 | } 81 | } 82 | 83 | describe('Agent Integration test', () => { 84 | 85 | let agent = new ProfilingAgent() 86 | let trigger = new DummyTrigger() 87 | let profiler = new DummyProfiler() 88 | let exporter = new DummyExporter() 89 | 90 | it('agent should start', () => { 91 | agent.register(trigger, profiler) 92 | assert(agent.isStarted() === false) 93 | agent.start({ 94 | logLevel: 4, 95 | exporter 96 | }) 97 | assert(agent.isStarted() === true) 98 | assert.throws(() => { 99 | agent.register(trigger, profiler) 100 | }, /You cannot register/) 101 | }) 102 | 103 | it('should trigger start and receive start hook in exporter', done => { 104 | const originalStart = exporter.onProfileStart 105 | exporter.onProfileStart = async (profile: Profile) => { 106 | assert(profile.name === 'test') 107 | assert(profile.kind === ProfileType.CPU_PROFILE) 108 | exporter.onProfileStart = originalStart 109 | done() 110 | } 111 | trigger.trigger(TriggerState.START) 112 | }) 113 | 114 | it('should trigger end and receive end hook in exporter', done => { 115 | const originalEnd = exporter.onProfileEnd 116 | exporter.onProfileEnd = async (profile: Profile) => { 117 | assert(profile.name === 'test') 118 | assert(profile.kind === ProfileType.CPU_PROFILE) 119 | assert(profile.data.toString() === 'test') 120 | exporter.onProfileEnd = originalEnd 121 | done() 122 | } 123 | trigger.trigger(TriggerState.END) 124 | }) 125 | 126 | it('should stop the agent and not be able to trigger anything', done => { 127 | agent.stop() 128 | assert.doesNotThrow(() => { 129 | agent.register(trigger, profiler) 130 | }) 131 | const originalStart = exporter.onProfileStart 132 | exporter.onProfileStart = async (profile: Profile) => { 133 | assert(false, 'should not be called') 134 | } 135 | trigger.trigger(TriggerState.START) 136 | setTimeout(_ => { 137 | exporter.onProfileStart = originalStart 138 | done() 139 | }, 200) 140 | }) 141 | }) 142 | -------------------------------------------------------------------------------- /packages/openprofiling-nodejs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "build", 5 | "rootDirs": ["src"], 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "strictNullChecks": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "test/**/*.ts" 18 | ], 19 | "compileOnSave": false 20 | } -------------------------------------------------------------------------------- /packages/openprofiling-nodejs/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /packages/openprofiling-trigger-http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openprofiling/trigger-http", 3 | "version": "0.2.2", 4 | "main": "build/index.js", 5 | "types": "build/index.d.ts", 6 | "repository": "openprofiling/openprofiling-node", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.json", 9 | "lint": "tslint --project . src/**/*.ts", 10 | "test": "mocha -r ts-node/register ./test/*.spec.ts", 11 | "test-coverage": "nyc mocha -r ts-node/register test/*.spec.ts", 12 | "report-coverage": "nyc report --reporter=json && codecov -f coverage/*.json -p ../..", 13 | "ci": "yarn test-coverage && yarn report-coverage", 14 | "prepublishOnly": "yarn build" 15 | }, 16 | "keywords": [ 17 | "openprofiling", 18 | "nodejs", 19 | "tracing", 20 | "profiling" 21 | ], 22 | "author": "Valentin Marchaud", 23 | "license": "Apache-2.0", 24 | "engines": { 25 | "node": ">=6.0" 26 | }, 27 | "files": [ 28 | "build/", 29 | "doc", 30 | "CHANGELOG.md", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "extension": [ 39 | ".ts" 40 | ], 41 | "exclude": [ 42 | "build/", 43 | "config/", 44 | "examples/", 45 | "test/" 46 | ], 47 | "cache": true, 48 | "all": true 49 | }, 50 | "devDependencies": { 51 | "codecov": "^3.4.0", 52 | "mocha": "^6.1.4", 53 | "nyc": "^14.1.1", 54 | "ts-node": "^8.2.0", 55 | "tslint": "^5.17.0", 56 | "tslint-config-standard": "^8.0.1", 57 | "typescript": "^3.7.4" 58 | }, 59 | "dependencies": { 60 | "@openprofiling/core": "^0.2.2" 61 | }, 62 | "gitHead": "a8098997633d0ffefed78ec7b4c12df64e47f3d5" 63 | } 64 | -------------------------------------------------------------------------------- /packages/openprofiling-trigger-http/src/http-trigger.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { BaseTrigger, TriggerOptions, TriggerState } from '@openprofiling/core' 4 | import * as http from 'http' 5 | 6 | export interface HttpTriggerOptions extends TriggerOptions { 7 | port?: number 8 | } 9 | 10 | export class HttpTrigger extends BaseTrigger { 11 | 12 | private isProfiling: boolean = false 13 | private server: http.Server 14 | 15 | protected options: HttpTriggerOptions 16 | 17 | constructor (options: HttpTriggerOptions) { 18 | super(`http-${options.port || 0}`, options) 19 | } 20 | 21 | init () { 22 | this.server = http.createServer(this.onRequest.bind(this)) 23 | const port = this.options.port || 0 24 | this.server.listen(port, () => { 25 | this.agent.logger.info(`HTTP Trigger is now listening on port ${port}`) 26 | }) 27 | } 28 | 29 | destroy () { 30 | this.server.close(err => { 31 | if (err) { 32 | this.agent.logger.error(`HTTP Trigger listening on ${this.options.port} has failed to stop`, err) 33 | } else { 34 | this.agent.logger.info(`HTTP Trigger that was listening on ${this.options.port} has stopped.`) 35 | } 36 | }) 37 | } 38 | 39 | onRequest (req: http.IncomingMessage, res: http.ServerResponse) { 40 | if (this.isProfiling) { 41 | this.agent.onTrigger(TriggerState.END, { source: this }) 42 | this.isProfiling = false 43 | } else { 44 | this.agent.onTrigger(TriggerState.START, { source: this }) 45 | this.isProfiling = true 46 | } 47 | res.writeHead(200) 48 | return res.end() 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /packages/openprofiling-trigger-http/src/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export * from './http-trigger' 4 | -------------------------------------------------------------------------------- /packages/openprofiling-trigger-http/test/http-trigger.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { HttpTrigger } from '../src' 3 | import * as assert from 'assert' 4 | import * as http from 'http' 5 | import { CoreAgent, BaseProfiler, TriggerState, BaseTrigger, TriggerEventOptions } from '@openprofiling/core' 6 | 7 | class DummyProfiler extends BaseProfiler { 8 | constructor () { 9 | super('dummny-profiler') 10 | } 11 | 12 | init () { 13 | return 14 | } 15 | 16 | destroy () { 17 | return 18 | } 19 | 20 | async onTrigger (state: TriggerState, options: TriggerEventOptions) { 21 | return 22 | } 23 | } 24 | 25 | describe('Http Trigger', () => { 26 | 27 | const agent: CoreAgent = new CoreAgent() 28 | const profiler = new DummyProfiler() 29 | const trigger = new HttpTrigger({ port: 4242 }) 30 | 31 | before(async () => { 32 | profiler.enable(agent) 33 | trigger.enable(agent) 34 | agent.start({ 35 | reactions: [ 36 | { 37 | profiler, 38 | trigger 39 | } 40 | ], 41 | logLevel: 4 42 | }) 43 | }) 44 | 45 | it('should export a trigger implementation', () => { 46 | assert(HttpTrigger.prototype instanceof BaseTrigger) 47 | }) 48 | 49 | it('should receive trigger start inside profiler', (done) => { 50 | const originalOnTrigger = profiler.onTrigger 51 | profiler.onTrigger = async function (state, { source }) { 52 | assert(source === trigger, 'should be http trigger') 53 | assert(state === TriggerState.START, 'should be starting profile') 54 | profiler.onTrigger = originalOnTrigger 55 | return done() 56 | } 57 | http.get('http://localhost:4242/') 58 | }) 59 | 60 | it('should receive trigger end inside profiler', (done) => { 61 | const originalOnTrigger = profiler.onTrigger 62 | profiler.onTrigger = async function (state, { source }) { 63 | assert(source === trigger, 'should be the http trigger') 64 | assert(state === TriggerState.END, 'state should be ending profile') 65 | profiler.onTrigger = originalOnTrigger 66 | return done() 67 | } 68 | http.get('http://localhost:4242/') 69 | }) 70 | 71 | it('should disable the trigger', (done) => { 72 | trigger.disable() 73 | const req = http.get('http://localhost:4242/') 74 | req.on('error', err => { 75 | // @ts-ignore 76 | assert(err.code === 'ECONNREFUSED', 'server should have been stopped') 77 | return done() 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /packages/openprofiling-trigger-http/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "build", 5 | "rootDirs": ["src"], 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "strictNullChecks": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "test/**/*.ts" 18 | ], 19 | "compileOnSave": false 20 | } -------------------------------------------------------------------------------- /packages/openprofiling-trigger-http/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /packages/openprofiling-trigger-signal/README.md: -------------------------------------------------------------------------------- 1 | # OpenProfiling NodeJS - Signal Trigger 2 | 3 | This trigger is most probably the easier to use because you just need to send a signal to the process (which is generally straightforward even with containers). 4 | 5 | ### Advantages 6 | 7 | - No specific setup, one CLI command away 8 | 9 | ### Drawbacks 10 | 11 | - Limited list of signal to listen on (see [official doc](https://nodejs.org/dist/latest-v10.x/docs/api/process.html#process_signal_events)) 12 | - I advise to use the `SIGUSR1` and `SIGUSR2` signal since they are reserved for user-space behavior. 13 | - Behavior on Windows might be a little more complicated since there a no concept of `signals` on it (see official doc at the end of the previous link) 14 | 15 | ### How to use 16 | 17 | In the following example, when the profile will be done it will be written on disk: 18 | 19 | ```ts 20 | import { ProfilingAgent } from '@openprofiling/nodejs' 21 | import { FileExporter } from '@openprofiling/exporter-file' 22 | import { InspectorCPUProfiler } from '@openprofiling/inspector-cpu-profiler' 23 | import { SignalTrigger } from '@openprofiling/trigger-signal' 24 | 25 | const profilingAgent = new ProfilingAgent() 26 | // you just need to precise which signal the trigger need to listen 27 | // little advise: only use SIGUSR1 or SIGUSR2 28 | profilingAgent.register(new SignalTrigger({ signal: 'SIGUSR2' }), new InspectorCPUProfiler()) 29 | profilingAgent.start({ exporter: new FileExporter() }) 30 | ``` 31 | 32 | Then to initiate the trigger, just send a signal to the desirated process: 33 | 34 | linux/macos: 35 | ```bash 36 | kill -s USR1 37 | ``` 38 | 39 | You can find the PID via `htop`, `ps aux` or just log your process pid with `console.log('Process pid is ' + process.pid`)` when your application start. -------------------------------------------------------------------------------- /packages/openprofiling-trigger-signal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openprofiling/trigger-signal", 3 | "version": "0.2.2", 4 | "main": "build/index.js", 5 | "types": "build/index.d.ts", 6 | "repository": "openprofiling/openprofiling-node", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.json", 9 | "lint": "tslint --project . src/**/*.ts", 10 | "test": "mocha -r ts-node/register ./test/*.spec.ts", 11 | "test-coverage": "nyc mocha -r ts-node/register test/*.spec.ts", 12 | "report-coverage": "nyc report --reporter=json && codecov -f coverage/*.json -p ../..", 13 | "ci": "yarn test-coverage && yarn report-coverage", 14 | "prepublishOnly": "yarn build" 15 | }, 16 | "keywords": [ 17 | "openprofiling", 18 | "nodejs", 19 | "tracing", 20 | "profiling" 21 | ], 22 | "author": "Valentin Marchaud", 23 | "license": "Apache-2.0", 24 | "engines": { 25 | "node": ">=6.0" 26 | }, 27 | "files": [ 28 | "build/", 29 | "doc", 30 | "CHANGELOG.md", 31 | "LICENSE", 32 | "README.md" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "nyc": { 38 | "extension": [ 39 | ".ts" 40 | ], 41 | "exclude": [ 42 | "build/", 43 | "config/", 44 | "examples/", 45 | "test/" 46 | ], 47 | "cache": true, 48 | "all": true 49 | }, 50 | "devDependencies": { 51 | "codecov": "^3.4.0", 52 | "mocha": "^6.1.4", 53 | "nyc": "^14.1.1", 54 | "ts-node": "^8.1.0", 55 | "tslint": "^5.16.0", 56 | "tslint-config-standard": "^8.0.1", 57 | "typescript": "^3.7.4" 58 | }, 59 | "dependencies": { 60 | "@openprofiling/core": "^0.2.2" 61 | }, 62 | "gitHead": "a8098997633d0ffefed78ec7b4c12df64e47f3d5" 63 | } 64 | -------------------------------------------------------------------------------- /packages/openprofiling-trigger-signal/src/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export * from './signal-trigger' 4 | -------------------------------------------------------------------------------- /packages/openprofiling-trigger-signal/src/signal-trigger.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { BaseTrigger, TriggerOptions, TriggerState } from '@openprofiling/core' 4 | 5 | export interface SignalTriggerOptions extends TriggerOptions { 6 | signal: NodeJS.Signals 7 | } 8 | 9 | export class SignalTrigger extends BaseTrigger { 10 | 11 | private isProfiling: boolean = false 12 | private handler: () => void 13 | 14 | protected options: SignalTriggerOptions 15 | 16 | constructor (options: SignalTriggerOptions) { 17 | super(`signal-${options.signal.toLowerCase()}`, options) 18 | } 19 | 20 | init () { 21 | this.handler = this.onSignal.bind(this) 22 | process.on(this.options.signal, this.handler) 23 | } 24 | 25 | destroy () { 26 | process.removeListener(this.options.signal, this.handler) 27 | } 28 | 29 | onSignal () { 30 | if (this.isProfiling) { 31 | this.agent.onTrigger(TriggerState.END, { source: this }) 32 | this.isProfiling = false 33 | } else { 34 | this.agent.onTrigger(TriggerState.START, { source: this }) 35 | this.isProfiling = true 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /packages/openprofiling-trigger-signal/test/signal-trigger.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { SignalTrigger } from '../src' 3 | import * as assert from 'assert' 4 | import { CoreAgent, BaseProfiler, TriggerState, Trigger, BaseTrigger, TriggerEventOptions } from '@openprofiling/core' 5 | 6 | type onTriggerCallback = (trigger: Trigger, state: TriggerState) => {} 7 | 8 | class DummyProfiler extends BaseProfiler { 9 | constructor () { 10 | super('dummny-profiler') 11 | } 12 | 13 | init () { 14 | return 15 | } 16 | 17 | destroy () { 18 | return 19 | } 20 | 21 | async onTrigger (state: TriggerState, options: TriggerEventOptions) { 22 | return 23 | } 24 | } 25 | 26 | describe('Signal Trigger', () => { 27 | 28 | const agent: CoreAgent = new CoreAgent() 29 | const profiler = new DummyProfiler() 30 | const trigger = new SignalTrigger({ signal: 'SIGUSR1' }) 31 | 32 | before(async () => { 33 | profiler.enable(agent) 34 | trigger.enable(agent) 35 | agent.start({ 36 | reactions: [ 37 | { 38 | profiler, 39 | trigger 40 | } 41 | ], 42 | logLevel: 4 43 | }) 44 | }) 45 | 46 | it('should export a trigger implementation', () => { 47 | assert(SignalTrigger.prototype instanceof BaseTrigger) 48 | }) 49 | 50 | it('should receive trigger start inside profiler', (done) => { 51 | const originalOnTrigger = profiler.onTrigger 52 | profiler.onTrigger = async function (state, { source }) { 53 | assert(source === trigger, 'should be http trigger') 54 | assert(state === TriggerState.START, 'should be starting profile') 55 | profiler.onTrigger = originalOnTrigger 56 | return done() 57 | } 58 | process.kill(process.pid, 'SIGUSR1') 59 | }) 60 | 61 | it('should receive trigger end inside profiler', (done) => { 62 | const originalOnTrigger = profiler.onTrigger 63 | profiler.onTrigger = async function (state, { source }) { 64 | assert(source === trigger, 'should be the http trigger') 65 | assert(state === TriggerState.END, 'state should be ending profile') 66 | profiler.onTrigger = originalOnTrigger 67 | return done() 68 | } 69 | process.kill(process.pid, 'SIGUSR1') 70 | }) 71 | 72 | it('should do nothing with other signal', (done) => { 73 | const originalOnTrigger = profiler.onTrigger 74 | profiler.onTrigger = async function (state) { 75 | assert(false, 'should not receive anything') 76 | } 77 | process.on('SIGUSR2', () => { 78 | profiler.onTrigger = originalOnTrigger 79 | return done() 80 | }) 81 | process.kill(process.pid, 'SIGUSR2') 82 | }) 83 | 84 | it('should disable the trigger', (done) => { 85 | trigger.disable() 86 | const originalOnTrigger = profiler.onTrigger 87 | profiler.onTrigger = async function (state) { 88 | assert(false, 'should not receive anything') 89 | } 90 | process.on('SIGUSR1', () => { 91 | profiler.onTrigger = originalOnTrigger 92 | return done() 93 | }) 94 | process.kill(process.pid, 'SIGUSR1') 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /packages/openprofiling-trigger-signal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "build", 5 | "rootDirs": ["src"], 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "strictNullChecks": true, 11 | "resolveJsonModule": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "test/**/*.ts" 18 | ], 19 | "compileOnSave": false 20 | } -------------------------------------------------------------------------------- /packages/openprofiling-trigger-signal/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | --------------------------------------------------------------------------------