xref: /aosp_15_r20/external/crosvm/tools/contrib/memstats_chart/plot.py (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
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