Performance Monitoring #222
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Performance Monitoring | |
| on: | |
| schedule: | |
| # Run performance tests daily at 2 AM UTC | |
| - cron: '0 2 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| test-type: | |
| description: 'Type of performance test to run' | |
| required: true | |
| type: choice | |
| options: | |
| - 'full' | |
| - 'client-only' | |
| - 'api-specific' | |
| default: 'full' | |
| env: | |
| NODE_VERSION: '18' | |
| jobs: | |
| # Job 1: Client Performance Benchmarks | |
| client-performance: | |
| name: Client Performance Benchmarks | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build project | |
| run: npm run build | |
| - name: Client initialization benchmark | |
| run: | | |
| echo "⚡ Testing client initialization performance..." | |
| node -e " | |
| const { performance } = require('perf_hooks'); | |
| const { OzonSellerApiClient, createApiKey, createClientId } = require('./dist/index.cjs'); | |
| console.log('🔄 Running client initialization benchmark...'); | |
| const iterations = 1000; | |
| const times = []; | |
| for (let i = 0; i < iterations; i++) { | |
| const startTime = performance.now(); | |
| const client = new OzonSellerApiClient({ | |
| apiKey: createApiKey('test-key-1234567890123456789012345678901234567890'), | |
| clientId: createClientId('123456') | |
| }); | |
| const endTime = performance.now(); | |
| times.push(endTime - startTime); | |
| } | |
| const avgTime = times.reduce((sum, time) => sum + time, 0) / times.length; | |
| const minTime = Math.min(...times); | |
| const maxTime = Math.max(...times); | |
| const p95Time = times.sort((a, b) => a - b)[Math.floor(times.length * 0.95)]; | |
| console.log(\`📊 Client Initialization Benchmark (\${iterations} iterations):\`); | |
| console.log(\` Average: \${avgTime.toFixed(2)}ms\`); | |
| console.log(\` Minimum: \${minTime.toFixed(2)}ms\`); | |
| console.log(\` Maximum: \${maxTime.toFixed(2)}ms\`); | |
| console.log(\` 95th percentile: \${p95Time.toFixed(2)}ms\`); | |
| // Performance thresholds (adjusted for CI environment) | |
| const thresholds = { | |
| average: 50, // 50ms average (CI is slower) | |
| p95: 100, // 100ms 95th percentile | |
| maximum: 200 // 200ms maximum | |
| }; | |
| let failed = false; | |
| if (avgTime > thresholds.average) { | |
| console.log(\`❌ Average initialization time (\${avgTime.toFixed(2)}ms) exceeds threshold (\${thresholds.average}ms)\`); | |
| failed = true; | |
| } | |
| if (p95Time > thresholds.p95) { | |
| console.log(\`❌ 95th percentile initialization time (\${p95Time.toFixed(2)}ms) exceeds threshold (\${thresholds.p95}ms)\`); | |
| failed = true; | |
| } | |
| if (maxTime > thresholds.maximum) { | |
| console.log(\`❌ Maximum initialization time (\${maxTime.toFixed(2)}ms) exceeds threshold (\${thresholds.maximum}ms)\`); | |
| failed = true; | |
| } | |
| if (!failed) { | |
| console.log('✅ All performance thresholds met'); | |
| } else { | |
| process.exit(1); | |
| } | |
| " | |
| - name: API access performance benchmark | |
| run: | | |
| echo "⚡ Testing API access performance..." | |
| node -e " | |
| const { performance } = require('perf_hooks'); | |
| const { OzonSellerApiClient, createApiKey, createClientId } = require('./dist/index.cjs'); | |
| console.log('🔄 Running API access benchmark...'); | |
| const client = new OzonSellerApiClient({ | |
| apiKey: createApiKey('test-key-1234567890123456789012345678901234567890'), | |
| clientId: createClientId('123456') | |
| }); | |
| const apis = [ | |
| 'analytics', 'finance', 'product', 'pricingStrategy', 'returns', 'return', | |
| 'quants', 'review', 'chat', 'questionsAnswers', 'brand', 'certification', | |
| 'fbs', 'deliveryFbs', 'deliveryRfbs', 'fbo', 'fbsRfbsMarks', 'rfbsReturns', | |
| 'supplier', 'warehouse', 'fboSupplyRequest', 'report', 'premium', | |
| 'pricesStocks', 'betaMethod', 'promos', 'pass', 'cancellation', 'category', | |
| 'digital', 'barcode', 'polygon', 'sellerRating' | |
| ]; | |
| const iterations = 10000; | |
| let totalTime = 0; | |
| const startTime = performance.now(); | |
| for (let i = 0; i < iterations; i++) { | |
| // Randomly access different APIs | |
| const randomApi = apis[Math.floor(Math.random() * apis.length)]; | |
| const api = client[randomApi]; | |
| // Just access the API object (no method calls) | |
| if (!api) { | |
| console.error(\`❌ Failed to access \${randomApi} API\`); | |
| process.exit(1); | |
| } | |
| } | |
| const endTime = performance.now(); | |
| totalTime = endTime - startTime; | |
| const avgAccessTime = totalTime / iterations; | |
| console.log(\`📊 API Access Benchmark (\${iterations} iterations):\`); | |
| console.log(\` Total time: \${totalTime.toFixed(2)}ms\`); | |
| console.log(\` Average access time: \${avgAccessTime.toFixed(4)}ms\`); | |
| console.log(\` Operations per second: \${(iterations / (totalTime / 1000)).toFixed(0)}\`); | |
| if (avgAccessTime > 0.1) { // 0.1ms threshold (adjusted for CI) | |
| console.log(\`❌ Average API access time (\${avgAccessTime.toFixed(4)}ms) exceeds threshold (0.1ms)\`); | |
| process.exit(1); | |
| } | |
| console.log('✅ API access performance acceptable'); | |
| " | |
| # Job 2: Memory Usage Analysis | |
| memory-analysis: | |
| name: Memory Usage Analysis | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build project | |
| run: npm run build | |
| - name: Memory usage benchmark | |
| run: | | |
| echo "🧠 Testing memory usage..." | |
| node -e " | |
| const { OzonSellerApiClient, createApiKey, createClientId } = require('./dist/index.cjs'); | |
| console.log('🔄 Running memory usage analysis...'); | |
| // Measure initial memory usage | |
| const initialMemory = process.memoryUsage(); | |
| console.log('📊 Initial memory usage:'); | |
| console.log(\` RSS: \${(initialMemory.rss / 1024 / 1024).toFixed(2)} MB\`); | |
| console.log(\` Heap Used: \${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)} MB\`); | |
| console.log(\` Heap Total: \${(initialMemory.heapTotal / 1024 / 1024).toFixed(2)} MB\`); | |
| // Create multiple client instances to test memory scaling | |
| const clients = []; | |
| const clientCount = 100; | |
| for (let i = 0; i < clientCount; i++) { | |
| clients.push(new OzonSellerApiClient({ | |
| apiKey: createApiKey('test-key-1234567890123456789012345678901234567890'), | |
| clientId: createClientId('123456') | |
| })); | |
| } | |
| // Force garbage collection if available (CI environment compatible) | |
| try { | |
| if (global.gc) { | |
| global.gc(); | |
| } | |
| } catch (e) { | |
| console.log('⚠️ Garbage collection not available in this environment'); | |
| } | |
| const afterClientsMemory = process.memoryUsage(); | |
| console.log(\`📊 Memory usage after creating \${clientCount} clients:\`); | |
| console.log(\` RSS: \${(afterClientsMemory.rss / 1024 / 1024).toFixed(2)} MB\`); | |
| console.log(\` Heap Used: \${(afterClientsMemory.heapUsed / 1024 / 1024).toFixed(2)} MB\`); | |
| console.log(\` Heap Total: \${(afterClientsMemory.heapTotal / 1024 / 1024).toFixed(2)} MB\`); | |
| const memoryIncrease = afterClientsMemory.heapUsed - initialMemory.heapUsed; | |
| const memoryPerClient = memoryIncrease / clientCount; | |
| console.log(\`📈 Memory increase: \${(memoryIncrease / 1024 / 1024).toFixed(2)} MB\`); | |
| console.log(\`📦 Memory per client: \${(memoryPerClient / 1024).toFixed(2)} KB\`); | |
| // Memory thresholds (adjusted for CI environment) | |
| const maxMemoryPerClient = 100 * 1024; // 100KB per client (CI overhead) | |
| const maxTotalMemory = 200 * 1024 * 1024; // 200MB total | |
| if (memoryPerClient > maxMemoryPerClient) { | |
| console.log(\`❌ Memory per client (\${(memoryPerClient / 1024).toFixed(2)} KB) exceeds threshold (\${maxMemoryPerClient / 1024} KB)\`); | |
| process.exit(1); | |
| } | |
| if (afterClientsMemory.heapUsed > maxTotalMemory) { | |
| console.log(\`❌ Total memory usage (\${(afterClientsMemory.heapUsed / 1024 / 1024).toFixed(2)} MB) exceeds threshold (\${maxTotalMemory / 1024 / 1024} MB)\`); | |
| process.exit(1); | |
| } | |
| console.log('✅ Memory usage within acceptable limits'); | |
| " | |
| # Job 3: Bundle Size Analysis | |
| bundle-analysis: | |
| name: Bundle Size Analysis | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build project | |
| run: npm run build | |
| - name: Analyze bundle sizes | |
| run: | | |
| echo "📦 Analyzing bundle sizes..." | |
| # Calculate total build size | |
| total_size=$(du -sb dist/ | cut -f1) | |
| echo "📊 Total build size: $((total_size / 1024))KB" | |
| # Analyze individual components | |
| echo "📋 Component sizes:" | |
| # Main entry point | |
| main_size=$(stat -f%z dist/index.cjs 2>/dev/null || stat -c%s dist/index.cjs) | |
| echo " Main bundle: $((main_size / 1024))KB" | |
| # Core components | |
| if [ -f "dist/core/client.js" ]; then | |
| client_size=$(stat -f%z dist/core/client.js 2>/dev/null || stat -c%s dist/core/client.js) | |
| echo " Client: $((client_size / 1024))KB" | |
| fi | |
| if [ -f "dist/core/http.js" ]; then | |
| http_size=$(stat -f%z dist/core/http.js 2>/dev/null || stat -c%s dist/core/http.js) | |
| echo " HTTP Client: $((http_size / 1024))KB" | |
| fi | |
| # API categories total | |
| categories_size=$(du -sb dist/categories/ | cut -f1) | |
| echo " All API Categories: $((categories_size / 1024))KB" | |
| # Largest API categories | |
| echo "📈 Largest API categories:" | |
| find dist/categories -name "index.js" -exec stat -f"%z %N" {} \; 2>/dev/null | \ | |
| sort -nr | head -5 | while read size file; do | |
| category=$(basename $(dirname "$file")) | |
| echo " $category: $((size / 1024))KB" | |
| done | |
| # Bundle size thresholds | |
| max_total_size=$((2 * 1024 * 1024)) # 2MB | |
| max_main_size=$((500 * 1024)) # 500KB | |
| if [ $total_size -gt $max_total_size ]; then | |
| echo "❌ Total bundle size ($((total_size / 1024 / 1024))MB) exceeds threshold ($((max_total_size / 1024 / 1024))MB)" | |
| exit 1 | |
| fi | |
| if [ $main_size -gt $max_main_size ]; then | |
| echo "❌ Main bundle size ($((main_size / 1024))KB) exceeds threshold ($((max_main_size / 1024))KB)" | |
| exit 1 | |
| fi | |
| echo "✅ Bundle sizes within acceptable limits" | |
| - name: Check for dead code | |
| run: | | |
| echo "🔍 Checking for potential dead code..." | |
| # Look for unused exports (basic check) | |
| echo "📋 Checking exports usage..." | |
| # This is a simplified check - in a real scenario you might use tools like webpack-bundle-analyzer | |
| npm run build 2>&1 | grep -i "warning\|unused" || echo "No obvious dead code warnings found" | |
| echo "✅ Dead code analysis completed" | |
| # Job 4: API-Specific Performance Tests | |
| api-performance: | |
| name: API-Specific Performance Tests | |
| runs-on: ubuntu-latest | |
| if: github.event.inputs.test-type == 'api-specific' || github.event.inputs.test-type == 'full' || github.event_name == 'schedule' | |
| strategy: | |
| matrix: | |
| api-group: [ | |
| "high-usage", # product, analytics, finance | |
| "fulfillment", # fbs, fbo, delivery | |
| "marketing" # promos, report, premium | |
| ] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build project | |
| run: npm run build | |
| - name: Test ${{ matrix.api-group }} API performance | |
| run: | | |
| echo "⚡ Testing ${{ matrix.api-group }} API performance..." | |
| node -e " | |
| const { performance } = require('perf_hooks'); | |
| const { OzonSellerApiClient, createApiKey, createClientId } = require('./dist/index.cjs'); | |
| const client = new OzonSellerApiClient({ | |
| apiKey: createApiKey('test-key-1234567890123456789012345678901234567890'), | |
| clientId: createClientId('123456') | |
| }); | |
| const apiGroups = { | |
| 'high-usage': ['product', 'analytics', 'finance'], | |
| 'fulfillment': ['fbs', 'fbo', 'deliveryFbs'], | |
| 'marketing': ['promos', 'report', 'premium'] | |
| }; | |
| const apis = apiGroups['${{ matrix.api-group }}']; | |
| const iterations = 1000; | |
| console.log(\`🔄 Testing \${apis.join(', ')} APIs (\${iterations} iterations each)...\`); | |
| for (const apiName of apis) { | |
| const times = []; | |
| for (let i = 0; i < iterations; i++) { | |
| const startTime = performance.now(); | |
| // Access API and get method names (simulates real usage pattern) | |
| const api = client[apiName]; | |
| const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(api)) | |
| .filter(name => name !== 'constructor'); | |
| const endTime = performance.now(); | |
| times.push(endTime - startTime); | |
| } | |
| const avgTime = times.reduce((sum, time) => sum + time, 0) / times.length; | |
| const p95Time = times.sort((a, b) => a - b)[Math.floor(times.length * 0.95)]; | |
| console.log(\`📊 \${apiName} API (\${Object.getOwnPropertyNames(Object.getPrototypeOf(client[apiName])).filter(n => n !== 'constructor').length} methods):\`); | |
| console.log(\` Average access: \${avgTime.toFixed(4)}ms\`); | |
| console.log(\` 95th percentile: \${p95Time.toFixed(4)}ms\`); | |
| if (avgTime > 1.0) { // 1ms threshold (CI adjusted) | |
| console.log(\`❌ \${apiName} average access time exceeds threshold\`); | |
| process.exit(1); | |
| } | |
| } | |
| console.log('✅ All API performance tests passed'); | |
| " | |
| # Job 5: Performance Regression Detection | |
| regression-detection: | |
| name: Performance Regression Detection | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'schedule' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build project | |
| run: npm run build | |
| - name: Run regression benchmarks | |
| run: | | |
| echo "📈 Running performance regression detection..." | |
| # Create or use baseline performance data file | |
| if [ ! -f "performance-baseline.json" ]; then | |
| echo "📋 Creating performance baseline (first run)..." | |
| node -e " | |
| const { performance } = require('perf_hooks'); | |
| const { OzonSellerApiClient, createApiKey, createClientId } = require('./dist/index.cjs'); | |
| const iterations = 1000; | |
| const times = []; | |
| for (let i = 0; i < iterations; i++) { | |
| const startTime = performance.now(); | |
| const client = new OzonSellerApiClient({ | |
| apiKey: createApiKey('test-key-1234567890123456789012345678901234567890'), | |
| clientId: createClientId('123456') | |
| }); | |
| const endTime = performance.now(); | |
| times.push(endTime - startTime); | |
| } | |
| const baseline = { | |
| client_init_avg: times.reduce((sum, time) => sum + time, 0) / times.length, | |
| client_init_p95: times.sort((a, b) => a - b)[Math.floor(times.length * 0.95)], | |
| timestamp: new Date().toISOString(), | |
| version: require('./package.json').version | |
| }; | |
| require('fs').writeFileSync('performance-baseline.json', JSON.stringify(baseline, null, 2)); | |
| console.log('✅ Baseline created:', JSON.stringify(baseline, null, 2)); | |
| " | |
| echo "⚠️ Baseline created - skipping regression test for first run" | |
| echo "✅ Regression detection setup completed" | |
| exit 0 | |
| fi | |
| # Compare current performance against baseline | |
| node -e " | |
| const fs = require('fs'); | |
| const { performance } = require('perf_hooks'); | |
| const { OzonSellerApiClient, createApiKey, createClientId } = require('./dist/index.cjs'); | |
| const baseline = JSON.parse(fs.readFileSync('performance-baseline.json', 'utf8')); | |
| console.log('📊 Baseline performance:', baseline); | |
| const iterations = 1000; | |
| const times = []; | |
| for (let i = 0; i < iterations; i++) { | |
| const startTime = performance.now(); | |
| const client = new OzonSellerApiClient({ | |
| apiKey: createApiKey('test-key-1234567890123456789012345678901234567890'), | |
| clientId: createClientId('123456') | |
| }); | |
| const endTime = performance.now(); | |
| times.push(endTime - startTime); | |
| } | |
| const current = { | |
| client_init_avg: times.reduce((sum, time) => sum + time, 0) / times.length, | |
| client_init_p95: times.sort((a, b) => a - b)[Math.floor(times.length * 0.95)] | |
| }; | |
| console.log('📊 Current performance:', current); | |
| const regressionThreshold = 2.0; // 100% regression threshold (CI environment) | |
| const avgRegression = current.client_init_avg / baseline.client_init_avg; | |
| const p95Regression = current.client_init_p95 / baseline.client_init_p95; | |
| console.log(\`📈 Performance comparison:\`); | |
| console.log(\` Average: \${(avgRegression * 100).toFixed(1)}% of baseline\`); | |
| console.log(\` P95: \${(p95Regression * 100).toFixed(1)}% of baseline\`); | |
| if (avgRegression > regressionThreshold) { | |
| console.log(\`❌ Performance regression detected in average time: \${(avgRegression * 100).toFixed(1)}% > \${regressionThreshold * 100}%\`); | |
| process.exit(1); | |
| } | |
| if (p95Regression > regressionThreshold) { | |
| console.log(\`❌ Performance regression detected in P95 time: \${(p95Regression * 100).toFixed(1)}% > \${regressionThreshold * 100}%\`); | |
| process.exit(1); | |
| } | |
| console.log('✅ No performance regression detected'); | |
| " | |
| # Job 6: Performance Report | |
| performance-report: | |
| name: Generate Performance Report | |
| runs-on: ubuntu-latest | |
| needs: [client-performance, memory-analysis, bundle-analysis, api-performance] | |
| if: always() | |
| steps: | |
| - name: Generate performance summary | |
| run: | | |
| echo "📊 Performance Monitoring Report" | |
| echo "================================" | |
| echo "Date: $(date -u +"%Y-%m-%d %H:%M:%S UTC")" | |
| echo "Node.js: ${{ env.NODE_VERSION }}" | |
| echo "" | |
| echo "🎯 Test Results:" | |
| echo "- Client Performance: ${{ needs.client-performance.result }}" | |
| echo "- Memory Analysis: ${{ needs.memory-analysis.result }}" | |
| echo "- Bundle Analysis: ${{ needs.bundle-analysis.result }}" | |
| echo "- API Performance: ${{ needs.api-performance.result }}" | |
| echo "" | |
| if [ "${{ needs.client-performance.result }}" = "success" ] && \ | |
| [ "${{ needs.memory-analysis.result }}" = "success" ] && \ | |
| [ "${{ needs.bundle-analysis.result }}" = "success" ] && \ | |
| [ "${{ needs.api-performance.result }}" = "success" ]; then | |
| echo "✅ All performance tests passed" | |
| echo "🚀 OZON Seller API SDK performance is optimal" | |
| else | |
| echo "❌ Some performance tests failed" | |
| echo "⚠️ Performance degradation detected - investigation required" | |
| fi | |
| echo "" | |
| echo "📈 Performance Targets (CI Environment):" | |
| echo "- Client init: <50ms average, <100ms P95" | |
| echo "- Memory per client: <100KB" | |
| echo "- Bundle size: <2MB total, <500KB main" | |
| echo "- API access: <1ms average" |