marjoram

Performance Testing Guidelines

Overview

Performance tests can be notoriously brittle and unreliable across different hardware, operating systems, and CI environments. This document outlines the strategies implemented in Marjoram to create robust performance tests.

Challenges with Performance Testing

Common Issues

Solutions Implemented

1. Baseline Comparison Instead of Absolute Timing

Before (Brittle):

const start = performance.now();
performOperation();
const duration = performance.now() - start;
expect(duration).toBeLessThan(100); // Fixed threshold - fails on slow systems

After (Robust):

const testOperation = () => {
  const start = performance.now();
  performOperation();
  return performance.now() - start;
};

const baselineOperation = () => {
  const start = performance.now();
  simpleEquivalentOperation();
  return performance.now() - start;
};

const ratio = testOperation() / baselineOperation();
expect(ratio).toBeLessThan(10); // Relative performance - more reliable

2. Multiple Iterations and Averaging

const performanceBenchmark = (testFn, baselineFn, iterations = 3) => {
  const testTimes = [];
  const baselineTimes = [];
  
  for (let i = 0; i < iterations; i++) {
    testTimes.push(testFn());
    baselineTimes.push(baselineFn());
  }
  
  const avgTest = testTimes.reduce((a, b) => a + b) / iterations;
  const avgBaseline = baselineTimes.reduce((a, b) => a + b) / iterations;
  
  return avgTest / avgBaseline;
};

3. Environment Detection and Adaptive Thresholds

const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';

const PERF_CONFIG = {
  skipTimingTests: isCI || isSlow,
  maxRatio: isCI ? 20 : 10, // More lenient in CI
  iterations: isCI ? 1 : 3   // Fewer iterations in CI
};

4. Conditional Test Skipping

// Skip timing-sensitive tests in unreliable environments
const testFn = PERF_CONFIG.skipTimingTests ? test.skip : test;

testFn("should handle rapid updates efficiently", () => {
  // Test only runs in reliable environments
});

5. Debugging and Observability

// Log performance metrics for debugging CI issues
console.log(`Test=${testTime.toFixed(2)}ms, Baseline=${baselineTime.toFixed(2)}ms, Ratio=${ratio.toFixed(2)}`);

Test Categories

Always Run (High Reliability)

Conditionally Run (Timing Sensitive)

Best Practices

✅ Do

❌ Don’t

Configuration

The performance test configuration can be customized via environment variables:

# Skip all timing-sensitive tests
CI=true npm test

# Force run all tests (use with caution in CI)
FORCE_PERF_TESTS=true npm test

# Adjust performance ratio tolerance
PERF_RATIO_TOLERANCE=15 npm test

Monitoring Performance Over Time

For continuous performance monitoring, consider:

  1. Separate performance test suite - Run as a separate job that can fail without blocking releases
  2. Performance budgets - Track performance over time with tools like bundlesize
  3. Benchmark tracking - Store performance metrics in a time series database
  4. Regression detection - Alert when performance degrades significantly

Example Implementation

See __tests__/edge-cases/performance.test.ts for a complete implementation of these patterns.

CI Integration

In GitHub Actions, performance tests are automatically adjusted:

- name: Run Tests
  run: npm test
  env:
    CI: true  # Automatically set by GitHub Actions

The tests will automatically: