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 }