diff --git a/cpu_performance.js b/cpu_performance.js new file mode 100644 index 0000000..044a467 --- /dev/null +++ b/cpu_performance.js @@ -0,0 +1,344 @@ +// JavaScript reference implementation of the CPU Performance API. +// +// This module is a direct translation of the Chromium C++ implementation in +// cpu_performance.cc. Its purpose is to make the classification rules +// described informally by the specification (index.bs) easy to inspect, +// reproduce, and test in isolation. The high-level shape of the API matches +// the specification: `navigator.cpuPerformance` returns a non-negative +// integer, where 0 means "unknown" and higher values mean higher tiers. +// The specification currently defines tiers 1..4; future revisions are +// expected to add 5, 6, and so on. + +'use strict'; + +const Manufacturer = Object.freeze({ + UNKNOWN: 'unknown', + AMD: 'amd', + APPLE: 'apple', + INTEL: 'intel', + MEDIATEK: 'mediatek', + MICROSOFT: 'microsoft', + QUALCOMM: 'qualcomm', + SAMSUNG: 'samsung', +}); + +function trimAndCollapseWhitespace(text) { + // \p{Z} matches unicode separators (including NBSP). + // \x1C\x1F match the file and unit separator characters. + return text + .replace(/^[\s\p{Z}\x1C\x1F]+/u, '') + .replace(/[\s\p{Z}\x1C\x1F]+$/u, '') + .replace(/[\s\p{Z}\x1C\x1F]+/gu, ' '); +} + +function getManufacturer(cpuModel) { + if (/\bAMD\b/i.test(cpuModel)) return Manufacturer.AMD; + if (/\bApple\b/i.test(cpuModel)) return Manufacturer.APPLE; + if (/\b(Intel|Celeron|Pentium)\b/i.test(cpuModel)) return Manufacturer.INTEL; + if (/\bMediaTek\b/i.test(cpuModel)) return Manufacturer.MEDIATEK; + if (/\bMicrosoft\b/i.test(cpuModel)) return Manufacturer.MICROSOFT; + if (/\b(Qualcomm|Snapdragon)\b/i.test(cpuModel)) return Manufacturer.QUALCOMM; + if (/\bSamsung\b/i.test(cpuModel)) return Manufacturer.SAMSUNG; + return Manufacturer.UNKNOWN; +} + +function splitCpuModel(cpuModel) { + let text = String(cpuModel == null ? '' : cpuModel); + + text = trimAndCollapseWhitespace(text); + + const manufacturer = getManufacturer(text); + + // Remove everything between parentheses. + text = text.replace(/\([^)]*\)/g, ' '); + + // Remove special characters. + text = text.replace(/\$|®|™/g, ' '); + + // Remove "GHz" patterns. + text = text.replace(/@( )?\d[.,]\d+([~\-]\d[.,]\d+)?( )?GHz\b/gi, ''); + text = text.replace(/\b\d[.,]\d+([~\-]\d[.,]\d+)?( )?GHz\b/gi, ''); + + text = trimAndCollapseWhitespace(text); + + // Remove trailing special characters. + text = text.replace(/(^| )?[@~\-,.]$/g, ''); + + // Remove filler words. + text = text.replace(/\bCPU\b/gi, ''); + text = text.replace(/\bMobile\b/gi, ''); + text = text.replace(/\bProcessor\b/gi, ''); + text = text.replace(/\bSilicon\b/gi, ''); + text = text.replace(/\bSOC\b/gi, ''); + text = text.replace(/\bTechnology\b/gi, ''); + + text = trimAndCollapseWhitespace(text); + + switch (manufacturer) { + case Manufacturer.AMD: + // ReplaceFirst: strip everything up to and including the first "AMD" token. + text = text.replace(/.*?\bAMD\b/i, ''); + text = trimAndCollapseWhitespace(text); + text = text.replace(/\bFX -/gi, 'FX-'); + text = text.replace(/\+( )?(AMD )?Radeon.*/gi, ''); + text = text.replace( + /\b(RADEON )?R\d+, \d+ COMPUTE CORES \d+C\+\d+G\b/gi, + '', + ); + text = text.replace(/\bwith (AMD )?Radeon.*/gi, ''); + text = text.replace(/\bw\/( )?(AMD )?Radeon.*/gi, ''); + text = text.replace(/\bRadeon.*/gi, ''); + + text = text.replace(/\b\w+( |-)Core\b/gi, ''); + text = text.replace(/\b\d+-Core(s)?\b/gi, ''); + + text = text.replace(/\bAPU\b/gi, ''); + text = text.replace(/\bCreator Edition\b/gi, ''); + text = text.replace(/\bDesktop Kit\b/gi, ''); + + text = text.replace(/\b(3250C) 15W\b/gi, '$1'); + break; + case Manufacturer.APPLE: + text = text.replace(/.*?\bApple\b/i, ''); + break; + case Manufacturer.INTEL: + text = text.replace(/.*?\bIntel\b/i, ''); + text = trimAndCollapseWhitespace(text); + text = text.replace(/\b(Core)(2)\b/gi, '$1 $2'); + text = text.replace(/\b(Core i\d+)( )?-( )?/gi, '$1-'); + text = text.replace(/\b(Core i\d+) (M) (\d+)\b/gi, '$1-$3$2'); + text = text.replace(/\b(Core i\d+) ([LQU]) (\d+)\b/gi, '$1-$3$2M'); + text = text.replace(/\b(Core i\d+) (\d+)\b/gi, '$1-$2'); + text = text.replace(/\b(Celeron|Pentium) Dual(-Core)?\b/gi, '$1'); + text = text.replace(/\b0+$/g, ''); + break; + default: + // For all other manufacturers including UNKNOWN, return an empty model. + return { manufacturer, model: '' }; + } + + text = trimAndCollapseWhitespace(text); + return { manufacturer, model: text }; +} + +function getTierFromCores(cores) { + if (cores >= 1 && cores <= 2) return 1; + if (cores >= 3 && cores <= 4) return 2; + if (cores >= 5 && cores <= 12) return 3; + if (cores >= 13) return 4; + return 0; +} + +function getTierFromCpuInfo(cpuModel, cores) { + if (cores <= 0) return 0; + if (cores <= 1) return 1; + + const { manufacturer, model } = splitCpuModel(cpuModel); + const m = (re) => re.test(model); + + if (cores <= 4) { + switch (manufacturer) { + case Manufacturer.AMD: + // 2 cores, K8 + K10 + Mobile Trinity + if ( + cores === 2 && + (m(/^Athlon 64\b/) || + m(/^Athlon II\b/) || + m(/^Athlon X2\b/) || + m(/^Phenom II\b/) || + m(/^Sempron X2\b/) || + m(/^Turion II\b/) || + m(/^Turion X2\b/) || + // Llano (~2011) + m(/^(A4|E2)-[3]\d\d\d[A-Z]*\b/) || + // Trinity (~2012) + m(/^(A4|A6)-[4]\d\d\dM[A-Z]*\b/)) + ) { + return 1; + } + // 2 cores, Bobcat + Jaguar + if ( + cores === 2 && + (m(/^(C|E|E1|E2|T|Z)-\w*\b/) || + m(/^(A4)-[1]\d\d\d[A-Z]*\b/) || + m(/^(GX)-[2]\d\d[A-Z]*\b/) || + m(/^Sempron 2650\b/)) + ) { + return 1; + } + // 2 cores, Entry-Level Stoney Ridge 2019 6W Re-Release + if (cores === 2 && m(/^A4-9120[Ce]\b/)) { + return 1; + } + // 4 cores, >= Zen + if ( + cores === 4 && + m(/^Ryzen\b/) && + // not 2 cores, Zen (~2017) + !m(/^Ryzen 3 Pro 2100GE\b/) && + !m(/^Ryzen 3 Pro 3050GE\b/) && + !m(/^Ryzen 3 2200U\b/) && + !m(/^Ryzen 3 3200U\b/) && + !m(/^Ryzen 3 3250[UC]\b/) && + !m(/^Ryzen Embedded R[1]\d\d\d[A-Z]*\b/) && + // not 2 cores, Zen+ (~2018) + !m(/^Ryzen Embedded R2312\b/) + ) { + return 3; + } + break; + case Manufacturer.INTEL: + // 2-4 cores, E-Core, Bonnell + Saltwell + if ( + cores >= 2 && cores <= 4 && + (// Silverthorne (~2008) + m(/^Atom (Z5)\d\d[A-Z]*\b/) || + // Diamondville (~2008) + m(/^Atom (2|3|N2)\d\d[A-Z]*\b/) || + // Pineview (~2010) + m(/^Atom (D4|N4|D5|N5)\d\d[A-Z]*\b/) || + // Tunnel Creek (~2010) / Stellarton (~2010) + m(/^Atom (E6)\d\d[A-Z]*\b/) || + // Lincroft (~2010) + m(/^Atom (Z6)\d\d[A-Z]*\b/) || + // Cedarview (~2011) + m(/^Atom (D2|N2)\d\d\d[A-Z]*\b/) || + // Penwell (~2012) + Cloverview (~2012) + m(/^Atom (Z2)\d\d\d[A-Z]*\b/)) + ) { + return 1; + } + // 2-4 cores, E-Core, Silvermont + Airmont + if ( + cores >= 2 && cores <= 4 && + (// Bay Trail-D (~2013) + m(/^Celeron (J1)\d\d\d[A-Z]*\b/) || + m(/^Pentium (J2)\d\d\d[A-Z]*\b/) || + // Avoton (~2013) / Rangeley (~2013) + m(/^Atom (C2)[35]\d\d[A-Z]*\b/) || + // Bay Trail-I (~2013) + m(/^Atom (E3)[8]\d\d[A-Z]*\b/) || + // Bay Trail-M (~2013) + m(/^Celeron (N2)\d\d\d[A-Z]*\b/) || + m(/^Pentium (N3)[5]\d\d[A-Z]*\b/) || + // Bay Trail-T (~2013) + Merrifield (~2014) + Moorefield (~2014) + m(/^Atom (Z3)\d\d\d[A-Z]*\b/) || + // Braswell (~2016) + m(/^Atom x[5]-(E8)\d\d\d[A-Z]*\b/) || + m(/^Celeron (J3|N3)[01]\d\d[A-Z]*\b/) || + m(/^Pentium (J3|N3)[7]\d\d[A-Z]*\b/) || + // Cherry Trail-T (~2015) + m(/^Atom x[57]-(Z8)\d\d\d[A-Z]*\b/)) + ) { + return 1; + } + // 2 cores, E-Core, Goldmont + if ( + cores === 2 && + (// Apollo Lake (~2016) + m(/^Atom x[5]-(A3|E3)\d\d\d[A-Z]*\b/) || + m(/^Celeron (J3|N3)[34]\d\d[A-Z]*\b/) || + // Denverton (~2017) + m(/^Atom (C3)\d\d\d[A-Z]*\b/)) + ) { + return 1; + } + // 2 cores, P-Core, Merom + Penryn + if ( + cores === 2 && + (// Merom (~2006) + Conroe (~2006) + Penryn (~2007) + m(/^Celeron (E1|SU2|T1|T3)\d\d\d[A-Z]*\b/) || + m(/^Core 2 Duo\b/) || + m(/^Core 2 Extreme\b/) || + m(/^Pentium (E2|SU2|SU4|T2|T3|T4)\d\d\d[A-Z]*\b/) || + m(/^Xeon (3|E3|L3)\d\d\d[A-Z]*\b/)) + ) { + return 1; + } + // 2 cores, P-Core, Nehalem + Westmere + if ( + cores === 2 && + (// Arrandale (~2010) + m(/^Celeron (P4|U3)\d\d\d[A-Z]*\b/) || + m(/^Pentium (P6|U5)\d\d\d[A-Z]*\b/)) + ) { + return 1; + } + // 2 cores, P-Core, Mobile Sandy Bridge + Mobile Ivy Bridge + if ( + cores === 2 && + (// Sandy Bridge-M (~2011) + m(/^Celeron (7|8|B7|B8)\d\d[A-Z]*\b/) || + m(/^Pentium (9|B9)\d\d[A-Z]*\b/) || + // Ivy Bridge-M (~2012) + m(/^Celeron (1)\d\d\d[A-Z]*\b/) || + m(/^Pentium (2|A1)\d\d\d[A-Z]*\b/)) + ) { + return 1; + } + // 4 cores, E-Core, >= Gracemont + if ( + cores === 4 && + (m(/^N\d\d+\b/) || + // Alder Lake-N (~2023) + m(/^Atom x7425E\b/)) + ) { + return 3; + } + break; + default: + // Any other manufacturer with at most 2 cores is demoted to tier 1. + if (cores <= 2) return 1; + break; + } + return 2; + } + + if (cores <= 10) { + switch (manufacturer) { + case Manufacturer.APPLE: + // 8+ cores, M-series + if (cores >= 8 && m(/^M\d+\b/)) return 4; + break; + case Manufacturer.INTEL: + // 8+ cores, >= Meteor Lake + if (cores >= 8 && m(/^Core Ultra\b/)) return 4; + break; + default: + break; + } + return 3; + } + + return 4; +} + +// Spec-shaped accessor. Returns an object that exposes a `cpuPerformance` +// getter behaving like `navigator.cpuPerformance` as defined in index.bs: +// a non-negative integer where 0 means "unknown" and higher values mean +// higher tiers. +function createNavigatorCpuPerformance({ cpuModel = '', cores = 0 } = {}) { + const tier = getTierFromCpuInfo(cpuModel, cores); + return Object.freeze({ + get cpuPerformance() { + return tier; + }, + }); +} + +const exports_ = { + Manufacturer, + trimAndCollapseWhitespace, + getManufacturer, + splitCpuModel, + getTierFromCores, + getTierFromCpuInfo, + createNavigatorCpuPerformance, +}; + +// Support both Node.js (require) and a plain browser + + + diff --git a/index.bs b/index.bs index aa599dc..545ff33 100644 --- a/index.bs +++ b/index.bs @@ -120,14 +120,44 @@ returned in case the API's implementation is unable to classify the user device. ## Computing Performance Tier Value ## {#computing-performance-tier-value} -Classification of user devices to [=performance tiers=] is -[=implementation-defined=] and should be interpreted by web applications -according to their particular needs. However, implementations of this API should -adhere to the following rules: - -1. **Consistency**: The mapping of devices to [=performance tiers=] should - reflect the [=CPU=] [=performance=] that can be measured with specific - benchmarks, ideally using programming tools provided by a browser +
+ To compute the performance tier of a user device's [=CPU=]: + + 1. Let |cores| be the number of [=cores=] of the [=CPU=] as reported by + the operating system, or null if the operating system does not report a + [=core=] count. + 2. If |cores| is null or 0, return 0. + 3. If |cores| is 1, return 1. + 4. If |cores| is between 2 and 4 (inclusive), return 2. + 5. If |cores| is between 5 and 10 (inclusive), return 3. + 6. Return 4. +
+ +NOTE: Implementations are permitted to adjust the returned [=performance +tier=] when the [=CPU=] model is known to perform substantially better or +worse than is typical for its [=core=] count. Such adjustments typically +shift the [=performance tier=] by no more than a single tier level (i.e., by ++1 or −1), and require reliable information about the [=CPU=] model +from the operating system. The set of [=CPU=] models for which such +adjustments are applied is implementation-defined. + +NOTE: A JavaScript [reference +implementation](https://wicg.github.io/cpu-performance/cpu_performance.js) of +this algorithm — including a representative set of model-based adjustments +derived from the implementation in the Chromium browser engine — is available +in the [cpu-performance repository](https://github.com/WICG/cpu-performance), +along with an [interactive demo +page](https://wicg.github.io/cpu-performance/demo.html) that illustrates how +the algorithm and its model-based heuristics classify a wide range of +real-world devices. These are not normative; they represent a suggested +implementation of this section's algorithm with model-based adjustments +applied. + +Implementations of this API should also adhere to the following rules: + +1. **Consistency**: Any model-based adjustments to the [=performance tier=] + should reflect the [=CPU=] [=performance=] that can be measured with + specific benchmarks, ideally using programming tools provided by a browser (JavaScript, WebAssembly, etc.) and measured in ideal conditions. More powerful devices should not be classified in lower [=performance tiers=] than less powerful devices. @@ -151,21 +181,12 @@ adhere to the following rules: break the behavior of obsolete machines running obsolete applications. 3. **User privacy**: To avoid user fingerprinting, implementations should - classify a fairly large number of user devices in each [=performance tier=] - (see also [[#security-privacy]]). In particular, implementations that are - based on some [=CPU=] model database should not return the special value 0 - for new user devices that are not contained in the database. The special - value should be returned only when the implementation cannot obtain - information about the [=CPU=] from the operating system. - -Implementations may (but are not required to) compute the [=performance tier=] -value for a given user device based on the following characteristics: - -- the [=CPU=] model; -- the number of [=cores=], [=physical core|physical=] and/or [=logical - core|logical=]; -- the [=frequency=]; and -- other available [=CPU=] characteristics. + ensure that a fairly large number of user devices fall into each + [=performance tier=] (see also [[#security-privacy]]). In particular, + model-based adjustments should be coarse enough that each [=performance + tier=] still contains many distinct [=CPU=] models, and the special value + 0 should be returned only when the implementation cannot obtain + information about the number of [=cores=] from the operating system. Javascript API {#js-api} @@ -183,10 +204,10 @@ Javascript API {#js-api} The cpuPerformance getter steps are: - 1. Let |tier| be an {{unsigned short}} representing the [=performance - tier=] of the device's [=CPU=], generated in an - [=implementation-defined=] way, considering the recommendations - and constraints described in [[#computing-performance-tier-value]]. + 1. Let |tier| be the result of [=compute the performance tier|computing + the performance tier=] of the device's [=CPU=], possibly with + model-based adjustments as described in + [[#computing-performance-tier-value]]. 2. [=Assert=]: 0 ≤ |tier| ≤ 4. 3. Return |tier|.