#
# Migration test graph plotting
#
# Copyright (c) 2016 Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see .
#
import sys
class Plot(object):
    # Generated using
    # http://tools.medialab.sciences-po.fr/iwanthue/
    COLORS = ["#CD54D0",
              "#79D94C",
              "#7470CD",
              "#D2D251",
              "#863D79",
              "#76DDA6",
              "#D4467B",
              "#61923D",
              "#CB9CCA",
              "#D98F36",
              "#8CC8DA",
              "#CE4831",
              "#5E7693",
              "#9B803F",
              "#412F4C",
              "#CECBA6",
              "#6D3229",
              "#598B73",
              "#C8827C",
              "#394427"]
    def __init__(self,
                 reports,
                 migration_iters,
                 total_guest_cpu,
                 split_guest_cpu,
                 qemu_cpu,
                 vcpu_cpu):
        self._reports = reports
        self._migration_iters = migration_iters
        self._total_guest_cpu = total_guest_cpu
        self._split_guest_cpu = split_guest_cpu
        self._qemu_cpu = qemu_cpu
        self._vcpu_cpu = vcpu_cpu
        self._color_idx = 0
    def _next_color(self):
        color = self.COLORS[self._color_idx]
        self._color_idx += 1
        if self._color_idx >= len(self.COLORS):
            self._color_idx = 0
        return color
    def _get_progress_label(self, progress):
        if progress:
            return "\n\n" + "\n".join(
                ["Status: %s" % progress._status,
                 "Iteration: %d" % progress._ram._iterations,
                 "Throttle: %02d%%" % progress._throttle_pcent,
                 "Dirty rate: %dMB/s" % (progress._ram._dirty_rate_pps * 4 / 1024.0)])
        else:
            return "\n\n" + "\n".join(
                ["Status: %s" % "none",
                 "Iteration: %d" % 0])
    def _find_start_time(self, report):
        startqemu = report._qemu_timings._records[0]._timestamp
        startguest = report._guest_timings._records[0]._timestamp
        if startqemu < startguest:
            return startqemu
        else:
            return stasrtguest
    def _get_guest_max_value(self, report):
        maxvalue = 0
        for record in report._guest_timings._records:
            if record._value > maxvalue:
                maxvalue = record._value
        return maxvalue
    def _get_qemu_max_value(self, report):
        maxvalue = 0
        oldvalue = None
        oldtime = None
        for record in report._qemu_timings._records:
            if oldvalue is not None:
                cpudelta = (record._value - oldvalue) / 1000.0
                timedelta = record._timestamp - oldtime
                if timedelta == 0:
                    continue
                util = cpudelta / timedelta * 100.0
            else:
                util = 0
            oldvalue = record._value
            oldtime = record._timestamp
            if util > maxvalue:
                maxvalue = util
        return maxvalue
    def _get_total_guest_cpu_graph(self, report, starttime):
        xaxis = []
        yaxis = []
        labels = []
        progress_idx = -1
        for record in report._guest_timings._records:
            while ((progress_idx + 1) < len(report._progress_history) and
                   report._progress_history[progress_idx + 1]._now < record._timestamp):
                progress_idx = progress_idx + 1
            if progress_idx >= 0:
                progress = report._progress_history[progress_idx]
            else:
                progress = None
            xaxis.append(record._timestamp - starttime)
            yaxis.append(record._value)
            labels.append(self._get_progress_label(progress))
        from plotly import graph_objs as go
        return go.Scatter(x=xaxis,
                          y=yaxis,
                          name="Guest PIDs: %s" % report._scenario._name,
                          mode='lines',
                          line={
                              "dash": "solid",
                              "color": self._next_color(),
                              "shape": "linear",
                              "width": 1
                          },
                          text=labels)
    def _get_split_guest_cpu_graphs(self, report, starttime):
        threads = {}
        for record in report._guest_timings._records:
            if record._tid in threads:
                continue
            threads[record._tid] = {
                "xaxis": [],
                "yaxis": [],
                "labels": [],
            }
        progress_idx = -1
        for record in report._guest_timings._records:
            while ((progress_idx + 1) < len(report._progress_history) and
                   report._progress_history[progress_idx + 1]._now < record._timestamp):
                progress_idx = progress_idx + 1
            if progress_idx >= 0:
                progress = report._progress_history[progress_idx]
            else:
                progress = None
            threads[record._tid]["xaxis"].append(record._timestamp - starttime)
            threads[record._tid]["yaxis"].append(record._value)
            threads[record._tid]["labels"].append(self._get_progress_label(progress))
        graphs = []
        from plotly import graph_objs as go
        for tid in threads.keys():
            graphs.append(
                go.Scatter(x=threads[tid]["xaxis"],
                           y=threads[tid]["yaxis"],
                           name="PID %s: %s" % (tid, report._scenario._name),
                           mode="lines",
                           line={
                               "dash": "solid",
                               "color": self._next_color(),
                               "shape": "linear",
                               "width": 1
                           },
                           text=threads[tid]["labels"]))
        return graphs
    def _get_migration_iters_graph(self, report, starttime):
        xaxis = []
        yaxis = []
        labels = []
        for progress in report._progress_history:
            xaxis.append(progress._now - starttime)
            yaxis.append(0)
            labels.append(self._get_progress_label(progress))
        from plotly import graph_objs as go
        return go.Scatter(x=xaxis,
                          y=yaxis,
                          text=labels,
                          name="Migration iterations",
                          mode="markers",
                          marker={
                              "color": self._next_color(),
                              "symbol": "star",
                              "size": 5
                          })
    def _get_qemu_cpu_graph(self, report, starttime):
        xaxis = []
        yaxis = []
        labels = []
        progress_idx = -1
        first = report._qemu_timings._records[0]
        abstimestamps = [first._timestamp]
        absvalues = [first._value]
        for record in report._qemu_timings._records[1:]:
            while ((progress_idx + 1) < len(report._progress_history) and
                   report._progress_history[progress_idx + 1]._now < record._timestamp):
                progress_idx = progress_idx + 1
            if progress_idx >= 0:
                progress = report._progress_history[progress_idx]
            else:
                progress = None
            oldvalue = absvalues[-1]
            oldtime = abstimestamps[-1]
            cpudelta = (record._value - oldvalue) / 1000.0
            timedelta = record._timestamp - oldtime
            if timedelta == 0:
                continue
            util = cpudelta / timedelta * 100.0
            abstimestamps.append(record._timestamp)
            absvalues.append(record._value)
            xaxis.append(record._timestamp - starttime)
            yaxis.append(util)
            labels.append(self._get_progress_label(progress))
        from plotly import graph_objs as go
        return go.Scatter(x=xaxis,
                          y=yaxis,
                          yaxis="y2",
                          name="QEMU: %s" % report._scenario._name,
                          mode='lines',
                          line={
                              "dash": "solid",
                              "color": self._next_color(),
                              "shape": "linear",
                              "width": 1
                          },
                          text=labels)
    def _get_vcpu_cpu_graphs(self, report, starttime):
        threads = {}
        for record in report._vcpu_timings._records:
            if record._tid in threads:
                continue
            threads[record._tid] = {
                "xaxis": [],
                "yaxis": [],
                "labels": [],
                "absvalue": [record._value],
                "abstime": [record._timestamp],
            }
        progress_idx = -1
        for record in report._vcpu_timings._records:
            while ((progress_idx + 1) < len(report._progress_history) and
                   report._progress_history[progress_idx + 1]._now < record._timestamp):
                progress_idx = progress_idx + 1
            if progress_idx >= 0:
                progress = report._progress_history[progress_idx]
            else:
                progress = None
            oldvalue = threads[record._tid]["absvalue"][-1]
            oldtime = threads[record._tid]["abstime"][-1]
            cpudelta = (record._value - oldvalue) / 1000.0
            timedelta = record._timestamp - oldtime
            if timedelta == 0:
                continue
            util = cpudelta / timedelta * 100.0
            if util > 100:
                util = 100
            threads[record._tid]["absvalue"].append(record._value)
            threads[record._tid]["abstime"].append(record._timestamp)
            threads[record._tid]["xaxis"].append(record._timestamp - starttime)
            threads[record._tid]["yaxis"].append(util)
            threads[record._tid]["labels"].append(self._get_progress_label(progress))
        graphs = []
        from plotly import graph_objs as go
        for tid in threads.keys():
            graphs.append(
                go.Scatter(x=threads[tid]["xaxis"],
                           y=threads[tid]["yaxis"],
                           yaxis="y2",
                           name="VCPU %s: %s" % (tid, report._scenario._name),
                           mode="lines",
                           line={
                               "dash": "solid",
                               "color": self._next_color(),
                               "shape": "linear",
                               "width": 1
                           },
                           text=threads[tid]["labels"]))
        return graphs
    def _generate_chart_report(self, report):
        graphs = []
        starttime = self._find_start_time(report)
        if self._total_guest_cpu:
            graphs.append(self._get_total_guest_cpu_graph(report, starttime))
        if self._split_guest_cpu:
            graphs.extend(self._get_split_guest_cpu_graphs(report, starttime))
        if self._qemu_cpu:
            graphs.append(self._get_qemu_cpu_graph(report, starttime))
        if self._vcpu_cpu:
            graphs.extend(self._get_vcpu_cpu_graphs(report, starttime))
        if self._migration_iters:
            graphs.append(self._get_migration_iters_graph(report, starttime))
        return graphs
    def _generate_annotation(self, starttime, progress):
        return {
            "text": progress._status,
            "x": progress._now - starttime,
            "y": 10,
        }
    def _generate_annotations(self, report):
        starttime = self._find_start_time(report)
        annotations = {}
        started = False
        for progress in report._progress_history:
            if progress._status == "setup":
                continue
            if progress._status not in annotations:
                annotations[progress._status] = self._generate_annotation(starttime, progress)
        return annotations.values()
    def _generate_chart(self):
        from plotly.offline import plot
        from plotly import graph_objs as go
        graphs = []
        yaxismax = 0
        yaxismax2 = 0
        for report in self._reports:
            graphs.extend(self._generate_chart_report(report))
            maxvalue = self._get_guest_max_value(report)
            if maxvalue > yaxismax:
                yaxismax = maxvalue
            maxvalue = self._get_qemu_max_value(report)
            if maxvalue > yaxismax2:
                yaxismax2 = maxvalue
        yaxismax += 100
        if not self._qemu_cpu:
            yaxismax2 = 110
        yaxismax2 += 10
        annotations = []
        if self._migration_iters:
            for report in self._reports:
                annotations.extend(self._generate_annotations(report))
        layout = go.Layout(title="Migration comparison",
                           xaxis={
                               "title": "Wallclock time (secs)",
                               "showgrid": False,
                           },
                           yaxis={
                               "title": "Memory update speed (ms/GB)",
                               "showgrid": False,
                               "range": [0, yaxismax],
                           },
                           yaxis2={
                               "title": "Hostutilization (%)",
                               "overlaying": "y",
                               "side": "right",
                               "range": [0, yaxismax2],
                               "showgrid": False,
                           },
                           annotations=annotations)
        figure = go.Figure(data=graphs, layout=layout)
        return plot(figure,
                    show_link=False,
                    include_plotlyjs=False,
                    output_type="div")
    def _generate_report(self):
        pieces = []
        for report in self._reports:
            pieces.append("""
Report %s
""" % report._scenario._name)
            pieces.append("""
  
    | Test config | 
  
    | Emulator: | %s | 
  
    | Kernel: | %s | 
  
    | Ramdisk: | %s | 
  
    | Transport: | %s | 
  
    | Host: | %s | 
""" % (report._binary, report._kernel,
       report._initrd, report._transport, report._dst_host))
            hardware = report._hardware
            pieces.append("""
  
    | Hardware config | 
  
    | CPUs: | %d | 
  
    | RAM: | %d GB | 
  
    | Source CPU bind: | %s | 
  
    | Source RAM bind: | %s | 
  
    | Dest CPU bind: | %s | 
  
    | Dest RAM bind: | %s | 
  
    | Preallocate RAM: | %s | 
  
    | Locked RAM: | %s | 
  
    | Huge pages: | %s | 
""" % (hardware._cpus, hardware._mem,
       ",".join(hardware._src_cpu_bind),
       ",".join(hardware._src_mem_bind),
       ",".join(hardware._dst_cpu_bind),
       ",".join(hardware._dst_mem_bind),
       "yes" if hardware._prealloc_pages else "no",
       "yes" if hardware._locked_pages else "no",
       "yes" if hardware._huge_pages else "no"))
            scenario = report._scenario
            pieces.append("""
  
    | Scenario config | 
  
    | Max downtime: | %d milli-sec | 
  
    | Max bandwidth: | %d MB/sec | 
  
    | Max iters: | %d | 
  
    | Max time: | %d secs | 
  
    | Pause: | %s | 
  
    | Pause iters: | %d | 
  
    | Post-copy: | %s | 
  
    | Post-copy iters: | %d | 
  
    | Auto-converge: | %s | 
  
    | Auto-converge iters: | %d | 
  
    | MT compression: | %s | 
  
    | MT compression threads: | %d | 
  
    | XBZRLE compression: | %s | 
  
    | XBZRLE compression cache: | %d%% of RAM | 
""" % (scenario._downtime, scenario._bandwidth,
       scenario._max_iters, scenario._max_time,
       "yes" if scenario._pause else "no", scenario._pause_iters,
       "yes" if scenario._post_copy else "no", scenario._post_copy_iters,
       "yes" if scenario._auto_converge else "no", scenario._auto_converge_step,
       "yes" if scenario._compression_mt else "no", scenario._compression_mt_threads,
       "yes" if scenario._compression_xbzrle else "no", scenario._compression_xbzrle_cache))
            pieces.append("""
""")
        return "\n".join(pieces)
    def _generate_style(self):
        return """
#report table tr th {
    text-align: right;
}
#report table tr td {
    text-align: left;
}
#report table tr.subhead th {
    background: rgb(192, 192, 192);
    text-align: center;
}
"""
    def generate_html(self, fh):
        print >>fh, """
  
    
    
    Migration report
  
  
    Migration report
    Chart summary
    
""" % self._generate_style()
        print >>fh, self._generate_chart()
        print >>fh, """
    
    Report details
    
"""
        print >>fh, self._generate_report()
        print >>fh, """
    
  
"""
    def generate(self, filename):
        if filename is None:
            self.generate_html(sys.stdout)
        else:
            with open(filename, "w") as fh:
                self.generate_html(fh)