github.com/jgbaldwinbrown/perf@v0.1.1/benchfmt/files.go (about) 1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package benchfmt 6 7 import ( 8 "fmt" 9 "os" 10 "strings" 11 ) 12 13 // A Files reads benchmark results from a sequence of input files. 14 // 15 // This reader adds a ".file" configuration key to the output Results 16 // corresponding to each path read in. By default, this will be the 17 // file name directly from Paths, except that duplicate strings will 18 // be disambiguated by appending "#N". If AllowLabels is true, then 19 // entries in Path may be of the form label=path, and the label part 20 // will be used for .file (without any disambiguation). 21 type Files struct { 22 // Paths is the list of file names to read in. 23 // 24 // If AllowLabels is set, these strings may be of the form 25 // label=path, and the label part will be used for the 26 // ".file" key in the results. 27 Paths []string 28 29 // AllowStdin indicates that the path "-" should be treated as 30 // stdin and if the file list is empty, it should be treated 31 // as consisting of stdin. 32 // 33 // This is generally the desired behavior when the file list 34 // comes from command-line flags. 35 AllowStdin bool 36 37 // AllowLabels indicates that custom labels are allowed in 38 // Paths. 39 // 40 // This is generally the desired behavior when the file list 41 // comes from command-line flags, as it allows users to 42 // override .file. 43 AllowLabels bool 44 45 // inputs is the sequence of remaining inputs, or nil if this 46 // Files has not started yet. Note that this distinguishes nil 47 // from length 0. 48 inputs []input 49 50 reader Reader 51 file *os.File 52 isStdin bool 53 err error 54 } 55 56 type input struct { 57 path string 58 label string 59 isStdin bool 60 isLabeled bool 61 } 62 63 // init does first-use initialization of f. 64 func (f *Files) init() { 65 // Set f.inputs to a non-nil slice to indicate initialization 66 // has happened. 67 f.inputs = []input{} 68 69 // Parse the paths. Doing this first simplifies iteration and 70 // disambiguation. 71 pathCount := make(map[string]int) 72 if f.AllowStdin && len(f.Paths) == 0 { 73 f.inputs = append(f.inputs, input{"-", "-", true, false}) 74 } 75 for _, path := range f.Paths { 76 // Parse the label. 77 label := path 78 isLabeled := false 79 if i := strings.Index(path, "="); f.AllowLabels && i >= 0 { 80 label, path = path[:i], path[i+1:] 81 isLabeled = true 82 } else { 83 pathCount[path]++ 84 } 85 86 isStdin := f.AllowStdin && path == "-" 87 f.inputs = append(f.inputs, input{path, label, isStdin, isLabeled}) 88 } 89 90 // If the same path is given multiple times, disambiguate its 91 // .file. Otherwise, the results have indistinguishable 92 // configurations, which just doubles up samples, which is 93 // generally not what users are expecting. For overridden 94 // labels, we do exactly what the user says. 95 pathI := make(map[string]int) 96 for i := range f.inputs { 97 inp := &f.inputs[i] 98 if inp.isLabeled || pathCount[inp.path] == 1 { 99 continue 100 } 101 // Disambiguate. 102 inp.label = fmt.Sprintf("%s#%d", inp.path, pathI[inp.path]) 103 pathI[inp.path]++ 104 } 105 } 106 107 // Scan advances the reader to the next result in the sequence of 108 // files and reports whether a result was read. The caller should use 109 // the Result method to get the result. If Scan reaches the end of the 110 // file sequence, or if an I/O error occurs, it returns false. In this 111 // case, the caller should use the Err method to check for errors. 112 func (f *Files) Scan() bool { 113 if f.err != nil { 114 return false 115 } 116 117 if f.inputs == nil { 118 f.init() 119 } 120 121 for { 122 if f.file == nil { 123 // Open the next file. 124 if len(f.inputs) == 0 { 125 // We're out of inputs. 126 return false 127 } 128 inp := f.inputs[0] 129 f.inputs = f.inputs[1:] 130 131 if inp.isStdin { 132 f.isStdin, f.file = true, os.Stdin 133 } else { 134 file, err := os.Open(inp.path) 135 if err != nil { 136 f.err = err 137 return false 138 } 139 f.isStdin, f.file = false, file 140 } 141 142 // Prepare the reader. Because ".file" is not 143 // valid syntax for file configuration keys in 144 // the file itself, there's no danger of it 145 // being overwritten. 146 f.reader.Reset(f.file, inp.path, ".file", inp.label) 147 } 148 149 // Try to get the next result. 150 if f.reader.Scan() { 151 return true 152 } 153 err := f.reader.Err() 154 if err != nil { 155 f.err = err 156 break 157 } 158 // Just an EOF. Close this file and open the next. 159 if !f.isStdin { 160 f.file.Close() 161 } 162 f.file = nil 163 } 164 // We're out of files. 165 return false 166 } 167 168 // Result returns the record that was just read by Scan. 169 // See Reader.Result. 170 func (f *Files) Result() Record { 171 return f.reader.Result() 172 } 173 174 // Err returns the I/O error that stopped Scan, if any. 175 // If Scan stopped because it read each file to completion, 176 // or if Scan has not yet returned false, Err returns nil. 177 func (f *Files) Err() error { 178 return f.err 179 } 180 181 // Units returns the accumulated unit metadata. 182 // See Reader.Units. 183 func (f *Files) Units() map[UnitMetadataKey]*UnitMetadata { 184 return f.reader.Units() 185 }