├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── bench.yml │ └── tests.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .taprc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── benchmarks ├── base.js ├── doc.js └── docResourceUsage.js ├── examples ├── activeHandles.js ├── basic.js ├── gc.js ├── gcAggregate.js ├── gcFlags.js ├── onlyOneMetric.js ├── ref.js ├── resourceUsage.js └── start.js ├── index.d.ts ├── index.js ├── index.test-d.ts ├── lib ├── config.js ├── constants.js ├── cpu.js ├── debug.js ├── errors.js ├── eventLoopDelay.js ├── eventLoopUtilization.js ├── gc.js ├── resourceUsage.js ├── sampler.js └── symbols.js ├── lint-staged.config.js ├── package.json ├── test ├── config.test.js ├── diagnostics_channels.test.js ├── doc.test.js ├── esm │ ├── esm.test.js │ ├── namedExports.mjs │ └── selfResolution.mjs ├── eventLoopUtilization.test.js ├── gc.test.js └── resourceUsage.test.js └── types ├── constants.d.ts ├── cpuMetric.d.ts ├── errors.d.ts ├── eventLoopDelayMetric.d.ts ├── eventLoopUtilizationMetric.d.ts ├── gcMetric.d.ts ├── resourceUsageMetric.d.ts └── sampler.d.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | validate.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true 6 | }, 7 | extends: 'standard', 8 | overrides: [ 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 'latest' 12 | }, 13 | rules: { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | ignore: 11 | - dependency-name: "@types/node" 12 | versions: [">10"] 13 | schedule: 14 | interval: "daily" 15 | open-pull-requests-limit: 10 16 | 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "daily" 21 | open-pull-requests-limit: 10 22 | -------------------------------------------------------------------------------- /.github/workflows/bench.yml: -------------------------------------------------------------------------------- 1 | name: Benchmarks 2 | on: 3 | - push 4 | - pull_request 5 | 6 | jobs: 7 | benchmark_next: 8 | name: benchmark next 9 | 10 | strategy: 11 | matrix: 12 | node-version: [20.x] 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout Code 18 | uses: actions/checkout@v4.1.1 19 | with: 20 | ref: ${{ github.base_ref }} 21 | 22 | - name: Setup Node 23 | uses: actions/setup-node@v4.0.0 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: Install Modules 28 | run: npm i 29 | - name: Run Benchmark 30 | run: npm run bench 31 | 32 | - name: Run Benchmark Resource Usage 33 | run: npm run bench:resourceUsage 34 | 35 | benchmark_branch: 36 | name: benchmark branch 37 | 38 | strategy: 39 | matrix: 40 | node-version: [20.x] 41 | 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - name: Checkout Code 46 | uses: actions/checkout@v4.1.1 47 | 48 | - name: Setup Node 49 | uses: actions/setup-node@v4.0.0 50 | with: 51 | node-version: ${{ matrix.node-version }} 52 | 53 | - name: Install Modules 54 | run: npm i 55 | 56 | - name: Run Benchmark 57 | run: npm run bench 58 | 59 | - name: Run Benchmark Resource Usage 60 | run: npm run bench:resourceUsage 61 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - next 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | env: 12 | CI: true 13 | 14 | jobs: 15 | test: 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | node-version: [18.x, 20.x] 20 | 21 | runs-on: ${{ matrix.os }} 22 | 23 | steps: 24 | - uses: actions/checkout@v4.1.1 25 | 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v4.0.0 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | 31 | - name: Install 32 | run: npm i 33 | 34 | - name: Lint 35 | run: npm run lint 36 | 37 | - name: Run tests 38 | run: npm run test:ci 39 | 40 | - name: Coverage report 41 | uses: codecov/codecov-action@v3.1.4 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # next.js build output 79 | .next 80 | 81 | # nuxt.js build output 82 | .nuxt 83 | 84 | # gatsby files 85 | .cache/ 86 | public 87 | 88 | # vuepress build output 89 | .vuepress/dist 90 | 91 | # Serverless directories 92 | .serverless/ 93 | 94 | # FuseBox cache 95 | .fusebox/ 96 | 97 | # DynamoDB Local files 98 | .dynamodb/ 99 | 100 | # TernJS port file 101 | .tern-port 102 | 103 | *.swp 104 | .clinic 105 | .DS_Store 106 | 107 | # Lock files 108 | package-lock.json 109 | yarn.lock 110 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | npm test 6 | -------------------------------------------------------------------------------- /.taprc: -------------------------------------------------------------------------------- 1 | no-check-coverage: true 2 | node-arg: ['--trace-deprecation'] 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [5.0.4](https://github.com/dnlup/doc/compare/v5.0.3...v5.0.4) (2025-02-18) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * gc ([f612783](https://github.com/dnlup/doc/commit/f612783edf1af397ae174057b46c5f0b30589de9)) 11 | 12 | ### [5.0.3](https://github.com/dnlup/doc/compare/v5.0.2...v5.0.3) (2023-12-18) 13 | 14 | ### [5.0.2](https://github.com/dnlup/doc/compare/v5.0.1...v5.0.2) (2023-11-09) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * **types:** add missing unref option ([6ad9075](https://github.com/dnlup/doc/commit/6ad9075c4c40487b1ff166c6466842158c5a767a)) 20 | 21 | ### [5.0.1](https://github.com/dnlup/doc/compare/v5.0.0...v5.0.1) (2023-11-02) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * **types:** fix exports ([a62f988](https://github.com/dnlup/doc/commit/a62f9886285cbe2fbb8fe383a2467d18c5c7005a)) 27 | 28 | ## [5.0.0](https://github.com/dnlup/doc/compare/v4.0.1...v5.0.0) (2023-11-02) 29 | 30 | 31 | ### ⚠ BREAKING CHANGES 32 | 33 | * minimum Node.js version supported is 18 34 | 35 | ### Features 36 | 37 | * add diagnostics channel support ([97ca4b4](https://github.com/dnlup/doc/commit/97ca4b4233934ecc90d9a06ae17102c44b474d4e)), closes [#579](https://github.com/dnlup/doc/issues/579) 38 | * use node histogram ([eb20316](https://github.com/dnlup/doc/commit/eb20316d1e58e3cd4818b15756d6477ee0efb72a)), closes [#442](https://github.com/dnlup/doc/issues/442) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * fix types and remove dead code ([abdfbee](https://github.com/dnlup/doc/commit/abdfbee551bc3cb8f16d7579275a2ce22741984d)) 44 | * **gc:** compute to zero nan values ([ebd33a0](https://github.com/dnlup/doc/commit/ebd33a00c164ef15b17af97e728ade48ca845274)) 45 | * remove deprecation notice ([60d6f09](https://github.com/dnlup/doc/commit/60d6f09eac7fb8002e285012f16ca329303f9265)) 46 | * **types:** fix exported types ([410bbf0](https://github.com/dnlup/doc/commit/410bbf0c19634a971316586e4ab460d9e9717125)) 47 | 48 | 49 | * drop older node versions ([70fa43d](https://github.com/dnlup/doc/commit/70fa43dc7fcaf84532a88f7b41403252ee3186c0)) 50 | 51 | ### [4.0.1](https://github.com/dnlup/doc/compare/v4.0.0...v4.0.1) (2021-03-17) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * export errors ([e0d49a2](https://github.com/dnlup/doc/commit/e0d49a210beb5e493ba49d9a787b02dbf1cceedd)) 57 | 58 | ## [4.0.0](https://github.com/dnlup/doc/compare/v4.0.0-3...v4.0.0) (2021-03-16) 59 | 60 | ## [4.0.0-3](https://github.com/dnlup/doc/compare/v4.0.0-2...v4.0.0-3) (2021-03-15) 61 | 62 | 63 | ### Features 64 | 65 | * add esm support ([e965b97](https://github.com/dnlup/doc/commit/e965b975052a23f0d473f470ea020f131a8861d7)) 66 | * **eventlooputilization:** expose single metrics ([3fca137](https://github.com/dnlup/doc/commit/3fca137d0e1af89d15cc0eaf37e93fadb96a1a2f)) 67 | 68 | ## [4.0.0-2](https://github.com/dnlup/doc/compare/v4.0.0-1...v4.0.0-2) (2021-03-14) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * **gc:** add start and stop ([300737d](https://github.com/dnlup/doc/commit/300737deca0036de6b4d566767241bae8c49e202)) 74 | 75 | ## [4.0.0-1](https://github.com/dnlup/doc/compare/v4.0.0-0...v4.0.0-1) (2021-03-13) 76 | 77 | 78 | ### ⚠ BREAKING CHANGES 79 | 80 | * **config:** the option `eventLoopOptions` has been replaced by 81 | `eventLoopDelayOptions`. 82 | 83 | ### Bug Fixes 84 | 85 | * **config:** rename eventLoopOptions ([289b10d](https://github.com/dnlup/doc/commit/289b10d666b0a00b3dac97aac8489134cbf9e7de)) 86 | 87 | ## [4.0.0-0](https://github.com/dnlup/doc/compare/v3.1.0...v4.0.0-0) (2021-03-13) 88 | 89 | 90 | ### ⚠ BREAKING CHANGES 91 | 92 | * **gc:** the gc stats are completely different now and there are 2 new options for its initialization. The types definitions changes reflect this too. 93 | 94 | ### Features 95 | 96 | * **gc:** use histogramjs ([fe7731a](https://github.com/dnlup/doc/commit/fe7731a3c4e545bbf204aea857a3cbb19d6711d1)) 97 | 98 | ## [3.1.0](https://github.com/dnlup/doc/compare/v3.0.3...v3.1.0) (2021-01-11) 99 | 100 | 101 | ### Features 102 | 103 | * add support flags ([3d0fd39](https://github.com/dnlup/doc/commit/3d0fd396817fa1f06ddcfc161d2ac37dc0327121)) 104 | 105 | ### [3.0.3](https://github.com/dnlup/doc/compare/v3.0.2...v3.0.3) (2020-11-10) 106 | 107 | ### [3.0.2](https://github.com/dnlup/doc/compare/v3.0.1...v3.0.2) (2020-10-28) 108 | 109 | ### [3.0.1](https://github.com/dnlup/doc/compare/v3.0.0...v3.0.1) (2020-10-23) 110 | 111 | ## [3.0.0](https://github.com/dnlup/doc/compare/v2.0.2...v3.0.0) (2020-10-23) 112 | 113 | 114 | ### ⚠ BREAKING CHANGES 115 | 116 | * **config:** message errors and instances are changed. 117 | 118 | ### Features 119 | 120 | * **sampler:** add event loop utilization metric ([43243db](https://github.com/dnlup/doc/commit/43243db33b6ee6b1c24da2f51489d3a5f072602f)) 121 | * **sampler:** add resourceUsage metric ([f83c1e8](https://github.com/dnlup/doc/commit/f83c1e885c0be448c1debeb68c4a46deac8b9a86)) 122 | * **types:** add resourceUsage types ([4e1e01c](https://github.com/dnlup/doc/commit/4e1e01ced353d9de0f70a5e58c862e84f38113c1)) 123 | * **types:** use NodeJS types where possible ([b7148f1](https://github.com/dnlup/doc/commit/b7148f1b8f573baa8792603efddfc89f383b8e07)) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * **config:** drop ajv-cli and build step ([b143d15](https://github.com/dnlup/doc/commit/b143d153b1df55d4e32770696491b2e7b5c31205)) 129 | 130 | 131 | ### [2.0.2](https://github.com/dnlup/doc/compare/v2.0.1...v2.0.2) (2020-10-04) 132 | 133 | 134 | ### Bug Fixes 135 | 136 | * **symbols:** fix typo ([d5f39b4](https://github.com/dnlup/doc/commit/d5f39b4f2d946c57200fda063da47e4e9c3cb5f7)) 137 | 138 | ### [2.0.1](https://github.com/dnlup/doc/compare/v2.0.0...v2.0.1) (2020-10-03) 139 | 140 | 141 | ### Bug Fixes 142 | 143 | * **sampler:** pass options from kOptions ([90fddd7](https://github.com/dnlup/doc/commit/90fddd765ff4163b21d8802923e26d415e6e1163)) 144 | 145 | ## [2.0.0](https://github.com/dnlup/doc/compare/v1.2.0-0...v2.0.0) (2020-09-22) 146 | 147 | 148 | ### ⚠ BREAKING CHANGES 149 | 150 | * the exported class is named Sampler and not Doc anymore 151 | * **types:** types are not accessible using `Doc.` notation. 152 | `DocInstance` has been renamed to `Doc` and declared as a class. 153 | * **gc:** the metric is not exposed to the event handler anymore, 154 | but it is attached directly to the Doc instance. 155 | * **eventLoopDelay:** the eventLoopDelay metric is not exposed anymore to the 156 | event handler, but it is attached to the Doc instance. 157 | * **cpu:** the cpu metric is not exposed anymore to the 158 | event handler, but it is attached to the Doc instance. The event name is 159 | changed from `data` to `sample`. 160 | 161 | ### Features 162 | 163 | * **eventLoopDelay:** expose compute method ([928670f](https://github.com/dnlup/doc/commit/928670f5a7f8989e62484d78fd18f80b1b135b3b)) 164 | * attach remaining metrics and add start options ([a5945cd](https://github.com/dnlup/doc/commit/a5945cd94202bc3ea31dbe55a9bb64e87036f5cd)) 165 | * **config:** add JSON schema validator ([4efdedd](https://github.com/dnlup/doc/commit/4efdeddf20be807f077d0336765c244b4e195a19)) 166 | * **config:** allow selection of metrics to collect ([89619a0](https://github.com/dnlup/doc/commit/89619a0c24e8099a373a97a524ea5cd2bcb3e608)) 167 | * **cpu:** attach cpu state to doc instance ([479e095](https://github.com/dnlup/doc/commit/479e09545a8a7fd7184196599b2c3f1174959d16)) 168 | * **eventLoopDelay:** attach object to doc instance ([85d7b20](https://github.com/dnlup/doc/commit/85d7b201ca93bcfd3f2dc54508f8d82935c2a81e)) 169 | * **gc:** attach metric to doc instance ([ac516d9](https://github.com/dnlup/doc/commit/ac516d9ada68ec018ba804201023b588f4da5c42)) 170 | * add activeHandles metric ([59d0710](https://github.com/dnlup/doc/commit/59d0710274c53960159a18521cf39c0c79a1cd56)) 171 | * add activeHandles metric ([1676e68](https://github.com/dnlup/doc/commit/1676e68335473629310b9c7f9e9e5281f89f9691)) 172 | * improve gc metric ([ea77ea7](https://github.com/dnlup/doc/commit/ea77ea7f0b8835b1fa05c67f0fcd576e005bdb7d)) 173 | 174 | 175 | ### Bug Fixes 176 | 177 | * **eventLoopDelay:** use Symbol for sample method ([d4021e0](https://github.com/dnlup/doc/commit/d4021e0d3b4a9115e8fb1b642c89111e0561fdda)) 178 | * **gc:** use symbols for GCAggregatedEntry methods ([356b37a](https://github.com/dnlup/doc/commit/356b37a18209fd555a6904f5e1f1dc1502a41e90)) 179 | * **lib:** fix wrong name used for options symbol ([865ba82](https://github.com/dnlup/doc/commit/865ba82a8b851f22c51108d90e3dde65ca25e3b9)) 180 | * **sampler:** exit if timer is initialized on `start` ([408b1b3](https://github.com/dnlup/doc/commit/408b1b3291e0803713978e5abc636732d0a98407)) 181 | * **types:** remove undefined from gc stats ([289d252](https://github.com/dnlup/doc/commit/289d252a8f51b8a148314cdf9178128a81894afd)) 182 | * **types:** use camel case for enum and use jsdoc ([f90bd60](https://github.com/dnlup/doc/commit/f90bd6065364b4126f1eb2d7c8fe7a88f2d005c7)) 183 | 184 | 185 | * rename Doc to Sampler and move it to lib ([2ed25ab](https://github.com/dnlup/doc/commit/2ed25ab940e35f55d2a3aad70ad92bf7b4affe5d)) 186 | * **types:** remove Doc namespace ([4a3ecc2](https://github.com/dnlup/doc/commit/4a3ecc2c568a281c33e90fe3083750e884265fc1)) 187 | 188 | ## [1.2.0-0](https://github.com/dnlup/doc/compare/v1.1.0...v1.2.0-0) (2020-07-03) 189 | 190 | 191 | ### Features 192 | 193 | * use PerformanceObserver for gc stats ([fae15d2](https://github.com/dnlup/doc/commit/fae15d27ec041173ab709a707c5cce2d7740562d)) 194 | 195 | 196 | ### Bug Fixes 197 | 198 | * gc types are number | undefined ([fae1cf4](https://github.com/dnlup/doc/commit/fae1cf4a4ff6ca32f0cc4771265564156954a65e)) 199 | * split gc into separate file to make testing easier ([fae108f](https://github.com/dnlup/doc/commit/fae108f0a528015efeaf78ab42c60c247a36e0b9)) 200 | 201 | ## [1.1.0](https://github.com/dnlup/doc/compare/v1.0.4-0...v1.1.0) (2020-06-30) 202 | 203 | 204 | ### Features 205 | 206 | * add TypeScript typings file ([fae1f3b](https://github.com/dnlup/doc/commit/fae1f3bf5429881b416fd52ddeddf2a91dee52f6)) 207 | 208 | 209 | ### Bug Fixes 210 | 211 | * include typings file when published ([fae1012](https://github.com/dnlup/doc/commit/fae1012cbc7629ca0310e0a551c2c9f86036d41e)) 212 | 213 | ### [1.0.4-0](https://github.com/dnlup/doc/compare/v1.0.3...v1.0.4-0) (2020-06-12) 214 | 215 | ### [1.0.3](https://github.com/dnlup/doc/compare/v1.0.2...v1.0.3) (2020-05-25) 216 | 217 | ### [1.0.2](https://github.com/dnlup/doc/compare/v1.0.1...v1.0.2) (2020-03-31) 218 | 219 | 220 | ### Bug Fixes 221 | 222 | * fix cpu percentage ([57d535f](https://github.com/dnlup/doc/commit/57d535f3d28e27383a0cb55d936856d346a8bfd3)) 223 | 224 | ### 1.0.1 (2020-03-30) 225 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | You found a bug or want to discuss and implement a new feature? This project welcomes contributions. 4 | 5 | The code follows the [standardjs](https://standardjs.com/) style guide. 6 | 7 | Every contribution should pass the existing tests or implementing new ones if that's the case. 8 | 9 | ```bash 10 | # Run tests locally 11 | $ npm test 12 | 13 | # Run js tests 14 | $ npm test:js 15 | 16 | # Run typescript types tests 17 | $ npm test:ts 18 | 19 | # Lint all the code 20 | $ npm lint 21 | 22 | # Lint only js files 23 | $ npm lint:js 24 | 25 | # Lint only typescript files 26 | $ npm lint:ts 27 | 28 | # Create the TOC in the README 29 | $ npm run doc 30 | ``` 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, 2021, Daniele Belardi and the doc Contributors 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # doc 2 | 3 | [![npm version](https://badge.fury.io/js/%40dnlup%2Fdoc.svg)](https://badge.fury.io/js/%40dnlup%2Fdoc) 4 | ![Tests](https://github.com/dnlup/doc/workflows/Tests/badge.svg) 5 | ![Benchmarks](https://github.com/dnlup/doc/workflows/Benchmarks/badge.svg) 6 | [![codecov](https://codecov.io/gh/dnlup/doc/branch/next/graph/badge.svg?token=2I4S01J2X3)](https://codecov.io/gh/dnlup/doc) 7 | [![Known Vulnerabilities](https://snyk.io/test/github/dnlup/doc/badge.svg?targetFile=package.json)](https://snyk.io/test/github/dnlup/doc?targetFile=package.json) 8 | 9 | > Get usage and health data about your Node.js process. 10 | 11 | `doc` is a small module that helps you collect health metrics about your Node.js process. 12 | It does that by using only the API available on Node itself (no native dependencies). 13 | It doesn't have any ties with an APM platform, so you are free to use anything you want for that purpose. 14 | Its API lets you access both computed and raw values, where possible. 15 | 16 | 17 | 18 | - [Installation](#installation) 19 | * [latest stable version](#latest-stable-version) 20 | * [latest development version](#latest-development-version) 21 | - [Usage](#usage) 22 | * [Importing with CommonJS](#importing-with-commonjs) 23 | * [Importing with ESM](#importing-with-esm) 24 | * [Note](#note) 25 | - [Enable/disable metrics collection](#enabledisable-metrics-collection) 26 | - [Garbage collection](#garbage-collection) 27 | - [Active handles](#active-handles) 28 | * [Examples](#examples) 29 | - [API](#api) 30 | * [`doc([options])`](#docoptions) 31 | * [Class: `doc.Sampler`](#class-docsampler) 32 | + [new `doc.Sampler([options])`](#new-docsampleroptions) 33 | + [Event: '`sample`'](#event-sample) 34 | + [`sampler.start()`](#samplerstart) 35 | + [`sampler.stop()`](#samplerstop) 36 | + [`sampler.cpu`](#samplercpu) 37 | + [`sampler.resourceUsage`](#samplerresourceusage) 38 | + [`sampler.eventLoopDelay`](#samplereventloopdelay) 39 | + [`sampler.eventLoopUtilization`](#samplereventlooputilization) 40 | + [`sampler.gc`](#samplergc) 41 | + [`sampler.activeHandles`](#sampleractivehandles) 42 | + [`sampler.memory`](#samplermemory) 43 | * [Class: `CpuMetric`](#class-cpumetric) 44 | + [`cpuMetric.usage`](#cpumetricusage) 45 | + [`cpuMetric.raw`](#cpumetricraw) 46 | * [Class: `ResourceUsageMetric`](#class-resourceusagemetric) 47 | + [`resourceUsage.cpu`](#resourceusagecpu) 48 | + [`resourceUsage.raw`](#resourceusageraw) 49 | * [Class: `EventLoopDelayMetric`](#class-eventloopdelaymetric) 50 | + [`eventLoopDelay.computed`](#eventloopdelaycomputed) 51 | + [`eventLoopDelay.raw`](#eventloopdelayraw) 52 | + [`eventLoopDelay.compute(raw)`](#eventloopdelaycomputeraw) 53 | * [Class: `EventLoopUtilizationMetric`](#class-eventlooputilizationmetric) 54 | + [`eventLoopUtilization.idle`](#eventlooputilizationidle) 55 | + [`eventLoopUtilization.active`](#eventlooputilizationactive) 56 | + [`eventLoopUtilization.utilization`](#eventlooputilizationutilization) 57 | + [`eventLoopUtilization.raw`](#eventlooputilizationraw) 58 | * [Class: `GCMetric`](#class-gcmetric) 59 | + [`new GCMetric(options)`](#new-gcmetricoptions) 60 | * [`gcMetric.pause`](#gcmetricpause) 61 | + [`gcMetric.major`](#gcmetricmajor) 62 | + [`gcMetric.minor`](#gcmetricminor) 63 | + [`gcMetric.incremental`](#gcmetricincremental) 64 | + [`gcMetric.weakCb`](#gcmetricweakcb) 65 | * [Class: `GCEntry`](#class-gcentry) 66 | + [`new GCEntry()`](#new-gcentry) 67 | + [`gcEntry.totalDuration`](#gcentrytotalduration) 68 | + [`gcEntry.totalCount`](#gcentrytotalcount) 69 | + [`gcEntry.mean`](#gcentrymean) 70 | + [`gcEntry.max`](#gcentrymax) 71 | + [`gcEntry.min`](#gcentrymin) 72 | + [`gcEntry.stdDeviation`](#gcentrystddeviation) 73 | + [`gcEntry.getPercentile(percentile)`](#gcentrygetpercentilepercentile) 74 | * [Class: `GCAggregatedEntry`](#class-gcaggregatedentry) 75 | + [`new GCAggregatedEntry()`](#new-gcaggregatedentry) 76 | + [`gcAggregatedEntry.flags`](#gcaggregatedentryflags) 77 | + [`gcAggregatedEntry.flags.no`](#gcaggregatedentryflagsno) 78 | + [`gcAggregatedEntry.flags.constructRetained`](#gcaggregatedentryflagsconstructretained) 79 | + [`gcAggregatedEntry.flags.forced`](#gcaggregatedentryflagsforced) 80 | + [`gcAggregatedEntry.flags.synchronousPhantomProcessing`](#gcaggregatedentryflagssynchronousphantomprocessing) 81 | + [`gcAggregatedEntry.flags.allAvailableGarbage`](#gcaggregatedentryflagsallavailablegarbage) 82 | + [`gcAggregatedEntry.flags.allExternalMemory`](#gcaggregatedentryflagsallexternalmemory) 83 | + [`gcAggregatedEntry.flags.scheduleIdle`](#gcaggregatedentryflagsscheduleidle) 84 | * [`doc.errors`](#docerrors) 85 | * [Diagnostics Channel support](#diagnostics-channel-support) 86 | - [License](#license) 87 | 88 | 89 | 90 | ## Installation 91 | 92 | ###### latest stable version 93 | 94 | ```bash 95 | $ npm i @dnlup/doc 96 | ``` 97 | 98 | ###### latest development version 99 | 100 | ```bash 101 | $ npm i @dnlup/doc@next 102 | ``` 103 | 104 | ## Usage 105 | 106 | You can import the module by using either CommonJS or ESM. 107 | 108 | By default `doc` returns a [`Sampler`](#class-docsampler) instance that collects metrics about cpu, memory usage, event loop delay and event loop utilization. 109 | 110 | ###### Importing with CommonJS 111 | 112 | ```js 113 | const doc = require('@dnlup/doc') 114 | 115 | const sampler = doc() // Use the default options 116 | 117 | sampler.on('sample', () => { 118 | doStuffWithCpuUsage(sampler.cpu.usage) 119 | doStuffWithMemoryUsage(sampler.memory) 120 | doStuffWithEventLoopDelay(sampler.eventLoopDelay.computed) 121 | doStuffWithEventLoopUtilization(sampler.eventLoopUtilization.utilization) // Available only on Node versions that support it 122 | }) 123 | ``` 124 | 125 | ###### Importing with ESM 126 | 127 | ```js 128 | import doc from '@dnlup/doc' 129 | 130 | const sampler = doc() 131 | 132 | sampler.on('sample', () => { 133 | doStuffWithCpuUsage(sampler.cpu.usage) 134 | doStuffWithMemoryUsage(sampler.memory) 135 | doStuffWithEventLoopDelay(sampler.eventLoopDelay.computed) 136 | doStuffWithEventLoopUtilization(sampler.eventLoopUtilization.utilization) // Available only on Node versions that support it 137 | }) 138 | ``` 139 | 140 | ###### Note 141 | 142 | A `Sampler` holds a snapshot of the metrics taken at the specified sample interval. 143 | This behavior makes the instance stateful. On every tick, a new snapshot will overwrite the previous one. 144 | 145 | ##### Enable/disable metrics collection 146 | 147 | You can disable the metrics that you don't need. 148 | 149 | ```js 150 | const doc = require('@dnlup/doc') 151 | 152 | // Collect only the event loop delay 153 | const sampler = doc({ collect: { cpu: false, memory: false } }) 154 | 155 | sampler.on('sample', () => { 156 | // `sampler.cpu` will be `undefined` 157 | // `sampler.memory` will be `undefined` 158 | doStuffWithEventLoopDelay(sampler.eventLoopDelay.computed) 159 | doStuffWithEventLoopUtilization(sampler.eventLoopUtilization.utilization) // Available only on Node versions that support it 160 | }) 161 | ``` 162 | 163 | You can enable more metrics if you need them. 164 | 165 | ##### Garbage collection 166 | 167 | ```js 168 | const doc = require('@dnlup/doc') 169 | 170 | const sampler = doc({ collect: { gc: true } }) 171 | sampler.on('sample', () => { 172 | doStuffWithCpuUsage(sampler.cpu.usage) 173 | doStuffWithMemoryUsage(sampler.memory) 174 | doStuffWithEventLoopDelay(sampler.eventLoopDelay.computed) 175 | doStuffWithEventLoopUtilization(sampler.eventLoopUtilization.utilization) // Available only on Node versions that support it 176 | doStuffWithGarbageCollectionDuration(sampler.gc.pause) 177 | }) 178 | ``` 179 | 180 | ##### Active handles 181 | 182 | ```js 183 | const doc = require('@dnlup/doc') 184 | 185 | const sampler = doc({ collect: { activeHandles: true } }) 186 | 187 | sampler.on('sample', () => { 188 | doStuffWithCpuUsage(sampler.cpu.usage) 189 | doStuffWithMemoryUsage(sampler.memory) 190 | doStuffWithEventLoopDelay(sampler.eventLoopDelay.computed) 191 | doStuffWithEventLoopUtilization(sampler.eventLoopUtilization.utilization) // Available only on Node versions that support it 192 | doStuffWithActiveHandles(sampler.activeHandles) 193 | }) 194 | ``` 195 | 196 | ### Examples 197 | 198 | You can find more examples in the [`examples`](./examples) folder. 199 | 200 | ## API 201 | 202 | ### `doc([options])` 203 | 204 | It creates a metrics [`Sampler`](#class-docsampler) instance with the given options. 205 | 206 | * `options` ``: same as the `Sampler` [`options`](#new-docsampleroptions). 207 | * Returns: [``](#class-docsampler) 208 | 209 | ### Class: `doc.Sampler` 210 | 211 | * Extends [`EventEmitter`](https://nodejs.org/dist/latest-v18.x/docs/api/events.html#events_class_eventemitter). 212 | 213 | Metrics sampler. 214 | 215 | It collects the selected metrics at a regular interval. A `Sampler` instance is stateful so, on each tick, 216 | only the values of the last sample are available. Each time the sampler emits the [`sample`](#event-sample) event, it will overwrite the previous one. 217 | 218 | #### new `doc.Sampler([options])` 219 | 220 | * `options` `` 221 | * `sampleInterval` ``: sample interval (ms) to get a sample. On each `sampleInterval` ms a [`sample`](#event-sample) event is emitted. **Default:** `1000` Under the hood the package uses [`monitorEventLoopDelay`](https://nodejs.org/docs/latest-v18.x/api/perf_hooks.html#perf_hooks_perf_hooks_monitoreventloopdelay_options) to track the event loop delay. 222 | * `autoStart` ``: start automatically to collect metrics. **Default:** `true`. 223 | * `unref` ``: [unref](https://nodejs.org/dist/latest-v18.x/docs/api/timers.html#timers_timeout_unref) the timer used to schedule the sampling interval. **Default:** `true`. 224 | * `gcOptions` ``: Garbage collection options 225 | * `aggregate` ``: Track and aggregate statistics about each garbage collection operation (see https://nodejs.org/docs/latest-v18.x/api/perf_hooks.html#perf_hooks_performanceentry_kind). **Default:** `false` 226 | * `flags` ``: , Track statistics about the flags of each (aggregated) garbage collection operation (see https://nodejs.org/docs/latest-v18.x/api/perf_hooks.html#perf_hooks_performanceentry_flags). `aggregate` has to be `true` to enable this option. **Default:** `true` on Node version `12.17.0` and newer. 227 | * `eventLoopDelayOptions` ``: Options to setup [`monitorEventLoopDelay`](https://nodejs.org/docs/latest-v18.x/api/perf_hooks.html#perf_hooks_perf_hooks_monitoreventloopdelay_options). **Default:** `{ resolution: 10 }` 228 | * `collect` ``: enable/disable the collection of specific metrics. 229 | * `cpu` ``: enable cpu metric. **Default:** `true`. 230 | * `resourceUsage` ``: enable [resourceUsage](https://nodejs.org/docs/latest-v18.x/api/process.html#process_process_resourceusage) metric. **Default:** `false`. 231 | * `eventLoopDelay` ``: enable eventLoopDelay metric. **Default:** `true`. 232 | * `eventLoopUtilization` ``: enable [eventLoopUtilization](https://nodejs.org/docs/latest-v18.x/api/perf_hooks.html#perf_hooks_performance_eventlooputilization_utilization1_utilization2) metric. **Default:** `true` on Node version `12.19.0` and newer. 233 | * `memory` ``: enable memory metric. **Default:** `true`. 234 | * `gc` ``: enable garbage collection metric. **Default:** `false`. 235 | * `activeHandles` ``: enable active handles collection metric. **Default:** `false`. 236 | 237 | If `options.collect.resourceUsage` is set to `true`, `options.collect.cpu` will be set to false because the cpu metric is already available in the [`resource usage metric`](#samplerresourceusage). 238 | 239 | #### Event: '`sample`' 240 | 241 | Emitted every `sampleInterval`, it signals that new data the sampler has collected new data. 242 | 243 | #### `sampler.start()` 244 | 245 | Start collecting metrics. 246 | 247 | #### `sampler.stop()` 248 | 249 | Stop collecting metrics. 250 | 251 | #### `sampler.cpu` 252 | 253 | * [``](#class-cpumetric) 254 | 255 | Resource usage metric instance. 256 | 257 | #### `sampler.resourceUsage` 258 | 259 | * [``](#class-resourceusagemetric) 260 | 261 | Resource usage metric instance. 262 | 263 | #### `sampler.eventLoopDelay` 264 | 265 | * [``](#class-eventloopdelaymetric) 266 | 267 | Event loop delay metric instance. 268 | 269 | #### `sampler.eventLoopUtilization` 270 | 271 | * [``](#class-eventlooputilizationmetric) 272 | 273 | Event loop utilization metric instance. 274 | 275 | #### `sampler.gc` 276 | 277 | * [``](#class-gcmetric) 278 | 279 | Garbage collector metric instance. 280 | 281 | #### `sampler.activeHandles` 282 | 283 | * `` 284 | 285 | Number of active handles returned by `process._getActiveHandles()`. 286 | 287 | #### `sampler.memory` 288 | 289 | * `` 290 | 291 | Object returned by [`process.memoryUsage()`](https://nodejs.org/dist/latest-v18.x/docs/api/process.html#process_process_memoryusage). 292 | 293 | ### Class: `CpuMetric` 294 | 295 | It exposes both computed and raw values of the cpu usage. 296 | 297 | #### `cpuMetric.usage` 298 | 299 | * `` 300 | 301 | Cpu usage in percentage. 302 | 303 | #### `cpuMetric.raw` 304 | 305 | * `` 306 | 307 | Raw value returned by [`process.cpuUsage()`](https://nodejs.org/dist/latest-v18.x/docs/api/process.html#process_process_cpuusage_previousvalue). 308 | 309 | ### Class: `ResourceUsageMetric` 310 | 311 | It exposes both computed and raw values of the process resource usage. 312 | 313 | #### `resourceUsage.cpu` 314 | 315 | * `` 316 | 317 | Cpu usage in percentage. 318 | 319 | #### `resourceUsage.raw` 320 | 321 | * `` 322 | 323 | Raw value returned by [`process.resourceUsage()`](https://nodejs.org/docs/latest-v18.x/api/process.html#process_process_resourceusage). 324 | 325 | ### Class: `EventLoopDelayMetric` 326 | 327 | It exposes both computed and raw values about the event loop delay. 328 | 329 | #### `eventLoopDelay.computed` 330 | 331 | * `` 332 | 333 | Event loop delay in milliseconds. It computes this value using the `mean` of the [`Histogram`](https://nodejs.org/dist/latest-v18.x/docs/api/perf_hooks.html#perf_hooks_class_histogram) instance. 334 | 335 | #### `eventLoopDelay.raw` 336 | 337 | * `` 338 | 339 | Exposes the [`Histogram`](https://nodejs.org/dist/latest-v18.x/docs/api/perf_hooks.html#perf_hooks_class_histogram) instance. 340 | 341 | #### `eventLoopDelay.compute(raw)` 342 | 343 | * `raw` `` The raw value obtained using the [`Histogram`](https://nodejs.org/dist/latest-v18.x/docs/api/perf_hooks.html#perf_hooks_class_histogram) API. 344 | * Returns `` The computed delay value. 345 | 346 | ### Class: `EventLoopUtilizationMetric` 347 | 348 | It exposes statistics about the event loop utilization. 349 | 350 | #### `eventLoopUtilization.idle` 351 | 352 | * `` 353 | 354 | The `idle` value in the object returned by [`performance.eventLoopUtilization()`](https://nodejs.org/docs/latest-v18.x/api/perf_hooks.html#perf_hooks_performance_eventlooputilization_utilization1_utilization2) during the `sampleInterval` window. 355 | 356 | #### `eventLoopUtilization.active` 357 | 358 | * `` 359 | 360 | The `active` value in the object returned by [`performance.eventLoopUtilization()`](https://nodejs.org/docs/latest-v18.x/api/perf_hooks.html#perf_hooks_performance_eventlooputilization_utilization1_utilization2) during the `sampleInterval` window. 361 | #### `eventLoopUtilization.utilization` 362 | 363 | * `` 364 | 365 | The `utilization` value in the object returned by [`performance.eventLoopUtilization()`](https://nodejs.org/docs/latest-v18.x/api/perf_hooks.html#perf_hooks_performance_eventlooputilization_utilization1_utilization2) during the `sampleInterval` window. 366 | 367 | #### `eventLoopUtilization.raw` 368 | 369 | * `` 370 | 371 | Raw value returned by [`performance.eventLoopUtilization()`](https://nodejs.org/docs/latest-v18.x/api/perf_hooks.html#perf_hooks_performance_eventlooputilization_utilization1_utilization2) during the `sampleInterval` window. 372 | 373 | ### Class: `GCMetric` 374 | 375 | It exposes the garbage collector activity statistics in the specified `sampleInterval` using hdr histograms. 376 | 377 | #### `new GCMetric(options)` 378 | 379 | * `options` ``: Configuration options 380 | * `aggregate` ``: See `gcOptions.aggregate` in the [Sampler options](#new-docsampleroptions). 381 | * `flags` ``: See `gcOptions.flags` in the [Sampler options](#new-docsampleroptions). 382 | 383 | ### `gcMetric.pause` 384 | 385 | * [``](#class-gcentry) 386 | 387 | It tracks the global activity of the garbage collector. 388 | 389 | #### `gcMetric.major` 390 | 391 | * [``](#class-gcentry) | [``](#class-gcaggregatedentry) 392 | 393 | The activity of the operation of type `major`. It's present only if `GCMetric` has been created with the option `aggregate` equal to `true`. 394 | 395 | See [`performanceEntry.kind`](https://nodejs.org/dist/latest-v18.x/docs/api/perf_hooks.html#perf_hooks_performanceentry_kind). 396 | 397 | #### `gcMetric.minor` 398 | 399 | * [``](#class-gcentry) | [``](#class-gcaggregatedentry) 400 | 401 | The activity of the operation of type `minor`. It's present only if `GCMetric` has been created with the option `aggregate` equal to `true`. 402 | 403 | See [`performanceEntry.kind`](https://nodejs.org/dist/latest-v18.x/docs/api/perf_hooks.html#perf_hooks_performanceentry_kind). 404 | 405 | #### `gcMetric.incremental` 406 | 407 | * [``](#class-gcentry) | [``](#class-gcaggregatedentry) 408 | 409 | The activity of the operation of type `incremental`. It's present only if `GCMetric` has been created with the option `aggregate` equal to `true`. 410 | 411 | See [`performanceEntry.kind`](https://nodejs.org/dist/latest-v18.x/docs/api/perf_hooks.html#perf_hooks_performanceentry_kind). 412 | 413 | #### `gcMetric.weakCb` 414 | 415 | * [``](#class-gcentry) | [``](#class-gcaggregatedentry) 416 | 417 | The activity of the operation of type `weakCb`. It's present only if `GCMetric` has been created with the option `aggregate` equal to `true`. 418 | 419 | See [`performanceEntry.kind`](https://nodejs.org/dist/latest-v18.x/docs/api/perf_hooks.html#perf_hooks_performanceentry_kind). 420 | 421 | ### Class: `GCEntry` 422 | 423 | It contains garbage collection data, represented with an [histogram](https://nodejs.org/dist/latest-v18.x/docs/api/perf_hooks.html#class-recordablehistogram-extends-histogram). All timing values are expressed in nanoseconds. 424 | 425 | #### `new GCEntry()` 426 | 427 | The initialization doesn't require options. It is created internally by a [`GCMetric`](#class-gcmetric). 428 | 429 | #### `gcEntry.totalDuration` 430 | 431 | * `` 432 | 433 | It is the total time of the entry in nanoseconds. 434 | 435 | #### `gcEntry.totalCount` 436 | 437 | * `` 438 | 439 | It is the total number of operations counted. 440 | 441 | #### `gcEntry.mean` 442 | 443 | * `` 444 | 445 | It is the mean value of the entry in nanoseconds. 446 | #### `gcEntry.max` 447 | 448 | * `` 449 | 450 | It is the maximum value of the entry in nanoseconds. 451 | #### `gcEntry.min` 452 | 453 | * `` 454 | 455 | It is the minimum value of the entry in nanoseconds. 456 | 457 | #### `gcEntry.stdDeviation` 458 | 459 | * `` 460 | 461 | It is the standard deviation of the entry in nanoseconds. 462 | 463 | #### `gcEntry.getPercentile(percentile)` 464 | 465 | * `percentile` ``: Get a percentile from the histogram. 466 | * Returns `` The percentile 467 | 468 | ### Class: `GCAggregatedEntry` 469 | 470 | It extends [`GCEntry`](#class-gcentry) and contains garbage collection data plus the flags associated with it (see https://nodejs.org/dist/latest-v18.x/docs/api/perf_hooks.html#performanceentryflags). 471 | 472 | #### `new GCAggregatedEntry()` 473 | 474 | The initialization doesn't require options. It is created internally by a [`GCMetric`](#class-gcmetric). 475 | 476 | #### `gcAggregatedEntry.flags` 477 | 478 | * `` 479 | 480 | This object contains the various histograms of each flag. 481 | #### `gcAggregatedEntry.flags.no` 482 | 483 | * [``](#class-gcentry) 484 | 485 | #### `gcAggregatedEntry.flags.constructRetained` 486 | 487 | * [``](#class-gcentry) 488 | 489 | 490 | #### `gcAggregatedEntry.flags.forced` 491 | 492 | * [``](#class-gcentry) 493 | 494 | 495 | #### `gcAggregatedEntry.flags.synchronousPhantomProcessing` 496 | 497 | * [``](#class-gcentry) 498 | 499 | 500 | #### `gcAggregatedEntry.flags.allAvailableGarbage` 501 | 502 | * [``](#class-gcentry) 503 | 504 | 505 | #### `gcAggregatedEntry.flags.allExternalMemory` 506 | 507 | * [``](#class-gcentry) 508 | 509 | #### `gcAggregatedEntry.flags.scheduleIdle` 510 | 511 | * [``](#class-gcentry) 512 | 513 | 514 | ### `doc.errors` 515 | 516 | In the `errors` object are exported all the custom errors used by the module. 517 | 518 | | Error | Error Code | Description | 519 | |-------|------------|-------------| 520 | | `InvalidArgumentError` | `DOC_ERR_INVALID_ARG` | An invalid option or argument was used | 521 | | `NotSupportedError` | `DOC_ERR_NOT_SUPPORTED` | A metric is not supported on the Node.js version used | 522 | 523 | ### Diagnostics Channel support 524 | 525 | Node [diagnostics channel](https://nodejs.org/dist/latest-v20.x/docs/api/diagnostics_channel.html) are supported. 526 | 527 | ```js 528 | const diagnosticsChannel = require('diagnostics_channel') 529 | const doc = require('@dnlup/doc) 530 | 531 | diagnosticsChannel.subscribe(doc.constants.DOC_CHANNEL, s => { 532 | console.log('A new instance', s) 533 | }) 534 | 535 | diagnosticsChannel.subscribe(doc.constants.DOC_SAMPLES_CHANNEL, s => { 536 | console.log('A new sample', s) 537 | }) 538 | 539 | doc() 540 | ``` 541 | 542 | ## License 543 | 544 | [ISC](./LICENSE) 545 | -------------------------------------------------------------------------------- /benchmarks/base.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createServer } = require('http') 4 | const port = process.env.PORT || 0 5 | 6 | function handle (req, res) { 7 | res.writeHead(200) 8 | res.end('{ "ok": true }') 9 | } 10 | 11 | const server = createServer(handle) 12 | server.listen(port) 13 | server.on('listening', () => console.log(`server listening on port ${server.address().port}`)) 14 | -------------------------------------------------------------------------------- /benchmarks/doc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createServer } = require('http') 4 | const { eventLoopUtilization } = require('perf_hooks').performance 5 | const doc = require('../') 6 | 7 | function handle (req, res) { 8 | res.writeHead(200) 9 | res.end('{ "ok": true }') 10 | } 11 | 12 | const port = process.env.PORT || 0 13 | const sampler = doc({ 14 | sampleInterval: 50, 15 | gcOptions: { 16 | aggregate: true 17 | }, 18 | collect: { 19 | gc: true, 20 | activeHandles: true 21 | } 22 | }) 23 | const server = createServer(handle) 24 | 25 | /* eslint-disable no-unused-vars */ 26 | sampler.on('sample', () => { 27 | const cpu = { 28 | usage: sampler.cpu.usage, 29 | raw: sampler.cpu.raw 30 | } 31 | const eventLoopDelay = { 32 | computed: sampler.eventLoopDelay.computed, 33 | raw: sampler.eventLoopDelay.raw 34 | } 35 | if (eventLoopUtilization) { 36 | const eventLoopUtilization = sampler.eventLoopUtilization.utilization 37 | } 38 | const memory = sampler.memory 39 | const gc = { 40 | major: sampler.gc.major, 41 | minor: sampler.gc.minor, 42 | incremental: sampler.gc.incremental, 43 | weakCb: sampler.gc.weakCb 44 | } 45 | const flags = gc.minor.flags 46 | if (flags) { 47 | const no = flags.no 48 | } 49 | const activeHandles = sampler.activeHandles 50 | /* eslint-enable no-unused-vars */ 51 | }) 52 | 53 | server.listen(port) 54 | server.on('listening', () => console.error(`server listening on port ${server.address().port}`)) 55 | -------------------------------------------------------------------------------- /benchmarks/docResourceUsage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createServer } = require('http') 4 | const { eventLoopUtilization } = require('perf_hooks').performance 5 | const doc = require('../') 6 | 7 | function handle (req, res) { 8 | res.writeHead(200) 9 | res.end('{ "ok": true }') 10 | } 11 | 12 | const port = process.env.PORT || 0 13 | const sampler = doc({ 14 | sampleInterval: 50, 15 | gcOptions: { 16 | aggregate: true 17 | }, 18 | collect: { 19 | resourceUsage: true, 20 | gc: true, 21 | activeHandles: true 22 | } 23 | }) 24 | 25 | const server = createServer(handle) 26 | /* eslint-disable no-unused-vars */ 27 | sampler.on('sample', () => { 28 | const cpu = { 29 | usage: sampler.resourceUsage.cpu, 30 | raw: sampler.resourceUsage.raw 31 | } 32 | const eventLoopDelay = { 33 | computed: sampler.eventLoopDelay.computed, 34 | raw: sampler.eventLoopDelay.raw 35 | } 36 | if (eventLoopUtilization) { 37 | const eventLoopUtilization = sampler.eventLoopUtilization.raw 38 | } 39 | const memory = sampler.memory 40 | const gc = { 41 | major: sampler.gc.major, 42 | minor: sampler.gc.minor, 43 | incremental: sampler.gc.incremental, 44 | weakCb: sampler.gc.weakCb 45 | } 46 | const activeHandles = gc.activeHandles 47 | /* eslint-enable no-unused-vars */ 48 | }) 49 | 50 | server.listen(port) 51 | 52 | server.on('listening', () => console.error(`server listening on port ${server.address().port}`)) 53 | -------------------------------------------------------------------------------- /examples/activeHandles.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const doc = require('..') 4 | 5 | // Keep the event loop alive 6 | const noop = () => {} 7 | setInterval(noop, 1000) 8 | 9 | // Initialize a sampler with the default options 10 | const sampler = doc({ 11 | collect: { 12 | activeHandles: true 13 | } 14 | }) 15 | 16 | // On sample callback 17 | const onSample = () => { 18 | console.table({ 19 | cpu: sampler.cpu.usage, 20 | ...sampler.memory, 21 | eventLoopDelay: sampler.eventLoopDelay.computed, 22 | eventLoopUtilization: doc.eventLoopUtilizationSupported ? sampler.eventLoopUtilization.utilization : 'Not Supported', 23 | activeHandles: sampler.activeHandles 24 | }) 25 | } 26 | 27 | sampler.on('sample', onSample) 28 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const doc = require('..') 4 | 5 | // Keep the event loop alive 6 | const noop = () => {} 7 | setInterval(noop, 1000) 8 | 9 | // Initialize a sampler with the default options 10 | const sampler = doc() 11 | 12 | // On sample callback 13 | const onSample = () => { 14 | console.table({ 15 | cpu: sampler.cpu.usage, 16 | ...sampler.memory, 17 | eventLoopDelay: sampler.eventLoopDelay.computed, 18 | eventLoopUtilization: doc.eventLoopUtilizationSupported ? sampler.eventLoopUtilization.utilization : 'Not Supported' 19 | }) 20 | } 21 | 22 | sampler.on('sample', onSample) 23 | -------------------------------------------------------------------------------- /examples/gc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const doc = require('..') 4 | 5 | // Keep the event loop alive 6 | const noop = () => {} 7 | setInterval(noop, 1000) 8 | 9 | // Initialize a sampler with the default options 10 | const sampler = doc({ 11 | collect: { 12 | gc: true 13 | } 14 | }) 15 | 16 | // On sample callback 17 | const onSample = () => { 18 | console.table({ 19 | cpu: sampler.cpu.usage, 20 | ...sampler.memory, 21 | eventLoopDelay: sampler.eventLoopDelay.computed, 22 | eventLoopUtilization: doc.eventLoopUtilizationSupported ? sampler.eventLoopUtilization.utilization : 'Not Supported', 23 | gc: sampler.gc.pause.mean, 24 | 'gc(99)': sampler.gc.pause.getPercentile(99) 25 | }) 26 | } 27 | 28 | sampler.on('sample', onSample) 29 | -------------------------------------------------------------------------------- /examples/gcAggregate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const doc = require('..') 4 | 5 | // Keep the event loop alive 6 | const noop = () => {} 7 | setInterval(noop, 1000) 8 | 9 | // Initialize a sampler with the default options 10 | const sampler = doc({ 11 | gcOptions: { 12 | aggregate: true, 13 | flags: false 14 | }, 15 | collect: { 16 | gc: true 17 | } 18 | }) 19 | 20 | // On sample callback 21 | const onSample = () => { 22 | console.table({ 23 | cpu: sampler.cpu.usage, 24 | ...sampler.memory, 25 | eventLoopDelay: sampler.eventLoopDelay.computed, 26 | eventLoopUtilization: doc.eventLoopUtilizationSupported ? sampler.eventLoopUtilization.utilization : 'Not Supported', 27 | gc: sampler.gc.pause.mean, 28 | 'gc(99)': sampler.gc.pause.getPercentile(99), 29 | 'gc.minor': sampler.gc.minor.mean, 30 | 'gc.minor(99)': sampler.gc.minor.getPercentile(99) 31 | }) 32 | } 33 | 34 | sampler.on('sample', onSample) 35 | -------------------------------------------------------------------------------- /examples/gcFlags.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const doc = require('..') 4 | 5 | if (!doc.gcFlagsSupported) { 6 | console.error('GC Flags are not supported on your Node.js version') 7 | process.exit(1) 8 | } 9 | 10 | // Keep the event loop alive 11 | const noop = () => {} 12 | setInterval(noop, 1000) 13 | 14 | // Initialize a sampler with the default options 15 | const sampler = doc({ 16 | gcOptions: { 17 | aggregate: true, 18 | flags: true 19 | }, 20 | collect: { 21 | gc: true 22 | } 23 | }) 24 | 25 | // On sample callback 26 | const onSample = () => { 27 | console.table({ 28 | cpu: sampler.cpu.usage, 29 | ...sampler.memory, 30 | eventLoopDelay: sampler.eventLoopDelay.computed, 31 | eventLoopUtilization: doc.eventLoopUtilizationSupported ? sampler.eventLoopUtilization.utilization : 'Not Supported', 32 | gc: sampler.gc.pause.mean, 33 | 'gc(99)': sampler.gc.pause.getPercentile(99), 34 | 'gc.minor': sampler.gc.minor.mean, 35 | 'gc.minor(99)': sampler.gc.minor.getPercentile(99), 36 | 'gc.minor.flags.no': sampler.gc.minor.flags.no.mean, 37 | 'gc.minor.flags.no(99)': sampler.gc.minor.flags.no.getPercentile(99) 38 | }) 39 | } 40 | 41 | sampler.on('sample', onSample) 42 | -------------------------------------------------------------------------------- /examples/onlyOneMetric.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const doc = require('..') 4 | 5 | // Keep the event loop alive 6 | const noop = () => {} 7 | setInterval(noop, 1000) 8 | 9 | // Initialize a sampler with the default options 10 | const sampler = doc({ 11 | collect: { 12 | cpu: true, 13 | memory: false, 14 | eventLoopDelay: false, 15 | eventLoopUtilization: false 16 | } 17 | }) 18 | 19 | // On sample callback 20 | const onSample = () => { 21 | console.table({ 22 | cpu: sampler.cpu.usage 23 | }) 24 | } 25 | 26 | sampler.on('sample', onSample) 27 | -------------------------------------------------------------------------------- /examples/ref.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const doc = require('..') 4 | 5 | // Initialize a sampler that keeps the event loop alive 6 | const sampler = doc({ 7 | unref: false 8 | }) 9 | 10 | // On sample callback 11 | const onSample = () => { 12 | console.table({ 13 | cpu: sampler.cpu.usage, 14 | ...sampler.memory, 15 | eventLoopDelay: sampler.eventLoopDelay.computed, 16 | eventLoopUtilization: doc.eventLoopUtilizationSupported ? sampler.eventLoopUtilization.utilization : 'Not Supported' 17 | }) 18 | } 19 | 20 | sampler.on('sample', onSample) 21 | 22 | process.on('SIGINT', () => { 23 | sampler.stop() 24 | }) 25 | -------------------------------------------------------------------------------- /examples/resourceUsage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const doc = require('..') 4 | 5 | // Keep the event loop alive 6 | const noop = () => {} 7 | setInterval(noop, 1000) 8 | 9 | // Initialize a sampler with the default options 10 | const sampler = doc({ 11 | collect: { 12 | resourceUsage: true 13 | } 14 | }) 15 | 16 | // On sample callback 17 | const onSample = () => { 18 | console.table({ 19 | cpu: sampler.resourceUsage.cpu, 20 | maxRSS: sampler.resourceUsage.raw.maxRSS, 21 | ...sampler.memory, 22 | eventLoopDelay: sampler.eventLoopDelay.computed, 23 | eventLoopUtilization: doc.eventLoopUtilizationSupported ? sampler.eventLoopUtilization.raw.utilization : 'Not Supported' 24 | }) 25 | } 26 | 27 | sampler.on('sample', onSample) 28 | -------------------------------------------------------------------------------- /examples/start.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const doc = require('..') 4 | 5 | // Keep the event loop alive 6 | const noop = () => {} 7 | const interval = setInterval(noop, 1000) 8 | 9 | // Initialize a sampler with the default options 10 | const sampler = doc({ 11 | autoStart: false 12 | }) 13 | 14 | // On sample callback 15 | const onSample = () => { 16 | console.table({ 17 | cpu: sampler.cpu.usage, 18 | ...sampler.memory, 19 | eventLoopDelay: sampler.eventLoopDelay.computed, 20 | eventLoopUtilization: doc.eventLoopUtilizationSupported ? sampler.eventLoopUtilization.utilization : 'Not Supported' 21 | }) 22 | } 23 | 24 | sampler.on('sample', onSample) 25 | 26 | let counter = 0 27 | 28 | // In this example, you need t send a SIGINT signal to start the sampler, then another SIGINT signal to exit. 29 | process.on('SIGINT', () => { 30 | if (counter === 0) { 31 | sampler.start() 32 | counter++ 33 | return 34 | } 35 | clearInterval(interval) 36 | }) 37 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Sampler, 3 | SamplerOptions, 4 | InstancesDiagnosticChannelHookData, 5 | SamplesDiagnosticChannelHookData 6 | } from './types/sampler' 7 | import errors from './types/errors' 8 | import { CPUMetric } from './types/cpuMetric' 9 | import { EventLoopDelayMetric } from './types/eventLoopDelayMetric' 10 | import { ResourceUsageMetric } from './types/resourceUsageMetric' 11 | import { EventLoopUtilizationMetric } from './types/eventLoopUtilizationMetric' 12 | import { GCEntry, GCAggregatedEntry, GCMetric } from './types/gcMetric' 13 | import * as constants from './types/constants' 14 | 15 | declare namespace doc { 16 | export { errors } 17 | export { 18 | Sampler, 19 | SamplerOptions, 20 | InstancesDiagnosticChannelHookData, 21 | SamplesDiagnosticChannelHookData, 22 | CPUMetric, 23 | EventLoopDelayMetric, 24 | ResourceUsageMetric, 25 | EventLoopUtilizationMetric, 26 | GCEntry, 27 | GCAggregatedEntry, 28 | GCMetric, 29 | constants 30 | } 31 | export const createSampler: (options?: SamplerOptions) => Sampler 32 | export { createSampler as default } 33 | } 34 | declare function doc(options?: doc.SamplerOptions): doc.Sampler 35 | export = doc 36 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Sampler = require('./lib/sampler') 4 | const errors = require('./lib/errors') 5 | const constants = require('./lib/constants') 6 | 7 | function createSampler (options = {}) { 8 | return new Sampler(options) 9 | } 10 | 11 | module.exports = createSampler 12 | module.exports.doc = createSampler 13 | module.exports.default = createSampler 14 | 15 | module.exports.Sampler = Sampler 16 | module.exports.errors = errors 17 | module.exports.constants = constants 18 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType, expectError, expectAssignable } from 'tsd' 2 | import doc, { 3 | errors, 4 | Sampler, 5 | CPUMetric, 6 | ResourceUsageMetric, 7 | EventLoopDelayMetric, 8 | EventLoopUtilizationMetric, 9 | GCMetric 10 | } from '.' 11 | 12 | expectAssignable(new errors.InvalidArgumentError()) 13 | expectAssignable<'InvalidArgumentError'>(new errors.InvalidArgumentError().name) 14 | expectAssignable<'DOC_ERR_INVALID_ARG'>(new errors.InvalidArgumentError().code) 15 | 16 | let sampler: Sampler 17 | 18 | // These should work 19 | sampler = doc() 20 | sampler = doc({}) 21 | sampler = doc({ sampleInterval: 1234 }) 22 | sampler = doc({ eventLoopDelayOptions: { resolution: 5678 } }) 23 | sampler = doc({ collect: { cpu: false, gc: true } }) 24 | sampler = doc({ collect: { activeHandles: true } }) 25 | sampler = doc({ collect: { gc: true, activeHandles: true } }) 26 | 27 | expectType<() => void>(sampler.start) 28 | expectType<() => void>(sampler.stop) 29 | 30 | sampler.on('sample', () => { 31 | expectType(sampler.cpu) 32 | expectType(sampler.resourceUsage) 33 | expectType(sampler.eventLoopDelay) 34 | expectType(sampler.eventLoopUtilization) 35 | expectType(sampler.gc) 36 | expectType(sampler.memory) 37 | expectType(sampler.activeHandles) 38 | }) 39 | 40 | // These should not 41 | expectError(() => { doc(1) }) 42 | expectError(() => { doc('string') }) 43 | expectError(() => { doc(null) }) 44 | expectError(() => { doc({ foo: 'bar' }) }) 45 | expectError(() => { doc({ sampleInterval: 'bar' }) }) 46 | expectError(() => { doc({ eventLoopDelayOptions: 'bar' }) }) 47 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('./debug') 4 | const { InvalidArgumentError } = require('./errors') 5 | 6 | // This object serves as both a default config and config schema. 7 | const DEFAULTS = { 8 | sampleInterval: 1000, 9 | autoStart: true, 10 | unref: true, 11 | eventLoopDelayOptions: { 12 | resolution: 10 13 | }, 14 | gcOptions: { 15 | aggregate: false, 16 | flags: true 17 | }, 18 | collect: { 19 | cpu: true, 20 | memory: true, 21 | resourceUsage: false, 22 | eventLoopDelay: true, 23 | eventLoopUtilization: true, 24 | gc: false, 25 | activeHandles: false 26 | } 27 | } 28 | 29 | function validate (conf) { 30 | if (conf.sampleInterval === undefined || conf.sampleInterval === null) { 31 | conf.sampleInterval = DEFAULTS.sampleInterval 32 | } 33 | 34 | if (typeof conf.sampleInterval !== 'number') { 35 | throw new InvalidArgumentError(`sampleInterval must be a number, received ${typeof conf.sampleInterval} ${conf.sampleInterval}`) 36 | } 37 | 38 | if (conf.sampleInterval < 1) { 39 | throw new InvalidArgumentError(`sampleInterval must be > 1, received ${conf.sampleInterval}`) 40 | } 41 | 42 | if (typeof conf.autoStart !== 'boolean') { 43 | throw new InvalidArgumentError(`autoStart must be a boolean, received ${typeof conf.autoStart} ${conf.autoStart}`) 44 | } 45 | 46 | if (typeof conf.unref !== 'boolean') { 47 | throw new InvalidArgumentError(`unref must be a boolean, received ${typeof conf.unref} ${conf.unref}`) 48 | } 49 | 50 | for (const [key, value] of Object.entries(conf.gcOptions)) { 51 | if (typeof value !== 'boolean') { 52 | throw new InvalidArgumentError(`gcOptions.${key} must be a boolean, received ${typeof value} ${value}`) 53 | } 54 | } 55 | 56 | if (conf.eventLoopDelayOptions.resolution === undefined || conf.eventLoopDelayOptions.resolution === null) { 57 | conf.eventLoopDelayOptions.resolution = DEFAULTS.eventLoopDelayOptions.resolution 58 | } 59 | 60 | if (typeof conf.eventLoopDelayOptions.resolution !== 'number') { 61 | throw new InvalidArgumentError(`eventLoopDelayOptions.resolution must be a number, received ${typeof conf.eventLoopDelayOptions.resolution} ${conf.eventLoopDelayOptions.resolution}`) 62 | } 63 | 64 | if (conf.eventLoopDelayOptions.resolution < 1) { 65 | throw new InvalidArgumentError(`eventLoopDelayOptions.resolution must be > 1, received ${conf.eventLoopDelayOptions.resolution}`) 66 | } 67 | 68 | if (conf.eventLoopDelayOptions.resolution > conf.sampleInterval) { 69 | throw new InvalidArgumentError(`eventLoopDelayOptions.resolution must be < sampleInterval, received ${conf.eventLoopDelayOptions.resolution}`) 70 | } 71 | 72 | for (const [key, value] of Object.entries(conf.collect)) { 73 | if (typeof value !== 'boolean') { 74 | throw new InvalidArgumentError(`collect.${key} must be a boolean, received ${typeof value} ${value}`) 75 | } 76 | } 77 | 78 | if (conf.collect.cpu && conf.collect.resourceUsage) { 79 | conf.collect.cpu = false 80 | debug('disabling cpu metric because resourceUsage is enabled') 81 | } 82 | } 83 | 84 | function config ({ 85 | eventLoopDelayOptions, 86 | gcOptions, 87 | collect, 88 | ...rest 89 | } = {}) { 90 | const merged = Object.assign({}, DEFAULTS, { 91 | ...rest, 92 | eventLoopDelayOptions: Object.assign({}, DEFAULTS.eventLoopDelayOptions, eventLoopDelayOptions), 93 | gcOptions: Object.assign({}, DEFAULTS.gcOptions, gcOptions), 94 | collect: Object.assign({}, DEFAULTS.collect, collect) 95 | }) 96 | 97 | const opts = { 98 | sampleInterval: merged.sampleInterval, 99 | autoStart: merged.autoStart, 100 | unref: merged.unref, 101 | eventLoopDelayOptions: { 102 | resolution: merged.eventLoopDelayOptions.resolution 103 | }, 104 | gcOptions: { 105 | aggregate: merged.gcOptions.aggregate, 106 | flags: merged.gcOptions.flags 107 | }, 108 | collect: { 109 | cpu: merged.collect.cpu, 110 | memory: merged.collect.memory, 111 | resourceUsage: merged.collect.resourceUsage, 112 | eventLoopDelay: merged.collect.eventLoopDelay, 113 | eventLoopUtilization: merged.collect.eventLoopUtilization, 114 | gc: merged.collect.gc, 115 | activeHandles: merged.collect.activeHandles 116 | } 117 | } 118 | validate(opts) 119 | return opts 120 | } 121 | 122 | module.exports = config 123 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.DOC_CHANNEL = 'dnlup.doc.sampler' 4 | exports.DOC_SAMPLES_CHANNEL = 'dnlup.doc.samples' 5 | -------------------------------------------------------------------------------- /lib/cpu.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { 4 | kRawMetric, 5 | kComputedMetric, 6 | kSample, 7 | kReset 8 | } = require('./symbols') 9 | const kCpuLastSample = Symbol('kCpuLastSample') 10 | 11 | class CpuMetric { 12 | constructor () { 13 | this[kCpuLastSample] = process.cpuUsage() 14 | this[kRawMetric] = this[kCpuLastSample] 15 | this[kComputedMetric] = 0 16 | } 17 | 18 | [kSample] (elapsedNs) { 19 | this[kRawMetric] = process.cpuUsage(this[kCpuLastSample]) 20 | this[kComputedMetric] = 100 * ((this[kRawMetric].user + this[kRawMetric].system) / (elapsedNs / 1e3)) 21 | } 22 | 23 | get usage () { 24 | return this[kComputedMetric] 25 | } 26 | 27 | get raw () { 28 | return this[kRawMetric] 29 | } 30 | 31 | [kReset] () { 32 | this[kCpuLastSample] = process.cpuUsage() 33 | } 34 | } 35 | 36 | module.exports = CpuMetric 37 | -------------------------------------------------------------------------------- /lib/debug.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('util').debuglog('@dnlup/doc') 4 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class InvalidArgumentError extends Error { 4 | constructor (message) { 5 | super(message) 6 | Error.captureStackTrace(this, InvalidArgumentError) 7 | this.name = 'InvalidArgumentError' 8 | this.code = 'DOC_ERR_INVALID_ARG' 9 | } 10 | } 11 | 12 | module.exports = { 13 | InvalidArgumentError 14 | } 15 | -------------------------------------------------------------------------------- /lib/eventLoopDelay.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { monitorEventLoopDelay } = require('perf_hooks') 4 | 5 | const { 6 | kRawMetric, 7 | kComputedMetric, 8 | kOptions, 9 | kReset, 10 | kSample 11 | } = require('./symbols') 12 | 13 | const DEFAULTS = { 14 | resolution: 10 15 | } 16 | 17 | class EventLoopDelayMetric { 18 | constructor (opts) { 19 | this[kOptions] = Object.assign({}, DEFAULTS, opts) 20 | this[kRawMetric] = monitorEventLoopDelay(this[kOptions]) 21 | this[kRawMetric].enable() 22 | } 23 | 24 | get raw () { 25 | return this[kRawMetric] 26 | } 27 | 28 | get computed () { 29 | return this[kComputedMetric] 30 | } 31 | 32 | compute (raw) { 33 | // In some cases the mean value is NaN because 34 | // there are not enough samples. 35 | const value = isNaN(raw) ? 0 : raw 36 | const delta = value / 1e6 - this[kOptions].resolution 37 | return Math.max(0, delta) 38 | } 39 | 40 | [kSample] (elapsedNs, sampleInterval) { 41 | this[kComputedMetric] = this.compute(this[kRawMetric].mean) 42 | } 43 | 44 | [kReset] () { 45 | if (monitorEventLoopDelay) { 46 | this[kRawMetric].reset() 47 | } 48 | } 49 | } 50 | 51 | module.exports = EventLoopDelayMetric 52 | -------------------------------------------------------------------------------- /lib/eventLoopUtilization.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { eventLoopUtilization } = require('perf_hooks').performance 4 | const { 5 | kRawMetric, 6 | kSample, 7 | kReset 8 | } = require('./symbols') 9 | const kLastSample = Symbol('kLastSample') 10 | 11 | class EventLoopUtiizationMetric { 12 | constructor () { 13 | this[kLastSample] = eventLoopUtilization() 14 | this[kRawMetric] = this[kLastSample] 15 | } 16 | 17 | get utilization () { 18 | return this[kRawMetric].utilization 19 | } 20 | 21 | get active () { 22 | return this[kRawMetric].active 23 | } 24 | 25 | get idle () { 26 | return this[kRawMetric].idle 27 | } 28 | 29 | get raw () { 30 | return this[kRawMetric] 31 | } 32 | 33 | [kSample] () { 34 | this[kRawMetric] = eventLoopUtilization(this[kLastSample]) 35 | } 36 | 37 | [kReset] () { 38 | this[kLastSample] = eventLoopUtilization() 39 | } 40 | } 41 | 42 | module.exports = EventLoopUtiizationMetric 43 | -------------------------------------------------------------------------------- /lib/gc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { PerformanceObserver, constants, createHistogram } = require('perf_hooks') 4 | const { 5 | NODE_PERFORMANCE_GC_MAJOR, 6 | NODE_PERFORMANCE_GC_MINOR, 7 | NODE_PERFORMANCE_GC_INCREMENTAL, 8 | NODE_PERFORMANCE_GC_WEAKCB, 9 | NODE_PERFORMANCE_GC_FLAGS_NO, 10 | NODE_PERFORMANCE_GC_FLAGS_CONSTRUCT_RETAINED, 11 | NODE_PERFORMANCE_GC_FLAGS_FORCED, 12 | NODE_PERFORMANCE_GC_FLAGS_SYNCHRONOUS_PHANTOM_PROCESSING, 13 | NODE_PERFORMANCE_GC_FLAGS_ALL_AVAILABLE_GARBAGE, 14 | NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY, 15 | NODE_PERFORMANCE_GC_FLAGS_SCHEDULE_IDLE 16 | } = constants 17 | const { 18 | kSample, 19 | kReset, 20 | kObserverCallback, 21 | kStart, 22 | kStop 23 | } = require('./symbols') 24 | 25 | const kHistogram = Symbol('kHistogram') 26 | const kTotalDuration = Symbol('kTotalDuration') 27 | 28 | class GCEntry { 29 | constructor () { 30 | this[kHistogram] = createHistogram({ 31 | lowestDiscernibleValue: 1 32 | }) 33 | this[kTotalDuration] = 0 34 | } 35 | 36 | get totalDuration () { 37 | return this[kTotalDuration] 38 | } 39 | 40 | get totalCount () { 41 | return this[kHistogram].count 42 | } 43 | 44 | get mean () { 45 | return isNaN(this[kHistogram].mean) ? 0 : this[kHistogram].mean 46 | } 47 | 48 | get max () { 49 | return this[kHistogram].max 50 | } 51 | 52 | get min () { 53 | return this[kHistogram].min 54 | } 55 | 56 | get stdDeviation () { 57 | return isNaN(this[kHistogram].stddev) ? 0 : this[kHistogram].stddev 58 | } 59 | 60 | getPercentile (percentile) { 61 | return this[kHistogram].percentile(percentile) 62 | } 63 | 64 | [kSample] (ns) { 65 | this[kTotalDuration] += ns 66 | /** 67 | * We have to adjust the value here because `record` 68 | * only accepts integer values: 69 | * https://github.com/nodejs/node/blob/cdad3d8fe5f468aec6549fd59db73a3bfe063e3c/lib/internal/histogram.js#L283-L284 70 | */ 71 | const val = Math.round(ns) 72 | if (val > 0) { 73 | this[kHistogram].record(val) 74 | } 75 | } 76 | 77 | [kReset] () { 78 | this[kTotalDuration] = 0 79 | this[kHistogram].reset() 80 | } 81 | } 82 | 83 | const kFlags = Symbol('kFlags') 84 | const kProxyFlags = Symbol('kProxyFlags') 85 | 86 | const proxyFlagsHandler = { 87 | get (flags, prop) { 88 | switch (prop) { 89 | case 'no': 90 | return flags.get(NODE_PERFORMANCE_GC_FLAGS_NO) 91 | case 'constructRetained': 92 | return flags.get(NODE_PERFORMANCE_GC_FLAGS_CONSTRUCT_RETAINED) 93 | case 'forced': 94 | return flags.get(NODE_PERFORMANCE_GC_FLAGS_FORCED) 95 | case 'synchronousPhantomProcessing': 96 | return flags.get(NODE_PERFORMANCE_GC_FLAGS_SYNCHRONOUS_PHANTOM_PROCESSING) 97 | case 'allAvailableGarbage': 98 | return flags.get(NODE_PERFORMANCE_GC_FLAGS_ALL_AVAILABLE_GARBAGE) 99 | case 'allExternalMemory': 100 | return flags.get(NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY) 101 | case 'scheduleIdle': 102 | return flags.get(NODE_PERFORMANCE_GC_FLAGS_SCHEDULE_IDLE) 103 | } 104 | } 105 | } 106 | 107 | class GCAggregatedEntry extends GCEntry { 108 | constructor () { 109 | super() 110 | this[kFlags] = new Map([ 111 | [NODE_PERFORMANCE_GC_FLAGS_NO, new GCEntry()], 112 | [NODE_PERFORMANCE_GC_FLAGS_CONSTRUCT_RETAINED, new GCEntry()], 113 | [NODE_PERFORMANCE_GC_FLAGS_FORCED, new GCEntry()], 114 | [NODE_PERFORMANCE_GC_FLAGS_SYNCHRONOUS_PHANTOM_PROCESSING, new GCEntry()], 115 | [NODE_PERFORMANCE_GC_FLAGS_ALL_AVAILABLE_GARBAGE, new GCEntry()], 116 | [NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY, new GCEntry()], 117 | [NODE_PERFORMANCE_GC_FLAGS_SCHEDULE_IDLE, new GCEntry()] 118 | ]) 119 | this[kProxyFlags] = new Proxy(this[kFlags], proxyFlagsHandler) 120 | } 121 | 122 | get flags () { 123 | return this[kProxyFlags] 124 | } 125 | 126 | [kSample] (ns, gcEntry) { 127 | super[kSample](ns) 128 | const flag = gcEntry.detail.flags 129 | const entry = this[kFlags].get(flag) 130 | 131 | /* istanbul ignore next */ 132 | if (!entry) { 133 | return 134 | } 135 | entry[kSample](ns) 136 | } 137 | 138 | [kReset] () { 139 | super[kReset]() 140 | for (const entry of this[kFlags].values()) { 141 | entry[kReset]() 142 | } 143 | } 144 | } 145 | 146 | const kPause = Symbol('kPause') 147 | const kObserver = Symbol('kObserver') 148 | const kGCEntries = Symbol('kGCEntries') 149 | 150 | class GCMetric { 151 | constructor ({ 152 | aggregate, 153 | flags 154 | } = {}) { 155 | this[kPause] = new GCEntry() 156 | if (aggregate) { 157 | const Entry = flags ? GCAggregatedEntry : GCEntry 158 | this[kGCEntries] = new Map([ 159 | [NODE_PERFORMANCE_GC_MAJOR, new Entry(flags)], 160 | [NODE_PERFORMANCE_GC_MINOR, new Entry(flags)], 161 | [NODE_PERFORMANCE_GC_INCREMENTAL, new Entry(flags)], 162 | [NODE_PERFORMANCE_GC_WEAKCB, new Entry(flags)] 163 | ]) 164 | } else { 165 | this[kGCEntries] = null 166 | } 167 | 168 | this[kObserver] = new PerformanceObserver(this[kObserverCallback].bind(this)) 169 | } 170 | 171 | [kObserverCallback] (list) { 172 | for (const gcEntry of list.getEntries()) { 173 | this[kSample](gcEntry) 174 | } 175 | } 176 | 177 | [kStart] () { 178 | this[kObserver].observe({ entryTypes: ['gc'], buffered: true }) 179 | } 180 | 181 | [kStop] () { 182 | this[kObserver].disconnect() 183 | } 184 | 185 | [kReset] () { 186 | this[kPause][kReset]() 187 | 188 | if (this[kGCEntries] === null) { 189 | return 190 | } 191 | for (const entry of this[kGCEntries].values()) { 192 | entry[kReset]() 193 | } 194 | } 195 | 196 | [kSample] (gcEntry) { 197 | const ns = gcEntry.duration * 1e6 198 | 199 | this[kPause][kSample](ns) 200 | 201 | if (this[kGCEntries] === null) { 202 | return 203 | } 204 | const entry = this[kGCEntries].get(gcEntry.detail.kind) 205 | /* istanbul ignore next */ 206 | if (!entry) { 207 | return 208 | } 209 | entry[kSample](ns, gcEntry) 210 | } 211 | 212 | get pause () { 213 | return this[kPause] 214 | } 215 | 216 | get major () { 217 | if (this[kGCEntries] === null) { 218 | return 219 | } 220 | return this[kGCEntries].get(NODE_PERFORMANCE_GC_MAJOR) 221 | } 222 | 223 | get minor () { 224 | if (this[kGCEntries] === null) { 225 | return 226 | } 227 | return this[kGCEntries].get(NODE_PERFORMANCE_GC_MINOR) 228 | } 229 | 230 | get incremental () { 231 | if (this[kGCEntries] === null) { 232 | return 233 | } 234 | return this[kGCEntries].get(NODE_PERFORMANCE_GC_INCREMENTAL) 235 | } 236 | 237 | get weakCb () { 238 | if (this[kGCEntries] === null) { 239 | return 240 | } 241 | return this[kGCEntries].get(NODE_PERFORMANCE_GC_WEAKCB) 242 | } 243 | } 244 | 245 | module.exports = { 246 | GCEntry, 247 | GCAggregatedEntry, 248 | GCMetric 249 | } 250 | -------------------------------------------------------------------------------- /lib/resourceUsage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { 4 | kRawMetric, 5 | kComputedMetric, 6 | kSample, 7 | kReset 8 | } = require('./symbols') 9 | 10 | const kLastSample = Symbol('kLastSample') 11 | 12 | class ResourceUsageMetric { 13 | constructor () { 14 | this[kLastSample] = process.resourceUsage() 15 | this[kRawMetric] = this[kLastSample] 16 | this[kComputedMetric] = 0 17 | } 18 | 19 | get cpu () { 20 | return this[kComputedMetric] 21 | } 22 | 23 | [kSample] (elapsedNs) { 24 | this[kRawMetric] = process.resourceUsage() 25 | const cpu = { 26 | user: this[kRawMetric].userCPUTime - this[kLastSample].userCPUTime, 27 | system: this[kRawMetric].systemCPUTime - this[kLastSample].systemCPUTime 28 | } 29 | this[kComputedMetric] = 100 * ((cpu.user + cpu.system) / (elapsedNs / 1e3)) 30 | } 31 | 32 | get raw () { 33 | return this[kRawMetric] 34 | } 35 | 36 | [kReset] () { 37 | this[kLastSample] = process.resourceUsage() 38 | } 39 | } 40 | 41 | module.exports = ResourceUsageMetric 42 | -------------------------------------------------------------------------------- /lib/sampler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EventEmitter = require('events') 4 | const diagnosticsChannel = require('diagnostics_channel') 5 | const EventLoopDelayMetric = require('./eventLoopDelay') 6 | const EventLoopUtilizationMetric = require('./eventLoopUtilization') 7 | const CpuMetric = require('./cpu') 8 | const ResourceUsageMetric = require('./resourceUsage') 9 | const { GCMetric } = require('./gc') 10 | const config = require('./config') 11 | const { hrtime2ns } = require('@dnlup/hrtime-utils') 12 | const { 13 | kOptions, 14 | kTimer, 15 | kStarted, 16 | kLastSampleTime, 17 | kEventLoopDelay, 18 | kEventLoopUtilization, 19 | kCpu, 20 | kResourceUsage, 21 | kGC, 22 | kActiveHandles, 23 | kMemory, 24 | kEmitSample, 25 | kSample, 26 | kReset, 27 | kStart, 28 | kStop 29 | } = require('./symbols') 30 | const { DOC_CHANNEL, DOC_SAMPLES_CHANNEL } = require('./constants') 31 | 32 | const samples = diagnosticsChannel.channel(DOC_SAMPLES_CHANNEL) 33 | const instances = diagnosticsChannel.channel(DOC_CHANNEL) 34 | 35 | class Sampler extends EventEmitter { 36 | constructor (options) { 37 | super() 38 | this[kOptions] = config(options) 39 | this[kLastSampleTime] = process.hrtime() 40 | 41 | if (this[kOptions].collect.eventLoopDelay) { 42 | this[kEventLoopDelay] = new EventLoopDelayMetric(this[kOptions].eventLoopDelayOptions) 43 | } 44 | 45 | if (this[kOptions].collect.eventLoopUtilization) { 46 | this[kEventLoopUtilization] = new EventLoopUtilizationMetric() 47 | } 48 | 49 | if (this[kOptions].collect.cpu) { 50 | this[kCpu] = new CpuMetric() 51 | } 52 | 53 | if (this[kOptions].collect.resourceUsage) { 54 | this[kResourceUsage] = new ResourceUsageMetric() 55 | } 56 | 57 | if (this[kOptions].collect.gc) { 58 | this[kGC] = new GCMetric(this[kOptions].gcOptions) 59 | } 60 | 61 | this[kSample]() 62 | if (this[kOptions].autoStart) { 63 | this.start() 64 | } 65 | if (instances.hasSubscribers) { 66 | process.nextTick(() => { 67 | try { 68 | // On Node 18 the `hasSubscribers` check is not enough so this call fails 69 | // with an uncaught exception if there are no subscribers. 70 | instances.publish(this) 71 | } catch (_) {} 72 | }) 73 | } 74 | } 75 | 76 | start () { 77 | if (this[kStarted]) return 78 | if (this[kOptions].collect.gc) { 79 | this[kGC][kStart]() 80 | } 81 | this[kTimer] = setInterval(this[kEmitSample].bind(this), this[kOptions].sampleInterval) 82 | if (this[kOptions].unref) { 83 | this[kTimer].unref() 84 | } 85 | this[kStarted] = true 86 | } 87 | 88 | stop () { 89 | clearInterval(this[kTimer]) 90 | this[kStarted] = false 91 | this[kReset]() 92 | if (this[kOptions].collect.gc) { 93 | this[kGC][kStop]() 94 | } 95 | } 96 | 97 | get cpu () { 98 | return this[kCpu] 99 | } 100 | 101 | get resourceUsage () { 102 | return this[kResourceUsage] 103 | } 104 | 105 | get eventLoopDelay () { 106 | return this[kEventLoopDelay] 107 | } 108 | 109 | get eventLoopUtilization () { 110 | return this[kEventLoopUtilization] 111 | } 112 | 113 | get gc () { 114 | return this[kGC] 115 | } 116 | 117 | get activeHandles () { 118 | return this[kActiveHandles] 119 | } 120 | 121 | get memory () { 122 | return this[kMemory] 123 | } 124 | 125 | [kEmitSample] () { 126 | this[kSample]() 127 | this.emit('sample') 128 | this[kReset]() 129 | if (samples.hasSubscribers) { 130 | try { 131 | // On Node 18 the `hasSubscribers` check is not enough so this call fails 132 | // with an uncaught exception if there are no subscribers. 133 | samples.publish(this) 134 | } catch (_) {} 135 | } 136 | } 137 | 138 | [kSample] () { 139 | const nextSampleTime = process.hrtime() 140 | const elapsedNs = hrtime2ns(nextSampleTime) - hrtime2ns(this[kLastSampleTime]) 141 | 142 | if (this[kOptions].collect.eventLoopDelay) { 143 | this[kEventLoopDelay][kSample](elapsedNs, this[kOptions].sampleInterval) 144 | } 145 | 146 | if (this[kOptions].collect.eventLoopUtilization) { 147 | this[kEventLoopUtilization][kSample]() 148 | } 149 | 150 | if (this[kOptions].collect.activeHandles) { 151 | this[kActiveHandles] = process._getActiveHandles().length 152 | } 153 | 154 | if (this[kOptions].collect.cpu) { 155 | this[kCpu][kSample](elapsedNs) 156 | } 157 | 158 | if (this[kOptions].collect.resourceUsage) { 159 | this[kResourceUsage][kSample](elapsedNs) 160 | } 161 | 162 | if (this[kOptions].collect.memory) { 163 | this[kMemory] = process.memoryUsage() 164 | } 165 | 166 | this[kLastSampleTime] = nextSampleTime 167 | } 168 | 169 | [kReset] () { 170 | if (this[kOptions].collect.eventLoopDelay) { 171 | this[kEventLoopDelay][kReset]() 172 | } 173 | 174 | if (this[kOptions].collect.eventLoopUtilization) { 175 | this[kEventLoopUtilization][kReset]() 176 | } 177 | 178 | if (this[kOptions].collect.cpu) { 179 | this[kCpu][kReset]() 180 | } 181 | 182 | if (this[kOptions].collect.resourceUsage) { 183 | this[kResourceUsage][kReset]() 184 | } 185 | 186 | if (this[kOptions].collect.gc) { 187 | this[kGC][kReset]() 188 | } 189 | } 190 | } 191 | 192 | module.exports = Sampler 193 | -------------------------------------------------------------------------------- /lib/symbols.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | kOptions: Symbol('kOptions'), 5 | kTimer: Symbol('kTimer'), 6 | kStarted: Symbol('kStarted'), 7 | kLastSampleTime: Symbol('kLastSampleTime'), 8 | kEventLoopDelay: Symbol('kEventLoopDelay'), 9 | kEventLoopUtilization: Symbol('kEventLoopUtilization'), 10 | kCpu: Symbol('kCpu'), 11 | kResourceUsage: Symbol('kResourceUsage'), 12 | kGC: Symbol('kGC'), 13 | kActiveHandles: Symbol('kActiveHandles'), 14 | kMemory: Symbol('kMemory'), 15 | kEmitSample: Symbol('kEmitSample'), 16 | kStart: Symbol('kStart'), 17 | kStop: Symbol('kStop'), 18 | kSample: Symbol('kSample'), 19 | kReset: Symbol('kReset'), 20 | kRawMetric: Symbol('kRawMetric'), 21 | kComputedMetric: Symbol('kComputedMetric'), 22 | kObserverCallback: Symbol('kObserverCallback') 23 | } 24 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.md': filenames => { 3 | const list = filenames.map(filename => `'markdown-toc -i ${filename}`) 4 | return list 5 | }, 6 | '*.{js}': ['eslint'] 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dnlup/doc", 3 | "version": "5.0.4", 4 | "description": "Get usage and health data about your Node.js process", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "exports": "./index.js", 8 | "scripts": { 9 | "lint": "eslint .", 10 | "test": "npm run test:js && npm run test:ts && npm run test:esm", 11 | "pretest:ci": "npm run lint", 12 | "test:ci": "npm run test:js -- --coverage-report=lcovonly && npm run test:ts && npm run test:esm", 13 | "test:js": "tap -J test/*.test.js", 14 | "test:ts": "attw --pack && tsd", 15 | "test:esm": "tap --no-coverage test/esm/*.test.js", 16 | "trace:ic:server": "PORT=3000 deoptigate benchmarks/doc.js", 17 | "trace:ic:resourceUsage": "PORT=3000 deoptigate benchmarks/docResourceUsage.js", 18 | "trace:ic:load": "autocannon -d 60 -c 100 -p 10 localhost:3000", 19 | "bench": "npm run bench:base && npm run bench:doc", 20 | "bench:resourceUsage": "npm run bench:base && npm run bench:doc:resourceUsage", 21 | "bench:base": "PORT=3000 concurrently -k -s first \"node benchmarks/base.js\" \"node -e 'setTimeout(()=>{}, 1000)' && autocannon -d 60 -c 100 -p 10 localhost:3000\"", 22 | "bench:doc": "PORT=3000 concurrently -k -s first \"node benchmarks/doc.js\" \"node -e 'setTimeout(()=>{}, 1000)' && autocannon -d 60 -c 100 -p 10 localhost:3000\"", 23 | "bench:doc:resourceUsage": "PORT=3000 concurrently -k -s first \"node benchmarks/docResourceUsage.js\" \"node -e 'setTimeout(()=>{}, 1000)' && autocannon -d 60 -c 100 -p 10 localhost:3000\"", 24 | "doc": "markdown-toc -i README.md", 25 | "prerelease": "npm cit", 26 | "release": "HUSKY=0 standard-version --sign", 27 | "postrelease": "npm run push && npm publish", 28 | "prenext": "npm cit", 29 | "next": "HUSKY=0 standard-version --sign --prerelease", 30 | "postnext": "npm run push && npm publish --tag next", 31 | "push": "git push origin --follow-tags `git rev-parse --abbrev-ref HEAD`", 32 | "prepare": "husky install" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/dnlup/doc.git" 37 | }, 38 | "files": [ 39 | "lib", 40 | "types", 41 | "index.js", 42 | "index.d.ts" 43 | ], 44 | "keywords": [ 45 | "process", 46 | "memory", 47 | "cpu", 48 | "event", 49 | "loop", 50 | "delay", 51 | "health", 52 | "metrics" 53 | ], 54 | "author": "dnlup ", 55 | "license": "ISC", 56 | "publishConfig": { 57 | "access": "public" 58 | }, 59 | "bugs": { 60 | "url": "https://github.com/dnlup/doc/issues" 61 | }, 62 | "homepage": "https://github.com/dnlup/doc#readme", 63 | "engines": { 64 | "node": ">=18" 65 | }, 66 | "tsd": { 67 | "directory": "." 68 | }, 69 | "devDependencies": { 70 | "@arethetypeswrong/cli": "^0.13.1", 71 | "@types/node": "^20.6.3", 72 | "atomic-sleep": "^1.0.0", 73 | "autocannon": "^7.12.0", 74 | "concurrently": "^8.2.1", 75 | "deoptigate": "^0.7.1", 76 | "eslint": "^8.49.0", 77 | "eslint-config-standard": "^17.1.0", 78 | "eslint-plugin-import": "^2.28.1", 79 | "eslint-plugin-n": "^16.1.0", 80 | "eslint-plugin-node": "^11.1.0", 81 | "eslint-plugin-promise": "^6.1.1", 82 | "eslint-plugin-standard": "^5.0.0", 83 | "husky": "^8.0.3", 84 | "is-ci": "^3.0.1", 85 | "lint-staged": "^15.0.2", 86 | "markdown-toc": "^1.2.0", 87 | "semver": "^7.5.4", 88 | "standard-version": "^9.5.0", 89 | "tap": "^16.3.8", 90 | "tsd": "^0.29.0", 91 | "typescript": "^5.2.2" 92 | }, 93 | "dependencies": { 94 | "@dnlup/hrtime-utils": "^1.1.0" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/config.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const config = require('../lib/config') 5 | const { InvalidArgumentError } = require('../lib/errors') 6 | 7 | test('validation', t => { 8 | const list = [ 9 | { 10 | config: { 11 | sampleInterval: '3' 12 | }, 13 | instanceOf: InvalidArgumentError, 14 | message: 'sampleInterval must be a number, received string 3' 15 | }, 16 | { 17 | config: { 18 | sampleInterval: {} 19 | }, 20 | instanceOf: InvalidArgumentError, 21 | message: 'sampleInterval must be a number, received object [object Object]' 22 | }, 23 | { 24 | config: { 25 | sampleInterval: 0 26 | }, 27 | instanceOf: InvalidArgumentError, 28 | message: 'sampleInterval must be > 1, received 0' 29 | }, 30 | { 31 | config: { 32 | autoStart: 0 33 | }, 34 | instanceOf: InvalidArgumentError, 35 | message: 'autoStart must be a boolean, received number 0' 36 | }, 37 | { 38 | config: { 39 | unref: 0 40 | }, 41 | instanceOf: InvalidArgumentError, 42 | message: 'unref must be a boolean, received number 0' 43 | }, 44 | { 45 | config: { 46 | eventLoopDelayOptions: { 47 | resolution: '0' 48 | } 49 | }, 50 | instanceOf: InvalidArgumentError, 51 | message: 'eventLoopDelayOptions.resolution must be a number, received string 0' 52 | }, 53 | { 54 | config: { 55 | sampleInterval: 100, 56 | eventLoopDelayOptions: { 57 | resolution: 200 58 | } 59 | }, 60 | instanceOf: InvalidArgumentError, 61 | message: 'eventLoopDelayOptions.resolution must be < sampleInterval, received 200' 62 | }, 63 | { 64 | config: { 65 | eventLoopDelayOptions: { 66 | resolution: -200 67 | } 68 | }, 69 | instanceOf: InvalidArgumentError, 70 | message: 'eventLoopDelayOptions.resolution must be > 1, received -200' 71 | }, 72 | { 73 | config: { 74 | gcOptions: { 75 | aggregate: 1 76 | } 77 | }, 78 | instanceOf: InvalidArgumentError, 79 | message: 'gcOptions.aggregate must be a boolean, received number 1' 80 | }, 81 | { 82 | config: { 83 | gcOptions: { 84 | flags: 1 85 | } 86 | }, 87 | instanceOf: InvalidArgumentError, 88 | message: 'gcOptions.flags must be a boolean, received number 1' 89 | }, 90 | { 91 | config: { 92 | collect: { 93 | cpu: '' 94 | } 95 | }, 96 | instanceOf: InvalidArgumentError, 97 | message: 'collect.cpu must be a boolean, received string ' 98 | }, 99 | { 100 | config: { 101 | collect: { 102 | memory: '' 103 | } 104 | }, 105 | instanceOf: InvalidArgumentError, 106 | message: 'collect.memory must be a boolean, received string ' 107 | }, 108 | { 109 | config: { 110 | collect: { 111 | eventLoopDelay: '' 112 | } 113 | }, 114 | instanceOf: InvalidArgumentError, 115 | message: 'collect.eventLoopDelay must be a boolean, received string ' 116 | }, 117 | { 118 | config: { 119 | collect: { 120 | eventLoopUtilization: '' 121 | } 122 | }, 123 | instanceOf: InvalidArgumentError, 124 | message: 'collect.eventLoopUtilization must be a boolean, received string ' 125 | }, 126 | { 127 | config: { 128 | collect: { 129 | gc: '' 130 | } 131 | }, 132 | instanceOf: InvalidArgumentError, 133 | message: 'collect.gc must be a boolean, received string ' 134 | }, 135 | { 136 | config: { 137 | collect: { 138 | activeHandles: '' 139 | } 140 | }, 141 | instanceOf: InvalidArgumentError, 142 | message: 'collect.activeHandles must be a boolean, received string ' 143 | } 144 | ] 145 | 146 | for (const [index, item] of list.entries()) { 147 | const error = t.throws(() => config(item.config), item.instanceOf) 148 | t.equal(error.message, item.message, `list item ${index}`) 149 | } 150 | 151 | let opts = config({ eventLoopDelayOptions: {} }) 152 | t.ok(opts.eventLoopDelayOptions.resolution, 10) 153 | opts = config({ eventLoopDelayOptions: { resolution: null } }) 154 | t.ok(opts.eventLoopDelayOptions.resolution, 10) 155 | 156 | opts = config({ sampleInterval: null }) 157 | t.equal(opts.sampleInterval, 1000) 158 | t.end() 159 | }) 160 | 161 | test('should disable cpu if resourceUsage is enabled', t => { 162 | const opts = config({ 163 | collect: { 164 | cpu: true, 165 | resourceUsage: true 166 | } 167 | }) 168 | t.notOk(opts.collect.cpu) 169 | t.ok(opts.collect.resourceUsage) 170 | t.end() 171 | }) 172 | 173 | test('default options', t => { 174 | const opts = config() 175 | t.equal(opts.sampleInterval, 1000) 176 | t.ok(opts.autoStart) 177 | t.ok(opts.unref) 178 | t.same(opts.eventLoopDelayOptions, { resolution: 10 }) 179 | t.notOk(opts.gcOptions.aggregate) 180 | t.equal(opts.gcOptions.flags, true) 181 | t.ok(opts.collect.cpu) 182 | t.ok(opts.collect.memory) 183 | t.notOk(opts.collect.resourceUsage) 184 | t.ok(opts.collect.eventLoopDelay) 185 | t.equal(opts.collect.eventLoopUtilization, true) 186 | t.notOk(opts.collect.gc) 187 | t.notOk(opts.collect.activeHandles) 188 | t.end() 189 | }) 190 | -------------------------------------------------------------------------------- /test/diagnostics_channels.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const diagnosticsChannel = require('diagnostics_channel') 5 | const doc = require('../') 6 | 7 | test('diagnostics channel support', t => { 8 | t.plan(2) 9 | t.teardown(() => { 10 | sampler.stop() 11 | }) 12 | const onInstance = s => { 13 | t.equal(s, sampler) 14 | diagnosticsChannel.unsubscribe(doc.constants.DOC_CHANNEL, onInstance) 15 | } 16 | const onSample = (s) => { 17 | t.equal(s, sampler) 18 | diagnosticsChannel.unsubscribe(doc.constants.DOC_SAMPLES_CHANNEL, onSample) 19 | } 20 | diagnosticsChannel.subscribe(doc.constants.DOC_CHANNEL, onInstance) 21 | diagnosticsChannel.subscribe(doc.constants.DOC_SAMPLES_CHANNEL, onSample) 22 | const sampler = doc({ unref: false }) 23 | }) 24 | -------------------------------------------------------------------------------- /test/doc.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const { monitorEventLoopDelay } = require('perf_hooks') 5 | const isCi = require('is-ci') 6 | const { hrtime2ms } = require('@dnlup/hrtime-utils') 7 | const doc = require('../') 8 | const { kOptions } = require('../lib/symbols') 9 | 10 | const performDetailedCheck = process.platform === 'linux' || !isCi 11 | 12 | // Since the internal timer of the Sampler instance 13 | // is unref(ed) by default, manually schedule work on the event loop 14 | // to avoid premature exiting from the test 15 | function preventTestExitingEarly (t, ms) { 16 | const timeout = setTimeout(() => {}, ms) 17 | t.teardown(() => clearTimeout(timeout)) 18 | } 19 | 20 | test('sample', t => { 21 | t.plan(1) 22 | const start = process.hrtime() 23 | const sampler = doc() 24 | 25 | preventTestExitingEarly(t, 2000) 26 | 27 | sampler.once('sample', () => { 28 | const end = process.hrtime(start) 29 | const elapsed = hrtime2ms(end) 30 | if (monitorEventLoopDelay) { 31 | t.ok(elapsed >= 1000 && elapsed < 2000) 32 | } else { 33 | t.ok(elapsed >= 500 && elapsed < 1000) 34 | } 35 | }) 36 | }) 37 | 38 | test('cpu', t => { 39 | t.plan(3) 40 | const sampler = doc() 41 | 42 | preventTestExitingEarly(t, 2000) 43 | 44 | sampler.once('sample', () => { 45 | t.equal('number', typeof sampler.cpu.usage) 46 | t.equal('object', typeof sampler.cpu.raw) 47 | let check 48 | let expected 49 | const value = sampler.cpu.usage 50 | if (performDetailedCheck) { 51 | check = value > 0 && value < 30 52 | expected = '0 < value < 30' 53 | } else { 54 | // Apparently, sometimes cpu usage is zero on windows runners. 55 | check = value >= 0 56 | expected = 'value >= 0' 57 | } 58 | t.ok(check, `expected: ${expected}, value: ${value}`) 59 | }) 60 | }) 61 | 62 | test('memory', t => { 63 | t.plan(8) 64 | const sampler = doc() 65 | 66 | preventTestExitingEarly(t, 2000) 67 | 68 | sampler.once('sample', () => { 69 | t.equal('number', typeof sampler.memory.rss) 70 | t.equal('number', typeof sampler.memory.heapTotal) 71 | t.equal('number', typeof sampler.memory.heapUsed) 72 | t.equal('number', typeof sampler.memory.external) 73 | 74 | let check 75 | let expected 76 | let value = sampler.memory.rss 77 | if (performDetailedCheck) { 78 | const level = 150 * 1e6 79 | check = value > 0 && value < level 80 | expected = `0 < value < ${level}` 81 | } else { 82 | check = value > 0 83 | expected = 'value > 0' 84 | } 85 | t.ok(check, `expected: ${expected}, value: ${value}`) 86 | 87 | value = sampler.memory.heapTotal 88 | if (performDetailedCheck) { 89 | const level = 100 * 1e6 90 | check = value > 0 && value < level 91 | expected = `0 < value < ${level}` 92 | } else { 93 | check = value > 0 94 | expected = 'value > 0' 95 | } 96 | t.ok(check, `expected: ${expected}, value: ${value}`) 97 | 98 | value = sampler.memory.heapUsed 99 | if (performDetailedCheck) { 100 | const level = 80 * 1e6 101 | check = value > 0 && value < level 102 | expected = `0 < value < ${level}` 103 | } else { 104 | check = value > 0 105 | expected = 'value > 0' 106 | } 107 | t.ok(check, `expected: ${expected}, value: ${value}`) 108 | 109 | value = sampler.memory.external 110 | if (performDetailedCheck) { 111 | const level = 10 * 35e5 112 | check = value > 0 && value < level 113 | expected = `0 < value < ${level}` 114 | } else { 115 | check = sampler.memory.external > 0 116 | expected = 'value > 0' 117 | } 118 | t.ok(check, `expected: ${expected}, value: ${value}`) 119 | }) 120 | }) 121 | 122 | test('eventLoopDelay', t => { 123 | t.plan(3) 124 | const sampler = doc() 125 | 126 | preventTestExitingEarly(t, 2000) 127 | 128 | sampler.once('sample', () => { 129 | t.equal('number', typeof sampler.eventLoopDelay.computed) 130 | t.equal('ELDHistogram', sampler.eventLoopDelay.raw.constructor.name) 131 | let check 132 | let expected 133 | const value = sampler.eventLoopDelay.computed 134 | if (performDetailedCheck) { 135 | const level = 2 136 | check = value > 0 && value < level 137 | expected = `0 < value < ${level}` 138 | } else { 139 | check = value >= 0 140 | expected = 'value >= 0' 141 | } 142 | t.ok(check, `expected: ${expected}, value: ${value}`) 143 | }) 144 | }) 145 | 146 | test('eventLoopUtilization', t => { 147 | t.plan(7) 148 | const sampler = doc() 149 | 150 | preventTestExitingEarly(t, 2000) 151 | 152 | sampler.once('sample', () => { 153 | t.equal('number', typeof sampler.eventLoopUtilization.idle) 154 | t.equal('number', typeof sampler.eventLoopUtilization.active) 155 | t.equal('number', typeof sampler.eventLoopUtilization.utilization) 156 | t.equal('object', typeof sampler.eventLoopUtilization.raw) 157 | t.equal('number', typeof sampler.eventLoopUtilization.raw.idle) 158 | t.equal('number', typeof sampler.eventLoopUtilization.raw.active) 159 | t.equal('number', typeof sampler.eventLoopUtilization.raw.utilization) 160 | }) 161 | }) 162 | 163 | test('gc', t => { 164 | t.plan(25) 165 | const sampler = doc({ 166 | gcOptions: { 167 | aggregate: true 168 | }, 169 | collect: { 170 | gc: true 171 | } 172 | }) 173 | 174 | preventTestExitingEarly(t, 2000) 175 | 176 | const value = sampler.gc 177 | sampler.once('sample', () => { 178 | for (const key of ['pause', 'major', 'minor', 'incremental', 'weakCb']) { 179 | for (const subkey of ['mean', 'totalDuration', 'totalCount', 'max', 'stdDeviation']) { 180 | const message = `${key}.${subkey} expected: number, value: ${typeof value[key][subkey]}` 181 | t.ok(typeof value[key][subkey] === 'number', message) 182 | } 183 | } 184 | }) 185 | }) 186 | 187 | test('gc aggregation with flags', t => { 188 | t.plan(145) 189 | const sampler = doc({ 190 | gcOptions: { 191 | aggregate: true 192 | }, 193 | collect: { 194 | gc: true 195 | } 196 | }) 197 | 198 | preventTestExitingEarly(t, 2000) 199 | 200 | const value = sampler.gc 201 | sampler.once('sample', () => { 202 | t.ok(typeof value.pause.mean === 'number') 203 | t.ok(typeof value.pause.totalDuration === 'number') 204 | t.ok(typeof value.pause.totalCount === 'number') 205 | t.ok(typeof value.pause.max === 'number') 206 | t.ok(typeof value.pause.stdDeviation === 'number') 207 | for (const key of ['major', 'minor', 'incremental', 'weakCb']) { 208 | for (const flag of [ 209 | 'no', 210 | 'constructRetained', 211 | 'forced', 212 | 'synchronousPhantomProcessing', 213 | 'allAvailableGarbage', 214 | 'allExternalMemory', 215 | 'scheduleIdle' 216 | ]) { 217 | for (const subkey of ['mean', 'totalDuration', 'totalCount', 'max', 'stdDeviation']) { 218 | const message = `${key}.flags.${flag}.${subkey} expected: number, value: ${typeof value[key].flags[flag][subkey]}` 219 | t.ok(typeof value[key].flags[flag][subkey] === 'number', message) 220 | } 221 | } 222 | } 223 | }) 224 | }) 225 | 226 | test('resourceUsage', t => { 227 | const sampler = doc({ collect: { resourceUsage: true } }) 228 | preventTestExitingEarly(t, 2000) 229 | sampler.once('sample', () => { 230 | t.equal(sampler.cpu, undefined) 231 | t.equal(sampler.resourceUsage.constructor.name, 'ResourceUsageMetric') 232 | t.equal(sampler[kOptions].collect.cpu, false) 233 | t.equal(sampler[kOptions].collect.resourceUsage, true) 234 | t.equal(typeof sampler.resourceUsage.raw.userCPUTime, 'number') 235 | t.equal(typeof sampler.resourceUsage.raw.systemCPUTime, 'number') 236 | t.equal(typeof sampler.resourceUsage.raw.maxRSS, 'number') 237 | t.equal(typeof sampler.resourceUsage.raw.userCPUTime, 'number') 238 | t.equal(typeof sampler.resourceUsage.raw.sharedMemorySize, 'number') 239 | t.equal(typeof sampler.resourceUsage.raw.unsharedDataSize, 'number') 240 | t.equal(typeof sampler.resourceUsage.raw.unsharedStackSize, 'number') 241 | t.equal(typeof sampler.resourceUsage.raw.minorPageFault, 'number') 242 | t.equal(typeof sampler.resourceUsage.raw.majorPageFault, 'number') 243 | t.equal(typeof sampler.resourceUsage.raw.swappedOut, 'number') 244 | t.equal(typeof sampler.resourceUsage.raw.fsRead, 'number') 245 | t.equal(typeof sampler.resourceUsage.raw.fsWrite, 'number') 246 | t.equal(typeof sampler.resourceUsage.raw.ipcSent, 'number') 247 | t.equal(typeof sampler.resourceUsage.raw.ipcReceived, 'number') 248 | t.equal(typeof sampler.resourceUsage.raw.signalsCount, 'number') 249 | t.equal(typeof sampler.resourceUsage.raw.voluntaryContextSwitches, 'number') 250 | t.equal(typeof sampler.resourceUsage.raw.involuntaryContextSwitches, 'number') 251 | t.ok(sampler.resourceUsage.cpu >= 0, `value ${sampler.resourceUsage.cpu}`) 252 | t.end() 253 | }) 254 | }) 255 | 256 | test('activeHandles', t => { 257 | t.plan(1) 258 | const sampler = doc({ 259 | collect: { 260 | activeHandles: true 261 | } 262 | }) 263 | 264 | preventTestExitingEarly(t, 2000) 265 | 266 | sampler.once('sample', () => { 267 | const value = sampler.activeHandles 268 | const check = value > 0 269 | const expected = 'value > 0' 270 | t.ok(check, `expected: ${expected}, value: ${value}`) 271 | }) 272 | }) 273 | 274 | test('custom sample interval', t => { 275 | const start = process.hrtime() 276 | const sampler = doc({ sampleInterval: 2000 }) 277 | preventTestExitingEarly(t, 4000) 278 | sampler.once('sample', () => { 279 | const end = process.hrtime(start) 280 | const elapsed = hrtime2ms(end) 281 | const message = `expected: value >= 2000, value: ${elapsed}` 282 | // For some reason in the CI this is around 1999 283 | t.ok(elapsed >= 1900, message) 284 | t.end() 285 | }) 286 | }) 287 | 288 | test('stop', t => { 289 | t.plan(4) 290 | const sampler = doc({ collect: { gc: true } }) 291 | preventTestExitingEarly(t, 3000) 292 | 293 | sampler.on('sample', () => { 294 | // On Windows CI runners the cpu is zero, smh. 295 | t.ok(sampler.cpu.usage >= 0, `cpu value: ${sampler.cpu.usage}`) 296 | t.ok(sampler.memory.heapTotal > 0, `memory value: ${sampler.memory.heapTotal}`) 297 | // On Windows CI runners the delay is zero, smh. 298 | t.ok(sampler.eventLoopDelay.computed >= 0, `delay value: ${sampler.eventLoopDelay.computed}`) 299 | t.ok(sampler.gc.pause.max >= 0, `gc value: ${sampler.gc.pause.max}`) 300 | sampler.stop() 301 | }) 302 | }) 303 | 304 | test('start and stop', t => { 305 | t.plan(9) 306 | const sampler = doc({ collect: { gc: true } }) 307 | preventTestExitingEarly(t, 6000) 308 | 309 | let c = 0 310 | const start = process.hrtime() 311 | sampler.on('sample', () => { 312 | c++ 313 | const delta = hrtime2ms(process.hrtime(start)) 314 | // On Windows CI runners the cpu is zero, smh. 315 | t.ok(sampler.cpu.usage >= 0, `cpu value: ${sampler.cpu.usage}`) 316 | t.ok(sampler.memory.heapTotal > 0, `memory value: ${sampler.memory.heapTotal}`) 317 | // On Windows CI runners the delay is zero, smh. 318 | t.ok(sampler.eventLoopDelay.computed >= 0, `delay value: ${sampler.eventLoopDelay.computed}`) 319 | t.ok(sampler.gc.pause.max >= 0, `gc value: ${sampler.gc.pause.max}`) 320 | if (c === 2) { 321 | // On Node 10 this is near 2500, while on Node > 14 322 | // it is near 3500. There must be some differences 323 | // in rescheduling the timers there. 324 | t.ok(delta > 2500, `delta value: ${delta}`) 325 | return 326 | } 327 | sampler.stop() 328 | setTimeout(() => sampler.start(), 1500) 329 | }) 330 | }) 331 | -------------------------------------------------------------------------------- /test/esm/esm.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('tap') 2 | const { execSync } = require('child_process') 3 | const { join } = require('path') 4 | 5 | test('self resolution', t => { 6 | try { 7 | const file = join(__dirname, 'selfResolution.mjs') 8 | execSync(`node ${file}`) 9 | } catch (e) { 10 | t.error(e) 11 | } finally { 12 | t.end() 13 | } 14 | }) 15 | 16 | test('named exports', t => { 17 | try { 18 | const file = join(__dirname, 'namedExports.mjs') 19 | execSync(`node ${file}`) 20 | } catch (e) { 21 | t.error(e) 22 | } finally { 23 | t.end() 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /test/esm/namedExports.mjs: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'assert' 2 | import { doc, Sampler, errors } from '../../index.js' 3 | 4 | strictEqual(typeof doc, 'function') 5 | strictEqual(typeof Sampler, 'function') 6 | strictEqual(typeof errors, 'object') 7 | -------------------------------------------------------------------------------- /test/esm/selfResolution.mjs: -------------------------------------------------------------------------------- 1 | import doc from '@dnlup/doc' 2 | 3 | const sampler = doc() 4 | 5 | sampler.on('sample', () => {}) 6 | -------------------------------------------------------------------------------- /test/eventLoopUtilization.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const sleep = require('atomic-sleep') 5 | const EventLoopUtilizationMetric = require('../lib/eventLoopUtilization') 6 | const { 7 | kSample 8 | } = require('../lib/symbols') 9 | 10 | tap.test('raw metric', t => { 11 | const eluMetric = new EventLoopUtilizationMetric() 12 | setTimeout(() => { 13 | eluMetric[kSample]() 14 | sleep(1000) 15 | eluMetric[kSample]() 16 | t.ok(eluMetric.raw.utilization > 0.7) 17 | t.ok(eluMetric.utilization > 0.7) 18 | t.end() 19 | }, 10) 20 | }) 21 | -------------------------------------------------------------------------------- /test/gc.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const { constants } = require('perf_hooks') 5 | const { 6 | GCEntry, 7 | GCAggregatedEntry, 8 | GCMetric 9 | } = require('../lib/gc') 10 | const { 11 | kReset, 12 | kObserverCallback 13 | } = require('../lib/symbols') 14 | 15 | test('garbage collection metric without aggregation', t => { 16 | const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 17 | 18 | // Creates a fake list that's the same shape as PerformanceObserver 19 | const newFakeList = (kind) => ({ 20 | getEntries: () => data.map(x => ({ 21 | duration: x, 22 | detail: { 23 | kind 24 | } 25 | }) 26 | ) 27 | }) 28 | 29 | const gc = new GCMetric() 30 | 31 | // Send in some sample data. 32 | gc[kObserverCallback](newFakeList(constants.NODE_PERFORMANCE_GC_MAJOR)) 33 | gc[kObserverCallback](newFakeList(constants.NODE_PERFORMANCE_GC_MINOR)) 34 | gc[kObserverCallback](newFakeList(constants.NODE_PERFORMANCE_GC_INCREMENTAL)) 35 | gc[kObserverCallback](newFakeList(constants.NODE_PERFORMANCE_GC_WEAKCB)) 36 | 37 | t.ok(gc.major === undefined) 38 | t.ok(gc.minor === undefined) 39 | t.ok(gc.incremental === undefined) 40 | t.ok(gc.weakCb === undefined) 41 | 42 | const entryDurationSum = data.reduce((prev, curr) => prev + curr, 0) 43 | const approxMean = 5 * 1e6 44 | 45 | t.ok(gc.pause instanceof GCEntry) 46 | t.equal(gc.pause.totalCount, 4 * data.length) 47 | t.equal(gc.pause.totalDuration, 4 * entryDurationSum * 1e6) 48 | t.equal(gc.pause.max, 10002431) 49 | t.equal(gc.pause.min, 999936) 50 | t.ok(gc.pause.mean > approxMean) 51 | t.ok(gc.pause.getPercentile(99) > 10 * 1e6) 52 | t.ok(gc.pause.stdDeviation > 0) 53 | 54 | // Check it resets correctly 55 | gc[kReset]() 56 | t.equal(gc.pause.totalCount, 0) 57 | t.equal(gc.pause.totalDuration, 0) 58 | t.equal(gc.pause.max, 0) 59 | // Not testing min value because it looks like is not initialized to zero. 60 | // t.equal(gc.pause.min, 0) 61 | t.equal(gc.pause.getPercentile(99), 0) 62 | t.end() 63 | }) 64 | 65 | test('garbage collection metric with aggregation', t => { 66 | const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 67 | 68 | // Creates a fake list that's the same shape as PerformanceObserver 69 | const newFakeList = (kind) => ({ 70 | getEntries: () => data.map(x => ({ 71 | duration: x, 72 | detail: { 73 | kind 74 | } 75 | }) 76 | ) 77 | }) 78 | 79 | const gc = new GCMetric({ 80 | aggregate: true 81 | }) 82 | 83 | // Send in some sample data. 84 | gc[kObserverCallback](newFakeList(constants.NODE_PERFORMANCE_GC_MAJOR)) 85 | gc[kObserverCallback](newFakeList(constants.NODE_PERFORMANCE_GC_MINOR)) 86 | gc[kObserverCallback](newFakeList(constants.NODE_PERFORMANCE_GC_INCREMENTAL)) 87 | gc[kObserverCallback](newFakeList(constants.NODE_PERFORMANCE_GC_WEAKCB)) 88 | 89 | t.ok(gc.major instanceof GCEntry) 90 | t.ok(gc.minor instanceof GCEntry) 91 | t.ok(gc.incremental instanceof GCEntry) 92 | t.ok(gc.weakCb instanceof GCEntry) 93 | 94 | const entryDurationSum = data.reduce((prev, curr) => prev + curr, 0) 95 | const approxMean = 5 * 1e6 96 | 97 | t.ok(gc.pause instanceof GCEntry) 98 | t.equal(gc.pause.totalCount, 4 * data.length) 99 | t.equal(gc.pause.totalDuration, 4 * entryDurationSum * 1e6) 100 | t.equal(gc.pause.max, 10002431) 101 | t.equal(gc.pause.min, 999936) 102 | t.ok(gc.pause.mean > approxMean) 103 | t.ok(gc.pause.getPercentile(99) > 10 * 1e6) 104 | t.ok(gc.pause.stdDeviation > 0) 105 | 106 | for (const entry of [ 107 | 'major', 108 | 'minor', 109 | 'incremental', 110 | 'weakCb' 111 | ]) { 112 | const errorMessage = `Failed check for entry ${entry}` 113 | t.ok(gc[entry] instanceof GCEntry, errorMessage) 114 | t.ok(gc[entry].mean > approxMean, errorMessage) 115 | t.equal(gc[entry].totalCount, data.length, errorMessage) 116 | } 117 | 118 | // Check it resets correctly 119 | gc[kReset]() 120 | t.equal(gc.pause.totalCount, 0) 121 | t.equal(gc.pause.totalDuration, 0) 122 | t.equal(gc.pause.max, 0) 123 | // Not testing min value because it looks like is not initialized to zero. 124 | // A possible discussion to follow: 125 | // * https://github.com/HdrHistogram/HdrHistogramJS/issues/11 126 | // t.equal(gc.pause.min, 0) 127 | t.equal(gc.pause.getPercentile(99), 0) 128 | 129 | for (const entry of [ 130 | 'major', 131 | 'minor', 132 | 'incremental', 133 | 'weakCb' 134 | ]) { 135 | const errorMessage = `Failed check for entry ${entry}` 136 | t.equal(gc[entry].mean, 0, errorMessage) 137 | t.equal(gc[entry].totalCount, 0, errorMessage) 138 | t.ok(gc[entry].flags === undefined) 139 | } 140 | 141 | t.end() 142 | }) 143 | 144 | test('garbage collection metric with aggregation and flags', t => { 145 | const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 146 | 147 | // Creates a fake list that's the same shape as PerformanceObserver 148 | const newFakeList = (kind) => ({ 149 | getEntries: () => data.map(x => ({ 150 | duration: x, 151 | detail: { 152 | kind, 153 | flags: constants.NODE_PERFORMANCE_GC_FLAGS_NO 154 | } 155 | }) 156 | ) 157 | }) 158 | 159 | const gc = new GCMetric({ 160 | aggregate: true, 161 | flags: true 162 | }) 163 | 164 | // Send in some sample data. 165 | gc[kObserverCallback](newFakeList(constants.NODE_PERFORMANCE_GC_MAJOR)) 166 | gc[kObserverCallback](newFakeList(constants.NODE_PERFORMANCE_GC_MINOR)) 167 | gc[kObserverCallback](newFakeList(constants.NODE_PERFORMANCE_GC_INCREMENTAL)) 168 | gc[kObserverCallback](newFakeList(constants.NODE_PERFORMANCE_GC_WEAKCB)) 169 | 170 | t.ok(gc.major instanceof GCAggregatedEntry) 171 | t.ok(gc.minor instanceof GCAggregatedEntry) 172 | t.ok(gc.incremental instanceof GCAggregatedEntry) 173 | t.ok(gc.weakCb instanceof GCAggregatedEntry) 174 | 175 | const entryDurationSum = data.reduce((prev, curr) => prev + curr, 0) 176 | const approxMean = 5 * 1e6 177 | 178 | t.ok(gc.pause instanceof GCEntry) 179 | t.equal(gc.pause.totalCount, 4 * data.length) 180 | t.equal(gc.pause.totalDuration, 4 * entryDurationSum * 1e6) 181 | t.equal(gc.pause.max, 10002431) 182 | t.equal(gc.pause.min, 999936) 183 | t.ok(gc.pause.mean > approxMean) 184 | t.ok(gc.pause.getPercentile(99) > 10 * 1e6) 185 | t.ok(gc.pause.stdDeviation > 0) 186 | 187 | for (const entry of [ 188 | 'major', 189 | 'minor', 190 | 'incremental', 191 | 'weakCb' 192 | ]) { 193 | const errorMessage = `Failed check for entry ${entry}` 194 | t.ok(gc[entry] instanceof GCAggregatedEntry, errorMessage) 195 | t.ok(gc[entry].mean > approxMean, errorMessage) 196 | t.equal(gc[entry].totalCount, data.length, errorMessage) 197 | t.ok(gc.major.flags.no instanceof GCEntry) 198 | t.ok(gc.major.flags.no.getPercentile(99) > 0) 199 | for (const value of [ 200 | 'mean', 201 | 'min', 202 | 'max', 203 | 'stdDeviation', 204 | 'totalCount', 205 | 'totalDuration' 206 | ]) { 207 | const errorMessage = `Failed check for ${entry}.flags.no.${value}` 208 | t.ok(gc.major.flags.no[value] > 0, errorMessage) 209 | } 210 | 211 | for (const flag of [ 212 | 'constructRetained', 213 | 'forced', 214 | 'synchronousPhantomProcessing', 215 | 'allAvailableGarbage', 216 | 'allExternalMemory', 217 | 'scheduleIdle' 218 | ]) { 219 | for (const value of [ 220 | 'mean', 221 | 'max', 222 | // Not testing min value because it looks like is not initialized to zero. 223 | // A possible discussion to follow: 224 | // * https://github.com/HdrHistogram/HdrHistogramJS/issues/11 225 | // 'min', 226 | 'stdDeviation', 227 | 'totalCount', 228 | 'totalDuration' 229 | ]) { 230 | const errorMessage = `Failed check for ${entry}.flags.${flag}.${value}` 231 | t.ok(gc.major.flags[flag][value] === 0, errorMessage) 232 | } 233 | } 234 | } 235 | 236 | // Check it resets correctly 237 | gc[kReset]() 238 | t.equal(gc.pause.totalCount, 0) 239 | t.equal(gc.pause.totalDuration, 0) 240 | t.equal(gc.pause.max, 0) 241 | // Not testing min value because it looks like is not initialized to zero. 242 | // A possible discussion to follow: 243 | // * https://github.com/HdrHistogram/HdrHistogramJS/issues/11 244 | // t.equal(gc.pause.min, 0) 245 | t.equal(gc.pause.getPercentile(99), 0) 246 | 247 | for (const entry of [ 248 | 'major', 249 | 'minor', 250 | 'incremental', 251 | 'weakCb' 252 | ]) { 253 | const errorMessage = `Failed check for entry ${entry}` 254 | t.equal(gc[entry].mean, 0, errorMessage) 255 | t.equal(gc[entry].totalCount, 0, errorMessage) 256 | t.ok(gc.major.flags.no instanceof GCEntry) 257 | t.equal(gc.major.flags.no.getPercentile(99), 0, errorMessage) 258 | 259 | for (const flag of [ 260 | 'no', 261 | 'constructRetained', 262 | 'forced', 263 | 'synchronousPhantomProcessing', 264 | 'allAvailableGarbage', 265 | 'allExternalMemory', 266 | 'scheduleIdle' 267 | ]) { 268 | for (const value of [ 269 | 'mean', 270 | 'max', 271 | // Not testing min value because it looks like is not initialized to zero. 272 | // 'min', 273 | 'stdDeviation', 274 | 'totalCount', 275 | 'totalDuration' 276 | ]) { 277 | const errorMessage = `Failed check for ${entry}.flags.${flag}.${value}` 278 | t.ok(gc.major.flags[flag][value] === 0, errorMessage) 279 | } 280 | } 281 | } 282 | 283 | t.end() 284 | }) 285 | -------------------------------------------------------------------------------- /test/resourceUsage.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const { hrtime2ns } = require('@dnlup/hrtime-utils') 5 | const ResourceUsageMetric = require('../lib/resourceUsage') 6 | const { 7 | kSample 8 | } = require('../lib/symbols') 9 | 10 | const cpu = function (iterations = 1e6) { 11 | for (let i = 0; i < iterations; i++) { 12 | new Date(Date.now()) // eslint-disable-line no-new 13 | } 14 | } 15 | 16 | tap.test('raw metric', t => { 17 | const resourceUsage = new ResourceUsageMetric() 18 | const start = process.hrtime() 19 | cpu() 20 | const delta = process.hrtime(start) 21 | resourceUsage[kSample](hrtime2ns(delta)) 22 | t.equal(typeof resourceUsage.raw.userCPUTime, 'number') 23 | t.equal(typeof resourceUsage.raw.systemCPUTime, 'number') 24 | t.equal(typeof resourceUsage.raw.maxRSS, 'number') 25 | t.equal(typeof resourceUsage.raw.userCPUTime, 'number') 26 | t.equal(typeof resourceUsage.raw.sharedMemorySize, 'number') 27 | t.equal(typeof resourceUsage.raw.unsharedDataSize, 'number') 28 | t.equal(typeof resourceUsage.raw.unsharedStackSize, 'number') 29 | t.equal(typeof resourceUsage.raw.minorPageFault, 'number') 30 | t.equal(typeof resourceUsage.raw.majorPageFault, 'number') 31 | t.equal(typeof resourceUsage.raw.swappedOut, 'number') 32 | t.equal(typeof resourceUsage.raw.fsRead, 'number') 33 | t.equal(typeof resourceUsage.raw.fsWrite, 'number') 34 | t.equal(typeof resourceUsage.raw.ipcSent, 'number') 35 | t.equal(typeof resourceUsage.raw.ipcReceived, 'number') 36 | t.equal(typeof resourceUsage.raw.signalsCount, 'number') 37 | t.equal(typeof resourceUsage.raw.voluntaryContextSwitches, 'number') 38 | t.equal(typeof resourceUsage.raw.involuntaryContextSwitches, 'number') 39 | t.end() 40 | }) 41 | 42 | tap.test('computed metric', t => { 43 | const resourceUsage = new ResourceUsageMetric() 44 | const start = process.hrtime() 45 | cpu() 46 | const delta = process.hrtime(start) 47 | resourceUsage[kSample](hrtime2ns(delta)) 48 | t.ok(resourceUsage.cpu > 50, `value ${resourceUsage.cpu}`) 49 | t.end() 50 | }) 51 | -------------------------------------------------------------------------------- /types/constants.d.ts: -------------------------------------------------------------------------------- 1 | export const DOC_CHANNEL = 'dnlup.doc.sampler' 2 | export const DOC_SAMPLES_CHANNEL = 'dnlup.doc.samples' 3 | -------------------------------------------------------------------------------- /types/cpuMetric.d.ts: -------------------------------------------------------------------------------- 1 | export interface CPUMetric { 2 | /** 3 | * Usage percentage 4 | */ 5 | usage: number, 6 | /** 7 | * Raw value returned by `process.cpuUsage()` 8 | */ 9 | raw: NodeJS.CpuUsage 10 | } 11 | -------------------------------------------------------------------------------- /types/errors.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Errors { 2 | /** An argument or option was invalid */ 3 | export class InvalidArgumentError extends Error { 4 | name: 'InvalidArgumentError' 5 | code: 'DOC_ERR_INVALID_ARG' 6 | } 7 | } 8 | 9 | export = Errors 10 | -------------------------------------------------------------------------------- /types/eventLoopDelayMetric.d.ts: -------------------------------------------------------------------------------- 1 | import { IntervalHistogram } from "perf_hooks"; 2 | 3 | export interface EventLoopDelayMetric { 4 | /** 5 | * computed delay in milliseconds 6 | */ 7 | computed: number, 8 | raw: IntervalHistogram 9 | } 10 | -------------------------------------------------------------------------------- /types/eventLoopUtilizationMetric.d.ts: -------------------------------------------------------------------------------- 1 | import { EventLoopUtilization } from "perf_hooks" 2 | 3 | export interface EventLoopUtilizationMetric { 4 | /** 5 | * Raw metric value 6 | */ 7 | raw: EventLoopUtilization 8 | } 9 | -------------------------------------------------------------------------------- /types/gcMetric.d.ts: -------------------------------------------------------------------------------- 1 | import { RecordableHistogram } from 'perf_hooks'; 2 | 3 | declare enum GCFlag { 4 | /** perf_hooks.constants.NODE_PERFORMANCE_GC_FLAGS_NO */ 5 | No = 'no', 6 | /** perf_hooks.constants.NODE_PERFORMANCE_GC_FLAGS_CONSTRUCT_RETAINED */ 7 | ConstructRetained = 'constructRetained', 8 | /** perf_hooks.constants.NODE_PERFORMANCE_GC_FLAGS_FORCED */ 9 | Forced = 'forced', 10 | /** perf_hooks.constants.NODE_PERFORMANCE_GC_FLAGS_SYNCHRONOUS_PHANTOM_PROCESSING */ 11 | SynchronousPhantomProcessing = 'synchronousPhantomProcessing', 12 | /** perf_hooks.constants.NODE_PERFORMANCE_GC_FLAGS_ALL_AVAILABLE_GARBAGE */ 13 | AllAvailableGarbage = 'allAvailableGarbage', 14 | /** perf_hooks.constants.NODE_PERFORMANCE_GC_FLAGS_ALL_EXTERNAL_MEMORY */ 15 | AllExternalMemory = 'allExternalMemory', 16 | /** perf_hooks.constants.NODE_PERFORMANCE_GC_FLAGS_SCHEDULE_IDLE */ 17 | ScheduleIdle = 'scheduleIdle' 18 | } 19 | 20 | export class GCEntry { 21 | /** 22 | * Total time of the entry in nanoseconds 23 | */ 24 | totalDuration: number; 25 | /** 26 | * Total number of operations counted 27 | */ 28 | // @types/node is not aligned with Node implementation. Let's wait for a fix. 29 | // @ts-ignore 30 | totalCount: RecordableHistogram['count']; 31 | /** 32 | * Mean value in nanoseconds 33 | */ 34 | mean: RecordableHistogram['mean']; 35 | /** 36 | * Max value in nanoseconds 37 | */ 38 | max: RecordableHistogram['max']; 39 | /** 40 | * Min value in nanoseconds 41 | */ 42 | min: RecordableHistogram['min']; 43 | /** 44 | * Standard deviation in nanoseconds 45 | */ 46 | stdDeviation: RecordableHistogram['stddev']; 47 | /** 48 | * Get a percentile 49 | */ 50 | getValueAtPercentile: RecordableHistogram['percentile']; 51 | } 52 | 53 | export class GCAggregatedEntry extends GCEntry { 54 | flags: { 55 | no: GCEntry, 56 | constructRetained: GCEntry, 57 | forced: GCEntry, 58 | synchronousPhantomProcessing: GCEntry, 59 | allAvailableGarbage: GCEntry, 60 | allExternalMemory: GCEntry, 61 | scheduleIdle: GCEntry 62 | } 63 | } 64 | 65 | declare interface GCMetricOptions { 66 | /** 67 | * Aggregate statistic by the type of GC operation. 68 | */ 69 | aggregate: boolean, 70 | /** 71 | * Enable tracking of GC flags in aggregated metric 72 | */ 73 | flags: boolean 74 | } 75 | 76 | export class GCMetric { 77 | constructor(options: GCMetricOptions); 78 | pause: GCEntry; 79 | major: GCEntry|GCAggregatedEntry|undefined; 80 | minor: GCEntry|GCAggregatedEntry|undefined; 81 | incremental: GCEntry|GCAggregatedEntry|undefined; 82 | weakCb: GCEntry|GCAggregatedEntry|undefined; 83 | } 84 | -------------------------------------------------------------------------------- /types/resourceUsageMetric.d.ts: -------------------------------------------------------------------------------- 1 | import { resourceUsage } from "process"; 2 | 3 | type ResourceUsage = ReturnType 4 | 5 | export interface ResourceUsageMetric { 6 | /** 7 | * Cpu usage percentage 8 | */ 9 | cpu: number, 10 | /** 11 | * Raw vaule returned by `process.resourceUsage()` 12 | */ 13 | raw: ResourceUsage 14 | } 15 | -------------------------------------------------------------------------------- /types/sampler.d.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { CPUMetric } from './cpuMetric' 3 | import { EventLoopDelayMetric } from './eventLoopDelayMetric' 4 | import { ResourceUsageMetric } from './resourceUsageMetric' 5 | import { EventLoopUtilizationMetric } from './eventLoopUtilizationMetric' 6 | import { GCMetric, GCMetricOptions } from './gcMetric' 7 | 8 | declare interface SamplerOptions { 9 | /** 10 | * Sample interval (ms), each `sampleInterval` ms a data event is emitted. 11 | */ 12 | sampleInterval?: number, 13 | 14 | /** 15 | * Unreference the sampling timer. If set to `true` the timer 16 | * will leep the event loop alive. 17 | */ 18 | unref?: boolean 19 | /** 20 | * Garbage collection metric options 21 | */ 22 | gcOptions?: GCMetricOptions, 23 | 24 | /** 25 | * Options to setup `perf_hooks.monitorEventLoopDelay`. 26 | */ 27 | eventLoopDelayOptions?: { 28 | /** 29 | * The sampling rate in milliseconds. Must be greater than zero and than the sampleInterval. Default: 10. 30 | */ 31 | resolution: number, 32 | } 33 | /** 34 | * Enable/disable specific metrics 35 | */ 36 | collect?: { 37 | cpu?: boolean, 38 | resourceUsage?: boolean, 39 | eventLoopDelay?: boolean, 40 | eventLoopUtilization?: boolean, 41 | memory?: boolean, 42 | gc?: boolean, 43 | activeHandles?: boolean 44 | } 45 | } 46 | 47 | export class Sampler extends EventEmitter { 48 | constructor(options?: SamplerOptions) 49 | cpu?: CPUMetric 50 | resourceUsage?: ResourceUsageMetric 51 | eventLoopDelay?: EventLoopDelayMetric 52 | eventLoopUtilization?: EventLoopUtilizationMetric 53 | gc?: GCMetric 54 | memory?: NodeJS.MemoryUsage 55 | activeHandles?: number 56 | on(event: 'sample', listener: () => void): this 57 | start(): void 58 | stop(): void 59 | } 60 | 61 | export type InstancesDiagnosticChannelHookData = Sampler 62 | export type SamplesDiagnosticChannelHookData = Sampler 63 | --------------------------------------------------------------------------------