polyv-live-cli
Version:
CLI tool for managing PolyV live streaming services.
408 lines • 14 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RenderOptimizer = void 0;
const events_1 = require("events");
const crypto_1 = require("crypto");
class RenderOptimizer extends events_1.EventEmitter {
constructor(config = {}) {
super();
this.componentStates = new Map();
this.virtualNodes = new Map();
this.pendingOperations = new Map();
this.lastFrameTime = 0;
this.frameCount = 0;
this.isRunning = false;
this.config = {
minRenderInterval: 16,
maxRenderInterval: 1000,
enableIncrementalRendering: true,
enableRenderBatching: true,
batchWindow: 16,
enableVirtualDom: true,
targetFrameRate: 60,
enableProfiling: true,
maxPendingRenders: 100,
...config,
};
this.metrics = {
totalRenders: 0,
averageRenderTime: 0,
currentFps: 0,
skippedRenders: 0,
batchedRenders: 0,
incrementalRenders: 0,
virtualDomMemory: 0,
activeComponents: 0,
dirtyComponents: 0,
};
this.setupRenderLoop();
}
start() {
if (this.isRunning) {
return;
}
this.isRunning = true;
this.lastFrameTime = performance.now();
this.emit('optimizerStarted', {
timestamp: Date.now(),
config: this.config,
});
}
stop() {
if (!this.isRunning) {
return;
}
this.isRunning = false;
if (this.batchTimer) {
clearTimeout(this.batchTimer);
this.batchTimer = undefined;
}
if (this.renderTimer) {
clearInterval(this.renderTimer);
this.renderTimer = undefined;
}
this.emit('optimizerStopped', {
timestamp: Date.now(),
metrics: this.getMetrics(),
});
}
registerComponent(componentId, componentType, options = {}) {
const state = {
componentId,
componentType,
lastRenderTime: 0,
contentHash: '',
renderCount: 0,
averageRenderTime: 0,
isDirty: true,
priority: options.priority || 'medium',
isVisible: options.isVisible !== false,
dimensions: options.dimensions || { width: 0, height: 0, x: 0, y: 0 },
};
this.componentStates.set(componentId, state);
this.updateActiveComponentsCount();
this.emit('componentRegistered', {
componentId,
componentType,
state,
timestamp: Date.now(),
});
}
unregisterComponent(componentId) {
this.componentStates.delete(componentId);
this.virtualNodes.delete(componentId);
this.pendingOperations.delete(componentId);
this.updateActiveComponentsCount();
this.emit('componentUnregistered', {
componentId,
timestamp: Date.now(),
});
}
markDirty(componentId, force = false) {
const state = this.componentStates.get(componentId);
if (!state) {
return;
}
state.isDirty = true;
this.updateDirtyComponentsCount();
this.emit('componentMarkedDirty', {
componentId,
force,
timestamp: Date.now(),
});
if (force || this.shouldRenderImmediately(state)) {
this.scheduleRender(componentId, 'high');
}
}
scheduleRender(componentId, priority = 'medium', renderFn, force = false) {
if (!this.isRunning) {
return;
}
const state = this.componentStates.get(componentId);
if (!state && !force) {
return;
}
if (this.pendingOperations.size >= this.config.maxPendingRenders) {
this.emit('renderQueueFull', {
componentId,
queueSize: this.pendingOperations.size,
timestamp: Date.now(),
});
return;
}
const operation = {
componentId,
type: 'full',
priority,
renderFn: renderFn || (() => this.performDefaultRender(componentId)),
queuedAt: Date.now(),
force,
};
const existing = this.pendingOperations.get(componentId);
if (!existing || this.getPriorityValue(priority) > this.getPriorityValue(existing.priority)) {
this.pendingOperations.set(componentId, operation);
}
if (this.config.enableRenderBatching) {
this.scheduleBatchRender();
}
else {
this.processRenderOperation(operation);
}
}
updateContent(componentId, content) {
const state = this.componentStates.get(componentId);
if (!state) {
return false;
}
const newHash = this.generateContentHash(content);
if (newHash !== state.contentHash) {
state.contentHash = newHash;
state.isDirty = true;
this.updateDirtyComponentsCount();
if (this.config.enableVirtualDom) {
this.updateVirtualNode(componentId, content);
}
this.emit('contentUpdated', {
componentId,
contentHash: newHash,
timestamp: Date.now(),
});
return true;
}
return false;
}
updateVisibility(componentId, isVisible) {
const state = this.componentStates.get(componentId);
if (!state) {
return;
}
if (state.isVisible !== isVisible) {
state.isVisible = isVisible;
if (isVisible && state.isDirty) {
this.scheduleRender(componentId, 'medium');
}
this.emit('visibilityChanged', {
componentId,
isVisible,
timestamp: Date.now(),
});
}
}
updateDimensions(componentId, dimensions) {
const state = this.componentStates.get(componentId);
if (!state) {
return;
}
if (dimensions) {
state.dimensions = dimensions;
}
state.isDirty = true;
this.updateDirtyComponentsCount();
this.emit('dimensionsChanged', {
componentId,
dimensions,
timestamp: Date.now(),
});
}
getMetrics() {
this.updateMetrics();
return { ...this.metrics };
}
getComponentStates() {
return new Map(this.componentStates);
}
async flushRenders() {
const dirtyComponents = Array.from(this.componentStates.entries())
.filter(([_, state]) => state.isDirty)
.map(([componentId]) => componentId);
const renderPromises = dirtyComponents.map(componentId => this.performDefaultRender(componentId));
await Promise.all(renderPromises);
this.emit('rendersFlused', {
componentCount: dirtyComponents.length,
timestamp: Date.now(),
});
}
clearPendingRenders() {
this.pendingOperations.clear();
this.emit('pendingRendersCleared', {
timestamp: Date.now(),
});
}
updateConfig(newConfig) {
this.config = { ...this.config, ...newConfig };
this.emit('configUpdated', {
config: this.config,
timestamp: Date.now(),
});
}
generateContentHash(content) {
const serialized = JSON.stringify(content);
return (0, crypto_1.createHash)('md5').update(serialized).digest('hex');
}
shouldRenderImmediately(state) {
const now = Date.now();
const timeSinceLastRender = now - state.lastRenderTime;
return (state.priority === 'high' ||
timeSinceLastRender > this.config.maxRenderInterval ||
!state.isVisible);
}
getPriorityValue(priority) {
switch (priority) {
case 'low': return 1;
case 'medium': return 2;
case 'high': return 3;
default: return 2;
}
}
scheduleBatchRender() {
if (this.batchTimer) {
return;
}
this.batchTimer = setTimeout(() => {
this.processBatchRenders();
this.batchTimer = undefined;
}, this.config.batchWindow);
}
async processBatchRenders() {
if (this.pendingOperations.size === 0) {
return;
}
const operations = Array.from(this.pendingOperations.values())
.sort((a, b) => this.getPriorityValue(b.priority) - this.getPriorityValue(a.priority));
this.pendingOperations.clear();
const batchStartTime = performance.now();
this.emit('batchRenderStarted', {
operationCount: operations.length,
timestamp: Date.now(),
});
const highPriorityOps = operations.filter(op => op.priority === 'high');
const otherOps = operations.filter(op => op.priority !== 'high');
for (const operation of highPriorityOps) {
await this.processRenderOperation(operation);
}
if (otherOps.length > 0) {
await this.processBatchOperations(otherOps);
}
const batchTime = performance.now() - batchStartTime;
this.metrics.batchedRenders += operations.length;
this.emit('batchRenderCompleted', {
operationCount: operations.length,
batchTime,
timestamp: Date.now(),
});
}
async processBatchOperations(operations) {
const frameTimeout = 1000 / this.config.targetFrameRate;
let frameStartTime = performance.now();
for (const operation of operations) {
const now = performance.now();
if (now - frameStartTime > frameTimeout * 0.8) {
await new Promise(resolve => setTimeout(resolve, 0));
frameStartTime = performance.now();
}
await this.processRenderOperation(operation);
}
}
async processRenderOperation(operation) {
const state = this.componentStates.get(operation.componentId);
if (!state && !operation.force) {
return;
}
const now = Date.now();
const renderStartTime = performance.now();
try {
if (state && !operation.force) {
const timeSinceLastRender = now - state.lastRenderTime;
if (timeSinceLastRender < this.config.minRenderInterval) {
this.metrics.skippedRenders++;
return;
}
if (!state.isVisible && state.priority !== 'high') {
this.metrics.skippedRenders++;
return;
}
}
await operation.renderFn();
const renderTime = performance.now() - renderStartTime;
if (state) {
state.lastRenderTime = now;
state.renderCount++;
state.isDirty = false;
state.averageRenderTime = ((state.averageRenderTime * (state.renderCount - 1)) + renderTime) / state.renderCount;
}
this.metrics.totalRenders++;
this.updateAverageRenderTime(renderTime);
this.updateDirtyComponentsCount();
this.emit('renderCompleted', {
componentId: operation.componentId,
renderTime,
operationType: operation.type,
timestamp: now,
});
}
catch (error) {
this.emit('renderError', {
componentId: operation.componentId,
error,
timestamp: now,
});
}
}
async performDefaultRender(componentId) {
await new Promise(resolve => setTimeout(resolve, 1 + Math.random() * 5));
this.emit('defaultRenderPerformed', {
componentId,
timestamp: Date.now(),
});
}
updateVirtualNode(componentId, content) {
const node = {
type: 'component',
props: content.props || {},
children: content.children || [],
key: componentId,
hash: this.generateContentHash(content),
};
this.virtualNodes.set(componentId, node);
this.updateVirtualDomMemory();
}
setupRenderLoop() {
this.renderTimer = setInterval(() => {
if (this.isRunning) {
this.updateFpsMetrics();
}
}, 1000);
}
updateFpsMetrics() {
const now = performance.now();
const deltaTime = now - this.lastFrameTime;
if (deltaTime >= 1000) {
this.metrics.currentFps = this.frameCount;
this.frameCount = 0;
this.lastFrameTime = now;
}
else {
this.frameCount++;
}
}
updateAverageRenderTime(renderTime) {
this.metrics.averageRenderTime = ((this.metrics.averageRenderTime * (this.metrics.totalRenders - 1)) + renderTime) / this.metrics.totalRenders;
}
updateActiveComponentsCount() {
this.metrics.activeComponents = this.componentStates.size;
}
updateDirtyComponentsCount() {
this.metrics.dirtyComponents = Array.from(this.componentStates.values())
.filter(state => state.isDirty).length;
}
updateVirtualDomMemory() {
this.metrics.virtualDomMemory = this.virtualNodes.size * 1024;
}
updateMetrics() {
this.updateActiveComponentsCount();
this.updateDirtyComponentsCount();
this.updateVirtualDomMemory();
}
}
exports.RenderOptimizer = RenderOptimizer;
//# sourceMappingURL=render-optimizer.js.map