From 22cee3a89c698ddba523a3c6b922a6c54a346401 Mon Sep 17 00:00:00 2001 From: Rohit Jnagal Date: Sat, 20 Dec 2014 00:49:49 +0000 Subject: [PATCH] Add cpu topology information to machine endpoint. --- info/machine.go | 40 +++++++ manager/machine.go | 108 +++++++++++++++-- manager/testdata/cpuinfo | 251 +++++++++++++++++++++++++++++++++++++++ manager/topology_test.go | 83 +++++++++++++ 4 files changed, 470 insertions(+), 12 deletions(-) create mode 100644 manager/testdata/cpuinfo create mode 100644 manager/topology_test.go diff --git a/info/machine.go b/info/machine.go index 54f4dd7a..2171c6d9 100644 --- a/info/machine.go +++ b/info/machine.go @@ -22,6 +22,42 @@ type FsInfo struct { Capacity uint64 `json:"capacity"` } +type Node struct { + Id int `json:"node_id"` + Cores []Core `json:"cores"` +} + +type Core struct { + Id int `json:"core_id"` + Threads []int `json:"thread_ids"` +} + +func (self *Node) FindCore(id int) (bool, int) { + for i, n := range self.Cores { + if n.Id == id { + return true, i + } + } + return false, -1 +} + +func (self *Node) AddThread(thread int, core int) { + var coreIdx int + if core == -1 { + // Assume one hyperthread per core when topology data is missing. + core = thread + } + ok, coreIdx := self.FindCore(core) + + if !ok { + // New core + core := Core{Id: core} + self.Cores = append(self.Cores, core) + coreIdx = len(self.Cores) - 1 + } + self.Cores[coreIdx].Threads = append(self.Cores[coreIdx].Threads, thread) +} + type DiskInfo struct { // device name Name string `json:"name"` @@ -51,6 +87,10 @@ type MachineInfo struct { // Disk map DiskMap map[string]DiskInfo `json:"disk_map"` + + // Machine Topology + // Describes cpu layout and hierarchy. TODO(rjnagal): Add Memory hierarchy. + Topology []Node `json:"topology"` } type VersionInfo struct { diff --git a/manager/machine.go b/manager/machine.go index 9e92d125..a6b98434 100644 --- a/manager/machine.go +++ b/manager/machine.go @@ -31,7 +31,9 @@ import ( "github.com/google/cadvisor/utils/sysfs" ) -var numCpuRegexp = regexp.MustCompile("processor\\t*: +[0-9]+") +var cpuRegExp = regexp.MustCompile("processor\\t*: +([0-9]+)") +var coreRegExp = regexp.MustCompile("core id\\t*: +([0-9]+)") +var nodeRegExp = regexp.MustCompile("physical id\\t*: +([0-9]+)") var CpuClockSpeedMHz = regexp.MustCompile("cpu MHz\\t*: +([0-9]+.[0-9]+)") var memoryCapacityRegexp = regexp.MustCompile("MemTotal: *([0-9]+) kB") @@ -63,23 +65,99 @@ func getClockSpeed(procInfo []byte) (uint64, error) { return uint64(speed * 1000), nil } +func extractValue(s string, r *regexp.Regexp) (bool, int, error) { + matches := r.FindSubmatch([]byte(s)) + if len(matches) == 2 { + val, err := strconv.ParseInt(string(matches[1]), 10, 32) + if err != nil { + return true, -1, err + } + return true, int(val), nil + } + return false, -1, nil +} + +func findNode(nodes []info.Node, id int) (bool, int) { + for i, n := range nodes { + if n.Id == id { + return true, i + } + } + return false, -1 +} + +func addNode(nodes *[]info.Node, id int) int { + var idx int + if id == -1 { + // Some VMs don't fill topology data. Export single package. + id = 0 + } + + ok, idx := findNode(*nodes, id) + if !ok { + // New node + node := info.Node{Id: id} + *nodes = append(*nodes, node) + idx = len(*nodes) - 1 + } + return idx +} + +func getTopology(cpuinfo string) ([]info.Node, int, error) { + nodes := []info.Node{} + numCores := 0 + lastThread := -1 + lastCore := -1 + lastNode := -1 + for _, line := range strings.Split(cpuinfo, "\n") { + ok, val, err := extractValue(line, cpuRegExp) + if err != nil { + return nil, -1, fmt.Errorf("could not parse cpu info from %q: %s", line, err) + } + if ok { + thread := val + numCores++ + if lastThread != -1 { + // New cpu section. Save last one. + nodeIdx := addNode(&nodes, lastNode) + nodes[nodeIdx].AddThread(lastThread, lastCore) + lastCore = -1 + lastNode = -1 + } + lastThread = thread + } + ok, val, err = extractValue(line, coreRegExp) + if err != nil { + return nil, -1, fmt.Errorf("could not parse core info from %q: %s", line, err) + } + if ok { + lastCore = val + } + ok, val, err = extractValue(line, nodeRegExp) + if err != nil { + return nil, -1, fmt.Errorf("could not parse node info from %q: %s", line, err) + } + if ok { + lastNode = val + } + } + nodeIdx := addNode(&nodes, lastNode) + nodes[nodeIdx].AddThread(lastThread, lastCore) + if numCores < 1 { + return nil, numCores, fmt.Errorf("could not detect any cores") + } + return nodes, numCores, nil +} + func getMachineInfo(sysFs sysfs.SysFs) (*info.MachineInfo, error) { - // Get the number of CPUs from /proc/cpuinfo. - out, err := ioutil.ReadFile("/proc/cpuinfo") - if err != nil { - return nil, err - } - numCores := len(numCpuRegexp.FindAll(out, -1)) - if numCores == 0 { - return nil, fmt.Errorf("failed to count cores in output: %s", string(out)) - } - clockSpeed, err := getClockSpeed(out) + cpuinfo, err := ioutil.ReadFile("/proc/cpuinfo") + clockSpeed, err := getClockSpeed(cpuinfo) if err != nil { return nil, err } // Get the amount of usable memory from /proc/meminfo. - out, err = ioutil.ReadFile("/proc/meminfo") + out, err := ioutil.ReadFile("/proc/meminfo") if err != nil { return nil, err } @@ -109,11 +187,17 @@ func getMachineInfo(sysFs sysfs.SysFs) (*info.MachineInfo, error) { return nil, err } + topology, numCores, err := getTopology(string(cpuinfo)) + if err != nil { + return nil, err + } + machineInfo := &info.MachineInfo{ NumCores: numCores, CpuFrequency: clockSpeed, MemoryCapacity: memoryCapacity, DiskMap: diskMap, + Topology: topology, } for _, fs := range filesystems { diff --git a/manager/testdata/cpuinfo b/manager/testdata/cpuinfo new file mode 100644 index 00000000..ca2b722a --- /dev/null +++ b/manager/testdata/cpuinfo @@ -0,0 +1,251 @@ +processor : 0 +cpu family : 6 +stepping : 2 +microcode : 0x10 +cpu MHz : 1596.000 +cache size : 12288 KB +physical id : 0 +siblings : 6 +core id : 0 +cpu cores : 6 +apicid : 0 +initial apicid : 0 +fpu : yes +fpu_exception : yes +cpuid level : 11 +wp : yes +bogomips : 5333.60 +clflush size : 64 +cache_alignment : 64 +address sizes : 40 bits physical, 48 bits virtual + +processor : 1 +cpu family : 6 +stepping : 2 +microcode : 0x10 +cpu MHz : 1596.000 +cache size : 12288 KB +physical id : 0 +siblings : 6 +core id : 1 +cpu cores : 6 +apicid : 2 +initial apicid : 2 +fpu : yes +fpu_exception : yes +cpuid level : 11 +wp : yes +bogomips : 5333.60 +clflush size : 64 +cache_alignment : 64 +address sizes : 40 bits physical, 48 bits virtual + +processor : 2 +cpu family : 6 +stepping : 2 +microcode : 0x10 +cpu MHz : 1596.000 +cache size : 12288 KB +physical id : 0 +siblings : 6 +core id : 2 +cpu cores : 6 +apicid : 4 +initial apicid : 4 +fpu : yes +fpu_exception : yes +cpuid level : 11 +wp : yes +bogomips : 5333.60 +clflush size : 64 +cache_alignment : 64 +address sizes : 40 bits physical, 48 bits virtual + +processor : 3 +cpu family : 6 +stepping : 2 +microcode : 0x10 +cpu MHz : 1596.000 +cache size : 12288 KB +physical id : 1 +siblings : 6 +core id : 3 +cpu cores : 6 +apicid : 16 +initial apicid : 16 +fpu : yes +fpu_exception : yes +cpuid level : 11 +wp : yes +bogomips : 5333.60 +clflush size : 64 +cache_alignment : 64 +address sizes : 40 bits physical, 48 bits virtual + +processor : 4 +cpu family : 6 +stepping : 2 +microcode : 0x10 +cpu MHz : 1596.000 +cache size : 12288 KB +physical id : 1 +siblings : 6 +core id : 4 +cpu cores : 6 +apicid : 18 +initial apicid : 18 +fpu : yes +fpu_exception : yes +cpuid level : 11 +wp : yes +bogomips : 5333.60 +clflush size : 64 +cache_alignment : 64 +address sizes : 40 bits physical, 48 bits virtual + +processor : 5 +cpu family : 6 +stepping : 2 +microcode : 0x10 +cpu MHz : 1596.000 +cache size : 12288 KB +physical id : 1 +siblings : 6 +core id : 5 +cpu cores : 6 +apicid : 20 +initial apicid : 20 +fpu : yes +fpu_exception : yes +cpuid level : 11 +wp : yes +bogomips : 5333.60 +clflush size : 64 +cache_alignment : 64 +address sizes : 40 bits physical, 48 bits virtual + +processor : 6 +cpu family : 6 +stepping : 2 +microcode : 0x10 +cpu MHz : 2661.000 +cache size : 12288 KB +physical id : 0 +siblings : 6 +core id : 0 +cpu cores : 6 +apicid : 1 +initial apicid : 1 +fpu : yes +fpu_exception : yes +cpuid level : 11 +wp : yes +bogomips : 5333.60 +clflush size : 64 +cache_alignment : 64 +address sizes : 40 bits physical, 48 bits virtual + +processor : 7 +cpu family : 6 +stepping : 2 +microcode : 0x10 +cpu MHz : 2661.000 +cache size : 12288 KB +physical id : 0 +siblings : 6 +core id : 1 +cpu cores : 6 +apicid : 3 +initial apicid : 3 +fpu : yes +fpu_exception : yes +cpuid level : 11 +wp : yes +bogomips : 5333.60 +clflush size : 64 +cache_alignment : 64 +address sizes : 40 bits physical, 48 bits virtual + +processor : 8 +cpu family : 6 +stepping : 2 +microcode : 0x10 +cpu MHz : 1596.000 +cache size : 12288 KB +physical id : 0 +siblings : 6 +core id : 2 +cpu cores : 6 +apicid : 5 +initial apicid : 5 +fpu : yes +fpu_exception : yes +cpuid level : 11 +wp : yes +bogomips : 5333.60 +clflush size : 64 +cache_alignment : 64 +address sizes : 40 bits physical, 48 bits virtual + +processor : 9 +cpu family : 6 +stepping : 2 +microcode : 0x10 +cpu MHz : 2661.000 +cache size : 12288 KB +physical id : 1 +siblings : 6 +core id : 3 +cpu cores : 6 +apicid : 17 +initial apicid : 17 +fpu : yes +fpu_exception : yes +cpuid level : 11 +wp : yes +bogomips : 5333.60 +clflush size : 64 +cache_alignment : 64 +address sizes : 40 bits physical, 48 bits virtual + +processor : 10 +cpu family : 6 +stepping : 2 +microcode : 0x10 +cpu MHz : 1596.000 +cache size : 12288 KB +physical id : 1 +siblings : 6 +core id : 4 +cpu cores : 6 +apicid : 19 +initial apicid : 19 +fpu : yes +fpu_exception : yes +cpuid level : 11 +wp : yes +bogomips : 5333.60 +clflush size : 64 +cache_alignment : 64 +address sizes : 40 bits physical, 48 bits virtual +processor : 11 +cpu family : 6 +stepping : 2 +microcode : 0x10 +cpu MHz : 2661.000 +cache size : 12288 KB +physical id : 1 +siblings : 6 +core id : 5 +cpu cores : 6 +apicid : 21 +initial apicid : 21 +fpu : yes +fpu_exception : yes +cpuid level : 11 +wp : yes +bogomips : 5333.60 +clflush size : 64 +cache_alignment : 64 +address sizes : 40 bits physical, 48 bits virtual + diff --git a/manager/topology_test.go b/manager/topology_test.go new file mode 100644 index 00000000..75d18f72 --- /dev/null +++ b/manager/topology_test.go @@ -0,0 +1,83 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package manager + +import ( + "io/ioutil" + "reflect" + "testing" + + "github.com/google/cadvisor/info" +) + +func TestTopology(t *testing.T) { + testfile := "./testdata/cpuinfo" + testcpuinfo, err := ioutil.ReadFile(testfile) + if err != nil { + t.Fatalf("unable to read input test file %s", testfile) + } + topology, numCores, err := getTopology(string(testcpuinfo)) + if err != nil { + t.Errorf("failed to get topology for sample cpuinfo %s", string(testcpuinfo)) + } + + if numCores != 12 { + t.Errorf("Expected 12 cores, found %d", numCores) + } + expected_topology := []info.Node{} + numNodes := 2 + numCoresPerNode := 3 + numThreads := 2 + for i := 0; i < numNodes; i++ { + node := info.Node{Id: i} + for j := 0; j < numCoresPerNode; j++ { + core := info.Core{Id: i*numCoresPerNode + j} + for k := 0; k < numThreads; k++ { + core.Threads = append(core.Threads, k*numCoresPerNode*numNodes+core.Id) + } + node.Cores = append(node.Cores, core) + } + expected_topology = append(expected_topology, node) + } + + if !reflect.DeepEqual(topology, expected_topology) { + t.Errorf("Expected topology %+v, got %+v", expected_topology, topology) + } +} + +func TestTopologyWithSimpleCpuinfo(t *testing.T) { + topology, numCores, err := getTopology("processor\t: 0\n") + if err != nil { + t.Errorf("Expected cpuinfo with no topology data to succeed.") + } + node := info.Node{Id: 0} + core := info.Core{Id: 0} + core.Threads = append(core.Threads, 0) + node.Cores = append(node.Cores, core) + expected := []info.Node{node} + if !reflect.DeepEqual(topology, expected) { + t.Errorf("Expected topology %+v, got %+v", expected, topology) + } + if numCores != 1 { + t.Errorf("Expected 1 core, found %d", numCores) + } +} + +func TestTopologyEmptyCpuinfo(t *testing.T) { + _, _, err := getTopology("") + if err == nil { + t.Errorf("Expected empty cpuinfo to fail.") + } +}