1package libkb
2
3import (
4	"bufio"
5	"fmt"
6	"os"
7	"regexp"
8	"strings"
9	"time"
10
11	"golang.org/x/net/context"
12)
13
14// LogProfileContext for LogProfile
15type LogProfileContext struct {
16	Contextified
17	Path string
18}
19
20func (l *LogProfileContext) maxDuration(durations []time.Duration) time.Duration {
21	max := time.Duration(0)
22	for _, d := range durations {
23		if d > max {
24			max = d
25		}
26	}
27	return max
28}
29
30func (l *LogProfileContext) minDuration(durations []time.Duration) time.Duration {
31	if len(durations) == 0 {
32		return 0
33	}
34	min := durations[0]
35	for _, d := range durations {
36		if d < min {
37			min = d
38		}
39	}
40	return min
41}
42
43func (l *LogProfileContext) avgDuration(durations []time.Duration) time.Duration {
44	if len(durations) == 0 {
45		return 0
46	}
47	var total int64
48	for _, d := range durations {
49		total += d.Nanoseconds()
50	}
51	return time.Duration(total / int64(len(durations)))
52}
53
54func (l *LogProfileContext) format(fn string, durations []time.Duration) string {
55	return fmt.Sprintf(`
56		%v:
57			max: %v
58			avg: %v
59			min: %v
60			len: %v`,
61		fn, l.maxDuration(durations), l.avgDuration(durations), l.minDuration(durations), len(durations))
62}
63
64func (l *LogProfileContext) parseMatch(matches []string) (filename, fnName string, d time.Duration) {
65	if len(matches) != 4 {
66		return "", "", 0
67	}
68	filename = matches[1]
69	fnName = matches[2]
70	// Some log calls have fnName: args so we want to strip that.
71	fnName = strings.Split(fnName, ":")[0]
72	// Some log calls have fnName(args) so we want to strip that.
73	fnName = strings.Split(fnName, "(")[0]
74	d, err := time.ParseDuration(matches[3])
75	if err != nil {
76		l.G().Log.CDebugf(context.TODO(), "Unable to parse duration: %s", err)
77		return "", "", 0
78	}
79	return filename, fnName, d
80}
81
82func (l *LogProfileContext) LogProfile(path string) ([]string, error) {
83	f, err := os.Open(path)
84	if err != nil {
85		return nil, err
86	}
87	defer f.Close()
88
89	re := regexp.MustCompile(`keybase (\w*\.go)\:\d+.*- (.*) -> .* \[time=(\d+\.\w+)\]`)
90	// filename -> functionName -> [durations...]
91	profiles := map[string]map[string][]time.Duration{}
92	scanner := bufio.NewScanner(f)
93	scanner.Split(bufio.ScanLines)
94	for scanner.Scan() {
95		// We expect two groups, the function name and a duration
96		matches := re.FindAllStringSubmatch(scanner.Text(), -1)
97		if len(matches) == 0 {
98			continue
99		}
100		filename, fnName, d := l.parseMatch(matches[0])
101		if fnName == "" {
102			continue
103		}
104
105		data, ok := profiles[filename]
106		if !ok {
107			data = make(map[string][]time.Duration)
108		}
109		durations, ok := data[fnName]
110		if ok {
111			durations = append(durations, d)
112		} else {
113			durations = []time.Duration{d}
114		}
115		data[fnName] = durations
116		profiles[filename] = data
117	}
118	res := []string{}
119	for filename, data := range profiles {
120		res = append(res, filename)
121		for fnName, durations := range data {
122			res = append(res, l.format(fnName, durations))
123		}
124	}
125	return res, nil
126}
127