mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 05:32:00 +02:00
vanilla 19.0
This commit is contained in:
parent
991d2234ca
commit
d1963a3c3a
3066 changed files with 1651266 additions and 922560 deletions
393
odoo-bringout-oca-ocb-web/web/views/memory_template.xml
Normal file
393
odoo-bringout-oca-ocb-web/web/views/memory_template.xml
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
<?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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue