forked from ccfos/huatuo
251 lines
6.4 KiB
Go
251 lines
6.4 KiB
Go
// Copyright 2025 The HuaTuo Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package autotracing
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"huatuo-bamai/internal/conf"
|
|
"huatuo-bamai/internal/log"
|
|
"huatuo-bamai/internal/storage"
|
|
"huatuo-bamai/pkg/tracing"
|
|
|
|
"github.com/shirou/gopsutil/process"
|
|
)
|
|
|
|
func init() {
|
|
tracing.RegisterEventTracing("membust", newMemBurst)
|
|
}
|
|
|
|
func newMemBurst() (*tracing.EventTracingAttr, error) {
|
|
return &tracing.EventTracingAttr{
|
|
TracingData: &memBurstTracing{},
|
|
Internal: 10,
|
|
Flag: tracing.FlagTracing,
|
|
}, nil
|
|
}
|
|
|
|
type memBurstTracing struct{}
|
|
|
|
type MemoryTracingData struct {
|
|
TopMemoryUsage []ProcessMemoryInfo `json:"top_memory_usage"`
|
|
}
|
|
|
|
// ProcessMemoryInfo holds process information for sorting
|
|
type ProcessMemoryInfo struct {
|
|
PID int32 `json:"pid"`
|
|
ProcessName string `json:"process_name"`
|
|
MemorySize uint64 `json:"memory_size"`
|
|
}
|
|
|
|
// ByMemory is used to sorting processes by memory usage
|
|
type ByMemory []ProcessMemoryInfo
|
|
|
|
func (a ByMemory) Len() int { return len(a) }
|
|
func (a ByMemory) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a ByMemory) Less(i, j int) bool { return a[i].MemorySize > a[j].MemorySize }
|
|
|
|
// getTopMemoryProcesses returns the top N processes consuming the most memory.
|
|
func getTopMemoryProcesses(topN int) ([]ProcessMemoryInfo, error) {
|
|
processes, err := process.Processes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var pmInfos []ProcessMemoryInfo
|
|
for _, p := range processes {
|
|
memInfo, err := p.MemoryInfo()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
name, err := p.Name()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
pmInfos = append(pmInfos, ProcessMemoryInfo{
|
|
PID: p.Pid,
|
|
ProcessName: name,
|
|
MemorySize: memInfo.RSS,
|
|
})
|
|
}
|
|
|
|
// Sort the processes by memory usage
|
|
sort.Sort(ByMemory(pmInfos))
|
|
|
|
if len(pmInfos) < topN {
|
|
return pmInfos, nil
|
|
}
|
|
return pmInfos[:topN], nil
|
|
}
|
|
|
|
// pass required keys and readMemInfo will return their values according to /proc/meminfo
|
|
func readMemInfo(requiredKeys map[string]bool) (map[string]int, error) {
|
|
file, err := os.Open("/proc/meminfo")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
results := make(map[string]int)
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 2 {
|
|
continue
|
|
}
|
|
|
|
key := strings.Trim(fields[0], ":")
|
|
if _, ok := requiredKeys[key]; ok {
|
|
value, err := strconv.Atoi(strings.Trim(fields[1], " kB"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
results[key] = value
|
|
|
|
if len(results) == len(requiredKeys) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func checkAndRecordMemoryUsage(currentIndex *int, isHistoryFull *bool,
|
|
memTotal int, history []int, historyWindowLength, topNProcesses int,
|
|
burstRatio float64, anonThreshold int,
|
|
) ([]ProcessMemoryInfo, error) {
|
|
memInfo, err := readMemInfo(map[string]bool{
|
|
"Active(anon)": true,
|
|
"Inactive(anon)": true,
|
|
})
|
|
if err != nil {
|
|
log.Errorf("Error reading memory info: %v\n", err)
|
|
return []ProcessMemoryInfo{}, nil
|
|
}
|
|
|
|
currentSum := memInfo["Active(anon)"] + memInfo["Inactive(anon)"]
|
|
history[*currentIndex] = currentSum
|
|
|
|
if *currentIndex == historyWindowLength-1 {
|
|
*isHistoryFull = true
|
|
}
|
|
|
|
*currentIndex = (*currentIndex + 1) % historyWindowLength
|
|
|
|
log.Debugf("Checked memory status. active_anon=%v KiB inactive_anon=%v KiB\n", memInfo["Active(anon)"], memInfo["Inactive(anon)"])
|
|
|
|
if *isHistoryFull {
|
|
oldestSum := history[*currentIndex] // current index is the oldest element
|
|
if float64(currentSum) >= burstRatio*float64(oldestSum) && currentSum >= (anonThreshold*memTotal/100) {
|
|
topProcesses, err := getTopMemoryProcesses(topNProcesses)
|
|
if err == nil {
|
|
return topProcesses, nil
|
|
}
|
|
log.Errorf("Fail to getTopMemoryProcesses")
|
|
return []ProcessMemoryInfo{}, err
|
|
}
|
|
}
|
|
return []ProcessMemoryInfo{}, nil
|
|
}
|
|
|
|
// Core function
|
|
func (c *memBurstTracing) Start(ctx context.Context) error {
|
|
var err error
|
|
|
|
historyWindowLength := conf.Get().Tracing.MemoryBurst.HistoryWindowLength
|
|
sampleInterval := conf.Get().Tracing.MemoryBurst.SampleInterval
|
|
silencePeriod := conf.Get().Tracing.MemoryBurst.SilencePeriod
|
|
topNProcesses := conf.Get().Tracing.MemoryBurst.TopNProcesses
|
|
burstRatio := conf.Get().Tracing.MemoryBurst.BurstRatio
|
|
anonThreshold := conf.Get().Tracing.MemoryBurst.AnonThreshold
|
|
|
|
memInfo, err := readMemInfo(map[string]bool{"MemTotal": true})
|
|
if err != nil {
|
|
log.Infof("Error reading MemTotal from memory info: %v\n", err)
|
|
return err
|
|
}
|
|
memTotal := memInfo["MemTotal"]
|
|
history := make([]int, historyWindowLength) // circular buffer
|
|
var currentIndex int
|
|
var isHistoryFull bool // don't check memory burst until we have enough data
|
|
var topProcesses []ProcessMemoryInfo
|
|
lastReportTime := time.Now().Add(-24 * time.Hour)
|
|
|
|
_, err = checkAndRecordMemoryUsage(¤tIndex, &isHistoryFull, memTotal, history, historyWindowLength, topNProcesses, burstRatio, anonThreshold)
|
|
if err != nil {
|
|
log.Errorf("Fail to checkAndRecordMemoryUsage")
|
|
return err
|
|
}
|
|
|
|
for {
|
|
ticker := time.NewTicker(time.Duration(sampleInterval) * time.Second)
|
|
stoppedByUser := false
|
|
|
|
for range ticker.C {
|
|
topProcesses, err = checkAndRecordMemoryUsage(¤tIndex, &isHistoryFull, memTotal, history, historyWindowLength, topNProcesses, burstRatio, anonThreshold)
|
|
if err != nil {
|
|
log.Errorf("Fail to checkAndRecordMemoryUsage")
|
|
return err
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Info("Caller request to stop")
|
|
stoppedByUser = true
|
|
default:
|
|
}
|
|
|
|
if len(topProcesses) > 0 || stoppedByUser {
|
|
break
|
|
}
|
|
}
|
|
|
|
ticker.Stop()
|
|
|
|
if stoppedByUser {
|
|
break
|
|
}
|
|
|
|
currentTime := time.Now()
|
|
diff := currentTime.Sub(lastReportTime).Seconds()
|
|
if diff < float64(silencePeriod) {
|
|
continue
|
|
}
|
|
|
|
lastReportTime = currentTime
|
|
|
|
// save storage
|
|
caseData := &MemoryTracingData{
|
|
TopMemoryUsage: topProcesses,
|
|
}
|
|
storage.Save("memburst", "", time.Now(), caseData)
|
|
}
|
|
|
|
return nil
|
|
}
|