add server stats to /info endpoint

report the approximate hardware specs (CPU speed, cores, memory)
of the server in the /info endpoint. This may be useful when
benchmarking.

We do some workarounds because gopsutil's core count output is
confusingly different between Linux and Darwin, and the MHz output
is usually wrong on Linux. Intel's app notes say to just parse
the model string. Whyyyyyyy.
This commit is contained in:
Seebs 2019-02-11 13:50:24 -06:00
parent 5e0413eac1
commit 5ee87bbf7c
4 changed files with 187 additions and 8 deletions

19
api.go
View file

@ -1177,8 +1177,18 @@ func (api *API) Version() string {
// Info returns information about this server instance
func (api *API) Info() serverInfo {
si := api.server.systemInfo
// we don't report errors on failures to get this information
physicalCores, logicalCores, _ := si.CPUCores()
mhz, _ := si.CPUMHz()
mem, _ := si.MemTotal()
return serverInfo{
ShardWidth: ShardWidth,
ShardWidth: ShardWidth,
CPUPhysicalCores: physicalCores,
CPULogicalCores: logicalCores,
CPUMHz: mhz,
CPUType: si.CPUModel(),
Memory: mem,
}
}
@ -1213,7 +1223,12 @@ func (api *API) TranslateKeys(body io.Reader) ([]byte, error) {
}
type serverInfo struct {
ShardWidth uint64 `json:"shardWidth"`
ShardWidth uint64 `json:"shardWidth"`
Memory uint64 `json:"memory"`
CPUType string `json:"cpuType"`
CPUPhysicalCores int `json:"cpuPhysicalCores"`
CPULogicalCores int `json:"cpuLogicalCores"`
CPUMHz int `json:"cpuMHz"`
}
type apiMethod int

View file

@ -271,6 +271,9 @@ type SystemInfo interface {
MemFree() (uint64, error)
MemTotal() (uint64, error)
MemUsed() (uint64, error)
CPUModel() string
CPUCores() (physical int, logical int, err error)
CPUMHz() (int, error)
CPUArch() string
}
@ -327,3 +330,18 @@ func (n *nopSystemInfo) MemUsed() (uint64, error) {
func (n *nopSystemInfo) CPUArch() string {
return ""
}
// CPUModel returns the CPU model string
func (n *nopSystemInfo) CPUModel() string {
return "unknown"
}
// CPUMHz returns the CPU clock speed
func (n *nopSystemInfo) CPUMHz() (int, error) {
return 0, nil
}
// CPUCores returns the number of CPU cores (physical or logical)
func (n *nopSystemInfo) CPUCores() (physical, logical int, err error) {
return 0, 0, nil
}

View file

@ -16,8 +16,10 @@ package gopsutil
import (
"runtime"
"strings"
"github.com/pilosa/pilosa"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/host"
"github.com/shirou/gopsutil/mem"
)
@ -26,9 +28,13 @@ var _ pilosa.SystemInfo = NewSystemInfo()
// systemInfo is an implementation of pilosa.systemInfo that uses gopsutil to collect information about the host OS.
type systemInfo struct {
platform string
family string
osVersion string
platform string
family string
osVersion string
cpuModel string
cpuPhysicalCores int
cpuLogicalCores int
cpuMHz int
}
// Uptime returns the system uptime in seconds.
@ -40,6 +46,55 @@ func (s *systemInfo) Uptime() (uptime uint64, err error) {
return hostInfo.Uptime, nil
}
// cpuFrequencyMultipliers is a lookup table for prefixes on clock speeds.
// This is probably overkill.
var cpuFrequencyMultipliers = [256]int{
'M': 1,
'G': 1000,
'T': 1000 * 1000,
}
// computeHz determines the official rated speed of a CPU from its brand
// string. This insanity is *actually the official documented way to do
// this according to Intel*. There is also a cpuid leaf for the frequency,
// but I am not sure how supported it is, so.
func computeMHz(brandString string) int {
hz := strings.LastIndex(brandString, "Hz")
// ' 1MHz'
if hz < 3 {
return -1
}
multiplier := cpuFrequencyMultipliers[brandString[hz-1]]
if multiplier == 0 {
return -1
}
freq := 0
divisor := 0
decimalShift := 1
var i int
for i = hz - 2; i >= 0 && brandString[i] != ' '; i-- {
if brandString[i] >= '0' && brandString[i] <= '9' {
freq += int(brandString[i]-'0') * decimalShift
decimalShift *= 10
} else if brandString[i] == '.' {
if divisor != 0 {
return -1
}
divisor = decimalShift
} else {
return -1
}
}
// we didn't find a space
if i < 0 {
return -1
}
if divisor != 0 {
return (freq * multiplier) / divisor
}
return freq * multiplier
}
// collectPlatformInfo fetches and caches system platform information.
func (s *systemInfo) collectPlatformInfo() error {
var err error
@ -49,6 +104,45 @@ func (s *systemInfo) collectPlatformInfo() error {
return err
}
}
if s.cpuModel == "" {
infos, err := cpu.Info()
if err != nil || len(infos) == 0 {
s.cpuModel = "unknown"
// if err is nil, but we got no infos, we don't
// have a meaningful error to return.
return err
}
s.cpuModel = infos[0].ModelName
s.cpuMHz = computeMHz(s.cpuModel)
// gopsutil reports core and clock speed info inconsistently
// by OS
switch runtime.GOOS {
case "linux":
// Each reported "CPU" is a logical core. Some cores may
// have the same Core ID, which is a strictly numeric
// value which gopsutil returned as a string, which
// indicates that they're hyperthreading or similar things
// on the same physical core.
uniqueCores := make(map[string]struct{}, len(infos))
totalCores := 0
for _, info := range infos {
uniqueCores[info.CoreID] = struct{}{}
totalCores += int(info.Cores)
}
s.cpuPhysicalCores = len(uniqueCores)
s.cpuLogicalCores = totalCores
case "darwin":
fallthrough
default: // let's hope other systems give useful core info?
s.cpuPhysicalCores = int(infos[0].Cores)
// we have no way to know, let's try runtime
s.cpuLogicalCores = runtime.NumCPU()
}
if err != nil {
return err
}
}
return nil
}
@ -116,6 +210,33 @@ func (s *systemInfo) CPUArch() string {
return runtime.GOARCH
}
// CPUModel returns the CPU model string
func (s *systemInfo) CPUModel() string {
err := s.collectPlatformInfo()
if err != nil {
return "unknown"
}
return s.cpuModel
}
// CPUMhz returns the CPU clock speed
func (s *systemInfo) CPUMHz() (int, error) {
err := s.collectPlatformInfo()
if err != nil {
return 0, err
}
return s.cpuMHz, nil
}
// CPUCores returns the number of (physical or logical) CPU cores
func (s *systemInfo) CPUCores() (physical, logical int, err error) {
err = s.collectPlatformInfo()
if err != nil {
return 0, 0, err
}
return s.cpuPhysicalCores, s.cpuLogicalCores, nil
}
// NewSystemInfo is a constructor for the gopsutil implementation of SystemInfo.
func NewSystemInfo() *systemInfo {
return &systemInfo{}

View file

@ -19,7 +19,6 @@ import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
gohttp "net/http"
@ -56,8 +55,34 @@ func TestHandler_Endpoints(t *testing.T) {
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/info", nil))
if w.Code != gohttp.StatusOK {
t.Fatalf("unexpected status code: %d", w.Code)
} else if body := w.Body.String(); body != fmt.Sprintf("{\"shardWidth\":%d}\n", pilosa.ShardWidth) {
t.Fatalf("unexpected body: %s", body)
}
var details map[string]interface{}
body := w.Body.Bytes()
err := json.Unmarshal(body, &details)
if err != nil {
t.Fatalf("error unmarshalling json body [%s]: %v", body, err)
}
sw := details["shardWidth"]
if sw == nil {
t.Fatalf("no shardWidth in json body [%s]", body)
}
var n float64
var ok bool
if n, ok = sw.(float64); !ok {
t.Fatalf("shardWidth not float64 (%T) in json body [%s]", sw, body)
}
if uint64(n) != pilosa.ShardWidth {
t.Fatalf("incorrect shard width: got %d, expected %d", uint64(n), pilosa.ShardWidth)
}
count := details["cpuPhysicalCores"]
if count == nil {
t.Fatalf("no cpuPhysicalCores in json body [%s]", body)
}
if n, ok = count.(float64); !ok {
t.Fatalf("cpuPhysicalCores not float64 (%T) in json body [%s]", count, body)
}
if int(n) == 0 {
t.Fatal("cpu count should not be 0")
}
})