github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/build/buildkit_printer.go (about) 1 package build 2 3 import ( 4 "fmt" 5 "regexp" 6 "strings" 7 "time" 8 9 digest "github.com/opencontainers/go-digest" 10 "github.com/tonistiigi/units" 11 12 "github.com/tilt-dev/tilt/pkg/apis" 13 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 14 "github.com/tilt-dev/tilt/pkg/logger" 15 ) 16 17 type buildkitPrinter struct { 18 logger logger.Logger 19 vData map[digest.Digest]*vertexAndLogs 20 vOrder []digest.Digest 21 } 22 23 type vertex struct { 24 digest digest.Digest 25 name string 26 error string 27 started bool 28 startedTime *time.Time 29 completed bool 30 completedTime *time.Time 31 startPrinted bool 32 errorPrinted bool 33 completePrinted bool 34 durationPrinted time.Duration 35 cached bool 36 duration time.Duration 37 } 38 39 const internalPrefix = "[internal]" 40 const internalLoader = "[internal] load build context" 41 const logPrefix = " → " 42 43 var stageNameRegexp = regexp.MustCompile(`^\[.+\]`) 44 45 func (v *vertex) shouldHide() bool { 46 return strings.HasPrefix(v.name, internalPrefix) && v.name != internalLoader 47 } 48 49 func (v *vertex) isError() bool { 50 return len(v.error) > 0 51 } 52 53 func (v *vertex) humanName() string { 54 if v.name == internalLoader { 55 return "[background] read source files" 56 } 57 return v.name 58 } 59 60 func (v *vertex) stageName() string { 61 match := stageNameRegexp.FindString(v.name) 62 if match == "" { 63 // If we couldn't find a match, just return the whole 64 // human-readable name, so that the user has some hope of figuring out 65 // what went wrong. 66 return v.humanName() 67 } 68 return match 69 } 70 71 type vertexAndLogs struct { 72 vertex *vertex 73 logs []*vertexLog 74 logsPrinted int 75 logger logger.Logger 76 77 // A map of statuses, indexed by the layer id being downloaded. 78 statuses vertexStatusSet 79 80 // A combined status for all the downloadable layers, merged 81 // into a single status object. 82 lastPrintedStatus vertexStatus 83 } 84 85 type vertexLog struct { 86 vertex digest.Digest 87 msg []byte 88 } 89 90 type vertexStatus struct { 91 vertex digest.Digest 92 id string 93 total int64 94 current int64 95 timestamp time.Time 96 } 97 98 // The buildkit protocol represents each downloadable layer 99 // as a separate status object, identified by a layer ID. 100 // We want to present this to the user as a single, combined status 101 // that summarizes all layers. 102 type vertexStatusSet map[string]vertexStatus 103 104 func (s vertexStatusSet) combined() vertexStatus { 105 current := int64(0) 106 total := int64(0) 107 t := time.Time{} 108 for _, v := range s { 109 current += v.current 110 total += v.total 111 if v.timestamp.After(t) { 112 t = v.timestamp 113 } 114 } 115 return vertexStatus{ 116 current: current, 117 total: total, 118 timestamp: t, 119 } 120 } 121 122 func newBuildkitPrinter(l logger.Logger) *buildkitPrinter { 123 return &buildkitPrinter{ 124 logger: l, 125 vData: map[digest.Digest]*vertexAndLogs{}, 126 vOrder: []digest.Digest{}, 127 } 128 } 129 130 func (b *buildkitPrinter) toStageStatuses() []v1alpha1.DockerImageStageStatus { 131 result := make([]v1alpha1.DockerImageStageStatus, 0) 132 for _, digest := range b.vOrder { 133 if vl, ok := b.vData[digest]; ok { 134 v := vl.vertex 135 status := v1alpha1.DockerImageStageStatus{ 136 Name: v.name, 137 Cached: v.cached, 138 } 139 if v.startedTime != nil { 140 st := apis.NewMicroTime(*v.startedTime) 141 status.StartedAt = &st 142 } 143 if v.completedTime != nil { 144 ct := apis.NewMicroTime(*v.completedTime) 145 status.FinishedAt = &ct 146 } 147 if v.isError() { 148 status.Error = v.error 149 } 150 result = append(result, status) 151 } 152 } 153 return result 154 } 155 156 func (b *buildkitPrinter) parseAndPrint(vertexes []*vertex, logs []*vertexLog, statuses []*vertexStatus) error { 157 for _, v := range vertexes { 158 if vl, ok := b.vData[v.digest]; ok { 159 vl.vertex.started = v.started 160 vl.vertex.completed = v.completed 161 vl.vertex.startedTime = v.startedTime 162 vl.vertex.completedTime = v.completedTime 163 vl.vertex.cached = v.cached 164 165 // NOTE(nick): Fun fact! The buildkit protocol sends down multiple completion timestamps. 166 // We need to take the last one. 167 if v.duration > vl.vertex.duration { 168 vl.vertex.duration = v.duration 169 } 170 171 if v.isError() { 172 vl.vertex.error = v.error 173 } 174 } else { 175 b.vData[v.digest] = &vertexAndLogs{ 176 vertex: v, 177 logs: []*vertexLog{}, 178 logger: logger.NewPrefixedLogger(logPrefix, b.logger), 179 } 180 181 b.vOrder = append(b.vOrder, v.digest) 182 } 183 } 184 185 for _, l := range logs { 186 if vl, ok := b.vData[l.vertex]; ok { 187 vl.logs = append(vl.logs, l) 188 } 189 } 190 191 for _, s := range statuses { 192 if vl, ok := b.vData[s.vertex]; ok { 193 if vl.statuses == nil { 194 vl.statuses = vertexStatusSet{} 195 } 196 vl.statuses[s.id] = *s 197 } 198 } 199 200 for _, d := range b.vOrder { 201 vl, ok := b.vData[d] 202 if !ok { 203 return fmt.Errorf("Expected to find digest %s in %+v", d, b.vData) 204 } 205 206 v := vl.vertex 207 if v.started && !v.startPrinted && !v.shouldHide() { 208 cacheSuffix := "" 209 if v.cached { 210 cacheSuffix = " [cached]" 211 } 212 b.logger.WithFields(logger.Fields{logger.FieldNameProgressID: v.stageName()}). 213 Infof("%s%s", v.humanName(), cacheSuffix) 214 v.startPrinted = true 215 } 216 217 if v.isError() && !v.errorPrinted { 218 // TODO(nick): Should this be logger.Errorf? 219 b.logger.Infof("\nERROR IN: %s", v.humanName()) 220 v.errorPrinted = true 221 } 222 223 if v.isError() || !v.shouldHide() { 224 b.flushLogs(vl) 225 } 226 227 if !v.shouldHide() && 228 !v.cached && 229 !v.isError() { 230 231 var progressInBytes string 232 status := vl.statuses.combined() 233 shouldPrintProgress := false 234 if vl.lastPrintedStatus.total != status.total { 235 // print progress when the total has changed. That means we've started 236 // downloading a new layer. 237 shouldPrintProgress = true 238 } else if status.total > 0 { 239 // print progress when at least 1% of total has changed and at least 2 seconds have passed. 240 diff := float64(status.current) - float64(vl.lastPrintedStatus.current) 241 largeEnoughChange := diff/float64(status.total) >= 0.01 242 largeEnoughTime := status.timestamp.Sub(vl.lastPrintedStatus.timestamp) > 2*time.Second 243 shouldPrintProgress = largeEnoughChange && largeEnoughTime 244 } else if status.current > 0 { 245 // print progress when at least 5% of current has changed and at least 2 seconds have passed. 246 // We need to handle this case separately when we don't have a total estimate. 247 diff := float64(status.current) - float64(vl.lastPrintedStatus.current) 248 largeEnoughChange := diff/float64(status.current) >= 0.05 249 largeEnoughTime := status.timestamp.Sub(vl.lastPrintedStatus.timestamp) > 2*time.Second 250 shouldPrintProgress = largeEnoughChange && largeEnoughTime 251 } 252 253 if status.total != 0 { 254 progressInBytes = fmt.Sprintf(" %.2f / %.2f", units.Bytes(status.current), units.Bytes(status.total)) 255 } else if status.current != 0 { 256 progressInBytes = fmt.Sprintf(" %.2f", units.Bytes(status.current)) 257 } 258 259 // NOTE(nick): Fun fact! The buildkit protocol sends down multiple completion timestamps. 260 // We need to print the longest one. 261 shouldPrintCompletion := v.completed && v.duration > 10*time.Millisecond && 262 (!v.completePrinted || 263 v.durationPrinted < v.duration) 264 265 doneSuffix := "" 266 fields := logger.Fields{logger.FieldNameProgressID: v.stageName()} 267 if shouldPrintCompletion { 268 doneSuffix = fmt.Sprintf(" [done: %s]", v.duration.Truncate(time.Millisecond)) 269 v.completePrinted = true 270 v.durationPrinted = v.duration 271 fields[logger.FieldNameProgressMustPrint] = "1" 272 } 273 274 if shouldPrintCompletion || shouldPrintProgress { 275 b.logger.WithFields(fields). 276 Infof("%s%s%s", v.humanName(), progressInBytes, doneSuffix) 277 278 vl.lastPrintedStatus = status 279 } 280 } 281 } 282 283 return nil 284 } 285 286 func (b *buildkitPrinter) flushLogs(vl *vertexAndLogs) { 287 for vl.logsPrinted < len(vl.logs) { 288 l := vl.logs[vl.logsPrinted] 289 vl.logsPrinted++ 290 vl.logger.Write(logger.InfoLvl, l.msg) 291 } 292 }