Implement performance optimizations for chart updates: adaptive throttling, data sampling, dynamic bin adjustment, and request batching

This commit is contained in:
2025-08-27 08:21:58 +00:00
parent f8bb35e5be
commit c81d0b780d
2 changed files with 257 additions and 23 deletions

View File

@@ -374,6 +374,16 @@ def get_wealth_distribution(simulation_id: str):
if num_bins < 1 or num_bins > 50: if num_bins < 1 or num_bins > 50:
num_bins = 10 # Default to 10 bins num_bins = 10 # Default to 10 bins
# Optimize bin count based on agent count for better performance
agent_count = len(simulation.agents) if simulation.agents else 0
if agent_count > 0:
# Reduce bin count for small agent populations to improve performance
if agent_count < 50 and num_bins > agent_count // 2:
num_bins = max(3, agent_count // 2)
# Cap bin count for very large simulations to prevent performance issues
elif agent_count > 1000 and num_bins > 25:
num_bins = 25
# Get histogram data # Get histogram data
bin_labels, bin_counts = simulation.get_wealth_histogram(num_bins) bin_labels, bin_counts = simulation.get_wealth_histogram(num_bins)

View File

@@ -714,7 +714,8 @@ function startFallbackPolling() {
} }
try { try {
const response = await window.MarkovEconomics.utils.apiRequest( // Use enhanced API request with batching support
const response = await enhancedApiRequest(
`/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true` `/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true`
); );
@@ -772,8 +773,8 @@ async function startSimulation() {
// Update UI state // Update UI state
updateUIState('starting'); updateUIState('starting');
// Create simulation // Create simulation using enhanced API request
const createResponse = await window.MarkovEconomics.utils.apiRequest('/api/simulation', { const createResponse = await enhancedApiRequest('/api/simulation', {
method: 'POST', method: 'POST',
body: JSON.stringify(parameters) body: JSON.stringify(parameters)
}); });
@@ -796,8 +797,8 @@ async function startSimulation() {
debugLog('WebSocket not available, will use fallback polling'); debugLog('WebSocket not available, will use fallback polling');
} }
// Start simulation // Start simulation using enhanced API request
await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/start`, { await enhancedApiRequest(`/api/simulation/${currentSimulation.id}/start`, {
method: 'POST' method: 'POST'
}); });
@@ -825,7 +826,8 @@ async function stopSimulation() {
if (!currentSimulation.id) return; if (!currentSimulation.id) return;
try { try {
await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/stop`, { // Use enhanced API request with batching support
await enhancedApiRequest(`/api/simulation/${currentSimulation.id}/stop`, {
method: 'POST' method: 'POST'
}); });
@@ -989,15 +991,47 @@ function updateUIState(state) {
} }
/** /**
* Update simulation progress * Calculate adaptive update interval based on simulation parameters
* Reduces update frequency for larger simulations to improve performance
*/ */
function updateSimulationProgress(data) { function getAdaptiveUpdateInterval() {
// Update progress bar if (!currentSimulation.parameters.num_agents || !currentSimulation.parameters.iterations) {
const progressBar = document.getElementById('progressBar'); return 50; // Default to 50ms
const progressText = document.getElementById('progressText'); }
if (progressBar && progressText) { const agentCount = currentSimulation.parameters.num_agents;
const percentage = data.progress_percentage || 0; const iterationCount = currentSimulation.parameters.iterations;
// Base interval (ms)
let interval = 50;
// Increase interval for larger simulations
if (agentCount > 1000) {
interval = Math.min(200, 50 + (agentCount / 100));
} else if (agentCount > 500) {
interval = 100;
}
// Further adjust based on iteration count
if (iterationCount > 50000) {
interval *= 2;
} else if (iterationCount > 10000) {
interval *= 1.5;
}
// Cap at reasonable values
return Math.min(500, Math.max(20, interval));
}
/**
* Handle simulation progress updates
*/
function onSimulationProgress(data) {
// Update progress bar
const progressBar = document.getElementById('simulationProgressBar');
const progressText = document.getElementById('progressText');
if (progressBar && progressText && data.progress_percentage !== undefined) {
const percentage = Math.min(100, Math.max(0, data.progress_percentage));
progressBar.style.width = percentage + '%'; progressBar.style.width = percentage + '%';
progressText.textContent = percentage.toFixed(1) + '%'; progressText.textContent = percentage.toFixed(1) + '%';
} }
@@ -1027,10 +1061,20 @@ function updateSimulationProgress(data) {
// Store the distribution data properly // Store the distribution data properly
currentSimulation.data.distribution.labels = [...data.distribution.labels]; currentSimulation.data.distribution.labels = [...data.distribution.labels];
currentSimulation.data.distribution.counts = [...data.distribution.counts]; currentSimulation.data.distribution.counts = [...data.distribution.counts];
} else if (data.iteration % Math.max(1, Math.floor(currentSimulation.parameters.iterations / 50)) === 0) {
// Periodically fetch distribution data if not provided in the update
// But less frequently for larger simulations
fetchDistributionData().then(histogram => {
if (histogram && histogram.labels && histogram.counts) {
currentSimulation.data.distribution.labels = [...histogram.labels];
currentSimulation.data.distribution.counts = [...histogram.counts];
}
});
} }
// Throttle chart updates to improve performance // Adaptive throttling based on simulation size
if (!window.lastChartUpdate || (Date.now() - window.lastChartUpdate) > 50) { const adaptiveInterval = getAdaptiveUpdateInterval();
if (!window.lastChartUpdate || (Date.now() - window.lastChartUpdate) > adaptiveInterval) {
updateCharts(); updateCharts();
window.lastChartUpdate = Date.now(); window.lastChartUpdate = Date.now();
} }
@@ -1040,21 +1084,80 @@ function updateSimulationProgress(data) {
} }
/** /**
* Update charts with new data * Sample data for chart rendering to improve performance with large datasets
* @param {Array} data - Array of data points
* @param {number} maxPoints - Maximum number of points to display
* @returns {Array} - Sampled data
*/
function sampleDataForChart(data, maxPoints = 200) {
if (!Array.isArray(data) || data.length <= maxPoints) {
return data;
}
const sampled = [];
const step = Math.ceil(data.length / maxPoints);
for (let i = 0; i < data.length; i += step) {
sampled.push(data[i]);
}
return sampled;
}
/**
* Sample time series data for chart rendering
* @param {Object} chartData - Object containing time series data arrays
* @param {number} maxPoints - Maximum number of points to display
* @returns {Object} - Object with sampled data arrays
*/
function sampleTimeSeriesData(chartData, maxPoints = 200) {
if (!chartData.iterations || chartData.iterations.length <= maxPoints) {
return chartData;
}
const step = Math.ceil(chartData.iterations.length / maxPoints);
return {
iterations: sampleDataForChart(chartData.iterations, maxPoints),
totalWealth: sampleDataForChart(chartData.totalWealth, maxPoints),
giniCoefficients: sampleDataForChart(chartData.giniCoefficients, maxPoints),
capitalShare: sampleDataForChart(chartData.capitalShare, maxPoints),
top10Share: sampleDataForChart(chartData.top10Share, maxPoints)
};
}
/**
* Update charts with new data using sampling optimization
*/ */
function updateCharts() { function updateCharts() {
// Determine maximum points based on agent count for better performance
const maxChartPoints = currentSimulation.parameters.num_agents > 1000 ? 100 : 200;
// Wealth Evolution Chart // Wealth Evolution Chart
if (charts.wealthEvolution) { if (charts.wealthEvolution) {
charts.wealthEvolution.data.labels = currentSimulation.data.iterations; // Sample data for large datasets
charts.wealthEvolution.data.datasets[0].data = currentSimulation.data.totalWealth; const sampledData = sampleTimeSeriesData({
iterations: currentSimulation.data.iterations,
totalWealth: currentSimulation.data.totalWealth
}, maxChartPoints);
charts.wealthEvolution.data.labels = sampledData.iterations;
charts.wealthEvolution.data.datasets[0].data = sampledData.totalWealth;
charts.wealthEvolution.update('none'); charts.wealthEvolution.update('none');
} }
// Inequality Chart // Inequality Chart
if (charts.inequality) { if (charts.inequality) {
charts.inequality.data.labels = currentSimulation.data.iterations; // Sample data for large datasets
charts.inequality.data.datasets[0].data = currentSimulation.data.giniCoefficients; const sampledData = sampleTimeSeriesData({
charts.inequality.data.datasets[1].data = currentSimulation.data.top10Share; iterations: currentSimulation.data.iterations,
giniCoefficients: currentSimulation.data.giniCoefficients,
top10Share: currentSimulation.data.top10Share
}, maxChartPoints);
charts.inequality.data.labels = sampledData.iterations;
charts.inequality.data.datasets[0].data = sampledData.giniCoefficients;
charts.inequality.data.datasets[1].data = sampledData.top10Share;
charts.inequality.update('none'); charts.inequality.update('none');
} }
@@ -1135,7 +1238,8 @@ async function onSimulationComplete(data) {
// Fetch complete simulation data and populate charts // Fetch complete simulation data and populate charts
try { try {
debugLog('Fetching complete simulation data...'); debugLog('Fetching complete simulation data...');
const response = await window.MarkovEconomics.utils.apiRequest( // Use enhanced API request with batching support
const response = await enhancedApiRequest(
`/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true` `/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true`
); );
@@ -1169,7 +1273,8 @@ async function onSimulationComplete(data) {
// Fallback: try to get distribution data from dedicated endpoint // Fallback: try to get distribution data from dedicated endpoint
try { try {
debugLog('Attempting fallback distribution fetch...'); debugLog('Attempting fallback distribution fetch...');
const distResponse = await window.MarkovEconomics.utils.apiRequest( // Use enhanced API request with batching support
const distResponse = await enhancedApiRequest(
`/api/simulation/${currentSimulation.id}/distribution?bins=10` `/api/simulation/${currentSimulation.id}/distribution?bins=10`
); );
if (distResponse.histogram && distResponse.histogram.labels && distResponse.histogram.counts) { if (distResponse.histogram && distResponse.histogram.labels && distResponse.histogram.counts) {
@@ -1276,6 +1381,125 @@ function downloadFile(content, filename, contentType) {
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
} }
/**
* Calculate optimal bin count for distribution chart based on agent count
* @param {number} agentCount - Number of agents in the simulation
* @returns {number} - Optimal number of bins
*/
function getOptimalBinCount(agentCount) {
if (agentCount < 50) {
return Math.max(5, Math.floor(agentCount / 5));
} else if (agentCount < 200) {
return 10;
} else if (agentCount < 1000) {
return 15;
} else if (agentCount < 5000) {
return 20;
} else {
return 25; // Cap at 25 bins for very large simulations
}
}
/**
* Fetch distribution data with dynamic bin count using enhanced API requests
*/
async function fetchDistributionData() {
if (!currentSimulation.id) return null;
try {
// Calculate optimal bin count based on agent count
const binCount = getOptimalBinCount(currentSimulation.parameters.num_agents || 100);
// Use enhanced API request with batching support
const response = await enhancedApiRequest(
`/api/simulation/${currentSimulation.id}/distribution?bins=${binCount}`
);
return response.histogram;
} catch (error) {
debugLog('Error fetching distribution data', error);
return null;
}
}
/**
* Batch API requests to reduce network overhead
*/
class ApiRequestBatcher {
constructor() {
this.pendingRequests = [];
this.batchTimeout = null;
this.maxBatchSize = 5;
}
/**
* Add a request to the batch
*/
addRequest(url, options = {}) {
return new Promise((resolve, reject) => {
this.pendingRequests.push({ url, options, resolve, reject });
// If we've reached max batch size, flush immediately
if (this.pendingRequests.length >= this.maxBatchSize) {
this.flush();
} else if (!this.batchTimeout) {
// Otherwise, schedule a flush
this.batchTimeout = setTimeout(() => this.flush(), 50);
}
});
}
/**
* Flush all pending requests
*/
async flush() {
if (this.batchTimeout) {
clearTimeout(this.batchTimeout);
this.batchTimeout = null;
}
if (this.pendingRequests.length === 0) return;
// For now, we'll process requests individually since our API doesn't support batching
// In a real implementation, this would send a single batched request
const requests = [...this.pendingRequests];
this.pendingRequests = [];
// Process all requests
for (const request of requests) {
try {
const response = await fetch(request.url, request.options);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
request.resolve(data);
} catch (error) {
request.reject(error);
}
}
}
}
// Create a global instance
window.apiBatcher = new ApiRequestBatcher();
/**
* Enhanced API request function with batching support
*/
async function enhancedApiRequest(url, options = {}) {
// Use batching for certain types of requests
const shouldBatch = url.includes('/data') || url.includes('/distribution');
if (shouldBatch) {
return window.apiBatcher.addRequest(url, options);
} else {
// Fall back to regular API request for non-batchable requests
return window.MarkovEconomics.utils.apiRequest(url, options);
}
}
// Initialize when DOM is ready // Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Initial inequality warning update // Initial inequality warning update