diff --git a/OOMAnalyser.html b/OOMAnalyser.html
index 09f55e9..3a46140 100644
--- a/OOMAnalyser.html
+++ b/OOMAnalyser.html
@@ -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) {
Add support for newer kernels (suggested by Mikko Rantalainen)
Add support for journalctl output (suggested by Mikko Rantalainen)
Add support for newer process table format
+ Rework memory charts to show all items in legend
...
diff --git a/OOMAnalyser.py b/OOMAnalyser.py
index f27617b..09a6049 100644
--- a/OOMAnalyser.py
+++ b/OOMAnalyser.py
@@ -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 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 = """