vanilla 19.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:49:46 +02:00
parent 991d2234ca
commit d1963a3c3a
3066 changed files with 1651266 additions and 922560 deletions

View file

@ -0,0 +1,393 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="view_memory">
&lt;!DOCTYPE html&gt;
<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 &amp;&amp; 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 &amp;&amp; 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>