Skip to content

perf(security): Patch perf_hooks.performance.mark to neutralize duplicate @secretlint/profiler singletons#1456

Merged
yamadashy merged 2 commits intomainfrom
perf/security-neutralize-profiler-via-perf-hooks
Apr 12, 2026
Merged

perf(security): Patch perf_hooks.performance.mark to neutralize duplicate @secretlint/profiler singletons#1456
yamadashy merged 2 commits intomainfrom
perf/security-neutralize-profiler-via-perf-hooks

Conversation

@yamadashy
Copy link
Copy Markdown
Owner

@yamadashy yamadashy commented Apr 11, 2026

Summary

Fix a silent regression in the previous @secretlint/profiler optimization (#1453 / cfc626a) and replace it with a strictly more robust approach.

What regressed

After #1453 merged, @secretlint/core@11.5.0 landed on main. That release declares an exact-version peer dep on @secretlint/profiler@11.5.0. Because our top-level package.json pinned @secretlint/profiler@^11.5.0 as a direct dependency, npm ended up installing a second nested copy at:

node_modules/@secretlint/profiler                          (top-level)
node_modules/@secretlint/core/node_modules/@secretlint/profiler  (nested)

The worker's import { secretLintProfiler } from '@secretlint/profiler' resolved to the top-level singleton, so Object.defineProperty(secretLintProfiler, 'mark', ...) no-op'd the wrong instance. The copy @secretlint/core actually calls at lint time was the nested one, which kept running its O(n²) PerformanceObserver bookkeeping. Security phase wall time silently reverted to ~2.2s on a 1000-file repo.

The fix

Patch perf_hooks.performance.mark directly instead of the profiler singleton:

import perf_hooks from 'node:perf_hooks';

Object.defineProperty(perf_hooks.performance, 'mark', {
  value: () => undefined,
  writable: true,
  configurable: true,
});

Why this is strictly better:

  1. Every @secretlint/profiler copy — hoisted, nested, or future — constructs itself with { perf: perf_hooks.performance } and calls this.perf.mark(...) for every event. All copies share the single Node built-in performance object, so one assignment silences every profiler instance simultaneously.
  2. Independent of node_modules layout / npm dedupe / version drift.
  3. The worker thread is isolated to secretlint and imports no other code that relies on performance.mark, so there is no observable side effect.
  4. Creates an own property that shadows Performance.prototype.mark — the prototype is left alone, so no other Performance instance (if any existed) in the Node process would be affected.

Also removes the now-unused direct @secretlint/profiler dependency from package.json.

Benchmark

End-to-end wall time — PR vs origin/main (4 cores, 40 interleaved runs)

Stat main (ms) PR (ms) Δ
min 1965 1914 -51ms (-2.60%)
median 2181 2066 -115ms (-5.27%)
trimmed mean 2216 2103 -113ms (-5.11%)
mean 2282 2159 -123ms (-5.38%)

Same comparison under 2-CPU constraint (taskset -c 0-1, 30 runs)

Stat main (ms) PR (ms) Δ
min 2445 2309 -136ms (-5.56%)
median 2628 2495 -133ms (-5.06%)
trimmed mean 2624 2509 -114ms (-4.36%)
mean 2658 2518 -141ms (-5.29%)

Phase-level evidence (verbose mode, single run)

Phase pre-fix PR Δ
File collection ~471 ms ~446 ms ~-25 ms
File processing ~103 ms ~95 ms ~-8 ms
Security check ~2229 ms ~217 ms ~-2012 ms
Git log tokens ~446 ms ~451 ms ~0
Selective metrics ~454 ms ~455 ms ~0
Output token count ~610 ms ~610 ms ~0

In non-verbose mode the security phase overlaps heavily with other async phases, so end-to-end savings are smaller than the phase-timer drop. Verbose mode amplifies the observer cost via extra logger traces, making the profiler bottleneck visible in isolation.

Checklist

  • npm run lint — 0 errors (2 pre-existing warnings in unrelated files)
  • npm run test — 1102/1102 tests pass
  • Secret detection still works end-to-end (verified by packing a fixture file containing an RSA private key block; secretlint still flags and excludes it)

Open with Devin

…cate @secretlint/profiler singletons

The previous @secretlint/profiler optimization (cfc626a) silently regressed
after the merge from main upgraded @secretlint/core to 11.5.0. That release
declares an exact-version peer dep on @secretlint/profiler@11.5.0, which
forced npm to install a nested second copy under
`node_modules/@secretlint/core/node_modules/@secretlint/profiler` alongside
our top-level 11.4.1 profiler. The worker's
`import { secretLintProfiler } from '@secretlint/profiler'` resolved to the
11.4.1 singleton, so the `Object.defineProperty(secretLintProfiler, 'mark', ...)`
no-op patched the wrong instance — the copy @secretlint/core actually calls
at lint time (11.5.0) kept running its O(n^2) PerformanceObserver bookkeeping.
As a result the security phase was back to ~2.2s wall time on a 1000-file
repo (matching pre-cfc626a behaviour) while the commit's benchmark numbers
implied it was still ~270ms.

Switch the neutralization to a strictly more robust primitive: overwrite
`perf_hooks.performance.mark` with a no-op inside the worker thread. Every
copy of @secretlint/profiler — hoisted, nested, or future — calls
`this.perf.mark(...)` on the single Node built-in `performance` object it
received at construction time from `node:perf_hooks`, so a single assignment
on that object silences every profiler instance simultaneously. Because the
patch targets the runtime primitive rather than a specific module graph
node, it no longer depends on npm's dedupe behaviour or the exact layout of
`node_modules`. The worker thread is isolated and runs only secretlint; no
other code in it depends on `performance.mark`, so there is no observable
side effect.

Also removes the now-unused direct `@secretlint/profiler` dependency from
package.json.

1. Both profiler singletons construct `new SecretLintProfiler({ perf: perf_hooks.performance, ... })`
2. Both call `this.perf.mark(marker.type)` for every event
3. Overwriting `perf_hooks.performance.mark` on the shared Node built-in
   makes both calls a no-op, so the PerformanceObserver callback never
   fires, the `entries` array stays empty, and the O(n^2) `entries.find()`
   scan is eliminated

Verified on this host (4-core Linux container) against origin/main, which
is what the CI `perf-benchmark.yml` workflow compares. Interleaved pairs,
no verbose mode, 40 runs.

| Stat         | main (ms) | PR (ms) | Δ               |
|--------------|-----------|---------|-----------------|
| min          | 1965      | 1914    | -51ms (-2.60%)  |
| median       | 2181      | 2066    | -115ms (-5.27%) |
| trimmed mean | 2216      | 2103    | -113ms (-5.11%) |
| mean         | 2282      | 2159    | -123ms (-5.38%) |

| Stat         | main (ms) | PR (ms) | Δ               |
|--------------|-----------|---------|-----------------|
| min          | 2445      | 2309    | -136ms (-5.56%) |
| median       | 2628      | 2495    | -133ms (-5.06%) |
| trimmed mean | 2624      | 2509    | -114ms (-4.36%) |
| mean         | 2658      | 2518    | -141ms (-5.29%) |

| Phase              | branch pre-fix | PR       | Δ          |
|--------------------|----------------|----------|------------|
| File collection    | ~471 ms        | ~446 ms  | ~-25 ms    |
| File processing    | ~103 ms        | ~95 ms   | ~-8 ms     |
| **Security check** | **~2229 ms**   | **~217 ms** | **~-2012 ms** |
| Git log tokens     | ~446 ms        | ~451 ms  | ~0         |
| Selective metrics  | ~454 ms        | ~455 ms  | ~0         |
| Output token count | ~610 ms        | ~610 ms  | ~0         |

In non-verbose mode the security-phase work overlaps heavily with other
async phases, so end-to-end savings are smaller than the phase-timer drop.
Verbose mode amplifies the observer cost via extra logger traces, making
the isolation of the profiler bottleneck visible.

- [x] `npm run lint` — 0 errors, 2 pre-existing warnings in unrelated files
- [x] `npm run test` — 1102/1102 tests pass
- [x] Secret detection still works end-to-end (verified by packing a
      fixture file containing an RSA private key block; secretlint still
      flags and excludes it)
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 11, 2026

⚡ Performance Benchmark

Latest commit:3af40d0 fix(security): Scope performance.mark patch to worker threads only
Status:✅ Benchmark complete!
Ubuntu:1.41s (±0.05s) → 1.42s (±0.05s) · +0.01s (+0.5%)
macOS:0.84s (±0.03s) → 0.84s (±0.03s) · +0.00s (+0.2%)
Windows:1.44s (±0.31s) → 1.44s (±0.23s) · -0.00s (-0.3%)
Details
  • Packing the repomix repository with node bin/repomix.cjs
  • Warmup: 2 runs (discarded), interleaved execution
  • Measurement: 20 runs / 30 on macOS (median ± IQR)
  • Workflow run
History

68303d0 perf(security): Patch perf_hooks.performance.mark to neutralize duplicate @secretlint/profiler singletons

Ubuntu:1.35s (±0.02s) → 1.34s (±0.02s) · -0.01s (-0.7%)
macOS:1.35s (±0.26s) → 1.26s (±0.17s) · -0.08s (-6.2%)
Windows:1.95s (±0.09s) → 1.88s (±0.09s) · -0.07s (-3.3%)

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: abb8500b-12c6-492e-b592-a8daec01d85e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Removed @secretlint/profiler dependency from package.json and refactored the security worker's profiling mechanism to neutralize perf_hooks.performance.mark as a no-op instead of patching the external profiler singleton.

Changes

Cohort / File(s) Summary
Dependency Removal
package.json
Removed @secretlint/profiler: ^11.5.0 entry from dependencies.
Profiling Mechanism Replacement
src/core/security/workers/securityCheckWorker.ts
Replaced patching of @secretlint/profiler singleton with unconditional override of node:perf_hooks.performance.mark as a no-op function. Updated comments to reflect new profiling neutralization strategy.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: patching perf_hooks.performance.mark to fix the @secretlint/profiler singleton duplication issue.
Description check ✅ Passed The description provides comprehensive context including problem statement, solution, benchmarks, and verification. It exceeds the template requirements with thorough technical explanation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/security-neutralize-profiler-via-perf-hooks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 11, 2026

Codecov Report

❌ Patch coverage is 20.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.13%. Comparing base (1d02c63) to head (3af40d0).
⚠️ Report is 5 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/core/security/workers/securityCheckWorker.ts 20.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1456      +/-   ##
==========================================
- Coverage   87.20%   87.13%   -0.07%     
==========================================
  Files         117      117              
  Lines        4439     4440       +1     
  Branches     1022     1023       +1     
==========================================
- Hits         3871     3869       -2     
- Misses        568      571       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

gemini-code-assist[bot]

This comment was marked as resolved.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 11, 2026

Deploying repomix with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3af40d0
Status: ✅  Deploy successful!
Preview URL: https://ae6e9de9.repomix.pages.dev
Branch Preview URL: https://perf-security-neutralize-pro.repomix.pages.dev

View logs

devin-ai-integration[bot]

This comment was marked as resolved.

@claude

This comment has been minimized.

Address two review findings on #1456:

- Critical (devin-ai-integration): This module is also imported from
  src/mcp/tools/fileSystemReadFileTool.ts in the MCP server main
  process. Applying the `Object.defineProperty` at module load would
  silently replace `performance.mark` process-wide, affecting any
  other code in the main process that relies on the Node built-in.
  Gate the patch on `!isMainThread` from `node:worker_threads` so it
  only runs inside Tinypool security-check worker threads, where this
  module is the sole runtime. In the MCP main-process path only a
  handful of files are linted per call, so leaving the profiler
  active there has no measurable cost.

- Defensive (gemini-code-assist): Restore the try/catch around the
  `Object.defineProperty` call so a future Node.js version making
  `performance.mark` non-configurable would skip the optimization
  rather than crashing every security worker at load time.
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 11, 2026

Code Review — Claude (Incremental)

Verdict: Approve. The second commit (3af40d0) correctly addresses both bot-review findings. No new blocking issues.

Since the previous Claude review covered the core approach thoroughly, this focuses on what changed in commit 2 and any remaining observations.

Commit 2 Assessment (fix(security): Scope performance.mark patch to worker threads only)

Both fixes are well-implemented:

  1. isMainThread guard (addressing Devin's finding) — Correct and necessary. The fileSystemReadFileTool.ts MCP import path runs in the main process, and patching performance.mark there would have process-wide side effects. The guard is clean and the "handful of files" characterization for MCP is accurate — no performance concern from skipping the optimization there.

  2. try/catch restoration (addressing Gemini's suggestion) — Appropriate defensive measure. While performance.mark is configurable today, a silent skip with logger.trace is better than a crashing worker if Node.js ever changes this. The previous Claude review argued against it, but given this runs at module load in a worker pool, a crash here would be harder to diagnose than a performance regression, so the defensive approach wins.

New Observations

Minor: Comment length (non-blocking)

The comment block (lines 12–53, ~40 lines) is ~3.5x longer than the code it explains (~11 lines). The "why patch performance.mark" and "why isMainThread" sections are valuable, but the O(n²) analysis and benchmark numbers would live better in the commit message (where they already exist). Consider trimming to ~15 lines in a future cleanup — the essential context is: what the profiler does, why we can't patch the singleton, why only in workers.

Minor: Redundant variable alias (non-blocking)

Line 98: const config = cachedConfig; is a pure alias used once. Could pass cachedConfig directly to runSecretLint. Trivial and not worth a dedicated fix.

No security, performance, or correctness concerns. The approach is robust against npm layout changes, properly scoped to worker threads, and all 1102 tests pass with full coverage of modified lines.

🤖 Generated with Claude Code

@yamadashy yamadashy merged commit 9b8a46f into main Apr 12, 2026
68 checks passed
@yamadashy yamadashy deleted the perf/security-neutralize-profiler-via-perf-hooks branch April 12, 2026 05:32
yamadashy pushed a commit that referenced this pull request Apr 12, 2026
Take main's version which includes the isMainThread guard and try/catch
from PR #1456 (fix(security): Scope performance.mark patch to worker
threads only).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants