/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
function classifyMochitestFlavor(jobName) { const m = jobName.match(/mochitest-(\w+)/); if (m) { for (const [prefix, flavor] of MOCHITEST_FLAVOR_PREFIXES) { if (m[1].startsWith(prefix)) { return flavor;
}
}
} return"other";
}
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
} if (!fs.existsSync(PROFILE_CACHE_DIR)) {
fs.mkdirSync(PROFILE_CACHE_DIR, { recursive: true });
}
// Get date in YYYY-MM-DD format function getDateString(daysAgo = 0) { const date = new Date();
date.setDate(date.getDate() - daysAgo); return date.toISOString().split("T")[0];
}
async function fetchJson(url) { const response = await fetch(url); if (!response.ok) {
console.error(
`Failed to fetch ${url}: HTTP ${response.status} ${response.statusText}`
); returnnull;
} return response.json();
}
// Fetch commit push data from Treeherder API
async function fetchCommitData(project, revision) {
console.log(`Fetching commit data for ${project}:${revision}...`);
const result = await fetchJson(
`https://treeherder.mozilla.org/api/project/${project}/push/?full=true&count=10&revision=${revision}`
);
if (!result || !result.results || result.results.length === 0) { thrownew Error(
`No push found for revision ${revision} on project ${project}`
);
}
// Fetch jobs from push
async function fetchPushJobs(project, pushId) {
console.log(`Fetching jobs for push ID ${pushId}...`);
let allJobs = [];
let propertyNames = [];
let url = `https://treeherder.mozilla.org/api/jobs/?push_id=${pushId}`;
// The /jobs/ API is paginated, keep fetching until next is null while (url) { const result = await fetchJson(url); if (!result) { thrownew Error(`Failed to fetch jobs for push ID ${pushId}`);
}
console.log(
`Found ${harnessJobs.length} ${HARNESS} jobs out of ${allJobs.length} total jobs`
); return harnessJobs;
}
// Fetch test data from Firefox-CI ETL for a specific date
async function fetchHarnessData(targetDate) {
console.log(`Fetching ${HARNESS} test data for ${targetDate}...`);
// Fetch data from Firefox-CI ETL if not already cached if (!allJobsCache || !ignoreTasksCache) {
console.log(`Querying Firefox-CI ETL and loading ignore list...`);
// Fetch both Firefox-CI ETL data and ignore list in parallel const [etlResult, ignoreListResult] = await Promise.all([
fetchJson(FIREFOX_CI_ETL_URL),
fetchJson(IGNORE_LIST_URL),
]);
if (!etlResult) { thrownew Error("Failed to fetch data from Firefox-CI ETL");
}
if (!ignoreListResult) { thrownew Error("Failed to fetch ignore list from Treeherder");
}
// Build set of tasks to ignore
ignoreTasksCache = new Set(); for (const row of ignoreListResult.query_result.data.rows) {
ignoreTasksCache.add(row.task);
}
console.log(`Loaded ${ignoreTasksCache.size} tasks to ignore`);
const allJobs = etlResult.query_result.data.rows;
// Cache all harness jobs (don't filter by ignore list yet)
allJobsCache = allJobs.filter(job => job.name?.includes(HARNESS));
console.log(
`Cached ${allJobsCache.length} ${HARNESS} jobs from Firefox-CI ETL (out of ${allJobs.length} total jobs)`
);
}
// Filter cached jobs for the target date return allJobsCache.filter(job => job.start_time.startsWith(targetDate));
}
// Process jobs using worker threads with dynamic job distribution
async function processJobsWithWorkers(jobs, targetDate = null) { if (jobs.length === 0) { return [];
}
const dateStr = targetDate ? ` for ${targetDate}` : "";
console.log(
`Processing ${jobs.length} jobs${dateStr} using ${MAX_WORKERS} workers...`
);
const jobQueue = [...jobs]; const results = [];
let invalidJobCount = 0; const workers = [];
let completedJobs = 0;
let lastProgressTime = 0;
returnnew Promise((resolve, reject) => { // Track worker states const workerStates = new Map();
// Create workers for (let i = 0; i < MAX_WORKERS; i++) { const worker = new Worker(path.join(__dirname, "profile-worker.js"), {
workerData: {
profileCacheDir: PROFILE_CACHE_DIR,
taskclusterBaseUrl: TASKCLUSTER_BASE_URL,
},
});
if (message.result) { if (message.result.error) { // Only network_error is retryable, permanent errors count as invalid if (message.result.error !== "network_error") {
invalidJobCount++;
}
} else {
results.push(message.result);
}
}
// Show progress at most once per second, or on first/last job const now = Date.now(); if (
completedJobs === 1 ||
completedJobs === jobs.length ||
now - lastProgressTime >= 1000
) { const percentage = Math.round((completedJobs / jobs.length) * 100); const paddedCompleted = completedJobs
.toString()
.padStart(jobs.length.toString().length); const paddedPercentage = percentage.toString().padStart(3); // Pad to 3 chars for alignment (0-100%)
console.log(
` ${paddedPercentage}% ${paddedCompleted}/${jobs.length}`
);
lastProgressTime = now;
}
// Look up component for a test path function findComponentForPath(testPath) { if (!componentsData || !componentsData.paths) { returnnull;
}
const parts = testPath.split("/");
let current = componentsData.paths;
for (const part of parts) { if (typeof current === "number") { return current;
} if (typeof current === "object" && current !== null && part in current) {
current = current[part];
} else { returnnull;
}
}
returntypeof current === "number" ? current : null;
}
// Get component string from component ID function getComponentString(componentId) { if (!componentsData || !componentsData.components || componentId == null) { returnnull;
}
// Helper function to determine if a status should include message data function shouldIncludeMessage(status) { return status === "SKIP" || status.startsWith("FAIL");
}
// Create string tables and store raw data efficiently function createDataTables(jobResults) { const tables = {
jobNames: [],
testPaths: [],
testNames: [],
repositories: [],
statuses: [],
taskIds: [],
messages: [],
crashSignatures: [],
components: [],
commitIds: [],
};
// Maps for O(1) string lookups const stringMaps = {
jobNames: new Map(),
testPaths: new Map(),
testNames: new Map(),
repositories: new Map(),
statuses: new Map(),
taskIds: new Map(),
messages: new Map(),
crashSignatures: new Map(),
components: new Map(),
commitIds: new Map(),
};
// Task info maps task ID index to repository and job name indexes const taskInfo = {
repositoryIds: [],
jobNameIds: [],
commitIds: [],
};
// Test info maps test ID index to test path and name indexes const testInfo = {
testPathIds: [],
testNameIds: [],
componentIds: [],
};
// Map for fast testId lookup: fullPath -> testId const testIdMap = new Map();
// Test runs grouped by test ID, then by status ID // testRuns[testId] = array of status groups for that test const testRuns = [];
for (const timing of result.timings) { const fullPath = timing.path;
// Check if we already have this test
let testId = testIdMap.get(fullPath); if (testId === undefined) { // New test - need to process path/name split and create entry const lastSlashIndex = fullPath.lastIndexOf("/");
let testPath, testName; if (lastSlashIndex === -1) { // No directory, just the filename
testPath = "";
testName = fullPath;
} else {
testPath = fullPath.substring(0, lastSlashIndex);
testName = fullPath.substring(lastSlashIndex + 1);
}
// Look up the component for this test const componentIdRaw = findComponentForPath(fullPath); const componentString = getComponentString(componentIdRaw); const componentId = componentString
? findStringIndex("components", componentString)
: null;
// Store task info only once per unique task ID if (taskInfo.repositoryIds[taskIdId] === undefined) {
taskInfo.repositoryIds[taskIdId] = repositoryId;
taskInfo.jobNameIds[taskIdId] = jobNameId;
taskInfo.commitIds[taskIdId] = commitId;
}
// Initialize test group if it doesn't exist if (!testRuns[testId]) {
testRuns[testId] = [];
}
// Initialize status group within test if it doesn't exist
let statusGroup = testRuns[testId][statusId]; if (!statusGroup) {
statusGroup = {
taskIdIds: [],
durations: [],
timestamps: [],
}; // Include messageIds array for statuses that should have messages if (shouldIncludeMessage(timing.status)) {
statusGroup.messageIds = [];
} // Only include crash data arrays for CRASH status if (timing.status === "CRASH") {
statusGroup.crashSignatureIds = [];
statusGroup.minidumps = [];
}
testRuns[testId][statusId] = statusGroup;
}
// Add test run to the appropriate test/status group
statusGroup.taskIdIds.push(taskIdId);
statusGroup.durations.push(Math.round(timing.duration));
statusGroup.timestamps.push(timing.timestamp);
// Store message ID for statuses that should include messages (or null if no message) if (shouldIncludeMessage(timing.status)) { const messageId = timing.message
? findStringIndex("messages", timing.message)
: null;
statusGroup.messageIds.push(messageId);
}
// Store crash data for CRASH status (or null if not available) if (timing.status === "CRASH") { const crashSignatureId = timing.crashSignature
? findStringIndex("crashSignatures", timing.crashSignature)
: null;
statusGroup.crashSignatureIds.push(crashSignatureId);
statusGroup.minidumps.push(timing.minidump || null);
}
}
}
// Sort string tables by frequency and remap all indices for deterministic output and better compression function sortStringTablesByFrequency(dataStructure) { const { tables, taskInfo, testInfo, testRuns } = dataStructure;
// Count frequency of each index for each table const frequencyCounts = {
jobNames: new Array(tables.jobNames.length).fill(0),
testPaths: new Array(tables.testPaths.length).fill(0),
testNames: new Array(tables.testNames.length).fill(0),
repositories: new Array(tables.repositories.length).fill(0),
statuses: new Array(tables.statuses.length).fill(0),
taskIds: new Array(tables.taskIds.length).fill(0),
messages: new Array(tables.messages.length).fill(0),
crashSignatures: new Array(tables.crashSignatures.length).fill(0),
components: new Array(tables.components.length).fill(0),
commitIds: new Array(tables.commitIds.length).fill(0),
};
// Count taskInfo references for (const jobNameId of taskInfo.jobNameIds) { if (jobNameId !== undefined) {
frequencyCounts.jobNames[jobNameId]++;
}
} for (const repositoryId of taskInfo.repositoryIds) { if (repositoryId !== undefined) {
frequencyCounts.repositories[repositoryId]++;
}
} for (const commitId of taskInfo.commitIds) { if (commitId !== null) {
frequencyCounts.commitIds[commitId]++;
}
}
// Count testInfo references for (const testPathId of testInfo.testPathIds) {
frequencyCounts.testPaths[testPathId]++;
} for (const testNameId of testInfo.testNameIds) {
frequencyCounts.testNames[testNameId]++;
} for (const componentId of testInfo.componentIds) { if (componentId !== null) {
frequencyCounts.components[componentId]++;
}
}
// Count testRuns references for (const testGroup of testRuns) { if (!testGroup) { continue;
}
testGroup.forEach((statusGroup, statusId) => { if (!statusGroup) { return;
}
// Handle aggregated format (counts/days), bucket format (durations), // and detailed format (taskIdIds) if (statusGroup.taskIdIds) { // Check if taskIdIds is array of arrays (aggregated) or flat array (daily) const isArrayOfArrays =
!!statusGroup.taskIdIds.length &&
Array.isArray(statusGroup.taskIdIds[0]);
if (isArrayOfArrays) { // Aggregated format: array of arrays const totalRuns = statusGroup.taskIdIds.reduce(
(sum, arr) => sum + arr.length, 0
);
frequencyCounts.statuses[statusId] += totalRuns;
for (const taskIdIdsArray of statusGroup.taskIdIds) { for (const taskIdId of taskIdIdsArray) {
frequencyCounts.taskIds[taskIdId]++;
}
}
} else { // Daily format: flat array
frequencyCounts.statuses[statusId] += statusGroup.taskIdIds.length;
for (const taskIdId of statusGroup.taskIdIds) {
frequencyCounts.taskIds[taskIdId]++;
}
}
} elseif (
statusGroup.durations &&
Array.isArray(statusGroup.durations[0])
) { // Bucket pass format: durations is array of arrays const totalRuns = statusGroup.durations.reduce(
(sum, arr) => sum + arr.length, 0
);
frequencyCounts.statuses[statusId] += totalRuns;
} elseif (statusGroup.counts) { // Aggregated passing tests - count total runs const totalRuns = statusGroup.counts.reduce((a, b) => a + b, 0);
frequencyCounts.statuses[statusId] += totalRuns;
}
if (statusGroup.jobNameIds) { for (const jobNameId of statusGroup.jobNameIds) { if (jobNameId !== null) {
frequencyCounts.jobNames[jobNameId]++;
}
}
}
if (statusGroup.messageIds) { for (const messageId of statusGroup.messageIds) { if (messageId !== null) {
frequencyCounts.messages[messageId]++;
}
}
}
if (statusGroup.crashSignatureIds) { for (const crashSigId of statusGroup.crashSignatureIds) { if (crashSigId !== null) {
frequencyCounts.crashSignatures[crashSigId]++;
}
}
}
});
}
// Create sorted tables and index mappings (sorted by frequency descending) const sortedTables = {}; const indexMaps = {};
for (const [tableName, table] of Object.entries(tables)) { const counts = frequencyCounts[tableName];
// Create array with value, oldIndex, and count const indexed = table.map((value, oldIndex) => ({
value,
oldIndex,
count: counts[oldIndex],
}));
// Filter out unused entries and sort by count descending, // then by value for deterministic order when counts are equal const sorted = indexed
.filter(item => item.count > 0)
.sort((a, b) => { if (b.count !== a.count) { return b.count - a.count;
} return a.value.localeCompare(b.value);
});
// Remap taskInfo indices // taskInfo arrays are indexed by taskIdId, and when taskIds get remapped, // we need to rebuild the arrays at the new indices const sortedTaskInfo = {
repositoryIds: [],
jobNameIds: [],
commitIds: [],
}; const hasChunks = !!taskInfo.chunks; if (hasChunks) {
sortedTaskInfo.chunks = [];
}
// Remap testRuns indices const sortedTestRuns = testRuns.map(testGroup => { if (!testGroup) { return testGroup;
}
return testGroup.map(statusGroup => { if (!statusGroup) { return statusGroup;
}
// Bucket pass format: durations is array of arrays, with jobNameIds if (
statusGroup.durations &&
Array.isArray(statusGroup.durations[0]) &&
!statusGroup.taskIdIds
) { const remapped = {
durations: statusGroup.durations,
days: statusGroup.days,
}; if (statusGroup.jobNameIds) {
remapped.jobNameIds = statusGroup.jobNameIds.map(oldId =>
oldId === null ? null : indexMaps.jobNames.get(oldId)
);
} return remapped;
}
// Aggregated counts format (may have jobNameIds/messageIds in bucket files) if (statusGroup.counts && !statusGroup.taskIdIds) { const remapped = {
counts: statusGroup.counts,
days: statusGroup.days,
}; if (statusGroup.jobNameIds) {
remapped.jobNameIds = statusGroup.jobNameIds.map(oldId =>
oldId === null ? null : indexMaps.jobNames.get(oldId)
);
} if (statusGroup.messageIds) {
remapped.messageIds = statusGroup.messageIds.map(oldId =>
oldId === null ? null : indexMaps.messages.get(oldId)
);
} return remapped;
}
// Check if this is aggregated format (array of arrays) or daily format (flat array) const isArrayOfArrays =
!!statusGroup.taskIdIds.length &&
Array.isArray(statusGroup.taskIdIds[0]);
const remapped = {};
if (isArrayOfArrays) { // Aggregated format: array of arrays with days
remapped.taskIdIds = statusGroup.taskIdIds.map(taskIdIdsArray =>
taskIdIdsArray.map(oldId => indexMaps.taskIds.get(oldId))
);
remapped.days = statusGroup.days;
} else { // Daily format: flat array with durations and timestamps
remapped.taskIdIds = statusGroup.taskIdIds.map(oldId =>
indexMaps.taskIds.get(oldId)
);
remapped.durations = statusGroup.durations;
remapped.timestamps = statusGroup.timestamps;
}
// Remap message IDs for status groups that have messages if (statusGroup.messageIds) {
remapped.messageIds = statusGroup.messageIds.map(oldId =>
oldId === null ? null : indexMaps.messages.get(oldId)
);
}
// Remap crash data for CRASH status if (statusGroup.crashSignatureIds) {
remapped.crashSignatureIds = statusGroup.crashSignatureIds.map(oldId =>
oldId === null ? null : indexMaps.crashSignatures.get(oldId)
);
} if (statusGroup.minidumps) {
remapped.minidumps = statusGroup.minidumps;
}
return remapped;
});
});
// Remap statusId positions in testRuns (move status groups to their new positions) const finalTestRuns = sortedTestRuns.map(testGroup => { if (!testGroup) { return testGroup;
}
// Create resource usage data structure function createResourceUsageData(jobResults) { const jobNames = []; const jobNameMap = new Map(); const repositories = []; const repositoryMap = new Map(); const machineInfos = []; const machineInfoMap = new Map();
// Collect all job data first const jobDataList = [];
for (const result of jobResults) { if (!result || !result.resourceUsage) { continue;
}
// Extract chunk number from job name (e.g., "test-linux1804-64/opt-xpcshell-1" -> "test-linux1804-64/opt-xpcshell", chunk: 1)
let jobNameBase = result.jobName;
let chunkNumber = null; const match = result.jobName.match(/^(.+)-(\d+)$/); if (match) {
jobNameBase = match[1];
chunkNumber = parseInt(match[2], 10);
}
// Get or create job name index
let jobNameId = jobNameMap.get(jobNameBase); if (jobNameId === undefined) {
jobNameId = jobNames.length;
jobNames.push(jobNameBase);
jobNameMap.set(jobNameBase, jobNameId);
}
// Get or create repository index
let repositoryId = repositoryMap.get(result.repository); if (repositoryId === undefined) {
repositoryId = repositories.length;
repositories.push(result.repository);
repositoryMap.set(result.repository, repositoryId);
}
// Get or create machine info index const machineInfo = result.resourceUsage.machineInfo; const machineInfoKey = JSON.stringify(machineInfo);
let machineInfoId = machineInfoMap.get(machineInfoKey); if (machineInfoId === undefined) {
machineInfoId = machineInfos.length;
machineInfos.push(machineInfo);
machineInfoMap.set(machineInfoKey, machineInfoId);
}
// Combine taskId and retryId (omit .0 for retry 0) const taskIdString =
result.retryId === 0
? result.taskId
: `${result.taskId}.${result.retryId}`;
// Sort by start time
jobDataList.sort((a, b) => a.startTime - b.startTime);
// Apply differential compression to start times and build parallel arrays const jobs = {
jobNameIds: [],
chunks: [],
taskIds: [],
repositoryIds: [],
startTimes: [],
machineInfoIds: [],
maxMemories: [],
idleTimes: [],
singleCoreTimes: [],
cpuBuckets: [],
};
let previousStartTime = 0; for (const jobData of jobDataList) {
jobs.jobNameIds.push(jobData.jobNameId);
jobs.chunks.push(jobData.chunk);
jobs.taskIds.push(jobData.taskId);
jobs.repositoryIds.push(jobData.repositoryId);
// Differential compression: store difference from previous const timeDiff = jobData.startTime - previousStartTime;
jobs.startTimes.push(timeDiff);
previousStartTime = jobData.startTime;
// Common function to process jobs and create data structure
async function processJobsAndCreateData(
jobs,
targetLabel,
startTime,
metadata
) { if (jobs.length === 0) {
console.log(`No jobs found for ${targetLabel}.`); returnnull;
}
// Process jobs to extract test timings const jobProcessingStart = Date.now(); const { results: jobResults, invalidJobCount } = await processJobsWithWorkers(
jobs,
targetLabel
); const jobProcessingTime = Date.now() - jobProcessingStart;
console.log(
`Successfully processed ${jobResults.length} jobs in ${jobProcessingTime}ms`
);
// Create efficient data tables const dataTablesStart = Date.now();
let dataStructure = createDataTables(jobResults); const dataTablesTime = Date.now() - dataTablesStart;
console.log(`Created data tables in ${dataTablesTime}ms:`);
// Check if any test runs were extracted const hasTestRuns = !!dataStructure.testRuns.length; if (!hasTestRuns) {
console.log(`No test run data extracted for ${targetLabel}`); returnnull;
}
// Sort string tables by frequency for deterministic output and better compression const sortingStart = Date.now();
dataStructure = sortStringTablesByFrequency(dataStructure); const sortingTime = Date.now() - sortingStart;
console.log(`Sorted string tables by frequency in ${sortingTime}ms`);
// Convert absolute timestamps to relative and apply differential compression (in place) for (const testGroup of dataStructure.testRuns) { if (!testGroup) { continue;
}
for (const statusGroup of testGroup) { if (!statusGroup) { continue;
}
// Convert timestamps to relative in place for (let i = 0; i < statusGroup.timestamps.length; i++) {
statusGroup.timestamps[i] =
Math.floor(statusGroup.timestamps[i] / 1000) - startTime;
}
// Map to array of objects including crash data if present const runs = statusGroup.timestamps.map((ts, i) => { const run = {
timestamp: ts,
taskIdId: statusGroup.taskIdIds[i],
duration: statusGroup.durations[i],
}; // Include crash data if this is a CRASH status group if (statusGroup.crashSignatureIds) {
run.crashSignatureId = statusGroup.crashSignatureIds[i];
} if (statusGroup.minidumps) {
run.minidump = statusGroup.minidumps[i];
} // Include message data if this status group has messages if (statusGroup.messageIds) {
run.messageId = statusGroup.messageIds[i];
} return run;
});
// Sort by timestamp
runs.sort((a, b) => a.timestamp - b.timestamp);
// Apply differential compression in place for timestamps
let previousTimestamp = 0; for (const run of runs) { const currentTimestamp = run.timestamp;
run.timestamp = currentTimestamp - previousTimestamp;
previousTimestamp = currentTimestamp;
}
// Update in place
statusGroup.taskIdIds = runs.map(run => run.taskIdId);
statusGroup.durations = runs.map(run => run.duration);
statusGroup.timestamps = runs.map(run => run.timestamp); // Update crash data arrays if present if (statusGroup.crashSignatureIds) {
statusGroup.crashSignatureIds = runs.map(run => run.crashSignatureId);
} if (statusGroup.minidumps) {
statusGroup.minidumps = runs.map(run => run.minidump);
} // Update message data arrays if present if (statusGroup.messageIds) {
statusGroup.messageIds = runs.map(run => run.messageId);
}
}
}
async function processRevisionData(project, revision, forceRefetch = false) {
console.log(`Fetching ${HARNESS} test data for ${project}:${revision}`);
console.log(`=== Processing ${project}:${revision} ===`);
// Check if we already have data for this revision if (fs.existsSync(cacheFile) && !forceRefetch) {
console.log(`Data for ${project}:${revision} already exists. Skipping.`); returnnull;
}
if (forceRefetch) {
console.log(
`Force flag detected, re-fetching data for ${project}:${revision}...`
);
}
try { // Fetch push ID from revision const pushId = await fetchCommitData(project, revision);
// Fetch jobs for the push const jobs = await fetchPushJobs(project, pushId);
if (jobs.length === 0) {
console.log(`No ${HARNESS} jobs found for ${project}:${revision}.`); returnnull;
}
// Use the last_modified time of the first job as start time const startTime = jobs.length
? Math.floor(new Date(jobs[0].start_time).getTime() / 1000)
: Math.floor(Date.now() / 1000);
// Fetch previous run metadata from Taskcluster
async function fetchPreviousRunData() { try { // Fetch task info for the current task to get the index name from the routes. const taskUrl = `${TASKCLUSTER_BASE_URL}/api/queue/v1/task/${process.env.TASK_ID}`; const taskData = await fetchJson(taskUrl); if (!taskData) {
console.log(`Failed to fetch task info from ${taskUrl}`); return;
}
const routes = taskData.routes || []; // Find a route that starts with "index." and contains ".latest." const latestRoute = routes.find(
route => route.startsWith("index.") && route.includes(".latest.")
); if (!latestRoute) {
console.log(
`No route found with 'index.' prefix and '.latest.' in name. Available routes: ${JSON.stringify(routes)}`
); return;
}
// Remove "index." prefix from route to get index name const indexName = latestRoute.replace(/^index\./, "");
console.log(`Using index: ${indexName}`);
// Store artifacts URL for later use by processDateData const artifactsUrl = `${TASKCLUSTER_BASE_URL}/api/index/v1/task/${indexName}/artifacts/public`;
// Fetch the index.json from the previous run const indexUrl = `${artifactsUrl}/index.json`;
console.log(`Fetching previous run data from ${indexUrl}`); const indexData = await fetchJson(indexUrl); if (!indexData) {
console.log(`Failed to fetch index.json from ${indexUrl}`); return;
}
const dates = indexData.dates || [];
console.log(`Found ${dates.length} dates in previous run`);
previousRunData = {
dates: new Set(dates),
artifactsUrl,
};
// Fetch previous stats and populate dailyStatsMap const statsUrl = `${artifactsUrl}/${HARNESS}-stats.json`;
console.log(`Fetching previous stats from ${statsUrl}...`); const previousStats = await fetchJson(statsUrl); if (previousStats && previousStats.dates) {
console.log(`Found ${previousStats.dates.length} days of previous stats`); for (let i = 0; i < previousStats.dates.length; i++) { const date = previousStats.dates[i]; const entry = {
totalTestRuns: previousStats.totalTestRuns[i],
failedTestRuns: previousStats.failedTestRuns[i],
skippedTestRuns: previousStats.skippedTestRuns[i],
processedJobCount: previousStats.processedJobCount[i],
failedJobs: previousStats.failedJobs[i],
invalidJobs: previousStats.invalidJobs[i],
ignoredJobs: previousStats.ignoredJobs[i],
}; if (previousStats.flavors) {
entry.flavors = {}; for (const [flavor, data] of Object.entries(previousStats.flavors)) {
entry.flavors[flavor] = {
totalTestRuns: data.totalTestRuns[i],
failedTestRuns: data.failedTestRuns[i],
skippedTestRuns: data.skippedTestRuns[i],
processedJobCount: data.processedJobCount[i],
failedJobs: data.failedJobs[i],
ignoredJobs: data.ignoredJobs[i],
};
}
}
dailyStatsMap.set(date, entry);
}
}
console.log("Previous run metadata loaded\n");
} catch (error) {
console.log(`Error fetching previous run metadata: ${error.message}`);
}
}
// Process data for a single date
async function processDateData(
targetDate,
forceRefetch = false,
acceptIncomplete = false
) { const timingsFilename = `${HARNESS}-${targetDate}.json`; const resourcesFilename = `${HARNESS}-${targetDate}-resources.json`; const timingsPath = path.join(OUTPUT_DIR, timingsFilename); const resourcesPath = path.join(OUTPUT_DIR, resourcesFilename);
// Check if we already have data for this date if (fs.existsSync(timingsPath) && !forceRefetch) {
console.log(`Data for ${targetDate} already exists, recomputing stats.`); const testData = JSON.parse(fs.readFileSync(timingsPath, "utf-8")); const existing = dailyStatsMap.get(targetDate);
calculateStatsFromData(
testData,
targetDate,
existing?.ignoredJobs,
existing?.failedJobs
); return;
}
// Fetch jobs list first (needed for verification)
let allDateJobs; try {
allDateJobs = await fetchHarnessData(targetDate); if (allDateJobs.length === 0) {
console.log(`No jobs found for ${targetDate}.`); return;
}
} catch (error) {
console.error(`Error fetching jobs for ${targetDate}:`, error); return;
}
// Filter out ignored jobs const jobs = allDateJobs.filter(job => !ignoreTasksCache.has(job.task)); const ignoredJobsCount = allDateJobs.length - jobs.length; const failedJobsCount = jobs.filter(j => j.state === "failed").length;
// Per-flavor job counts from the raw job list
let flavorJobCounts = null; if (HARNESS === "mochitest") {
flavorJobCounts = {}; for (const job of allDateJobs) { const flavor = classifyMochitestFlavor(job.name); if (flavor === "other") { continue;
} if (!flavorJobCounts[flavor]) {
flavorJobCounts[flavor] = { total: 0, failed: 0, ignored: 0 };
} if (ignoreTasksCache.has(job.task)) {
flavorJobCounts[flavor].ignored++;
} else {
flavorJobCounts[flavor].total++; if (job.state === "failed") {
flavorJobCounts[flavor].failed++;
}
}
}
}
console.log(
`Found ${allDateJobs.length} jobs for ${targetDate} (${ignoredJobsCount} ignored, ${jobs.length} to process)`
);
if (jobs.length === 0) {
console.log(`No jobs to process for ${targetDate} after filtering.`); return;
}
// Try to fetch from previous run if available and not forcing refetch if (
!forceRefetch &&
previousRunData &&
previousRunData.dates.has(targetDate)
) { try { const [timings, resources] = await Promise.all([
fetchJson(`${previousRunData.artifactsUrl}/${timingsFilename}`),
fetchJson(`${previousRunData.artifactsUrl}/${resourcesFilename}`),
]);
if (acceptIncomplete) {
console.log(`No previous data available for ${targetDate}, skipping.`); return;
}
if (forceRefetch) {
console.log(`Force flag detected, re-fetching data for ${targetDate}...`);
}
try { // Calculate start of day timestamp for relative time calculation const startOfDay = new Date(targetDate + "T00:00:00.000Z"); const startTime = Math.floor(startOfDay.getTime() / 1000); // Convert to seconds
const stringMaps = {
jobNames: new Map(),
testPaths: new Map(),
testNames: new Map(),
repositories: new Map(),
statuses: new Map(),
taskIds: new Map(),
messages: new Map(),
crashSignatures: new Map(),
components: new Map(),
commitIds: new Map(),
};
function addToMergedTable(tableName, value) { if (value === null || value === undefined) { returnnull;
} const map = stringMaps[tableName];
let index = map.get(value); if (index === undefined) {
index = mergedTables[tableName].length;
mergedTables[tableName].push(value);
map.set(value, index);
} return index;
}
function compareNullable(a, b) { if (a === b) { return0;
} if (a === null || a === undefined) { return1;
} if (b === null || b === undefined) { return -1;
} return a - b;
}
console.log(
`Successfully created aggregated files with ${outputData.metadata.totalTestCount} tests`
);
console.log(` Tests with failures: ${testsWithFailures}`);
// Build testInfo and testRuns for this bucket using global indices; // sortStringTablesByFrequency will compact out unused table entries. const localTestInfo = {
testPathIds: [],
testNameIds: [],
componentIds: [],
}; const localTestRuns = [];
let testsWithFailures = 0;
// Collect all flavor names across all dates const allFlavors = new Set(); for (const date of allDates) { const stats = dailyStatsMap.get(date); if (stats.flavors) { for (const flavor of Object.keys(stats.flavors)) {
allFlavors.add(flavor);
}
}
}
for (const date of allDates) { const stats = dailyStatsMap.get(date);
output.totalTestRuns.push(stats.totalTestRuns);
output.failedTestRuns.push(stats.failedTestRuns);
output.skippedTestRuns.push(stats.skippedTestRuns);
output.processedJobCount.push(stats.processedJobCount);
output.failedJobs.push(stats.failedJobs);
output.invalidJobs.push(stats.invalidJobs);
output.ignoredJobs.push(stats.ignoredJobs);
// Not every date has every flavor (a flavor may not have run on a // given day, or flavor data may be missing for older dates carried // forward from a pre-flavor stats file), so fall back to 0. if (output.flavors) { for (const flavor of Object.keys(output.flavors)) { const fStats = stats.flavors?.[flavor];
output.flavors[flavor].totalTestRuns.push(fStats?.totalTestRuns || 0);
output.flavors[flavor].failedTestRuns.push(fStats?.failedTestRuns || 0);
output.flavors[flavor].skippedTestRuns.push(
fStats?.skippedTestRuns || 0
);
output.flavors[flavor].processedJobCount.push(
fStats?.processedJobCount || 0
);
output.flavors[flavor].failedJobs.push(fStats?.failedJobs || 0);
output.flavors[flavor].ignoredJobs.push(fStats?.ignoredJobs || 0);
}
}
}
const statsFileName = `${HARNESS}-stats.json`;
saveJsonFile(output, path.join(OUTPUT_DIR, statsFileName));
console.log(`${allDates.length} days (${allDates[0]} to ${allDates.at(-1)})`);
}
async function main() { const scriptStartTime = Date.now();
if (parts.length !== 2) {
console.error( "Error: --revision must be in format project:revision (e.g., try:abc123 or autoland:def456)"
);
process.exit(1);
}
for (const date of dates) {
console.log(`\n=== Processing ${date} ===`);
await processDateData(date, forceRefetch, acceptIncomplete);
// After the time limit, accept incomplete data from the previous run // instead of re-processing from scratch, to avoid losing data entirely. if (!acceptIncomplete) { const elapsedTime = Date.now() - scriptStartTime; if (elapsedTime > TIME_LIMIT_MS) { const remainingDates = dates.length - dates.indexOf(date) - 1; if (remainingDates > 0) {
console.log(
`\nStopping full processing after ${TIME_LIMIT_HOURS} hours. Accepting incomplete previous data for ${remainingDates} remaining date${remainingDates > 1 ? "s" : ""}.`
);
}
acceptIncomplete = true;
}
}
}
// Clear caches to free memory before aggregation
allJobsCache = null;
componentsData = null;
// Create index file with available dates const indexFile = path.join(OUTPUT_DIR, "index.json"); const availableDates = [];
// Scan for all harness-*.json files in the output directory const files = fs.readdirSync(OUTPUT_DIR); const pattern = new RegExp(`^${HARNESS}-(\\d{4}-\\d{2}-\\d{2})\\.json$`);
files.forEach(file => { const match = file.match(pattern); if (match) {
availableDates.push(match[1]);
}
});
// Sort dates in descending order (newest first)
availableDates.sort((a, b) => b.localeCompare(a));
fs.writeFileSync(
indexFile,
JSON.stringify({ dates: availableDates }, null, 2)
);
console.log(
`\nIndex file saved as ${indexFile} with ${availableDates.length} dates`
);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.