├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .nycrc.yaml ├── .taprc ├── BUPROF_A.html ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis ├── aggregate │ ├── aggregate-node.js │ ├── combine-as-aggregate-nodes.js │ ├── mark-http-aggregate-nodes.js │ ├── mark-module-aggregate-nodes.js │ ├── mark-party-aggregate-nodes.js │ └── name-aggregate-nodes.js ├── barrier │ ├── barrier-node.js │ ├── make-external-barrier-nodes.js │ ├── make-synchronous-barrier-nodes.js │ ├── name-barrier-nodes.js │ └── wrap-as-barrier-nodes.js ├── cluster │ ├── anonymise-cluster-frames.js │ ├── cluster-node.js │ └── combine-as-cluster-nodes.js ├── index.js ├── raw-event │ ├── join-as-raw-event.js │ └── raw-event.js ├── source │ ├── combine-as-source-nodes.js │ ├── filter-source-nodes.js │ ├── http-request-nodes.js │ ├── identify-source-nodes.js │ ├── restructure-net-source-nodes.js │ └── source-node.js ├── stack-trace │ ├── frames.js │ ├── stack-trace.js │ └── wrap-as-stack-trace.js ├── system-info.js └── trace-event │ ├── trace-event.js │ └── wrap-as-trace-event.js ├── collect ├── stack-trace.js └── system-info.js ├── debug ├── aggregate-nodes-to-dprof.js ├── dprof-dump.js ├── dprof-test-server.js ├── extract-aggregate-nodes.js ├── inspect-dump.js ├── inspect-test-server.js ├── visualize-all.js ├── visualize-mod.js └── visualize-watch.js ├── format ├── abstract-decoder.js ├── abstract-encoder.js ├── stack-trace-decoder.js ├── stack-trace-encoder.js ├── stack-trace.proto ├── system-info-decoder.js └── trace-event-decoder.js ├── index.js ├── injects ├── detect-port.js ├── logger.js └── no-cluster.js ├── package.json ├── screenshot.png ├── test ├── analysis-aggregate-combine.test.js ├── analysis-aggregate-mark-http.test.js ├── analysis-aggregate-mark-module.test.js ├── analysis-aggregate-mark-party.test.js ├── analysis-aggregate.test.js ├── analysis-barrier-make-external.test.js ├── analysis-barrier-make-synchronous-barrier-parent.test.js ├── analysis-barrier-make-synchronous.test.js ├── analysis-barrier-name-barrier-nodes.test.js ├── analysis-barrier-wrap.test.js ├── analysis-barrier.test.js ├── analysis-cluster-combine.test.js ├── analysis-cluster.test.js ├── analysis-raw-event-join.test.js ├── analysis-raw-event.test.js ├── analysis-source-combine.test.js ├── analysis-source-filter.test.js ├── analysis-source-http-requests.test.js ├── analysis-source-indentify.test.js ├── analysis-source-restructure-net.test.js ├── analysis-source.test.js ├── analysis-stack-trace-frames.test.js ├── analysis-stack-trace.test.js ├── analysis-system-info.test.js ├── analysis-trace-event.test.js ├── analysis-util │ ├── aggregate-node.js │ ├── barrier-node.js │ ├── cluster-node.js │ ├── index.js │ ├── source-node.js │ └── system-info.js ├── analysis.test.js ├── cmd-collect-analysing.test.js ├── cmd-collect-detect-port.test.js ├── cmd-collect-exit-sigint.script.js ├── cmd-collect-exit.test.js ├── cmd-collect-node-options-env.script.js ├── cmd-collect-node-options-env.test.js ├── cmd-collect.test.js ├── cmd-dest.test.js ├── cmd-no-cluster.cluster.js ├── cmd-no-cluster.script.js ├── cmd-no-cluster.test.js ├── cmd-visualize.test.js ├── collect-and-read.js ├── collect-get-logging-paths.test.js ├── collect-stack-trace.test.js ├── fixtures-wasm │ ├── .gitignore │ ├── package.json │ ├── say-hello.ts │ └── say-hello.wasm ├── format-stack-trace.test.js ├── format-trace-events.test.js ├── integration-servers.test.js ├── integration-timeout.test.js ├── node_modules │ └── fake-data-fetch │ │ ├── fetch-data.js │ │ ├── find-db.js │ │ └── index.js ├── servers │ ├── basic.js │ ├── express.js │ ├── external.js │ ├── latency.js │ └── quine.js ├── visualizer-data-callback-event.test.js ├── visualizer-data-dataset.test.js ├── visualizer-data-node.test.js ├── visualizer-layout-connections.test.js ├── visualizer-layout-layer.test.js ├── visualizer-layout-node-allocation.test.js ├── visualizer-layout-positioning.test.js ├── visualizer-layout-scale.test.js ├── visualizer-layout-stems.test.js ├── visualizer-layout.test.js ├── visualizer-line-coordinates.test.js ├── visualizer-util │ ├── fake-layered-nodes.js │ ├── fake-overlapping-nodes.js │ ├── fake-topology.js │ ├── fakedata.json │ ├── prepare-fake-nodes.js │ ├── sampledata-acmeair.json │ ├── sampledata-slowio.json │ └── verify-garbage-collection.js └── visualizer-validation.test.js └── visualizer ├── app-logo.svg ├── clinic-favicon.png.b64 ├── data ├── callback-event.js ├── data-node.js ├── dataset.js ├── frame.js └── index.js ├── draw ├── area-chart.js ├── banner.css ├── breadcrumb-panel.js ├── bubbleprof-ui.js ├── d3-subset.js ├── frames.css ├── frames.js ├── header.css ├── hover-box.js ├── html-content-types.js ├── html-content.js ├── index.js ├── interactive-key.js ├── lookup.js ├── side-bar-drag.js ├── static-key.js ├── svg-container.js ├── svg-node-diagram.js ├── svg-node-section.js └── svg-node.js ├── layout ├── collapsed-layout.js ├── connections.js ├── index.js ├── layout-node.js ├── layout.js ├── line-coordinates.js ├── node-allocation.js ├── positioning.js ├── scale.js └── stems.js ├── main.js ├── nearform-logo.svg ├── style.css └── validation.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request, workflow_dispatch] 2 | 3 | name: CI 4 | 5 | jobs: 6 | lint: 7 | name: Lint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Install Node.js 18 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: 18 15 | - name: Install dependencies 16 | run: npm install 17 | - name: Check linting 18 | run: npm run ci-lint 19 | 20 | test: 21 | name: Test 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | os: [ubuntu-latest, macos-latest, windows-latest] 26 | node-version: [16, 18, 20] 27 | 28 | runs-on: ${{matrix.os}} 29 | steps: 30 | - uses: actions/checkout@v2 31 | - name: Install Node.js ${{matrix.node-version}} 32 | uses: actions/setup-node@v1 33 | with: 34 | node-version: ${{matrix.node-version}} 35 | - name: Install dependencies 36 | run: npm install 37 | - name: Run tests 38 | run: npm run ci-cov 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # clinic-bubbleprof specific 2 | *.clinic-bubbleprof 3 | *.clinic-bubbleprof.html 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 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 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | /node_modules 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # OSX filesystem meta 64 | .DS_Store 65 | 66 | # Visual studio code 67 | .vscode 68 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nycrc.yaml: -------------------------------------------------------------------------------- 1 | # # This option avoid an wrap by nyc that affects our expected async events. 2 | # # See: https://github.com/clinicjs/node-clinic-bubbleprof/pull/382 3 | use-spawn-wrap: true 4 | -------------------------------------------------------------------------------- /.taprc: -------------------------------------------------------------------------------- 1 | timeout: 50 2 | jobs: 1 3 | statements: 95 4 | branches: 90 5 | functions: 95 6 | lines: 95 7 | test-env: NODE_OPTIONS=--no-warnings 8 | -------------------------------------------------------------------------------- /BUPROF_A.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicjs/node-clinic-bubbleprof/cf801d49299b8ecd25c5d9e88f662defc4bbad71/BUPROF_A.html -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [report@clinicjs.org][clinic]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [clinic]: mailto:report@clinicjs.org 74 | [homepage]: https://www.contributor-covenant.org 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) NearForm and Clinic.js Contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clinic.js Bubbleprof 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/clinicjs/node-clinic-bubbleprof.svg)](https://greenkeeper.io/) 4 | [![npm version][npm-version]][npm-url] [![Stability Stable][stability-stable]][stability-docs] [![Github Actions build status][actions-status]][actions-url] 5 | [![Downloads][npm-downloads]][npm-url] [![Code style][lint-standard]][lint-standard-url] 6 | 7 | Programmable interface to [Clinic.js][clinic-url] Bubbleprof. Learn more about Clinic.js: https://clinicjs.org/ 8 | 9 | ![Screenshot](screenshot.png) 10 | 11 | ## Issues 12 | 13 | To open an issue, please use the [main repository](https://github.com/clinicjs/node-clinic) with the `bubbleprof` label. 14 | 15 | ## Installation 16 | 17 | ```console 18 | npm i -S @clinic/bubbleprof 19 | ``` 20 | 21 | ## Supported node versions 22 | 23 | * Node.js 16 and above 24 | 25 | ## Example 26 | 27 | ```js 28 | const ClinicBubbleprof = require('@clinic/bubbleprof') 29 | const bubbleprof = new ClinicBubbleprof() 30 | 31 | bubbleprof.collect(['node', './path-to-script.js'], function (err, filepath) { 32 | if (err) throw err 33 | 34 | bubbleprof.visualize(filepath, filepath + '.html', function (err) { 35 | if (err) throw err 36 | }) 37 | }) 38 | ``` 39 | 40 | To get started with Clinic.js Bubbleprof you might want to take a look at the [examples 41 | repo](https://github.com/clinicjs/node-clinic-bubbleprof-examples). 42 | 43 | ## Documentation 44 | 45 | ```js 46 | const ClinicBubbleprof = require('@clinic/bubbleprof') 47 | const bubbleprof = new ClinicBubbleprof() 48 | ``` 49 | 50 | ### new ClinicBubbleprof([settings]) 51 | 52 | * settings [``][] 53 | * detectPort [``][] **Default**: false 54 | * debug [``][] If set to true, the generated html will not be minified. 55 | **Default**: false 56 | * dest [``][] The folder where the collected data is stored. 57 | **Default**: '.' 58 | 59 | #### `bubbleprof.collect(args, callback)` 60 | 61 | Starts a process by using: 62 | 63 | ```js 64 | const { spawn } = require('child_process') 65 | spawn(args[0], ['-r', 'sampler.js'].concat(args.slice(1))) 66 | ``` 67 | 68 | The injected sampler will produce a file in the current working directory, with 69 | the process `PID` in its filename. The filepath relative to the current working 70 | directory will be the value in the callback. 71 | 72 | stdout, stderr, and stdin will be relayed to the calling process. As will the 73 | `SIGINT` event. 74 | 75 | #### `bubbleprof.visualize(dataFilename, outputFilename, callback)` 76 | 77 | Will consume the data file specified by `dataFilename`, this data file will be 78 | produced by the sampler using `bubbleprof.collect`. 79 | 80 | `bubbleprof.visualize` will then output a standalone HTML file to 81 | `outputFilename`. When completed the callback will be called with no extra 82 | arguments, except a possible error. 83 | 84 | ## License 85 | [MIT](LICENSE) 86 | 87 | [stability-stable]: https://img.shields.io/badge/stability-stable-green.svg?style=flat-square 88 | [stability-docs]: https://nodejs.org/api/documentation.html#documentation_stability_index 89 | [npm-version]: https://img.shields.io/npm/v/@clinic/bubbleprof.svg?style=flat-square 90 | [npm-url]: https://www.npmjs.org/@clinic/bubbleprof 91 | [npm-downloads]: http://img.shields.io/npm/dm/@clinic/bubbleprof.svg?style=flat-square 92 | [lint-standard]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 93 | [lint-standard-url]: https://github.com/feross/standard 94 | [clinic-url]: https://github.com/clinicjs/node-clinic 95 | [``]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object 96 | [``]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type 97 | [``]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String 98 | [actions-status]: https://github.com/clinicjs/node-clinic-bubbleprof/workflows/CI/badge.svg 99 | [actions-url]: https://github.com/clinicjs/node-clinic-bubbleprof/actions 100 | -------------------------------------------------------------------------------- /analysis/aggregate/aggregate-node.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const util = require('util') 4 | const SourceNode = require('../source/source-node.js') 5 | const Frames = require('../stack-trace/frames.js') 6 | 7 | class Mark { 8 | constructor () { 9 | this.mark = [null, null, null] /* party, module, name */ 10 | } 11 | 12 | format () { 13 | if (this.mark[0] === null) { 14 | return 'null' 15 | } else if (this.mark[1] === null) { 16 | return `${this.mark[0]}` 17 | } else if (this.mark[2] === null) { 18 | return `${this.mark[0]}.${this.mark[1]}` 19 | } else { 20 | return `${this.mark[0]}.${this.mark[1]}.${this.mark[2]}` 21 | } 22 | } 23 | 24 | [util.inspect.custom] (depth, options) { 25 | if (depth < 0) { 26 | return `<${options.stylize('Mark', 'special')}>` 27 | } 28 | 29 | return `<${options.stylize('Mark', 'special')} ${options.stylize(this.format(), 'string')}>` 30 | } 31 | 32 | toJSON () { 33 | return this.mark 34 | } 35 | 36 | set (index, value) { 37 | if (index < 0 || index >= 3) { 38 | throw new RangeError(`index ${index} is out of range in mark object`) 39 | } 40 | 41 | this.mark[index] = value 42 | } 43 | 44 | get (index) { 45 | if (index < 0 || index >= 3) { 46 | throw new RangeError(`index ${index} is out of range in mark object`) 47 | } 48 | 49 | return this.mark[index] 50 | } 51 | } 52 | 53 | class AggregateNode { 54 | constructor (aggregateId, parentAggregateId) { 55 | this.aggregateId = aggregateId 56 | this.parentAggregateId = parentAggregateId 57 | this.children = [] 58 | this.sources = [] 59 | 60 | this.isRoot = false 61 | this.mark = new Mark() 62 | this.type = null 63 | this.name = null 64 | this.frames = new Frames([]) 65 | } 66 | 67 | [util.inspect.custom] (depth, options) { 68 | const nestedOptions = Object.assign({}, options, { 69 | depth: depth === null ? null : depth - 1 70 | }) 71 | if (depth === null) depth = Infinity 72 | 73 | if (depth < 0) { 74 | return `<${options.stylize('AggregateNode', 'special')}>` 75 | } 76 | 77 | const framesFormatted = util.inspect(this.frames, nestedOptions) 78 | const childrenFormatted = this.children 79 | .map((child) => options.stylize(child, 'number')) 80 | .join(', ') 81 | 82 | return `<${options.stylize('AggregateNode', 'special')}` + 83 | ` type:${options.stylize(this.type, 'string')},` + 84 | ` mark:${util.inspect(this.mark, nestedOptions)},` + 85 | ` aggregateId:${options.stylize(this.aggregateId, 'number')},` + 86 | ` parentAggregateId:${options.stylize(this.parentAggregateId, 'number')},` + 87 | ` sources.length:${options.stylize(this.sources.length, 'number')},` + 88 | ` children:[${childrenFormatted}],` + 89 | ` frames:${framesFormatted}>` 90 | } 91 | 92 | toJSON () { 93 | return { 94 | aggregateId: this.aggregateId, 95 | parentAggregateId: this.parentAggregateId, 96 | name: this.name, 97 | children: this.children, 98 | mark: this.mark.toJSON(), 99 | type: this.type, 100 | frames: this.frames.toJSON(), 101 | // frames and type are the same for all SourceNode's, so remove them 102 | // from the SourceNode data. 103 | sources: this.sources.map((source) => source.toJSON({ short: true })) 104 | } 105 | } 106 | 107 | makeRoot () { 108 | const root = new SourceNode(1) 109 | root.makeRoot() 110 | 111 | this.addSourceNode(root) 112 | this.isRoot = true 113 | this.mark.set(0, 'root') 114 | } 115 | 116 | addChild (aggregateId) { 117 | this.children.push(aggregateId) 118 | } 119 | 120 | getChildren () { 121 | return this.children 122 | } 123 | 124 | addSourceNode (sourceNode) { 125 | if (this.sources.length === 0) { 126 | this.type = sourceNode.type 127 | this.frames = sourceNode.frames 128 | } 129 | 130 | this.sources.push(sourceNode) 131 | } 132 | 133 | getSourceNodes (sourceNode) { 134 | return this.sources 135 | } 136 | } 137 | 138 | module.exports = AggregateNode 139 | -------------------------------------------------------------------------------- /analysis/aggregate/combine-as-aggregate-nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | const AggregateNode = require('./aggregate-node.js') 4 | 5 | class CombineAsAggregateNodes extends stream.Transform { 6 | constructor () { 7 | super({ 8 | readableObjectMode: true, 9 | writableObjectMode: true 10 | }) 11 | 12 | // Index incomming SourceNode by their parentAsyncId. This will tell 13 | // what parent SourceNode they have, which is necessary information to 14 | // build the aggregated tree. 15 | this._parentAsyncIdIndex = new Map() 16 | 17 | // the root node as aggregateId = 1 18 | const root = new AggregateNode(1, 0) 19 | root.makeRoot() 20 | 21 | // maintain a map of nodes such aggregateIds can be translated to AggregateNode 22 | // objects 23 | this._aggregateId = 2 24 | this._aggregateNodes = new Map() 25 | this._aggregateNodes.set(root.aggregateId, root) 26 | } 27 | 28 | _newAggregateNode (parentAggregateNode) { 29 | const childNode = new AggregateNode( 30 | this._aggregateId++, parentAggregateNode.aggregateId 31 | ) 32 | this._aggregateNodes.set(childNode.aggregateId, childNode) 33 | return childNode 34 | } 35 | 36 | _transform (sourceNode, encoding, callback) { 37 | if (this._parentAsyncIdIndex.has(sourceNode.parentAsyncId)) { 38 | this._parentAsyncIdIndex.get(sourceNode.parentAsyncId).push(sourceNode) 39 | } else { 40 | this._parentAsyncIdIndex.set(sourceNode.parentAsyncId, [sourceNode]) 41 | } 42 | 43 | callback(null) 44 | } 45 | 46 | _findAndAssignChildren (parentAggregateNode) { 47 | const identifierIndex = new Map() 48 | 49 | // get SourceNode belonging to the parent AggregateNode 50 | for (const parentSourceNode of parentAggregateNode.getSourceNodes()) { 51 | if (!this._parentAsyncIdIndex.has(parentSourceNode.asyncId)) { 52 | continue 53 | } 54 | 55 | // check children of current sourceNode of the AggregateNode 56 | const children = this._parentAsyncIdIndex.get(parentSourceNode.asyncId) 57 | for (const childSourceNode of children) { 58 | // if this is a new identifier create a new AggregateNode for it 59 | if (!identifierIndex.has(childSourceNode.identifier)) { 60 | const childNode = this._newAggregateNode(parentAggregateNode) 61 | identifierIndex.set(childSourceNode.identifier, childNode) 62 | parentAggregateNode.addChild(childNode.aggregateId) 63 | } 64 | 65 | // add SourceNode child to the new AggregateNode object 66 | identifierIndex.get(childSourceNode.identifier) 67 | .addSourceNode(childSourceNode) 68 | } 69 | } 70 | } 71 | 72 | _flush (callback) { 73 | // basic non-recursive BFS (breadth-first-search) 74 | const queue = [1] // root has aggregateId = 1 75 | while (queue.length > 0) { 76 | // get node from queue and assign children to it 77 | const aggregateId = queue.shift() 78 | const aggregateNode = this._aggregateNodes.get(aggregateId) 79 | this._findAndAssignChildren(aggregateNode) 80 | 81 | // once a node has been assigned all its children, no more mutations 82 | // will be done to the object. It can thus be pushed to the next stream. 83 | this.push(aggregateNode) 84 | 85 | // Add children of the newly updated node to the queue 86 | queue.push(...aggregateNode.getChildren()) 87 | } 88 | 89 | callback(null) 90 | } 91 | } 92 | 93 | module.exports = CombineAsAggregateNodes 94 | -------------------------------------------------------------------------------- /analysis/aggregate/mark-http-aggregate-nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | 4 | class MarkHttpAggregateNodes extends stream.Transform { 5 | constructor () { 6 | super({ 7 | readableObjectMode: true, 8 | writableObjectMode: true 9 | }) 10 | 11 | // Track servers 12 | this._tcpServerNodeIds = new Set() 13 | this._tcpOnconnectionNodeIds = new Set() 14 | } 15 | 16 | _transform (aggregateNode, encoding, done) { 17 | if (aggregateNode.type === 'TCPSERVERWRAP' || 18 | aggregateNode.type === 'PIPESERVERWRAP') { 19 | this._tcpServerNodeIds.add(aggregateNode.aggregateId) 20 | aggregateNode.mark.set(1, 'net') 21 | aggregateNode.mark.set(2, 'server') 22 | } else if (this._tcpServerNodeIds.has(aggregateNode.parentAggregateId) && 23 | (aggregateNode.type === 'TCPWRAP' || 24 | aggregateNode.type === 'PIPEWRAP')) { 25 | this._tcpOnconnectionNodeIds.add(aggregateNode.aggregateId) 26 | aggregateNode.mark.set(1, 'net') 27 | aggregateNode.mark.set(2, 'onconnection') 28 | } else if (this._tcpOnconnectionNodeIds.has(aggregateNode.parentAggregateId) && 29 | aggregateNode.type === 'HTTPPARSER') { 30 | aggregateNode.mark.set(1, 'net') 31 | aggregateNode.mark.set(2, 'onrequest') 32 | } 33 | 34 | done(null, aggregateNode) 35 | } 36 | } 37 | 38 | module.exports = MarkHttpAggregateNodes 39 | -------------------------------------------------------------------------------- /analysis/aggregate/mark-module-aggregate-nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | 4 | class MarkModuleAggregateNodes extends stream.Transform { 5 | constructor (systemInfo) { 6 | super({ 7 | readableObjectMode: true, 8 | writableObjectMode: true 9 | }) 10 | 11 | this.systemInfo = systemInfo 12 | } 13 | 14 | _transform (aggregateNode, encoding, done) { 15 | if (aggregateNode.mark.get(0) === 'external') { 16 | const firstModule = aggregateNode.frames 17 | .filter((frame) => !frame.isNodecore(this.systemInfo)) 18 | .map((frame) => frame.getModuleName(this.systemInfo)) 19 | .pop() 20 | 21 | aggregateNode.mark.set(1, firstModule.name) 22 | } 23 | 24 | done(null, aggregateNode) 25 | } 26 | } 27 | 28 | module.exports = MarkModuleAggregateNodes 29 | -------------------------------------------------------------------------------- /analysis/aggregate/mark-party-aggregate-nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | 4 | class MarkPartyAggregateNodes extends stream.Transform { 5 | constructor (systemInfo) { 6 | super({ 7 | readableObjectMode: true, 8 | writableObjectMode: true 9 | }) 10 | 11 | this.systemInfo = systemInfo 12 | } 13 | 14 | _transform (aggregateNode, encoding, done) { 15 | if (aggregateNode.isRoot) { 16 | return done(null, aggregateNode) 17 | } 18 | 19 | const fileFrames = aggregateNode.frames.filter((frame) => frame.fileName) 20 | 21 | // If there is no stack, the handle is created in C++. Check if 22 | // it is a nodecore handle. 23 | if (fileFrames.length === 0 && 24 | this.systemInfo.providers.has(aggregateNode.type)) { 25 | aggregateNode.mark.set(0, 'nodecore') // second party 26 | return done(null, aggregateNode) 27 | } 28 | 29 | // There are no frames, but the provider was not from nodecore. Assume 30 | // it is created by an external module. 31 | if (fileFrames.length === 0) { 32 | aggregateNode.mark.set(0, 'external') // third party 33 | return done(null, aggregateNode) 34 | } 35 | 36 | // There is a stack, check if it is purely internal to nodecore. 37 | if (fileFrames.every((frame) => frame.isNodecore(this.systemInfo))) { 38 | aggregateNode.mark.set(0, 'nodecore') // second party 39 | return done(null, aggregateNode) 40 | } 41 | 42 | // If frames are external (includes modecore), but not all are nodecore 43 | if (fileFrames.every((frame) => frame.isExternal(this.systemInfo))) { 44 | aggregateNode.mark.set(0, 'external') // third party 45 | return done(null, aggregateNode) 46 | } 47 | 48 | // The frame is not nodecore nor external, assume it is relevant to 49 | // the user. 50 | aggregateNode.mark.set(0, 'user') // first party 51 | return done(null, aggregateNode) 52 | } 53 | } 54 | 55 | module.exports = MarkPartyAggregateNodes 56 | -------------------------------------------------------------------------------- /analysis/aggregate/name-aggregate-nodes.js: -------------------------------------------------------------------------------- 1 | const { Transform } = require('stream') 2 | const path = require('path') 3 | 4 | class Name extends Transform { 5 | constructor (sysInfo) { 6 | super({ writableObjectMode: true, readableObjectMode: true }) 7 | this.systemInfo = sysInfo 8 | } 9 | 10 | _transform (data, enc, cb) { 11 | const name = getAggregateName(data, this.systemInfo) 12 | data.name = name 13 | cb(null, data) 14 | } 15 | } 16 | 17 | module.exports = Name 18 | 19 | function getAggregateName (aggregateNode, sysInfo) { 20 | const frames = aggregateNode.frames.filter(frame => frame.fileName) 21 | const interesting = toArray(frames.filter(frame => name(frame))) 22 | 23 | const userland = interesting.filter(isUserland(sysInfo)) 24 | 25 | // here is a list of observed generic identifiers where the lcoation should be better described, either by another 26 | // frame in the node, or by the filename - this can be added to 27 | const tooGeneric = [ 28 | 'module.exports', 29 | 'handler', 30 | 'async.series', 31 | 'async.parallel', 32 | 'Promise.all.then', 33 | 'Promise.all', 34 | 'Object' 35 | ] 36 | if (userland.length) { 37 | const uniqueNameOptions = [...new Set(userland.map(frame => name(frame)))] 38 | let nameCandidate = uniqueNameOptions.reduce((prev, curr) => !tooGeneric.includes(curr) ? curr : prev, null) 39 | 40 | if (!nameCandidate) { 41 | const sysPath = sysInfo.pathSeparator.includes('\\') ? path.win32 : path 42 | const filePath = userland[0].getFileNameWithoutModuleDirectory(sysInfo) 43 | nameCandidate = sysPath.basename(filePath) 44 | // we don't want a default name - let's take the containing dirname 45 | if (nameCandidate === 'index.js') { 46 | nameCandidate = sysPath.dirname(filePath).split(sysInfo.pathSeparator).pop() 47 | } 48 | } 49 | return nameCandidate 50 | } 51 | 52 | const modules = interesting.map(toModule(sysInfo)).filter(mod => mod) 53 | if (modules.length) return modules[0] 54 | 55 | return null 56 | } 57 | 58 | function toArray (frames) { 59 | // map converts the frame list into an actual array 60 | return frames.map(x => x) 61 | } 62 | 63 | function toModule (sysInfo) { 64 | return function (frame) { 65 | const mod = frame.getModuleName(sysInfo) 66 | if (mod) return name(frame) + '@' + mod.name 67 | } 68 | } 69 | 70 | function name (frame) { 71 | return frame.functionName || frame.typeName 72 | } 73 | 74 | function isUserland (sysInfo) { 75 | return frame => !frame.isExternal(sysInfo) && !frame.isNodecore(sysInfo) 76 | } 77 | -------------------------------------------------------------------------------- /analysis/barrier/make-external-barrier-nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | 4 | const EXTERNAL = Symbol('external') 5 | const USER = Symbol('user') 6 | const BOTH = Symbol('both') 7 | 8 | class MakeSynchronousBarrierNodes extends stream.Transform { 9 | constructor (systemInfo) { 10 | super({ 11 | readableObjectMode: true, 12 | writableObjectMode: true 13 | }) 14 | 15 | this._systemInfo = systemInfo 16 | this._placementStorage = new Map() 17 | } 18 | 19 | _transform (barrierNode, encoding, callback) { 20 | // Root is always in user scope 21 | if (barrierNode.isRoot) { 22 | this._placementStorage.set(barrierNode.barrierId, USER) 23 | return callback(null, barrierNode) 24 | } 25 | 26 | // If the node is already a barrier, there is no need to make it a barrier 27 | // again. 28 | let foundInternalNodes = false 29 | let foundExternalNodes = false 30 | for (const aggregateNode of barrierNode.nodes) { 31 | // If there are no frames, .every will still return true 32 | const isExternal = aggregateNode.frames 33 | .every((frame) => frame.isExternal(this._systemInfo)) 34 | 35 | if (isExternal) foundExternalNodes = true 36 | else foundInternalNodes = true 37 | } 38 | 39 | if (foundInternalNodes && foundExternalNodes) { 40 | this._placementStorage.set(barrierNode.barrierId, BOTH) 41 | } else if (foundExternalNodes) { 42 | this._placementStorage.set(barrierNode.barrierId, EXTERNAL) 43 | } else { 44 | this._placementStorage.set(barrierNode.barrierId, USER) 45 | } 46 | 47 | // The node is already a barrier, no point in making it a barrier again 48 | // This also means we don't have to handle the case where this BarrierNode 49 | // is placed in both scopes. 50 | if (!barrierNode.isWrapper) { 51 | return callback(null, barrierNode) 52 | } 53 | 54 | // Make it a barrier by comparing placement of this node and its parrent 55 | const parentPlacement = this._placementStorage.get(barrierNode.parentBarrierId) 56 | const placement = this._placementStorage.get(barrierNode.barrierId) 57 | 58 | // If the parent is placed both scopes, don't make this a wrapper 59 | // NOTE: we currently don't have a case where this can be true, so 60 | // it is hard to think about what makes sense. 61 | if (parentPlacement === BOTH) { 62 | return callback(null, barrierNode) 63 | } 64 | 65 | // If it changed from external to internal, or from internal to external (XOR) 66 | // Then make this BarrierNode a real barrier. 67 | if ((placement === EXTERNAL) ^ (parentPlacement === EXTERNAL)) { 68 | barrierNode.makeBarrier() 69 | } 70 | 71 | return callback(null, barrierNode) 72 | } 73 | } 74 | 75 | module.exports = MakeSynchronousBarrierNodes 76 | -------------------------------------------------------------------------------- /analysis/barrier/wrap-as-barrier-nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | const BarrierNode = require('./barrier-node.js') 4 | 5 | class WrapAsBarrierNodes extends stream.Transform { 6 | constructor () { 7 | super({ 8 | readableObjectMode: true, 9 | writableObjectMode: true 10 | }) 11 | } 12 | 13 | _transform (aggregateNode, encoding, callback) { 14 | const barrier = new BarrierNode( 15 | aggregateNode.aggregateId, aggregateNode.parentAggregateId 16 | ) 17 | barrier.initializeAsWrapper(aggregateNode, aggregateNode.children) 18 | callback(null, barrier) 19 | } 20 | } 21 | 22 | module.exports = WrapAsBarrierNodes 23 | -------------------------------------------------------------------------------- /analysis/cluster/anonymise-cluster-frames.js: -------------------------------------------------------------------------------- 1 | const stream = require('stream') 2 | 3 | class Anonymise extends stream.Transform { 4 | constructor (sysInfo) { 5 | super({ 6 | readableObjectMode: true, 7 | writableObjectMode: true 8 | }) 9 | 10 | this.systemInfo = sysInfo 11 | } 12 | 13 | _transform (data, enc, cb) { 14 | const sysInfo = this.systemInfo 15 | 16 | for (const node of data.nodes) { 17 | node.frames.forEach(anonymiseFrame) 18 | } 19 | 20 | cb(null, data) 21 | 22 | function anonymiseFrame (frame) { 23 | frame.anonymise(sysInfo) 24 | } 25 | } 26 | } 27 | 28 | module.exports = Anonymise 29 | -------------------------------------------------------------------------------- /analysis/cluster/cluster-node.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const util = require('util') 4 | 5 | class ClusterNode { 6 | constructor (clusterId, parentClusterId) { 7 | this.clusterId = clusterId 8 | this.parentClusterId = parentClusterId 9 | 10 | this.isRoot = false 11 | this.nodes = [] 12 | this.children = [] 13 | this.name = null 14 | 15 | this._lastSort = 0 16 | } 17 | 18 | sort () { 19 | if (this.nodes.length === this._lastSort) return 20 | this._lastSort = this.nodes.length 21 | this.nodes.sort(cmp) 22 | } 23 | 24 | [util.inspect.custom] (depth, options) { 25 | this.sort() 26 | 27 | const nestedOptions = Object.assign({}, options, { 28 | depth: depth === null ? null : depth - 1 29 | }) 30 | if (depth === null) depth = Infinity 31 | 32 | if (depth < 0) { 33 | return `<${options.stylize('ClusterNode', 'special')}>` 34 | } 35 | 36 | const padding = ' '.repeat(8) 37 | const nodesFormatted = this.nodes 38 | .map(function (aggregateNode) { 39 | return util.inspect(aggregateNode, nestedOptions) 40 | .split('\n') 41 | .join('\n' + padding) 42 | }) 43 | 44 | let inner 45 | if (depth < 1) { 46 | inner = nodesFormatted.join(', ') 47 | } else { 48 | inner = `\n${padding}` + nodesFormatted.join(`,\n${padding}`) 49 | } 50 | 51 | const childrenFormatted = this.children 52 | .map((child) => options.stylize(child, 'number')) 53 | .join(', ') 54 | 55 | return `<${options.stylize('ClusterNode', 'special')}` + 56 | ` clusterId:${options.stylize(this.clusterId, 'number')},` + 57 | ` parentClusterId:${options.stylize(this.parentClusterId, 'number')},` + 58 | ` name:${options.stylize(this.name, 'string')},` + 59 | ` children:[${childrenFormatted}],` + 60 | ` nodes:[${inner}]>` 61 | } 62 | 63 | toJSON () { 64 | this.sort() 65 | 66 | return { 67 | clusterId: this.clusterId, 68 | parentClusterId: this.parentClusterId, 69 | name: this.name, 70 | children: this.children, 71 | nodes: this.nodes.map((aggregateNode) => aggregateNode.toJSON()) 72 | } 73 | } 74 | 75 | makeRoot () { 76 | this.isRoot = true 77 | } 78 | 79 | addChild (clusterId) { 80 | this.children.push(clusterId) 81 | } 82 | 83 | insertBarrierNode (barrierNode) { 84 | if ((barrierNode.isRoot || !barrierNode.isWrapper) && !this.name) this.name = barrierNode.name 85 | this.nodes.push(...barrierNode.nodes) 86 | } 87 | } 88 | 89 | module.exports = ClusterNode 90 | 91 | function cmp (a, b) { 92 | return a.aggregateId - b.aggregateId 93 | } 94 | -------------------------------------------------------------------------------- /analysis/cluster/combine-as-cluster-nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | const ClusterNode = require('./cluster-node.js') 4 | 5 | class CombineAsClusterNodes extends stream.Transform { 6 | constructor () { 7 | super({ 8 | readableObjectMode: true, 9 | writableObjectMode: true 10 | }) 11 | 12 | this._clusterId = 2 13 | this._clusterNodeStroage = new Map() 14 | this._clusterIdIndexByBarrierId = new Map() 15 | 16 | // Create root ClusterNode 17 | const clusterNodeRoot = new ClusterNode(1, 0) 18 | clusterNodeRoot.makeRoot() 19 | this._clusterNodeStroage.set(clusterNodeRoot.clusterId, clusterNodeRoot) 20 | } 21 | 22 | _createClusterNode (parentClusterId) { 23 | const clusterNode = new ClusterNode(this._clusterId++, parentClusterId) 24 | this._clusterNodeStroage.set(clusterNode.clusterId, clusterNode) 25 | return clusterNode 26 | } 27 | 28 | _insertBarrierNode (clusterNode, barrierNode) { 29 | clusterNode.insertBarrierNode(barrierNode) 30 | this._clusterIdIndexByBarrierId.set( 31 | barrierNode.barrierId, 32 | clusterNode.clusterId 33 | ) 34 | } 35 | 36 | _transform (barrierNode, encoding, callback) { 37 | if (barrierNode.isRoot) { 38 | const clusterNode = this._clusterNodeStroage.get(1) 39 | this._insertBarrierNode(clusterNode, barrierNode) 40 | return callback(null) 41 | } 42 | 43 | // If not a wrapper, this barrierNode marks the begining of a new 44 | // cluster. 45 | if (!barrierNode.isWrapper) { 46 | const parentClusterId = this._clusterIdIndexByBarrierId.get( 47 | barrierNode.parentBarrierId 48 | ) 49 | const clusterNode = this._createClusterNode(parentClusterId) 50 | this._insertBarrierNode(clusterNode, barrierNode) 51 | 52 | const parentClusterNode = this._clusterNodeStroage.get(parentClusterId) 53 | parentClusterNode.addChild(clusterNode.clusterId) 54 | 55 | return callback(null) 56 | } 57 | 58 | // The cluster that this BarrierNode belongs too, will be the same 59 | // as its parent. This is because the BarrierNode is just a wrapper 60 | // for a AggregateNode. 61 | const clusterId = this._clusterIdIndexByBarrierId.get( 62 | barrierNode.parentBarrierId 63 | ) 64 | const clusterNode = this._clusterNodeStroage.get(clusterId) 65 | this._insertBarrierNode(clusterNode, barrierNode) 66 | return callback(null) 67 | } 68 | 69 | _flush (callback) { 70 | // basic non-recursive BFS (breadth-first-search) 71 | const queue = [1] // root has barrierId = 1 72 | while (queue.length > 0) { 73 | const clusterNode = this._clusterNodeStroage.get(queue.shift()) 74 | clusterNode.sort() 75 | this.push(clusterNode) 76 | 77 | // Add children of the newly updated node to the queue 78 | queue.push(...clusterNode.children) 79 | } 80 | 81 | callback(null) 82 | } 83 | } 84 | 85 | module.exports = CombineAsClusterNodes 86 | -------------------------------------------------------------------------------- /analysis/raw-event/join-as-raw-event.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | const RawEvent = require('./raw-event.js') 4 | const v8 = require('v8') 5 | 6 | const HEAP_MAX = v8.getHeapStatistics().heap_size_limit 7 | 8 | class JoinAsRawEvent extends stream.Readable { 9 | constructor (stackTrace, traceEvent) { 10 | super({ objectMode: true }) 11 | 12 | this._awaitRead = false 13 | 14 | this._stackTrace = stackTrace 15 | this._stackTraceAsyncId = 0 16 | this._stackTraceEnded = false 17 | this._reads = 0 18 | this._destroyed = false 19 | 20 | this._traceEvent = traceEvent 21 | this._traceEventAsyncId = 0 22 | this._traceEventEnded = false 23 | this._stackTrace.once('end', () => this._stackTraceEnd()) 24 | this._traceEvent.once('end', () => this._traceEventEnd()) 25 | } 26 | 27 | _stackTracePush (data) { 28 | this._awaitRead = false 29 | this._stackTraceAsyncId = Math.max(this._stackTraceAsyncId, data.asyncId) 30 | 31 | this.push(RawEvent.wrapStackTrace(data)) 32 | } 33 | 34 | _stackTraceEnd () { 35 | this._stackTraceEnded = true 36 | this._maybeEnded() 37 | } 38 | 39 | _traceEventPush (data) { 40 | this._awaitRead = false 41 | this._traceEventAsyncId = Math.max(this._traceEventAsyncId, data.asyncId) 42 | 43 | this.push(RawEvent.wrapTraceEvent(data)) 44 | } 45 | 46 | _traceEventEnd () { 47 | this._traceEventEnded = true 48 | this._maybeEnded() 49 | } 50 | 51 | _maybeEnded () { 52 | if (this._destroyed) return 53 | if (this._stackTraceEnded && this._traceEventEnded) { 54 | this.push(null) 55 | } else if (this._awaitRead) { 56 | // If more data is expected and only one stream ended, then make a manual 57 | // _read call, such that .push(data) is called. 58 | this._read(1) 59 | } 60 | } 61 | 62 | _read (size) { 63 | if (this._destroyed) return 64 | 65 | this._awaitRead = true 66 | this._reads++ 67 | 68 | /* istanbul ignore next */ 69 | if ((this._reads & 4095) === 0 && !hasFreeMemory()) { 70 | this._destroyed = true 71 | this.emit('truncate') 72 | this.push(null) 73 | this._stackTrace.destroy() 74 | this._traceEvent.destroy() 75 | return 76 | } 77 | 78 | // the asyncId's are approximately incrementing. Decide what 79 | // stream to read from by selecting the one where the asyncId is lowest 80 | if (this._traceEventEnded || ( 81 | this._stackTraceAsyncId < this._traceEventAsyncId && !this._stackTraceEnded 82 | )) { 83 | const data = this._stackTrace.read() 84 | if (data === null) { 85 | this._stackTrace.once('readable', () => { 86 | const data = this._stackTrace.read() 87 | if (data !== null) return this._stackTracePush(data) 88 | // end event handler will call .push() 89 | }) 90 | } else { 91 | return this._stackTracePush(data) 92 | } 93 | } else { 94 | const data = this._traceEvent.read() 95 | if (data === null) { 96 | this._traceEvent.once('readable', () => { 97 | const data = this._traceEvent.read() 98 | if (data !== null) return this._traceEventPush(data) 99 | // end event handler will call .push() 100 | }) 101 | } else { 102 | return this._traceEventPush(data) 103 | } 104 | } 105 | } 106 | } 107 | 108 | module.exports = JoinAsRawEvent 109 | 110 | function hasFreeMemory () { 111 | const used = process.memoryUsage().heapTotal / HEAP_MAX 112 | return used < 0.5 113 | } 114 | -------------------------------------------------------------------------------- /analysis/raw-event/raw-event.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const StackTrace = require('../stack-trace/stack-trace.js') 4 | const TraceEvent = require('../trace-event/trace-event.js') 5 | 6 | class RawEvent { 7 | constructor (type, asyncId, info) { 8 | this.type = type 9 | this.asyncId = asyncId 10 | this.info = info 11 | } 12 | 13 | toJSON () { 14 | return { 15 | type: this.type, 16 | asyncId: this.asyncId, 17 | info: this.info.toJSON() 18 | } 19 | } 20 | 21 | static wrapStackTrace (stackTrace) { 22 | if (!(stackTrace instanceof StackTrace)) { 23 | throw new TypeError('wrapStackTrace input must be a StackTrace instance') 24 | } 25 | 26 | return new RawEvent('stackTrace', stackTrace.asyncId, stackTrace) 27 | } 28 | 29 | static wrapTraceEvent (traceEvent) { 30 | if (!(traceEvent instanceof TraceEvent)) { 31 | throw new TypeError('wrapTraceEvent input must be a TraceEvent instance') 32 | } 33 | 34 | return new RawEvent('traceEvent', traceEvent.asyncId, traceEvent) 35 | } 36 | } 37 | 38 | module.exports = RawEvent 39 | -------------------------------------------------------------------------------- /analysis/source/combine-as-source-nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | const SourceNode = require('./source-node.js') 4 | 5 | class CombineAsSourceNodes extends stream.Transform { 6 | constructor () { 7 | super({ 8 | readableObjectMode: true, 9 | writableObjectMode: true 10 | }) 11 | 12 | this._storage = new Map() 13 | } 14 | 15 | _transform (rawEvent, encoding, callback) { 16 | // create new sourceNode if necessary 17 | if (!this._storage.has(rawEvent.asyncId)) { 18 | this._storage.set(rawEvent.asyncId, new SourceNode(rawEvent.asyncId)) 19 | } 20 | 21 | // add rawEvent to sourceNode 22 | const sourceNode = this._storage.get(rawEvent.asyncId) 23 | sourceNode.addRawEvent(rawEvent) 24 | 25 | // push sourceNode if complete and cleanup 26 | if (sourceNode.isComplete()) { 27 | this._storage.delete(rawEvent.asyncId) 28 | this.push(sourceNode) 29 | } 30 | 31 | callback(null) 32 | } 33 | 34 | _flush (callback) { 35 | // push incomplete nodes and cleanup 36 | for (const [asyncId, sourceNode] of this._storage) { 37 | this._storage.delete(asyncId) 38 | this.push(sourceNode) 39 | } 40 | 41 | callback(null) 42 | } 43 | } 44 | 45 | module.exports = CombineAsSourceNodes 46 | -------------------------------------------------------------------------------- /analysis/source/filter-source-nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | 4 | class FilterSourceNodes extends stream.Transform { 5 | constructor () { 6 | super({ 7 | readableObjectMode: true, 8 | writableObjectMode: true 9 | }) 10 | } 11 | 12 | _transform (sourceNode, encoding, callback) { 13 | // * If a SourceNode doesn't have a stack trace it is because it has been 14 | // filtered in the `logger.js`. 15 | // * TIMERWRAP are not really valuable to as they purely describe an 16 | // implementation detail in nodecore and never have any children as 17 | // triggerAsyncId. 18 | if (sourceNode.hasStackTrace() && sourceNode.type !== 'TIMERWRAP') { 19 | this.push(sourceNode) 20 | } 21 | callback(null) 22 | } 23 | } 24 | 25 | module.exports = FilterSourceNodes 26 | -------------------------------------------------------------------------------- /analysis/source/http-request-nodes.js: -------------------------------------------------------------------------------- 1 | const { Transform } = require('stream') 2 | 3 | function isHTTPEnd (sourceNode) { 4 | if (sourceNode.type !== 'TickObject') return false 5 | for (let i = 0; i < sourceNode.frames.length; i++) { 6 | const frame = sourceNode.frames.get(i) 7 | if (frame.typeName !== 'ServerResponse') continue 8 | if (frame.functionName !== 'end') continue 9 | return true 10 | } 11 | return false 12 | } 13 | 14 | function upsert (nodes, id) { 15 | const list = nodes.get(id) || [] 16 | if (!list.length) nodes.set(id, list) 17 | return list 18 | } 19 | 20 | class HTTPRequestNodes extends Transform { 21 | constructor (analysis) { 22 | super({ readableObjectMode: true, writableObjectMode: true }) 23 | this._nodes = new Map() 24 | this._minTime = 0 25 | this._maxTime = 0 26 | this._analysis = analysis 27 | } 28 | 29 | _transform (data, enc, cb) { 30 | if (!this._minTime || data.init < this._minTime) { 31 | this._minTime = data.init 32 | } 33 | if (!this._maxTime || data.init > this._maxTime) { 34 | this._maxTime = data.init 35 | } 36 | 37 | if (isHTTPEnd(data)) { 38 | const id = data.identifier 39 | upsert(this._nodes, id).push(data.asyncId) 40 | } 41 | 42 | cb(null, data) 43 | } 44 | 45 | _maxRequests () { 46 | let result = [] 47 | for (const nodes of this._nodes.values()) { 48 | if (nodes.length > result.length) result = nodes 49 | } 50 | return result 51 | } 52 | 53 | _flush (cb) { 54 | this._analysis.runtime = this._maxTime - this._minTime 55 | this._analysis.httpRequests = this._maxRequests() 56 | cb(null) 57 | } 58 | } 59 | 60 | module.exports = HTTPRequestNodes 61 | -------------------------------------------------------------------------------- /analysis/source/identify-source-nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | 4 | class IdentifySourceNodes extends stream.Transform { 5 | constructor () { 6 | super({ 7 | readableObjectMode: true, 8 | writableObjectMode: true 9 | }) 10 | } 11 | 12 | _transform (sourceNode, encoding, callback) { 13 | if (sourceNode.type === 'HTTPPARSER') { 14 | // HTTPPARSER can either a new allocated HTTPPARSER or a previusely 15 | // cached one. We don't want to sepreate these two cases, as this is 16 | // a nodecore implementation detail. Thus, ignore the stackTrace in 17 | // the identifier. 18 | sourceNode.setIdentifier('HTTPPARSER') 19 | } else if (sourceNode.frames.length === 0) { 20 | // TCPWRAP and perhaps some other cases, don't have a stack trace. 21 | // Identify those with just their provider type 22 | sourceNode.setIdentifier(sourceNode.type) 23 | } else { 24 | sourceNode.setIdentifier( 25 | `${sourceNode.type}\f${sourceNode.frames.formatPositionOnly()}` 26 | ) 27 | } 28 | 29 | callback(null, sourceNode) 30 | } 31 | } 32 | 33 | module.exports = IdentifySourceNodes 34 | -------------------------------------------------------------------------------- /analysis/source/restructure-net-source-nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | 4 | class RestructureNetSourceNodes extends stream.Transform { 5 | constructor () { 6 | super({ 7 | readableObjectMode: true, 8 | writableObjectMode: true 9 | }) 10 | 11 | // The root and the void is pre-observed 12 | this._observedAsyncIds = new Set([0, 1]) 13 | 14 | // Track what is servers and sockets 15 | this._serverAsyncIds = new Set() 16 | this._connectionAsyncIds = new Set() 17 | 18 | // Save source nodes with unobserved triggerAsyncId for later 19 | this._storageByTriggerAsyncId = new Map() 20 | } 21 | 22 | _processNode (sourceNode) { 23 | this._observedAsyncIds.add(sourceNode.asyncId) 24 | 25 | if (sourceNode.type === 'TCPSERVERWRAP' || 26 | sourceNode.type === 'PIPESERVERWRAP') { 27 | this._serverAsyncIds.add(sourceNode.asyncId) 28 | } else if (this._serverAsyncIds.has(sourceNode.triggerAsyncId) && 29 | (sourceNode.type === 'TCPWRAP' || 30 | sourceNode.type === 'PIPEWRAP')) { 31 | this._connectionAsyncIds.add(sourceNode.asyncId) 32 | } else if (this._connectionAsyncIds.has(sourceNode.triggerAsyncId) && 33 | !this._serverAsyncIds.has(sourceNode.executionAsyncId)) { 34 | // Set the children of a socket to the the caller. This can be 35 | // SHUTDOWNWRAP, WRITEWRAP or some internal nextTick. 36 | // Nodes that actually comes from the TCPWRAP (in ondata) will have 37 | // `sourceNode.executionAsyncId === sourceNode.triggerAsyncId` anyway. 38 | // In the initialzation of the connections, the executionAsyncId is 39 | // the server onconnection event. We don't want to bind those resources 40 | // to the server, rather they should just stay on the connection. 41 | sourceNode.setParentAsyncId(sourceNode.executionAsyncId) 42 | } 43 | } 44 | 45 | _processTree (subroot) { 46 | // process as much of the subtree as possible 47 | const queue = [subroot] 48 | 49 | while (queue.length > 0) { 50 | // get node from queue and processs it 51 | const sourceNode = queue.shift() 52 | this._processNode(sourceNode) 53 | 54 | // Once the node is processed push to stream 55 | this.push(sourceNode) 56 | 57 | // Add children of the newly updated node to the queue and delete them 58 | // from storage. 59 | if (this._storageByTriggerAsyncId.has(sourceNode.asyncId)) { 60 | const children = this._storageByTriggerAsyncId.get(sourceNode.asyncId) 61 | this._storageByTriggerAsyncId.delete(sourceNode.asyncId) 62 | queue.push(...children) 63 | } 64 | } 65 | } 66 | 67 | _saveNode (sourceNode) { 68 | if (!this._storageByTriggerAsyncId.has(sourceNode.triggerAsyncId)) { 69 | this._storageByTriggerAsyncId.set(sourceNode.triggerAsyncId, []) 70 | } 71 | this._storageByTriggerAsyncId.get(sourceNode.triggerAsyncId).push(sourceNode) 72 | } 73 | 74 | _transform (sourceNode, encoding, callback) { 75 | // If the triggerAsyncId is observed, then we have all the required 76 | // information for this SourceNode. 77 | if (this._observedAsyncIds.has(sourceNode.triggerAsyncId)) { 78 | this._processTree(sourceNode) 79 | } else { 80 | this._saveNode(sourceNode) 81 | } 82 | 83 | callback(null) 84 | } 85 | 86 | _flush (callback) { 87 | for (const sourceNodes of this._storageByTriggerAsyncId.values()) { 88 | for (const sourceNode of sourceNodes) { 89 | this.push(sourceNode) 90 | } 91 | } 92 | 93 | callback(null) 94 | } 95 | } 96 | 97 | module.exports = RestructureNetSourceNodes 98 | -------------------------------------------------------------------------------- /analysis/source/source-node.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const util = require('util') 4 | const crypto = require('crypto') 5 | const Frames = require('../stack-trace/frames.js') 6 | const StackTrace = require('../stack-trace/stack-trace.js') 7 | const TraceEvent = require('../trace-event/trace-event.js') 8 | const RawEvent = require('../raw-event/raw-event.js') 9 | 10 | class SourceNode { 11 | constructor (asyncId) { 12 | this.asyncId = asyncId 13 | this.identifier = null 14 | 15 | // parent 16 | this.parentAsyncId = null 17 | this.triggerAsyncId = null 18 | this.executionAsyncId = null 19 | 20 | // async type 21 | this.type = null 22 | 23 | // stack trace 24 | this.frames = null 25 | 26 | // event timestamps 27 | this.init = null 28 | this.before = [] 29 | this.after = [] 30 | this.destroy = null 31 | } 32 | 33 | [util.inspect.custom] (depth, options) { 34 | return `<${options.stylize('SourceNode', 'special')}` + 35 | ` type:${options.stylize(this.type, 'string')},` + 36 | ` asyncId:${options.stylize(this.asyncId, 'number')},` + 37 | ` parentAsyncId:${options.stylize(this.parentAsyncId, 'number')},` + 38 | ` triggerAsyncId:${options.stylize(this.triggerAsyncId, 'number')},` + 39 | ` executionAsyncId:${options.stylize(this.executionAsyncId, 'number')},` + 40 | ` identifier:${options.stylize(this.identifier && this.hash().slice(0, 16), 'special')}>` 41 | } 42 | 43 | hash () { 44 | return this.identifier && crypto.createHash('sha256').update(this.identifier).digest('hex') 45 | } 46 | 47 | toJSON ({ short } = { short: false }) { 48 | const json = { 49 | asyncId: this.asyncId, 50 | parentAsyncId: this.parentAsyncId, 51 | triggerAsyncId: this.triggerAsyncId, 52 | executionAsyncId: this.executionAsyncId, 53 | 54 | init: this.init, 55 | before: this.before, 56 | after: this.after, 57 | destroy: this.destroy 58 | } 59 | 60 | if (!short) { 61 | json.identifier = this.identifier 62 | json.type = this.type 63 | json.frames = this.frames === null ? null : this.frames.toJSON() 64 | } 65 | 66 | return json 67 | } 68 | 69 | makeRoot () { 70 | this.frames = new Frames([]) 71 | } 72 | 73 | setIdentifier (identifier) { 74 | this.identifier = identifier 75 | } 76 | 77 | setParentAsyncId (asyncId) { 78 | this.parentAsyncId = asyncId 79 | } 80 | 81 | addRawEvent (rawEvent) { 82 | if (!(rawEvent instanceof RawEvent)) { 83 | throw new TypeError('addRawEvent input must be a RawEvent instance') 84 | } 85 | 86 | switch (rawEvent.type) { 87 | case 'stackTrace': 88 | this.addStackTrace(rawEvent.info) 89 | break 90 | case 'traceEvent': 91 | this.addTraceEvent(rawEvent.info) 92 | break 93 | default: 94 | throw new Error('unknown RawEvent type value: ' + rawEvent.type) 95 | } 96 | } 97 | 98 | addStackTrace (stackTrace) { 99 | if (!(stackTrace instanceof StackTrace)) { 100 | throw new TypeError('addStackTrace input must be a StackTrace instance') 101 | } 102 | 103 | this.frames = stackTrace.frames 104 | } 105 | 106 | addTraceEvent (traceEvent) { 107 | if (!(traceEvent instanceof TraceEvent)) { 108 | throw new TypeError('addTraceEvent input must be a TraceEvent instance') 109 | } 110 | 111 | switch (traceEvent.event) { 112 | case 'init': 113 | this.type = traceEvent.type 114 | this.init = traceEvent.timestamp 115 | this.triggerAsyncId = traceEvent.triggerAsyncId 116 | this.executionAsyncId = traceEvent.executionAsyncId 117 | this.setParentAsyncId(traceEvent.triggerAsyncId) 118 | break 119 | case 'destroy': 120 | this.destroy = traceEvent.timestamp 121 | break 122 | case 'before': 123 | this.before.push(traceEvent.timestamp) 124 | break 125 | case 'after': 126 | this.after.push(traceEvent.timestamp) 127 | break 128 | default: 129 | throw new Error('unknown TraceEvent type value: ' + traceEvent.event) 130 | } 131 | } 132 | 133 | isComplete () { 134 | return (this.hasStackTrace() && 135 | this.init !== null && 136 | this.destroy !== null && 137 | this.before.length === this.after.length) 138 | } 139 | 140 | hasStackTrace () { 141 | return this.frames !== null 142 | } 143 | } 144 | 145 | module.exports = SourceNode 146 | -------------------------------------------------------------------------------- /analysis/stack-trace/stack-trace.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Frames = require('./frames.js') 4 | 5 | class StackTrace { 6 | constructor (data) { 7 | this.asyncId = data.asyncId 8 | this.frames = new Frames(data.frames) 9 | } 10 | 11 | toJSON () { 12 | return { 13 | asyncId: this.asyncId, 14 | frames: this.frames.toJSON() 15 | } 16 | } 17 | } 18 | 19 | module.exports = StackTrace 20 | -------------------------------------------------------------------------------- /analysis/stack-trace/wrap-as-stack-trace.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | const StackTrace = require('./stack-trace.js') 4 | 5 | class WrapAsStackTrace extends stream.Transform { 6 | constructor () { 7 | super({ 8 | readableObjectMode: true, 9 | writableObjectMode: true 10 | }) 11 | } 12 | 13 | _transform (data, encoding, callback) { 14 | callback(null, new StackTrace(data)) 15 | } 16 | } 17 | 18 | module.exports = WrapAsStackTrace 19 | -------------------------------------------------------------------------------- /analysis/system-info.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class SystemInfo { 4 | constructor (data) { 5 | this.providers = new Set(data.providers) 6 | this.mainDirectory = data.mainDirectory 7 | 8 | // Backwards compatibility with data collected in Bubbleprof <1.6.1, which contained a typo 9 | this.pathSeparator = data.pathSeparator || data.pathSeperator 10 | 11 | // Compute self-module directory for the special case that the main script 12 | // itself is in a node_modules directory. 13 | const mainDirectoryPath = this.mainDirectory 14 | .split(this.pathSeparator) 15 | if (mainDirectoryPath.includes('node_modules')) { 16 | let mainDirectoryIndex = mainDirectoryPath.lastIndexOf('node_modules') 17 | // module is in a @namespace 18 | if (mainDirectoryPath[mainDirectoryIndex + 1][0] === '@') { 19 | mainDirectoryIndex += 1 20 | } 21 | // add the module itself 22 | mainDirectoryIndex += 1 23 | 24 | // Join up the path, it will look like: 25 | // "/home/user/node_modules/@private/server" 26 | this.moduleDirectory = mainDirectoryPath.slice(0, mainDirectoryIndex + 1) 27 | .join(this.pathSeparator) 28 | } else { 29 | this.moduleDirectory = '' 30 | } 31 | } 32 | } 33 | 34 | module.exports = SystemInfo 35 | -------------------------------------------------------------------------------- /analysis/trace-event/trace-event.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class TraceEvent { 4 | constructor (data) { 5 | this.asyncId = data.asyncId 6 | this.event = data.event 7 | this.type = data.type 8 | this.timestamp = data.timestamp 9 | this.triggerAsyncId = data.triggerAsyncId 10 | this.executionAsyncId = data.executionAsyncId 11 | } 12 | 13 | toJSON () { 14 | return { 15 | asyncId: this.asyncId, 16 | event: this.event, 17 | type: this.type, 18 | timestamp: this.timestamp, 19 | triggerAsyncId: this.triggerAsyncId, 20 | executionAsyncId: this.executionAsyncId 21 | } 22 | } 23 | } 24 | 25 | module.exports = TraceEvent 26 | -------------------------------------------------------------------------------- /analysis/trace-event/wrap-as-trace-event.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | const TraceEvent = require('./trace-event.js') 4 | 5 | class WrapAsTraceEvent extends stream.Transform { 6 | constructor () { 7 | super({ 8 | readableObjectMode: true, 9 | writableObjectMode: true 10 | }) 11 | } 12 | 13 | _transform (data, encoding, callback) { 14 | callback(null, new TraceEvent(data)) 15 | } 16 | } 17 | 18 | module.exports = WrapAsTraceEvent 19 | -------------------------------------------------------------------------------- /collect/stack-trace.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function noFormat (errorObject, structuredStackTrace) { 4 | return structuredStackTrace 5 | } 6 | 7 | class Frame { 8 | constructor (frame) { 9 | this.functionName = frame.getFunctionName() || '' 10 | this.typeName = '' 11 | this.evalOrigin = '' 12 | this.fileName = '' 13 | this.lineNumber = 0 14 | this.columnNumber = 0 15 | 16 | this.isEval = false 17 | this.isConstructor = false 18 | this.isNative = false 19 | this.isToplevel = false 20 | 21 | // Only one of these can be true. Test in the order of most likely. 22 | if (frame.isToplevel()) { 23 | this.isToplevel = true 24 | } else if (frame.isConstructor()) { 25 | this.isConstructor = true 26 | } else if (frame.isNative()) { 27 | this.isNative = true 28 | } else { 29 | this.typeName = frame.getTypeName() 30 | } 31 | 32 | // Get source 33 | this.fileName = frame.getFileName() || '' 34 | this.lineNumber = ( 35 | frame.getLineNumber() || /* istanbul ignore next: no known case */ 0 36 | ) 37 | this.columnNumber = ( 38 | frame.getColumnNumber() || /* istanbul ignore next: no known case */ 0 39 | ) 40 | 41 | // If the fileName is empty, the error could be from an eval. Check 42 | // frame.isEval() to be sure. We check the `this.fileName` first to avoid 43 | // the overhead from `frame.isEval()` 44 | if (this.fileName === '' && frame.isEval()) { 45 | this.isEval = true 46 | this.evalOrigin = frame.getEvalOrigin() 47 | } 48 | 49 | if (this.typeName === null) { 50 | /* istanbul ignore if | only on node 16+ we have wasm fileNames */ 51 | if (this.fileName.startsWith('wasm://')) { 52 | this.typeName = 'wasm' 53 | } else { 54 | this.typeName = '' 55 | } 56 | } 57 | } 58 | } 59 | 60 | function stackTrace (skip) { 61 | // overwrite stack trace limit and formatting 62 | const restoreFormat = Error.prepareStackTrace 63 | const restoreLimit = Error.stackTraceLimit 64 | Error.prepareStackTrace = noFormat 65 | Error.stackTraceLimit = Infinity 66 | 67 | // collect stack trace 68 | const obj = {} 69 | Error.captureStackTrace(obj, stackTrace) 70 | const structuredStackTrace = obj.stack 71 | 72 | // restore limit and formatting 73 | Error.prepareStackTrace = restoreFormat 74 | Error.stackTraceLimit = restoreLimit 75 | 76 | // extract data 77 | const frames = structuredStackTrace.map((frame) => new Frame(frame)) 78 | 79 | // Don't include async_hooks frames 80 | return frames.slice(skip).filter(function (frame) { 81 | return (frame.fileName !== 'async_hooks.js' && 82 | frame.fileName !== 'internal/async_hooks.js' && 83 | frame.fileName !== 'node:internal/async_hooks') 84 | }) 85 | } 86 | module.exports = stackTrace 87 | -------------------------------------------------------------------------------- /collect/system-info.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const Module = require('module') 5 | const wrapProviders = require('async_hooks').asyncWrapProviders 6 | let asyncWrap 7 | 8 | if (wrapProviders) { 9 | asyncWrap = { Providers: wrapProviders } 10 | } else { 11 | asyncWrap = process.binding('async_wrap') // eslint-disable-line node/no-deprecated-api 12 | } 13 | 14 | function getMainDirectory () { 15 | if (process._eval != null) return process.cwd() 16 | 17 | let mainScriptIndex = 1 18 | // `nyc` wraps the script and puts the main script in the second argument 19 | // This is only needed for our `npm run ci-test` 20 | if (process.env.NYC_CONFIG && process.argv[1].includes('.node-spawn-wrap')) { 21 | mainScriptIndex += 1 22 | } 23 | 24 | if (process.argv[mainScriptIndex] && process.argv[mainScriptIndex] !== '-') { 25 | return path.dirname( 26 | Module._resolveFilename(process.argv[mainScriptIndex], null, true) 27 | ) 28 | } 29 | 30 | return process.cwd() 31 | } 32 | 33 | function systemInfo () { 34 | return { 35 | providers: [ 36 | 'TickObject', 'Timeout', 'Immediate', 37 | ...Object.keys(asyncWrap.Providers) 38 | ], 39 | pathSeparator: require('path').sep, 40 | mainDirectory: getMainDirectory() 41 | } 42 | } 43 | module.exports = systemInfo 44 | -------------------------------------------------------------------------------- /debug/aggregate-nodes-to-dprof.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | 4 | class AggregateToDprof extends stream.Transform { 5 | constructor () { 6 | super({ 7 | readableObjectMode: false, 8 | writableObjectMode: true 9 | }) 10 | 11 | this._nodes = [] 12 | this._rootChildren = null 13 | this._start = Infinity 14 | this._total = 0 15 | } 16 | 17 | _offset (time) { 18 | return (time - this._start) * 1e6 19 | } 20 | 21 | _transform (node, encoding, callback) { 22 | if (node.aggregateId === 1) { 23 | this._rootChildren = node.children 24 | return callback(null) 25 | } 26 | 27 | // Calculate aggregated start end end time 28 | const init = Math.min(...node.sources.map((source) => source.init)) 29 | const destroy = Math.max(...node.sources.map((source) => source.destroy)) 30 | 31 | // Update global end and start time 32 | this._start = Math.min(this._start, init) 33 | this._total = Math.max(this._total, destroy) 34 | 35 | // Calculate aggregated before and after times 36 | const before = [].concat(...node.sources.map((source) => source.before)) 37 | .sort((a, b) => a - b) 38 | const after = [].concat(...node.sources.map((source) => source.after)) 39 | .sort((a, b) => a - b) 40 | 41 | this._nodes.push({ 42 | name: `${node.type} <${node.mark.format()}>`, 43 | uid: node.aggregateId, 44 | parent: node.parentAggregateId, 45 | stack: node.frames.map(function (frame) { 46 | return { 47 | description: frame.format(), 48 | filename: frame.fileName, 49 | column: frame.columnNumber, 50 | line: frame.lineNumber 51 | } 52 | }), 53 | init: init, 54 | before: before, 55 | after: after, 56 | destroy: destroy, 57 | unref: [], 58 | ref: [], 59 | initRef: true, 60 | children: node.children 61 | }) 62 | 63 | callback(null) 64 | } 65 | 66 | _flush (callback) { 67 | this.push(JSON.stringify({ 68 | version: '1.0.1', // the version of dprof there generated this JSON file 69 | total: this._offset(this._total), // execution time in nanoseconds 70 | root: { 71 | name: 'root', 72 | uid: 1, 73 | parent: null, 74 | stack: [], 75 | init: 0, 76 | before: [0], 77 | after: [this._offset(this._total)], 78 | destroy: this._offset(this._total), 79 | unref: [], 80 | ref: [], 81 | initRef: true, 82 | children: this._rootChildren 83 | }, 84 | nodes: this._nodes.map(function (node) { 85 | node.init = this._offset(node.init) 86 | node.destroy = this._offset(node.destroy ? node.destroy : this._total) 87 | node.before = node.before.map((time) => this._offset(time)) 88 | node.after = node.after.map((time) => this._offset(time)) 89 | return node 90 | }, this) 91 | }, null, 1)) 92 | 93 | callback(null) 94 | } 95 | } 96 | 97 | module.exports = AggregateToDprof 98 | -------------------------------------------------------------------------------- /debug/dprof-dump.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const analysis = require('../analysis/index.js') 4 | const getLoggingPaths = require('@clinic/clinic-common').getLoggingPaths('bubbleprof') 5 | const SystemInfoDecoder = require('../format/system-info-decoder.js') 6 | const StackTraceDecoder = require('../format/stack-trace-decoder.js') 7 | const TraceEventDecoder = require('../format/trace-event-decoder.js') 8 | const ExtractAggregateNodes = require('./extract-aggregate-nodes.js') 9 | const AggregateNodesToDprof = require('./aggregate-nodes-to-dprof.js') 10 | 11 | // Load data 12 | const paths = getLoggingPaths({ path: process.argv[2] }) 13 | const systemInfoReader = fs.createReadStream(paths['/systeminfo']) 14 | .pipe(new SystemInfoDecoder()) 15 | const stackTraceReader = fs.createReadStream(paths['/stacktrace']) 16 | .pipe(new StackTraceDecoder()) 17 | const traceEventReader = fs.createReadStream(paths['/traceevent']) 18 | .pipe(new TraceEventDecoder()) 19 | 20 | // Print dprof file 21 | analysis(systemInfoReader, stackTraceReader, traceEventReader) 22 | .pipe(new ExtractAggregateNodes()) 23 | .pipe(new AggregateNodesToDprof()) 24 | .pipe(process.stdout) 25 | -------------------------------------------------------------------------------- /debug/dprof-test-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const async = require('async') 5 | const CollectAndRead = require('../test/collect-and-read.js') 6 | const analysis = require('../analysis/index.js') 7 | const ExtractAggregateNodes = require('./extract-aggregate-nodes.js') 8 | const AggregateNodesToDprof = require('./aggregate-nodes-to-dprof.js') 9 | 10 | function runServer (name) { 11 | const serverPath = path.resolve(__dirname, '..', 'test', 'servers', name + '.js') 12 | const cmd = new CollectAndRead(serverPath) 13 | 14 | // make two requests 15 | async.map( 16 | [0, 1], 17 | function makeRequest (requestId, done) { 18 | cmd.request('/', done) 19 | }, 20 | function (err) { 21 | if (err) throw err 22 | } 23 | ) 24 | 25 | // await result 26 | cmd.on('ready', function (systemInfoReader, stackTraceReader, traceEventReader) { 27 | analysis(systemInfoReader, stackTraceReader, traceEventReader) 28 | .pipe(new ExtractAggregateNodes()) 29 | .pipe(new AggregateNodesToDprof()) 30 | .pipe(process.stdout) 31 | }) 32 | } 33 | 34 | runServer(process.argv[2]) 35 | -------------------------------------------------------------------------------- /debug/extract-aggregate-nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const stream = require('stream') 3 | 4 | class ExtractAggregateNodes extends stream.Transform { 5 | constructor () { 6 | super({ 7 | readableObjectMode: true, 8 | writableObjectMode: true 9 | }) 10 | } 11 | 12 | _transform (clusterNode, encoding, callback) { 13 | for (const aggregateNode of clusterNode.nodes) { 14 | this.push(aggregateNode) 15 | } 16 | 17 | callback(null) 18 | } 19 | } 20 | 21 | module.exports = ExtractAggregateNodes 22 | -------------------------------------------------------------------------------- /debug/inspect-dump.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const inspectpoint = require('inspectpoint') 4 | const analysis = require('../analysis/index.js') 5 | const getLoggingPaths = require('@clinic/clinic-common').getLoggingPaths('bubbleprof') 6 | const SystemInfoDecoder = require('../format/system-info-decoder.js') 7 | const StackTraceDecoder = require('../format/stack-trace-decoder.js') 8 | const TraceEventDecoder = require('../format/trace-event-decoder.js') 9 | 10 | // Load data 11 | const paths = getLoggingPaths({ path: process.argv[2] }) 12 | const systemInfoReader = fs.createReadStream(paths['/systeminfo']) 13 | .pipe(new SystemInfoDecoder()) 14 | const stackTraceReader = fs.createReadStream(paths['/stacktrace']) 15 | .pipe(new StackTraceDecoder()) 16 | const traceEventReader = fs.createReadStream(paths['/traceevent']) 17 | .pipe(new TraceEventDecoder()) 18 | 19 | // Print data 20 | analysis(systemInfoReader, stackTraceReader, traceEventReader) 21 | .pipe(inspectpoint({ depth: null, colors: true })) 22 | .pipe(process.stdout) 23 | -------------------------------------------------------------------------------- /debug/inspect-test-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const async = require('async') 5 | const CollectAndRead = require('../test/collect-and-read.js') 6 | const inspectpoint = require('inspectpoint') 7 | const analysis = require('../analysis/index.js') 8 | 9 | const quiet = process.argv.includes('--quiet') 10 | 11 | function runServer (name) { 12 | const serverPath = path.resolve(__dirname, '..', 'test', 'servers', name + '.js') 13 | const cmd = new CollectAndRead({}, serverPath) 14 | 15 | // make two requests 16 | async.map( 17 | [0, 1], 18 | function makeRequest (requestId, done) { 19 | cmd.request('/', done) 20 | }, 21 | function (err) { 22 | if (err) throw err 23 | } 24 | ) 25 | 26 | // await result 27 | cmd.on('ready', function (systemInfoReader, stackTraceReader, traceEventReader) { 28 | const stream = analysis(systemInfoReader, stackTraceReader, traceEventReader) 29 | 30 | if (quiet) { 31 | stream.resume() 32 | } else { 33 | stream 34 | .pipe(inspectpoint({ depth: null, colors: true })) 35 | .pipe(process.stdout) 36 | } 37 | }) 38 | } 39 | 40 | runServer(process.argv[2]) 41 | -------------------------------------------------------------------------------- /debug/visualize-all.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const v = require('./visualize-mod.js') 3 | v.visualize({ debug: true }) 4 | -------------------------------------------------------------------------------- /debug/visualize-mod.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Tool = require('../') 4 | 5 | module.exports = { 6 | visualize: () => { 7 | for (const file of process.argv.slice(2).map(trim)) { 8 | const tool = new Tool({ debug: true }) 9 | 10 | tool.visualize( 11 | file, 12 | file + '.html', 13 | function (err) { 14 | if (err) { 15 | throw err 16 | } else { 17 | console.log('Wrote', file + '.html') 18 | } 19 | } 20 | ) 21 | } 22 | } 23 | } 24 | 25 | function trim (file) { 26 | return file.replace(/\/\\$/, '') 27 | } 28 | -------------------------------------------------------------------------------- /debug/visualize-watch.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const chokidar = require('chokidar') 4 | const v = require('./visualize-mod.js') 5 | 6 | const debounce = require('lodash/debounce') 7 | 8 | // this is useful when updating multiple files in just one go (i.e. checking-out a branch) 9 | const debVisualize = debounce(v.visualize, 100) 10 | 11 | chokidar 12 | .watch([ 13 | 'visualizer/**/*.css', 14 | 'visualizer/**/*.js', 15 | 'index.js' 16 | ], { 17 | ignoreInitial: true 18 | }) 19 | .on('all', (event, path) => { 20 | console.log(event, path) 21 | debVisualize() 22 | }) 23 | 24 | v.visualize() 25 | -------------------------------------------------------------------------------- /format/abstract-decoder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const stream = require('stream') 4 | 5 | const FRAME_PREFIX_SIZE = 4 // uint32 is 4 bytes 6 | const MAX_CHUNK_SIZE = 10 * 65536 7 | 8 | class AbstractDecoder extends stream.Transform { 9 | constructor (messageType, options) { 10 | super(Object.assign({ 11 | readableObjectMode: true, 12 | writableObjectMode: false 13 | }, options)) 14 | 15 | this._messageType = messageType 16 | 17 | this._buffers = [] 18 | this._bufferedLength = 0 19 | this._nextMessageLength = FRAME_PREFIX_SIZE 20 | this._awaitFramePrefix = true 21 | this._warned = false 22 | } 23 | 24 | _transform (chunk, encoding, callback) { 25 | // Join buffers if the concated buffer contains an object 26 | if (this._bufferedLength > 0 && 27 | this._bufferedLength + chunk.length >= this._nextMessageLength) { 28 | chunk = Buffer.concat(this._buffers.concat([chunk])) 29 | this._buffers = [] 30 | this._bufferedLength = 0 31 | } 32 | 33 | // decode as long as there is an entire object 34 | // This is implemented as a very basic state machine: 35 | while (chunk.length >= this._nextMessageLength) { 36 | switch (this._awaitFramePrefix) { 37 | case true: 38 | this._nextMessageLength = chunk.readUInt32BE(0) 39 | chunk = chunk.slice(FRAME_PREFIX_SIZE) 40 | this._awaitFramePrefix = false 41 | break 42 | case false: 43 | try { 44 | const msg = this._messageType.decode(chunk.slice(0, this._nextMessageLength)) 45 | this.push( 46 | msg 47 | ) 48 | } catch (_) 49 | /* istanbul ignore next: if we knew how to trigger it we wouldn't need the `catch` at all! */ 50 | { // eslint-disable-line brace-style 51 | // Recover from the 16 bit truncation bug in the encoder 52 | if (chunk.length < MAX_CHUNK_SIZE) { 53 | this._nextMessageLength += 65536 54 | break 55 | } 56 | if (!this._warned) { 57 | console.error('There was a decoding error with chunk (base64):') 58 | console.error(chunk.toString('base64')) 59 | console.error('Please open an issue on https://github.com/clinicjs/node-clinic-bubbleprof with the above output.') 60 | this._warned = true 61 | } 62 | } 63 | chunk = chunk.slice(this._nextMessageLength) 64 | this._nextMessageLength = FRAME_PREFIX_SIZE 65 | this._awaitFramePrefix = true 66 | break 67 | } 68 | } 69 | 70 | // add remaining chunk if there is data left 71 | if (chunk.length > 0) { 72 | this._buffers.push(chunk) 73 | this._bufferedLength += chunk.length 74 | } 75 | 76 | callback(null) 77 | } 78 | } 79 | 80 | module.exports = AbstractDecoder 81 | -------------------------------------------------------------------------------- /format/abstract-encoder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const stream = require('stream') 4 | 5 | const FRAME_PREFIX_SIZE = 4 // uint32 is 4 bytes 6 | 7 | class AbstractEncoder extends stream.Transform { 8 | constructor (messageType, options) { 9 | super(Object.assign({ 10 | readableObjectMode: false, 11 | writableObjectMode: true 12 | }, options)) 13 | 14 | this._messageType = messageType 15 | } 16 | 17 | _transform (message, encoding, callback) { 18 | const messageLength = this._messageType.encodingLength(message) 19 | 20 | const framedMessage = Buffer.alloc(messageLength + FRAME_PREFIX_SIZE) 21 | framedMessage.writeUInt32BE(messageLength, 0) 22 | this._messageType.encode(message, framedMessage, FRAME_PREFIX_SIZE) 23 | 24 | callback(null, framedMessage) 25 | } 26 | } 27 | 28 | module.exports = AbstractEncoder 29 | -------------------------------------------------------------------------------- /format/stack-trace-decoder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const protobuf = require('protocol-buffers') 6 | const AbstractDecoder = require('./abstract-decoder.js') 7 | 8 | const messages = protobuf( 9 | fs.readFileSync(path.resolve(__dirname, 'stack-trace.proto')) 10 | ) 11 | 12 | class StackTraceDecoder extends AbstractDecoder { 13 | constructor (options) { 14 | super(messages.StackTrace, options) 15 | } 16 | } 17 | 18 | module.exports = StackTraceDecoder 19 | -------------------------------------------------------------------------------- /format/stack-trace-encoder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const protobuf = require('protocol-buffers') 6 | const AbstractEncoder = require('./abstract-encoder.js') 7 | 8 | const messages = protobuf( 9 | fs.readFileSync(path.resolve(__dirname, 'stack-trace.proto')) 10 | ) 11 | 12 | class StackTraceEncoder extends AbstractEncoder { 13 | constructor (options) { 14 | super(messages.StackTrace, options) 15 | } 16 | } 17 | 18 | module.exports = StackTraceEncoder 19 | -------------------------------------------------------------------------------- /format/stack-trace.proto: -------------------------------------------------------------------------------- 1 | 2 | message StackTrace { 3 | required double asyncId = 1; 4 | repeated Frame frames = 2; 5 | 6 | message Frame { 7 | required string functionName = 1; 8 | required string typeName = 2; 9 | 10 | required bool isEval = 3; 11 | required bool isConstructor = 4; 12 | required bool isNative = 5; 13 | required bool isToplevel = 6; 14 | 15 | required string evalOrigin = 7; 16 | required string fileName = 8; 17 | required uint32 lineNumber = 9; 18 | required uint32 columnNumber = 10; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /format/system-info-decoder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const stream = require('stream') 4 | 5 | class SystemInfoDecoder extends stream.Transform { 6 | constructor () { 7 | super({ 8 | readableObjectMode: true, 9 | writableObjectMode: false 10 | }) 11 | 12 | this._data = [] 13 | } 14 | 15 | _transform (chunk, encoding, callback) { 16 | this._data.push(chunk) 17 | callback(null) 18 | } 19 | 20 | _flush (callback) { 21 | this.push( 22 | JSON.parse(Buffer.concat(this._data).toString()) 23 | ) 24 | callback(null) 25 | } 26 | } 27 | module.exports = SystemInfoDecoder 28 | -------------------------------------------------------------------------------- /format/trace-event-decoder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const stream = require('stream') 4 | const parser = require('@clinic/trace-events-parser') 5 | 6 | function has (object, property) { return Object.prototype.hasOwnProperty.call(object, property) } 7 | 8 | class TraceEvent { 9 | constructor (data) { 10 | const isCallback = data.name.slice(-'_CALLBACK'.length) === '_CALLBACK' 11 | 12 | if (data.ph === 'b' && !isCallback) { 13 | this.event = 'init' 14 | } else if (data.ph === 'e' && !isCallback) { 15 | this.event = 'destroy' 16 | } else if (data.ph === 'b' && isCallback) { 17 | this.event = 'before' 18 | } else if (data.ph === 'e' && isCallback) { 19 | this.event = 'after' 20 | } 21 | 22 | this.type = isCallback ? data.name.slice(0, -'_CALLBACK'.length) : data.name 23 | this.asyncId = parseInt(data.id, 16) 24 | this.timestamp = data.ts / 1000 // convert to ms 25 | this.triggerAsyncId = null 26 | this.executionAsyncId = null 27 | // The trace event format changed in Node 11.0.0. To support both, old and 28 | // new versions of Node, this checks for both data formats. 29 | const args = data.args.data || data.args 30 | if (has(args, 'triggerAsyncId')) { 31 | this.triggerAsyncId = args.triggerAsyncId 32 | } 33 | if (has(args, 'executionAsyncId')) { 34 | this.executionAsyncId = args.executionAsyncId 35 | } 36 | } 37 | } 38 | 39 | class TraceEventDecoder extends stream.Transform { 40 | constructor () { 41 | super({ 42 | readableObjectMode: true, 43 | writableObjectMode: false 44 | }) 45 | 46 | // trace-events-parser is synchronous so there is no need to think about 47 | // backpresure 48 | this.parser = parser() 49 | this.parser.on('data', (data) => { 50 | switch (data.ph) { 51 | case 'b': 52 | case 'e': 53 | this.push(new TraceEvent(data)) 54 | break 55 | default: 56 | // Fall-through 57 | } 58 | }) 59 | } 60 | 61 | _transform (chunk, encoding, callback) { 62 | this.parser.write(chunk, encoding) 63 | callback(null) 64 | } 65 | 66 | _flush (callback) { 67 | this.parser.end() 68 | callback(null) 69 | } 70 | } 71 | module.exports = TraceEventDecoder 72 | -------------------------------------------------------------------------------- /injects/detect-port.js: -------------------------------------------------------------------------------- 1 | const onlisten = require('on-net-listen') 2 | const fs = require('fs') 3 | const net = require('net') 4 | const logger = require('./logger') 5 | 6 | onlisten(function (addr) { 7 | // we do async activity below and we do not want that to pollute the 8 | // analysis. we use the skipThis flag to opt out of collecting stats here. 9 | logger.skipThis = true 10 | this.destroy() 11 | const port = Buffer.from(addr.port + '') 12 | fs.writeSync(3, port, 0, port.length) 13 | signal(3, function () { 14 | process.emit('beforeExit') 15 | }) 16 | logger.skipThis = false 17 | }) 18 | 19 | function signal (fd, cb) { 20 | const s = new net.Socket({ fd, readable: true, writable: false }) 21 | s.unref() 22 | s.on('error', () => {}) 23 | s.on('close', cb) 24 | } 25 | -------------------------------------------------------------------------------- /injects/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const makeDir = require('mkdirp') 5 | const asyncHooks = require('async_hooks') 6 | const stackTrace = require('../collect/stack-trace.js') 7 | const systemInfo = require('../collect/system-info.js') 8 | const StackTraceEncoder = require('../format/stack-trace-encoder.js') 9 | const getLoggingPaths = require('@clinic/clinic-common').getLoggingPaths('bubbleprof') 10 | const checkForTranspiledCode = require('@clinic/clinic-common').checkForTranspiledCode 11 | 12 | process.nextTick(function () { 13 | if (process.mainModule && checkForTranspiledCode(process.mainModule.filename)) { 14 | // Show warning to user 15 | fs.writeSync(3, 'source_warning', null, 'utf8') 16 | } 17 | }) 18 | 19 | // create dirname 20 | const paths = getLoggingPaths({ 21 | path: process.env.NODE_CLINIC_BUBBLEPROF_DATA_PATH, 22 | identifier: process.env.NODE_CLINIC_BUBBLEPROF_NAME || process.pid 23 | }) 24 | makeDir.sync(paths['/']) 25 | 26 | // write system file 27 | fs.writeFileSync(paths['/systeminfo'], JSON.stringify(systemInfo(), null, 2)) 28 | 29 | // setup encoded states file 30 | const encoder = new StackTraceEncoder() 31 | const out = encoder.pipe( 32 | fs.createWriteStream(paths['/stacktrace'], { 33 | // Open log file synchronously to ensure that that .write() only 34 | // corresponds to one async action. This makes it easy to filter .write() 35 | // in async_hooks. 36 | fd: fs.openSync(paths['/stacktrace'], 'w'), 37 | highWaterMark: 16384 * 100 // 1.6MB 38 | }) 39 | ) 40 | 41 | // log stack traces, export a flag to opt out of logging for internals 42 | exports.skipThis = false 43 | const skipAsyncIds = new Set() 44 | const hook = asyncHooks.createHook({ 45 | init (asyncId, type, triggerAsyncId) { 46 | // Save the asyncId such nested async operations can be skiped later. 47 | if (exports.skipThis) return skipAsyncIds.add(asyncId) 48 | // This is a nested async operations, skip this and track futher nested 49 | // async operations. 50 | if (skipAsyncIds.has(triggerAsyncId)) return skipAsyncIds.add(asyncId) 51 | 52 | // Track async events that comes from this async operation 53 | exports.skipThis = true 54 | encoder.write({ 55 | asyncId: asyncId, 56 | frames: stackTrace(2) 57 | }) 58 | exports.skipThis = false 59 | }, 60 | 61 | destroy (asyncId) { 62 | skipAsyncIds.delete(asyncId) 63 | } 64 | }) 65 | hook.enable() 66 | 67 | // before process exits, flush the encoded data to the sample file 68 | process.once('beforeExit', function () { 69 | hook.disable() 70 | encoder.end() 71 | out.on('close', function () { 72 | process.exit() 73 | }) 74 | }) 75 | 76 | // NOTE: Workaround until https://github.com/nodejs/node/issues/18476 is solved 77 | exports.skipThis = true 78 | process.on('SIGINT', function () { 79 | if (process.listenerCount('SIGINT') === 1) process.emit('beforeExit') 80 | }) 81 | exports.skipThis = false 82 | -------------------------------------------------------------------------------- /injects/no-cluster.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster') 2 | 3 | cluster.on('fork', () => { 4 | throw new Error('clinic bubbleprof does not support clustering.') 5 | }) 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@clinic/bubbleprof", 3 | "description": "Programmable interface to Clinic.js Bubbleprof", 4 | "repository": "clinicjs/node-clinic-bubbleprof", 5 | "version": "10.0.0", 6 | "scripts": { 7 | "test": "standard | snazzy && tap --no-cov test/*.test.js", 8 | "test:file": "standard | snazzy && tap --no-cov", 9 | "ci-lint": "standard | snazzy", 10 | "ci-test": "tap test/*.test.js", 11 | "ci-cov": "tap test/*.test.js", 12 | "lint": "standard --fix | snazzy", 13 | "visualize-watch": "node debug/visualize-watch.js", 14 | "visualize-all": "node debug/visualize-all.js" 15 | }, 16 | "devDependencies": { 17 | "chokidar": "^3.4.3", 18 | "cross-platform-sock": "^1.0.0", 19 | "express": "^4.16.2", 20 | "inspectpoint": "^0.2.2", 21 | "rimraf": "^3.0.0", 22 | "semver": "^7.0.0", 23 | "shuffle-array": "^1.0.1", 24 | "snazzy": "^9.0.0", 25 | "standard": "^16.0.3", 26 | "startpoint": "^0.3.2", 27 | "tap": "^15.0.10" 28 | }, 29 | "keywords": [], 30 | "license": "MIT", 31 | "dependencies": { 32 | "@clinic/clinic-common": "^7.0.0", 33 | "@clinic/node-trace-log-join": "^2.0.0", 34 | "@clinic/trace-events-parser": "^2.0.0", 35 | "array-flatten": "^3.0.0", 36 | "async": "^3.0.1", 37 | "d3-axis": "^1.0.8", 38 | "d3-color": "^1.4.0", 39 | "d3-drag": "^1.2.3", 40 | "d3-ease": "^1.0.3", 41 | "d3-format": "^1.3.0", 42 | "d3-interpolate": "^1.2.0", 43 | "d3-scale": "^3.0.0", 44 | "d3-selection": "^1.3.0", 45 | "d3-shape": "^1.2.0", 46 | "d3-time": "^1.0.8", 47 | "d3-time-format": "^2.1.1", 48 | "d3-transition": "^1.1.1", 49 | "endpoint": "^0.4.5", 50 | "lodash": "^4.14.0", 51 | "minify-stream": "^2.0.1", 52 | "mkdirp": "^1.0.0", 53 | "on-net-listen": "^1.0.0", 54 | "protocol-buffers": "^4.0.4", 55 | "pump": "^3.0.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicjs/node-clinic-bubbleprof/cf801d49299b8ecd25c5d9e88f662defc4bbad71/screenshot.png -------------------------------------------------------------------------------- /test/analysis-aggregate-combine.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const endpoint = require('endpoint') 5 | const startpoint = require('startpoint') 6 | const { FakeSourceNode } = require('./analysis-util') 7 | const CombineAsAggregateNodes = require('../analysis/aggregate/combine-as-aggregate-nodes.js') 8 | 9 | test('Aggregate Node - combine', function (t) { 10 | const serverNode = new FakeSourceNode({ 11 | asyncId: 2, 12 | frames: [{ fileName: 'server.js' }], 13 | identifier: 'server.js', 14 | type: 'CUSTOM_SERVER', 15 | triggerAsyncId: 1, 16 | executionAsyncId: 1, 17 | init: 1, 18 | destroy: 10 19 | }) 20 | 21 | const socketNodes = [] 22 | const logNodes = [] 23 | const endNodes = [] 24 | for (let i = 0; i < 2; i++) { 25 | const socketAsyncId = 3 + i * 3 26 | const logAsyncId = 4 + i * 3 27 | const endAsyncId = 5 + i * 3 28 | 29 | const socketNodeSocket = new FakeSourceNode({ 30 | asyncId: socketAsyncId, 31 | frames: [{ fileName: 'server.js' }], 32 | identifier: 'server.js', 33 | type: 'CUSTOM_SOCKET', 34 | executionAsyncId: 0, 35 | triggerAsyncId: 2, 36 | init: 2 + i * 2, 37 | destroy: 4 + i * 2 38 | }) 39 | socketNodes.push(socketNodeSocket) 40 | 41 | const socketNodelog = new FakeSourceNode({ 42 | asyncId: logAsyncId, 43 | frames: [{ fileName: 'log.js' }], 44 | identifier: 'log.js', 45 | type: 'CUSTOM_LOG', 46 | executionAsyncId: socketAsyncId, 47 | triggerAsyncId: socketAsyncId, 48 | init: 3 + i * 2, 49 | destroy: 4 + i * 2 50 | }) 51 | logNodes.push(socketNodelog) 52 | 53 | const socketNodeEnd = new FakeSourceNode({ 54 | asyncId: endAsyncId, 55 | frames: [{ fileName: 'server.js' }], 56 | identifier: 'server.js', 57 | type: 'CUSTOM_END', 58 | executionAsyncId: logAsyncId, 59 | triggerAsyncId: socketAsyncId, 60 | init: 3 + i * 2, 61 | destroy: 4 + i * 2 62 | }) 63 | endNodes.push(socketNodeEnd) 64 | } 65 | 66 | const sourceNodes = [serverNode, ...socketNodes, ...logNodes, ...endNodes] 67 | startpoint(sourceNodes, { objectMode: true }) 68 | .pipe(new CombineAsAggregateNodes()) 69 | .pipe(endpoint({ objectMode: true }, function (err, aggregateNodes) { 70 | if (err) return t.error(err) 71 | 72 | // root 73 | t.strictSame(aggregateNodes[0].toJSON(), { 74 | aggregateId: 1, 75 | parentAggregateId: 0, 76 | children: [2], 77 | sources: [aggregateNodes[0].sources[0].toJSON({ short: true })], 78 | mark: ['root', null, null], 79 | name: null, 80 | type: null, 81 | frames: [] 82 | }) 83 | 84 | // server 85 | t.strictSame(aggregateNodes[1].toJSON(), { 86 | aggregateId: 2, 87 | parentAggregateId: 1, 88 | children: [3], 89 | sources: [serverNode.toJSON({ short: true })], 90 | mark: [null, null, null], 91 | name: null, 92 | type: 'CUSTOM_SERVER', 93 | frames: [{ fileName: 'server.js' }] 94 | }) 95 | 96 | // socket 97 | t.strictSame(aggregateNodes[2].toJSON(), { 98 | aggregateId: 3, 99 | parentAggregateId: 2, 100 | children: [4, 5], 101 | sources: socketNodes.map((source) => source.toJSON({ short: true })), 102 | mark: [null, null, null], 103 | name: null, 104 | type: 'CUSTOM_SOCKET', 105 | frames: [{ fileName: 'server.js' }] 106 | }) 107 | 108 | // log 109 | t.strictSame(aggregateNodes[3].toJSON(), { 110 | aggregateId: 4, 111 | parentAggregateId: 3, 112 | children: [], 113 | sources: logNodes.map((source) => source.toJSON({ short: true })), 114 | mark: [null, null, null], 115 | name: null, 116 | type: 'CUSTOM_LOG', 117 | frames: [{ fileName: 'log.js' }] 118 | }) 119 | 120 | // end 121 | t.strictSame(aggregateNodes[4].toJSON(), { 122 | aggregateId: 5, 123 | parentAggregateId: 3, 124 | children: [], 125 | sources: endNodes.map((source) => source.toJSON({ short: true })), 126 | mark: [null, null, null], 127 | name: null, 128 | type: 'CUSTOM_END', 129 | frames: [{ fileName: 'server.js' }] 130 | }) 131 | 132 | t.end() 133 | })) 134 | }) 135 | -------------------------------------------------------------------------------- /test/analysis-aggregate-mark-module.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const endpoint = require('endpoint') 5 | const startpoint = require('startpoint') 6 | const MarkModuleAggregateNodes = require('../analysis/aggregate/mark-module-aggregate-nodes.js') 7 | const { FakeAggregateNode, FakeSystemInfo } = require('./analysis-util') 8 | 9 | test('Aggregate Node - mark module', function (t) { 10 | const aggregateNodeRoot = new FakeAggregateNode({ 11 | aggregateId: 1, 12 | parentAggregateId: 0, 13 | children: [2, 3, 4], 14 | isRoot: true 15 | }) 16 | 17 | const aggregateNodeNodecore = new FakeAggregateNode({ 18 | aggregateId: 2, 19 | parentAggregateId: 1, 20 | children: [], 21 | frames: [ 22 | { fileName: 'internal/process.js' } 23 | ], 24 | mark: ['nodecore', null, null], 25 | type: 'TickObject' 26 | }) 27 | 28 | const aggregateNodeInternal = new FakeAggregateNode({ 29 | aggregateId: 3, 30 | parentAggregateId: 1, 31 | children: [], 32 | frames: [ 33 | { fileName: '/user/internal/index.js' }, 34 | { fileName: 'internal/process.js' } 35 | ], 36 | mark: ['user', null, null], 37 | type: 'TickObject' 38 | }) 39 | 40 | const aggregateNodeExternal = new FakeAggregateNode({ 41 | aggregateId: 4, 42 | parentAggregateId: 1, 43 | children: [], 44 | frames: [ 45 | { fileName: '/node_modules/external/index.js' }, 46 | { fileName: '/node_modules/external/node_modules/deep/index.js' }, 47 | { fileName: 'internal/process.js' } 48 | ], 49 | mark: ['external', null, null], 50 | type: 'TickObject' 51 | }) 52 | 53 | const aggregateNodeEval = new FakeAggregateNode({ 54 | aggregateId: 2, 55 | parentAggregateId: 1, 56 | children: [], 57 | frames: [ 58 | { fileName: '/node_modules/promise/lib/core.js' }, 59 | { fileName: '', isEval: true, evalOrigin: 'eval at denodeifyWithoutCount (/node_modules/promise/lib/node-extensions.js:90:10)' } 60 | ], 61 | mark: ['external', null, null], 62 | type: 'TickObject' 63 | }) 64 | 65 | const systemInfo = new FakeSystemInfo('/') 66 | const aggregateNodesInput = [ 67 | aggregateNodeRoot, aggregateNodeNodecore, 68 | aggregateNodeInternal, aggregateNodeExternal, 69 | aggregateNodeEval 70 | ] 71 | 72 | startpoint(aggregateNodesInput, { objectMode: true }) 73 | .pipe(new MarkModuleAggregateNodes(systemInfo)) 74 | .pipe(endpoint({ objectMode: true }, function (err, aggregateNodesOutput) { 75 | if (err) return t.error(err) 76 | t.strictSame( 77 | aggregateNodesOutput[0].mark.toJSON(), 78 | ['root', null, null] 79 | ) 80 | t.strictSame( 81 | aggregateNodesOutput[1].mark.toJSON(), 82 | ['nodecore', null, null] 83 | ) 84 | t.strictSame( 85 | aggregateNodesOutput[2].mark.toJSON(), 86 | ['user', null, null] 87 | ) 88 | t.strictSame( 89 | aggregateNodesOutput[3].mark.toJSON(), 90 | ['external', 'deep', null] 91 | ) 92 | t.strictSame( 93 | aggregateNodesOutput[4].mark.toJSON(), 94 | ['external', 'promise', null] 95 | ) 96 | t.end() 97 | })) 98 | }) 99 | -------------------------------------------------------------------------------- /test/analysis-aggregate-mark-party.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const endpoint = require('endpoint') 5 | const startpoint = require('startpoint') 6 | const MarkPartyAggregateNodes = require('../analysis/aggregate/mark-party-aggregate-nodes.js') 7 | const { FakeAggregateNode, FakeSystemInfo } = require('./analysis-util') 8 | 9 | test('Aggregate Node - mark party', function (t) { 10 | const aggregateNodeRoot = new FakeAggregateNode({ 11 | aggregateId: 1, 12 | parentAggregateId: 0, 13 | children: [2, 3, 4, 5, 6], 14 | isRoot: true 15 | }) 16 | 17 | const aggregateNodeNoFramesNodecore = new FakeAggregateNode({ 18 | aggregateId: 2, 19 | parentAggregateId: 1, 20 | children: [], 21 | frames: [], 22 | type: 'TCPWRAP' 23 | }) 24 | 25 | const aggregateNodeNoFramesExternal = new FakeAggregateNode({ 26 | aggregateId: 3, 27 | parentAggregateId: 1, 28 | children: [], 29 | frames: [], 30 | type: 'CUSTOM' 31 | }) 32 | 33 | const aggregateNodeFramesNodecore = new FakeAggregateNode({ 34 | aggregateId: 4, 35 | parentAggregateId: 1, 36 | children: [], 37 | frames: [ 38 | { fileName: 'internal/process.js' }, 39 | { fileName: 'internal/node_bootstrap.js' } 40 | ], 41 | type: 'TickObject' 42 | }) 43 | 44 | const aggregateNodeFramesExternal = new FakeAggregateNode({ 45 | aggregateId: 5, 46 | parentAggregateId: 1, 47 | children: [], 48 | frames: [ 49 | { fileName: '/node_modules/external/node_modules/deep/index.js' }, 50 | { fileName: '/node_modules/external/index.js' }, 51 | { fileName: 'internal/process.js' }, 52 | { fileName: 'internal/node_bootstrap.js' } 53 | ], 54 | type: 'TickObject' 55 | }) 56 | 57 | const aggregateNodeFramesUser = new FakeAggregateNode({ 58 | aggregateId: 6, 59 | parentAggregateId: 1, 60 | children: [], 61 | frames: [ 62 | { fileName: '/user/internal/index.js' }, 63 | { fileName: '/node_modules/external/node_modules/deep/index.js' }, 64 | { fileName: '/node_modules/external/index.js' }, 65 | { fileName: 'internal/process.js' }, 66 | { fileName: 'internal/node_bootstrap.js' } 67 | ], 68 | type: 'TickObject' 69 | }) 70 | 71 | const systemInfo = new FakeSystemInfo('/') 72 | const aggregateNodesInput = [ 73 | aggregateNodeRoot, 74 | aggregateNodeNoFramesNodecore, aggregateNodeNoFramesExternal, 75 | aggregateNodeFramesNodecore, aggregateNodeFramesExternal, 76 | aggregateNodeFramesUser 77 | ] 78 | 79 | startpoint(aggregateNodesInput, { objectMode: true }) 80 | .pipe(new MarkPartyAggregateNodes(systemInfo)) 81 | .pipe(endpoint({ objectMode: true }, function (err, aggregateNodesOutput) { 82 | if (err) return t.error(err) 83 | t.strictSame( 84 | aggregateNodesOutput[0].mark.toJSON(), 85 | ['root', null, null] 86 | ) 87 | t.strictSame( 88 | aggregateNodesOutput[1].mark.toJSON(), 89 | ['nodecore', null, null] 90 | ) 91 | t.strictSame( 92 | aggregateNodesOutput[2].mark.toJSON(), 93 | ['external', null, null] 94 | ) 95 | t.strictSame( 96 | aggregateNodesOutput[3].mark.toJSON(), 97 | ['nodecore', null, null] 98 | ) 99 | t.strictSame( 100 | aggregateNodesOutput[4].mark.toJSON(), 101 | ['external', null, null] 102 | ) 103 | t.strictSame( 104 | aggregateNodesOutput[5].mark.toJSON(), 105 | ['user', null, null] 106 | ) 107 | t.end() 108 | })) 109 | }) 110 | -------------------------------------------------------------------------------- /test/analysis-barrier-wrap.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const endpoint = require('endpoint') 5 | const startpoint = require('startpoint') 6 | const WrapAsBarrierNode = require('../analysis/barrier/wrap-as-barrier-nodes.js') 7 | const { FakeAggregateNode } = require('./analysis-util') 8 | 9 | test('Barrier Node - wrap', function (t) { 10 | const aggregateNode = new FakeAggregateNode({ 11 | aggregateId: 2, 12 | parentAggregateId: 1, 13 | children: [3, 4], 14 | type: 'CUSTOM', 15 | frames: [] 16 | }) 17 | 18 | startpoint([aggregateNode], { objectMode: true }) 19 | .pipe(new WrapAsBarrierNode()) 20 | .pipe(endpoint({ objectMode: true }, function (err, nodes) { 21 | if (err) return t.error(err) 22 | 23 | t.strictSame(nodes[0].toJSON(), { 24 | barrierId: 2, 25 | parentBarrierId: 1, 26 | children: [3, 4], 27 | name: null, 28 | isWrapper: true, 29 | nodes: [aggregateNode.toJSON()] 30 | }) 31 | t.end() 32 | })) 33 | }) 34 | -------------------------------------------------------------------------------- /test/analysis-raw-event.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const StackTrace = require('../analysis/stack-trace/stack-trace.js') 5 | const TraceEvent = require('../analysis/trace-event/trace-event.js') 6 | const RawEvent = require('../analysis/raw-event/raw-event.js') 7 | 8 | test('Raw Event - RawEvent.wrapTraceEvent', function (t) { 9 | const stackTraceObject = new StackTrace({ asyncId: 2, frames: [] }) 10 | const traceEventObject = new TraceEvent({ 11 | asyncId: 2, 12 | timestamp: 1, 13 | event: 'init', 14 | type: 'custom', 15 | triggerAsyncId: 1, 16 | executionAsyncId: 0 17 | }) 18 | 19 | t.strictSame(RawEvent.wrapTraceEvent(traceEventObject).toJSON(), { 20 | type: 'traceEvent', 21 | asyncId: 2, 22 | info: traceEventObject.toJSON() 23 | }) 24 | 25 | t.throws( 26 | () => RawEvent.wrapTraceEvent(stackTraceObject), 27 | new TypeError('wrapTraceEvent input must be a TraceEvent instance') 28 | ) 29 | 30 | t.end() 31 | }) 32 | 33 | test('Raw Event - RawEvent.wrapStackTrace', function (t) { 34 | const stackTraceObject = new StackTrace({ asyncId: 2, frames: [] }) 35 | const traceEventObject = new TraceEvent({ 36 | asyncId: 2, 37 | timestamp: 1, 38 | event: 'init', 39 | type: 'custom', 40 | triggerAsyncId: 1, 41 | executionAsyncId: 0 42 | }) 43 | 44 | t.strictSame(RawEvent.wrapStackTrace(stackTraceObject).toJSON(), { 45 | type: 'stackTrace', 46 | asyncId: 2, 47 | info: stackTraceObject.toJSON() 48 | }) 49 | 50 | t.throws( 51 | () => RawEvent.wrapStackTrace(traceEventObject), 52 | new TypeError('wrapStackTrace input must be a StackTrace instance') 53 | ) 54 | 55 | t.end() 56 | }) 57 | -------------------------------------------------------------------------------- /test/analysis-source-combine.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const endpoint = require('endpoint') 5 | const startpoint = require('startpoint') 6 | const RawEvent = require('../analysis/raw-event/raw-event.js') 7 | const StackTrace = require('../analysis/stack-trace/stack-trace.js') 8 | const TraceEvent = require('../analysis/trace-event/trace-event.js') 9 | const CombineAsSourceNodes = require('../analysis/source/combine-as-source-nodes.js') 10 | 11 | test('Source Node - combine', function (t) { 12 | const joined = startpoint([ 13 | RawEvent.wrapTraceEvent(new TraceEvent({ 14 | event: 'init', 15 | type: 'HAS_STACK', 16 | asyncId: 1, 17 | triggerAsyncId: 0, 18 | executionAsyncId: 0, 19 | timestamp: 1 20 | })), 21 | RawEvent.wrapStackTrace(new StackTrace({ 22 | asyncId: 1, 23 | frames: [] 24 | })), 25 | RawEvent.wrapTraceEvent(new TraceEvent({ 26 | event: 'before', 27 | asyncId: 1, 28 | timestamp: 2 29 | })), 30 | RawEvent.wrapTraceEvent(new TraceEvent({ 31 | event: 'after', 32 | asyncId: 1, 33 | timestamp: 3 34 | })), 35 | RawEvent.wrapTraceEvent(new TraceEvent({ 36 | event: 'destroy', 37 | asyncId: 1, 38 | timestamp: 4 39 | })), 40 | RawEvent.wrapTraceEvent(new TraceEvent({ 41 | event: 'init', 42 | type: 'NO_STACK', 43 | asyncId: 2, 44 | triggerAsyncId: 1, 45 | executionAsyncId: 1, 46 | timestamp: 5 47 | })), 48 | RawEvent.wrapTraceEvent(new TraceEvent({ 49 | event: 'destroy', 50 | asyncId: 2, 51 | timestamp: 6 52 | })), 53 | RawEvent.wrapTraceEvent(new TraceEvent({ 54 | event: 'init', 55 | type: 'NO_DESTROY', 56 | asyncId: 3, 57 | triggerAsyncId: 1, 58 | executionAsyncId: 1, 59 | timestamp: 7 60 | })) 61 | ], { objectMode: true }) 62 | 63 | joined 64 | .pipe(new CombineAsSourceNodes()) 65 | .pipe(endpoint({ objectMode: true }, function (err, data) { 66 | if (err) return t.error(err) 67 | 68 | const sourceNodes = new Map(data.map((node) => [node.asyncId, node])) 69 | t.equal(data.length, 3) 70 | t.equal(sourceNodes.size, 3) 71 | 72 | t.strictSame(sourceNodes.get(1).toJSON(), { 73 | asyncId: 1, 74 | triggerAsyncId: 0, 75 | executionAsyncId: 0, 76 | parentAsyncId: 0, 77 | type: 'HAS_STACK', 78 | frames: [], 79 | identifier: null, 80 | init: 1, 81 | before: [2], 82 | after: [3], 83 | destroy: 4 84 | }) 85 | 86 | t.strictSame(sourceNodes.get(2).toJSON(), { 87 | asyncId: 2, 88 | triggerAsyncId: 1, 89 | executionAsyncId: 1, 90 | parentAsyncId: 1, 91 | type: 'NO_STACK', 92 | frames: null, 93 | identifier: null, 94 | init: 5, 95 | before: [], 96 | after: [], 97 | destroy: 6 98 | }) 99 | 100 | t.strictSame(sourceNodes.get(3).toJSON(), { 101 | asyncId: 3, 102 | triggerAsyncId: 1, 103 | executionAsyncId: 1, 104 | parentAsyncId: 1, 105 | type: 'NO_DESTROY', 106 | frames: null, 107 | identifier: null, 108 | init: 7, 109 | before: [], 110 | after: [], 111 | destroy: null 112 | }) 113 | 114 | t.end() 115 | })) 116 | }) 117 | -------------------------------------------------------------------------------- /test/analysis-source-filter.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const endpoint = require('endpoint') 5 | const startpoint = require('startpoint') 6 | const FilterSourceNodes = require('../analysis/source/filter-source-nodes.js') 7 | const { FakeSourceNode } = require('./analysis-util') 8 | 9 | test('Source Node - filter', function (t) { 10 | const nodeNotFiltered = new FakeSourceNode({ 11 | asyncId: 1, 12 | frames: [], 13 | type: 'NOT_FILTERED', 14 | triggerAsyncId: 0, 15 | executionAsyncId: 0, 16 | init: 1, 17 | destroy: 2 18 | }) 19 | 20 | const nodeNoStack = new FakeSourceNode({ 21 | asyncId: 2, 22 | type: 'NO_STACK_TRACE', 23 | triggerAsyncId: 0, 24 | executionAsyncId: 0, 25 | init: 1, 26 | destroy: 2 27 | }) 28 | 29 | const nodeTimer = new FakeSourceNode({ 30 | asyncId: 3, 31 | frames: [], 32 | type: 'TIMERWRAP', 33 | triggerAsyncId: 0, 34 | executionAsyncId: 0, 35 | init: 1, 36 | destroy: 2 37 | }) 38 | 39 | startpoint([nodeNotFiltered, nodeNoStack, nodeTimer], { objectMode: true }) 40 | .pipe(new FilterSourceNodes()) 41 | .pipe(endpoint({ objectMode: true }, function (err, nodes) { 42 | if (err) return t.error(err) 43 | 44 | t.equal(nodes.length, 1) 45 | t.equal(nodes[0], nodeNotFiltered) 46 | t.end() 47 | })) 48 | }) 49 | -------------------------------------------------------------------------------- /test/analysis-source-http-requests.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const startpoint = require('startpoint') 5 | const HTTPRequestNodes = require('../analysis/source/http-request-nodes.js') 6 | const { FakeSourceNode } = require('./analysis-util') 7 | 8 | test('Source Node - http requests', function (t) { 9 | const nodeServerResponse = new FakeSourceNode({ 10 | asyncId: 1, 11 | frames: [{ typeName: 'ServerResponse', functionName: 'end' }], 12 | type: 'TickObject', 13 | triggerAsyncId: 0, 14 | executionAsyncId: 0, 15 | init: 1, 16 | destroy: 2 17 | }) 18 | 19 | const nodeNoStack = new FakeSourceNode({ 20 | asyncId: 2, 21 | type: 'NO_STACK_TRACE', 22 | triggerAsyncId: 0, 23 | executionAsyncId: 0, 24 | init: 1, 25 | destroy: 2 26 | }) 27 | 28 | const nodeTimer = new FakeSourceNode({ 29 | asyncId: 3, 30 | frames: [], 31 | type: 'TIMERWRAP', 32 | triggerAsyncId: 0, 33 | executionAsyncId: 0, 34 | init: 3, 35 | destroy: 2 36 | }) 37 | 38 | const digest = { runtime: 0, httpRequests: [] } 39 | const stream = startpoint([nodeServerResponse, nodeNoStack, nodeTimer], { objectMode: true }) 40 | .pipe(new HTTPRequestNodes(digest)) 41 | 42 | stream.resume() 43 | stream.on('end', function () { 44 | t.equal(digest.runtime, 2) 45 | t.same(digest.httpRequests, [1]) 46 | t.end() 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/analysis-source-indentify.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const endpoint = require('endpoint') 5 | const startpoint = require('startpoint') 6 | const IdentifySourceNodes = require('../analysis/source/identify-source-nodes.js') 7 | const { FakeSourceNode } = require('./analysis-util') 8 | 9 | test('Source Node - indentify', function (t) { 10 | const sourceNodeHttpParserA = new FakeSourceNode({ 11 | asyncId: 1, 12 | frames: [{ 13 | fileName: 'should-be-ignored-a.js' 14 | }], 15 | type: 'HTTPPARSER', 16 | triggerAsyncId: 0, 17 | executionAsyncId: 0, 18 | init: 1, 19 | destroy: 2 20 | }) 21 | 22 | const sourceNodeHttpParserB = new FakeSourceNode({ 23 | asyncId: 1, 24 | frames: [{ 25 | fileName: 'should-be-ignored-b.js' 26 | }], 27 | type: 'HTTPPARSER', 28 | triggerAsyncId: 0, 29 | executionAsyncId: 0, 30 | init: 1, 31 | destroy: 2 32 | }) 33 | 34 | const sourceNodeNoFramesA = new FakeSourceNode({ 35 | asyncId: 2, 36 | frames: [], 37 | type: 'NO_FRAMES_A', 38 | triggerAsyncId: 0, 39 | executionAsyncId: 0, 40 | init: 1, 41 | destroy: 2 42 | }) 43 | 44 | const sourceNodeNoFramesB = new FakeSourceNode({ 45 | asyncId: 2, 46 | frames: [], 47 | type: 'NO_FRAMES_B', 48 | triggerAsyncId: 0, 49 | executionAsyncId: 0, 50 | init: 1, 51 | destroy: 2 52 | }) 53 | 54 | const sourceNodeWithFramesA = new FakeSourceNode({ 55 | asyncId: 2, 56 | frames: [{ 57 | fileName: 'source-1.js' 58 | }], 59 | type: 'TYPE_B', 60 | triggerAsyncId: 0, 61 | executionAsyncId: 0, 62 | init: 1, 63 | destroy: 2 64 | }) 65 | 66 | const sourceNodeWithFramesB = new FakeSourceNode({ 67 | asyncId: 2, 68 | frames: [{ 69 | fileName: 'source-1.js' 70 | }], 71 | type: 'TYPE_A', 72 | triggerAsyncId: 0, 73 | executionAsyncId: 0, 74 | init: 1, 75 | destroy: 2 76 | }) 77 | 78 | const sourceNodeWithFramesC = new FakeSourceNode({ 79 | asyncId: 2, 80 | frames: [{ 81 | fileName: 'source-2.js' 82 | }], 83 | type: 'TYPE_A', 84 | triggerAsyncId: 0, 85 | executionAsyncId: 0, 86 | init: 1, 87 | destroy: 2 88 | }) 89 | 90 | const sourceNodesInput = [ 91 | sourceNodeHttpParserA, sourceNodeHttpParserB, 92 | sourceNodeNoFramesA, sourceNodeNoFramesB, 93 | sourceNodeWithFramesA, sourceNodeWithFramesB, sourceNodeWithFramesC 94 | ] 95 | 96 | startpoint(sourceNodesInput, { objectMode: true }) 97 | .pipe(new IdentifySourceNodes()) 98 | .pipe(endpoint({ objectMode: true }, function (err, sourceNodesOutput) { 99 | if (err) return t.error(err) 100 | 101 | for (const sourceNode of sourceNodesOutput) { 102 | t.equal(typeof sourceNode.identifier, 'string') 103 | t.ok(sourceNode.identifier.length > 0) 104 | } 105 | 106 | t.equal( 107 | sourceNodesOutput[0].identifier, // sourceNodeHttpParserA 108 | sourceNodesOutput[1].identifier, // sourceNodeHttpParserB 109 | 'HTTPPARSER ignores frames' 110 | ) 111 | 112 | t.not( 113 | sourceNodesOutput[2].identifier, // sourceNodeNoFramesA 114 | sourceNodesOutput[3].identifier, // sourceNodeNoFramesB 115 | 'Without frames the type matters' 116 | ) 117 | 118 | t.not( 119 | sourceNodesOutput[4].identifier, // sourceNodeWithFramesA 120 | sourceNodesOutput[5].identifier, // sourceNodeWithFramesB 121 | 'With frames the type matters' 122 | ) 123 | 124 | t.not( 125 | sourceNodesOutput[5].identifier, // sourceNodeWithFramesB 126 | sourceNodesOutput[6].identifier, // sourceNodeWithFramesC 127 | 'With frames the frames matters' 128 | ) 129 | 130 | t.end() 131 | })) 132 | }) 133 | -------------------------------------------------------------------------------- /test/analysis-stack-trace.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const endpoint = require('endpoint') 5 | const startpoint = require('startpoint') 6 | const Frames = require('../analysis/stack-trace/frames.js') 7 | const StackTrace = require('../analysis/stack-trace/stack-trace.js') 8 | const WrapAsStackTrace = require('../analysis/stack-trace/wrap-as-stack-trace.js') 9 | 10 | test('Stack Trace - stream wrap', function (t) { 11 | const input = [{ 12 | asyncId: 1, 13 | frames: [{ 14 | fileName: 'test.js' 15 | }] 16 | }] 17 | 18 | startpoint(input, { objectMode: true }) 19 | .pipe(new WrapAsStackTrace()) 20 | .pipe(endpoint({ objectMode: true }, function (err, output) { 21 | if (err) return t.error(err) 22 | 23 | t.strictSame( 24 | output, 25 | input.map((data) => new StackTrace(data)) 26 | ) 27 | 28 | t.strictSame( 29 | output.map((data) => data.frames), 30 | input.map((data) => new Frames(data.frames)) 31 | ) 32 | t.end() 33 | })) 34 | }) 35 | 36 | test('Stack Trace - toJSON', function (t) { 37 | const input = { 38 | asyncId: 1, 39 | frames: [{ 40 | fileName: 'test.js' 41 | }] 42 | } 43 | 44 | t.strictSame(new StackTrace(input).toJSON(), input) 45 | t.end() 46 | }) 47 | -------------------------------------------------------------------------------- /test/analysis-system-info.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const { FakeSystemInfo } = require('./analysis-util') 5 | 6 | test('Stack Trace - isExternal', function (t) { 7 | const root = new FakeSystemInfo('/') 8 | const modules = new FakeSystemInfo('/node_modules/internal') 9 | const modulesDeep = new FakeSystemInfo('/node_modules/internal/deep') 10 | const modulesPrivate = new FakeSystemInfo('/node_modules/@private/internal') 11 | 12 | t.equal(root.moduleDirectory, '') 13 | t.equal(modules.moduleDirectory, '/node_modules/internal') 14 | t.equal(modulesDeep.moduleDirectory, '/node_modules/internal') 15 | t.equal(modulesPrivate.moduleDirectory, 16 | '/node_modules/@private/internal') 17 | 18 | t.end() 19 | }) 20 | -------------------------------------------------------------------------------- /test/analysis-trace-event.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const endpoint = require('endpoint') 5 | const startpoint = require('startpoint') 6 | const TraceEvent = require('../analysis/trace-event/trace-event.js') 7 | const WrapAsTraceEvent = require('../analysis/trace-event/wrap-as-trace-event.js') 8 | 9 | test('Trace Event - stream wrap', function (t) { 10 | const input = [{ 11 | asyncId: 1, 12 | event: 'init', 13 | type: 'custom', 14 | timestamp: 1, 15 | triggerAsyncId: 1, 16 | executionAsyncId: 0 17 | }] 18 | 19 | startpoint(input, { objectMode: true }) 20 | .pipe(new WrapAsTraceEvent()) 21 | .pipe(endpoint({ objectMode: true }, function (err, output) { 22 | if (err) return t.error(err) 23 | 24 | t.strictSame( 25 | output, 26 | input.map((data) => new TraceEvent(data)) 27 | ) 28 | 29 | t.end() 30 | })) 31 | }) 32 | 33 | test('Trace Event - toJSON', function (t) { 34 | const input = { 35 | asyncId: 1, 36 | event: 'init', 37 | type: 'custom', 38 | timestamp: 1, 39 | triggerAsyncId: 1, 40 | executionAsyncId: 0 41 | } 42 | 43 | t.strictSame(new TraceEvent(input).toJSON(), input) 44 | t.end() 45 | }) 46 | -------------------------------------------------------------------------------- /test/analysis-util/aggregate-node.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const AggregateNode = require('../../analysis/aggregate/aggregate-node.js') 4 | const FakeSourceNode = require('./source-node.js') 5 | 6 | class FakeAggregateNode extends AggregateNode { 7 | constructor (data) { 8 | super(data.aggregateId, data.parentAggregateId) 9 | 10 | if (data.isRoot) { 11 | this.makeRoot() 12 | } 13 | 14 | if (data.children) { 15 | for (const childAggregateId of data.children) { 16 | this.addChild(childAggregateId) 17 | } 18 | } 19 | 20 | if (data.type || data.frames) { 21 | this.addSourceNode(new FakeSourceNode({ 22 | asyncId: data.aggregateId, 23 | frames: data.frames, 24 | type: data.type, 25 | triggerAsyncId: data.parentAggregateId, 26 | executionAsyncId: data.parentAggregateId, 27 | init: 1, 28 | destroy: 2, 29 | identifier: data.aggregateId.toString() 30 | })) 31 | } 32 | 33 | if (data.mark) { 34 | this.mark.set(0, data.mark[0]) 35 | this.mark.set(1, data.mark[1]) 36 | this.mark.set(2, data.mark[2]) 37 | } 38 | } 39 | } 40 | 41 | module.exports = FakeAggregateNode 42 | -------------------------------------------------------------------------------- /test/analysis-util/barrier-node.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BarrierNode = require('../../analysis/barrier/barrier-node.js') 4 | const FakeAggregateNode = require('./aggregate-node.js') 5 | 6 | class FakeBarrierNode extends BarrierNode { 7 | constructor (data) { 8 | super(data.barrierId, data.parentBarrierId) 9 | 10 | const nodes = data.nodes.map((nodeData) => new FakeAggregateNode(nodeData)) 11 | 12 | if (data.name) { 13 | this.setName(data.name) 14 | } 15 | 16 | if (data.nodes.length === 1) { 17 | this.initializeAsWrapper(nodes[0], data.children) 18 | if (!data.isWrapper) this.makeBarrier() 19 | } 20 | 21 | if (data.nodes.length > 1) { 22 | this.initializeAsCombined(nodes, data.children) 23 | } 24 | } 25 | } 26 | 27 | module.exports = FakeBarrierNode 28 | -------------------------------------------------------------------------------- /test/analysis-util/cluster-node.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const CluterNode = require('../../analysis/cluster/cluster-node.js') 4 | const FakeBarrierNode = require('./barrier-node.js') 5 | 6 | class FakeClusterNode extends CluterNode { 7 | constructor (data) { 8 | super(data.clusterId, data.parentClusterId) 9 | 10 | if (data.isRoot) { 11 | this.makeRoot() 12 | } 13 | 14 | if (data.children) { 15 | for (const childAggregateId of data.children) { 16 | this.addChild(childAggregateId) 17 | } 18 | } 19 | 20 | if (data.nodes) { 21 | const barrierNode = new FakeBarrierNode({ 22 | barrierId: data.nodes 23 | .map((aggregateNode) => aggregateNode.aggregateId) 24 | .sort((a, b) => a - b) 25 | .shift(), 26 | parentBarrierId: data.nodes 27 | .map((aggregateNode) => aggregateNode.parentAggregateId) 28 | .sort((a, b) => a - b) 29 | .shift(), 30 | children: [].concat( 31 | data.nodes.map((aggregateNode) => aggregateNode.children) 32 | ), 33 | nodes: data.nodes 34 | }) 35 | 36 | this.insertBarrierNode(barrierNode) 37 | } 38 | } 39 | } 40 | 41 | module.exports = FakeClusterNode 42 | -------------------------------------------------------------------------------- /test/analysis-util/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | FakeAggregateNode: require('./aggregate-node.js'), 5 | FakeBarrierNode: require('./barrier-node.js'), 6 | FakeClusterNode: require('./cluster-node.js'), 7 | FakeSourceNode: require('./source-node.js'), 8 | FakeSystemInfo: require('./system-info.js') 9 | } 10 | -------------------------------------------------------------------------------- /test/analysis-util/source-node.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const SourceNode = require('../../analysis/source/source-node.js') 4 | const StackTrace = require('../../analysis/stack-trace/stack-trace.js') 5 | const TraceEvent = require('../../analysis/trace-event/trace-event.js') 6 | 7 | function has (object, property) { return Object.prototype.hasOwnProperty.call(object, property) } 8 | 9 | class FakeSourceNode extends SourceNode { 10 | constructor (data) { 11 | super(data.asyncId) 12 | 13 | if (data.frames) { 14 | this.addStackTrace(new StackTrace({ 15 | asyncId: data.asyncId, 16 | frames: data.frames 17 | })) 18 | } 19 | 20 | if (data.init) { 21 | this.addTraceEvent(new TraceEvent({ 22 | event: 'init', 23 | type: data.type, 24 | asyncId: data.asyncId, 25 | triggerAsyncId: data.triggerAsyncId, 26 | executionAsyncId: data.executionAsyncId, 27 | timestamp: data.init 28 | })) 29 | } 30 | 31 | const before = has(data, 'before') ? data.before : [] 32 | const after = has(data, 'after') ? data.after : [] 33 | 34 | for (let i = 0; i < Math.max(before.length, after.length); i++) { 35 | if (i < before.length) { 36 | this.addTraceEvent(new TraceEvent({ 37 | event: 'before', 38 | type: data.type, 39 | asyncId: data.asyncId, 40 | timestamp: before[i] 41 | })) 42 | } 43 | 44 | if (i < after.length) { 45 | this.addTraceEvent(new TraceEvent({ 46 | event: 'after', 47 | type: data.type, 48 | asyncId: data.asyncId, 49 | timestamp: after[i] 50 | })) 51 | } 52 | } 53 | 54 | if (data.destroy) { 55 | this.addTraceEvent(new TraceEvent({ 56 | event: 'destroy', 57 | type: data.type, 58 | asyncId: data.asyncId, 59 | timestamp: data.destroy 60 | })) 61 | } 62 | 63 | if (data.identifier) { 64 | this.setIdentifier(data.identifier) 65 | } 66 | } 67 | } 68 | 69 | module.exports = FakeSourceNode 70 | -------------------------------------------------------------------------------- /test/analysis-util/system-info.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const asyncWrap = process.binding('async_wrap') // eslint-disable-line node/no-deprecated-api 4 | const SystemInfo = require('../../analysis/system-info.js') 5 | 6 | class FakeSystemInfo extends SystemInfo { 7 | constructor (mainDirectory) { 8 | super({ 9 | providers: [ 10 | 'TickObject', 'Timeout', 'Immediate', 11 | ...Object.keys(asyncWrap.Providers) 12 | ], 13 | pathSeparator: '/', 14 | mainDirectory: mainDirectory // test directory 15 | }) 16 | } 17 | } 18 | 19 | module.exports = FakeSystemInfo 20 | -------------------------------------------------------------------------------- /test/cmd-collect-analysing.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const rimraf = require('rimraf') 5 | const ClinicBubbleprof = require('../index.js') 6 | 7 | test('test collect - emits "analysing" event', function (t) { 8 | const tool = new ClinicBubbleprof() 9 | 10 | function cleanup (err, dirname) { 11 | t.error(err) 12 | t.match(dirname, /^[0-9]+\.clinic-bubbleprof$/) 13 | rimraf(dirname, (err) => { 14 | t.error(err) 15 | t.end() 16 | }) 17 | } 18 | 19 | let seenAnalysing = false 20 | tool.on('analysing', () => { 21 | seenAnalysing = true 22 | }) 23 | 24 | tool.collect( 25 | [process.execPath, '-e', 'setTimeout(() => {}, 123)'], 26 | function (err, dirname) { 27 | if (err) return cleanup(err, dirname) 28 | 29 | t.ok(seenAnalysing) // should've happened before this callback 30 | cleanup(null, dirname) 31 | } 32 | ) 33 | }) 34 | -------------------------------------------------------------------------------- /test/cmd-collect-detect-port.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const http = require('http') 5 | const CollectAndRead = require('./collect-and-read.js') 6 | 7 | test('cmd - collect - detect server port', function (t) { 8 | const cmd = new CollectAndRead({ detectPort: true }, '-e', ` 9 | const http = require('http') 10 | http.createServer(onrequest).listen(0) 11 | 12 | function onrequest (req, res) { 13 | this.close() 14 | res.end('from server') 15 | } 16 | `) 17 | 18 | cmd.tool.on('port', function (port) { 19 | t.ok(typeof port === 'number') 20 | t.ok(port > 0) 21 | 22 | http.get(`http://127.0.0.1:${port}`, function (res) { 23 | const buf = [] 24 | res.on('data', data => buf.push(data)) 25 | res.on('end', function () { 26 | t.same(Buffer.concat(buf), Buffer.from('from server')) 27 | t.end() 28 | }) 29 | }) 30 | process.on('exit', function () { 31 | cmd.cleanup() 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/cmd-collect-exit-sigint.script.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const CollectAndRead = require(path.resolve('collect-and-read.js')) 5 | const cmd = new CollectAndRead({}, '-e', ` 6 | setInterval(() => {}, 100) 7 | process.once('SIGINT', function () { 8 | console.log('SIGINT received') 9 | process.kill(process.pid, 'SIGINT') 10 | }) 11 | console.log('listening for SIGINT') 12 | `) 13 | cmd.on('ready', function () { 14 | cmd.cleanup() 15 | }) 16 | process.on('exit', function () { 17 | cmd.cleanup() 18 | }) 19 | -------------------------------------------------------------------------------- /test/cmd-collect-exit.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const path = require('path') 5 | const async = require('async') 6 | const { spawn } = require('child_process') 7 | const endpoint = require('endpoint') 8 | const CollectAndRead = require('./collect-and-read.js') 9 | 10 | testNotWindows('cmd - collect - external SIGINT is relayed', function (t) { 11 | const child = spawn( 12 | process.execPath, [ 13 | path.resolve(__dirname, 'cmd-collect-exit-sigint.script.js') 14 | ], { 15 | cwd: __dirname 16 | } 17 | ) 18 | 19 | child.stdout.once('data', () => child.kill('SIGINT')) 20 | 21 | async.parallel({ 22 | stdout (done) { child.stdout.pipe(endpoint(done)) }, 23 | stderr (done) { child.stderr.pipe(endpoint(done)) } 24 | }, function (err, output) { 25 | if (err) return t.error(err) 26 | 27 | // Expect the WARNING output to be shown 28 | t.ok(output.stderr.toString().split('\n').length, 1) 29 | t.equal(output.stdout.toString(), 30 | 'listening for SIGINT\nSIGINT received\n') 31 | t.end() 32 | }) 33 | }) 34 | 35 | test('cmd - collect - non-success exit code should not throw', function (t) { 36 | const cmd = new CollectAndRead({}, '--expose-gc', '-e', 'process.exit(1)') 37 | cmd.on('error', t.error.bind(t)) 38 | cmd.on('ready', function () { 39 | t.end() 40 | }) 41 | }) 42 | 43 | function testNotWindows (msg, fn) { 44 | if (process.platform !== 'win32') test(msg, fn) 45 | } 46 | -------------------------------------------------------------------------------- /test/cmd-collect-node-options-env.script.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const CollectAndRead = require(path.resolve('collect-and-read.js')) 5 | const cmd = new CollectAndRead({}, '-p', 'Error.stackTraceLimit') 6 | cmd.on('ready', function () { 7 | cmd.cleanup() 8 | }) 9 | -------------------------------------------------------------------------------- /test/cmd-collect-node-options-env.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const path = require('path') 5 | const async = require('async') 6 | const { spawn } = require('child_process') 7 | const endpoint = require('endpoint') 8 | 9 | test('cmd - collect - NODE_OPTIONS environment is not ignored', function (t) { 10 | const child = spawn( 11 | process.execPath, [ 12 | path.resolve(__dirname, 'cmd-collect-node-options-env.script.js') 13 | ], { 14 | env: Object.assign({}, process.env, { 15 | NODE_OPTIONS: '--no-warnings --stack-trace-limit=4013' 16 | }), 17 | cwd: __dirname 18 | } 19 | ) 20 | 21 | async.parallel({ 22 | stdout (done) { child.stdout.pipe(endpoint(done)) }, 23 | stderr (done) { child.stderr.pipe(endpoint(done)) } 24 | }, function (err, output) { 25 | if (err) return t.error(err) 26 | 27 | // Expect the WARNING output to be shown 28 | t.ok(output.stderr.toString().split('\n').length, 1) 29 | t.equal(output.stdout.toString().trim(), '4013') 30 | t.end() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/cmd-collect.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const async = require('async') 5 | const endpoint = require('endpoint') 6 | const semver = require('semver') 7 | const CollectAndRead = require('./collect-and-read.js') 8 | 9 | test('collect command produces data files with content', function (t) { 10 | const cmd = new CollectAndRead({}, '-e', 'setTimeout(() => {}, 200)') 11 | cmd.on('error', t.error.bind(t)) 12 | cmd.on('ready', function (systemInfoReader, stackTraceReader, traceEventReader) { 13 | async.parallel({ 14 | systemInfo (done) { 15 | // collect tracked asyncIds 16 | systemInfoReader 17 | .pipe(endpoint({ objectMode: true }, function (err, data) { 18 | if (err) return done(err) 19 | 20 | done(null, data[0]) 21 | })) 22 | }, 23 | 24 | stackTrace (done) { 25 | // collect tracked asyncIds 26 | stackTraceReader 27 | .pipe(endpoint({ objectMode: true }, function (err, data) { 28 | if (err) return done(err) 29 | 30 | const stackTraceMap = new Map() 31 | for (const stackTrace of data) { 32 | stackTraceMap.set(stackTrace.asyncId, stackTrace) 33 | } 34 | 35 | done(null, stackTraceMap) 36 | })) 37 | }, 38 | 39 | traceEvent (done) { 40 | // collect traceEvent for all asyncIds 41 | traceEventReader 42 | .pipe(endpoint({ objectMode: true }, function (err, data) { 43 | if (err) return done(err) 44 | 45 | const traceEventMap = new Map() 46 | for (const traceEvent of data) { 47 | if (!traceEventMap.has(traceEvent.asyncId)) { 48 | traceEventMap.set(traceEvent.asyncId, []) 49 | } 50 | traceEventMap.get(traceEvent.asyncId).push(traceEvent) 51 | } 52 | 53 | done(null, traceEventMap) 54 | })) 55 | } 56 | }, function (err, output) { 57 | if (err) return t.error(err) 58 | 59 | // filter untracked events out 60 | for (const asyncId of output.traceEvent.keys()) { 61 | if (!output.stackTrace.has(asyncId)) { 62 | output.traceEvent.delete(asyncId) 63 | } 64 | } 65 | 66 | // Expect all tracked asyncIds to be found in traceEvent 67 | t.strictSame( 68 | Array.from(output.stackTrace.keys()).sort(), 69 | Array.from(output.traceEvent.keys()).sort() 70 | ) 71 | 72 | // Get async operation types 73 | const asyncOperationTypes = [] 74 | for (const trackedTraceEvent of output.traceEvent.values()) { 75 | asyncOperationTypes.push(trackedTraceEvent[0].type) 76 | } 77 | 78 | let expected = ['Timeout'] 79 | if (semver.satisfies(process.version, '>= 12.16.0 < 12.17.0')) { 80 | // A `Promise.resolve()` call was added to bootstrap code in Node 12.16.x: https://github.com/nodejs/node/pull/30624 81 | // Node.js 12.17.0 does not appear to show this `resolve()` call in its trace event log. 82 | expected = ['PROMISE', 'Timeout'] 83 | } else if (semver.satisfies(process.version, '>= 15.0.0 < 18.0.0')) { 84 | // See: https://github.com/clinicjs/node-clinic-bubbleprof/pull/382#issuecomment-962766194 85 | expected = ['TickObject', 'Timeout'] 86 | } else if (semver.satisfies(process.version, '>= 18.0.0')) { 87 | expected = ['TickObject', 'TickObject', 'Timeout'] 88 | } 89 | 90 | t.strictSame(asyncOperationTypes.sort(), expected) 91 | 92 | t.end() 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /test/cmd-dest.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tap').test 2 | const fs = require('fs') 3 | const rimraf = require('rimraf') 4 | const ClinicBubbleprof = require('../index.js') 5 | 6 | test('cmd - test collect - custom output destination', (t) => { 7 | const tool = new ClinicBubbleprof({ debug: true, dest: 'test-output-destination' }) 8 | 9 | function cleanup (err, dirname) { 10 | t.error(err) 11 | t.match(dirname, /^test-output-destination[/\\][0-9]+\.clinic-bubbleprof$/) 12 | 13 | rimraf('test-output-destination', (err) => { 14 | t.error(err) 15 | t.end() 16 | }) 17 | } 18 | 19 | tool.collect( 20 | [process.execPath, '-e', 'setTimeout(() => {}, 200)'], 21 | function (err, dirname) { 22 | if (err) return cleanup(err, dirname) 23 | 24 | t.ok(fs.statSync(dirname).isDirectory()) 25 | 26 | tool.visualize(dirname, `${dirname}.html`, (err) => { 27 | if (err) return cleanup(err, dirname) 28 | 29 | t.ok(fs.statSync(`${dirname}.html`).isFile()) 30 | 31 | cleanup(null, dirname) 32 | }) 33 | } 34 | ) 35 | }) 36 | -------------------------------------------------------------------------------- /test/cmd-no-cluster.cluster.js: -------------------------------------------------------------------------------- 1 | const cluster = require('cluster') 2 | if (cluster.isMaster) { 3 | cluster.fork() 4 | } 5 | -------------------------------------------------------------------------------- /test/cmd-no-cluster.script.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const rimraf = require('rimraf') 3 | const ClinicBubbleprof = require('../index.js') 4 | 5 | const bubble = new ClinicBubbleprof({}) 6 | bubble.collect([ 7 | process.execPath, 8 | path.join(__dirname, 'cmd-no-cluster.cluster.js') 9 | ], (err, result) => { 10 | rimraf.sync(result) 11 | if (err) throw err 12 | }) 13 | -------------------------------------------------------------------------------- /test/cmd-no-cluster.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const { spawn } = require('child_process') 5 | const endpoint = require('endpoint') 6 | const rimraf = require('rimraf') 7 | const ClinicBubbleprof = require('../index.js') 8 | 9 | test('collect command stops when cluster is used', function (t) { 10 | t.plan(3) 11 | 12 | const bubble = new ClinicBubbleprof({}) 13 | bubble.collect([process.execPath, '-e', 'require("cluster")'], (err, result) => { 14 | t.error(err, 'should not crash when cluster is required but not used') 15 | rimraf.sync(result) 16 | }) 17 | 18 | const proc = spawn(process.execPath, [ 19 | require.resolve('./cmd-no-cluster.script.js') 20 | ], { stdio: 'pipe' }) 21 | 22 | proc.stderr.pipe(endpoint((err, buf) => { 23 | t.error(err) 24 | t.ok(buf.toString('utf8').includes('does not support clustering'), 'should crash once cluster is used') 25 | })) 26 | }) 27 | -------------------------------------------------------------------------------- /test/cmd-visualize.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const test = require('tap').test 5 | const async = require('async') 6 | const rimraf = require('rimraf') 7 | const ClinicBubbleprof = require('../index.js') 8 | 9 | test('cmd - test visualization', function (t) { 10 | const tool = new ClinicBubbleprof() 11 | 12 | function cleanup (err, dirname) { 13 | t.error(err) 14 | 15 | async.parallel([ 16 | (done) => rimraf(dirname, done), 17 | (done) => fs.unlink(dirname + '.html', done) 18 | ], function (err) { 19 | t.error(err) 20 | t.end() 21 | }) 22 | } 23 | 24 | tool.collect( 25 | [process.execPath, '-e', 'setTimeout(() => {}, 200)'], 26 | function (err, dirname) { 27 | if (err) return cleanup(err, dirname) 28 | 29 | tool.visualize(dirname, dirname + '.html', function (err) { 30 | if (err) return cleanup(err, dirname) 31 | 32 | fs.readFile(dirname + '.html', function (err, content) { 33 | if (err) return cleanup(err, dirname) 34 | 35 | t.ok(content.length > 1024) 36 | cleanup(null, dirname) 37 | }) 38 | }) 39 | } 40 | ) 41 | }) 42 | -------------------------------------------------------------------------------- /test/collect-and-read.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const http = require('http') 5 | const path = require('path') 6 | const async = require('async') 7 | const rimraf = require('rimraf') 8 | const events = require('events') 9 | const endpoint = require('endpoint') 10 | const xsock = require('cross-platform-sock') 11 | const getLoggingPaths = require('@clinic/clinic-common').getLoggingPaths('bubbleprof') 12 | const ClinicBubbleprof = require('../index.js') 13 | const SystemInfoDecoder = require('../format/system-info-decoder.js') 14 | const StackTraceDecoder = require('../format/stack-trace-decoder.js') 15 | const TraceEventDecoder = require('../format/trace-event-decoder.js') 16 | 17 | const sock = xsock(path.join(__dirname, 'test-server.sock')) 18 | 19 | function waitForFile (filepath, timeout, callback) { 20 | if (process.platform === 'win32') return setTimeout(callback, timeout) 21 | 22 | fs.access(filepath, function (err) { 23 | if (!err) return callback(null) 24 | if (timeout <= 0) { 25 | return callback(new Error('server did not listen within timeout')) 26 | } 27 | 28 | setTimeout(function () { 29 | waitForFile(filepath, timeout - 50, callback) 30 | }, Math.min(timeout, 50)) 31 | }) 32 | } 33 | 34 | class CollectAndRead extends events.EventEmitter { 35 | constructor (options, ...args) { 36 | super() 37 | const self = this 38 | const tool = this.tool = new ClinicBubbleprof(options) 39 | 40 | xsock.unlink(sock, function (err) { 41 | if (err && err.code !== 'ENOENT') return self.emit('error', err) 42 | 43 | tool.collect([process.execPath, ...args], function (err, dirname) { 44 | self.files = getLoggingPaths({ path: dirname }) 45 | 46 | if (err) return self.emit('error', err) 47 | 48 | const systeminfo = fs.createReadStream(self.files['/systeminfo']) 49 | .pipe(new SystemInfoDecoder()) 50 | const stacktrace = fs.createReadStream(self.files['/stacktrace']) 51 | .pipe(new StackTraceDecoder()) 52 | const traceevent = fs.createReadStream(self.files['/traceevent']) 53 | .pipe(new TraceEventDecoder()) 54 | 55 | self._setupAutoCleanup(systeminfo, stacktrace, traceevent) 56 | self.emit('ready', systeminfo, stacktrace, traceevent) 57 | }) 58 | }) 59 | } 60 | 61 | request (href, callback) { 62 | waitForFile(sock, 1000, function (err) { 63 | if (err) return callback(err) 64 | http.get({ 65 | socketPath: sock, 66 | path: href 67 | }, function (res) { 68 | res.pipe(endpoint(callback)) 69 | }) 70 | }) 71 | } 72 | 73 | cleanup () { 74 | rimraf.sync(this.files['/']) 75 | } 76 | 77 | _setupAutoCleanup (systeminfo, stacktrace, traceevent) { 78 | const self = this 79 | 80 | async.parallel({ 81 | systemInfo (done) { 82 | systeminfo.once('end', function () { 83 | fs.unlink(self.files['/systeminfo'], function (err) { 84 | if (err) return done(err) 85 | done(null) 86 | }) 87 | }) 88 | }, 89 | stackTraces (done) { 90 | stacktrace.once('end', function () { 91 | fs.unlink(self.files['/stacktrace'], function (err) { 92 | if (err) return done(err) 93 | done(null) 94 | }) 95 | }) 96 | }, 97 | traveEvents (done) { 98 | traceevent.once('end', function () { 99 | fs.unlink(self.files['/traceevent'], function (err) { 100 | if (err) return done(err) 101 | done(null) 102 | }) 103 | }) 104 | } 105 | }, function (err, output) { 106 | if (err) return self.emit('error', err) 107 | 108 | fs.rmdir(self.files['/'], function (err) { 109 | if (err) return self.emit('error', err) 110 | }) 111 | }) 112 | } 113 | } 114 | 115 | module.exports = CollectAndRead 116 | -------------------------------------------------------------------------------- /test/collect-get-logging-paths.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const path = require('path') 5 | const getLoggingPaths = require('@clinic/clinic-common').getLoggingPaths('bubbleprof') 6 | 7 | test('Collect - logging path - identifier', function (t) { 8 | const paths = getLoggingPaths({ identifier: 1062 }) 9 | 10 | t.strictSame(paths, { 11 | '/': '1062.clinic-bubbleprof', 12 | '/systeminfo': path.normalize('1062.clinic-bubbleprof/1062.clinic-bubbleprof-systeminfo'), 13 | '/stacktrace': path.normalize('1062.clinic-bubbleprof/1062.clinic-bubbleprof-stacktrace'), 14 | '/traceevent': path.normalize('1062.clinic-bubbleprof/1062.clinic-bubbleprof-traceevent') 15 | }) 16 | t.end() 17 | }) 18 | 19 | test('Collect - logging path - path', function (t) { 20 | const paths = getLoggingPaths({ path: path.normalize('/root/1062.clinic-bubbleprof') }) 21 | 22 | t.strictSame(paths, { 23 | '/': path.normalize('/root/1062.clinic-bubbleprof'), 24 | '/systeminfo': path.normalize('/root/1062.clinic-bubbleprof/1062.clinic-bubbleprof-systeminfo'), 25 | '/stacktrace': path.normalize('/root/1062.clinic-bubbleprof/1062.clinic-bubbleprof-stacktrace'), 26 | '/traceevent': path.normalize('/root/1062.clinic-bubbleprof/1062.clinic-bubbleprof-traceevent') 27 | }) 28 | t.end() 29 | }) 30 | 31 | test('Collect - logging path - bad type', function (t) { 32 | t.throws( 33 | () => getLoggingPaths({}), 34 | new Error('missing either identifier or path value') 35 | ) 36 | t.end() 37 | }) 38 | -------------------------------------------------------------------------------- /test/fixtures-wasm/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules 3 | -------------------------------------------------------------------------------- /test/fixtures-wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fixtures-wasm", 3 | "scripts": { 4 | "build": "asc say-hello.ts --binaryFile ./say-hello.wasm --runtime stub" 5 | }, 6 | "devDependencies": { 7 | "assemblyscript": "^0.19.20" 8 | }, 9 | "private": true 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures-wasm/say-hello.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | trace('hello') 3 | -------------------------------------------------------------------------------- /test/fixtures-wasm/say-hello.wasm: -------------------------------------------------------------------------------- 1 | asm``||||| envtracep 2 | memory A 3 | <5A ADDDDD  &A  4 | hello -------------------------------------------------------------------------------- /test/format-stack-trace.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const stackTrace = require('../collect/stack-trace.js') 5 | const StackTraceDecoder = require('../format/stack-trace-decoder.js') 6 | const StackTraceEncoder = require('../format/stack-trace-encoder.js') 7 | 8 | function produceExample (asyncId) { 9 | // produce examples where the number of frames depends on the id. 10 | // This is to ensure the message length isn't the same for all messages. 11 | return (function recursive (left) { 12 | if (left === 0) { 13 | return { 14 | asyncId: asyncId, 15 | frames: stackTrace().map((frame) => Object.assign({}, frame)) 16 | } 17 | } else { 18 | return recursive(left - 1) 19 | } 20 | })(asyncId) 21 | } 22 | 23 | test('format - stack trace - basic encoder-decoder works', function (t) { 24 | t.plan(2) 25 | const encoder = new StackTraceEncoder() 26 | const decoder = new StackTraceDecoder() 27 | encoder.pipe(decoder) 28 | 29 | const output = [] 30 | decoder.on('data', (example) => output.push(example)) 31 | 32 | const input = [] 33 | for (let i = 0; i < 2; i++) { 34 | const example = produceExample(i) 35 | encoder.write(example) 36 | input.push(example) 37 | } 38 | 39 | decoder.once('end', function () { 40 | t.strictSame(input, output) 41 | }) 42 | 43 | encoder.on('end', () => { 44 | t.pass('close emitted') 45 | }) 46 | 47 | encoder.end() 48 | }) 49 | 50 | test('format - stack trace - partial decoding', function (t) { 51 | const encoder = new StackTraceEncoder() 52 | const decoder = new StackTraceDecoder() 53 | 54 | // encode a sample 55 | const example1 = produceExample(1) 56 | encoder.write(example1) 57 | const example1Encoded = encoder.read() 58 | 59 | const example2 = produceExample(2) 60 | encoder.write(example2) 61 | const example2Encoded = encoder.read() 62 | 63 | const example3 = produceExample(3) 64 | encoder.write(example3) 65 | const example3Encoded = encoder.read() 66 | 67 | const example4 = produceExample(4) 68 | encoder.write(example4) 69 | const example4Encoded = encoder.read() 70 | 71 | const example5 = produceExample(5) 72 | encoder.write(example5) 73 | const example5Encoded = encoder.read() 74 | 75 | // partial message length 76 | decoder.write(example1Encoded.slice(0, 1)) 77 | t.equal(decoder.read(), null) 78 | 79 | // message length complete, partial message 80 | decoder.write(example1Encoded.slice(1, 20)) 81 | t.equal(decoder.read(), null) 82 | 83 | // message complete, next message incomplete 84 | decoder.write(Buffer.concat([ 85 | example1Encoded.slice(20), 86 | example2Encoded.slice(0, 30) 87 | ])) 88 | t.strictSame(decoder.read(), example1) 89 | t.equal(decoder.read(), null) 90 | 91 | // ended previuse sample, but a partial remains 92 | decoder.write(Buffer.concat([ 93 | example2Encoded.slice(30), 94 | example3Encoded.slice(0, 40) 95 | ])) 96 | t.strictSame(decoder.read(), example2) 97 | t.equal(decoder.read(), null) 98 | 99 | // Ended previuse, no partial remains 100 | decoder.write(Buffer.concat([ 101 | example3Encoded.slice(40), 102 | example4Encoded 103 | ])) 104 | t.strictSame(decoder.read(), example3) 105 | t.strictSame(decoder.read(), example4) 106 | t.equal(decoder.read(), null) 107 | 108 | // No previuse ended 109 | decoder.write(example5Encoded) 110 | t.strictSame(decoder.read(), example5) 111 | 112 | // No more data 113 | t.equal(decoder.read(), null) 114 | t.end() 115 | }) 116 | -------------------------------------------------------------------------------- /test/format-trace-events.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const endpoint = require('endpoint') 5 | const TraceEventDecoder = require('../format/trace-event-decoder.js') 6 | 7 | test('format - trace event - decoder', function (t) { 8 | const init = { 9 | pid: process.pid, 10 | ts: 1000, 11 | ph: 'b', 12 | cat: 'node.async_hooks', 13 | name: 'TYPENAME', 14 | id: '0x2', 15 | args: { 16 | triggerAsyncId: 1, 17 | executionAsyncId: 0 18 | } 19 | } 20 | 21 | const before = { 22 | pid: process.pid, 23 | ts: 2000, 24 | ph: 'b', 25 | cat: 'node.async_hooks', 26 | name: 'TYPENAME_CALLBACK', 27 | id: '0x2', 28 | args: {} 29 | } 30 | 31 | const after = { 32 | pid: process.pid, 33 | ts: 3000, 34 | ph: 'e', 35 | cat: 'node.async_hooks', 36 | name: 'TYPENAME_CALLBACK', 37 | id: '0x2', 38 | args: {} 39 | } 40 | 41 | const destroy = { 42 | pid: process.pid, 43 | ts: 4000, 44 | ph: 'e', 45 | cat: 'node.async_hooks', 46 | name: 'TYPENAME', 47 | id: '0x2', 48 | args: {} 49 | } 50 | 51 | const decoder = new TraceEventDecoder() 52 | decoder.end(JSON.stringify({ 53 | traceEvents: [init, before, after, destroy] 54 | })) 55 | 56 | decoder.pipe(endpoint({ objectMode: true }, function (err, data) { 57 | if (err) return t.error(err) 58 | 59 | // Remove prototype constructor 60 | const traceEvent = data.map((v) => Object.assign({}, v)) 61 | 62 | t.strictSame(traceEvent, [{ 63 | event: 'init', 64 | type: 'TYPENAME', 65 | asyncId: 2, 66 | triggerAsyncId: 1, 67 | executionAsyncId: 0, 68 | timestamp: 1 69 | }, { 70 | event: 'before', 71 | type: 'TYPENAME', 72 | asyncId: 2, 73 | triggerAsyncId: null, 74 | executionAsyncId: null, 75 | timestamp: 2 76 | }, { 77 | event: 'after', 78 | type: 'TYPENAME', 79 | asyncId: 2, 80 | triggerAsyncId: null, 81 | executionAsyncId: null, 82 | timestamp: 3 83 | }, { 84 | event: 'destroy', 85 | type: 'TYPENAME', 86 | asyncId: 2, 87 | triggerAsyncId: null, 88 | executionAsyncId: null, 89 | timestamp: 4 90 | }]) 91 | 92 | t.end() 93 | })) 94 | }) 95 | -------------------------------------------------------------------------------- /test/integration-servers.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const path = require('path') 5 | const async = require('async') 6 | const semver = require('semver') 7 | const endpoint = require('endpoint') 8 | const CollectAndRead = require('./collect-and-read.js') 9 | const analysis = require('../analysis/index.js') 10 | 11 | const skipHTTPPARSER = semver.gte(process.version, '12.0.0') 12 | ? 'Node 12 uses a new http parser that does not generate HTTPPARSER aggregate nodes' 13 | : false 14 | 15 | function runServer (name, callback) { 16 | const serverPath = path.resolve(__dirname, 'servers', name + '.js') 17 | const cmd = new CollectAndRead({}, serverPath) 18 | 19 | // make two requests 20 | async.map( 21 | [0, 1], 22 | function makeRequest (requestId, done) { 23 | cmd.request('/', done) 24 | }, 25 | function (err) { 26 | if (err) return callback(err) 27 | } 28 | ) 29 | 30 | // await result 31 | cmd.on('error', callback) 32 | cmd.on('ready', function (systemInfoReader, stackTraceReader, traceEventReader) { 33 | analysis(systemInfoReader, stackTraceReader, traceEventReader) 34 | .pipe(endpoint({ objectMode: true }, callback)) 35 | }) 36 | } 37 | 38 | test('basic server aggregates HTTPPARSER', { skip: skipHTTPPARSER }, function (t) { 39 | runServer('basic', function (err, nodes) { 40 | if (err) return t.error(err) 41 | 42 | // Get AggregateNodes from BarrierNodes 43 | const aggregateNodes = [].concat( 44 | ...nodes.map((barrierNode) => barrierNode.nodes) 45 | ) 46 | 47 | const httpParserNodes = aggregateNodes.filter(function (aggregateNode) { 48 | return aggregateNode.sources[0].type === 'HTTPPARSER' 49 | }) 50 | 51 | // HTTPPARSER can have different stacks, because it was either new or 52 | // a cached HTTPPARSER. Check that these two cases are aggregated into 53 | // one node. 54 | t.equal(httpParserNodes.length, 1) 55 | t.end() 56 | }) 57 | }) 58 | 59 | test('latency server has http.connection.end cluster', { skip: skipHTTPPARSER }, function (t) { 60 | runServer('latency', function (err, nodes) { 61 | if (err) return t.error(err) 62 | 63 | const endName = nodes.some(c => c.name.includes('http.connection.end')) 64 | t.ok(endName, 'has http.connection.end name') 65 | t.end() 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /test/integration-timeout.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const semver = require('semver') 5 | const endpoint = require('endpoint') 6 | const CollectAndRead = require('./collect-and-read.js') 7 | const analysis = require('../analysis/index.js') 8 | 9 | test('collect-analysis pipeline', function (t) { 10 | const cmd = new CollectAndRead({}, '-e', 'setTimeout(() => {}, 200)') 11 | cmd.on('error', t.error.bind(t)) 12 | cmd.on('ready', function (systemInfoReader, stackTraceReader, traceEventReader) { 13 | analysis(systemInfoReader, stackTraceReader, traceEventReader) 14 | .pipe(endpoint({ objectMode: true }, function (err, nodes) { 15 | if (err) return t.error(err) 16 | 17 | // Get AggregateNodes from BarrierNodes 18 | const aggregateNodes = [].concat( 19 | ...nodes.map((barrierNode) => barrierNode.nodes) 20 | ) 21 | 22 | const nodeMap = new Map( 23 | aggregateNodes.map((node) => [node.aggregateId, node]) 24 | ) 25 | if (semver.satisfies(process.version, '>= 12.16.0 < 12.17.0')) { 26 | t.equal(nodes.length, 3) 27 | t.equal(nodeMap.size, 3) 28 | // aggregateId = 1 is the root and points to the Timeout and a PROMISE from Node.js's initialization code 29 | t.strictSame(nodeMap.get(1).children, [2, 3]) 30 | } else { 31 | t.equal(nodes.length, 2) 32 | t.equal(nodeMap.size, 2) 33 | // aggregateId = 1 is the root and points to the Timeout 34 | t.strictSame(nodeMap.get(1).children, [2]) 35 | } 36 | 37 | // aggregateId = 2 is the Timeout 38 | t.equal(nodeMap.get(2).sources[0].type, 'Timeout') 39 | 40 | t.end() 41 | })) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/node_modules/fake-data-fetch/fetch-data.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function fetchData (fakeContent, callback) { 4 | setTimeout(function () { 5 | callback(null, fakeContent) 6 | }, 100) 7 | } 8 | -------------------------------------------------------------------------------- /test/node_modules/fake-data-fetch/find-db.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function findDB (fakeContent, callback) { 4 | setTimeout(function () { 5 | callback(null, fakeContent) 6 | }, 100) 7 | } 8 | -------------------------------------------------------------------------------- /test/node_modules/fake-data-fetch/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const findDB = require('./find-db.js') 3 | const fetchData = require('./fetch-data.js') 4 | 5 | module.exports = function fakeDataFetch (fakeContent, callback) { 6 | findDB(fakeContent, function (err, db) { 7 | if (err) return callback(err, null) 8 | fetchData(db, function (err, output) { 9 | callback(err, output) 10 | }) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /test/servers/basic.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const http = require('http') 6 | const xsock = require('cross-platform-sock') 7 | 8 | const sock = xsock(path.join(__dirname, '../test-server.sock')) 9 | 10 | let connections = 0 11 | const server = http.createServer(function (req, res) { 12 | res.end('almost empty') 13 | if (++connections === 2) { 14 | server.close() 15 | } 16 | }) 17 | 18 | try { 19 | fs.unlinkSync(sock) 20 | } catch (err) { 21 | if (err.code !== 'ENOENT') { 22 | console.error('could not unlink test-server.sock:', err.stack) 23 | } 24 | } 25 | server.listen(sock) 26 | -------------------------------------------------------------------------------- /test/servers/express.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const express = require('express') 6 | const xsock = require('cross-platform-sock') 7 | 8 | const sock = xsock(path.join(__dirname, '../test-server.sock')) 9 | const app = express() 10 | 11 | try { 12 | fs.unlinkSync(sock) 13 | } catch (err) { 14 | if (err.code !== 'ENOENT') { 15 | console.error('could not unlink test-server.sock:', err.stack) 16 | } 17 | } 18 | const server = app.listen(sock) 19 | 20 | let connections = 0 21 | app.use(express.urlencoded({ extended: true })) 22 | app.get('/', function (req, res) { 23 | res.end('almost empty') 24 | if (++connections === 2) { 25 | server.close() 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /test/servers/external.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const http = require('http') 5 | const path = require('path') 6 | const async = require('async') 7 | const fakeDataFetch = require('fake-data-fetch') 8 | const xsock = require('cross-platform-sock') 9 | 10 | const sock = xsock(path.join(__dirname, '../test-server.sock')) 11 | 12 | let connections = 0 13 | const server = http.createServer(function (req, res) { 14 | async.parallel({ 15 | db1 (done) { fakeDataFetch('db1', done) }, 16 | db2 (done) { fakeDataFetch('db2', done) } 17 | }, function (err, result) { 18 | if (err) throw err 19 | res.end(JSON.stringify(result)) 20 | }) 21 | 22 | if (++connections === 2) { 23 | server.close() 24 | } 25 | }) 26 | 27 | try { 28 | fs.unlinkSync(sock) 29 | } catch (err) { 30 | if (err.code !== 'ENOENT') { 31 | console.error('could not unlink test-server.sock:', err.stack) 32 | } 33 | } 34 | server.listen(sock) 35 | -------------------------------------------------------------------------------- /test/servers/latency.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const http = require('http') 6 | const xsock = require('cross-platform-sock') 7 | 8 | const sock = xsock(path.join(__dirname, '../test-server.sock')) 9 | 10 | let connections = 0 11 | const server = http.createServer(function (req, res) { 12 | setTimeout(function () { 13 | res.end('almost empty') 14 | }, 10) 15 | if (++connections === 2) { 16 | server.close() 17 | } 18 | }) 19 | 20 | try { 21 | fs.unlinkSync(sock) 22 | } catch (err) { 23 | if (err.code !== 'ENOENT') { 24 | console.error('could not unlink test-server.sock:', err.stack) 25 | } 26 | } 27 | server.listen(sock) 28 | -------------------------------------------------------------------------------- /test/servers/quine.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const http = require('http') 6 | const xsock = require('cross-platform-sock') 7 | 8 | const sock = xsock(path.join(__dirname, '../test-server.sock')) 9 | 10 | let connections = 0 11 | const server = http.createServer(function request (req, res) { 12 | fs.readFile(__filename, function read (err, content) { 13 | if (err) throw err 14 | res.end(content) 15 | 16 | if (++connections === 2) { 17 | server.close() 18 | } 19 | }) 20 | }) 21 | 22 | try { 23 | fs.unlinkSync(sock) 24 | } catch (err) { 25 | if (err.code !== 'ENOENT') { 26 | console.error('could not unlink test-server.sock:', err.stack) 27 | } 28 | } 29 | server.listen(sock) 30 | -------------------------------------------------------------------------------- /test/visualizer-data-dataset.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tap').test 2 | const loadData = require('../visualizer/data/index.js') 3 | const fakeJson = require('./visualizer-util/fakedata.json') 4 | const DataSet = require('../visualizer/data/dataset.js') 5 | const acmeairJson = require('./visualizer-util/sampledata-acmeair.json') 6 | 7 | function round4dp (num) { 8 | return typeof num === 'number' ? Number(num.toFixed(4)) : num 9 | } 10 | 11 | test('Visualizer dataset - fake json', function (t) { 12 | const dataSet = loadData({ debugMode: true }, fakeJson) 13 | 14 | t.equal(dataSet.clusterNodes.size, 2) 15 | t.equal(dataSet.aggregateNodes.size, 2) 16 | t.equal(dataSet.sourceNodes.length, 2) 17 | 18 | t.end() 19 | }) 20 | 21 | test('Visualizer data - DataSet - empty data file', function (t) { 22 | t.throws(() => { 23 | loadData() 24 | }, new Error('No valid data found, data.json is typeof object')) 25 | 26 | t.end() 27 | }) 28 | 29 | test('Visualizer data - DataSet - invalid getByNodeType', function (t) { 30 | t.throws(() => { 31 | const dataSet = loadData({ debugMode: true }, fakeJson) 32 | dataSet.getByNodeType('InvalidNode', 0) 33 | }, new Error('Invalid key "InvalidNode" (typeof string) passed, valid keys are: AggregateNode, ClusterNode')) 34 | 35 | t.end() 36 | }) 37 | 38 | test('Visualizer data - DataSet - access invalid node id', function (t) { 39 | const dataSet = loadData({ debugMode: true }, fakeJson) 40 | 41 | t.equal(dataSet.getByNodeType('ClusterNode', 'foo'), undefined) 42 | 43 | t.end() 44 | }) 45 | 46 | test('Visualizer dataset - wallTime from real sample data', function (t) { 47 | const dataSet = new DataSet(acmeairJson, { wallTimeSlices: 100 }) 48 | 49 | // Ensure stats calculated from real profile data subset don't changed from unexpected future feature side effects 50 | t.equal(dataSet.wallTime.profileStart, 6783474.641) 51 | t.equal(dataSet.wallTime.profileEnd, 6786498.31) 52 | dataSet.processData() 53 | t.equal(round4dp(dataSet.wallTime.profileDuration), 3023.6690) 54 | t.equal(round4dp(dataSet.wallTime.msPerSlice), 30.2367) 55 | 56 | // todo - unify variable and class names with UI labels 57 | const typeDecimals = [ 58 | dataSet.getDecimal('typeCategory', 'networks'), 59 | dataSet.getDecimal('typeCategory', 'files-streams'), 60 | dataSet.getDecimal('typeCategory', 'crypto'), 61 | dataSet.getDecimal('typeCategory', 'timing-promises'), 62 | dataSet.getDecimal('typeCategory', 'other') 63 | ] 64 | t.equal(round4dp(typeDecimals[0]), 0.8461) 65 | t.equal(round4dp(typeDecimals[1]), 0.0017) 66 | t.equal(round4dp(typeDecimals[2]), 0) 67 | t.equal(round4dp(typeDecimals[3]), 0.1521) 68 | t.equal(round4dp(typeDecimals[4]), 0) 69 | t.equal(round4dp(typeDecimals.reduce((accum, num) => accum + num, 0)), 1) 70 | t.equal(dataSet.decimals.typeCategory.size, 5) 71 | 72 | const partyDecimals = [ 73 | dataSet.getDecimal('party', 'user'), 74 | dataSet.getDecimal('party', 'external'), 75 | dataSet.getDecimal('party', 'nodecore'), 76 | dataSet.getDecimal('party', 'root') 77 | ] 78 | t.equal(round4dp(partyDecimals[0]), 0.2968) 79 | t.equal(round4dp(partyDecimals[1]), 0.6829) 80 | t.equal(round4dp(partyDecimals[2]), 0.0203) 81 | t.equal(round4dp(partyDecimals[3]), 0) 82 | t.equal(round4dp(partyDecimals.reduce((accum, num) => accum + num, 0)), 1) 83 | t.equal(dataSet.decimals.party.size, 4) 84 | 85 | t.equal(dataSet.getDecimal('typeCategory', 'some-key-not-in-map'), null) 86 | t.equal(dataSet.getDecimal('party', 'some-key-not-in-map'), null) 87 | 88 | t.end() 89 | }) 90 | 91 | test('Visualizer data - invalid calls to dataSet.wallTime.getSegments', function (t) { 92 | const { wallTime } = loadData({ debugMode: true }, acmeairJson) 93 | 94 | t.throws(() => { 95 | wallTime.getSegments(6782000, 6786000) 96 | }, new Error('Wall time segment start time (6782000) precedes profile start time (6783474.641)')) 97 | 98 | t.throws(() => { 99 | wallTime.getSegments(6786000, 6789000) 100 | }, new Error('Wall time segment end time (6789000) exceeds profile end time (6786498.31)')) 101 | 102 | t.throws(() => { 103 | wallTime.getSegments(6787000, 6786000) 104 | }, new Error('Wall time segment start time (6787000) doesn’t precede segment end time (6786000)')) 105 | 106 | t.end() 107 | }) 108 | -------------------------------------------------------------------------------- /test/visualizer-layout-connections.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const loadData = require('../visualizer/data/index.js') 5 | const generateLayout = require('../visualizer/layout/index.js') 6 | const { isNumber } = require('../visualizer/validation.js') 7 | const slowioJson = require('./visualizer-util/sampledata-slowio.json') 8 | const Connection = require('../visualizer/layout/connections.js') 9 | 10 | const fakeScale = { 11 | settings: { 12 | labelMinimumSpace: 5, 13 | lineWidth: 3 14 | }, 15 | getCircleRadius: (x) => x * 2, 16 | getLineLength: (x) => x * 3 17 | } 18 | 19 | test('Visualizer layout - scale - calculates visible circle radius based on within of the node and the scale', function (t) { 20 | const dataSet = loadData({ debugMode: true }, slowioJson) 21 | const layout = generateLayout(dataSet) 22 | 23 | const parentLayoutNode = layout.layoutNodes.get(1) 24 | const childLayoutNode = layout.layoutNodes.get(3) 25 | 26 | const connection = new Connection(parentLayoutNode, childLayoutNode, fakeScale) 27 | 28 | t.ok(isNumber(parentLayoutNode.getWithinTime())) 29 | const expectedParentRadius = fakeScale.getCircleRadius(parentLayoutNode.getWithinTime()) 30 | t.equal(connection.getOriginRadius(), expectedParentRadius) 31 | 32 | t.ok(isNumber(childLayoutNode.getWithinTime())) 33 | const expectedChildRadius = fakeScale.getCircleRadius(childLayoutNode.getWithinTime()) 34 | t.equal(connection.getTargetRadius(), expectedChildRadius) 35 | 36 | t.end() 37 | }) 38 | 39 | test('Visualizer layout - scale - calculates visible line length based on between of the child node and the scale', function (t) { 40 | const dataSet = loadData({ debugMode: true }, slowioJson) 41 | const layout = generateLayout(dataSet) 42 | 43 | const parentLayoutNode = layout.layoutNodes.get(1) 44 | const childLayoutNode = layout.layoutNodes.get(3) 45 | 46 | const connection = new Connection(parentLayoutNode, childLayoutNode, fakeScale) 47 | t.ok(isNumber(childLayoutNode.getBetweenTime())) 48 | const expectedVisibleLength = fakeScale.getLineLength(childLayoutNode.getBetweenTime()) 49 | t.equal(connection.getVisibleLineLength(), expectedVisibleLength) 50 | 51 | t.end() 52 | }) 53 | 54 | test('Visualizer layout - scale - calculates distance between centers', function (t) { 55 | const dataSet = loadData({ debugMode: true }, slowioJson) 56 | const layout = generateLayout(dataSet) 57 | 58 | const parentLayoutNode = layout.layoutNodes.get(1) 59 | const childLayoutNode = layout.layoutNodes.get(3) 60 | 61 | const connection = new Connection(parentLayoutNode, childLayoutNode, fakeScale) 62 | 63 | const expectedParentRadius = fakeScale.getCircleRadius(parentLayoutNode.getWithinTime()) 64 | const expectedChildRadius = fakeScale.getCircleRadius(childLayoutNode.getWithinTime()) 65 | const expectedVisibleLength = fakeScale.getLineLength(childLayoutNode.getBetweenTime()) 66 | 67 | const expectedDistance = expectedParentRadius + 68 | expectedChildRadius + 69 | expectedVisibleLength + 70 | (fakeScale.settings.labelMinimumSpace * 2) + 71 | fakeScale.settings.lineWidth 72 | t.equal(connection.getDistanceBetweenCenters(), expectedDistance) 73 | 74 | t.end() 75 | }) 76 | -------------------------------------------------------------------------------- /test/visualizer-layout-stems.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tap').test 4 | const loadData = require('../visualizer/data/index.js') 5 | const slowioJson = require('./visualizer-util/sampledata-slowio.json') 6 | const generateLayout = require('../visualizer/layout/index.js') 7 | 8 | const { mockTopology } = require('./visualizer-util/fake-topology.js') 9 | 10 | test('Visualizer layout - stems - calculates between and diameter based on stats', function (t) { 11 | const dataSet = loadData({ debugMode: true }, slowioJson) 12 | const layout = generateLayout(dataSet) 13 | 14 | const layoutNode = layout.layoutNodes.get(16) 15 | const stem = layoutNode.stem 16 | t.equal(stem.raw.ownBetween, layoutNode.node.getBetweenTime()) 17 | t.equal(stem.raw.ownDiameter, layoutNode.node.getWithinTime() / Math.PI) 18 | 19 | t.end() 20 | }) 21 | 22 | test('Visualizer layout - stems - calculates length based on ancestors and scale', function (t) { 23 | const dataSet = loadData({ debugMode: true }, slowioJson) 24 | const layout = generateLayout(dataSet, { labelMinimumSpace: 2, lineWidth: 3 }) 25 | 26 | const stem = layout.layoutNodes.get(16).stem 27 | const totalStemLength = stem.lengths 28 | t.same(stem.ancestors.ids, [1, 5, 7, 8, 10]) 29 | t.equal(totalStemLength.scalable.toFixed(8), '21897.14445863') 30 | t.equal(totalStemLength.absolute, (2 * 2 * 5) + (3 * 5)) 31 | t.equal(totalStemLength.rawTotal, totalStemLength.scalable + totalStemLength.absolute) 32 | 33 | const toOwnLength = id => { 34 | const ancestorStem = layout.layoutNodes.get(id).stem 35 | return ancestorStem.raw.ownBetween + ancestorStem.raw.ownDiameter 36 | } 37 | const sum = (a, b) => a + b 38 | const totalAncestorsLength = stem.ancestors.ids.map(toOwnLength).reduce(sum, 0) 39 | // Floating point precision acting up here, hence `.toFixed()` both sides 40 | t.equal((totalStemLength.scalable - totalAncestorsLength).toFixed(8), (stem.raw.ownBetween + stem.raw.ownDiameter).toFixed(8)) 41 | 42 | t.end() 43 | }) 44 | 45 | test('Visualizer layout - stems - identifies leaves', function (t) { 46 | const topology = [ 47 | ['1.9', 1], 48 | ['1.2.3.4', 1], 49 | ['1.2.3.5', 1], 50 | ['1.2.6.7', 1], 51 | ['1.2.8', 1] 52 | ] 53 | 54 | const dataSet = loadData({ debugMode: true }, mockTopology(topology)) 55 | const layout = generateLayout(dataSet, { labelMinimumSpace: 0, lineWidth: 0 }) 56 | 57 | t.same(layout.layoutNodes.get(1).stem.leaves.ids, [4, 5, 7, 8, 9]) 58 | t.same(layout.layoutNodes.get(9).stem.leaves.ids, []) 59 | t.same(layout.layoutNodes.get(2).stem.leaves.ids, [4, 5, 7, 8]) 60 | t.same(layout.layoutNodes.get(3).stem.leaves.ids, [4, 5]) 61 | t.same(layout.layoutNodes.get(6).stem.leaves.ids, [7]) 62 | t.same(layout.layoutNodes.get(4).stem.leaves.ids, []) 63 | t.same(layout.layoutNodes.get(5).stem.leaves.ids, []) 64 | t.same(layout.layoutNodes.get(7).stem.leaves.ids, []) 65 | t.same(layout.layoutNodes.get(8).stem.leaves.ids, []) 66 | 67 | t.end() 68 | }) 69 | -------------------------------------------------------------------------------- /test/visualizer-util/fake-topology.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function mockClusterNode ({ id, parentId, children, stemLength }) { 4 | children.sort((a, b) => a - b) 5 | const clusterNode = { 6 | // ClusterNode 7 | clusterId: id, 8 | parentClusterId: parentId, 9 | mark: new Map([['party', 'user']]), 10 | nodes: [ 11 | { 12 | // AggregateNode 13 | aggregateId: id, 14 | parentAggregateId: parentId, 15 | children, 16 | frames: [], 17 | sources: [ 18 | { 19 | // SourceNode 20 | asyncId: id, 21 | i: id, 22 | init: 0, 23 | before: [stemLength], 24 | after: [stemLength], 25 | aggregateId: id, 26 | clusterId: id 27 | } 28 | ] 29 | } 30 | ], 31 | children 32 | } 33 | if (parentId === 0) { 34 | clusterNode.isRoot = true 35 | } 36 | return clusterNode 37 | } 38 | 39 | function mockTopology (topology) { 40 | const clusterNodes = new Map() 41 | for (const instruction of topology) { 42 | const ids = instruction[0].split('.').map(id => parseInt(id)) 43 | const totalStemLength = instruction[1] 44 | const lastId = ids[ids.length - 1] 45 | for (let i = 0; i < ids.length; ++i) { 46 | const id = ids[i] 47 | if (clusterNodes.get(id)) { 48 | continue 49 | } 50 | const parentId = ids[i - 1] || 0 51 | const parentCluster = clusterNodes.get(parentId) 52 | if (parentCluster) { 53 | if (!parentCluster.children.includes(id)) { 54 | parentCluster.children.push(id) 55 | parentCluster.children.sort((a, b) => a - b) 56 | } 57 | } 58 | const fillerValue = parentId === 0 ? Math.PI : 1 // Root's value is within, not between 59 | const clusterNode = mockClusterNode({ 60 | id, 61 | parentId, 62 | children: [], 63 | stemLength: id === lastId ? totalStemLength : fillerValue 64 | }) 65 | clusterNodes.set(id, clusterNode) 66 | } 67 | } 68 | return { data: [...clusterNodes.values()] } 69 | } 70 | 71 | function topologyToSplitArrays (topology, numeric = true) { 72 | return topology.map(d => d[0].split('.').map(i => numeric ? parseInt(i) : i)) 73 | } 74 | 75 | function topologyToOrderedLeaves (topology, numeric = true) { 76 | return topologyToSplitArrays(topology, numeric).map(d => d.reverse()[0]) 77 | } 78 | 79 | function topologyToSortedIds (topology, numeric = true) { 80 | const splitArrays = topologyToSplitArrays(topology, numeric) 81 | const maxDepth = splitArrays.reduce((max, arr) => arr.length > max ? arr.length : max, 0) 82 | const orderedUniqueIds = [] 83 | for (let depth = 0; depth < maxDepth; depth++) { 84 | for (let arrayIndex = 0; arrayIndex < splitArrays.length; arrayIndex++) { 85 | const id = splitArrays[arrayIndex][depth] 86 | if (id && !orderedUniqueIds.includes(id)) orderedUniqueIds.push(id) 87 | } 88 | } 89 | return orderedUniqueIds 90 | } 91 | 92 | module.exports = { 93 | mockClusterNode, 94 | mockTopology, 95 | topologyToOrderedLeaves, 96 | topologyToSortedIds 97 | } 98 | -------------------------------------------------------------------------------- /test/visualizer-util/fakedata.json: -------------------------------------------------------------------------------- 1 | {"data":[{"clusterId":1,"parentClusterId":0,"name":"miscellaneous","children":[2],"nodes":[{"aggregateId":1,"parentAggregateId":0,"children":[2],"mark":["root",null,null],"type":null,"frames":[],"sources":[{"asyncId":1,"parentAsyncId":null,"triggerAsyncId":null,"executionAsyncId":null,"init":null,"before":[],"after":[],"destroy":null}]}]},{"clusterId":2,"parentClusterId":1,"name":"fake-cluster","children":[],"nodes":[{"aggregateId":2,"parentAggregateId":1,"children":[],"mark":["user",null,null],"type":"RANDOMBYTESREQUEST","frames":[{"functionName":"fake-native-frame","isNative":true},{"functionName":"FakeConstructor","isConstructor":true,"fileName":"/fake/nameless/frame.fk","lineNumber":12,"columnNumber":34},{"typeName":"fakeType","fileName":"/fake/nameless/frame.fk","lineNumber":43,"columnNumber":21},{"isEval":true,"evalOrigin":"/fake/eval/frame"}],"sources":[{"asyncId":23,"parentAsyncId":1,"triggerAsyncId":1,"executionAsyncId":1,"init":6783514.515,"before":[],"after":[],"destroy":null}]}]}]} -------------------------------------------------------------------------------- /test/visualizer-util/prepare-fake-nodes.js: -------------------------------------------------------------------------------- 1 | const { 2 | clusterNodes, 3 | aggregateNodes, 4 | dummyCallbackEvents, 5 | expectedClusterResults, 6 | expectedAggregateResults, 7 | expectedTypeCategories, 8 | expectedTypeSubCategories, 9 | expectedDecimalsTo5Places 10 | } = require('./fake-overlapping-nodes.js') 11 | 12 | class TestClusterNode { 13 | constructor (clusterId) { 14 | const clusterNode = clusterNodes.get(clusterId) 15 | Object.assign(this, clusterNode) 16 | this.id = this.clusterId = clusterId 17 | this.mark = new Map([['party', 'user']]) 18 | } 19 | } 20 | 21 | class TestAggregateNode { 22 | constructor (aggregateId) { 23 | Object.assign(this, aggregateNodes.get(aggregateId)) 24 | this.id = this.aggregateId = aggregateId 25 | this.frames = this.frames || [] 26 | this.sources = [] 27 | } 28 | } 29 | 30 | const fakeNodes = [] 31 | 32 | for (const [aggregateId] of aggregateNodes) { 33 | aggregateNodes.set(aggregateId, new TestAggregateNode(aggregateId)) 34 | } 35 | 36 | for (const [clusterId, clusterNode] of clusterNodes) { 37 | for (let i = 0; i < clusterNode.nodes.length; i++) { 38 | const aggregateId = clusterNode.nodes[i] 39 | clusterNode.nodes[i] = aggregateNodes.get(aggregateId) 40 | } 41 | clusterNodes.set(clusterId, new TestClusterNode(clusterId)) 42 | fakeNodes.push(clusterNodes.get(clusterId)) 43 | } 44 | 45 | let asyncId = 0 // Give root node asyncId 0 so first 'real' node gets 1 46 | 47 | for (const dummyEvent of dummyCallbackEvents) { 48 | const aggregateNode = aggregateNodes.get(dummyEvent.aggregateId) 49 | if (typeof dummyEvent.sourceKey !== 'undefined') { 50 | // Add this to an existing source 51 | const source = aggregateNode.sources[dummyEvent.sourceKey] 52 | source.before.push(dummyEvent.before) 53 | source.after.push(dummyEvent.after) 54 | } else { 55 | // Create a new source 56 | aggregateNode.sources.push({ 57 | asyncId, 58 | init: dummyEvent.delayStart, 59 | before: [dummyEvent.before], 60 | after: [dummyEvent.after], 61 | // .destroy isn't currently used in these tests or defined in test data, 62 | // if it's undefined give it a valid random value for completeness 63 | destroy: dummyEvent.destroy || dummyEvent.after + Math.random() 64 | }) 65 | asyncId++ 66 | } 67 | } 68 | 69 | // Attach pre-computed withins to fake ClusterNodes 70 | for (const expected of expectedClusterResults.values()) { 71 | expected.withinValue = expected.async.within + expected.sync 72 | } 73 | 74 | // Attach pre-computed withins to fake AggregateNodes 75 | for (const expected of expectedAggregateResults.values()) { 76 | expected.withinValue = expected.sync 77 | } 78 | 79 | module.exports = { 80 | fakeNodes, 81 | expectedClusterResults, 82 | expectedAggregateResults, 83 | expectedTypeCategories, 84 | expectedTypeSubCategories, 85 | expectedDecimalsTo5Places 86 | } 87 | -------------------------------------------------------------------------------- /test/visualizer-util/verify-garbage-collection.js: -------------------------------------------------------------------------------- 1 | const test = require('tap').test 2 | const DataSet = require('../../visualizer/data/dataset.js') 3 | const { 4 | fakeNodes 5 | } = require('./prepare-fake-nodes.js') 6 | const { GCKey, getGCCount } = require('gckey') 7 | 8 | /** 9 | * These tests are not run by default as they depend on installation of a native addon. 10 | * Use and modify them if you suspect there is a possible memory leak or garbage collection issue 11 | * 12 | * To run: 13 | * 1: Clone https://github.com/jasnell/gckey.git into node_modules 14 | * 2: `npm install` in node_modules/gckey to compile 15 | * 3: Run this test script directly, with the --expose-gc flag, for example: 16 | * node --expose-gc test/visualizer-util/verify-garbage-collection.js 17 | */ 18 | 19 | if (typeof global.gc !== 'function') throw new Error('This test must be run with the --expose-gc flag') 20 | 21 | const dataSet = new DataSet(fakeNodes) 22 | 23 | // Add GCkeys so we can count how many items are garbage collected 24 | for (const callbackEvent of dataSet.callbackEvents.array) { 25 | // In native addon, GCKey increments a counter in V8 when it is garbage collected 26 | callbackEvent.gcTracker = new GCKey() 27 | } 28 | 29 | { // Confirm that GCkey is working correctly - each of these should increase gc count by 1 30 | const throwAway = new GCKey() 31 | if (throwAway) { 32 | const testTracker = new GCKey() 33 | dataSet.testTracker = testTracker 34 | } 35 | } 36 | const callbackEventsGCExpected = dataSet.callbackEvents.array.length + 2 37 | 38 | // This should free up all the tracked objects for garbage collection 39 | dataSet.processData() 40 | dataSet.testTracker = null 41 | 42 | setImmediate(() => { 43 | test('Visualizer data - ensure callbackEvents are garbage collected', function (t) { 44 | global.gc() 45 | const amountGarbageCollected = getGCCount() 46 | t.equal(amountGarbageCollected, callbackEventsGCExpected) 47 | t.end() 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /visualizer/app-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /visualizer/clinic-favicon.png.b64: -------------------------------------------------------------------------------- 1 | data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADsAAABACAYAAACkwA+xAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABJ0RVh0U29mdHdhcmUAZXpnaWYuY29toMOzWAAACKlJREFUaIHtm2tsVMcVx39ndtfeNd4FO2AbgiEY8zCGhoRHsaUSLAFOpaK2lFRqokqJSBsiSJpUKG1UiEhJqPqUGhKaVBCpKmmlFkikfogNKS8p5p2IBNshtYHEBj8wLN5d1ut9TT/Y3TXgx967d2lev0/3yjNnz9935twzZ+YKt4mKmvqFCD9BZCFwJ3BRRI4hsT/WLZ194nb4IJn+gQW1Hxbble0lNN8Zxou3ovHYkyeq57Rk0peMiV1y4IC9N1K4FmEzGncKXYKi9W+7u9lS//3ycCZ8yojYRTUN94qSV0EvMNxZ84HYZE3d0rIjVvtlqdglB94fE4o4nxf0WsCWhikN7Iwpefr40rIrFrlnndhFtY0rROLbQCZaZRPoQHjmyNKyvyKi0zWWttjKf384VcfUKyDV6doahkNaYo8fXTanMR0jpsXOO3nSkX0156caNgHOdJxIkYiGPzgdrk0Hq6aEzBgwJbaytmGxFv4EzDLTP02aBFlbt7xsr9GOhsRW1NTnY+NXaPmR0b7WI/8UHV5XV313Z8o9UmqltVS80/hD0fxew1jT/lnPNWDTkbqyrWyS+EiNRxRbsa9xDlq/ClRa4V0mEDglqDXvLp95coR2gzPvXydzsrNcz2iRZ4Esow4UOR2UurO5I9uO227DH43R1Rul2d9Leyhi1FwqRDWyzdYjG9799kz/YA0GFVtR2/gtRG8F7jLya6PsigeK86kqdDPNPXSA/tgf4kCHn10tV7keHXH0GeWiaP1UXXX5rpv/cIPYr9WeHpWrsl7TWj9kxLoAq4rzeWTqWMY4Uk+croVj7Dh3mT0tXtLOGG72SeSNQDz82AfVd18f6CcACw7UF9kjsheYY8Soy6bYOHsCSwpuzPV745r3vddp74kQiMZx220UuRzMzcshW904oA52+tl85hI9Mcuf8pmoQy87UVXeDv1iy/9Rn+UZI/uAxUYsuWyKV+ZPZqYnOWRbgmFeP9fFoU4/oUGcd9oU9xW4WV0ylok5yVDQ6Oth3clPLRcscErnur9xpLK4RwF4xsgGDApVAs/NnnCD0J0XrvBQ3Tlq27oHFQoQisWpbevmwbpz/O2TZI5f5nGxcfYEy1/eGuYR8D8LIBU19fkoOQ94jBh5YFI+T88oTNz/prGdt1q9hp1ZOTGP9WVFifvffdTOnhbjdkbAR1xPUVr4JgaF5toVj5Qkc4udF66YEgqwp9V7wxNeXTKWHLsyZWsYPNjU/UqQ+UZ7rirOT0Td1mCYPzddTsuT15ouc7GnrziRl2VnVXFeWvYGJa4XKMT4+rOqMBl5tzdfJqrTe3FE4prtzV1J+wWGBlpqiNypIJ5KfShBkdORSBhCsTiHLwcs8eVwp5/eeN8/bbrHSaHTYYndJDpXgRgSW+rOTlyfvtYzZNQ1Sk8szmlvEOh7H07NzR6+g1GEXIUm10ifsdnJ/3hbj7VFwLaeZM48zmm31DYat0JSKnMm8DiSkdIXsTYB6I7EEtejDaSdKZKrQEZ9Vhwak5W0dy0cG6alCTS5CrShSNDVG01cF7msDSJFA4LSwN+xAhHsCsHQ5Gjy9yau5+bl4LJZkwC4bIq5eTlAX9G4KWCqpjYkGhwKbUxsRyjCf/x9jmQrYXGBoSk/JPcVuMnqXw2d9YXoDFn7ZAG7AmNiAfZ3JAsBq0vGYpf00neHEh6dOi5xf6DDl5a9ITAndlfL1UQAmZiTxZpp40boMTxrSguY0D//veEou03m2SPgMDXhrkfjvH4umQ8/OPkOVk40l8+uLM7jB5PzE/c7mrsIWl+qAdAKMDU5drd4OdiZHM7ry4p4YnohDpXakHYo4YnphayfmVze7e/w8WZmnipAVCr2NgQBl5neLpvi5fmTKPMku1/qibC9+TKHOv2DVh1cNsWSAjerp45LDF2Ahu4e1p361LL0cxCuS8W+Bl+Km8WDMlwN6rQ3SHsogi8Sw+OwUeTsq0Fl3fT093f4eKG+LZNCAbqlYm/DVSCtBaTQN/dWl4y7IQsaCW84yo7mLt5stb66eDMCXXYg7Wxe0zeH327rZlVxHlUFHmZ4hq4bn/WF2N/hY3erN1PB6FYfdd+cPQdMsdp44f92BLLseBw2fJEYV8JRmvy9dAyzI2ATYV5+TmKJ1+Tv5T1vkFiaBQKgyY7Gn4n9uI5QZFhRg1HqdvJc+XhKb9pNaA2GebG+jdPXgum4FFCIHnRf5HZT6nayY+FdtwiFvsTlpXmTKE1nQS/4FWBNXSUN7CJsLB+feEcHojF2tXjZ1eJNLCkdStgwewI2s6mpJmAH5SfjsXB47s3PSdS1AtEYDx89z6X+qsXfP7nCXxaVkGtXTHc7uScvh5NXrw9nbggkoNC61UK/TTGw3vR2W3dCKPSVamrauhP309wmh7LWFxVKbsu5wXSwJH6KHFfE4jVARtZUqTKwILCsaDTjB6SRE1wOqsePTtx/PKCtAXy2cKRGABbta9gsmg2mvU0TuwhvVJZQ3L+rF4j2bX4JsHz8aHL7t0PO+kI8evyC4XeuaF6sq561QQE47a4XNRy1VkLqRLVmS30bkf4iea5d8b3iPFYW5yWEhuOaF+ovGRcKp3rDwS0ACuBg1ZRQzKG/C5yxUoQRTl8L8vCx83zku7X21BTo5cfHL9AcMDyEz9jirDi1Yn4QLDpmYCU2Ee7Jy0nsPDT5e3nfRLo47DGDgZg9QPIZIbUDJAOpqGtxid/3M7NHg/4PRDWyzemI/+JgVfmgWeEX5tBXTMtjx6rLTo3QLgW+LMf5BvLlOKh5E5/XI7im6sZ11bMOh/ODcwV+Dli7KTM0EQ2/HhN1zDYjFD4/x+YPom2PH6me8VE6Rr76IMIs8/Y1j3bo8C+/8J+6DOSz+hGT5UfJAI7eP+u9bEd7JfAUQqoFvaBo/byvWy/IhFD46sPDzFD5zpkF8bh6UuDrwESgFTgqKr71dn1S+l89HGvyrb52XgAAADV0RVh0Q29tbWVudABDb252ZXJ0ZWQgd2l0aCBlemdpZi5jb20gU1ZHIHRvIFBORyBjb252ZXJ0ZXIsKeMjAAAAAElFTkSuQmCC -------------------------------------------------------------------------------- /visualizer/data/frame.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class Frame { 4 | constructor (frame) { 5 | this.functionName = frame.functionName 6 | this.typeName = frame.typeName 7 | this.evalOrigin = frame.evalOrigin 8 | this.fileName = frame.fileName 9 | this.lineNumber = frame.lineNumber 10 | this.columnNumber = frame.columnNumber 11 | this.isEval = frame.isEval 12 | this.isConstructor = frame.isConstructor 13 | this.isNative = frame.isNative 14 | this.isToplevel = frame.isToplevel 15 | this.party = this.getFrameParty(this.fileName) 16 | } 17 | 18 | getName () { 19 | let name = this.functionName ? this.functionName : '' 20 | if (this.isEval) { 21 | // no change 22 | } else if (this.isToplevel) { 23 | // no change 24 | } else if (this.isConstructor) { 25 | name = 'new ' + name 26 | } else if (this.isNative) { 27 | name = 'native ' + name 28 | } else { 29 | name = this.typeName + '.' + name 30 | } 31 | return name 32 | } 33 | 34 | getFormatted (name) { 35 | let formatted = ' at ' + name 36 | if (this.isEval) { 37 | formatted += ' ' + this.evalOrigin 38 | } else { 39 | formatted += ' ' + this.fileName 40 | formatted += ':' + (this.lineNumber > 0 ? this.lineNumber : '') 41 | formatted += (this.columnNumber > 0 ? ':' + this.columnNumber : '') 42 | } 43 | return formatted 44 | } 45 | 46 | // TODO: move this logic to analysis, add property there, then trim file paths 47 | getFrameParty (fileName) { 48 | if (!fileName) return ['empty', 'no file'] 49 | 50 | // If first character is / or it's a letter followed by :\ 51 | if (fileName.charAt(0) === '.' || fileName.charAt(0) === '/' || fileName.match(/^[a-zA-Z]:\\/)) { 52 | // ...then this is a Unix or Windows style local file path 53 | 54 | if (fileName.match(/(?:\\|\/)node_modules(?:\\|\/)/)) { 55 | const directories = fileName.split(/\\|\//) 56 | const moduleName = directories[directories.lastIndexOf('node_modules') + 1] 57 | return ['external', `module ${moduleName}`] 58 | } 59 | return ['user', 'your application'] 60 | } 61 | return ['nodecore', 'node core'] 62 | } 63 | } 64 | 65 | module.exports = Frame 66 | -------------------------------------------------------------------------------- /visualizer/data/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const DataSet = require('./dataset.js') 4 | 5 | // 'json' optional arg allows json to be passed in for browserless tests 6 | function loadData (settings = {}, json = getDataFromPage()) { 7 | const dataSet = new DataSet(json, settings) 8 | dataSet.processData() 9 | return dataSet 10 | } 11 | 12 | function getDataFromPage () { 13 | if (typeof document === 'object') { 14 | const dataElement = document.querySelector('#clinic-data') 15 | return JSON.parse(dataElement.textContent) 16 | } 17 | return {} 18 | } 19 | 20 | module.exports = loadData 21 | -------------------------------------------------------------------------------- /visualizer/draw/banner.css: -------------------------------------------------------------------------------- 1 | /* Banner layout */ 2 | #banner { 3 | display: flex; 4 | align-items: center; 5 | height: var(--banner-height); 6 | background: var(--banner-bg-color); 7 | position: relative; 8 | justify-content: space-between; 9 | padding: 7px 18px; 10 | } 11 | #banner #main-logo { 12 | display: flex; 13 | align-items: center; 14 | text-decoration: none; 15 | } 16 | #banner #main-logo span{ 17 | color: var(--banner-logo-color); 18 | font-weight: bold; 19 | font-size: 23px; 20 | } 21 | 22 | #banner #main-logo svg { 23 | display: block; 24 | height: 51px; 25 | width: auto; 26 | margin-right: 12px; 27 | } 28 | 29 | #banner #company-logo svg { 30 | display: block; 31 | height: 21px; 32 | width: auto; 33 | 34 | } -------------------------------------------------------------------------------- /visualizer/draw/breadcrumb-panel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const HtmlContent = require('./html-content.js') 4 | 5 | class BreadcrumbPanel extends HtmlContent { 6 | constructor (d3Container, contentProperties = {}) { 7 | super(d3Container, contentProperties) 8 | 9 | this.originalUI = contentProperties.originalUI 10 | this.topmostUI = contentProperties.originalUI 11 | contentProperties.originalUI.on('setTopmostUI', (newTopmostUI) => { 12 | this.topmostUI = newTopmostUI 13 | this.draw() 14 | }) 15 | 16 | document.addEventListener('keydown', (e) => { 17 | if (e.keyCode === 27 && this.topmostUI !== this.originalUI) { 18 | // ESC button 19 | if (this.topmostUI.selectedDataNode) { 20 | return this.topmostUI.clearFrames() 21 | } 22 | const lastUI = this.topmostUI 23 | const targetUI = this.topmostUI.clearSublayout() 24 | this.originalUI.emit('navigation', { from: lastUI, to: targetUI }) 25 | } 26 | }) 27 | } 28 | 29 | initializeElements () { 30 | super.initializeElements() 31 | this.d3Element.classed('panel', true) 32 | this.d3Element.classed('breadcrumbs-panel', true) 33 | } 34 | 35 | draw () { 36 | super.draw() 37 | 38 | this.d3Element.selectAll('label').remove() 39 | 40 | let ui = this.topmostUI 41 | while (ui) { 42 | this.addLabel(ui) 43 | ui = ui.parentUI 44 | } 45 | } 46 | 47 | addLabel (ui) { 48 | const fullLabelText = ui.name || 'Main View' 49 | const labelText = trimToNearestSpace(fullLabelText) 50 | this.d3Element 51 | .insert('label', ':first-child') // i.e. prepend instead of append 52 | .classed('breadcrumb', true) 53 | .property('textContent', labelText) 54 | .property('title', fullLabelText) 55 | .on('click', () => { 56 | this.traverseUp(ui) 57 | }) 58 | 59 | if (ui !== this.originalUI) { 60 | this.d3Element 61 | .insert('label', ':first-child') // i.e. prepend instead of append 62 | .classed('breadcrumb-separator', true) 63 | .property('textContent', '➥') 64 | } 65 | } 66 | 67 | traverseUp (targetUI) { 68 | this.topmostUI.queueAnimation('breadcrumb', (animationQueue) => { 69 | if (this.topmostUI !== targetUI) { 70 | const currentUI = this.topmostUI.traverseUp(targetUI, { animationQueue }) 71 | if (this.topmostUI !== currentUI) { 72 | return 73 | } 74 | } 75 | // Didn't animate, kick the queue 76 | animationQueue.execute() 77 | }) 78 | } 79 | } 80 | 81 | // Attempts to aesthetically limit string length 82 | // Initially it breaks string into words (space split) 83 | // Then it tries to detect a natural break that's not far from the max (15 +/- 3) 84 | // And prioritize such break over a hard cut, if available 85 | function trimToNearestSpace (str) { 86 | const trimThreshold = 15 87 | if (str.length < trimThreshold) return str 88 | const acceptableStretch = 3 89 | let trimmed = '' 90 | for (const word of str.split(' ')) { 91 | const combined = trimmed + ' ' + word 92 | if (combined.length > trimThreshold + acceptableStretch) { 93 | const previousSpaceAvailable = trimmed.length > trimThreshold - acceptableStretch && trimmed.length <= trimThreshold 94 | const resolved = previousSpaceAvailable ? trimmed : combined.slice(0, trimThreshold + 1) 95 | trimmed = resolved 96 | break 97 | } 98 | trimmed = combined 99 | } 100 | trimmed = trimmed.trim() // drop excess spaces 101 | return trimmed + '…' 102 | } 103 | 104 | module.exports = BreadcrumbPanel 105 | -------------------------------------------------------------------------------- /visualizer/draw/d3-subset.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const selection = require('d3-selection') 4 | 5 | const d3 = Object.assign( 6 | {}, 7 | // d3.event 8 | // d3.select 9 | selection, 10 | // d3.scaleLinear 11 | // d3.scaleTime 12 | require('d3-scale'), 13 | // d3.arc 14 | // d3.area 15 | // d3.pie 16 | // d3.stack 17 | require('d3-shape'), 18 | // d3.axisBottom 19 | require('d3-axis'), 20 | // d3.drag 21 | require('d3-drag'), 22 | // d3.easeCubicInOut 23 | require('d3-ease'), 24 | // d3.format 25 | require('d3-format'), 26 | // d3.interpolateNumber 27 | require('d3-interpolate'), 28 | // d3.timeFormat 29 | require('d3-time-format'), 30 | // d3.timeHour 31 | // d3.timeMinute 32 | // d3.timeSecond 33 | require('d3-time'), 34 | // d3.selection().transition() 35 | require('d3-transition'), 36 | require('d3-color') 37 | ) 38 | 39 | // This property changes after importing so we fake a live binding. 40 | Object.defineProperty(d3, 'event', { 41 | get () { return selection.event } 42 | }) 43 | 44 | module.exports = d3 45 | -------------------------------------------------------------------------------- /visualizer/draw/frames.css: -------------------------------------------------------------------------------- 1 | #frames-panel { 2 | margin: 18px 0; 3 | padding: 0px 28px; 4 | color: var(--max-contrast); 5 | font-size: var(--main-text-size); 6 | } 7 | 8 | #frames-panel .heading { 9 | margin: 12px 0; 10 | } 11 | 12 | #frames-panel .heading { 13 | margin: 12px 0; 14 | } 15 | 16 | #frames-panel .jump-to-node { 17 | display: block; 18 | float: right; 19 | font-weight: bold; 20 | color: var(--cyan); 21 | cursor: pointer; 22 | margin-right: 32px; 23 | } 24 | 25 | #frames-panel .sub-collapse-control { 26 | cursor: pointer; 27 | color: var(--cyan); 28 | } 29 | 30 | #frames-panel .this-node > .sub-collapse-control { 31 | font-weight: bold; 32 | color: var(--cyan-highlight); 33 | } 34 | 35 | #frames-panel .user > .sub-collapse-control { 36 | color: var(--cyan-highlight); 37 | } 38 | 39 | #frames-panel .this-node > .sub-collapse-control:hover, 40 | #frames-panel .this-node > .sub-collapse-control:focus, 41 | #frames-panel .user > .sub-collapse-control:hover, 42 | #frames-panel .user > .sub-collapse-control:focus { 43 | color: var(--cyan-strong); 44 | } 45 | 46 | #frames-panel .sub-collapse-control:hover, 47 | #frames-panel .sub-collapse-control:focus, 48 | #frames-panel .jump-to-node:hover, 49 | #frames-panel .jump-to-node:focus { 50 | color: var(--cyan-highlight); 51 | } 52 | 53 | #frames-panel .collapsed .frame-group .sub-collapse-control { 54 | display: none; 55 | } 56 | 57 | #frames-panel .collapsed .frame-item { 58 | display: none; 59 | } 60 | 61 | #frames-panel .frame-group .delays { 62 | font-weight: normal; 63 | color: var(--grey-highlight); 64 | padding-bottom: 3px; 65 | margin: 0; 66 | } 67 | 68 | #frames-panel .frame-group .delays .figure { 69 | color: var(--max-contrast); 70 | } 71 | 72 | #frames-panel .frame-group.node-frame-group { 73 | padding: 3px 0 0 3px; 74 | } 75 | 76 | #frames-panel .node-frame-group .frame-group, 77 | #frames-panel .frame-group .delays { 78 | border-left: 1px solid var(--primary-grey); 79 | margin-left: 3px; 80 | padding-left: 15px; 81 | } 82 | 83 | #frames-panel .frame-group.collapsed .delays { 84 | border-left: none; 85 | } 86 | 87 | #frames-panel .frame-group:not(.collapsed) .frame-group:last-child, 88 | #frames-panel pre.frame-item:last-child { 89 | padding-bottom: 9px; 90 | } 91 | 92 | #frames-panel .frame-item { 93 | margin: 0 0 0 4px; 94 | padding: 4px 0; 95 | } 96 | 97 | #frames-panel pre.frame-item { 98 | white-space: pre-wrap; 99 | padding-left: 14px; 100 | } 101 | 102 | #frames-panel div.frame-item { 103 | padding: 8px 20px; 104 | } 105 | 106 | #frames-panel .frame-group .frame-item { 107 | border-left: solid 1px var(--cyan); 108 | } 109 | 110 | #frames-panel .collapsed .sub-collapse-control .arrow { 111 | transform: rotate(90deg); 112 | } 113 | 114 | #frames-panel .sub-collapse-control .arrow { 115 | display: inline-block; 116 | margin: 0 6px -3px -3px; 117 | } -------------------------------------------------------------------------------- /visualizer/draw/html-content-types.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // This lookup object of HTML content types is necessary to prevent circular dependencies 4 | // e.g. if an A contains a B which contains an A 5 | module.exports = { 6 | // Parent class, for generic HTML content with optional collapse, load indicator, etc 7 | HtmlContent: require('./html-content.js'), 8 | 9 | // Sub classes which extend HtmlContent 10 | BreadcrumbPanel: require('./breadcrumb-panel.js'), 11 | Frames: require('./frames.js'), 12 | HoverBox: require('./hover-box.js'), 13 | InteractiveKey: require('./interactive-key.js'), 14 | AreaChart: require('./area-chart.js'), 15 | Lookup: require('./lookup.js'), 16 | SvgContainer: require('./svg-container.js'), 17 | SideBarDrag: require('./side-bar-drag.js') 18 | } 19 | -------------------------------------------------------------------------------- /visualizer/draw/side-bar-drag.js: -------------------------------------------------------------------------------- 1 | const HtmlContent = require('./html-content.js') 2 | const d3 = require('./d3-subset.js') 3 | const spinner = require('@clinic/clinic-common/spinner') 4 | 5 | class SideBarDrag extends HtmlContent { 6 | constructor (d3Container, contentProperties) { 7 | super(d3Container, contentProperties) 8 | 9 | this.topMostUI = this.ui 10 | this.ui.on('setTopmostUI', (topMostUI) => { 11 | this.topMostUI = topMostUI 12 | }) 13 | } 14 | 15 | initializeElements () { 16 | super.initializeElements() 17 | 18 | this.spinner = spinner.attachTo(this.ui.getNodeLinkSection().d3Element.node()) 19 | 20 | let lastPercent = 0 21 | this.d3DragBehaviour = d3.drag() 22 | .on('start', () => { 23 | lastPercent = this.getCurrentDragWidth({ x: 0 }) 24 | d3.select('body').style('cursor', 'ew-resize') 25 | }) 26 | .on('drag', () => { 27 | this.showRedrawing() 28 | const percent = this.getCurrentDragWidth(d3.event) 29 | if (percent !== lastPercent) { 30 | this.setNodeLinkWidth(percent) 31 | } 32 | lastPercent = percent 33 | }) 34 | .on('end', () => { 35 | const percent = this.getCurrentDragWidth(d3.event) 36 | if (percent !== lastPercent) { 37 | this.setNodeLinkWidth(percent) 38 | } 39 | lastPercent = percent 40 | 41 | this.redrawLayout() 42 | d3.select('body').style('cursor', null) 43 | }) 44 | 45 | this.d3Element.call(this.d3DragBehaviour) 46 | } 47 | 48 | showRedrawing (show = true) { 49 | if (show) { 50 | this.spinner.show('Redrawing...') 51 | } else { 52 | this.spinner.hide() 53 | } 54 | } 55 | 56 | setNodeLinkWidth (percent) { 57 | const sideBar = this.ui.sections.get('side-bar') 58 | const nodeLink = this.ui.sections.get('node-link') 59 | const footer = this.ui.sections.get('footer') 60 | // FIXME this is probably a private API. Is this alright or should the sidebar be its own HtmlContent subclass so it can handle this internally? 61 | const callbacksOverTime = sideBar.content.get('area-chart') 62 | const framesButton = footer.collapseControl 63 | const framesPanel = footer.d3ContentWrapper 64 | 65 | nodeLink.d3Element 66 | .style('width', `${percent}%`) 67 | framesPanel 68 | .style('width', `${percent}%`) 69 | sideBar.d3Element 70 | .style('width', `${100 - percent}%`) 71 | framesButton.d3Element 72 | .style('width', `${100 - percent}%`) 73 | 74 | // newPercent / defaultPercent 75 | // maintaining aspect ratio 76 | callbacksOverTime.chartHeightScale = (100 - percent) / 25 77 | callbacksOverTime.draw() 78 | } 79 | 80 | redrawLayout () { 81 | this.topMostUI.redrawLayout() 82 | this.topMostUI.originalUI.redrawLayout() 83 | this.showRedrawing(false) 84 | } 85 | 86 | getCurrentDragWidth ({ x }) { 87 | const rect = this.d3Element.node() 88 | .getBoundingClientRect() 89 | 90 | const leftOffset = 24 + rect.left 91 | let pxSize = leftOffset + x 92 | const pxAvailable = window.innerWidth 93 | 94 | if (pxSize < 400) pxSize = 400 95 | if (pxAvailable - pxSize < 250) pxSize = pxAvailable - 250 96 | return Math.round(pxSize / pxAvailable * 100) 97 | } 98 | } 99 | 100 | module.exports = SideBarDrag 101 | -------------------------------------------------------------------------------- /visualizer/draw/static-key.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const extLinkIcon = require('@clinic/clinic-common/icons/external-link') 3 | 4 | const svgSample = ` 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | example–1.2 s 16 | 17 | 18 | ` 19 | 20 | const keyHtml = ` 21 |
${svgSample}
22 |

23 | ⬋ Grouped async operations. Size represents time spent executing code and waiting for responses. 24 |

25 |

26 | The straight line segment represents async operations in this group initiated in the previous group. 27 |

28 |

29 | Colors indicate type and area of the grouped operations (labels above expand to give more details). 30 |

31 |

How to start exploring this

32 |

33 | The diagram shows how groups of async operations branch out from the start point of this application, which is at the top centre. 34 |

35 |

36 | Click on bubbles to explore deeper. When you reach groupings of only one async operation, call stacks are shown, allowing you to find the code behind the biggest delays. 37 |

38 |

39 | See also the walkthrough and guides on the ClinicJs website ${extLinkIcon}. 40 |

41 | ` 42 | 43 | module.exports = keyHtml 44 | -------------------------------------------------------------------------------- /visualizer/draw/svg-container.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const HtmlContent = require('./html-content.js') 4 | const SvgNodeDiagram = require('./svg-node-diagram.js') 5 | 6 | class SvgContainer extends HtmlContent { 7 | constructor (parentContent, contentProperties = {}) { 8 | const defaultProperties = { 9 | htmlElementType: 'svg' 10 | } 11 | super(parentContent, Object.assign(defaultProperties, contentProperties)) 12 | 13 | if (contentProperties.svgBounds) { 14 | const defaultBounds = { 15 | minX: 0, 16 | minY: 0, 17 | width: null, // Set from layout.settings 18 | height: null, // Set in layout.scale 19 | preserveAspectRatio: 'xMidYMid meet', 20 | minimumDistanceFromEdge: 20 21 | } 22 | this.svgBounds = Object.assign(defaultBounds, contentProperties.svgBounds) 23 | } 24 | 25 | this.svgNodeDiagram = new SvgNodeDiagram(this) 26 | this.ui.svgNodeDiagram = this.svgNodeDiagram 27 | 28 | this.ui.on('setData', () => { 29 | this.setData() 30 | }) 31 | } 32 | 33 | setData () { 34 | const { 35 | minX, 36 | minY, 37 | preserveAspectRatio 38 | } = this.svgBounds 39 | this.svgBounds.height = this.ui.layout.scale.finalSvgHeight || this.ui.layout.settings.svgHeight 40 | this.svgBounds.width = this.ui.layout.settings.svgWidth 41 | 42 | this.d3Element 43 | .attr('viewBox', `${minX} ${minY} ${this.svgBounds.width} ${this.svgBounds.height}`) 44 | .attr('preserveAspectRatio', preserveAspectRatio) 45 | } 46 | 47 | initializeElements () { 48 | super.initializeElements() 49 | 50 | this.d3Element 51 | .attr('id', this.contentProperties.id) 52 | .classed('bubbleprof', true) 53 | 54 | this.svgNodeDiagram.initializeElements() 55 | } 56 | 57 | animate (previousUI) { 58 | this.svgNodeDiagram.animate(previousUI) 59 | } 60 | 61 | draw () { 62 | this.svgNodeDiagram.draw() 63 | } 64 | } 65 | 66 | module.exports = SvgContainer 67 | -------------------------------------------------------------------------------- /visualizer/layout/connections.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class Connection { 4 | constructor (originLayoutNode, targetLayoutNode, scale) { 5 | this.originId = originLayoutNode.id 6 | this.originLayoutNode = originLayoutNode 7 | this.originNode = originLayoutNode.node.shortcutTo || originLayoutNode.node 8 | 9 | this.targetId = targetLayoutNode.id 10 | this.targetLayoutNode = targetLayoutNode 11 | this.targetNode = targetLayoutNode.node.shortcutTo || targetLayoutNode.node 12 | 13 | this.scale = scale 14 | } 15 | 16 | // Avoid duplication of values so stats can be swtiched/recalculated with settings 17 | // If recalculating these proves to be a performance problem, consider caching values 18 | getOriginRadius () { return this.scale.getCircleRadius(this.originLayoutNode.getWithinTime()) } 19 | getTargetRadius () { return this.scale.getCircleRadius(this.targetLayoutNode.getWithinTime()) } 20 | getVisibleLineLength () { return this.scale.getLineLength(this.targetLayoutNode.getBetweenTime()) } 21 | getDistanceBetweenCenters () { 22 | return this.getOriginRadius() + 23 | this.getVisibleLineLength() + 24 | this.getTargetRadius() + 25 | // Leave a gap at both ends so any text labels are readable 26 | // Only one lineWidth because it increases distance by half a line width at each end 27 | this.scale.settings.labelMinimumSpace * 2 + this.scale.settings.lineWidth 28 | } 29 | } 30 | 31 | module.exports = Connection 32 | -------------------------------------------------------------------------------- /visualizer/layout/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Layout = require('./layout.js') 4 | 5 | function generateLayout (dataSet, settings) { 6 | settings = Object.assign({ 7 | debugMode: dataSet.settings.debugMode 8 | }, settings) 9 | const layout = new Layout({ dataNodes: [...dataSet.clusterNodes.values()] }, settings) 10 | 11 | // This can be interrupted in tests etc 12 | layout.generate(settings) 13 | return layout 14 | } 15 | 16 | module.exports = generateLayout 17 | -------------------------------------------------------------------------------- /visualizer/layout/stems.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { radiusFromCircumference } = require('./line-coordinates.js') 4 | const { validateNumber } = require('../validation.js') 5 | 6 | function getNodeAncestorIds (parent) { 7 | return parent ? [...parent.stem.ancestors.ids, parent.id] : [] 8 | } 9 | 10 | class Stem { 11 | constructor (layout, layoutNode) { 12 | this.layout = layout 13 | 14 | // Ancestor stem stats must be calculated before descendent stem stats 15 | const parent = layoutNode.parent 16 | if (parent && !parent.stem) { 17 | parent.stem = new Stem(layout, parent) 18 | } 19 | 20 | this.ancestors = { 21 | totalBetween: 0, 22 | totalDiameter: 0, 23 | ids: getNodeAncestorIds(parent) 24 | } 25 | this.leaves = { 26 | ids: [] 27 | } 28 | this.raw = { 29 | ownBetween: layoutNode.getBetweenTime(), 30 | ownDiameter: radiusFromCircumference(layoutNode.getWithinTime()) * 2 31 | } 32 | 33 | this.shortcutsInStem = layoutNode.node.constructor.name === 'ShortcutNode' ? 1 : 0 34 | 35 | for (const ancestorId of this.ancestors.ids) { 36 | const ancestor = layout.layoutNodes.get(ancestorId) 37 | const ancestorStem = ancestor.stem 38 | 39 | if (ancestor.node.constructor.name === 'ShortcutNode') this.shortcutsInStem++ 40 | 41 | this.ancestors.totalBetween += ancestorStem.raw.ownBetween 42 | this.ancestors.totalDiameter += ancestorStem.raw.ownDiameter 43 | if (!layoutNode.children.length) { 44 | ancestorStem.leaves.ids.push(layoutNode.id) 45 | } 46 | } 47 | this.update() 48 | } 49 | 50 | update () { 51 | if (!this.lengths) { 52 | const absolute = this.getAbsoluteLength() 53 | const scalable = this.ancestors.totalBetween + this.ancestors.totalDiameter + this.raw.ownBetween + this.raw.ownDiameter 54 | this.lengths = { 55 | absolute: validateNumber(absolute, this.getValidationMessage()), 56 | scalable: validateNumber(scalable, this.getValidationMessage()), 57 | rawTotal: absolute + scalable 58 | } 59 | } 60 | if (typeof this.layout.scale.prescaleFactor === 'number') { 61 | this.lengths.prescaledTotal = this.lengths.absolute + (this.lengths.scalable * this.layout.scale.prescaleFactor) 62 | } 63 | if (typeof this.layout.scale.scaleFactor === 'number') { 64 | const { settings, scale } = this.layout 65 | this.scaled = { 66 | ownBetween: (settings.labelMinimumSpace * 2) + settings.lineWidth + scale.getLineLength(this.raw.ownBetween), 67 | ownDiameter: scale.getLineLength(this.raw.ownDiameter) 68 | } 69 | this.lengths.scaledTotal = this.lengths.absolute + (this.lengths.scalable * this.layout.scale.scaleFactor) 70 | } 71 | } 72 | 73 | getAbsoluteLength (stemLength = this.ancestors.ids.length) { 74 | const { 75 | labelMinimumSpace, 76 | lineWidth, 77 | shortcutLength 78 | } = this.layout.settings 79 | return ((labelMinimumSpace * 2) + lineWidth) * stemLength + shortcutLength * this.shortcutsInStem 80 | } 81 | 82 | getValidationMessage () { 83 | return `for stem with: 84 | ancestor ids [${this.ancestors.ids.join(', ')}], length ${this.ancestors.ids.length}); 85 | leaves [${this.leaves.ids.join(', ')}], length ${this.leaves.ids.length}; 86 | ` 87 | } 88 | 89 | pickMostAccurateTotal () { 90 | const { rawTotal, prescaledTotal, scaledTotal } = this.lengths 91 | return scaledTotal || prescaledTotal || rawTotal 92 | } 93 | 94 | static pickLeavesByLongest (layoutNodes) { 95 | const byLongest = (leafA, leafB) => leafB.stem.pickMostAccurateTotal() - leafA.stem.pickMostAccurateTotal() 96 | const byLeafOnly = layoutNode => !layoutNode.children.length 97 | return [...layoutNodes.values()].filter(byLeafOnly).sort(byLongest) 98 | } 99 | } 100 | 101 | module.exports = Stem 102 | -------------------------------------------------------------------------------- /visualizer/main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const drawOuterUI = require('./draw/index.js') 4 | const loadFonts = require('@clinic/clinic-common/behaviours/font-loader') 5 | 6 | // Called on font load or timeout 7 | const drawUi = () => { 8 | document.body.classList.remove('is-loading-font') 9 | document.body.classList.add('is-font-loaded') 10 | 11 | // Currently no headless browser testing, only test browser-independent logic 12 | /* istanbul ignore next */ 13 | const ui = drawOuterUI() 14 | 15 | // TODO: look into moving the below into a Worker to do in parrallel with drawOuterUI 16 | setTimeout(() => { 17 | const loadData = require('./data/index.js') 18 | const generateLayout = require('./layout/index.js') 19 | 20 | const dataSet = loadData({ 21 | debugMode: process.env.DEBUG_MODE 22 | }) 23 | if (dataSet.settings.debugMode) { 24 | window.data = dataSet 25 | console.log('data is exposed on window.data') 26 | } 27 | 28 | const layout = generateLayout(dataSet, Object.assign({ collapseNodes: true }, ui.getSettingsForLayout())) 29 | if (dataSet.settings.debugMode) { 30 | window.layout = layout 31 | console.log('layout is exposed on window.layout') 32 | } 33 | 34 | /* istanbul ignore next */ 35 | ui.setData(layout, dataSet) 36 | 37 | /* istanbul ignore next */ 38 | ui.draw() 39 | 40 | /* istanbul ignore next */ 41 | ui.complete() 42 | }) 43 | } 44 | 45 | // Orchestrate font loading 46 | setTimeout(loadFonts) 47 | 48 | drawUi() 49 | -------------------------------------------------------------------------------- /visualizer/nearform-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /visualizer/validation.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Helper functions for validating data 4 | function isNumber (num) { 5 | return typeof num === 'number' && !Number.isNaN(num) 6 | } 7 | function numberiseIfNumericString (str) { 8 | if (typeof str !== 'string' || !str.length) return str 9 | const num = Number(str) 10 | return isNumber(num) ? num : str 11 | } 12 | function validateKey (key, validOptions) { 13 | if (typeof key !== 'string' || validOptions.indexOf(key) === -1) { 14 | throw new Error(`Invalid key "${key}" (typeof ${typeof key}) passed, valid keys are: ${validOptions.join(', ')}`) 15 | } 16 | return true 17 | } 18 | function validateNumber (num, targetDescription = '', conditions = {}) { 19 | const defaultConditions = { 20 | isFinite: true, 21 | aboveZero: false 22 | } 23 | conditions = Object.assign(defaultConditions, conditions) 24 | if (targetDescription) targetDescription += ': ' 25 | 26 | if (!isNumber(num)) { 27 | throw new Error(`${targetDescription}Got ${typeof num} ${num}, must be a number`) 28 | } 29 | if (conditions.aboveZero && num <= 0) { 30 | throw new Error(`${targetDescription}Got ${num}, must be > 0`) 31 | } 32 | if (conditions.isFinite && !isFinite(num)) { 33 | throw new Error(`${targetDescription}Got ${num}, must be finite`) 34 | } 35 | 36 | return num 37 | } 38 | 39 | // Keep latest key increments in a weak map so they're specific to each object but allow GC 40 | const countersByObj = new WeakMap() 41 | 42 | const mapTest = (key, map) => !map.has(key) 43 | function uniqueMapKey (key, map, separator = '_', startingNum = 0) { 44 | return getUniqueKey(key, map, mapTest, startingNum, separator) 45 | } 46 | 47 | const objectTest = (key, object) => !Object.prototype.hasOwnProperty.call(object, key) 48 | function uniqueObjectKey (key, object, separator = '_', startingNum = 0) { 49 | return getUniqueKey(key, object, objectTest, startingNum, separator) 50 | } 51 | 52 | function getUniqueKey (key, obj, test, startingNum, separator) { 53 | let countersKeyed = countersByObj.get(obj) || {} 54 | if (!countersKeyed) { 55 | countersKeyed = {} 56 | countersByObj.set(obj, countersKeyed) 57 | } 58 | 59 | startingNum = Math.max(countersKeyed[key + separator] || 0, startingNum) 60 | const result = incrementKeyUntilUnique(key, obj, test, startingNum, separator) 61 | countersKeyed[key + separator] = result.counter 62 | return result.testKey 63 | } 64 | 65 | function incrementKeyUntilUnique (key, obj, test, counter, separator) { 66 | const testKey = counter ? ('' + key + separator + counter) : key 67 | if (test(testKey, obj)) { 68 | return { testKey, counter } 69 | } 70 | return incrementKeyUntilUnique(key, obj, test, counter + 1, separator) 71 | } 72 | 73 | function removeFromCounter (id, key, obj, separator = '_') { 74 | const countersKeyed = countersByObj.get(obj) 75 | if (countersKeyed && typeof countersKeyed[key + separator] === 'number') { 76 | const counter = numberiseIfNumericString(id.replace(key + separator, '')) 77 | if (isNumber(counter)) countersKeyed[key + separator] = counter - 1 78 | } 79 | } 80 | 81 | module.exports = { 82 | isNumber, 83 | numberiseIfNumericString, 84 | validateKey, 85 | validateNumber, 86 | uniqueMapKey, 87 | uniqueObjectKey, 88 | removeFromCounter 89 | } 90 | --------------------------------------------------------------------------------