import { Logger } from '../utils/Logger.js';
import { EventEmitter } from 'events';
/**
* Component instance tracking data
*/
export interface ComponentInstance {
id: string;
name: string;
filePath: string;
props: Record<string, any>;
state: Record<string, any>;
hooks: HookInstance[];
renderCount: number;
lastRenderTime: number;
renderDuration: number;
totalRenderTime: number;
averageRenderTime: number;
parent?: string;
children: string[];
renderHistory: RenderEvent[];
}
export interface RenderEvent {
timestamp: number;
duration: number;
reason: 'props' | 'state' | 'context' | 'parent' | 'force' | 'unknown';
propsChanged: string[];
stateChanged: string[];
phase: 'mount' | 'update' | 'unmount';
}
export interface HookInstance {
type: string;
index: number;
value: any;
dependencies?: any[];
}
export interface ComponentStateChange {
componentId: string;
componentName: string;
type: 'props' | 'state' | 'hook';
property: string;
oldValue: any;
newValue: any;
timestamp: number;
}
export interface PropsDrillingDetection {
prop: string;
path: string[];
depth: number;
components: string[];
suggestion: string;
}
/**
* Manages React component instances and tracks state changes
*/
export class ComponentManager extends EventEmitter {
private logger: Logger;
private components: Map<string, ComponentInstance> = new Map();
private stateChanges: ComponentStateChange[] = [];
private propsDrillingCache: Map<string, PropsDrillingDetection[]> = new Map();
private renderStartTimes: Map<string, number> = new Map();
constructor() {
super();
this.logger = new Logger('ComponentManager');
}
/**
* Register a new component instance
*/
registerComponent(component: ComponentInstance): void {
this.components.set(component.id, component);
this.logger.debug(`Registered component: ${component.name}`, { id: component.id });
this.emit('componentRegistered', component);
}
/**
* Update component state
*/
updateComponentState(
componentId: string,
property: string,
newValue: any,
type: 'props' | 'state' | 'hook' = 'state'
): void {
const component = this.components.get(componentId);
if (!component) {
this.logger.warn(`Component not found: ${componentId}`);
return;
}
const oldValue = type === 'props' ? component.props[property] : component.state[property];
// Update the component
if (type === 'props') {
component.props[property] = newValue;
} else if (type === 'state') {
component.state[property] = newValue;
}
// Record the state change
const change: ComponentStateChange = {
componentId,
componentName: component.name,
type,
property,
oldValue,
newValue,
timestamp: Date.now()
};
this.stateChanges.push(change);
this.logger.debug(`State change recorded`, change);
this.emit('stateChange', change);
// Trigger re-render tracking
this.trackRender(componentId);
}
/**
* Start tracking a component render
*/
startRender(componentId: string, reason: RenderEvent['reason'] = 'unknown', changedProps: string[] = [], changedState: string[] = []): void {
this.renderStartTimes.set(componentId, performance.now());
this.logger.debug(`Render started for ${componentId}`, {
reason,
changedProps,
changedState
});
}
/**
* Track component render completion
*/
trackRender(componentId: string, reason: RenderEvent['reason'] = 'unknown', changedProps: string[] = [], changedState: string[] = []): void {
const component = this.components.get(componentId);
if (!component) return;
const startTime = this.renderStartTimes.get(componentId);
const endTime = performance.now();
const duration = startTime ? endTime - startTime : 0;
// Update render metrics
component.renderCount++;
component.lastRenderTime = Date.now();
component.renderDuration = duration;
component.totalRenderTime += duration;
component.averageRenderTime = component.totalRenderTime / component.renderCount;
// Add to render history
const renderEvent: RenderEvent = {
timestamp: component.lastRenderTime,
duration,
reason,
propsChanged: changedProps,
stateChanged: changedState,
phase: component.renderCount === 1 ? 'mount' : 'update'
};
component.renderHistory.push(renderEvent);
// Keep only recent render history (last 50 renders)
if (component.renderHistory.length > 50) {
component.renderHistory = component.renderHistory.slice(-50);
}
// Clean up start time
this.renderStartTimes.delete(componentId);
this.logger.debug(`Render tracked for ${component.name}`, {
renderCount: component.renderCount,
duration,
averageRenderTime: component.averageRenderTime,
reason
});
this.emit('componentRender', {
componentId,
componentName: component.name,
renderCount: component.renderCount,
timestamp: component.lastRenderTime,
duration,
reason,
changedProps,
changedState
});
// Emit render metric for performance monitor
this.emit('renderMetric', {
componentName: component.name,
renderTime: duration,
renderCount: component.renderCount,
propsChanged: changedProps.length > 0,
stateChanged: changedState.length > 0,
contextChanged: reason === 'context',
timestamp: component.lastRenderTime,
callStack: this.captureCallStack()
});
}
/**
* Capture call stack for render analysis
*/
private captureCallStack(): string[] {
const stack = new Error().stack;
if (!stack) return [];
return stack
.split('\n')
.slice(2, 10) // Skip first 2 lines and take next 8
.map(line => line.trim())
.filter(line => line.length > 0);
}
/**
* Update component hooks
*/
updateComponentHooks(componentId: string, hooks: HookInstance[]): void {
const component = this.components.get(componentId);
if (!component) return;
const oldHooks = component.hooks;
component.hooks = hooks;
// Check for hook changes
hooks.forEach((hook, index) => {
const oldHook = oldHooks[index];
if (oldHook && oldHook.value !== hook.value) {
this.updateComponentState(componentId, `hook_${hook.type}_${index}`, hook.value, 'hook');
}
});
}
/**
* Get component by ID
*/
getComponent(componentId: string): ComponentInstance | undefined {
return this.components.get(componentId);
}
/**
* Get all components
*/
getAllComponents(): ComponentInstance[] {
return Array.from(this.components.values());
}
/**
* Get components by name
*/
getComponentsByName(name: string): ComponentInstance[] {
return Array.from(this.components.values()).filter(c => c.name === name);
}
/**
* Get component state changes
*/
getStateChanges(componentId?: string, limit: number = 100): ComponentStateChange[] {
let changes = this.stateChanges;
if (componentId) {
changes = changes.filter(c => c.componentId === componentId);
}
return changes
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, limit);
}
/**
* Detect props drilling patterns
*/
detectPropsDrilling(): PropsDrillingDetection[] {
const detections: PropsDrillingDetection[] = [];
const propPaths = new Map<string, string[]>();
// Build prop paths through component tree
this.components.forEach(component => {
Object.keys(component.props).forEach(prop => {
const path = this.buildPropPath(component.id, prop);
if (path.length > 2) { // Props passed through more than 2 levels
const key = `${prop}_${path.join('_')}`;
propPaths.set(key, path);
}
});
});
// Convert to detections
propPaths.forEach((path, key) => {
const prop = key.split('_')[0];
if (prop) {
detections.push({
prop,
path,
depth: path.length,
components: path,
suggestion: `Consider using React Context or state management for prop "${prop}"`
});
}
});
return detections;
}
/**
* Build prop path through component hierarchy
*/
private buildPropPath(componentId: string, prop: string): string[] {
const component = this.components.get(componentId);
if (!component) return [];
const path = [component.name];
// Check if parent has the same prop
if (component.parent) {
const parent = this.components.get(component.parent);
if (parent && parent.props[prop] !== undefined) {
const parentPath = this.buildPropPath(component.parent, prop);
return [...parentPath, ...path];
}
}
return path;
}
/**
* Get comprehensive render performance metrics
*/
getRenderMetrics(): {
totalRenders: number;
averageRenderTime: number;
heavyComponents: Array<{ name: string; renderCount: number; averageRenderTime: number; totalRenderTime: number }>;
recentRenders: Array<{ componentName: string; timestamp: number; duration: number; reason: string }>;
slowRenders: Array<{ componentName: string; duration: number; timestamp: number; reason: string }>;
renderTrends: Array<{ componentName: string; trend: 'improving' | 'degrading' | 'stable' }>;
} {
const components = Array.from(this.components.values());
const totalRenders = components.reduce((sum, c) => sum + c.renderCount, 0);
const totalRenderTime = components.reduce((sum, c) => sum + c.totalRenderTime, 0);
const averageRenderTime = totalRenders > 0 ? totalRenderTime / totalRenders : 0;
// Recent renders with detailed info
const recentRenders = components
.filter(c => c.renderHistory.length > 0)
.flatMap(c => c.renderHistory.slice(-5).map(r => ({
componentName: c.name,
timestamp: r.timestamp,
duration: r.duration,
reason: r.reason
})))
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, 20);
// Heavy components with detailed metrics
const heavyComponents = components
.filter(c => c.renderCount > 5)
.map(c => ({
name: c.name,
renderCount: c.renderCount,
averageRenderTime: c.averageRenderTime,
totalRenderTime: c.totalRenderTime
}))
.sort((a, b) => b.averageRenderTime - a.averageRenderTime)
.slice(0, 10);
// Slow renders (> 16ms for 60fps)
const slowRenders = components
.flatMap(c => c.renderHistory
.filter(r => r.duration > 16)
.map(r => ({
componentName: c.name,
duration: r.duration,
timestamp: r.timestamp,
reason: r.reason
}))
)
.sort((a, b) => b.duration - a.duration)
.slice(0, 15);
// Render trends analysis
const renderTrends = components
.filter(c => c.renderHistory.length >= 10)
.map(c => {
const recent = c.renderHistory.slice(-10);
const older = c.renderHistory.slice(-20, -10);
if (older.length === 0) return { componentName: c.name, trend: 'stable' as const };
const recentAvg = recent.reduce((sum, r) => sum + r.duration, 0) / recent.length;
const olderAvg = older.reduce((sum, r) => sum + r.duration, 0) / older.length;
const improvement = (olderAvg - recentAvg) / olderAvg;
if (improvement > 0.1) return { componentName: c.name, trend: 'improving' as const };
if (improvement < -0.1) return { componentName: c.name, trend: 'degrading' as const };
return { componentName: c.name, trend: 'stable' as const };
});
return {
totalRenders,
averageRenderTime,
heavyComponents,
recentRenders,
slowRenders,
renderTrends
};
}
/**
* Detect unnecessary re-renders
*/
detectUnnecessaryRerenders(): Array<{
componentName: string;
reason: string;
suggestion: string;
}> {
const issues: Array<{ componentName: string; reason: string; suggestion: string }> = [];
this.components.forEach(component => {
// Check for components with high render count but stable props
if (component.renderCount > 20) {
const recentChanges = this.getStateChanges(component.id, 10);
const propChanges = recentChanges.filter(c => c.type === 'props');
if (propChanges.length < component.renderCount * 0.1) {
issues.push({
componentName: component.name,
reason: 'High render count with few prop changes',
suggestion: 'Consider wrapping with React.memo or optimizing parent component'
});
}
}
// Check for components that re-render due to object/array props
const objectProps = Object.entries(component.props).filter(([_, value]) =>
typeof value === 'object' && value !== null
);
if (objectProps.length > 0 && component.renderCount > 5) {
issues.push({
componentName: component.name,
reason: 'Potential object/array prop causing re-renders',
suggestion: 'Consider memoizing object/array props or using useMemo'
});
}
});
return issues;
}
/**
* Clear old state changes to prevent memory leaks
*/
clearOldStateChanges(olderThanMs: number = 300000): void { // 5 minutes default
const cutoff = Date.now() - olderThanMs;
const initialLength = this.stateChanges.length;
this.stateChanges = this.stateChanges.filter(change => change.timestamp > cutoff);
const removed = initialLength - this.stateChanges.length;
if (removed > 0) {
this.logger.debug(`Cleared ${removed} old state changes`);
}
}
/**
* Remove component instance
*/
removeComponent(componentId: string): void {
const component = this.components.get(componentId);
if (component) {
this.components.delete(componentId);
this.logger.debug(`Removed component: ${component.name}`, { id: componentId });
this.emit('componentRemoved', component);
}
}
/**
* Clear all components and state changes
*/
clear(): void {
this.components.clear();
this.stateChanges = [];
this.propsDrillingCache.clear();
this.logger.info('Cleared all component data');
}
/**
* Get component tree structure
*/
getComponentTree(): any {
const tree: any = {};
// Find root components (no parent)
const roots = Array.from(this.components.values()).filter(c => !c.parent);
const buildTree = (component: ComponentInstance): any => {
const children = component.children.map(childId => {
const child = this.components.get(childId);
return child ? buildTree(child) : null;
}).filter(Boolean);
return {
id: component.id,
name: component.name,
renderCount: component.renderCount,
children
};
};
roots.forEach(root => {
tree[root.name] = buildTree(root);
});
return tree;
}
}