blob: ad7c4e6e20bdc0a70bcaab9ea52b7d6ae1432f51 [file]
// 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 (
"encoding/json"
"errors"
"fmt"
"math"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"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"
)
// CoverageStats holds the line coverage metrics for a specific code segment.
type CoverageStats struct {
LinesFound int `json:"lines_found"`
LinesHit int `json:"lines_hit"`
Percentage float64 `json:"percentage"`
}
// IterationCoverage aggregates coverage statistics across different project components.
type IterationCoverage struct {
TintCore CoverageStats `json:"tint_core"`
Mesa CoverageStats `json:"mesa"`
DirectX CoverageStats `json:"directx"`
}
// 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
PerfScore float64
ActualSeconds float64
ActualRuns int
NormalizedSecs float64
Coverage IterationCoverage
}
// SummaryPoint represents a single statistical data point in a fuzzer's
// coverage trajectory, summarizing metrics across multiple iterations.
type SummaryPoint struct {
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,
}
return ac.run()
}
// analyzeConfig holds the configuration and state for the analysis task.
type analyzeConfig struct {
*taskConfig
settings ExperimentSettings
resultsDir string
machines []string
}
// 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
}
data, err := ac.gatherData()
if err != nil {
return err
}
if err := ac.printRawCSV(data); err != nil {
return err
}
stats := calculateStats(data)
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() ([]IterationData, error) {
var allData []IterationData
for _, machine := range ac.machines {
machineData, err := ac.gatherMachineData(machine)
if err != nil {
return nil, err
}
allData = append(allData, machineData...)
}
if len(allData) == 0 {
return nil, fmt.Errorf("no completed iterations found to analyze")
}
fmt.Printf("Successfully loaded coverage data for %d task iterations.\n", len(allData))
return allData, nil
}
// gatherMachineData scans a specific machine's results directory and gathers iteration data for all fuzzers.
func (ac *analyzeConfig) gatherMachineData(machine string) ([]IterationData, error) {
machineDir := filepath.Join(ac.resultsDir, machine)
var machineData []IterationData
for _, fuzzer := range ac.settings.Fuzzers {
fuzzerData, err := ac.gatherFuzzerData(machine, machineDir, fuzzer)
if err != nil {
return nil, err
}
machineData = append(machineData, fuzzerData...)
}
return machineData, nil
}
// gatherFuzzerData gathers iteration data for a fuzzer across its supported corpora.
func (ac *analyzeConfig) gatherFuzzerData(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.gatherCorpusData(machine, machineDir, fuzzer, corpus)
if err != nil {
return nil, err
}
fuzzerData = append(fuzzerData, corpusData...)
}
return fuzzerData, nil
}
// gatherCorpusData gathers iteration data for a specific fuzzer and corpus across all configured durations and iterations.
func (ac *analyzeConfig) gatherCorpusData(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.gatherIterationData(machine, machineDir, fuzzer, corpus, limitType, limitValue, limitStr, i)
if err != nil {
return nil, err
}
if iterData != nil {
corpusData = append(corpusData, *iterData)
}
}
}
return corpusData, nil
}
// gatherIterationData loads and parses the state and coverage for a single fuzzer iteration.
func (ac *analyzeConfig) gatherIterationData(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.PerfScore <= 0 {
return nil, fmt.Errorf("invalid perf score (%f) for %s/%s iter %d\n", state.PerfScore, fuzzer, corpus.Name, iter)
}
// Run or load coverage
cov, err := getOrRunCoverage(ac.taskConfig, machine, fuzzer, corpus, limitStr, iter, iterDir)
if err != nil {
return nil, fmt.Errorf("failed to get/run coverage for %s/%s iter %d: %v\n", fuzzer, corpus.Name, iter, err)
}
// Normalization: CPU Seconds = (ActualSeconds * PerfScore) / 1000
normalizedSecs := (state.ActualSeconds * state.PerfScore) / 1000.0
return &IterationData{
Machine: machine,
Fuzzer: fuzzer,
Corpus: corpus.Name,
LimitType: limitType,
LimitValue: limitValue,
Iteration: iter,
PerfScore: state.PerfScore,
ActualSeconds: state.ActualSeconds,
ActualRuns: state.ActualRuns,
NormalizedSecs: normalizedSecs,
Coverage: cov,
}, nil
}
// printRawCSV builds and writes a CSV file containing the raw metrics of each fuzzer iteration.
func (ac *analyzeConfig) printRawCSV(data []IterationData) error {
var csvBuilder strings.Builder
csvBuilder.WriteString("Machine,Fuzzer,Corpus,LimitType,LimitValue,Iteration,PerfScore,ActualSeconds,ActualRuns,NormalizedCPUSeconds," +
"TintCore_LinesFound,TintCore_LinesHit,TintCore_CoveragePercent," +
"Mesa_LinesFound,Mesa_LinesHit,Mesa_CoveragePercent," +
"DirectX_LinesFound,DirectX_LinesHit,DirectX_CoveragePercent\n")
for _, ci := range data {
csvBuilder.WriteString(fmt.Sprintf("%s,%s,%s,%s,%d,%d,%.4f,%.2f,%d,%.4f,%d,%d,%.2f,%d,%d,%.2f,%d,%d,%.2f\n",
ci.Machine,
ci.Fuzzer,
ci.Corpus,
ci.LimitType,
ci.LimitValue,
ci.Iteration,
ci.PerfScore,
ci.ActualSeconds,
ci.ActualRuns,
ci.NormalizedSecs,
ci.Coverage.TintCore.LinesFound,
ci.Coverage.TintCore.LinesHit,
ci.Coverage.TintCore.Percentage,
ci.Coverage.Mesa.LinesFound,
ci.Coverage.Mesa.LinesHit,
ci.Coverage.Mesa.Percentage,
ci.Coverage.DirectX.LinesFound,
ci.Coverage.DirectX.LinesHit,
ci.Coverage.DirectX.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) map[string][]SummaryPoint {
type accumulatorKey struct {
Fuzzer string
Corpus string
LimitType string
LimitValue int
Segment string
}
type accumulatorVal struct {
NormalizedSecs []float64
CoveragePercent []float64
}
accumulations := make(map[accumulatorKey]*accumulatorVal)
for _, d := range data {
segments := []struct {
Name string
Stats CoverageStats
}{
{"Tint Core", d.Coverage.TintCore},
{"Mesa", d.Coverage.Mesa},
{"DirectX", d.Coverage.DirectX},
}
for _, seg := range segments {
if seg.Stats.LinesFound == 0 {
continue // skip reporting empty segments (e.g. Mesa if not a Mesa fuzzer)
}
key := accumulatorKey{
Fuzzer: d.Fuzzer,
Corpus: d.Corpus,
LimitType: d.LimitType,
LimitValue: d.LimitValue,
Segment: seg.Name,
}
val, ok := accumulations[key]
if !ok {
val = &accumulatorVal{}
accumulations[key] = val
}
val.NormalizedSecs = append(val.NormalizedSecs, d.NormalizedSecs)
val.CoveragePercent = append(val.CoveragePercent, seg.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].Segment != keys[j].Segment {
return keys[i].Segment < keys[j].Segment
}
if keys[i].LimitType != keys[j].LimitType {
return keys[i].LimitType < keys[j].LimitType
}
return keys[i].LimitValue < keys[j].LimitValue
})
summaries := make(map[string][]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 {
deltaNormalizedSecs = normalizedSecsStd / math.Sqrt(float64(N))
}
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)
}
sKey := fmt.Sprintf("%s - %s (%s)", k.Fuzzer, k.Corpus, k.Segment)
summaries[sKey] = append(summaries[sKey], SummaryPoint{
LimitType: k.LimitType,
LimitValue: k.LimitValue,
NormSecsAvg: normalizedSecsAvg,
NormSecsSem: deltaNormalizedSecs,
CovAvg: coverageAvg,
CovSem: deltaCoverage,
CovRate: coverageRate,
CovRateError: deltaCoverageRate,
N: N,
})
}
return summaries
}
// printReport builds and writes a Markdown report summarizing fuzzer performance and coverage.
func (ac *analyzeConfig) printReport(summaries map[string][]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")
// Sort summaries keys
var summariesKeys []string
for k := range summaries {
summariesKeys = append(summariesKeys, k)
}
sort.Strings(summariesKeys)
for _, sk := range summariesKeys {
points := summaries[sk]
reportBuilder.WriteString(fmt.Sprintf("### %s\n\n", sk))
headers := []string{"Samples (N)", "Target Limit", "Normalized CPU Seconds (Avg ± SEM)", "Coverage % (Avg ± SEM)", "Coverage Rate (%/sec) (Avg ± SEM)"}
var rows [][]string
for _, pt := range points {
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,
})
}
reportBuilder.WriteString(formatMarkdownTable(headers, rows))
reportBuilder.WriteString("\n")
}
// 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, machine string, fuzzer string, corpus CorpusDef, limitStr string, iter int, iterDir string) (IterationCoverage, error) {
var coverage IterationCoverage
coverageDir, err := filepath.Abs(filepath.Join(t.analyzePath, "coverage", machine, fuzzer, corpus.Name, limitStr, fmt.Sprintf("iter_%d", iter)))
if err != nil {
return coverage, 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 {
if err := json.Unmarshal(covBytes, &coverage); err == nil {
return coverage, nil
}
}
}
// Ensure coverage dir exists
if err := t.osWrapper.MkdirAll(coverageDir, 0755); err != nil {
return coverage, fmt.Errorf("failed to create coverage dir: %w", err)
}
binDir, err := filepath.Abs(filepath.Join(t.analyzePath, "bin"))
if err != nil {
return coverage, err
}
// Find .profraw files inside the original iteration directory
profrawFiles, err := findProfrawFiles(iterDir)
if err != nil {
return coverage, fmt.Errorf("failed to scan for .profraw files inside %s: %w", iterDir, err)
}
if len(profrawFiles) == 0 {
return coverage, 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 coverage, err
}
if err := mergeProfrawFiles(t, profrawFiles, profdataPath); err != nil {
return coverage, err
}
fmt.Printf("Generating coverage for %s/%s (%s) iter %d...\n", fuzzer, corpus.Name, limitStr, iter)
if err := generateLcovReport(t, fuzzer, binDir, coverageDir, profdataPath); err != nil {
return coverage, err
}
// Parse generated LCOV file
lcovFile, err := findLcovFile(coverageDir)
if err != nil {
return coverage, fmt.Errorf("failed to find generated LCOV file: %w", err)
}
lcovContent, err := t.osWrapper.ReadFile(lcovFile)
if err != nil {
return coverage, fmt.Errorf("failed to read generated LCOV file: %w", err)
}
coverage = parseLcov(string(lcovContent))
// Cache result
coverageBytes, _ := json.MarshalIndent(coverage, "", " ")
_ = t.osWrapper.WriteFile(coverageJsonPath, coverageBytes, 0644)
return coverage, 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, inputs []string, output string) error {
llvmProfDataPath := filepath.Join(fileutils.DawnRoot(t.osWrapper), "third_party", "llvm-build", "Release+Asserts", "bin", "llvm-profdata"+fileutils.ExeExt)
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")
cmdArgs := []string{
scriptPath,
fuzzer,
"--no-component-view",
"-b", binDir,
"-o", output,
"--format", "lcov",
"-p", inputs,
}
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 Core, Mesa, and DirectX segments based on file paths.
func parseLcov(content string) IterationCoverage {
lines := strings.Split(content, "\n")
var currentFile string
var inTintCore, inMesa, inDirectX bool
metrics := map[string]*CoverageStats{
"tint_core": {},
"mesa": {},
"directx": {},
}
for _, line := range lines {
line = strings.TrimSpace(line)
if after, ok := strings.CutPrefix(line, "SF:"); ok {
currentFile = after
currentFile = filepath.ToSlash(currentFile)
inTintCore = strings.Contains(currentFile, "src/tint/") || strings.Contains(currentFile, "src/utils/")
inMesa = strings.Contains(currentFile, "third_party/mesa/")
inDirectX = strings.Contains(currentFile, "third_party/directx")
} else if after, ok := strings.CutPrefix(line, "LF:"); ok {
val, _ := strconv.Atoi(after)
if inTintCore {
metrics["tint_core"].LinesFound += val
}
if inMesa {
metrics["mesa"].LinesFound += val
}
if inDirectX {
metrics["directx"].LinesFound += val
}
} else if after, ok := strings.CutPrefix(line, "LH:"); ok {
val, _ := strconv.Atoi(after)
if inTintCore {
metrics["tint_core"].LinesHit += val
}
if inMesa {
metrics["mesa"].LinesHit += val
}
if inDirectX {
metrics["directx"].LinesHit += val
}
}
}
for _, stats := range metrics {
if stats.LinesFound > 0 {
stats.Percentage = (float64(stats.LinesHit) / float64(stats.LinesFound)) * 100.0
}
}
return IterationCoverage{
TintCore: *metrics["tint_core"],
Mesa: *metrics["mesa"],
DirectX: *metrics["directx"],
}
}
// 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()
}