diff --git a/host/miniprofiler/web_server.py b/host/miniprofiler/web_server.py index 093c4b1..9869a00 100644 --- a/host/miniprofiler/web_server.py +++ b/host/miniprofiler/web_server.py @@ -93,6 +93,119 @@ class ProfilerWebServer: """Get statistics table data.""" return jsonify(self.analyzer.to_statistics_json()) + @self.app.route('/api/sample/flamegraph') + def sample_flamegraph(): + """Get sample flame graph data.""" + import os + import json + sample_file = os.path.join(os.path.dirname(__file__), '../tests/sample_flamegraph.json') + if os.path.exists(sample_file): + with open(sample_file, 'r') as f: + return jsonify(json.load(f)) + return jsonify({"error": "Sample data not found"}), 404 + + @self.app.route('/api/sample/statistics') + def sample_statistics(): + """Get sample statistics data.""" + import os + import json + sample_file = os.path.join(os.path.dirname(__file__), '../tests/sample_statistics.json') + if os.path.exists(sample_file): + with open(sample_file, 'r') as f: + return jsonify(json.load(f)) + return jsonify({"error": "Sample data not found"}), 404 + + @self.app.route('/api/sample/timeline') + def sample_timeline(): + """Get sample timeline data.""" + import os + import json + sample_file = os.path.join(os.path.dirname(__file__), '../tests/sample_timeline.json') + if os.path.exists(sample_file): + with open(sample_file, 'r') as f: + return jsonify(json.load(f)) + return jsonify({"error": "Sample data not found"}), 404 + + @self.app.route('/api/sample/generate') + def generate_sample(): + """Generate sample profiling data on-the-fly.""" + import sys + import os + + # Add tests directory to path to import sample data generator + tests_dir = os.path.join(os.path.dirname(__file__), '../tests') + if tests_dir not in sys.path: + sys.path.insert(0, tests_dir) + + try: + from sample_data_generator import generate_sample_profile_data, FUNCTIONS + + # Generate sample records + records = generate_sample_profile_data(num_iterations=20) + + # Create temporary analyzer to process the data + temp_analyzer = ProfileAnalyzer() + temp_analyzer.add_records(records) + + # Generate all three visualization formats + flamegraph_data = temp_analyzer.to_flamegraph_json() + stats_data = temp_analyzer.to_statistics_json() + timeline_data = temp_analyzer.to_timeline_json() + + # Add human-readable function names to flamegraph + def add_names(node): + """Recursively add function names to flamegraph nodes.""" + if 'name' in node and node['name'].startswith('func_0x'): + try: + # Extract address from "func_0x08000100" format + addr = int(node['name'][5:], 16) # Skip "func_" + if addr in FUNCTIONS: + node['name'] = FUNCTIONS[addr] + except ValueError: + pass + if 'children' in node: + for child in node['children']: + add_names(child) + + add_names(flamegraph_data) + + # Add function names to statistics + logger.debug(f"Processing {len(stats_data)} statistics entries") + logger.debug(f"FUNCTIONS has {len(FUNCTIONS)} entries") + for stat in stats_data: + if stat['name'].startswith('func_0x'): + try: + addr = int(stat['name'][5:], 16) # Skip "func_" + if addr in FUNCTIONS: + old_name = stat['name'] + stat['name'] = FUNCTIONS[addr] + logger.debug(f"Replaced {old_name} with {stat['name']}") + else: + logger.debug(f"Address 0x{addr:08x} not in FUNCTIONS") + except ValueError as e: + logger.error(f"ValueError parsing {stat['name']}: {e}") + + # Add function names to timeline + for event in timeline_data: + if event['name'].startswith('func_0x'): + try: + addr = int(event['name'][5:], 16) # Skip "func_" + if addr in FUNCTIONS: + event['name'] = FUNCTIONS[addr] + except ValueError: + pass + + # Return combined response + return jsonify({ + 'flamegraph': flamegraph_data, + 'statistics': stats_data, + 'timeline': timeline_data + }) + + except Exception as e: + logger.error(f"Error generating sample data: {e}") + return jsonify({"error": str(e)}), 500 + def _setup_socketio_handlers(self): """Setup SocketIO event handlers.""" diff --git a/host/web/static/js/app.js b/host/web/static/js/app.js index 3deeca1..ceed2ae 100644 --- a/host/web/static/js/app.js +++ b/host/web/static/js/app.js @@ -4,7 +4,7 @@ let socket = null; let isConnected = false; let isProfiling = false; -let flamegraph = null; +let flamegraphChart = null; // Initialize on page load document.addEventListener('DOMContentLoaded', function() { @@ -208,38 +208,53 @@ function showTab(tabName) { function initializeFlameGraph() { const width = document.getElementById('flamegraph').offsetWidth; - flamegraph = d3.flamegraph() + flamegraph = flamegraph() .width(width) .cellHeight(18) .transitionDuration(750) .minFrameSize(5) .transitionEase(d3.easeCubic) - .sort(true) - .title("") - .differential(false) - .selfValue(false); + .sort(true); // Color scheme - flamegraph.setColorMapper((d, originalColor) => { + flamegraphChart.setColorMapper((d, originalColor) => { // Color by depth for better visualization - const hue = (d.data.depth * 30) % 360; + const depth = d.depth || 0; + const hue = (depth * 30) % 360; return d3.hsl(hue, 0.6, 0.5); }); d3.select("#flamegraph") .datum({name: "root", value: 0, children: []}) - .call(flamegraph); + .call(flamegraphChart); } function updateFlameGraph(data) { - if (!flamegraph) { - initializeFlameGraph(); + // Initialize if not already created + if (!flamegraphChart) { + const width = document.getElementById('flamegraph').offsetWidth; + + flamegraphChart = flamegraph() + .width(width) + .cellHeight(18) + .transitionDuration(750) + .minFrameSize(5) + .transitionEase(d3.easeCubic) + .sort(true); + + // Color scheme + flamegraphChart.setColorMapper((d, originalColor) => { + // Color by depth for better visualization + const depth = d.depth || 0; + const hue = (depth * 30) % 360; + return d3.hsl(hue, 0.6, 0.5); + }); } // Update flame graph with new data d3.select("#flamegraph") .datum(data) - .call(flamegraph); + .call(flamegraphChart); } // Timeline Visualization @@ -330,11 +345,54 @@ function updateStatistics(data) { tbody.innerHTML = html; } +// Load Sample Data +async function loadSampleData() { + try { + updateStatus('Generating sample data...', false); + + // Generate and fetch sample data in one call + const response = await fetch('/api/sample/generate'); + + if (!response.ok) { + throw new Error('Failed to generate sample data'); + } + + const data = await response.json(); + const { flamegraph: flamegraphData, statistics: statsData, timeline: timelineData } = data; + + // Update all visualizations + updateFlameGraph(flamegraphData); + updateStatistics(statsData); + updateTimeline(timelineData); + + // Update summary + const totalRecords = timelineData.length; + const totalFunctions = statsData.length; + const totalTime = statsData.reduce((sum, s) => sum + s.total_us, 0); + const hottest = statsData[0]; // Already sorted by total_us + + updateSummary({ + total_records: totalRecords, + total_functions: totalFunctions, + total_time_us: totalTime, + hottest_function: hottest ? hottest.name : null, + hottest_time_us: hottest ? hottest.total_us : 0 + }); + + updateStatus('Sample data loaded', false); + console.log('Sample data loaded successfully'); + } catch (error) { + console.error('Error loading sample data:', error); + alert(`Error loading sample data: ${error.message}`); + updateStatus('Error loading sample data', false); + } +} + // Handle window resize for flame graph window.addEventListener('resize', function() { if (flamegraph) { const width = document.getElementById('flamegraph').offsetWidth; - flamegraph.width(width); - flamegraph.update(); + flamegraphChart.width(width); + flamegraphChart.update(); } }); diff --git a/host/web/templates/index.html b/host/web/templates/index.html index c553b0e..67b4d0c 100644 --- a/host/web/templates/index.html +++ b/host/web/templates/index.html @@ -5,10 +5,10 @@