@erosnicolau/animated-text
Version:
Advanced React animated text component with comprehensive animation effects
272 lines (271 loc) • 10.8 kB
JavaScript
/**
* FPS-Based Performance Monitor - Real-time frame rate monitoring
*/
class FPSPerformanceMonitor {
constructor() {
this.frames = [];
this.lastTime = performance.now();
this.animationId = null;
this.activeAnimations = 0;
this.isMonitoring = false;
this.lowFPSStartTime = 0;
this.performanceModeTimeout = null;
this.manuallyForcedFlag = false; // Track if manually forced to prevent auto-recovery
this.config = {
fpsThreshold: 60, // 60 FPS threshold for production
sampleSize: 60,
activationDelay: 2000,
recoveryTime: 10000,
enableLogging: true,
storageKey: 'fps-performance-mode'
};
this.measureFrame = () => {
const currentTime = performance.now();
const deltaTime = currentTime - this.lastTime;
this.lastTime = currentTime;
// Calculate current frame rate
const frameFPS = 1000 / deltaTime;
// Store in rolling window
this.frames.push(frameFPS);
if (this.frames.length > this.config.sampleSize) {
this.frames.shift();
}
// Calculate average FPS
const averageFPS = this.frames.reduce((a, b) => a + b, 0) / this.frames.length;
// Check for performance issues
this.checkPerformance(averageFPS);
// Continue monitoring if still active
if (this.isMonitoring && this.activeAnimations > 0) {
this.animationId = requestAnimationFrame(this.measureFrame);
}
else {
this.stopMonitoring();
}
};
this.initializeFromStorage();
}
static getInstance() {
if (!FPSPerformanceMonitor.instance) {
FPSPerformanceMonitor.instance = new FPSPerformanceMonitor();
}
return FPSPerformanceMonitor.instance;
}
registerAnimation(animationId) {
this.activeAnimations++;
if (this.config.enableLogging) {
console.log(`[FPS Monitor] Animation registered: ${animationId} (${this.activeAnimations} total active)`);
}
if (!this.isMonitoring) {
this.startMonitoring();
}
}
unregisterAnimation(animationId) {
this.activeAnimations = Math.max(0, this.activeAnimations - 1);
if (this.config.enableLogging) {
console.log(`[FPS Monitor] Animation unregistered: ${animationId} (${this.activeAnimations} total active)`);
}
if (this.activeAnimations === 0) {
this.stopMonitoring();
}
}
startMonitoring() {
if (this.isMonitoring)
return;
this.isMonitoring = true;
this.frames = [];
this.lastTime = performance.now();
this.lowFPSStartTime = 0;
if (this.config.enableLogging) {
console.log('[FPS Monitor] Started monitoring');
}
this.measureFrame();
}
stopMonitoring() {
if (!this.isMonitoring)
return;
this.isMonitoring = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
this.frames = [];
this.lowFPSStartTime = 0;
if (this.config.enableLogging) {
console.log('[FPS Monitor] Stopped monitoring');
}
}
checkPerformance(currentFPS) {
// Don't auto-trigger if manually forced
if (this.manuallyForcedFlag) {
return;
}
const now = performance.now();
if (currentFPS < this.config.fpsThreshold) {
if (this.lowFPSStartTime === 0) {
this.lowFPSStartTime = now;
}
else if (now - this.lowFPSStartTime >= this.config.activationDelay) {
this.activatePerformanceMode(false); // false = auto-triggered
this.lowFPSStartTime = 0;
}
}
else {
this.lowFPSStartTime = 0;
}
}
activatePerformanceMode(isManual = false) {
try {
localStorage.setItem(this.config.storageKey, 'true');
localStorage.setItem(`${this.config.storageKey}-timestamp`, Date.now().toString());
if (isManual) {
this.manuallyForcedFlag = true;
localStorage.setItem(`${this.config.storageKey}-manual`, 'true');
}
// Dispatch storage event to notify all components
window.dispatchEvent(new StorageEvent('storage', {
key: this.config.storageKey,
newValue: 'true',
storageArea: localStorage
}));
if (this.config.enableLogging) {
console.warn(`[FPS Monitor] Performance mode ACTIVATED ${isManual ? '(manually forced)' : `(FPS below ${this.config.fpsThreshold})`}`);
}
// Clear any existing recovery timer
if (this.performanceModeTimeout) {
clearTimeout(this.performanceModeTimeout);
}
// Only set recovery timer if not manually forced
if (!this.manuallyForcedFlag) {
this.performanceModeTimeout = setTimeout(() => {
this.deactivatePerformanceMode();
}, this.config.recoveryTime);
}
}
catch {
if (this.config.enableLogging) {
console.warn('[FPS Monitor] localStorage unavailable');
}
}
}
deactivatePerformanceMode() {
try {
localStorage.removeItem(this.config.storageKey);
localStorage.removeItem(`${this.config.storageKey}-timestamp`);
localStorage.removeItem(`${this.config.storageKey}-manual`);
this.manuallyForcedFlag = false;
// Dispatch storage event to notify all components
window.dispatchEvent(new StorageEvent('storage', {
key: this.config.storageKey,
newValue: null,
storageArea: localStorage
}));
if (this.config.enableLogging) {
console.log('[FPS Monitor] Performance mode DEACTIVATED - system recovered');
}
if (this.performanceModeTimeout) {
clearTimeout(this.performanceModeTimeout);
this.performanceModeTimeout = null;
}
}
catch {
if (this.config.enableLogging) {
console.warn('[FPS Monitor] localStorage unavailable during deactivation');
}
}
}
isPerformanceModeActive() {
try {
return localStorage.getItem(this.config.storageKey) === 'true';
}
catch {
return false;
}
}
isManuallyForced() {
try {
return localStorage.getItem(`${this.config.storageKey}-manual`) === 'true';
}
catch {
return false;
}
}
getMetrics() {
const currentFPS = this.frames.length > 0 ? this.frames.reduce((a, b) => a + b, 0) / this.frames.length : 0;
const timeInLowFPS = this.lowFPSStartTime > 0 ? performance.now() - this.lowFPSStartTime : 0;
return {
currentFPS: Math.round(currentFPS * 10) / 10,
isMonitoring: this.isMonitoring,
activeAnimations: this.activeAnimations,
isPerformanceModeActive: this.isPerformanceModeActive(),
isManuallyForced: this.isManuallyForced(),
fpsThreshold: this.config.fpsThreshold,
timeInLowFPS
};
}
updateFPSThreshold(newThreshold) {
this.config.fpsThreshold = newThreshold; // No limits for debugging
if (this.config.enableLogging) {
console.log(`[FPS Monitor] FPS threshold updated to ${this.config.fpsThreshold}`);
}
}
initializeFromStorage() {
try {
const isActive = this.isPerformanceModeActive();
const isManual = this.isManuallyForced();
if (isActive) {
this.manuallyForcedFlag = isManual;
if (isManual) {
// If manually forced, don't set any recovery timer
if (this.config.enableLogging) {
console.log('[FPS Monitor] Resumed manually forced performance mode');
}
}
else {
// If auto-triggered, continue with recovery timer
const timestamp = localStorage.getItem(`${this.config.storageKey}-timestamp`);
if (timestamp) {
const activatedTime = parseInt(timestamp, 10);
const timeElapsed = Date.now() - activatedTime;
const remainingTime = Math.max(0, this.config.recoveryTime - timeElapsed);
if (remainingTime > 0) {
this.performanceModeTimeout = setTimeout(() => {
this.deactivatePerformanceMode();
}, remainingTime);
if (this.config.enableLogging) {
console.log(`[FPS Monitor] Resumed auto-triggered performance mode - ${Math.round(remainingTime / 1000)}s remaining`);
}
}
else {
this.deactivatePerformanceMode();
}
}
}
}
}
catch {
if (this.config.enableLogging) {
console.warn('[FPS Monitor] Could not initialize from localStorage');
}
}
}
forceActivatePerformanceMode() {
this.activatePerformanceMode(true); // true = manually forced
}
forceDeactivatePerformanceMode() {
this.deactivatePerformanceMode();
}
reset() {
this.stopMonitoring();
this.activeAnimations = 0;
this.manuallyForcedFlag = false;
this.deactivatePerformanceMode();
}
}
// Export singleton instance
export const fpsPerformanceMonitor = FPSPerformanceMonitor.getInstance();
// Export convenient static methods
export const registerAnimation = (animationId) => fpsPerformanceMonitor.registerAnimation(animationId);
export const unregisterAnimation = (animationId) => fpsPerformanceMonitor.unregisterAnimation(animationId);
export const isPerformanceModeActive = () => fpsPerformanceMonitor.isPerformanceModeActive();
export const getFPSMetrics = () => fpsPerformanceMonitor.getMetrics();
export const updateFPSThreshold = (threshold) => fpsPerformanceMonitor.updateFPSThreshold(threshold);