github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/tools/syz-testbed/table.go (about)

     1  // Copyright 2021 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package main
     5  
     6  import (
     7  	"encoding/csv"
     8  	"fmt"
     9  	"math"
    10  	"os"
    11  	"sort"
    12  
    13  	"github.com/google/syzkaller/pkg/stats"
    14  )
    15  
    16  type Cell = interface{}
    17  
    18  // All tables that syz-testbed generates have named columns and rows.
    19  // Table type simplifies generation and processing of such tables.
    20  type Table struct {
    21  	TopLeftHeader string
    22  	ColumnHeaders []string
    23  	Cells         map[string]map[string]Cell
    24  }
    25  
    26  type ValueCell struct {
    27  	Value         float64
    28  	Sample        *stats.Sample
    29  	PercentChange *float64
    30  	PValue        *float64
    31  }
    32  
    33  type RatioCell struct {
    34  	TrueCount  int
    35  	TotalCount int
    36  }
    37  
    38  type BoolCell struct {
    39  	Value bool
    40  }
    41  
    42  func NewValueCell(sample *stats.Sample) *ValueCell {
    43  	return &ValueCell{Value: sample.Median(), Sample: sample}
    44  }
    45  
    46  func (c *ValueCell) String() string {
    47  	const fractionCutoff = 100
    48  	if math.Abs(c.Value) < fractionCutoff {
    49  		return fmt.Sprintf("%.1f", c.Value)
    50  	}
    51  	return fmt.Sprintf("%.0f", math.Round(c.Value))
    52  }
    53  
    54  func NewRatioCell(trueCount, totalCount int) *RatioCell {
    55  	return &RatioCell{trueCount, totalCount}
    56  }
    57  
    58  func (c *RatioCell) Float64() float64 {
    59  	if c.TotalCount == 0 {
    60  		return 0
    61  	}
    62  	return float64(c.TrueCount) / float64(c.TotalCount)
    63  }
    64  
    65  func (c *RatioCell) String() string {
    66  	return fmt.Sprintf("%.1f%% (%d/%d)", c.Float64()*100.0, c.TrueCount, c.TotalCount)
    67  }
    68  
    69  func NewBoolCell(value bool) *BoolCell {
    70  	return &BoolCell{
    71  		Value: value,
    72  	}
    73  }
    74  
    75  func (c *BoolCell) String() string {
    76  	if c.Value {
    77  		return "YES"
    78  	}
    79  	return "NO"
    80  }
    81  
    82  func NewTable(topLeft string, columns ...string) *Table {
    83  	return &Table{
    84  		TopLeftHeader: topLeft,
    85  		ColumnHeaders: columns,
    86  	}
    87  }
    88  
    89  func (t *Table) Get(row, column string) Cell {
    90  	if t.Cells == nil {
    91  		return nil
    92  	}
    93  	rowMap := t.Cells[row]
    94  	if rowMap == nil {
    95  		return nil
    96  	}
    97  	return rowMap[column]
    98  }
    99  
   100  func (t *Table) Set(row, column string, value Cell) {
   101  	if t.Cells == nil {
   102  		t.Cells = make(map[string]map[string]Cell)
   103  	}
   104  	rowMap, ok := t.Cells[row]
   105  	if !ok {
   106  		rowMap = make(map[string]Cell)
   107  		t.Cells[row] = rowMap
   108  	}
   109  	rowMap[column] = value
   110  }
   111  
   112  func (t *Table) AddColumn(column string) {
   113  	t.ColumnHeaders = append(t.ColumnHeaders, column)
   114  }
   115  
   116  func (t *Table) AddRow(row string, cells ...Cell) {
   117  	if len(cells) != len(t.ColumnHeaders) {
   118  		panic("AddRow: the length of the row does not equal the number of columns")
   119  	}
   120  	for i, col := range t.ColumnHeaders {
   121  		t.Set(row, col, cells[i])
   122  	}
   123  }
   124  
   125  func (t *Table) SortedRows() []string {
   126  	rows := []string{}
   127  	for key := range t.Cells {
   128  		rows = append(rows, key)
   129  	}
   130  	sort.Strings(rows)
   131  	return rows
   132  }
   133  
   134  func (t *Table) ToStrings() [][]string {
   135  	table := [][]string{}
   136  	headers := append([]string{t.TopLeftHeader}, t.ColumnHeaders...)
   137  	table = append(table, headers)
   138  	if t.Cells != nil {
   139  		rowHeaders := t.SortedRows()
   140  		for _, row := range rowHeaders {
   141  			tableRow := []string{row}
   142  			for _, column := range t.ColumnHeaders {
   143  				tableRow = append(tableRow, fmt.Sprintf("%s", t.Get(row, column)))
   144  			}
   145  			table = append(table, tableRow)
   146  		}
   147  	}
   148  	return table
   149  }
   150  
   151  func (t *Table) SaveAsCsv(fileName string) error {
   152  	f, err := os.Create(fileName)
   153  	if err != nil {
   154  		return err
   155  	}
   156  	defer f.Close()
   157  	return csv.NewWriter(f).WriteAll(t.ToStrings())
   158  }
   159  
   160  func (t *Table) SetRelativeValues(baseColumn string) error {
   161  	for rowName, row := range t.Cells {
   162  		baseCell := t.Get(rowName, baseColumn)
   163  		if baseCell == nil {
   164  			continue
   165  		}
   166  		baseValueCell, ok := baseCell.(*ValueCell)
   167  		if !ok {
   168  			return fmt.Errorf("base column cell is not a ValueCell, %T", baseCell)
   169  		}
   170  		baseSample := baseValueCell.Sample.RemoveOutliers()
   171  		for column, cell := range row {
   172  			if column == baseColumn {
   173  				continue
   174  			}
   175  			valueCell, ok := cell.(*ValueCell)
   176  			if !ok {
   177  				continue
   178  			}
   179  			if baseValueCell.Value != 0 {
   180  				valueDiff := valueCell.Value - baseValueCell.Value
   181  				valueCell.PercentChange = new(float64)
   182  				*valueCell.PercentChange = valueDiff / baseValueCell.Value * 100
   183  			}
   184  
   185  			cellSample := valueCell.Sample.RemoveOutliers()
   186  			pval, err := stats.UTest(baseSample, cellSample)
   187  			if err == nil {
   188  				// Sometimes it fails because there are too few samples.
   189  				valueCell.PValue = new(float64)
   190  				*valueCell.PValue = pval
   191  			}
   192  		}
   193  	}
   194  	return nil
   195  }
   196  
   197  func (t *Table) GetFooterValue(column string) Cell {
   198  	nonEmptyCells := 0
   199  	ratioCells := []*RatioCell{}
   200  	boolCells := []*BoolCell{}
   201  	valueCells := []*ValueCell{}
   202  	for rowName := range t.Cells {
   203  		cell := t.Get(rowName, column)
   204  		if cell == nil {
   205  			continue
   206  		}
   207  		nonEmptyCells++
   208  
   209  		switch v := cell.(type) {
   210  		case *RatioCell:
   211  			ratioCells = append(ratioCells, v)
   212  		case *BoolCell:
   213  			boolCells = append(boolCells, v)
   214  		case *ValueCell:
   215  			valueCells = append(valueCells, v)
   216  		}
   217  	}
   218  	if nonEmptyCells == 0 {
   219  		return ""
   220  	}
   221  	switch nonEmptyCells {
   222  	case len(ratioCells):
   223  		var sum, count float64
   224  		for _, cell := range ratioCells {
   225  			sum += cell.Float64()
   226  			count++
   227  		}
   228  		return fmt.Sprintf("%.1f%%", sum/count*100.0)
   229  	case len(valueCells):
   230  		var sum, count float64
   231  		for _, cell := range valueCells {
   232  			sum += cell.Value
   233  			count++
   234  		}
   235  		return fmt.Sprintf("%.1f", sum/count)
   236  	case len(boolCells):
   237  		yes := 0
   238  		for _, cell := range boolCells {
   239  			if cell.Value {
   240  				yes++
   241  			}
   242  		}
   243  		return NewRatioCell(yes, len(t.Cells))
   244  	default:
   245  		// Column has mixed type cells, we cannot do anything here.
   246  		return ""
   247  	}
   248  }