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