├── codecov.yaml ├── suppressions └── lsan_suppr.txt ├── .github ├── CODEOWNERS ├── workflows │ ├── lint.yml │ ├── pr-labels.yml │ ├── package-size.yml │ ├── release.yml │ └── build.yml └── PULL_REQUEST_TEMPLATE.md ├── .eslintrc.json ├── .eslintignore ├── .gitlab-ci.yml ├── tools ├── build │ ├── Dockerfile.alpine │ ├── Dockerfile.linux │ ├── build.sh │ └── linux_build_and_test.sh ├── kokoro │ ├── system-test │ │ ├── continuous │ │ │ ├── linux.cfg │ │ │ ├── linux-v8-canary.cfg │ │ │ └── linux-prebuild.cfg │ │ └── presubmit │ │ │ ├── linux.cfg │ │ │ └── linux-prebuild.cfg │ └── release │ │ ├── linux.cfg │ │ ├── common.cfg │ │ └── publish.cfg ├── retry.sh └── publish.sh ├── ts ├── test │ ├── check_profile.ts │ ├── worker2.ts │ ├── test-worker-threads.ts │ ├── test-profile-encoder.ts │ ├── oom.ts │ ├── test-heap-profiler.ts │ ├── worker.ts │ └── test-profile-serializer.ts └── src │ ├── time-profiler-bindings.ts │ ├── profile-encoder.ts │ ├── logger.ts │ ├── heap-profiler-bindings.ts │ ├── index.ts │ ├── v8-types.ts │ ├── time-profiler.ts │ ├── heap-profiler.ts │ └── sourcemapper │ └── sourcemapper.ts ├── .gitignore ├── .editorconfig ├── renovate.json ├── tsconfig.json ├── appveyor.yml ├── system-test ├── busybench │ ├── tsconfig.json │ ├── package.json │ └── src │ │ └── busybench.ts ├── Dockerfile.node10-alpine ├── Dockerfile.node12-alpine ├── Dockerfile.node14-alpine ├── Dockerfile.node15-alpine ├── Dockerfile.node16-alpine ├── busybench-js │ ├── package.json │ └── src │ │ └── busybench.js ├── Dockerfile.linux ├── system_test.sh └── test.sh ├── .nycrc ├── scripts ├── .eslintrc.json └── cctest.js ├── benchmark └── sirun │ ├── .eslintrc.json │ ├── runall.sh │ ├── wall-profiler │ ├── meta.json │ └── index.js │ └── run-all-variants.js ├── .prettierrc.js ├── bindings ├── translate-heap-profile.hh ├── contexts.hh ├── translate-time-profile.hh ├── wrap.hh ├── per-isolate-data.hh ├── profilers │ ├── heap.hh │ └── wall.hh ├── binding.cc ├── per-isolate-data.cc ├── defer.hh ├── test │ └── binding.cc ├── profile-translator.hh ├── thread-cpu-clock.hh ├── translate-heap-profile.cc ├── thread-cpu-clock.cc └── translate-time-profile.cc ├── .gitlab └── benchmarks.yml ├── CONTRIBUTING.md ├── package.json ├── .clang-format ├── README.md ├── binding.gyp ├── doc └── sample_context_in_cped.md └── LICENSE /codecov.yaml: -------------------------------------------------------------------------------- 1 | ignore: 2 | proto 3 | -------------------------------------------------------------------------------- /suppressions/lsan_suppr.txt: -------------------------------------------------------------------------------- 1 | leak:CRYPTO_zalloc -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @szegedi @nsavoire @r1viollet 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/coverage 3 | build/ 4 | proto/ 5 | out/ 6 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - benchmarks 3 | 4 | include: ".gitlab/benchmarks.yml" 5 | -------------------------------------------------------------------------------- /tools/build/Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | RUN apk add --no-cache python curl bash python g++ make 3 | -------------------------------------------------------------------------------- /tools/kokoro/system-test/continuous/linux.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | build_file: "pprof-nodejs/system-test/system_test.sh" 4 | -------------------------------------------------------------------------------- /tools/kokoro/system-test/presubmit/linux.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | build_file: "pprof-nodejs/system-test/system_test.sh" 4 | -------------------------------------------------------------------------------- /ts/test/check_profile.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | if (fs.existsSync(process.argv[1])) { 4 | fs.writeFileSync('oom_check.log', 'ok'); 5 | } else { 6 | fs.writeFileSync('oom_check.log', 'ko'); 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | .coverage 3 | .vscode 4 | /*.build 5 | /build 6 | out 7 | node_modules 8 | system-test/busybench/package-lock.json 9 | system-test/busybench-js/package-lock.json 10 | prebuilds 11 | -------------------------------------------------------------------------------- /tools/kokoro/system-test/continuous/linux-v8-canary.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | build_file: "pprof-nodejs/system-test/system_test.sh" 4 | 5 | env_vars { 6 | key: "RUN_ONLY_V8_CANARY_TEST" 7 | value: "true" 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":preserveSemverRanges", 5 | ":pinDigestsDisabled" 6 | ], 7 | "packageRules": [ 8 | { 9 | "extends": "packages:linters", 10 | "groupName": "linters" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | - run: yarn 14 | - run: yarn lint 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/tsconfig-google.json", 3 | "compilerOptions": { 4 | "rootDir": "ts", 5 | "outDir": "out", 6 | "target": "es2020", 7 | "esModuleInterop": true, 8 | }, 9 | "include": [ 10 | "ts/**/*.ts" 11 | ], 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "6" 4 | - nodejs_version: "8" 5 | - nodejs_version: "10" 6 | - nodejs_version: "11" 7 | 8 | install: 9 | - ps: Install-Product node $env:nodejs_version 10 | - npm install 11 | 12 | test_script: 13 | - node --version 14 | - npm --version 15 | - npm test 16 | 17 | build: off 18 | -------------------------------------------------------------------------------- /system-test/busybench/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/tsconfig-google.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "build", 6 | "lib": [ "es2015" ], 7 | "target": "es2015" 8 | }, 9 | "include": [ 10 | "src/*.ts", 11 | "src/**/*.ts", 12 | "test/*.ts", 13 | "test/**/*.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tools/kokoro/system-test/continuous/linux-prebuild.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | before_action { 4 | fetch_keystore { 5 | keystore_resource { 6 | keystore_config_id: 72935 7 | keyname: "cloud-profiler-e2e-service-account-key" 8 | } 9 | } 10 | } 11 | 12 | build_file: "pprof-nodejs/tools/build/linux_build_and_test.sh" 13 | -------------------------------------------------------------------------------- /tools/kokoro/system-test/presubmit/linux-prebuild.cfg: -------------------------------------------------------------------------------- 1 | # Format: //devtools/kokoro/config/proto/build.proto 2 | 3 | before_action { 4 | fetch_keystore { 5 | keystore_resource { 6 | keystore_config_id: 72935 7 | keyname: "cloud-profiler-e2e-service-account-key" 8 | } 9 | } 10 | } 11 | 12 | build_file: "pprof-nodejs/tools/build/linux_build_and_test.sh" 13 | -------------------------------------------------------------------------------- /system-test/Dockerfile.node10-alpine: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine as builder 2 | RUN apk add --no-cache git 3 | WORKDIR /root/ 4 | RUN go get github.com/google/pprof 5 | 6 | 7 | FROM node:10-alpine 8 | 9 | ARG ADDITIONAL_PACKAGES 10 | 11 | RUN apk add --no-cache bash $ADDITIONAL_PACKAGES 12 | WORKDIR /root/ 13 | COPY --from=builder /go/bin/pprof /bin 14 | RUN chmod a+x /bin/pprof 15 | -------------------------------------------------------------------------------- /system-test/Dockerfile.node12-alpine: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine as builder 2 | RUN apk add --no-cache git 3 | WORKDIR /root/ 4 | RUN go get github.com/google/pprof 5 | 6 | 7 | FROM node:12-alpine 8 | 9 | ARG ADDITIONAL_PACKAGES 10 | 11 | RUN apk add --no-cache bash $ADDITIONAL_PACKAGES 12 | WORKDIR /root/ 13 | COPY --from=builder /go/bin/pprof /bin 14 | RUN chmod a+x /bin/pprof 15 | -------------------------------------------------------------------------------- /system-test/Dockerfile.node14-alpine: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine as builder 2 | RUN apk add --no-cache git 3 | WORKDIR /root/ 4 | RUN go get github.com/google/pprof 5 | 6 | 7 | FROM node:14-alpine 8 | 9 | ARG ADDITIONAL_PACKAGES 10 | 11 | RUN apk add --no-cache bash $ADDITIONAL_PACKAGES 12 | WORKDIR /root/ 13 | COPY --from=builder /go/bin/pprof /bin 14 | RUN chmod a+x /bin/pprof 15 | -------------------------------------------------------------------------------- /system-test/Dockerfile.node15-alpine: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine as builder 2 | RUN apk add --no-cache git 3 | WORKDIR /root/ 4 | RUN go get github.com/google/pprof 5 | 6 | 7 | FROM node:15-alpine 8 | 9 | ARG ADDITIONAL_PACKAGES 10 | 11 | RUN apk add --no-cache bash $ADDITIONAL_PACKAGES 12 | WORKDIR /root/ 13 | COPY --from=builder /go/bin/pprof /bin 14 | RUN chmod a+x /bin/pprof 15 | -------------------------------------------------------------------------------- /system-test/Dockerfile.node16-alpine: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine as builder 2 | RUN apk add --no-cache git 3 | WORKDIR /root/ 4 | RUN go get github.com/google/pprof 5 | 6 | 7 | FROM node:16-alpine 8 | 9 | ARG ADDITIONAL_PACKAGES 10 | 11 | RUN apk add --no-cache bash $ADDITIONAL_PACKAGES 12 | WORKDIR /root/ 13 | COPY --from=builder /go/bin/pprof /bin 14 | RUN chmod a+x /bin/pprof 15 | -------------------------------------------------------------------------------- /system-test/busybench-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "busybench", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": {}, 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "engines": { 14 | "node": ">=12" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/pr-labels.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Labels 2 | on: 3 | pull_request: 4 | types: [opened, labeled, unlabeled, synchronize] 5 | branches: 6 | - 'main' 7 | jobs: 8 | label: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: mheap/github-action-required-labels@v1 12 | with: 13 | mode: exactly 14 | count: 1 15 | labels: "semver-patch, semver-minor, semver-major" 16 | -------------------------------------------------------------------------------- /tools/build/Dockerfile.linux: -------------------------------------------------------------------------------- 1 | # Docker image on which pre-compiled binaries for non-alpine linux are built. 2 | # An older Docker image is used intentionally, because the resulting binaries 3 | # are dynamically linked to certain C++ libraries, like libstdc++. Using an 4 | # older docker image allows for some backwards compatibility. 5 | 6 | # node:14-stretch images has dependencies required to build pre-built binaries 7 | # already installed. 8 | FROM node:14-stretch 9 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "report-dir": "./.coverage", 3 | "reporter": "lcov", 4 | "exclude": [ 5 | "src/*{/*,/**/*}.js", 6 | "src/*/v*/*.js", 7 | "test/**/*.js", 8 | "build/test" 9 | ], 10 | "watermarks": { 11 | "branches": [ 12 | 95, 13 | 100 14 | ], 15 | "functions": [ 16 | 95, 17 | 100 18 | ], 19 | "lines": [ 20 | 95, 21 | 100 22 | ], 23 | "statements": [ 24 | 95, 25 | 100 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scripts/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "ecmaVersion": 2020 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "standard" 9 | ], 10 | "env": { 11 | "node" : true 12 | }, 13 | "rules": { 14 | "max-len": [2, 120, 2], 15 | "no-var": 2, 16 | "no-console": 2, 17 | "prefer-const": 2, 18 | "object-curly-spacing": [2, "always"], 19 | "import/no-extraneous-dependencies": 2, 20 | "standard/no-callback-literal": 0, 21 | "no-prototype-builtins": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /benchmark/sirun/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "ecmaVersion": 2020 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "standard" 9 | ], 10 | "env": { 11 | "node" : true 12 | }, 13 | "rules": { 14 | "max-len": [2, 120, 2], 15 | "no-var": 2, 16 | "no-console": 2, 17 | "prefer-const": 2, 18 | "object-curly-spacing": [2, "always"], 19 | "import/no-extraneous-dependencies": 2, 20 | "standard/no-callback-literal": 0, 21 | "no-prototype-builtins": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **What does this PR do?**: 2 | 3 | 4 | **Motivation**: 5 | 6 | 7 | **Additional Notes**: 8 | 9 | 10 | **How to test the change?**: 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /system-test/busybench/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "busybench", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "build/src/busybench.js", 6 | "types": "build/src/busybench.d.ts", 7 | "files": [ 8 | "build/src" 9 | ], 10 | "license": "Apache-2.0", 11 | "keywords": [], 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1", 14 | "check": "gts check", 15 | "clean": "gts clean", 16 | "compile": "tsc -p .", 17 | "fix": "gts fix", 18 | "prepare": "npm run compile", 19 | "pretest": "npm run compile", 20 | "posttest": "npm run check" 21 | }, 22 | "devDependencies": {}, 23 | "dependencies": {}, 24 | "engines": { 25 | "node": ">=12" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module.exports = { 16 | endOfLine:"auto", 17 | ...require('gts/.prettierrc.json') 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/package-size.yml: -------------------------------------------------------------------------------- 1 | name: Package Size 2 | 3 | on: 4 | pull_request: 5 | schedule: 6 | - cron: '0 4 * * *' 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref || github.run_id }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | package-size-report: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: '22' 23 | - run: yarn 24 | - name: Compute module size tree and report 25 | uses: qard/heaviest-objects-in-the-universe@v1 26 | with: 27 | github-token: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /benchmark/sirun/runall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | if [ -n "${MAJOR_NODE_VERSION:-}" ]; then 6 | if test -f ~/.nvm/nvm.sh; then 7 | source ~/.nvm/nvm.sh 8 | else 9 | source "${NVM_DIR:-usr/local/nvm}/nvm.sh" 10 | fi 11 | 12 | nvm use "${MAJOR_NODE_VERSION}" 13 | 14 | pushd ../../ 15 | npm install 16 | popd 17 | fi 18 | 19 | VERSION=$(node -v) 20 | echo "using Node.js ${VERSION}" 21 | 22 | for d in *; do 23 | if [ -d "${d}" ]; then 24 | pushd "$d" 25 | time node ../run-all-variants.js >> ../results.ndjson 26 | popd 27 | fi 28 | done 29 | 30 | if [ "${DEBUG_RESULTS:-false}" == "true" ]; then 31 | echo "Benchmark Results:" 32 | cat ./results.ndjson 33 | fi 34 | 35 | echo "all tests for ${VERSION} have now completed." 36 | -------------------------------------------------------------------------------- /tools/retry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | retry() { 17 | for attempt in {1..3}; do 18 | [ $attempt == 1 ] || sleep 10 # Backing off after a failed attempt. 19 | "${@}" && return 0 20 | done 21 | return 1 22 | } 23 | -------------------------------------------------------------------------------- /system-test/Dockerfile.linux: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-stretch as builder 2 | RUN apt-get update && apt-get install -y \ 3 | git \ 4 | && rm -rf /var/lib/apt/lists/* 5 | WORKDIR /root/ 6 | RUN go get github.com/google/pprof 7 | 8 | FROM debian:stretch 9 | 10 | ARG NODE_VERSION 11 | ARG NVM_NODEJS_ORG_MIRROR 12 | ARG ADDITIONAL_PACKAGES 13 | ARG VERIFY_TIME_LINE_NUMBERS 14 | 15 | RUN apt-get update && apt-get install -y curl $ADDITIONAL_PACKAGES \ 16 | && rm -rf /var/lib/apt/lists/* 17 | 18 | ENV NVM_DIR /bin/.nvm 19 | RUN mkdir -p $NVM_DIR 20 | 21 | 22 | # Install nvm with node and npm 23 | RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash \ 24 | && . $NVM_DIR/nvm.sh \ 25 | && nvm install $NODE_VERSION 26 | 27 | ENV BASH_ENV /root/.bashrc 28 | 29 | WORKDIR /root/ 30 | COPY --from=builder /go/bin/pprof /bin 31 | -------------------------------------------------------------------------------- /bindings/translate-heap-profile.hh: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Datadog, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include 20 | 21 | namespace dd { 22 | 23 | v8::Local TranslateAllocationProfile( 24 | v8::AllocationProfile::Node* node); 25 | 26 | } // namespace dd 27 | -------------------------------------------------------------------------------- /tools/kokoro/release/linux.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Kokoro config for job in release workflow which builds non-alpine Linux 16 | # binaries. 17 | 18 | # Location of the build script in this repository. 19 | build_file: "pprof-nodejs/tools/build/linux_build_and_test.sh" 20 | -------------------------------------------------------------------------------- /tools/kokoro/release/common.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | before_action { 16 | fetch_keystore { 17 | keystore_resource { 18 | keystore_config_id: 72935 19 | keyname: "cloud-profiler-e2e-service-account-key" 20 | } 21 | } 22 | } 23 | 24 | env_vars { 25 | key: "BUILD_TYPE" 26 | value: "release" 27 | } 28 | -------------------------------------------------------------------------------- /scripts/cctest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { execSync } = require('child_process') 4 | const { existsSync } = require('fs') 5 | const { join } = require('path') 6 | 7 | const name = process.argv[2] || 'test_dd_pprof' 8 | 9 | const cmd = [ 10 | 'node-gyp', 11 | 'configure', 12 | 'build', 13 | '--build_tests' 14 | ].join(' ') 15 | 16 | execSync(cmd, { stdio: [0, 1, 2] }) 17 | 18 | function findBuild (mode) { 19 | const path = join(__dirname, '..', 'build', mode, name) + '.node' 20 | if (!existsSync(path)) { 21 | // eslint-disable-next-line no-console 22 | console.warn(`No ${mode} binary found for ${name} at: ${path}`) 23 | return 24 | } 25 | return path 26 | } 27 | 28 | const path = findBuild('Release') || findBuild('Debug') 29 | if (!path) { 30 | // eslint-disable-next-line no-console 31 | console.error(`No ${name} build found`) 32 | process.exitCode = 1 33 | } else { 34 | execSync(`node ${path}`, { stdio: [0, 1, 2] }) 35 | } 36 | -------------------------------------------------------------------------------- /bindings/contexts.hh: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Datadog, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include 20 | #include 21 | 22 | namespace dd { 23 | 24 | struct NodeInfo { 25 | v8::Local contexts; 26 | uint32_t hitcount; 27 | }; 28 | 29 | using ContextsByNode = std::unordered_map; 30 | } // namespace dd 31 | -------------------------------------------------------------------------------- /tools/kokoro/release/publish.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Kokoro config for job in release workflow which publishes module to npm. 16 | 17 | # Get npm token from Keystore 18 | before_action { 19 | fetch_keystore { 20 | keystore_resource { 21 | keystore_config_id: 72935 22 | keyname: "pprof-npm-token" 23 | } 24 | } 25 | } 26 | 27 | build_file: "pprof-nodejs/tools/publish.sh" 28 | -------------------------------------------------------------------------------- /ts/src/time-profiler-bindings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import {join} from 'path'; 17 | 18 | const findBinding = require('node-gyp-build'); 19 | const profiler = findBinding(join(__dirname, '..', '..')); 20 | 21 | export const TimeProfiler = profiler.TimeProfiler; 22 | export const constants = profiler.constants; 23 | export const getNativeThreadId = profiler.getNativeThreadId; 24 | -------------------------------------------------------------------------------- /bindings/translate-time-profile.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include 20 | #include "contexts.hh" 21 | 22 | namespace dd { 23 | 24 | v8::Local TranslateTimeProfile( 25 | const v8::CpuProfile* profile, 26 | bool includeLineInfo, 27 | ContextsByNode* contextsByNode = nullptr, 28 | bool hasCpuTime = false, 29 | int64_t nonJSThreadsCpuTime = 0); 30 | 31 | } // namespace dd 32 | -------------------------------------------------------------------------------- /ts/src/profile-encoder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {promisify} from 'util'; 18 | import {gzip, gzipSync} from 'zlib'; 19 | 20 | import {Profile} from 'pprof-format'; 21 | 22 | const gzipPromise = promisify(gzip); 23 | 24 | export function encode(profile: Profile): Promise { 25 | return profile.encodeAsync().then(gzipPromise); 26 | } 27 | 28 | export function encodeSync(profile: Profile): Buffer { 29 | return gzipSync(profile.encode()); 30 | } 31 | -------------------------------------------------------------------------------- /bindings/wrap.hh: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Datadog, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include // cppcheck-suppress missingIncludeSystem 20 | 21 | namespace dd { 22 | 23 | class LabelWrap { 24 | protected: 25 | v8::Global handle_; 26 | 27 | public: 28 | LabelWrap(v8::Local object) 29 | : handle_(v8::Isolate::GetCurrent(), object) {} 30 | 31 | v8::Local handle() { 32 | return handle_.Get(v8::Isolate::GetCurrent()); 33 | } 34 | }; 35 | 36 | }; // namespace dd 37 | -------------------------------------------------------------------------------- /ts/test/worker2.ts: -------------------------------------------------------------------------------- 1 | import {parentPort} from 'node:worker_threads'; 2 | import {time} from '../src/index'; 3 | import {satisfies} from 'semver'; 4 | 5 | const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); 6 | 7 | const DURATION_MILLIS = 1000; 8 | const INTERVAL_MICROS = 10000; 9 | const withContexts = 10 | process.platform === 'darwin' || process.platform === 'linux'; 11 | 12 | const useCPED = 13 | withContexts && 14 | ((satisfies(process.versions.node, '>=24.0.0') && 15 | !process.execArgv.includes('--no-async-context-frame')) || 16 | (satisfies(process.versions.node, '>=22.7.0') && 17 | process.execArgv.includes('--experimental-async-context-frame'))); 18 | 19 | const collectAsyncId = 20 | withContexts && satisfies(process.versions.node, '>=24.0.0'); 21 | 22 | time.start({ 23 | durationMillis: DURATION_MILLIS, 24 | intervalMicros: INTERVAL_MICROS, 25 | withContexts: withContexts, 26 | collectCpuTime: withContexts, 27 | collectAsyncId: collectAsyncId, 28 | useCPED: useCPED, 29 | }); 30 | 31 | parentPort?.on('message', () => { 32 | delay(50).then(() => { 33 | parentPort?.postMessage('hello'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /ts/src/logger.ts: -------------------------------------------------------------------------------- 1 | export interface Logger { 2 | error(...args: Array<{}>): void; 3 | trace(...args: Array<{}>): void; 4 | debug(...args: Array<{}>): void; 5 | info(...args: Array<{}>): void; 6 | warn(...args: Array<{}>): void; 7 | fatal(...args: Array<{}>): void; 8 | } 9 | 10 | export class NullLogger implements Logger { 11 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 12 | info(...args: Array<{}>): void { 13 | return; 14 | } 15 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 16 | error(...args: Array<{}>): void { 17 | return; 18 | } 19 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 20 | trace(...args: Array<{}>): void { 21 | return; 22 | } 23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 24 | warn(...args: Array<{}>): void { 25 | return; 26 | } 27 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 28 | fatal(...args: Array<{}>): void { 29 | return; 30 | } 31 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 32 | debug(...args: Array<{}>): void { 33 | return; 34 | } 35 | } 36 | 37 | export let logger = new NullLogger(); 38 | 39 | export function setLogger(newLogger: Logger) { 40 | logger = newLogger; 41 | } 42 | -------------------------------------------------------------------------------- /bindings/per-isolate-data.hh: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Datadog, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | namespace dd { 25 | 26 | struct HeapProfilerState; 27 | 28 | class PerIsolateData { 29 | private: 30 | Nan::Global wall_profiler_constructor; 31 | std::shared_ptr heap_profiler_state; 32 | 33 | PerIsolateData() {} 34 | 35 | public: 36 | static PerIsolateData* For(v8::Isolate* isolate); 37 | 38 | Nan::Global& WallProfilerConstructor(); 39 | std::shared_ptr& GetHeapProfilerState(); 40 | }; 41 | 42 | } // namespace dd 43 | -------------------------------------------------------------------------------- /bindings/profilers/heap.hh: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include 20 | 21 | namespace dd { 22 | 23 | class HeapProfiler { 24 | public: 25 | // Signature: 26 | // startSamplingHeapProfiler() 27 | static NAN_METHOD(StartSamplingHeapProfiler); 28 | 29 | // Signature: 30 | // stopSamplingHeapProfiler() 31 | static NAN_METHOD(StopSamplingHeapProfiler); 32 | 33 | // Signature: 34 | // getAllocationProfile(): AllocationProfileNode 35 | static NAN_METHOD(GetAllocationProfile); 36 | 37 | static NAN_METHOD(MonitorOutOfMemory); 38 | 39 | static NAN_MODULE_INIT(Init); 40 | }; 41 | 42 | } // namespace dd 43 | -------------------------------------------------------------------------------- /tools/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | . $(dirname $0)/retry.sh 18 | 19 | set -eo pipefail 20 | 21 | # Install desired version of Node.js 22 | retry curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash >/dev/null 23 | export NVM_DIR="$HOME/.nvm" >/dev/null 24 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" >/dev/null 25 | 26 | retry nvm install 10 &>/dev/null 27 | 28 | cd $(dirname $0)/.. 29 | 30 | NPM_TOKEN=$(cat $KOKORO_KEYSTORE_DIR/72935_pprof-npm-token) 31 | echo "//wombat-dressing-room.appspot.com/:_authToken=${NPM_TOKEN}" > ~/.npmrc 32 | 33 | retry npm install --quiet 34 | npm publish --access=public \ 35 | --registry=https://wombat-dressing-room.appspot.com 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - v[0-9]+.x 7 | 8 | jobs: 9 | build: 10 | uses: Datadog/action-prebuildify/.github/workflows/build.yml@main 11 | with: 12 | target-name: 'dd_pprof' # target name in binding.gyp 13 | package-manager: 'npm' # npm or yarn 14 | cache: true # enable caching of dependencies based on lockfile 15 | min-node-version: 18 16 | skip: 'linux-arm,linux-ia32' # skip building for these platforms 17 | 18 | publish: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | environment: npm 22 | permissions: 23 | id-token: write # Required for OIDC 24 | contents: write 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: actions/download-artifact@v4 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: '24' 31 | registry-url: 'https://registry.npmjs.org' 32 | - run: npm install 33 | - run: npm publish 34 | - id: pkg 35 | run: | 36 | content=`cat ./package.json | tr '\n' ' '` 37 | echo "json=$content" >> $GITHUB_OUTPUT 38 | - run: | 39 | git tag v${{ fromJson(steps.pkg.outputs.json).version }} 40 | git push origin v${{ fromJson(steps.pkg.outputs.json).version }} 41 | -------------------------------------------------------------------------------- /benchmark/sirun/wall-profiler/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "profiler", 3 | "run": "node index.js", 4 | "cachegrind": true, 5 | "iterations": 10, 6 | "variants": { 7 | "idle-no-wall-profiler": { 8 | "env": { 9 | "CONCURRENCY": "0", 10 | "REQUEST_FREQUENCY": "0", 11 | "SAMPLE_FREQUENCY": "0" 12 | } 13 | }, 14 | "idle-with-wall-profiler": { 15 | "env": { 16 | "CONCURRENCY": "0", 17 | "REQUEST_FREQUENCY": "0", 18 | "SAMPLE_FREQUENCY": "999" 19 | } 20 | }, 21 | "light-load-no-wall-profiler": { 22 | "env": { 23 | "CONCURRENCY": "5", 24 | "REQUEST_FREQUENCY": "5", 25 | "SAMPLE_FREQUENCY": "0" 26 | } 27 | }, 28 | "light-load-with-wall-profiler": { 29 | "env": { 30 | "CONCURRENCY": "5", 31 | "REQUEST_FREQUENCY": "5", 32 | "SAMPLE_FREQUENCY": "999" 33 | } 34 | }, 35 | "heavy-load-no-wall-profiler": { 36 | "env": { 37 | "CONCURRENCY": "15", 38 | "REQUEST_FREQUENCY": "50", 39 | "SAMPLE_FREQUENCY": "0" 40 | } 41 | }, 42 | "heavy-load-with-wall-profiler": { 43 | "env": { 44 | "CONCURRENCY": "15", 45 | "REQUEST_FREQUENCY": "50", 46 | "SAMPLE_FREQUENCY": "999" 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tools/build/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Fail on any error. 18 | set -e pipefail 19 | 20 | # Display commands 21 | set -x 22 | 23 | cd $(dirname $0)/../.. 24 | BASE_DIR=$PWD 25 | 26 | ARTIFACTS_OUT="${BASE_DIR}/artifacts" 27 | mkdir -p "$ARTIFACTS_OUT" 28 | 29 | npm install --quiet 30 | 31 | for version in 10.0.0 12.0.0 14.0.0 15.0.0 16.0.0 32 | do 33 | ./node_modules/.bin/node-pre-gyp configure rebuild package \ 34 | --target=$version --target_arch="x64" 35 | cp -r build/stage/* "${ARTIFACTS_OUT}/" 36 | rm -rf build 37 | done 38 | 39 | # Remove node_modules directory. When this script is run in a docker container 40 | # with user root, then a system test running after this script cannot run npm 41 | # install. 42 | rm -r node_modules 43 | -------------------------------------------------------------------------------- /ts/test/test-worker-threads.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line node/no-unsupported-features/node-builtins 2 | import {execFile} from 'child_process'; 3 | import {promisify} from 'util'; 4 | import {Worker} from 'worker_threads'; 5 | 6 | const exec = promisify(execFile); 7 | 8 | describe('Worker Threads', () => { 9 | // eslint-ignore-next-line prefer-array-callback 10 | it('should work', function () { 11 | this.timeout(20000); 12 | const nbWorkers = 2; 13 | return exec('node', ['./out/test/worker.js', String(nbWorkers)]); 14 | }); 15 | 16 | it('should not crash when worker is terminated', async function () { 17 | this.timeout(30000); 18 | const nruns = 5; 19 | const concurrentWorkers = 20; 20 | for (let i = 0; i < nruns; i++) { 21 | const workers = []; 22 | for (let j = 0; j < concurrentWorkers; j++) { 23 | const worker = new Worker('./out/test/worker2.js'); 24 | worker.postMessage('hello'); 25 | 26 | worker.on('message', () => { 27 | worker.terminate(); 28 | }); 29 | 30 | workers.push( 31 | new Promise((resolve, reject) => { 32 | worker.on('exit', exitCode => { 33 | if (exitCode === 1) { 34 | resolve(); 35 | } else { 36 | reject(new Error('Worker exited with code 0')); 37 | } 38 | }); 39 | }) 40 | ); 41 | } 42 | await Promise.all(workers); 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /bindings/binding.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Datadog, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include "profilers/heap.hh" 22 | #include "profilers/wall.hh" 23 | 24 | #ifdef __linux__ 25 | #include 26 | #include 27 | #endif 28 | 29 | static NAN_METHOD(GetNativeThreadId) { 30 | #ifdef __APPLE__ 31 | uint64_t native_id; 32 | (void)pthread_threadid_np(NULL, &native_id); 33 | #elif defined(__linux__) 34 | pid_t native_id = syscall(SYS_gettid); 35 | #elif defined(_MSC_VER) 36 | DWORD native_id = GetCurrentThreadId(); 37 | #endif 38 | info.GetReturnValue().Set(v8::Integer::New(info.GetIsolate(), native_id)); 39 | } 40 | 41 | #if defined(__GNUC__) && !defined(__clang__) 42 | #pragma GCC diagnostic push 43 | #pragma GCC diagnostic ignored "-Wcast-function-type" 44 | #endif 45 | NODE_MODULE_INIT(/* exports, module, context */) { 46 | #if defined(__GNUC__) && !defined(__clang__) 47 | #pragma GCC diagnostic pop 48 | #endif 49 | 50 | dd::HeapProfiler::Init(exports); 51 | dd::WallProfiler::Init(exports); 52 | Nan::SetMethod(exports, "getNativeThreadId", GetNativeThreadId); 53 | } 54 | -------------------------------------------------------------------------------- /ts/test/test-profile-encoder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {promisify} from 'util'; 18 | import {gunzip as gunzipCallback, gunzipSync} from 'zlib'; 19 | 20 | import {Profile} from 'pprof-format'; 21 | import {encode, encodeSync} from '../src/profile-encoder'; 22 | 23 | import {decodedTimeProfile, timeProfile} from './profiles-for-tests'; 24 | 25 | const assert = require('assert'); 26 | const gunzip = promisify(gunzipCallback); 27 | 28 | describe('profile-encoded', () => { 29 | describe('encode', () => { 30 | it('should encode profile such that the encoded profile can be decoded', async () => { 31 | const encoded = await encode(timeProfile); 32 | const unzipped = await gunzip(encoded); 33 | const decoded = Profile.decode(unzipped); 34 | assert.deepEqual(decoded, decodedTimeProfile); 35 | }); 36 | }); 37 | describe('encodeSync', () => { 38 | it('should encode profile such that the encoded profile can be decoded', () => { 39 | const encoded = encodeSync(timeProfile); 40 | const unzipped = gunzipSync(encoded); 41 | const decoded = Profile.decode(unzipped); 42 | assert.deepEqual(decoded, decodedTimeProfile); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /system-test/system_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Trap all errors. 4 | trap "echo '** AT LEAST ONE OF TESTS FAILED **'" ERR 5 | 6 | # Fail on any error, show commands run. 7 | set -eox pipefail 8 | 9 | . $(dirname $0)/../tools/retry.sh 10 | 11 | cd $(dirname $0) 12 | 13 | if [[ -z "$BINARY_HOST" ]]; then 14 | ADDITIONAL_PACKAGES="python3 g++ make" 15 | fi 16 | 17 | if [[ "$RUN_ONLY_V8_CANARY_TEST" == "true" ]]; then 18 | NVM_NODEJS_ORG_MIRROR="https://nodejs.org/download/v8-canary" 19 | NODE_VERSIONS=(node) 20 | else 21 | NODE_VERSIONS=(10 12 14 15 16) 22 | fi 23 | 24 | for i in ${NODE_VERSIONS[@]}; do 25 | # Test Linux support for the given node version. 26 | retry docker build -f Dockerfile.linux --build-arg NODE_VERSION=$i \ 27 | --build-arg ADDITIONAL_PACKAGES="$ADDITIONAL_PACKAGES" \ 28 | --build-arg NVM_NODEJS_ORG_MIRROR="$NVM_NODEJS_ORG_MIRROR" \ 29 | -t node$i-linux . 30 | 31 | docker run -v $PWD/..:/src -e BINARY_HOST="$BINARY_HOST" node$i-linux \ 32 | /src/system-test/test.sh 33 | 34 | # Test support for accurate line numbers with node versions supporting this 35 | # feature. 36 | if [ "$i" != "10" ]; then 37 | docker run -v $PWD/..:/src -e BINARY_HOST="$BINARY_HOST" \ 38 | -e VERIFY_TIME_LINE_NUMBERS="true" node$i-linux \ 39 | /src/system-test/test.sh 40 | fi 41 | 42 | # Skip running on alpine if NVM_NODEJS_ORG_MIRROR is specified. 43 | if [[ ! -z "$NVM_NODEJS_ORG_MIRROR" ]]; then 44 | continue 45 | fi 46 | 47 | # Test Alpine support for the given node version. 48 | retry docker build -f Dockerfile.node$i-alpine \ 49 | --build-arg ADDITIONAL_PACKAGES="$ADDITIONAL_PACKAGES" -t node$i-alpine . 50 | 51 | docker run -v $PWD/..:/src -e BINARY_HOST="$BINARY_HOST" node$i-alpine \ 52 | /src/system-test/test.sh 53 | done 54 | 55 | echo '** ALL TESTS PASSED **' 56 | -------------------------------------------------------------------------------- /ts/test/oom.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-console */ 4 | import {Worker, isMainThread, threadId} from 'worker_threads'; 5 | import {heap} from '../src/index'; 6 | import path from 'path'; 7 | 8 | const nworkers = Number(process.argv[2] || 0); 9 | const workerMaxOldGenerationSizeMb = process.argv[3]; 10 | const maxCount = Number(process.argv[4] || 12); 11 | const sleepMs = Number(process.argv[5] || 50); 12 | const sizeQuantum = Number(process.argv[6] || 5 * 1024 * 1024); 13 | 14 | console.log(`${isMainThread ? 'Main thread' : `Worker ${threadId}`}: \ 15 | nworkers=${nworkers} workerMaxOldGenerationSizeMb=${workerMaxOldGenerationSizeMb} \ 16 | maxCount=${maxCount} sleepMs=${sleepMs} sizeQuantum=${sizeQuantum}`); 17 | 18 | heap.start(1024 * 1024, 64); 19 | heap.monitorOutOfMemory(0, 0, false, [ 20 | process.execPath, 21 | path.join(__dirname, 'check_profile.js'), 22 | ]); 23 | 24 | if (isMainThread) { 25 | for (let i = 0; i < nworkers; i++) { 26 | const worker = new Worker(__filename, { 27 | argv: [0, ...process.argv.slice(3)], 28 | ...(workerMaxOldGenerationSizeMb 29 | ? {resourceLimits: {maxOldGenerationSizeMb: 50}} 30 | : {}), 31 | }); 32 | const threadId = worker.threadId; 33 | worker 34 | .on('error', err => { 35 | console.log(`Worker ${threadId} error: ${err}`); 36 | }) 37 | .on('exit', code => { 38 | console.log(`Worker ${threadId} exit: ${code}`); 39 | }); 40 | } 41 | } 42 | 43 | const leak: number[][] = []; 44 | let count = 0; 45 | 46 | function foo(size: number) { 47 | count += 1; 48 | const n = size / 8; 49 | const x: number[] = []; 50 | x.length = n; 51 | for (let i = 0; i < n; i++) { 52 | x[i] = Math.random(); 53 | } 54 | leak.push(x); 55 | 56 | if (count < maxCount) { 57 | setTimeout(() => foo(size), sleepMs); 58 | } 59 | } 60 | 61 | setTimeout(() => foo(sizeQuantum), sleepMs); 62 | -------------------------------------------------------------------------------- /bindings/per-isolate-data.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Datadog, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include "per-isolate-data.hh" 22 | 23 | namespace dd { 24 | 25 | static std::unordered_map per_isolate_data_; 26 | static std::mutex mutex; 27 | 28 | PerIsolateData* PerIsolateData::For(v8::Isolate* isolate) { 29 | const std::lock_guard lock(mutex); 30 | auto maybe = per_isolate_data_.find(isolate); 31 | if (maybe != per_isolate_data_.end()) { 32 | return &maybe->second; 33 | } 34 | 35 | per_isolate_data_.emplace(std::make_pair(isolate, PerIsolateData())); 36 | 37 | auto pair = per_isolate_data_.find(isolate); 38 | auto perIsolateData = &pair->second; 39 | 40 | node::AddEnvironmentCleanupHook( 41 | isolate, 42 | [](void* data) { 43 | const std::lock_guard lock(mutex); 44 | per_isolate_data_.erase(static_cast(data)); 45 | }, 46 | isolate); 47 | 48 | return perIsolateData; 49 | } 50 | 51 | Nan::Global& PerIsolateData::WallProfilerConstructor() { 52 | return wall_profiler_constructor; 53 | } 54 | 55 | std::shared_ptr& PerIsolateData::GetHeapProfilerState() { 56 | return heap_profiler_state; 57 | } 58 | 59 | } // namespace dd 60 | -------------------------------------------------------------------------------- /benchmark/sirun/run-all-variants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const childProcess = require('child_process') 4 | const path = require('path') 5 | const readline = require('readline') 6 | 7 | process.env.DD_TRACE_TELEMETRY_ENABLED = 'false' 8 | 9 | function exec (...args) { 10 | return new Promise((resolve, reject) => { 11 | const proc = childProcess.spawn(...args) 12 | streamAddVersion(proc.stdout) 13 | proc.on('error', reject) 14 | proc.on('exit', (code) => { 15 | if (code === 0) { 16 | resolve() 17 | } else { 18 | reject(new Error('Process exited with non-zero code.')) 19 | } 20 | }) 21 | }) 22 | } 23 | 24 | const metaJson = require(path.join(process.cwd(), 'meta.json')) 25 | 26 | const env = Object.assign({}, process.env, { DD_TRACE_STARTUP_LOGS: 'false' }) 27 | 28 | function streamAddVersion (input) { 29 | input.rl = readline.createInterface({ input }) 30 | input.rl.on('line', function (line) { 31 | try { 32 | const json = JSON.parse(line.toString()) 33 | json.nodeVersion = process.versions.node 34 | // eslint-disable-next-line no-console 35 | console.log(JSON.stringify(json)) 36 | } catch (e) { 37 | // eslint-disable-next-line no-console 38 | console.log(line) 39 | } 40 | }) 41 | } 42 | 43 | function getStdio () { 44 | return ['inherit', 'pipe', 'inherit'] 45 | } 46 | 47 | (async () => { 48 | try { 49 | if (metaJson.variants) { 50 | const variants = metaJson.variants 51 | for (const variant in variants) { 52 | const variantEnv = Object.assign({}, env, { SIRUN_VARIANT: variant }) 53 | await exec('sirun', ['meta.json'], { env: variantEnv, stdio: getStdio() }) 54 | } 55 | } else { 56 | await exec('sirun', ['meta.json'], { env, stdio: getStdio() }) 57 | } 58 | } catch (e) { 59 | setImmediate(() => { 60 | throw e // Older Node versions don't fail on uncaught promise rejections. 61 | }) 62 | } 63 | })() 64 | -------------------------------------------------------------------------------- /bindings/defer.hh: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Datadog, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include 20 | 21 | namespace details { 22 | 23 | struct DeferDummy {}; 24 | 25 | template 26 | class DeferHolder { 27 | public: 28 | DeferHolder(DeferHolder&&) = default; 29 | DeferHolder(const DeferHolder&) = delete; 30 | DeferHolder& operator=(DeferHolder&&) = delete; 31 | DeferHolder& operator=(const DeferHolder&) = delete; 32 | 33 | template 34 | explicit DeferHolder(T&& f) : _func(std::forward(f)) {} 35 | 36 | ~DeferHolder() { reset(); } 37 | 38 | void reset() { 39 | if (_active) { 40 | _func(); 41 | _active = false; 42 | } 43 | } 44 | 45 | void release() { _active = false; } 46 | 47 | private: 48 | F _func; 49 | bool _active = true; 50 | }; 51 | 52 | template 53 | DeferHolder operator*(DeferDummy, F&& f) { 54 | return DeferHolder{std::forward(f)}; 55 | } 56 | 57 | } // namespace details 58 | 59 | template 60 | details::DeferHolder make_defer(F&& f) { 61 | return details::DeferHolder{std::forward(f)}; 62 | } 63 | 64 | #define DEFER_(LINE) zz_defer##LINE 65 | #define DEFER(LINE) DEFER_(LINE) 66 | #define defer \ 67 | [[maybe_unused]] const auto& DEFER(__COUNTER__) = details::DeferDummy{}* [&]() 68 | -------------------------------------------------------------------------------- /bindings/test/binding.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Datadog, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include "nan.h" 22 | #include "node.h" 23 | #include "tap.h" 24 | #include "v8.h" 25 | 26 | #if defined(__GNUC__) && !defined(__clang__) 27 | #pragma GCC diagnostic push 28 | #pragma GCC diagnostic ignored "-Wcast-function-type" 29 | #endif 30 | NODE_MODULE_INIT(/* exports, module, context */) { 31 | #if defined(__GNUC__) && !defined(__clang__) 32 | #pragma GCC diagnostic pop 33 | #endif 34 | 35 | Tap t; 36 | const char* env_var = std::getenv("TEST"); 37 | std::string name(env_var == nullptr ? "" : env_var); 38 | 39 | std::unordered_map> tests = {}; 40 | 41 | if (name.empty()) { 42 | t.plan(tests.size()); 43 | for (auto test : tests) { 44 | t.test(test.first, test.second); 45 | } 46 | } else { 47 | t.plan(1); 48 | if (tests.count(name)) { 49 | t.test(name, tests[name]); 50 | } else { 51 | std::ostringstream s; 52 | s << "Unknown test: " << name; 53 | t.fail(s.str()); 54 | } 55 | } 56 | 57 | // End test and set `process.exitCode` 58 | int exitCode = t.end(); 59 | auto processKey = Nan::New("process").ToLocalChecked(); 60 | auto process = Nan::Get(context->Global(), processKey).ToLocalChecked(); 61 | Nan::Set(process.As(), 62 | Nan::New("exitCode").ToLocalChecked(), 63 | Nan::New(exitCode)); 64 | } 65 | -------------------------------------------------------------------------------- /ts/src/heap-profiler-bindings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as path from 'path'; 18 | 19 | import {AllocationProfileNode} from './v8-types'; 20 | 21 | const findBinding = require('node-gyp-build'); 22 | const profiler = findBinding(path.join(__dirname, '..', '..')); 23 | 24 | // Wrappers around native heap profiler functions. 25 | 26 | export function startSamplingHeapProfiler( 27 | heapIntervalBytes: number, 28 | heapStackDepth: number 29 | ) { 30 | profiler.heapProfiler.startSamplingHeapProfiler( 31 | heapIntervalBytes, 32 | heapStackDepth 33 | ); 34 | } 35 | 36 | export function stopSamplingHeapProfiler() { 37 | profiler.heapProfiler.stopSamplingHeapProfiler(); 38 | } 39 | 40 | export function getAllocationProfile(): AllocationProfileNode { 41 | return profiler.heapProfiler.getAllocationProfile(); 42 | } 43 | 44 | export type NearHeapLimitCallback = (profile: AllocationProfileNode) => void; 45 | 46 | export function monitorOutOfMemory( 47 | heapLimitExtensionSize: number, 48 | maxHeapLimitExtensionCount: number, 49 | dumpHeapProfileOnSdterr: boolean, 50 | exportCommand: Array | undefined, 51 | callback: NearHeapLimitCallback | undefined, 52 | callbackMode: number, 53 | isMainThread: boolean 54 | ) { 55 | profiler.heapProfiler.monitorOutOfMemory( 56 | heapLimitExtensionSize, 57 | maxHeapLimitExtensionCount, 58 | dumpHeapProfileOnSdterr, 59 | exportCommand, 60 | callback, 61 | callbackMode, 62 | isMainThread 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /benchmark/sirun/wall-profiler/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const profiler = require('../../../out/src/time-profiler') 4 | const { createServer, request } = require('http') 5 | 6 | const concurrency = Number(process.env.CONCURRENCY || '10') 7 | const requestFrequency = Number(process.env.REQUEST_FREQUENCY || '15') 8 | const sampleFrequency = Number(process.env.SAMPLE_FREQUENCY || '999') 9 | 10 | const server = createServer((req, res) => { 11 | setImmediate(() => { 12 | res.end('hello') 13 | }) 14 | }) 15 | 16 | function get (options) { 17 | return new Promise((resolve, reject) => { 18 | const req = request(options, (res) => { 19 | const chunks = [] 20 | res.on('error', reject) 21 | res.on('data', chunks.push.bind(chunks)) 22 | res.on('end', () => { 23 | resolve(Buffer.concat(chunks)) 24 | }) 25 | }) 26 | req.on('error', reject) 27 | req.end() 28 | }) 29 | } 30 | 31 | function delay (ms) { 32 | return new Promise(resolve => setTimeout(resolve, ms)) 33 | } 34 | 35 | async function storm (requestFrequency, task) { 36 | const gap = (1 / requestFrequency) * 1e9 37 | while (server.listening) { 38 | const start = process.hrtime.bigint() 39 | try { 40 | await task() 41 | } catch (e) { 42 | // Ignore ECONNRESET if server is shutting down 43 | if (e.code !== 'ECONNRESET' || server.listening) { 44 | throw e 45 | } 46 | } 47 | const end = process.hrtime.bigint() 48 | const remainder = gap - Number(end - start) 49 | await delay(Math.max(0, remainder / 1e6)) 50 | } 51 | } 52 | 53 | server.listen(8080, '0.0.0.0', async () => { 54 | if (!concurrency) return 55 | const { address, port } = server.address() 56 | const getter = get.bind(null, { 57 | hostname: address, 58 | path: '/', 59 | port 60 | }) 61 | const task = storm.bind(null, requestFrequency, getter) 62 | const tasks = Array.from({ length: concurrency }, task) 63 | await Promise.all(tasks) 64 | }) 65 | 66 | if (sampleFrequency !== 0) { 67 | profiler.start({ intervalMicros: 1e6 / sampleFrequency }) 68 | } 69 | 70 | setTimeout(() => { 71 | if (profiler.isStarted()) { 72 | profiler.stop() 73 | } 74 | server.close() 75 | }, 1000) 76 | -------------------------------------------------------------------------------- /.gitlab/benchmarks.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | BASE_CI_IMAGE: 486234852809.dkr.ecr.us-east-1.amazonaws.com/ci/benchmarking-platform:pprof-nodejs 3 | 4 | .benchmark_base: 5 | tags: ["runner:apm-k8s-tweaked-metal"] 6 | image: $BASE_CI_IMAGE 7 | stage: benchmarks 8 | rules: 9 | - if: $CI_COMMIT_TAG 10 | when: never 11 | - when: on_success 12 | variables: 13 | UPSTREAM_PROJECT_ID: $CI_PROJECT_ID 14 | UPSTREAM_PROJECT_NAME: $CI_PROJECT_NAME 15 | UPSTREAM_BRANCH: $CI_COMMIT_REF_NAME 16 | UPSTREAM_COMMIT_SHA: $CI_COMMIT_SHA 17 | 18 | KUBERNETES_SERVICE_ACCOUNT_OVERWRITE: pprof-nodejs 19 | FF_USE_LEGACY_KUBERNETES_EXECUTION_STRATEGY: "true" 20 | before_script: 21 | - git config --global url."https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.ddbuild.io/DataDog/".insteadOf "https://github.com/DataDog/" 22 | - git clone --branch pprof-nodejs https://github.com/DataDog/benchmarking-platform /platform 23 | 24 | benchmarks: 25 | extends: .benchmark_base 26 | interruptible: true 27 | timeout: 1h 28 | script: 29 | - export ARTIFACTS_DIR="$(pwd)/reports/${CI_JOB_ID}" && (mkdir -p "${ARTIFACTS_DIR}" || :) 30 | - cd /platform 31 | - ./steps/capture-hardware-software-info.sh 32 | - ./steps/run-benchmarks.sh 33 | parallel: 34 | matrix: 35 | - MAJOR_NODE_VERSION: 18 36 | - MAJOR_NODE_VERSION: 20 37 | - MAJOR_NODE_VERSION: 22 38 | - MAJOR_NODE_VERSION: 24 39 | # TODO: Re-enable this once support for Node 25 is merged on main 40 | # - MAJOR_NODE_VERSION: 25 41 | artifacts: 42 | name: "reports" 43 | paths: 44 | - reports/ 45 | expire_in: 3 months 46 | 47 | benchmarks-pr-comment: 48 | extends: .benchmark_base 49 | needs: 50 | - job: benchmarks 51 | artifacts: true 52 | script: 53 | - export ARTIFACTS_DIR="$(pwd)/reports" 54 | - cd /platform 55 | - find "$ARTIFACTS_DIR" 56 | - ./steps/aggregate-results.sh 57 | - find "$ARTIFACTS_DIR" 58 | - source "$ARTIFACTS_DIR/.env" 59 | - ./steps/analyze-results.sh 60 | - "./steps/upload-results-to-s3.sh || :" 61 | - "./steps/post-pr-comment.sh || :" 62 | artifacts: 63 | name: "reports" 64 | paths: 65 | - reports/ 66 | expire_in: 3 months 67 | -------------------------------------------------------------------------------- /tools/build/linux_build_and_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2018 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | . $(dirname $0)/../retry.sh 18 | 19 | # Fail on any error. 20 | set -e pipefail 21 | 22 | # Display commands 23 | set -x 24 | 25 | if [[ -z "$BUILD_TYPE" ]]; then 26 | case $KOKORO_JOB_TYPE in 27 | CONTINUOUS_INTEGRATION) 28 | BUILD_TYPE=continuous 29 | ;; 30 | PRESUBMIT_GITHUB) 31 | BUILD_TYPE=presubmit 32 | ;; 33 | RELEASE) 34 | BUILD_TYPE=release 35 | ;; 36 | *) 37 | echo "Unknown build type: ${KOKORO_JOB_TYPE}" 38 | exit 1 39 | ;; 40 | esac 41 | fi 42 | 43 | cd $(dirname $0)/../.. 44 | BASE_DIR=$PWD 45 | 46 | retry docker build -t build-linux -f tools/build/Dockerfile.linux tools/build 47 | retry docker run -v "${BASE_DIR}":"${BASE_DIR}" build-linux \ 48 | "${BASE_DIR}/tools/build/build.sh" 49 | 50 | retry docker build -t build-alpine -f tools/build/Dockerfile.alpine tools/build 51 | retry docker run -v "${BASE_DIR}":"${BASE_DIR}" build-alpine \ 52 | "${BASE_DIR}/tools/build/build.sh" 53 | 54 | GCS_LOCATION="cprof-e2e-nodejs-artifacts/pprof-nodejs/kokoro/${BUILD_TYPE}/${KOKORO_BUILD_NUMBER}" 55 | retry gcloud auth activate-service-account \ 56 | --key-file="${KOKORO_KEYSTORE_DIR}/72935_cloud-profiler-e2e-service-account-key" 57 | 58 | retry gsutil cp -r "${BASE_DIR}/artifacts/." "gs://${GCS_LOCATION}/" 59 | 60 | # Test the agent 61 | export BINARY_HOST="https://storage.googleapis.com/${GCS_LOCATION}" 62 | "${BASE_DIR}/system-test/system_test.sh" 63 | 64 | if [ "$BUILD_TYPE" == "release" ]; then 65 | retry gsutil cp -r "${BASE_DIR}/artifacts/." "gs://cloud-profiler/pprof-nodejs/release" 66 | fi 67 | -------------------------------------------------------------------------------- /bindings/profile-translator.hh: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Datadog, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | 19 | namespace dd { 20 | class ProfileTranslator { 21 | v8::Isolate* isolate = v8::Isolate::GetCurrent(); 22 | v8::Local context = isolate->GetCurrentContext(); 23 | v8::Local emptyArray = v8::Array::New(isolate, 0); 24 | 25 | protected: 26 | v8::Local NewObject() { return v8::Object::New(isolate); } 27 | 28 | v8::Local NewInteger(int x) { 29 | return v8::Integer::New(isolate, x); 30 | } 31 | 32 | v8::Local NewBoolean(bool x) { 33 | return v8::Boolean::New(isolate, x); 34 | } 35 | 36 | template 37 | v8::Local NewNumber(T x) { 38 | return v8::Number::New(isolate, x); 39 | } 40 | 41 | v8::Local NewArray(int length) { 42 | return length == 0 ? emptyArray : v8::Array::New(isolate, length); 43 | } 44 | 45 | v8::Local NewString(const char* str) { 46 | return v8::String::NewFromUtf8(isolate, str).ToLocalChecked(); 47 | } 48 | 49 | v8::MaybeLocal Get(v8::Local arr, uint32_t index) { 50 | return arr->Get(context, index); 51 | } 52 | 53 | v8::Maybe Set(v8::Local arr, 54 | uint32_t index, 55 | v8::Local value) { 56 | return arr->Set(context, index, value); 57 | } 58 | 59 | v8::Maybe Set(v8::Local obj, 60 | v8::Local key, 61 | v8::Local value) { 62 | return obj->Set(context, key, value); 63 | } 64 | 65 | ProfileTranslator() = default; 66 | }; 67 | }; // namespace dd 68 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | asan: 11 | strategy: 12 | matrix: 13 | version: [18, 20, 22, 24, 25] 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.version }} 20 | - run: npm install 21 | - run: npm run test:js-asan 22 | 23 | valgrind: 24 | strategy: 25 | matrix: 26 | version: [18, 20, 22, 24, 25] 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v3 30 | - uses: actions/setup-node@v3 31 | with: 32 | node-version: ${{ matrix.version }} 33 | - run: sudo apt-get update && sudo apt-get install valgrind 34 | - run: npm install 35 | - run: npm run test:js-valgrind 36 | 37 | build: 38 | uses: Datadog/action-prebuildify/.github/workflows/build.yml@main 39 | with: 40 | target-name: 'dd_pprof' # target name in binding.gyp 41 | package-manager: 'npm' # npm or yarn 42 | cache: true # enable caching of dependencies based on lockfile 43 | min-node-version: 18 44 | skip: 'linux-arm,linux-ia32' # skip building for these platforms 45 | 46 | dev_publish: 47 | needs: build 48 | runs-on: ubuntu-latest 49 | if: github.ref == 'refs/heads/main' 50 | environment: npm 51 | env: 52 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions/download-artifact@v4 56 | - uses: actions/setup-node@v3 57 | with: 58 | registry-url: 'https://registry.npmjs.org' 59 | - run: npm install 60 | - id: pkg 61 | run: | 62 | content=`cat ./package.json | tr '\n' ' '` 63 | echo "json=$content" >> $GITHUB_OUTPUT 64 | - run: npm version --no-git-tag-version ${{ fromJson(steps.pkg.outputs.json).version }}-$(git rev-parse --short HEAD)+${{ github.run_id }}.${{ github.run_attempt }} 65 | - run: npm publish --tag dev 66 | 67 | build-successful: 68 | if: always() 69 | needs: [build] 70 | runs-on: ubuntu-latest 71 | steps: 72 | - name: Determine if everything is passing 73 | run: exit 1 74 | if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} 75 | -------------------------------------------------------------------------------- /ts/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import {writeFileSync} from 'fs'; 17 | 18 | import * as heapProfiler from './heap-profiler'; 19 | import {encodeSync} from './profile-encoder'; 20 | import * as timeProfiler from './time-profiler'; 21 | export { 22 | AllocationProfileNode, 23 | TimeProfileNode, 24 | ProfileNode, 25 | LabelSet, 26 | } from './v8-types'; 27 | 28 | export {encode, encodeSync} from './profile-encoder'; 29 | export {SourceMapper} from './sourcemapper/sourcemapper'; 30 | export {setLogger} from './logger'; 31 | export {getNativeThreadId} from './time-profiler'; 32 | 33 | export const time = { 34 | profile: timeProfiler.profile, 35 | start: timeProfiler.start, 36 | stop: timeProfiler.stop, 37 | getContext: timeProfiler.getContext, 38 | setContext: timeProfiler.setContext, 39 | isStarted: timeProfiler.isStarted, 40 | v8ProfilerStuckEventLoopDetected: 41 | timeProfiler.v8ProfilerStuckEventLoopDetected, 42 | getState: timeProfiler.getState, 43 | constants: timeProfiler.constants, 44 | }; 45 | 46 | export const heap = { 47 | start: heapProfiler.start, 48 | stop: heapProfiler.stop, 49 | profile: heapProfiler.profile, 50 | convertProfile: heapProfiler.convertProfile, 51 | v8Profile: heapProfiler.v8Profile, 52 | monitorOutOfMemory: heapProfiler.monitorOutOfMemory, 53 | CallbackMode: heapProfiler.CallbackMode, 54 | }; 55 | 56 | // If loaded with --require, start profiling. 57 | if (module.parent && module.parent.id === 'internal/preload') { 58 | time.start({}); 59 | process.on('exit', () => { 60 | // The process is going to terminate imminently. All work here needs to 61 | // be synchronous. 62 | const profile = time.stop(); 63 | const buffer = encodeSync(profile); 64 | writeFileSync(`pprof-profile-${process.pid}.pb.gz`, buffer); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /bindings/thread-cpu-clock.hh: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Datadog, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #ifdef __linux__ 24 | #include 25 | #elif __APPLE__ 26 | #include 27 | #elif _WIN32 28 | #include 29 | #endif 30 | 31 | namespace dd { 32 | 33 | struct CurrentThreadCpuClock { 34 | using duration = std::chrono::nanoseconds; 35 | using rep = duration::rep; 36 | using period = duration::period; 37 | using time_point = std::chrono::time_point; 38 | 39 | static constexpr bool is_steady = true; 40 | 41 | static time_point now() noexcept; 42 | }; 43 | 44 | struct ProcessCpuClock { 45 | using duration = std::chrono::nanoseconds; 46 | using rep = duration::rep; 47 | using period = duration::period; 48 | using time_point = std::chrono::time_point; 49 | 50 | static constexpr bool is_steady = true; 51 | 52 | static time_point now() noexcept; 53 | }; 54 | 55 | class ThreadCpuClock { 56 | public: 57 | using duration = std::chrono::nanoseconds; 58 | using rep = duration::rep; 59 | using period = duration::period; 60 | using time_point = std::chrono::time_point; 61 | 62 | static constexpr bool is_steady = true; 63 | 64 | ThreadCpuClock(); 65 | time_point now() const noexcept; 66 | 67 | private: 68 | #ifdef __linux__ 69 | clockid_t clockid_; 70 | #elif __APPLE__ 71 | mach_port_t thread_; 72 | #elif _WIN32 73 | HANDLE thread_; 74 | #endif 75 | }; 76 | 77 | class ThreadCpuStopWatch { 78 | public: 79 | ThreadCpuStopWatch() { last_ = clock_.now(); } 80 | 81 | ThreadCpuClock::duration GetAndReset() { 82 | auto now = clock_.now(); 83 | auto d = now - last_; 84 | last_ = now; 85 | return d; 86 | } 87 | 88 | private: 89 | ThreadCpuClock clock_; 90 | ThreadCpuClock::time_point last_; 91 | }; 92 | 93 | } // namespace dd 94 | -------------------------------------------------------------------------------- /ts/src/v8-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Type Definitions based on implementation in bindings/ 18 | 19 | export interface TimeProfile { 20 | /** Time in nanoseconds at which profile was stopped. */ 21 | endTime: number; 22 | topDownRoot: TimeProfileNode; 23 | /** Time in nanoseconds at which profile was started. */ 24 | startTime: number; 25 | hasCpuTime?: boolean; 26 | /** CPU time of non-JS threads, only reported for the main worker thread */ 27 | nonJSThreadsCpuTime?: number; 28 | } 29 | 30 | export interface ProfileNode { 31 | // name is the function name. 32 | name?: string; 33 | scriptName: string; 34 | scriptId?: number; 35 | lineNumber?: number; 36 | columnNumber?: number; 37 | children: ProfileNode[]; 38 | } 39 | 40 | export interface TimeProfileNodeContext { 41 | context?: object; 42 | timestamp: bigint; // end of sample taking; in microseconds since epoch 43 | cpuTime?: number; // cpu time in nanoseconds 44 | asyncId?: number; // async_hooks.executionAsyncId() at the time of sample taking 45 | } 46 | 47 | export interface TimeProfileNode extends ProfileNode { 48 | hitCount: number; 49 | contexts?: TimeProfileNodeContext[]; 50 | } 51 | 52 | export interface AllocationProfileNode extends ProfileNode { 53 | allocations: Allocation[]; 54 | } 55 | 56 | export interface Allocation { 57 | sizeBytes: number; 58 | count: number; 59 | } 60 | export interface LabelSet { 61 | [key: string]: string | number; 62 | } 63 | 64 | export interface GenerateAllocationLabelsFunction { 65 | ({node}: {node: AllocationProfileNode}): LabelSet; 66 | } 67 | 68 | export interface GenerateTimeLabelsArgs { 69 | node: TimeProfileNode; 70 | context?: TimeProfileNodeContext; 71 | } 72 | 73 | export interface GenerateTimeLabelsFunction { 74 | (args: GenerateTimeLabelsArgs): LabelSet; 75 | } 76 | 77 | export interface TimeProfilerMetrics { 78 | usedAsyncContextCount: number; 79 | totalAsyncContextCount: number; 80 | } 81 | -------------------------------------------------------------------------------- /system-test/busybench/src/busybench.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {writeFile as writeFilePromise} from 'fs/promises'; 18 | // eslint-disable-next-line node/no-extraneous-import 19 | import {encode, heap, SourceMapper, time} from 'pprof'; 20 | 21 | const startTime: number = Date.now(); 22 | const testArr: number[][] = []; 23 | 24 | /** 25 | * Fills several arrays, then calls itself with setTimeout. 26 | * It continues to do this until durationSeconds after the startTime. 27 | */ 28 | function busyLoop(durationSeconds: number) { 29 | for (let i = 0; i < testArr.length; i++) { 30 | for (let j = 0; j < testArr[i].length; j++) { 31 | testArr[i][j] = Math.sqrt(j * testArr[i][j]); 32 | } 33 | } 34 | if (Date.now() - startTime < 1000 * durationSeconds) { 35 | setTimeout(() => busyLoop(durationSeconds), 5); 36 | } 37 | } 38 | 39 | function benchmark(durationSeconds: number) { 40 | // Allocate 16 MiB in 64 KiB chunks. 41 | for (let i = 0; i < 16 * 16; i++) { 42 | testArr[i] = new Array(64 * 1024); 43 | } 44 | busyLoop(durationSeconds); 45 | } 46 | 47 | async function collectAndSaveTimeProfile( 48 | durationSeconds: number, 49 | sourceMapper: SourceMapper 50 | ): Promise { 51 | const profile = await time.profile({ 52 | durationMillis: 1000 * durationSeconds, 53 | sourceMapper, 54 | }); 55 | const buf = await encode(profile); 56 | await writeFilePromise('time.pb.gz', buf); 57 | } 58 | 59 | async function collectAndSaveHeapProfile( 60 | sourceMapper: SourceMapper 61 | ): Promise { 62 | const profile = await heap.profile(undefined, sourceMapper); 63 | const buf = await encode(profile); 64 | await writeFilePromise('heap.pb.gz', buf); 65 | } 66 | 67 | async function collectAndSaveProfiles(): Promise { 68 | const sourceMapper = await SourceMapper.create([process.cwd()]); 69 | collectAndSaveTimeProfile(durationSeconds, sourceMapper); 70 | collectAndSaveHeapProfile(sourceMapper); 71 | } 72 | 73 | const durationSeconds = Number(process.argv.length > 2 ? process.argv[2] : 30); 74 | heap.start(512 * 1024, 64); 75 | benchmark(durationSeconds); 76 | 77 | collectAndSaveProfiles(); 78 | -------------------------------------------------------------------------------- /system-test/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | trap "cd $(dirname $0)/.. && npm run clean" EXIT 4 | trap "echo '** TEST FAILED **'" ERR 5 | 6 | . $(dirname $0)/../tools/retry.sh 7 | 8 | function timeout_after() { 9 | # timeout on Node 11 alpine image requires -t to specify time. 10 | if [[ -f /bin/busybox ]] && [[ $(node -v) =~ ^v11.* ]]; then 11 | timeout -t "${@}" 12 | else 13 | timeout "${@}" 14 | fi 15 | } 16 | 17 | npm_install() { 18 | timeout_after 60 npm install --quiet "${@}" 19 | } 20 | 21 | set -eox pipefail 22 | cd $(dirname $0)/.. 23 | 24 | NODEDIR=$(dirname $(dirname $(which node))) 25 | 26 | # TODO: Remove when a new version of nan (current version 2.12.1) is released. 27 | # For v8-canary tests, we need to use the version of NAN on github, which 28 | # contains unreleased fixes that allow the native component to be compiled 29 | # with Node's V8 canary build. 30 | [ -z $NVM_NODEJS_ORG_MIRROR ] \ 31 | || retry npm_install https://github.com/nodejs/nan.git 32 | 33 | retry npm_install --nodedir="$NODEDIR" \ 34 | ${BINARY_HOST:+--pprof_binary_host_mirror=$BINARY_HOST} >/dev/null 35 | 36 | npm run compile 37 | npm pack --quiet 38 | VERSION=$(node -e "console.log(require('./package.json').version);") 39 | PROFILER="$PWD/pprof-$VERSION.tgz" 40 | 41 | if [[ "$VERIFY_TIME_LINE_NUMBERS" == "true" ]]; then 42 | BENCHDIR="$PWD/system-test/busybench-js" 43 | BENCHPATH="src/busybench.js" 44 | else 45 | BENCHDIR="$PWD/system-test/busybench" 46 | BENCHPATH="build/src/busybench.js" 47 | fi 48 | 49 | TESTDIR=$(mktemp -d) 50 | cp -r "$BENCHDIR" "$TESTDIR/busybench" 51 | cd "$TESTDIR/busybench" 52 | 53 | retry npm_install typescript gts @types/node >/dev/null 54 | retry npm_install --nodedir="$NODEDIR" \ 55 | $([ -z "$BINARY_HOST" ] && echo "--build-from-source=pprof" \ 56 | || echo "--pprof_binary_host_mirror=$BINARY_HOST")\ 57 | "$PROFILER">/dev/null 58 | 59 | if [[ "$VERIFY_TIME_LINE_NUMBERS" != "true" ]]; then 60 | npm run compile 61 | fi 62 | 63 | node -v 64 | node --trace-warnings "$BENCHPATH" 10 $VERIFY_TIME_LINE_NUMBERS 65 | 66 | if [[ "$VERIFY_TIME_LINE_NUMBERS" == "true" ]]; then 67 | pprof -lines -top -nodecount=2 time.pb.gz 68 | pprof -lines -top -nodecount=2 time.pb.gz | \ 69 | grep "busyLoop.*src/busybench.js:3[3-5]" 70 | pprof -filefunctions -top -nodecount=2 heap.pb.gz 71 | pprof -filefunctions -top -nodecount=2 heap.pb.gz | \ 72 | grep "busyLoop.*src/busybench.js" 73 | else 74 | pprof -filefunctions -top -nodecount=2 time.pb.gz 75 | pprof -filefunctions -top -nodecount=2 time.pb.gz | \ 76 | grep "busyLoop.*src/busybench.ts" 77 | pprof -filefunctions -top -nodecount=2 heap.pb.gz 78 | pprof -filefunctions -top -nodecount=2 heap.pb.gz | \ 79 | grep "busyLoop.*src/busybench.ts" 80 | fi 81 | 82 | 83 | echo '** TEST PASSED **' 84 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | **Table of contents** 4 | 5 | * [Contributor License Agreements](#contributor-license-agreements) 6 | * [Contributing a patch](#contributing-a-patch) 7 | * [Running the tests](#running-the-tests) 8 | * [Releasing the library](#releasing-the-library) 9 | 10 | ## Contributor License Agreements 11 | 12 | We'd love to accept your sample apps and patches! Before we can take them, we 13 | have to jump a couple of legal hurdles. 14 | 15 | Please fill out either the individual or corporate Contributor License Agreement 16 | (CLA). 17 | 18 | * If you are an individual writing original source code and you're sure you 19 | own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual). 20 | * If you work for a company that wants to allow you to contribute your work, 21 | then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate). 22 | 23 | Follow either of the two links above to access the appropriate CLA and 24 | instructions for how to sign and return it. Once we receive it, we'll be able to 25 | accept your pull requests. 26 | 27 | ## Contributing A Patch 28 | 29 | 1. Submit an issue describing your proposed change to the repo in question. 30 | 1. The repo owner will respond to your issue promptly. 31 | 1. If your proposed change is accepted, and you haven't already done so, sign a 32 | Contributor License Agreement (see details above). 33 | 1. Fork the desired repo, develop and test your code changes. 34 | 1. Ensure that your code adheres to the existing style in the code to which 35 | you are contributing. 36 | 1. Ensure that your code has an appropriate set of tests which all pass. 37 | 1. Submit a pull request. 38 | 39 | ## Running the tests 40 | 41 | 1. [Prepare your environment for Node.js setup][setup]. 42 | 43 | 1. Install dependencies: 44 | ```sh 45 | npm install 46 | ``` 47 | 48 | 1. Run the tests: 49 | ```sh 50 | npm test 51 | ``` 52 | 53 | 1. Lint (and maybe fix) any changes: 54 | ```sh 55 | npm run fix 56 | ``` 57 | 58 | [setup]: https://cloud.google.com/nodejs/docs/setup 59 | 60 | # Running the system test 61 | The system test starts a simple benchmark, uses this module to collect a time 62 | and a heap profile, and verifies that the profiles contain functions from 63 | within the benchmark. 64 | 65 | To run the system test, [golang](https://golang.org/) must be installed. 66 | 67 | The following command can be used to run the system test with all supported 68 | versions of Node.JS: 69 | ```sh 70 | sh system-test/system_test.sh 71 | ``` 72 | 73 | To run the system test with the v8 canary build, use: 74 | ```sh 75 | RUN_ONLY_V8_CANARY_TEST=true sh system-test/system_test.sh 76 | ``` -------------------------------------------------------------------------------- /system-test/busybench-js/src/busybench.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const fs = require('fs'); 18 | // eslint-disable-next-line node/no-missing-require 19 | const pprof = require('pprof'); 20 | 21 | const writeFilePromise = fs.promises.writeFile; 22 | 23 | const startTime = Date.now(); 24 | const testArr = []; 25 | 26 | /** 27 | * Fills several arrays, then calls itself with setTimeout. 28 | * It continues to do this until durationSeconds after the startTime. 29 | */ 30 | function busyLoop(durationSeconds) { 31 | for (let i = 0; i < testArr.length; i++) { 32 | for (let j = 0; j < testArr[i].length; j++) { 33 | testArr[i][j] = Math.sqrt(j * testArr[i][j]); 34 | } 35 | } 36 | if (Date.now() - startTime < 1000 * durationSeconds) { 37 | setTimeout(() => busyLoop(durationSeconds), 5); 38 | } 39 | } 40 | 41 | function benchmark(durationSeconds) { 42 | // Allocate 16 MiB in 64 KiB chunks. 43 | for (let i = 0; i < 16 * 16; i++) { 44 | testArr[i] = new Array(64 * 1024); 45 | } 46 | busyLoop(durationSeconds); 47 | } 48 | 49 | async function collectAndSaveTimeProfile( 50 | durationSeconds, 51 | sourceMapper, 52 | lineNumbers 53 | ) { 54 | const profile = await pprof.time.profile({ 55 | durationMillis: 1000 * durationSeconds, 56 | lineNumbers: lineNumbers, 57 | sourceMapper: sourceMapper, 58 | }); 59 | const buf = await pprof.encode(profile); 60 | await writeFilePromise('time.pb.gz', buf); 61 | } 62 | 63 | async function collectAndSaveHeapProfile(sourceMapper) { 64 | const profile = pprof.heap.profile(undefined, sourceMapper); 65 | const buf = await pprof.encode(profile); 66 | await writeFilePromise('heap.pb.gz', buf); 67 | } 68 | 69 | async function collectAndSaveProfiles(collectLineNumberTimeProfile) { 70 | const sourceMapper = await pprof.SourceMapper.create([process.cwd()]); 71 | collectAndSaveHeapProfile(sourceMapper); 72 | collectAndSaveTimeProfile( 73 | durationSeconds / 2, 74 | sourceMapper, 75 | collectLineNumberTimeProfile 76 | ); 77 | } 78 | 79 | const durationSeconds = Number(process.argv.length > 2 ? process.argv[2] : 30); 80 | const collectLineNumberTimeProfile = Boolean( 81 | process.argv.length > 3 ? process.argv[3] : false 82 | ); 83 | 84 | pprof.heap.start(512 * 1024, 64); 85 | benchmark(durationSeconds); 86 | 87 | collectAndSaveProfiles(collectLineNumberTimeProfile); 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@datadog/pprof", 3 | "version": "6.0.0-pre", 4 | "description": "pprof support for Node.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/DataDog/pprof-nodejs.git" 8 | }, 9 | "main": "out/src/index.js", 10 | "types": "out/src/index.d.ts", 11 | "scripts": { 12 | "build:asan": "node-gyp configure build --jobs=max --address_sanitizer", 13 | "build:tsan": "node-gyp configure build --jobs=max --thread_sanitizer", 14 | "build": "node-gyp configure build --jobs=max", 15 | "codecov": "nyc report --reporter=json && codecov -f coverage/*.json", 16 | "compile": "tsc -p .", 17 | "fix": "gts fix", 18 | "format": "clang-format --style file -i --glob='bindings/**/*.{h,hh,cpp,cc}'", 19 | "install": "exit 0", 20 | "lint": "jsgl --local . && gts check && clang-format --style file -n -Werror --glob='bindings/**/*.{h,hh,cpp,cc}'", 21 | "prepare": "npm run compile && npm run rebuild", 22 | "pretest:js-asan": "npm run compile && npm run build:asan", 23 | "pretest:js-tsan": "npm run compile && npm run build:tsan", 24 | "pretest:js-valgrind": "npm run pretest", 25 | "pretest": "npm run compile", 26 | "rebuild": "node-gyp rebuild --jobs=max", 27 | "test:cpp": "node scripts/cctest.js", 28 | "test:js-asan": "LSAN_OPTIONS='suppressions=./suppressions/lsan_suppr.txt' LD_PRELOAD=`gcc -print-file-name=libasan.so` mocha out/test/test-*.js", 29 | "test:js-tsan": "LD_PRELOAD=`gcc -print-file-name=libtsan.so` mocha out/test/test-*.js", 30 | "test:js-valgrind": "valgrind --leak-check=full mocha out/test/test-*.js", 31 | "test:js": "nyc mocha -r source-map-support/register out/test/test-*.js", 32 | "test": "npm run test:js" 33 | }, 34 | "author": { 35 | "name": "Google Inc." 36 | }, 37 | "license": "Apache-2.0", 38 | "dependencies": { 39 | "delay": "^5.0.0", 40 | "node-gyp-build": "<4.0", 41 | "p-limit": "^3.1.0", 42 | "pprof-format": "^2.2.1", 43 | "source-map": "^0.7.4" 44 | }, 45 | "devDependencies": { 46 | "@types/mocha": "^10.0.1", 47 | "@types/node": ">=16", 48 | "@types/sinon": "^10.0.15", 49 | "@types/tmp": "^0.2.3", 50 | "@typescript-eslint/eslint-plugin": "^5.60.1", 51 | "clang-format": "^1.8.0", 52 | "codecov": "^3.8.2", 53 | "deep-copy": "^1.4.2", 54 | "eslint-config-standard": "^17.1.0", 55 | "eslint-plugin-import": "^2.26.0", 56 | "eslint-plugin-n": "^16.0.1", 57 | "eslint-plugin-promise": "^6.1.1", 58 | "gts": "^4.0.1", 59 | "js-green-licenses": "^4.0.0", 60 | "mocha": "^10.2.0", 61 | "nan": "^2.22.2", 62 | "nyc": "^15.1.0", 63 | "semver": "^7.7.2", 64 | "sinon": "^15.2.0", 65 | "source-map-support": "^0.5.21", 66 | "tmp": "0.2.4", 67 | "typescript": "^5.9.2" 68 | }, 69 | "files": [ 70 | "out/src", 71 | "out/third_party/cloud-debug-nodejs", 72 | "proto", 73 | "package-lock.json", 74 | "package.json", 75 | "README.md", 76 | "scripts/preinstall.js", 77 | "scripts/require-package-json.js", 78 | "scripts/should_rebuild.js", 79 | "prebuilds" 80 | ], 81 | "nyc": { 82 | "exclude": [ 83 | "proto", 84 | "out/test", 85 | "out/system-test" 86 | ] 87 | }, 88 | "engines": { 89 | "node": ">=16" 90 | }, 91 | "//": "Temporary fix to make nan@2.22.2 work with Node 24", 92 | "postinstall": "sed -i '' 's/^.* Holder() const.*//' ./node_modules/nan/nan_callbacks_12_inl.h" 93 | } 94 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: Google 4 | AccessModifierOffset: -1 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlines: Right 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: true 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: Inline 15 | AllowShortIfStatementsOnASingleLine: true 16 | AllowShortLoopsOnASingleLine: true 17 | AlwaysBreakAfterDefinitionReturnType: None 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: false 20 | AlwaysBreakTemplateDeclarations: true 21 | BinPackArguments: false 22 | BinPackParameters: false 23 | BraceWrapping: 24 | AfterClass: false 25 | AfterControlStatement: false 26 | AfterEnum: false 27 | AfterFunction: false 28 | AfterNamespace: false 29 | AfterObjCDeclaration: false 30 | AfterStruct: false 31 | AfterUnion: false 32 | AfterExternBlock: false 33 | BeforeCatch: false 34 | BeforeElse: false 35 | IndentBraces: false 36 | SplitEmptyFunction: true 37 | SplitEmptyRecord: true 38 | SplitEmptyNamespace: true 39 | BreakBeforeBinaryOperators: None 40 | BreakBeforeBraces: Attach 41 | BreakBeforeInheritanceComma: false 42 | BreakBeforeTernaryOperators: true 43 | BreakConstructorInitializersBeforeComma: false 44 | BreakConstructorInitializers: BeforeColon 45 | BreakAfterJavaFieldAnnotations: false 46 | BreakStringLiterals: true 47 | ColumnLimit: 80 48 | CommentPragmas: '^ IWYU pragma:' 49 | CompactNamespaces: false 50 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 51 | ConstructorInitializerIndentWidth: 4 52 | ContinuationIndentWidth: 4 53 | Cpp11BracedListStyle: true 54 | DerivePointerAlignment: false 55 | DisableFormat: false 56 | ExperimentalAutoDetectBinPacking: false 57 | FixNamespaceComments: true 58 | ForEachMacros: 59 | - foreach 60 | - Q_FOREACH 61 | - BOOST_FOREACH 62 | IncludeBlocks: Preserve 63 | IncludeCategories: 64 | - Regex: '^' 65 | Priority: 2 66 | - Regex: '^<.*\.h>' 67 | Priority: 1 68 | - Regex: '^<.*' 69 | Priority: 2 70 | - Regex: '.*' 71 | Priority: 3 72 | IncludeIsMainRegex: '([-_](test|unittest))?$' 73 | IndentCaseLabels: true 74 | IndentPPDirectives: None 75 | IndentWidth: 2 76 | IndentWrappedFunctionNames: false 77 | JavaScriptQuotes: Leave 78 | JavaScriptWrapImports: true 79 | KeepEmptyLinesAtTheStartOfBlocks: false 80 | MacroBlockBegin: '' 81 | MacroBlockEnd: '' 82 | MaxEmptyLinesToKeep: 1 83 | NamespaceIndentation: None 84 | ObjCBlockIndentWidth: 2 85 | ObjCSpaceAfterProperty: false 86 | ObjCSpaceBeforeProtocolList: false 87 | PenaltyBreakAssignment: 2 88 | PenaltyBreakBeforeFirstCallParameter: 1 89 | PenaltyBreakComment: 300 90 | PenaltyBreakFirstLessLess: 120 91 | PenaltyBreakString: 1000 92 | PenaltyExcessCharacter: 1000000 93 | PenaltyReturnTypeOnItsOwnLine: 200 94 | PointerAlignment: Left 95 | ReflowComments: true 96 | SortIncludes: true 97 | SortUsingDeclarations: true 98 | SpaceAfterCStyleCast: false 99 | SpaceAfterTemplateKeyword: true 100 | SpaceBeforeAssignmentOperators: true 101 | SpaceBeforeParens: ControlStatements 102 | SpaceInEmptyParentheses: false 103 | SpacesBeforeTrailingComments: 2 104 | SpacesInAngles: false 105 | SpacesInContainerLiterals: true 106 | SpacesInCStyleCastParentheses: false 107 | SpacesInParentheses: false 108 | SpacesInSquareBrackets: false 109 | Standard: Auto 110 | TabWidth: 8 111 | UseTab: Never 112 | -------------------------------------------------------------------------------- /bindings/translate-heap-profile.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Datadog, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include "translate-heap-profile.hh" 18 | #include "profile-translator.hh" 19 | 20 | namespace dd { 21 | 22 | namespace { 23 | class HeapProfileTranslator : ProfileTranslator { 24 | #define NODE_FIELDS \ 25 | X(name) \ 26 | X(scriptName) \ 27 | X(scriptId) \ 28 | X(lineNumber) \ 29 | X(columnNumber) \ 30 | X(children) \ 31 | X(allocations) 32 | 33 | #define ALLOCATION_FIELDS \ 34 | X(sizeBytes) \ 35 | X(count) 36 | 37 | #define X(name) v8::Local str_##name = NewString(#name); 38 | NODE_FIELDS 39 | ALLOCATION_FIELDS 40 | #undef X 41 | 42 | public: 43 | v8::Local TranslateAllocationProfile( 44 | v8::AllocationProfile::Node* node) { 45 | v8::Local children = NewArray(node->children.size()); 46 | for (size_t i = 0; i < node->children.size(); i++) { 47 | Set(children, i, TranslateAllocationProfile(node->children[i])); 48 | } 49 | 50 | v8::Local allocations = NewArray(node->allocations.size()); 51 | for (size_t i = 0; i < node->allocations.size(); i++) { 52 | auto alloc = node->allocations[i]; 53 | Set(allocations, 54 | i, 55 | CreateAllocation(NewNumber(alloc.size), NewNumber(alloc.count))); 56 | } 57 | 58 | return CreateNode(node->name, 59 | node->script_name, 60 | NewInteger(node->script_id), 61 | NewInteger(node->line_number), 62 | NewInteger(node->column_number), 63 | children, 64 | allocations); 65 | } 66 | 67 | private: 68 | v8::Local CreateNode(v8::Local name, 69 | v8::Local scriptName, 70 | v8::Local scriptId, 71 | v8::Local lineNumber, 72 | v8::Local columnNumber, 73 | v8::Local children, 74 | v8::Local allocations) { 75 | v8::Local js_node = NewObject(); 76 | #define X(name) Set(js_node, str_##name, name); 77 | NODE_FIELDS 78 | #undef X 79 | #undef NODE_FIELDS 80 | return js_node; 81 | } 82 | 83 | v8::Local CreateAllocation(v8::Local count, 84 | v8::Local sizeBytes) { 85 | v8::Local js_alloc = NewObject(); 86 | #define X(name) Set(js_alloc, str_##name, name); 87 | ALLOCATION_FIELDS 88 | #undef X 89 | #undef ALLOCATION_FIELDS 90 | return js_alloc; 91 | } 92 | 93 | public: 94 | explicit HeapProfileTranslator() {} 95 | }; 96 | } // namespace 97 | 98 | v8::Local TranslateAllocationProfile( 99 | v8::AllocationProfile::Node* node) { 100 | return HeapProfileTranslator().TranslateAllocationProfile(node); 101 | } 102 | 103 | } // namespace dd 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pprof support for Node.js 2 | 3 | [![NPM Version][npm-image]][npm-url] 4 | [![Build Status][build-image]][build-url] 5 | [![Known Vulnerabilities][snyk-image]][snyk-url] 6 | 7 | [pprof][pprof-url] support for Node.js. 8 | 9 | ## Prerequisites 10 | 1. Your application will need to be using Node.js 18 or greater. 11 | 12 | 2. The `pprof` module has a native component that is used to collect profiles 13 | with v8's CPU and Heap profilers. You may need to install additional 14 | dependencies to build this module. 15 | * `pprof` has prebuilt binaries available for Linux arm64/x64, 16 | Alpine Linux x64, macOS arm64/x64, and Windows x64 for Node 18/20/22/24/25. 17 | No additional dependencies are required. 18 | * For other environments: on environments that `pprof` does not have 19 | prebuilt binaries for, the module 20 | [`node-gyp`](https://www.npmjs.com/package/node-gyp) will be used to 21 | build binaries. See `node-gyp`'s 22 | [documentation](https://github.com/nodejs/node-gyp#installation) 23 | for information on dependencies required to build binaries with `node-gyp`. 24 | 25 | 3. The [`pprof`][pprof-url] CLI can be used to view profiles collected with 26 | this module. Instructions for installing the `pprof` CLI can be found 27 | [here][pprof-install-url]. 28 | 29 | ## Basic Set-up 30 | 31 | Install [`pprof`][npm-url] with `npm` or add to your `package.json`. 32 | ```sh 33 | # Install through npm while saving to the local 'package.json' 34 | npm install --save @datadog/pprof 35 | ``` 36 | 37 | ## Using the Profiler 38 | 39 | ### Collect a Wall Time Profile 40 | 41 | #### In code: 42 | 1. Update code to collect and save a profile: 43 | ```javascript 44 | const profile = await pprof.time.profile({ 45 | durationMillis: 10000, // time in milliseconds for which to 46 | // collect profile. 47 | }); 48 | const buf = await pprof.encode(profile); 49 | fs.writeFile('wall.pb.gz', buf, (err) => { 50 | if (err) throw err; 51 | }); 52 | ``` 53 | 54 | 2. View the profile with command line [`pprof`][pprof-url]: 55 | ```sh 56 | pprof -http=: wall.pb.gz 57 | ``` 58 | 59 | #### Requiring from the command line 60 | 61 | 1. Start program from the command line: 62 | ```sh 63 | node --require @datadog/pprof app.js 64 | ``` 65 | 66 | 2. A wall time profile for the job will be saved in 67 | `pprof-profile-${process.pid}.pb.gz`. View the profile with command line 68 | [`pprof`][pprof-url]: 69 | ```sh 70 | pprof -http=: pprof-profile-${process.pid}.pb.gz 71 | ``` 72 | 73 | ### Collect a Heap Profile 74 | 1. Enable heap profiling at the start of the application: 75 | ```javascript 76 | // The average number of bytes between samples. 77 | const intervalBytes = 512 * 1024; 78 | 79 | // The maximum stack depth for samples collected. 80 | const stackDepth = 64; 81 | 82 | heap.start(intervalBytes, stackDepth); 83 | ``` 84 | 2. Collect heap profiles: 85 | 86 | * Collecting and saving a profile in profile.proto format: 87 | ```javascript 88 | const profile = await pprof.heap.profile(); 89 | const buf = await pprof.encode(profile); 90 | fs.writeFile('heap.pb.gz', buf, (err) => { 91 | if (err) throw err; 92 | }) 93 | ``` 94 | 95 | * View the profile with command line [`pprof`][pprof-url]. 96 | ```sh 97 | pprof -http=: heap.pb.gz 98 | ``` 99 | 100 | * Collecting a heap profile with V8 allocation profile format: 101 | ```javascript 102 | const profile = await pprof.heap.v8Profile(); 103 | ``` 104 | 105 | [build-image]: https://github.com/Datadog/pprof-nodejs/actions/workflows/build.yml/badge.svg?branch=main 106 | [build-url]: https://github.com/Datadog/pprof-nodejs/actions/workflows/build.yml 107 | [npm-image]: https://badge.fury.io/js/@datadog%2Fpprof.svg 108 | [npm-url]: https://npmjs.org/package/@datadog/pprof 109 | [pprof-url]: https://github.com/google/pprof 110 | [pprof-install-url]: https://github.com/google/pprof#building-pprof 111 | [snyk-image]: https://snyk.io/test/github/Datadog/pprof-nodejs/badge.svg 112 | [snyk-url]: https://snyk.io/test/github/Datadog/pprof-nodejs 113 | -------------------------------------------------------------------------------- /bindings/thread-cpu-clock.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Datadog, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include "thread-cpu-clock.hh" 18 | 19 | #ifdef __linux__ 20 | #include 21 | #include 22 | #include 23 | #elif __APPLE__ 24 | #define _DARWIN_C_SOURCE 25 | #include 26 | #include 27 | #include 28 | #elif _WIN32 29 | #include 30 | #endif 31 | 32 | namespace dd { 33 | 34 | namespace { 35 | constexpr std::chrono::nanoseconds timespec_to_duration(timespec ts) { 36 | return std::chrono::seconds{ts.tv_sec} + std::chrono::nanoseconds{ts.tv_nsec}; 37 | } 38 | 39 | #ifdef _WIN32 40 | constexpr std::chrono::nanoseconds filetime_to_nanos(FILETIME t) { 41 | return std::chrono::nanoseconds{ 42 | ((static_cast(t.dwHighDateTime) << 32) | 43 | static_cast(t.dwLowDateTime)) * 44 | 100}; 45 | } 46 | #endif 47 | } // namespace 48 | 49 | CurrentThreadCpuClock::time_point CurrentThreadCpuClock::now() noexcept { 50 | #ifndef _WIN32 51 | timespec ts; 52 | clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts); 53 | return time_point{timespec_to_duration(ts)}; 54 | #else 55 | FILETIME creationTime, exitTime, kernelTime, userTime; 56 | if (!GetThreadTimes(GetCurrentThread(), 57 | &creationTime, 58 | &exitTime, 59 | &kernelTime, 60 | &userTime)) { 61 | return {}; 62 | } 63 | return time_point{filetime_to_nanos(kernelTime) + 64 | filetime_to_nanos(userTime)}; 65 | #endif 66 | } 67 | 68 | ProcessCpuClock::time_point ProcessCpuClock::now() noexcept { 69 | #ifndef _WIN32 70 | timespec ts; 71 | clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts); 72 | return time_point{timespec_to_duration(ts)}; 73 | #else 74 | FILETIME creationTime, exitTime, kernelTime, userTime; 75 | if (!GetProcessTimes(GetCurrentProcess(), 76 | &creationTime, 77 | &exitTime, 78 | &kernelTime, 79 | &userTime)) { 80 | return {}; 81 | } 82 | return time_point{filetime_to_nanos(kernelTime) + 83 | filetime_to_nanos(userTime)}; 84 | #endif 85 | } 86 | 87 | ThreadCpuClock::ThreadCpuClock() { 88 | #ifdef __linux__ 89 | pthread_getcpuclockid(pthread_self(), &clockid_); 90 | #elif __APPLE__ 91 | thread_ = mach_thread_self(); 92 | #elif _WIN32 93 | thread_ = GetCurrentThread(); 94 | #endif 95 | } 96 | 97 | ThreadCpuClock::time_point ThreadCpuClock::now() const noexcept { 98 | #ifdef __linux__ 99 | timespec ts; 100 | if (clock_gettime(clockid_, &ts)) { 101 | return {}; 102 | } 103 | return time_point{timespec_to_duration(ts)}; 104 | #elif __APPLE__ 105 | mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT; 106 | thread_basic_info_data_t info; 107 | kern_return_t kr = 108 | thread_info(thread_, THREAD_BASIC_INFO, (thread_info_t)&info, &count); 109 | 110 | if (kr != KERN_SUCCESS) { 111 | return {}; 112 | } 113 | 114 | return time_point{ 115 | std::chrono::seconds{info.user_time.seconds + info.system_time.seconds} + 116 | std::chrono::microseconds{info.user_time.microseconds + 117 | info.system_time.microseconds}}; 118 | #elif _WIN32 119 | FILETIME creationTime, exitTime, kernelTime, userTime; 120 | if (!GetThreadTimes( 121 | thread_, &creationTime, &exitTime, &kernelTime, &userTime)) { 122 | return {}; 123 | } 124 | return time_point{filetime_to_nanos(kernelTime) + 125 | filetime_to_nanos(userTime)}; 126 | #endif 127 | 128 | return {}; 129 | } 130 | 131 | } // namespace dd 132 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "variables": { 3 | "address_sanitizer%": 0, # enable address + undefined behaviour sanitizer 4 | "thread_sanitizer%": 0, # enable thread sanitizer, 5 | "build_tests%": 0 6 | }, 7 | "conditions": [ 8 | [ 9 | "build_tests != 'true'", 10 | { 11 | "targets": [ 12 | { 13 | "target_name": "dd_pprof", 14 | "sources": [ 15 | "bindings/profilers/heap.cc", 16 | "bindings/profilers/wall.cc", 17 | "bindings/per-isolate-data.cc", 18 | "bindings/thread-cpu-clock.cc", 19 | "bindings/translate-heap-profile.cc", 20 | "bindings/translate-time-profile.cc", 21 | "bindings/binding.cc" 22 | ], 23 | "include_dirs": [ 24 | "bindings", 25 | " | undefined; 42 | let gSourceMapper: SourceMapper | undefined; 43 | let gIntervalMicros: Microseconds; 44 | let gV8ProfilerStuckEventLoopDetected = 0; 45 | 46 | /** Make sure to stop profiler before node shuts down, otherwise profiling 47 | * signal might cause a crash if it occurs during shutdown */ 48 | process.once('exit', () => { 49 | if (isStarted()) stop(); 50 | }); 51 | 52 | export interface TimeProfilerOptions { 53 | /** time in milliseconds for which to collect profile. */ 54 | durationMillis?: Milliseconds; 55 | /** average time in microseconds between samples */ 56 | intervalMicros?: Microseconds; 57 | sourceMapper?: SourceMapper; 58 | 59 | /** 60 | * This configuration option is experimental. 61 | * When set to true, functions will be aggregated at the line level, rather 62 | * than at the function level. 63 | * This defaults to false. 64 | */ 65 | lineNumbers?: boolean; 66 | withContexts?: boolean; 67 | workaroundV8Bug?: boolean; 68 | collectCpuTime?: boolean; 69 | collectAsyncId?: boolean; 70 | useCPED?: boolean; 71 | } 72 | 73 | const DEFAULT_OPTIONS: TimeProfilerOptions = { 74 | durationMillis: DEFAULT_DURATION_MILLIS, 75 | intervalMicros: DEFAULT_INTERVAL_MICROS, 76 | lineNumbers: false, 77 | withContexts: false, 78 | workaroundV8Bug: true, 79 | collectCpuTime: false, 80 | collectAsyncId: false, 81 | useCPED: false, 82 | }; 83 | 84 | export async function profile(options: TimeProfilerOptions = {}) { 85 | options = {...DEFAULT_OPTIONS, ...options}; 86 | start(options); 87 | await delay(options.durationMillis!); 88 | return stop(); 89 | } 90 | 91 | // Temporarily retained for backwards compatibility with older tracer 92 | export function start(options: TimeProfilerOptions = {}) { 93 | options = {...DEFAULT_OPTIONS, ...options}; 94 | if (gProfiler) { 95 | throw new Error('Wall profiler is already started'); 96 | } 97 | 98 | gProfiler = new TimeProfiler({...options, isMainThread}); 99 | gSourceMapper = options.sourceMapper; 100 | gIntervalMicros = options.intervalMicros!; 101 | gV8ProfilerStuckEventLoopDetected = 0; 102 | 103 | gProfiler.start(); 104 | 105 | // If contexts are enabled without using CPED, set an initial empty context 106 | if (options.withContexts && !options.useCPED) { 107 | setContext({}); 108 | } 109 | } 110 | 111 | export function stop( 112 | restart = false, 113 | generateLabels?: GenerateTimeLabelsFunction, 114 | lowCardinalityLabels?: string[] 115 | ) { 116 | if (!gProfiler) { 117 | throw new Error('Wall profiler is not started'); 118 | } 119 | 120 | const profile = gProfiler.stop(restart); 121 | if (restart) { 122 | gV8ProfilerStuckEventLoopDetected = 123 | gProfiler.v8ProfilerStuckEventLoopDetected(); 124 | // Workaround for v8 bug, where profiler event processor thread is stuck in 125 | // a loop eating 100% CPU, leading to empty profiles. 126 | // Fully stop and restart the profiler to reset the profile to a valid state. 127 | if (gV8ProfilerStuckEventLoopDetected > 0) { 128 | gProfiler.stop(false); 129 | gProfiler.start(); 130 | } 131 | } else { 132 | gV8ProfilerStuckEventLoopDetected = 0; 133 | } 134 | 135 | const serializedProfile = serializeTimeProfile( 136 | profile, 137 | gIntervalMicros, 138 | gSourceMapper, 139 | true, 140 | generateLabels, 141 | lowCardinalityLabels 142 | ); 143 | if (!restart) { 144 | gProfiler.dispose(); 145 | gProfiler = undefined; 146 | gSourceMapper = undefined; 147 | } 148 | return serializedProfile; 149 | } 150 | 151 | export function getState() { 152 | if (!gProfiler) { 153 | throw new Error('Wall profiler is not started'); 154 | } 155 | return gProfiler.state; 156 | } 157 | 158 | export function setContext(context?: object) { 159 | if (!gProfiler) { 160 | throw new Error('Wall profiler is not started'); 161 | } 162 | gProfiler.context = context; 163 | } 164 | 165 | export function getContext() { 166 | if (!gProfiler) { 167 | throw new Error('Wall profiler is not started'); 168 | } 169 | return gProfiler.context; 170 | } 171 | 172 | export function getMetrics(): TimeProfilerMetrics { 173 | if (!gProfiler) { 174 | throw new Error('Wall profiler is not started'); 175 | } 176 | return gProfiler.metrics as TimeProfilerMetrics; 177 | } 178 | 179 | export function isStarted() { 180 | return !!gProfiler; 181 | } 182 | 183 | // Return 0 if no issue detected, 1 if possible issue, 2 if issue detected for certain 184 | export function v8ProfilerStuckEventLoopDetected() { 185 | return gV8ProfilerStuckEventLoopDetected; 186 | } 187 | 188 | export const constants = { 189 | kSampleCount, 190 | GARBAGE_COLLECTION_FUNCTION_NAME, 191 | NON_JS_THREADS_FUNCTION_NAME, 192 | }; 193 | export {getNativeThreadId}; 194 | -------------------------------------------------------------------------------- /ts/src/heap-profiler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Profile} from 'pprof-format'; 18 | 19 | import { 20 | getAllocationProfile, 21 | startSamplingHeapProfiler, 22 | stopSamplingHeapProfiler, 23 | monitorOutOfMemory as monitorOutOfMemoryImported, 24 | } from './heap-profiler-bindings'; 25 | import {serializeHeapProfile} from './profile-serializer'; 26 | import {SourceMapper} from './sourcemapper/sourcemapper'; 27 | import { 28 | AllocationProfileNode, 29 | GenerateAllocationLabelsFunction, 30 | } from './v8-types'; 31 | import {isMainThread} from 'worker_threads'; 32 | 33 | let enabled = false; 34 | let heapIntervalBytes = 0; 35 | let heapStackDepth = 0; 36 | 37 | /* 38 | * Collects a heap profile when heapProfiler is enabled. Otherwise throws 39 | * an error. 40 | * 41 | * Data is returned in V8 allocation profile format. 42 | */ 43 | export function v8Profile(): AllocationProfileNode { 44 | if (!enabled) { 45 | throw new Error('Heap profiler is not enabled.'); 46 | } 47 | return getAllocationProfile(); 48 | } 49 | 50 | /** 51 | * Collects a profile and returns it serialized in pprof format. 52 | * Throws if heap profiler is not enabled. 53 | * 54 | * @param ignoreSamplePath 55 | * @param sourceMapper 56 | */ 57 | export function profile( 58 | ignoreSamplePath?: string, 59 | sourceMapper?: SourceMapper, 60 | generateLabels?: GenerateAllocationLabelsFunction 61 | ): Profile { 62 | return convertProfile( 63 | v8Profile(), 64 | ignoreSamplePath, 65 | sourceMapper, 66 | generateLabels 67 | ); 68 | } 69 | 70 | export function convertProfile( 71 | rootNode: AllocationProfileNode, 72 | ignoreSamplePath?: string, 73 | sourceMapper?: SourceMapper, 74 | generateLabels?: GenerateAllocationLabelsFunction 75 | ): Profile { 76 | const startTimeNanos = Date.now() * 1000 * 1000; 77 | // Add node for external memory usage. 78 | // Current type definitions do not have external. 79 | // TODO: remove any once type definition is updated to include external. 80 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 81 | const {external}: {external: number} = process.memoryUsage() as any; 82 | if (external > 0) { 83 | const externalNode: AllocationProfileNode = { 84 | name: '(external)', 85 | scriptName: '', 86 | children: [], 87 | allocations: [{sizeBytes: external, count: 1}], 88 | }; 89 | rootNode.children.push(externalNode); 90 | } 91 | return serializeHeapProfile( 92 | rootNode, 93 | startTimeNanos, 94 | heapIntervalBytes, 95 | ignoreSamplePath, 96 | sourceMapper, 97 | generateLabels 98 | ); 99 | } 100 | 101 | /** 102 | * Starts heap profiling. If heap profiling has already been started with 103 | * the same parameters, this is a noop. If heap profiler has already been 104 | * started with different parameters, this throws an error. 105 | * 106 | * @param intervalBytes - average number of bytes between samples. 107 | * @param stackDepth - maximum stack depth for samples collected. 108 | */ 109 | export function start(intervalBytes: number, stackDepth: number) { 110 | if (enabled) { 111 | throw new Error( 112 | `Heap profiler is already started with intervalBytes ${heapIntervalBytes} and stackDepth ${stackDepth}` 113 | ); 114 | } 115 | heapIntervalBytes = intervalBytes; 116 | heapStackDepth = stackDepth; 117 | startSamplingHeapProfiler(heapIntervalBytes, heapStackDepth); 118 | enabled = true; 119 | } 120 | 121 | // Stops heap profiling. If heap profiling has not been started, does nothing. 122 | export function stop() { 123 | if (enabled) { 124 | enabled = false; 125 | stopSamplingHeapProfiler(); 126 | } 127 | } 128 | 129 | export type NearHeapLimitCallback = (profile: Profile) => void; 130 | 131 | export const CallbackMode = { 132 | Async: 1, 133 | Interrupt: 2, 134 | Both: 3, 135 | }; 136 | 137 | /** 138 | * Add monitoring for v8 heap, heap profiler must already be started. 139 | * When an out of heap memory event occurs: 140 | * - an extension of heap memory of |heapLimitExtensionSize| bytes is 141 | * requested to v8. This extension can occur |maxHeapLimitExtensionCount| 142 | * number of times. If the extension amount is not enough to satisfy 143 | * memory allocation that triggers GC and OOM, process will abort. 144 | * - heap profile is dumped as folded stacks on stderr if 145 | * |dumpHeapProfileOnSdterr| is true 146 | * - heap profile is dumped in temporary file and a new process is spawned 147 | * with |exportCommand| arguments and profile path appended at the end. 148 | * - |callback| is called. Callback can be invoked only if 149 | * heapLimitExtensionSize is enough for the process to continue. Invocation 150 | * will be done by a RequestInterrupt if |callbackMode| is Interrupt or Both, 151 | * this might be unsafe since Isolate should not be reentered 152 | * from RequestInterrupt, but this allows to interrupt synchronous code. 153 | * Otherwise the callback is scheduled to be called asynchronously. 154 | * @param heapLimitExtensionSize - amount of bytes heap should be expanded 155 | * with upon OOM 156 | * @param maxHeapLimitExtensionCount - maximum number of times heap size 157 | * extension can occur 158 | * @param dumpHeapProfileOnSdterr - dump heap profile on stderr upon OOM 159 | * @param exportCommand - command to execute upon OOM, filepath of a 160 | * temporary file containing heap profile will be appended 161 | * @param callback - callback to call when OOM occurs 162 | * @param callbackMode 163 | */ 164 | export function monitorOutOfMemory( 165 | heapLimitExtensionSize: number, 166 | maxHeapLimitExtensionCount: number, 167 | dumpHeapProfileOnSdterr: boolean, 168 | exportCommand?: Array, 169 | callback?: NearHeapLimitCallback, 170 | callbackMode?: number 171 | ) { 172 | if (!enabled) { 173 | throw new Error( 174 | 'Heap profiler must already be started to call monitorOutOfMemory' 175 | ); 176 | } 177 | let newCallback; 178 | if (typeof callback !== 'undefined') { 179 | newCallback = (profile: AllocationProfileNode) => { 180 | callback(convertProfile(profile)); 181 | }; 182 | } 183 | monitorOutOfMemoryImported( 184 | heapLimitExtensionSize, 185 | maxHeapLimitExtensionCount, 186 | dumpHeapProfileOnSdterr, 187 | exportCommand || [], 188 | newCallback, 189 | typeof callbackMode !== 'undefined' ? callbackMode : CallbackMode.Async, 190 | isMainThread 191 | ); 192 | } 193 | -------------------------------------------------------------------------------- /bindings/profilers/wall.hh: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Datadog, Inc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include "contexts.hh" 20 | #include "thread-cpu-clock.hh" 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | namespace dd { 30 | 31 | struct Result { 32 | Result() = default; 33 | explicit Result(const char* msg) : success{false}, msg{msg} {}; 34 | 35 | bool success = true; 36 | std::string msg; 37 | }; 38 | 39 | using ContextPtr = std::shared_ptr>; 40 | 41 | class PersistentContextPtr; 42 | 43 | class WallProfiler : public Nan::ObjectWrap { 44 | public: 45 | enum class CollectionMode { kNoCollect, kPassThrough, kCollectContexts }; 46 | enum Fields { kSampleCount, kFieldCount }; 47 | 48 | private: 49 | std::chrono::microseconds samplingPeriod_{0}; 50 | v8::CpuProfiler* cpuProfiler_ = nullptr; 51 | 52 | bool useCPED_ = false; 53 | // If we aren't using the CPED, we use a single context ptr stored here. 54 | ContextPtr curContext_; 55 | // Otherwise we'll use an internal field in objects stored in CPED. We must 56 | // construct objects with an internal field count of 1 and a specially 57 | // constructed prototype. 58 | v8::Global cpedProxyTemplate_; 59 | v8::Global cpedProxyProto_; 60 | v8::Global cpedProxySymbol_; 61 | 62 | // We track live context pointers in a set to avoid memory leaks. They will 63 | // be deleted when the profiler is disposed. 64 | std::unordered_set liveContextPtrs_; 65 | // Context pointers belonging to GC'd CPED objects register themselves here. 66 | // They will be reused. 67 | std::deque deadContextPtrs_; 68 | 69 | std::atomic gcCount = 0; 70 | std::atomic setInProgress_ = false; 71 | double gcAsyncId; 72 | ContextPtr gcContext_; 73 | 74 | std::atomic collectionMode_; 75 | std::atomic noCollectCallCount_; 76 | v8::ProfilerId profileId_; 77 | uint64_t profileIdx_ = 0; 78 | bool includeLines_ = false; 79 | bool withContexts_ = false; 80 | bool started_ = false; 81 | bool workaroundV8Bug_; 82 | static inline constexpr bool detectV8Bug_ = true; 83 | bool collectCpuTime_; 84 | bool collectAsyncId_; 85 | bool isMainThread_; 86 | int v8ProfilerStuckEventLoopDetected_ = 0; 87 | ProcessCpuClock::time_point startProcessCpuTime_{}; 88 | int64_t startThreadCpuTime_ = 0; 89 | /* threadCpuStopWatch_ is used to measure CPU consumed by JS thread owning the 90 | * WallProfiler object during profiling period of main worker thread. */ 91 | ThreadCpuStopWatch threadCpuStopWatch_; 92 | uint32_t* fields_; 93 | v8::Global jsArray_; 94 | 95 | struct SampleContext { 96 | ContextPtr context; 97 | int64_t time_from; 98 | int64_t time_to; 99 | int64_t cpu_time; 100 | double async_id; 101 | }; 102 | 103 | using ContextBuffer = std::vector; 104 | ContextBuffer contexts_; 105 | 106 | ~WallProfiler() = default; 107 | void Dispose(v8::Isolate* isolate, bool removeFromMap); 108 | 109 | // A new CPU profiler object will be created each time profiling is started 110 | // to work around https://bugs.chromium.org/p/v8/issues/detail?id=11051. 111 | v8::CpuProfiler* CreateV8CpuProfiler(); 112 | 113 | ContextsByNode GetContextsByNode(v8::CpuProfile* profile, 114 | ContextBuffer& contexts, 115 | int64_t startCpuTime); 116 | 117 | bool waitForSignal(uint64_t targetCallCount = 0); 118 | static void CleanupHook(void* data); 119 | void Cleanup(v8::Isolate* isolate); 120 | 121 | ContextPtr GetContextPtr(v8::Isolate* isolate); 122 | ContextPtr GetContextPtrSignalSafe(v8::Isolate* isolate); 123 | 124 | void SetCurrentContextPtr(v8::Isolate* isolate, v8::Local context); 125 | 126 | public: 127 | /** 128 | * @param samplingPeriodMicros sampling interval, in microseconds 129 | * @param durationMicros the duration of sampling, in microseconds. This 130 | * parameter is informative; it is up to the caller to call the Stop method 131 | * every period. The parameter is used to preallocate data structures that 132 | * should not be reallocated in async signal safe code. 133 | * @param useCPED whether to use the V8 ContinuationPreservedEmbedderData to 134 | * store the current sampling context. It can be used if AsyncLocalStorage 135 | * uses the AsyncContextFrame implementation (experimental in Node 23, default 136 | * in Node 24.) 137 | */ 138 | explicit WallProfiler(std::chrono::microseconds samplingPeriod, 139 | std::chrono::microseconds duration, 140 | bool includeLines, 141 | bool withContexts, 142 | bool workaroundV8bug, 143 | bool collectCpuTime, 144 | bool collectAsyncId, 145 | bool isMainThread, 146 | bool useCPED); 147 | 148 | v8::Local GetContext(v8::Isolate*); 149 | void SetContext(v8::Isolate*, v8::Local); 150 | void PushContext(int64_t time_from, 151 | int64_t time_to, 152 | int64_t cpu_time, 153 | v8::Isolate* isolate); 154 | v8::Local GetMetrics(v8::Isolate*); 155 | 156 | Result StartImpl(); 157 | v8::ProfilerId StartInternal(); 158 | Result StopImpl(bool restart, v8::Local& profile); 159 | 160 | CollectionMode collectionMode() { 161 | auto res = collectionMode_.load(std::memory_order_relaxed); 162 | if (res == CollectionMode::kNoCollect) { 163 | noCollectCallCount_.fetch_add(1, std::memory_order_relaxed); 164 | } 165 | std::atomic_signal_fence(std::memory_order_acquire); 166 | return res; 167 | } 168 | 169 | bool collectCpuTime() const { return collectCpuTime_; } 170 | 171 | bool interceptSignal() const { return withContexts_ || workaroundV8Bug_; } 172 | 173 | int v8ProfilerStuckEventLoopDetected() const { 174 | return v8ProfilerStuckEventLoopDetected_; 175 | } 176 | 177 | ThreadCpuClock::duration GetAndResetThreadCpu() { 178 | return threadCpuStopWatch_.GetAndReset(); 179 | } 180 | 181 | double GetAsyncId(v8::Isolate* isolate); 182 | void OnGCStart(v8::Isolate* isolate); 183 | void OnGCEnd(); 184 | 185 | void MarkDeadPersistentContextPtr(PersistentContextPtr* ptr); 186 | 187 | static NAN_METHOD(New); 188 | static NAN_METHOD(Start); 189 | static NAN_METHOD(Stop); 190 | static NAN_METHOD(V8ProfilerStuckEventLoopDetected); 191 | static NAN_METHOD(Dispose); 192 | static NAN_MODULE_INIT(Init); 193 | static NAN_GETTER(GetContext); 194 | static NAN_SETTER(SetContext); 195 | static NAN_GETTER(SharedArrayGetter); 196 | static NAN_GETTER(GetMetrics); 197 | }; 198 | 199 | } // namespace dd 200 | -------------------------------------------------------------------------------- /ts/test/test-heap-profiler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as sinon from 'sinon'; 18 | 19 | import * as heapProfiler from '../src/heap-profiler'; 20 | import * as v8HeapProfiler from '../src/heap-profiler-bindings'; 21 | import {AllocationProfileNode, LabelSet} from '../src/v8-types'; 22 | import {fork} from 'child_process'; 23 | import path from 'path'; 24 | import fs from 'fs'; 25 | 26 | import { 27 | heapProfileExcludePath, 28 | heapProfileIncludePath, 29 | heapProfileWithExternal, 30 | v8HeapProfile, 31 | v8HeapWithPathProfile, 32 | heapProfileIncludePathWithLabels, 33 | } from './profiles-for-tests'; 34 | 35 | const copy = require('deep-copy'); 36 | const assert = require('assert'); 37 | 38 | describe('HeapProfiler', () => { 39 | let startStub: sinon.SinonStub<[number, number], void>; 40 | let stopStub: sinon.SinonStub<[], void>; 41 | let profileStub: sinon.SinonStub<[], AllocationProfileNode>; 42 | let dateStub: sinon.SinonStub<[], number>; 43 | let memoryUsageStub: sinon.SinonStub<[], NodeJS.MemoryUsage>; 44 | beforeEach(() => { 45 | startStub = sinon.stub(v8HeapProfiler, 'startSamplingHeapProfiler'); 46 | stopStub = sinon.stub(v8HeapProfiler, 'stopSamplingHeapProfiler'); 47 | dateStub = sinon.stub(Date, 'now').returns(0); 48 | }); 49 | 50 | afterEach(() => { 51 | heapProfiler.stop(); 52 | startStub.restore(); 53 | stopStub.restore(); 54 | profileStub.restore(); 55 | dateStub.restore(); 56 | memoryUsageStub.restore(); 57 | }); 58 | describe('profile', () => { 59 | it('should return a profile equal to the expected profile when external memory is allocated', async () => { 60 | profileStub = sinon 61 | .stub(v8HeapProfiler, 'getAllocationProfile') 62 | .returns(copy(v8HeapProfile)); 63 | memoryUsageStub = sinon.stub(process, 'memoryUsage').returns({ 64 | external: 1024, 65 | rss: 2048, 66 | heapTotal: 4096, 67 | heapUsed: 2048, 68 | arrayBuffers: 512, 69 | }); 70 | const intervalBytes = 1024 * 512; 71 | const stackDepth = 32; 72 | heapProfiler.start(intervalBytes, stackDepth); 73 | const profile = heapProfiler.profile(); 74 | assert.deepEqual(heapProfileWithExternal, profile); 75 | }); 76 | 77 | it('should return a profile equal to the expected profile when including all samples', async () => { 78 | profileStub = sinon 79 | .stub(v8HeapProfiler, 'getAllocationProfile') 80 | .returns(copy(v8HeapWithPathProfile)); 81 | memoryUsageStub = sinon.stub(process, 'memoryUsage').returns({ 82 | external: 0, 83 | rss: 2048, 84 | heapTotal: 4096, 85 | heapUsed: 2048, 86 | arrayBuffers: 512, 87 | }); 88 | const intervalBytes = 1024 * 512; 89 | const stackDepth = 32; 90 | heapProfiler.start(intervalBytes, stackDepth); 91 | const profile = heapProfiler.profile(); 92 | assert.deepEqual(heapProfileIncludePath, profile); 93 | }); 94 | 95 | it('should return a profile equal to the expected profile when excluding profiler samples', async () => { 96 | profileStub = sinon 97 | .stub(v8HeapProfiler, 'getAllocationProfile') 98 | .returns(copy(v8HeapWithPathProfile)); 99 | memoryUsageStub = sinon.stub(process, 'memoryUsage').returns({ 100 | external: 0, 101 | rss: 2048, 102 | heapTotal: 4096, 103 | heapUsed: 2048, 104 | arrayBuffers: 512, 105 | }); 106 | const intervalBytes = 1024 * 512; 107 | const stackDepth = 32; 108 | heapProfiler.start(intervalBytes, stackDepth); 109 | const profile = heapProfiler.profile('@google-cloud/profiler'); 110 | assert.deepEqual(heapProfileExcludePath, profile); 111 | }); 112 | 113 | it('should return a profile equal to the expected profile when adding labels', async () => { 114 | profileStub = sinon 115 | .stub(v8HeapProfiler, 'getAllocationProfile') 116 | .returns(copy(v8HeapWithPathProfile)); 117 | memoryUsageStub = sinon.stub(process, 'memoryUsage').returns({ 118 | external: 0, 119 | rss: 2048, 120 | heapTotal: 4096, 121 | heapUsed: 2048, 122 | arrayBuffers: 512, 123 | }); 124 | const intervalBytes = 1024 * 512; 125 | const stackDepth = 32; 126 | heapProfiler.start(intervalBytes, stackDepth); 127 | const labels: LabelSet = {baz: 'bar'}; 128 | const profile = heapProfiler.profile(undefined, undefined, () => { 129 | return labels; 130 | }); 131 | assert.deepEqual(heapProfileIncludePathWithLabels, profile); 132 | }); 133 | 134 | it('should throw error when not started', () => { 135 | assert.throws( 136 | () => { 137 | heapProfiler.profile(); 138 | }, 139 | (err: Error) => { 140 | return err.message === 'Heap profiler is not enabled.'; 141 | } 142 | ); 143 | }); 144 | 145 | it('should throw error when started then stopped', () => { 146 | const intervalBytes = 1024 * 512; 147 | const stackDepth = 32; 148 | heapProfiler.start(intervalBytes, stackDepth); 149 | heapProfiler.stop(); 150 | assert.throws( 151 | () => { 152 | heapProfiler.profile(); 153 | }, 154 | (err: Error) => { 155 | return err.message === 'Heap profiler is not enabled.'; 156 | } 157 | ); 158 | }); 159 | }); 160 | 161 | describe('start', () => { 162 | it('should call startSamplingHeapProfiler', () => { 163 | const intervalBytes1 = 1024 * 512; 164 | const stackDepth1 = 32; 165 | heapProfiler.start(intervalBytes1, stackDepth1); 166 | assert.ok( 167 | startStub.calledWith(intervalBytes1, stackDepth1), 168 | 'expected startSamplingHeapProfiler to be called' 169 | ); 170 | }); 171 | it('should throw error when enabled and started with different parameters', () => { 172 | const intervalBytes1 = 1024 * 512; 173 | const stackDepth1 = 32; 174 | heapProfiler.start(intervalBytes1, stackDepth1); 175 | assert.ok( 176 | startStub.calledWith(intervalBytes1, stackDepth1), 177 | 'expected startSamplingHeapProfiler to be called' 178 | ); 179 | startStub.resetHistory(); 180 | const intervalBytes2 = 1024 * 128; 181 | const stackDepth2 = 64; 182 | try { 183 | heapProfiler.start(intervalBytes2, stackDepth2); 184 | } catch (e) { 185 | assert.strictEqual( 186 | (e as Error).message, 187 | 'Heap profiler is already started with intervalBytes 524288 and' + 188 | ' stackDepth 64' 189 | ); 190 | } 191 | assert.ok( 192 | !startStub.called, 193 | 'expected startSamplingHeapProfiler not to be called second time' 194 | ); 195 | }); 196 | }); 197 | 198 | describe('stop', () => { 199 | it('should not call stopSamplingHeapProfiler if profiler not started', () => { 200 | heapProfiler.stop(); 201 | assert.ok(!stopStub.called, 'stop() should have been no-op.'); 202 | }); 203 | it('should call stopSamplingHeapProfiler if profiler started', () => { 204 | heapProfiler.start(1024 * 512, 32); 205 | heapProfiler.stop(); 206 | assert.ok( 207 | stopStub.called, 208 | 'expected stopSamplingHeapProfiler to be called' 209 | ); 210 | }); 211 | }); 212 | }); 213 | 214 | describe('OOMMonitoring', () => { 215 | it('should call external process upon OOM', async function () { 216 | // this test is very slow on some configs (asan/valgrind) 217 | this.timeout(20000); 218 | const proc = fork(path.join(__dirname, 'oom.js'), { 219 | execArgv: ['--max-old-space-size=50'], 220 | }); 221 | const checkFilePath = 'oom_check.log'; 222 | if (fs.existsSync(checkFilePath)) { 223 | fs.unlinkSync(checkFilePath); 224 | } 225 | // wait for proc to exit 226 | await new Promise((resolve, reject) => { 227 | proc.on('exit', code => { 228 | if (code === 0) { 229 | reject(); 230 | } else { 231 | resolve(); 232 | } 233 | }); 234 | }); 235 | assert.equal(fs.readFileSync(checkFilePath), 'ok'); 236 | fs.unlinkSync(checkFilePath); 237 | }); 238 | }); 239 | -------------------------------------------------------------------------------- /ts/test/worker.ts: -------------------------------------------------------------------------------- 1 | import {Worker, isMainThread, workerData, parentPort} from 'worker_threads'; 2 | import {pbkdf2} from 'crypto'; 3 | import {time} from '../src/index'; 4 | import {Profile, ValueType} from 'pprof-format'; 5 | import {getAndVerifyPresence, getAndVerifyString} from './profiles-for-tests'; 6 | import {satisfies} from 'semver'; 7 | 8 | import assert from 'assert'; 9 | import {AsyncLocalStorage} from 'async_hooks'; 10 | 11 | const DURATION_MILLIS = 1000; 12 | const intervalMicros = 10000; 13 | const withContexts = 14 | process.platform === 'darwin' || process.platform === 'linux'; 15 | const useCPED = 16 | withContexts && 17 | ((satisfies(process.versions.node, '>=24.0.0') && 18 | !process.execArgv.includes('--no-async-context-frame')) || 19 | (satisfies(process.versions.node, '>=22.7.0') && 20 | process.execArgv.includes('--experimental-async-context-frame'))); 21 | const collectAsyncId = 22 | withContexts && satisfies(process.versions.node, '>=24.0.0'); 23 | 24 | function createWorker(durationMs: number): Promise { 25 | return new Promise((resolve, reject) => { 26 | const profiles: Profile[] = []; 27 | new Worker(__filename, {workerData: {durationMs}}) 28 | .on('exit', exitCode => { 29 | if (exitCode !== 0) reject(); 30 | setTimeout(() => { 31 | // Run a second worker after the first one exited to test for proper 32 | // cleanup after first worker. This used to segfault. 33 | new Worker(__filename, {workerData: {durationMs}}) 34 | .on('exit', exitCode => { 35 | if (exitCode !== 0) reject(); 36 | resolve(profiles); 37 | }) 38 | .on('error', reject) 39 | .on('message', profile => { 40 | profiles.push(profile); 41 | }); 42 | }, Math.floor(Math.random() * durationMs)); 43 | }) 44 | .on('error', reject) 45 | .on('message', profile => { 46 | profiles.push(profile); 47 | }); 48 | }); 49 | } 50 | 51 | async function executeWorkers(nbWorkers: number, durationMs: number) { 52 | const workers = []; 53 | for (let i = 0; i < nbWorkers; i++) { 54 | workers.push(createWorker(durationMs)); 55 | } 56 | return Promise.all(workers).then(profiles => profiles.flat()); 57 | } 58 | 59 | function getCpuUsage() { 60 | const cpu = process.cpuUsage(); 61 | return cpu.user + cpu.system; 62 | } 63 | 64 | async function main(durationMs: number) { 65 | if (useCPED) new AsyncLocalStorage().enterWith(1); 66 | time.start({ 67 | durationMillis: durationMs * 3, 68 | intervalMicros, 69 | withContexts, 70 | collectCpuTime: withContexts, 71 | useCPED: useCPED, 72 | collectAsyncId: collectAsyncId, 73 | }); 74 | if (withContexts) { 75 | time.setContext({}); 76 | } 77 | 78 | const cpu0 = getCpuUsage(); 79 | const nbWorkers = Number(process.argv[2] ?? 2); 80 | 81 | // start workers 82 | const workers = executeWorkers(nbWorkers, durationMs); 83 | 84 | const deadline = Date.now() + durationMs; 85 | // wait for all work to finish 86 | await Promise.all([bar(deadline), foo(deadline)]); 87 | const workerProfiles = await workers; 88 | 89 | // restart and check profile 90 | const profile1 = time.stop(true); 91 | const cpu1 = getCpuUsage(); 92 | 93 | workerProfiles.forEach(checkProfile); 94 | checkProfile(profile1); 95 | if (withContexts) { 96 | checkCpuTime(profile1, cpu1 - cpu0, workerProfiles); 97 | } 98 | const newDeadline = Date.now() + durationMs; 99 | await Promise.all([bar(newDeadline), foo(newDeadline)]); 100 | 101 | const profile2 = time.stop(); 102 | const cpu2 = getCpuUsage(); 103 | checkProfile(profile2); 104 | if (withContexts) { 105 | checkCpuTime(profile2, cpu2 - cpu1); 106 | } 107 | } 108 | 109 | async function worker(durationMs: number) { 110 | if (useCPED) new AsyncLocalStorage().enterWith(1); 111 | time.start({ 112 | durationMillis: durationMs, 113 | intervalMicros, 114 | withContexts, 115 | collectCpuTime: withContexts, 116 | useCPED: useCPED, 117 | collectAsyncId: collectAsyncId, 118 | }); 119 | if (withContexts) { 120 | time.setContext({}); 121 | } 122 | 123 | const deadline = Date.now() + durationMs; 124 | await Promise.all([bar(deadline), foo(deadline)]); 125 | 126 | const profile = time.stop(); 127 | parentPort?.postMessage(profile); 128 | } 129 | 130 | if (isMainThread) { 131 | main(DURATION_MILLIS); 132 | } else { 133 | worker(workerData.durationMs); 134 | } 135 | 136 | function valueName(profile: Profile, vt: ValueType) { 137 | const type = getAndVerifyString(profile.stringTable!, vt, 'type'); 138 | const unit = getAndVerifyString(profile.stringTable!, vt, 'unit'); 139 | return `${type}/${unit}`; 140 | } 141 | 142 | function sampleName(profile: Profile, sampleType: ValueType[]) { 143 | return sampleType.map(valueName.bind(null, profile)); 144 | } 145 | 146 | function getCpuTime(profile: Profile) { 147 | let jsCpuTime = 0; 148 | let nonJsCpuTime = 0; 149 | if (!withContexts) return {jsCpuTime, nonJsCpuTime}; 150 | for (const sample of profile.sample!) { 151 | const locationId = sample.locationId[0]; 152 | const location = getAndVerifyPresence( 153 | profile.location!, 154 | locationId as number 155 | ); 156 | const functionId = location.line![0].functionId; 157 | const fn = getAndVerifyPresence(profile.function!, functionId as number); 158 | const fn_name = profile.stringTable.strings[fn.name as number]; 159 | if (fn_name === time.constants.NON_JS_THREADS_FUNCTION_NAME) { 160 | nonJsCpuTime += sample.value![2] as number; 161 | assert.strictEqual(sample.value![0], 0); 162 | assert.strictEqual(sample.value![1], 0); 163 | } else { 164 | jsCpuTime += sample.value![2] as number; 165 | } 166 | } 167 | 168 | return {jsCpuTime, nonJsCpuTime}; 169 | } 170 | 171 | function checkCpuTime( 172 | profile: Profile, 173 | processCpuTimeMicros: number, 174 | workerProfiles: Profile[] = [], 175 | maxRelativeError = 0.1 176 | ) { 177 | let workersJsCpuTime = 0; 178 | let workersNonJsCpuTime = 0; 179 | 180 | for (const workerProfile of workerProfiles) { 181 | const {jsCpuTime, nonJsCpuTime} = getCpuTime(workerProfile); 182 | workersJsCpuTime += jsCpuTime; 183 | workersNonJsCpuTime += nonJsCpuTime; 184 | } 185 | 186 | const {jsCpuTime: mainJsCpuTime, nonJsCpuTime: mainNonJsCpuTime} = 187 | getCpuTime(profile); 188 | 189 | // workers should not report non-JS CPU time 190 | assert.strictEqual( 191 | workersNonJsCpuTime, 192 | 0, 193 | 'worker non-JS CPU time should be null' 194 | ); 195 | 196 | const totalCpuTimeMicros = 197 | (mainJsCpuTime + mainNonJsCpuTime + workersJsCpuTime) / 1000; 198 | const err = 199 | Math.abs(totalCpuTimeMicros - processCpuTimeMicros) / processCpuTimeMicros; 200 | const msg = `process cpu time: ${ 201 | processCpuTimeMicros / 1000 202 | }ms\ntotal profile cpu time: ${ 203 | totalCpuTimeMicros / 1000 204 | }ms\nmain JS cpu time: ${mainJsCpuTime / 1000000}ms\nworker JS cpu time: ${ 205 | workersJsCpuTime / 1000000 206 | }\nnon-JS cpu time: ${mainNonJsCpuTime / 1000000}ms\nerror: ${err}`; 207 | assert.ok( 208 | err <= maxRelativeError, 209 | `total profile CPU time should be close to process cpu time:\n${msg}` 210 | ); 211 | } 212 | 213 | function checkProfile(profile: Profile) { 214 | assert.deepStrictEqual(sampleName(profile, profile.sampleType!), [ 215 | 'sample/count', 216 | 'wall/nanoseconds', 217 | ...(withContexts ? ['cpu/nanoseconds'] : []), 218 | ]); 219 | assert.strictEqual(typeof profile.timeNanos, 'number'); 220 | assert.strictEqual(typeof profile.durationNanos, 'number'); 221 | assert.strictEqual(typeof profile.period, 'number'); 222 | assert.strictEqual( 223 | valueName(profile, profile.periodType!), 224 | 'wall/nanoseconds' 225 | ); 226 | 227 | assert.ok(profile.sample.length > 0, 'No samples'); 228 | 229 | for (const sample of profile.sample!) { 230 | assert.deepStrictEqual(sample.label, []); 231 | 232 | for (const value of sample.value!) { 233 | assert.strictEqual(typeof value, 'number'); 234 | } 235 | 236 | for (const locationId of sample.locationId!) { 237 | const location = getAndVerifyPresence( 238 | profile.location!, 239 | locationId as number 240 | ); 241 | 242 | for (const {functionId, line} of location.line!) { 243 | const fn = getAndVerifyPresence( 244 | profile.function!, 245 | functionId as number 246 | ); 247 | 248 | getAndVerifyString(profile.stringTable!, fn, 'name'); 249 | getAndVerifyString(profile.stringTable!, fn, 'systemName'); 250 | getAndVerifyString(profile.stringTable!, fn, 'filename'); 251 | assert.strictEqual(typeof line, 'number'); 252 | } 253 | } 254 | } 255 | } 256 | 257 | async function bar(deadline: number) { 258 | let done = false; 259 | setTimeout(() => { 260 | done = true; 261 | }, deadline - Date.now()); 262 | while (!done) { 263 | await new Promise(resolve => { 264 | pbkdf2('secret', 'salt', 100000, 64, 'sha512', () => { 265 | resolve(); 266 | }); 267 | }); 268 | } 269 | } 270 | 271 | function fooWork() { 272 | let sum = 0; 273 | for (let i = 0; i < 1e7; i++) { 274 | sum += sum; 275 | } 276 | return sum; 277 | } 278 | 279 | async function foo(deadline: number) { 280 | let done = false; 281 | setTimeout(() => { 282 | done = true; 283 | }, deadline - Date.now()); 284 | 285 | while (!done) { 286 | await new Promise(resolve => { 287 | fooWork(); 288 | setImmediate(() => resolve()); 289 | }); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /bindings/translate-time-profile.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include "translate-time-profile.hh" 18 | #include 19 | #include "profile-translator.hh" 20 | 21 | namespace dd { 22 | 23 | namespace { 24 | class TimeProfileTranslator : ProfileTranslator { 25 | private: 26 | ContextsByNode* contextsByNode; 27 | v8::Local emptyArray = NewArray(0); 28 | v8::Local zero = NewInteger(0); 29 | 30 | #define FIELDS \ 31 | X(name) \ 32 | X(scriptName) \ 33 | X(scriptId) \ 34 | X(lineNumber) \ 35 | X(columnNumber) \ 36 | X(hitCount) \ 37 | X(children) \ 38 | X(contexts) 39 | 40 | #define X(name) v8::Local str_##name = NewString(#name); 41 | FIELDS 42 | #undef X 43 | 44 | v8::Local getContextsForNode(const v8::CpuProfileNode* node, 45 | uint32_t& hitcount) { 46 | hitcount = node->GetHitCount(); 47 | if (!contextsByNode) { 48 | // custom contexts are not enabled, keep the node hitcount and return 49 | // empty array 50 | return emptyArray; 51 | } 52 | 53 | auto it = contextsByNode->find(node); 54 | auto contexts = emptyArray; 55 | if (it != contextsByNode->end()) { 56 | hitcount = it->second.hitcount; 57 | contexts = it->second.contexts; 58 | } else { 59 | // no context found for node, discard it since every sample taken from 60 | // signal handler should have a matching context if it does not, it means 61 | // sample was captured by a deopt event 62 | hitcount = 0; 63 | } 64 | return contexts; 65 | } 66 | 67 | v8::Local CreateTimeNode(v8::Local name, 68 | v8::Local scriptName, 69 | v8::Local scriptId, 70 | v8::Local lineNumber, 71 | v8::Local columnNumber, 72 | v8::Local hitCount, 73 | v8::Local children, 74 | v8::Local contexts) { 75 | v8::Local js_node = NewObject(); 76 | #define X(name) Set(js_node, str_##name, name); 77 | FIELDS 78 | #undef X 79 | #undef FIELDS 80 | return js_node; 81 | } 82 | 83 | v8::Local GetLineNumberTimeProfileChildren( 84 | const v8::CpuProfileNode* node) { 85 | unsigned int index = 0; 86 | v8::Local children; 87 | int32_t count = node->GetChildrenCount(); 88 | 89 | unsigned int hitLineCount = node->GetHitLineCount(); 90 | unsigned int hitCount = node->GetHitCount(); 91 | auto scriptId = NewInteger(node->GetScriptId()); 92 | if (hitLineCount > 0) { 93 | std::vector entries(hitLineCount); 94 | node->GetLineTicks(&entries[0], hitLineCount); 95 | children = NewArray(count + hitLineCount); 96 | for (const v8::CpuProfileNode::LineTick entry : entries) { 97 | Set(children, 98 | index++, 99 | CreateTimeNode(node->GetFunctionName(), 100 | node->GetScriptResourceName(), 101 | scriptId, 102 | NewInteger(entry.line), 103 | // V8 14+ (Node.js 25+) added column field to LineTick struct 104 | #if V8_MAJOR_VERSION >= 14 105 | NewInteger(entry.column), 106 | #else 107 | zero, 108 | #endif 109 | NewInteger(entry.hit_count), 110 | emptyArray, 111 | emptyArray)); 112 | } 113 | } else if (hitCount > 0) { 114 | // Handle nodes for pseudo-functions like "process" and "garbage 115 | // collection" which do not have hit line counts. 116 | children = NewArray(count + 1); 117 | Set(children, 118 | index++, 119 | CreateTimeNode(node->GetFunctionName(), 120 | node->GetScriptResourceName(), 121 | scriptId, 122 | NewInteger(node->GetLineNumber()), 123 | NewInteger(node->GetColumnNumber()), 124 | NewInteger(hitCount), 125 | emptyArray, 126 | emptyArray)); 127 | } else { 128 | children = NewArray(count); 129 | } 130 | 131 | for (int32_t i = 0; i < count; i++) { 132 | Set(children, 133 | index++, 134 | TranslateLineNumbersTimeProfileNode(node, node->GetChild(i))); 135 | }; 136 | 137 | return children; 138 | } 139 | 140 | v8::Local TranslateLineNumbersTimeProfileNode( 141 | const v8::CpuProfileNode* parent, const v8::CpuProfileNode* node) { 142 | return CreateTimeNode(parent->GetFunctionName(), 143 | parent->GetScriptResourceName(), 144 | NewInteger(parent->GetScriptId()), 145 | NewInteger(node->GetLineNumber()), 146 | NewInteger(node->GetColumnNumber()), 147 | zero, 148 | GetLineNumberTimeProfileChildren(node), 149 | emptyArray); 150 | } 151 | 152 | // In profiles with line level accurate line numbers, a node's line number 153 | // and column number refer to the line/column from which the function was 154 | // called. 155 | v8::Local TranslateLineNumbersTimeProfileRoot( 156 | const v8::CpuProfileNode* node) { 157 | int32_t count = node->GetChildrenCount(); 158 | std::vector> childrenArrs(count); 159 | int32_t childCount = 0; 160 | for (int32_t i = 0; i < count; i++) { 161 | v8::Local c = 162 | GetLineNumberTimeProfileChildren(node->GetChild(i)); 163 | childCount = childCount + c->Length(); 164 | childrenArrs[i] = c; 165 | } 166 | 167 | v8::Local children = NewArray(childCount); 168 | int32_t idx = 0; 169 | for (int32_t i = 0; i < count; i++) { 170 | v8::Local arr = childrenArrs[i]; 171 | for (uint32_t j = 0; j < arr->Length(); j++) { 172 | Set(children, idx, Get(arr, j).ToLocalChecked()); 173 | idx++; 174 | } 175 | } 176 | 177 | return CreateTimeNode(node->GetFunctionName(), 178 | node->GetScriptResourceName(), 179 | NewInteger(node->GetScriptId()), 180 | NewInteger(node->GetLineNumber()), 181 | NewInteger(node->GetColumnNumber()), 182 | zero, 183 | children, 184 | emptyArray); 185 | } 186 | 187 | v8::Local TranslateTimeProfileNode( 188 | const v8::CpuProfileNode* node) { 189 | int32_t count = node->GetChildrenCount(); 190 | v8::Local children = NewArray(count); 191 | for (int32_t i = 0; i < count; i++) { 192 | Set(children, i, TranslateTimeProfileNode(node->GetChild(i))); 193 | } 194 | 195 | uint32_t hitcount = 0; 196 | auto contexts = getContextsForNode(node, hitcount); 197 | 198 | return CreateTimeNode(node->GetFunctionName(), 199 | node->GetScriptResourceName(), 200 | NewInteger(node->GetScriptId()), 201 | NewInteger(node->GetLineNumber()), 202 | NewInteger(node->GetColumnNumber()), 203 | NewInteger(hitcount), 204 | children, 205 | contexts); 206 | } 207 | 208 | public: 209 | explicit TimeProfileTranslator(ContextsByNode* nls = nullptr) 210 | : contextsByNode(nls) {} 211 | 212 | v8::Local TranslateTimeProfile(const v8::CpuProfile* profile, 213 | bool includeLineInfo, 214 | bool hasCpuTime, 215 | int64_t nonJSThreadsCpuTime) { 216 | v8::Local js_profile = NewObject(); 217 | 218 | if (includeLineInfo) { 219 | Set(js_profile, 220 | NewString("topDownRoot"), 221 | TranslateLineNumbersTimeProfileRoot(profile->GetTopDownRoot())); 222 | } else { 223 | Set(js_profile, 224 | NewString("topDownRoot"), 225 | TranslateTimeProfileNode(profile->GetTopDownRoot())); 226 | } 227 | Set(js_profile, NewString("startTime"), NewNumber(profile->GetStartTime())); 228 | Set(js_profile, NewString("endTime"), NewNumber(profile->GetEndTime())); 229 | Set(js_profile, NewString("hasCpuTime"), NewBoolean(hasCpuTime)); 230 | 231 | Set(js_profile, 232 | NewString("nonJSThreadsCpuTime"), 233 | NewNumber(nonJSThreadsCpuTime)); 234 | return js_profile; 235 | } 236 | }; 237 | } // namespace 238 | 239 | v8::Local TranslateTimeProfile(const v8::CpuProfile* profile, 240 | bool includeLineInfo, 241 | ContextsByNode* contextsByNode, 242 | bool hasCpuTime, 243 | int64_t nonJSThreadsCpuTime) { 244 | return TimeProfileTranslator(contextsByNode) 245 | .TranslateTimeProfile( 246 | profile, includeLineInfo, hasCpuTime, nonJSThreadsCpuTime); 247 | } 248 | 249 | } // namespace dd 250 | -------------------------------------------------------------------------------- /doc/sample_context_in_cped.md: -------------------------------------------------------------------------------- 1 | # Storing Sample Context in V8 Continuation-Preserved Embedder Data 2 | 3 | ## What is the Sample Context? 4 | Datadog's Node.js profiler has the ability to store a custom object that it will 5 | then associate with collected CPU samples. We refer to this object as the 6 | "sample context." A higher-level embedding (typically, dd-trace-js) will then 7 | update the sample context to keep it current with changes in the execution. A 8 | typical piece of data sample context stores is the tracing span ID, so whenever 9 | it changes, the sample context needs to be updated. 10 | 11 | ## How is the Sample Context stored and updated? 12 | Before Node 23, the sample context would be stored in a 13 | `std::shared_ptr>` field on the C++ `WallProfiler` 14 | instance. (In fact, due to the need for ensuring atomic updates and shared 15 | pointers not being effectively updateable atomically it's actually a pair of 16 | fields with an atomic pointer-to-shared-pointer switching between them, but I 17 | digress.) Due to it being a single piece of instance state, it had to be updated 18 | every time the active span changed, possibly on every invocation of 19 | `AsyncLocalStorage.enterWith` and `.run`, but even more importantly on every 20 | async context change, and for that we needed to register a "before" callback 21 | with `async_hooks.createHook`. This meant that we needed to both update the 22 | sample context on every async context change, but more importantly it also meant 23 | we needed to use `async_hooks.createHook` which is getting deprecated in Node. 24 | Current documentation for it is not exactly a shining endorsement: 25 | > Please migrate away from this API, if you can. We do not recommend using the 26 | > createHook, AsyncHook, and executionAsyncResource APIs as they have usability 27 | > issues, safety risks, and performance implications. 28 | 29 | Fortunately, first the V8 engine and then Node.js gave us building blocks for a 30 | better solution. 31 | 32 | ## V8 Continuation-Preserved Embedder Data and Node.js Async Context Frame 33 | In the V8 engine starting from version 12 (the one shipping with Node 22) 34 | `v8::Isolate` exposes an API to set and get embedder-specific data on it so that 35 | it is preserved across executions that are logical continuations of each other 36 | (essentially: across promise chains; this includes await expressions.) Even 37 | though the APIs are exposed on the isolate, the data is stored on a 38 | per-continuation basis and the engine takes care to return the right one when 39 | `Isolate::GetContinuationPreservedEmbedderData()` method is invoked. We will 40 | refer to continuation-preserved embedder data as "CPED" from now on. 41 | 42 | Starting with Node.js 23, CPED is used to implement data storage behind Node.js 43 | `AsyncLocalStorage` API. This dovetails nicely with our needs as all the 44 | span-related data we set on the sample context is normally managed in an async 45 | local storage (ALS) by the tracer. An application can create any number of 46 | ALSes, and each ALS manages a single value per async context. This value is 47 | somewhat confusingly called the "store" of the async local storage, making it 48 | important to not confuse the terms "storage" (an identity with multiple values, 49 | one per async context) and "store", which is a value of a storage within a 50 | particular async context. 51 | 52 | The new implementation for storing ALS stores introduces an internal Node.js 53 | class named `AsyncContextFrame` (ACF) which is a map that uses ALSes as keys, 54 | and their stores as the map values, essentially providing a mapping from an ALS 55 | to its store in the current async context. (This implementation is very similar 56 | to how e.g. Java implements `ThreadLocal`, which is a close analogue to ALS in 57 | Node.js.) ACF instances are then stored in CPED. 58 | 59 | ## Storing the Sample Context in CPED, take one 60 | Node.js – as the embedder of V8 – commandeers the CPED to store instances of 61 | ACF in it. This means that our profiler can't directly store our sample context 62 | in the CPED, because then we'd overwrite the ACF reference already in there and 63 | break Node.js. Our first attempt at solving this was to –- since ACF is "just" 64 | an ordinary JavaScript object -- to define a new property on it, and store our 65 | sample context in it! JavaScript properties can have strings, numbers, or 66 | symbols as their keys, with symbols being the recommended practice to define 67 | properties that are hidden from unrelated code as symbols are private to their 68 | creator and only compare equal to themselves. Thus we created a private symbol in 69 | the profiler instance for our property key, and our logic for storing the sample 70 | context thus becomes: 71 | * get the CPED from the V8 isolate 72 | * if it is not an object, do nothing (we can't set the sample context) 73 | * otherwise set the sample context as a value in the object with our property 74 | key. 75 | 76 | Unfortunately, this approach is not signal safe. When we want to read the value 77 | in the signal handler, it now needs to retrieve the CPED, which creates a V8 78 | `Local`, and then it needs to read a property on it, which creates 79 | another `Local`. It also needs to retrieve the current context, and a `Local` 80 | for the symbol used as a key – four `Local`s in total. V8 tracks the object 81 | addresses pointed to by locals so that GC doesn't touch them. It tracks them in 82 | a series of arrays, and if the current array fills up, it needs to allocate a 83 | new one. As we know, allocation is unsafe in a signal handler, hence our 84 | problem. We were thinking of a solution where we check if there is at least 4 85 | slots free in the current array, but then our profiler's operation would be at 86 | mercy of V8 internal state. 87 | 88 | ## Storing the Sample Context in CPED, take two 89 | 90 | Next we thought of replacing the `AsyncContextFrame` object in CPED with one we 91 | created with an internal field – we can store and retrieve an arbitrary `void *` 92 | in it with `{Get|Set}AlignedPointerInInternalField` methods. The initial idea 93 | was to leverage JavaScript's property of being a prototype-based language and 94 | set the original CPED object as the prototype of our replacement, so that all 95 | its methods would keep being invoked. This unfortunately didn't work because 96 | the `AsyncContextFrame` is a `Map` and our replacement object doesn't have the 97 | internal structure of V8's implementation of a map. The final solution turned 98 | out to be the one where we store the original ACF as a property in our 99 | replacement object (now effectively, a proxy to the ACF), and define all the 100 | `Map` methods and properties on the proxy so that they are invoked on the ACF. 101 | Even though the proxy does not pass an `instanceof Map` check, it is duck-typed 102 | as a map. We even encapsulated this behavior in a special prototype object, so 103 | the operations to set the context are: 104 | * retrieve the ACF from CPED 105 | * create a new object (the proxy) with one internal field 106 | * set the ACF as a special property in the proxy 107 | * set the prototype of the proxy to our prototype that defines all the proxied 108 | methods and properties to forward through the proxy-referenced ACF. 109 | * store our sample context in the internal field of the proxy 110 | * set the proxy object as the CPED. 111 | 112 | Now, a keen eyed reader will notice that in the signal handler we still need to 113 | call `Isolate::GetContinuationPreservedEmbedderData` which still creates a 114 | `Local`. That would be true, except that we can import the `v8-internals.h` 115 | header and directly read the address of the object by reading into the isolate 116 | at the offset `kContinuationPreservedEmbedderDataOffset` declared in it. 117 | 118 | 119 | The chain of data now looks something like this: 120 | ``` 121 | v8::Isolate (from Isolate::GetCurrent()) 122 | +-> current continuation (internally managed by V8) 123 | +-- our proxy object 124 | +-- node::AsyncContextFrame (in proxy's private property, for forwarding method calls) 125 | +-- prototype: declares functions and properties that forward to the AsyncContextFrame 126 | +-- dd:PersistentContextPtr* (in proxy's internal field) 127 | +-> std::shared_ptr> (in PersistentContextPtr's context field) 128 | +-> v8::Global (in shared_ptr) 129 | +-> v8::Value (the actual sample context object) 130 | 131 | ``` 132 | The last 3 steps are the same as when CPED is not being used, except `context` 133 | is directly represented in the `WallProfiler`, so then it looks like this: 134 | ``` 135 | dd::WallProfiler 136 | +-> std::shared_ptr> (in either WallProfiler::ptr1 or ptr2) 137 | +-> v8::Global (in shared_ptr) 138 | +-> v8::Value (the actual sample context object) 139 | ``` 140 | 141 | ### Memory allocations and garbage collection 142 | We need to allocate a `PersistentContextPtr` (PCP) instance for every proxy we 143 | create. The PCP has two concerns: it both has a shared pointer to the V8 global 144 | that carries the sample context, and it also has a V8 weak reference to the 145 | proxy object it is encapsulated within. This allows us to detect (since weak 146 | references allow for GC callbacks) when the proxy object gets garbage collected, 147 | and at that time the PCP itself can be either deleted or reused. We have an 148 | optimization where we don't delete PCPs -- the assumption is that the number of 149 | live ACFs (and thus proxies, and thus PCPs) will be constant for a server 150 | application under load, so instead of doing a high amount of small new/delete 151 | operations that can fragment the native heap, we keep the ones we'd delete in a 152 | dequeue instead and reuse them. 153 | 154 | ## Odds and ends 155 | And that's mostly it! There are few more small odds and ends to make it work 156 | safely. We still need to guard reading the value in the signal handler while 157 | it's being written. We guard by introducing an atomic boolean and proper signal 158 | fencing. 159 | 160 | The signal handler code also needs to be prevented from trying to access the 161 | data while GC is in progress. For this reason, we register GC prologue and 162 | epilogue callbacks with the V8 isolate so we can know when GCs are ongoing and 163 | the signal handler will refrain from reading the CPED field during them. We'll 164 | however grab the current sample context from the CPED and store it in a profiler 165 | instance field in the GC prologue and use it for any samples taken during GC. 166 | 167 | ## Changes in dd-trace-js 168 | For completeness, we'll describe the changes in dd-trace-js here as well. The 169 | main change is that with Node 24, we no longer require async hooks. The 170 | instrumentation points for `AsyncLocalStorage.enterWith` and 171 | `AsyncLocalStorage.run` remain in place – they are the only ones that are needed 172 | now. 173 | 174 | There are some small performance optimizations that no longer apply with the new 175 | approach, though. For one, with the old approach we did some data conversions 176 | (span IDs to bigint, a tag array to endpoint string) in a sample when a sample 177 | was captured. With the new approach, we do these conversions for all sample 178 | contexts during profile serialization. Doing them after each sample capture 179 | amortized their cost, possibly reducing the latency induced at serialization 180 | time. With the old approach we also called `SetContext` only once per sampling – 181 | we'd install a sample context to be used for the next sample, and then kept 182 | updating a `ref` field in it with a reference to the actual data from pure 183 | JavaScript code. Since we no longer have a single sample context (but one per 184 | continuation) we can not do this anymore, and we need to call `SetContext` on 185 | every ACF change. The cost of this (basically, going into a native call from 186 | JavaScript) are still well offset by not having to use async hooks and do work 187 | on every async context change. We could arguably simplify the code by removing 188 | those small optimizations. 189 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /ts/src/sourcemapper/sourcemapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Originally copied from cloud-debug-nodejs's sourcemapper.ts from 18 | // https://github.com/googleapis/cloud-debug-nodejs/blob/7bdc2f1f62a3b45b7b53ea79f9444c8ed50e138b/src/agent/io/sourcemapper.ts 19 | // Modified to map from generated code to source code, rather than from source 20 | // code to generated code. 21 | 22 | import * as fs from 'fs'; 23 | import * as path from 'path'; 24 | import * as sourceMap from 'source-map'; 25 | import {logger} from '../logger'; 26 | import pLimit from 'p-limit'; 27 | 28 | const readFile = fs.promises.readFile; 29 | 30 | const CONCURRENCY = 10; 31 | const MAP_EXT = '.map'; 32 | 33 | function error(msg: string) { 34 | logger.debug(`Error: ${msg}`); 35 | return new Error(msg); 36 | } 37 | 38 | export interface MapInfoCompiled { 39 | mapFileDir: string; 40 | mapConsumer: sourceMap.RawSourceMap; 41 | } 42 | 43 | export interface GeneratedLocation { 44 | file: string; 45 | name?: string; 46 | line: number; 47 | column: number; 48 | } 49 | 50 | export interface SourceLocation { 51 | file?: string; 52 | name?: string; 53 | line?: number; 54 | column?: number; 55 | } 56 | 57 | /** 58 | * @param {!Map} infoMap The map that maps input source files to 59 | * SourceMapConsumer objects that are used to calculate mapping information 60 | * @param {string} mapPath The path to the source map file to process. The 61 | * path should be relative to the process's current working directory 62 | * @private 63 | */ 64 | async function processSourceMap( 65 | infoMap: Map, 66 | mapPath: string, 67 | debug: boolean 68 | ): Promise { 69 | // this handles the case when the path is undefined, null, or 70 | // the empty string 71 | if (!mapPath || !mapPath.endsWith(MAP_EXT)) { 72 | throw error(`The path "${mapPath}" does not specify a source map file`); 73 | } 74 | mapPath = path.normalize(mapPath); 75 | 76 | let contents; 77 | try { 78 | contents = await readFile(mapPath, 'utf8'); 79 | } catch (e) { 80 | throw error('Could not read source map file ' + mapPath + ': ' + e); 81 | } 82 | 83 | let consumer: sourceMap.RawSourceMap; 84 | try { 85 | // TODO: Determine how to reconsile the type conflict where `consumer` 86 | // is constructed as a SourceMapConsumer but is used as a 87 | // RawSourceMap. 88 | // TODO: Resolve the cast of `contents as any` (This is needed because the 89 | // type is expected to be of `RawSourceMap` but the existing 90 | // working code uses a string.) 91 | consumer = (await new sourceMap.SourceMapConsumer( 92 | contents as {} as sourceMap.RawSourceMap 93 | )) as {} as sourceMap.RawSourceMap; 94 | } catch (e) { 95 | throw error( 96 | 'An error occurred while reading the ' + 97 | 'sourceMap file ' + 98 | mapPath + 99 | ': ' + 100 | e 101 | ); 102 | } 103 | 104 | /* If the source map file defines a "file" attribute, use it as 105 | * the output file where the path is relative to the directory 106 | * containing the map file. Otherwise, use the name of the output 107 | * file (with the .map extension removed) as the output file. 108 | 109 | * With nextjs/webpack, when there are subdirectories in `pages` directory, 110 | * the generated source maps do not reference correctly the generated files 111 | * in their `file` property. 112 | * For example if the generated file / source maps have paths: 113 | * /pages/sub/foo.js(.map) 114 | * foo.js.map will have ../pages/sub/foo.js as `file` property instead of 115 | * ../../pages/sub/foo.js 116 | * To workaround this, check first if file referenced in `file` property 117 | * exists and if it does not, check if generated file exists alongside the 118 | * source map file. 119 | */ 120 | const dir = path.dirname(mapPath); 121 | const generatedPathCandidates = []; 122 | if (consumer.file) { 123 | generatedPathCandidates.push(path.resolve(dir, consumer.file)); 124 | } 125 | const samePath = path.resolve(dir, path.basename(mapPath, MAP_EXT)); 126 | if ( 127 | generatedPathCandidates.length === 0 || 128 | generatedPathCandidates[0] !== samePath 129 | ) { 130 | generatedPathCandidates.push(samePath); 131 | } 132 | 133 | for (const generatedPath of generatedPathCandidates) { 134 | try { 135 | await fs.promises.access(generatedPath, fs.constants.F_OK); 136 | infoMap.set(generatedPath, {mapFileDir: dir, mapConsumer: consumer}); 137 | if (debug) { 138 | logger.debug(`Loaded source map for ${generatedPath} => ${mapPath}`); 139 | } 140 | return; 141 | } catch (err) { 142 | if (debug) { 143 | logger.debug(`Generated path ${generatedPath} does not exist`); 144 | } 145 | } 146 | } 147 | if (debug) { 148 | logger.debug(`Unable to find generated file for ${mapPath}`); 149 | } 150 | } 151 | 152 | export class SourceMapper { 153 | infoMap: Map; 154 | debug: boolean; 155 | 156 | static async create( 157 | searchDirs: string[], 158 | debug = false 159 | ): Promise { 160 | if (debug) { 161 | logger.debug( 162 | `Looking for source map files in dirs: [${searchDirs.join(', ')}]` 163 | ); 164 | } 165 | const mapFiles: string[] = []; 166 | for (const dir of searchDirs) { 167 | try { 168 | const mf = await getMapFiles(dir); 169 | mf.forEach(mapFile => { 170 | mapFiles.push(path.resolve(dir, mapFile)); 171 | }); 172 | } catch (e) { 173 | throw error(`failed to get source maps from ${dir}: ${e}`); 174 | } 175 | } 176 | if (debug) { 177 | logger.debug(`Found source map files: [${mapFiles.join(', ')}]`); 178 | } 179 | return createFromMapFiles(mapFiles, debug); 180 | } 181 | 182 | /** 183 | * @param {Array.} sourceMapPaths An array of paths to .map source map 184 | * files that should be processed. The paths should be relative to the 185 | * current process's current working directory 186 | * @param {Logger} logger A logger that reports errors that occurred while 187 | * processing the given source map files 188 | * @constructor 189 | */ 190 | constructor(debug = false) { 191 | this.infoMap = new Map(); 192 | this.debug = debug; 193 | } 194 | 195 | /** 196 | * Used to get the information about the transpiled file from a given input 197 | * source file provided there isn't any ambiguity with associating the input 198 | * path to exactly one output transpiled file. 199 | * 200 | * @param inputPath The (possibly relative) path to the original source file. 201 | * @return The `MapInfoCompiled` object that describes the transpiled file 202 | * associated with the specified input path. `null` is returned if either 203 | * zero files are associated with the input path or if more than one file 204 | * could possibly be associated with the given input path. 205 | */ 206 | private getMappingInfo(inputPath: string): MapInfoCompiled | null { 207 | const normalizedPath = path.normalize(inputPath); 208 | if (this.infoMap.has(normalizedPath)) { 209 | return this.infoMap.get(normalizedPath) as MapInfoCompiled; 210 | } 211 | return null; 212 | } 213 | 214 | /** 215 | * Used to determine if the source file specified by the given path has 216 | * a .map file and an output file associated with it. 217 | * 218 | * If there is no such mapping, it could be because the input file is not 219 | * the input to a transpilation process or it is the input to a transpilation 220 | * process but its corresponding .map file was not given to the constructor 221 | * of this mapper. 222 | * 223 | * @param {string} inputPath The path to an input file that could 224 | * possibly be the input to a transpilation process. The path should be 225 | * relative to the process's current working directory. 226 | */ 227 | hasMappingInfo(inputPath: string): boolean { 228 | return this.getMappingInfo(inputPath) !== null; 229 | } 230 | 231 | /** 232 | * @param {string} inputPath The path to an input file that could possibly 233 | * be the input to a transpilation process. The path should be relative to 234 | * the process's current working directory 235 | * @param {number} The line number in the input file where the line number is 236 | * zero-based. 237 | * @param {number} (Optional) The column number in the line of the file 238 | * specified where the column number is zero-based. 239 | * @return {Object} The object returned has a "file" attribute for the 240 | * path of the output file associated with the given input file (where the 241 | * path is relative to the process's current working directory), 242 | * a "line" attribute of the line number in the output file associated with 243 | * the given line number for the input file, and an optional "column" number 244 | * of the column number of the output file associated with the given file 245 | * and line information. 246 | * 247 | * If the given input file does not have mapping information associated 248 | * with it then the input location is returned. 249 | */ 250 | mappingInfo(location: GeneratedLocation): SourceLocation { 251 | const inputPath = path.normalize(location.file); 252 | const entry = this.getMappingInfo(inputPath); 253 | if (entry === null) { 254 | if (this.debug) { 255 | logger.debug( 256 | `Source map lookup failed: no map found for ${location.file} (normalized: ${inputPath})` 257 | ); 258 | } 259 | return location; 260 | } 261 | 262 | const generatedPos = { 263 | line: location.line, 264 | column: location.column > 0 ? location.column - 1 : 0, // SourceMapConsumer expects column to be 0-based 265 | }; 266 | 267 | // TODO: Determine how to remove the explicit cast here. 268 | const consumer: sourceMap.SourceMapConsumer = 269 | entry.mapConsumer as {} as sourceMap.SourceMapConsumer; 270 | 271 | // When column is 0, we don't have real column info (e.g., from V8's LineTick 272 | // which only provides line numbers). Use LEAST_UPPER_BOUND to find the first 273 | // mapping on this line instead of failing because there's nothing at column 0. 274 | const bias = 275 | generatedPos.column === 0 276 | ? sourceMap.SourceMapConsumer.LEAST_UPPER_BOUND 277 | : sourceMap.SourceMapConsumer.GREATEST_LOWER_BOUND; 278 | 279 | const pos = consumer.originalPositionFor({...generatedPos, bias}); 280 | if (pos.source === null) { 281 | if (this.debug) { 282 | logger.debug( 283 | `Source map lookup failed for ${location.name}(${location.file}:${location.line}:${location.column})` 284 | ); 285 | } 286 | return location; 287 | } 288 | 289 | const loc = { 290 | file: path.resolve(entry.mapFileDir, pos.source), 291 | line: pos.line || undefined, 292 | name: pos.name || location.name, 293 | column: pos.column === null ? undefined : pos.column + 1, // convert column back to 1-based 294 | }; 295 | 296 | if (this.debug) { 297 | logger.debug( 298 | `Source map lookup succeeded for ${location.name}(${location.file}:${location.line}:${location.column}) => ${loc.name}(${loc.file}:${loc.line}:${loc.column})` 299 | ); 300 | } 301 | return loc; 302 | } 303 | } 304 | 305 | async function createFromMapFiles( 306 | mapFiles: string[], 307 | debug: boolean 308 | ): Promise { 309 | const limit = pLimit(CONCURRENCY); 310 | const mapper = new SourceMapper(debug); 311 | const promises: Array> = mapFiles.map(mapPath => 312 | limit(() => processSourceMap(mapper.infoMap, mapPath, debug)) 313 | ); 314 | try { 315 | await Promise.all(promises); 316 | } catch (err) { 317 | throw error( 318 | 'An error occurred while processing the source map files' + err 319 | ); 320 | } 321 | return mapper; 322 | } 323 | 324 | function isErrnoException(e: unknown): e is NodeJS.ErrnoException { 325 | return e instanceof Error && 'code' in e; 326 | } 327 | 328 | function isNonFatalError(error: unknown) { 329 | const nonFatalErrors = ['ENOENT', 'EPERM', 'EACCES', 'ELOOP']; 330 | 331 | return ( 332 | isErrnoException(error) && error.code && nonFatalErrors.includes(error.code) 333 | ); 334 | } 335 | 336 | async function* walk( 337 | dir: string, 338 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 339 | fileFilter = (filename: string) => true, 340 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 341 | directoryFilter = (root: string, dirname: string) => true 342 | ): AsyncIterable { 343 | async function* walkRecursive(dir: string): AsyncIterable { 344 | try { 345 | for await (const d of await fs.promises.opendir(dir)) { 346 | const entry = path.join(dir, d.name); 347 | if (d.isDirectory() && directoryFilter(dir, d.name)) { 348 | yield* walkRecursive(entry); 349 | } else if (d.isFile() && fileFilter(d.name)) { 350 | // check that the file is readable 351 | await fs.promises.access(entry, fs.constants.R_OK); 352 | yield entry; 353 | } 354 | } 355 | } catch (error) { 356 | if (!isNonFatalError(error)) { 357 | throw error; 358 | } else { 359 | logger.debug(() => `Non fatal error: ${error}`); 360 | } 361 | } 362 | } 363 | 364 | yield* walkRecursive(dir); 365 | } 366 | 367 | async function getMapFiles(baseDir: string): Promise { 368 | const mapFiles: string[] = []; 369 | for await (const entry of walk( 370 | baseDir, 371 | filename => /\.[cm]?js\.map$/.test(filename), 372 | (root, dirname) => 373 | root !== '/proc' && dirname !== '.git' && dirname !== 'node_modules' 374 | )) { 375 | mapFiles.push(path.relative(baseDir, entry)); 376 | } 377 | return mapFiles; 378 | } 379 | -------------------------------------------------------------------------------- /ts/test/test-profile-serializer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import * as sinon from 'sinon'; 17 | import * as tmp from 'tmp'; 18 | 19 | import { 20 | NON_JS_THREADS_FUNCTION_NAME, 21 | serializeHeapProfile, 22 | serializeTimeProfile, 23 | } from '../src/profile-serializer'; 24 | import {SourceMapper} from '../src/sourcemapper/sourcemapper'; 25 | import {Label, Profile} from 'pprof-format'; 26 | import {TimeProfile, TimeProfileNode} from '../src/v8-types'; 27 | import { 28 | anonymousFunctionHeapProfile, 29 | getAndVerifyPresence, 30 | getAndVerifyString, 31 | heapProfile, 32 | heapSourceProfile, 33 | labelEncodingProfile, 34 | mapDirPath, 35 | timeProfile, 36 | timeSourceProfile, 37 | v8AnonymousFunctionHeapProfile, 38 | v8HeapGeneratedProfile, 39 | v8HeapProfile, 40 | v8TimeGeneratedProfile, 41 | v8TimeProfile, 42 | } from './profiles-for-tests'; 43 | 44 | const assert = require('assert'); 45 | 46 | function getNonJSThreadsSample(profile: Profile): Number[] | null { 47 | for (const sample of profile.sample!) { 48 | const locationId = sample.locationId[0]; 49 | const location = getAndVerifyPresence( 50 | profile.location!, 51 | locationId as number 52 | ); 53 | const functionId = location.line![0].functionId; 54 | const fn = getAndVerifyPresence(profile.function!, functionId as number); 55 | const fn_name = profile.stringTable.strings[fn.name as number]; 56 | if (fn_name === NON_JS_THREADS_FUNCTION_NAME) { 57 | return sample.value as Number[]; 58 | } 59 | } 60 | 61 | return null; 62 | } 63 | 64 | describe('profile-serializer', () => { 65 | let dateStub: sinon.SinonStub<[], number>; 66 | 67 | before(() => { 68 | dateStub = sinon.stub(Date, 'now').returns(0); 69 | }); 70 | after(() => { 71 | dateStub.restore(); 72 | }); 73 | 74 | describe('serializeTimeProfile', () => { 75 | it('should produce expected profile', () => { 76 | const timeProfileOut = serializeTimeProfile(v8TimeProfile, 1000); 77 | assert.deepEqual(timeProfileOut, timeProfile); 78 | }); 79 | 80 | it('should omit non-jS threads CPU time when profile has no CPU time', () => { 81 | const timeProfile: TimeProfile = { 82 | startTime: 0, 83 | endTime: 10 * 1000 * 1000, 84 | hasCpuTime: false, 85 | nonJSThreadsCpuTime: 1000, 86 | topDownRoot: { 87 | name: '(root)', 88 | scriptName: 'root', 89 | scriptId: 0, 90 | lineNumber: 0, 91 | columnNumber: 0, 92 | hitCount: 0, 93 | children: [], 94 | }, 95 | }; 96 | const timeProfileOut = serializeTimeProfile(timeProfile, 1000); 97 | assert.equal(getNonJSThreadsSample(timeProfileOut), null); 98 | const timeProfileOutWithLabels = serializeTimeProfile( 99 | timeProfile, 100 | 1000, 101 | undefined, 102 | false, 103 | () => { 104 | return {foo: 'bar'}; 105 | } 106 | ); 107 | assert.equal(getNonJSThreadsSample(timeProfileOutWithLabels), null); 108 | }); 109 | 110 | it('should omit non-jS threads CPU time when it is zero', () => { 111 | const timeProfile: TimeProfile = { 112 | startTime: 0, 113 | endTime: 10 * 1000 * 1000, 114 | hasCpuTime: true, 115 | nonJSThreadsCpuTime: 0, 116 | topDownRoot: { 117 | name: '(root)', 118 | scriptName: 'root', 119 | scriptId: 0, 120 | lineNumber: 0, 121 | columnNumber: 0, 122 | hitCount: 0, 123 | children: [], 124 | }, 125 | }; 126 | const timeProfileOut = serializeTimeProfile(timeProfile, 1000); 127 | assert.equal(getNonJSThreadsSample(timeProfileOut), null); 128 | const timeProfileOutWithLabels = serializeTimeProfile( 129 | timeProfile, 130 | 1000, 131 | undefined, 132 | false, 133 | () => { 134 | return {foo: 'bar'}; 135 | } 136 | ); 137 | assert.equal(getNonJSThreadsSample(timeProfileOutWithLabels), null); 138 | }); 139 | 140 | it('should produce Non-JS thread sample with zero wall time', () => { 141 | const timeProfile: TimeProfile = { 142 | startTime: 0, 143 | endTime: 10 * 1000 * 1000, 144 | hasCpuTime: true, 145 | nonJSThreadsCpuTime: 1000, 146 | topDownRoot: { 147 | name: '(root)', 148 | scriptName: 'root', 149 | scriptId: 0, 150 | lineNumber: 0, 151 | columnNumber: 0, 152 | hitCount: 0, 153 | children: [], 154 | }, 155 | }; 156 | const timeProfileOut = serializeTimeProfile(timeProfile, 1000); 157 | const values = getNonJSThreadsSample(timeProfileOut); 158 | assert.notEqual(values, null); 159 | assert.equal(values![0], 0); 160 | assert.equal(values![1], 0); 161 | assert.equal(values![2], 1000); 162 | const timeProfileOutWithLabels = serializeTimeProfile( 163 | timeProfile, 164 | 1000, 165 | undefined, 166 | false, 167 | () => { 168 | return {foo: 'bar'}; 169 | } 170 | ); 171 | const valuesWithLabels = getNonJSThreadsSample(timeProfileOutWithLabels); 172 | assert.notEqual(valuesWithLabels, null); 173 | assert.equal(valuesWithLabels![0], 0); 174 | assert.equal(valuesWithLabels![1], 0); 175 | assert.equal(valuesWithLabels![2], 1000); 176 | }); 177 | }); 178 | 179 | describe('label builder', () => { 180 | it('should accept strings, numbers, and bigints', () => { 181 | const profileOut = serializeTimeProfile(labelEncodingProfile, 1000); 182 | const st = profileOut.stringTable; 183 | assert.deepEqual(profileOut.sample[0].label, [ 184 | new Label({key: st.dedup('someStr'), str: st.dedup('foo')}), 185 | new Label({key: st.dedup('someNum'), num: 42}), 186 | new Label({key: st.dedup('someBigint'), num: 18446744073709551557n}), 187 | ]); 188 | }); 189 | }); 190 | 191 | describe('serializeHeapProfile', () => { 192 | it('should produce expected profile', () => { 193 | const heapProfileOut = serializeHeapProfile(v8HeapProfile, 0, 512 * 1024); 194 | assert.deepEqual(heapProfileOut, heapProfile); 195 | }); 196 | it('should produce expected profile when there is anonymous function', () => { 197 | const heapProfileOut = serializeHeapProfile( 198 | v8AnonymousFunctionHeapProfile, 199 | 0, 200 | 512 * 1024 201 | ); 202 | assert.deepEqual(heapProfileOut, anonymousFunctionHeapProfile); 203 | }); 204 | }); 205 | 206 | describe('source map specified', () => { 207 | let sourceMapper: SourceMapper; 208 | before(async () => { 209 | const sourceMapFiles = [mapDirPath]; 210 | sourceMapper = await SourceMapper.create(sourceMapFiles); 211 | }); 212 | 213 | describe('serializeHeapProfile', () => { 214 | it('should produce expected profile', () => { 215 | const heapProfileOut = serializeHeapProfile( 216 | v8HeapGeneratedProfile, 217 | 0, 218 | 512 * 1024, 219 | undefined, 220 | sourceMapper 221 | ); 222 | assert.deepEqual(heapProfileOut, heapSourceProfile); 223 | }); 224 | }); 225 | 226 | describe('serializeTimeProfile', () => { 227 | it('should produce expected profile', () => { 228 | const timeProfileOut = serializeTimeProfile( 229 | v8TimeGeneratedProfile, 230 | 1000, 231 | sourceMapper 232 | ); 233 | assert.deepEqual(timeProfileOut, timeSourceProfile); 234 | }); 235 | }); 236 | 237 | after(() => { 238 | tmp.setGracefulCleanup(); 239 | }); 240 | }); 241 | 242 | describe('source map with column 0 (LineTick simulation)', () => { 243 | // This tests the LEAST_UPPER_BOUND fallback for when V8's LineTick 244 | // doesn't provide column information (column=0) 245 | let sourceMapper: SourceMapper; 246 | let testMapDir: string; 247 | 248 | // Line in source.ts that the first call maps to (column 10) 249 | const FIRST_CALL_SOURCE_LINE = 100; 250 | // Line in source.ts that the second call maps to (column 25) 251 | const SECOND_CALL_SOURCE_LINE = 200; 252 | 253 | before(async () => { 254 | // Create a source map simulating: return fib(n-1) + fib(n-2) 255 | // Same function called twice on the same line at different columns 256 | testMapDir = tmp.dirSync().name; 257 | const {SourceMapGenerator} = await import('source-map'); 258 | const fs = await import('fs'); 259 | const path = await import('path'); 260 | 261 | const mapGen = new SourceMapGenerator({file: 'generated.js'}); 262 | 263 | // First fib() call at column 10 -> maps to source line 100 264 | mapGen.addMapping({ 265 | source: path.join(testMapDir, 'source.ts'), 266 | name: 'fib', 267 | generated: {line: 5, column: 10}, 268 | original: {line: FIRST_CALL_SOURCE_LINE, column: 0}, 269 | }); 270 | 271 | // Second fib() call at column 25 -> maps to source line 200 272 | mapGen.addMapping({ 273 | source: path.join(testMapDir, 'source.ts'), 274 | name: 'fib', 275 | generated: {line: 5, column: 25}, 276 | original: {line: SECOND_CALL_SOURCE_LINE, column: 0}, 277 | }); 278 | 279 | fs.writeFileSync( 280 | path.join(testMapDir, 'generated.js.map'), 281 | mapGen.toString() 282 | ); 283 | fs.writeFileSync(path.join(testMapDir, 'generated.js'), ''); 284 | 285 | sourceMapper = await SourceMapper.create([testMapDir]); 286 | }); 287 | 288 | it('should map column 0 to first mapping on line (LEAST_UPPER_BOUND fallback)', () => { 289 | const path = require('path'); 290 | // Simulate LineTick entry with column=0 (no column info from V8 < 14) 291 | // This is the fallback behavior when LineTick.column is not available 292 | const childNode: TimeProfileNode = { 293 | name: 'fib', 294 | scriptName: path.join(testMapDir, 'generated.js'), 295 | scriptId: 1, 296 | lineNumber: 5, 297 | columnNumber: 0, // LineTick has no column in V8 < 14 298 | hitCount: 1, 299 | children: [], 300 | }; 301 | const v8Profile: TimeProfile = { 302 | startTime: 0, 303 | endTime: 1000000, 304 | topDownRoot: { 305 | name: '(root)', 306 | scriptName: 'root', 307 | scriptId: 0, 308 | lineNumber: 0, 309 | columnNumber: 0, 310 | hitCount: 0, 311 | children: [childNode], 312 | }, 313 | }; 314 | 315 | const profile = serializeTimeProfile(v8Profile, 1000, sourceMapper); 316 | 317 | assert.strictEqual(profile.location!.length, 1); 318 | const loc = profile.location![0]; 319 | const line = loc.line![0]; 320 | const func = getAndVerifyPresence( 321 | profile.function!, 322 | line.functionId as number 323 | ); 324 | const filename = getAndVerifyString( 325 | profile.stringTable, 326 | func, 327 | 'filename' 328 | ); 329 | 330 | // Should be mapped to source.ts 331 | assert.ok( 332 | filename.includes('source.ts'), 333 | `Expected source.ts but got ${filename}` 334 | ); 335 | // With column 0 and LEAST_UPPER_BOUND, should map to FIRST mapping (line 100) 336 | assert.strictEqual( 337 | line.line, 338 | FIRST_CALL_SOURCE_LINE, 339 | 'Column 0 should use LEAST_UPPER_BOUND to find first mapping on line' 340 | ); 341 | }); 342 | 343 | it('should map to second call when column points to it (V8 14+ with LineTick.column)', () => { 344 | const path = require('path'); 345 | // Simulate V8 14+ behavior where LineTick has actual column data 346 | // Column 26 is after the second mapping at column 25 347 | const childNode: TimeProfileNode = { 348 | name: 'fib', 349 | scriptName: path.join(testMapDir, 'generated.js'), 350 | scriptId: 1, 351 | lineNumber: 5, 352 | columnNumber: 26, // V8 14+ provides actual column from LineTick 353 | hitCount: 1, 354 | children: [], 355 | }; 356 | const v8Profile: TimeProfile = { 357 | startTime: 0, 358 | endTime: 1000000, 359 | topDownRoot: { 360 | name: '(root)', 361 | scriptName: 'root', 362 | scriptId: 0, 363 | lineNumber: 0, 364 | columnNumber: 0, 365 | hitCount: 0, 366 | children: [childNode], 367 | }, 368 | }; 369 | 370 | const profile = serializeTimeProfile(v8Profile, 1000, sourceMapper); 371 | 372 | assert.strictEqual(profile.location!.length, 1); 373 | const loc = profile.location![0]; 374 | const line = loc.line![0]; 375 | 376 | // Column 26 with GREATEST_LOWER_BOUND should map to second call (line 200) 377 | assert.strictEqual( 378 | line.line, 379 | SECOND_CALL_SOURCE_LINE, 380 | 'Column 26 should use GREATEST_LOWER_BOUND to find mapping at column 25' 381 | ); 382 | }); 383 | 384 | it('should map to first call when column points to it (V8 14+ with LineTick.column)', () => { 385 | const path = require('path'); 386 | // Simulate V8 14+ behavior where LineTick has actual column data 387 | // Column 11 is after the first mapping at column 10 but before second at 25 388 | const childNode: TimeProfileNode = { 389 | name: 'fib', 390 | scriptName: path.join(testMapDir, 'generated.js'), 391 | scriptId: 1, 392 | lineNumber: 5, 393 | columnNumber: 11, // V8 14+ provides actual column from LineTick 394 | hitCount: 1, 395 | children: [], 396 | }; 397 | const v8Profile: TimeProfile = { 398 | startTime: 0, 399 | endTime: 1000000, 400 | topDownRoot: { 401 | name: '(root)', 402 | scriptName: 'root', 403 | scriptId: 0, 404 | lineNumber: 0, 405 | columnNumber: 0, 406 | hitCount: 0, 407 | children: [childNode], 408 | }, 409 | }; 410 | 411 | const profile = serializeTimeProfile(v8Profile, 1000, sourceMapper); 412 | 413 | assert.strictEqual(profile.location!.length, 1); 414 | const loc = profile.location![0]; 415 | const line = loc.line![0]; 416 | 417 | // Column 11 with GREATEST_LOWER_BOUND should map to first call (line 100) 418 | assert.strictEqual( 419 | line.line, 420 | FIRST_CALL_SOURCE_LINE, 421 | 'Column 11 should use GREATEST_LOWER_BOUND to find mapping at column 10' 422 | ); 423 | }); 424 | }); 425 | }); 426 | --------------------------------------------------------------------------------