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  }