| /* | |
| * | |
| * Copyright 2019 gRPC authors. | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| * | |
| */ | |
| package main | |
| import ( | |
| "encoding/binary" | |
| "encoding/json" | |
| "fmt" | |
| "os" | |
| "sort" | |
| "strings" | |
| ppb "google.golang.org/grpc/profiling/proto" | |
| ) | |
| type jsonNode struct { | |
| Name string `json:"name"` | |
| Cat string `json:"cat"` | |
| ID string `json:"id"` | |
| Cname string `json:"cname"` | |
| Phase string `json:"ph"` | |
| Timestamp float64 `json:"ts"` | |
| PID string `json:"pid"` | |
| TID string `json:"tid"` | |
| } | |
| // Catapult does not allow specifying colours manually; a 20-odd predefined | |
| // labels are used (that don't make much sense outside the context of | |
| // Chromium). See this for more details: | |
| // | |
| // https://github.com/catapult-project/catapult/blob/bef344f7017fc9e04f7049d0f58af6d9ce9f4ab6/tracing/tracing/base/color_scheme.html#L29 | |
| func hashCname(tag string) string { | |
| if strings.Contains(tag, "encoding") { | |
| return "rail_response" | |
| } | |
| if strings.Contains(tag, "compression") { | |
| return "cq_build_passed" | |
| } | |
| if strings.Contains(tag, "transport") { | |
| if strings.Contains(tag, "blocking") { | |
| return "rail_animation" | |
| } | |
| return "good" | |
| } | |
| if strings.Contains(tag, "header") { | |
| return "cq_build_attempt_failed" | |
| } | |
| if tag == "/" { | |
| return "heap_dump_stack_frame" | |
| } | |
| if strings.Contains(tag, "flow") || strings.Contains(tag, "tmp") { | |
| return "heap_dump_stack_frame" | |
| } | |
| return "" | |
| } | |
| // filterCounter identifies the counter-th instance of a timer of the type | |
| // `filter` within a Stat. This, in conjunction with the counter data structure | |
| // defined below, is used to draw flows between linked loopy writer/reader | |
| // events with application goroutine events in trace-viewer. This is possible | |
| // because enqueues and dequeues are ordered -- that is, the first dequeue must | |
| // be dequeueing the first enqueue operation. | |
| func filterCounter(stat *ppb.Stat, filter string, counter int) int { | |
| localCounter := 0 | |
| for i := 0; i < len(stat.Timers); i++ { | |
| if stat.Timers[i].Tags == filter { | |
| if localCounter == counter { | |
| return i | |
| } | |
| localCounter++ | |
| } | |
| } | |
| return -1 | |
| } | |
| // counter is state object used to store and retrieve the number of timers of a | |
| // particular type that have been seen. | |
| type counter struct { | |
| c map[string]int | |
| } | |
| func newCounter() *counter { | |
| return &counter{c: make(map[string]int)} | |
| } | |
| func (c *counter) GetAndInc(s string) int { | |
| ret := c.c[s] | |
| c.c[s]++ | |
| return ret | |
| } | |
| func catapultNs(sec int64, nsec int32) float64 { | |
| return float64((sec * 1000000000) + int64(nsec)) | |
| } | |
| // streamStatsCatapultJSONSingle processes a single proto Stat object to return | |
| // an array of jsonNodes in trace-viewer's format. | |
| func streamStatsCatapultJSONSingle(stat *ppb.Stat, baseSec int64, baseNsec int32) []jsonNode { | |
| if len(stat.Timers) == 0 { | |
| return nil | |
| } | |
| connectionCounter := binary.BigEndian.Uint64(stat.Metadata[0:8]) | |
| streamID := binary.BigEndian.Uint32(stat.Metadata[8:12]) | |
| opid := fmt.Sprintf("/%s/%d/%d", stat.Tags, connectionCounter, streamID) | |
| var loopyReaderGoID, loopyWriterGoID int64 | |
| for i := 0; i < len(stat.Timers) && (loopyReaderGoID == 0 || loopyWriterGoID == 0); i++ { | |
| if strings.Contains(stat.Timers[i].Tags, "/loopyReader") { | |
| loopyReaderGoID = stat.Timers[i].GoId | |
| } else if strings.Contains(stat.Timers[i].Tags, "/loopyWriter") { | |
| loopyWriterGoID = stat.Timers[i].GoId | |
| } | |
| } | |
| lrc, lwc := newCounter(), newCounter() | |
| var result []jsonNode | |
| result = append(result, | |
| jsonNode{ | |
| Name: "loopyReaderTmp", | |
| ID: opid, | |
| Cname: hashCname("tmp"), | |
| Phase: "i", | |
| Timestamp: 0, | |
| PID: fmt.Sprintf("/%s/%d/loopyReader", stat.Tags, connectionCounter), | |
| TID: fmt.Sprintf("%d", loopyReaderGoID), | |
| }, | |
| jsonNode{ | |
| Name: "loopyWriterTmp", | |
| ID: opid, | |
| Cname: hashCname("tmp"), | |
| Phase: "i", | |
| Timestamp: 0, | |
| PID: fmt.Sprintf("/%s/%d/loopyWriter", stat.Tags, connectionCounter), | |
| TID: fmt.Sprintf("%d", loopyWriterGoID), | |
| }, | |
| ) | |
| for i := 0; i < len(stat.Timers); i++ { | |
| categories := stat.Tags | |
| pid, tid := opid, fmt.Sprintf("%d", stat.Timers[i].GoId) | |
| if stat.Timers[i].GoId == loopyReaderGoID { | |
| pid, tid = fmt.Sprintf("/%s/%d/loopyReader", stat.Tags, connectionCounter), fmt.Sprintf("%d", stat.Timers[i].GoId) | |
| var flowEndID int | |
| var flowEndPID, flowEndTID string | |
| switch stat.Timers[i].Tags { | |
| case "/http2/recv/header": | |
| flowEndID = filterCounter(stat, "/grpc/stream/recv/header", lrc.GetAndInc("/http2/recv/header")) | |
| if flowEndID != -1 { | |
| flowEndPID = opid | |
| flowEndTID = fmt.Sprintf("%d", stat.Timers[flowEndID].GoId) | |
| } else { | |
| logger.Infof("cannot find %s/grpc/stream/recv/header for %s/http2/recv/header", opid, opid) | |
| } | |
| case "/http2/recv/dataFrame/loopyReader": | |
| flowEndID = filterCounter(stat, "/recvAndDecompress", lrc.GetAndInc("/http2/recv/dataFrame/loopyReader")) | |
| if flowEndID != -1 { | |
| flowEndPID = opid | |
| flowEndTID = fmt.Sprintf("%d", stat.Timers[flowEndID].GoId) | |
| } else { | |
| logger.Infof("cannot find %s/recvAndDecompress for %s/http2/recv/dataFrame/loopyReader", opid, opid) | |
| } | |
| default: | |
| flowEndID = -1 | |
| } | |
| if flowEndID != -1 { | |
| flowID := fmt.Sprintf("lrc begin:/%d%s end:/%d%s begin:(%d, %s, %s) end:(%d, %s, %s)", connectionCounter, stat.Timers[i].Tags, connectionCounter, stat.Timers[flowEndID].Tags, i, pid, tid, flowEndID, flowEndPID, flowEndTID) | |
| result = append(result, | |
| jsonNode{ | |
| Name: fmt.Sprintf("%s/flow", opid), | |
| Cat: categories + ",flow", | |
| ID: flowID, | |
| Cname: hashCname("flow"), | |
| Phase: "s", | |
| Timestamp: catapultNs(stat.Timers[i].EndSec-baseSec, stat.Timers[i].EndNsec-baseNsec), | |
| PID: pid, | |
| TID: tid, | |
| }, | |
| jsonNode{ | |
| Name: fmt.Sprintf("%s/flow", opid), | |
| Cat: categories + ",flow", | |
| ID: flowID, | |
| Cname: hashCname("flow"), | |
| Phase: "f", | |
| Timestamp: catapultNs(stat.Timers[flowEndID].BeginSec-baseSec, stat.Timers[flowEndID].BeginNsec-baseNsec), | |
| PID: flowEndPID, | |
| TID: flowEndTID, | |
| }, | |
| ) | |
| } | |
| } else if stat.Timers[i].GoId == loopyWriterGoID { | |
| pid, tid = fmt.Sprintf("/%s/%d/loopyWriter", stat.Tags, connectionCounter), fmt.Sprintf("%d", stat.Timers[i].GoId) | |
| var flowBeginID int | |
| var flowBeginPID, flowBeginTID string | |
| switch stat.Timers[i].Tags { | |
| case "/http2/recv/header/loopyWriter/registerOutStream": | |
| flowBeginID = filterCounter(stat, "/http2/recv/header", lwc.GetAndInc("/http2/recv/header/loopyWriter/registerOutStream")) | |
| flowBeginPID = fmt.Sprintf("/%s/%d/loopyReader", stat.Tags, connectionCounter) | |
| flowBeginTID = fmt.Sprintf("%d", loopyReaderGoID) | |
| case "/http2/send/dataFrame/loopyWriter/preprocess": | |
| flowBeginID = filterCounter(stat, "/transport/enqueue", lwc.GetAndInc("/http2/send/dataFrame/loopyWriter/preprocess")) | |
| if flowBeginID != -1 { | |
| flowBeginPID = opid | |
| flowBeginTID = fmt.Sprintf("%d", stat.Timers[flowBeginID].GoId) | |
| } else { | |
| logger.Infof("cannot find /%d/transport/enqueue for /%d/http2/send/dataFrame/loopyWriter/preprocess", connectionCounter, connectionCounter) | |
| } | |
| default: | |
| flowBeginID = -1 | |
| } | |
| if flowBeginID != -1 { | |
| flowID := fmt.Sprintf("lwc begin:/%d%s end:/%d%s begin:(%d, %s, %s) end:(%d, %s, %s)", connectionCounter, stat.Timers[flowBeginID].Tags, connectionCounter, stat.Timers[i].Tags, flowBeginID, flowBeginPID, flowBeginTID, i, pid, tid) | |
| result = append(result, | |
| jsonNode{ | |
| Name: fmt.Sprintf("/%s/%d/%d/flow", stat.Tags, connectionCounter, streamID), | |
| Cat: categories + ",flow", | |
| ID: flowID, | |
| Cname: hashCname("flow"), | |
| Phase: "s", | |
| Timestamp: catapultNs(stat.Timers[flowBeginID].EndSec-baseSec, stat.Timers[flowBeginID].EndNsec-baseNsec), | |
| PID: flowBeginPID, | |
| TID: flowBeginTID, | |
| }, | |
| jsonNode{ | |
| Name: fmt.Sprintf("/%s/%d/%d/flow", stat.Tags, connectionCounter, streamID), | |
| Cat: categories + ",flow", | |
| ID: flowID, | |
| Cname: hashCname("flow"), | |
| Phase: "f", | |
| Timestamp: catapultNs(stat.Timers[i].BeginSec-baseSec, stat.Timers[i].BeginNsec-baseNsec), | |
| PID: pid, | |
| TID: tid, | |
| }, | |
| ) | |
| } | |
| } | |
| result = append(result, | |
| jsonNode{ | |
| Name: fmt.Sprintf("%s%s", opid, stat.Timers[i].Tags), | |
| Cat: categories, | |
| ID: opid, | |
| Cname: hashCname(stat.Timers[i].Tags), | |
| Phase: "B", | |
| Timestamp: catapultNs(stat.Timers[i].BeginSec-baseSec, stat.Timers[i].BeginNsec-baseNsec), | |
| PID: pid, | |
| TID: tid, | |
| }, | |
| jsonNode{ | |
| Name: fmt.Sprintf("%s%s", opid, stat.Timers[i].Tags), | |
| Cat: categories, | |
| ID: opid, | |
| Cname: hashCname(stat.Timers[i].Tags), | |
| Phase: "E", | |
| Timestamp: catapultNs(stat.Timers[i].EndSec-baseSec, stat.Timers[i].EndNsec-baseNsec), | |
| PID: pid, | |
| TID: tid, | |
| }, | |
| ) | |
| } | |
| return result | |
| } | |
| // timerBeginIsBefore compares two proto Timer objects to determine if the | |
| // first comes before the second chronologically. | |
| func timerBeginIsBefore(ti *ppb.Timer, tj *ppb.Timer) bool { | |
| if ti.BeginSec == tj.BeginSec { | |
| return ti.BeginNsec < tj.BeginNsec | |
| } | |
| return ti.BeginSec < tj.BeginSec | |
| } | |
| // streamStatsCatapultJSON receives a *snapshot and the name of a JSON file to | |
| // write to. The grpc-go profiling snapshot is processed and converted to a | |
| // JSON format that can be understood by trace-viewer. | |
| func streamStatsCatapultJSON(s *snapshot, streamStatsCatapultJSONFileName string) (err error) { | |
| logger.Infof("calculating stream stats filters") | |
| filterArray := strings.Split(*flagStreamStatsFilter, ",") | |
| filter := make(map[string]bool) | |
| for _, f := range filterArray { | |
| filter[f] = true | |
| } | |
| logger.Infof("filter stream stats for %s", *flagStreamStatsFilter) | |
| var streamStats []*ppb.Stat | |
| for _, stat := range s.StreamStats { | |
| if _, ok := filter[stat.Tags]; ok { | |
| streamStats = append(streamStats, stat) | |
| } | |
| } | |
| logger.Infof("sorting timers within all stats") | |
| for id := range streamStats { | |
| sort.Slice(streamStats[id].Timers, func(i, j int) bool { | |
| return timerBeginIsBefore(streamStats[id].Timers[i], streamStats[id].Timers[j]) | |
| }) | |
| } | |
| logger.Infof("sorting stream stats") | |
| sort.Slice(streamStats, func(i, j int) bool { | |
| if len(streamStats[j].Timers) == 0 { | |
| return true | |
| } else if len(streamStats[i].Timers) == 0 { | |
| return false | |
| } | |
| pi := binary.BigEndian.Uint64(streamStats[i].Metadata[0:8]) | |
| pj := binary.BigEndian.Uint64(streamStats[j].Metadata[0:8]) | |
| if pi == pj { | |
| return timerBeginIsBefore(streamStats[i].Timers[0], streamStats[j].Timers[0]) | |
| } | |
| return pi < pj | |
| }) | |
| // Clip the last stat as it's from the /Profiling/GetStreamStats call that we | |
| // made to retrieve the stats themselves. This likely happened millions of | |
| // nanoseconds after the last stream we want to profile, so it'd just make | |
| // the catapult graph less readable. | |
| if len(streamStats) > 0 { | |
| streamStats = streamStats[:len(streamStats)-1] | |
| } | |
| // All timestamps use the earliest timestamp available as the reference. | |
| logger.Infof("calculating the earliest timestamp across all timers") | |
| var base *ppb.Timer | |
| for _, stat := range streamStats { | |
| for _, timer := range stat.Timers { | |
| if base == nil || timerBeginIsBefore(base, timer) { | |
| base = timer | |
| } | |
| } | |
| } | |
| logger.Infof("converting %d stats to catapult JSON format", len(streamStats)) | |
| var jsonNodes []jsonNode | |
| for _, stat := range streamStats { | |
| jsonNodes = append(jsonNodes, streamStatsCatapultJSONSingle(stat, base.BeginSec, base.BeginNsec)...) | |
| } | |
| logger.Infof("marshalling catapult JSON") | |
| b, err := json.Marshal(jsonNodes) | |
| if err != nil { | |
| logger.Errorf("cannot marshal JSON: %v", err) | |
| return err | |
| } | |
| logger.Infof("creating catapult JSON file") | |
| streamStatsCatapultJSONFile, err := os.Create(streamStatsCatapultJSONFileName) | |
| if err != nil { | |
| logger.Errorf("cannot create file %s: %v", streamStatsCatapultJSONFileName, err) | |
| return err | |
| } | |
| defer streamStatsCatapultJSONFile.Close() | |
| logger.Infof("writing catapult JSON to disk") | |
| _, err = streamStatsCatapultJSONFile.Write(b) | |
| if err != nil { | |
| logger.Errorf("cannot write marshalled JSON: %v", err) | |
| return err | |
| } | |
| logger.Infof("successfully wrote catapult JSON file %s", streamStatsCatapultJSONFileName) | |
| return nil | |
| } |