OSDN Git Service

run time performance measure (#553)
authorPaladz <yzhu101@uottawa.ca>
Wed, 28 Oct 2020 06:02:28 +0000 (14:02 +0800)
committerGitHub <noreply@github.com>
Wed, 28 Oct 2020 06:02:28 +0000 (14:02 +0800)
* run time performance measure

* allow for loop cont time

* add read me doc

Co-authored-by: paladz <colt@ColtdeMacBook-Pro.local>
log/log.go
toolbar/measure/README.md [new file with mode: 0644]
toolbar/measure/method.go [new file with mode: 0644]
toolbar/measure/timer.go [new file with mode: 0644]

index e16ceea..5563cd0 100644 (file)
@@ -20,7 +20,10 @@ const (
        maxAge       int64 = 604800
 )
 
-var defaultFormatter = &logrus.TextFormatter{DisableColors: true}
+var defaultFormatter = &logrus.TextFormatter{
+       DisableColors:   true,
+       TimestampFormat: time.StampMicro,
+}
 
 func InitLogFile(config *config.Config) error {
        logPath := config.LogDir()
diff --git a/toolbar/measure/README.md b/toolbar/measure/README.md
new file mode 100644 (file)
index 0000000..8661be1
--- /dev/null
@@ -0,0 +1,21 @@
+Measure is a tool for help precise analysis of the performance of the goroutine, and it's designed for thread-safe. 
+Just simply call Start() and defer End() on top of the function it will handle all the stack issue and print the tree
+struct result when the root function is ended.
+
+#sample usage
+func processBlock() {
+       measure.Start()
+       defer measure.End()
+
+       ......
+}
+
+
+#sample result
+|--github.com/bytom/vapor/protocol.(*Chain).processBlock: 9.009746ms (100.00)
+  |--github.com/bytom/vapor/protocol.(*Chain).saveBlock: 8.086023ms (89.75)
+    |--github.com/bytom/vapor/protocol.(*Chain).validateSign: 1.443966ms (17.86)
+    |--github.com/bytom/vapor/protocol/validation.ValidateBlock: 195.333µs (2.42)
+      |--github.com/bytom/vapor/protocol/validation.ValidateBlockHeader: 26.48µs (13.56)
+      |--github.com/bytom/vapor/protocol/validation.ValidateTxs: 88.312µs (45.21)
+  |--github.com/bytom/vapor/protocol.(*Chain).connectBlock: 767.073µs (8.51)
\ No newline at end of file
diff --git a/toolbar/measure/method.go b/toolbar/measure/method.go
new file mode 100644 (file)
index 0000000..dbb3b63
--- /dev/null
@@ -0,0 +1,83 @@
+package measure
+
+import (
+       "fmt"
+       "runtime/debug"
+       "strings"
+       "sync"
+
+       log "github.com/sirupsen/logrus"
+)
+
+const (
+       logModule = "measure"
+)
+
+var store sync.Map
+
+// Start trigger record of stack trace run time record as a graph view
+func Start() {
+       routineID, stacks, err := traceStacks()
+       if err != nil {
+               log.WithFields(log.Fields{"module": logModule, "err": err}).Error("fail on measure get stacks")
+               return
+       }
+
+       data, ok := store.Load(routineID)
+       if !ok {
+               store.Store(routineID, NewTimer(stacks[0]))
+               return
+       }
+
+       if err := data.(*Timer).StartTimer(stacks); err != nil {
+               log.WithFields(log.Fields{"module": logModule, "err": err, "routine": routineID, "stack": stacks}).Error("fail on start timer")
+       }
+}
+
+// End end the stack trace run time
+func End() {
+       routineID, stacks, err := traceStacks()
+       if err != nil {
+               log.WithFields(log.Fields{"module": logModule, "err": err}).Error("fail on measure get stacks")
+               return
+       }
+
+       data, ok := store.Load(routineID)
+       if !ok {
+               log.WithFields(log.Fields{"module": logModule, "err": err}).Error("fail on measure timer by routine ID")
+               return
+       }
+
+       rootTimer := data.(*Timer)
+       if err := rootTimer.EndTimer(stacks); err != nil {
+               log.WithFields(log.Fields{"module": logModule, "err": err, "routine": routineID, "stack": stacks}).Error("fail on end timer")
+       }
+
+       if rootTimer.IsEnd() {
+               log.WithField("module", logModule).Info(rootTimer.String())
+               store.Delete(routineID)
+       }
+}
+
+func traceStacks() (string, []string, error) {
+       stacks := []string{}
+       for _, stack := range strings.Split(string(debug.Stack()), "\n") {
+               // skip the file path stack
+               if strings.HasPrefix(stack, "   ") {
+                       continue
+               }
+
+               // delete the func memory address stuff
+               if subPos := strings.LastIndexAny(stack, "("); subPos > 0 {
+                       stacks = append(stacks, stack[:subPos])
+               } else {
+                       stacks = append(stacks, stack)
+               }
+       }
+
+       if len(stacks) < 4 {
+               return "", nil, fmt.Errorf("fail to decode stack")
+       }
+
+       return stacks[0], stacks[4:], nil
+}
diff --git a/toolbar/measure/timer.go b/toolbar/measure/timer.go
new file mode 100644 (file)
index 0000000..801a4a2
--- /dev/null
@@ -0,0 +1,129 @@
+package measure
+
+import (
+       "fmt"
+       "strings"
+       "time"
+)
+
+const (
+       summaryPrefix = "|--"
+)
+
+// Timer is created for analysis of the function performance
+type Timer struct {
+       name  string
+       start *time.Time
+       total time.Duration
+
+       subtimerMap map[string]*Timer
+}
+
+// NewTimer create a new timer, only use for root timer only
+func NewTimer(name string) *Timer {
+       now := time.Now()
+       return &Timer{name: name, start: &now, subtimerMap: map[string]*Timer{}}
+}
+
+// StartTimer start track time for sub func
+func (t *Timer) StartTimer(stacks []string) error {
+       stacks, err := t.locateStack(stacks)
+       if err != nil {
+               return nil
+       }
+
+       return t.startSubtimer(stacks)
+}
+
+// String implement the print interface
+func (t *Timer) String() string {
+       return t.summary(0, t.total)
+}
+
+// EndTimer always run on end of the func
+func (t *Timer) EndTimer(stacks []string) error {
+       stacks, err := t.locateStack(stacks)
+       if err != nil {
+               return err
+       }
+
+       return t.endSubtimer(stacks)
+}
+
+// IsEnd check wheather the ticker is close
+func (t *Timer) IsEnd() bool {
+       return t.start == nil
+}
+
+func (t *Timer) startSubtimer(stacks []string) error {
+       if len(stacks) == 0 {
+               if !t.IsEnd() {
+                       return fmt.Errorf("try to start an unclose timer")
+               }
+
+               now := time.Now()
+               t.start = &now
+               return nil
+       }
+
+       nextStack := stacks[len(stacks)-1]
+       if _, ok := t.subtimerMap[nextStack]; !ok {
+               t.subtimerMap[nextStack] = &Timer{name: nextStack, subtimerMap: map[string]*Timer{}}
+       }
+
+       return t.subtimerMap[nextStack].startSubtimer(stacks[:len(stacks)-1])
+}
+
+func (t *Timer) endSubtimer(stacks []string) error {
+       if len(stacks) == 0 {
+               if t.IsEnd() {
+                       return fmt.Errorf("timer didn't start")
+               }
+
+               t.total += time.Now().Sub(*t.start)
+               t.start = nil
+               return nil
+       }
+
+       subtimer, ok := t.subtimerMap[stacks[len(stacks)-1]]
+       if !ok {
+               return fmt.Errorf("endSubtimer didn't find sub timer")
+       }
+
+       return subtimer.endSubtimer(stacks[:len(stacks)-1])
+}
+
+// locateStack is using to exclude dust stacks
+func (t *Timer) locateStack(stacks []string) ([]string, error) {
+       for i := len(stacks) - 1; i >= 0; i-- {
+               if stacks[i] == t.name {
+                       return stacks[:i], nil
+               }
+       }
+
+       return nil, fmt.Errorf("locateStack didn't match the expect stack")
+}
+
+// summary will convert the time spend graph to tree string
+func (t *Timer) summary(depth int, parentDuration time.Duration) string {
+       result := strings.Repeat("  ", depth) + summaryPrefix
+       result += t.name + ": "
+       if !t.IsEnd() {
+               return result + "<timer didn't ends>\n"
+       }
+
+       result += fmt.Sprintf("%s (%.2f)\n", t.total.String(), float64(t.total.Nanoseconds())/float64(parentDuration.Nanoseconds())*100)
+
+       // handle the case that skip middle level time measure case
+       nextDepth, total := depth+1, t.total
+       if t.total == 0 {
+               result = ""
+               nextDepth, total = depth, parentDuration
+       }
+
+       for _, sub := range t.subtimerMap {
+               result += sub.summary(nextDepth, total)
+       }
+
+       return result
+}