blob: 973788d11f588b6a8da1e51adb8ab5fe2d21d2e5 [file] [log] [blame]
// Copyright 2022 The Go 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 benchfmt
import (
"fmt"
"os"
"strings"
)
// A Files reads benchmark results from a sequence of input files.
//
// This reader adds a ".file" configuration key to the output Results
// corresponding to each path read in. By default, this will be the
// file name directly from Paths, except that duplicate strings will
// be disambiguated by appending "#N". If AllowLabels is true, then
// entries in Path may be of the form label=path, and the label part
// will be used for .file (without any disambiguation).
type Files struct {
// Paths is the list of file names to read in.
//
// If AllowLabels is set, these strings may be of the form
// label=path, and the label part will be used for the
// ".file" key in the results.
Paths []string
// AllowStdin indicates that the path "-" should be treated as
// stdin and if the file list is empty, it should be treated
// as consisting of stdin.
//
// This is generally the desired behavior when the file list
// comes from command-line flags.
AllowStdin bool
// AllowLabels indicates that custom labels are allowed in
// Paths.
//
// This is generally the desired behavior when the file list
// comes from command-line flags, as it allows users to
// override .file.
AllowLabels bool
// inputs is the sequence of remaining inputs, or nil if this
// Files has not started yet. Note that this distinguishes nil
// from length 0.
inputs []input
reader Reader
file *os.File
isStdin bool
err error
}
type input struct {
path string
label string
isStdin bool
isLabeled bool
}
// init does first-use initialization of f.
func (f *Files) init() {
// Set f.inputs to a non-nil slice to indicate initialization
// has happened.
f.inputs = []input{}
// Parse the paths. Doing this first simplifies iteration and
// disambiguation.
pathCount := make(map[string]int)
if f.AllowStdin && len(f.Paths) == 0 {
f.inputs = append(f.inputs, input{"-", "-", true, false})
}
for _, path := range f.Paths {
// Parse the label.
label := path
isLabeled := false
if i := strings.Index(path, "="); f.AllowLabels && i >= 0 {
label, path = path[:i], path[i+1:]
isLabeled = true
} else {
pathCount[path]++
}
isStdin := f.AllowStdin && path == "-"
f.inputs = append(f.inputs, input{path, label, isStdin, isLabeled})
}
// If the same path is given multiple times, disambiguate its
// .file. Otherwise, the results have indistinguishable
// configurations, which just doubles up samples, which is
// generally not what users are expecting. For overridden
// labels, we do exactly what the user says.
pathI := make(map[string]int)
for i := range f.inputs {
inp := &f.inputs[i]
if inp.isLabeled || pathCount[inp.path] == 1 {
continue
}
// Disambiguate.
inp.label = fmt.Sprintf("%s#%d", inp.path, pathI[inp.path])
pathI[inp.path]++
}
}
// Scan advances the reader to the next result in the sequence of
// files and reports whether a result was read. The caller should use
// the Result method to get the result. If Scan reaches the end of the
// file sequence, or if an I/O error occurs, it returns false. In this
// case, the caller should use the Err method to check for errors.
func (f *Files) Scan() bool {
if f.err != nil {
return false
}
if f.inputs == nil {
f.init()
}
for {
if f.file == nil {
// Open the next file.
if len(f.inputs) == 0 {
// We're out of inputs.
return false
}
inp := f.inputs[0]
f.inputs = f.inputs[1:]
if inp.isStdin {
f.isStdin, f.file = true, os.Stdin
} else {
file, err := os.Open(inp.path)
if err != nil {
f.err = err
return false
}
f.isStdin, f.file = false, file
}
// Prepare the reader. Because ".file" is not
// valid syntax for file configuration keys in
// the file itself, there's no danger of it
// being overwritten.
f.reader.Reset(f.file, inp.path, ".file", inp.label)
}
// Try to get the next result.
if f.reader.Scan() {
return true
}
err := f.reader.Err()
if err != nil {
f.err = err
break
}
// Just an EOF. Close this file and open the next.
if !f.isStdin {
f.file.Close()
}
f.file = nil
}
// We're out of files.
return false
}
// Result returns the record that was just read by Scan.
// See Reader.Result.
func (f *Files) Result() Record {
return f.reader.Result()
}
// Err returns the I/O error that stopped Scan, if any.
// If Scan stopped because it read each file to completion,
// or if Scan has not yet returned false, Err returns nil.
func (f *Files) Err() error {
return f.err
}
// Units returns the accumulated unit metadata.
// See Reader.Units.
func (f *Files) Units() map[UnitMetadataKey]*UnitMetadata {
return f.reader.Units()
}