| // 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() | |
| } |