@razorpay/blade
Version:
The Design System that powers Razorpay
456 lines (430 loc) • 15.1 kB
JavaScript
import _defineProperty from '@babel/runtime/helpers/defineProperty';
import _classCallCheck from '@babel/runtime/helpers/classCallCheck';
import _createClass from '@babel/runtime/helpers/createClass';
/**
* PerformanceManager
*
* Detects GPU tier and device capability, then produces recommended
* RzpGlass render settings. Uses a layered heuristic approach:
*
* 1. WEBGL_debug_renderer_info – parse GPU renderer/vendor strings
* 2. failIfMajorPerformanceCaveat – fast browser-level GPU check
* 3. Device signals – deviceMemory, hardwareConcurrency, mobile UA
*
* Tier definitions:
* high – discrete / flagship mobile GPU, ample memory
* mid – integrated / mid-range mobile GPU
* low – software renderer, very old GPU, or constrained device
* unknown – could not determine (treated as mid for safety)
*/
// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Known renderer pattern lists
// ---------------------------------------------------------------------------
/**
* Patterns that indicate a software / CPU renderer (potato tier).
* These devices cannot run WebGL at any usable framerate.
*/
var POTATO_TIER_PATTERNS = [/swiftshader/, /llvmpipe/, /softpipe/, /microsoft basic render/, /virgl/];
/**
* Patterns that strongly indicate a low-end GPU.
* Tested against the lowercased GL_RENDERER string.
*/
var LOW_TIER_PATTERNS = [
// Old Intel integrated
/intel.*hd\s*(graphics)?\s*(2000|3000|4000|400|500|510|520|530)/, /intel.*gma/,
// Old AMD integrated
/amd.*radeon.*r[2-5]\s/, /amd.*radeon.*hd\s*(6|7)\d{3}/,
// Very old NVIDIA
/nvidia.*geforce\s*(4|5|6|7|8|9)\d{2}[^0]/,
// Old mobile GPUs
/mali-(4|t[0-9]|g5[0-7])\d*/, /adreno\s*(3|4)\d{2}/, /powervr.*sgx/, /vivante/, /gc\d{3}[^0-9]/ // Vivante GC series
];
/**
* Patterns that indicate a high-end GPU.
*/
var HIGH_TIER_PATTERNS = [
// NVIDIA discrete
/nvidia.*rtx/, /nvidia.*gtx\s*1[0-9]{3}/, /nvidia.*gtx\s*[2-9]\d{3}/, /nvidia.*quadro/, /nvidia.*titan/,
// AMD discrete
/amd.*rx\s*(5|6|7)\d{3}/, /amd.*radeon\s*(pro|rx)\s*(vega|5|6|7)/, /radeon\s*r9/,
// Apple Silicon
/apple\s*(m[1-9]|a1[5-9]|a[2-9]\d)/,
// Modern mobile flagship
/adreno\s*(7[3-9]\d|8\d{2})/, /mali-g(7[1-9]|[89]\d|[1-9]\d{2})/, /gpu\s*family\s*(apple\s*[5-9]|apple\s*[1-9]\d)/];
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function isMobileDevice() {
return /android|iphone|ipad|ipod|mobile/i.test(navigator.userAgent);
}
function getDeviceMemoryGB() {
var mem = navigator.deviceMemory;
return mem != null ? mem : null;
}
function getCpuCores() {
var _navigator$hardwareCo;
return (_navigator$hardwareCo = navigator.hardwareConcurrency) !== null && _navigator$hardwareCo !== void 0 ? _navigator$hardwareCo : 2;
}
/**
* Probes the WEBGL_debug_renderer_info extension to get raw GPU strings.
* Returns nulls if the extension is unavailable or blocked (privacy protection).
*/
function getGpuStrings(gl) {
var ext = gl.getExtension('WEBGL_debug_renderer_info');
if (!ext) return {
renderer: null,
vendor: null
};
var renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
var vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL);
return {
renderer: renderer,
vendor: vendor
};
}
/**
* Checks whether the browser signals a major performance caveat (software/swiftshader).
*/
function checkMajorPerformanceCaveat() {
var canvas = document.createElement('canvas');
var gl = canvas.getContext('webgl', {
failIfMajorPerformanceCaveat: true
});
return gl === null;
}
/**
* Classify a GPU into a tier using the renderer / vendor strings.
* Returns null if classification is inconclusive.
*/
function classifyByRendererString(renderer, vendor) {
if (!renderer && !vendor) return null;
var combined = "".concat(renderer !== null && renderer !== void 0 ? renderer : '', " ").concat(vendor !== null && vendor !== void 0 ? vendor : '').toLowerCase();
for (var _i = 0, _POTATO_TIER_PATTERNS = POTATO_TIER_PATTERNS; _i < _POTATO_TIER_PATTERNS.length; _i++) {
var pattern = _POTATO_TIER_PATTERNS[_i];
if (pattern.test(combined)) return 'potato';
}
for (var _i2 = 0, _LOW_TIER_PATTERNS = LOW_TIER_PATTERNS; _i2 < _LOW_TIER_PATTERNS.length; _i2++) {
var _pattern = _LOW_TIER_PATTERNS[_i2];
if (_pattern.test(combined)) return 'low';
}
for (var _i3 = 0, _HIGH_TIER_PATTERNS = HIGH_TIER_PATTERNS; _i3 < _HIGH_TIER_PATTERNS.length; _i3++) {
var _pattern2 = _HIGH_TIER_PATTERNS[_i3];
if (_pattern2.test(combined)) return 'high';
}
return null; // inconclusive – fall through to device signals
}
/**
* Derive a tier purely from device-level signals (no GPU string needed).
*/
function classifyByDeviceSignals(memoryGB, cores, mobile) {
// Very constrained device
if (memoryGB !== null && memoryGB <= 2) return 'low';
if (cores <= 2) return 'low';
// Well-resourced device
if (memoryGB !== null && memoryGB >= 8 && cores >= 8 && !mobile) return 'high';
if (memoryGB !== null && memoryGB >= 6 && cores >= 6) return 'high';
return 'mid';
}
// ---------------------------------------------------------------------------
// Per-tier render settings
// ---------------------------------------------------------------------------
var RENDER_SETTINGS = {
high: {
// ~4K equivalent
maxPixelCount: 1920 * 1080 * 4,
minPixelRatio: 2
},
mid: {
// ~1080p equivalent
maxPixelCount: 1920 * 1080 * 2,
minPixelRatio: 1
},
low: {
// ~720p max
maxPixelCount: 1280 * 720,
minPixelRatio: 1
},
potato: {
// Software renderer – show static fallback immediately
maxPixelCount: 0,
minPixelRatio: 1
},
unknown: {
// Treat conservatively – same as mid
maxPixelCount: 1920 * 1080 * 2,
minPixelRatio: 1
}
};
/**
* Maps PerformanceLevel → RenderSettings so callers can apply the right
* pixel budget when the level changes. Reuses RENDER_SETTINGS values so the
* two tables stay in sync.
*
* 3 (full) → high tier settings
* 2 (stable) → mid tier settings
* 1 (degraded) → low tier settings
* 0 (fallback) → low tier settings (canvas will be hidden anyway)
*/
var LEVEL_RENDER_SETTINGS = {
3: RENDER_SETTINGS.high,
2: RENDER_SETTINGS.mid,
1: RENDER_SETTINGS.low,
0: RENDER_SETTINGS.low
};
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Builds a PerformanceProfile by inspecting the provided WebGL context and
* available browser / navigator APIs. No async work is performed.
*
* @example
* ```ts
* const profile = PerformanceManager.detect(gl);
* console.log(profile.tier); // 'high' | 'mid' | 'low' | 'unknown'
*
* const mount = new RzpGlassMount(
* el,
* assets,
* {},
* 0,
* profile.renderSettings.minPixelRatio,
* profile.renderSettings.maxPixelCount,
* );
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
var PerformanceManager = /*#__PURE__*/function () {
function PerformanceManager() {
_classCallCheck(this, PerformanceManager);
}
return _createClass(PerformanceManager, null, [{
key: "detect",
value:
/**
* Detect GPU tier and return a full PerformanceProfile.
*
* @param gl - An existing WebGLRenderingContext (e.g. from RzpGlassMount).
* If not provided, a temporary offscreen context is created.
*/
function detect(gl) {
var ownedCanvas = false;
var ctx = gl !== null && gl !== void 0 ? gl : null;
if (!ctx) {
var _canvas$getContext;
var canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
ctx = (_canvas$getContext = canvas.getContext('webgl')) !== null && _canvas$getContext !== void 0 ? _canvas$getContext : null;
ownedCanvas = true;
}
var mobile = isMobileDevice();
var memoryGB = getDeviceMemoryGB();
var cores = getCpuCores();
var hasMajorPerformanceCaveat = checkMajorPerformanceCaveat();
var gpuRenderer = null;
var gpuVendor = null;
if (ctx) {
var _getGpuStrings = getGpuStrings(ctx),
renderer = _getGpuStrings.renderer,
vendor = _getGpuStrings.vendor;
gpuRenderer = renderer;
gpuVendor = vendor;
if (ownedCanvas) {
// Let the browser GC the temp context – no explicit destroy needed
ctx = null;
}
}
// Immediate potato-tier signal from caveat check (software renderer)
var tier;
if (hasMajorPerformanceCaveat) {
tier = 'potato';
} else {
var _classifyByRendererSt;
tier = (_classifyByRendererSt = classifyByRendererString(gpuRenderer, gpuVendor)) !== null && _classifyByRendererSt !== void 0 ? _classifyByRendererSt : classifyByDeviceSignals(memoryGB, cores, mobile);
}
// Downgrade high → mid on mobile (thermal / power constraints)
if (tier === 'high' && mobile) {
tier = 'mid';
}
return {
tier: tier,
gpuRenderer: gpuRenderer,
gpuVendor: gpuVendor,
deviceMemory: memoryGB,
hardwareConcurrency: cores,
isMobile: mobile,
hasMajorPerformanceCaveat: hasMajorPerformanceCaveat,
renderSettings: RENDER_SETTINGS[tier]
};
}
/**
* Convenience: returns only the recommended RenderSettings without the
* full diagnostic data.
*/
}, {
key: "getRenderSettings",
value: function getRenderSettings(gl) {
return PerformanceManager.detect(gl).renderSettings;
}
}]);
}(); // ---------------------------------------------------------------------------
// WebGLPerformanceController
// ---------------------------------------------------------------------------
/** Performance level: 3 = full quality, 0 = static fallback */
var TIER_INITIAL_STATE = {
high: 3,
mid: 2,
low: 1,
potato: 0,
unknown: 2
};
/**
* Monitors real-time FPS and fires onLevelChange when quality should be adjusted.
* Initial level is seeded from the GPU tier detected by PerformanceManager.
*
* Level semantics:
* 3 – full quality
* 2 – stable
* 1 – degraded
* 0 – static fallback (onLevelChange(0) fired)
*
* @example
* ```ts
* const controller = new WebGLPerformanceController({
* canvas,
* gl,
* onLevelChange: (level) => {
* if (level === 0) showStaticFallback();
* },
* });
*
* // When done:
* controller.dispose();
* ```
*/
var WebGLPerformanceController = /*#__PURE__*/function () {
function WebGLPerformanceController(_ref) {
var _this = this;
var gl = _ref.gl,
_ref$onLevelChange = _ref.onLevelChange,
onLevelChange = _ref$onLevelChange === void 0 ? null : _ref$onLevelChange;
_classCallCheck(this, WebGLPerformanceController);
_defineProperty(this, "cooldown", 3000);
_defineProperty(this, "lastChange", 0);
_defineProperty(this, "frameCount", 0);
_defineProperty(this, "lastTime", performance.now());
_defineProperty(this, "fps", 60);
_defineProperty(this, "rafId", null);
_defineProperty(this, "disposed", false);
_defineProperty(this, "handleVisibilityChange", function () {
if (document.hidden) {
// Pause the monitoring loop — RAF is throttled while hidden and would
// produce artificially low FPS readings that trigger false fallbacks.
if (_this.rafId !== null) {
cancelAnimationFrame(_this.rafId);
_this.rafId = null;
}
} else {
// Reset FPS state so the stale hidden-period sample is discarded,
// then resume monitoring from a clean baseline.
_this.frameCount = 0;
_this.lastTime = performance.now();
_this.fps = 60;
_this.startMonitoring();
}
});
this.onLevelChange = onLevelChange;
var _PerformanceManager$d = PerformanceManager.detect(gl),
tier = _PerformanceManager$d.tier;
this.level = TIER_INITIAL_STATE[tier];
if (this.level === 0) {
this.forceStaticFallback();
return;
}
onLevelChange === null || onLevelChange === void 0 || onLevelChange(this.level);
this.startMonitoring();
document.addEventListener('visibilitychange', this.handleVisibilityChange);
}
return _createClass(WebGLPerformanceController, [{
key: "setLevel",
value: function setLevel(level) {
var _this$onLevelChange;
if (this.level === level) return;
var now = performance.now();
if (now - this.lastChange < this.cooldown) return;
this.level = level;
this.lastChange = now;
if (level === 0) {
this.forceStaticFallback();
return;
}
(_this$onLevelChange = this.onLevelChange) === null || _this$onLevelChange === void 0 || _this$onLevelChange.call(this, level);
}
}, {
key: "forceStaticFallback",
value: function forceStaticFallback() {
var _this$onLevelChange2;
this.level = 0;
(_this$onLevelChange2 = this.onLevelChange) === null || _this$onLevelChange2 === void 0 || _this$onLevelChange2.call(this, 0);
}
}, {
key: "evaluatePerformance",
value: function evaluatePerformance() {
if (this.fps < 20) {
this.setLevel(0);
} else if (this.fps < 40) {
this.setLevel(1);
} else if (this.fps < 55) {
this.setLevel(2);
} else {
this.setLevel(3);
}
}
}, {
key: "startMonitoring",
value: function startMonitoring() {
var _this2 = this;
var _loop = function loop() {
if (_this2.disposed) return;
var now = performance.now();
_this2.frameCount++;
if (now - _this2.lastTime >= 1000) {
_this2.fps = _this2.frameCount;
_this2.frameCount = 0;
_this2.lastTime = now;
_this2.evaluatePerformance();
}
_this2.rafId = requestAnimationFrame(_loop);
};
this.rafId = requestAnimationFrame(_loop);
}
}, {
key: "isPotato",
value: function isPotato() {
return this.level === 0;
}
/** Current performance level (3 = full, 0 = fallback) */
}, {
key: "getLevel",
value: function getLevel() {
return this.level;
}
/** Stop the monitoring loop and release resources */
}, {
key: "dispose",
value: function dispose() {
this.disposed = true;
if (this.rafId !== null) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
}
}]);
}();
export { LEVEL_RENDER_SETTINGS, RENDER_SETTINGS, WebGLPerformanceController };
//# sourceMappingURL=PerformanceManager.js.map