Replace Pyasciigraph by termgraph for freq graphs
authorAntoine Busque <abusque@efficios.com>
Thu, 28 Jan 2016 22:16:57 +0000 (17:16 -0500)
committerAntoine Busque <abusque@efficios.com>
Thu, 11 Feb 2016 18:58:52 +0000 (13:58 -0500)
The only remaining uses of Pyasciigraph were limited to frequency
graphs, so this commit introduces a replacement under the termgraph
module to display such frequency graphs.

This effectively eliminates the Pyasciigraph dependency, which is now
removed from the source tree.

Signed-off-by: Antoine Busque <abusque@efficios.com>
lttnganalyses/ascii_graph/__init__.py [deleted file]
lttnganalyses/cli/io.py
lttnganalyses/cli/irq.py
lttnganalyses/cli/sched.py
lttnganalyses/cli/termgraph.py

diff --git a/lttnganalyses/ascii_graph/__init__.py b/lttnganalyses/ascii_graph/__init__.py
deleted file mode 100644 (file)
index 167c716..0000000
+++ /dev/null
@@ -1,229 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright (c) 2012 Pierre-Francois Carpentier <carpentier.pf@gmail.com>
-#
-# https://github.com/kakwa/py-ascii-graph/
-#
-# Permission is hereby granted, free of charge, to any person obtaining
-# a copy of this software and associated documentation files (the
-# "Software"), to deal in the Software without restriction, including
-# without limitation the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the Software, and to
-# permit persons to whom the Software is furnished to do so, subject to
-# the following conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-from __future__ import unicode_literals
-import sys
-import os
-
-
-class Pyasciigraph:
-    def __init__(self, line_length=79, min_graph_length=50,
-                 separator_length=2):
-        """Constructor of Pyasciigraph
-
-        :param int line_length: the max number of char on a line
-                if any line cannot be shorter,
-                it will go over this limit
-        :param int min_graph_length: the min number of char used by the graph
-        :param int separator_length: the length of field separator
-        """
-        self.line_length = line_length
-        self.separator_length = separator_length
-        self.min_graph_length = min_graph_length
-
-    def _u(self, x):
-        if sys.version < '3':
-            import codecs
-            return codecs.unicode_escape_decode(x)[0]
-        else:
-            return x
-
-    def _get_maximum(self, data):
-        all_max = {}
-        all_max['value_max_length'] = 0
-        all_max['info_max_length'] = 0
-        all_max['max_value'] = 0
-
-        for (info, value) in data:
-            if value > all_max['max_value']:
-                all_max['max_value'] = value
-
-            if len(info) > all_max['info_max_length']:
-                all_max['info_max_length'] = len(info)
-
-            if len(str(value)) > all_max['value_max_length']:
-                all_max['value_max_length'] = len(str(value))
-        return all_max
-
-    def _gen_graph_string(self, value, max_value, graph_length, start_value):
-        if max_value == 0:
-            number_of_square = int(value * graph_length)
-        else:
-            number_of_square = int(value * graph_length / max_value)
-        number_of_space = int(start_value - number_of_square)
-        return '█' * number_of_square + self._u(' ') * number_of_space
-
-    def _console_size(self):
-        TERMSIZE = 80
-        return int(os.environ.get('COLUMNS', TERMSIZE)) - 1
-
-    def _gen_info_string(self, info, start_info, line_length, info_before):
-        number_of_space = (line_length - start_info - len(info))
-        if info_before:
-            return self._u(' ') * number_of_space + info
-        else:
-            return info + self._u(' ') * number_of_space
-
-    def _gen_value_string(self, value, start_value, start_info, unit, count):
-        if not count:
-            v = str("%0.02f" % value)
-        else:
-            # we don't want to add .00 to count values (only integers)
-            v = str(value)
-        number_space = start_info -\
-            start_value -\
-            len(v) -\
-            self.separator_length
-
-        return ' ' * number_space +\
-            v + str(unit) +\
-            ' ' * self.separator_length
-
-    def _sanitize_string(self, string):
-        # get the type of a unicode string
-        unicode_type = type(self._u('t'))
-        input_type = type(string)
-        if input_type is str:
-            if sys.version_info.major < 3:  # pragma: no cover
-                info = string
-            else:
-                info = string
-        elif input_type is unicode_type:
-            info = string
-        elif input_type is int or input_type is float:
-            if sys.version_info.major < 3:  # pragma: no cover
-                info = string
-            else:
-                info = str(string)
-        return info
-
-    def _sanitize_data(self, data):
-        ret = []
-        for item in data:
-            ret.append((self._sanitize_string(item[0]), item[1]))
-        return ret
-
-    def graph(self, label, data, sort=0, with_value=True, unit="",
-              info_before=False, count=False):
-        """function generating the graph
-
-        :param string label: the label of the graph
-        :param iterable data: the data (list of tuple (info, value))
-                info must be "castable" to a unicode string
-                value must be an int or a float
-        :param int sort: flag sorted
-                0: not sorted (same order as given) (default)
-                1: increasing order
-                2: decreasing order
-        :param boolean with_value: flag printing value
-                True: print the numeric value (default)
-                False: don't print the numeric value
-        :rtype: a list of strings (each lines)
-
-        """
-        result = []
-        san_data = self._sanitize_data(data)
-        san_label = self._sanitize_string(label)
-
-        if sort == 1:
-            san_data = sorted(san_data, key=lambda value: value[1],
-                              reverse=False)
-        elif sort == 2:
-            san_data = sorted(san_data, key=lambda value: value[1],
-                              reverse=True)
-
-        all_max = self._get_maximum(san_data)
-
-        real_line_length = max(self.line_length, len(label))
-
-        min_line_length = self.min_graph_length +\
-            2 * self.separator_length +\
-            all_max['value_max_length'] +\
-            all_max['info_max_length']
-
-        if min_line_length < real_line_length:
-            # calcul of where to start info
-            start_info = self.line_length -\
-                all_max['info_max_length']
-            # calcul of where to start value
-            start_value = start_info -\
-                self.separator_length -\
-                all_max['value_max_length']
-            # calcul of where to end graph
-            graph_length = start_value -\
-                self.separator_length
-        else:
-            # calcul of where to start value
-            start_value = self.min_graph_length +\
-                self.separator_length
-            # calcul of where to start info
-            start_info = start_value +\
-                all_max['value_max_length'] +\
-                self.separator_length
-            # calcul of where to end graph
-            graph_length = self.min_graph_length
-            # calcul of the real line length
-            real_line_length = min_line_length
-
-        real_line_length = min(real_line_length, self._console_size())
-        result.append(san_label)
-        result.append(self._u('#') * real_line_length)
-
-        for item in san_data:
-            info = item[0]
-            value = item[1]
-
-            graph_string = self._gen_graph_string(
-                value,
-                all_max['max_value'],
-                graph_length,
-                start_value)
-
-            if with_value:
-                value_string = self._gen_value_string(
-                    value,
-                    start_value,
-                    start_info, unit, count)
-            else:
-                value_string = ""
-
-            info_string = self._gen_info_string(
-                info,
-                start_info,
-                real_line_length, info_before)
-            if info_before:
-                new_line = info_string + " " + graph_string + value_string
-            else:
-                new_line = graph_string + value_string + info_string
-            result.append(new_line)
-
-        return result
-
-if __name__ == '__main__':
-    test = [('long_label', 423), ('sl', 1234), ('line3', 531),
-            ('line4', 200), ('line5', 834)]
-    graph = Pyasciigraph()
-    for line in graph.graph('test print', test):
-        print(line)
index afe66aa109fce689999c35a0910e257e5f4dced7..cdba86a6edc8f42a698489a654e70628e6a175b9 100644 (file)
@@ -32,7 +32,6 @@ from ..core import io
 from ..common import format_utils
 from .command import Command
 from ..linuxautomaton import common
-from ..ascii_graph import Pyasciigraph
 
 
 _UsageTables = collections.namedtuple('_UsageTables', [
@@ -809,29 +808,15 @@ class IoAnalysisCommand(Command):
         return syscall_tables + disk_tables
 
     def _print_one_freq(self, result_table):
-        if not result_table.rows:
-            return
-
-        graph = Pyasciigraph()
-        graph_data = []
-
-        for row in result_table.rows:
-            graph_data.append(('%0.03f' % row.latency_lower.to_us(),
-                               row.count.value))
-
-        title = '{} {} (usec)'.format(result_table.title,
-                                      result_table.subtitle)
-        graph_lines = graph.graph(
-            title,
-            graph_data,
-            info_before=True,
-            count=True
+        graph = termgraph.FreqGraph(
+            data=result_table.rows,
+            get_value=lambda row: row.count.value,
+            get_lower_bound=lambda row: row.latency_lower.to_us(),
+            title='{} {}'.format(result_table.title, result_table.subtitle),
+            unit='µs'
         )
 
-        for line in graph_lines:
-            print(line)
-
-        print()
+        graph.print_graph()
 
     def _print_freq(self, freq_tables):
         for freq_table in freq_tables:
index af1dca92a521e8e4d9c8dd39f1fd11a497692d11..facbb2b8ebd5b767175a2b3b3b39e90b24ccda61 100644 (file)
@@ -27,9 +27,9 @@ import math
 import statistics
 import sys
 from . import mi
+from . import termgraph
 from .command import Command
 from ..core import irq as core_irq
-from ..ascii_graph import Pyasciigraph
 from ..linuxautomaton import common, sv
 
 
@@ -487,28 +487,17 @@ class IrqAnalysisCommand(Command):
         return statistics.stdev(raise_latencies)
 
     def _print_frequency_distribution(self, freq_table):
-        graph = Pyasciigraph()
-        graph_data = []
-
-        for row in freq_table.rows:
-            # The graph data format is a tuple (info, value). Here info
-            # is the lower bound of the bucket, value the bucket's count
-            lower_bound_us = row.duration_lower.to_us()
-            count = row.count.value
-
-            graph_data.append(('%0.03f' % lower_bound_us, count))
-
-        title_fmt = 'Handler duration frequency distribution {} (usec)'
-
-        graph_lines = graph.graph(
-            title_fmt.format(freq_table.subtitle),
-            graph_data,
-            info_before=True,
-            count=True
+        title_fmt = 'Handler duration frequency distribution {}'
+
+        graph = termgraph.FreqGraph(
+            data=freq_table.rows,
+            get_value=lambda row: row.count.value,
+            get_lower_bound=lambda row: row.duration_lower.to_us(),
+            title=title_fmt.format(freq_table.subtitle),
+            unit='µs'
         )
 
-        for line in graph_lines:
-            print(line)
+        graph.print_graph()
 
     def _filter_irq(self, irq):
         if type(irq) is sv.HardIRQ:
index f4d252e7d2fb1eeb9adacbab3204a6f815a2592e..cdd4dd9bd9f6076701f6b54cb1f8999f835034aa 100644 (file)
@@ -27,10 +27,10 @@ import operator
 import statistics
 import collections
 from . import mi
+from . import termgraph
 from ..core import sched
 from .command import Command
 from ..linuxautomaton import common
-from ..ascii_graph import Pyasciigraph
 
 
 _SchedStats = collections.namedtuple('_SchedStats', [
@@ -797,34 +797,22 @@ class SchedAnalysisCommand(Command):
                 print(row_str)
 
     def _print_frequency_distribution(self, freq_table):
-        graph = Pyasciigraph()
-        graph_data = []
-
-        for row in freq_table.rows:
-            # The graph data format is a tuple (info, value). Here info
-            # is the lower bound of the bucket, value the bucket's count
-            lower_bound_us = row.duration_lower.to_us()
-            count = row.count.value
-
-            graph_data.append(('%0.03f' % lower_bound_us, count))
-
-        title_fmt = 'Scheduling latency (µs) frequency distribution - {}'
-
-        graph_lines = graph.graph(
-            title_fmt.format(freq_table.subtitle),
-            graph_data,
-            info_before=True,
-            count=True
+        title_fmt = 'Scheduling latency frequency distribution - {}'
+
+        graph = termgraph.FreqGraph(
+            data=freq_table.rows,
+            get_value=lambda row: row.count.value,
+            get_lower_bound=lambda row: row.duration_lower.to_us(),
+            title=title_fmt.format(freq_table.subtitle),
+            unit='µs'
         )
 
-        for line in graph_lines:
-            print(line)
+        graph.print_graph()
+
 
     def _print_freq(self, freq_tables):
         for freq_table in freq_tables:
-            if freq_table.rows:
-                print()
-                self._print_frequency_distribution(freq_table)
+            self._print_frequency_distribution(freq_table)
 
     def _validate_transform_args(self, args):
         # If neither --total nor --per-prio are specified, default
index 8fca91a4705537009969977f13a29211d7faf169..2ba614791f3153fa3a4ff4de0a7a9a81b80e8756 100644 (file)
 from collections import namedtuple
 
 
-GraphDatum = namedtuple('GraphDatum', ['value', 'value_str', 'label'])
-
-
-class BarGraph():
-    MAX_BAR_WIDTH = 80
+GraphDatum = namedtuple('GraphDatum', ['value', 'value_str'])
+BarGraphDatum = namedtuple('BarGraphDatum', ['value', 'value_str', 'label'])
+FreqGraphDatum = namedtuple(
+    'FreqGraphDatum', ['value', 'value_str', 'lower_bound']
+)
+
+class Graph():
+    MAX_GRAPH_WIDTH = 80
     BAR_CHAR = '█'
     HR_CHAR = '#'
 
-    def __init__(self, data, get_value, get_label, get_value_str=None,
-                 title=None, label_header=None, unit=None):
-        self._max_value = 0
-        self._max_value_len = 0
+    def __init__(self, data, get_value, get_value_str, title, unit):
+        self._data = data
+        self._get_value = get_value
         self._title = title
-        self._label_header = label_header
         self._unit = unit
+        self._max_value = 0
+        self._max_value_len = 0
 
-        self._data = self._transform_data(
-            data, get_value, get_value_str, get_label
-        )
+        if get_value_str is not None:
+            self._get_value_str_cb = get_value_str
+        else:
+            self._get_value_str_cb = Graph._get_value_str_default
 
-    def _transform_data(self, data, get_value, get_value_str, get_label):
+    def _transform_data(self, data):
         graph_data = []
-        if get_value_str is None:
-            get_value_str = self._get_value_str
 
         for datum in data:
-            value = get_value(datum)
-            value_str = get_value_str(value)
-            value_len = len(value_str)
-            label = get_label(datum)
+            graph_datum = self._get_graph_datum(datum)
 
-            if value > self._max_value:
-                self._max_value = value
-            if value_len > self._max_value_len:
-                self._max_value_len = value_len
-
-            graph_data.append(GraphDatum(value, value_str, label))
+            if graph_datum.value > self._max_value:
+                self._max_value = graph_datum.value
+            if len(graph_datum.value_str) > self._max_value_len:
+                self._max_value_len = len(graph_datum.value_str)
 
+            graph_data.append(graph_datum)
 
         return graph_data
 
-    def _get_graph_header(self):
-        if not self._label_header:
-            return self._title
+    def _get_value_str(self, value):
+        return self._get_value_str_cb(value)
 
-        title_len = len(self._title)
-        space_width = (self.MAX_BAR_WIDTH - title_len) + \
-                      1 + self._max_value_len + 1
+    def _get_graph_datum(self, datum):
+        value = self._get_value(datum)
+        value_str = self._get_value_str(value)
 
-        return self._title + ' ' * space_width + self._label_header
+        return GraphDatum(value, value_str)
 
-    def _get_value_str(self, value):
+    def _print_header(self):
+        if self._title:
+            print(self._title)
+
+    def _print_separator(self):
+        print(self.HR_CHAR * self.MAX_GRAPH_WIDTH)
+
+    def _print_body(self):
+        raise NotImplementedError()
+
+    def print_graph(self):
+        if not self._data:
+            return
+
+        self._print_header()
+        self._print_separator()
+        self._print_body()
+        print()
+
+    @staticmethod
+    def _get_value_str_default(value):
         if isinstance(value, float):
             value_str = '{:0.02f}'.format(value)
         else:
             value_str = str(value)
 
+        return value_str
+
+
+class BarGraph(Graph):
+    def __init__(self, data, get_value, get_label, get_value_str=None,
+                 title=None, label_header=None, unit=None):
+        super().__init__(data, get_value, get_value_str, title, unit)
+
+        self._get_label = get_label
+        self._label_header = label_header
+        self._data = self._transform_data(self._data)
+
+    def _get_graph_datum(self, datum):
+        value = self._get_value(datum)
+        value_str = self._get_value_str(value)
+        label = self._get_label(datum)
+
+        return BarGraphDatum(value, value_str, label)
+
+    def _get_value_str(self, value):
+        value_str = super()._get_value_str(value)
         if self._unit:
             value_str += ' ' + self._unit
 
         return value_str
 
+    def _get_graph_header(self):
+        if not self._label_header:
+            return self._title
+
+        title_len = len(self._title)
+        space_width = (self.MAX_GRAPH_WIDTH - title_len) + \
+                      1 + self._max_value_len + 1
+
+        return self._title + ' ' * space_width + self._label_header
+
+    def _print_header(self):
+        header = self._get_graph_header()
+        print(header)
+
     def _get_bar_str(self, datum):
-        bar_width = int(self.MAX_BAR_WIDTH * datum.value / self._max_value)
-        space_width = self.MAX_BAR_WIDTH - bar_width
+        bar_width = int(self.MAX_GRAPH_WIDTH * datum.value / self._max_value)
+        space_width = self.MAX_GRAPH_WIDTH - bar_width
         bar_str = self.BAR_CHAR * bar_width + ' ' * space_width
 
         return bar_str
 
-    def print_graph(self):
-        if not self._data:
-            return
+    def _print_body(self):
+        for datum in self._data:
+            bar_str = self._get_bar_str(datum)
+            value_padding = ' ' * (self._max_value_len - len(datum.value_str))
+            print(bar_str, value_padding + datum.value_str, datum.label)
 
-        header = self._get_graph_header()
+
+class FreqGraph(Graph):
+    LOWER_BOUND_WIDTH = 8
+
+    def __init__(self, data, get_value, get_lower_bound,
+                 get_value_str=None, title=None, unit=None):
+        super().__init__(data, get_value, get_value_str, title, unit)
+
+        self._get_lower_bound = get_lower_bound
+        self._data = self._transform_data(self._data)
+
+    def _get_graph_datum(self, datum):
+        value = self._get_value(datum)
+        value_str = self._get_value_str(value)
+        lower_bound = self._get_lower_bound(datum)
+
+        return FreqGraphDatum(value, value_str, lower_bound)
+
+    def _print_header(self):
+        header = self._title
+        if self._unit:
+            header += ' ({})'.format(self._unit)
 
         print(header)
-        print(self.HR_CHAR * self.MAX_BAR_WIDTH)
+
+    def _get_bar_str(self, datum):
+        max_width = self.MAX_GRAPH_WIDTH - self.LOWER_BOUND_WIDTH
+        bar_width = int(max_width * datum.value / self._max_value)
+        space_width = max_width - bar_width
+        bar_str = self.BAR_CHAR * bar_width + ' ' * space_width
+
+        return bar_str
+
+    def _print_body(self):
         for datum in self._data:
+            bound_str = FreqGraph._get_bound_str(datum)
             bar_str = self._get_bar_str(datum)
             value_padding = ' ' * (self._max_value_len - len(datum.value_str))
-            print(bar_str, value_padding + datum.value_str, datum.label)
+            print(bound_str, bar_str, value_padding + datum.value_str)
 
-        print()
+    @staticmethod
+    def _get_bound_str(datum):
+        return '{:>7.03f}'.format(datum.lower_bound)
This page took 0.030914 seconds and 5 git commands to generate.