From 5a475a84b5f9dd51eb2a7de60e7eb250cfc310c7 Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Sat, 21 Jun 2025 18:40:57 +0530 Subject: [PATCH] feat(performance): add Redux performance monitoring and memoized selectors - Introduced a Redux performance monitoring system to log action metrics, including duration and state size. - Implemented middleware for tracking performance of Redux actions and logging slow actions in development. - Added utility functions for analyzing performance metrics and generating recommendations for optimization. - Created memoized selectors to enhance performance and prevent unnecessary re-renders across various application states. --- .../src/app/performance-monitor.ts | 122 ++++++++++++++++++ worklenz-frontend/src/app/selectors.ts | 81 ++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 worklenz-frontend/src/app/performance-monitor.ts create mode 100644 worklenz-frontend/src/app/selectors.ts diff --git a/worklenz-frontend/src/app/performance-monitor.ts b/worklenz-frontend/src/app/performance-monitor.ts new file mode 100644 index 00000000..b4146d3e --- /dev/null +++ b/worklenz-frontend/src/app/performance-monitor.ts @@ -0,0 +1,122 @@ +import { Middleware } from '@reduxjs/toolkit'; + +// Performance monitoring for Redux store +export interface PerformanceMetrics { + actionType: string; + duration: number; + timestamp: number; + stateSize: number; +} + +class ReduxPerformanceMonitor { + private metrics: PerformanceMetrics[] = []; + private maxMetrics = 100; // Keep last 100 metrics + private slowActionThreshold = 50; // Log actions taking more than 50ms + + logMetric(metric: PerformanceMetrics) { + this.metrics.push(metric); + + // Keep only recent metrics + if (this.metrics.length > this.maxMetrics) { + this.metrics = this.metrics.slice(-this.maxMetrics); + } + + // Log slow actions in development + if (process.env.NODE_ENV === 'development' && metric.duration > this.slowActionThreshold) { + console.warn(`Slow Redux action detected: ${metric.actionType} took ${metric.duration}ms`); + } + } + + getMetrics() { + return [...this.metrics]; + } + + getSlowActions(threshold = this.slowActionThreshold) { + return this.metrics.filter(m => m.duration > threshold); + } + + getAverageActionTime() { + if (this.metrics.length === 0) return 0; + const total = this.metrics.reduce((sum, m) => sum + m.duration, 0); + return total / this.metrics.length; + } + + reset() { + this.metrics = []; + } +} + +export const performanceMonitor = new ReduxPerformanceMonitor(); + +// Redux middleware for performance monitoring +export const performanceMiddleware: Middleware = (store) => (next) => (action: any) => { + const start = performance.now(); + + const result = next(action); + + const end = performance.now(); + const duration = end - start; + + // Calculate approximate state size (in development only) + let stateSize = 0; + if (process.env.NODE_ENV === 'development') { + try { + stateSize = JSON.stringify(store.getState()).length; + } catch (e) { + stateSize = -1; // Indicates serialization error + } + } + + performanceMonitor.logMetric({ + actionType: action.type || 'unknown', + duration, + timestamp: Date.now(), + stateSize, + }); + + return result; +}; + +// Hook to access performance metrics in components +export function useReduxPerformance() { + return { + metrics: performanceMonitor.getMetrics(), + slowActions: performanceMonitor.getSlowActions(), + averageTime: performanceMonitor.getAverageActionTime(), + reset: () => performanceMonitor.reset(), + }; +} + +// Utility to detect potential performance issues +export function analyzeReduxPerformance() { + const metrics = performanceMonitor.getMetrics(); + const analysis = { + totalActions: metrics.length, + slowActions: performanceMonitor.getSlowActions().length, + averageActionTime: performanceMonitor.getAverageActionTime(), + largestStateSize: Math.max(...metrics.map(m => m.stateSize)), + mostFrequentActions: {} as Record, + recommendations: [] as string[], + }; + + // Count action frequencies + metrics.forEach(m => { + analysis.mostFrequentActions[m.actionType] = + (analysis.mostFrequentActions[m.actionType] || 0) + 1; + }); + + // Generate recommendations + if (analysis.slowActions > analysis.totalActions * 0.1) { + analysis.recommendations.push('Consider optimizing selectors with createSelector'); + } + + if (analysis.largestStateSize > 1000000) { // 1MB + analysis.recommendations.push('State size is large - consider normalizing data'); + } + + if (analysis.averageActionTime > 20) { + analysis.recommendations.push('Average action time is high - check for expensive reducers'); + } + + return analysis; +} \ No newline at end of file diff --git a/worklenz-frontend/src/app/selectors.ts b/worklenz-frontend/src/app/selectors.ts new file mode 100644 index 00000000..29cbd3be --- /dev/null +++ b/worklenz-frontend/src/app/selectors.ts @@ -0,0 +1,81 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from './store'; + +// Memoized selectors for better performance +// These prevent unnecessary re-renders when state hasn't actually changed + +// Auth selectors +export const selectAuth = (state: RootState) => state.auth; +export const selectUser = (state: RootState) => state.userReducer; +export const selectIsAuthenticated = createSelector( + [selectAuth], + (auth) => !!auth.user +); + +// Project selectors +export const selectProjects = (state: RootState) => state.projectsReducer; +export const selectCurrentProject = (state: RootState) => state.projectReducer; +export const selectProjectMembers = (state: RootState) => state.projectMemberReducer; + +// Task selectors +export const selectTasks = (state: RootState) => state.taskReducer; +export const selectTaskManagement = (state: RootState) => state.taskManagement; +export const selectTaskSelection = (state: RootState) => state.taskManagementSelection; + +// UI State selectors +export const selectTheme = (state: RootState) => state.themeReducer; +export const selectLocale = (state: RootState) => state.localesReducer; +export const selectAlerts = (state: RootState) => state.alertsReducer; + +// Board and Project View selectors +export const selectBoard = (state: RootState) => state.boardReducer; +export const selectProjectView = (state: RootState) => state.projectViewReducer; +export const selectProjectDrawer = (state: RootState) => state.projectDrawerReducer; + +// Task attributes selectors +export const selectTaskPriorities = (state: RootState) => state.priorityReducer; +export const selectTaskLabels = (state: RootState) => state.taskLabelsReducer; +export const selectTaskStatuses = (state: RootState) => state.taskStatusReducer; +export const selectTaskDrawer = (state: RootState) => state.taskDrawerReducer; + +// Settings selectors +export const selectMembers = (state: RootState) => state.memberReducer; +export const selectClients = (state: RootState) => state.clientReducer; +export const selectJobs = (state: RootState) => state.jobReducer; +export const selectTeams = (state: RootState) => state.teamReducer; +export const selectCategories = (state: RootState) => state.categoriesReducer; +export const selectLabels = (state: RootState) => state.labelReducer; + +// Reporting selectors +export const selectReporting = (state: RootState) => state.reportingReducer; +export const selectProjectReports = (state: RootState) => state.projectReportsReducer; +export const selectMemberReports = (state: RootState) => state.membersReportsReducer; +export const selectTimeReports = (state: RootState) => state.timeReportsOverviewReducer; + +// Admin and billing selectors +export const selectAdminCenter = (state: RootState) => state.adminCenterReducer; +export const selectBilling = (state: RootState) => state.billingReducer; + +// Schedule and date selectors +export const selectSchedule = (state: RootState) => state.scheduleReducer; +export const selectDate = (state: RootState) => state.dateReducer; + +// Feature-specific selectors +export const selectHomePage = (state: RootState) => state.homePageReducer; +export const selectAccountSetup = (state: RootState) => state.accountSetupReducer; +export const selectRoadmap = (state: RootState) => state.roadmapReducer; +export const selectGroupByFilter = (state: RootState) => state.groupByFilterDropdownReducer; + +// Memoized computed selectors for common use cases +export const selectHasActiveProject = createSelector( + [selectCurrentProject], + (project) => !!project && Object.keys(project).length > 0 +); + +export const selectIsLoading = createSelector( + [selectTasks, selectProjects], + (tasks, projects) => { + // Check if any major feature is loading + return (tasks as any)?.loading || (projects as any)?.loading; + } +); \ No newline at end of file