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)
	}
}