github.com/gagliardetto/golang-go@v0.0.0-20201020153340-53909ea70814/cmd/trace/trace.go (about) 1 // Copyright 2014 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 main 6 7 import ( 8 "encoding/json" 9 "fmt" 10 "github.com/gagliardetto/golang-go/not-internal/trace" 11 "io" 12 "log" 13 "math" 14 "net/http" 15 "path/filepath" 16 "runtime" 17 "runtime/debug" 18 "sort" 19 "strconv" 20 "strings" 21 "time" 22 ) 23 24 func init() { 25 http.HandleFunc("/trace", httpTrace) 26 http.HandleFunc("/jsontrace", httpJsonTrace) 27 http.HandleFunc("/trace_viewer_html", httpTraceViewerHTML) 28 http.HandleFunc("/webcomponents.min.js", webcomponentsJS) 29 } 30 31 // httpTrace serves either whole trace (goid==0) or trace for goid goroutine. 32 func httpTrace(w http.ResponseWriter, r *http.Request) { 33 _, err := parseTrace() 34 if err != nil { 35 http.Error(w, err.Error(), http.StatusInternalServerError) 36 return 37 } 38 if err := r.ParseForm(); err != nil { 39 http.Error(w, err.Error(), http.StatusInternalServerError) 40 return 41 } 42 html := strings.ReplaceAll(templTrace, "{{PARAMS}}", r.Form.Encode()) 43 w.Write([]byte(html)) 44 45 } 46 47 // https://chromium.googlesource.com/catapult/+/9508452e18f130c98499cb4c4f1e1efaedee8962/tracing/docs/embedding-trace-viewer.md 48 // This is almost verbatim copy of https://chromium-review.googlesource.com/c/catapult/+/2062938/2/tracing/bin/index.html 49 var templTrace = ` 50 <html> 51 <head> 52 <script src="/webcomponents.min.js"></script> 53 <script> 54 'use strict'; 55 56 function onTraceViewerImportFail() { 57 document.addEventListener('DOMContentLoaded', function() { 58 document.body.textContent = 59 '/trace_viewer_full.html is missing. File a bug in https://golang.org/issue'; 60 }); 61 } 62 </script> 63 64 <link rel="import" href="/trace_viewer_html" 65 onerror="onTraceViewerImportFail(event)"> 66 67 <style type="text/css"> 68 html, body { 69 box-sizing: border-box; 70 overflow: hidden; 71 margin: 0px; 72 padding: 0; 73 width: 100%; 74 height: 100%; 75 } 76 #trace-viewer { 77 width: 100%; 78 height: 100%; 79 } 80 #trace-viewer:focus { 81 outline: none; 82 } 83 </style> 84 <script> 85 'use strict'; 86 (function() { 87 var viewer; 88 var url; 89 var model; 90 91 function load() { 92 var req = new XMLHttpRequest(); 93 var isBinary = /[.]gz$/.test(url) || /[.]zip$/.test(url); 94 req.overrideMimeType('text/plain; charset=x-user-defined'); 95 req.open('GET', url, true); 96 if (isBinary) 97 req.responseType = 'arraybuffer'; 98 99 req.onreadystatechange = function(event) { 100 if (req.readyState !== 4) 101 return; 102 103 window.setTimeout(function() { 104 if (req.status === 200) 105 onResult(isBinary ? req.response : req.responseText); 106 else 107 onResultFail(req.status); 108 }, 0); 109 }; 110 req.send(null); 111 } 112 113 function onResultFail(err) { 114 var overlay = new tr.ui.b.Overlay(); 115 overlay.textContent = err + ': ' + url + ' could not be loaded'; 116 overlay.title = 'Failed to fetch data'; 117 overlay.visible = true; 118 } 119 120 function onResult(result) { 121 model = new tr.Model(); 122 var opts = new tr.importer.ImportOptions(); 123 opts.shiftWorldToZero = false; 124 var i = new tr.importer.Import(model, opts); 125 var p = i.importTracesWithProgressDialog([result]); 126 p.then(onModelLoaded, onImportFail); 127 } 128 129 function onModelLoaded() { 130 viewer.model = model; 131 viewer.viewTitle = "trace"; 132 133 if (!model || model.bounds.isEmpty) 134 return; 135 var sel = window.location.hash.substr(1); 136 if (sel === '') 137 return; 138 var parts = sel.split(':'); 139 var range = new (tr.b.Range || tr.b.math.Range)(); 140 range.addValue(parseFloat(parts[0])); 141 range.addValue(parseFloat(parts[1])); 142 viewer.trackView.viewport.interestRange.set(range); 143 } 144 145 function onImportFail(err) { 146 var overlay = new tr.ui.b.Overlay(); 147 overlay.textContent = tr.b.normalizeException(err).message; 148 overlay.title = 'Import error'; 149 overlay.visible = true; 150 } 151 152 document.addEventListener('WebComponentsReady', function() { 153 var container = document.createElement('track-view-container'); 154 container.id = 'track_view_container'; 155 156 viewer = document.createElement('tr-ui-timeline-view'); 157 viewer.track_view_container = container; 158 Polymer.dom(viewer).appendChild(container); 159 160 viewer.id = 'trace-viewer'; 161 viewer.globalMode = true; 162 Polymer.dom(document.body).appendChild(viewer); 163 164 url = '/jsontrace?{{PARAMS}}'; 165 load(); 166 }); 167 }()); 168 </script> 169 </head> 170 <body> 171 </body> 172 </html> 173 ` 174 175 // httpTraceViewerHTML serves static part of trace-viewer. 176 // This URL is queried from templTrace HTML. 177 func httpTraceViewerHTML(w http.ResponseWriter, r *http.Request) { 178 http.ServeFile(w, r, filepath.Join(runtime.GOROOT(), "misc", "trace", "trace_viewer_full.html")) 179 } 180 181 func webcomponentsJS(w http.ResponseWriter, r *http.Request) { 182 http.ServeFile(w, r, filepath.Join(runtime.GOROOT(), "misc", "trace", "webcomponents.min.js")) 183 } 184 185 // httpJsonTrace serves json trace, requested from within templTrace HTML. 186 func httpJsonTrace(w http.ResponseWriter, r *http.Request) { 187 defer debug.FreeOSMemory() 188 defer reportMemoryUsage("after httpJsonTrace") 189 // This is an AJAX handler, so instead of http.Error we use log.Printf to log errors. 190 res, err := parseTrace() 191 if err != nil { 192 log.Printf("failed to parse trace: %v", err) 193 return 194 } 195 196 params := &traceParams{ 197 parsed: res, 198 endTime: math.MaxInt64, 199 } 200 201 if goids := r.FormValue("goid"); goids != "" { 202 // If goid argument is present, we are rendering a trace for this particular goroutine. 203 goid, err := strconv.ParseUint(goids, 10, 64) 204 if err != nil { 205 log.Printf("failed to parse goid parameter %q: %v", goids, err) 206 return 207 } 208 analyzeGoroutines(res.Events) 209 g, ok := gs[goid] 210 if !ok { 211 log.Printf("failed to find goroutine %d", goid) 212 return 213 } 214 params.mode = modeGoroutineOriented 215 params.startTime = g.StartTime 216 if g.EndTime != 0 { 217 params.endTime = g.EndTime 218 } else { // The goroutine didn't end. 219 params.endTime = lastTimestamp() 220 } 221 params.maing = goid 222 params.gs = trace.RelatedGoroutines(res.Events, goid) 223 } else if taskids := r.FormValue("taskid"); taskids != "" { 224 taskid, err := strconv.ParseUint(taskids, 10, 64) 225 if err != nil { 226 log.Printf("failed to parse taskid parameter %q: %v", taskids, err) 227 return 228 } 229 annotRes, _ := analyzeAnnotations() 230 task, ok := annotRes.tasks[taskid] 231 if !ok || len(task.events) == 0 { 232 log.Printf("failed to find task with id %d", taskid) 233 return 234 } 235 goid := task.events[0].G 236 params.mode = modeGoroutineOriented | modeTaskOriented 237 params.startTime = task.firstTimestamp() - 1 238 params.endTime = task.lastTimestamp() + 1 239 params.maing = goid 240 params.tasks = task.descendants() 241 gs := map[uint64]bool{} 242 for _, t := range params.tasks { 243 // find only directly involved goroutines 244 for k, v := range t.RelatedGoroutines(res.Events, 0) { 245 gs[k] = v 246 } 247 } 248 params.gs = gs 249 } else if taskids := r.FormValue("focustask"); taskids != "" { 250 taskid, err := strconv.ParseUint(taskids, 10, 64) 251 if err != nil { 252 log.Printf("failed to parse focustask parameter %q: %v", taskids, err) 253 return 254 } 255 annotRes, _ := analyzeAnnotations() 256 task, ok := annotRes.tasks[taskid] 257 if !ok || len(task.events) == 0 { 258 log.Printf("failed to find task with id %d", taskid) 259 return 260 } 261 params.mode = modeTaskOriented 262 params.startTime = task.firstTimestamp() - 1 263 params.endTime = task.lastTimestamp() + 1 264 params.tasks = task.descendants() 265 } 266 267 start := int64(0) 268 end := int64(math.MaxInt64) 269 if startStr, endStr := r.FormValue("start"), r.FormValue("end"); startStr != "" && endStr != "" { 270 // If start/end arguments are present, we are rendering a range of the trace. 271 start, err = strconv.ParseInt(startStr, 10, 64) 272 if err != nil { 273 log.Printf("failed to parse start parameter %q: %v", startStr, err) 274 return 275 } 276 end, err = strconv.ParseInt(endStr, 10, 64) 277 if err != nil { 278 log.Printf("failed to parse end parameter %q: %v", endStr, err) 279 return 280 } 281 } 282 283 c := viewerDataTraceConsumer(w, start, end) 284 if err := generateTrace(params, c); err != nil { 285 log.Printf("failed to generate trace: %v", err) 286 return 287 } 288 } 289 290 type Range struct { 291 Name string 292 Start int 293 End int 294 StartTime int64 295 EndTime int64 296 } 297 298 func (r Range) URL() string { 299 return fmt.Sprintf("/trace?start=%d&end=%d", r.Start, r.End) 300 } 301 302 // splitTrace splits the trace into a number of ranges, 303 // each resulting in approx 100MB of json output 304 // (trace viewer can hardly handle more). 305 func splitTrace(res trace.ParseResult) []Range { 306 params := &traceParams{ 307 parsed: res, 308 endTime: math.MaxInt64, 309 } 310 s, c := splittingTraceConsumer(100 << 20) // 100M 311 if err := generateTrace(params, c); err != nil { 312 dief("%v\n", err) 313 } 314 return s.Ranges 315 } 316 317 type splitter struct { 318 Ranges []Range 319 } 320 321 func splittingTraceConsumer(max int) (*splitter, traceConsumer) { 322 type eventSz struct { 323 Time float64 324 Sz int 325 } 326 327 var ( 328 data = ViewerData{Frames: make(map[string]ViewerFrame)} 329 330 sizes []eventSz 331 cw countingWriter 332 ) 333 334 s := new(splitter) 335 336 return s, traceConsumer{ 337 consumeTimeUnit: func(unit string) { 338 data.TimeUnit = unit 339 }, 340 consumeViewerEvent: func(v *ViewerEvent, required bool) { 341 if required { 342 // Store required events inside data 343 // so flush can include them in the required 344 // part of the trace. 345 data.Events = append(data.Events, v) 346 return 347 } 348 enc := json.NewEncoder(&cw) 349 enc.Encode(v) 350 sizes = append(sizes, eventSz{v.Time, cw.size + 1}) // +1 for ",". 351 cw.size = 0 352 }, 353 consumeViewerFrame: func(k string, v ViewerFrame) { 354 data.Frames[k] = v 355 }, 356 flush: func() { 357 // Calculate size of the mandatory part of the trace. 358 // This includes stack traces and thread names. 359 cw.size = 0 360 enc := json.NewEncoder(&cw) 361 enc.Encode(data) 362 minSize := cw.size 363 364 // Then calculate size of each individual event 365 // and group them into ranges. 366 sum := minSize 367 start := 0 368 for i, ev := range sizes { 369 if sum+ev.Sz > max { 370 startTime := time.Duration(sizes[start].Time * 1000) 371 endTime := time.Duration(ev.Time * 1000) 372 ranges = append(ranges, Range{ 373 Name: fmt.Sprintf("%v-%v", startTime, endTime), 374 Start: start, 375 End: i + 1, 376 StartTime: int64(startTime), 377 EndTime: int64(endTime), 378 }) 379 start = i + 1 380 sum = minSize 381 } else { 382 sum += ev.Sz + 1 383 } 384 } 385 if len(ranges) <= 1 { 386 s.Ranges = nil 387 return 388 } 389 390 if end := len(sizes) - 1; start < end { 391 ranges = append(ranges, Range{ 392 Name: fmt.Sprintf("%v-%v", time.Duration(sizes[start].Time*1000), time.Duration(sizes[end].Time*1000)), 393 Start: start, 394 End: end, 395 StartTime: int64(sizes[start].Time * 1000), 396 EndTime: int64(sizes[end].Time * 1000), 397 }) 398 } 399 s.Ranges = ranges 400 }, 401 } 402 } 403 404 type countingWriter struct { 405 size int 406 } 407 408 func (cw *countingWriter) Write(data []byte) (int, error) { 409 cw.size += len(data) 410 return len(data), nil 411 } 412 413 type traceParams struct { 414 parsed trace.ParseResult 415 mode traceviewMode 416 startTime int64 417 endTime int64 418 maing uint64 // for goroutine-oriented view, place this goroutine on the top row 419 gs map[uint64]bool // Goroutines to be displayed for goroutine-oriented or task-oriented view 420 tasks []*taskDesc // Tasks to be displayed. tasks[0] is the top-most task 421 } 422 423 type traceviewMode uint 424 425 const ( 426 modeGoroutineOriented traceviewMode = 1 << iota 427 modeTaskOriented 428 ) 429 430 type traceContext struct { 431 *traceParams 432 consumer traceConsumer 433 frameTree frameNode 434 frameSeq int 435 arrowSeq uint64 436 gcount uint64 437 438 heapStats, prevHeapStats heapStats 439 threadStats, prevThreadStats threadStats 440 gstates, prevGstates [gStateCount]int64 441 442 regionID int // last emitted region id. incremented in each emitRegion call. 443 } 444 445 type heapStats struct { 446 heapAlloc uint64 447 nextGC uint64 448 } 449 450 type threadStats struct { 451 insyscallRuntime int64 // system goroutine in syscall 452 insyscall int64 // user goroutine in syscall 453 prunning int64 // thread running P 454 } 455 456 type frameNode struct { 457 id int 458 children map[uint64]frameNode 459 } 460 461 type gState int 462 463 const ( 464 gDead gState = iota 465 gRunnable 466 gRunning 467 gWaiting 468 gWaitingGC 469 470 gStateCount 471 ) 472 473 type gInfo struct { 474 state gState // current state 475 name string // name chosen for this goroutine at first EvGoStart 476 isSystemG bool 477 start *trace.Event // most recent EvGoStart 478 markAssist *trace.Event // if non-nil, the mark assist currently running. 479 } 480 481 type ViewerData struct { 482 Events []*ViewerEvent `json:"traceEvents"` 483 Frames map[string]ViewerFrame `json:"stackFrames"` 484 TimeUnit string `json:"displayTimeUnit"` 485 486 // This is where mandatory part of the trace starts (e.g. thread names) 487 footer int 488 } 489 490 type ViewerEvent struct { 491 Name string `json:"name,omitempty"` 492 Phase string `json:"ph"` 493 Scope string `json:"s,omitempty"` 494 Time float64 `json:"ts"` 495 Dur float64 `json:"dur,omitempty"` 496 Pid uint64 `json:"pid"` 497 Tid uint64 `json:"tid"` 498 ID uint64 `json:"id,omitempty"` 499 Stack int `json:"sf,omitempty"` 500 EndStack int `json:"esf,omitempty"` 501 Arg interface{} `json:"args,omitempty"` 502 Cname string `json:"cname,omitempty"` 503 Category string `json:"cat,omitempty"` 504 } 505 506 type ViewerFrame struct { 507 Name string `json:"name"` 508 Parent int `json:"parent,omitempty"` 509 } 510 511 type NameArg struct { 512 Name string `json:"name"` 513 } 514 515 type TaskArg struct { 516 ID uint64 `json:"id"` 517 StartG uint64 `json:"start_g,omitempty"` 518 EndG uint64 `json:"end_g,omitempty"` 519 } 520 521 type RegionArg struct { 522 TaskID uint64 `json:"taskid,omitempty"` 523 } 524 525 type SortIndexArg struct { 526 Index int `json:"sort_index"` 527 } 528 529 type traceConsumer struct { 530 consumeTimeUnit func(unit string) 531 consumeViewerEvent func(v *ViewerEvent, required bool) 532 consumeViewerFrame func(key string, f ViewerFrame) 533 flush func() 534 } 535 536 const ( 537 procsSection = 0 // where Goroutines or per-P timelines are presented. 538 statsSection = 1 // where counters are presented. 539 tasksSection = 2 // where Task hierarchy & timeline is presented. 540 ) 541 542 // generateTrace generates json trace for trace-viewer: 543 // https://github.com/google/trace-viewer 544 // Trace format is described at: 545 // https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/view 546 // If mode==goroutineMode, generate trace for goroutine goid, otherwise whole trace. 547 // startTime, endTime determine part of the trace that we are interested in. 548 // gset restricts goroutines that are included in the resulting trace. 549 func generateTrace(params *traceParams, consumer traceConsumer) error { 550 defer consumer.flush() 551 552 ctx := &traceContext{traceParams: params} 553 ctx.frameTree.children = make(map[uint64]frameNode) 554 ctx.consumer = consumer 555 556 ctx.consumer.consumeTimeUnit("ns") 557 maxProc := 0 558 ginfos := make(map[uint64]*gInfo) 559 stacks := params.parsed.Stacks 560 561 getGInfo := func(g uint64) *gInfo { 562 info, ok := ginfos[g] 563 if !ok { 564 info = &gInfo{} 565 ginfos[g] = info 566 } 567 return info 568 } 569 570 // Since we make many calls to setGState, we record a sticky 571 // error in setGStateErr and check it after every event. 572 var setGStateErr error 573 setGState := func(ev *trace.Event, g uint64, oldState, newState gState) { 574 info := getGInfo(g) 575 if oldState == gWaiting && info.state == gWaitingGC { 576 // For checking, gWaiting counts as any gWaiting*. 577 oldState = info.state 578 } 579 if info.state != oldState && setGStateErr == nil { 580 setGStateErr = fmt.Errorf("expected G %d to be in state %d, but got state %d", g, oldState, newState) 581 } 582 ctx.gstates[info.state]-- 583 ctx.gstates[newState]++ 584 info.state = newState 585 } 586 587 for _, ev := range ctx.parsed.Events { 588 // Handle state transitions before we filter out events. 589 switch ev.Type { 590 case trace.EvGoStart, trace.EvGoStartLabel: 591 setGState(ev, ev.G, gRunnable, gRunning) 592 info := getGInfo(ev.G) 593 info.start = ev 594 case trace.EvProcStart: 595 ctx.threadStats.prunning++ 596 case trace.EvProcStop: 597 ctx.threadStats.prunning-- 598 case trace.EvGoCreate: 599 newG := ev.Args[0] 600 info := getGInfo(newG) 601 if info.name != "" { 602 return fmt.Errorf("duplicate go create event for go id=%d detected at offset %d", newG, ev.Off) 603 } 604 605 stk, ok := stacks[ev.Args[1]] 606 if !ok || len(stk) == 0 { 607 return fmt.Errorf("invalid go create event: missing stack information for go id=%d at offset %d", newG, ev.Off) 608 } 609 610 fname := stk[0].Fn 611 info.name = fmt.Sprintf("G%v %s", newG, fname) 612 info.isSystemG = isSystemGoroutine(fname) 613 614 ctx.gcount++ 615 setGState(ev, newG, gDead, gRunnable) 616 case trace.EvGoEnd: 617 ctx.gcount-- 618 setGState(ev, ev.G, gRunning, gDead) 619 case trace.EvGoUnblock: 620 setGState(ev, ev.Args[0], gWaiting, gRunnable) 621 case trace.EvGoSysExit: 622 setGState(ev, ev.G, gWaiting, gRunnable) 623 if getGInfo(ev.G).isSystemG { 624 ctx.threadStats.insyscallRuntime-- 625 } else { 626 ctx.threadStats.insyscall-- 627 } 628 case trace.EvGoSysBlock: 629 setGState(ev, ev.G, gRunning, gWaiting) 630 if getGInfo(ev.G).isSystemG { 631 ctx.threadStats.insyscallRuntime++ 632 } else { 633 ctx.threadStats.insyscall++ 634 } 635 case trace.EvGoSched, trace.EvGoPreempt: 636 setGState(ev, ev.G, gRunning, gRunnable) 637 case trace.EvGoStop, 638 trace.EvGoSleep, trace.EvGoBlock, trace.EvGoBlockSend, trace.EvGoBlockRecv, 639 trace.EvGoBlockSelect, trace.EvGoBlockSync, trace.EvGoBlockCond, trace.EvGoBlockNet: 640 setGState(ev, ev.G, gRunning, gWaiting) 641 case trace.EvGoBlockGC: 642 setGState(ev, ev.G, gRunning, gWaitingGC) 643 case trace.EvGCMarkAssistStart: 644 getGInfo(ev.G).markAssist = ev 645 case trace.EvGCMarkAssistDone: 646 getGInfo(ev.G).markAssist = nil 647 case trace.EvGoWaiting: 648 setGState(ev, ev.G, gRunnable, gWaiting) 649 case trace.EvGoInSyscall: 650 // Cancel out the effect of EvGoCreate at the beginning. 651 setGState(ev, ev.G, gRunnable, gWaiting) 652 if getGInfo(ev.G).isSystemG { 653 ctx.threadStats.insyscallRuntime++ 654 } else { 655 ctx.threadStats.insyscall++ 656 } 657 case trace.EvHeapAlloc: 658 ctx.heapStats.heapAlloc = ev.Args[0] 659 case trace.EvNextGC: 660 ctx.heapStats.nextGC = ev.Args[0] 661 } 662 if setGStateErr != nil { 663 return setGStateErr 664 } 665 if ctx.gstates[gRunnable] < 0 || ctx.gstates[gRunning] < 0 || ctx.threadStats.insyscall < 0 || ctx.threadStats.insyscallRuntime < 0 { 666 return fmt.Errorf("invalid state after processing %v: runnable=%d running=%d insyscall=%d insyscallRuntime=%d", ev, ctx.gstates[gRunnable], ctx.gstates[gRunning], ctx.threadStats.insyscall, ctx.threadStats.insyscallRuntime) 667 } 668 669 // Ignore events that are from uninteresting goroutines 670 // or outside of the interesting timeframe. 671 if ctx.gs != nil && ev.P < trace.FakeP && !ctx.gs[ev.G] { 672 continue 673 } 674 if !withinTimeRange(ev, ctx.startTime, ctx.endTime) { 675 continue 676 } 677 678 if ev.P < trace.FakeP && ev.P > maxProc { 679 maxProc = ev.P 680 } 681 682 // Emit trace objects. 683 switch ev.Type { 684 case trace.EvProcStart: 685 if ctx.mode&modeGoroutineOriented != 0 { 686 continue 687 } 688 ctx.emitInstant(ev, "proc start", "") 689 case trace.EvProcStop: 690 if ctx.mode&modeGoroutineOriented != 0 { 691 continue 692 } 693 ctx.emitInstant(ev, "proc stop", "") 694 case trace.EvGCStart: 695 ctx.emitSlice(ev, "GC") 696 case trace.EvGCDone: 697 case trace.EvGCSTWStart: 698 if ctx.mode&modeGoroutineOriented != 0 { 699 continue 700 } 701 ctx.emitSlice(ev, fmt.Sprintf("STW (%s)", ev.SArgs[0])) 702 case trace.EvGCSTWDone: 703 case trace.EvGCMarkAssistStart: 704 // Mark assists can continue past preemptions, so truncate to the 705 // whichever comes first. We'll synthesize another slice if 706 // necessary in EvGoStart. 707 markFinish := ev.Link 708 goFinish := getGInfo(ev.G).start.Link 709 fakeMarkStart := *ev 710 text := "MARK ASSIST" 711 if markFinish == nil || markFinish.Ts > goFinish.Ts { 712 fakeMarkStart.Link = goFinish 713 text = "MARK ASSIST (unfinished)" 714 } 715 ctx.emitSlice(&fakeMarkStart, text) 716 case trace.EvGCSweepStart: 717 slice := ctx.makeSlice(ev, "SWEEP") 718 if done := ev.Link; done != nil && done.Args[0] != 0 { 719 slice.Arg = struct { 720 Swept uint64 `json:"Swept bytes"` 721 Reclaimed uint64 `json:"Reclaimed bytes"` 722 }{done.Args[0], done.Args[1]} 723 } 724 ctx.emit(slice) 725 case trace.EvGoStart, trace.EvGoStartLabel: 726 info := getGInfo(ev.G) 727 if ev.Type == trace.EvGoStartLabel { 728 ctx.emitSlice(ev, ev.SArgs[0]) 729 } else { 730 ctx.emitSlice(ev, info.name) 731 } 732 if info.markAssist != nil { 733 // If we're in a mark assist, synthesize a new slice, ending 734 // either when the mark assist ends or when we're descheduled. 735 markFinish := info.markAssist.Link 736 goFinish := ev.Link 737 fakeMarkStart := *ev 738 text := "MARK ASSIST (resumed, unfinished)" 739 if markFinish != nil && markFinish.Ts < goFinish.Ts { 740 fakeMarkStart.Link = markFinish 741 text = "MARK ASSIST (resumed)" 742 } 743 ctx.emitSlice(&fakeMarkStart, text) 744 } 745 case trace.EvGoCreate: 746 ctx.emitArrow(ev, "go") 747 case trace.EvGoUnblock: 748 ctx.emitArrow(ev, "unblock") 749 case trace.EvGoSysCall: 750 ctx.emitInstant(ev, "syscall", "") 751 case trace.EvGoSysExit: 752 ctx.emitArrow(ev, "sysexit") 753 case trace.EvUserLog: 754 ctx.emitInstant(ev, formatUserLog(ev), "user event") 755 case trace.EvUserTaskCreate: 756 ctx.emitInstant(ev, "task start", "user event") 757 case trace.EvUserTaskEnd: 758 ctx.emitInstant(ev, "task end", "user event") 759 } 760 // Emit any counter updates. 761 ctx.emitThreadCounters(ev) 762 ctx.emitHeapCounters(ev) 763 ctx.emitGoroutineCounters(ev) 764 } 765 766 ctx.emitSectionFooter(statsSection, "STATS", 0) 767 768 if ctx.mode&modeTaskOriented != 0 { 769 ctx.emitSectionFooter(tasksSection, "TASKS", 1) 770 } 771 772 if ctx.mode&modeGoroutineOriented != 0 { 773 ctx.emitSectionFooter(procsSection, "G", 2) 774 } else { 775 ctx.emitSectionFooter(procsSection, "PROCS", 2) 776 } 777 778 ctx.emitFooter(&ViewerEvent{Name: "thread_name", Phase: "M", Pid: procsSection, Tid: trace.GCP, Arg: &NameArg{"GC"}}) 779 ctx.emitFooter(&ViewerEvent{Name: "thread_sort_index", Phase: "M", Pid: procsSection, Tid: trace.GCP, Arg: &SortIndexArg{-6}}) 780 781 ctx.emitFooter(&ViewerEvent{Name: "thread_name", Phase: "M", Pid: procsSection, Tid: trace.NetpollP, Arg: &NameArg{"Network"}}) 782 ctx.emitFooter(&ViewerEvent{Name: "thread_sort_index", Phase: "M", Pid: procsSection, Tid: trace.NetpollP, Arg: &SortIndexArg{-5}}) 783 784 ctx.emitFooter(&ViewerEvent{Name: "thread_name", Phase: "M", Pid: procsSection, Tid: trace.TimerP, Arg: &NameArg{"Timers"}}) 785 ctx.emitFooter(&ViewerEvent{Name: "thread_sort_index", Phase: "M", Pid: procsSection, Tid: trace.TimerP, Arg: &SortIndexArg{-4}}) 786 787 ctx.emitFooter(&ViewerEvent{Name: "thread_name", Phase: "M", Pid: procsSection, Tid: trace.SyscallP, Arg: &NameArg{"Syscalls"}}) 788 ctx.emitFooter(&ViewerEvent{Name: "thread_sort_index", Phase: "M", Pid: procsSection, Tid: trace.SyscallP, Arg: &SortIndexArg{-3}}) 789 790 // Display rows for Ps if we are in the default trace view mode (not goroutine-oriented presentation) 791 if ctx.mode&modeGoroutineOriented == 0 { 792 for i := 0; i <= maxProc; i++ { 793 ctx.emitFooter(&ViewerEvent{Name: "thread_name", Phase: "M", Pid: procsSection, Tid: uint64(i), Arg: &NameArg{fmt.Sprintf("Proc %v", i)}}) 794 ctx.emitFooter(&ViewerEvent{Name: "thread_sort_index", Phase: "M", Pid: procsSection, Tid: uint64(i), Arg: &SortIndexArg{i}}) 795 } 796 } 797 798 // Display task and its regions if we are in task-oriented presentation mode. 799 if ctx.mode&modeTaskOriented != 0 { 800 // sort tasks based on the task start time. 801 sortedTask := make([]*taskDesc, 0, len(ctx.tasks)) 802 for _, task := range ctx.tasks { 803 sortedTask = append(sortedTask, task) 804 } 805 sort.SliceStable(sortedTask, func(i, j int) bool { 806 ti, tj := sortedTask[i], sortedTask[j] 807 if ti.firstTimestamp() == tj.firstTimestamp() { 808 return ti.lastTimestamp() < tj.lastTimestamp() 809 } 810 return ti.firstTimestamp() < tj.firstTimestamp() 811 }) 812 813 for i, task := range sortedTask { 814 ctx.emitTask(task, i) 815 816 // If we are in goroutine-oriented mode, we draw regions. 817 // TODO(hyangah): add this for task/P-oriented mode (i.e., focustask view) too. 818 if ctx.mode&modeGoroutineOriented != 0 { 819 for _, s := range task.regions { 820 ctx.emitRegion(s) 821 } 822 } 823 } 824 } 825 826 // Display goroutine rows if we are either in goroutine-oriented mode. 827 if ctx.mode&modeGoroutineOriented != 0 { 828 for k, v := range ginfos { 829 if !ctx.gs[k] { 830 continue 831 } 832 ctx.emitFooter(&ViewerEvent{Name: "thread_name", Phase: "M", Pid: procsSection, Tid: k, Arg: &NameArg{v.name}}) 833 } 834 // Row for the main goroutine (maing) 835 ctx.emitFooter(&ViewerEvent{Name: "thread_sort_index", Phase: "M", Pid: procsSection, Tid: ctx.maing, Arg: &SortIndexArg{-2}}) 836 // Row for GC or global state (specified with G=0) 837 ctx.emitFooter(&ViewerEvent{Name: "thread_sort_index", Phase: "M", Pid: procsSection, Tid: 0, Arg: &SortIndexArg{-1}}) 838 } 839 840 return nil 841 } 842 843 func (ctx *traceContext) emit(e *ViewerEvent) { 844 ctx.consumer.consumeViewerEvent(e, false) 845 } 846 847 func (ctx *traceContext) emitFooter(e *ViewerEvent) { 848 ctx.consumer.consumeViewerEvent(e, true) 849 } 850 func (ctx *traceContext) emitSectionFooter(sectionID uint64, name string, priority int) { 851 ctx.emitFooter(&ViewerEvent{Name: "process_name", Phase: "M", Pid: sectionID, Arg: &NameArg{name}}) 852 ctx.emitFooter(&ViewerEvent{Name: "process_sort_index", Phase: "M", Pid: sectionID, Arg: &SortIndexArg{priority}}) 853 } 854 855 func (ctx *traceContext) time(ev *trace.Event) float64 { 856 // Trace viewer wants timestamps in microseconds. 857 return float64(ev.Ts) / 1000 858 } 859 860 func withinTimeRange(ev *trace.Event, s, e int64) bool { 861 if evEnd := ev.Link; evEnd != nil { 862 return ev.Ts <= e && evEnd.Ts >= s 863 } 864 return ev.Ts >= s && ev.Ts <= e 865 } 866 867 func tsWithinRange(ts, s, e int64) bool { 868 return s <= ts && ts <= e 869 } 870 871 func (ctx *traceContext) proc(ev *trace.Event) uint64 { 872 if ctx.mode&modeGoroutineOriented != 0 && ev.P < trace.FakeP { 873 return ev.G 874 } else { 875 return uint64(ev.P) 876 } 877 } 878 879 func (ctx *traceContext) emitSlice(ev *trace.Event, name string) { 880 ctx.emit(ctx.makeSlice(ev, name)) 881 } 882 883 func (ctx *traceContext) makeSlice(ev *trace.Event, name string) *ViewerEvent { 884 // If ViewerEvent.Dur is not a positive value, 885 // trace viewer handles it as a non-terminating time interval. 886 // Avoid it by setting the field with a small value. 887 durationUsec := ctx.time(ev.Link) - ctx.time(ev) 888 if ev.Link.Ts-ev.Ts <= 0 { 889 durationUsec = 0.0001 // 0.1 nanoseconds 890 } 891 sl := &ViewerEvent{ 892 Name: name, 893 Phase: "X", 894 Time: ctx.time(ev), 895 Dur: durationUsec, 896 Tid: ctx.proc(ev), 897 Stack: ctx.stack(ev.Stk), 898 EndStack: ctx.stack(ev.Link.Stk), 899 } 900 901 // grey out non-overlapping events if the event is not a global event (ev.G == 0) 902 if ctx.mode&modeTaskOriented != 0 && ev.G != 0 { 903 // include P information. 904 if t := ev.Type; t == trace.EvGoStart || t == trace.EvGoStartLabel { 905 type Arg struct { 906 P int 907 } 908 sl.Arg = &Arg{P: ev.P} 909 } 910 // grey out non-overlapping events. 911 overlapping := false 912 for _, task := range ctx.tasks { 913 if _, overlapped := task.overlappingDuration(ev); overlapped { 914 overlapping = true 915 break 916 } 917 } 918 if !overlapping { 919 sl.Cname = colorLightGrey 920 } 921 } 922 return sl 923 } 924 925 func (ctx *traceContext) emitTask(task *taskDesc, sortIndex int) { 926 taskRow := uint64(task.id) 927 taskName := task.name 928 durationUsec := float64(task.lastTimestamp()-task.firstTimestamp()) / 1e3 929 930 ctx.emitFooter(&ViewerEvent{Name: "thread_name", Phase: "M", Pid: tasksSection, Tid: taskRow, Arg: &NameArg{fmt.Sprintf("T%d %s", task.id, taskName)}}) 931 ctx.emit(&ViewerEvent{Name: "thread_sort_index", Phase: "M", Pid: tasksSection, Tid: taskRow, Arg: &SortIndexArg{sortIndex}}) 932 ts := float64(task.firstTimestamp()) / 1e3 933 sl := &ViewerEvent{ 934 Name: taskName, 935 Phase: "X", 936 Time: ts, 937 Dur: durationUsec, 938 Pid: tasksSection, 939 Tid: taskRow, 940 Cname: pickTaskColor(task.id), 941 } 942 targ := TaskArg{ID: task.id} 943 if task.create != nil { 944 sl.Stack = ctx.stack(task.create.Stk) 945 targ.StartG = task.create.G 946 } 947 if task.end != nil { 948 sl.EndStack = ctx.stack(task.end.Stk) 949 targ.EndG = task.end.G 950 } 951 sl.Arg = targ 952 ctx.emit(sl) 953 954 if task.create != nil && task.create.Type == trace.EvUserTaskCreate && task.create.Args[1] != 0 { 955 ctx.arrowSeq++ 956 ctx.emit(&ViewerEvent{Name: "newTask", Phase: "s", Tid: task.create.Args[1], ID: ctx.arrowSeq, Time: ts, Pid: tasksSection}) 957 ctx.emit(&ViewerEvent{Name: "newTask", Phase: "t", Tid: taskRow, ID: ctx.arrowSeq, Time: ts, Pid: tasksSection}) 958 } 959 } 960 961 func (ctx *traceContext) emitRegion(s regionDesc) { 962 if s.Name == "" { 963 return 964 } 965 966 if !tsWithinRange(s.firstTimestamp(), ctx.startTime, ctx.endTime) && 967 !tsWithinRange(s.lastTimestamp(), ctx.startTime, ctx.endTime) { 968 return 969 } 970 971 ctx.regionID++ 972 regionID := ctx.regionID 973 974 id := s.TaskID 975 scopeID := fmt.Sprintf("%x", id) 976 name := s.Name 977 978 sl0 := &ViewerEvent{ 979 Category: "Region", 980 Name: name, 981 Phase: "b", 982 Time: float64(s.firstTimestamp()) / 1e3, 983 Tid: s.G, // only in goroutine-oriented view 984 ID: uint64(regionID), 985 Scope: scopeID, 986 Cname: pickTaskColor(s.TaskID), 987 } 988 if s.Start != nil { 989 sl0.Stack = ctx.stack(s.Start.Stk) 990 } 991 ctx.emit(sl0) 992 993 sl1 := &ViewerEvent{ 994 Category: "Region", 995 Name: name, 996 Phase: "e", 997 Time: float64(s.lastTimestamp()) / 1e3, 998 Tid: s.G, 999 ID: uint64(regionID), 1000 Scope: scopeID, 1001 Cname: pickTaskColor(s.TaskID), 1002 Arg: RegionArg{TaskID: s.TaskID}, 1003 } 1004 if s.End != nil { 1005 sl1.Stack = ctx.stack(s.End.Stk) 1006 } 1007 ctx.emit(sl1) 1008 } 1009 1010 type heapCountersArg struct { 1011 Allocated uint64 1012 NextGC uint64 1013 } 1014 1015 func (ctx *traceContext) emitHeapCounters(ev *trace.Event) { 1016 if ctx.prevHeapStats == ctx.heapStats { 1017 return 1018 } 1019 diff := uint64(0) 1020 if ctx.heapStats.nextGC > ctx.heapStats.heapAlloc { 1021 diff = ctx.heapStats.nextGC - ctx.heapStats.heapAlloc 1022 } 1023 if tsWithinRange(ev.Ts, ctx.startTime, ctx.endTime) { 1024 ctx.emit(&ViewerEvent{Name: "Heap", Phase: "C", Time: ctx.time(ev), Pid: 1, Arg: &heapCountersArg{ctx.heapStats.heapAlloc, diff}}) 1025 } 1026 ctx.prevHeapStats = ctx.heapStats 1027 } 1028 1029 type goroutineCountersArg struct { 1030 Running uint64 1031 Runnable uint64 1032 GCWaiting uint64 1033 } 1034 1035 func (ctx *traceContext) emitGoroutineCounters(ev *trace.Event) { 1036 if ctx.prevGstates == ctx.gstates { 1037 return 1038 } 1039 if tsWithinRange(ev.Ts, ctx.startTime, ctx.endTime) { 1040 ctx.emit(&ViewerEvent{Name: "Goroutines", Phase: "C", Time: ctx.time(ev), Pid: 1, Arg: &goroutineCountersArg{uint64(ctx.gstates[gRunning]), uint64(ctx.gstates[gRunnable]), uint64(ctx.gstates[gWaitingGC])}}) 1041 } 1042 ctx.prevGstates = ctx.gstates 1043 } 1044 1045 type threadCountersArg struct { 1046 Running int64 1047 InSyscall int64 1048 } 1049 1050 func (ctx *traceContext) emitThreadCounters(ev *trace.Event) { 1051 if ctx.prevThreadStats == ctx.threadStats { 1052 return 1053 } 1054 if tsWithinRange(ev.Ts, ctx.startTime, ctx.endTime) { 1055 ctx.emit(&ViewerEvent{Name: "Threads", Phase: "C", Time: ctx.time(ev), Pid: 1, Arg: &threadCountersArg{ 1056 Running: ctx.threadStats.prunning, 1057 InSyscall: ctx.threadStats.insyscall}}) 1058 } 1059 ctx.prevThreadStats = ctx.threadStats 1060 } 1061 1062 func (ctx *traceContext) emitInstant(ev *trace.Event, name, category string) { 1063 if !tsWithinRange(ev.Ts, ctx.startTime, ctx.endTime) { 1064 return 1065 } 1066 1067 cname := "" 1068 if ctx.mode&modeTaskOriented != 0 { 1069 taskID, isUserAnnotation := isUserAnnotationEvent(ev) 1070 1071 show := false 1072 for _, task := range ctx.tasks { 1073 if isUserAnnotation && task.id == taskID || task.overlappingInstant(ev) { 1074 show = true 1075 break 1076 } 1077 } 1078 // grey out or skip if non-overlapping instant. 1079 if !show { 1080 if isUserAnnotation { 1081 return // don't display unrelated user annotation events. 1082 } 1083 cname = colorLightGrey 1084 } 1085 } 1086 var arg interface{} 1087 if ev.Type == trace.EvProcStart { 1088 type Arg struct { 1089 ThreadID uint64 1090 } 1091 arg = &Arg{ev.Args[0]} 1092 } 1093 ctx.emit(&ViewerEvent{ 1094 Name: name, 1095 Category: category, 1096 Phase: "I", 1097 Scope: "t", 1098 Time: ctx.time(ev), 1099 Tid: ctx.proc(ev), 1100 Stack: ctx.stack(ev.Stk), 1101 Cname: cname, 1102 Arg: arg}) 1103 } 1104 1105 func (ctx *traceContext) emitArrow(ev *trace.Event, name string) { 1106 if ev.Link == nil { 1107 // The other end of the arrow is not captured in the trace. 1108 // For example, a goroutine was unblocked but was not scheduled before trace stop. 1109 return 1110 } 1111 if ctx.mode&modeGoroutineOriented != 0 && (!ctx.gs[ev.Link.G] || ev.Link.Ts < ctx.startTime || ev.Link.Ts > ctx.endTime) { 1112 return 1113 } 1114 1115 if ev.P == trace.NetpollP || ev.P == trace.TimerP || ev.P == trace.SyscallP { 1116 // Trace-viewer discards arrows if they don't start/end inside of a slice or instant. 1117 // So emit a fake instant at the start of the arrow. 1118 ctx.emitInstant(&trace.Event{P: ev.P, Ts: ev.Ts}, "unblock", "") 1119 } 1120 1121 color := "" 1122 if ctx.mode&modeTaskOriented != 0 { 1123 overlapping := false 1124 // skip non-overlapping arrows. 1125 for _, task := range ctx.tasks { 1126 if _, overlapped := task.overlappingDuration(ev); overlapped { 1127 overlapping = true 1128 break 1129 } 1130 } 1131 if !overlapping { 1132 return 1133 } 1134 } 1135 1136 ctx.arrowSeq++ 1137 ctx.emit(&ViewerEvent{Name: name, Phase: "s", Tid: ctx.proc(ev), ID: ctx.arrowSeq, Time: ctx.time(ev), Stack: ctx.stack(ev.Stk), Cname: color}) 1138 ctx.emit(&ViewerEvent{Name: name, Phase: "t", Tid: ctx.proc(ev.Link), ID: ctx.arrowSeq, Time: ctx.time(ev.Link), Cname: color}) 1139 } 1140 1141 func (ctx *traceContext) stack(stk []*trace.Frame) int { 1142 return ctx.buildBranch(ctx.frameTree, stk) 1143 } 1144 1145 // buildBranch builds one branch in the prefix tree rooted at ctx.frameTree. 1146 func (ctx *traceContext) buildBranch(parent frameNode, stk []*trace.Frame) int { 1147 if len(stk) == 0 { 1148 return parent.id 1149 } 1150 last := len(stk) - 1 1151 frame := stk[last] 1152 stk = stk[:last] 1153 1154 node, ok := parent.children[frame.PC] 1155 if !ok { 1156 ctx.frameSeq++ 1157 node.id = ctx.frameSeq 1158 node.children = make(map[uint64]frameNode) 1159 parent.children[frame.PC] = node 1160 ctx.consumer.consumeViewerFrame(strconv.Itoa(node.id), ViewerFrame{fmt.Sprintf("%v:%v", frame.Fn, frame.Line), parent.id}) 1161 } 1162 return ctx.buildBranch(node, stk) 1163 } 1164 1165 func isSystemGoroutine(entryFn string) bool { 1166 // This mimics runtime.isSystemGoroutine as closely as 1167 // possible. 1168 return entryFn != "runtime.main" && strings.HasPrefix(entryFn, "runtime.") 1169 } 1170 1171 // firstTimestamp returns the timestamp of the first event record. 1172 func firstTimestamp() int64 { 1173 res, _ := parseTrace() 1174 if len(res.Events) > 0 { 1175 return res.Events[0].Ts 1176 } 1177 return 0 1178 } 1179 1180 // lastTimestamp returns the timestamp of the last event record. 1181 func lastTimestamp() int64 { 1182 res, _ := parseTrace() 1183 if n := len(res.Events); n > 1 { 1184 return res.Events[n-1].Ts 1185 } 1186 return 0 1187 } 1188 1189 type jsonWriter struct { 1190 w io.Writer 1191 enc *json.Encoder 1192 } 1193 1194 func viewerDataTraceConsumer(w io.Writer, start, end int64) traceConsumer { 1195 frames := make(map[string]ViewerFrame) 1196 enc := json.NewEncoder(w) 1197 written := 0 1198 index := int64(-1) 1199 1200 io.WriteString(w, "{") 1201 return traceConsumer{ 1202 consumeTimeUnit: func(unit string) { 1203 io.WriteString(w, `"displayTimeUnit":`) 1204 enc.Encode(unit) 1205 io.WriteString(w, ",") 1206 }, 1207 consumeViewerEvent: func(v *ViewerEvent, required bool) { 1208 index++ 1209 if !required && (index < start || index > end) { 1210 // not in the range. Skip! 1211 return 1212 } 1213 if written == 0 { 1214 io.WriteString(w, `"traceEvents": [`) 1215 } 1216 if written > 0 { 1217 io.WriteString(w, ",") 1218 } 1219 enc.Encode(v) 1220 // TODO: get rid of the extra \n inserted by enc.Encode. 1221 // Same should be applied to splittingTraceConsumer. 1222 written++ 1223 }, 1224 consumeViewerFrame: func(k string, v ViewerFrame) { 1225 frames[k] = v 1226 }, 1227 flush: func() { 1228 io.WriteString(w, `], "stackFrames":`) 1229 enc.Encode(frames) 1230 io.WriteString(w, `}`) 1231 }, 1232 } 1233 } 1234 1235 // Mapping from more reasonable color names to the reserved color names in 1236 // https://github.com/catapult-project/catapult/blob/master/tracing/tracing/base/color_scheme.html#L50 1237 // The chrome trace viewer allows only those as cname values. 1238 const ( 1239 colorLightMauve = "thread_state_uninterruptible" // 182, 125, 143 1240 colorOrange = "thread_state_iowait" // 255, 140, 0 1241 colorSeafoamGreen = "thread_state_running" // 126, 200, 148 1242 colorVistaBlue = "thread_state_runnable" // 133, 160, 210 1243 colorTan = "thread_state_unknown" // 199, 155, 125 1244 colorIrisBlue = "background_memory_dump" // 0, 180, 180 1245 colorMidnightBlue = "light_memory_dump" // 0, 0, 180 1246 colorDeepMagenta = "detailed_memory_dump" // 180, 0, 180 1247 colorBlue = "vsync_highlight_color" // 0, 0, 255 1248 colorGrey = "generic_work" // 125, 125, 125 1249 colorGreen = "good" // 0, 125, 0 1250 colorDarkGoldenrod = "bad" // 180, 125, 0 1251 colorPeach = "terrible" // 180, 0, 0 1252 colorBlack = "black" // 0, 0, 0 1253 colorLightGrey = "grey" // 221, 221, 221 1254 colorWhite = "white" // 255, 255, 255 1255 colorYellow = "yellow" // 255, 255, 0 1256 colorOlive = "olive" // 100, 100, 0 1257 colorCornflowerBlue = "rail_response" // 67, 135, 253 1258 colorSunsetOrange = "rail_animation" // 244, 74, 63 1259 colorTangerine = "rail_idle" // 238, 142, 0 1260 colorShamrockGreen = "rail_load" // 13, 168, 97 1261 colorGreenishYellow = "startup" // 230, 230, 0 1262 colorDarkGrey = "heap_dump_stack_frame" // 128, 128, 128 1263 colorTawny = "heap_dump_child_node_arrow" // 204, 102, 0 1264 colorLemon = "cq_build_running" // 255, 255, 119 1265 colorLime = "cq_build_passed" // 153, 238, 102 1266 colorPink = "cq_build_failed" // 238, 136, 136 1267 colorSilver = "cq_build_abandoned" // 187, 187, 187 1268 colorManzGreen = "cq_build_attempt_runnig" // 222, 222, 75 1269 colorKellyGreen = "cq_build_attempt_passed" // 108, 218, 35 1270 colorAnotherGrey = "cq_build_attempt_failed" // 187, 187, 187 1271 ) 1272 1273 var colorForTask = []string{ 1274 colorLightMauve, 1275 colorOrange, 1276 colorSeafoamGreen, 1277 colorVistaBlue, 1278 colorTan, 1279 colorMidnightBlue, 1280 colorIrisBlue, 1281 colorDeepMagenta, 1282 colorGreen, 1283 colorDarkGoldenrod, 1284 colorPeach, 1285 colorOlive, 1286 colorCornflowerBlue, 1287 colorSunsetOrange, 1288 colorTangerine, 1289 colorShamrockGreen, 1290 colorTawny, 1291 colorLemon, 1292 colorLime, 1293 colorPink, 1294 colorSilver, 1295 colorManzGreen, 1296 colorKellyGreen, 1297 } 1298 1299 func pickTaskColor(id uint64) string { 1300 idx := id % uint64(len(colorForTask)) 1301 return colorForTask[idx] 1302 }