blob: 5ade342ce56fbbf3c9c83d274b631949eff58c70 [file] [log] [blame]
// Pieces of standalone interrogation common across platforms--or at least testable on arbitrary
// platforms.
package standalone
import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"howett.net/plist"
)
// Split a macOS version like 1.2.3 into an array of versions of all precisions, like ["Mac",
// "Mac-1", "Mac-1.2", "Mac-1.2.3"].
func macVersionsOfAllPrecisions(version string) []string {
subversions := strings.Split(version, ".")
ret := []string{"Mac", "Mac-" + subversions[0]}
for i, subversion := range subversions[1:] {
ret = append(ret, ret[i+1]+"."+subversion)
}
return ret
}
// intelModel extracts the CPU model name from its display name and returns it. For example, it
// pulls "i7-9750H" out of "Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz". Returns "" if extraction
// fails.
func intelModel(brandString string) string {
regexes := []*regexp.Regexp{
regexp.MustCompile(` ([a-zA-Z]\d-\d{4}[A-Z]{0,2} [vV]\d) `),
regexp.MustCompile(` ([a-zA-Z]\d-\d{4}[A-Z]{0,2}) `),
regexp.MustCompile(` ([A-Z]\d{4}[A-Z]{0,2}) `),
regexp.MustCompile(` ((:?[A-Z][a-z]+ )+GCE)`),
}
for _, regex := range regexes {
if matches := regex.FindStringSubmatch(brandString); len(matches) >= 2 {
return matches[1]
}
}
return ""
}
// macCPUs is the brains behind the CPUs() function, broken off for testing.
func macCPUs(arch string, vendor string, brandString string) ([]string, error) {
// gopsutil can spit out i386, i686, arm, aarch64, ia64, or x86_64 under Windows; "" as a
// fallback; whatever uname's utsname.machine struct member (a string) has on POSIX. On our lab
// Macs, that's arm64 or x64_86 (exposed by uname -m). Our lab Linuxes: x86_64.
arch_to_isa := map[string]string{
"x86_64": "x86",
"amd64": "x86",
"i386": "x86",
"i686": "x86",
"aarch64": "arm64",
"arm64": "arm64",
}
isa, ok := arch_to_isa[arch]
if !ok {
return nil, skerr.Fmt("host had unknown architecture: %s", arch)
}
var bitness string
// Fall back to int-size test if the width isn't explicit in the arch name:
if strings.HasSuffix(arch, "64") || strconv.IntSize > 1<<32 {
bitness = "64"
} else {
bitness = "32"
}
ret := []string{isa, fmt.Sprintf("%s-%s", isa, bitness)}
var model string
if vendor == "GenuineIntel" {
model = intelModel(brandString)
} else {
model = strings.ReplaceAll(brandString, " ", "_")
}
if len(model) > 0 {
ret = append(ret, fmt.Sprintf("%s-%s-%s", isa, bitness, model))
}
return ret, nil
}
type macGPU struct {
// Any string field missing from the plist ends up "".
ID string `plist:"spdisplays_device-id"`
VendorID string `plist:"spdisplays_vendor-id"`
Vendor string `plist:"spdisplays_vendor"`
Version string `plist:"spdisplays_gmux-version"`
Model string `plist:"sppci_model"`
}
// gpusFromSystemProfilerXML returns the GPUs found on a Mac by System Profiler, as expressed
// through its -xml output option. If the output doesn't look valid, returns an error and no GPUs.
// If there are no GPUs, will return 0 GPUs even if no error occurs.
func gpusFromSystemProfilerXML(xml string) ([]macGPU, error) {
type OuterDict struct {
GPUs []macGPU `plist:"_items"`
}
var profilerOutput []OuterDict
_, err := plist.Unmarshal([]byte(xml), &profilerOutput)
if err != nil {
return nil, skerr.Wrapf(err, "couldn't unmarshal XML from System Profiler")
}
if len(profilerOutput) < 1 {
return nil, skerr.Fmt("System Profiler returned no info")
}
return profilerOutput[0].GPUs, nil
}
// dimensionsFromGPUs turns a slice of Mac GPUs into Swarming-style dimensions, e.g. ["Intel
// (8086)", "Intel Coffee Lake H UHD Graphics 630 (8086:3e9b)"]. If there are no GPUs, return
// ["none"].
func dimensionsFromMacGPUs(gpus []macGPU) []string {
var dimensions []string
for _, gpu := range gpus {
if gpu.ID == "" {
continue
}
gpuID := strings.TrimPrefix(gpu.ID, "0x")
var vendorID gpuVendorID
if gpu.VendorID != "" {
// NVidia
vendorID = gpuVendorID(strings.TrimPrefix(gpu.VendorID, "0x"))
} else if gpu.Vendor != "" {
// Intel and ATI
re := regexp.MustCompile(`\(0x([0-9a-f]{4})\)`)
if matches := re.FindStringSubmatch(gpu.Vendor); matches != nil {
vendorID = gpuVendorID(matches[1])
}
}
vendorName := ""
if gpu.Model != "" {
ok := false
// The first word is pretty much always the company name:
vendorName, _, ok = strings.Cut(gpu.Model, " ")
if !ok {
// This has apparently never happened in the history of Swarming's introspection
// code, because it would have raised an uncaught exception there:
sklog.Warningf("GPU device name was only a single word: %s", gpu.Model)
continue
}
}
// macOS 10.13 stopped including the vendor ID in the spdisplays_vendor string. Infer it
// from the vendor name instead.
if vendorID == "" {
vendorID = gpuVendorNameToID(vendorName)
}
if vendorID == "" && gpu.Vendor != "" {
re := regexp.MustCompile(`sppci_vendor_([a-z]+)$`)
if matches := re.FindStringSubmatch(gpu.Vendor); matches != nil {
vendorName = matches[1]
vendorID = gpuVendorNameToID(vendorName)
}
}
if vendorID == "" {
vendorID = gpuVendorID("UNKNOWN")
} else if vendorID == "15ad" {
// This is VMWare, which we consider as not having any GPUs.
return []string{"none"}
}
dimensions = append(dimensions, string(vendorID), fmt.Sprintf("%s:%s", vendorID, gpuID))
// Add GPU version:
if gpu.Version != "" {
re := regexp.MustCompile(`([0-9.]+) \[([0-9.]+)\]`)
if matches := re.FindStringSubmatch(gpu.Version); matches != nil {
dimensions = append(dimensions, fmt.Sprintf("%s:%s-%s-%s", vendorID, gpuID, matches[1], matches[2]))
}
}
}
if len(dimensions) == 0 {
dimensions = []string{"none"}
}
sort.Strings(dimensions)
return dimensions
}