diff options
| author | Stefan Hajnoczi <stefanha@redhat.com> | 2024-12-12 18:45:09 -0500 |
|---|---|---|
| committer | Stefan Hajnoczi <stefanha@redhat.com> | 2024-12-12 18:45:09 -0500 |
| commit | bdce9bc9179bd7b6f4e12c759dd3cd6794e26a6b (patch) | |
| tree | ac50a528b74309d205ce97bf770d7e8b1b0fd5c4 /tests/migration-stress/guestperf/plot.py | |
| parent | 1eec82cc06cd68b6ffaf13ba8337fac0080c7bce (diff) | |
| parent | de8bc62cc4871fa97d1891ebb96e782b0c75b56a (diff) | |
| download | focaccia-qemu-bdce9bc9179bd7b6f4e12c759dd3cd6794e26a6b.tar.gz focaccia-qemu-bdce9bc9179bd7b6f4e12c759dd3cd6794e26a6b.zip | |
Merge tag 'qtest-20241212-pull-request' of https://gitlab.com/farosas/qemu into staging
Qtest pull request - TIMEOUT_MULTIPLIER setting to allow tests to take longer when asan is enabled - New qtest_system_reset() wrapper to properly wait for a system reset - Split of migration-test.c into multiple files under qtest/migration/ # -----BEGIN PGP SIGNATURE----- # # iQJEBAABCAAuFiEEqhtIsKIjJqWkw2TPx5jcdBvsMZ0FAmda+84QHGZhcm9zYXNA # c3VzZS5kZQAKCRDHmNx0G+wxnXWZEACcxW/P9EiLxIeCf4ke7/qq+8HSOOLdQrEa # hP5ZCzLlY0K8KQ9fhTGvc6UgRBsScXcLuj4eoR8Gke9e4tID39XVdkion/x6OCiv # grC2Ll9/BNrWtR8DiXs3krsU1CXhav6qcT56bMiXV3Qo+KsTmB7RkpXpbkZLZxkn # P2aqZlYFS0zZ3PZQceCasQEzB6Dls7o1wNXvgSJXr2E4IuIEcbyWtQpr5DfctnfN # STbqGTgMXADyjlqh3CWwSLsZQvIVoN9+3twOrZn4j0D+fyEsTjKsZPlUpRtr72ap # FVT+zVIUfxieBlDTsRSwcieD1ePFUUjl7ikQFwypGs33TI9VDTsB88IG7h0/080o # 7dYbSMSG0pGEq9QIB8jkV26OZi3y00L/zmLMNDsN6nxKN4mSySlfVD/B/Jb70Smy # lwbLklnr921kqsF89Bsq8YRyg8bbGXtx2ifGROgqF+JtrxRW0ElF9K/h4HQtPjuD # nZJ4rtrDIv1iMTLJwbNu39fhxvuQowdBYTrbPMgw+xIv++LodqYJSDxbEX+FaDqI # LMf772jiAgesl7qe1d9TJizhvonyi9PIdcUILg0SH0H1KvoSjpS3mlGgvo8zgvh+ # zvhhzkFGDIHmlCvOaIYyCpadJ5SMMS627q/Q13X3DHjUTkpPUhxGgc9oO7T39+iA # 4dOLDRjUcg== # =H6xv # -----END PGP SIGNATURE----- # gpg: Signature made Thu 12 Dec 2024 10:05:50 EST # gpg: using RSA key AA1B48B0A22326A5A4C364CFC798DC741BEC319D # gpg: issuer "farosas@suse.de" # gpg: Good signature from "Fabiano Rosas <farosas@suse.de>" [unknown] # gpg: aka "Fabiano Almeida Rosas <fabiano.rosas@suse.com>" [unknown] # gpg: WARNING: The key's User ID is not certified with a trusted signature! # gpg: There is no indication that the signature belongs to the owner. # Primary key fingerprint: AA1B 48B0 A223 26A5 A4C3 64CF C798 DC74 1BEC 319D * tag 'qtest-20241212-pull-request' of https://gitlab.com/farosas/qemu: (22 commits) tests/qtest/migration: Split validation tests + misc tests/qtest/migration-test: Fix and enable test_ignore_shared tests/qtest/migration: Split CPR tests tests/qtest/migration: Split precopy tests tests/qtest/migration: Split file tests tests/qtest/migration: Split postcopy tests tests/qtest/migration: Split compression tests from migration-test.c tests/qtest/migration: Split TLS tests from migration-test.c tests/qtest/migration: Move common test code tests/qtest/migration: Isolate test initialization tests/qtest/migration: Move kvm_dirty_ring_supported to utils tests/qtest/migration: Move ufd_version_check to utils tests/qtest/migration: Rename migration-helpers.c tests/qtest/migration: Move qmp helpers to a separate file tests/qtest/migration: Move bootfile code to its own file tests/migration: Disambiguate guestperf vs. a-b tests/qtest/migration: Stop calling everything "test" tests/qtest/migration: Standardize hook names tests/qtest: Use qtest_system_reset_nowait() where appropriate tests/qtest: Use qtest_system_reset() instead of open-coded versions ... Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
Diffstat (limited to 'tests/migration-stress/guestperf/plot.py')
| -rw-r--r-- | tests/migration-stress/guestperf/plot.py | 623 |
1 files changed, 623 insertions, 0 deletions
diff --git a/tests/migration-stress/guestperf/plot.py b/tests/migration-stress/guestperf/plot.py new file mode 100644 index 0000000000..30b3f668d0 --- /dev/null +++ b/tests/migration-stress/guestperf/plot.py @@ -0,0 +1,623 @@ +# +# 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.1 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 <http://www.gnu.org/licenses/>. +# + +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(""" +<h3>Report %s</h3> +<table> +""" % report._scenario._name) + + pieces.append(""" + <tr class="subhead"> + <th colspan="2">Test config</th> + </tr> + <tr> + <th>Emulator:</th> + <td>%s</td> + </tr> + <tr> + <th>Kernel:</th> + <td>%s</td> + </tr> + <tr> + <th>Ramdisk:</th> + <td>%s</td> + </tr> + <tr> + <th>Transport:</th> + <td>%s</td> + </tr> + <tr> + <th>Host:</th> + <td>%s</td> + </tr> +""" % (report._binary, report._kernel, + report._initrd, report._transport, report._dst_host)) + + hardware = report._hardware + pieces.append(""" + <tr class="subhead"> + <th colspan="2">Hardware config</th> + </tr> + <tr> + <th>CPUs:</th> + <td>%d</td> + </tr> + <tr> + <th>RAM:</th> + <td>%d GB</td> + </tr> + <tr> + <th>Source CPU bind:</th> + <td>%s</td> + </tr> + <tr> + <th>Source RAM bind:</th> + <td>%s</td> + </tr> + <tr> + <th>Dest CPU bind:</th> + <td>%s</td> + </tr> + <tr> + <th>Dest RAM bind:</th> + <td>%s</td> + </tr> + <tr> + <th>Preallocate RAM:</th> + <td>%s</td> + </tr> + <tr> + <th>Locked RAM:</th> + <td>%s</td> + </tr> + <tr> + <th>Huge pages:</th> + <td>%s</td> + </tr> +""" % (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(""" + <tr class="subhead"> + <th colspan="2">Scenario config</th> + </tr> + <tr> + <th>Max downtime:</th> + <td>%d milli-sec</td> + </tr> + <tr> + <th>Max bandwidth:</th> + <td>%d MB/sec</td> + </tr> + <tr> + <th>Max iters:</th> + <td>%d</td> + </tr> + <tr> + <th>Max time:</th> + <td>%d secs</td> + </tr> + <tr> + <th>Pause:</th> + <td>%s</td> + </tr> + <tr> + <th>Pause iters:</th> + <td>%d</td> + </tr> + <tr> + <th>Post-copy:</th> + <td>%s</td> + </tr> + <tr> + <th>Post-copy iters:</th> + <td>%d</td> + </tr> + <tr> + <th>Auto-converge:</th> + <td>%s</td> + </tr> + <tr> + <th>Auto-converge iters:</th> + <td>%d</td> + </tr> + <tr> + <th>MT compression:</th> + <td>%s</td> + </tr> + <tr> + <th>MT compression threads:</th> + <td>%d</td> + </tr> + <tr> + <th>XBZRLE compression:</th> + <td>%s</td> + </tr> + <tr> + <th>XBZRLE compression cache:</th> + <td>%d%% of RAM</td> + </tr> +""" % (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(""" +</table> +""") + + 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("""<html> + <head> + <script type="text/javascript" src="plotly.min.js"> + </script> + <style type="text/css"> +%s + </style> + <title>Migration report</title> + </head> + <body> + <h1>Migration report</h1> + <h2>Chart summary</h2> + <div id="chart"> +""" % self._generate_style(), file=fh) + print(self._generate_chart(), file=fh) + print(""" + </div> + <h2>Report details</h2> + <div id="report"> +""", file=fh) + print(self._generate_report(), file=fh) + print(""" + </div> + </body> +</html> +""", file=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) |