qemu/tests/migration/guestperf/plot.py
<<
>>
Prefs
   1#
   2# Migration test graph plotting
   3#
   4# Copyright (c) 2016 Red Hat, Inc.
   5#
   6# This library is free software; you can redistribute it and/or
   7# modify it under the terms of the GNU Lesser General Public
   8# License as published by the Free Software Foundation; either
   9# version 2.1 of the License, or (at your option) any later version.
  10#
  11# This library is distributed in the hope that it will be useful,
  12# but WITHOUT ANY WARRANTY; without even the implied warranty of
  13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14# Lesser General Public License for more details.
  15#
  16# You should have received a copy of the GNU Lesser General Public
  17# License along with this library; if not, see <http://www.gnu.org/licenses/>.
  18#
  19
  20import sys
  21
  22
  23class Plot(object):
  24
  25    # Generated using
  26    # http://tools.medialab.sciences-po.fr/iwanthue/
  27    COLORS = ["#CD54D0",
  28              "#79D94C",
  29              "#7470CD",
  30              "#D2D251",
  31              "#863D79",
  32              "#76DDA6",
  33              "#D4467B",
  34              "#61923D",
  35              "#CB9CCA",
  36              "#D98F36",
  37              "#8CC8DA",
  38              "#CE4831",
  39              "#5E7693",
  40              "#9B803F",
  41              "#412F4C",
  42              "#CECBA6",
  43              "#6D3229",
  44              "#598B73",
  45              "#C8827C",
  46              "#394427"]
  47
  48    def __init__(self,
  49                 reports,
  50                 migration_iters,
  51                 total_guest_cpu,
  52                 split_guest_cpu,
  53                 qemu_cpu,
  54                 vcpu_cpu):
  55
  56        self._reports = reports
  57        self._migration_iters = migration_iters
  58        self._total_guest_cpu = total_guest_cpu
  59        self._split_guest_cpu = split_guest_cpu
  60        self._qemu_cpu = qemu_cpu
  61        self._vcpu_cpu = vcpu_cpu
  62        self._color_idx = 0
  63
  64    def _next_color(self):
  65        color = self.COLORS[self._color_idx]
  66        self._color_idx += 1
  67        if self._color_idx >= len(self.COLORS):
  68            self._color_idx = 0
  69        return color
  70
  71    def _get_progress_label(self, progress):
  72        if progress:
  73            return "\n\n" + "\n".join(
  74                ["Status: %s" % progress._status,
  75                 "Iteration: %d" % progress._ram._iterations,
  76                 "Throttle: %02d%%" % progress._throttle_pcent,
  77                 "Dirty rate: %dMB/s" % (progress._ram._dirty_rate_pps * 4 / 1024.0)])
  78        else:
  79            return "\n\n" + "\n".join(
  80                ["Status: %s" % "none",
  81                 "Iteration: %d" % 0])
  82
  83    def _find_start_time(self, report):
  84        startqemu = report._qemu_timings._records[0]._timestamp
  85        startguest = report._guest_timings._records[0]._timestamp
  86        if startqemu < startguest:
  87            return startqemu
  88        else:
  89            return stasrtguest
  90
  91    def _get_guest_max_value(self, report):
  92        maxvalue = 0
  93        for record in report._guest_timings._records:
  94            if record._value > maxvalue:
  95                maxvalue = record._value
  96        return maxvalue
  97
  98    def _get_qemu_max_value(self, report):
  99        maxvalue = 0
 100        oldvalue = None
 101        oldtime = None
 102        for record in report._qemu_timings._records:
 103            if oldvalue is not None:
 104                cpudelta = (record._value - oldvalue) / 1000.0
 105                timedelta = record._timestamp - oldtime
 106                if timedelta == 0:
 107                    continue
 108                util = cpudelta / timedelta * 100.0
 109            else:
 110                util = 0
 111            oldvalue = record._value
 112            oldtime = record._timestamp
 113
 114            if util > maxvalue:
 115                maxvalue = util
 116        return maxvalue
 117
 118    def _get_total_guest_cpu_graph(self, report, starttime):
 119        xaxis = []
 120        yaxis = []
 121        labels = []
 122        progress_idx = -1
 123        for record in report._guest_timings._records:
 124            while ((progress_idx + 1) < len(report._progress_history) and
 125                   report._progress_history[progress_idx + 1]._now < record._timestamp):
 126                progress_idx = progress_idx + 1
 127
 128            if progress_idx >= 0:
 129                progress = report._progress_history[progress_idx]
 130            else:
 131                progress = None
 132
 133            xaxis.append(record._timestamp - starttime)
 134            yaxis.append(record._value)
 135            labels.append(self._get_progress_label(progress))
 136
 137        from plotly import graph_objs as go
 138        return go.Scatter(x=xaxis,
 139                          y=yaxis,
 140                          name="Guest PIDs: %s" % report._scenario._name,
 141                          mode='lines',
 142                          line={
 143                              "dash": "solid",
 144                              "color": self._next_color(),
 145                              "shape": "linear",
 146                              "width": 1
 147                          },
 148                          text=labels)
 149
 150    def _get_split_guest_cpu_graphs(self, report, starttime):
 151        threads = {}
 152        for record in report._guest_timings._records:
 153            if record._tid in threads:
 154                continue
 155            threads[record._tid] = {
 156                "xaxis": [],
 157                "yaxis": [],
 158                "labels": [],
 159            }
 160
 161        progress_idx = -1
 162        for record in report._guest_timings._records:
 163            while ((progress_idx + 1) < len(report._progress_history) and
 164                   report._progress_history[progress_idx + 1]._now < record._timestamp):
 165                progress_idx = progress_idx + 1
 166
 167            if progress_idx >= 0:
 168                progress = report._progress_history[progress_idx]
 169            else:
 170                progress = None
 171
 172            threads[record._tid]["xaxis"].append(record._timestamp - starttime)
 173            threads[record._tid]["yaxis"].append(record._value)
 174            threads[record._tid]["labels"].append(self._get_progress_label(progress))
 175
 176
 177        graphs = []
 178        from plotly import graph_objs as go
 179        for tid in threads.keys():
 180            graphs.append(
 181                go.Scatter(x=threads[tid]["xaxis"],
 182                           y=threads[tid]["yaxis"],
 183                           name="PID %s: %s" % (tid, report._scenario._name),
 184                           mode="lines",
 185                           line={
 186                               "dash": "solid",
 187                               "color": self._next_color(),
 188                               "shape": "linear",
 189                               "width": 1
 190                           },
 191                           text=threads[tid]["labels"]))
 192        return graphs
 193
 194    def _get_migration_iters_graph(self, report, starttime):
 195        xaxis = []
 196        yaxis = []
 197        labels = []
 198        for progress in report._progress_history:
 199            xaxis.append(progress._now - starttime)
 200            yaxis.append(0)
 201            labels.append(self._get_progress_label(progress))
 202
 203        from plotly import graph_objs as go
 204        return go.Scatter(x=xaxis,
 205                          y=yaxis,
 206                          text=labels,
 207                          name="Migration iterations",
 208                          mode="markers",
 209                          marker={
 210                              "color": self._next_color(),
 211                              "symbol": "star",
 212                              "size": 5
 213                          })
 214
 215    def _get_qemu_cpu_graph(self, report, starttime):
 216        xaxis = []
 217        yaxis = []
 218        labels = []
 219        progress_idx = -1
 220
 221        first = report._qemu_timings._records[0]
 222        abstimestamps = [first._timestamp]
 223        absvalues = [first._value]
 224
 225        for record in report._qemu_timings._records[1:]:
 226            while ((progress_idx + 1) < len(report._progress_history) and
 227                   report._progress_history[progress_idx + 1]._now < record._timestamp):
 228                progress_idx = progress_idx + 1
 229
 230            if progress_idx >= 0:
 231                progress = report._progress_history[progress_idx]
 232            else:
 233                progress = None
 234
 235            oldvalue = absvalues[-1]
 236            oldtime = abstimestamps[-1]
 237
 238            cpudelta = (record._value - oldvalue) / 1000.0
 239            timedelta = record._timestamp - oldtime
 240            if timedelta == 0:
 241                continue
 242            util = cpudelta / timedelta * 100.0
 243
 244            abstimestamps.append(record._timestamp)
 245            absvalues.append(record._value)
 246
 247            xaxis.append(record._timestamp - starttime)
 248            yaxis.append(util)
 249            labels.append(self._get_progress_label(progress))
 250
 251        from plotly import graph_objs as go
 252        return go.Scatter(x=xaxis,
 253                          y=yaxis,
 254                          yaxis="y2",
 255                          name="QEMU: %s" % report._scenario._name,
 256                          mode='lines',
 257                          line={
 258                              "dash": "solid",
 259                              "color": self._next_color(),
 260                              "shape": "linear",
 261                              "width": 1
 262                          },
 263                          text=labels)
 264
 265    def _get_vcpu_cpu_graphs(self, report, starttime):
 266        threads = {}
 267        for record in report._vcpu_timings._records:
 268            if record._tid in threads:
 269                continue
 270            threads[record._tid] = {
 271                "xaxis": [],
 272                "yaxis": [],
 273                "labels": [],
 274                "absvalue": [record._value],
 275                "abstime": [record._timestamp],
 276            }
 277
 278        progress_idx = -1
 279        for record in report._vcpu_timings._records:
 280            while ((progress_idx + 1) < len(report._progress_history) and
 281                   report._progress_history[progress_idx + 1]._now < record._timestamp):
 282                progress_idx = progress_idx + 1
 283
 284            if progress_idx >= 0:
 285                progress = report._progress_history[progress_idx]
 286            else:
 287                progress = None
 288
 289            oldvalue = threads[record._tid]["absvalue"][-1]
 290            oldtime = threads[record._tid]["abstime"][-1]
 291
 292            cpudelta = (record._value - oldvalue) / 1000.0
 293            timedelta = record._timestamp - oldtime
 294            if timedelta == 0:
 295                continue
 296            util = cpudelta / timedelta * 100.0
 297            if util > 100:
 298                util = 100
 299
 300            threads[record._tid]["absvalue"].append(record._value)
 301            threads[record._tid]["abstime"].append(record._timestamp)
 302
 303            threads[record._tid]["xaxis"].append(record._timestamp - starttime)
 304            threads[record._tid]["yaxis"].append(util)
 305            threads[record._tid]["labels"].append(self._get_progress_label(progress))
 306
 307
 308        graphs = []
 309        from plotly import graph_objs as go
 310        for tid in threads.keys():
 311            graphs.append(
 312                go.Scatter(x=threads[tid]["xaxis"],
 313                           y=threads[tid]["yaxis"],
 314                           yaxis="y2",
 315                           name="VCPU %s: %s" % (tid, report._scenario._name),
 316                           mode="lines",
 317                           line={
 318                               "dash": "solid",
 319                               "color": self._next_color(),
 320                               "shape": "linear",
 321                               "width": 1
 322                           },
 323                           text=threads[tid]["labels"]))
 324        return graphs
 325
 326    def _generate_chart_report(self, report):
 327        graphs = []
 328        starttime = self._find_start_time(report)
 329        if self._total_guest_cpu:
 330            graphs.append(self._get_total_guest_cpu_graph(report, starttime))
 331        if self._split_guest_cpu:
 332            graphs.extend(self._get_split_guest_cpu_graphs(report, starttime))
 333        if self._qemu_cpu:
 334            graphs.append(self._get_qemu_cpu_graph(report, starttime))
 335        if self._vcpu_cpu:
 336            graphs.extend(self._get_vcpu_cpu_graphs(report, starttime))
 337        if self._migration_iters:
 338            graphs.append(self._get_migration_iters_graph(report, starttime))
 339        return graphs
 340
 341    def _generate_annotation(self, starttime, progress):
 342        return {
 343            "text": progress._status,
 344            "x": progress._now - starttime,
 345            "y": 10,
 346        }
 347
 348    def _generate_annotations(self, report):
 349        starttime = self._find_start_time(report)
 350        annotations = {}
 351        started = False
 352        for progress in report._progress_history:
 353            if progress._status == "setup":
 354                continue
 355            if progress._status not in annotations:
 356                annotations[progress._status] = self._generate_annotation(starttime, progress)
 357
 358        return annotations.values()
 359
 360    def _generate_chart(self):
 361        from plotly.offline import plot
 362        from plotly import graph_objs as go
 363
 364        graphs = []
 365        yaxismax = 0
 366        yaxismax2 = 0
 367        for report in self._reports:
 368            graphs.extend(self._generate_chart_report(report))
 369
 370            maxvalue = self._get_guest_max_value(report)
 371            if maxvalue > yaxismax:
 372                yaxismax = maxvalue
 373
 374            maxvalue = self._get_qemu_max_value(report)
 375            if maxvalue > yaxismax2:
 376                yaxismax2 = maxvalue
 377
 378        yaxismax += 100
 379        if not self._qemu_cpu:
 380            yaxismax2 = 110
 381        yaxismax2 += 10
 382
 383        annotations = []
 384        if self._migration_iters:
 385            for report in self._reports:
 386                annotations.extend(self._generate_annotations(report))
 387
 388        layout = go.Layout(title="Migration comparison",
 389                           xaxis={
 390                               "title": "Wallclock time (secs)",
 391                               "showgrid": False,
 392                           },
 393                           yaxis={
 394                               "title": "Memory update speed (ms/GB)",
 395                               "showgrid": False,
 396                               "range": [0, yaxismax],
 397                           },
 398                           yaxis2={
 399                               "title": "Hostutilization (%)",
 400                               "overlaying": "y",
 401                               "side": "right",
 402                               "range": [0, yaxismax2],
 403                               "showgrid": False,
 404                           },
 405                           annotations=annotations)
 406
 407        figure = go.Figure(data=graphs, layout=layout)
 408
 409        return plot(figure,
 410                    show_link=False,
 411                    include_plotlyjs=False,
 412                    output_type="div")
 413
 414
 415    def _generate_report(self):
 416        pieces = []
 417        for report in self._reports:
 418            pieces.append("""
 419<h3>Report %s</h3>
 420<table>
 421""" % report._scenario._name)
 422
 423            pieces.append("""
 424  <tr class="subhead">
 425    <th colspan="2">Test config</th>
 426  </tr>
 427  <tr>
 428    <th>Emulator:</th>
 429    <td>%s</td>
 430  </tr>
 431  <tr>
 432    <th>Kernel:</th>
 433    <td>%s</td>
 434  </tr>
 435  <tr>
 436    <th>Ramdisk:</th>
 437    <td>%s</td>
 438  </tr>
 439  <tr>
 440    <th>Transport:</th>
 441    <td>%s</td>
 442  </tr>
 443  <tr>
 444    <th>Host:</th>
 445    <td>%s</td>
 446  </tr>
 447""" % (report._binary, report._kernel,
 448       report._initrd, report._transport, report._dst_host))
 449
 450            hardware = report._hardware
 451            pieces.append("""
 452  <tr class="subhead">
 453    <th colspan="2">Hardware config</th>
 454  </tr>
 455  <tr>
 456    <th>CPUs:</th>
 457    <td>%d</td>
 458  </tr>
 459  <tr>
 460    <th>RAM:</th>
 461    <td>%d GB</td>
 462  </tr>
 463  <tr>
 464    <th>Source CPU bind:</th>
 465    <td>%s</td>
 466  </tr>
 467  <tr>
 468    <th>Source RAM bind:</th>
 469    <td>%s</td>
 470  </tr>
 471  <tr>
 472    <th>Dest CPU bind:</th>
 473    <td>%s</td>
 474  </tr>
 475  <tr>
 476    <th>Dest RAM bind:</th>
 477    <td>%s</td>
 478  </tr>
 479  <tr>
 480    <th>Preallocate RAM:</th>
 481    <td>%s</td>
 482  </tr>
 483  <tr>
 484    <th>Locked RAM:</th>
 485    <td>%s</td>
 486  </tr>
 487  <tr>
 488    <th>Huge pages:</th>
 489    <td>%s</td>
 490  </tr>
 491""" % (hardware._cpus, hardware._mem,
 492       ",".join(hardware._src_cpu_bind),
 493       ",".join(hardware._src_mem_bind),
 494       ",".join(hardware._dst_cpu_bind),
 495       ",".join(hardware._dst_mem_bind),
 496       "yes" if hardware._prealloc_pages else "no",
 497       "yes" if hardware._locked_pages else "no",
 498       "yes" if hardware._huge_pages else "no"))
 499
 500            scenario = report._scenario
 501            pieces.append("""
 502  <tr class="subhead">
 503    <th colspan="2">Scenario config</th>
 504  </tr>
 505  <tr>
 506    <th>Max downtime:</th>
 507    <td>%d milli-sec</td>
 508  </tr>
 509  <tr>
 510    <th>Max bandwidth:</th>
 511    <td>%d MB/sec</td>
 512  </tr>
 513  <tr>
 514    <th>Max iters:</th>
 515    <td>%d</td>
 516  </tr>
 517  <tr>
 518    <th>Max time:</th>
 519    <td>%d secs</td>
 520  </tr>
 521  <tr>
 522    <th>Pause:</th>
 523    <td>%s</td>
 524  </tr>
 525  <tr>
 526    <th>Pause iters:</th>
 527    <td>%d</td>
 528  </tr>
 529  <tr>
 530    <th>Post-copy:</th>
 531    <td>%s</td>
 532  </tr>
 533  <tr>
 534    <th>Post-copy iters:</th>
 535    <td>%d</td>
 536  </tr>
 537  <tr>
 538    <th>Auto-converge:</th>
 539    <td>%s</td>
 540  </tr>
 541  <tr>
 542    <th>Auto-converge iters:</th>
 543    <td>%d</td>
 544  </tr>
 545  <tr>
 546    <th>MT compression:</th>
 547    <td>%s</td>
 548  </tr>
 549  <tr>
 550    <th>MT compression threads:</th>
 551    <td>%d</td>
 552  </tr>
 553  <tr>
 554    <th>XBZRLE compression:</th>
 555    <td>%s</td>
 556  </tr>
 557  <tr>
 558    <th>XBZRLE compression cache:</th>
 559    <td>%d%% of RAM</td>
 560  </tr>
 561""" % (scenario._downtime, scenario._bandwidth,
 562       scenario._max_iters, scenario._max_time,
 563       "yes" if scenario._pause else "no", scenario._pause_iters,
 564       "yes" if scenario._post_copy else "no", scenario._post_copy_iters,
 565       "yes" if scenario._auto_converge else "no", scenario._auto_converge_step,
 566       "yes" if scenario._compression_mt else "no", scenario._compression_mt_threads,
 567       "yes" if scenario._compression_xbzrle else "no", scenario._compression_xbzrle_cache))
 568
 569            pieces.append("""
 570</table>
 571""")
 572
 573        return "\n".join(pieces)
 574
 575    def _generate_style(self):
 576        return """
 577#report table tr th {
 578    text-align: right;
 579}
 580#report table tr td {
 581    text-align: left;
 582}
 583#report table tr.subhead th {
 584    background: rgb(192, 192, 192);
 585    text-align: center;
 586}
 587
 588"""
 589
 590    def generate_html(self, fh):
 591        print("""<html>
 592  <head>
 593    <script type="text/javascript" src="plotly.min.js">
 594    </script>
 595    <style type="text/css">
 596%s
 597    </style>
 598    <title>Migration report</title>
 599  </head>
 600  <body>
 601    <h1>Migration report</h1>
 602    <h2>Chart summary</h2>
 603    <div id="chart">
 604""" % self._generate_style(), file=fh)
 605        print(self._generate_chart(), file=fh)
 606        print("""
 607    </div>
 608    <h2>Report details</h2>
 609    <div id="report">
 610""", file=fh)
 611        print(self._generate_report(), file=fh)
 612        print("""
 613    </div>
 614  </body>
 615</html>
 616""", file=fh)
 617
 618    def generate(self, filename):
 619        if filename is None:
 620            self.generate_html(sys.stdout)
 621        else:
 622            with open(filename, "w") as fh:
 623                self.generate_html(fh)
 624