1#!/usr/bin/env python3 2 3# Copyright 2023 The ChromiumOS Authors 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7from collections import defaultdict 8import argparse 9import subprocess 10import os 11import json 12import pandas as pd 13import plotly 14import plotly.graph_objects as go 15import plotly.express as px 16 17 18class BalloonRecord: 19 def __init__(self, rec): 20 byte_to_gb = 1024.0**3 21 self.total = rec["balloon_stats"]["stats"]["total_memory"] / byte_to_gb 22 self.free = rec["balloon_stats"]["stats"]["free_memory"] / byte_to_gb 23 self.disk_caches = rec["balloon_stats"]["stats"]["disk_caches"] / byte_to_gb 24 self.avail = rec["balloon_stats"]["stats"]["available_memory"] / byte_to_gb 25 26 self.shared_memory = (rec["balloon_stats"]["stats"]["shared_memory"] or 0.0) / byte_to_gb 27 self.unevictable_memory = ( 28 rec["balloon_stats"]["stats"]["unevictable_memory"] or 0.0 29 ) / byte_to_gb 30 self.balloon_actual = (rec["balloon_stats"]["balloon_actual"] or 0.0) / byte_to_gb 31 32 33class Records: 34 def __init__(self) -> None: 35 self.data = [] 36 37 def add(self, timestamp, name, mem_usage) -> None: 38 self.data.append({"boot time (sec)": timestamp, "process": name, "PSS (GB)": mem_usage}) 39 40 41def memstat_plot(data, args) -> str: 42 names = set() 43 for rec in data: 44 for p in rec["stats"]: 45 names.add(p["name"]) 46 47 recs = Records() 48 ballon_sizes = [[], []] 49 50 total_memory_size = BalloonRecord(data[-1]).total 51 for rec in data: 52 timestamp = int(rec["timestamp"]) 53 54 balloon = None 55 if rec["balloon_stats"]: 56 balloon = BalloonRecord(rec) 57 58 # Dict: name -> (dict: field -> value) 59 # Example: { "crosvm": {"Pss": 100, "Rss": 120, ...}, "virtio-blk": ... } 60 proc_to_smaps = {name: defaultdict(int) for name in names} 61 62 # Summarize multiple processes using the same name such as multiple virtiofs devices. 63 for p in rec["stats"]: 64 name = p["name"] 65 for key in p["smaps"]: 66 val = p["smaps"][key] 67 # Convert the value from KB to GB. 68 proc_to_smaps[name][key] += val / (1024.0**2) 69 70 for p in rec["stats"]: 71 name = p["name"] 72 smaps = proc_to_smaps[name] 73 74 if name != "crosvm": 75 # TODO: We may want to track VmPTE too. 76 # https://chromium-review.googlesource.com/c/crosvm/crosvm/+/4712086/comment/9e08afd5_2fd05550/ 77 recs.add(timestamp, name, smaps["Private_Dirty"]) 78 continue 79 80 assert name == "crosvm" 81 if not balloon: 82 recs.add(timestamp, "crosvm (guest disk caches)", 0) 83 recs.add(timestamp, "crosvm (guest shared memory)", 0) 84 recs.add(timestamp, "crosvm (guest unevictable)", 0) 85 recs.add(timestamp, "crosvm (guest used)", 0) 86 87 recs.add(timestamp, "crosvm (host)", smaps["Rss"]) 88 89 ballon_sizes[0].append(timestamp) 90 ballon_sizes[1].append(total_memory_size) 91 92 continue 93 94 recs.add(timestamp, "crosvm (guest disk caches)", balloon.disk_caches) 95 recs.add(timestamp, "crosvm (guest shared memory)", balloon.shared_memory) 96 recs.add(timestamp, "crosvm (guest unevictable)", balloon.unevictable_memory) 97 98 # (guest used) = (guest total = host's RSS) - (free + balloon_actual + disk caches + shared memory) 99 guest_used = ( 100 balloon.total 101 - balloon.free 102 - balloon.balloon_actual 103 - balloon.disk_caches 104 - balloon.shared_memory 105 - balloon.unevictable_memory 106 ) 107 assert guest_used >= 0 108 if guest_used > proc_to_smaps["crosvm"]["Rss"]: 109 print( 110 "WARNING: guest_used > crosvm RSS: {} > {}".format( 111 guest_used, proc_to_smaps["crosvm"]["Rss"] 112 ) 113 ) 114 115 recs.add(timestamp, "crosvm (guest used)", guest_used) 116 crosvm_host = ( 117 proc_to_smaps["crosvm"]["Rss"] 118 - guest_used 119 - balloon.disk_caches 120 - balloon.shared_memory 121 - balloon.unevictable_memory 122 ) 123 if crosvm_host < 0: 124 print("WARNING: crosvm (host) < 0: {}".format(crosvm_host)) 125 recs.add(timestamp, "crosvm (host)", crosvm_host) 126 127 ballon_sizes[0].append(timestamp) 128 ballon_sizes[1].append(balloon.total - balloon.balloon_actual) 129 130 df = pd.DataFrame(recs.data) 131 fig = px.area( 132 df, 133 x="boot time (sec)", 134 y="Memory usage (GB)", 135 color="process", 136 ) 137 fig.update_layout(title={"text": args.title}) 138 fig.add_trace( 139 go.Scatter( 140 x=ballon_sizes[0], 141 y=ballon_sizes[1], 142 mode="lines", 143 name="(total memory) - (balloon size)", 144 ) 145 ) 146 147 base, _ = os.path.splitext(args.input) 148 outname = base + "." + args.format 149 if args.format == "html": 150 fig.write_html(outname) 151 else: 152 plotly.io.write_image(fig, outname, format="png") 153 print(f"{outname} is written") 154 return outname 155 156 157def main(): 158 parser = argparse.ArgumentParser(description="Plot JSON generated by memstats_chart") 159 parser.add_argument("-i", "--input", required=True, help="input JSON file path") 160 parser.add_argument("--format", choices=["html", "png"], default="html") 161 parser.add_argument("--title", default="crosvm memory usage") 162 args = parser.parse_args() 163 164 with open(args.input) as f: 165 data = json.load(f) 166 167 outfile = memstat_plot(data, args) 168 169 try: 170 subprocess.run(["google-chrome", outfile]) 171 except Exception as e: 172 print(f"Failed to open {outfile} with google-chrome: {e}") 173 174 175main() 176