Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/testing/timings/   (Firefox Browser Version 153.0.1©)  Datei vom 27.6.2026 mit Größe 27 kB image not shown  

Quelle  JSON_FORMAT.md   Sprache: unbekannt

 
Spracherkennung für: .md vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

# Test Harness JSON Data Format Documentation

This document describes the JSON file formats created by `fetch-test-data.js`.

## Overview

The script generates two types of JSON files for each date or try commit:

1. **Test timing data**: `{harness}-{date}.json` or `{harness}-{project}-{revision}.json`
2. **Resource usage data**: `{harness}-{date}-resources.json` or `{harness}-{project}-{revision}-resources.json`

Where `{harness}` is the test harness name (e.g., `xpcshell`, `mochitest`).

Both formats use string tables and index-based lookups to minimize file size.

---

## Test Timing Data Format

### Top-Level Structure

```json
{
  "metadata": { ... },
  "tables": { ... },
  "taskInfo": { ... },
  "testInfo": { ... },
  "testRuns": [ ... ]
}
```

### metadata

Contains information about the data collection:

```json
{
  "date": "2025-10-14",              // Date of the data (for date-based queries)
  "revision": "abc123...",           // Try commit revision (for try-based queries)
  "pushId": 12345,                   // Treeherder push ID (for try-based queries)
  "startTime": 1760400000,           // Unix timestamp (seconds) used as base for relative timestamps
  "generatedAt": "2025-10-15T14:24:33.451Z",  // ISO timestamp when file was created
  "jobCount": 3481,                  // Number of jobs fetched
  "processedJobCount": 3481          // Number of jobs successfully processed
}
```

### tables

String tables for efficient storage. All strings are deduplicated and stored once, sorted by frequency (most frequently used first for better compression):

```json
{
  "jobNames": [                      // Job names (e.g., "test-linux1804-64/opt-xpcshell")
    "test-linux1804-64/opt-xpcshell",
    "test-macosx1015-64/debug-xpcshell",
    ...
  ],
  "testPaths": [                     // Test file paths (e.g., "dom/indexedDB/test/unit")
    "dom/indexedDB/test/unit",
    "toolkit/components/extensions/test/xpcshell",
    ...
  ],
  "testNames": [                     // Test filenames (e.g., "test_foo.js")
    "test_foo.js",
    "test_bar.js",
    ...
  ],
  "repositories": [                  // Repository names
    "mozilla-central",
    "autoland",
    "try",
    ...
  ],
  "statuses": [                      // Test run statuses
    "PASS-PARALLEL",
    "PASS-SEQUENTIAL",
    "SKIP",
    "FAIL-PARALLEL",
    "TIMEOUT-SEQUENTIAL",
    "CRASH",
    "EXPECTED-FAIL",
    ...
  ],
  "taskIds": [                       // TaskCluster task IDs with retry (always includes .retryId)
    "YJJe4a0CRIqbAmcCo8n63w.0",      // Retry 0
    "XPPf5b1DRJrcBndDp9o74x.1",      // Retry 1
    ...
  ],
  "messages": [                      // Test messages (for SKIP and FAIL statuses)
    "skip-if: os == 'linux'",
    "disabled due to bug 123456",
    "Expected 5, got 10",              // Failure message
    ...
  ],
  "crashSignatures": [               // Crash signatures (only for crashed tests)
    "mozilla::dom::Something::Crash",
    "EMPTY: no crashing thread identified",
    ...
  ],
  "components": [                    // Bugzilla components (Product :: Component format)
    "Core :: Storage: IndexedDB",
    "Testing :: XPCShell Harness",
    "Firefox :: General",
    ...
  ],
  "commitIds": [                     // Commit IDs from repository (extracted from profile.meta.sourceURL)
    "f37a6863f87aeeb870b16223045ea7614b1ba0a7",
    "abc123def456789012345678901234567890abcd",
    ...
  ]
}
```

### taskInfo

Maps task IDs to their associated job names, repositories, and commit IDs. These are parallel arrays indexed by `taskIdId`:

```json
{
  "repositoryIds": [0102, ...],  // Index into tables.repositories
  "jobNameIds": [0011, ...],     // Index into tables.jobNames
  "commitIds": [010, null, ...]    // Index into tables.commitIds (null if not available)
}
```

**Example lookup:**
```javascript
const taskIdId = 5;
const taskId = tables.taskIds[taskIdId];           // "YJJe4a0CRIqbAmcCo8n63w.0"
const repository = tables.repositories[taskInfo.repositoryIds[taskIdId]];  // "mozilla-central"
const jobName = tables.jobNames[taskInfo.jobNameIds[taskIdId]];           // "test-linux1804-64/opt-xpcshell"
const commitIdIdx = taskInfo.commitIds[taskIdId];
const commitId = commitIdIdx !== null ? tables.commitIds[commitIdIdx] : null;  // "f37a6863f87a..." or null
```

### testInfo

Maps test IDs to their test paths, names, and components. These are parallel arrays indexed by `testId`:

```json
{
  "testPathIds": [0012, ...],    // Index into tables.testPaths
  "testNameIds": [0123, ...],    // Index into tables.testNames
  "componentIds": [5512, null, ...] // Index into tables.components (null if unknown)
}
```

**Example lookup:**
```javascript
const testId = 10;
const testPath = tables.testPaths[testInfo.testPathIds[testId]];  // "dom/indexedDB/test/unit"
const testName = tables.testNames[testInfo.testNameIds[testId]];  // "test_foo.js"
const fullPath = testPath ? `${testPath}/${testName}` : testName;
const componentId = testInfo.componentIds[testId];
const component = componentId !== null ? tables.components[componentId] : "Unknown";  // "Core :: Storage: IndexedDB"
```

### testRuns

2D sparse array structure: `testRuns[testId][statusId]`

- First dimension: `testId` (index into testInfo arrays)
- Second dimension: `statusId` (index into tables.statuses)

Each `testRuns[testId][statusId]` contains data for all runs of that test with that specific status. If a test never had a particular status, that array position contains `null`:

```json
[
  // testId 0
  [
    // statusId 0 (e.g., "PASS-PARALLEL")
    {
      "taskIdIds": [51218, ...],       // Indices into tables.taskIds
      "durations": [123414561289, ...], // Test durations in milliseconds
      "timestamps": [01523, ...]        // Differential compressed timestamps (seconds relative to metadata.startTime)
    },
    // statusId 1 - this test never had that status
    null,
    // statusId 2 (e.g., "SKIP")
    {
      "taskIdIds": [4567, ...],
      "durations": [00, ...],
      "timestamps": [100200, ...],
      "messageIds": [55, ...]            // Present for SKIP and FAIL statuses - indices into tables.messages (null if no message)
    },
    // statusId 3 (e.g., "FAIL-PARALLEL")
    {
      "taskIdIds": [78, ...],
      "durations": [1234, ...],
      "timestamps": [250, ...],
      "messageIds": [12, ...]              // Present for SKIP and FAIL statuses - indices into tables.messages (null if no message)
    },
    // statusId 4 (e.g., "CRASH")
    {
      "taskIdIds": [89, ...],
      "durations": [5678, ...],
      "timestamps": [300, ...],
      "crashSignatureIds": [2, ...],       // Only present for CRASH status - indices into tables.crashSignatures (null if none)
      "minidumps": ["12345678-abcd-1234-abcd-1234567890ab", ...]   // Only present for CRASH status - minidump IDs or null
    }
  ],
  // testId 1
  [ ... ],
  ...
]
```

**Timestamp decompression:**
```javascript
// Timestamps are differentially compressed
let currentTime = metadata.startTime;  // Base timestamp in seconds
const decompressedTimestamps = statusGroup.timestamps.map(diff => {
    currentTime += diff;
    return currentTime;
});
```

**Example: Get all runs of a specific test:**
```javascript
const testId = 10;
const testGroup = testRuns[testId];

for (let statusId = 0; statusId < testGroup.length; statusId++) {
    const statusGroup = testGroup[statusId];
    if (!statusGroup) continue;  // This test never had this status

    const status = tables.statuses[statusId];
    console.log(`Status: ${status}, Runs: ${statusGroup.taskIdIds.length}`);

    // Decompress timestamps
    let currentTime = metadata.startTime;
    for (let i = 0; i < statusGroup.taskIdIds.length; i++) {
        currentTime += statusGroup.timestamps[i];
        const taskId = tables.taskIds[statusGroup.taskIdIds[i]];
        const duration = statusGroup.durations[i];
        console.log(`  Task: ${taskId}, Duration: ${duration}ms, Time: ${currentTime}`);
    }
}
```

---

## Resource Usage Data Format

### Top-Level Structure

```json
{
  "jobNames": [ ... ],
  "repositories": [ ... ],
  "machineInfos": [ ... ],
  "jobs": { ... }
}
```

### Lookup Tables

```json
{
  "jobNames": [                      // Base job names without chunk numbers
    "test-linux1804-64/opt-xpcshell",
    "test-macosx1015-64/debug-xpcshell",
    ...
  ],
  "repositories": [                  // Repository names
    "mozilla-central",
    "autoland",
    ...
  ],
  "machineInfos": [                  // Machine specifications (memory in GB, rounded to 1 decimal)
    {
      "logicalCPUs": 8,
      "physicalCPUs": 4,
      "mainMemory": 15.6             // GB
    },
    {
      "logicalCPUs": 16,
      "physicalCPUs": 8,
      "mainMemory": 31.4
    },
    ...
  ]
}
```

### jobs

Parallel arrays containing resource usage data for each job, sorted by start time:

```json
{
  "jobNameIds": [0011, ...],                              // Indices into jobNames array
  "chunks": [1212, ...],                                  // Chunk numbers (null if job name has no chunk)
  "taskIds": ["YJJe4a0CRIqbAmcCo8n63w", "XPPf5b1DRJrcBndDp9o74x.1", ...], // Task IDs (format: "taskId" for retry 0, "taskId.retryId" for retry > 0)
  "repositoryIds": [0011, ...],                           // Indices into repositories array
  "startTimes": [01502345, ...],       // Differential compressed timestamps (seconds)
  "machineInfoIds": [0011, ...],       // Indices into machineInfos array
  "maxMemories": [1234567890, ...],          // Maximum memory used (bytes)
  "idleTimes": [12345, ...],                 // Time with <50% of one core used (milliseconds)
  "singleCoreTimes": [45678, ...],           // Time using ~1 core (0.75-1.25 cores, milliseconds)
  "cpuBuckets": [                            // CPU usage time distribution (milliseconds per bucket)
    [1002003004005006007008009001000],  // Job 0: [0-10%, 10-20%, ..., 90-100%]
    [1502503504505506507508509501050],  // Job 1
    ...
  ]
}
```

**CPU Buckets Explanation:**
- Array of 10 values representing time spent in each CPU usage range
- Bucket 00-10% CPU usage
- Bucket 110-20% CPU usage
- ...
- Bucket 990-100% CPU usage
- Values are in milliseconds

**Idle Time Calculation:**
- Idle = CPU usage < (50% of one core)
- For 8-core machine: idle = CPU usage < 6.25%
- For 16-core machine: idle = CPU usage < 3.125%

**Single Core Time Calculation:**
- Single core = CPU usage between 0.75 and 1.25 cores
- For 8-core machine: 9.375% - 15.625%
- For 16-core machine: 4.6875% - 7.8125%

**Start Time Decompression:**
```javascript
let currentTime = 0;  // Start times are relative to each other
const decompressedStartTimes = jobs.startTimes.map(diff => {
    currentTime += diff;
    return currentTime;
});
```

**Example: Get full information for a job:**
```javascript
const jobIndex = 5;
const jobName = jobNames[jobs.jobNameIds[jobIndex]];
const chunk = jobs.chunks[jobIndex];  // May be null
const fullJobName = chunk !== null ? `${jobName}-${chunk}` : jobName;
const taskId = jobs.taskIds[jobIndex];
const repository = repositories[jobs.repositoryIds[jobIndex]];
const machineInfo = machineInfos[jobs.machineInfoIds[jobIndex]];

// Decompress start time
let currentTime = 0;
for (let i = 0; i <= jobIndex; i++) {
    currentTime += jobs.startTimes[i];
}
const startTime = currentTime;  // seconds since epoch

const maxMemoryGB = jobs.maxMemories[jobIndex] / (1024 * 1024 * 1024);
const idleTimeSeconds = jobs.idleTimes[jobIndex] / 1000;
const singleCoreTimeSeconds = jobs.singleCoreTimes[jobIndex] / 1000;
const cpuDistribution = jobs.cpuBuckets[jobIndex];
const totalTime = cpuDistribution.reduce((sum, val) => sum + val, 0);
const idlePercent = (idleTimeSeconds * 1000 / totalTime) * 100;
```

---

## Data Compression Techniques

The format uses several compression techniques to minimize file size:

1. **String Tables**: All repeated strings (job names, test paths, etc.) are stored once and referenced by index
2. **Frequency Sorting**: Strings are sorted by usage frequency (most common first) so that frequently-used items have smaller index values, reducing the number of digits in the serialized JSON
3. **Differential Compression**: Timestamps are stored as differences from the previous value
4. **Parallel Arrays**: Instead of arrays of objects, data is stored in parallel arrays to avoid repeating key names
5. **Sparse Arrays**: In testRuns, status groups that don't exist are stored as `null`
6. **Combined IDs**: TaskCluster task IDs and retry IDs are combined into a single string format: `"taskId.retryId"`
7. **Chunk Extraction**: Job chunk numbers are extracted and stored separately from base job names

---

## Index File Format

The `index.json` file lists all available dates:

```json
{
  "dates": [
    "2025-10-15",
    "2025-10-14",
    "2025-10-13",
    ...
  ]
}
```

Dates are sorted in descending order (newest first).

---

## Statistics File Format

The `{harness}-stats.json` file provides aggregate statistics for each date:

```json
{
  "metadata": {
    "generatedAt": "2025-10-15T14:24:33.451Z",
    "harness": "xpcshell"
  },
  "dates": [
    "2025-10-13",
    "2025-10-14",
    "2025-10-15",
    ...
  ],
  "totalTestRuns": [449874510245231, ...],
  "failedTestRuns": [267189234, ...],
  "skippedTestRuns": [123411981245, ...],
  "processedJobCount": [347234653481, ...],
  "failedJobs": [178142156, ...],
  "invalidJobs": [251823, ...],
  "ignoredJobs": [434745, ...]
}
```

All arrays are parallel - the value at index `i` corresponds to the date at `dates[i]`.

### Field Definitions

- **totalTestRuns**: Total number of test runs across processed jobs
- **failedTestRuns**: Number of test runs with FAIL, CRASH, or TIMEOUT status
- **skippedTestRuns**: Number of test runs with SKIP status (excluding run-if conditional skips)
- **processedJobCount**: Number of jobs successfully processed (test data extracted)
- **failedJobs**: Number of jobs with state='failed' (from the Firefox-CI ETL database query)
- **invalidJobs**: Number of jobs that didn't upload a valid resource usage profile
- **ignoredJobs**: Number of jobs filtered out by the ignore list (annotated jobs - failures that sheriffs marked as due to patches that were later reverted or fixed)

### Job Counts Relationship

The total number of jobs for a date equals:
```
Total Jobs = processedJobCount + invalidJobs + ignoredJobs
```

### Notes

- Statistics are cumulative from previous runs - new dates update the file
- The file is generated after the index file and before aggregated failures

---

## Notes

- All timestamps in test timing data are in **seconds**
- All durations are in **milliseconds**
- Memory values in machineInfos are in **GB** (rounded to 1 decimal place)
- Memory values in jobs.maxMemories are in **bytes**
- The `testRuns` array is sparse - `testRuns[testId][statusId]` may be `null` if that test never had that status
- **Task ID formats differ between files:**
  - Test timing data: Always includes retry suffix (e.g., `"YJJe4a0CRIqbAmcCo8n63w.0"`)
  - Resource usage data: Omits `.0` for retry 0 (e.g., `"YJJe4a0CRIqbAmcCo8n63w"`), includes suffix for retries > 0 (e.g., `"YJJe4a0CRIqbAmcCo8n63w.1"`)
- **Component mapping:** Components are fetched from the TaskCluster index `gecko.v2.mozilla-central.latest.source.source-bugzilla-info` and mapped to test paths. The component ID in `testInfo.componentIds` may be `null` if the test path is not found in the mapping
- Components are formatted as `"Product :: Component"` (e.g., `"Core :: Storage: IndexedDB"`)
- The data structure is optimized for sequential access patterns used by the dashboards

---

## Aggregated Files Format

When running with `--days N` where N > 1, two aggregated files are generated:

1. **`xpcshell-issues-with-taskids.json`** (~30MB for 21 days): Includes task IDs for all non-passing runs, allowing drill-down to specific CI tasks. Passing runs and non-passing runs are both aggregated by day.

2. **`xpcshell-issues.json`** (~15MB for 21 days): No task IDs or minidumps - all runs are aggregated to counts only. Optimized for fast dashboard initial load.

### Detailed File (xpcshell-issues-with-taskids.json)

#### Differences from Daily Files

#### 1. Metadata Changes

```json
{
  "metadata": {
    "startDate": "2025-11-12",           // First date in the range (earliest)
    "endDate": "2025-12-02",             // Last date in the range (most recent)
    "days": 21,                          // Number of days aggregated
    "startTime": 1731456000,             // Unix timestamp for startDate at 00:00:00 UTC
    "generatedAt": "...",
    "totalTestCount": 4506,              // Total number of unique tests
    "testsWithFailures": 3614,           // Number of tests that had at least one non-passing run
    "aggregatedFrom": [...]              // Array of source filenames
  }
}
```

Additional fields:
- `startDate`, `endDate`, `days` indicate the date range
- `startTime` is the base timestamp for the entire aggregated period (00:00:00 UTC on `startDate`)
- `testsWithFailures` counts tests with any non-passing status
- `aggregatedFrom` lists all source files that were merged

#### 2. Passing Test Runs Are Aggregated

**Daily files** store individual runs for all statuses:
```json
{
  "taskIdIds": [123456789],
  "durations": [150016001550],
  "timestamps": [360036007200]
}
```

**Aggregated file** stores only counts per day for passing statuses (status starts with "PASS"):
```json
{
  "counts": [150200180145, ...],
  "days": [0111, ...]
}
```

Where:
- `counts[i]` = total number of passing runs in that day
- `days[i]` = differential compressed day offset (days since previous bucket)
- No `taskIdIds` or `durations` arrays
- Typically sparse - only days with passing runs are included

**Decompressing days:**
```javascript
let currentDay = 0;
const absoluteDays = [];
for (const delta of days) {
  currentDay += delta;
  absoluteDays.push(currentDay);
}
// absoluteDays[i] is now the day number (0 = startTime, 1 = startTime + 1 day, etc.)
```

**Example: Calculate pass rate for a test on day 5:**
```javascript
const testId = 0;
const day = 5; // 5 days after startDate

// Find pass status
const passStatusId = data.tables.statuses.findIndex(s => s.startsWith("PASS"));
const passGroup = data.testRuns[testId]?.[passStatusId];

// Count passes on day 5
let passCount = 0;
let currentDay = 0;
if (passGroup) {
  for (let i = 0; i < passGroup.days.length; i++) {
    currentDay += passGroup.days[i];
    if (currentDay === day) {
      passCount += passGroup.counts[i];
    }
  }
}
```

#### 3. All Test Runs Aggregated by Day

Both passing and non-passing test runs are aggregated by day. The difference is in what data is preserved:

**Passing tests** (status starts with "PASS"):
```json
{
  "counts": [150200180],
  "days": [011]
}
```

**Non-passing tests** (FAIL, CRASH, TIMEOUT, SKIP, etc.):
```json
{
  "taskIdIds": [
    [4567],      // Task IDs that failed on day 0 with message 23
    [891256],  // Task IDs that failed on day 1 with message 23
    [34]           // Task IDs that failed on day 2 with message 24
  ],
  "days": [011],
  "messageIds": [232324],
  "crashSignatureIds": [556],
  "minidumps": [
    ["abc123", "def456"],    // Minidumps for crashes on day 0
    ["ghi789", null, "jkl"],  // Minidumps for crashes on day 1
    [null]                    // Minidumps for crashes on day 2
  ]
}
```

Key differences from per-date files:
- `taskIdIds` is an **array of arrays** - one array per (day, message, crashSignature) bucket
- `minidumps` is an **array of arrays** - parallel to `taskIdIds`, preserving minidump for each task
- `days` provides differentially compressed day offsets
- Durations are **removed**
- Individual timestamps are **removed** - only the day bucket is preserved
- Failures with different messages or crash signatures are in separate buckets

#### 4. String Tables Are Merged

All string tables are merged and deduplicated across all input days. A string that appears in multiple daily files will only appear once in the aggregated file.

#### 5. TaskInfo Only Contains Failed Tasks

Since passing runs don't store `taskIdIds`, the `taskInfo` object only contains mappings for tasks that appear in non-passing test runs. This significantly reduces the size of these arrays.

#### 6. Platform-Irrelevant Tests Are Filtered

SKIP tests with messages starting with "run-if" are filtered out during aggregation. These represent tests that are not relevant on certain platforms (e.g., "run-if = os == 'win'") and are not actual issues. The dashboard would filter these out anyway, so excluding them reduces file size.

### Use Cases

**Show pass/fail trends over time:**
- Passing runs: Use `counts` and `days` arrays
- Failing runs: Count taskIds in buckets within day ranges using `days`

**Investigate specific failures:**
- Task IDs preserved for all non-passing runs
- Can identify which tasks/jobs/repos had failures
- Can see error messages, crash signatures, and minidumps

**Calculate overall pass rate:**
```javascript
const testId = 0;
const passStatusId = data.tables.statuses.findIndex(s => s.startsWith("PASS"));
const failStatusId = data.tables.statuses.indexOf("FAIL");

// Total passes
const totalPasses = data.testRuns[testId]?.[passStatusId]?.counts.reduce((a, b) => a + b, 0) ?? 0;

// Total fails - count all taskIds across all buckets
const failGroup = data.testRuns[testId]?.[failStatusId];
const totalFails = failGroup?.taskIdIds.reduce((sum, arr) => sum + arr.length, 0) ?? 0;

const passRate = totalPasses / (totalPasses + totalFails);
```

---

### Small File (xpcshell-issues.json)

This file omits task IDs and minidumps to minimize file size for fast dashboard loading.

#### Differences from xpcshell-issues-with-taskids.json

#### 1. No taskInfo or taskIds

The `taskInfo` object and `tables.taskIds` array are completely omitted since all runs are aggregated.

#### 2. Reduced String Tables

Only includes tables needed for aggregated data:
```json
{
  "tables": {
    "testPaths": [...],
    "testNames": [...],
    "statuses": [...],
    "messages": [...],           // Kept for failure details
    "crashSignatures": [...],    // Kept for crash details
    "components": [...]
    // No jobNames, repositories, or taskIds
  }
}
```

#### 3. No Task IDs - Only Counts

All status groups use counts instead of task ID arrays:

```json
{
  "counts": [51283],
  "days": [0111],
  "messageIds": [23232424],           // For failures with different messages
  "crashSignatureIds": [5656]         // For crashes with different signatures
  // Note: taskIdIds and minidumps are NOT included in this file
}
```

Failures with different messages or crash signatures are bucketed separately, preserving distinct failure modes.

Task IDs and minidumps are omitted to reduce size. They are available in the detailed file.

**Example:** A test that fails 5 times on day 3 with message A and 3 times with message B will have two entries:
```json
{
  "counts": [53],
  "days": [30],  // Both on same day, so second delta is 0
  "messageIds": [2324]
}
```

---

## Bucket Files Format (Per-Test Detail)

When running with `--days N` where N > 164 bucket files are generated per harness alongside the aggregated issues files:

```
xpcshell-00.json
xpcshell-01.json
...
xpcshell-3f.json
```

Each test is assigned to a bucket using a deterministic hash of its full path. The dashboard loads only the bucket file containing the requested test, avoiding downloading the full dataset.

### Bucket Assignment

The `getBucketIndex` hash function (identical to the dashboard's `getChunkIndex`):

```javascript
function getBucketIndex(fullPath, totalBuckets = 64) {
    let hash = 0;
    for (let i = 0; i < fullPath.length; i++) {
        hash = ((hash << 5) - hash + fullPath.charCodeAt(i)) | 0;
    }
    return ((hash % totalBuckets) + totalBuckets) % totalBuckets;
}
```

The bucket index (0-63) is encoded as two-digit lowercase hex in the filename: bucket 10 = `0a`, bucket 63 = `3f`.

### Top-Level Structure

```json
{
  "metadata": { ... },
  "tables": { ... },
  "taskInfo": { ... },
  "testInfo": { ... },
  "testRuns": [ ... ]
}
```

### metadata

```json
{
  "startDate": "2025-11-12",
  "endDate": "2025-12-02",
  "days": 21,
  "startTime": 1731456000,
  "generatedAt": "...",
  "totalTestCount": 85,
  "testsWithFailures": 12,
  "totalBuckets": 64,
  "bucketIndex": 10,
  "aggregatedFrom": ["xpcshell-2025-11-12.json", ...]
}
```

Additional fields compared to issues files:
- `totalBuckets`: Always 64
- `bucketIndex`: The bucket number (0-63) for this file
- `totalTestCount` and `testsWithFailures` are scoped to this bucket only

### tables

Same string table names as issues files, but scoped to only the strings used by tests in this bucket. One key difference:

- `jobNames` contain **chunk-stripped** base names (e.g., `"test-linux1804-64/opt-xpcshell"` instead of `"test-linux1804-64/opt-xpcshell-1"`). The `-cf` (confirm-failures) suffix is preserved: `"test-linux1804-64/opt-mochitest-browser-chrome-14-cf"` becomes `"test-linux1804-64/opt-mochitest-browser-chrome-cf"` with chunk `14`.

### taskInfo

```json
{
  "repositoryIds": [010, ...],
  "jobNameIds": [001, ...],
  "commitIds": [01, null, ...],
  "chunks": [12, null, ...]
}
```

The `chunks` array is new compared to issues files. It stores the chunk number extracted from the original job name at each taskIdId position. `null` means the job had no chunk suffix.

### testRuns Format Differences

Bucket files use three distinct status group formats depending on the status type:

#### Passing Tests (status starts with "PASS")

```json
{
  "durations": [[12341456], [128913001100], ...],
  "days": [01, ...],
  "jobNameIds": [01, ...]
}
```

- `durations[i]` is an **array of individual pass durations** (ms) for the job on that day
- `jobNameIds[i]` is the index into `tables.jobNames` for that entry
- Entries are bucketed by (day, jobNameId) — one entry per unique combination
- No `taskIdIds` or `counts` — use `durations[i].length` to get the count

#### Skip Tests (status = "SKIP")

```json
{
  "counts": [512, ...],
  "days": [01, ...],
  "jobNameIds": [01, ...],
  "messageIds": [33, ...]
}
```

- `jobNameIds[i]` identifies which job name produced these skips
- Entries are bucketed by (day, jobNameId, messageId)
- `messageIds` may be `null` for skips without a message

#### Failing Tests (FAIL, CRASH, TIMEOUT, etc.)

Same format as `xpcshell-issues-with-taskids.json`:

```json
{
  "taskIdIds": [[4567], [891256], ...],
  "days": [01, ...],
  "messageIds": [2323, ...],
  "crashSignatureIds": [55, ...],
  "minidumps": [["abc", "def"], ["ghi", null, "jkl"], ...]
}
```

Task IDs are resolved to job names via `taskInfo.jobNameIds[taskIdId]` and chunk numbers via `taskInfo.chunks[taskIdId]`.

### Example: Get durations for a specific test by job name

```javascript
const testId = 5;
const passStatusId = data.tables.statuses.findIndex(s => s.startsWith("PASS"));
const sg = data.testRuns[testId]?.[passStatusId];
if (sg?.durations) {
  for (let i = 0; i < sg.durations.length; i++) {
    const jobName = data.tables.jobNames[sg.jobNameIds[i]];
    const durations = sg.durations[i];  // Array of ms values
    console.log(`${jobName}: ${durations.length} runs, median ${durations.sort((a,b)=>a-b)[Math.floor(durations.length/2)]}ms`);
  }
}
```

[Dauer der Verarbeitung: 0.44 Sekunden]