Skip to content

feat: add cipher performance profiling framework - #647

Merged
csxark merged 1 commit into
csxark:mainfrom
Ayush-0918:feat/performance-profiling-framework-604
Jul 31, 2026
Merged

csxark merged 1 commit into
csxark:mainfrom
Ayush-0918:feat/performance-profiling-framework-604

Conversation

@Ayush-0918

@Ayush-0918 Ayush-0918 commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR introduces a reusable performance profiling and regression testing framework for CryptoViz.

Changes

  • Added a modular performance profiling engine.
  • Added baseline storage and comparison utilities.
  • Added regression detection with configurable thresholds.
  • Added structured report generation (Text, JSON, Markdown).
  • Added CI utilities for automated regression detection.
  • Added comprehensive unit tests.

Related Issue

Type of Change

  • New Feature
  • Bug Fix
  • Documentation
  • UI/UX Improvement
  • Refactoring
  • Performance Improvement

Testing

  • npm run typecheck
  • npm test
  • Unit tests added (85 passing)

Checklist

  • Code follows project guidelines.
  • Changes tested locally.
  • Focuses on a single issue.

Summary by CodeRabbit

  • New Features
    • Added cross-platform performance profiling for execution time, memory, throughput, and latency.
    • Added baseline management and comparison to identify improvements, regressions, and stable results.
    • Added CI integration with threshold checks, annotations, trend data, and Prometheus metrics.
    • Added text, JSON, and Markdown performance reports with export support.
  • Tests
    • Added comprehensive coverage for profiling, baselines, CI checks, and report generation.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

@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.

@github-actions

Copy link
Copy Markdown
Contributor

🎉 Thank You for Your Contribution

Hello @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

  • ✅ Linked the related issue
  • ✅ No merge conflicts
  • ✅ Synced with the latest main branch
  • ✅ Build passes successfully
  • ✅ All tests pass

Ensuring these requirements are met helps streamline the review process and enables maintainers to review your contribution more efficiently.

❤️ Support CryptoViz

If you find CryptoViz helpful, consider supporting the project by:

  • ⭐ Starring the repository
  • 🍴 Forking the repository
  • 👤 Following Ark on GitHub
  • 💼 Connecting on LinkedIn
  • 🌐 Visiting the Portfolio

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.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Performance profiling and regression framework

Layer / File(s) Summary
Profiling contracts and measurements
lib/performance/types.ts, lib/performance/profiler.ts, tests/unit/performance/profiler.test.ts
Defines performance data structures and collects execution, memory, throughput, and latency metrics across Node.js and browser environments.
Baseline storage and comparison
lib/performance/baseline.ts, tests/unit/performance/baseline.test.ts
Stores baseline snapshots, compares current profiles with configurable thresholds, classifies regression states, and supports import, export, and summaries.
Performance report generation
lib/performance/reporter.ts, tests/unit/performance/reporter.test.ts
Builds performance reports and formats them as text, JSON, or Markdown. Node.js exports reports to files.
CI regression workflow and exports
lib/performance/ci.ts, lib/performance/index.ts, tests/unit/performance/ci.test.ts
Runs regression checks, validates thresholds and baselines, generates CI annotations and Jenkins markup, exports trend metrics, and exposes the framework entry point.

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
Loading

Suggested labels: ECSoC26, ECSoC26-L3, good-pr

Suggested reviewers: csxark

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add cipher performance profiling framework' accurately summarizes the main change, which is the addition of a comprehensive performance profiling and regression testing framework across multiple modules.
Linked Issues check ✅ Passed The PR fulfills all requirements from issue #604: profiling engine records execution time and memory usage, baseline storage and comparison utilities are implemented, performance reports are generated in multiple formats, and CI integration utilities provide regression checks.
Out of Scope Changes check ✅ Passed All changes directly support the performance profiling framework objective. The minor semicolon addition in cipher.worker.ts is a stylistic fix within scope. No unrelated features or changes are introduced.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Warning

⚠️ This pull request shows signs of AI-generated slop (redundant_comments). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (6)
tests/unit/performance/profiler.test.ts (1)

66-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing direct unit tests for measureMemoryUsage.

measureMemoryUsage in lib/performance/profiler.ts (lines 116-161) has nontrivial platform-specific branches (Chrome performance.memory, Node process.memoryUsage, GC forcing), but no describe('measureMemoryUsage', ...) block exists here, unlike the sibling measureExecutionTime coverage above. The profile() tests only check that metrics.memoryUsage exists, not that the underlying measurement logic behaves correctly for each branch.

Add direct tests mocking process.memoryUsage and performance.memory to 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 win

GC-forcing relies on exception-based feature detection, duplicated in two places.

Both measureMemoryUsage (lines 143-150) and profile (lines 322-329) read global as { gc?: () => void } without first checking whether global exists. In a real browser without a global polyfill, this throws a ReferenceError on every call, which the surrounding try/catch silently 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 win

Loose outputFormat type lets an unvalidated string reach the reporter.

outputFormat is typed as 'text' | 'json' | 'markdown' | string (Line 33). Adding | string to 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 to PerformanceReporter.exportReportToFile without 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 | 🔵 Trivial

Document that profiles without baselines bypass regression enforcement.

batchCompare (called at Line 55) silently drops any profile without a stored baseline, so newly added ciphers produce totalChecked: 0/success: true with no regression signal at all. This matches the test at tests/unit/performance/ci.test.ts Lines 122-128, so it's by design, not a bug. Consider calling out in getRecommendedCIConfig (or wiring validateBaselinesExist into 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 win

Use the existing bulk saveBaselines API instead of looping per profile.

Each call to BaselineManager.saveBaseline inside this loop performs a full loadBaselines + full saveBaselines (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 | 🔵 Trivial

Test performs real filesystem writes with no cleanup and weak assertions.

This test writes to /tmp/test-report.txt on the real filesystem and leaves the artifact behind. Its assertion logic is trivial either way: if exportReportToFile succeeds, nothing is asserted about file content; if it throws, the only check is that an error message is defined, which passes for any error. Mock fs (for example, with vi.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

📥 Commits

Reviewing files that changed from the base of the PR and between cec4647 and 42f4783.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • lib/performance/baseline.ts
  • lib/performance/ci.ts
  • lib/performance/index.ts
  • lib/performance/profiler.ts
  • lib/performance/reporter.ts
  • lib/performance/types.ts
  • lib/workers/cipher.worker.ts
  • tests/unit/performance/baseline.test.ts
  • tests/unit/performance/ci.test.ts
  • tests/unit/performance/profiler.test.ts
  • tests/unit/performance/reporter.test.ts
💤 Files with no reviewable changes (1)
  • lib/workers/cipher.worker.ts

Comment on lines +78 to +81
} catch (error) {
console.error('Failed to load baselines:', error)
return new Map()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment on lines +216 to +283
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment thread lib/performance/ci.ts
Comment on lines +213 to +231
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`,
)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment on lines +42 to +44
cpuCount: process.env?.UV_THREADPOOL_SIZE
? parseInt(process.env.UV_THREADPOOL_SIZE, 10)
: (os.cpus()?.length || 1),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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"
done

Repository: 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:


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.

Suggested change
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.

Comment on lines +267 to +275
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]}`
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment on lines +305 to +376
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,
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Comment on lines +395 to +403
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]}`
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment on lines +408 to +416
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')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

Comment on lines +30 to +63
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
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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))
JS

Repository: 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))
JS

Repository: 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.

Comment on lines +226 to +240
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
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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.

@csxark csxark added ECSoC26 Elite Coders Summer of Code 2026 good-backend and removed needs review labels Jul 31, 2026
@csxark
csxark merged commit 5d45bb4 into csxark:main Jul 31, 2026
3 of 4 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🎉 Pull Request Merged

Hello @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 CryptoViz

If you find CryptoViz helpful, consider supporting the project by:

  • ⭐ Starring the repository
  • 🍴 Forking the repository
  • 👤 Following Ark on GitHub
  • 💼 Connecting on LinkedIn
  • 🌐 Visiting the Portfolio

Your support helps increase the project's visibility and encourages continued development. Thank you for being a part of the CryptoViz community!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ECSoC26-L3 ECSoC26 Elite Coders Summer of Code 2026 good-backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]Add Comprehensive Cipher Performance Profiling and Regression Testing Framework

2 participants