huatuo/core/autotracing/memburst.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(&currentIndex, &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(&currentIndex, &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
}