ATLAS Offline Software
dlldep.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 # Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
4 #
5 # @file: dlldep.py
6 #
7 # @purpose: Create a graph of the runtime dependencies of a dynamic library using ldd.
8 # The dependencies are printed in DOT (graphviz) language.
9 #
10 # @author: Frank Winklmeier (CERN)
11 # based on Dominik Seichter's 'dependencies.sh':
12 # http://domseichter.blogspot.com/2008/02/visualize-dependencies-of-binaries-and.html
13 #
14 
15 from __future__ import print_function
16 
17 import sys
18 from os.path import basename
19 import subprocess as sp
20 import re
21 
22 import PyUtils.Dso as Dso
23 
24 class Cache:
25  """Global cache of already processed files"""
26 
27  files = {} # Global cache of already processed libs
28  stats = [] # Global statistics
29 
30  def __init__(self):
31  self.myfiles = {} # Dependencies of the currently processed lib
32  self.dotsrc = [] # DOT source code
33 
34  def add(self, shlib):
35  """Add file to local and global cache"""
36  self.myfiles[shlib.lib] = shlib
37  Cache.files[shlib.lib] = shlib
38 
39  def writeDOT(self, file):
40  for d in self.dotsrc: print (file, d, file=file)
41 
42  def dot(self, code, style={}):
43  """Output a line of dot code"""
44  if len(style)>0:
45  code += ' ['
46  for k,v in style.items():
47  code += '%s="%s" ' % (k,v)
48  code += ']'
49 
50  self.dotsrc.append(code)
51  return
52 
53 
54 class SharedLib:
55  """Represent a shared library with name, dependencies and other stats"""
56 
57  def __init__(self, distance, lib):
58  import os.path as osp
59  lib = osp.expanduser(osp.expandvars(lib))
60  if not osp.exists(lib):
61  l = Dso.find_library(lib)
62  if l:
63  lib = l
64  assert osp.exists(lib), "no such path [%s]" % (lib,)
65  self.lib = lib # path of library
66  self.distance = distance # distance from root lib
67  self.deplibs = self._getLibs(lib) # direct dependencies
68 
69  def _getLibs(self, lib):
70  """Get direct dependencies of shared library"""
71 
72  # First check if already in global cache
73  cachedlib = Cache.files.get(lib)
74  if cachedlib: return cachedlib.deplibs
75 
76  # Run readelf to find direct dependencies
77  # Note: ldd itself does recursions so we cannot use it here
78  encargs = {'encoding' : 'utf-8'}
79  p = sp.Popen(["readelf","-d",lib], stdout=sp.PIPE, **encargs)
80  output = p.communicate()[0]
81  if p.returncode != 0:
82  print ("Cannot run 'readelf' on",lib)
83  return []
84 
85  libs = []
86  for l in output.split("\n"):
87  if l.find("NEEDED")==-1: continue
88  libs += [l.split()[-1].strip("[]")]
89 
90  # Run ldd to find full path of libraries
91  p = sp.Popen(["ldd",lib], stdout=sp.PIPE, **encargs)
92  output = p.communicate()[0]
93  if p.returncode != 0:
94  print ("Cannot run 'ldd' on",lib)
95  return []
96 
97  libpaths = []
98  for l in output.split("\n"):
99  fields = l.strip().split()
100  if len(fields)!=4: continue
101  path = fields[2]
102  if (fields[0] in libs) and len(path)>0:
103  libpaths += [path]
104 
105  return libpaths
106 
107 
108 class Color:
109  """Helper class for colored nodes"""
110 
111  default = "white"
112  scheme = "rdbu8"
113  projects = {"DetCommon" : 2,
114  "AtlasCore" : 1,
115  "AtlasEvent" : 3,
116  "AtlasConditions" : 4,
117  "AtlasReconstruction" : 5,
118  "AtlasSimulation" : 6,
119  "AtlasTrigger" : 7,
120  "AtlasAnalysis" : 8
121  }
122 
123  @classmethod
124  def get(cls, lib):
125  for p,c in cls.projects.items():
126  if lib.find(p)!=-1: return "/%s/%s" % (cls.scheme, c)
127  return cls.default
128 
129 
130 class Stats:
131  """Statistics calculated from Cache object"""
132 
133  def __init__(self, lib, cache):
134  self.lib = lib
135  self.depTotal = len(cache.myfiles)-1 # do not count ourselves
136  self.depDirect = 0
137 
138  for lib in cache.myfiles.values():
139  if lib.distance==1: self.depDirect += 1
140  return
141 
142 
143 def anaLib(lib, opt, cache, select = [], ignore = [], depth = 0):
144  """Get dependencies of shared library recursively"""
145 
146  def process(path):
147  """Should this lib be processed?"""
148  for regexp in select:
149  if regexp.match(path): return True
150  if len(select)>0: return False
151 
152  for regexp in ignore:
153  if regexp.match(path): return False
154  return True
155 
156  if opt.maxdepth and depth>=opt.maxdepth: return
157 
158  # Check if we analyzed this lib already
159  cachedlib = cache.myfiles.get(lib)
160  if cachedlib:
161  # Always save minimum distance
162  if depth<cachedlib.distance: cachedlib.distance=depth
163  return
164 
165  shlib = SharedLib(depth, lib)
166  cache.add(shlib)
167 
168  for l in shlib.deplibs:
169  if process(l):
170  cache.dot(' "%s" -> "%s"' % (basename(lib), basename(l)))
171  anaLib(l, opt, cache, select, ignore, depth+1)
172 
173  return
174 
175 
176 def processLib(lib, opt, dotFileName = None):
177  """Process one library"""
178 
179  cache = Cache()
180  dot = cache.dot # shortcut
181 
182  dot('digraph DependencyTree {')
183  dot(' ratio=0.9 nodesep=0.05') # some reasonable default values
184  dot(' "%s" [shape=box]' % basename(lib))
185 
186  select = []
187  ignore = [] # currently not used
188  if opt.filter:
189  for f in opt.filter: select += [re.compile(f)]
190  else:
191  select = [re.compile(".*atlas/software.*")]
192 
193  anaLib(lib, opt, cache, select, ignore)
194 
195  # Declare style of all nodes
196  for l,v in cache.myfiles.items():
197  style = {}
198  # Special style for direct dependencies
199  if v.distance==1:
200  style["shape"] = "box"
201 
202  if not opt.nocolor:
203  style["style"] = "filled"
204  style["fillcolor"] = Color.get(l)
205 
206  dot(' "%s"' % (basename(l)), style)
207 
208  dot('}')
209 
210  # Write output to file
211  if dotFileName: outFile = open(dotFileName, "w")
212  else: outFile = open(basename(lib)+".dot", "w")
213 
214  cache.writeDOT(outFile)
215 
216  # Calculate statistics
217  if opt.stats:
218  st = Stats(lib, cache)
219  Cache.stats += [st]
220  return st
221 
222  return None
223 
224 
225 
227  """Print statistics"""
228  import operator
229 
230  print ("%-50s %7s %7s" % ("Library dependencies","Direct","Total"))
231  print ("-"*70)
232  for s in sorted(Cache.stats, key=operator.attrgetter("depDirect"), reverse=True):
233  print ("%-50s %7d %7d" % (basename(s.lib), s.depDirect, s.depTotal))
234 
235  return
236 
237 
238 def main():
239 
240  import optparse
241  parser = optparse.OptionParser(description="Create runtime dependecy graph for shared library. The output is a graph in DOT language. To visualize it use, e.g. 'dot -O -Tps mygraph.dot'. The rectangular nodes represent direct dependencies. Nodes belonging to the same project have the same color.",
242  usage="%prog [OPTIONS] LIB [LIB...]")
243 
244  parser.add_option("-o", "--output",
245  help="File for DOT source code (default is LIB.dot)")
246 
247  parser.add_option("-d", "--maxdepth", type="int",
248  help="Maximum depth of dependency tree [1..]")
249 
250  parser.add_option("-f", "--filter", action="append",
251  help="Only analyze libraries matching regular expression (can be specified multiple times) [default: .*atlas/software.*]")
252 
253  parser.add_option("--nocolor", action="store_true",
254  help="Do not use colors")
255 
256  parser.add_option("-s", "--stats", action="store_true",
257  help="Print statistics")
258 
259  (opt, args) = parser.parse_args()
260  if len(args)==0:
261  parser.error("Invalid number of arguments specified")
262 
263 
264  if len(args)>1 and opt.output:
265  print ("Multiple libraries specified. Ignoring output file name.")
266  opt.output = None
267 
268  for lib in args:
269  processLib(lib, opt, opt.output)
270 
271  if opt.stats:
272  printStats()
273 
274  return 0
275 
276 
277 if __name__ == "__main__":
278  try:
279  sys.exit(main())
280  except KeyboardInterrupt:
281  sys.exit(1)
282 
dlldep.SharedLib.__init__
def __init__(self, distance, lib)
Definition: dlldep.py:57
dlldep.Cache
Definition: dlldep.py:24
dlldep.Stats.lib
lib
Definition: dlldep.py:134
dlldep.Cache.__init__
def __init__(self)
Definition: dlldep.py:30
dlldep.Cache.writeDOT
def writeDOT(self, file)
Definition: dlldep.py:39
dlldep.Stats.__init__
def __init__(self, lib, cache)
Definition: dlldep.py:133
dlldep.Color
Definition: dlldep.py:108
dlldep.SharedLib._getLibs
def _getLibs(self, lib)
Definition: dlldep.py:69
dlldep.processLib
def processLib(lib, opt, dotFileName=None)
Definition: dlldep.py:176
dlldep.anaLib
def anaLib(lib, opt, cache, select=[], ignore=[], depth=0)
Definition: dlldep.py:143
dumpHVPathFromNtuple.append
bool append
Definition: dumpHVPathFromNtuple.py:91
dlldep.Cache.dotsrc
dotsrc
Definition: dlldep.py:32
SUSY_SimplifiedModel_PostInclude.process
string process
Definition: SUSY_SimplifiedModel_PostInclude.py:42
dlldep.Color.default
default
Definition: dlldep.py:111
DerivationFramework::TriggerMatchingUtils::sorted
std::vector< typename T::value_type > sorted(T begin, T end)
Helper function to create a sorted vector from an unsorted one.
dlldep.Color.get
def get(cls, lib)
Definition: dlldep.py:124
dlldep.SharedLib
Definition: dlldep.py:54
TrigJetMonitorAlgorithm.items
items
Definition: TrigJetMonitorAlgorithm.py:79
dlldep.SharedLib.deplibs
deplibs
Definition: dlldep.py:67
dlldep.printStats
def printStats()
Definition: dlldep.py:226
dlldep.Cache.dot
def dot(self, code, style={})
Definition: dlldep.py:42
Trk::open
@ open
Definition: BinningType.h:40
dlldep.Cache.myfiles
myfiles
Definition: dlldep.py:31
dlldep.Stats
Definition: dlldep.py:130
dlldep.Cache.add
def add(self, shlib)
Definition: dlldep.py:34
dlldep.SharedLib.distance
distance
Definition: dlldep.py:66
dlldep.Stats.depTotal
depTotal
Definition: dlldep.py:135
dlldep.main
def main()
Definition: dlldep.py:238
dlldep.Stats.depDirect
depDirect
Definition: dlldep.py:136
dot
Definition: dot.py:1
dlldep.Color.projects
projects
Definition: dlldep.py:113
dlldep.SharedLib.lib
lib
Definition: dlldep.py:65
dlldep.Color.scheme
scheme
Definition: dlldep.py:112
Trk::split
@ split
Definition: LayerMaterialProperties.h:38
beamspotman.basename
basename
Definition: beamspotman.py:640