diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 34bb9eb5799f..c58d8606d4de 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 65e97eca7e48..000000000000 Binary files a/packages/profiling-node/bin/darwin-arm64-130/profiling-node.node and /dev/null differ diff --git a/packages/profiling-node/binding.gyp b/packages/profiling-node/binding.gyp deleted file mode 100644 index 1c1aad075e39..000000000000 --- a/packages/profiling-node/binding.gyp +++ /dev/null @@ -1,20 +0,0 @@ -{ - "targets": [ - { - "target_name": "sentry_cpu_profiler", - "sources": [ "bindings/cpu_profiler.cc" ], - # Silence gcc8 deprecation warning https://github.com/nodejs/nan/issues/807#issuecomment-455750192 - "cflags": ["-Wno-cast-function-type"] - }, - ], - 'conditions': [ - [ 'OS=="win"', { - 'defines': [ - # Stop 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..f6713484381b 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -40,53 +40,34 @@ }, "files": [ "/lib", - "/bindings", - "/binding.gyp", "package.json", - "/scripts/binaries.js", - "/scripts/check-build.js", - "/scripts/copy-target.js", "/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", - "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", "@sentry/node": "9.0.0-alpha.0", - "detect-libc": "^2.0.2", - "node-abi": "^3.61.0" + "@sentry-internal/node-cpu-profiler": "^2.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" + "@types/node": "^18.19.1" }, "volta": { "extends": "../../package.json" diff --git a/packages/profiling-node/rollup.npm.config.mjs b/packages/profiling-node/rollup.npm.config.mjs index a9c148306709..05327bc1a29a 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 profiling 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..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 './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..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 './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..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 '../src/cpu_profiler'; import { _nodeProfilingIntegration } from '../src/integration'; function makeClientWithHooks(): [Sentry.NodeClient, Transport] { diff --git a/scripts/ci-unit-tests.ts b/scripts/ci-unit-tests.ts index 85f852052bac..13cbc6957d5c 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 = [ @@ -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', diff --git a/yarn.lock b/yarn.lock index 505d52f463d0..f47e402fd49f 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" @@ -8113,11 +8121,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" @@ -11915,15 +11918,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" @@ -12757,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" @@ -12775,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== @@ -22334,7 +22321,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== @@ -27764,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"