Repository navigation
feat: add cipher performance profiling framework - #647
Conversation
Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@Ayush-0918 is attempting to deploy a commit to the csxark's projects Team on Vercel. A member of the Team first needs to authorize it. |
🎉 Thank You for Your ContributionHello @Ayush-0918, Thank you for submitting a Pull Request to CryptoViz. We appreciate the time and effort you've invested in contributing to the project. Your Pull Request has been received successfully and will be reviewed by the maintainers as soon as possible. 📋 Pull Request Checklist
Ensuring these requirements are met helps streamline the review process and enables maintainers to review your contribution more efficiently. ❤️ Support CryptoVizIf you find CryptoViz helpful, consider supporting the project by:
Your support helps increase the project's visibility and encourages continued development. Thank you for being a part of the CryptoViz community! Thank you once again for contributing to CryptoViz. We appreciate your support and look forward to reviewing your contribution. |
📝 WalkthroughWalkthroughThe PR adds a cross-runtime performance framework. It defines performance types, collects cipher metrics, stores and compares baselines, generates reports, integrates regression checks with CI systems, and adds unit tests for the framework. ChangesPerformance profiling and regression framework
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant CipherOperation
participant PerformanceProfiler
participant BaselineManager
participant PerformanceReporter
participant PerformanceCI
PerformanceProfiler->>CipherOperation: execute measured iterations
CipherOperation-->>PerformanceProfiler: timing and memory measurements
PerformanceProfiler-->>BaselineManager: PerformanceProfile
BaselineManager-->>PerformanceCI: PerformanceComparison
PerformanceCI->>PerformanceReporter: generate report
PerformanceReporter-->>PerformanceCI: formatted report
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment Warning |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (6)
tests/unit/performance/profiler.test.ts (1)
66-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing direct unit tests for
measureMemoryUsage.
measureMemoryUsageinlib/performance/profiler.ts(lines 116-161) has nontrivial platform-specific branches (Chromeperformance.memory, Nodeprocess.memoryUsage, GC forcing), but nodescribe('measureMemoryUsage', ...)block exists here, unlike the siblingmeasureExecutionTimecoverage above. Theprofile()tests only check thatmetrics.memoryUsageexists, not that the underlying measurement logic behaves correctly for each branch.Add direct tests mocking
process.memoryUsageandperformance.memoryto cover both runtime branches and the zero-iterations case.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/performance/profiler.test.ts` around lines 66 - 98, Add a dedicated describe('measureMemoryUsage') test block alongside describe('measureExecutionTime') in the profiler tests. Mock process.memoryUsage and performance.memory to exercise both Node and Chrome memory-measurement branches, assert the returned measurement counts and values, and include a zero-iterations case that returns an empty array.lib/performance/profiler.ts (1)
143-150: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winGC-forcing relies on exception-based feature detection, duplicated in two places.
Both
measureMemoryUsage(lines 143-150) andprofile(lines 322-329) readglobal as { gc?: () => void }without first checking whetherglobalexists. In a real browser without aglobalpolyfill, this throws aReferenceErroron every call, which the surroundingtry/catchsilently swallows. Functionally this is safe, but using exceptions as control flow inside a hot loop (default 100 iterations) adds unnecessary overhead in browser runs, and the logic is duplicated verbatim.Use
typeof global !== 'undefined'instead, which never throws for an undeclared identifier, and extract the check into one shared private helper used by both call sites.♻️ Proposed refactor
+ private static tryForceGc(): void { + if (typeof global !== 'undefined' && typeof (global as { gc?: () => void }).gc === 'function') { + ;(global as { gc: () => void }).gc() + } + }Then replace both occurrences:
- try { - const globalWithGc = global as { gc?: () => void } - if (typeof globalWithGc.gc === 'function') { - globalWithGc.gc() - } - } catch { - // Ignore if gc is not available - } + this.tryForceGc()Also applies to: 322-329
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/performance/profiler.ts` around lines 143 - 150, Extract the duplicated GC detection and invocation from measureMemoryUsage and profile into one shared private helper. In that helper, guard access with typeof global !== 'undefined' before casting and invoking global.gc, preserving the no-op behavior when GC is unavailable; replace both existing try/catch blocks with calls to the helper.lib/performance/ci.ts (3)
29-36: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winLoose
outputFormattype lets an unvalidated string reach the reporter.
outputFormatis typed as'text' | 'json' | 'markdown' | string(Line 33). Adding| stringto a literal union widens it for editor autocomplete purposes and, combined with the unchecked cast at Line 86 (opts.outputFormat as 'text' | 'json' | 'markdown'), lets any string value pass through toPerformanceReporter.exportReportToFilewithout validation. If a caller passes an unsupported format string, this fails silently downstream instead of at the CI options boundary.Narrow the type to the three literals (drop
| string) and validate before casting, defaulting to'text'when the value doesn't match a known format.Also applies to: 84-88
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/performance/ci.ts` around lines 29 - 36, The CIOptions interface has an overly permissive type for outputFormat by including the bare string type alongside the three literal formats, which allows any string value to pass through without validation. Narrow the outputFormat type to only the three literal values (remove the | string union) in the CIOptions interface definition. Then, in the code around line 86 where the cast occurs (before passing to PerformanceReporter.exportReportToFile), add validation logic that checks if the outputFormat value matches one of the known formats ('text', 'json', or 'markdown'), defaulting to 'text' if it does not, before performing the cast to the narrowed literal type.
42-104: 📐 Maintainability & Code Quality | 🔵 TrivialDocument that profiles without baselines bypass regression enforcement.
batchCompare(called at Line 55) silently drops any profile without a stored baseline, so newly added ciphers producetotalChecked: 0/success: truewith no regression signal at all. This matches the test attests/unit/performance/ci.test.tsLines 122-128, so it's by design, not a bug. Consider calling out ingetRecommendedCIConfig(or wiringvalidateBaselinesExistinto the recommended pipeline) that a CI step should also assert baseline coverage, so a newly introduced cipher can't slip through both checks unnoticed.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/performance/ci.ts` around lines 42 - 104, Add documentation in getRecommendedCIConfig (or by integrating validateBaselinesExist into the recommended pipeline) to explain that batchCompare silently excludes profiles without stored baselines from regression checks. This ensures users understand that newly added profiles bypass regression enforcement entirely and will report success with totalChecked: 0 unless baseline coverage is explicitly validated as part of the CI flow.
303-311: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUse the existing bulk
saveBaselinesAPI instead of looping per profile.Each call to
BaselineManager.saveBaselineinside this loop performs a fullloadBaselines+ fullsaveBaselines(which serializes and rewrites the entire baseline file/localStorage entry) for every single profile. For n profiles, that's n full read/write cycles of the whole baseline store instead of one.
BaselineManager.saveBaselines(map)already accepts a batch, so this can be reduced to a single load, in-memory merge, and single save.♻️ Proposed refactor
static createCIBaseline( profiles: PerformanceProfile[], version: string, commitHash: string, ): void { - for (const profile of profiles) { - BaselineManager.saveBaseline(profile, version, commitHash) - } + const baselines = BaselineManager.loadBaselines() + for (const profile of profiles) { + baselines.set(profile.cipherId, BaselineManager.createBaseline(profile, version, commitHash)) + } + BaselineManager.saveBaselines(baselines) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/performance/ci.ts` around lines 303 - 311, Replace the for loop in createCIBaseline that calls BaselineManager.saveBaseline for each profile with a single call to BaselineManager.saveBaselines. Build a map from the profiles array that matches the format saveBaselines expects, passing the version and commitHash parameters appropriately so that all profiles are persisted in a single load-merge-save operation instead of n separate operations.tests/unit/performance/reporter.test.ts (1)
242-257: 📐 Maintainability & Code Quality | 🔵 TrivialTest performs real filesystem writes with no cleanup and weak assertions.
This test writes to
/tmp/test-report.txton the real filesystem and leaves the artifact behind. Its assertion logic is trivial either way: ifexportReportToFilesucceeds, nothing is asserted about file content; if it throws, the only check is that an error message is defined, which passes for any error. Mockfs(for example, withvi.mock('fs', ...)) to test the write path deterministically and remove the real I/O side effect.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/performance/reporter.test.ts` around lines 242 - 257, Replace the real filesystem call in the “should not throw when process exists but fs fails gracefully” test with a mocked fs.writeFile implementation, such as via vi.mock, and assert the expected write behavior deterministically. Remove the try/catch and /tmp/test-report.txt dependency so the test validates PerformanceReporter.exportReportToFile without leaving filesystem artifacts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/performance/baseline.ts`:
- Around line 78-81: Update loadBaselines and saveBaselines to propagate read
and write failures after logging instead of returning an empty Map or normal
success; adjust saveBaseline and compare/batchCompare callers as needed so
PerformanceCI.createCIBaseline and other callers can detect and react to
persistence errors.
- Around line 216-283: Guard the percentage calculations in the
performance-difference flow before division: averageTimeChangePercent in the
execution metrics, averageMemChangePercent in the memory metrics, and
throughputChangePercent in the optional throughput block. Handle zero baseline
values with an appropriate finite fallback that preserves the actual change
direction and avoids passing Infinity or NaN to determineMetricStatus.
In `@lib/performance/ci.ts`:
- Around line 213-231: Update the threshold guards in the performance validation
logic to check each configured value with an explicit !== undefined comparison,
including maxExecutionTimeMs, maxMemoryBytes, and minThroughputBytesPerSecond.
Preserve the existing metric comparisons and throughput presence check while
ensuring thresholds set to 0 are enforced.
In `@lib/performance/profiler.ts`:
- Around line 305-376: Enforce opts.timeoutMs throughout the profile execution
path, including warmup, measureExecutionTime, and measureMemoryUsage iteration
calls, by applying a timeout guard that rejects when an invocation exceeds the
configured limit. Ensure the resulting timeout error propagates from profile()
with clear context and that timers are cleaned up after successful or failed
iterations.
- Around line 267-275: The formatBytes function fails to handle fractional byte
values between 0 and 1, where Math.log produces a negative result, causing index
to become negative and units[index] to resolve as undefined. Update the index
calculation in formatBytes to clamp the result to a minimum of 0 by wrapping the
existing Math.min call with Math.max, ensuring the index stays within the valid
range of 0 to units.length - 1 for all input values including fractional bytes.
- Around line 42-44: Update the cpuCount assignment in the profiler’s
EnvironmentInfo construction to always derive the value from os.cpus(), using
the existing fallback of 1, and remove the UV_THREADPOOL_SIZE override.
In `@lib/performance/reporter.ts`:
- Around line 395-403: Clamp the unit index calculated in formatBytes to a
minimum of 0 before indexing units, while retaining the existing upper bound.
Ensure fractional byte values below 1 render with the B unit and preserve the
current formatting for zero and larger values.
- Around line 408-416: The environment check in exportReportToFile at line 409
is weaker than the one already established in getFallbackEnvironment at line 78,
allowing bundler shims with partial process definitions to bypass the guard and
cause ReferenceError when requiring fs and path modules. Replace the typeof
process check in exportReportToFile with the same process.versions?.node check
pattern used in getFallbackEnvironment to properly distinguish actual Node.js
environments from browser bundler shims.
In `@tests/unit/performance/profiler.test.ts`:
- Around line 30-63: Update the browser test around
PerformanceProfiler.getEnvironmentInfo so it no longer returns when Vitest runs
under Node; temporarily stub process.versions.node to a falsy value before
setting the browser globals, then restore the original process.versions.node
value during cleanup along with window and navigator.
In `@tests/unit/performance/reporter.test.ts`:
- Around line 226-240: Update the browser-environment test around
PerformanceReporter.exportReportToFile so global.process is restored in a
finally block, ensuring cleanup runs whether the toThrow assertion passes or
fails. Preserve the existing assertion and original process reference.
---
Nitpick comments:
In `@lib/performance/ci.ts`:
- Around line 29-36: The CIOptions interface has an overly permissive type for
outputFormat by including the bare string type alongside the three literal
formats, which allows any string value to pass through without validation.
Narrow the outputFormat type to only the three literal values (remove the |
string union) in the CIOptions interface definition. Then, in the code around
line 86 where the cast occurs (before passing to
PerformanceReporter.exportReportToFile), add validation logic that checks if the
outputFormat value matches one of the known formats ('text', 'json', or
'markdown'), defaulting to 'text' if it does not, before performing the cast to
the narrowed literal type.
- Around line 42-104: Add documentation in getRecommendedCIConfig (or by
integrating validateBaselinesExist into the recommended pipeline) to explain
that batchCompare silently excludes profiles without stored baselines from
regression checks. This ensures users understand that newly added profiles
bypass regression enforcement entirely and will report success with
totalChecked: 0 unless baseline coverage is explicitly validated as part of the
CI flow.
- Around line 303-311: Replace the for loop in createCIBaseline that calls
BaselineManager.saveBaseline for each profile with a single call to
BaselineManager.saveBaselines. Build a map from the profiles array that matches
the format saveBaselines expects, passing the version and commitHash parameters
appropriately so that all profiles are persisted in a single load-merge-save
operation instead of n separate operations.
In `@lib/performance/profiler.ts`:
- Around line 143-150: Extract the duplicated GC detection and invocation from
measureMemoryUsage and profile into one shared private helper. In that helper,
guard access with typeof global !== 'undefined' before casting and invoking
global.gc, preserving the no-op behavior when GC is unavailable; replace both
existing try/catch blocks with calls to the helper.
In `@tests/unit/performance/profiler.test.ts`:
- Around line 66-98: Add a dedicated describe('measureMemoryUsage') test block
alongside describe('measureExecutionTime') in the profiler tests. Mock
process.memoryUsage and performance.memory to exercise both Node and Chrome
memory-measurement branches, assert the returned measurement counts and values,
and include a zero-iterations case that returns an empty array.
In `@tests/unit/performance/reporter.test.ts`:
- Around line 242-257: Replace the real filesystem call in the “should not throw
when process exists but fs fails gracefully” test with a mocked fs.writeFile
implementation, such as via vi.mock, and assert the expected write behavior
deterministically. Remove the try/catch and /tmp/test-report.txt dependency so
the test validates PerformanceReporter.exportReportToFile without leaving
filesystem artifacts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 46d2fc40-3622-4947-8d61-2f205b1c4eec
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (11)
lib/performance/baseline.tslib/performance/ci.tslib/performance/index.tslib/performance/profiler.tslib/performance/reporter.tslib/performance/types.tslib/workers/cipher.worker.tstests/unit/performance/baseline.test.tstests/unit/performance/ci.test.tstests/unit/performance/profiler.test.tstests/unit/performance/reporter.test.ts
💤 Files with no reviewable changes (1)
- lib/workers/cipher.worker.ts
| } catch (error) { | ||
| console.error('Failed to load baselines:', error) | ||
| return new Map() | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Save failures are silently discarded; callers get no signal.
If saveBaselines fails (disk full, permission error, storage quota in the browser), Line 108-110 logs the error but returns normally. saveBaseline (Lines 134-143) has no way to detect this, so a caller like PerformanceCI.createCIBaseline will believe a baseline was persisted even though it was not. The same pattern applies to loadBaselines (Lines 78-81): a corrupted or unreadable baseline file silently becomes an empty Map, so compare/batchCompare silently report "no baseline" instead of surfacing that historical data could not be read.
Propagate the failure so callers (and eventually CI) can react, for example by returning a boolean/result or rethrowing after logging.
🛠️ Proposed fix to surface save failures
- static saveBaselines(baselines: Map<string, PerformanceBaseline>): void {
+ static saveBaselines(baselines: Map<string, PerformanceBaseline>): boolean {
try {
const data = JSON.stringify(Array.from(baselines.values()), null, 2)
if (this.isBrowser()) {
localStorage.setItem(STORAGE_KEY, data)
} else {
// Node.js environment
...
fs.writeFileSync(filePath, data, 'utf-8')
}
+ return true
} catch (error) {
console.error('Failed to save baselines:', error)
+ return false
}
}Also applies to: 108-110
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/performance/baseline.ts` around lines 78 - 81, Update loadBaselines and
saveBaselines to propagate read and write failures after logging instead of
returning an empty Map or normal success; adjust saveBaseline and
compare/batchCompare callers as needed so PerformanceCI.createCIBaseline and
other callers can detect and react to persistence errors.
| const current = profile.metrics.executionTime | ||
| const baselineExec = baseline.metrics.executionTime | ||
|
|
||
| const averageTimeChange = current.averageMs - baselineExec.averageMs | ||
| const averageTimeChangePercent = (averageTimeChange / baselineExec.averageMs) * 100 | ||
| const p95TimeChange = current.p95Ms - baselineExec.p95Ms | ||
| const p95TimeChangePercent = (p95TimeChange / baselineExec.p95Ms) * 100 | ||
|
|
||
| const execTimeStatus = this.determineMetricStatus( | ||
| averageTimeChangePercent, | ||
| thresholds.executionTimeRegressionPercent, | ||
| thresholds.minimumChangePercent, | ||
| false, // Lower is better for execution time | ||
| ) | ||
|
|
||
| const currentMem = profile.metrics.memoryUsage | ||
| const baselineMem = baseline.metrics.memoryUsage | ||
|
|
||
| const averageMemChange = currentMem.averageBytes - baselineMem.averageBytes | ||
| const averageMemChangePercent = (averageMemChange / baselineMem.averageBytes) * 100 | ||
| const peakMemChange = currentMem.peakBytes - baselineMem.peakBytes | ||
| const peakMemChangePercent = (peakMemChange / baselineMem.peakBytes) * 100 | ||
|
|
||
| const memStatus = this.determineMetricStatus( | ||
| averageMemChangePercent, | ||
| thresholds.memoryRegressionPercent, | ||
| thresholds.minimumChangePercent, | ||
| false, // Lower is better for memory | ||
| ) | ||
|
|
||
| const differences: PerformanceDifferences = { | ||
| executionTime: { | ||
| averageChange: averageTimeChange, | ||
| averageChangePercent: averageTimeChangePercent, | ||
| p95Change: p95TimeChange, | ||
| p95ChangePercent: p95TimeChangePercent, | ||
| status: execTimeStatus, | ||
| }, | ||
| memoryUsage: { | ||
| averageChange: averageMemChange, | ||
| averageChangePercent: averageMemChangePercent, | ||
| peakChange: peakMemChange, | ||
| peakChangePercent: peakMemChangePercent, | ||
| status: memStatus, | ||
| }, | ||
| } | ||
|
|
||
| // Add throughput comparison if available | ||
| if (profile.metrics.throughput && baseline.metrics.throughput) { | ||
| const currentThroughput = profile.metrics.throughput.bytesPerSecond | ||
| const baselineThroughput = baseline.metrics.throughput.bytesPerSecond | ||
| const throughputChange = currentThroughput - baselineThroughput | ||
| const throughputChangePercent = (throughputChange / baselineThroughput) * 100 | ||
|
|
||
| differences.throughput = { | ||
| change: throughputChange, | ||
| changePercent: throughputChangePercent, | ||
| status: this.determineMetricStatus( | ||
| throughputChangePercent, | ||
| thresholds.throughputRegressionPercent, | ||
| thresholds.minimumChangePercent, | ||
| true, // Higher is better for throughput | ||
| ), | ||
| } | ||
| } | ||
|
|
||
| return differences | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Guard against zero-valued baseline metrics before dividing.
averageTimeChangePercent, averageMemChangePercent, and throughputChangePercent all divide by a baseline metric (baselineExec.averageMs, baselineMem.averageBytes, baselineThroughput) without checking for zero. If a baseline metric is 0 (for example an extremely fast operation rounding to 0ms), the result is Infinity or NaN. determineMetricStatus then compares NaN against thresholds, which is always false, and falls through to the higherIsBetter branch where changePercent > 0 on NaN is false — producing a spurious 'degraded' status regardless of the actual direction of change.
Add a guard for a zero baseline value before computing the percentage change.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/performance/baseline.ts` around lines 216 - 283, Guard the percentage
calculations in the performance-difference flow before division:
averageTimeChangePercent in the execution metrics, averageMemChangePercent in
the memory metrics, and throughputChangePercent in the optional throughput
block. Handle zero baseline values with an appropriate finite fallback that
preserves the actual change direction and avoids passing Infinity or NaN to
determineMetricStatus.
| if (thresholds.maxExecutionTimeMs && profile.metrics.executionTime.averageMs > thresholds.maxExecutionTimeMs) { | ||
| failures.push( | ||
| `Execution time ${profile.metrics.executionTime.averageMs.toFixed(3)}ms exceeds threshold ${thresholds.maxExecutionTimeMs}ms`, | ||
| ) | ||
| } | ||
|
|
||
| if (thresholds.maxMemoryBytes && profile.metrics.memoryUsage.averageBytes > thresholds.maxMemoryBytes) { | ||
| failures.push( | ||
| `Memory usage ${profile.metrics.memoryUsage.averageBytes} bytes exceeds threshold ${thresholds.maxMemoryBytes} bytes`, | ||
| ) | ||
| } | ||
|
|
||
| if (thresholds.minThroughputBytesPerSecond && profile.metrics.throughput) { | ||
| if (profile.metrics.throughput.bytesPerSecond < thresholds.minThroughputBytesPerSecond) { | ||
| failures.push( | ||
| `Throughput ${profile.metrics.throughput.bytesPerSecond.toFixed(0)} bytes/s below threshold ${thresholds.minThroughputBytesPerSecond} bytes/s`, | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Truthy checks skip a threshold value of 0.
if (thresholds.maxExecutionTimeMs && ...), the same pattern for maxMemoryBytes, and minThroughputBytesPerSecond (Lines 213, 219, 225-226) treat 0 as "no threshold configured" because 0 is falsy in JavaScript. A caller who deliberately sets a strict threshold of 0 (or who computes a threshold value that evaluates to 0) has that check silently skipped instead of enforced.
Use an explicit !== undefined check instead of relying on truthiness.
🛠️ Proposed fix
- if (thresholds.maxExecutionTimeMs && profile.metrics.executionTime.averageMs > thresholds.maxExecutionTimeMs) {
+ if (thresholds.maxExecutionTimeMs !== undefined && profile.metrics.executionTime.averageMs > thresholds.maxExecutionTimeMs) {
...
}
- if (thresholds.maxMemoryBytes && profile.metrics.memoryUsage.averageBytes > thresholds.maxMemoryBytes) {
+ if (thresholds.maxMemoryBytes !== undefined && profile.metrics.memoryUsage.averageBytes > thresholds.maxMemoryBytes) {
...
}
- if (thresholds.minThroughputBytesPerSecond && profile.metrics.throughput) {
+ if (thresholds.minThroughputBytesPerSecond !== undefined && profile.metrics.throughput) {
...
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (thresholds.maxExecutionTimeMs && profile.metrics.executionTime.averageMs > thresholds.maxExecutionTimeMs) { | |
| failures.push( | |
| `Execution time ${profile.metrics.executionTime.averageMs.toFixed(3)}ms exceeds threshold ${thresholds.maxExecutionTimeMs}ms`, | |
| ) | |
| } | |
| if (thresholds.maxMemoryBytes && profile.metrics.memoryUsage.averageBytes > thresholds.maxMemoryBytes) { | |
| failures.push( | |
| `Memory usage ${profile.metrics.memoryUsage.averageBytes} bytes exceeds threshold ${thresholds.maxMemoryBytes} bytes`, | |
| ) | |
| } | |
| if (thresholds.minThroughputBytesPerSecond && profile.metrics.throughput) { | |
| if (profile.metrics.throughput.bytesPerSecond < thresholds.minThroughputBytesPerSecond) { | |
| failures.push( | |
| `Throughput ${profile.metrics.throughput.bytesPerSecond.toFixed(0)} bytes/s below threshold ${thresholds.minThroughputBytesPerSecond} bytes/s`, | |
| ) | |
| } | |
| } | |
| if (thresholds.maxExecutionTimeMs !== undefined && profile.metrics.executionTime.averageMs > thresholds.maxExecutionTimeMs) { | |
| failures.push( | |
| `Execution time ${profile.metrics.executionTime.averageMs.toFixed(3)}ms exceeds threshold ${thresholds.maxExecutionTimeMs}ms`, | |
| ) | |
| } | |
| if (thresholds.maxMemoryBytes !== undefined && profile.metrics.memoryUsage.averageBytes > thresholds.maxMemoryBytes) { | |
| failures.push( | |
| `Memory usage ${profile.metrics.memoryUsage.averageBytes} bytes exceeds threshold ${thresholds.maxMemoryBytes} bytes`, | |
| ) | |
| } | |
| if (thresholds.minThroughputBytesPerSecond !== undefined && profile.metrics.throughput) { | |
| if (profile.metrics.throughput.bytesPerSecond < thresholds.minThroughputBytesPerSecond) { | |
| failures.push( | |
| `Throughput ${profile.metrics.throughput.bytesPerSecond.toFixed(0)} bytes/s below threshold ${thresholds.minThroughputBytesPerSecond} bytes/s`, | |
| ) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/performance/ci.ts` around lines 213 - 231, Update the threshold guards in
the performance validation logic to check each configured value with an explicit
!== undefined comparison, including maxExecutionTimeMs, maxMemoryBytes, and
minThroughputBytesPerSecond. Preserve the existing metric comparisons and
throughput presence check while ensuring thresholds set to 0 are enforced.
| cpuCount: process.env?.UV_THREADPOOL_SIZE | ||
| ? parseInt(process.env.UV_THREADPOOL_SIZE, 10) | ||
| : (os.cpus()?.length || 1), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' 'Files matching profiler.ts:'
fd -a 'profiler\.ts$' . || true
printf '\n%s\n' 'Relevant code around cpuCount:'
if [ -f lib/performance/profiler.ts ]; then
sed -n '1,90p' lib/performance/profiler.ts | cat -n
fi
printf '\n%s\n' 'Search for cpuCount and EnvironmentInfo usages:'
rg -n "cpuCount|EnvironmentInfo|uv_threadpool|UV_THREADPOOL_SIZE" . || true
printf '\n%s\n' 'Package/runtime hints:'
for f in package.json nuxt.config.ts nuxt.config.js app.config.ts app.vue; do
[ -f "$f" ] && printf '\n--- %s ---\n' "$f" && sed -n '1,220p' "$f"
doneRepository: csxark/CryptoViz
Length of output: 6821
🌐 Web query:
Node.js UV_THREADPOOL_SIZE environment variable meaning libuv thread pool size documentation
💡 Result:
UV_THREADPOOL_SIZE is an environment variable used to configure the number of threads in the libuv thread pool [1][2]. Libuv, the multi-platform C library that provides the asynchronous I/O capabilities for Node.js, uses this pool to offload tasks that cannot be handled by the operating system's native asynchronous I/O APIs [3]. Key details about UV_THREADPOOL_SIZE include: Default and Limits: The default size of the thread pool is 4 [1][2]. It can be increased by setting the UV_THREADPOOL_SIZE environment variable at startup, up to a maximum of 1024 [1][2]. Usage and Scope: The thread pool is global and shared across all event loops [1][2]. It is used internally for: - All file system operations (excluding file watchers and explicitly synchronous methods) [1][3]. - DNS functions such as getaddrinfo and getnameinfo [1]. - Certain crypto functions, including randomBytes when used without a callback [3]. Important Initialization Note: The thread pool is initialized as part of the Node.js runtime startup process [4][5]. Consequently, attempting to set UV_THREADPOOL_SIZE from within your JavaScript code using process.env.UV_THREADPOOL_SIZE is not guaranteed to work, as the thread pool may have already been created before your code executes [4][5]. To ensure the custom size is applied, the variable must be set in the environment before starting the Node.js process [1][4]. Performance Implications: Because the thread pool is fixed in size, I/O-intensive tasks can cause performance bottlenecks if multiple operations compete for the available threads [3]. Increasing this value can improve throughput for I/O-heavy applications but also increases memory consumption [1][2].
Citations:
- 1: https://docs.libuv.org/en/stable/threadpool.html
- 2: https://docs.libuv.org/en/latest/threadpool.html
- 3: nodejs/node@449549b
- 4: nodejs/node@955690e6cf
- 5: doc: clarify UV_THREADPOOL_SIZE env var usage nodejs/node#55832
Derive cpuCount from os.cpus(), not UV_THREADPOOL_SIZE.
UV_THREADPOOL_SIZE configures libuv's async I/O thread pool, not the number of CPU cores. If this variable is set for I/O tuning, EnvironmentInfo.cpuCount can report a value unrelated to the actual hardware while also affecting baseline comparisons.
🐛 Proposed fix
- cpuCount: process.env?.UV_THREADPOOL_SIZE
- ? parseInt(process.env.UV_THREADPOOL_SIZE, 10)
- : (os.cpus()?.length || 1),
+ cpuCount: os.cpus()?.length || 1,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| cpuCount: process.env?.UV_THREADPOOL_SIZE | |
| ? parseInt(process.env.UV_THREADPOOL_SIZE, 10) | |
| : (os.cpus()?.length || 1), | |
| cpuCount: os.cpus()?.length || 1, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/performance/profiler.ts` around lines 42 - 44, Update the cpuCount
assignment in the profiler’s EnvironmentInfo construction to always derive the
value from os.cpus(), using the existing fallback of 1, and remove the
UV_THREADPOOL_SIZE override.
| static formatBytes(bytes: number): string { | ||
| if (bytes === 0) return '0 B' | ||
| const units = ['B', 'KB', 'MB', 'GB', 'TB'] | ||
| const index = Math.min( | ||
| Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)), | ||
| units.length - 1, | ||
| ) | ||
| return `${(bytes / Math.pow(1024, index)).toFixed(index === 0 ? 0 : 2)} ${units[index]}` | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
formatBytes can produce undefined unit for fractional byte values.
For 0 < bytes < 1 (possible for bytesPerSecond on very small inputSize/large duration combinations, or fractional average deltas), Math.log(bytes) is negative, so index becomes negative and units[index] resolves to undefined, producing a string like "512.00 undefined" instead of a valid unit label.
🐛 Proposed fix
const index = Math.min(
- Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)),
+ Math.max(Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)), 0),
units.length - 1,
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| static formatBytes(bytes: number): string { | |
| if (bytes === 0) return '0 B' | |
| const units = ['B', 'KB', 'MB', 'GB', 'TB'] | |
| const index = Math.min( | |
| Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)), | |
| units.length - 1, | |
| ) | |
| return `${(bytes / Math.pow(1024, index)).toFixed(index === 0 ? 0 : 2)} ${units[index]}` | |
| } | |
| static formatBytes(bytes: number): string { | |
| if (bytes === 0) return '0 B' | |
| const units = ['B', 'KB', 'MB', 'GB', 'TB'] | |
| const index = Math.min( | |
| Math.max(Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)), 0), | |
| units.length - 1, | |
| ) | |
| return `${(bytes / Math.pow(1024, index)).toFixed(index === 0 ? 0 : 2)} ${units[index]}` | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/performance/profiler.ts` around lines 267 - 275, The formatBytes function
fails to handle fractional byte values between 0 and 1, where Math.log produces
a negative result, causing index to become negative and units[index] to resolve
as undefined. Update the index calculation in formatBytes to clamp the result to
a minimum of 0 by wrapping the existing Math.min call with Math.max, ensuring
the index stays within the valid range of 0 to units.length - 1 for all input
values including fractional bytes.
| static async profile( | ||
| cipherId: string, | ||
| cipherName: string, | ||
| category: 'classical' | 'symmetric' | 'asymmetric' | 'hash', | ||
| operation: 'encrypt' | 'decrypt' | 'hash', | ||
| fn: () => void | Promise<void>, | ||
| inputSize: number, | ||
| options: Partial<ProfilingOptions> = {}, | ||
| ): Promise<PerformanceProfile> { | ||
| const opts = { ...this.DEFAULT_OPTIONS, ...options } | ||
|
|
||
| // Warmup phase | ||
| for (let i = 0; i < opts.warmupIterations; i++) { | ||
| await fn() | ||
| } | ||
|
|
||
| // Force garbage collection if available | ||
| try { | ||
| const globalWithGc = global as { gc?: () => void } | ||
| if (typeof globalWithGc.gc === 'function') { | ||
| globalWithGc.gc() | ||
| } | ||
| } catch { | ||
| // Ignore if gc is not available | ||
| } | ||
|
|
||
| const environment = this.getEnvironmentInfo() | ||
|
|
||
| // Collect baseline memory | ||
| const baselineMemory = this.getBaselineMemory() | ||
|
|
||
| // Measure execution time | ||
| const timeMeasurements = await this.measureExecutionTime(fn, opts.iterations) | ||
|
|
||
| // Measure memory usage if requested | ||
| let memoryMeasurements: number[] = [] | ||
| if (opts.collectMemory) { | ||
| memoryMeasurements = await this.measureMemoryUsage(fn, opts.iterations) | ||
| } | ||
|
|
||
| // Calculate metrics | ||
| const executionTimeMetrics = this.calculateExecutionTimeMetrics(timeMeasurements) | ||
| const memoryMetrics = this.calculateMemoryMetrics(memoryMeasurements, baselineMemory) | ||
|
|
||
| let throughputMetrics: ThroughputMetrics | undefined | ||
| if (opts.collectThroughput) { | ||
| throughputMetrics = this.calculateThroughputMetrics( | ||
| inputSize * opts.iterations, | ||
| executionTimeMetrics.totalMs, | ||
| opts.iterations, | ||
| ) | ||
| } | ||
|
|
||
| let latencyMetrics: LatencyMetrics | undefined | ||
| if (opts.collectLatency) { | ||
| latencyMetrics = this.calculateLatencyMetrics(executionTimeMetrics) | ||
| } | ||
|
|
||
| const metrics = this.mergeMetrics(executionTimeMetrics, memoryMetrics, throughputMetrics, latencyMetrics) | ||
|
|
||
| return { | ||
| cipherId, | ||
| cipherName, | ||
| category, | ||
| timestamp: new Date(), | ||
| environment, | ||
| metrics, | ||
| operation, | ||
| inputSize, | ||
| iterations: opts.iterations, | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
timeoutMs is defined but never enforced.
ProfilingOptions.timeoutMs and DEFAULT_OPTIONS.timeoutMs (line 25) exist, but no code path in profile, measureExecutionTime, or measureMemoryUsage uses it to bound the profiling run. If the profiled fn() hangs or runs unexpectedly long, profile() has no guard against blocking indefinitely. This works against the PR's stated goal of providing "regression checks suitable for CI integration," where an unbounded hang could stall a CI job.
Wrap the iteration loop (or each iteration) with a timeout guard, for example using Promise.race against a timer that rejects after opts.timeoutMs, and propagate a clear timeout error from profile().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/performance/profiler.ts` around lines 305 - 376, Enforce opts.timeoutMs
throughout the profile execution path, including warmup, measureExecutionTime,
and measureMemoryUsage iteration calls, by applying a timeout guard that rejects
when an invocation exceeds the configured limit. Ensure the resulting timeout
error propagates from profile() with clear context and that timers are cleaned
up after successful or failed iterations.
| private static formatBytes(bytes: number): string { | ||
| if (bytes === 0) return '0 B' | ||
| const units = ['B', 'KB', 'MB', 'GB', 'TB'] | ||
| const index = Math.min( | ||
| Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)), | ||
| units.length - 1, | ||
| ) | ||
| return `${(bytes / Math.pow(1024, index)).toFixed(index === 0 ? 0 : 2)} ${units[index]}` | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
formatBytes produces undefined unit for fractional byte values below 1.
Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)) returns a negative index when 0 < |bytes| < 1. For example, bytes = 0.5 yields index = -1, and units[-1] is undefined, producing output like "512.00 undefined" instead of "0.50 B".
This is reachable: formatBytes renders comparison.differences.memoryUsage.averageChange (Line 224, 374) and throughput.change (Line 232, 381), both of which are computed differences that can legitimately be fractional values under 1 byte. Clamp the index to a minimum of 0.
🐛 Proposed fix
private static formatBytes(bytes: number): string {
if (bytes === 0) return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
const index = Math.min(
- Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)),
+ Math.max(Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)), 0),
units.length - 1,
)
return `${(bytes / Math.pow(1024, index)).toFixed(index === 0 ? 0 : 2)} ${units[index]}`
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private static formatBytes(bytes: number): string { | |
| if (bytes === 0) return '0 B' | |
| const units = ['B', 'KB', 'MB', 'GB', 'TB'] | |
| const index = Math.min( | |
| Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)), | |
| units.length - 1, | |
| ) | |
| return `${(bytes / Math.pow(1024, index)).toFixed(index === 0 ? 0 : 2)} ${units[index]}` | |
| } | |
| private static formatBytes(bytes: number): string { | |
| if (bytes === 0) return '0 B' | |
| const units = ['B', 'KB', 'MB', 'GB', 'TB'] | |
| const index = Math.min( | |
| Math.max(Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)), 0), | |
| units.length - 1, | |
| ) | |
| return `${(bytes / Math.pow(1024, index)).toFixed(index === 0 ? 0 : 2)} ${units[index]}` | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/performance/reporter.ts` around lines 395 - 403, Clamp the unit index
calculated in formatBytes to a minimum of 0 before indexing units, while
retaining the existing upper bound. Ensure fractional byte values below 1 render
with the B unit and preserve the current formatting for zero and larger values.
| static exportReportToFile(report: PerformanceReport, filePath: string, format: 'text' | 'json' | 'markdown' = 'text'): void { | ||
| if (typeof process === 'undefined') { | ||
| throw new Error('File export is only available in Node.js environment') | ||
| } | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| const fs = require('fs') | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| const path = require('path') |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Node-environment check is weaker than the one used in getFallbackEnvironment.
exportReportToFile guards with typeof process === 'undefined'. getFallbackEnvironment (Line 78) uses the stronger process.versions?.node check. Many bundlers (webpack/Next.js) define a partial process shim on the client (for process.env.NODE_ENV) without process.versions.node. In that case, typeof process === 'undefined' is false, execution proceeds past Line 409, and require('fs')/require('path') throw an uncaught ReferenceError in the browser instead of the intended, friendly 'File export is only available in Node.js environment' message.
Align the check with the one already used in getFallbackEnvironment.
🐛 Proposed fix
- if (typeof process === 'undefined') {
+ if (typeof process === 'undefined' || !process.versions?.node) {
throw new Error('File export is only available in Node.js environment')
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| static exportReportToFile(report: PerformanceReport, filePath: string, format: 'text' | 'json' | 'markdown' = 'text'): void { | |
| if (typeof process === 'undefined') { | |
| throw new Error('File export is only available in Node.js environment') | |
| } | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const fs = require('fs') | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const path = require('path') | |
| static exportReportToFile(report: PerformanceReport, filePath: string, format: 'text' | 'json' | 'markdown' = 'text'): void { | |
| if (typeof process === 'undefined' || !process.versions?.node) { | |
| throw new Error('File export is only available in Node.js environment') | |
| } | |
| // eslint-disable-next-line `@typescript-eslint/no-require-imports` | |
| const fs = require('fs') | |
| // eslint-disable-next-line `@typescript-eslint/no-require-imports` | |
| const path = require('path') |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/performance/reporter.ts` around lines 408 - 416, The environment check in
exportReportToFile at line 409 is weaker than the one already established in
getFallbackEnvironment at line 78, allowing bundler shims with partial process
definitions to bypass the guard and cause ReferenceError when requiring fs and
path modules. Replace the typeof process check in exportReportToFile with the
same process.versions?.node check pattern used in getFallbackEnvironment to
properly distinguish actual Node.js environments from browser bundler shims.
| it('should return browser environment info when in browser', () => { | ||
| // Skip this test in Node.js environment | ||
| if (typeof process !== 'undefined' && process.versions?.node) { | ||
| return | ||
| } | ||
|
|
||
| // Mock browser environment | ||
| const originalWindow = global.window | ||
| const originalNavigator = global.navigator | ||
|
|
||
| // @ts-expect-error - Mocking browser environment | ||
| delete global.window | ||
| // @ts-expect-error - Mocking browser environment | ||
| delete global.navigator | ||
|
|
||
| // @ts-expect-error - Mocking browser environment | ||
| global.window = {} | ||
| // @ts-expect-error - Mocking browser environment | ||
| global.navigator = { | ||
| platform: 'MacIntel', | ||
| hardwareConcurrency: 8, | ||
| userAgent: 'Mozilla/5.0', | ||
| } | ||
|
|
||
| const env = PerformanceProfiler.getEnvironmentInfo() | ||
|
|
||
| expect(env.runtime).toBe('browser') | ||
| expect(env.platform).toBe('MacIntel') | ||
| expect(env.cpuCount).toBe(8) | ||
|
|
||
| // Restore | ||
| global.window = originalWindow | ||
| global.navigator = originalNavigator | ||
| }) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Locate test file"
fd -a 'profiler.test.ts' . || true
echo "Inspect relevant test file"
if [ -f tests/unit/performance/profiler.test.ts ]; then
wc -l tests/unit/performance/profiler.test.ts
sed -n '1,120p' tests/unit/performance/profiler.test.ts | cat -n
fi
echo "Inspect profiler implementation around getEnvironmentInfo"
if [ -f src/performance/profiler.ts ] || [ -f src/profiler.ts ]; then
PROFILE=$(fd -a 'profiler.ts' . | grep -E 'performance/profiler.ts|src/profiler.ts$' | head -n1 || true)
if [ -n "$PROFILE" ]; then
echo "$PROFILE"
wc -l "$PROFILE"
ast-grep outline "$PROFILE" --match getEnvironmentInfo --view expanded || true
rg -n -C 8 "getEnvironmentInfo|measureMemoryUsage|getMemoryUsage" "$PROFILE"
fi
fi
echo "Check Vitest config test environment"
rg -n "testEnvironment|jsdom|environment|environment:" -g 'vitest.config.*' -g 'vite.config.*' -g 'package.json' | head -n 100 || true
echo "Check package scripts/test command"
if [ -f package.json ]; then
node -e "const p=require('./package.json'); console.log(JSON.stringify({scripts:p.scripts, devDependencies:p.devDependencies, dependencies:p.dependencies}, null, 2))"
fi
echo "Node process runtime check"
node - <<'JS'
console.log(typeof process, process.globalThis !== undefined, process.versions?.node, Object.keys(process.versions || {}))
process.versions = {}
console.log('after clearing:', typeof process.versions.node, Object.keys(process.versions))
JSRepository: csxark/CryptoViz
Length of output: 5025
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Find performance profiler files"
fd -a 'profiler.ts|types.ts' . | sed -n '1,120p'
echo "Locate PerformanceProfiler export"
rg -n "class PerformanceProfiler|getEnvironmentInfo" . --glob '!node_modules' --glob '!dist' --glob '!build' | sed -n '1,120p'
echo "Check vitest/vite config files if any"
git ls-files | rg '(^|/)(vitest|vite)\.config\.[^/]+$|package\.json$' | sed -n '1,80p'
echo "Search testEnvironment in tracked files"
rg -n "testEnvironment|environment:|jsdom|node" -g 'vitest.config.*' -g 'vite.config.*' -g 'package.json' . | sed -n '1,160p' || true
echo "Node runtime facts"
node - <<'JS'
console.log(JSON.stringify({
processDefined: typeof process !== 'undefined',
versionNodeValues: process.versions.node,
versionsNodeTruthy: Boolean(process.versions?.node),
}, null, 2))
JSRepository: csxark/CryptoViz
Length of output: 1398
Make the browser test actually execute.
This test returns before setting window/navigator because Vitest runs in Node and process.versions?.node is truthy even with the jsdom environment. Stub process.versions.node to falsy for this case instead of skipping the assertions, and restore it in cleanup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/unit/performance/profiler.test.ts` around lines 30 - 63, Update the
browser test around PerformanceProfiler.getEnvironmentInfo so it no longer
returns when Vitest runs under Node; temporarily stub process.versions.node to a
falsy value before setting the browser globals, then restore the original
process.versions.node value during cleanup along with window and navigator.
| it('should throw error in browser environment', () => { | ||
| const report = PerformanceReporter.generateReport([mockProfile], [mockComparison]) | ||
|
|
||
| // Mock browser environment | ||
| const originalProcess = global.process | ||
| // @ts-expect-error - Mocking browser environment | ||
| delete global.process | ||
|
|
||
| expect(() => { | ||
| PerformanceReporter.exportReportToFile(report, '/tmp/report.txt', 'text') | ||
| }).toThrow('File export is only available in Node.js environment') | ||
|
|
||
| // Restore | ||
| global.process = originalProcess | ||
| }) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Restore global.process in a finally block to prevent test pollution.
If the expect(...).toThrow(...) assertion fails, global.process = originalProcess (Line 239) never runs, since the assertion failure throws before reaching it. global.process stays deleted for the remainder of the test file, which can cause unrelated cascading failures in subsequent tests (for example, the following exportReportToFile test relies on typeof process !== 'undefined').
🐛 Proposed fix
const originalProcess = global.process
// `@ts-expect-error` - Mocking browser environment
delete global.process
- expect(() => {
- PerformanceReporter.exportReportToFile(report, '/tmp/report.txt', 'text')
- }).toThrow('File export is only available in Node.js environment')
-
- // Restore
- global.process = originalProcess
+ try {
+ expect(() => {
+ PerformanceReporter.exportReportToFile(report, '/tmp/report.txt', 'text')
+ }).toThrow('File export is only available in Node.js environment')
+ } finally {
+ // Restore
+ global.process = originalProcess
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('should throw error in browser environment', () => { | |
| const report = PerformanceReporter.generateReport([mockProfile], [mockComparison]) | |
| // Mock browser environment | |
| const originalProcess = global.process | |
| // @ts-expect-error - Mocking browser environment | |
| delete global.process | |
| expect(() => { | |
| PerformanceReporter.exportReportToFile(report, '/tmp/report.txt', 'text') | |
| }).toThrow('File export is only available in Node.js environment') | |
| // Restore | |
| global.process = originalProcess | |
| }) | |
| it('should throw error in browser environment', () => { | |
| const report = PerformanceReporter.generateReport([mockProfile], [mockComparison]) | |
| // Mock browser environment | |
| const originalProcess = global.process | |
| // `@ts-expect-error` - Mocking browser environment | |
| delete global.process | |
| try { | |
| expect(() => { | |
| PerformanceReporter.exportReportToFile(report, '/tmp/report.txt', 'text') | |
| }).toThrow('File export is only available in Node.js environment') | |
| } finally { | |
| // Restore | |
| global.process = originalProcess | |
| } | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/unit/performance/reporter.test.ts` around lines 226 - 240, Update the
browser-environment test around PerformanceReporter.exportReportToFile so
global.process is restored in a finally block, ensuring cleanup runs whether the
toThrow assertion passes or fails. Preserve the existing assertion and original
process reference.
🎉 Pull Request MergedHello @Ayush-0918, Thank you for your contribution to CryptoViz. Your Pull Request has been reviewed and successfully merged into the project. We sincerely appreciate the time and effort you invested in improving the project. Contributions like yours help make CryptoViz better for the entire community. We look forward to your future contributions and hope to collaborate with you again. ❤️ Support CryptoVizIf you find CryptoViz helpful, consider supporting the project by:
Your support helps increase the project's visibility and encourages continued development. Thank you for being a part of the CryptoViz community! |
Summary
This PR introduces a reusable performance profiling and regression testing framework for CryptoViz.
Changes
Related Issue
Type of Change
Testing
Checklist
Summary by CodeRabbit