xref: /openbmc/qemu/tests/migration/guestperf/plot.py (revision c63ca4ff)
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