Report CPU/mem usage in GUI
This commit is contained in:
parent
cb33f27f23
commit
832c0ffad0
23
gui.go
23
gui.go
@ -8,6 +8,8 @@ import (
|
|||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"bitbucket.org/tebeka/nrsc"
|
"bitbucket.org/tebeka/nrsc"
|
||||||
"github.com/calmh/syncthing/model"
|
"github.com/calmh/syncthing/model"
|
||||||
@ -22,6 +24,7 @@ func startGUI(addr string, m *model.Model) {
|
|||||||
router.Get("/rest/connections", restGetConnections)
|
router.Get("/rest/connections", restGetConnections)
|
||||||
router.Get("/rest/config", restGetConfig)
|
router.Get("/rest/config", restGetConfig)
|
||||||
router.Get("/rest/need", restGetNeed)
|
router.Get("/rest/need", restGetNeed)
|
||||||
|
router.Get("/rest/system", restGetSystem)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
mr := martini.New()
|
mr := martini.New()
|
||||||
@ -34,6 +37,7 @@ func startGUI(addr string, m *model.Model) {
|
|||||||
warnln("GUI not possible:", err)
|
warnln("GUI not possible:", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRoot(w http.ResponseWriter, r *http.Request) {
|
func getRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -100,6 +104,25 @@ func restGetNeed(m *model.Model, w http.ResponseWriter) {
|
|||||||
json.NewEncoder(w).Encode(gfs)
|
json.NewEncoder(w).Encode(gfs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cpuUsagePercent float64
|
||||||
|
var cpuUsageLock sync.RWMutex
|
||||||
|
|
||||||
|
func restGetSystem(w http.ResponseWriter) {
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
|
||||||
|
res := make(map[string]interface{})
|
||||||
|
res["goroutines"] = runtime.NumGoroutine()
|
||||||
|
res["alloc"] = m.Alloc
|
||||||
|
res["sys"] = m.Sys
|
||||||
|
cpuUsageLock.RLock()
|
||||||
|
res["cpuPercent"] = cpuUsagePercent
|
||||||
|
cpuUsageLock.RUnlock()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(res)
|
||||||
|
}
|
||||||
|
|
||||||
func nrscStatic(path string) interface{} {
|
func nrscStatic(path string) interface{} {
|
||||||
if err := nrsc.Initialize(); err != nil {
|
if err := nrsc.Initialize(); err != nil {
|
||||||
panic("Unable to initialize nrsc: " + err.Error())
|
panic("Unable to initialize nrsc: " + err.Error())
|
||||||
|
|||||||
34
gui/app.js
34
gui/app.js
@ -26,6 +26,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$scope.refresh = function () {
|
$scope.refresh = function () {
|
||||||
|
$http.get("/rest/system").success(function (data) {
|
||||||
|
$scope.system = data;
|
||||||
|
});
|
||||||
$http.get("/rest/model").success(function (data) {
|
$http.get("/rest/model").success(function (data) {
|
||||||
$scope.model = data;
|
$scope.model = data;
|
||||||
modelGetSucceeded();
|
modelGetSucceeded();
|
||||||
@ -71,16 +74,19 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
|||||||
setInterval($scope.refresh, 10000);
|
setInterval($scope.refresh, 10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
function decimals(num) {
|
function decimals(val, num) {
|
||||||
if (num > 100) {
|
if (val === 0) { return 0; }
|
||||||
return 0;
|
var digits = Math.floor(Math.log(Math.abs(val))/Math.log(10));
|
||||||
}
|
var decimals = Math.max(0, num - digits);
|
||||||
if (num > 10) {
|
return decimals;
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncthing.filter('natural', function() {
|
||||||
|
return function(input, valid) {
|
||||||
|
return input.toFixed(decimals(input, valid));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
syncthing.filter('binary', function() {
|
syncthing.filter('binary', function() {
|
||||||
return function(input) {
|
return function(input) {
|
||||||
if (input === undefined) {
|
if (input === undefined) {
|
||||||
@ -88,15 +94,15 @@ syncthing.filter('binary', function() {
|
|||||||
}
|
}
|
||||||
if (input > 1024 * 1024 * 1024) {
|
if (input > 1024 * 1024 * 1024) {
|
||||||
input /= 1024 * 1024 * 1024;
|
input /= 1024 * 1024 * 1024;
|
||||||
return input.toFixed(decimals(input)) + ' Gi';
|
return input.toFixed(decimals(input, 2)) + ' Gi';
|
||||||
}
|
}
|
||||||
if (input > 1024 * 1024) {
|
if (input > 1024 * 1024) {
|
||||||
input /= 1024 * 1024;
|
input /= 1024 * 1024;
|
||||||
return input.toFixed(decimals(input)) + ' Mi';
|
return input.toFixed(decimals(input, 2)) + ' Mi';
|
||||||
}
|
}
|
||||||
if (input > 1024) {
|
if (input > 1024) {
|
||||||
input /= 1024;
|
input /= 1024;
|
||||||
return input.toFixed(decimals(input)) + ' Ki';
|
return input.toFixed(decimals(input, 2)) + ' Ki';
|
||||||
}
|
}
|
||||||
return Math.round(input) + ' ';
|
return Math.round(input) + ' ';
|
||||||
}
|
}
|
||||||
@ -109,15 +115,15 @@ syncthing.filter('metric', function() {
|
|||||||
}
|
}
|
||||||
if (input > 1000 * 1000 * 1000) {
|
if (input > 1000 * 1000 * 1000) {
|
||||||
input /= 1000 * 1000 * 1000;
|
input /= 1000 * 1000 * 1000;
|
||||||
return input.toFixed(decimals(input)) + ' G';
|
return input.toFixed(decimals(input, 2)) + ' G';
|
||||||
}
|
}
|
||||||
if (input > 1000 * 1000) {
|
if (input > 1000 * 1000) {
|
||||||
input /= 1000 * 1000;
|
input /= 1000 * 1000;
|
||||||
return input.toFixed(decimals(input)) + ' M';
|
return input.toFixed(decimals(input, 2)) + ' M';
|
||||||
}
|
}
|
||||||
if (input > 1000) {
|
if (input > 1000) {
|
||||||
input /= 1000;
|
input /= 1000;
|
||||||
return input.toFixed(decimals(input)) + ' k';
|
return input.toFixed(decimals(input, 2)) + ' k';
|
||||||
}
|
}
|
||||||
return Math.round(input) + ' ';
|
return Math.round(input) + ' ';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,27 +47,42 @@ html, body {
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h2>Synchronization</h2>
|
<div class="panel" ng-class="{'panel-success': model.needBytes === 0, 'panel-primary': model.needBytes !== 0}">
|
||||||
<div class="progress">
|
<div class="panel-heading"><h3 class="panel-title">Synchronization</h3></div>
|
||||||
<div class="progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"
|
<div class="panel-body">
|
||||||
ng-class="{'progress-bar-success': model.needBytes === 0, 'progress-bar-info': model.needBytes !== 0}"
|
<div class="progress">
|
||||||
style="width: {{100 * model.inSyncBytes / model.globalBytes | number:2}}%;">
|
<div class="progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"
|
||||||
{{100 * model.inSyncBytes / model.globalBytes | number:0}}%
|
ng-class="{'progress-bar-success': model.needBytes === 0, 'progress-bar-info': model.needBytes !== 0}"
|
||||||
|
style="width: {{100 * model.inSyncBytes / model.globalBytes | number:2}}%;">
|
||||||
|
{{100 * model.inSyncBytes / model.globalBytes | alwaysNumber | number:0}}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p ng-show="model.needBytes > 0">Need {{model.needFiles | alwaysNumber}} files, {{model.needBytes | binary}}B</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p ng-show="model.needBytes > 0">Need {{model.needFiles | alwaysNumber}} files, {{model.needBytes | binary}}B</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h1>Repository Status</h1>
|
<div class="panel panel-info">
|
||||||
|
<div class="panel-heading"><h3 class="panel-title">Repository</h3></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p>Cluster contains {{model.globalFiles | alwaysNumber}} files, {{model.globalBytes | binary}}B
|
||||||
|
<span class="text-muted">(+{{model.globalDeleted | alwaysNumber}} delete records)</span></p>
|
||||||
|
|
||||||
<p>Cluster contains {{model.globalFiles | alwaysNumber}} files, {{model.globalBytes | binary}}B
|
<p>Local repository has {{model.localFiles | alwaysNumber}} files, {{model.localBytes | binary}}B
|
||||||
<span class="text-muted">(+{{model.globalDeleted | alwaysNumber}} delete records)</span></p>
|
<span class="text-muted">(+{{model.localDeleted | alwaysNumber}} delete records)</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>Local repository has {{model.localFiles | alwaysNumber}} files, {{model.localBytes | binary}}B
|
<div class="panel panel-info">
|
||||||
<span class="text-muted">(+{{model.localDeleted | alwaysNumber}} delete records)</span></p>
|
<div class="panel-heading"><h3 class="panel-title">System</h3></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p>{{system.sys | binary}}B RAM allocated, {{system.alloc | binary}}B in use</p>
|
||||||
|
<p>{{system.cpuPercent | alwaysNumber | natural:1}}% CPU, {{system.goroutines | alwaysNumber}} goroutines</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div ng-show="model.needFiles > 0">
|
<div ng-show="model.needFiles > 0">
|
||||||
<h2>Files to Synchronize</h2>
|
<h2>Files to Synchronize</h2>
|
||||||
@ -80,32 +95,34 @@ html, body {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h1>Cluster Status</h1>
|
<div class="panel panel-info">
|
||||||
<table class="table table-condensed">
|
<div class="panel-heading"><h3 class="panel-title">Cluster</h3></div>
|
||||||
<tbody>
|
<table class="table table-condensed">
|
||||||
<tr ng-repeat="(node, address) in config.nodes" ng-class="{'text-primary': !!connections[node]}">
|
<tbody>
|
||||||
<td><abbr class="text-monospace" title="{{node}}">{{node | short}}</abbr></td>
|
<tr ng-repeat="(node, address) in config.nodes" ng-class="{'text-primary': !!connections[node]}">
|
||||||
<td>
|
<td><abbr class="text-monospace" title="{{node}}">{{node | short}}</abbr></td>
|
||||||
<span ng-show="!!connections[node]">
|
<td>
|
||||||
<span class="glyphicon glyphicon-link"></span>
|
<span ng-show="!!connections[node]">
|
||||||
{{connections[node].Address}}
|
<span class="glyphicon glyphicon-link"></span>
|
||||||
</span>
|
{{connections[node].Address}}
|
||||||
<span ng-hide="!!connections[node]">
|
</span>
|
||||||
<span class="glyphicon glyphicon-cog"></span>
|
<span ng-hide="!!connections[node]">
|
||||||
{{address}}
|
<span class="glyphicon glyphicon-cog"></span>
|
||||||
</span>
|
{{address}}
|
||||||
</td>
|
</span>
|
||||||
<td class="text-right">
|
</td>
|
||||||
<abbr title="{{connections[node].InBytesTotal | binary}}B">{{connections[node].inbps | metric}}b/s</abbr>
|
<td class="text-right">
|
||||||
<span class="text-muted glyphicon glyphicon-cloud-download"></span>
|
<abbr title="{{connections[node].InBytesTotal | binary}}B">{{connections[node].inbps | metric}}b/s</abbr>
|
||||||
</td>
|
<span class="text-muted glyphicon glyphicon-cloud-download"></span>
|
||||||
<td class="text-right">
|
</td>
|
||||||
<abbr title="{{connections[node].OutBytesTotal | binary}}B">{{connections[node].outbps | metric}}b/s</abbr>
|
<td class="text-right">
|
||||||
<span class="text-muted glyphicon glyphicon-cloud-upload"></span>
|
<abbr title="{{connections[node].OutBytesTotal | binary}}B">{{connections[node].outbps | metric}}b/s</abbr>
|
||||||
</td>
|
<span class="text-muted glyphicon glyphicon-cloud-upload"></span>
|
||||||
</tr>
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -123,7 +140,7 @@ html, body {
|
|||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header alert alert-danger">
|
<div class="modal-header alert alert-danger">
|
||||||
<h4 class="modal-title">
|
<h4 class="modal-title">
|
||||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||||
Connection Error
|
Connection Error
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
31
gui_unix.go
Normal file
31
gui_unix.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
//+build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
go trackCPUUsage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func trackCPUUsage() {
|
||||||
|
var prevUsage int64
|
||||||
|
var prevTime = time.Now().UnixNano()
|
||||||
|
var rusage syscall.Rusage
|
||||||
|
for {
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
syscall.Getrusage(syscall.RUSAGE_SELF, &rusage)
|
||||||
|
curTime := time.Now().UnixNano()
|
||||||
|
timeDiff := curTime - prevTime
|
||||||
|
curUsage := rusage.Utime.Nano() + rusage.Stime.Nano()
|
||||||
|
usageDiff := curUsage - prevUsage
|
||||||
|
cpuUsageLock.Lock()
|
||||||
|
cpuUsagePercent = 100 * float64(usageDiff) / float64(timeDiff)
|
||||||
|
cpuUsageLock.Unlock()
|
||||||
|
prevTime = curTime
|
||||||
|
prevUsage = curUsage
|
||||||
|
}
|
||||||
|
}
|
||||||
10
main.go
10
main.go
@ -10,6 +10,8 @@ import (
|
|||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -81,6 +83,14 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(os.Getenv("GOGC")) == 0 {
|
||||||
|
debug.SetGCPercent(25)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(os.Getenv("GOMAXPROCS")) == 0 {
|
||||||
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
}
|
||||||
|
|
||||||
if len(opts.Debug.TraceModel) > 0 || opts.Debug.LogSource {
|
if len(opts.Debug.TraceModel) > 0 || opts.Debug.LogSource {
|
||||||
logger = log.New(os.Stderr, "", log.Lshortfile|log.Ldate|log.Ltime|log.Lmicroseconds)
|
logger = log.New(os.Stderr, "", log.Lshortfile|log.Ldate|log.Ltime|log.Lmicroseconds)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user