mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 09:12:04 +02:00
393 lines
19 KiB
XML
393 lines
19 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<odoo>
|
|
<template id="view_memory">
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>Memory Usage Visualization</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels"></script>
|
|
<style>
|
|
/* General body and layout styles */
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
margin: 20px;
|
|
background-color: #f9f9f9;
|
|
}
|
|
.container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
/* Row definitions for the new layout */
|
|
.top-row {
|
|
height: 40vh; /* Line graph takes 40% of the viewport height */
|
|
}
|
|
.bottom-row {
|
|
display: flex;
|
|
gap: 20px;
|
|
/* The height of this row will be determined by its content */
|
|
}
|
|
|
|
/* Chart container styles */
|
|
.chart-container {
|
|
position: relative;
|
|
background: #fff;
|
|
border: 1px solid #ddd;
|
|
border-radius: 5px;
|
|
padding: 15px;
|
|
height: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Specific styling for the bottom row elements */
|
|
.bar-chart-wrapper {
|
|
flex: 2; /* Bar chart takes 2/3 of the width */
|
|
min-height: 400px; /* Minimum height for the bar chart */
|
|
}
|
|
.traceback-panel {
|
|
flex: 1; /* Traceback panel takes 1/3 of the width */
|
|
background: #f5f5f5;
|
|
border: 1px solid #ddd;
|
|
border-radius: 5px;
|
|
padding: 15px;
|
|
overflow-y: auto;
|
|
min-height: 400px; /* Match min-height for alignment */
|
|
height: fit-content; /* Adjust height to content */
|
|
}
|
|
|
|
/* Traceback panel content styling */
|
|
.traceback-header {
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
color: #333;
|
|
}
|
|
.traceback-item {
|
|
background: white;
|
|
margin: 5px 0;
|
|
padding: 8px;
|
|
border-radius: 3px;
|
|
border-left: 3px solid #007acc;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
}
|
|
.traceback-file {
|
|
color: #007acc;
|
|
font-weight: bold;
|
|
}
|
|
.copyable {
|
|
cursor: pointer;
|
|
user-select: all;
|
|
padding: 2px 4px;
|
|
border-radius: 3px;
|
|
transition: background-color 0.2s;
|
|
}
|
|
.copyable:hover {
|
|
background-color: #e8f4f8;
|
|
}
|
|
.traceback-line {
|
|
color: #666;
|
|
}
|
|
.no-selection {
|
|
color: #999;
|
|
font-style: italic;
|
|
text-align: center;
|
|
padding-top: 50px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<!-- Top row for the full-width line chart -->
|
|
<div class="top-row">
|
|
<div class="chart-container">
|
|
<canvas id="totalMemoryChart"></canvas>
|
|
</div>
|
|
</div>
|
|
<!-- Bottom row for the bar chart and traceback panel -->
|
|
<div class="bottom-row">
|
|
<div class="bar-chart-wrapper chart-container" id="memoryChartContainer">
|
|
<canvas id="memoryChart"></canvas>
|
|
</div>
|
|
<div class="traceback-panel" id="tracebackPanel">
|
|
<div class="no-selection">Click a point on the top chart, then a bar below to see details.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Get data from Odoo template
|
|
const rawData = JSON.parse(atob('<t t-esc="memory_graph"/>'));
|
|
|
|
// --- UTILITY FUNCTIONS ---
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
|
|
const value = bytes / Math.pow(k, i);
|
|
return `${value.toFixed(1)} ${sizes[i]}`;
|
|
}
|
|
|
|
function formatTimestamp(timestamp) {
|
|
const date = new Date(timestamp * 1000);
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
function getLastFileFromTraceback(traceback) {
|
|
if (!traceback || traceback.length === 0) return "unknown:0";
|
|
const lastFrame = traceback[traceback.length - 1];
|
|
return `${lastFrame[0]}:${lastFrame[1]}`;
|
|
}
|
|
|
|
function getFullTracebackKey(traceback) {
|
|
if (!traceback || traceback.length === 0) return "unknown";
|
|
return traceback.map(frame => `${frame[0]}:${frame[1]}`).join(' -> ');
|
|
}
|
|
|
|
// --- DYNAMIC UI UPDATES ---
|
|
function updateChartHeight(chart, dataLength) {
|
|
const container = document.getElementById('memoryChartContainer');
|
|
if (dataLength === 0) {
|
|
container.style.height = '400px'; // Reset to min-height
|
|
chart.resize();
|
|
return;
|
|
}
|
|
|
|
const minBarHeight = 35; // Minimum height per bar for readability
|
|
const padding = 100; // Extra space for axes and labels
|
|
const newHeight = Math.max(400, (dataLength * minBarHeight) + padding);
|
|
|
|
container.style.height = newHeight + 'px';
|
|
chart.resize();
|
|
}
|
|
|
|
function showTraceback(traceback, size, fullTracebackKey) {
|
|
const panel = document.getElementById('tracebackPanel');
|
|
if (!traceback || traceback.length === 0) {
|
|
panel.innerHTML = '<div class="no-selection">No traceback available</div>';
|
|
return;
|
|
}
|
|
|
|
let html = `<div class="traceback-header">Traceback (${formatBytes(size)})</div>`;
|
|
|
|
if (fullTracebackKey) {
|
|
html += `
|
|
<div style="margin-bottom: 10px; padding: 8px; background: #e8f4f8; border-radius: 3px; font-size: 11px; color: #555;">
|
|
<strong>Full Path:</strong> <span class="copyable" title="Click to select for copying">${fullTracebackKey}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
traceback.forEach(frame => {
|
|
const [filename, lineNo, functionName, code] = frame;
|
|
html += `
|
|
<div class="traceback-item">
|
|
<div class="traceback-file copyable" title="Click to select for copying">${filename}:${lineNo}</div>
|
|
<div>in <strong>${functionName || 'unknown'}</strong></div>
|
|
${code ? `<div class="traceback-line">${code}</div>` : ''}
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
panel.innerHTML = html;
|
|
}
|
|
|
|
// --- CHART LOGIC ---
|
|
const totalSizes = rawData.map(entry =>
|
|
entry.samples.reduce((sum, sample) => sum + sample.size, 0)
|
|
);
|
|
const lineLabels = rawData.map(entry => formatTimestamp(entry.start));
|
|
|
|
let selectedIndexes = [];
|
|
let currentBreakdownData = [];
|
|
|
|
const updateBarChart = () => {
|
|
if (selectedIndexes.length === 1) {
|
|
const entry = rawData[selectedIndexes[0]];
|
|
const groupedData = {};
|
|
entry.samples.forEach(sample => {
|
|
const key = getLastFileFromTraceback(sample.traceback);
|
|
if (!groupedData[key]) {
|
|
groupedData[key] = { size: 0, traceback: sample.traceback, samples: [] };
|
|
}
|
|
groupedData[key].size += sample.size;
|
|
groupedData[key].samples.push(sample);
|
|
});
|
|
|
|
currentBreakdownData = Object.entries(groupedData).map(([key, data]) => ({
|
|
label: key,
|
|
size: data.size,
|
|
traceback: data.traceback,
|
|
samples: data.samples
|
|
})).sort((a, b) => b.size - a.size);
|
|
|
|
breakdownChart.data.labels = currentBreakdownData.map(item => item.label);
|
|
breakdownChart.data.datasets = [{
|
|
label: `Memory Usage`,
|
|
data: currentBreakdownData.map(item => item.size),
|
|
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
|
borderColor: 'rgba(54, 162, 235, 1)',
|
|
borderWidth: 1
|
|
}];
|
|
breakdownChart.options.plugins.title.text = `Memory Breakdown for ${formatTimestamp(entry.start)}`;
|
|
|
|
} else if (selectedIndexes.length === 2) {
|
|
const [idx1, idx2] = selectedIndexes.sort((a, b) => a - b);
|
|
const entry1 = rawData[idx1];
|
|
const entry2 = rawData[idx2];
|
|
|
|
const groupDataByFullTraceback = (entry) => {
|
|
const grouped = {};
|
|
entry.samples.forEach(sample => {
|
|
const key = getFullTracebackKey(sample.traceback);
|
|
if (!grouped[key]) {
|
|
grouped[key] = { size: 0, traceback: sample.traceback, lastFile: getLastFileFromTraceback(sample.traceback) };
|
|
}
|
|
grouped[key].size += sample.size;
|
|
});
|
|
return grouped;
|
|
};
|
|
|
|
const data1 = groupDataByFullTraceback(entry1);
|
|
const data2 = groupDataByFullTraceback(entry2);
|
|
const labelSet = new Set([...Object.keys(data1), ...Object.keys(data2)]);
|
|
|
|
let diffData = [];
|
|
labelSet.forEach(fullTracebackKey => {
|
|
const size1 = data1[fullTracebackKey]?.size || 0;
|
|
const size2 = data2[fullTracebackKey]?.size || 0;
|
|
const diff = size2 - size1;
|
|
if (diff !== 0) {
|
|
const item = data2[fullTracebackKey] || data1[fullTracebackKey];
|
|
diffData.push({
|
|
label: item.lastFile,
|
|
diff: diff,
|
|
traceback: item.traceback,
|
|
fullTracebackKey: fullTracebackKey
|
|
});
|
|
}
|
|
});
|
|
|
|
currentBreakdownData = diffData.sort((a, b) => b.diff - a.diff);
|
|
|
|
breakdownChart.data.labels = currentBreakdownData.map(item => item.label);
|
|
breakdownChart.data.datasets = [{
|
|
label: `Difference`,
|
|
data: currentBreakdownData.map(item => item.diff),
|
|
backgroundColor: item => item.raw >= 0 ? 'rgba(75, 192, 192, 0.6)' : 'rgba(255, 99, 132, 0.6)',
|
|
borderColor: item => item.raw >= 0 ? 'rgba(75, 192, 192, 1)' : 'rgba(255, 99, 132, 1)',
|
|
borderWidth: 1
|
|
}];
|
|
breakdownChart.options.plugins.title.text = `Memory Difference: ${formatTimestamp(entry2.start)} vs ${formatTimestamp(entry1.start)}`;
|
|
|
|
} else {
|
|
currentBreakdownData = [];
|
|
breakdownChart.data.labels = [];
|
|
breakdownChart.data.datasets = [];
|
|
breakdownChart.options.plugins.title.text = 'Memory Breakdown';
|
|
document.getElementById('tracebackPanel').innerHTML =
|
|
'<div class="no-selection">Click a point on the top chart, then a bar below to see details.</div>';
|
|
}
|
|
|
|
updateChartHeight(breakdownChart, currentBreakdownData.length);
|
|
breakdownChart.update();
|
|
totalChart.update();
|
|
};
|
|
|
|
// --- CHART INSTANTIATION ---
|
|
const totalChart = new Chart(document.getElementById('totalMemoryChart'), {
|
|
type: 'line',
|
|
data: {
|
|
labels: lineLabels,
|
|
datasets: [{
|
|
label: 'Total Memory Usage',
|
|
data: totalSizes,
|
|
fill: false,
|
|
borderColor: 'rgba(75, 192, 192, 1)',
|
|
tension: 0.1,
|
|
pointBackgroundColor: ctx => selectedIndexes.includes(ctx.dataIndex) ? 'rgba(255, 99, 132, 1)' : 'rgba(75, 192, 192, 1)',
|
|
pointRadius: ctx => selectedIndexes.includes(ctx.dataIndex) ? 7 : 4,
|
|
pointBorderWidth: ctx => selectedIndexes.includes(ctx.dataIndex) ? 3 : 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
title: { display: true, text: 'Total Memory Usage Over Time (Click to select, Shift+Click to compare)' },
|
|
tooltip: { callbacks: { label: context => `Total: ${formatBytes(context.raw)}` } }
|
|
},
|
|
onClick: (evt, elements) => {
|
|
if (elements.length > 0) {
|
|
const idx = elements[0].index;
|
|
const isSelected = selectedIndexes.includes(idx);
|
|
if (evt.native.shiftKey) {
|
|
if (isSelected) {
|
|
selectedIndexes = selectedIndexes.filter(i => i !== idx);
|
|
} else {
|
|
selectedIndexes.push(idx);
|
|
if (selectedIndexes.length > 2) selectedIndexes.shift();
|
|
}
|
|
} else {
|
|
selectedIndexes = isSelected && selectedIndexes.length === 1 ? [] : [idx];
|
|
}
|
|
} else {
|
|
selectedIndexes = [];
|
|
}
|
|
updateBarChart();
|
|
},
|
|
scales: {
|
|
y: { beginAtZero: true, ticks: { callback: formatBytes }, title: { display: true, text: 'Total Memory' } },
|
|
x: { title: { display: true, text: 'Time' } }
|
|
}
|
|
}
|
|
});
|
|
|
|
const breakdownChart = new Chart(document.getElementById('memoryChart'), {
|
|
type: 'bar',
|
|
data: { labels: [], datasets: [] },
|
|
options: {
|
|
indexAxis: 'y',
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
layout: { padding: { right: 120 } },
|
|
plugins: {
|
|
legend: { display: false },
|
|
title: { display: true, text: 'Memory Breakdown' },
|
|
tooltip: { callbacks: { label: context => `${formatBytes(context.raw)}` } },
|
|
datalabels: {
|
|
anchor: 'end',
|
|
align: 'right',
|
|
color: '#000',
|
|
font: { weight: 'bold', size: 11 },
|
|
formatter: (value) => formatBytes(value),
|
|
display: true,
|
|
clip: false
|
|
}
|
|
},
|
|
onClick: (evt, elements) => {
|
|
if (elements.length > 0 && currentBreakdownData.length > 0) {
|
|
const item = currentBreakdownData[elements[0].index];
|
|
const fullKey = item.fullTracebackKey || getFullTracebackKey(item.traceback);
|
|
showTraceback(item.traceback, Math.abs(item.diff || item.size), fullKey);
|
|
}
|
|
},
|
|
scales: {
|
|
x: { beginAtZero: false, ticks: { callback: formatBytes }, title: { display: true, text: 'Memory Usage / Difference' } },
|
|
y: { title: { display: true, text: 'File:Line' }, ticks: { maxRotation: 0, font: { size: 11 } } }
|
|
}
|
|
},
|
|
plugins: [ChartDataLabels]
|
|
});
|
|
|
|
// Initial render
|
|
updateBarChart();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
</template>
|
|
</odoo>
|