gitee.com/ks-custle/core-gm@v0.0.0-20230922171213-b83bdd97b62c/grpc/profiling/cmd/catapult.go (about)

     1  /*
     2   *
     3   * Copyright 2019 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  package main
    20  
    21  import (
    22  	"encoding/binary"
    23  	"encoding/json"
    24  	"fmt"
    25  	"os"
    26  	"sort"
    27  	"strings"
    28  
    29  	ppb "gitee.com/ks-custle/core-gm/grpc/profiling/proto"
    30  )
    31  
    32  type jsonNode struct {
    33  	Name      string  `json:"name"`
    34  	Cat       string  `json:"cat"`
    35  	ID        string  `json:"id"`
    36  	Cname     string  `json:"cname"`
    37  	Phase     string  `json:"ph"`
    38  	Timestamp float64 `json:"ts"`
    39  	PID       string  `json:"pid"`
    40  	TID       string  `json:"tid"`
    41  }
    42  
    43  // Catapult does not allow specifying colours manually; a 20-odd predefined
    44  // labels are used (that don't make much sense outside the context of
    45  // Chromium). See this for more details:
    46  //
    47  // https://github.com/catapult-project/catapult/blob/bef344f7017fc9e04f7049d0f58af6d9ce9f4ab6/tracing/tracing/base/color_scheme.html#L29
    48  func hashCname(tag string) string {
    49  	if strings.Contains(tag, "encoding") {
    50  		return "rail_response"
    51  	}
    52  
    53  	if strings.Contains(tag, "compression") {
    54  		return "cq_build_passed"
    55  	}
    56  
    57  	if strings.Contains(tag, "transport") {
    58  		if strings.Contains(tag, "blocking") {
    59  			return "rail_animation"
    60  		}
    61  		return "good"
    62  	}
    63  
    64  	if strings.Contains(tag, "header") {
    65  		return "cq_build_attempt_failed"
    66  	}
    67  
    68  	if tag == "/" {
    69  		return "heap_dump_stack_frame"
    70  	}
    71  
    72  	if strings.Contains(tag, "flow") || strings.Contains(tag, "tmp") {
    73  		return "heap_dump_stack_frame"
    74  	}
    75  
    76  	return ""
    77  }
    78  
    79  // filterCounter identifies the counter-th instance of a timer of the type
    80  // `filter` within a Stat. This, in conjunction with the counter data structure
    81  // defined below, is used to draw flows between linked loopy writer/reader
    82  // events with application goroutine events in trace-viewer. This is possible
    83  // because enqueues and dequeues are ordered -- that is, the first dequeue must
    84  // be dequeueing the first enqueue operation.
    85  func filterCounter(stat *ppb.Stat, filter string, counter int) int {
    86  	localCounter := 0
    87  	for i := 0; i < len(stat.Timers); i++ {
    88  		if stat.Timers[i].Tags == filter {
    89  			if localCounter == counter {
    90  				return i
    91  			}
    92  			localCounter++
    93  		}
    94  	}
    95  
    96  	return -1
    97  }
    98  
    99  // counter is state object used to store and retrieve the number of timers of a
   100  // particular type that have been seen.
   101  type counter struct {
   102  	c map[string]int
   103  }
   104  
   105  func newCounter() *counter {
   106  	return &counter{c: make(map[string]int)}
   107  }
   108  
   109  func (c *counter) GetAndInc(s string) int {
   110  	ret := c.c[s]
   111  	c.c[s]++
   112  	return ret
   113  }
   114  
   115  func catapultNs(sec int64, nsec int32) float64 {
   116  	return float64((sec * 1000000000) + int64(nsec))
   117  }
   118  
   119  // streamStatsCatapultJSONSingle processes a single proto Stat object to return
   120  // an array of jsonNodes in trace-viewer's format.
   121  func streamStatsCatapultJSONSingle(stat *ppb.Stat, baseSec int64, baseNsec int32) []jsonNode {
   122  	if len(stat.Timers) == 0 {
   123  		return nil
   124  	}
   125  
   126  	connectionCounter := binary.BigEndian.Uint64(stat.Metadata[0:8])
   127  	streamID := binary.BigEndian.Uint32(stat.Metadata[8:12])
   128  	opid := fmt.Sprintf("/%s/%d/%d", stat.Tags, connectionCounter, streamID)
   129  
   130  	var loopyReaderGoID, loopyWriterGoID int64
   131  	for i := 0; i < len(stat.Timers) && (loopyReaderGoID == 0 || loopyWriterGoID == 0); i++ {
   132  		if strings.Contains(stat.Timers[i].Tags, "/loopyReader") {
   133  			loopyReaderGoID = stat.Timers[i].GoId
   134  		} else if strings.Contains(stat.Timers[i].Tags, "/loopyWriter") {
   135  			loopyWriterGoID = stat.Timers[i].GoId
   136  		}
   137  	}
   138  
   139  	lrc, lwc := newCounter(), newCounter()
   140  
   141  	var result []jsonNode
   142  	result = append(result,
   143  		jsonNode{
   144  			Name:      "loopyReaderTmp",
   145  			ID:        opid,
   146  			Cname:     hashCname("tmp"),
   147  			Phase:     "i",
   148  			Timestamp: 0,
   149  			PID:       fmt.Sprintf("/%s/%d/loopyReader", stat.Tags, connectionCounter),
   150  			TID:       fmt.Sprintf("%d", loopyReaderGoID),
   151  		},
   152  		jsonNode{
   153  			Name:      "loopyWriterTmp",
   154  			ID:        opid,
   155  			Cname:     hashCname("tmp"),
   156  			Phase:     "i",
   157  			Timestamp: 0,
   158  			PID:       fmt.Sprintf("/%s/%d/loopyWriter", stat.Tags, connectionCounter),
   159  			TID:       fmt.Sprintf("%d", loopyWriterGoID),
   160  		},
   161  	)
   162  
   163  	for i := 0; i < len(stat.Timers); i++ {
   164  		categories := stat.Tags
   165  		pid, tid := opid, fmt.Sprintf("%d", stat.Timers[i].GoId)
   166  
   167  		if stat.Timers[i].GoId == loopyReaderGoID {
   168  			pid, tid = fmt.Sprintf("/%s/%d/loopyReader", stat.Tags, connectionCounter), fmt.Sprintf("%d", stat.Timers[i].GoId)
   169  
   170  			var flowEndID int
   171  			var flowEndPID, flowEndTID string
   172  			switch stat.Timers[i].Tags {
   173  			case "/http2/recv/header":
   174  				flowEndID = filterCounter(stat, "/grpc/stream/recv/header", lrc.GetAndInc("/http2/recv/header"))
   175  				if flowEndID != -1 {
   176  					flowEndPID = opid
   177  					flowEndTID = fmt.Sprintf("%d", stat.Timers[flowEndID].GoId)
   178  				} else {
   179  					logger.Infof("cannot find %s/grpc/stream/recv/header for %s/http2/recv/header", opid, opid)
   180  				}
   181  			case "/http2/recv/dataFrame/loopyReader":
   182  				flowEndID = filterCounter(stat, "/recvAndDecompress", lrc.GetAndInc("/http2/recv/dataFrame/loopyReader"))
   183  				if flowEndID != -1 {
   184  					flowEndPID = opid
   185  					flowEndTID = fmt.Sprintf("%d", stat.Timers[flowEndID].GoId)
   186  				} else {
   187  					logger.Infof("cannot find %s/recvAndDecompress for %s/http2/recv/dataFrame/loopyReader", opid, opid)
   188  				}
   189  			default:
   190  				flowEndID = -1
   191  			}
   192  
   193  			if flowEndID != -1 {
   194  				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)
   195  				result = append(result,
   196  					jsonNode{
   197  						Name:      fmt.Sprintf("%s/flow", opid),
   198  						Cat:       categories + ",flow",
   199  						ID:        flowID,
   200  						Cname:     hashCname("flow"),
   201  						Phase:     "s",
   202  						Timestamp: catapultNs(stat.Timers[i].EndSec-baseSec, stat.Timers[i].EndNsec-baseNsec),
   203  						PID:       pid,
   204  						TID:       tid,
   205  					},
   206  					jsonNode{
   207  						Name:      fmt.Sprintf("%s/flow", opid),
   208  						Cat:       categories + ",flow",
   209  						ID:        flowID,
   210  						Cname:     hashCname("flow"),
   211  						Phase:     "f",
   212  						Timestamp: catapultNs(stat.Timers[flowEndID].BeginSec-baseSec, stat.Timers[flowEndID].BeginNsec-baseNsec),
   213  						PID:       flowEndPID,
   214  						TID:       flowEndTID,
   215  					},
   216  				)
   217  			}
   218  		} else if stat.Timers[i].GoId == loopyWriterGoID {
   219  			pid, tid = fmt.Sprintf("/%s/%d/loopyWriter", stat.Tags, connectionCounter), fmt.Sprintf("%d", stat.Timers[i].GoId)
   220  
   221  			var flowBeginID int
   222  			var flowBeginPID, flowBeginTID string
   223  			switch stat.Timers[i].Tags {
   224  			case "/http2/recv/header/loopyWriter/registerOutStream":
   225  				flowBeginID = filterCounter(stat, "/http2/recv/header", lwc.GetAndInc("/http2/recv/header/loopyWriter/registerOutStream"))
   226  				flowBeginPID = fmt.Sprintf("/%s/%d/loopyReader", stat.Tags, connectionCounter)
   227  				flowBeginTID = fmt.Sprintf("%d", loopyReaderGoID)
   228  			case "/http2/send/dataFrame/loopyWriter/preprocess":
   229  				flowBeginID = filterCounter(stat, "/transport/enqueue", lwc.GetAndInc("/http2/send/dataFrame/loopyWriter/preprocess"))
   230  				if flowBeginID != -1 {
   231  					flowBeginPID = opid
   232  					flowBeginTID = fmt.Sprintf("%d", stat.Timers[flowBeginID].GoId)
   233  				} else {
   234  					logger.Infof("cannot find /%d/transport/enqueue for /%d/http2/send/dataFrame/loopyWriter/preprocess", connectionCounter, connectionCounter)
   235  				}
   236  			default:
   237  				flowBeginID = -1
   238  			}
   239  
   240  			if flowBeginID != -1 {
   241  				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)
   242  				result = append(result,
   243  					jsonNode{
   244  						Name:      fmt.Sprintf("/%s/%d/%d/flow", stat.Tags, connectionCounter, streamID),
   245  						Cat:       categories + ",flow",
   246  						ID:        flowID,
   247  						Cname:     hashCname("flow"),
   248  						Phase:     "s",
   249  						Timestamp: catapultNs(stat.Timers[flowBeginID].EndSec-baseSec, stat.Timers[flowBeginID].EndNsec-baseNsec),
   250  						PID:       flowBeginPID,
   251  						TID:       flowBeginTID,
   252  					},
   253  					jsonNode{
   254  						Name:      fmt.Sprintf("/%s/%d/%d/flow", stat.Tags, connectionCounter, streamID),
   255  						Cat:       categories + ",flow",
   256  						ID:        flowID,
   257  						Cname:     hashCname("flow"),
   258  						Phase:     "f",
   259  						Timestamp: catapultNs(stat.Timers[i].BeginSec-baseSec, stat.Timers[i].BeginNsec-baseNsec),
   260  						PID:       pid,
   261  						TID:       tid,
   262  					},
   263  				)
   264  			}
   265  		}
   266  
   267  		result = append(result,
   268  			jsonNode{
   269  				Name:      fmt.Sprintf("%s%s", opid, stat.Timers[i].Tags),
   270  				Cat:       categories,
   271  				ID:        opid,
   272  				Cname:     hashCname(stat.Timers[i].Tags),
   273  				Phase:     "B",
   274  				Timestamp: catapultNs(stat.Timers[i].BeginSec-baseSec, stat.Timers[i].BeginNsec-baseNsec),
   275  				PID:       pid,
   276  				TID:       tid,
   277  			},
   278  			jsonNode{
   279  				Name:      fmt.Sprintf("%s%s", opid, stat.Timers[i].Tags),
   280  				Cat:       categories,
   281  				ID:        opid,
   282  				Cname:     hashCname(stat.Timers[i].Tags),
   283  				Phase:     "E",
   284  				Timestamp: catapultNs(stat.Timers[i].EndSec-baseSec, stat.Timers[i].EndNsec-baseNsec),
   285  				PID:       pid,
   286  				TID:       tid,
   287  			},
   288  		)
   289  	}
   290  
   291  	return result
   292  }
   293  
   294  // timerBeginIsBefore compares two proto Timer objects to determine if the
   295  // first comes before the second chronologically.
   296  func timerBeginIsBefore(ti *ppb.Timer, tj *ppb.Timer) bool {
   297  	if ti.BeginSec == tj.BeginSec {
   298  		return ti.BeginNsec < tj.BeginNsec
   299  	}
   300  	return ti.BeginSec < tj.BeginSec
   301  }
   302  
   303  // streamStatsCatapulJSON receives a *snapshot and the name of a JSON file to
   304  // write to. The grpc-go profiling snapshot is processed and converted to a
   305  // JSON format that can be understood by trace-viewer.
   306  func streamStatsCatapultJSON(s *snapshot, streamStatsCatapultJSONFileName string) (err error) {
   307  	logger.Infof("calculating stream stats filters")
   308  	filterArray := strings.Split(*flagStreamStatsFilter, ",")
   309  	filter := make(map[string]bool)
   310  	for _, f := range filterArray {
   311  		filter[f] = true
   312  	}
   313  
   314  	logger.Infof("filter stream stats for %s", *flagStreamStatsFilter)
   315  	var streamStats []*ppb.Stat
   316  	for _, stat := range s.StreamStats {
   317  		if _, ok := filter[stat.Tags]; ok {
   318  			streamStats = append(streamStats, stat)
   319  		}
   320  	}
   321  
   322  	logger.Infof("sorting timers within all stats")
   323  	for id := range streamStats {
   324  		sort.Slice(streamStats[id].Timers, func(i, j int) bool {
   325  			return timerBeginIsBefore(streamStats[id].Timers[i], streamStats[id].Timers[j])
   326  		})
   327  	}
   328  
   329  	logger.Infof("sorting stream stats")
   330  	sort.Slice(streamStats, func(i, j int) bool {
   331  		if len(streamStats[j].Timers) == 0 {
   332  			return true
   333  		} else if len(streamStats[i].Timers) == 0 {
   334  			return false
   335  		}
   336  		pi := binary.BigEndian.Uint64(streamStats[i].Metadata[0:8])
   337  		pj := binary.BigEndian.Uint64(streamStats[j].Metadata[0:8])
   338  		if pi == pj {
   339  			return timerBeginIsBefore(streamStats[i].Timers[0], streamStats[j].Timers[0])
   340  		}
   341  
   342  		return pi < pj
   343  	})
   344  
   345  	// Clip the last stat as it's from the /Profiling/GetStreamStats call that we
   346  	// made to retrieve the stats themselves. This likely happened millions of
   347  	// nanoseconds after the last stream we want to profile, so it'd just make
   348  	// the catapult graph less readable.
   349  	if len(streamStats) > 0 {
   350  		streamStats = streamStats[:len(streamStats)-1]
   351  	}
   352  
   353  	// All timestamps use the earliest timestamp available as the reference.
   354  	logger.Infof("calculating the earliest timestamp across all timers")
   355  	var base *ppb.Timer
   356  	for _, stat := range streamStats {
   357  		for _, timer := range stat.Timers {
   358  			if base == nil || timerBeginIsBefore(base, timer) {
   359  				base = timer
   360  			}
   361  		}
   362  	}
   363  
   364  	logger.Infof("converting %d stats to catapult JSON format", len(streamStats))
   365  	var jsonNodes []jsonNode
   366  	for _, stat := range streamStats {
   367  		jsonNodes = append(jsonNodes, streamStatsCatapultJSONSingle(stat, base.BeginSec, base.BeginNsec)...)
   368  	}
   369  
   370  	logger.Infof("marshalling catapult JSON")
   371  	b, err := json.Marshal(jsonNodes)
   372  	if err != nil {
   373  		logger.Errorf("cannot marshal JSON: %v", err)
   374  		return err
   375  	}
   376  
   377  	logger.Infof("creating catapult JSON file")
   378  	streamStatsCatapultJSONFile, err := os.Create(streamStatsCatapultJSONFileName)
   379  	if err != nil {
   380  		logger.Errorf("cannot create file %s: %v", streamStatsCatapultJSONFileName, err)
   381  		return err
   382  	}
   383  	defer streamStatsCatapultJSONFile.Close()
   384  
   385  	logger.Infof("writing catapult JSON to disk")
   386  	_, err = streamStatsCatapultJSONFile.Write(b)
   387  	if err != nil {
   388  		logger.Errorf("cannot write marshalled JSON: %v", err)
   389  		return err
   390  	}
   391  
   392  	logger.Infof("successfully wrote catapult JSON file %s", streamStatsCatapultJSONFileName)
   393  	return nil
   394  }