fix CPU History intervals
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run

This commit is contained in:
mhsanaei 2025-09-17 01:08:59 +02:00
parent bc274d1e1f
commit 22afa50901
No known key found for this signature in database
GPG key ID: D875CD086CF668A0
4 changed files with 640 additions and 614 deletions

View file

@ -21,17 +21,14 @@ type ServerController struct {
serverService service.ServerService
settingService service.SettingService
lastStatus *service.Status
lastGetStatusTime time.Time
lastStatus *service.Status
lastVersions []string
lastGetVersionsTime time.Time
lastGetVersionsTime int64 // unix seconds
}
func NewServerController(g *gin.RouterGroup) *ServerController {
a := &ServerController{
lastGetStatusTime: time.Now(),
}
a := &ServerController{}
a.initRouter(g)
a.startTask()
return a
@ -40,7 +37,7 @@ func NewServerController(g *gin.RouterGroup) *ServerController {
func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.GET("/status", a.status)
g.GET("/cpuHistory", a.getCpuHistory)
g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket)
g.GET("/getXrayVersion", a.getXrayVersion)
g.GET("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb)
@ -79,35 +76,34 @@ func (a *ServerController) startTask() {
})
}
func (a *ServerController) status(c *gin.Context) {
a.lastGetStatusTime = time.Now()
func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) }
jsonObj(c, a.lastStatus, nil)
}
// getCpuHistory returns recent CPU utilization points.
// Query param q=minutes (int). Bounds: 1..360 (6 hours). Defaults to 60.
func (a *ServerController) getCpuHistory(c *gin.Context) {
minsStr := c.Query("q")
mins := 60
if minsStr != "" {
if v, err := strconv.Atoi(minsStr); err == nil {
mins = v
}
func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
bucketStr := c.Param("bucket")
bucket, err := strconv.Atoi(bucketStr)
if err != nil || bucket <= 0 {
jsonMsg(c, "invalid bucket", fmt.Errorf("bad bucket"))
return
}
if mins < 1 {
mins = 1
allowed := map[int]bool{
2: true, // Real-time view
30: true, // 30s intervals
60: true, // 1m intervals
120: true, // 2m intervals
180: true, // 3m intervals
300: true, // 5m intervals
}
if mins > 360 {
mins = 360
if !allowed[bucket] {
jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket"))
return
}
res := a.serverService.GetCpuHistory(mins)
jsonObj(c, res, nil)
points := a.serverService.AggregateCpuHistory(bucket, 60)
jsonObj(c, points, nil)
}
func (a *ServerController) getXrayVersion(c *gin.Context) {
now := time.Now()
if now.Sub(a.lastGetVersionsTime) <= time.Minute {
now := time.Now().Unix()
if now-a.lastGetVersionsTime <= 60 { // 1 minute cache
jsonObj(c, a.lastVersions, nil)
return
}
@ -119,7 +115,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
}
a.lastVersions = versions
a.lastGetVersionsTime = time.Now()
a.lastGetVersionsTime = now
jsonObj(c, versions, nil)
}
@ -137,7 +133,6 @@ func (a *ServerController) updateGeofile(c *gin.Context) {
}
func (a *ServerController) stopXrayService(c *gin.Context) {
a.lastGetStatusTime = time.Now()
err := a.serverService.StopXrayService()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err)
@ -253,9 +248,7 @@ func (a *ServerController) importDB(c *gin.Context) {
defer file.Close()
// Always restart Xray before return
defer a.serverService.RestartXrayService()
defer func() {
a.lastGetStatusTime = time.Now()
}()
// lastGetStatusTime removed; no longer needed
// Import it
err = a.serverService.ImportDB(file)
if err != nil {

View file

@ -23,13 +23,13 @@ func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
g = g.Group("/xray")
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
g.GET("/getXrayResult", a.getXrayResult)
g.POST("/", a.getXraySetting)
g.POST("/update", a.updateSetting)
g.GET("/getXrayResult", a.getXrayResult)
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
g.POST("/warp/:action", a.warp)
g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
g.POST("/update", a.updateSetting)
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
}

File diff suppressed because it is too large Load diff

View file

@ -94,22 +94,81 @@ type Release struct {
}
type ServerService struct {
xrayService XrayService
inboundService InboundService
cachedIPv4 string
cachedIPv6 string
noIPv6 bool
// CPU utilization smoothing state
mu sync.Mutex
lastCPUTimes cpu.TimesStat
hasLastCPUSample bool
emaCPU float64
// CPU history buffer (in-memory, protected by mu)
cpuHistory []CPUSample
cpuCapacity int
xrayService XrayService
inboundService InboundService
cachedIPv4 string
cachedIPv6 string
noIPv6 bool
mu sync.Mutex
lastCPUTimes cpu.TimesStat
hasLastCPUSample bool
emaCPU float64
cpuHistory []CPUSample
cachedCpuSpeedMhz float64
lastCpuInfoAttempt time.Time
}
// CPUSample represents a single CPU utilization sample with timestamp
// AggregateCpuHistory returns up to maxPoints averaged buckets of size bucketSeconds over recent data.
func (s *ServerService) AggregateCpuHistory(bucketSeconds int, maxPoints int) []map[string]any {
if bucketSeconds <= 0 || maxPoints <= 0 {
return nil
}
cutoff := time.Now().Add(-time.Duration(bucketSeconds*maxPoints) * time.Second).Unix()
s.mu.Lock()
// find start index (history sorted ascending)
hist := s.cpuHistory
// binary-ish scan (simple linear from end since size capped ~10800 is fine)
startIdx := 0
for i := len(hist) - 1; i >= 0; i-- {
if hist[i].T < cutoff {
startIdx = i + 1
break
}
}
if startIdx >= len(hist) {
s.mu.Unlock()
return []map[string]any{}
}
slice := hist[startIdx:]
// copy for unlock
tmp := make([]CPUSample, len(slice))
copy(tmp, slice)
s.mu.Unlock()
if len(tmp) == 0 {
return []map[string]any{}
}
var out []map[string]any
var acc []float64
bSize := int64(bucketSeconds)
curBucket := (tmp[0].T / bSize) * bSize
flush := func(ts int64) {
if len(acc) == 0 {
return
}
sum := 0.0
for _, v := range acc {
sum += v
}
avg := sum / float64(len(acc))
out = append(out, map[string]any{"t": ts, "cpu": avg})
acc = acc[:0]
}
for _, p := range tmp {
b := (p.T / bSize) * bSize
if b != curBucket {
flush(curBucket)
curBucket = b
}
acc = append(acc, p.Cpu)
}
flush(curBucket)
if len(out) > maxPoints {
out = out[len(out)-maxPoints:]
}
return out
}
// CPUSample single CPU utilization sample
type CPUSample struct {
T int64 `json:"t"` // unix seconds
Cpu float64 `json:"cpu"` // percent 0..100
@ -168,13 +227,30 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
status.LogicalPro = runtime.NumCPU()
cpuInfos, err := cpu.Info()
if err != nil {
logger.Warning("get cpu info failed:", err)
} else if len(cpuInfos) > 0 {
status.CpuSpeedMhz = cpuInfos[0].Mhz
} else {
logger.Warning("could not find cpu info")
if status.CpuSpeedMhz = s.cachedCpuSpeedMhz; s.cachedCpuSpeedMhz == 0 && time.Since(s.lastCpuInfoAttempt) > 5*time.Minute {
s.lastCpuInfoAttempt = time.Now()
done := make(chan struct{})
go func() {
defer close(done)
cpuInfos, err := cpu.Info()
if err != nil {
logger.Warning("get cpu info failed:", err)
return
}
if len(cpuInfos) > 0 {
s.cachedCpuSpeedMhz = cpuInfos[0].Mhz
status.CpuSpeedMhz = s.cachedCpuSpeedMhz
} else {
logger.Warning("could not find cpu info")
}
}()
select {
case <-done:
case <-time.After(1500 * time.Millisecond):
logger.Warning("cpu info query timed out; will retry later")
}
} else if s.cachedCpuSpeedMhz != 0 {
status.CpuSpeedMhz = s.cachedCpuSpeedMhz
}
// Uptime
@ -322,55 +398,21 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
return status
}
// AppendCpuSample appends a CPU sample into the in-memory history with capacity trimming.
func (s *ServerService) AppendCpuSample(t time.Time, v float64) {
const capacity = 9000 // ~5 hours @ 2s interval
s.mu.Lock()
defer s.mu.Unlock()
if s.cpuCapacity == 0 {
s.cpuCapacity = 10800 // ~6 hours at 2s per sample
}
p := CPUSample{T: t.Unix(), Cpu: v}
s.cpuHistory = append(s.cpuHistory, p)
if len(s.cpuHistory) > s.cpuCapacity {
drop := len(s.cpuHistory) - s.cpuCapacity
s.cpuHistory = s.cpuHistory[drop:]
if n := len(s.cpuHistory); n > 0 && s.cpuHistory[n-1].T == p.T {
s.cpuHistory[n-1] = p
} else {
s.cpuHistory = append(s.cpuHistory, p)
}
if len(s.cpuHistory) > capacity {
s.cpuHistory = s.cpuHistory[len(s.cpuHistory)-capacity:]
}
}
// GetCpuHistory returns samples from the last 'mins' minutes (bounded 1..360).
func (s *ServerService) GetCpuHistory(mins int) []CPUSample {
if mins < 1 {
mins = 1
}
if mins > 360 {
mins = 360
}
cutoff := time.Now().Add(-time.Duration(mins) * time.Minute).Unix()
s.mu.Lock()
defer s.mu.Unlock()
if len(s.cpuHistory) == 0 {
return nil
}
// find first index >= cutoff (linear scan from end is fine for these sizes)
i := len(s.cpuHistory) - 1
for ; i >= 0; i-- {
if s.cpuHistory[i].T < cutoff {
i++
break
}
}
if i < 0 {
i = 0
}
// copy to avoid exposing internal slice
out := make([]CPUSample, len(s.cpuHistory)-i)
copy(out, s.cpuHistory[i:])
return out
}
// sampleCPUUtilization returns a smoothed total CPU utilization percentage across all logical processors.
// It computes utilization from CPU time deltas (non-blocking) and applies an exponential moving average
// to reduce spikes similar to Task Manager's smoothing.
func (s *ServerService) sampleCPUUtilization() (float64, error) {
// Prefer native Windows API to avoid external deps for CPU percent
if runtime.GOOS == "windows" {