blob: f81b70bff25f82afe52db4f93c364d563ea6bf3c [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package vdltest
import (
"fmt"
"io"
"sort"
"strconv"
"strings"
"unicode/utf8"
"v.io/v23/vdl"
)
// statsTable is a table of statistics, represented as a sequence of rows. Each
// row has a map of columns for easy updating.
type statsTable struct {
Rows []statRow
}
// statsTableInfo holds info used for nice formatting.
type statsTableInfo struct {
Headers []string // Sorted column headers
RowNameSize int // Size of row.Name column
ColumnSizes statColumns // Size of each data column
}
// ColumnSize returns the size of the column with the given header.
func (info statsTableInfo) ColumnSize(header string) int {
total := 0
for index, size := range info.ColumnSizes.Map[header] {
if index > 0 {
total += 1 // account for comma between each value
}
total += size
}
return total
}
func (t *statsTable) computeInfo() statsTableInfo {
var info statsTableInfo
// Gather max size information over all values in the table.
for _, row := range t.Rows {
if size := len(row.Name); size > info.RowNameSize {
info.RowNameSize = size
}
for header, slice := range row.Columns.Map {
for index, value := range slice {
// Update ColumnSizes with the size of the largest printed value.
old := info.ColumnSizes.Get(header, index)
if size := len(strconv.Itoa(value)); size > old {
info.ColumnSizes.Set(header, index, size)
}
}
}
}
// Collect Headers for sorting, and adjust ColumnSizes if necessary.
for header, sizes := range info.ColumnSizes.Map {
info.Headers = append(info.Headers, header)
// If the header size is bigger than the computed column size, add padding
// to ColumnSizes so that the table aligns correctly. Spread the padding
// evenly across each size, with leftover added to the first size.
//
// There's no need to adjust anything if the header size is smaller than
// what's computed; Print takes care of that.
//
// Note that we only trim spaces from the header to determine its size, but
// not to perform lookups. It's useful to use leading spaces in headers to
// force a specific header ordering, so we sort and perform lookups with the
// spaces intact, but remove them when printing.
if pad := len(strings.TrimSpace(header)) - info.ColumnSize(header); pad > 0 {
total, padPerSize := 0, pad/len(sizes)
for index := range sizes {
sizes[index] += padPerSize
total += padPerSize
}
sizes[0] += pad - total
}
}
sort.Strings(info.Headers)
return info
}
func centerString(s string, size int) string {
if len(s) >= size {
return s
}
center := strings.Repeat(" ", (size-len(s))/2)
center += s
center += strings.Repeat(" ", size-len(center))
return center
}
func fp(w io.Writer, format string, args ...interface{}) error {
_, err := fmt.Fprintf(w, format, args...)
return err
}
// Print prints t into a nicely formatted table.
func (t *statsTable) Print(w io.Writer) error {
info := t.computeInfo()
// Print column headers.
if err := t.printRow(w, info, statRowBreak); err != nil {
return err
}
if err := fp(w, "|"+strings.Repeat(" ", info.RowNameSize)); err != nil {
return err
}
for _, header := range info.Headers {
// Just like in computeInfo, we only trim spaces from the header for
// printing, but leave them intact for lookups.
centered := centerString(strings.TrimSpace(header), info.ColumnSize(header))
if err := fp(w, "|"+centered); err != nil {
return err
}
}
if err := fp(w, "|\n"); err != nil {
return err
}
// Print each row.
if err := t.printRow(w, info, statRowBreak); err != nil {
return err
}
for _, row := range t.Rows {
if err := t.printRow(w, info, row); err != nil {
return err
}
}
return t.printRow(w, info, statRowBreak)
}
func (t *statsTable) printRow(w io.Writer, info statsTableInfo, row statRow) error {
if row.IsBreak() {
// Print row break.
total := info.RowNameSize + 2 // +2 for table borders
for _, header := range info.Headers {
total += info.ColumnSize(header) + 1 // +1 for column borders
}
return fp(w, strings.Repeat("-", total)+"\n")
}
// Print each column.
if err := fp(w, "|%-[1]*[2]s", info.RowNameSize, row.Name); err != nil {
return err
}
for _, header := range info.Headers {
if err := fp(w, "|"); err != nil {
return err
}
for index, size := range info.ColumnSizes.Map[header] {
if index > 0 {
if err := fp(w, ","); err != nil {
return err
}
}
if err := fp(w, "%[1]*[2]d", size, row.Columns.Get(header, index)); err != nil {
return err
}
}
}
if err := fp(w, "|"); err != nil {
return err
}
// Print each property.
for _, prop := range row.Props {
if err := prop.Print(w); err != nil {
return err
}
}
return fp(w, "\n")
}
// statRow represents a single row in a statsTable. Each row contains columns
// of data, as well as a list of properties.
type statRow struct {
Name string
Columns statColumns
Props []statProp
}
var statRowBreak = statRow{Name: "-"}
func (r *statRow) IsBreak() bool {
return r.Name == statRowBreak.Name && r.Columns.IsEmpty() && len(r.Props) == 0
}
func (r *statRow) PropIndex(name string) int {
for ix, prop := range r.Props {
if name == prop.Name {
return ix
}
}
return -1
}
func (r *statRow) UpdateProp(name string, value int, accumulate bool) {
index := r.PropIndex(name)
if index == -1 {
// Add a new property, init both min and max.
r.Props = append(r.Props, statProp{name, value, value})
return
}
// Found an existing property, update it.
prop := &r.Props[index]
switch {
case accumulate:
// Min and max are always equal, so they act as a single value.
prop.Min += value
prop.Max += value
default:
if value < prop.Min {
prop.Min = value
}
if value > prop.Max {
prop.Max = value
}
}
}
// statColumns holds the columns for a single statRow, represented as a map from
// column headers to int values. Each column may hold multiple values.
type statColumns struct {
Map map[string][]int
}
func (x *statColumns) IsEmpty() bool {
return len(x.Map) == 0
}
func (x *statColumns) Get(header string, index int) int {
if len(x.Map[header]) <= index {
// The map entry doesn't exist, or the slice isn't big enough. Either way
// there isn't an existing value.
return 0
}
return x.Map[header][index]
}
func (x *statColumns) Set(header string, index, value int) {
if x.Map == nil {
x.Map = make(map[string][]int)
}
slice := x.Map[header]
for len(slice) <= index {
slice = append(slice, 0)
}
slice[index] = value
x.Map[header] = slice
}
func (x *statColumns) Delta(header string, index, delta int) {
if len(x.Map[header]) <= index {
x.Set(header, index, delta)
return
}
slice := x.Map[header]
slice[index] += delta
x.Map[header] = slice
}
// statProp is a single int property held in statRow. We maintain the min/max
// value of the property as it is collected.
type statProp struct {
Name string // Name of the property
Min int // Min value of the property
Max int // Max value of the property
}
func (p statProp) Print(w io.Writer) error {
switch {
case p.Min == p.Max:
return fp(w, " [%s=%d]", p.Name, p.Max)
case p.Min == 0:
return fp(w, " [%s max=%d]", p.Name, p.Max)
default:
return fp(w, " [%s min=%d max=%d]", p.Name, p.Min, p.Max)
}
}
// typePropFn gathers the value of a single type property, used to update the
// statProp in a statRow.
type typePropFn struct {
Name string // Name of the property
Fn func(*vdl.Type) int // Fn that extracts the property from a type
Accumulate bool // Accumulate results rather than min/max
}
// typeStatsCollector collects a statsTable over all given Types.
type typeStatsCollector struct {
Types []*vdl.Type
Table statsTable
}
// Collect collects stats across all types for the named stat row, given the
// predicate fn. We keep counts of the number of types that match the
// predicate, as well as the number of types containing a type that matches the
// predicate.
//
// The propFns are only run against types that match the predicate, where
// properties are either accumulated or compared for min/max.
func (c *typeStatsCollector) Collect(name string, fn func(*vdl.Type) bool, propFns ...typePropFn) {
row := statRow{Name: name}
for _, tt := range c.Types {
if fn(tt) {
// The predicate matches. Update the total count and the properties. The
// leading space in " Total" ensures the Total column shows up first.
row.Columns.Delta(" Total", 0, 1)
for _, propFn := range propFns {
row.UpdateProp(propFn.Name, propFn.Fn(tt), propFn.Accumulate)
}
}
// Update the contains count by walking through tt until the predicate
// matches. We invert the predicate since tt.Walk will early-exit when we
// return false; thus false means that the predicate matched.
contains := !tt.Walk(vdl.WalkAll, func(visit *vdl.Type) bool {
return !fn(visit)
})
if contains {
row.Columns.Delta("Contains", 0, 1)
}
}
c.Table.Rows = append(c.Table.Rows, row)
}
// PrintTypeStats prints statistics gathered from types into w.
func PrintTypeStats(w io.Writer, types ...*vdl.Type) error {
stats := typeStatsCollector{Types: types}
// Collect stats by kind.
for kind := vdl.Any; kind <= vdl.Union; kind++ {
var fns []typePropFn
switch kind {
case vdl.Array:
fns = append(fns, typePropFn{Name: "len", Fn: (*vdl.Type).Len})
case vdl.List, vdl.Set, vdl.Map:
fn := func(tt *vdl.Type) int {
return predToProp(tt.Name() == "")
}
fns = append(fns, typePropFn{Name: "unnamed", Fn: fn, Accumulate: true})
case vdl.Enum:
fns = append(fns, typePropFn{Name: "labels", Fn: (*vdl.Type).NumEnumLabel})
case vdl.Struct, vdl.Union:
fns = append(fns, typePropFn{Name: "fields", Fn: (*vdl.Type).NumField})
}
stats.Collect(kind.String(), func(tt *vdl.Type) bool {
return tt.Kind() == kind
}, fns...)
}
stats.Table.Rows = append(stats.Table.Rows, statRowBreak)
// Collect stats by type predicate.
stats.Collect("IsNamed", func(tt *vdl.Type) bool {
return tt.Name() != ""
})
stats.Collect("IsUnnamed", func(tt *vdl.Type) bool {
return tt.Name() == ""
})
stats.Collect("IsNumber", func(tt *vdl.Type) bool {
return tt.Kind().IsNumber()
})
stats.Collect("IsErrorType", func(tt *vdl.Type) bool {
return tt == vdl.ErrorType || tt.Name() == "VError"
})
stats.Collect("IsBytes", (*vdl.Type).IsBytes)
stats.Collect("IsPartOfCycle", (*vdl.Type).IsPartOfCycle)
stats.Collect("CanBeNamed", (*vdl.Type).CanBeNamed)
stats.Collect("CanBeKey", (*vdl.Type).CanBeKey)
stats.Collect("CanBeNil", (*vdl.Type).CanBeNil)
stats.Collect("CanBeOptional", (*vdl.Type).CanBeOptional)
// Print stats table.
if err := fp(w, "Types: %d\n", len(types)); err != nil {
return err
}
return stats.Table.Print(w)
}
func predToProp(pred bool) int {
if pred {
return 1
}
return 0
}
// entryPropFn gathers the value of a single entry property, used to update the
// statProp in a statRow.
type entryPropFn struct {
Name string // Name of the property
Fn func(EntryValue) int // Fn that extracts the property from an entry
Accumulate bool // Accumulate results rather than min/max
}
// entryStatsCollector collects a statsTable over all given Entries.
type entryStatsCollector struct {
Entries []EntryValue
Table statsTable
}
// Collect collects stats across all entries for the named stat row, given the
// predicate fn.
//
// The propFns are only run against entries that match the predicate, where
// properties are either accumulated or compared for min/max.
func (c *entryStatsCollector) Collect(name string, fn func(EntryValue) bool, propFns ...entryPropFn) {
const nonCanonicalPropName = "!can"
row := statRow{Name: name}
if name != "any" && name != "typeobject" {
// Ensure the "!can" property shows up first.
row.UpdateProp(nonCanonicalPropName, 0, false)
}
for i, e := range c.Entries {
if fn(e) {
// The predicate matches. Update the stats table.
subColumn := 1
if e.IsCanonical() {
subColumn = 0
// Track the non-canonical entries for a given target, which always
// follow immediately after the canonical entry.
if row.PropIndex(nonCanonicalPropName) != -1 {
noncanon := 0
for j := i + 1; j < len(c.Entries); j++ {
if !vdl.EqualValue(e.Target, c.Entries[j].Target) {
break
}
noncanon++
}
row.UpdateProp(nonCanonicalPropName, noncanon, false)
}
}
// The leading space in " Total" ensures the Total column shows up first.
// Strip trailing numbers in e.Label to aggregate Random{0,1,...} under
// the same column header.
row.Columns.Delta(" Total", subColumn, 1)
header := strings.TrimRight(e.Label, "0123456789")
row.Columns.Delta(header, subColumn, 1)
if e.Source.IsZero() {
row.Columns.Delta("isZero", subColumn, 1)
}
for _, propFn := range propFns {
row.UpdateProp(propFn.Name, propFn.Fn(e), propFn.Accumulate)
}
// Add some error checking.
if e.Source.Kind() == vdl.String {
// TODO(toddw): Walk through source and check all strings.
if !utf8.ValidString(e.Source.RawString()) {
row.UpdateProp("ERROR: INVALID UTF8", 1, true)
}
}
}
}
c.Table.Rows = append(c.Table.Rows, row)
}
// PrintEntryStats prints statistics gathered from entries into w.
func PrintEntryStats(w io.Writer, entries ...EntryValue) error {
stats := entryStatsCollector{Entries: entries}
// Collect stats by summary stats.
stats.Collect("total", func(e EntryValue) bool {
return true
})
stats.Collect("isZero", func(e EntryValue) bool {
return e.Source.IsZero()
})
stats.Table.Rows = append(stats.Table.Rows, statRowBreak)
// Collect stats by source kind.
for kind := vdl.Any; kind <= vdl.Union; kind++ {
var fns []entryPropFn
switch kind {
case vdl.Array, vdl.List, vdl.Set, vdl.Map:
fns = append(fns, entryPropFn{Name: "len", Fn: func(e EntryValue) int {
return e.Source.Len()
}})
case vdl.Any, vdl.Optional:
fn := func(e EntryValue) int {
return predToProp(e.Source.IsNil())
}
fns = append(fns, entryPropFn{Name: "nil", Fn: fn, Accumulate: true})
}
stats.Collect(kind.String(), func(e EntryValue) bool {
return e.Source.Kind() == kind
}, fns...)
}
stats.Table.Rows = append(stats.Table.Rows, statRowBreak)
// Collect stats by source type predicate.
stats.Collect("IsNamed", func(e EntryValue) bool {
return e.Source.Type().Name() != ""
})
stats.Collect("IsUnnamed", func(e EntryValue) bool {
return e.Source.Type().Name() == ""
})
stats.Collect("IsBytes", func(e EntryValue) bool {
return e.Source.Type().IsBytes()
})
// Print stats table.
if err := fp(w, "Entries: %d\n", len(entries)); err != nil {
return err
}
if err := stats.Table.Print(w); err != nil {
return err
}
const footer = `
Each column has a pair of entry counts (canonical,non-canonical), computed
from the Source value. An entry is canonical if Target == Source.
!can tracks the number of non-canonical entries for each unique target.
+{Max,Min}: Target set to positive max and min values.
-{Max,Min}: Target set to negative max and min values.
Full: Target is entirely non-zero, except for cyclic types.
NilAny: Target is optional(nil), source is any(nil).
Random: Target is random value.
Zero: Target is zero value.
`
return fp(w, footer)
}