mirror of
https://github.com/octoleo/syncthing.git
synced 2025-02-02 03:48:26 +00:00
Number formatting
This commit is contained in:
parent
518a4eb6ee
commit
caad69a312
69
analytics.go
Normal file
69
analytics.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "sort"
|
||||||
|
|
||||||
|
type analytic struct {
|
||||||
|
Key string
|
||||||
|
Count int
|
||||||
|
Percentage float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type analyticList []analytic
|
||||||
|
|
||||||
|
func (l analyticList) Less(a, b int) bool {
|
||||||
|
return l[b].Count < l[a].Count // inverse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l analyticList) Swap(a, b int) {
|
||||||
|
l[a], l[b] = l[b], l[a]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l analyticList) Len() int {
|
||||||
|
return len(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a list of frequency analytics for a given list of strings.
|
||||||
|
func analyticsFor(ss []string) []analytic {
|
||||||
|
m := make(map[string]int)
|
||||||
|
t := 0
|
||||||
|
for _, s := range ss {
|
||||||
|
m[s]++
|
||||||
|
t++
|
||||||
|
}
|
||||||
|
|
||||||
|
l := make([]analytic, 0, len(m))
|
||||||
|
for k, c := range m {
|
||||||
|
l = append(l, analytic{k, c, 100 * float64(c) / float64(t)})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(analyticList(l))
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func statsForInts(data []int) [4]float64 {
|
||||||
|
var res [4]float64
|
||||||
|
if len(data) == 0 {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Ints(data)
|
||||||
|
res[0] = float64(data[int(float64(len(data))*0.05)])
|
||||||
|
res[1] = float64(data[len(data)/2])
|
||||||
|
res[2] = float64(data[int(float64(len(data))*0.95)])
|
||||||
|
res[3] = float64(data[len(data)-1])
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func statsForFloats(data []float64) [4]float64 {
|
||||||
|
var res [4]float64
|
||||||
|
if len(data) == 0 {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Float64s(data)
|
||||||
|
res[0] = data[int(float64(len(data))*0.05)]
|
||||||
|
res[1] = data[len(data)/2]
|
||||||
|
res[2] = data[int(float64(len(data))*0.95)]
|
||||||
|
res[3] = data[len(data)-1]
|
||||||
|
return res
|
||||||
|
}
|
71
formatting.go
Normal file
71
formatting.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func number(isBinary bool, v float64) string {
|
||||||
|
if isBinary {
|
||||||
|
return binary(v)
|
||||||
|
} else {
|
||||||
|
return metric(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type prefix struct {
|
||||||
|
Prefix string
|
||||||
|
Multiplier float64
|
||||||
|
}
|
||||||
|
|
||||||
|
var metricPrefixes = []prefix{
|
||||||
|
{"G", 1e9},
|
||||||
|
{"M", 1e6},
|
||||||
|
{"k", 1e3},
|
||||||
|
}
|
||||||
|
|
||||||
|
var binaryPrefixes = []prefix{
|
||||||
|
{"Gi", 1 << 30},
|
||||||
|
{"Mi", 1 << 20},
|
||||||
|
{"Ki", 1 << 10},
|
||||||
|
}
|
||||||
|
|
||||||
|
func metric(v float64) string {
|
||||||
|
return withPrefix(v, metricPrefixes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func binary(v float64) string {
|
||||||
|
return withPrefix(v, binaryPrefixes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func withPrefix(v float64, ps []prefix) string {
|
||||||
|
for _, p := range ps {
|
||||||
|
if v >= p.Multiplier {
|
||||||
|
return fmt.Sprintf("%.1f %s", v/p.Multiplier, p.Prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprint(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// commatize returns a number with sep as thousands separators. Handles
|
||||||
|
// integers and plain floats.
|
||||||
|
func commatize(sep, s string) string {
|
||||||
|
var b bytes.Buffer
|
||||||
|
fs := strings.SplitN(s, ".", 2)
|
||||||
|
|
||||||
|
l := len(fs[0])
|
||||||
|
for i := range fs[0] {
|
||||||
|
b.Write([]byte{s[i]})
|
||||||
|
if i < l-1 && (l-i)%3 == 1 {
|
||||||
|
b.WriteString(sep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fs) > 1 && len(fs[1]) > 0 {
|
||||||
|
b.WriteString(".")
|
||||||
|
b.WriteString(fs[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
220
main.go
220
main.go
@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -14,7 +13,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -28,48 +26,9 @@ var (
|
|||||||
tpl *template.Template
|
tpl *template.Template
|
||||||
)
|
)
|
||||||
|
|
||||||
type category struct {
|
|
||||||
Key string
|
|
||||||
Descr string
|
|
||||||
Unit string
|
|
||||||
}
|
|
||||||
|
|
||||||
var categories = []category{
|
|
||||||
{Key: "totFiles", Descr: "Files Managed per Node", Unit: ""},
|
|
||||||
{Key: "maxFiles", Descr: "Files in Largest Repo", Unit: ""},
|
|
||||||
{Key: "totMiB", Descr: "Data Managed per Node", Unit: "MiB"},
|
|
||||||
{Key: "maxMiB", Descr: "Data in Largest Repo", Unit: "MiB"},
|
|
||||||
{Key: "numNodes", Descr: "Number of Nodes in Cluster", Unit: ""},
|
|
||||||
{Key: "numRepos", Descr: "Number of Repositories Configured", Unit: ""},
|
|
||||||
{Key: "memoryUsage", Descr: "Memory Usage", Unit: "MiB"},
|
|
||||||
{Key: "memorySize", Descr: "System Memory", Unit: "MiB"},
|
|
||||||
{Key: "sha256Perf", Descr: "SHA-256 Hashing Performance", Unit: "MiB/s"},
|
|
||||||
}
|
|
||||||
|
|
||||||
var numRe = regexp.MustCompile(`\d\d\d$`)
|
|
||||||
var funcs = map[string]interface{}{
|
var funcs = map[string]interface{}{
|
||||||
"number": func(n interface{}) string {
|
|
||||||
var s string
|
|
||||||
switch n := n.(type) {
|
|
||||||
case int:
|
|
||||||
s = fmt.Sprint(n)
|
|
||||||
case float64:
|
|
||||||
s = fmt.Sprintf("%.02f", n)
|
|
||||||
default:
|
|
||||||
return fmt.Sprint(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
l := len(s)
|
|
||||||
for i := range s {
|
|
||||||
b.Write([]byte{s[i]})
|
|
||||||
if (l-i)%3 == 1 {
|
|
||||||
b.WriteString(",")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
},
|
|
||||||
"commatize": commatize,
|
"commatize": commatize,
|
||||||
|
"number": number,
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -203,12 +162,26 @@ func fileList() ([]string, error) {
|
|||||||
|
|
||||||
l := make([]string, 0, len(files))
|
l := make([]string, 0, len(files))
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
l = append(l, f)
|
si, err := os.Stat(f)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if time.Since(si.ModTime()) < 24*time.Hour {
|
||||||
|
l = append(l, f)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type category struct {
|
||||||
|
Values [4]float64
|
||||||
|
Key string
|
||||||
|
Descr string
|
||||||
|
Unit string
|
||||||
|
Binary bool
|
||||||
|
}
|
||||||
|
|
||||||
var reportCache map[string]interface{}
|
var reportCache map[string]interface{}
|
||||||
var reportMutex sync.Mutex
|
var reportMutex sync.Mutex
|
||||||
|
|
||||||
@ -270,22 +243,78 @@ func getReport(key string) map[string]interface{} {
|
|||||||
maxFiles = append(maxFiles, rep.RepoMaxFiles)
|
maxFiles = append(maxFiles, rep.RepoMaxFiles)
|
||||||
}
|
}
|
||||||
if rep.TotMiB > 0 {
|
if rep.TotMiB > 0 {
|
||||||
totMiB = append(totMiB, rep.TotMiB)
|
totMiB = append(totMiB, rep.TotMiB*(1<<20))
|
||||||
}
|
}
|
||||||
if rep.RepoMaxMiB > 0 {
|
if rep.RepoMaxMiB > 0 {
|
||||||
maxMiB = append(maxMiB, rep.RepoMaxMiB)
|
maxMiB = append(maxMiB, rep.RepoMaxMiB*(1<<20))
|
||||||
}
|
}
|
||||||
if rep.MemoryUsageMiB > 0 {
|
if rep.MemoryUsageMiB > 0 {
|
||||||
memoryUsage = append(memoryUsage, rep.MemoryUsageMiB)
|
memoryUsage = append(memoryUsage, rep.MemoryUsageMiB*(1<<20))
|
||||||
}
|
}
|
||||||
if rep.SHA256Perf > 0 {
|
if rep.SHA256Perf > 0 {
|
||||||
sha256Perf = append(sha256Perf, rep.SHA256Perf)
|
sha256Perf = append(sha256Perf, rep.SHA256Perf*(1<<20))
|
||||||
}
|
}
|
||||||
if rep.MemorySize > 0 {
|
if rep.MemorySize > 0 {
|
||||||
memorySize = append(memorySize, rep.MemorySize)
|
memorySize = append(memorySize, rep.MemorySize*(1<<20))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var categories []category
|
||||||
|
categories = append(categories, category{
|
||||||
|
Values: statsForInts(totFiles),
|
||||||
|
Descr: "Files Managed per Node",
|
||||||
|
})
|
||||||
|
|
||||||
|
categories = append(categories, category{
|
||||||
|
Values: statsForInts(maxFiles),
|
||||||
|
Descr: "Files in Largest Repo",
|
||||||
|
})
|
||||||
|
|
||||||
|
categories = append(categories, category{
|
||||||
|
Values: statsForInts(totMiB),
|
||||||
|
Descr: "Data Managed per Node",
|
||||||
|
Unit: "B",
|
||||||
|
Binary: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
categories = append(categories, category{
|
||||||
|
Values: statsForInts(maxMiB),
|
||||||
|
Descr: "Data in Largest Repo",
|
||||||
|
Unit: "B",
|
||||||
|
Binary: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
categories = append(categories, category{
|
||||||
|
Values: statsForInts(numNodes),
|
||||||
|
Descr: "Number of Nodes in Cluster",
|
||||||
|
})
|
||||||
|
|
||||||
|
categories = append(categories, category{
|
||||||
|
Values: statsForInts(numRepos),
|
||||||
|
Descr: "Number of Repositories Configured",
|
||||||
|
})
|
||||||
|
|
||||||
|
categories = append(categories, category{
|
||||||
|
Values: statsForInts(memoryUsage),
|
||||||
|
Descr: "Memory Usage",
|
||||||
|
Unit: "B",
|
||||||
|
Binary: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
categories = append(categories, category{
|
||||||
|
Values: statsForInts(memorySize),
|
||||||
|
Descr: "System Memory",
|
||||||
|
Unit: "B",
|
||||||
|
Binary: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
categories = append(categories, category{
|
||||||
|
Values: statsForFloats(sha256Perf),
|
||||||
|
Descr: "SHA-256 Hashing Performance",
|
||||||
|
Unit: "B/s",
|
||||||
|
Binary: true,
|
||||||
|
})
|
||||||
|
|
||||||
r := make(map[string]interface{})
|
r := make(map[string]interface{})
|
||||||
r["key"] = key
|
r["key"] = key
|
||||||
r["nodes"] = nodes
|
r["nodes"] = nodes
|
||||||
@ -293,85 +322,12 @@ func getReport(key string) map[string]interface{} {
|
|||||||
r["versions"] = analyticsFor(versions)
|
r["versions"] = analyticsFor(versions)
|
||||||
r["platforms"] = analyticsFor(platforms)
|
r["platforms"] = analyticsFor(platforms)
|
||||||
r["os"] = analyticsFor(oses)
|
r["os"] = analyticsFor(oses)
|
||||||
r["numRepos"] = statsForInts(numRepos)
|
|
||||||
r["numNodes"] = statsForInts(numNodes)
|
|
||||||
r["totFiles"] = statsForInts(totFiles)
|
|
||||||
r["maxFiles"] = statsForInts(maxFiles)
|
|
||||||
r["totMiB"] = statsForInts(totMiB)
|
|
||||||
r["maxMiB"] = statsForInts(maxMiB)
|
|
||||||
r["memoryUsage"] = statsForInts(memoryUsage)
|
|
||||||
r["sha256Perf"] = statsForFloats(sha256Perf)
|
|
||||||
r["memorySize"] = statsForInts(memorySize)
|
|
||||||
|
|
||||||
reportCache = r
|
reportCache = r
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
type analytic struct {
|
|
||||||
Key string
|
|
||||||
Count int
|
|
||||||
Percentage float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type analyticList []analytic
|
|
||||||
|
|
||||||
func (l analyticList) Less(a, b int) bool {
|
|
||||||
return l[b].Count < l[a].Count // inverse
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l analyticList) Swap(a, b int) {
|
|
||||||
l[a], l[b] = l[b], l[a]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l analyticList) Len() int {
|
|
||||||
return len(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a list of frequency analytics for a given list of strings.
|
|
||||||
func analyticsFor(ss []string) []analytic {
|
|
||||||
m := make(map[string]int)
|
|
||||||
t := 0
|
|
||||||
for _, s := range ss {
|
|
||||||
m[s]++
|
|
||||||
t++
|
|
||||||
}
|
|
||||||
|
|
||||||
l := make([]analytic, 0, len(m))
|
|
||||||
for k, c := range m {
|
|
||||||
l = append(l, analytic{k, c, 100 * float64(c) / float64(t)})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(analyticList(l))
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func statsForInts(data []int) map[string]int {
|
|
||||||
sort.Ints(data)
|
|
||||||
res := make(map[string]int, 4)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
res["fp"] = data[int(float64(len(data))*0.05)]
|
|
||||||
res["med"] = data[len(data)/2]
|
|
||||||
res["nfp"] = data[int(float64(len(data))*0.95)]
|
|
||||||
res["max"] = data[len(data)-1]
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func statsForFloats(data []float64) map[string]float64 {
|
|
||||||
sort.Float64s(data)
|
|
||||||
res := make(map[string]float64, 4)
|
|
||||||
if len(data) == 0 {
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
res["fp"] = data[int(float64(len(data))*0.05)]
|
|
||||||
res["med"] = data[len(data)/2]
|
|
||||||
res["nfp"] = data[int(float64(len(data))*0.95)]
|
|
||||||
res["max"] = data[len(data)-1]
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureDir(dir string, mode int) {
|
func ensureDir(dir string, mode int) {
|
||||||
fi, err := os.Stat(dir)
|
fi, err := os.Stat(dir)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@ -395,28 +351,6 @@ func transformVersion(v string) string {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
// commatize returns a number with sep as thousands separators. Handles
|
|
||||||
// integers and plain floats.
|
|
||||||
func commatize(sep, s string) string {
|
|
||||||
var b bytes.Buffer
|
|
||||||
fs := strings.SplitN(s, ".", 2)
|
|
||||||
|
|
||||||
l := len(fs[0])
|
|
||||||
for i := range fs[0] {
|
|
||||||
b.Write([]byte{s[i]})
|
|
||||||
if i < l-1 && (l-i)%3 == 1 {
|
|
||||||
b.WriteString(sep)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fs) > 1 && len(fs[1]) > 0 {
|
|
||||||
b.WriteString(".")
|
|
||||||
b.WriteString(fs[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// timestamp returns a time stamp for the current hour, to be used as a cache key
|
// timestamp returns a time stamp for the current hour, to be used as a cache key
|
||||||
func timestamp() string {
|
func timestamp() string {
|
||||||
return time.Now().Format("20060102T15")
|
return time.Now().Format("20060102T15")
|
||||||
|
@ -30,7 +30,7 @@ found in the LICENSE file.
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h1>Syncthing Usage Data</h1>
|
<h1>Syncthing Usage Data</h1>
|
||||||
<p>
|
<p>
|
||||||
This is the aggregated usage report data for today and yesterday. Data based on <b>{{.nodes}}</b> nodes that have reported in.
|
This is the aggregated usage report data for the last 24 hours. Data based on <b>{{.nodes}}</b> nodes that have reported in.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
@ -40,13 +40,13 @@ found in the LICENSE file.
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range $cat := .categories}}
|
{{range .categories}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{$cat.Descr}}</td>
|
<td>{{.Descr}}</td>
|
||||||
<td class="text-right">{{index $ $cat.Key "fp" | printf "%v" | commatize " "}} {{$cat.Unit}}</td>
|
<td class="text-right">{{index .Values 0 | number .Binary | commatize " "}}{{.Unit}}</td>
|
||||||
<td class="text-right">{{index $ $cat.Key "med" | printf "%v" | commatize " "}} {{$cat.Unit}}</td>
|
<td class="text-right">{{index .Values 1 | number .Binary | commatize " "}}{{.Unit}}</td>
|
||||||
<td class="text-right">{{index $ $cat.Key "nfp" | printf "%v" | commatize " "}} {{$cat.Unit}}</td>
|
<td class="text-right">{{index .Values 2 | number .Binary | commatize " "}}{{.Unit}}</td>
|
||||||
<td class="text-right">{{index $ $cat.Key "max" | printf "%v" | commatize " "}} {{$cat.Unit}}</td>
|
<td class="text-right">{{index .Values 3 | number .Binary | commatize " "}}{{.Unit}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user