blob: 7ab108c5a7997250e277efe4e1ead3ad851bca0f [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2020 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import concurrent.futures
import csv
import functools
import json
import logging
import os
import pathlib
import re
import shutil
import subprocess
import sys
from urllib import parse
class StepInfo:
ABOUT_COLUMNS = (
"step_name",
"mem_bytes_inc",
"mem_bytes_net",
"top_mem_type",
"bottom_mem_type",
"num_objects_inc",
"num_objects_net",
)
# 3 columns separated by " | ".
_TABLE_ROW_PAT = re.compile(r"([^|]+) \| ([^|]+) \| ([^|]+)$")
_UNIT_TO_MULTIPLE = {"B": 1, "KB": 1 << 10, "MB": 1 << 20, "GB": 1 << 30}
def __init__(self, index: int, src_path: str):
self.src_path = src_path.lstrip("/")
self.step_name = os.path.dirname(src_path.split("/+/u/")[1])
self.debug_log = None
def download_log(self):
self.debug_log = subprocess.check_output(
("logdog", "-host", "logs.chromium.org", "cat", self.src_path),
encoding="utf-8",
)
def about(self):
mem_inc, mem_net = float(0), float(0)
top_mem_type, bottom_mem_type = ("", 0), ("", 0)
obj_inc, obj_net = 0, 0
for line in self.debug_log.split("\n"):
# Optimization to exit early.
# This is printed at the end of the memory snapshot.
if line.endswith("Ends --------"):
break
match = self._TABLE_ROW_PAT.match(line)
if not match:
continue
type_name, num_objs, mem = line.split(" | ")
try:
num_objs = int(num_objs)
except ValueError:
continue
if num_objs > 0:
obj_inc += num_objs
obj_net += num_objs
mem_magnitude, mem_unit = mem.split()
num_bytes = float(mem_magnitude) * self._UNIT_TO_MULTIPLE[mem_unit]
mem_net += num_bytes
if num_bytes > 0:
mem_inc += num_bytes
type_name = type_name.strip()
if num_bytes > top_mem_type[1]:
top_mem_type = type_name, num_bytes
if num_bytes < bottom_mem_type[1]:
bottom_mem_type = type_name, num_bytes
return (
self.step_name,
str(mem_inc),
str(mem_net),
top_mem_type[0],
bottom_mem_type[0],
str(obj_inc),
str(obj_net),
)
parser = argparse.ArgumentParser()
parser.add_argument("build_url", help="Milo URL for the build of interest")
parser.add_argument("-o", "--output", help="path to which the output will be written")
def main():
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
args = parser.parse_args()
build_url = args.build_url
if not shutil.which("logdog"):
sys.exit("logdog not found in $PATH")
if not shutil.which("bb"):
sys.exit("bb not found in $PATH")
build_url_parsed = parse.urlparse(build_url)
build_id = pathlib.PurePosixPath(build_url_parsed.path).name.lstrip("b")
logging.info("Getting build %s", build_id)
build_dict = json.loads(
subprocess.check_output(
("bb", "get", "-json", "-steps", "-p", build_id), encoding="utf-8"
)
)
if (
not build_dict["input"]["properties"]
.get("$recipe_engine", {})
.get("memory_profiler", {})
.get("enable_snapshot")
):
sys.exit(
"Build %s does not have memory profiling enabled. See "
"https://chromium.googlesource.com/infra/luci/recipes-py/+/HEAD/doc/"
"user_guide.md#memory-leak" % build_url
)
step_infos = []
for step_d in build_dict["steps"]:
for log_d in step_d.get("logs", ()):
if log_d["name"] == "$debug":
step_infos.append(
StepInfo(len(step_infos), parse.urlparse(log_d["url"]).path)
)
break
logging.info("Downloading debug logs")
executor = concurrent.futures.ThreadPoolExecutor()
completed_procs = []
for step_info in step_infos:
completed_procs.append(executor.submit(step_info.download_log))
executor.shutdown(wait=True)
for cp in completed_procs:
cp.result() # Raise any errors.
logging.info("Writing summary to %s", args.output)
with open(args.output, "w", encoding="utf-8") as summary_f:
writer = csv.writer(summary_f)
writer.writerow(StepInfo.ABOUT_COLUMNS)
for step_info in step_infos:
writer.writerow(step_info.about())
if __name__ == "__main__":
main()