github.com/onsi/gomega@v1.32.0/gmeasure/cache.go (about) 1 package gmeasure 2 3 import ( 4 "crypto/md5" 5 "encoding/json" 6 "fmt" 7 "os" 8 "path/filepath" 9 10 "github.com/onsi/gomega/internal/gutil" 11 ) 12 13 const CACHE_EXT = ".gmeasure-cache" 14 15 /* 16 ExperimentCache provides a director-and-file based cache of experiments 17 */ 18 type ExperimentCache struct { 19 Path string 20 } 21 22 /* 23 NewExperimentCache creates and initializes a new cache. Path must point to a directory (if path does not exist, NewExperimentCache will create a directory at path). 24 25 Cached Experiments are stored as separate files in the cache directory - the filename is a hash of the Experiment name. Each file contains two JSON-encoded objects - a CachedExperimentHeader that includes the experiment's name and cache version number, and then the Experiment itself. 26 */ 27 func NewExperimentCache(path string) (ExperimentCache, error) { 28 stat, err := os.Stat(path) 29 if os.IsNotExist(err) { 30 err := os.MkdirAll(path, 0777) 31 if err != nil { 32 return ExperimentCache{}, err 33 } 34 } else if !stat.IsDir() { 35 return ExperimentCache{}, fmt.Errorf("%s is not a directory", path) 36 } 37 38 return ExperimentCache{ 39 Path: path, 40 }, nil 41 } 42 43 /* 44 CachedExperimentHeader captures the name of the Cached Experiment and its Version 45 */ 46 type CachedExperimentHeader struct { 47 Name string 48 Version int 49 } 50 51 func (cache ExperimentCache) hashOf(name string) string { 52 return fmt.Sprintf("%x", md5.Sum([]byte(name))) 53 } 54 55 func (cache ExperimentCache) readHeader(filename string) (CachedExperimentHeader, error) { 56 out := CachedExperimentHeader{} 57 f, err := os.Open(filepath.Join(cache.Path, filename)) 58 if err != nil { 59 return out, err 60 } 61 defer f.Close() 62 err = json.NewDecoder(f).Decode(&out) 63 return out, err 64 } 65 66 /* 67 List returns a list of all Cached Experiments found in the cache. 68 */ 69 func (cache ExperimentCache) List() ([]CachedExperimentHeader, error) { 70 var out []CachedExperimentHeader 71 names, err := gutil.ReadDir(cache.Path) 72 if err != nil { 73 return out, err 74 } 75 for _, name := range names { 76 if filepath.Ext(name) != CACHE_EXT { 77 continue 78 } 79 header, err := cache.readHeader(name) 80 if err != nil { 81 return out, err 82 } 83 out = append(out, header) 84 } 85 return out, nil 86 } 87 88 /* 89 Clear empties out the cache - this will delete any and all detected cache files in the cache directory. Use with caution! 90 */ 91 func (cache ExperimentCache) Clear() error { 92 names, err := gutil.ReadDir(cache.Path) 93 if err != nil { 94 return err 95 } 96 for _, name := range names { 97 if filepath.Ext(name) != CACHE_EXT { 98 continue 99 } 100 err := os.Remove(filepath.Join(cache.Path, name)) 101 if err != nil { 102 return err 103 } 104 } 105 return nil 106 } 107 108 /* 109 Load fetches an experiment from the cache. Lookup occurs by name. Load requires that the version numer in the cache is equal to or greater than the passed-in version. 110 111 If an experiment with corresponding name and version >= the passed-in version is found, it is unmarshaled and returned. 112 113 If no experiment is found, or the cached version is smaller than the passed-in version, Load will return nil. 114 115 When paired with Ginkgo you can cache experiments and prevent potentially expensive recomputation with this pattern: 116 117 const EXPERIMENT_VERSION = 1 //bump this to bust the cache and recompute _all_ experiments 118 119 Describe("some experiments", func() { 120 var cache gmeasure.ExperimentCache 121 var experiment *gmeasure.Experiment 122 123 BeforeEach(func() { 124 cache = gmeasure.NewExperimentCache("./gmeasure-cache") 125 name := CurrentSpecReport().LeafNodeText 126 experiment = cache.Load(name, EXPERIMENT_VERSION) 127 if experiment != nil { 128 AddReportEntry(experiment) 129 Skip("cached") 130 } 131 experiment = gmeasure.NewExperiment(name) 132 AddReportEntry(experiment) 133 }) 134 135 It("foo runtime", func() { 136 experiment.SampleDuration("runtime", func() { 137 //do stuff 138 }, gmeasure.SamplingConfig{N:100}) 139 }) 140 141 It("bar runtime", func() { 142 experiment.SampleDuration("runtime", func() { 143 //do stuff 144 }, gmeasure.SamplingConfig{N:100}) 145 }) 146 147 AfterEach(func() { 148 if !CurrentSpecReport().State.Is(types.SpecStateSkipped) { 149 cache.Save(experiment.Name, EXPERIMENT_VERSION, experiment) 150 } 151 }) 152 }) 153 */ 154 func (cache ExperimentCache) Load(name string, version int) *Experiment { 155 path := filepath.Join(cache.Path, cache.hashOf(name)+CACHE_EXT) 156 f, err := os.Open(path) 157 if err != nil { 158 return nil 159 } 160 defer f.Close() 161 dec := json.NewDecoder(f) 162 header := CachedExperimentHeader{} 163 dec.Decode(&header) 164 if header.Version < version { 165 return nil 166 } 167 out := NewExperiment("") 168 err = dec.Decode(out) 169 if err != nil { 170 return nil 171 } 172 return out 173 } 174 175 /* 176 Save stores the passed-in experiment to the cache with the passed-in name and version. 177 */ 178 func (cache ExperimentCache) Save(name string, version int, experiment *Experiment) error { 179 path := filepath.Join(cache.Path, cache.hashOf(name)+CACHE_EXT) 180 f, err := os.Create(path) 181 if err != nil { 182 return err 183 } 184 defer f.Close() 185 enc := json.NewEncoder(f) 186 err = enc.Encode(CachedExperimentHeader{ 187 Name: name, 188 Version: version, 189 }) 190 if err != nil { 191 return err 192 } 193 return enc.Encode(experiment) 194 } 195 196 /* 197 Delete removes the experiment with the passed-in name from the cache 198 */ 199 func (cache ExperimentCache) Delete(name string) error { 200 path := filepath.Join(cache.Path, cache.hashOf(name)+CACHE_EXT) 201 return os.Remove(path) 202 }