mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-09-19 00:13:03 +00:00
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
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:
parent
bc274d1e1f
commit
22afa50901
4 changed files with 640 additions and 614 deletions
|
@ -21,17 +21,14 @@ type ServerController struct {
|
||||||
serverService service.ServerService
|
serverService service.ServerService
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
|
|
||||||
lastStatus *service.Status
|
lastStatus *service.Status
|
||||||
lastGetStatusTime time.Time
|
|
||||||
|
|
||||||
lastVersions []string
|
lastVersions []string
|
||||||
lastGetVersionsTime time.Time
|
lastGetVersionsTime int64 // unix seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServerController(g *gin.RouterGroup) *ServerController {
|
func NewServerController(g *gin.RouterGroup) *ServerController {
|
||||||
a := &ServerController{
|
a := &ServerController{}
|
||||||
lastGetStatusTime: time.Now(),
|
|
||||||
}
|
|
||||||
a.initRouter(g)
|
a.initRouter(g)
|
||||||
a.startTask()
|
a.startTask()
|
||||||
return a
|
return a
|
||||||
|
@ -40,7 +37,7 @@ func NewServerController(g *gin.RouterGroup) *ServerController {
|
||||||
func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||||
|
|
||||||
g.GET("/status", a.status)
|
g.GET("/status", a.status)
|
||||||
g.GET("/cpuHistory", a.getCpuHistory)
|
g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket)
|
||||||
g.GET("/getXrayVersion", a.getXrayVersion)
|
g.GET("/getXrayVersion", a.getXrayVersion)
|
||||||
g.GET("/getConfigJson", a.getConfigJson)
|
g.GET("/getConfigJson", a.getConfigJson)
|
||||||
g.GET("/getDb", a.getDb)
|
g.GET("/getDb", a.getDb)
|
||||||
|
@ -79,35 +76,34 @@ func (a *ServerController) startTask() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ServerController) status(c *gin.Context) {
|
func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) }
|
||||||
a.lastGetStatusTime = time.Now()
|
|
||||||
|
|
||||||
jsonObj(c, a.lastStatus, nil)
|
func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
|
||||||
}
|
bucketStr := c.Param("bucket")
|
||||||
|
bucket, err := strconv.Atoi(bucketStr)
|
||||||
// getCpuHistory returns recent CPU utilization points.
|
if err != nil || bucket <= 0 {
|
||||||
// Query param q=minutes (int). Bounds: 1..360 (6 hours). Defaults to 60.
|
jsonMsg(c, "invalid bucket", fmt.Errorf("bad bucket"))
|
||||||
func (a *ServerController) getCpuHistory(c *gin.Context) {
|
return
|
||||||
minsStr := c.Query("q")
|
|
||||||
mins := 60
|
|
||||||
if minsStr != "" {
|
|
||||||
if v, err := strconv.Atoi(minsStr); err == nil {
|
|
||||||
mins = v
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if mins < 1 {
|
allowed := map[int]bool{
|
||||||
mins = 1
|
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 {
|
if !allowed[bucket] {
|
||||||
mins = 360
|
jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
res := a.serverService.GetCpuHistory(mins)
|
points := a.serverService.AggregateCpuHistory(bucket, 60)
|
||||||
jsonObj(c, res, nil)
|
jsonObj(c, points, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ServerController) getXrayVersion(c *gin.Context) {
|
func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||||
now := time.Now()
|
now := time.Now().Unix()
|
||||||
if now.Sub(a.lastGetVersionsTime) <= time.Minute {
|
if now-a.lastGetVersionsTime <= 60 { // 1 minute cache
|
||||||
jsonObj(c, a.lastVersions, nil)
|
jsonObj(c, a.lastVersions, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -119,7 +115,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
a.lastVersions = versions
|
a.lastVersions = versions
|
||||||
a.lastGetVersionsTime = time.Now()
|
a.lastGetVersionsTime = now
|
||||||
|
|
||||||
jsonObj(c, versions, nil)
|
jsonObj(c, versions, nil)
|
||||||
}
|
}
|
||||||
|
@ -137,7 +133,6 @@ func (a *ServerController) updateGeofile(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ServerController) stopXrayService(c *gin.Context) {
|
func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||||
a.lastGetStatusTime = time.Now()
|
|
||||||
err := a.serverService.StopXrayService()
|
err := a.serverService.StopXrayService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err)
|
jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err)
|
||||||
|
@ -253,9 +248,7 @@ func (a *ServerController) importDB(c *gin.Context) {
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
// Always restart Xray before return
|
// Always restart Xray before return
|
||||||
defer a.serverService.RestartXrayService()
|
defer a.serverService.RestartXrayService()
|
||||||
defer func() {
|
// lastGetStatusTime removed; no longer needed
|
||||||
a.lastGetStatusTime = time.Now()
|
|
||||||
}()
|
|
||||||
// Import it
|
// Import it
|
||||||
err = a.serverService.ImportDB(file)
|
err = a.serverService.ImportDB(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -23,13 +23,13 @@ func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
|
||||||
|
|
||||||
func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||||
g = g.Group("/xray")
|
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("/", a.getXraySetting)
|
||||||
g.POST("/update", a.updateSetting)
|
|
||||||
g.GET("/getXrayResult", a.getXrayResult)
|
|
||||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
|
||||||
g.POST("/warp/:action", a.warp)
|
g.POST("/warp/:action", a.warp)
|
||||||
g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
|
g.POST("/update", a.updateSetting)
|
||||||
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
1017
web/html/index.html
1017
web/html/index.html
File diff suppressed because it is too large
Load diff
|
@ -94,22 +94,81 @@ type Release struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerService struct {
|
type ServerService struct {
|
||||||
xrayService XrayService
|
xrayService XrayService
|
||||||
inboundService InboundService
|
inboundService InboundService
|
||||||
cachedIPv4 string
|
cachedIPv4 string
|
||||||
cachedIPv6 string
|
cachedIPv6 string
|
||||||
noIPv6 bool
|
noIPv6 bool
|
||||||
// CPU utilization smoothing state
|
mu sync.Mutex
|
||||||
mu sync.Mutex
|
lastCPUTimes cpu.TimesStat
|
||||||
lastCPUTimes cpu.TimesStat
|
hasLastCPUSample bool
|
||||||
hasLastCPUSample bool
|
emaCPU float64
|
||||||
emaCPU float64
|
cpuHistory []CPUSample
|
||||||
// CPU history buffer (in-memory, protected by mu)
|
cachedCpuSpeedMhz float64
|
||||||
cpuHistory []CPUSample
|
lastCpuInfoAttempt time.Time
|
||||||
cpuCapacity int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
type CPUSample struct {
|
||||||
T int64 `json:"t"` // unix seconds
|
T int64 `json:"t"` // unix seconds
|
||||||
Cpu float64 `json:"cpu"` // percent 0..100
|
Cpu float64 `json:"cpu"` // percent 0..100
|
||||||
|
@ -168,13 +227,30 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||||
|
|
||||||
status.LogicalPro = runtime.NumCPU()
|
status.LogicalPro = runtime.NumCPU()
|
||||||
|
|
||||||
cpuInfos, err := cpu.Info()
|
if status.CpuSpeedMhz = s.cachedCpuSpeedMhz; s.cachedCpuSpeedMhz == 0 && time.Since(s.lastCpuInfoAttempt) > 5*time.Minute {
|
||||||
if err != nil {
|
s.lastCpuInfoAttempt = time.Now()
|
||||||
logger.Warning("get cpu info failed:", err)
|
done := make(chan struct{})
|
||||||
} else if len(cpuInfos) > 0 {
|
go func() {
|
||||||
status.CpuSpeedMhz = cpuInfos[0].Mhz
|
defer close(done)
|
||||||
} else {
|
cpuInfos, err := cpu.Info()
|
||||||
logger.Warning("could not find 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
|
// Uptime
|
||||||
|
@ -322,55 +398,21 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||||
return 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) {
|
func (s *ServerService) AppendCpuSample(t time.Time, v float64) {
|
||||||
|
const capacity = 9000 // ~5 hours @ 2s interval
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
if s.cpuCapacity == 0 {
|
|
||||||
s.cpuCapacity = 10800 // ~6 hours at 2s per sample
|
|
||||||
}
|
|
||||||
p := CPUSample{T: t.Unix(), Cpu: v}
|
p := CPUSample{T: t.Unix(), Cpu: v}
|
||||||
s.cpuHistory = append(s.cpuHistory, p)
|
if n := len(s.cpuHistory); n > 0 && s.cpuHistory[n-1].T == p.T {
|
||||||
if len(s.cpuHistory) > s.cpuCapacity {
|
s.cpuHistory[n-1] = p
|
||||||
drop := len(s.cpuHistory) - s.cpuCapacity
|
} else {
|
||||||
s.cpuHistory = s.cpuHistory[drop:]
|
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) {
|
func (s *ServerService) sampleCPUUtilization() (float64, error) {
|
||||||
// Prefer native Windows API to avoid external deps for CPU percent
|
// Prefer native Windows API to avoid external deps for CPU percent
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
|
|
Loading…
Reference in a new issue