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:
@@ -93,6 +93,119 @@ class ProfilerWebServer:
|
|||||||
"""Get statistics table data."""
|
"""Get statistics table data."""
|
||||||
return jsonify(self.analyzer.to_statistics_json())
|
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):
|
def _setup_socketio_handlers(self):
|
||||||
"""Setup SocketIO event handlers."""
|
"""Setup SocketIO event handlers."""
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
let socket = null;
|
let socket = null;
|
||||||
let isConnected = false;
|
let isConnected = false;
|
||||||
let isProfiling = false;
|
let isProfiling = false;
|
||||||
let flamegraph = null;
|
let flamegraphChart = null;
|
||||||
|
|
||||||
// Initialize on page load
|
// Initialize on page load
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
@@ -208,38 +208,53 @@ function showTab(tabName) {
|
|||||||
function initializeFlameGraph() {
|
function initializeFlameGraph() {
|
||||||
const width = document.getElementById('flamegraph').offsetWidth;
|
const width = document.getElementById('flamegraph').offsetWidth;
|
||||||
|
|
||||||
flamegraph = d3.flamegraph()
|
flamegraph = flamegraph()
|
||||||
.width(width)
|
.width(width)
|
||||||
.cellHeight(18)
|
.cellHeight(18)
|
||||||
.transitionDuration(750)
|
.transitionDuration(750)
|
||||||
.minFrameSize(5)
|
.minFrameSize(5)
|
||||||
.transitionEase(d3.easeCubic)
|
.transitionEase(d3.easeCubic)
|
||||||
.sort(true)
|
.sort(true);
|
||||||
.title("")
|
|
||||||
.differential(false)
|
|
||||||
.selfValue(false);
|
|
||||||
|
|
||||||
// Color scheme
|
// Color scheme
|
||||||
flamegraph.setColorMapper((d, originalColor) => {
|
flamegraphChart.setColorMapper((d, originalColor) => {
|
||||||
// Color by depth for better visualization
|
// 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);
|
return d3.hsl(hue, 0.6, 0.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
d3.select("#flamegraph")
|
d3.select("#flamegraph")
|
||||||
.datum({name: "root", value: 0, children: []})
|
.datum({name: "root", value: 0, children: []})
|
||||||
.call(flamegraph);
|
.call(flamegraphChart);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFlameGraph(data) {
|
function updateFlameGraph(data) {
|
||||||
if (!flamegraph) {
|
// Initialize if not already created
|
||||||
initializeFlameGraph();
|
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
|
// Update flame graph with new data
|
||||||
d3.select("#flamegraph")
|
d3.select("#flamegraph")
|
||||||
.datum(data)
|
.datum(data)
|
||||||
.call(flamegraph);
|
.call(flamegraphChart);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeline Visualization
|
// Timeline Visualization
|
||||||
@@ -330,11 +345,54 @@ function updateStatistics(data) {
|
|||||||
tbody.innerHTML = html;
|
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
|
// Handle window resize for flame graph
|
||||||
window.addEventListener('resize', function() {
|
window.addEventListener('resize', function() {
|
||||||
if (flamegraph) {
|
if (flamegraph) {
|
||||||
const width = document.getElementById('flamegraph').offsetWidth;
|
const width = document.getElementById('flamegraph').offsetWidth;
|
||||||
flamegraph.width(width);
|
flamegraphChart.width(width);
|
||||||
flamegraph.update();
|
flamegraphChart.update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>MiniProfiler</title>
|
<title>MiniProfiler</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
<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://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
||||||
<script src="https://d3js.org/d3.v7.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>
|
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
<button id="stop-btn" onclick="stopProfiling()" disabled>Stop Profiling</button>
|
<button id="stop-btn" onclick="stopProfiling()" disabled>Stop Profiling</button>
|
||||||
<button id="clear-btn" onclick="clearData()" disabled>Clear Data</button>
|
<button id="clear-btn" onclick="clearData()" disabled>Clear Data</button>
|
||||||
<button id="reset-btn" onclick="resetBuffers()" disabled>Reset Buffers</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>
|
||||||
|
|
||||||
<div class="status-display">
|
<div class="status-display">
|
||||||
|
|||||||
Reference in New Issue
Block a user