diff --git a/api.go b/api.go index fc4ca41c8..19e41732d 100644 --- a/api.go +++ b/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 diff --git a/diagnostics.go b/diagnostics.go index 1eb3f2606..1f9466772 100644 --- a/diagnostics.go +++ b/diagnostics.go @@ -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 +} diff --git a/gopsutil/systeminfo.go b/gopsutil/systeminfo.go index 290e1278c..b50885241 100644 --- a/gopsutil/systeminfo.go +++ b/gopsutil/systeminfo.go @@ -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{} diff --git a/server/handler_test.go b/server/handler_test.go index b8e4e6c4f..701d019ae 100644 --- a/server/handler_test.go +++ b/server/handler_test.go @@ -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") } })