| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/cpu_performance/cpu_performance.h" |
| |
| #include <optional> |
| |
| #include "base/functional/bind.h" |
| #include "base/no_destructor.h" |
| #include "base/strings/string_util.h" |
| #include "base/synchronization/lock.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/thread_pool.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/re2/src/re2/re2.h" |
| |
| // The implementation of the CPU Performance API is experimental and subject to |
| // change. |
| |
| namespace content::cpu_performance { |
| |
| namespace { |
| |
| bool Search(std::string_view text, const re2::RE2& re) { |
| return re2::RE2::PartialMatch(text, re); |
| } |
| |
| void Replace(std::string* text, |
| const re2::RE2& re, |
| std::string_view replacement) { |
| re2::RE2::GlobalReplace(text, re, replacement); |
| } |
| |
| void ReplaceFirst(std::string* text, |
| const re2::RE2& re, |
| std::string_view replacement) { |
| re2::RE2::Replace(text, re, replacement); |
| } |
| |
| void TrimAndCollapseWhitespace(std::string* text) { |
| // \p{Z} matches unicode separators (including NBSP). |
| // \x1C\x1F match the file and unit separator characters. |
| Replace(text, "^[\\s\\p{Z}\\x1C\\x1F]+", ""); |
| Replace(text, "[\\s\\p{Z}\\x1C\\x1F]+$", ""); |
| Replace(text, "[\\s\\p{Z}\\x1C\\x1F]+", " "); |
| } |
| |
| // Returns the manufacturer. |
| Manufacturer GetManufacturer(std::string_view cpu_model) { |
| if (Search(cpu_model, "(?i)\\bAMD\\b")) { |
| return Manufacturer::kAMD; |
| } else if (Search(cpu_model, "(?i)\\bApple\\b")) { |
| return Manufacturer::kApple; |
| } else if (Search(cpu_model, "(?i)\\b(Intel|Celeron|Pentium)\\b")) { |
| return Manufacturer::kIntel; |
| } else if (Search(cpu_model, "(?i)\\bMediaTek\\b")) { |
| return Manufacturer::kMediaTek; |
| } else if (Search(cpu_model, "(?i)\\bMicrosoft\\b")) { |
| return Manufacturer::kMicrosoft; |
| } else if (Search(cpu_model, "(?i)\\b(Qualcomm|Snapdragon)\\b")) { |
| return Manufacturer::kQualcomm; |
| } else if (Search(cpu_model, "(?i)\\bSamsung\\b")) { |
| return Manufacturer::kSamsung; |
| } |
| return Manufacturer::kUnknown; |
| } |
| |
| } // anonymous namespace |
| |
| Tier TierFromInt(int value) { |
| CHECK_LE(static_cast<int>(Tier::kUnknown), value); |
| CHECK_GE(static_cast<int>(Tier::kUltra), value); |
| return static_cast<Tier>(value); |
| } |
| |
| std::pair<Manufacturer, std::string> SplitCpuModel(std::string_view cpu_model) { |
| std::string text(cpu_model); |
| |
| TrimAndCollapseWhitespace(&text); |
| |
| Manufacturer manufacturer = GetManufacturer(text); |
| |
| // Remove everything between parentheses. |
| Replace(&text, "\\([^)]*\\)", " "); |
| |
| // Remove special characters. |
| Replace(&text, "\\$|®|™", " "); |
| |
| // Remove "GHz" patterns. |
| Replace(&text, "(?i)@( )?\\d[.,]\\d+([~\\-]\\d[.,]\\d+)?( )?GHz\\b", ""); |
| Replace(&text, "(?i)\\b\\d[.,]\\d+([~\\-]\\d[.,]\\d+)?( )?GHz\\b", ""); |
| |
| TrimAndCollapseWhitespace(&text); |
| |
| // Remove trailing special characters. |
| Replace(&text, "(^| )?[@~\\-,.]$", ""); |
| |
| // Remove filler words. |
| Replace(&text, "(?i)\\bCPU\\b", ""); |
| Replace(&text, "(?i)\\bMobile\\b", ""); |
| Replace(&text, "(?i)\\bProcessor\\b", ""); |
| Replace(&text, "(?i)\\bSilicon\\b", ""); |
| Replace(&text, "(?i)\\bSOC\\b", ""); |
| Replace(&text, "(?i)\\bTechnology\\b", ""); |
| |
| TrimAndCollapseWhitespace(&text); |
| |
| // Processing specific to each manufacturer. |
| switch (manufacturer) { |
| case Manufacturer::kAMD: |
| ReplaceFirst(&text, "(?i).*?\\bAMD\\b", ""); |
| TrimAndCollapseWhitespace(&text); |
| Replace(&text, "(?i)\\bFX -", "FX-"); |
| Replace(&text, "(?i)\\+( )?(AMD )?Radeon.*", ""); |
| Replace(&text, |
| "(?i)\\b(RADEON )?R\\d+, \\d+ COMPUTE CORES \\d+C\\+\\d+G\\b", |
| ""); |
| Replace(&text, "(?i)\\bwith (AMD )?Radeon.*", ""); |
| Replace(&text, "(?i)\\bw\\/( )?(AMD )?Radeon.*", ""); |
| Replace(&text, "(?i)\\bRadeon.*", ""); |
| |
| Replace(&text, "(?i)\\b\\w+( |-)Core\\b", ""); |
| Replace(&text, "(?i)\\b\\d+-Core(s)?\\b", ""); |
| |
| Replace(&text, "(?i)\\bAPU\\b", ""); |
| Replace(&text, "(?i)\\bCreator Edition\\b", ""); |
| Replace(&text, "(?i)\\bDesktop Kit\\b", ""); |
| |
| Replace(&text, "(?i)\\b(3250C) 15W\\b", "\\1"); |
| break; |
| case Manufacturer::kApple: |
| ReplaceFirst(&text, "(?i).*?\\bApple\\b", ""); |
| break; |
| case Manufacturer::kIntel: |
| ReplaceFirst(&text, "(?i).*?\\bIntel\\b", ""); |
| TrimAndCollapseWhitespace(&text); |
| Replace(&text, "(?i)\\b(Core)(2)\\b", "\\1 \\2"); |
| Replace(&text, "(?i)\\b(Core i\\d+)( )?-( )?", "\\1-"); |
| Replace(&text, "(?i)\\b(Core i\\d+) (M) (\\d+)\\b", "\\1-\\3\\2"); |
| Replace(&text, "(?i)\\b(Core i\\d+) ([LQU]) (\\d+)\\b", "\\1-\\3\\2M"); |
| Replace(&text, "(?i)\\b(Core i\\d+) (\\d+)\\b", "\\1-\\2"); |
| Replace(&text, "(?i)\\b(Celeron|Pentium) Dual(-Core)?\\b", "\\1"); |
| Replace(&text, "\\b0+$", ""); |
| break; |
| default: |
| // For all other manufacturers including kUnknown, we just return an |
| // empty model string. |
| return {manufacturer, ""}; |
| } |
| |
| TrimAndCollapseWhitespace(&text); |
| return {manufacturer, text}; |
| } |
| |
| namespace { |
| struct CpuInfoState { |
| base::Lock lock; |
| Tier tier = Tier::kUnknown; |
| std::string model; |
| int cores = 0; |
| }; |
| |
| CpuInfoState& GetCpuInfoState() { |
| static base::NoDestructor<CpuInfoState> state; |
| return *state; |
| } |
| |
| void InitializeFromCores() { |
| int cores = base::SysInfo::NumberOfProcessors(); |
| Tier tier = GetTierFromCores(cores); |
| |
| base::AutoLock auto_lock(GetCpuInfoState().lock); |
| GetCpuInfoState().cores = cores; |
| GetCpuInfoState().tier = tier; |
| } |
| |
| void InitializeFromCpuInfo() { |
| std::string model = base::SysInfo::CPUModelName(); |
| int cores = base::SysInfo::NumberOfProcessors(); |
| Tier tier = GetTierFromCpuInfo(model, cores); |
| |
| base::AutoLock auto_lock(GetCpuInfoState().lock); |
| GetCpuInfoState().model = std::move(model); |
| GetCpuInfoState().cores = cores; |
| GetCpuInfoState().tier = tier; |
| } |
| |
| } // anonymous namespace |
| |
| Tier GetTier() { |
| base::AutoLock auto_lock(GetCpuInfoState().lock); |
| return GetCpuInfoState().tier; |
| } |
| |
| std::string GetModel() { |
| base::AutoLock auto_lock(GetCpuInfoState().lock); |
| return GetCpuInfoState().model; |
| } |
| |
| int GetCores() { |
| base::AutoLock auto_lock(GetCpuInfoState().lock); |
| return GetCpuInfoState().cores; |
| } |
| |
| void Initialize() { |
| if (base::FeatureList::IsEnabled(blink::features::kCpuPerformance)) { |
| // Use a simple, default implementation which is non-blocking, so that we |
| // have a reasonable computed value in the unlikely case that the tier is |
| // fetched by renderer initialization before the task executes. |
| InitializeFromCores(); |
| |
| // Post the task that will update the tier asynchronously, using the more |
| // accurate (but potentially blocking) implementation. Note that this task |
| // is a "MayBlock" because of base::SysInfo::CPUModelName(), which on Linux |
| // reads /proc/cpuinfo. |
| base::ThreadPool::PostTask( |
| FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE}, |
| base::BindOnce(&InitializeFromCpuInfo)); |
| } |
| } |
| |
| Tier GetTierFromCores(int cores) { |
| if (cores >= 1 && cores <= 2) { |
| return Tier::kLow; |
| } else if (cores >= 3 && cores <= 4) { |
| return Tier::kMid; |
| } else if (cores >= 5 && cores <= 12) { |
| return Tier::kHigh; |
| } else if (cores >= 13) { |
| return Tier::kUltra; |
| } |
| return Tier::kUnknown; |
| } |
| |
| Tier GetTierFromCpuInfo(std::string_view cpu_model, int cores) { |
| // Number of cores is unknown. |
| if (cores <= 0) { |
| return Tier::kUnknown; |
| } |
| |
| if (cores <= 1) { |
| return Tier::kLow; |
| } |
| |
| auto [manufacturer, model] = SplitCpuModel(cpu_model); |
| |
| if (cores <= 4) { |
| switch (manufacturer) { |
| case Manufacturer::kAMD: |
| // 2 cores, K8 + K10 + Mobile Trinity |
| if (cores == 2 && |
| (Search(model, "^Athlon 64\\b") || Search(model, "^Athlon II\\b") || |
| Search(model, "^Athlon X2\\b") || Search(model, "^Phenom II\\b") || |
| Search(model, "^Sempron X2\\b") || |
| Search(model, "^Turion II\\b") || Search(model, "^Turion X2\\b") || |
| // Llano (~2011) |
| Search(model, "^(A4|E2)-[3]\\d\\d\\d[A-Z]*\\b") || |
| // Trinity (~2012) |
| Search(model, "^(A4|A6)-[4]\\d\\d\\dM[A-Z]*\\b"))) { |
| return Tier::kLow; |
| } |
| // 2 cores, Bobcat + Jaguar |
| if (cores == 2 && (Search(model, "^(C|E|E1|E2|T|Z)-\\w*\\b") || |
| Search(model, "^(A4)-[1]\\d\\d\\d[A-Z]*\\b") || |
| Search(model, "^(GX)-[2]\\d\\d[A-Z]*\\b") || |
| Search(model, "^Sempron 2650\\b"))) { |
| return Tier::kLow; |
| } |
| // 2 cores, Entry-Level Stoney Ridge 2019 6W Re-Release |
| if (cores == 2 && Search(model, "^A4-9120[Ce]\\b")) { |
| return Tier::kLow; |
| } |
| // 4 cores, >= Zen |
| if (cores == 4 && Search(model, "^Ryzen\\b") && |
| // not 2 cores, Zen (~2017) |
| !Search(model, "^Ryzen 3 Pro 2100GE\\b") && |
| !Search(model, "^Ryzen 3 Pro 3050GE\\b") && |
| !Search(model, "^Ryzen 3 2200U\\b") && |
| !Search(model, "^Ryzen 3 3200U\\b") && |
| !Search(model, "^Ryzen 3 3250[UC]\\b") && |
| !Search(model, "^Ryzen Embedded R[1]\\d\\d\\d[A-Z]*\\b") && |
| // not 2 cores, Zen+ (~2018) |
| !Search(model, "^Ryzen Embedded R2312\\b")) { |
| return Tier::kHigh; |
| } |
| break; |
| case Manufacturer::kIntel: |
| // 2-4 cores, E-Core, Bonnell + Saltwell |
| if (cores >= 2 && cores <= 4 && |
| // Silverthorne (~2008) |
| (Search(model, "^Atom (Z5)\\d\\d[A-Z]*\\b") || |
| // Diamondville (~2008) |
| Search(model, "^Atom (2|3|N2)\\d\\d[A-Z]*\\b") || |
| // Pineview (~2010) |
| Search(model, "^Atom (D4|N4|D5|N5)\\d\\d[A-Z]*\\b") || |
| // Tunnel Creek (~2010) / Stellarton (~2010) |
| Search(model, "^Atom (E6)\\d\\d[A-Z]*\\b") || |
| // Lincroft (~2010) |
| Search(model, "^Atom (Z6)\\d\\d[A-Z]*\\b") || |
| // Cedarview (~2011) |
| Search(model, "^Atom (D2|N2)\\d\\d\\d[A-Z]*\\b") || |
| // Penwell (~2012) + Cloverview (~2012) |
| Search(model, "^Atom (Z2)\\d\\d\\d[A-Z]*\\b"))) { |
| return Tier::kLow; |
| } |
| // 2-4 cores, E-Core, Silvermont + Airmont |
| if (cores >= 2 && cores <= 4 && |
| // Bay Trail-D (~2013) |
| (Search(model, "^Celeron (J1)\\d\\d\\d[A-Z]*\\b") || |
| Search(model, "^Pentium (J2)\\d\\d\\d[A-Z]*\\b") || |
| // Avoton (~2013) / Rangeley (~2013) |
| Search(model, "^Atom (C2)[35]\\d\\d[A-Z]*\\b") || |
| // Bay Trail-I (~2013) |
| Search(model, "^Atom (E3)[8]\\d\\d[A-Z]*\\b") || |
| // Bay Trail-M (~2013) |
| Search(model, "^Celeron (N2)\\d\\d\\d[A-Z]*\\b") || |
| Search(model, "^Pentium (N3)[5]\\d\\d[A-Z]*\\b") || |
| // Bay Trail-T (~2013) + Merrifield (~2014) + Moorefield (~2014) |
| Search(model, "^Atom (Z3)\\d\\d\\d[A-Z]*\\b") || |
| // Braswell (~2016) |
| Search(model, "^Atom x[5]-(E8)\\d\\d\\d[A-Z]*\\b") || |
| Search(model, "^Celeron (J3|N3)[01]\\d\\d[A-Z]*\\b") || |
| Search(model, "^Pentium (J3|N3)[7]\\d\\d[A-Z]*\\b") || |
| // Cherry Trail-T (~2015) |
| Search(model, "^Atom x[57]-(Z8)\\d\\d\\d[A-Z]*\\b"))) { |
| return Tier::kLow; |
| } |
| // 2 cores, E-Core, Goldmont |
| if (cores == 2 && |
| // Apollo Lake (~2016) |
| (Search(model, "^Atom x[5]-(A3|E3)\\d\\d\\d[A-Z]*\\b") || |
| Search(model, "^Celeron (J3|N3)[34]\\d\\d[A-Z]*\\b") || |
| // Denverton (~2017) |
| Search(model, "^Atom (C3)\\d\\d\\d[A-Z]*\\b"))) { |
| return Tier::kLow; |
| } |
| // 2 cores, P-Core, Merom + Penryn |
| if (cores == 2 && |
| // Merom (~2006) + Conroe (~2006) + Penryn (~2007) |
| (Search(model, "^Celeron (E1|SU2|T1|T3)\\d\\d\\d[A-Z]*\\b") || |
| Search(model, "^Core 2 Duo\\b") || |
| Search(model, "^Core 2 Extreme\\b") || |
| Search(model, |
| "^Pentium (E2|SU2|SU4|T2|T3|T4)\\d\\d\\d[A-Z]*\\b") || |
| Search(model, "^Xeon (3|E3|L3)\\d\\d\\d[A-Z]*\\b"))) { |
| return Tier::kLow; |
| } |
| // 2 cores, P-Core, Nehalem + Westmere |
| if (cores == 2 && |
| // Arrandale (~2010) |
| (Search(model, "^Celeron (P4|U3)\\d\\d\\d[A-Z]*\\b") || |
| Search(model, "^Pentium (P6|U5)\\d\\d\\d[A-Z]*\\b"))) { |
| return Tier::kLow; |
| } |
| // 2 cores, P-Core, Mobile Sandy Bridge + Mobile Ivy Bridge |
| if (cores == 2 && |
| // Sandy Bridge-M (~2011) |
| (Search(model, "^Celeron (7|8|B7|B8)\\d\\d[A-Z]*\\b") || |
| Search(model, "^Pentium (9|B9)\\d\\d[A-Z]*\\b") || |
| // Ivy Bridge-M (~2012) |
| Search(model, "^Celeron (1)\\d\\d\\d[A-Z]*\\b") || |
| Search(model, "^Pentium (2|A1)\\d\\d\\d[A-Z]*\\b"))) { |
| return Tier::kLow; |
| } |
| // 4 cores, E-Core, >= Gracemont |
| if (cores == 4 && (Search(model, "^N\\d\\d+\\b") || |
| // Alder Lake-N (~2023) |
| Search(model, "^Atom x7425E\\b"))) { |
| return Tier::kHigh; |
| } |
| break; |
| default: |
| if (cores <= 2) { |
| // Any other manufacturer |
| return Tier::kLow; |
| } |
| break; |
| } |
| return Tier::kMid; |
| } |
| |
| if (cores <= 10) { |
| switch (manufacturer) { |
| case Manufacturer::kApple: |
| // 8+ cores, M-series |
| if (cores >= 8 && Search(model, "^M\\d+\\b")) { |
| return Tier::kUltra; |
| } |
| break; |
| case Manufacturer::kIntel: |
| // 8+ cores, >= Meteor Lake |
| if (cores >= 8 && Search(model, "^Core Ultra\\b")) { |
| return Tier::kUltra; |
| } |
| break; |
| default: |
| break; |
| } |
| return Tier::kHigh; |
| } |
| |
| return Tier::kUltra; |
| } |
| |
| } // namespace content::cpu_performance |