UNPKG

@razorpay/blade

Version:

The Design System that powers Razorpay

456 lines (430 loc) 15.1 kB
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