Rework memory charts to show all items in legend

This commit is contained in:
Carsten Grohmann 2022-01-11 17:26:19 +01:00
parent a549a76fa2
commit 9e22c5cdec
2 changed files with 287 additions and 139 deletions

View File

@ -66,7 +66,6 @@ THIS PROGRAM COMES WITH NO WARRANTY
.js-mem-usage__svg {
display: block;
max-height: 200px;
}
.js-killed-proc-score--show {
@ -944,6 +943,7 @@ window.onerror = function (msg, url, lineNo, columnNo, errorObj) {
<li>Add support for newer kernels (suggested by Mikko Rantalainen)</li>
<li>Add support for journalctl output (suggested by Mikko Rantalainen)</li>
<li>Add support for newer process table format</li>
<li>Rework memory charts to show all items in legend</li>
<li>...</li>
</ol>

View File

@ -5,7 +5,7 @@
# Copyright (c) 2017-2022 Carsten Grohmann
# License: MIT (see LICENSE.txt)
# THIS PROGRAM COMES WITH NO WARRANTY
import math
import re
DEBUG = False
@ -65,6 +65,9 @@ class Node:
def appendChild(self, *args, **kwargs):
return
def setAttribute(self, *args, **kwargs):
return
# __pragma__ ('noskip')
@ -1078,7 +1081,6 @@ class OOMAnalyser:
self.oom_result.details[item] = int(self.oom_result.details[item])
except:
error('Converting item "{}={}" to integer failed'.format(item, self.oom_result.details[item]))
# __pragma__ ('nojsiter')
def _convert_pstable_values_to_integer(self):
@ -1267,6 +1269,281 @@ class OOMAnalyser:
return True
class SVGChart:
"""
Creates a horizontal stacked bar chart with a legend underneath.
The entries of the legend are arranged from left to right and from top to bottom.
"""
cfg = dict(
chart_height=150,
chart_width=600,
label_height=80,
legend_entry_width=160,
legend_margin=7,
title_height=20,
title_margin=10,
css_class='js-mem-usage__svg', # CSS class for SVG diagram
)
"""Basic chart configuration"""
# generated with Colorgorical http://vrl.cs.brown.edu/color
colors = [
'#aee39a',
'#344b46',
'#1ceaf9',
'#5d99aa',
'#32e195',
'#b02949',
'#deae9e',
'#805257',
'#add51f',
'#544793',
'#a794d3',
'#e057e1',
'#769b5a',
'#76f014',
'#621da6',
'#ffce54',
'#d64405',
'#bb8801',
'#096013',
'#ff0087'
]
"""20 different colors for memory usage diagrams"""
max_entries_per_row = 3
"""Maximum chart legend entries per row"""
namespace = 'http://www.w3.org/2000/svg'
def __init__(self):
super().__init__()
self.cfg['bar_topleft_x'] = 0
self.cfg['bar_topleft_y'] = self.cfg['title_height'] + self.cfg['title_margin']
self.cfg['bar_bottomleft_x'] = self.cfg['bar_topleft_x']
self.cfg['bar_bottomleft_y'] = self.cfg['bar_topleft_y'] + self.cfg['chart_height']
self.cfg['bar_bottomright_x'] = self.cfg['bar_topleft_x'] + self.cfg['chart_width']
self.cfg['bar_bottomright_y'] = self.cfg['bar_topleft_y'] + self.cfg['chart_height']
self.cfg['legend_topleft_x'] = self.cfg['bar_topleft_x']
self.cfg['legend_topleft_y'] = self.cfg['bar_topleft_y'] + self.cfg['legend_margin']
self.cfg['legend_width'] = self.cfg['legend_entry_width'] + self.cfg['legend_margin'] + \
self.cfg['legend_entry_width']
self.cfg['diagram_height'] = self.cfg['chart_height'] + self.cfg['title_margin'] + self.cfg['title_height']
self.cfg['diagram_width'] = self.cfg['chart_width']
self.cfg['title_bottommiddle_y'] = self.cfg['title_height']
self.cfg['title_bottommiddle_x'] = self.cfg['diagram_width'] // 2
# __pragma__ ('kwargs')
def create_element(self, tag, **kwargs):
"""
Create an SVG element of the given tag.
@note: Underscores in the argument names will be replaced by minus
@param str tag: Type of element to be created
@rtype: Node
"""
element = document.createElementNS(self.namespace, tag)
# __pragma__ ('jsiter')
for k in kwargs:
k2 = k.replace('_', '-')
element.setAttribute(k2, kwargs[k])
# __pragma__ ('nojsiter')
return element
# __pragma__ ('nokwargs')
# __pragma__ ('kwargs')
def create_element_text(self, text, **kwargs):
"""
Create an SVG text element
@note: Underscores in the argument names will be replaced by minus
@param str text: Text
@rtype: Node
"""
element = self.create_element('text', **kwargs)
element.textContent = text
return element
# __pragma__ ('nokwargs')
def create_element_svg(self, height, width, css_class=None):
"""Return a SVG element"""
svg = self.create_element('svg', version='1.1', height=height, width=width,
viewBox='0 0 {} {}'.format(width, height))
if css_class:
svg.setAttribute('class', css_class)
return svg
def create_rectangle(self, x, y, width, height, color=None, title=None):
"""
Return a rect-element in a group container
If a title is given, the container also contains a <title> element.
"""
g = self.create_element('g')
rect = self.create_element('rect', x=x, y=y, width=width, height=height)
if color:
rect.setAttribute('fill', color)
if title:
t = self.create_element('title')
t.textContent = title
g.appendChild(t)
g.appendChild(rect)
return g
def create_legend_entry(self, color, desc, pos):
"""
Create a legend entry for the given position. Both elements of the entry are grouped within a g-element.
@param str color: Colour of the entry
@param str desc: Description
@param int pos: Continuous position
@rtype: Node
"""
label_group = self.create_element('g', id=desc)
color_rect = self.create_rectangle(0, 0, 20, 20, color)
label_group.appendChild(color_rect)
desc_element = self.create_element_text(desc, x='30', y='18')
desc_element.textContent = desc
label_group.appendChild(desc_element)
# move group to right position
x, y = self.legend_calc_xy(pos)
label_group.setAttribute('transform', 'translate({}, {})'.format(x, y))
return label_group
def legend_max_row(self, pos):
"""
Returns the maximum number of rows in the legend
@param int pos: Continuous position
"""
max_row = math.ceil(pos / self.max_entries_per_row)
return max_row
def legend_max_col(self, pos):
"""
Returns the maximum number of columns in the legend
@param int pos: Continuous position
@rtype: int
"""
if pos < self.max_entries_per_row:
return pos
return self.max_entries_per_row
def legend_calc_x(self, column):
"""
Calculate the X-axis using the given column
@type column: int
@rtype: int
"""
x = self.cfg['bar_bottomleft_x'] + self.cfg['legend_margin']
x += column * (self.cfg['legend_margin'] + self.cfg['legend_entry_width'])
return x
def legend_calc_y(self, row):
"""
Calculate the Y-axis using the given row
@type row: int
@rtype: int
"""
y = self.cfg['bar_bottomleft_y'] + self.cfg['legend_margin']
y += row * 40
return y
def legend_calc_xy(self, pos):
"""
Calculate the X-axis and Y-axis
@param int pos: Continuous position
@rtype: int, int
"""
if not pos:
col = 0
row = 0
else:
col = pos % self.max_entries_per_row
row = math.floor(pos / self.max_entries_per_row)
x = self.cfg['bar_bottomleft_x'] + self.cfg['legend_margin']
y = self.cfg['bar_bottomleft_y'] + self.cfg['legend_margin']
x += col * (self.cfg['legend_margin'] + self.cfg['legend_entry_width'])
y += row * 40
return x, y
def generate_bar_area(self, elements):
"""
Generate colord stacked bars. All entries are group within a g-element.
@rtype: Node
"""
bar_group = self.create_element('g', id='bar_group', stroke='black', stroke_width=2)
current_x = 0
total_length = sum([length for unused, length in elements])
for i, two in enumerate(elements):
name, length = two
color = self.colors[i % len(self.colors)]
rect_len = int(length / total_length * self.cfg['chart_width'])
if rect_len == 0:
rect_len = 1
rect = self.create_rectangle(current_x, self.cfg['bar_topleft_y'], rect_len, self.cfg['chart_height'],
color, name)
current_x += rect_len
bar_group.appendChild(rect)
return bar_group
def generate_legend(self, elements):
"""
Generate a legend for all elements. All entries are group within a g-element.
@rtype: Node
"""
legend_group = self.create_element('g', id='legend_group')
for i, two in enumerate(elements):
element_name = two[0]
color = self.colors[i % len(self.colors)]
label_group = self.create_legend_entry(color, element_name, i)
legend_group.appendChild(label_group)
# re-calculate chart height after all legend entries added
self.cfg['diagram_height'] = self.legend_calc_y(self.legend_max_row(len(elements)))
return legend_group
def generate_chart(self, title, *elements):
"""
Return a SVG bar chart for all elements
@param str title: Chart title
@param elements: List of tuple with name and length of the entry (not normalized)
@rtype: Node
"""
filtered_elements = [(name, length) for name, length in elements if length > 0]
bar_group = self.generate_bar_area(filtered_elements)
legend_group = self.generate_legend(filtered_elements)
svg = self.create_element_svg(self.cfg['diagram_height'], self.cfg['diagram_width'], self.cfg['css_class'])
chart_title = self.create_element_text(title, font_size=self.cfg['title_height'], font_weight="bold",
stroke_width='0', text_anchor='middle',
x=self.cfg['title_bottommiddle_x'], y=self.cfg['title_bottommiddle_y'])
svg.appendChild(chart_title)
svg.appendChild(bar_group)
svg.appendChild(legend_group)
return svg
class OOMDisplay:
"""Display the OOM analysis"""
@ -1503,41 +1780,6 @@ Out of memory: Killed process 651 (unattended-upgr) total-vm:108020kB, anon-rss:
sort_order = None
"""Sort order for process values"""
svg_namespace = 'http://www.w3.org/2000/svg'
# generated with Colorgorical http://vrl.cs.brown.edu/color
svg_colors_mem = [
'#aee39a',
'#344b46',
'#1ceaf9',
'#5d99aa',
'#32e195',
'#b02949',
'#deae9e',
'#805257',
'#add51f',
'#544793',
'#a794d3',
'#e057e1',
'#769b5a',
'#76f014',
'#621da6',
'#ffce54',
'#d64405',
'#bb8801',
'#096013',
'#ff0087'
]
"""20 different colors for memory usage diagram"""
# generated with ColorBrewer (v2.0) https://colorbrewer2.org/?type=diverging&scheme=PuOr&n=3
svg_colors_swap = [
'#f1a340',
'#f7f7f7',
'#998ec3'
]
"""3 different colors for swap usage diagram"""
svg_array_updown = """
<svg width="8" height="11">
<use xlink:href="#svg_array_updown" />
@ -1741,100 +1983,6 @@ Out of memory: Killed process 651 (unattended-upgr) total-vm:108020kB, anon-rss:
'pstable__row-oom-score-adj--width')
element.firstChild.textContent = "col {}".format(i + 1)
def svg_create_element(self, height, width, css_class):
"""Return an empty SVG element"""
svg = document.createElementNS(self.svg_namespace, 'svg')
svg.setAttribute('version', '1.1')
svg.setAttribute('height', height)
svg.setAttribute('width', width)
svg.setAttribute('viewBox', '0 0 {} {}'.format(width, height))
svg.setAttribute('class', css_class)
return svg
def svg_create_rect(self, x=0, y=0, width=0, height=0, color=None, title=None):
g = document.createElementNS(self.svg_namespace, 'g')
rect = document.createElementNS(self.svg_namespace, 'rect')
if x:
rect.setAttribute('x', x)
if y:
rect.setAttribute('y', y)
if width:
rect.setAttribute('width', width)
if height:
rect.setAttribute('height', height)
if color:
rect.setAttribute('fill', color)
if title:
t = document.createElementNS(self.svg_namespace, 'title')
t.textContent = title
g.appendChild(t)
g.appendChild(rect)
return g
def svg_generate_bar_chart(self, color_list, *elements):
"""Generate a SVG bar chart"""
bar_height = 100
label_height = 80
length_factor = 5
overall_height = bar_height + label_height
overall_width = 100 * length_factor
css_class = 'js-mem-usage__svg'
svg = self.svg_create_element(overall_height, overall_width, css_class)
sum_all_elements = sum([length for unused, length in elements])
current_pos = 0
bar_group = document.createElementNS(self.svg_namespace, 'g')
bar_group.setAttribute('id', 'bar_group')
bar_group.setAttribute('stroke', 'black')
bar_group.setAttribute('stroke-width', 2)
nr_processed_elements = 0
for title, length in elements:
# length is None/undefined is the regular expression doesn't find any values
if not length:
continue
rect_len = int(100 * length / sum_all_elements) * length_factor
if not rect_len:
continue
color = color_list[nr_processed_elements % len(color_list)]
rect = self.svg_create_rect(current_pos, 0, rect_len, bar_height, color, title)
bar_group.appendChild(rect)
label_group = document.createElementNS(self.svg_namespace, 'g')
label_group.setAttribute('id', title)
color_rect = self.svg_create_rect(0, 0, 20, 20, color)
color_rect.setAttribute('stroke', 'black')
color_rect.setAttribute('stroke-width', 2)
text = document.createElementNS(self.svg_namespace, 'text')
text.setAttribute('x', '30')
text.setAttribute('y', '18')
text.setAttribute('stroke-width', 0)
text.textContent = title
label_group.appendChild(color_rect)
label_group.appendChild(text)
# TODO replace hardcoded values
x = 5 + 125 * (nr_processed_elements // 2)
y = bar_height + 10 + (nr_processed_elements % 2) * 40
label_group.setAttribute('transform', 'translate({}, {})'.format(x, y))
bar_group.appendChild(label_group)
current_pos += rect_len
nr_processed_elements += 1
svg.appendChild(bar_group)
return svg
def copy_example_rhel7_to_form(self):
document.getElementById('textarea_oom').value = self.example_rhel7
@ -1919,12 +2067,11 @@ Out of memory: Killed process 651 (unattended-upgr) total-vm:108020kB, anon-rss:
# show/hide swap space
if self.oom_result.swap_active:
# generate swap usage diagram
svg_swap = self.svg_generate_bar_chart(
self.svg_colors_swap,
('Swap Used', self.oom_result.details['swap_used_kb']),
('Swap Free', self.oom_result.details['swap_free_kb']),
('Swap Cached', self.oom_result.details['swap_cache_kb']),
)
svg = SVGChart()
svg_swap = svg.generate_chart('Swap Summary',
('Swap Used', self.oom_result.details['swap_used_kb']),
('Swap Free', self.oom_result.details['swap_free_kb']),
('Swap Cached', self.oom_result.details['swap_cache_kb']))
elem_svg_swap = document.getElementById('svg_swap')
elem_svg_swap.appendChild(svg_swap)
show_elements('.js-swap-active--show')
@ -1957,7 +2104,8 @@ Out of memory: Killed process 651 (unattended-upgr) total-vm:108020kB, anon-rss:
)
chart_elements = [(title, self.oom_result.details[value]) for title, value in ram_title_attr
if value in self.oom_result.details]
svg_ram = self.svg_generate_bar_chart(self.svg_colors_mem, *chart_elements)
svg = SVGChart()
svg_ram = svg.generate_chart('RAM Summary', *chart_elements)
elem_svg_ram = document.getElementById('svg_ram')
elem_svg_ram.appendChild(svg_ram)