blob: 071c0b3b55d373ed4270e4ecf5527ef7342001c1 [file] [edit]
// Copyright 2026 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"unicode/utf8"
"dawn.googlesource.com/dawn/tools/src/fileutils"
)
// Analyze mode path stems that get used multiple times
const (
kAnalyzeResultsDir = "results"
kAnalyzeReportFile = "experiment_report.md"
kAnalyzeCsvFile = "raw_iteration_data.csv"
kAnalyzeStatsCsvFile = "calculated_statistics.csv"
)
// CoverageStats holds the line coverage metrics for a specific code component.
type CoverageStats struct {
LinesFound int `json:"lines_found"`
LinesHit int `json:"lines_hit"`
Percentage float64 `json:"percentage"`
}
type CoverageComponent uint8
const (
CoverageComponentTint = iota
CoverageComponentMesa
CoverageComponentDXC
)
func (c CoverageComponent) String() string {
switch c {
case CoverageComponentTint:
return "Tint"
case CoverageComponentMesa:
return "Mesa"
case CoverageComponentDXC:
return "DXC"
default:
return "<unknown>"
}
}
// IterationCoverage aggregates coverage statistics across different project components.
type IterationCoverage map[CoverageComponent]*CoverageStats
// IterationData holds the raw performance and coverage metrics for a single fuzzer iteration.
type IterationData struct {
Machine string
Fuzzer string
Corpus string
LimitType string
LimitValue int
Iteration int
NormalizationScore float64
NormalizationError float64
ActualSeconds float64
ActualRuns int
NormalizedSecs float64
NormalizedSecsError float64
Coverage IterationCoverage
}
// SummaryPoint represents a single statistical data point in a fuzzer's
// coverage trajectory, summarizing metrics across multiple iterations.
type SummaryPoint struct {
Fuzzer string
Corpus string
Component string
LimitType string
LimitValue int
NormSecsAvg float64
NormSecsSem float64
CovAvg float64
CovSem float64
CovRate float64
CovRateError float64
N int
}
// runAnalyze entry point for running the analysis task.
func runAnalyze(t *taskConfig) error {
settings, err := loadExperimentSettings(t, t.analyzePath)
if err != nil {
return err
}
resultsDir := filepath.Join(t.analyzePath, kAnalyzeResultsDir)
if !fileutils.IsDir(resultsDir, t.osWrapper) {
return fmt.Errorf("results directory '%s' not found", resultsDir)
}
ac := &analyzeConfig{
taskConfig: t,
settings: settings,
resultsDir: resultsDir,
data: make([]IterationData, 0),
}
return ac.run()
}
// analyzeConfig holds the configuration and state for the analysis task.
type analyzeConfig struct {
*taskConfig
settings ExperimentSettings
resultsDir string
machines []string
data []IterationData
}
// run generates a Markdown performance and coverage report by analyzing
// the results of completed fuzzing iterations stored in the report path. Also
// emits full/raw stats to a .csv file for processing by other tools.
func (ac *analyzeConfig) run() error {
if err := ac.findMachines(); err != nil {
return err
}
err := ac.gatherData()
if err != nil {
return err
}
if err := ac.printRawCSV(); err != nil {
return err
}
stats := calculateStats(ac.data)
if err := ac.printStatsCSV(stats); err != nil {
return err
}
if err := ac.printReport(stats); err != nil {
return err
}
return nil
}
// findMachines scans the results directory to identify and populate the list of machine names to analyze,
// filtering by a specific machine if configured. Returns an error if no machine result can be found.
func (ac *analyzeConfig) findMachines() error {
var machines []string
if ac.machineName != "" {
mPath := filepath.Join(ac.resultsDir, ac.machineName)
if !fileutils.IsDir(mPath, ac.osWrapper) {
return fmt.Errorf("specified machine results directory '%s' not found", mPath)
}
machines = append(machines, ac.machineName)
} else {
// Scan results Dir
files, err := ac.osWrapper.ReadDir(ac.resultsDir)
if err != nil {
return fmt.Errorf("failed to read results directory: %w", err)
}
for _, f := range files {
if f.IsDir() {
machines = append(machines, f.Name())
}
}
}
if len(machines) == 0 {
return fmt.Errorf("no machine results found under '%s'", ac.resultsDir)
}
ac.machines = machines
fmt.Printf("Analyzing results from machines: %s\n", strings.Join(ac.machines, ", "))
return nil
}
// gatherData scans the results directory for each machine, loads performance
// scores, and generates a list of iteration data for analysis.
func (ac *analyzeConfig) gatherData() error {
for _, machine := range ac.machines {
machineData, err := ac.calculateMachineDataTasks(machine)
if err != nil {
return err
}
ac.data = append(ac.data, machineData...)
}
if len(ac.data) == 0 {
return fmt.Errorf("no completed iterations found to analyze")
}
err := ac.runDataTasks()
if err != nil {
return err
}
fmt.Printf("Successfully loaded coverage data for %d task iterations.\n", len(ac.data))
return nil
}
// runDataTasks executes all the pending coverage tasks to populate the iteration data
func (ac *analyzeConfig) runDataTasks() error {
fmt.Printf("Processing coverage data for %d task iterations using %d parallel jobs...\n", len(ac.data), ac.numProcesses)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var errs []error
var errsMutex sync.Mutex
dataChan := make(chan *IterationData, len(ac.data))
for i := range ac.data {
dataChan <- &ac.data[i]
}
close(dataChan)
var wg sync.WaitGroup
for i := 0; i < ac.numProcesses; i++ {
wg.Go(func() {
for {
select {
case <-ctx.Done():
return
default:
}
select {
case <-ctx.Done():
return
case d, ok := <-dataChan:
if !ok {
return
}
if err := getOrRunCoverage(ac.taskConfig, d); err != nil {
errsMutex.Lock()
errs = append(errs, err)
errsMutex.Unlock()
cancel()
return
}
}
}
})
}
wg.Wait()
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
// calculateMachineDataTasks calculates tasks for gathering coverage data for all the iterations run by a machine.
func (ac *analyzeConfig) calculateMachineDataTasks(machine string) ([]IterationData, error) {
machineDir := filepath.Join(ac.resultsDir, machine)
var machineData []IterationData
for _, fuzzer := range ac.settings.Fuzzers {
fuzzerData, err := ac.calculateFuzzerDataTasks(machine, machineDir, fuzzer)
if err != nil {
return nil, err
}
machineData = append(machineData, fuzzerData...)
}
return machineData, nil
}
// calculateFuzzerDataTasks calculates tasks for gathering coverage data for a fuzzer across its supported corpora.
func (ac *analyzeConfig) calculateFuzzerDataTasks(machine, machineDir, fuzzer string) ([]IterationData, error) {
var corpora []CorpusDef
cfg, ok := fuzzerConfigs[fuzzer]
if !ok {
return nil, fmt.Errorf("unknown fuzzer: %s", fuzzer)
}
switch cfg.mode {
case FuzzModeWgsl:
corpora = ac.settings.WgslCorpora
case FuzzModeIr:
corpora = ac.settings.IrCorpora
default:
return nil, fmt.Errorf("unknown fuzz mode %d", cfg.mode)
}
var fuzzerData []IterationData
for _, corpus := range corpora {
corpusData, err := ac.calculateCorpusDataTasks(machine, machineDir, fuzzer, corpus)
if err != nil {
return nil, err
}
fuzzerData = append(fuzzerData, corpusData...)
}
return fuzzerData, nil
}
// calculateCorpusDataTasks calculates tasks for gathering coverage data for a specific fuzzer and corpus across all configured durations and iterations.
func (ac *analyzeConfig) calculateCorpusDataTasks(machine, machineDir, fuzzer string, corpus CorpusDef) ([]IterationData, error) {
var corpusData []IterationData
for _, dDef := range ac.settings.Durations {
limitType := "seconds"
limitValue := 0
if dDef.Seconds != nil {
limitType = "seconds"
limitValue = *dDef.Seconds
} else if dDef.Runs != nil {
limitType = "runs"
limitValue = *dDef.Runs
}
iterations := ac.settings.DefaultIterations
if dDef.Iterations != nil {
iterations = *dDef.Iterations
}
limitStr := fmt.Sprintf("%s_%d", limitType, limitValue)
for i := 1; i <= iterations; i++ {
iterData, err := ac.calculateIterationCoverageTasks(machine, machineDir, fuzzer, corpus, limitType, limitValue, limitStr, i)
if err != nil {
return nil, err
}
if iterData != nil {
corpusData = append(corpusData, *iterData)
}
}
}
return corpusData, nil
}
// calculateIterationCoverageTasks calculates a task for gathering coverage data for a single fuzzer iteration.
func (ac *analyzeConfig) calculateIterationCoverageTasks(machine, machineDir, fuzzer string, corpus CorpusDef, limitType string, limitValue int, limitStr string, iter int) (*IterationData, error) {
iterDir := filepath.Join(machineDir, fuzzer, corpus.Name, limitStr, fmt.Sprintf("iter_%d", iter))
statePath := filepath.Join(iterDir, "state.json")
stateBytes, err := ac.osWrapper.ReadFile(statePath)
if err != nil {
return nil, err
}
var state IterState
if err := json.Unmarshal(stateBytes, &state); err != nil {
return nil, err
}
if state.Status != "completed" {
return nil, fmt.Errorf("incomplete run for %s/%s iter %d\n", fuzzer, corpus.Name, iter)
}
if state.NormalizationScore <= 0 {
return nil, fmt.Errorf("invalid normalization score (%f) for %s/%s iter %d\n", state.NormalizationScore, fuzzer, corpus.Name, iter)
}
// Normalization: CPU Seconds = (ActualSeconds * NormalizationScore) / 1000
normalizedSecs := (state.ActualSeconds * state.NormalizationScore) / 1000.0
normalizedSecsErr := (state.ActualSeconds * state.NormalizationError) / 1000.0
return &IterationData{
Machine: machine,
Fuzzer: fuzzer,
Corpus: corpus.Name,
LimitType: limitType,
LimitValue: limitValue,
Iteration: iter,
NormalizationScore: state.NormalizationScore,
NormalizationError: state.NormalizationError,
ActualSeconds: state.ActualSeconds,
ActualRuns: state.ActualRuns,
NormalizedSecs: normalizedSecs,
NormalizedSecsError: normalizedSecsErr,
}, nil
}
// printRawCSV builds and writes a CSV file containing the raw metrics of each fuzzer iteration.
func (ac *analyzeConfig) printRawCSV() error {
var csvBuilder strings.Builder
csvBuilder.WriteString("Machine,Fuzzer,Corpus,LimitType,LimitValue,Iteration,NormalizationScore,NormalizationError,ActualSeconds,ActualRuns,NormalizedCPUSeconds,NormalizedCPUSecondsError," +
"Tint_LinesFound,Tint_LinesHit,Tint_CoveragePercent," +
"Mesa_LinesFound,Mesa_LinesHit,Mesa_CoveragePercent," +
"DXC_LinesFound,DXC_LinesHit,DXC_CoveragePercent\n")
for _, d := range ac.data {
csvBuilder.WriteString(fmt.Sprintf("%s,%s,%s,%s,%d,%d,%.4f,%.4f,%.2f,%d,%.4f,%.4f,%d,%d,%.2f,%d,%d,%.2f,%d,%d,%.2f\n",
d.Machine,
d.Fuzzer,
d.Corpus,
d.LimitType,
d.LimitValue,
d.Iteration,
d.NormalizationScore,
d.NormalizationError,
d.ActualSeconds,
d.ActualRuns,
d.NormalizedSecs,
d.NormalizedSecsError,
d.Coverage[CoverageComponentTint].LinesFound,
d.Coverage[CoverageComponentTint].LinesHit,
d.Coverage[CoverageComponentTint].Percentage,
d.Coverage[CoverageComponentMesa].LinesFound,
d.Coverage[CoverageComponentMesa].LinesHit,
d.Coverage[CoverageComponentMesa].Percentage,
d.Coverage[CoverageComponentDXC].LinesFound,
d.Coverage[CoverageComponentDXC].LinesHit,
d.Coverage[CoverageComponentDXC].Percentage,
))
}
csvFile := filepath.Join(ac.analyzePath, kAnalyzeCsvFile)
if err := ac.osWrapper.WriteFile(csvFile, []byte(csvBuilder.String()), 0644); err != nil {
return fmt.Errorf("failed to write raw CSV file: %w", err)
}
fmt.Printf("Raw iteration metrics exported successfully to: %s\n", csvFile)
return nil
}
// calculateStats computes statistical summaries for coverage percentages,
// normalized CPU seconds, and coverage rates across fuzzer runs.
func calculateStats(data []IterationData) []SummaryPoint {
type accumulatorKey struct {
Fuzzer string
Corpus string
LimitType string
LimitValue int
Component string
}
type accumulatorVal struct {
NormalizedSecs []float64
NormalizedSecsErr []float64
CoveragePercent []float64
}
accumulations := make(map[accumulatorKey]*accumulatorVal)
for _, d := range data {
for comp, stats := range d.Coverage {
if stats.LinesFound == 0 {
continue // skip reporting empty components (e.g. Mesa if not a Mesa fuzzer)
}
key := accumulatorKey{
Fuzzer: d.Fuzzer,
Corpus: d.Corpus,
LimitType: d.LimitType,
LimitValue: d.LimitValue,
Component: comp.String(),
}
val, ok := accumulations[key]
if !ok {
val = &accumulatorVal{}
accumulations[key] = val
}
val.NormalizedSecs = append(val.NormalizedSecs, d.NormalizedSecs)
val.NormalizedSecsErr = append(val.NormalizedSecsErr, d.NormalizedSecsError)
val.CoveragePercent = append(val.CoveragePercent, stats.Percentage)
}
}
// Sort keys for deterministic output
var keys []accumulatorKey
for k := range accumulations {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].Fuzzer != keys[j].Fuzzer {
return keys[i].Fuzzer < keys[j].Fuzzer
}
if keys[i].Corpus != keys[j].Corpus {
return keys[i].Corpus < keys[j].Corpus
}
if keys[i].Component != keys[j].Component {
return keys[i].Component < keys[j].Component
}
if keys[i].LimitType != keys[j].LimitType {
return keys[i].LimitType < keys[j].LimitType
}
return keys[i].LimitValue < keys[j].LimitValue
})
var summaries []SummaryPoint
for _, k := range keys {
val := accumulations[k]
coverageAvg, coverageStd := computeAvgAndStdDev(val.CoveragePercent)
normalizedSecsAvg, normalizedSecsStd := computeAvgAndStdDev(val.NormalizedSecs)
N := len(val.CoveragePercent)
// deltaX is the Standard Error of Mean for a value X.
// δX = σC / sqrt(N), where σFoo is the standard deviation of Foo, and N is the number of samples of Foo.
deltaCoverage := 0.0
if N > 0 {
deltaCoverage = coverageStd / math.Sqrt(float64(N))
}
deltaNormalizedSecs := 0.0
if N > 0 {
statErr := normalizedSecsStd / math.Sqrt(float64(N))
// Calculate systematic error from NormalizationScore
sysErrSum := 0.0
for i := range N {
sysErrSum += val.NormalizedSecsErr[i]
}
sysErr := sysErrSum / float64(N)
deltaNormalizedSecs = math.Sqrt(statErr*statErr + sysErr*sysErr)
}
coverageRate := 0.0
if normalizedSecsAvg > 0 {
coverageRate = coverageAvg / normalizedSecsAvg
}
// Quadrature error propagation for R, rate of a value X
// R = X / Time
// δR = R * sqrt((δX / X)^2 + (δT / T)^2)
deltaCoverageRate := 0.0
if coverageRate > 0 && coverageAvg > 0 && normalizedSecsAvg > 0 {
relativeDeltaCoverage := deltaCoverage / coverageAvg
relativeDeltaNormalizedSecs := deltaNormalizedSecs / normalizedSecsAvg
deltaCoverageRate = coverageRate * math.Sqrt(relativeDeltaCoverage*relativeDeltaCoverage+relativeDeltaNormalizedSecs*relativeDeltaNormalizedSecs)
}
summaries = append(summaries, SummaryPoint{
Fuzzer: k.Fuzzer,
Corpus: k.Corpus,
Component: k.Component,
LimitType: k.LimitType,
LimitValue: k.LimitValue,
NormSecsAvg: normalizedSecsAvg,
NormSecsSem: deltaNormalizedSecs,
CovAvg: coverageAvg,
CovSem: deltaCoverage,
CovRate: coverageRate,
CovRateError: deltaCoverageRate,
N: N,
})
}
return summaries
}
// printStatsCSV builds and writes a CSV file containing the calculated statistics.
func (ac *analyzeConfig) printStatsCSV(stats []SummaryPoint) error {
var csvBuilder strings.Builder
csvBuilder.WriteString("Fuzzer,Corpus,Component,Samples,LimitType,LimitValue,NormalizedCPUSecondsAvg,NormalizedCPUSecondsSEM,CoveragePercentAvg,CoveragePercentSEM,CoverageRateAvg,CoverageRateSEM\n")
for _, pt := range stats {
csvBuilder.WriteString(fmt.Sprintf("%s,%s,%s,%d,%s,%d,%.4f,%.4f,%.2f,%.2f,%.6f,%.6f\n",
pt.Fuzzer,
pt.Corpus,
pt.Component,
pt.N,
pt.LimitType,
pt.LimitValue,
pt.NormSecsAvg,
pt.NormSecsSem,
pt.CovAvg,
pt.CovSem,
pt.CovRate,
pt.CovRateError,
))
}
csvFile := filepath.Join(ac.analyzePath, kAnalyzeStatsCsvFile)
if err := ac.osWrapper.WriteFile(csvFile, []byte(csvBuilder.String()), 0644); err != nil {
return fmt.Errorf("failed to write calculated statistics CSV file: %w", err)
}
fmt.Printf("Calculated statistics exported successfully to: %s\n", csvFile)
return nil
}
// printReport builds and writes a Markdown report summarizing fuzzer performance and coverage.
func (ac *analyzeConfig) printReport(stats []SummaryPoint) error {
var reportBuilder strings.Builder
reportBuilder.WriteString(fmt.Sprintf("# Experiment Performance and Coverage Report: %s\n\n", ac.settings.Name))
reportBuilder.WriteString(fmt.Sprintf("- **Git Hash**: `%s`\n", ac.settings.Hash))
if ac.machineName != "" {
reportBuilder.WriteString(fmt.Sprintf("- **Target Machine**: `%s`\n", ac.machineName))
} else {
reportBuilder.WriteString("- **Machines Merged**:\n")
for _, m := range ac.machines {
reportBuilder.WriteString(fmt.Sprintf(" - `%s`\n", m))
}
}
reportBuilder.WriteString("\n## Detailed Coverage Summaries\n\n")
headers := []string{"Samples (N)", "Target Limit", "Normalized CPU Seconds (Avg ± SEM)", "Coverage % (Avg ± SEM)", "Coverage Rate (%/sec) (Avg ± SEM)"}
var currentGroup string
var rows [][]string
flushTable := func() {
if len(rows) > 0 {
reportBuilder.WriteString(fmt.Sprintf("### %s\n\n", currentGroup))
reportBuilder.WriteString(formatMarkdownTable(headers, rows))
reportBuilder.WriteString("\n")
rows = nil
}
}
for _, pt := range stats {
group := fmt.Sprintf("%s - %s (%s)", pt.Fuzzer, pt.Corpus, pt.Component)
if group != currentGroup {
flushTable()
currentGroup = group
}
limitStr := fmt.Sprintf("%d %s", pt.LimitValue, pt.LimitType)
normSecsStr := fmt.Sprintf("%.2f ± %.2f", pt.NormSecsAvg, pt.NormSecsSem)
covStr := fmt.Sprintf("%.2f%% ± %.2f%%", pt.CovAvg, pt.CovSem)
covRateStr := fmt.Sprintf("%.6f ± %.6f", pt.CovRate, pt.CovRateError)
nStr := fmt.Sprintf("%d", pt.N)
rows = append(rows, []string{
nStr,
limitStr,
normSecsStr,
covStr,
covRateStr,
})
}
flushTable()
// Save Report File
reportFile := filepath.Join(ac.analyzePath, kAnalyzeReportFile)
if err := ac.osWrapper.WriteFile(reportFile, []byte(reportBuilder.String()), 0644); err != nil {
return fmt.Errorf("failed to write report file: %w", err)
}
fmt.Printf("\nReport generated successfully at: %s\n", reportFile)
fmt.Println("\n--- SUMMARY REPORT ---")
fmt.Println(reportBuilder.String()[:min(1500, len(reportBuilder.String()))] + "...\n[Report truncated in output]")
fmt.Println("----------------------")
return nil
}
// getOrRunCoverage retrieves calculated coverage data for an iteration or executes
// the coverage collection if the data isn't present.
func getOrRunCoverage(t *taskConfig, d *IterationData) error {
limitStr := fmt.Sprintf("%s_%d", d.LimitType, d.LimitValue)
iterDir := filepath.Join(t.analyzePath, kAnalyzeResultsDir, d.Machine, d.Fuzzer, d.Corpus, limitStr, fmt.Sprintf("iter_%d", d.Iteration))
coverageDir, err := filepath.Abs(filepath.Join(t.analyzePath, "coverage", d.Machine, d.Fuzzer, d.Corpus, limitStr, fmt.Sprintf("iter_%d", d.Iteration)))
if err != nil {
return err
}
coverageJsonPath := filepath.Join(coverageDir, "coverage.json")
// If coverage already exists, load and return
if fileutils.IsFile(coverageJsonPath, t.osWrapper) {
covBytes, err := t.osWrapper.ReadFile(coverageJsonPath)
if err == nil {
var coverage IterationCoverage
if err := json.Unmarshal(covBytes, &coverage); err == nil {
d.Coverage = coverage
return nil
}
}
}
// Ensure coverage dir exists
if err := t.osWrapper.MkdirAll(coverageDir, 0755); err != nil {
return fmt.Errorf("failed to create coverage dir: %w", err)
}
binDir, err := filepath.Abs(filepath.Join(t.analyzePath, "bin"))
if err != nil {
return err
}
// Find .profraw files inside the original iteration directory
profrawFiles, err := findProfrawFiles(iterDir)
if err != nil {
return fmt.Errorf("failed to scan for .profraw files inside %s: %w", iterDir, err)
}
if len(profrawFiles) == 0 {
return fmt.Errorf("no .profraw file found inside iteration directory '%s' (make sure the experiment ran with coverage enabled)", iterDir)
}
profdataPath, err := filepath.Abs(filepath.Join(iterDir, "coverage.profdata"))
if err != nil {
return err
}
if err := mergeProfrawFiles(t, binDir, profrawFiles, profdataPath); err != nil {
return err
}
fmt.Printf("Generating coverage for %s/%s/%s (%s) iter %d...\n", d.Machine, d.Fuzzer, d.Corpus, limitStr, d.Iteration)
if err := generateLcovReport(t, d.Fuzzer, binDir, coverageDir, profdataPath); err != nil {
return err
}
// Parse generated LCOV file
lcovFile, err := findLcovFile(coverageDir)
if err != nil {
return fmt.Errorf("failed to find generated LCOV file: %w", err)
}
lcovContent, err := t.osWrapper.ReadFile(lcovFile)
if err != nil {
return fmt.Errorf("failed to read generated LCOV file: %w", err)
}
d.Coverage = parseLcov(string(lcovContent))
// Cache result
coverageBytes, _ := json.MarshalIndent(d.Coverage, "", " ")
_ = t.osWrapper.WriteFile(coverageJsonPath, coverageBytes, 0644)
return nil
}
// findProfrawFiles recursively searches for and returns a list of all .profraw files within the specified directory.
func findProfrawFiles(dir string) ([]string, error) {
var files []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && filepath.Ext(path) == ".profraw" {
files = append(files, path)
}
return nil
})
return files, err
}
// mergeProfrawFiles combines multiple .profraw files into a single sparse .profdata file using llvm-profdata.
func mergeProfrawFiles(t *taskConfig, binDir string, inputs []string, output string) error {
llvmProfDataPath := filepath.Join(binDir, "llvm-profdata"+fileutils.ExeExt)
if !fileutils.IsExe(llvmProfDataPath, t.osWrapper) {
return fmt.Errorf("hermetic LLVM tool 'llvm-profdata' not found in experiment bin directory: %s", llvmProfDataPath)
}
mergeArgs := []string{"merge", "-o", output, "-sparse=true"}
mergeArgs = append(mergeArgs, inputs...)
if _, err := t.runCmd(llvmProfDataPath, mergeArgs...); err != nil {
return fmt.Errorf("failed to merge .profraw files using %s: %w", llvmProfDataPath, err)
}
return nil
}
// generateLcovReport runs coverage.py on a pre-generated coverage.profdata file.
func generateLcovReport(t *taskConfig, fuzzer string, binDir string, output string, inputs string) error {
scriptPath := filepath.Join(fileutils.DawnRoot(t.osWrapper), "tools", "code_coverage", "coverage.py")
llvmCovPath := filepath.Join(binDir, "llvm-cov"+fileutils.ExeExt)
if !fileutils.IsExe(llvmCovPath, t.osWrapper) {
return fmt.Errorf("hermetic LLVM tool 'llvm-cov' not found in experiment bin directory: %s", llvmCovPath)
}
cmdArgs := []string{
scriptPath,
fuzzer,
"--no-component-view",
"-b", binDir,
"-o", output,
"--format", "lcov",
"-p", inputs,
"--coverage-tools-dir", binDir,
}
if _, err := t.runCmd("vpython3", cmdArgs...); err != nil {
return fmt.Errorf("failed to execute coverage.py: %w", err)
}
return nil
}
// findLcovFile searches for the first .lcov file within the specified directory.
func findLcovFile(dir string) (string, error) {
var foundPath string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && filepath.Ext(path) == ".lcov" {
foundPath = path
return filepath.SkipAll // Stop searching
}
return nil
})
if err != nil && !errors.Is(err, filepath.SkipAll) {
return "", err
}
if foundPath == "" {
return "", fmt.Errorf("no LCOV file found in %s", dir)
}
return foundPath, nil
}
// parseLcov parses the contents of an LCOV file and categorizes coverage metrics
// into Tint, Mesa, and DXC components based on file paths.
func parseLcov(content string) IterationCoverage {
lines := strings.Split(content, "\n")
var currentFile string
currentlyIn := map[CoverageComponent]bool{
CoverageComponentTint: false,
CoverageComponentMesa: false,
CoverageComponentDXC: false,
}
metrics := IterationCoverage{
CoverageComponentTint: {},
CoverageComponentMesa: {},
CoverageComponentDXC: {},
}
for _, line := range lines {
line = strings.TrimSpace(line)
if after, ok := strings.CutPrefix(line, "SF:"); ok {
currentFile = after
currentFile = filepath.ToSlash(currentFile)
currentlyIn[CoverageComponentTint] = strings.Contains(currentFile, "src/tint/") || strings.Contains(currentFile, "src/utils/")
currentlyIn[CoverageComponentMesa] = strings.Contains(currentFile, "third_party/mesa/")
currentlyIn[CoverageComponentDXC] = strings.Contains(currentFile, "third_party/directx")
} else if after, ok := strings.CutPrefix(line, "LF:"); ok {
val, _ := strconv.Atoi(after)
for comp, in := range currentlyIn {
if in {
metrics[comp].LinesFound += val
}
}
} else if after, ok := strings.CutPrefix(line, "LH:"); ok {
val, _ := strconv.Atoi(after)
for comp, in := range currentlyIn {
if in {
metrics[comp].LinesHit += val
}
}
}
}
for _, stats := range metrics {
if stats.LinesFound > 0 {
stats.Percentage = (float64(stats.LinesHit) / float64(stats.LinesFound)) * 100.0
}
}
return metrics
}
// computeAvgAndStdDev calculates the arithmetic mean and sample standard deviation for a slice of float64.
func computeAvgAndStdDev(vals []float64) (avg float64, sd float64) {
if len(vals) == 0 {
return 0, 0
}
sum := 0.0
for _, v := range vals {
sum += v
}
avg = sum / float64(len(vals))
if len(vals) < 2 {
return avg, 0
}
sqSum := 0.0
for _, v := range vals {
diff := v - avg
sqSum += diff * diff
}
variance := sqSum / float64(len(vals)-1)
sd = math.Sqrt(variance)
return avg, sd
}
func formatMarkdownTable(headers []string, rows [][]string) string {
numCols := len(headers)
colWidths := make([]int, numCols)
// Calculate column widths from headers
for i, h := range headers {
colWidths[i] = utf8.RuneCountInString(h)
}
// Calculate column widths from rows
for _, row := range rows {
for i, val := range row {
valLen := utf8.RuneCountInString(val)
if i < numCols && valLen > colWidths[i] {
colWidths[i] = valLen
}
}
}
var sb strings.Builder
// Write header row
sb.WriteString("|")
for i, h := range headers {
padding := colWidths[i] - utf8.RuneCountInString(h)
sb.WriteString(" " + h + strings.Repeat(" ", padding) + " |")
}
sb.WriteString("\n")
// Write separator row
sb.WriteString("|")
for i := range numCols {
sb.WriteString(" " + strings.Repeat("-", colWidths[i]) + " |")
}
sb.WriteString("\n")
// Write data rows
for _, row := range rows {
sb.WriteString("|")
for i, val := range row {
padding := 0
if i < numCols {
padding = colWidths[i] - utf8.RuneCountInString(val)
}
sb.WriteString(" " + val + strings.Repeat(" ", padding) + " |")
}
sb.WriteString("\n")
}
return sb.String()
}