1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
package logic
import (
"fmt"
"os"
"path/filepath"
"regexp"
"rss_parrot/shared"
"runtime"
"runtime/pprof"
"time"
)
const startDelaySec = 10
const profilerLoopSec = 60
// defaultProfileKeepDays is how long dumps are kept if profile_keep_days is
// not set.
const defaultProfileKeepDays = 7
// dumpTimeFormat names the dump files. purgeOld deletes only files whose names
// match dumpName, so that pointing profile_dir at a directory that holds
// anything else cannot cost that.
const dumpTimeFormat = "2006-01-02!15-04-05"
var dumpName = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}!\d{2}-\d{2}-\d{2}\.txt$`)
type IProfiler interface {
}
// profiler writes a dump of every goroutine to profile_dir once a minute, and
// deletes its dumps older than profile_keep_days. Without profile_dir it does
// nothing.
type profiler struct {
profileDir string
profileKeepDays int
running bool
}
func NewProfiler(cfg *shared.Config, logger shared.ILogger) IProfiler {
keepDays := cfg.ProfileKeepDays
if keepDays <= 0 {
keepDays = defaultProfileKeepDays
}
prof := profiler{profileDir: cfg.ProfileDir, profileKeepDays: keepDays}
if prof.profileDir == "" {
return &prof
}
logger.Infof("Writing goroutine dumps to %s every %d seconds, keeping %d days",
prof.profileDir, profilerLoopSec, prof.profileKeepDays)
prof.running = true
go func() {
time.Sleep(startDelaySec * time.Second)
prof.profilerLoop(logger)
}()
return &prof
}
func saveProfile(profileDir string, now time.Time) error {
fname := fmt.Sprintf("%v.txt", now.Format(dumpTimeFormat))
profPath := filepath.Join(profileDir, fname)
f, err := os.Create(profPath)
if err != nil {
return err
}
defer f.Close()
numGoroutine := runtime.NumGoroutine()
if _, err = fmt.Fprintf(f, "Goroutine count: %d\n\n", numGoroutine); err != nil {
return err
}
if err = pprof.Lookup("goroutine").WriteTo(f, 2); err != nil {
return err
}
return nil
}
// purgeOld deletes the dumps in profileDir (not below it) older than
// retentionDays, and nothing else.
func purgeOld(profileDir string, retentionDays int, now time.Time) error {
cutoff := now.AddDate(0, 0, -retentionDays)
entries, err := os.ReadDir(profileDir)
if err != nil {
return err
}
for _, e := range entries {
if !e.Type().IsRegular() || !dumpName.MatchString(e.Name()) {
continue
}
info, err := e.Info()
if err != nil {
continue
}
if info.ModTime().Before(cutoff) {
if err := os.Remove(filepath.Join(profileDir, e.Name())); err != nil {
return err
}
}
}
return nil
}
func (prof *profiler) saveProfileAndPurgeOld() error {
now := time.Now()
if err := saveProfile(prof.profileDir, now); err != nil {
return err
}
return purgeOld(prof.profileDir, prof.profileKeepDays, now)
}
func (prof *profiler) profilerLoop(logger shared.ILogger) {
failing := false
for {
if err := prof.saveProfileAndPurgeOld(); err != nil {
if !failing {
logger.Errorf("Failed to write goroutine dump to %s: %v", prof.profileDir, err)
}
failing = true
} else {
failing = false
}
time.Sleep(profilerLoopSec * time.Second)
}
}
|