mirror of
https://github.com/DeNNiiInc/Web-Page-Performance-Test.git
synced 2026-04-18 04:05:58 +00:00
269 lines
7.6 KiB
JavaScript
269 lines
7.6 KiB
JavaScript
/**
|
|
* 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
|
|
};
|