Added option to view sample data in the web app.

- Fixed an issue with the CDN link for d3-flamegraph
- Added option for verbose logging to help with debugging
This commit is contained in:
Atharva Sawant
2025-11-28 09:11:13 +05:30
parent 3d4c3f7f04
commit fc7214adaf
3 changed files with 188 additions and 16 deletions

View File

@@ -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."""

View File

@@ -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();
}
});

View File

@@ -5,10 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MiniProfiler</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/d3-flame-graph/4.1.3/d3-flamegraph.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/d3-flamegraph.css">
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-flame-graph/4.1.3/d3-flamegraph.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/d3-flamegraph.min.js"></script>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
</head>
<body>
@@ -33,6 +33,7 @@
<button id="stop-btn" onclick="stopProfiling()" disabled>Stop Profiling</button>
<button id="clear-btn" onclick="clearData()" disabled>Clear Data</button>
<button id="reset-btn" onclick="resetBuffers()" disabled>Reset Buffers</button>
<button id="load-sample-btn" onclick="loadSampleData()" style="background: #4e4e4e;">Load Sample Data</button>
</div>
<div class="status-display">