2
0

plot.py 19 KB

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