├── .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 | [](https://img.shields.io/npm/v/@openprofiling/core.svg)
8 | [](https://cloud.drone.io/vmarchaud/openprofiling-node)
9 | [](https://codecov.io/gh/vmarchaud/openprofiling-node)
10 | [](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 |
--------------------------------------------------------------------------------