mirror of
https://github.com/DeNNiiInc/Web-Page-Performance-Test.git
synced 2026-04-17 20:05:58 +00:00
Remove multi-run feature completely - simplify to single test execution only
This commit is contained in:
268
lib/multi-run.js
268
lib/multi-run.js
@@ -1,268 +0,0 @@
|
|||||||
/**
|
|
||||||
* Multi-Run Test Execution Module
|
|
||||||
* Handles running multiple performance tests and calculating statistics
|
|
||||||
*/
|
|
||||||
|
|
||||||
const runner = require('./runner');
|
|
||||||
const { Pool } = require('pg');
|
|
||||||
const dbConfig = require('./db-config');
|
|
||||||
|
|
||||||
const pool = new Pool(dbConfig);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Statistical helper functions
|
|
||||||
*/
|
|
||||||
function median(values) {
|
|
||||||
if (values.length === 0) return 0;
|
|
||||||
const sorted = [...values].sort((a, b) => a - b);
|
|
||||||
const mid = Math.floor(sorted.length / 2);
|
|
||||||
return sorted.length % 2 === 0
|
|
||||||
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
||||||
: sorted[mid];
|
|
||||||
}
|
|
||||||
|
|
||||||
function average(values) {
|
|
||||||
if (values.length === 0) return 0;
|
|
||||||
return values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stddev(values) {
|
|
||||||
if (values.length === 0) return 0;
|
|
||||||
const avg = average(values);
|
|
||||||
const squareDiffs = values.map(v => Math.pow(v - avg, 2));
|
|
||||||
return Math.sqrt(average(squareDiffs));
|
|
||||||
}
|
|
||||||
|
|
||||||
function findClosestToMedian(values, medianValue) {
|
|
||||||
let closestIndex = 0;
|
|
||||||
let minDiff = Math.abs(values[0] - medianValue);
|
|
||||||
|
|
||||||
values.forEach((v, i) => {
|
|
||||||
const diff = Math.abs(v - medianValue);
|
|
||||||
if (diff < minDiff) {
|
|
||||||
minDiff = diff;
|
|
||||||
closestIndex = i;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return closestIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute multiple test runs and calculate statistics
|
|
||||||
*/
|
|
||||||
async function executeMultipleRuns(suiteId, url, isMobile, runCount, userUuid, userIp) {
|
|
||||||
const results = [];
|
|
||||||
const testIds = [];
|
|
||||||
|
|
||||||
console.log(`[Multi-Run] Starting ${runCount} runs for suite ${suiteId}`);
|
|
||||||
|
|
||||||
for (let i = 1; i <= runCount; i++) {
|
|
||||||
try {
|
|
||||||
console.log(`[Multi-Run] Running test ${i}/${runCount}...`);
|
|
||||||
|
|
||||||
// Generate unique test ID for this run
|
|
||||||
const testId = `${suiteId}-run${i}`;
|
|
||||||
testIds.push(testId);
|
|
||||||
|
|
||||||
// Run individual test
|
|
||||||
const result = await runner.runTest(url, {
|
|
||||||
isMobile,
|
|
||||||
userUuid,
|
|
||||||
userIp,
|
|
||||||
suiteId,
|
|
||||||
runNumber: i,
|
|
||||||
testId
|
|
||||||
});
|
|
||||||
|
|
||||||
results.push(result);
|
|
||||||
|
|
||||||
// Update progress
|
|
||||||
await updateSuiteProgress(suiteId, i);
|
|
||||||
|
|
||||||
console.log(`[Multi-Run] Completed run ${i}/${runCount}`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[Multi-Run] Run ${i} failed:`, error);
|
|
||||||
// Continue with other runs even if one fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.length === 0) {
|
|
||||||
await markSuiteFailed(suiteId);
|
|
||||||
throw new Error('All test runs failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate statistics
|
|
||||||
console.log(`[Multi-Run] Calculating statistics for ${results.length} runs`);
|
|
||||||
await calculateStatistics(suiteId, results, testIds);
|
|
||||||
|
|
||||||
// Mark suite as complete
|
|
||||||
await completeSuite(suiteId);
|
|
||||||
|
|
||||||
console.log(`[Multi-Run] Suite ${suiteId} completed`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
suiteId,
|
|
||||||
completedRuns: results.length,
|
|
||||||
totalRuns: runCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate statistics for all runs and update suite
|
|
||||||
*/
|
|
||||||
async function calculateStatistics(suiteId, results, testIds) {
|
|
||||||
const metrics = {
|
|
||||||
performanceScore: [],
|
|
||||||
lcp: [],
|
|
||||||
cls: [],
|
|
||||||
tbt: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract metrics from each result
|
|
||||||
results.forEach(r => {
|
|
||||||
const lr = r.lighthouseResult;
|
|
||||||
metrics.performanceScore.push(lr.categories.performance.score * 100);
|
|
||||||
metrics.lcp.push(lr.audits['largest-contentful-paint']?.numericValue || 0);
|
|
||||||
metrics.cls.push(lr.audits['cumulative-layout-shift']?.numericValue || 0);
|
|
||||||
metrics.tbt.push(lr.audits['total-blocking-time']?.numericValue || 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate statistics
|
|
||||||
const stats = {
|
|
||||||
median_performance_score: median(metrics.performanceScore),
|
|
||||||
avg_performance_score: average(metrics.performanceScore),
|
|
||||||
stddev_performance_score: stddev(metrics.performanceScore),
|
|
||||||
median_lcp: median(metrics.lcp),
|
|
||||||
avg_lcp: average(metrics.lcp),
|
|
||||||
stddev_lcp: stddev(metrics.lcp),
|
|
||||||
median_cls: median(metrics.cls),
|
|
||||||
avg_cls: average(metrics.cls),
|
|
||||||
stddev_cls: stddev(metrics.cls),
|
|
||||||
median_tbt: median(metrics.tbt),
|
|
||||||
avg_tbt: average(metrics.tbt),
|
|
||||||
stddev_tbt: stddev(metrics.tbt)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find median run (closest to median performance score)
|
|
||||||
const medianPerfScore = stats.median_performance_score;
|
|
||||||
const closestRunIndex = findClosestToMedian(metrics.performanceScore, medianPerfScore);
|
|
||||||
const medianTestId = testIds[closestRunIndex];
|
|
||||||
|
|
||||||
console.log(`[Multi-Run] Median run is #${closestRunIndex + 1} (${medianTestId})`);
|
|
||||||
|
|
||||||
// Mark median run
|
|
||||||
await markMedianRun(medianTestId);
|
|
||||||
|
|
||||||
// Update suite with statistics
|
|
||||||
await updateSuiteStats(suiteId, stats);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database operations
|
|
||||||
*/
|
|
||||||
async function createSuite(suiteId, userUuid, url, deviceType, runCount) {
|
|
||||||
const query = `
|
|
||||||
INSERT INTO test_suites (suite_id, user_uuid, url, device_type, run_count, status)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, 'running')
|
|
||||||
`;
|
|
||||||
await pool.query(query, [suiteId, userUuid, url, deviceType, runCount]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateSuiteProgress(suiteId, completedRuns) {
|
|
||||||
const query = `
|
|
||||||
UPDATE test_suites
|
|
||||||
SET completed_runs = $1
|
|
||||||
WHERE suite_id = $2
|
|
||||||
`;
|
|
||||||
await pool.query(query, [completedRuns, suiteId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateSuiteStats(suiteId, stats) {
|
|
||||||
const query = `
|
|
||||||
UPDATE test_suites SET
|
|
||||||
median_performance_score = $1,
|
|
||||||
avg_performance_score = $2,
|
|
||||||
stddev_performance_score = $3,
|
|
||||||
median_lcp = $4,
|
|
||||||
avg_lcp = $5,
|
|
||||||
stddev_lcp = $6,
|
|
||||||
median_cls = $7,
|
|
||||||
avg_cls = $8,
|
|
||||||
stddev_cls = $9,
|
|
||||||
median_tbt = $10,
|
|
||||||
avg_tbt = $11,
|
|
||||||
stddev_tbt = $12
|
|
||||||
WHERE suite_id = $13
|
|
||||||
`;
|
|
||||||
await pool.query(query, [
|
|
||||||
stats.median_performance_score,
|
|
||||||
stats.avg_performance_score,
|
|
||||||
stats.stddev_performance_score,
|
|
||||||
stats.median_lcp,
|
|
||||||
stats.avg_lcp,
|
|
||||||
stats.stddev_lcp,
|
|
||||||
stats.median_cls,
|
|
||||||
stats.avg_cls,
|
|
||||||
stats.stddev_cls,
|
|
||||||
stats.median_tbt,
|
|
||||||
stats.avg_tbt,
|
|
||||||
stats.stddev_tbt,
|
|
||||||
suiteId
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function completeSuite(suiteId) {
|
|
||||||
const query = `
|
|
||||||
UPDATE test_suites
|
|
||||||
SET status = 'completed', completed_at = NOW()
|
|
||||||
WHERE suite_id = $1
|
|
||||||
`;
|
|
||||||
await pool.query(query, [suiteId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function markSuiteFailed(suiteId) {
|
|
||||||
const query = `
|
|
||||||
UPDATE test_suites
|
|
||||||
SET status = 'failed'
|
|
||||||
WHERE suite_id = $1
|
|
||||||
`;
|
|
||||||
await pool.query(query, [suiteId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function markMedianRun(testId) {
|
|
||||||
const query = `
|
|
||||||
UPDATE test_results
|
|
||||||
SET is_median = TRUE
|
|
||||||
WHERE test_id = $1
|
|
||||||
`;
|
|
||||||
await pool.query(query, [testId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSuiteStatus(suiteId) {
|
|
||||||
const query = `
|
|
||||||
SELECT
|
|
||||||
s.*,
|
|
||||||
json_agg(
|
|
||||||
json_build_object(
|
|
||||||
'test_id', r.test_id,
|
|
||||||
'run_number', r.run_number,
|
|
||||||
'is_median', r.is_median,
|
|
||||||
'created_at', r.created_at
|
|
||||||
) ORDER BY r.run_number
|
|
||||||
) FILTER (WHERE r.test_id IS NOT NULL) as runs
|
|
||||||
FROM test_suites s
|
|
||||||
LEFT JOIN test_results r ON s.suite_id = r.suite_id
|
|
||||||
WHERE s.suite_id = $1
|
|
||||||
GROUP BY s.id
|
|
||||||
`;
|
|
||||||
const result = await pool.query(query, [suiteId]);
|
|
||||||
return result.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
executeMultipleRuns,
|
|
||||||
createSuite,
|
|
||||||
getSuiteStatus
|
|
||||||
};
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
-- Multi-Run Statistics Database Migration
|
|
||||||
-- Creates test_suites table and modifies test_results for multi-run support
|
|
||||||
|
|
||||||
-- Create test_suites table to group multiple test runs
|
|
||||||
CREATE TABLE IF NOT EXISTS test_suites (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
suite_id TEXT UNIQUE NOT NULL,
|
|
||||||
user_uuid TEXT NOT NULL,
|
|
||||||
url TEXT NOT NULL,
|
|
||||||
device_type TEXT,
|
|
||||||
run_count INTEGER DEFAULT 1,
|
|
||||||
completed_runs INTEGER DEFAULT 0,
|
|
||||||
status TEXT DEFAULT 'running', -- running, completed, failed
|
|
||||||
created_at TIMESTAMP DEFAULT NOW(),
|
|
||||||
completed_at TIMESTAMP,
|
|
||||||
|
|
||||||
-- Statistical results (calculated after all runs complete)
|
|
||||||
median_performance_score NUMERIC,
|
|
||||||
avg_performance_score NUMERIC,
|
|
||||||
stddev_performance_score NUMERIC,
|
|
||||||
median_lcp NUMERIC,
|
|
||||||
avg_lcp NUMERIC,
|
|
||||||
stddev_lcp NUMERIC,
|
|
||||||
median_cls NUMERIC,
|
|
||||||
avg_cls NUMERIC,
|
|
||||||
stddev_cls NUMERIC,
|
|
||||||
median_tbt NUMERIC,
|
|
||||||
avg_tbt NUMERIC,
|
|
||||||
stddev_tbt NUMERIC
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Add columns to test_results for multi-run support
|
|
||||||
ALTER TABLE test_results
|
|
||||||
ADD COLUMN IF NOT EXISTS suite_id TEXT,
|
|
||||||
ADD COLUMN IF NOT EXISTS run_number INTEGER DEFAULT 1,
|
|
||||||
ADD COLUMN IF NOT EXISTS is_median BOOLEAN DEFAULT FALSE;
|
|
||||||
|
|
||||||
-- Create index for faster lookups
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_suite_id ON test_results(suite_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_test_suites_user ON test_suites(user_uuid);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_test_suites_status ON test_suites(status);
|
|
||||||
|
|
||||||
-- Comment
|
|
||||||
COMMENT ON TABLE test_suites IS 'Groups multiple test runs for statistical analysis';
|
|
||||||
COMMENT ON COLUMN test_results.suite_id IS 'Links individual run to parent test suite';
|
|
||||||
COMMENT ON COLUMN test_results.run_number IS 'Run number within the suite (1-10)';
|
|
||||||
COMMENT ON COLUMN test_results.is_median IS 'TRUE if this run represents the median performance';
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
-- Add filmstrip column to test_results table
|
|
||||||
ALTER TABLE test_results
|
|
||||||
ADD COLUMN filmstrip JSONB DEFAULT '[]'::jsonb;
|
|
||||||
|
|
||||||
-- Comment on column
|
|
||||||
COMMENT ON COLUMN test_results.filmstrip IS 'Array of filmstrip screenshots/thumbnails from Lighthouse';
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
const db = require('../lib/db');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const migrationFile = path.join(__dirname, '002_add_filmstrip.sql');
|
|
||||||
|
|
||||||
async function runMigration() {
|
|
||||||
try {
|
|
||||||
console.log('Reading migration file:', migrationFile);
|
|
||||||
const sql = fs.readFileSync(migrationFile, 'utf8');
|
|
||||||
|
|
||||||
console.log('Applying migration...');
|
|
||||||
await db.pool.query(sql);
|
|
||||||
|
|
||||||
console.log('✅ Migration 002 applied successfully!');
|
|
||||||
process.exit(0);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code === '42701') { // duplicate_column
|
|
||||||
console.log('⚠️ Column already exists. Skipping.');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
console.error('❌ Migration failed:', err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runMigration();
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Database Migration Script for Multi-Run Support
|
|
||||||
# Run this on the production server to apply schema changes
|
|
||||||
|
|
||||||
set -e # Exit on error
|
|
||||||
|
|
||||||
echo "=== Multi-Run Statistics Migration ==="
|
|
||||||
echo "Starting database migration..."
|
|
||||||
|
|
||||||
# Database connection details
|
|
||||||
DB_HOST="202.171.184.108"
|
|
||||||
DB_USER="postgres"
|
|
||||||
DB_NAME="WebPerformance"
|
|
||||||
DB_PORT="5432"
|
|
||||||
|
|
||||||
# Get script directory
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
||||||
MIGRATION_FILE="$SCRIPT_DIR/001_multi_run_support.sql"
|
|
||||||
|
|
||||||
# Check if migration file exists
|
|
||||||
if [ ! -f "$MIGRATION_FILE" ]; then
|
|
||||||
echo "Error: Migration file not found: $MIGRATION_FILE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Migration file: $MIGRATION_FILE"
|
|
||||||
echo "Target database: $DB_NAME on $DB_HOST"
|
|
||||||
echo ""
|
|
||||||
read -p "Continue with migration? (yes/no): " confirm
|
|
||||||
|
|
||||||
if [ "$confirm" != "yes" ]; then
|
|
||||||
echo "Migration cancelled"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run migration
|
|
||||||
echo "Applying migration..."
|
|
||||||
PGPASSWORD='X@gon2005!#$' psql -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -p "$DB_PORT" -f "$MIGRATION_FILE"
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo ""
|
|
||||||
echo "✅ Migration completed successfully!"
|
|
||||||
echo ""
|
|
||||||
echo "New tables/columns created:"
|
|
||||||
echo " - test_suites (new table)"
|
|
||||||
echo " - test_results.suite_id (new column)"
|
|
||||||
echo " - test_results.run_number (new column)"
|
|
||||||
echo " - test_results.is_median (new column)"
|
|
||||||
echo ""
|
|
||||||
echo "You can now deploy the application code."
|
|
||||||
else
|
|
||||||
echo ""
|
|
||||||
echo "❌ Migration failed!"
|
|
||||||
echo "Please check the error messages above."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
48
server.js
48
server.js
@@ -35,63 +35,23 @@ app.get("/api/git-info", (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// API Endpoint: Run Test (supports multi-run)
|
// API Endpoint: Run Test
|
||||||
app.post("/api/run-test", async (req, res) => {
|
app.post("/api/run-test", async (req, res) => {
|
||||||
const { url, isMobile, runs = 1, captureFilmstrip = false } = req.body;
|
const { url, isMobile, captureFilmstrip = false } = req.body;
|
||||||
const userUuid = req.headers['x-user-uuid'];
|
const userUuid = req.headers['x-user-uuid'];
|
||||||
const userIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
const userIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
||||||
|
|
||||||
if (!url) return res.status(400).json({ error: "URL is required" });
|
if (!url) return res.status(400).json({ error: "URL is required" });
|
||||||
|
|
||||||
// Validate run count
|
|
||||||
if (runs < 1 || runs > 10) {
|
|
||||||
return res.status(400).json({ error: "Runs must be between 1 and 10" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Single run (original behavior)
|
const result = await runner.runTest(url, { isMobile, userUuid, userIp, captureFilmstrip });
|
||||||
if (runs === 1) {
|
return res.json(result);
|
||||||
const result = await runner.runTest(url, { isMobile, userUuid, userIp, captureFilmstrip });
|
|
||||||
return res.json(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi-run
|
|
||||||
const multiRun = require('./lib/multi-run');
|
|
||||||
const suiteId = runner.generateTestId();
|
|
||||||
|
|
||||||
// Create suite record
|
|
||||||
await multiRun.createSuite(suiteId, userUuid, url, isMobile ? 'mobile' : 'desktop', runs, captureFilmstrip);
|
|
||||||
|
|
||||||
// Return suite ID immediately
|
|
||||||
res.json({ suiteId, runs, status: 'running' });
|
|
||||||
|
|
||||||
// Execute runs asynchronously
|
|
||||||
multiRun.executeMultipleRuns(suiteId, url, isMobile, runs, userUuid, userIp, captureFilmstrip)
|
|
||||||
.catch(error => console.error('Multi-run execution failed:', error));
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Test failed:", error);
|
console.error("Test failed:", error);
|
||||||
res.status(500).json({ error: "Test failed", details: error.message });
|
res.status(500).json({ error: "Test failed", details: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// API Endpoint: Suite Status (for multi-run progress tracking)
|
|
||||||
app.get("/api/suite-status/:suiteId", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const multiRun = require('./lib/multi-run');
|
|
||||||
const suite = await multiRun.getSuiteStatus(req.params.suiteId);
|
|
||||||
|
|
||||||
if (!suite) {
|
|
||||||
return res.status(404).json({ error: "Suite not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(suite);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Suite status error:", error);
|
|
||||||
res.status(500).json({ error: "Failed to get suite status" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// API Endpoint: History
|
// API Endpoint: History
|
||||||
app.get("/api/history", async (req, res) => {
|
app.get("/api/history", async (req, res) => {
|
||||||
const userUuid = req.headers['x-user-uuid'];
|
const userUuid = req.headers['x-user-uuid'];
|
||||||
|
|||||||
Reference in New Issue
Block a user