2 # Migration test graph plotting
4 # Copyright (c) 2016 Red Hat, Inc.
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 of the License, or (at your option) any later version.
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.
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/>.
26 # http://tools.medialab.sciences-po.fr/iwanthue/
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
64 def _next_color(self
):
65 color
= self
.COLORS
[self
._color
_idx
]
67 if self
._color
_idx
>= len(self
.COLORS
):
71 def _get_progress_label(self
, 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)])
79 return "\n\n" + "\n".join(
80 ["Status: %s" % "none",
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
:
91 def _get_guest_max_value(self
, report
):
93 for record
in report
._guest
_timings
._records
:
94 if record
._value
> maxvalue
:
95 maxvalue
= record
._value
98 def _get_qemu_max_value(self
, report
):
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
108 util
= cpudelta
/ timedelta
* 100.0
111 oldvalue
= record
._value
112 oldtime
= record
._timestamp
118 def _get_total_guest_cpu_graph(self
, report
, starttime
):
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
128 if progress_idx
>= 0:
129 progress
= report
._progress
_history
[progress_idx
]
133 xaxis
.append(record
._timestamp
- starttime
)
134 yaxis
.append(record
._value
)
135 labels
.append(self
._get
_progress
_label
(progress
))
137 from plotly
import graph_objs
as go
138 return go
.Scatter(x
=xaxis
,
140 name
="Guest PIDs: %s" % report
._scenario
._name
,
144 "color": self
._next
_color
(),
150 def _get_split_guest_cpu_graphs(self
, report
, starttime
):
152 for record
in report
._guest
_timings
._records
:
153 if record
._tid
in threads
:
155 threads
[record
._tid
] = {
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
167 if progress_idx
>= 0:
168 progress
= report
._progress
_history
[progress_idx
]
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
))
178 from plotly
import graph_objs
as go
179 for tid
in threads
.keys():
181 go
.Scatter(x
=threads
[tid
]["xaxis"],
182 y
=threads
[tid
]["yaxis"],
183 name
="PID %s: %s" % (tid
, report
._scenario
._name
),
187 "color": self
._next
_color
(),
191 text
=threads
[tid
]["labels"]))
194 def _get_migration_iters_graph(self
, report
, starttime
):
198 for progress
in report
._progress
_history
:
199 xaxis
.append(progress
._now
- starttime
)
201 labels
.append(self
._get
_progress
_label
(progress
))
203 from plotly
import graph_objs
as go
204 return go
.Scatter(x
=xaxis
,
207 name
="Migration iterations",
210 "color": self
._next
_color
(),
215 def _get_qemu_cpu_graph(self
, report
, starttime
):
221 first
= report
._qemu
_timings
._records
[0]
222 abstimestamps
= [first
._timestamp
]
223 absvalues
= [first
._value
]
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
230 if progress_idx
>= 0:
231 progress
= report
._progress
_history
[progress_idx
]
235 oldvalue
= absvalues
[-1]
236 oldtime
= abstimestamps
[-1]
238 cpudelta
= (record
._value
- oldvalue
) / 1000.0
239 timedelta
= record
._timestamp
- oldtime
242 util
= cpudelta
/ timedelta
* 100.0
244 abstimestamps
.append(record
._timestamp
)
245 absvalues
.append(record
._value
)
247 xaxis
.append(record
._timestamp
- starttime
)
249 labels
.append(self
._get
_progress
_label
(progress
))
251 from plotly
import graph_objs
as go
252 return go
.Scatter(x
=xaxis
,
255 name
="QEMU: %s" % report
._scenario
._name
,
259 "color": self
._next
_color
(),
265 def _get_vcpu_cpu_graphs(self
, report
, starttime
):
267 for record
in report
._vcpu
_timings
._records
:
268 if record
._tid
in threads
:
270 threads
[record
._tid
] = {
274 "absvalue": [record
._value
],
275 "abstime": [record
._timestamp
],
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
284 if progress_idx
>= 0:
285 progress
= report
._progress
_history
[progress_idx
]
289 oldvalue
= threads
[record
._tid
]["absvalue"][-1]
290 oldtime
= threads
[record
._tid
]["abstime"][-1]
292 cpudelta
= (record
._value
- oldvalue
) / 1000.0
293 timedelta
= record
._timestamp
- oldtime
296 util
= cpudelta
/ timedelta
* 100.0
300 threads
[record
._tid
]["absvalue"].append(record
._value
)
301 threads
[record
._tid
]["abstime"].append(record
._timestamp
)
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
))
309 from plotly
import graph_objs
as go
310 for tid
in threads
.keys():
312 go
.Scatter(x
=threads
[tid
]["xaxis"],
313 y
=threads
[tid
]["yaxis"],
315 name
="VCPU %s: %s" % (tid
, report
._scenario
._name
),
319 "color": self
._next
_color
(),
323 text
=threads
[tid
]["labels"]))
326 def _generate_chart_report(self
, report
):
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
))
334 graphs
.append(self
._get
_qemu
_cpu
_graph
(report
, starttime
))
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
))
341 def _generate_annotation(self
, starttime
, progress
):
343 "text": progress
._status
,
344 "x": progress
._now
- starttime
,
348 def _generate_annotations(self
, report
):
349 starttime
= self
._find
_start
_time
(report
)
352 for progress
in report
._progress
_history
:
353 if progress
._status
== "setup":
355 if progress
._status
not in annotations
:
356 annotations
[progress
._status
] = self
._generate
_annotation
(starttime
, progress
)
358 return annotations
.values()
360 def _generate_chart(self
):
361 from plotly
.offline
import plot
362 from plotly
import graph_objs
as go
367 for report
in self
._reports
:
368 graphs
.extend(self
._generate
_chart
_report
(report
))
370 maxvalue
= self
._get
_guest
_max
_value
(report
)
371 if maxvalue
> yaxismax
:
374 maxvalue
= self
._get
_qemu
_max
_value
(report
)
375 if maxvalue
> yaxismax2
:
379 if not self
._qemu
_cpu
:
384 if self
._migration
_iters
:
385 for report
in self
._reports
:
386 annotations
.extend(self
._generate
_annotations
(report
))
388 layout
= go
.Layout(title
="Migration comparison",
390 "title": "Wallclock time (secs)",
394 "title": "Memory update speed (ms/GB)",
396 "range": [0, yaxismax
],
399 "title": "Hostutilization (%)",
402 "range": [0, yaxismax2
],
405 annotations
=annotations
)
407 figure
= go
.Figure(data
=graphs
, layout
=layout
)
411 include_plotlyjs
=False,
415 def _generate_report(self
):
417 for report
in self
._reports
:
421 """ % report
._scenario
._name
)
425 <th colspan="2">Test config</th>
447 """ % (report
._binary
, report
._kernel
,
448 report
._initrd
, report
._transport
, report
._dst
_host
))
450 hardware
= report
._hardware
453 <th colspan="2">Hardware config</th>
464 <th>Source CPU bind:</th>
468 <th>Source RAM bind:</th>
472 <th>Dest CPU bind:</th>
476 <th>Dest RAM bind:</th>
480 <th>Preallocate RAM:</th>
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"))
500 scenario
= report
._scenario
503 <th colspan="2">Scenario config</th>
506 <th>Max downtime:</th>
507 <td>%d milli-sec</td>
510 <th>Max bandwidth:</th>
526 <th>Pause iters:</th>
534 <th>Post-copy iters:</th>
538 <th>Auto-converge:</th>
542 <th>Auto-converge iters:</th>
546 <th>MT compression:</th>
550 <th>MT compression threads:</th>
554 <th>XBZRLE compression:</th>
558 <th>XBZRLE compression cache:</th>
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
))
573 return "\n".join(pieces
)
575 def _generate_style(self
):
577 #report table tr th {
580 #report table tr td {
583 #report table tr.subhead th {
584 background: rgb(192, 192, 192);
590 def generate_html(self
, fh
):
591 print >>fh
, """<html>
593 <script type="text/javascript" src="plotly.min.js">
595 <style type="text/css">
598 <title>Migration report</title>
601 <h1>Migration report</h1>
602 <h2>Chart summary</h2>
604 """ % self
._generate
_style
()
605 print >>fh
, self
._generate
_chart
()
608 <h2>Report details</h2>
611 print >>fh
, self
._generate
_report
()
618 def generate(self
, filename
):
620 self
.generate_html(sys
.stdout
)
622 with
open(filename
, "w") as fh
:
623 self
.generate_html(fh
)