mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-07 09:05:55 +00:00
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:
parent
5e0413eac1
commit
5ee87bbf7c
4 changed files with 187 additions and 8 deletions
19
api.go
19
api.go
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue