diff --git a/go.mod b/go.mod index ee1aaafbe..c85cdf898 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( github.com/google/go-querystring v1.2.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect - github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/klauspost/cpuid/v2 v2.4.1 // indirect github.com/lucasb-eyer/go-colorful v1.4.1 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.24 // indirect diff --git a/go.sum b/go.sum index 35282824e..38863dfff 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,8 @@ github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dv github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= -github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= -github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/klauspost/cpuid/v2 v2.4.1 h1:I9KxadAkvo+QjtcEE2f2tyfEvmIla/T0m+14PrZNl0U= +github.com/klauspost/cpuid/v2 v2.4.1/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= diff --git a/vendor/github.com/klauspost/cpuid/v2/cpuid.go b/vendor/github.com/klauspost/cpuid/v2/cpuid.go index db903f46a..741af8ac8 100644 --- a/vendor/github.com/klauspost/cpuid/v2/cpuid.go +++ b/vendor/github.com/klauspost/cpuid/v2/cpuid.go @@ -68,6 +68,8 @@ const ( THead Andes SpacemiT + Microchip + MIPS lastVendor ) @@ -477,6 +479,7 @@ func init() { // exported CPU variable. func Detect() { // Set defaults + topoSnapshot.Store(nil) CPU.ThreadsPerCore = 1 CPU.Cache.L1I = -1 CPU.Cache.L1D = -1 @@ -1223,10 +1226,14 @@ func (c *CPUInfo) cacheSize() { // 2 = Instruction Cache c.Cache.L1I = size } else { - if c.Cache.L1D < 0 { - c.Cache.L1I = size + // Unified cache; apply to whichever of L1D/L1I is still + // unset. L1D/L1I are zeroed for Intel and pre-populated + // from CPUID 0x80000005 for AMD/Hygon, so check for both + // the -1 sentinel and an unset zero value. + if c.Cache.L1D <= 0 { + c.Cache.L1D = size } - if c.Cache.L1I < 0 { + if c.Cache.L1I <= 0 { c.Cache.L1I = size } } @@ -1295,10 +1302,12 @@ func (c *CPUInfo) cacheSize() { // Inst cache c.Cache.L1I = size default: - if c.Cache.L1D < 0 { - c.Cache.L1I = size + // Unified cache; apply to whichever of L1D/L1I is still + // unset (see the Intel path for the <= 0 rationale). + if c.Cache.L1D <= 0 { + c.Cache.L1D = size } - if c.Cache.L1I < 0 { + if c.Cache.L1I <= 0 { c.Cache.L1I = size } } diff --git a/vendor/github.com/klauspost/cpuid/v2/featureid_string.go b/vendor/github.com/klauspost/cpuid/v2/featureid_string.go index f88ead130..c219be92c 100644 --- a/vendor/github.com/klauspost/cpuid/v2/featureid_string.go +++ b/vendor/github.com/klauspost/cpuid/v2/featureid_string.go @@ -356,12 +356,14 @@ func _() { _ = x[THead-33] _ = x[Andes-34] _ = x[SpacemiT-35] - _ = x[lastVendor-36] + _ = x[Microchip-36] + _ = x[MIPS-37] + _ = x[lastVendor-38] } -const _Vendor_name = "VendorUnknownIntelAMDVIATransmetaNSCKVMMSVMVMwareXenHVMBhyveHygonSiSRDCAmpereARMBroadcomCaviumDECFujitsuInfineonMotorolaNVIDIAAMCCQualcommMarvellQEMUQNXACRNSREAppleSiFiveStarFiveTHeadAndesSpacemiTlastVendor" +const _Vendor_name = "VendorUnknownIntelAMDVIATransmetaNSCKVMMSVMVMwareXenHVMBhyveHygonSiSRDCAmpereARMBroadcomCaviumDECFujitsuInfineonMotorolaNVIDIAAMCCQualcommMarvellQEMUQNXACRNSREAppleSiFiveStarFiveTHeadAndesSpacemiTMicrochipMIPSlastVendor" -var _Vendor_index = [...]uint8{0, 13, 18, 21, 24, 33, 36, 39, 43, 49, 55, 60, 65, 68, 71, 77, 80, 88, 94, 97, 104, 112, 120, 126, 130, 138, 145, 149, 152, 156, 159, 164, 170, 178, 183, 188, 196, 206} +var _Vendor_index = [...]uint8{0, 13, 18, 21, 24, 33, 36, 39, 43, 49, 55, 60, 65, 68, 71, 77, 80, 88, 94, 97, 104, 112, 120, 126, 130, 138, 145, 149, 152, 156, 159, 164, 170, 178, 183, 188, 196, 205, 209, 219} func (i Vendor) String() string { idx := int(i) - 0 diff --git a/vendor/github.com/klauspost/cpuid/v2/riscv_isa.go b/vendor/github.com/klauspost/cpuid/v2/riscv_isa.go index 5a87679bd..b4df7c4ff 100644 --- a/vendor/github.com/klauspost/cpuid/v2/riscv_isa.go +++ b/vendor/github.com/klauspost/cpuid/v2/riscv_isa.go @@ -78,10 +78,11 @@ func parseISAString(c *CPUInfo, isa string) { } var riscvVendorMap = map[uint64]Vendor{ - 0x489: SiFive, - 0x5b7: StarFive, - 0x5b1: THead, + 0x029: Microchip, + 0x127: MIPS, 0x31e: Andes, + 0x489: SiFive, + 0x5b7: THead, 0x710: SpacemiT, } diff --git a/vendor/github.com/klauspost/cpuid/v2/topology.go b/vendor/github.com/klauspost/cpuid/v2/topology.go new file mode 100644 index 000000000..957281f03 --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/v2/topology.go @@ -0,0 +1,480 @@ +// Copyright (c) 2026 Klaus Post, released under MIT License. See LICENSE file. + +package cpuid + +import ( + "cmp" + "math/bits" + "slices" + "sync/atomic" + + "github.com/klauspost/cpuid/v2/topology" +) + +// logicalCPU holds the topology-relevant CPUID data of a single logical +// processor. It is produced by parseLogicalCPU on whichever CPU the calling +// goroutine currently runs, so a full tree needs one per online CPU (see +// ScanTopology); a single one drives the extrapolated snapshot. +type logicalCPU struct { + apicID uint32 // (x2)APIC id + vendor Vendor // + smtShift uint8 // apicID>>smtShift == core id + pkgShift uint8 // apicID>>pkgShift == package id + l3Shift uint8 // apicID>>l3Shift == L3 (group) id; 0 if no L3 + nodeID int // AMD node id (leaf 0x8000001E ECX) + coreType topology.CoreType // + model string // + caches []cpuCache // +} + +// cpuCache pairs a decoded cache with the addressable number of logical +// processors that share it, which is what determines its sharing domain. +type cpuCache struct { + c topology.Cache + numShare int +} + +// topoSnapshot caches the extrapolated single-CPU topology; Detect resets it. +var topoSnapshot atomic.Pointer[topology.System] + +// Topology returns the CPU topology as a fixed hierarchy of +// System → Package → Group → Core → Thread. +// +// It is extrapolated from the current core's CPUID and the detected core +// counts, so counts and cache totals are accurate for homogeneous CPUs but +// individual cores of a hybrid CPU cannot be classified. For an exact, +// per-core tree (real efficiency/performance mapping and CCD membership) use +// ScanTopology, which briefly pins the current goroutine to each CPU. +func (c CPUInfo) Topology() *topology.System { + if t := topoSnapshot.Load(); t != nil { + return t + } + t := snapshotTopology() + topoSnapshot.Store(t) + return t +} + +// parseLogicalCPU reads the topology leaves of the current logical processor. +func parseLogicalCPU() logicalCPU { + var lc logicalCPU + lc.vendor, _ = vendorID() + mfi := maxFunctionID() + mef := maxExtendedFunction() + + // Extended topology enumeration: leaf 0x1F (V2) is preferred over 0x0B. + // EAX[4:0] is the right-shift of the x2APIC id (EDX) that yields the id of + // the next coarser level; ECX[15:8] is the level type (1=SMT, 2=Core, …). + topoLeaf := uint32(0) + if mfi >= 0x1f { + if _, ebx, _, _ := cpuidex(0x1f, 0); ebx&0xffff != 0 { + topoLeaf = 0x1f + } + } + if topoLeaf == 0 && mfi >= 0xb { + if _, ebx, _, _ := cpuidex(0xb, 0); ebx&0xffff != 0 { + topoLeaf = 0xb + } + } + switch { + case topoLeaf != 0: + var maxShift uint8 + for sub := range uint32(16) { + eax, ebx, ecx, edx := cpuidex(topoLeaf, sub) + if ebx&0xffff == 0 { + break + } + lc.apicID = edx + shift := uint8(eax & 0x1f) + if (ecx>>8)&0xff == 1 { // SMT level + lc.smtShift = shift + } + if shift > maxShift { + maxShift = shift + } + } + lc.pkgShift = maxShift + case mfi >= 1: + // Legacy 8-bit initial APIC id. + _, ebx, _, _ := cpuid(1) + lc.apicID = (ebx >> 24) & 0xff + lc.smtShift = ceilLog2(threadsPerCore()) + lc.pkgShift = 8 + } + + if lc.vendor == AMD || lc.vendor == Hygon { + if mef >= 0x8000001e { + eax, ebx, ecx, _ := cpuid(0x8000001e) + if topoLeaf == 0 { + lc.apicID = eax // Extended APIC id, authoritative on AMD. + } + // EBX[15:8]+1 == threads per core, but only on Zen (fam>=0x17) and + // Hygon (fam 0x18); pre-Zen "modules" (fam 0x15/0x16) are not SMT. + if fam, _, _ := familyModel(); lc.smtShift == 0 && fam >= 0x17 { + lc.smtShift = ceilLog2(int((ebx>>8)&0xff) + 1) + } + lc.nodeID = int(ecx & 0xff) + } + } + + lc.caches = parseCaches(lc.vendor, mfi, mef) + for _, cc := range lc.caches { + if cc.c.Level == 3 { + lc.l3Shift = ceilLog2(cc.numShare) + } + } + lc.coreType, lc.model = parseCoreType(lc.vendor, mfi) + return lc +} + +// parseCaches decodes the deterministic cache leaves (Intel 0x04 / AMD +// 0x8000001D). AMD only populates 0x8000001D when TopologyExtensions is set. +func parseCaches(vendor Vendor, mfi, mef uint32) []cpuCache { + var out []cpuCache + switch vendor { + case Intel: + if mfi < 4 { + return nil + } + for i := range uint32(16) { + eax, ebx, ecx, edx := cpuidex(4, i) + if eax&0xf == 0 { + break + } + out = append(out, decodeCache(eax, ebx, ecx, edx)) + } + case AMD, Hygon: + topext := false + if mef >= 0x80000001 { + _, _, ecx, _ := cpuid(0x80000001) + topext = ecx&(1<<22) != 0 + } + if mef >= 0x8000001d && topext { + for i := range uint32(16) { + eax, ebx, ecx, edx := cpuidex(0x8000001d, i) + if eax&0xf == 0 { + break + } + out = append(out, decodeCache(eax, ebx, ecx, edx)) + } + } + } + return out +} + +// decodeCache reads one subleaf of leaf 0x04 / 0x8000001D, which share layout. +func decodeCache(eax, ebx, ecx, edx uint32) cpuCache { + line := int(ebx&0xfff) + 1 + parts := int((ebx>>12)&0x3ff) + 1 + ways := int((ebx>>22)&0x3ff) + 1 + sets := int(ecx) + 1 + return cpuCache{ + c: topology.Cache{ + Level: int((eax >> 5) & 7), + Type: topology.CacheType(eax & 0xf), // 1=Data, 2=Instruction, 3=Unified + Size: line * parts * ways * sets, + LineSize: line, + Ways: ways, + Sets: sets, + Inclusive: edx&2 != 0, + }, + numShare: int((eax>>14)&0xfff) + 1, + } +} + +// parseCoreType classifies the current core from Intel's hybrid leaf 0x1A. +// The leaf reads 0 on non-hybrid Intel parts (so they fall through to +// performance), but all-efficiency parts populate it without the hybrid flag, +// so it is read whenever present rather than gated on that flag. +func parseCoreType(vendor Vendor, mfi uint32) (topology.CoreType, string) { + if vendor == Intel && mfi >= 0x1a { + switch eax, _, _, _ := cpuid(0x1a); eax >> 24 { + case 0x40: + return topology.Performance, "" + case 0x20: + return topology.Efficiency, "" + } + } + if vendor == Intel || vendor == AMD || vendor == Hygon { + return topology.Performance, "" + } + return topology.Unknown, "" +} + +// assembleTopology builds the exact tree from one record per logical CPU. +// Package/group/core membership is derived by masking the x2APIC id, and each +// cache instance is attached to the deepest node that contains every logical +// processor sharing it, so aggregate sizes never double-count. +func assembleTopology(cpus []logicalCPU) *topology.System { + sys := &topology.System{Scanned: true, Online: len(cpus)} + if len(cpus) == 0 { + return sys + } + sys.Vendor = cpus[0].vendor.String() + + groupShiftOf := func(lc logicalCPU) uint8 { + s := lc.l3Shift + if s == 0 { + s = lc.pkgShift // no L3: one group spans the package + } + if s < lc.smtShift { + s = lc.smtShift + } + if s == 0 { + s = 32 + } + return s + } + + type coreData struct { + typ topology.CoreType + model string + threads []topology.Thread + } + cores := map[uint32]*coreData{} + groupOf := map[uint32]uint32{} // coreKey -> groupKey + pkgOf := map[uint32]uint32{} // groupKey -> pkgKey + nodeOf := map[uint32]int{} // groupKey -> NUMA node + pkgSet := map[uint32]bool{} + var pkgKeys, groupKeys, coreKeys []uint32 + + for i, lc := range cpus { + pk := lc.apicID >> lc.pkgShift + gk := lc.apicID >> groupShiftOf(lc) + ck := lc.apicID >> lc.smtShift + cd := cores[ck] + if cd == nil { + cd = &coreData{typ: lc.coreType, model: lc.model} + cores[ck] = cd + coreKeys = append(coreKeys, ck) + groupOf[ck] = gk + } + if _, ok := pkgOf[gk]; !ok { + pkgOf[gk] = pk + groupKeys = append(groupKeys, gk) + } + nodeOf[gk] = lc.nodeID + if !pkgSet[pk] { + pkgSet[pk] = true + pkgKeys = append(pkgKeys, pk) + } + cd.threads = append(cd.threads, topology.Thread{APICID: lc.apicID, ID: i}) + } + + coreCaches, groupCaches, pkgCaches := attachCaches(cpus, groupShiftOf) + + slices.Sort(pkgKeys) + slices.Sort(groupKeys) + slices.Sort(coreKeys) + + coreID, groupID := 0, 0 + for pi, pk := range pkgKeys { + p := topology.Package{ID: pi, Caches: sortCaches(pkgCaches[pk])} + for _, gk := range groupKeys { + if pkgOf[gk] != pk { + continue + } + g := topology.Group{ID: groupID, NUMANode: nodeOf[gk], Caches: sortCaches(groupCaches[gk])} + groupID++ + for _, ck := range coreKeys { + if groupOf[ck] != gk { + continue + } + cd := cores[ck] + slices.SortFunc(cd.threads, func(a, b topology.Thread) int { return cmp.Compare(a.APICID, b.APICID) }) + g.Cores = append(g.Cores, topology.Core{ + ID: coreID, + Type: cd.typ, + Model: cd.model, + Threads: cd.threads, + Caches: sortCaches(coreCaches[ck]), + }) + coreID++ + } + p.Groups = append(p.Groups, g) + } + sys.Packages = append(sys.Packages, p) + } + return sys +} + +// attachCaches groups every reported cache into instances by sharing domain +// and returns them keyed by the core/group/package they belong to. +func attachCaches(cpus []logicalCPU, groupShiftOf func(logicalCPU) uint8) (core, group, pkg map[uint32][]topology.Cache) { + type cacheKey struct { + level int + typ topology.CacheType + dom uint32 + } + type cacheAgg struct { + c topology.Cache + cores map[uint32]bool + groups map[uint32]bool + pkgs map[uint32]bool + shared int + } + insts := map[cacheKey]*cacheAgg{} + for _, lc := range cpus { + pk := lc.apicID >> lc.pkgShift + gk := lc.apicID >> groupShiftOf(lc) + ck := lc.apicID >> lc.smtShift + for _, cc := range lc.caches { + key := cacheKey{cc.c.Level, cc.c.Type, lc.apicID >> ceilLog2(cc.numShare)} + a := insts[key] + if a == nil { + a = &cacheAgg{c: cc.c, cores: map[uint32]bool{}, groups: map[uint32]bool{}, pkgs: map[uint32]bool{}} + insts[key] = a + } + a.cores[ck], a.groups[gk], a.pkgs[pk] = true, true, true + a.shared++ + } + } + core, group, pkg = map[uint32][]topology.Cache{}, map[uint32][]topology.Cache{}, map[uint32][]topology.Cache{} + for _, a := range insts { + c := a.c + c.SharedThreads = a.shared + switch { + case len(a.cores) == 1: + k := onlyKey(a.cores) + core[k] = append(core[k], c) + case len(a.groups) == 1: + k := onlyKey(a.groups) + group[k] = append(group[k], c) + default: + k := onlyKey(a.pkgs) + pkg[k] = append(pkg[k], c) + } + } + return core, group, pkg +} + +// snapshotTopology extrapolates a homogeneous tree from the current core and +// the detected core counts, without pinning to other CPUs. +func snapshotTopology() *topology.System { + if cpuid == nil { + return countsTopology() // Non-x86: no CPUID, build from detected counts. + } + lc := parseLogicalCPU() + tpc := max(threadsPerCore(), 1) + physical := max(physicalCores(), 1) + logical := max(logicalCores(), physical*tpc) + + var l1d, l1i, l2, l3 *topology.Cache + l3Share := 0 + for i := range lc.caches { + cc := &lc.caches[i] + switch { + case cc.c.Level == 1 && cc.c.Type == topology.Instruction: + l1i = &cc.c + case cc.c.Level == 1: + l1d = &cc.c + case cc.c.Level == 2: + l2 = &cc.c + case cc.c.Level == 3: + l3 = &cc.c + l3Share = cc.numShare + } + } + + coresPerGroup := physical + if l3Share > 0 && l3Share/tpc > 0 { + coresPerGroup = l3Share / tpc + } + coresPerGroup = min(coresPerGroup, physical) + groups := 1 + if coresPerGroup > 0 { + groups = (physical + coresPerGroup - 1) / coresPerGroup + } + + sys := &topology.System{Vendor: lc.vendor.String(), Online: logical} + pkg := topology.Package{} + coreID, apic, remaining := 0, uint32(0), physical + for g := range groups { + grp := topology.Group{ID: g, NUMANode: lc.nodeID} + n := min(coresPerGroup, remaining) + for range n { + core := topology.Core{ID: coreID, Type: lc.coreType, Model: lc.model} + for range tpc { + core.Threads = append(core.Threads, topology.Thread{APICID: apic, ID: int(apic)}) + apic++ + } + addCache(&core.Caches, l1d, tpc) + addCache(&core.Caches, l1i, tpc) + addCache(&core.Caches, l2, tpc) + grp.Cores = append(grp.Cores, core) + coreID++ + } + addCache(&grp.Caches, l3, n*tpc) + remaining -= n + pkg.Groups = append(pkg.Groups, grp) + } + sys.Packages = []topology.Package{pkg} + return sys +} + +func addCache(dst *[]topology.Cache, c *topology.Cache, shared int) { + if c == nil || c.Size == 0 { + return + } + cc := *c + cc.SharedThreads = shared + *dst = append(*dst, cc) +} + +// countsTopology builds a minimal tree from the already-detected counts and +// cache sizes, used where CPUID is not available (non-x86, or noasm builds). +func countsTopology() *topology.System { + tpc := max(CPU.ThreadsPerCore, 1) + physical := CPU.PhysicalCores + if physical < 1 { + physical = max(1, CPU.LogicalCores/tpc) + } + sys := &topology.System{Vendor: CPU.VendorString, Online: CPU.LogicalCores} + var grp topology.Group + id := 0 + for c := range physical { + core := topology.Core{ID: c, Type: topology.Performance} + for range tpc { + core.Threads = append(core.Threads, topology.Thread{ID: id}) + id++ + } + addCacheVal(&core.Caches, 1, topology.Data, CPU.Cache.L1D, tpc) + addCacheVal(&core.Caches, 1, topology.Instruction, CPU.Cache.L1I, tpc) + addCacheVal(&core.Caches, 2, topology.Unified, CPU.Cache.L2, tpc) + grp.Cores = append(grp.Cores, core) + } + addCacheVal(&grp.Caches, 3, topology.Unified, CPU.Cache.L3, physical*tpc) + sys.Packages = []topology.Package{{Groups: []topology.Group{grp}}} + return sys +} + +func addCacheVal(dst *[]topology.Cache, level int, t topology.CacheType, size, shared int) { + if size <= 0 { + return + } + *dst = append(*dst, topology.Cache{Level: level, Type: t, Size: size, SharedThreads: shared}) +} + +func sortCaches(cs []topology.Cache) []topology.Cache { + slices.SortFunc(cs, func(a, b topology.Cache) int { + if a.Level != b.Level { + return cmp.Compare(a.Level, b.Level) + } + return cmp.Compare(a.Type, b.Type) + }) + return cs +} + +func onlyKey(m map[uint32]bool) uint32 { + for k := range m { + return k + } + return 0 +} + +// ceilLog2 returns the smallest s such that 1<= n. +func ceilLog2(n int) uint8 { + if n <= 1 { + return 0 + } + return uint8(bits.Len(uint(n - 1))) +} diff --git a/vendor/github.com/klauspost/cpuid/v2/topology/topology.go b/vendor/github.com/klauspost/cpuid/v2/topology/topology.go new file mode 100644 index 000000000..894d20b85 --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/v2/topology/topology.go @@ -0,0 +1,389 @@ +// Copyright (c) 2026 Klaus Post, released under MIT License. See LICENSE file. + +// Package topology describes the layout of a system's CPUs as a fixed +// hierarchy: System → Package → Group → Core → Thread. +// +// A Package is a socket (a physical "CPU"). A Group is a set of cores that +// share a last-level cache — an AMD CCX/CCD or an Intel LLC tile; there is +// always at least one, so a monolithic chip has a single Group holding the +// whole L3. A Core carries its type (performance/efficiency/…), its shared +// per-core caches and its logical Threads. +// +// Caches are attached to the level that owns them: L1/L2 on a Core, the +// shared L3 on a Group, a package-wide last-level cache on a Package. Because +// every instance is stored exactly once, the TotalCache aggregate never +// double-counts: System.TotalCache(3, Unified) is the combined L3 across all +// groups (e.g. all CCDs). +// +// The package holds types and queries only; it is filled in by the cpuid +// package and has no dependencies of its own. +package topology + +import ( + "fmt" + "strings" +) + +// CacheType identifies what a cache stores. The values match the type field +// of CPUID leaves 0x04 / 0x8000001D. +type CacheType uint8 + +const ( + Data CacheType = iota + 1 // Data cache + Instruction // Instruction cache + Unified // Unified cache (holds both) +) + +func (t CacheType) String() string { + switch t { + case Data: + return "Data" + case Instruction: + return "Instruction" + case Unified: + return "Unified" + } + return "Unknown" +} + +// MarshalText renders the cache type as its name, so it serializes as a string +// in JSON and other text encodings rather than a number. +func (t CacheType) MarshalText() ([]byte, error) { return []byte(t.String()), nil } + +// CoreType is the microarchitectural class of a core. On heterogeneous +// ("hybrid") CPUs this distinguishes performance from efficiency cores. +type CoreType uint8 + +const ( + Unknown CoreType = iota // Type could not be determined + Performance // Intel P-core (leaf 0x1A == 0x40), AMD classic Zen core + Efficiency // Intel E-core (leaf 0x1A == 0x20) + LPEfficiency // Intel low-power E-core on the SoC tile + Dense // AMD dense core (Zen 4c / 5c) +) + +func (t CoreType) String() string { + switch t { + case Performance: + return "Performance" + case Efficiency: + return "Efficiency" + case LPEfficiency: + return "LP-Efficiency" + case Dense: + return "Dense" + } + return "Unknown" +} + +// MarshalText renders the core type as its name for JSON and other text +// encodings. +func (t CoreType) MarshalText() ([]byte, error) { return []byte(t.String()), nil } + +// Cache describes a single cache instance. +type Cache struct { + Level int // 1, 2, 3, … + Type CacheType // Data, Instruction or Unified + Size int // Size in bytes of this instance + LineSize int // Line size in bytes + Ways int // Associativity + Sets int // Number of sets + SharedThreads int // Logical processors that share this instance + Inclusive bool // Inclusive of lower cache levels +} + +// Thread is a single logical processor. +type Thread struct { + APICID uint32 // (x2)APIC id + ID int // OS logical CPU number if known from a scan, else the enumeration index +} + +// Core is a single physical core and the threads it runs. +type Core struct { + ID int + Type CoreType + Model string // Microarchitecture label if known, e.g. "Golden Cove" + Threads []Thread // More than one indicates SMT / hyper-threading + Caches []Cache // Caches private to this core, typically L1 and L2 +} + +// NumThreads returns the number of logical processors on the core. +func (c *Core) NumThreads() int { return len(c.Threads) } + +// TotalCache returns the size in bytes of the core-private caches of the +// given level and type. +func (c *Core) TotalCache(level int, t CacheType) int { return sumCaches(c.Caches, level, t) } + +// Group is a set of cores sharing a last-level cache — an AMD CCX/CCD or an +// Intel LLC tile. There is always at least one per package. +type Group struct { + ID int + NUMANode int // AMD node id (leaf 0x8000001E ECX); 0 when unknown or single-node + Cores []Core // Cores in the group + Caches []Cache // Caches shared by the whole group, typically L3 +} + +// NumCores returns the number of cores in the group. +func (g *Group) NumCores() int { return len(g.Cores) } + +// NumThreads returns the number of logical processors below the group. +func (g *Group) NumThreads() int { return countThreads(g.Cores) } + +// CoresByType returns a count of cores per CoreType within the group. +func (g *Group) CoresByType() map[CoreType]int { return coresByType(g.Cores) } + +// TotalCache sums the caches of the given level and type at and below the +// group, counting each instance once. +func (g *Group) TotalCache(level int, t CacheType) int { + return sumCaches(g.Caches, level, t) + coresCache(g.Cores, level, t) +} + +// Package is a socket / physical CPU. +type Package struct { + ID int + Groups []Group + Caches []Cache // Package-wide caches, if any +} + +// NumCores returns the number of cores in the package. +func (p *Package) NumCores() int { + n := 0 + for i := range p.Groups { + n += len(p.Groups[i].Cores) + } + return n +} + +// NumThreads returns the number of logical processors in the package. +func (p *Package) NumThreads() int { + n := 0 + for i := range p.Groups { + n += p.Groups[i].NumThreads() + } + return n +} + +// CoresByType returns a count of cores per CoreType within the package. +func (p *Package) CoresByType() map[CoreType]int { + m := map[CoreType]int{} + for i := range p.Groups { + mergeCounts(m, p.Groups[i].Cores) + } + return m +} + +// Cores returns every core in the package. +func (p *Package) Cores() []Core { + var out []Core + for i := range p.Groups { + out = append(out, p.Groups[i].Cores...) + } + return out +} + +// TotalCache sums the caches of the given level and type at and below the +// package, counting each instance once. +func (p *Package) TotalCache(level int, t CacheType) int { + n := sumCaches(p.Caches, level, t) + for i := range p.Groups { + n += p.Groups[i].TotalCache(level, t) + } + return n +} + +// System is the root of the topology: the whole machine. +type System struct { + Vendor string // CPU vendor string + Scanned bool // true if built from a per-core scan, false if extrapolated from one core + Online int // Logical processors observed + Packages []Package // Physical packages / sockets +} + +// NumPackages returns the number of packages (sockets). +func (s *System) NumPackages() int { return len(s.Packages) } + +// NumCores returns the total number of physical cores in the system. +func (s *System) NumCores() int { + n := 0 + for i := range s.Packages { + n += s.Packages[i].NumCores() + } + return n +} + +// NumThreads returns the total number of logical processors in the system. +func (s *System) NumThreads() int { + n := 0 + for i := range s.Packages { + n += s.Packages[i].NumThreads() + } + return n +} + +// CoresByType returns a count of cores per CoreType across the system. +func (s *System) CoresByType() map[CoreType]int { + m := map[CoreType]int{} + for i := range s.Packages { + for j := range s.Packages[i].Groups { + mergeCounts(m, s.Packages[i].Groups[j].Cores) + } + } + return m +} + +// Cores returns every core in the system. +func (s *System) Cores() []Core { + var out []Core + for i := range s.Packages { + out = append(out, s.Packages[i].Cores()...) + } + return out +} + +// TotalCache sums the caches of the given level and type across the whole +// system, counting each instance once. For example TotalCache(3, Unified) +// returns the combined L3 across all groups. +func (s *System) TotalCache(level int, t CacheType) int { + n := 0 + for i := range s.Packages { + n += s.Packages[i].TotalCache(level, t) + } + return n +} + +func sumCaches(cs []Cache, level int, t CacheType) int { + n := 0 + for _, c := range cs { + if c.Level == level && c.Type == t { + n += c.Size + } + } + return n +} + +func coresCache(cs []Core, level int, t CacheType) int { + n := 0 + for i := range cs { + n += sumCaches(cs[i].Caches, level, t) + } + return n +} + +func countThreads(cs []Core) int { + n := 0 + for i := range cs { + n += len(cs[i].Threads) + } + return n +} + +func coresByType(cs []Core) map[CoreType]int { + m := map[CoreType]int{} + mergeCounts(m, cs) + return m +} + +func mergeCounts(m map[CoreType]int, cs []Core) { + for i := range cs { + m[cs[i].Type]++ + } +} + +// String renders the topology as an indented tree, followed by a summary. +func (s *System) String() string { + var b strings.Builder + kind := "extrapolated from one core" + if s.Scanned { + kind = "scanned" + } + fmt.Fprintf(&b, "System: %s (%s) — %d package(s), %d core(s), %d thread(s)\n", + s.Vendor, kind, s.NumPackages(), s.NumCores(), s.NumThreads()) + for pi := range s.Packages { + p := &s.Packages[pi] + fmt.Fprintf(&b, " Package #%d%s\n", p.ID, cacheSuffix(p.Caches)) + for gi := range p.Groups { + g := &p.Groups[gi] + numa := "" + if g.NUMANode != 0 { + numa = fmt.Sprintf(" NUMA#%d", g.NUMANode) + } + fmt.Fprintf(&b, " Group #%d%s%s\n", g.ID, numa, cacheSuffix(g.Caches)) + for ci := range g.Cores { + c := &g.Cores[ci] + model := "" + if c.Model != "" { + model = " " + c.Model + } + fmt.Fprintf(&b, " Core #%d (%s%s) threads:%s%s\n", + c.ID, c.Type, model, threadList(c.Threads), cacheSuffix(c.Caches)) + } + } + } + if types := s.CoresByType(); len(types) > 1 || !hasOnly(types, Performance) { + b.WriteString(" Cores by type:") + for _, t := range []CoreType{Performance, Efficiency, LPEfficiency, Dense, Unknown} { + if n := types[t]; n > 0 { + fmt.Fprintf(&b, " %s=%d", t, n) + } + } + b.WriteByte('\n') + } + fmt.Fprintf(&b, " Total cache: L1D %s, L1I %s, L2 %s, L3 %s\n", + humanBytes(s.TotalCache(1, Data)), humanBytes(s.TotalCache(1, Instruction)), + humanBytes(s.TotalCache(2, Unified)), humanBytes(s.TotalCache(3, Unified))) + return b.String() +} + +func hasOnly(m map[CoreType]int, t CoreType) bool { + for k := range m { + if k != t { + return false + } + } + return true +} + +func cacheSuffix(cs []Cache) string { + if len(cs) == 0 { + return "" + } + parts := make([]string, 0, len(cs)) + for _, c := range cs { + parts = append(parts, fmt.Sprintf("L%d%s %s", c.Level, cacheShort(c.Type), humanBytes(c.Size))) + } + return " [" + strings.Join(parts, " ") + "]" +} + +func cacheShort(t CacheType) string { + switch t { + case Data: + return "d" + case Instruction: + return "i" + } + return "" +} + +func threadList(ts []Thread) string { + var b strings.Builder + for i, t := range ts { + if i > 0 { + b.WriteByte(',') + } + fmt.Fprintf(&b, " %d", t.ID) + } + return b.String() +} + +func humanBytes(b int) string { + switch { + case b <= 0: + return "0" + case b%(1<<20) == 0: + return fmt.Sprintf("%dMB", b>>20) + case b%(1<<10) == 0: + return fmt.Sprintf("%dKB", b>>10) + default: + return fmt.Sprintf("%dB", b) + } +} diff --git a/vendor/github.com/klauspost/cpuid/v2/topology_scan.go b/vendor/github.com/klauspost/cpuid/v2/topology_scan.go new file mode 100644 index 000000000..4af6f2ad0 --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/v2/topology_scan.go @@ -0,0 +1,34 @@ +// Copyright (c) 2026 Klaus Post, released under MIT License. See LICENSE file. + +package cpuid + +import ( + "errors" + + "github.com/klauspost/cpuid/v2/topology" +) + +// ErrTopologyScanUnavailable is returned by ScanTopology when a per-core scan +// is not possible on the current platform, or CPU affinity could not be set. +// The returned topology is then the extrapolated snapshot from CPU.Topology. +var ErrTopologyScanUnavailable = errors.New("cpuid: per-core topology scan unavailable; returning extrapolated snapshot") + +// ScanTopology returns the exact CPU topology by reading CPUID on every online +// logical processor. +// +// To sample each processor it locks the calling goroutine to an OS thread and +// pins that thread to each CPU in turn, so it has transient scheduling side +// effects. Call it explicitly when you need a precise tree (real +// efficiency/performance core mapping, CCD membership); it is not run +// automatically. +// +// It is supported on linux/amd64 and windows/amd64. On any other platform, or +// if affinity cannot be set (e.g. a restrictive cgroup cpuset), it returns +// CPU.Topology() together with ErrTopologyScanUnavailable. +func ScanTopology() (*topology.System, error) { + cpus, err := scanLogicalCPUs() + if err != nil { + return CPU.Topology(), err + } + return assembleTopology(cpus), nil +} diff --git a/vendor/github.com/klauspost/cpuid/v2/topology_scan_linux_amd64.go b/vendor/github.com/klauspost/cpuid/v2/topology_scan_linux_amd64.go new file mode 100644 index 000000000..9e45365a9 --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/v2/topology_scan_linux_amd64.go @@ -0,0 +1,43 @@ +// Copyright (c) 2026 Klaus Post, released under MIT License. See LICENSE file. + +//go:build linux && amd64 && !noasm && !appengine && !gccgo + +package cpuid + +import ( + "runtime" + + "golang.org/x/sys/unix" +) + +// scanLogicalCPUs samples CPUID on every CPU in the calling thread's affinity +// mask by pinning the thread to each in turn. sched_setaffinity migrates the +// thread synchronously when its current CPU leaves the mask, so CPUID reflects +// the intended processor once the call returns. +func scanLogicalCPUs() ([]logicalCPU, error) { + var orig unix.CPUSet + if err := unix.SchedGetaffinity(0, &orig); err != nil { + return nil, ErrTopologyScanUnavailable + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + defer unix.SchedSetaffinity(0, &orig) + + var cpus []logicalCPU + for i := range len(orig) * 64 { + if !orig.IsSet(i) { + continue + } + var set unix.CPUSet + set.Set(i) + if unix.SchedSetaffinity(0, &set) != nil { + continue + } + cpus = append(cpus, parseLogicalCPU()) + } + if len(cpus) == 0 { + return nil, ErrTopologyScanUnavailable + } + return cpus, nil +} diff --git a/vendor/github.com/klauspost/cpuid/v2/topology_scan_stub.go b/vendor/github.com/klauspost/cpuid/v2/topology_scan_stub.go new file mode 100644 index 000000000..31c742462 --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/v2/topology_scan_stub.go @@ -0,0 +1,11 @@ +// Copyright (c) 2026 Klaus Post, released under MIT License. See LICENSE file. + +//go:build !amd64 || noasm || appengine || gccgo || (!linux && !windows) + +package cpuid + +// scanLogicalCPUs is unavailable on this platform; ScanTopology falls back to +// the extrapolated snapshot. +func scanLogicalCPUs() ([]logicalCPU, error) { + return nil, ErrTopologyScanUnavailable +} diff --git a/vendor/github.com/klauspost/cpuid/v2/topology_scan_windows_amd64.go b/vendor/github.com/klauspost/cpuid/v2/topology_scan_windows_amd64.go new file mode 100644 index 000000000..9b79c1c10 --- /dev/null +++ b/vendor/github.com/klauspost/cpuid/v2/topology_scan_windows_amd64.go @@ -0,0 +1,75 @@ +// Copyright (c) 2026 Klaus Post, released under MIT License. See LICENSE file. + +//go:build windows && amd64 && !noasm && !appengine && !gccgo + +package cpuid + +import ( + "runtime" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + kernel32 = windows.NewLazySystemDLL("kernel32.dll") + procGetCurrentThread = kernel32.NewProc("GetCurrentThread") + procGetCurrentProcess = kernel32.NewProc("GetCurrentProcess") + procGetProcessAffinityMask = kernel32.NewProc("GetProcessAffinityMask") + procSetThreadAffinityMask = kernel32.NewProc("SetThreadAffinityMask") + procGetCurrentProcessorNumber = kernel32.NewProc("GetCurrentProcessorNumber") + procGetActiveProcessorGroupCnt = kernel32.NewProc("GetActiveProcessorGroupCount") +) + +// scanLogicalCPUs samples CPUID on every CPU in the process affinity mask by +// pinning the current thread to each in turn. It covers a single processor +// group (up to 64 logical CPUs); larger systems are not fully enumerated. +func scanLogicalCPUs() ([]logicalCPU, error) { + // A single process affinity mask only covers one processor group (<=64 + // CPUs). Rather than record a partial tree on a machine with more than one + // group, bail out and let ScanTopology fall back to the snapshot, whose + // counts come from the (group-aware) OS totals. + if n, _, _ := procGetActiveProcessorGroupCnt.Call(); n > 1 { + return nil, ErrTopologyScanUnavailable + } + + proc, _, _ := procGetCurrentProcess.Call() + var procMask, sysMask uintptr + if ret, _, _ := procGetProcessAffinityMask.Call(proc, uintptr(unsafe.Pointer(&procMask)), uintptr(unsafe.Pointer(&sysMask))); ret == 0 || procMask == 0 { + return nil, ErrTopologyScanUnavailable + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + th, _, _ := procGetCurrentThread.Call() + defer procSetThreadAffinityMask.Call(th, procMask) + + var cpus []logicalCPU + for i := range 64 { + bit := uintptr(1) << i + if procMask&bit == 0 { + continue + } + if prev, _, _ := procSetThreadAffinityMask.Call(th, bit); prev == 0 { + continue + } + // SetThreadAffinityMask may defer migration; wait until we are on CPU i. + // If it never lands there, skip it rather than record another CPU's data. + migrated := false + for range 10000 { + if cur, _, _ := procGetCurrentProcessorNumber.Call(); uint32(cur) == uint32(i) { + migrated = true + break + } + runtime.Gosched() + } + if !migrated { + continue + } + cpus = append(cpus, parseLogicalCPU()) + } + if len(cpus) == 0 { + return nil, ErrTopologyScanUnavailable + } + return cpus, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b5d33c68a..0e4be80cb 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -283,9 +283,10 @@ github.com/kelseyhightower/envconfig # github.com/kevinburke/ssh_config v1.6.0 ## explicit; go 1.18 github.com/kevinburke/ssh_config -# github.com/klauspost/cpuid/v2 v2.4.0 +# github.com/klauspost/cpuid/v2 v2.4.1 ## explicit; go 1.24.0 github.com/klauspost/cpuid/v2 +github.com/klauspost/cpuid/v2/topology # github.com/lucasb-eyer/go-colorful v1.4.1 ## explicit; go 1.12 github.com/lucasb-eyer/go-colorful