From 63c34f195ac2224e02b19a251f152a003049b87b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tomasz=20Wilczy=C5=84ski?= <twilczynski@naver.com>
Date: Tue, 1 Dec 2020 22:11:30 +0900
Subject: [PATCH] Revert "gui, lib/api: Remove CPU & RAM measurements (fixes
 #6249) (#6393)"
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This reverts commit c7d6a6d78042876d9a27b13df2ae45783dca7c37.

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
---
 gui/default/index.html            |  8 ++++
 lib/api/api.go                    | 14 ++++--
 lib/api/api_test.go               |  7 +--
 lib/api/mocked_cpuusage_test.go   | 13 ++++++
 lib/rc/rc.go                      |  1 +
 lib/syncthing/cpuusage.go         | 59 +++++++++++++++++++++++
 lib/syncthing/cpuusage_solaris.go | 78 +++++++++++++++++++++++++++++++
 lib/syncthing/cpuusage_unix.go    | 18 +++++++
 lib/syncthing/cpuusage_windows.go | 27 +++++++++++
 lib/syncthing/syncthing.go        |  5 +-
 10 files changed, 223 insertions(+), 7 deletions(-)
 create mode 100644 lib/api/mocked_cpuusage_test.go
 create mode 100644 lib/syncthing/cpuusage.go
 create mode 100644 lib/syncthing/cpuusage_solaris.go
 create mode 100644 lib/syncthing/cpuusage_unix.go
 create mode 100644 lib/syncthing/cpuusage_windows.go

diff --git a/gui/default/index.html b/gui/default/index.html
index fe282d48..5aa6feea 100644
--- a/gui/default/index.html
+++ b/gui/default/index.html
@@ -640,6 +640,14 @@
                           </span>
                       </td>
                     </tr>
+                    <tr>
+                      <th><span class="fas fa-fw fa-microchip"></span>&nbsp;<span translate>RAM Utilization</span></th>
+                      <td class="text-right">{{system.sys | binary}}B</td>
+                    </tr>
+                    <tr>
+                      <th><span class="fas fa-fw fa-tachometer-alt"></span>&nbsp;<span translate>CPU Utilization</span></th>
+                      <td class="text-right">{{system.cpuPercent | alwaysNumber | percent}}</td>
+                    </tr>
                     <tr>
                       <th><span class="fas fa-fw fa-sitemap"></span>&nbsp;<span translate>Listeners</span></th>
                       <td class="text-right">
diff --git a/lib/api/api.go b/lib/api/api.go
index e857344e..fc859f9d 100644
--- a/lib/api/api.go
+++ b/lib/api/api.go
@@ -82,6 +82,7 @@ type service struct {
 	connectionsService   connections.Service
 	fss                  model.FolderSummaryService
 	urService            *ur.Service
+	cpu                  Rater
 	contr                Controller
 	noUpgrade            bool
 	tlsDefaultCommonName string
@@ -95,6 +96,10 @@ type service struct {
 	systemLog logger.Recorder
 }
 
+type Rater interface {
+	Rate() float64
+}
+
 type Controller interface {
 	ExitUpgrading()
 	Restart()
@@ -107,7 +112,7 @@ type Service interface {
 	WaitForStart() error
 }
 
-func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, contr Controller, noUpgrade bool) Service {
+func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, cpu Rater, contr Controller, noUpgrade bool) Service {
 	s := &service{
 		id:      id,
 		cfg:     cfg,
@@ -125,6 +130,7 @@ func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonNam
 		urService:            urService,
 		guiErrors:            errors,
 		systemLog:            systemLog,
+		cpu:                  cpu,
 		contr:                contr,
 		noUpgrade:            noUpgrade,
 		tlsDefaultCommonName: tlsDefaultCommonName,
@@ -314,7 +320,7 @@ func (s *service) serve(ctx context.Context) {
 	configBuilder.registerGUI("/rest/config/gui")
 
 	// Deprecated config endpoints
-	configBuilder.registerConfigDeprecated("/rest/system/config") // POST instead of PUT
+	configBuilder.registerConfig("/rest/system/config") // POST instead of PUT
 	configBuilder.registerConfigInsync("/rest/system/config/insync")
 
 	// Debug endpoints, not for general use
@@ -916,7 +922,9 @@ func (s *service) getSystemStatus(w http.ResponseWriter, r *http.Request) {
 
 	res["connectionServiceStatus"] = s.connectionsService.ListenerStatus()
 	res["lastDialStatus"] = s.connectionsService.ConnectionStatus()
-	res["cpuPercent"] = 0 // deprecated from API
+	// cpuUsage.Rate() is in milliseconds per second, so dividing by ten
+	// gives us percent
+	res["cpuPercent"] = s.cpu.Rate() / 10 / float64(runtime.NumCPU())
 	res["pathSeparator"] = string(filepath.Separator)
 	res["urVersionMax"] = ur.Version
 	res["uptime"] = s.urService.UptimeS()
diff --git a/lib/api/api_test.go b/lib/api/api_test.go
index de7e3a80..16265fec 100644
--- a/lib/api/api_test.go
+++ b/lib/api/api_test.go
@@ -113,7 +113,7 @@ func TestStopAfterBrokenConfig(t *testing.T) {
 	}
 	w := config.Wrap("/dev/null", cfg, events.NoopLogger)
 
-	srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, false).(*service)
+	srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, nil, false).(*service)
 	defer os.Remove(token)
 	srv.started = make(chan string)
 
@@ -590,11 +590,12 @@ func startHTTP(cfg config.Wrapper) (string, *suture.Supervisor, error) {
 	connections := new(mockedConnections)
 	errorLog := new(mockedLoggerRecorder)
 	systemLog := new(mockedLoggerRecorder)
+	cpu := new(mockedCPUService)
 	addrChan := make(chan string)
 
 	// Instantiate the API service
 	urService := ur.New(cfg, m, connections, false)
-	svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, &mockedFolderSummaryService{}, errorLog, systemLog, nil, false).(*service)
+	svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, &mockedFolderSummaryService{}, errorLog, systemLog, cpu, nil, false).(*service)
 	defer os.Remove(token)
 	svc.started = addrChan
 
@@ -1093,7 +1094,7 @@ func TestEventMasks(t *testing.T) {
 	cfg := new(mockedConfig)
 	defSub := new(mockedEventSub)
 	diskSub := new(mockedEventSub)
-	svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, false).(*service)
+	svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, nil, false).(*service)
 	defer os.Remove(token)
 
 	if mask := svc.getEventMask(""); mask != DefaultEventMask {
diff --git a/lib/api/mocked_cpuusage_test.go b/lib/api/mocked_cpuusage_test.go
new file mode 100644
index 00000000..c89a1dba
--- /dev/null
+++ b/lib/api/mocked_cpuusage_test.go
@@ -0,0 +1,13 @@
+// Copyright (C) 2017 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package api
+
+type mockedCPUService struct{}
+
+func (*mockedCPUService) Rate() float64 {
+	return 42
+}
diff --git a/lib/rc/rc.go b/lib/rc/rc.go
index f21d0e31..f953ac85 100644
--- a/lib/rc/rc.go
+++ b/lib/rc/rc.go
@@ -618,6 +618,7 @@ func (p *Process) Connections() (map[string]ConnectionStats, error) {
 
 type SystemStatus struct {
 	Alloc         int64
+	CPUPercent    float64
 	Goroutines    int
 	MyID          protocol.DeviceID
 	PathSeparator string
diff --git a/lib/syncthing/cpuusage.go b/lib/syncthing/cpuusage.go
new file mode 100644
index 00000000..695859c9
--- /dev/null
+++ b/lib/syncthing/cpuusage.go
@@ -0,0 +1,59 @@
+// Copyright (C) 2017 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package syncthing
+
+import (
+	"math"
+	"time"
+
+	metrics "github.com/rcrowley/go-metrics"
+)
+
+const cpuTickRate = 5 * time.Second
+
+type cpuService struct {
+	avg  metrics.EWMA
+	stop chan struct{}
+}
+
+func newCPUService() *cpuService {
+	return &cpuService{
+		// 10 second average. Magic alpha value comes from looking at EWMA package
+		// definitions of EWMA1, EWMA5. The tick rate *must* be five seconds (hard
+		// coded in the EWMA package).
+		avg:  metrics.NewEWMA(1 - math.Exp(-float64(cpuTickRate)/float64(time.Second)/10.0)),
+		stop: make(chan struct{}),
+	}
+}
+
+func (s *cpuService) Serve() {
+	// Initialize prevUsage to an actual value returned by cpuUsage
+	// instead of zero, because at least Windows returns a huge negative
+	// number here that then slowly increments...
+	prevUsage := cpuUsage()
+	ticker := time.NewTicker(cpuTickRate)
+	defer ticker.Stop()
+	for {
+		select {
+		case <-ticker.C:
+			curUsage := cpuUsage()
+			s.avg.Update(int64((curUsage - prevUsage) / time.Millisecond))
+			prevUsage = curUsage
+			s.avg.Tick()
+		case <-s.stop:
+			return
+		}
+	}
+}
+
+func (s *cpuService) Stop() {
+	close(s.stop)
+}
+
+func (s *cpuService) Rate() float64 {
+	return s.avg.Rate()
+}
diff --git a/lib/syncthing/cpuusage_solaris.go b/lib/syncthing/cpuusage_solaris.go
new file mode 100644
index 00000000..74a4e280
--- /dev/null
+++ b/lib/syncthing/cpuusage_solaris.go
@@ -0,0 +1,78 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//+build solaris
+
+package syncthing
+
+import (
+	"encoding/binary"
+	"fmt"
+	"os"
+	"time"
+)
+
+type id_t int32
+type ulong_t uint32
+
+type timestruc_t struct {
+	Tv_sec  int64
+	Tv_nsec int64
+}
+
+func (tv timestruc_t) Nano() int64 {
+	return tv.Tv_sec*1e9 + tv.Tv_nsec
+}
+
+type prusage_t struct {
+	Pr_lwpid    id_t        /* lwp id.  0: process or defunct */
+	Pr_count    int32       /* number of contributing lwps */
+	Pr_tstamp   timestruc_t /* real time stamp, time of read() */
+	Pr_create   timestruc_t /* process/lwp creation time stamp */
+	Pr_term     timestruc_t /* process/lwp termination time stamp */
+	Pr_rtime    timestruc_t /* total lwp real (elapsed) time */
+	Pr_utime    timestruc_t /* user level CPU time */
+	Pr_stime    timestruc_t /* system call CPU time */
+	Pr_ttime    timestruc_t /* other system trap CPU time */
+	Pr_tftime   timestruc_t /* text page fault sleep time */
+	Pr_dftime   timestruc_t /* data page fault sleep time */
+	Pr_kftime   timestruc_t /* kernel page fault sleep time */
+	Pr_ltime    timestruc_t /* user lock wait sleep time */
+	Pr_slptime  timestruc_t /* all other sleep time */
+	Pr_wtime    timestruc_t /* wait-cpu (latency) time */
+	Pr_stoptime timestruc_t /* stopped time */
+	Pr_minf     ulong_t     /* minor page faults */
+	Pr_majf     ulong_t     /* major page faults */
+	Pr_nswap    ulong_t     /* swaps */
+	Pr_inblk    ulong_t     /* input blocks */
+	Pr_oublk    ulong_t     /* output blocks */
+	Pr_msnd     ulong_t     /* messages sent */
+	Pr_mrcv     ulong_t     /* messages received */
+	Pr_sigs     ulong_t     /* signals received */
+	Pr_vctx     ulong_t     /* voluntary context switches */
+	Pr_ictx     ulong_t     /* involuntary context switches */
+	Pr_sysc     ulong_t     /* system calls */
+	Pr_ioch     ulong_t     /* chars read and written */
+
+}
+
+var procFile = fmt.Sprintf("/proc/%d/usage", os.Getpid())
+
+func cpuUsage() time.Duration {
+	fd, err := os.Open(procFile)
+	if err != nil {
+		return 0
+	}
+
+	var rusage prusage_t
+	err = binary.Read(fd, binary.LittleEndian, rusage)
+	fd.Close()
+	if err != nil {
+		return 0
+	}
+
+	return time.Duration(rusage.Pr_utime.Nano() + rusage.Pr_stime.Nano())
+}
diff --git a/lib/syncthing/cpuusage_unix.go b/lib/syncthing/cpuusage_unix.go
new file mode 100644
index 00000000..2b149abb
--- /dev/null
+++ b/lib/syncthing/cpuusage_unix.go
@@ -0,0 +1,18 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//+build !windows,!solaris
+
+package syncthing
+
+import "syscall"
+import "time"
+
+func cpuUsage() time.Duration {
+	var rusage syscall.Rusage
+	syscall.Getrusage(syscall.RUSAGE_SELF, &rusage)
+	return time.Duration(rusage.Utime.Nano() + rusage.Stime.Nano())
+}
diff --git a/lib/syncthing/cpuusage_windows.go b/lib/syncthing/cpuusage_windows.go
new file mode 100644
index 00000000..6627b8af
--- /dev/null
+++ b/lib/syncthing/cpuusage_windows.go
@@ -0,0 +1,27 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//+build windows
+
+package syncthing
+
+import "syscall"
+import "time"
+
+func cpuUsage() time.Duration {
+	handle, err := syscall.GetCurrentProcess()
+	if err != nil {
+		return 0
+	}
+	defer syscall.CloseHandle(handle)
+
+	var ctime, etime, ktime, utime syscall.Filetime
+	if err := syscall.GetProcessTimes(handle, &ctime, &etime, &ktime, &utime); err != nil {
+		return 0
+	}
+
+	return time.Duration(ktime.Nanoseconds() + utime.Nanoseconds())
+}
diff --git a/lib/syncthing/syncthing.go b/lib/syncthing/syncthing.go
index e9c5acd2..63f651ed 100644
--- a/lib/syncthing/syncthing.go
+++ b/lib/syncthing/syncthing.go
@@ -409,10 +409,13 @@ func (a *App) setupGUI(m model.Model, defaultSub, diskSub events.BufferedSubscri
 		l.Warnln("Insecure admin access is enabled.")
 	}
 
+	cpu := newCPUService()
+	a.mainService.Add(cpu)
+
 	summaryService := model.NewFolderSummaryService(a.cfg, m, a.myID, a.evLogger)
 	a.mainService.Add(summaryService)
 
-	apiSvc := api.New(a.myID, a.cfg, a.opts.AssetDir, tlsDefaultCommonName, m, defaultSub, diskSub, a.evLogger, discoverer, connectionsService, urService, summaryService, errors, systemLog, &controller{a}, a.opts.NoUpgrade)
+	apiSvc := api.New(a.myID, a.cfg, a.opts.AssetDir, tlsDefaultCommonName, m, defaultSub, diskSub, a.evLogger, discoverer, connectionsService, urService, summaryService, errors, systemLog, cpu, &controller{a}, a.opts.NoUpgrade)
 	a.mainService.Add(apiSvc)
 
 	if err := apiSvc.WaitForStart(); err != nil {
