github.com/maxgio92/test-infra@v0.1.0/hack/analyze-memory-profiles.py (about)

     1  #!/usr/bin/env python3
     2  
     3  # Copyright 2021 The Kubernetes Authors.
     4  #
     5  # Licensed under the Apache License, Version 2.0 (the "License");
     6  # you may not use this file except in compliance with the License.
     7  # You may obtain a copy of the License at
     8  #
     9  #     http://www.apache.org/licenses/LICENSE-2.0
    10  #
    11  # Unless required by applicable law or agreed to in writing, software
    12  # distributed under the License is distributed on an "AS IS" BASIS,
    13  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  # See the License for the specific language governing permissions and
    15  # limitations under the License.
    16  
    17  # This script is meant to be used to analyze memory profiles created by the Prow binaries when
    18  # the --profile-memory-usage flag is passed. The interval of profiling can be set with the
    19  # --memory-profile-interval flag. This tool can also be used on the output of the sidecar utility
    20  # when the sidecar.Options.WriteMemoryProfile option has been set. The tools will write sequential
    21  # profiles into a directory, from which this script can load the data, create time series and
    22  # visualize them.
    23  
    24  import os
    25  import pathlib
    26  import subprocess
    27  import sys
    28  from datetime import datetime
    29  
    30  import matplotlib.dates as mdates
    31  import matplotlib.pyplot as plt
    32  import matplotlib.ticker as ticker
    33  from matplotlib.font_manager import FontProperties
    34  
    35  if len(sys.argv) != 2:
    36      print("[ERROR] Expected the directory containing profiles as the only argument.")
    37      print("Usage: {} ./path/to/profiles/".format(sys.argv[0]))
    38      sys.exit(1)
    39  
    40  profile_dir = sys.argv[1]
    41  
    42  
    43  def parse_bytes(value):
    44      # we will either see a raw number or one with a suffix
    45      value = value.decode("utf-8")
    46      if not value.endswith("B"):
    47          return float(value)
    48  
    49      suffix = value[-2:]
    50      multiple = 1
    51      if suffix == "KB":
    52          multiple = 1024
    53      elif suffix == "MB":
    54          multiple = 1024 * 1024
    55      elif suffix == "GB":
    56          multiple = 1024 * 1024 * 1024
    57  
    58      return float(value[:-2]) * multiple
    59  
    60  
    61  overall_name = "overall".encode("utf-8")
    62  dates_by_name = {overall_name: []}
    63  flat_usage_over_time = {overall_name: []}
    64  cumulative_usage_over_time = {overall_name: []}
    65  max_usage = 0
    66  
    67  for subdir, dirs, files in os.walk(profile_dir):
    68      for file in files:
    69          full_path = os.path.join(subdir, file)
    70          date = datetime.fromtimestamp(pathlib.Path(full_path).stat().st_mtime)
    71          output = subprocess.run(
    72              ["go", "tool", "pprof", "-top", "-inuse_space", full_path],
    73              check=True, stdout=subprocess.PIPE
    74          )
    75          # The output of go tool pprof will look like:
    76          #
    77          # File: sidecar
    78          # Type: inuse_space
    79          # Time: Mar 19, 2021 at 10:30am (PDT)
    80          # Showing nodes accounting for 66.05MB, 100% of 66.05MB total
    81          #       flat  flat%   sum%        cum   cum%
    82          #       64MB 96.90% 96.90%       64MB 96.90%  google.golang.org/api/internal/gensupport...
    83          #
    84          # We want to parse all of the lines after the header and metadata.
    85          lines = output.stdout.splitlines()
    86          usage = parse_bytes(lines[3].split()[-2])
    87          if usage > max_usage:
    88              max_usage = usage
    89          data_index = 0
    90          for i in range(len(lines)):
    91              if lines[i].split()[0].decode("utf-8") == "flat":
    92                  data_index = i + 1
    93                  break
    94          flat_overall = 0
    95          cumulative_overall = 0
    96          for line in lines[data_index:]:
    97              parts = line.split()
    98              name = parts[5]
    99              if name not in dates_by_name:
   100                  dates_by_name[name] = []
   101              dates_by_name[name].append(date)
   102              if name not in flat_usage_over_time:
   103                  flat_usage_over_time[name] = []
   104              flat_usage = parse_bytes(parts[0])
   105              flat_usage_over_time[name].append(flat_usage)
   106              flat_overall += flat_usage
   107              if name not in cumulative_usage_over_time:
   108                  cumulative_usage_over_time[name] = []
   109              cumulative_usage = parse_bytes(parts[3])
   110              cumulative_usage_over_time[name].append(cumulative_usage)
   111              cumulative_overall += cumulative_usage
   112          dates_by_name[overall_name].append(date)
   113          flat_usage_over_time[overall_name].append(flat_overall)
   114          cumulative_usage_over_time[overall_name].append(cumulative_overall)
   115  
   116  plt.rcParams.update({'font.size': 22})
   117  fig = plt.figure(figsize=(30, 18))
   118  plt.subplots_adjust(right=0.7)
   119  ax = plt.subplot(211)
   120  for name in dates_by_name:
   121      dates = mdates.date2num(dates_by_name[name])
   122      values = flat_usage_over_time[name]
   123      # we only want to show the top couple callsites, or our legend gets noisy
   124      if max(values) > 0.01 * max_usage:
   125          ax.plot_date(dates, values,
   126                       label="{} (max: {:,.0f}MB)".format(name.decode("utf-8"), max(values) / (1024 * 1024)),
   127                       linestyle='solid')
   128      else:
   129          ax.plot_date(dates, values, linestyle='solid')
   130  ax.set_yscale('log')
   131  ax.set_ylim(bottom=10*1024*1024)
   132  formatter = ticker.FuncFormatter(lambda y, pos: '{:,.0f}'.format(y / (1024 * 1024)) + 'MB')
   133  ax.yaxis.set_major_formatter(formatter)
   134  plt.xlabel("Time")
   135  plt.ylabel("Flat Space In Use (bytes)")
   136  plt.title("Space In Use By Callsite")
   137  fontP = FontProperties()
   138  fontP.set_size('xx-small')
   139  plt.legend(bbox_to_anchor=(1, 1), loc='upper left', prop=fontP)
   140  
   141  ax = plt.subplot(212)
   142  for name in dates_by_name:
   143      dates = mdates.date2num(dates_by_name[name])
   144      values = cumulative_usage_over_time[name]
   145      # we only want to show the top couple callsites, or our legend gets noisy
   146      if max(values) > 0.01 * max_usage:
   147          ax.plot_date(dates, values,
   148                       label="{} (max: {:,.0f}MB)".format(name.decode("utf-8"), max(values) / (1024 * 1024)),
   149                       linestyle='solid')
   150      else:
   151          ax.plot_date(dates, values, linestyle='solid')
   152  ax.set_yscale('log')
   153  ax.set_ylim(bottom=10*1024*1024)
   154  ax.yaxis.set_major_formatter(formatter)
   155  plt.xlabel("Time")
   156  plt.ylabel("Cumulative Space In Use (bytes)")
   157  fontP = FontProperties()
   158  fontP.set_size('xx-small')
   159  plt.legend(bbox_to_anchor=(1, 1), loc='upper left', prop=fontP)
   160  
   161  plt.show()