From 7f78a844dd86a809fcfdb8719eb8fe65b208dd6e Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 27 Jan 2025 15:18:31 +0100 Subject: [PATCH 1/9] feat(profiling-node): Move native modules to new repository --- .github/workflows/build.yml | 438 +----- .../bin/darwin-arm64-130/profiling-node.node | Bin 115304 -> 0 bytes packages/profiling-node/binding.gyp | 20 - .../profiling-node/bindings/cpu_profiler.cc | 1226 ----------------- packages/profiling-node/clang-format.js | 26 - packages/profiling-node/package.json | 25 +- packages/profiling-node/rollup.npm.config.mjs | 23 +- packages/profiling-node/scripts/binaries.js | 27 - .../profiling-node/scripts/check-build.js | 56 - .../profiling-node/scripts/copy-target.js | 27 - packages/profiling-node/src/cpu_profiler.ts | 224 --- packages/profiling-node/src/integration.ts | 16 +- .../profiling-node/src/spanProfileUtils.ts | 2 +- packages/profiling-node/src/utils.ts | 13 +- packages/profiling-node/test/bindings.test.ts | 30 - .../profiling-node/test/cpu_profiler.test.ts | 364 ----- .../test/spanProfileUtils.test.ts | 2 +- 17 files changed, 36 insertions(+), 2483 deletions(-) delete mode 100755 packages/profiling-node/bin/darwin-arm64-130/profiling-node.node delete mode 100644 packages/profiling-node/binding.gyp delete mode 100644 packages/profiling-node/bindings/cpu_profiler.cc delete mode 100644 packages/profiling-node/clang-format.js delete mode 100644 packages/profiling-node/scripts/binaries.js delete mode 100644 packages/profiling-node/scripts/check-build.js delete mode 100644 packages/profiling-node/scripts/copy-target.js delete mode 100644 packages/profiling-node/src/cpu_profiler.ts delete mode 100644 packages/profiling-node/test/bindings.test.ts delete mode 100644 packages/profiling-node/test/cpu_profiler.test.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24f3ee0454f2..120f15b1ef79 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,8 +84,6 @@ jobs: echo "COMMIT_MESSAGE=$(git log -n 1 --pretty=format:%s $COMMIT_SHA)" >> $GITHUB_ENV # Most changed packages are determined in job_build via Nx - # However, for profiling-node we only want to run certain things when in this specific package - # something changed, not in any of the dependencies (which include core, utils, ...) - name: Determine changed packages uses: dorny/paths-filter@v3.0.1 id: changed @@ -93,9 +91,6 @@ jobs: filters: | workflow: - '.github/**' - profiling_node: - - 'packages/profiling-node/**' - - 'dev-packages/e2e-tests/test-applications/node-profiling/**' any_code: - '!**/*.md' @@ -109,7 +104,6 @@ jobs: # Note: These next three have to be checked as strings ('true'/'false')! is_base_branch: ${{ github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/v9' || github.ref == 'refs/heads/v8'}} is_release: ${{ startsWith(github.ref, 'refs/heads/release/') }} - changed_profiling_node: ${{ steps.changed.outputs.profiling_node == 'true' }} changed_ci: ${{ steps.changed.outputs.workflow == 'true' }} changed_any_code: ${{ steps.changed.outputs.any_code == 'true' }} @@ -198,7 +192,6 @@ jobs: changed_deno: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry/deno') }} changed_bun: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry/bun') }} changed_browser_integration: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry-internal/browser-integration-tests') }} - # If you are looking for changed_profiling_node, this is defined in job_get_metadata job_check_branches: name: Check PR branches @@ -316,7 +309,7 @@ jobs: job_artifacts: name: Upload Artifacts - needs: [job_get_metadata, job_build, job_compile_bindings_profiling_node] + needs: [job_get_metadata, job_build] runs-on: ubuntu-20.04 # Build artifacts are only needed for releasing workflow. if: needs.job_get_metadata.outputs.is_release == 'true' @@ -334,13 +327,6 @@ jobs: with: dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Extract Profiling Node Prebuilt Binaries - uses: actions/download-artifact@v4 - with: - pattern: profiling-node-binaries-${{ github.sha }}-* - path: ${{ github.workspace }}/packages/profiling-node/lib/ - merge-multiple: true - - name: Pack tarballs run: yarn build:tarball @@ -498,37 +484,6 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} - job_profiling_node_unit_tests: - name: Node Profiling Unit Tests - needs: [job_get_metadata, job_build] - if: | - needs.job_build.outputs.changed_node == 'true' || - needs.job_get_metadata.outputs.changed_profiling_node == 'true' || - github.event_name != 'pull_request' - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Check out current commit - uses: actions/checkout@v4 - with: - ref: ${{ env.HEAD_COMMIT }} - - uses: actions/setup-node@v4 - with: - node-version: 20 - - uses: actions/setup-python@v5 - with: - python-version: '3.11.7' - - name: Restore caches - uses: ./.github/actions/restore-cache - with: - dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Build Configure node-gyp - run: yarn lerna run build:bindings:configure --scope @sentry/profiling-node - - name: Build Bindings for Current Environment - run: yarn build --scope @sentry/profiling-node - - name: Unit Test - run: yarn lerna run test --scope @sentry/profiling-node - job_browser_playwright_tests: name: Playwright ${{ matrix.bundle }}${{ matrix.project && matrix.project != 'chromium' && format(' {0}', matrix.project) || ''}}${{ matrix.shard && format(' ({0}/{1})', matrix.shard, matrix.shards) || ''}} Tests needs: [job_get_metadata, job_build] @@ -786,12 +741,10 @@ jobs: name: Prepare E2E tests # We want to run this if: # - The build job was successful, not skipped - # - AND if the profiling node bindings were either successful or skipped if: | always() && - needs.job_build.result == 'success' && - (needs.job_compile_bindings_profiling_node.result == 'success' || needs.job_compile_bindings_profiling_node.result == 'skipped') - needs: [job_get_metadata, job_build, job_compile_bindings_profiling_node] + needs.job_build.result == 'success' + needs: [job_get_metadata, job_build] runs-on: ubuntu-20.04-large-js timeout-minutes: 15 outputs: @@ -823,26 +776,6 @@ jobs: # On develop branch, we want to _store_ the cache (so it can be used by other branches), but never _restore_ from it restore-keys: ${{ env.NX_CACHE_RESTORE_KEYS }} - # Rebuild profiling by compiling TS and pull the precompiled binary artifacts - - name: Build Profiling Node - if: | - (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || - (needs.job_get_metadata.outputs.is_release == 'true') || - (github.event_name != 'pull_request') - run: yarn lerna run build:lib --scope @sentry/profiling-node - - - name: Extract Profiling Node Prebuilt Binaries - if: | - (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || - (needs.job_get_metadata.outputs.is_release == 'true') || - (github.event_name != 'pull_request') - uses: actions/download-artifact@v4 - with: - pattern: profiling-node-binaries-${{ github.sha }}-* - path: ${{ github.workspace }}/packages/profiling-node/lib/ - merge-multiple: true - # End rebuild profiling - - name: Build tarballs run: yarn build:tarball @@ -1089,137 +1022,20 @@ jobs: directory: dist workingDirectory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} - job_profiling_e2e_tests: - name: E2E ${{ matrix.label || matrix.test-application }} Test - # We only run E2E tests for non-fork PRs because the E2E tests require secrets to work and they can't be accessed from forks - # Dependabot specifically also has access to secrets - # We need to add the `always()` check here because the previous step has this as well :( - # See: https://github.com/actions/runner/issues/2205 - if: - # Only run profiling e2e tests if profiling node bindings have changed - always() && needs.job_e2e_prepare.result == 'success' && - (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && - ( - (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || - (needs.job_get_metadata.outputs.is_release == 'true') - ) - needs: [job_get_metadata, job_build, job_e2e_prepare] - runs-on: ubuntu-22.04 - timeout-minutes: 15 - env: - E2E_TEST_AUTH_TOKEN: ${{ secrets.E2E_TEST_AUTH_TOKEN }} - E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }} - E2E_TEST_SENTRY_ORG_SLUG: 'sentry-javascript-sdks' - E2E_TEST_SENTRY_PROJECT: 'sentry-javascript-e2e-tests' - strategy: - fail-fast: false - matrix: - test-application: ['node-profiling'] - build-command: - - false - label: - - false - steps: - - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v4 - with: - ref: ${{ env.HEAD_COMMIT }} - - - uses: pnpm/action-setup@v4 - with: - version: 9.4.0 - - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Restore caches - uses: ./.github/actions/restore-cache - with: - dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - - - name: Build Profiling Node - run: yarn lerna run build:lib --scope @sentry/profiling-node - - - name: Extract Profiling Node Prebuilt Binaries - uses: actions/download-artifact@v4 - with: - pattern: profiling-node-binaries-${{ github.sha }}-* - path: ${{ github.workspace }}/packages/profiling-node/lib/ - merge-multiple: true - - - name: Restore tarball cache - uses: actions/cache/restore@v4 - id: restore-tarball-cache - with: - path: ${{ github.workspace }}/packages/*/*.tgz - key: ${{ env.BUILD_CACHE_TARBALL_KEY }} - - - name: Build tarballs if not cached - if: steps.restore-tarball-cache.outputs.cache-hit != 'true' - run: yarn build:tarball - - - name: Install Playwright - uses: ./.github/actions/install-playwright - with: - browsers: chromium - - - name: Get node version - id: versions - run: | - echo "echo node=$(jq -r '.volta.node' dev-packages/e2e-tests/package.json)" >> $GITHUB_OUTPUT - - - name: Validate Verdaccio - run: yarn test:validate - working-directory: dev-packages/e2e-tests - - - name: Prepare Verdaccio - run: yarn test:prepare - working-directory: dev-packages/e2e-tests - env: - E2E_TEST_PUBLISH_SCRIPT_NODE_VERSION: ${{ steps.versions.outputs.node }} - - - name: Setup xvfb and update ubuntu dependencies - run: | - sudo apt-get install xvfb x11-xkb-utils xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic x11-apps - sudo apt-get install build-essential clang libdbus-1-dev libgtk2.0-dev \ - libnotify-dev libgconf2-dev \ - libasound2-dev libcap-dev libcups2-dev libxtst-dev \ - libxss1 libnss3-dev gcc-multilib g++-multilib - - - name: Install dependencies - working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} - run: yarn install --ignore-engines --frozen-lockfile - - - name: Build E2E app - working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} - timeout-minutes: 7 - run: yarn ${{ matrix.build-command || 'test:build' }} - - - name: Run E2E test - working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} - timeout-minutes: 10 - run: | - xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:assert - job_required_jobs_passed: name: All required jobs passed or were skipped needs: [ job_build, - job_compile_bindings_profiling_node, job_browser_unit_tests, job_bun_unit_tests, job_deno_unit_tests, job_node_unit_tests, - job_profiling_node_unit_tests, job_node_integration_tests, job_browser_playwright_tests, job_browser_loader_tests, job_remix_integration_tests, job_e2e_tests, - job_profiling_e2e_tests, job_artifacts, job_lint, job_check_format, @@ -1233,251 +1049,3 @@ jobs: if: contains(needs.*.result, 'failure') run: | echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1 - - job_compile_bindings_profiling_node: - name: Compile profiling-node (v${{ matrix.node }}) ${{ matrix.target_platform || matrix.os }}, ${{ matrix.arch || matrix.container }}, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }} - needs: [job_get_metadata, job_build] - # Compiling bindings can be very slow (especially on windows), so only run precompile - # Skip precompile unless we are on a release branch as precompile slows down CI times. - if: | - (needs.job_get_metadata.outputs.changed_profiling_node == 'true') || - (needs.job_get_metadata.outputs.is_release == 'true') - runs-on: ${{ matrix.os }} - container: - image: ${{ matrix.container }} - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - include: - # x64 glibc - - os: ubuntu-20.04 - node: 18 - binary: linux-x64-glibc-108 - - os: ubuntu-20.04 - node: 20 - binary: linux-x64-glibc-115 - - os: ubuntu-20.04 - node: 22 - binary: linux-x64-glibc-127 - - # x64 musl - - os: ubuntu-20.04 - container: node:18-alpine3.17 - node: 18 - binary: linux-x64-musl-108 - - os: ubuntu-20.04 - container: node:20-alpine3.17 - node: 20 - binary: linux-x64-musl-115 - - os: ubuntu-20.04 - container: node:22-alpine3.18 - node: 22 - binary: linux-x64-musl-127 - - # arm64 glibc - - os: ubuntu-20.04 - arch: arm64 - node: 18 - binary: linux-arm64-glibc-108 - - os: ubuntu-20.04 - arch: arm64 - node: 20 - binary: linux-arm64-glibc-115 - - os: ubuntu-20.04 - arch: arm64 - node: 22 - binary: linux-arm64-glibc-127 - - # arm64 musl - - os: ubuntu-20.04 - arch: arm64 - container: node:18-alpine3.17 - node: 18 - binary: linux-arm64-musl-108 - - os: ubuntu-20.04 - arch: arm64 - container: node:20-alpine3.17 - node: 20 - binary: linux-arm64-musl-115 - - os: ubuntu-20.04 - arch: arm64 - container: node:22-alpine3.18 - node: 22 - binary: linux-arm64-musl-127 - - # macos x64 - - os: macos-13 - node: 18 - arch: x64 - binary: darwin-x64-108 - - os: macos-13 - node: 20 - arch: x64 - binary: darwin-x64-115 - - os: macos-13 - node: 22 - arch: x64 - binary: darwin-x64-127 - - # macos arm64 - - os: macos-13 - arch: arm64 - node: 18 - target_platform: darwin - binary: darwin-arm64-108 - - os: macos-13 - arch: arm64 - node: 20 - target_platform: darwin - binary: darwin-arm64-115 - - os: macos-13 - arch: arm64 - node: 22 - target_platform: darwin - binary: darwin-arm64-127 - - # windows x64 - - os: windows-2022 - node: 18 - arch: x64 - binary: win32-x64-108 - - os: windows-2022 - node: 20 - arch: x64 - binary: win32-x64-115 - - os: windows-2022 - node: 22 - arch: x64 - binary: win32-x64-127 - - steps: - - name: Setup (alpine) - if: contains(matrix.container, 'alpine') - run: | - apk add --no-cache build-base git g++ make curl python3 - ln -sf python3 /usr/bin/python - - - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v4 - with: - ref: ${{ env.HEAD_COMMIT }} - - # Note: On alpine images, this does nothing - # The node version will be the one that is installed in the image - # If you want to change the node version, you need to change the image - # For non-alpine imgages, this will install the correct version of node - - name: Setup Node - uses: actions/setup-node@v4 - if: contains(matrix.container, 'alpine') == false - with: - node-version: ${{ matrix.node }} - - - name: Restore dependency cache - uses: actions/cache/restore@v4 - id: restore-dependencies - with: - path: ${{ env.CACHED_DEPENDENCY_PATHS }} - key: ${{ needs.job_build.outputs.dependency_cache_key }} - enableCrossOsArchive: true - - - name: Increase yarn network timeout on Windows - if: contains(matrix.os, 'windows') - run: yarn config set network-timeout 600000 -g - - - name: Install dependencies - if: steps.restore-dependencies.outputs.cache-hit != 'true' - run: yarn install --ignore-engines --frozen-lockfile - env: - SKIP_PLAYWRIGHT_BROWSER_INSTALL: "1" - - - name: Configure safe directory - run: | - git config --global --add safe.directory "*" - - - name: Setup python - uses: actions/setup-python@v5 - if: ${{ !contains(matrix.container, 'alpine') }} - id: python-setup - with: - python-version: '3.8.10' - - - name: Setup (arm64| ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }}) - if: matrix.arch == 'arm64' && !contains(matrix.container, 'alpine') && matrix.target_platform != 'darwin' - run: | - sudo apt-get update - sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu - - - name: Setup Musl - if: contains(matrix.container, 'alpine') - run: | - cd packages/profiling-node - curl -OL https://musl.cc/aarch64-linux-musl-cross.tgz - tar -xzvf aarch64-linux-musl-cross.tgz - $(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc --version - - # configure node-gyp - - name: Configure node-gyp - if: matrix.arch != 'arm64' - run: | - cd packages/profiling-node - yarn build:bindings:configure - - - name: Configure node-gyp (arm64, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }}) - if: matrix.arch == 'arm64' && matrix.target_platform != 'darwin' - run: | - cd packages/profiling-node - yarn build:bindings:configure:arm64 - - - name: Configure node-gyp (arm64, darwin) - if: matrix.arch == 'arm64' && matrix.target_platform == 'darwin' - run: | - cd packages/profiling-node - yarn build:bindings:configure:arm64 - - # build bindings - - name: Build Bindings - if: matrix.arch != 'arm64' - run: | - yarn lerna run build:bindings --scope @sentry/profiling-node - - - name: Build Bindings (arm64, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }}) - if: matrix.arch == 'arm64' && contains(matrix.container, 'alpine') && matrix.target_platform != 'darwin' - run: | - cd packages/profiling-node - CC=$(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc \ - CXX=$(pwd)/aarch64-linux-musl-cross/bin/aarch64-linux-musl-g++ \ - BUILD_ARCH=arm64 \ - yarn build:bindings - - - name: Build Bindings (arm64, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }}) - if: matrix.arch == 'arm64' && !contains(matrix.container, 'alpine') && matrix.target_platform != 'darwin' - run: | - cd packages/profiling-node - CC=aarch64-linux-gnu-gcc \ - CXX=aarch64-linux-gnu-g++ \ - BUILD_ARCH=arm64 \ - yarn build:bindings:arm64 - - - name: Build Bindings (arm64, darwin) - if: matrix.arch == 'arm64' && matrix.target_platform == 'darwin' - run: | - cd packages/profiling-node - BUILD_PLATFORM=darwin \ - BUILD_ARCH=arm64 \ - yarn build:bindings:arm64 - - - name: Build profiling-node & its dependencies - run: yarn build --scope @sentry/profiling-node - - - name: Test Bindings - if: matrix.arch != 'arm64' - run: | - yarn lerna run test --scope @sentry/profiling-node - - - name: Archive Binary - uses: actions/upload-artifact@v4 - with: - name: profiling-node-binaries-${{ github.sha }}-${{ matrix.binary }} - path: ${{ github.workspace }}/packages/profiling-node/lib/sentry_cpu_profiler-${{matrix.binary}}.node - if-no-files-found: error diff --git a/packages/profiling-node/bin/darwin-arm64-130/profiling-node.node b/packages/profiling-node/bin/darwin-arm64-130/profiling-node.node deleted file mode 100755 index 65e97eca7e48d05a45e91933dd230fcc8c543d8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 115304 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 from defining macros that conflict with - # std::min() and std::max(). We don't use (much) - # but we still inherit it from uv.h. - 'NOMINMAX', - ] - }], - ], -} diff --git a/packages/profiling-node/bindings/cpu_profiler.cc b/packages/profiling-node/bindings/cpu_profiler.cc deleted file mode 100644 index bf3762867769..000000000000 --- a/packages/profiling-node/bindings/cpu_profiler.cc +++ /dev/null @@ -1,1226 +0,0 @@ -#ifndef NOMINMAX -#define NOMINMAX -#endif - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -static const uint8_t kMaxStackDepth(128); -static const float kSamplingFrequency(99.0); // 99 to avoid lockstep sampling -static const float kSamplingHz(1 / kSamplingFrequency); -static const int kSamplingInterval(kSamplingHz * 1e6); -static const v8::CpuProfilingNamingMode - kNamingMode(v8::CpuProfilingNamingMode::kDebugNaming); -static const v8::CpuProfilingLoggingMode - kDefaultLoggingMode(v8::CpuProfilingLoggingMode::kEagerLogging); - -enum ProfileFormat { - kFormatThread = 0, - kFormatChunk = 1, -}; - -// Allow users to override the default logging mode via env variable. This is -// useful because sometimes the flow of the profiled program can be to execute -// many sequential transaction - in that case, it may be preferable to set eager -// logging to avoid paying the high cost of profiling for each individual -// transaction (one example for this are jest tests when run with --runInBand -// option). -static const char *kEagerLoggingMode = "eager"; -static const char *kLazyLoggingMode = "lazy"; - -v8::CpuProfilingLoggingMode GetLoggingMode() { - static const char *logging_mode(getenv("SENTRY_PROFILER_LOGGING_MODE")); - - // most times this wont be set so just bail early - if (!logging_mode) { - return kDefaultLoggingMode; - } - - // other times it'll likely be set to lazy as eager is the default - if (strcmp(logging_mode, kLazyLoggingMode) == 0) { - return v8::CpuProfilingLoggingMode::kLazyLogging; - } else if (strcmp(logging_mode, kEagerLoggingMode) == 0) { - return v8::CpuProfilingLoggingMode::kEagerLogging; - } - - return kDefaultLoggingMode; -} - -uint64_t timestamp_milliseconds() { - return std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); -} - -class SentryProfile; -class Profiler; - -enum class ProfileStatus { - kNotStarted, - kStarted, - kStopped, -}; - -class MeasurementsTicker { -private: - uv_timer_t *timer; - uint64_t period_ms; - std::unordered_map> - heap_listeners; - std::unordered_map> - cpu_listeners; - v8::Isolate *isolate; - v8::HeapStatistics heap_stats; - uv_cpu_info_t cpu_stats; - -public: - MeasurementsTicker(uv_loop_t *loop) - : period_ms(100), isolate(v8::Isolate::GetCurrent()) { - timer = new uv_timer_t; - uv_timer_init(loop, timer); - uv_handle_set_data((uv_handle_t *)timer, this); - uv_ref((uv_handle_t *)timer); - } - - static void ticker(uv_timer_t *); - // Memory listeners - void heap_callback(); - void add_heap_listener( - std::string &profile_id, - const std::function cb); - void remove_heap_listener( - std::string &profile_id, - const std::function &cb); - - // CPU listeners - void cpu_callback(); - void add_cpu_listener(std::string &profile_id, - const std::function cb); - void remove_cpu_listener(std::string &profile_id, - const std::function &cb); - - size_t listener_count(); - - ~MeasurementsTicker() { - uv_handle_t *handle = (uv_handle_t *)timer; - - uv_timer_stop(timer); - uv_unref(handle); - - if (!uv_is_closing(handle)) { - uv_close(handle, [](uv_handle_t *handle) { delete handle; }); - } - } -}; - -size_t MeasurementsTicker::listener_count() { - return heap_listeners.size() + cpu_listeners.size(); -} - -// Heap tickers -void MeasurementsTicker::heap_callback() { - isolate->GetHeapStatistics(&heap_stats); - uint64_t ts = uv_hrtime(); - - for (auto cb : heap_listeners) { - cb.second(ts, heap_stats); - } -} - -void MeasurementsTicker::add_heap_listener( - std::string &profile_id, - const std::function cb) { - heap_listeners.emplace(profile_id, cb); - - if (listener_count() == 1) { - uv_timer_set_repeat(timer, period_ms); - uv_timer_start(timer, ticker, 0, period_ms); - } -} - -void MeasurementsTicker::remove_heap_listener( - std::string &profile_id, - const std::function &cb) { - heap_listeners.erase(profile_id); - - if (listener_count() == 0) { - uv_timer_stop(timer); - } -}; - -// CPU tickers -void MeasurementsTicker::cpu_callback() { - uv_cpu_info_t *cpu = &cpu_stats; - int count; - int err = uv_cpu_info(&cpu, &count); - - if (err) { - return; - } - - if (count < 1) { - return; - } - - uint64_t ts = uv_hrtime(); - uint64_t total = 0; - uint64_t idle_total = 0; - - for (int i = 0; i < count; i++) { - uv_cpu_info_t *core = cpu + i; - - total += core->cpu_times.user; - total += core->cpu_times.nice; - total += core->cpu_times.sys; - total += core->cpu_times.idle; - total += core->cpu_times.irq; - - idle_total += core->cpu_times.idle; - } - - double idle_avg = idle_total / count; - double total_avg = total / count; - double rate = 1.0 - idle_avg / total_avg; - - if (rate < 0.0 || isinf(rate) || isnan(rate)) { - rate = 0.0; - } - - auto it = cpu_listeners.begin(); - while (it != cpu_listeners.end()) { - if (it->second(ts, rate)) { - it = cpu_listeners.erase(it); - } else { - ++it; - } - }; - - uv_free_cpu_info(cpu, count); -}; - -void MeasurementsTicker::ticker(uv_timer_t *handle) { - if (handle == nullptr) { - return; - } - - MeasurementsTicker *self = static_cast(handle->data); - self->heap_callback(); - self->cpu_callback(); -} - -void MeasurementsTicker::add_cpu_listener( - std::string &profile_id, const std::function cb) { - cpu_listeners.emplace(profile_id, cb); - - if (listener_count() == 1) { - uv_timer_set_repeat(timer, period_ms); - uv_timer_start(timer, ticker, 0, period_ms); - } -} - -void MeasurementsTicker::remove_cpu_listener( - std::string &profile_id, const std::function &cb) { - cpu_listeners.erase(profile_id); - - if (listener_count() == 0) { - uv_timer_stop(timer); - } -}; - -class Profiler { -public: - std::unordered_map active_profiles; - - MeasurementsTicker measurements_ticker; - v8::CpuProfiler *cpu_profiler; - - explicit Profiler(const napi_env &env, v8::Isolate *isolate) - : measurements_ticker(uv_default_loop()), - cpu_profiler( - v8::CpuProfiler::New(isolate, kNamingMode, GetLoggingMode())) {} -}; - -class SentryProfile { -private: - uint64_t started_at; - uint64_t timestamp; - uint16_t heap_write_index = 0; - uint16_t cpu_write_index = 0; - - std::vector heap_stats_ts; - std::vector heap_stats_usage; - - std::vector cpu_stats_ts; - std::vector cpu_stats_usage; - - const std::function memory_sampler_cb; - const std::function cpu_sampler_cb; - - ProfileStatus status = ProfileStatus::kNotStarted; - std::string id; - -public: - explicit SentryProfile(const char *id) - : started_at(uv_hrtime()), timestamp(timestamp_milliseconds()), - memory_sampler_cb([this](uint64_t ts, v8::HeapStatistics &stats) { - if ((heap_write_index >= heap_stats_ts.capacity()) || - heap_write_index >= heap_stats_usage.capacity()) { - return true; - } - - heap_stats_ts.insert(heap_stats_ts.begin() + heap_write_index, - ts - started_at); - heap_stats_usage.insert( - heap_stats_usage.begin() + heap_write_index, - static_cast(stats.used_heap_size())); - ++heap_write_index; - - return false; - }), - - cpu_sampler_cb([this](uint64_t ts, double rate) { - if (cpu_write_index >= cpu_stats_ts.capacity() || - cpu_write_index >= cpu_stats_usage.capacity()) { - return true; - } - cpu_stats_ts.insert(cpu_stats_ts.begin() + cpu_write_index, - ts - started_at); - cpu_stats_usage.insert(cpu_stats_usage.begin() + cpu_write_index, - rate); - ++cpu_write_index; - return false; - }), - - status(ProfileStatus::kNotStarted), id(id) { - heap_stats_ts.reserve(300); - heap_stats_usage.reserve(300); - cpu_stats_ts.reserve(300); - cpu_stats_usage.reserve(300); - } - - const std::vector &heap_usage_timestamps() const; - const std::vector &heap_usage_values() const; - const uint16_t &heap_usage_write_index() const; - - const std::vector &cpu_usage_timestamps() const; - const std::vector &cpu_usage_values() const; - const uint16_t &cpu_usage_write_index() const; - const uint64_t &profile_start_timestamp() const; - - void Start(Profiler *profiler); - v8::CpuProfile *Stop(Profiler *profiler); -}; - -void SentryProfile::Start(Profiler *profiler) { - v8::Local profile_title = - v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), id.c_str(), - v8::NewStringType::kNormal) - .ToLocalChecked(); - - started_at = uv_hrtime(); - timestamp = timestamp_milliseconds(); - - // Initialize the CPU Profiler - profiler->cpu_profiler->StartProfiling( - profile_title, v8::CpuProfilingMode::kCallerLineNumbers, true, - v8::CpuProfilingOptions::kNoSampleLimit); - - // listen for memory sample ticks - profiler->measurements_ticker.add_cpu_listener(id, cpu_sampler_cb); - profiler->measurements_ticker.add_heap_listener(id, memory_sampler_cb); - - status = ProfileStatus::kStarted; -} - -v8::CpuProfile *SentryProfile::Stop(Profiler *profiler) { - // Stop the CPU Profiler - v8::CpuProfile *profile = profiler->cpu_profiler->StopProfiling( - v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), id.c_str(), - v8::NewStringType::kNormal) - .ToLocalChecked()); - - // Remove the memory sampler - profiler->measurements_ticker.remove_heap_listener(id, memory_sampler_cb); - profiler->measurements_ticker.remove_cpu_listener(id, cpu_sampler_cb); - // If for some reason stopProfiling was called with an invalid profile title - // or if that title had somehow been stopped already, profile will be null. - status = ProfileStatus::kStopped; - return profile; -} - -// Memory getters -const std::vector &SentryProfile::heap_usage_timestamps() const { - return heap_stats_ts; -}; - -const std::vector &SentryProfile::heap_usage_values() const { - return heap_stats_usage; -}; - -const uint16_t &SentryProfile::heap_usage_write_index() const { - return heap_write_index; -}; - -// CPU getters -const std::vector &SentryProfile::cpu_usage_timestamps() const { - return cpu_stats_ts; -}; - -const std::vector &SentryProfile::cpu_usage_values() const { - return cpu_stats_usage; -}; -const uint16_t &SentryProfile::cpu_usage_write_index() const { - return cpu_write_index; -}; -const uint64_t &SentryProfile::profile_start_timestamp() const { - return timestamp; -} - -static void CleanupSentryProfile(Profiler *profiler, - SentryProfile *sentry_profile, - const std::string &profile_id) { - if (sentry_profile == nullptr) { - return; - } - - sentry_profile->Stop(profiler); - profiler->active_profiles.erase(profile_id); - delete sentry_profile; -}; - -#ifdef _WIN32 -static const char kPlatformSeparator = '\\'; -static const char kWinDiskPrefix = ':'; -#else -static const char kPlatformSeparator = '/'; -#endif - -static const char kSentryPathDelimiter = '.'; -static const char kSentryFileDelimiter = ':'; -static const std::string kNodeModulesPath = - std::string("node_modules") + kPlatformSeparator; - -static void GetFrameModule(const std::string &abs_path, std::string &module) { - if (abs_path.empty()) { - return; - } - - module = abs_path; - - // Drop .js extension - size_t module_len = module.length(); - if (module.compare(module_len - 3, 3, ".js") == 0) { - module = module.substr(0, module_len - 3); - } - - // Drop anything before and including node_modules/ - size_t node_modules_pos = module.rfind(kNodeModulesPath); - if (node_modules_pos != std::string::npos) { - module = module.substr(node_modules_pos + 13); - } - - // Replace all path separators with dots except the last one, that one is - // replaced with a colon - int match_count = 0; - for (int pos = module.length() - 1; pos >= 0; pos--) { - // if there is a match and it's not the first character, replace it - if (module[pos] == kPlatformSeparator) { - module[pos] = - match_count == 0 ? kSentryFileDelimiter : kSentryPathDelimiter; - match_count++; - } - } - -#ifdef _WIN32 - // Strip out C: prefix. On Windows, the drive letter is not part of the module - // name - if (module[1] == kWinDiskPrefix) { - // We will try and strip our the disk prefix. - module = module.substr(2, std::string::npos); - } -#endif - - if (module[0] == '.') { - module = module.substr(1, std::string::npos); - } -} - -static napi_value GetFrameModuleWrapped(napi_env env, napi_callback_info info) { - size_t argc = 2; - napi_value argv[2]; - napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); - - size_t len; - assert(napi_get_value_string_utf8(env, argv[0], NULL, 0, &len) == napi_ok); - - char *abs_path = (char *)malloc(len + 1); - assert(napi_get_value_string_utf8(env, argv[0], abs_path, len + 1, &len) == - napi_ok); - - std::string module; - napi_value napi_module; - - GetFrameModule(abs_path, module); - - assert(napi_create_string_utf8(env, module.c_str(), NAPI_AUTO_LENGTH, - &napi_module) == napi_ok); - return napi_module; -} - -napi_value -CreateFrameNode(const napi_env &env, const v8::CpuProfileNode &node, - std::unordered_map &module_cache, - napi_value &resources) { - napi_value js_node; - napi_create_object(env, &js_node); - - napi_value lineno_prop; - napi_create_int32(env, node.GetLineNumber(), &lineno_prop); - napi_set_named_property(env, js_node, "lineno", lineno_prop); - - napi_value colno_prop; - napi_create_int32(env, node.GetColumnNumber(), &colno_prop); - napi_set_named_property(env, js_node, "colno", colno_prop); - - if (node.GetSourceType() != v8::CpuProfileNode::SourceType::kScript) { - napi_value system_frame_prop; - napi_get_boolean(env, false, &system_frame_prop); - napi_set_named_property(env, js_node, "in_app", system_frame_prop); - } - - napi_value function; - napi_create_string_utf8(env, node.GetFunctionNameStr(), NAPI_AUTO_LENGTH, - &function); - napi_set_named_property(env, js_node, "function", function); - - const char *resource = node.GetScriptResourceNameStr(); - - if (resource != nullptr) { - // resource is absolute path, set it on the abs_path property - napi_value abs_path_prop; - napi_create_string_utf8(env, resource, NAPI_AUTO_LENGTH, &abs_path_prop); - napi_set_named_property(env, js_node, "abs_path", abs_path_prop); - // Error stack traces are not relative to root dir, doing our own path - // normalization breaks people's code mapping configs so we need to leave it - // as is. - napi_set_named_property(env, js_node, "filename", abs_path_prop); - - std::string module; - std::string resource_str = std::string(resource); - - if (resource_str.empty()) { - return js_node; - } - - if (module_cache.find(resource_str) != module_cache.end()) { - module = module_cache[resource_str]; - } else { - napi_value resource; - napi_create_string_utf8(env, resource_str.c_str(), NAPI_AUTO_LENGTH, - &resource); - napi_set_element(env, resources, module_cache.size(), resource); - - GetFrameModule(resource_str, module); - module_cache.emplace(resource_str, module); - } - - if (!module.empty()) { - napi_value filename_prop; - napi_create_string_utf8(env, module.c_str(), NAPI_AUTO_LENGTH, - &filename_prop); - napi_set_named_property(env, js_node, "module", filename_prop); - } - } - - return js_node; -}; - -napi_value CreateSample(const napi_env &env, const enum ProfileFormat format, - const uint32_t stack_id, - const int64_t sample_timestamp_ns, - const double chunk_timestamp, - const uint32_t thread_id) { - napi_value js_node; - napi_create_object(env, &js_node); - - napi_value stack_id_prop; - napi_create_uint32(env, stack_id, &stack_id_prop); - napi_set_named_property(env, js_node, "stack_id", stack_id_prop); - - napi_value thread_id_prop; - napi_create_string_utf8(env, std::to_string(thread_id).c_str(), - NAPI_AUTO_LENGTH, &thread_id_prop); - napi_set_named_property(env, js_node, "thread_id", thread_id_prop); - - switch (format) { - case ProfileFormat::kFormatThread: { - napi_value timestamp; - napi_create_int64(env, sample_timestamp_ns, ×tamp); - napi_set_named_property(env, js_node, "elapsed_since_start_ns", timestamp); - } break; - case ProfileFormat::kFormatChunk: { - napi_value timestamp; - napi_create_double(env, chunk_timestamp, ×tamp); - napi_set_named_property(env, js_node, "timestamp", timestamp); - } break; - default: - break; - } - - return js_node; -}; - -std::string kDelimiter = std::string(";"); -std::string hashCpuProfilerNodeByPath(const v8::CpuProfileNode *node, - std::string &path) { - path.clear(); - - while (node != nullptr) { - path.append(std::to_string(node->GetNodeId())); - node = node->GetParent(); - } - - return path; -} - -static void GetSamples(const napi_env &env, const v8::CpuProfile *profile, - ProfileFormat format, - const uint64_t profile_start_timestamp_ms, - const uint32_t thread_id, napi_value &samples, - napi_value &stacks, napi_value &frames, - napi_value &resources) { - const int64_t profile_start_time_us = profile->GetStartTime(); - const int64_t sampleCount = profile->GetSamplesCount(); - - uint32_t unique_stack_id = 0; - uint32_t unique_frame_id = 0; - - // Initialize the lookup tables for stacks and frames, both of these are - // indexed in the sample format we are using to optimize for size. - std::unordered_map frame_lookup_table; - std::unordered_map stack_lookup_table; - std::unordered_map module_cache; - - // At worst, all stacks are unique so reserve the maximum amount of space - stack_lookup_table.reserve(sampleCount); - - std::string node_hash = ""; - - for (int i = 0; i < sampleCount; i++) { - uint32_t stack_index = unique_stack_id; - - const v8::CpuProfileNode *node = profile->GetSample(i); - const int64_t sample_timestamp_us = profile->GetSampleTimestamp(i); - - // If a node was only on top of the stack once, then it will only ever - // be inserted once and there is no need for hashing. - if (node->GetHitCount() > 1) { - hashCpuProfilerNodeByPath(node, node_hash); - - std::unordered_map::iterator - stack_index_cache_hit = stack_lookup_table.find(node_hash); - - // If we have a hit, update the stack index, otherwise - // insert it into the hash table and continue. - if (stack_index_cache_hit == stack_lookup_table.end()) { - stack_lookup_table.emplace(node_hash, stack_index); - } else { - stack_index = stack_index_cache_hit->second; - } - } - - uint64_t sample_delta_us = sample_timestamp_us - profile_start_time_us; - uint64_t sample_timestamp_ns = sample_delta_us * 1e3; - uint64_t sample_offset_from_profile_start_ms = - (sample_timestamp_us - profile_start_time_us) * 1e-3; - double seconds_since_start = - (profile_start_timestamp_ms + sample_offset_from_profile_start_ms) * - 1e-3; - - napi_value sample = nullptr; - sample = CreateSample(env, format, stack_index, sample_timestamp_ns, - seconds_since_start, thread_id); - - if (stack_index != unique_stack_id) { - napi_value index; - napi_create_uint32(env, i, &index); - napi_set_property(env, samples, index, sample); - continue; - } - - // A stack is a list of frames ordered from outermost (top) to innermost - // frame (bottom) - napi_value stack; - napi_create_array(env, &stack); - - uint32_t stack_depth = 0; - - while (node != nullptr && stack_depth < kMaxStackDepth) { - auto nodeId = node->GetNodeId(); - auto frame_index = frame_lookup_table.find(nodeId); - - // If the frame does not exist in the index - if (frame_index == frame_lookup_table.end()) { - frame_lookup_table.emplace(nodeId, unique_frame_id); - - napi_value frame_id; - napi_create_uint32(env, unique_frame_id, &frame_id); - - napi_value depth; - napi_create_uint32(env, stack_depth, &depth); - napi_set_property(env, stack, depth, frame_id); - napi_set_property(env, frames, frame_id, - CreateFrameNode(env, *node, module_cache, resources)); - - unique_frame_id++; - } else { - // If it was already indexed, just add it's id to the stack - napi_value depth; - napi_create_uint32(env, stack_depth, &depth); - - napi_value frame; - napi_create_uint32(env, frame_index->second, &frame); - napi_set_property(env, stack, depth, frame); - }; - - // Continue walking down the stack - node = node->GetParent(); - stack_depth++; - } - - napi_value napi_sample_index; - napi_value napi_stack_index; - - napi_create_uint32(env, i, &napi_sample_index); - napi_set_property(env, samples, napi_sample_index, sample); - napi_create_uint32(env, stack_index, &napi_stack_index); - napi_set_property(env, stacks, napi_stack_index, stack); - - unique_stack_id++; - } -} - -static napi_value TranslateMeasurementsDouble( - const napi_env &env, const enum ProfileFormat format, const char *unit, - const uint64_t profile_start_timestamp_ms, const uint16_t size, - const std::vector &values, - const std::vector ×tamps_ns) { - if (size > values.size() || size > timestamps_ns.size()) { - napi_throw_range_error(env, "NAPI_ERROR", - "CPU measurement size is larger than the number of " - "values or timestamps"); - return nullptr; - } - - if (values.size() != timestamps_ns.size()) { - napi_throw_range_error(env, "NAPI_ERROR", - "CPU measurement entries are corrupt, expected " - "values and timestamps to be of equal length"); - return nullptr; - } - - napi_value measurement; - napi_create_object(env, &measurement); - - napi_value unit_string; - napi_create_string_utf8(env, unit, NAPI_AUTO_LENGTH, &unit_string); - napi_set_named_property(env, measurement, "unit", unit_string); - - napi_value values_array; - napi_create_array(env, &values_array); - - uint16_t idx = size; - - for (size_t i = 0; i < idx; i++) { - napi_value entry; - napi_create_object(env, &entry); - - napi_value value; - if (napi_create_double(env, values[i], &value) != napi_ok) { - if (napi_create_double(env, 0.0, &value) != napi_ok) { - continue; - } - } - - napi_set_named_property(env, entry, "value", value); - - if (format == ProfileFormat::kFormatThread) { - napi_value ts; - napi_create_int64(env, timestamps_ns[i], &ts); - napi_set_named_property(env, entry, "elapsed_since_start_ns", ts); - } else if (format == ProfileFormat::kFormatChunk) { - napi_value ts; - napi_create_double( - env, profile_start_timestamp_ms + (timestamps_ns[i] * 1e-9), &ts); - napi_set_named_property(env, entry, "timestamp", ts); - } - - napi_set_element(env, values_array, i, entry); - } - - napi_set_named_property(env, measurement, "values", values_array); - - return measurement; -} - -static napi_value -TranslateMeasurements(const napi_env &env, const enum ProfileFormat format, - const char *unit, - const uint64_t profile_start_timestamp_ms, - const uint16_t size, const std::vector &values, - const std::vector ×tamps_ns) { - if (size > values.size() || size > timestamps_ns.size()) { - napi_throw_range_error(env, "NAPI_ERROR", - "Memory measurement size is larger than the number " - "of values or timestamps"); - return nullptr; - } - - if (values.size() != timestamps_ns.size()) { - napi_throw_range_error(env, "NAPI_ERROR", - "Memory measurement entries are corrupt, expected " - "values and timestamps to be of equal length"); - return nullptr; - } - - napi_value measurement; - napi_create_object(env, &measurement); - - napi_value unit_string; - napi_create_string_utf8(env, unit, NAPI_AUTO_LENGTH, &unit_string); - napi_set_named_property(env, measurement, "unit", unit_string); - - napi_value values_array; - napi_create_array(env, &values_array); - - for (size_t i = 0; i < size; i++) { - napi_value entry; - napi_create_object(env, &entry); - - napi_value value; - napi_create_int64(env, values[i], &value); - - napi_set_named_property(env, entry, "value", value); - switch (format) { - case ProfileFormat::kFormatThread: { - napi_value ts; - napi_create_int64(env, timestamps_ns[i], &ts); - napi_set_named_property(env, entry, "elapsed_since_start_ns", ts); - } break; - case ProfileFormat::kFormatChunk: { - napi_value ts; - napi_create_double( - env, profile_start_timestamp_ms + (timestamps_ns[i] * 1e-9), &ts); - napi_set_named_property(env, entry, "timestamp", ts); - } break; - default: - break; - } - napi_set_element(env, values_array, i, entry); - } - - napi_set_named_property(env, measurement, "values", values_array); - - return measurement; -} - -static napi_value TranslateProfile(const napi_env &env, - const v8::CpuProfile *profile, - const enum ProfileFormat format, - const uint64_t profile_start_timestamp_ms, - const uint32_t thread_id, - bool collect_resources) { - napi_value js_profile; - - napi_create_object(env, &js_profile); - - napi_value logging_mode; - napi_value samples; - napi_value stacks; - napi_value frames; - napi_value resources; - - napi_create_string_utf8( - env, - GetLoggingMode() == v8::CpuProfilingLoggingMode::kEagerLogging ? "eager" - : "lazy", - NAPI_AUTO_LENGTH, &logging_mode); - - napi_create_array(env, &samples); - napi_create_array(env, &stacks); - napi_create_array(env, &frames); - napi_create_array(env, &resources); - - napi_set_named_property(env, js_profile, "samples", samples); - napi_set_named_property(env, js_profile, "stacks", stacks); - napi_set_named_property(env, js_profile, "frames", frames); - napi_set_named_property(env, js_profile, "profiler_logging_mode", - logging_mode); - - GetSamples(env, profile, format, profile_start_timestamp_ms, thread_id, - samples, stacks, frames, resources); - - if (collect_resources) { - napi_set_named_property(env, js_profile, "resources", resources); - } else { - napi_create_array(env, &resources); - napi_set_named_property(env, js_profile, "resources", resources); - } - - return js_profile; -} - -static napi_value StartProfiling(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value argv[1]; - - assert(napi_get_cb_info(env, info, &argc, argv, NULL, NULL) == napi_ok); - - napi_valuetype callbacktype0; - assert(napi_typeof(env, argv[0], &callbacktype0) == napi_ok); - - if (callbacktype0 != napi_string) { - napi_throw_error( - env, "NAPI_ERROR", - "TypeError: StartProfiling expects a string as first argument."); - napi_value napi_null; - assert(napi_get_null(env, &napi_null) == napi_ok); - - return napi_null; - } - - size_t len; - assert(napi_get_value_string_utf8(env, argv[0], NULL, 0, &len) == napi_ok); - - char *title = (char *)malloc(len + 1); - assert(napi_get_value_string_utf8(env, argv[0], title, len + 1, &len) == - napi_ok); - - if (len < 1) { - napi_throw_error(env, "NAPI_ERROR", - "StartProfiling expects a non-empty string as first " - "argument, got an empty string."); - - napi_value napi_null; - assert(napi_get_null(env, &napi_null) == napi_ok); - - return napi_null; - } - - v8::Isolate *isolate = v8::Isolate::GetCurrent(); - assert(isolate != 0); - - Profiler *profiler; - assert(napi_get_instance_data(env, (void **)&profiler) == napi_ok); - - if (!profiler) { - napi_throw_error(env, "NAPI_ERROR", - "StartProfiling: Profiler is not initialized."); - - napi_value napi_null; - assert(napi_get_null(env, &napi_null) == napi_ok); - - return napi_null; - } - - const std::string profile_id(title); - // In case we have a collision, cleanup the old profile first - auto existing_profile = profiler->active_profiles.find(profile_id); - if (existing_profile != profiler->active_profiles.end()) { - existing_profile->second->Stop(profiler); - CleanupSentryProfile(profiler, existing_profile->second, profile_id); - } - - SentryProfile *sentry_profile = new SentryProfile(title); - sentry_profile->Start(profiler); - - profiler->active_profiles.emplace(profile_id, sentry_profile); - - napi_value napi_null; - assert(napi_get_null(env, &napi_null) == napi_ok); - - return napi_null; -} - -// StopProfiling(string title) -// https://v8docs.nodesource.com/node-18.2/d2/d34/classv8_1_1_cpu_profiler.html#a40ca4c8a8aa4c9233aa2a2706457cc80 -static napi_value StopProfiling(napi_env env, napi_callback_info info) { - size_t argc = 4; - napi_value argv[4]; - - assert(napi_get_cb_info(env, info, &argc, argv, NULL, NULL) == napi_ok); - - if (argc < 3) { - napi_throw_error(env, "NAPI_ERROR", - "StopProfiling expects at least three arguments."); - - napi_value napi_null; - assert(napi_get_null(env, &napi_null) == napi_ok); - - return napi_null; - } - - // Verify the first argument is a string - napi_valuetype callbacktype0; - assert(napi_typeof(env, argv[0], &callbacktype0) == napi_ok); - - if (callbacktype0 != napi_string) { - napi_throw_error(env, "NAPI_ERROR", - "StopProfiling expects a string as first argument."); - - napi_value napi_null; - assert(napi_get_null(env, &napi_null) == napi_ok); - - return napi_null; - } - - size_t len; - assert(napi_get_value_string_utf8(env, argv[0], NULL, 0, &len) == napi_ok); - - char *title = (char *)malloc(len + 1); - assert(napi_get_value_string_utf8(env, argv[0], title, len + 1, &len) == - napi_ok); - - if (len < 1) { - napi_throw_error( - env, "NAPI_ERROR", - "StopProfiling expects a non empty string as first argument."); - - napi_value napi_null; - assert(napi_get_null(env, &napi_null) == napi_ok); - - return napi_null; - } - - // Verify the second argument is a number - napi_valuetype callbacktype1; - assert(napi_typeof(env, argv[1], &callbacktype1) == napi_ok); - - if (callbacktype1 != napi_number) { - napi_throw_error(env, "NAPI_ERROR", - "StopProfiling expects a format type as second argument."); - - napi_value napi_null; - assert(napi_get_null(env, &napi_null) == napi_ok); - - return napi_null; - } - - // Verify the second argument is a number - napi_valuetype callbacktype2; - assert(napi_typeof(env, argv[2], &callbacktype2) == napi_ok); - - if (callbacktype2 != napi_number) { - napi_throw_error( - env, "NAPI_ERROR", - "StopProfiling expects a thread_id integer as third argument."); - - napi_value napi_null; - assert(napi_get_null(env, &napi_null) == napi_ok); - return napi_null; - } - - // Get the value of the second argument and convert it to uint8 - int32_t format; - assert(napi_get_value_int32(env, argv[1], &format) == napi_ok); - - // Get the value of the second argument and convert it to uint64 - int64_t thread_id; - assert(napi_get_value_int64(env, argv[2], &thread_id) == napi_ok); - - // Get profiler from instance data - Profiler *profiler; - assert(napi_get_instance_data(env, (void **)&profiler) == napi_ok); - - if (!profiler) { - napi_throw_error(env, "NAPI_ERROR", - "StopProfiling: Profiler is not initialized."); - - napi_value napi_null; - assert(napi_get_null(env, &napi_null) == napi_ok); - return napi_null; - } - - const std::string profile_id(title); - auto profile = profiler->active_profiles.find(profile_id); - - // If the profile was never started, silently ignore the call and return null - if (profile == profiler->active_profiles.end()) { - napi_value napi_null; - assert(napi_get_null(env, &napi_null) == napi_ok); - return napi_null; - } - - v8::CpuProfile *cpu_profile = profile->second->Stop(profiler); - - // If for some reason stopProfiling was called with an invalid profile title - // or if that title had somehow been stopped already, profile will be null. - if (!cpu_profile) { - CleanupSentryProfile(profiler, profile->second, profile_id); - - napi_value napi_null; - assert(napi_get_null(env, &napi_null) == napi_ok); - return napi_null; - }; - - napi_valuetype callbacktype3; - assert(napi_typeof(env, argv[3], &callbacktype3) == napi_ok); - - bool collect_resources; - napi_get_value_bool(env, argv[3], &collect_resources); - - const ProfileFormat format_type = static_cast(format); - - if (format_type != ProfileFormat::kFormatThread && - format_type != ProfileFormat::kFormatChunk) { - napi_throw_error( - env, "NAPI_ERROR", - "StopProfiling expects a valid format type as second argument."); - - napi_value napi_null; - assert(napi_get_null(env, &napi_null) == napi_ok); - return napi_null; - } - - napi_value js_profile = TranslateProfile( - env, cpu_profile, format_type, profile->second->profile_start_timestamp(), - thread_id, collect_resources); - - napi_value measurements; - napi_create_object(env, &measurements); - - if (profile->second->heap_usage_write_index() > 0) { - static const char *memory_unit = "byte"; - napi_value heap_usage_measurements = - TranslateMeasurements(env, format_type, memory_unit, - profile->second->profile_start_timestamp(), - profile->second->heap_usage_write_index(), - profile->second->heap_usage_values(), - profile->second->heap_usage_timestamps()); - - if (heap_usage_measurements != nullptr) { - napi_set_named_property(env, measurements, "memory_footprint", - heap_usage_measurements); - }; - }; - - if (profile->second->cpu_usage_write_index() > 0) { - static const char *cpu_unit = "percent"; - napi_value cpu_usage_measurements = TranslateMeasurementsDouble( - env, format_type, cpu_unit, profile->second->profile_start_timestamp(), - profile->second->cpu_usage_write_index(), - profile->second->cpu_usage_values(), - profile->second->cpu_usage_timestamps()); - - if (cpu_usage_measurements != nullptr) { - napi_set_named_property(env, measurements, "cpu_usage", - cpu_usage_measurements); - }; - }; - - napi_set_named_property(env, js_profile, "measurements", measurements); - - CleanupSentryProfile(profiler, profile->second, profile_id); - cpu_profile->Delete(); - - return js_profile; -}; - -void FreeAddonData(napi_env env, void *data, void *hint) { - Profiler *profiler = static_cast(data); - - if (profiler == nullptr) { - return; - } - - if (!profiler->active_profiles.empty()) { - for (auto &profile : profiler->active_profiles) { - CleanupSentryProfile(profiler, profile.second, profile.first); - } - } - - if (profiler->cpu_profiler != nullptr) { - profiler->cpu_profiler->Dispose(); - profiler->cpu_profiler = nullptr; - } - - delete profiler; -} - -napi_value Init(napi_env env, napi_value exports) { - v8::Isolate *isolate = v8::Isolate::GetCurrent(); - - if (isolate == nullptr) { - napi_throw_error(env, nullptr, - "Failed to initialize Sentry profiler: isolate is null."); - return NULL; - } - - Profiler *profiler = new Profiler(env, isolate); - profiler->cpu_profiler->SetSamplingInterval(kSamplingInterval); - - if (napi_set_instance_data(env, profiler, FreeAddonData, NULL) != napi_ok) { - napi_throw_error(env, nullptr, "Failed to set instance data for profiler."); - return NULL; - } - - napi_value start_profiling; - if (napi_create_function(env, "startProfiling", NAPI_AUTO_LENGTH, - StartProfiling, exports, - &start_profiling) != napi_ok) { - napi_throw_error(env, nullptr, "Failed to create startProfiling function."); - return NULL; - } - - if (napi_set_named_property(env, exports, "startProfiling", - start_profiling) != napi_ok) { - napi_throw_error(env, nullptr, - "Failed to set startProfiling property on exports."); - return NULL; - } - - napi_value stop_profiling; - if (napi_create_function(env, "stopProfiling", NAPI_AUTO_LENGTH, - StopProfiling, exports, - &stop_profiling) != napi_ok) { - napi_throw_error(env, nullptr, "Failed to create stopProfiling function."); - return NULL; - } - - if (napi_set_named_property(env, exports, "stopProfiling", stop_profiling) != - napi_ok) { - napi_throw_error(env, nullptr, - "Failed to set stopProfiling property on exports."); - return NULL; - } - - napi_value get_frame_module; - if (napi_create_function(env, "getFrameModule", NAPI_AUTO_LENGTH, - GetFrameModuleWrapped, exports, - &get_frame_module) != napi_ok) { - napi_throw_error(env, nullptr, "Failed to create getFrameModule function."); - return NULL; - } - - if (napi_set_named_property(env, exports, "getFrameModule", - get_frame_module) != napi_ok) { - napi_throw_error(env, nullptr, - "Failed to set getFrameModule property on exports."); - return NULL; - } - - return exports; -} - -NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/packages/profiling-node/clang-format.js b/packages/profiling-node/clang-format.js deleted file mode 100644 index dd001cf28ad7..000000000000 --- a/packages/profiling-node/clang-format.js +++ /dev/null @@ -1,26 +0,0 @@ -const child_process = require('child_process'); - -const args = ['--Werror', '-i', '--style=file', 'bindings/cpu_profiler.cc']; -const cmd = `./node_modules/.bin/clang-format ${args.join(' ')}`; - -try { - child_process.execSync(cmd); -} catch (e) { - // This fails on linux_arm64 - // eslint-disable-next-line no-console - console.log('Running clang format command failed.'); -} - -// eslint-disable-next-line no-console -console.log('clang-format: done, checking tree...'); - -const diff = child_process.execSync('git status --short').toString(); - -if (diff) { - // eslint-disable-next-line no-console - console.error('clang-format: check failed ❌'); - process.exit(1); -} - -// eslint-disable-next-line no-console -console.log('clang-format: check passed ✅'); diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index d07295ba2679..a9e6ed2f1fbd 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -49,25 +49,16 @@ "/scripts/prune-profiler-binaries.js" ], "scripts": { - "install": "node scripts/check-build.js", "clean": "rm -rf build && rm -rf lib", - "lint": "yarn lint:eslint && yarn lint:clang", - "lint:eslint": "eslint . --format stylish", - "lint:clang": "node clang-format.js", + "lint": "eslint . --format stylish", "fix": "eslint . --format stylish --fix", - "lint:fix": "yarn fix:eslint && yarn fix:clang", - "lint:fix:clang": "node clang-format.js --fix", - "build": "yarn build:lib && yarn build:bindings:configure && yarn build:bindings", + "build": "yarn build:lib", "build:lib": "yarn build:types && rollup -c rollup.npm.config.mjs", - "build:transpile": "yarn build:bindings:configure && yarn build:bindings && yarn build:lib", + "build:transpile": "yarn build:lib", "build:types:downlevel": "yarn downlevel-dts lib/types lib/types-ts3.8 --to ts3.8", "build:types": "tsc -p tsconfig.types.json && yarn build:types:downlevel", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:bindings:configure": "node-gyp configure", - "build:bindings:configure:arm64": "node-gyp configure --arch=arm64 --target_arch=arm64", - "build:bindings": "node-gyp build && node scripts/copy-target.js", - "build:bindings:arm64": "node-gyp build --arch=arm64 && node scripts/copy-target.js", - "build:dev": "yarn clean && yarn build:bindings:configure && yarn build", + "build:dev": "yarn clean && yarn build", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:watch": "run-p build:transpile:watch build:types:watch", "build:tarball": "npm pack", @@ -78,15 +69,11 @@ "dependencies": { "@sentry/core": "9.0.0-alpha.0", "@sentry/node": "9.0.0-alpha.0", - "detect-libc": "^2.0.2", - "node-abi": "^3.61.0" + "@sentry-internal/node-cpu-profiler": "1.0.0" }, "devDependencies": { "@types/node": "^18.19.1", - "@types/node-abi": "^3.0.3", - "clang-format": "^1.8.0", - "cross-env": "^7.0.3", - "node-gyp": "^9.4.1" + "cross-env": "^7.0.3" }, "volta": { "extends": "../../package.json" diff --git a/packages/profiling-node/rollup.npm.config.mjs b/packages/profiling-node/rollup.npm.config.mjs index a9c148306709..3eaf9267bb43 100644 --- a/packages/profiling-node/rollup.npm.config.mjs +++ b/packages/profiling-node/rollup.npm.config.mjs @@ -1,20 +1,19 @@ -import commonjs from '@rollup/plugin-commonjs'; -import replace from '@rollup/plugin-replace'; import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default makeNPMConfigVariants( makeBaseNPMConfig({ packageSpecificConfig: { - output: { dir: 'lib', preserveModules: false }, - plugins: [ - commonjs(), - replace({ - preventAssignment: false, - values: { - __IMPORT_META_URL_REPLACEMENT__: 'import.meta.url', - }, - }), - ], + output: { + dir: 'lib', + // set exports to 'named' or 'auto' so that rollup doesn't warn + exports: 'named', + // set preserveModules to false because for feedback we actually want + // to bundle everything into one file. + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? false + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), + }, }, }), ); diff --git a/packages/profiling-node/scripts/binaries.js b/packages/profiling-node/scripts/binaries.js deleted file mode 100644 index 2c0c6be2642b..000000000000 --- a/packages/profiling-node/scripts/binaries.js +++ /dev/null @@ -1,27 +0,0 @@ -const os = require('os'); -const path = require('path'); - -const abi = require('node-abi'); -const libc = require('detect-libc'); - -function getModuleName() { - const stdlib = libc.familySync(); - const platform = process.env['BUILD_PLATFORM'] || os.platform(); - const arch = process.env['BUILD_ARCH'] || os.arch(); - - if (platform === 'darwin' && arch === 'arm64') { - const identifier = [platform, 'arm64', abi.getAbi(process.versions.node, 'node')].filter(Boolean).join('-'); - return `sentry_cpu_profiler-${identifier}.node`; - } - - const identifier = [platform, arch, stdlib, abi.getAbi(process.versions.node, 'node')].filter(Boolean).join('-'); - - return `sentry_cpu_profiler-${identifier}.node`; -} - -const source = path.join(__dirname, '..', 'build', 'Release', 'sentry_cpu_profiler.node'); -const target = path.join(__dirname, '..', 'lib', getModuleName()); - -module.exports.source = source; -module.exports.target = target; -module.exports.getModuleName = getModuleName; diff --git a/packages/profiling-node/scripts/check-build.js b/packages/profiling-node/scripts/check-build.js deleted file mode 100644 index dda96e66b900..000000000000 --- a/packages/profiling-node/scripts/check-build.js +++ /dev/null @@ -1,56 +0,0 @@ -// This is a build script, so some logging is desirable as it allows -// us to follow the code path that triggered the error. -/* eslint-disable no-console */ -const fs = require('fs'); -const child_process = require('child_process'); -const binaries = require('./binaries.js'); - -function clean(err) { - return err.toString().trim(); -} - -function recompileFromSource() { - console.log('@sentry/profiling-node: Compiling from source...'); - let spawn = child_process.spawnSync('npm', ['run', 'build:bindings:configure'], { - stdio: ['inherit', 'inherit', 'pipe'], - env: process.env, - shell: true, - }); - - if (spawn.status !== 0) { - console.log('@sentry/profiling-node: Failed to configure gyp'); - console.log('@sentry/profiling-node:', clean(spawn.stderr)); - return; - } - - spawn = child_process.spawnSync('npm', ['run', 'build:bindings'], { - stdio: ['inherit', 'inherit', 'pipe'], - env: process.env, - shell: true, - }); - if (spawn.status !== 0) { - console.log('@sentry/profiling-node: Failed to build bindings'); - console.log('@sentry/profiling-node:', clean(spawn.stderr)); - return; - } -} - -if (fs.existsSync(binaries.target)) { - try { - console.log(`@sentry/profiling-node: Precompiled binary found, attempting to load ${binaries.target}`); - require(binaries.target); - console.log('@sentry/profiling-node: Precompiled binary found, skipping build from source.'); - } catch (e) { - console.log('@sentry/profiling-node: Precompiled binary found but failed loading'); - console.log('@sentry/profiling-node:', e); - try { - recompileFromSource(); - } catch (e) { - console.log('@sentry/profiling-node: Failed to compile from source'); - throw e; - } - } -} else { - console.log('@sentry/profiling-node: No precompiled binary found'); - recompileFromSource(); -} diff --git a/packages/profiling-node/scripts/copy-target.js b/packages/profiling-node/scripts/copy-target.js deleted file mode 100644 index 8277f1d45290..000000000000 --- a/packages/profiling-node/scripts/copy-target.js +++ /dev/null @@ -1,27 +0,0 @@ -// This is a build script, so some logging is desirable as it allows -// us to follow the code path that triggered the error. -/* eslint-disable no-console */ -const fs = require('fs'); -const path = require('path'); -const process = require('process'); -const binaries = require('./binaries.js'); - -const build = path.resolve(__dirname, '..', 'lib'); - -if (!fs.existsSync(build)) { - fs.mkdirSync(build, { recursive: true }); -} - -const source = path.join(__dirname, '..', 'build', 'Release', 'sentry_cpu_profiler.node'); -const target = path.join(__dirname, '..', 'lib', binaries.getModuleName()); - -if (!fs.existsSync(source)) { - console.log('Source file does not exist:', source); - process.exit(1); -} else { - if (fs.existsSync(target)) { - console.log('Target file already exists, overwriting it'); - } - console.log('Renaming', source, 'to', target); - fs.renameSync(source, target); -} diff --git a/packages/profiling-node/src/cpu_profiler.ts b/packages/profiling-node/src/cpu_profiler.ts deleted file mode 100644 index a9a6d65ce191..000000000000 --- a/packages/profiling-node/src/cpu_profiler.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { createRequire } from 'node:module'; -import { arch as _arch, platform as _platform } from 'node:os'; -import { join, resolve } from 'node:path'; -import { dirname } from 'node:path'; -import { env, versions } from 'node:process'; -import { fileURLToPath, pathToFileURL } from 'node:url'; -import { threadId } from 'node:worker_threads'; -import { familySync } from 'detect-libc'; -import { getAbi } from 'node-abi'; - -import { GLOBAL_OBJ, logger } from '@sentry/core'; -import { DEBUG_BUILD } from './debug-build'; -import type { - PrivateV8CpuProfilerBindings, - RawChunkCpuProfile, - RawThreadCpuProfile, - V8CpuProfilerBindings, -} from './types'; -import type { ProfileFormat } from './types'; - -declare const __IMPORT_META_URL_REPLACEMENT__: string; - -const stdlib = familySync(); -const platform = process.env['BUILD_PLATFORM'] || _platform(); -const arch = process.env['BUILD_ARCH'] || _arch(); -const abi = getAbi(versions.node, 'node'); -const identifier = [platform, arch, stdlib, abi].filter(c => c !== undefined && c !== null).join('-'); - -/** - * Imports cpp bindings based on the current platform and architecture. - */ -// eslint-disable-next-line complexity -export function importCppBindingsModule(): PrivateV8CpuProfilerBindings { - // We need to work around using import.meta.url directly with __IMPORT_META_URL_REPLACEMENT__ because jest complains about it. - const importMetaUrl = - typeof __IMPORT_META_URL_REPLACEMENT__ !== 'undefined' - ? // This case is always hit when the SDK is built - __IMPORT_META_URL_REPLACEMENT__ - : // This case is hit when the tests are run - pathToFileURL(__filename).href; - - const createdRequire = createRequire(importMetaUrl); - const esmCompatibleDirname = dirname(fileURLToPath(importMetaUrl)); - - // If a binary path is specified, use that. - if (env['SENTRY_PROFILER_BINARY_PATH']) { - const envPath = env['SENTRY_PROFILER_BINARY_PATH']; - return createdRequire(envPath); - } - - // If a user specifies a different binary dir, they are in control of the binaries being moved there - if (env['SENTRY_PROFILER_BINARY_DIR']) { - const binaryPath = join(resolve(env['SENTRY_PROFILER_BINARY_DIR']), `sentry_cpu_profiler-${identifier}`); - return createdRequire(`${binaryPath}.node`); - } - - // We need the fallthrough so that in the end, we can fallback to the dynamic require. - // This is for cases where precompiled binaries were not provided, but may have been compiled from source. - if (platform === 'darwin') { - if (arch === 'x64') { - if (abi === '93') { - return createdRequire('../sentry_cpu_profiler-darwin-x64-93.node'); - } - if (abi === '108') { - return createdRequire('../sentry_cpu_profiler-darwin-x64-108.node'); - } - if (abi === '115') { - return createdRequire('../sentry_cpu_profiler-darwin-x64-115.node'); - } - if (abi === '127') { - return createdRequire('../sentry_cpu_profiler-darwin-x64-127.node'); - } - } - - if (arch === 'arm64') { - if (abi === '93') { - return createdRequire('../sentry_cpu_profiler-darwin-arm64-93.node'); - } - if (abi === '108') { - return createdRequire('../sentry_cpu_profiler-darwin-arm64-108.node'); - } - if (abi === '115') { - return createdRequire('../sentry_cpu_profiler-darwin-arm64-115.node'); - } - if (abi === '127') { - return createdRequire('../sentry_cpu_profiler-darwin-arm64-127.node'); - } - } - } - - if (platform === 'win32') { - if (arch === 'x64') { - if (abi === '93') { - return createdRequire('../sentry_cpu_profiler-win32-x64-93.node'); - } - if (abi === '108') { - return createdRequire('../sentry_cpu_profiler-win32-x64-108.node'); - } - if (abi === '115') { - return createdRequire('../sentry_cpu_profiler-win32-x64-115.node'); - } - if (abi === '127') { - return createdRequire('../sentry_cpu_profiler-win32-x64-127.node'); - } - } - } - - if (platform === 'linux') { - if (arch === 'x64') { - if (stdlib === 'musl') { - if (abi === '93') { - return createdRequire('../sentry_cpu_profiler-linux-x64-musl-93.node'); - } - if (abi === '108') { - return createdRequire('../sentry_cpu_profiler-linux-x64-musl-108.node'); - } - if (abi === '115') { - return createdRequire('../sentry_cpu_profiler-linux-x64-musl-115.node'); - } - if (abi === '127') { - return createdRequire('../sentry_cpu_profiler-linux-x64-musl-127.node'); - } - } - if (stdlib === 'glibc') { - if (abi === '93') { - return createdRequire('../sentry_cpu_profiler-linux-x64-glibc-93.node'); - } - if (abi === '108') { - return createdRequire('../sentry_cpu_profiler-linux-x64-glibc-108.node'); - } - if (abi === '115') { - return createdRequire('../sentry_cpu_profiler-linux-x64-glibc-115.node'); - } - if (abi === '127') { - return createdRequire('../sentry_cpu_profiler-linux-x64-glibc-127.node'); - } - } - } - if (arch === 'arm64') { - if (stdlib === 'musl') { - if (abi === '93') { - return createdRequire('../sentry_cpu_profiler-linux-arm64-musl-93.node'); - } - if (abi === '108') { - return createdRequire('../sentry_cpu_profiler-linux-arm64-musl-108.node'); - } - if (abi === '115') { - return createdRequire('../sentry_cpu_profiler-linux-arm64-musl-115.node'); - } - if (abi === '127') { - return createdRequire('../sentry_cpu_profiler-linux-arm64-musl-127.node'); - } - } - - if (stdlib === 'glibc') { - if (abi === '93') { - return createdRequire('../sentry_cpu_profiler-linux-arm64-glibc-93.node'); - } - if (abi === '108') { - return createdRequire('../sentry_cpu_profiler-linux-arm64-glibc-108.node'); - } - if (abi === '115') { - return createdRequire('../sentry_cpu_profiler-linux-arm64-glibc-115.node'); - } - if (abi === '127') { - return createdRequire('../sentry_cpu_profiler-linux-arm64-glibc-127.node'); - } - } - } - } - - const built_from_source_path = resolve(esmCompatibleDirname, '..', `sentry_cpu_profiler-${identifier}`); - return createdRequire(`${built_from_source_path}.node`); -} - -const PrivateCpuProfilerBindings: PrivateV8CpuProfilerBindings = importCppBindingsModule(); - -class Bindings implements V8CpuProfilerBindings { - public startProfiling(name: string): void { - if (!PrivateCpuProfilerBindings) { - DEBUG_BUILD && logger.log('[Profiling] Bindings not loaded, ignoring call to startProfiling.'); - return; - } - - if (typeof PrivateCpuProfilerBindings.startProfiling !== 'function') { - DEBUG_BUILD && - logger.log('[Profiling] Native startProfiling function is not available, ignoring call to startProfiling.'); - return; - } - - return PrivateCpuProfilerBindings.startProfiling(name); - } - - public stopProfiling(name: string, format: ProfileFormat.THREAD): RawThreadCpuProfile | null; - public stopProfiling(name: string, format: ProfileFormat.CHUNK): RawChunkCpuProfile | null; - public stopProfiling( - name: string, - format: ProfileFormat.CHUNK | ProfileFormat.THREAD, - ): RawThreadCpuProfile | RawChunkCpuProfile | null { - if (!PrivateCpuProfilerBindings) { - DEBUG_BUILD && - logger.log('[Profiling] Bindings not loaded or profile was never started, ignoring call to stopProfiling.'); - return null; - } - - if (typeof PrivateCpuProfilerBindings.stopProfiling !== 'function') { - DEBUG_BUILD && - logger.log('[Profiling] Native stopProfiling function is not available, ignoring call to stopProfiling.'); - return null; - } - - return PrivateCpuProfilerBindings.stopProfiling( - name, - format as unknown as any, - threadId, - !!GLOBAL_OBJ._sentryDebugIds, - ); - } -} - -const CpuProfilerBindings = new Bindings(); - -export { PrivateCpuProfilerBindings }; -export { CpuProfilerBindings }; diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index ca9db531d1e9..4f347103722f 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -14,7 +14,7 @@ import { uuid4, } from '@sentry/core'; import type { NodeClient } from '@sentry/node'; -import { CpuProfilerBindings } from './cpu_profiler'; +import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler'; import { DEBUG_BUILD } from './debug-build'; import { NODE_MAJOR, NODE_VERSION } from './nodeVersion'; import { MAX_PROFILE_DURATION_MS, maybeProfileSpan, stopSpanProfile } from './spanProfileUtils'; @@ -120,8 +120,8 @@ function setupAutomatedSpanProfiling(client: NodeClient): void { const profilesToAddToEnvelope: Profile[] = []; for (const profiledTransaction of profiledTransactionEvents) { - const profileContext = profiledTransaction.contexts?.['profile']; - const profile_id = profileContext?.['profile_id']; + const profileContext = profiledTransaction.contexts?.profile; + const profile_id = profileContext?.profile_id; if (!profile_id) { throw new TypeError('[Profiling] cannot find profile for a transaction without a profile context'); @@ -129,7 +129,7 @@ function setupAutomatedSpanProfiling(client: NodeClient): void { // Remove the profile from the transaction context before sending, relay will take care of the rest. if (profileContext) { - delete profiledTransaction.contexts?.['profile']; + delete profiledTransaction.contexts?.profile; } const cpuProfile = takeFromProfileQueue(profile_id); @@ -400,7 +400,7 @@ class ContinuousProfiler { * Assigns thread_id and thread name context to a profiled event. */ private _assignThreadIdContext(event: Event): void { - if (!event?.['contexts']?.['profile']) { + if (!event?.contexts?.profile) { return; } @@ -410,10 +410,10 @@ class ContinuousProfiler { // @ts-expect-error the trace fallback value is wrong, though it should never happen // and in case it does, we dont want to override whatever was passed initially. - event.contexts['trace'] = { - ...(event.contexts?.['trace'] ?? {}), + event.contexts.trace = { + ...(event.contexts?.trace ?? {}), data: { - ...(event.contexts?.['trace']?.['data'] ?? {}), + ...(event.contexts?.trace?.data ?? {}), ['thread.id']: PROFILER_THREAD_ID_STRING, ['thread.name']: PROFILER_THREAD_NAME, }, diff --git a/packages/profiling-node/src/spanProfileUtils.ts b/packages/profiling-node/src/spanProfileUtils.ts index 1ee050ce22e5..5e4897533938 100644 --- a/packages/profiling-node/src/spanProfileUtils.ts +++ b/packages/profiling-node/src/spanProfileUtils.ts @@ -1,7 +1,7 @@ import type { CustomSamplingContext, Span } from '@sentry/core'; import { logger, spanIsSampled, spanToJSON, uuid4 } from '@sentry/core'; import type { NodeClient } from '@sentry/node'; -import { CpuProfilerBindings } from './cpu_profiler'; +import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler'; import { DEBUG_BUILD } from './debug-build'; import type { RawThreadCpuProfile } from './types'; import { isValidSampleRate } from './utils'; diff --git a/packages/profiling-node/src/utils.ts b/packages/profiling-node/src/utils.ts index baf3370d6ce8..e6ab3803ebdd 100644 --- a/packages/profiling-node/src/utils.ts +++ b/packages/profiling-node/src/utils.ts @@ -1,7 +1,6 @@ import * as os from 'os'; import type { Client, - Context, ContinuousThreadCpuProfile, DebugImage, DsnComponents, @@ -14,6 +13,7 @@ import type { ProfileChunkItem, SdkInfo, ThreadCpuProfile, + TransactionEvent, } from '@sentry/core'; import { createEnvelope, @@ -99,7 +99,7 @@ export function createProfilingEvent(client: Client, profile: RawThreadCpuProfil event_id: event.event_id ?? '', transaction: event.transaction ?? '', start_timestamp: event.start_timestamp ? event.start_timestamp * 1000 : Date.now(), - trace_id: event.contexts?.['trace']?.['trace_id'] ?? '', + trace_id: event.contexts?.trace?.trace_id ?? '', profile_id: profile.profile_id, }); } @@ -352,7 +352,7 @@ export function addProfilesToEnvelope(envelope: Envelope, profiles: Profile[]): * @returns {Event[]} */ export function findProfiledTransactionsFromEnvelope(envelope: Envelope): Event[] { - const events: Event[] = []; + const events: TransactionEvent[] = []; forEachEnvelopeItem(envelope, (item, type) => { if (type !== 'transaction') { @@ -361,18 +361,17 @@ export function findProfiledTransactionsFromEnvelope(envelope: Envelope): Event[ // First item is the type, so we can skip it, everything else is an event for (let j = 1; j < item.length; j++) { - const event = item[j]; + const event = item[j] as TransactionEvent; if (!event) { // Shouldn't happen, but lets be safe continue; } - // @ts-expect-error profile_id is not part of the metadata type - const profile_id = (event.contexts as Context)?.['profile']?.['profile_id']; + const profile_id = event.contexts?.profile?.profile_id; if (event && profile_id) { - events.push(item[j] as Event); + events.push(event); } } }); diff --git a/packages/profiling-node/test/bindings.test.ts b/packages/profiling-node/test/bindings.test.ts deleted file mode 100644 index 27361a87d941..000000000000 --- a/packages/profiling-node/test/bindings.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { platform } from 'os'; -// Contains unit tests for some of the C++ bindings. These functions -// are exported on the private bindings object, so we can test them and -// they should not be used outside of this file. -import { PrivateCpuProfilerBindings } from '../src/cpu_profiler'; - -const cases = [ - ['/Users/jonas/code/node_modules/@scope/package/file.js', '@scope.package:file'], - ['/Users/jonas/code/node_modules/package/dir/file.js', 'package.dir:file'], - ['/Users/jonas/code/node_modules/package/file.js', 'package:file'], - ['/Users/jonas/code/src/file.js', 'Users.jonas.code.src:file'], - - // Preserves non .js extensions - ['/Users/jonas/code/src/file.ts', 'Users.jonas.code.src:file.ts'], - // No extension - ['/Users/jonas/code/src/file', 'Users.jonas.code.src:file'], - // Edge cases that shouldn't happen in practice, but try and handle them so we don't crash - ['/Users/jonas/code/src/file.js', 'Users.jonas.code.src:file'], - ['', ''], -]; - -describe('GetFrameModule', () => { - it.each( - platform() === 'win32' - ? cases.map(([abs_path, expected]) => [abs_path ? `C:${abs_path.replace(/\//g, '\\')}` : '', expected]) - : cases, - )('%s => %s', (abs_path: string, expected: string) => { - expect(PrivateCpuProfilerBindings.getFrameModule(abs_path)).toBe(expected); - }); -}); diff --git a/packages/profiling-node/test/cpu_profiler.test.ts b/packages/profiling-node/test/cpu_profiler.test.ts deleted file mode 100644 index 9240ad636129..000000000000 --- a/packages/profiling-node/test/cpu_profiler.test.ts +++ /dev/null @@ -1,364 +0,0 @@ -import type { ContinuousThreadCpuProfile, ThreadCpuProfile } from '@sentry/core'; -import { CpuProfilerBindings, PrivateCpuProfilerBindings } from '../src/cpu_profiler'; -import type { RawThreadCpuProfile } from '../src/types'; -import { ProfileFormat } from '../src/types'; - -// Required because we test a hypothetical long profile -// and we cannot use advance timers as the c++ relies on -// actual event loop ticks that we cannot advance from jest. -jest.setTimeout(60_000); - -function fail(message: string): never { - throw new Error(message); -} - -const fibonacci = (n: number): number => { - if (n <= 1) { - return n; - } - return fibonacci(n - 1) + fibonacci(n - 2); -}; - -const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); -const profiled = async (name: string, fn: () => void) => { - CpuProfilerBindings.startProfiling(name); - await fn(); - return CpuProfilerBindings.stopProfiling(name, ProfileFormat.THREAD); -}; - -const assertValidSamplesAndStacks = ( - stacks: ThreadCpuProfile['stacks'], - samples: ThreadCpuProfile['samples'] | ContinuousThreadCpuProfile['samples'], -) => { - expect(stacks.length).toBeGreaterThan(0); - expect(samples.length).toBeGreaterThan(0); - expect(stacks.length <= samples.length).toBe(true); - - for (const sample of samples) { - if (sample.stack_id === undefined) { - throw new Error(`Sample ${JSON.stringify(sample)} has not stack id associated`); - } - if (!stacks[sample.stack_id]) { - throw new Error(`Failed to find stack for sample: ${JSON.stringify(sample)}`); - } - expect(stacks[sample.stack_id]).not.toBe(undefined); - } - - for (const stack of stacks) { - expect(stack).not.toBe(undefined); - } -}; - -const isValidMeasurementValue = (v: any) => { - if (isNaN(v)) return false; - return typeof v === 'number' && v > 0; -}; - -const assertValidMeasurements = (measurement: RawThreadCpuProfile['measurements']['memory_footprint'] | undefined) => { - if (!measurement) { - throw new Error('Measurement is undefined'); - } - expect(measurement).not.toBe(undefined); - expect(typeof measurement.unit).toBe('string'); - expect(measurement.unit.length).toBeGreaterThan(0); - - for (let i = 0; i < measurement.values.length; i++) { - expect(measurement?.values?.[i]?.elapsed_since_start_ns).toBeGreaterThan(0); - expect(measurement?.values?.[i]?.value).toBeGreaterThan(0); - } -}; - -describe('Private bindings', () => { - it('does not crash if collect resources is false', async () => { - PrivateCpuProfilerBindings.startProfiling!('profiled-program'); - await wait(100); - expect(() => { - const profile = PrivateCpuProfilerBindings.stopProfiling!('profiled-program', 0, 0, false); - if (!profile) throw new Error('No profile'); - }).not.toThrow(); - }); - - it('throws if invalid format is supplied', async () => { - PrivateCpuProfilerBindings.startProfiling!('profiled-program'); - await wait(100); - expect(() => { - const profile = PrivateCpuProfilerBindings.stopProfiling!('profiled-program', Number.MAX_SAFE_INTEGER, 0, false); - if (!profile) throw new Error('No profile'); - }).toThrow('StopProfiling expects a valid format type as second argument.'); - }); - - it('collects resources', async () => { - PrivateCpuProfilerBindings.startProfiling!('profiled-program'); - await wait(100); - - const profile = PrivateCpuProfilerBindings.stopProfiling!('profiled-program', 0, 0, true); - if (!profile) throw new Error('No profile'); - - expect(profile.resources.length).toBeGreaterThan(0); - - expect(new Set(profile.resources).size).toBe(profile.resources.length); - - for (const resource of profile.resources) { - expect(typeof resource).toBe('string'); - expect(resource).not.toBe(undefined); - } - }); - - it('does not collect resources', async () => { - PrivateCpuProfilerBindings.startProfiling!('profiled-program'); - await wait(100); - - const profile = PrivateCpuProfilerBindings.stopProfiling!('profiled-program', 0, 0, false); - if (!profile) throw new Error('No profile'); - - expect(profile.resources.length).toBe(0); - }); -}); - -describe('Profiler bindings', () => { - it('exports profiler binding methods', () => { - expect(typeof CpuProfilerBindings['startProfiling']).toBe('function'); - expect(typeof CpuProfilerBindings['stopProfiling']).toBe('function'); - }); - - it('profiles a program', async () => { - const profile = await profiled('profiled-program', async () => { - await wait(100); - }); - - if (!profile) fail('Profile is null'); - - assertValidSamplesAndStacks(profile.stacks, profile.samples); - }); - - it('adds thread_id info', async () => { - const profile = await profiled('profiled-program', async () => { - await wait(100); - }); - - if (!profile) fail('Profile is null'); - const samples = profile.samples; - - if (!samples.length) { - throw new Error('No samples'); - } - for (const sample of samples) { - expect(sample.thread_id).toBe('0'); - } - }); - - it('caps stack depth at 128', async () => { - const recurseToDepth = async (depth: number): Promise => { - if (depth === 0) { - // Wait a bit to make sure stack gets sampled here - await wait(1000); - return 0; - } - const v = await recurseToDepth(depth - 1); - return v; - }; - - const profile = await profiled('profiled-program', async () => { - await recurseToDepth(256); - }); - - if (!profile) fail('Profile is null'); - - for (const stack of profile.stacks) { - expect(stack.length).toBeLessThanOrEqual(128); - } - }); - - it('does not record two profiles when titles match', () => { - CpuProfilerBindings.startProfiling('same-title'); - CpuProfilerBindings.startProfiling('same-title'); - - const first = CpuProfilerBindings.stopProfiling('same-title', 0); - const second = CpuProfilerBindings.stopProfiling('same-title', 0); - - expect(first).not.toBe(null); - expect(second).toBe(null); - }); - - it('multiple calls with same title', () => { - CpuProfilerBindings.startProfiling('same-title'); - expect(() => { - CpuProfilerBindings.stopProfiling('same-title', 0); - CpuProfilerBindings.stopProfiling('same-title', 0); - }).not.toThrow(); - }); - - it('does not crash if stopTransaction is called before startTransaction', () => { - expect(CpuProfilerBindings.stopProfiling('does not exist', 0)).toBe(null); - }); - - it('does crash if name is invalid', () => { - expect(() => CpuProfilerBindings.stopProfiling('', 0)).toThrow(); - // @ts-expect-error test invalid input - expect(() => CpuProfilerBindings.stopProfiling(undefined)).toThrow(); - // @ts-expect-error test invalid input - expect(() => CpuProfilerBindings.stopProfiling(null)).toThrow(); - // @ts-expect-error test invalid input - expect(() => CpuProfilerBindings.stopProfiling({})).toThrow(); - }); - - it('does not throw if stopTransaction is called before startTransaction', () => { - expect(CpuProfilerBindings.stopProfiling('does not exist', 0)).toBe(null); - expect(() => CpuProfilerBindings.stopProfiling('does not exist', 0)).not.toThrow(); - }); - - it('compiles with eager logging by default', async () => { - const profile = await profiled('profiled-program', async () => { - await wait(100); - }); - - if (!profile) fail('Profile is null'); - expect(profile.profiler_logging_mode).toBe('eager'); - }); - - it('chunk format type', async () => { - const fn = async () => { - await wait(1000); - fibonacci(36); - await wait(1000); - }; - - CpuProfilerBindings.startProfiling('non nullable stack'); - await fn(); - const profile = CpuProfilerBindings.stopProfiling('non nullable stack', ProfileFormat.CHUNK); - - if (!profile) fail('Profile is null'); - - for (const sample of profile.samples) { - if (!('timestamp' in sample)) { - throw new Error(`Sample ${JSON.stringify(sample)} has no timestamp`); - } - expect(sample.timestamp).toBeDefined(); - // No older than a minute and not in the future. Timestamp is in seconds so convert to ms - // as the constructor expects ms. - expect(new Date((sample.timestamp as number) * 1e3).getTime()).toBeGreaterThan(Date.now() - 60 * 1e3); - expect(new Date((sample.timestamp as number) * 1e3).getTime()).toBeLessThanOrEqual(Date.now()); - } - }); - - it('stacks are not null', async () => { - const profile = await profiled('non nullable stack', async () => { - await wait(1000); - fibonacci(36); - await wait(1000); - }); - - if (!profile) fail('Profile is null'); - assertValidSamplesAndStacks(profile.stacks, profile.samples); - }); - - it('samples at ~99hz', async () => { - CpuProfilerBindings.startProfiling('profile'); - await wait(100); - const profile = CpuProfilerBindings.stopProfiling('profile', 0); - - if (!profile) fail('Profile is null'); - - // Exception for macos and windows - we seem to get way less samples there, but I'm not sure if that's due to poor - // performance of the actions runner, machine or something else. This needs more investigation to determine - // the cause of low sample count. https://github.com/actions/runner-images/issues/1336 seems relevant. - if (process.platform === 'darwin' || process.platform === 'win32') { - if (profile.samples.length < 2) { - fail(`Only ${profile.samples.length} samples obtained on ${process.platform}, expected at least 2`); - } - } else { - if (profile.samples.length < 6) { - fail(`Only ${profile.samples.length} samples obtained on ${process.platform}, expected at least 6`); - } - } - if (profile.samples.length > 15) { - fail(`Too many samples on ${process.platform}, got ${profile.samples.length}`); - } - }); - - it('collects memory footprint', async () => { - CpuProfilerBindings.startProfiling('profile'); - await wait(1000); - const profile = CpuProfilerBindings.stopProfiling('profile', 0); - - const heap_usage = profile?.measurements['memory_footprint']; - if (!heap_usage) { - throw new Error('memory_footprint is null'); - } - expect(heap_usage.values.length).toBeGreaterThan(6); - expect(heap_usage.values.length).toBeLessThanOrEqual(11); - expect(heap_usage.unit).toBe('byte'); - expect(heap_usage.values.every(v => isValidMeasurementValue(v.value))).toBe(true); - assertValidMeasurements(profile.measurements['memory_footprint']); - }); - - it('collects cpu usage', async () => { - CpuProfilerBindings.startProfiling('profile'); - await wait(1000); - const profile = CpuProfilerBindings.stopProfiling('profile', 0); - - const cpu_usage = profile?.measurements['cpu_usage']; - if (!cpu_usage) { - throw new Error('cpu_usage is null'); - } - expect(cpu_usage.values.length).toBeGreaterThan(6); - expect(cpu_usage.values.length).toBeLessThanOrEqual(11); - expect(cpu_usage.values.every(v => isValidMeasurementValue(v.value))).toBe(true); - expect(cpu_usage.unit).toBe('percent'); - assertValidMeasurements(profile.measurements['cpu_usage']); - }); - - it('does not overflow measurement buffer if profile runs longer than 30s', async () => { - CpuProfilerBindings.startProfiling('profile'); - await wait(35000); - const profile = CpuProfilerBindings.stopProfiling('profile', 0); - expect(profile).not.toBe(null); - expect(profile?.measurements?.['cpu_usage']?.values.length).toBeLessThanOrEqual(300); - expect(profile?.measurements?.['memory_footprint']?.values.length).toBeLessThanOrEqual(300); - }); - - // eslint-disable-next-line @sentry-internal/sdk/no-skipped-tests - it.skip('includes deopt reason', async () => { - // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#52-the-object-being-iterated-is-not-a-simple-enumerable - function iterateOverLargeHashTable() { - const table: Record = {}; - for (let i = 0; i < 1e5; i++) { - table[i] = i; - } - // eslint-disable-next-line - for (const _ in table) { - } - } - - const profile = await profiled('profiled-program', async () => { - iterateOverLargeHashTable(); - }); - - // @ts-expect-error deopt reasons are disabled for now as we need to figure out the backend support - const hasDeoptimizedFrame = profile.frames.some(f => f.deopt_reasons?.length > 0); - expect(hasDeoptimizedFrame).toBe(true); - }); - - it('does not crash if the native startProfiling function is not available', async () => { - const original = PrivateCpuProfilerBindings.startProfiling; - PrivateCpuProfilerBindings.startProfiling = undefined; - - expect(() => { - CpuProfilerBindings.startProfiling('profiled-program'); - }).not.toThrow(); - - PrivateCpuProfilerBindings.startProfiling = original; - }); - - it('does not crash if the native stopProfiling function is not available', async () => { - // eslint-disable-next-line @typescript-eslint/unbound-method - const original = PrivateCpuProfilerBindings.stopProfiling; - PrivateCpuProfilerBindings.stopProfiling = undefined; - - expect(() => { - CpuProfilerBindings.stopProfiling('profiled-program', 0); - }).not.toThrow(); - - PrivateCpuProfilerBindings.stopProfiling = original; - }); -}); diff --git a/packages/profiling-node/test/spanProfileUtils.test.ts b/packages/profiling-node/test/spanProfileUtils.test.ts index 9974eb6ebc64..746e25194def 100644 --- a/packages/profiling-node/test/spanProfileUtils.test.ts +++ b/packages/profiling-node/test/spanProfileUtils.test.ts @@ -5,7 +5,7 @@ import { GLOBAL_OBJ, createEnvelope, logger } from '@sentry/core'; import type { ProfilingIntegration } from '@sentry/core'; import type { ProfileChunk, Transport } from '@sentry/core'; import type { NodeClientOptions } from '@sentry/node/build/types/types'; -import { CpuProfilerBindings } from '../src/cpu_profiler'; +import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler'; import { _nodeProfilingIntegration } from '../src/integration'; function makeClientWithHooks(): [Sentry.NodeClient, Transport] { From 20cec074cfcf60e6a8e4a0d74be73db3ac88097d Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 28 Jan 2025 17:12:15 +0100 Subject: [PATCH 2/9] Use new package --- packages/profiling-node/package.json | 2 +- yarn.lock | 62 +++++++++++++++++++--------- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index a9e6ed2f1fbd..6c0bfc33ca8e 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -69,7 +69,7 @@ "dependencies": { "@sentry/core": "9.0.0-alpha.0", "@sentry/node": "9.0.0-alpha.0", - "@sentry-internal/node-cpu-profiler": "1.0.0" + "@sentry-internal/node-cpu-profiler": "^2.0.0" }, "devDependencies": { "@types/node": "^18.19.1", diff --git a/yarn.lock b/yarn.lock index 2e69bdb6d480..3a4ebe39d867 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6535,6 +6535,14 @@ "@angular-devkit/schematics" "14.2.13" jsonc-parser "3.1.0" +"@sentry-internal/node-cpu-profiler@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/node-cpu-profiler/-/node-cpu-profiler-2.0.0.tgz#76a0d363055876b91663769daee2d4b12321ba3b" + integrity sha512-0pZId+HY/AbNs1+CoCi8wogBWTrRv+DYeOgbevhekzMr5HYsA6PRY21NtHBXMbu0WcswFwaveDKR+sOW1EDHAA== + dependencies: + detect-libc "^2.0.2" + node-abi "^3.61.0" + "@sentry-internal/rrdom@2.31.0": version "2.31.0" resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.31.0.tgz#548773964167ec104d3cbb9d7a4b25103c091e06" @@ -7890,7 +7898,12 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8": +"@types/history-4@npm:@types/history@4.7.8": + version "4.7.8" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" + integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== + +"@types/history-5@npm:@types/history@4.7.8": version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -8054,11 +8067,6 @@ dependencies: "@types/unist" "^2" -"@types/node-abi@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/node-abi/-/node-abi-3.0.3.tgz#a8334d75fe45ccd4cdb2a6c1ae82540a7a76828c" - integrity sha512-5oos6sivyXcDEuVC5oX3+wLwfgrGZu4NIOn826PGAjPCHsqp2zSPTGU7H1Tv+GZBOiDUY3nBXY1MdaofSEt4fw== - "@types/node-cron@^3.0.11": version "3.0.11" resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.11.tgz#70b7131f65038ae63cfe841354c8aba363632344" @@ -11856,15 +11864,6 @@ cjs-module-lexer@^1.0.0, cjs-module-lexer@^1.2.2: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== -clang-format@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/clang-format/-/clang-format-1.8.0.tgz#7779df1c5ce1bc8aac1b0b02b4e479191ef21d46" - integrity sha512-pK8gzfu55/lHzIpQ1givIbWfn3eXnU7SfxqIwVgnn5jEM6j4ZJYjpFqFs4iSBPNedzRMmfjYjuQhu657WAXHXw== - dependencies: - async "^3.2.3" - glob "^7.0.0" - resolve "^1.1.6" - class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -22275,7 +22274,7 @@ node-gyp-build@^4.2.2, node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== -node-gyp@^9.0.0, node-gyp@^9.4.1: +node-gyp@^9.0.0: version "9.4.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" integrity sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ== @@ -27421,7 +27420,16 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0", string-width@4.2.3, "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@4.2.3, "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -27524,7 +27532,14 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -30314,7 +30329,16 @@ wrangler@^3.67.1: optionalDependencies: fsevents "~2.3.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@7.0.0, wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 0f074e64c703b65c9dc7375c4df72c28f45f08d5 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 28 Jan 2025 17:23:39 +0100 Subject: [PATCH 3/9] Lint --- packages/profiling-node/src/integration.ts | 2 +- packages/profiling-node/src/spanProfileUtils.ts | 2 +- packages/profiling-node/test/spanProfileUtils.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 4f347103722f..5b455f72974d 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -1,5 +1,6 @@ /* eslint-disable max-lines */ +import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler'; import type { Event, IntegrationFn, Profile, ProfileChunk, ProfilingIntegration, Span } from '@sentry/core'; import { LRUMap, @@ -14,7 +15,6 @@ import { uuid4, } from '@sentry/core'; import type { NodeClient } from '@sentry/node'; -import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler'; import { DEBUG_BUILD } from './debug-build'; import { NODE_MAJOR, NODE_VERSION } from './nodeVersion'; import { MAX_PROFILE_DURATION_MS, maybeProfileSpan, stopSpanProfile } from './spanProfileUtils'; diff --git a/packages/profiling-node/src/spanProfileUtils.ts b/packages/profiling-node/src/spanProfileUtils.ts index 5e4897533938..342075bde890 100644 --- a/packages/profiling-node/src/spanProfileUtils.ts +++ b/packages/profiling-node/src/spanProfileUtils.ts @@ -1,7 +1,7 @@ +import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler'; import type { CustomSamplingContext, Span } from '@sentry/core'; import { logger, spanIsSampled, spanToJSON, uuid4 } from '@sentry/core'; import type { NodeClient } from '@sentry/node'; -import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler'; import { DEBUG_BUILD } from './debug-build'; import type { RawThreadCpuProfile } from './types'; import { isValidSampleRate } from './utils'; diff --git a/packages/profiling-node/test/spanProfileUtils.test.ts b/packages/profiling-node/test/spanProfileUtils.test.ts index 746e25194def..758307d3fa34 100644 --- a/packages/profiling-node/test/spanProfileUtils.test.ts +++ b/packages/profiling-node/test/spanProfileUtils.test.ts @@ -1,11 +1,11 @@ import * as Sentry from '@sentry/node'; +import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler'; import { getMainCarrier } from '@sentry/core'; import { GLOBAL_OBJ, createEnvelope, logger } from '@sentry/core'; import type { ProfilingIntegration } from '@sentry/core'; import type { ProfileChunk, Transport } from '@sentry/core'; import type { NodeClientOptions } from '@sentry/node/build/types/types'; -import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler'; import { _nodeProfilingIntegration } from '../src/integration'; function makeClientWithHooks(): [Sentry.NodeClient, Transport] { From 85bded32788dd3d30d31879cce97d4590375e477 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 28 Jan 2025 17:33:09 +0100 Subject: [PATCH 4/9] Remove unnecessary `files` from `package.json` --- packages/profiling-node/package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index 6c0bfc33ca8e..3909b4f0e767 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -40,12 +40,7 @@ }, "files": [ "/lib", - "/bindings", - "/binding.gyp", "package.json", - "/scripts/binaries.js", - "/scripts/check-build.js", - "/scripts/copy-target.js", "/scripts/prune-profiler-binaries.js" ], "scripts": { From 06ad264d67aaee732d0a56e396ff0e69eadadd12 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 28 Jan 2025 17:41:19 +0100 Subject: [PATCH 5/9] Don't skip profiling tests --- scripts/ci-unit-tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci-unit-tests.ts b/scripts/ci-unit-tests.ts index 85f852052bac..fba65238e94c 100644 --- a/scripts/ci-unit-tests.ts +++ b/scripts/ci-unit-tests.ts @@ -6,7 +6,7 @@ const UNIT_TEST_ENV = process.env.UNIT_TEST_ENV as 'node' | 'browser' | undefine const RUN_AFFECTED = process.argv.includes('--affected'); // These packages are tested separately in CI, so no need to run them here -const DEFAULT_SKIP_PACKAGES = ['@sentry/profiling-node', '@sentry/bun', '@sentry/deno']; +const DEFAULT_SKIP_PACKAGES = ['@sentry/bun', '@sentry/deno']; // All other packages are run for multiple node versions const BROWSER_TEST_PACKAGES = [ From 76bb4d6aad6956573b7ec4bd6ad5a063f6cf2196 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 28 Jan 2025 17:58:09 +0100 Subject: [PATCH 6/9] Remove excluded test package --- scripts/ci-unit-tests.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/ci-unit-tests.ts b/scripts/ci-unit-tests.ts index fba65238e94c..13cbc6957d5c 100644 --- a/scripts/ci-unit-tests.ts +++ b/scripts/ci-unit-tests.ts @@ -17,7 +17,6 @@ const BROWSER_TEST_PACKAGES = [ '@sentry/angular', '@sentry/solid', '@sentry/svelte', - '@sentry/profiling-node', '@sentry-internal/browser-utils', '@sentry-internal/replay', '@sentry-internal/replay-canvas', From 05f18a94a6748ab40e3a552efeb10c001d3275ef Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 28 Jan 2025 18:45:42 +0100 Subject: [PATCH 7/9] Stop overriding the binary path in tests --- packages/profiling-node/package.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index 3909b4f0e767..f6713484381b 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -57,9 +57,9 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:watch": "run-p build:transpile:watch build:types:watch", "build:tarball": "npm pack", - "test:watch": "cross-env SENTRY_PROFILER_BINARY_DIR=build jest --watch", + "test:watch": "jest --watch", "test:bundle": "node test-binaries.esbuild.js", - "test": "cross-env SENTRY_PROFILER_BINARY_DIR=lib jest --config jest.config.js" + "test": "jest --config jest.config.js" }, "dependencies": { "@sentry/core": "9.0.0-alpha.0", @@ -67,8 +67,7 @@ "@sentry-internal/node-cpu-profiler": "^2.0.0" }, "devDependencies": { - "@types/node": "^18.19.1", - "cross-env": "^7.0.3" + "@types/node": "^18.19.1" }, "volta": { "extends": "../../package.json" From dd26eac17ecacf0e6c06ee805cfab9e7751edf49 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 28 Jan 2025 18:47:16 +0100 Subject: [PATCH 8/9] yarn.lock --- yarn.lock | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5568da31fb21..f47e402fd49f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12751,13 +12751,6 @@ cronstrue@^2.50.0: resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.50.0.tgz#eabba0f915f186765258b707b7a3950c663b5573" integrity sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg== -cross-env@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" - integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== - dependencies: - cross-spawn "^7.0.1" - cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -12769,7 +12762,7 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -27758,7 +27751,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2" From a40d1d1ed072ae3ba29ddd00a777de859380d63a Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 28 Jan 2025 19:57:16 +0100 Subject: [PATCH 9/9] Update packages/profiling-node/rollup.npm.config.mjs Co-authored-by: Abhijeet Prasad --- packages/profiling-node/rollup.npm.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/profiling-node/rollup.npm.config.mjs b/packages/profiling-node/rollup.npm.config.mjs index 3eaf9267bb43..05327bc1a29a 100644 --- a/packages/profiling-node/rollup.npm.config.mjs +++ b/packages/profiling-node/rollup.npm.config.mjs @@ -7,7 +7,7 @@ export default makeNPMConfigVariants( dir: 'lib', // set exports to 'named' or 'auto' so that rollup doesn't warn exports: 'named', - // set preserveModules to false because for feedback we actually want + // set preserveModules to false because for profiling we actually want // to bundle everything into one file. preserveModules: process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined