Index: tools/deep_memory_profiler/lib/policy.py |
diff --git a/tools/deep_memory_profiler/lib/policy.py b/tools/deep_memory_profiler/lib/policy.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..d7a38977eb702ccb0058f6a5850e01a96a5b7ceb |
--- /dev/null |
+++ b/tools/deep_memory_profiler/lib/policy.py |
@@ -0,0 +1,404 @@ |
+# Copyright 2013 The Chromium 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 json |
+import logging |
+import os |
+import re |
+ |
+ |
+LOGGER = logging.getLogger('dmprof') |
+ |
+BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
+POLICIES_JSON_PATH = os.path.join(BASE_PATH, 'policies.json') |
+ |
+# Heap Profile Policy versions |
+ |
+# POLICY_DEEP_1 DOES NOT include allocation_type columns. |
+# mmap regions are distincted w/ mmap frames in the pattern column. |
+POLICY_DEEP_1 = 'POLICY_DEEP_1' |
+ |
+# POLICY_DEEP_2 DOES include allocation_type columns. |
+# mmap regions are distincted w/ the allocation_type column. |
+POLICY_DEEP_2 = 'POLICY_DEEP_2' |
+ |
+# POLICY_DEEP_3 is in JSON format. |
+POLICY_DEEP_3 = 'POLICY_DEEP_3' |
+ |
+# POLICY_DEEP_3 contains typeinfo. |
+POLICY_DEEP_4 = 'POLICY_DEEP_4' |
+ |
+ |
+class Rule(object): |
+ """Represents one matching rule in a policy file.""" |
+ |
+ def __init__(self, |
+ name, |
+ allocator_type, |
+ stackfunction_pattern=None, |
+ stacksourcefile_pattern=None, |
+ typeinfo_pattern=None, |
+ mappedpathname_pattern=None, |
+ mappedpermission_pattern=None, |
+ sharedwith=None): |
+ self._name = name |
+ self._allocator_type = allocator_type |
+ |
+ self._stackfunction_pattern = None |
+ if stackfunction_pattern: |
+ self._stackfunction_pattern = re.compile( |
+ stackfunction_pattern + r'\Z') |
+ |
+ self._stacksourcefile_pattern = None |
+ if stacksourcefile_pattern: |
+ self._stacksourcefile_pattern = re.compile( |
+ stacksourcefile_pattern + r'\Z') |
+ |
+ self._typeinfo_pattern = None |
+ if typeinfo_pattern: |
+ self._typeinfo_pattern = re.compile(typeinfo_pattern + r'\Z') |
+ |
+ self._mappedpathname_pattern = None |
+ if mappedpathname_pattern: |
+ self._mappedpathname_pattern = re.compile(mappedpathname_pattern + r'\Z') |
+ |
+ self._mappedpermission_pattern = None |
+ if mappedpermission_pattern: |
+ self._mappedpermission_pattern = re.compile( |
+ mappedpermission_pattern + r'\Z') |
+ |
+ self._sharedwith = [] |
+ if sharedwith: |
+ self._sharedwith = sharedwith |
+ |
+ @property |
+ def name(self): |
+ return self._name |
+ |
+ @property |
+ def allocator_type(self): |
+ return self._allocator_type |
+ |
+ @property |
+ def stackfunction_pattern(self): |
+ return self._stackfunction_pattern |
+ |
+ @property |
+ def stacksourcefile_pattern(self): |
+ return self._stacksourcefile_pattern |
+ |
+ @property |
+ def typeinfo_pattern(self): |
+ return self._typeinfo_pattern |
+ |
+ @property |
+ def mappedpathname_pattern(self): |
+ return self._mappedpathname_pattern |
+ |
+ @property |
+ def mappedpermission_pattern(self): |
+ return self._mappedpermission_pattern |
+ |
+ @property |
+ def sharedwith(self): |
+ return self._sharedwith |
+ |
+ |
+class Policy(object): |
+ """Represents a policy, a content of a policy file.""" |
+ |
+ def __init__(self, rules, version, components): |
+ self._rules = rules |
+ self._version = version |
+ self._components = components |
+ |
+ @property |
+ def rules(self): |
+ return self._rules |
+ |
+ @property |
+ def version(self): |
+ return self._version |
+ |
+ @property |
+ def components(self): |
+ return self._components |
+ |
+ def find_rule(self, component_name): |
+ """Finds a rule whose name is |component_name|. """ |
+ for rule in self._rules: |
+ if rule.name == component_name: |
+ return rule |
+ return None |
+ |
+ def find_malloc(self, bucket): |
+ """Finds a matching component name which a given |bucket| belongs to. |
+ |
+ Args: |
+ bucket: A Bucket object to be searched for. |
+ |
+ Returns: |
+ A string representing a component name. |
+ """ |
+ assert not bucket or bucket.allocator_type == 'malloc' |
+ |
+ if not bucket: |
+ return 'no-bucket' |
+ if bucket.component_cache: |
+ return bucket.component_cache |
+ |
+ stackfunction = bucket.symbolized_joined_stackfunction |
+ stacksourcefile = bucket.symbolized_joined_stacksourcefile |
+ typeinfo = bucket.symbolized_typeinfo |
+ if typeinfo.startswith('0x'): |
+ typeinfo = bucket.typeinfo_name |
+ |
+ for rule in self._rules: |
+ if (rule.allocator_type == 'malloc' and |
+ (not rule.stackfunction_pattern or |
+ rule.stackfunction_pattern.match(stackfunction)) and |
+ (not rule.stacksourcefile_pattern or |
+ rule.stacksourcefile_pattern.match(stacksourcefile)) and |
+ (not rule.typeinfo_pattern or rule.typeinfo_pattern.match(typeinfo))): |
+ bucket.component_cache = rule.name |
+ return rule.name |
+ |
+ assert False |
+ |
+ def find_mmap(self, region, bucket_set, |
+ pageframe=None, group_pfn_counts=None): |
+ """Finds a matching component which a given mmap |region| belongs to. |
+ |
+ It uses |bucket_set| to match with backtraces. If |pageframe| is given, |
+ it considers memory sharing among processes. |
+ |
+ NOTE: Don't use Bucket's |component_cache| for mmap regions because they're |
+ classified not only with bucket information (mappedpathname for example). |
+ |
+ Args: |
+ region: A tuple representing a memory region. |
+ bucket_set: A BucketSet object to look up backtraces. |
+ pageframe: A PageFrame object representing a pageframe maybe including |
+ a pagecount. |
+ group_pfn_counts: A dict mapping a PFN to the number of times the |
+ the pageframe is mapped by the known "group (Chrome)" processes. |
+ |
+ Returns: |
+ A string representing a component name. |
+ """ |
+ assert region[0] == 'hooked' |
+ bucket = bucket_set.get(region[1]['bucket_id']) |
+ assert not bucket or bucket.allocator_type == 'mmap' |
+ |
+ if not bucket: |
+ return 'no-bucket', None |
+ |
+ stackfunction = bucket.symbolized_joined_stackfunction |
+ stacksourcefile = bucket.symbolized_joined_stacksourcefile |
+ sharedwith = self._categorize_pageframe(pageframe, group_pfn_counts) |
+ |
+ for rule in self._rules: |
+ if (rule.allocator_type == 'mmap' and |
+ (not rule.stackfunction_pattern or |
+ rule.stackfunction_pattern.match(stackfunction)) and |
+ (not rule.stacksourcefile_pattern or |
+ rule.stacksourcefile_pattern.match(stacksourcefile)) and |
+ (not rule.mappedpathname_pattern or |
+ rule.mappedpathname_pattern.match(region[1]['vma']['name'])) and |
+ (not rule.mappedpermission_pattern or |
+ rule.mappedpermission_pattern.match( |
+ region[1]['vma']['readable'] + |
+ region[1]['vma']['writable'] + |
+ region[1]['vma']['executable'] + |
+ region[1]['vma']['private'])) and |
+ (not rule.sharedwith or |
+ not pageframe or sharedwith in rule.sharedwith)): |
+ return rule.name, bucket |
+ |
+ assert False |
+ |
+ def find_unhooked(self, region, pageframe=None, group_pfn_counts=None): |
+ """Finds a matching component which a given unhooked |region| belongs to. |
+ |
+ If |pageframe| is given, it considers memory sharing among processes. |
+ |
+ Args: |
+ region: A tuple representing a memory region. |
+ pageframe: A PageFrame object representing a pageframe maybe including |
+ a pagecount. |
+ group_pfn_counts: A dict mapping a PFN to the number of times the |
+ the pageframe is mapped by the known "group (Chrome)" processes. |
+ |
+ Returns: |
+ A string representing a component name. |
+ """ |
+ assert region[0] == 'unhooked' |
+ sharedwith = self._categorize_pageframe(pageframe, group_pfn_counts) |
+ |
+ for rule in self._rules: |
+ if (rule.allocator_type == 'unhooked' and |
+ (not rule.mappedpathname_pattern or |
+ rule.mappedpathname_pattern.match(region[1]['vma']['name'])) and |
+ (not rule.mappedpermission_pattern or |
+ rule.mappedpermission_pattern.match( |
+ region[1]['vma']['readable'] + |
+ region[1]['vma']['writable'] + |
+ region[1]['vma']['executable'] + |
+ region[1]['vma']['private'])) and |
+ (not rule.sharedwith or |
+ not pageframe or sharedwith in rule.sharedwith)): |
+ return rule.name |
+ |
+ assert False |
+ |
+ @staticmethod |
+ def load(filename, filetype): |
+ """Loads a policy file of |filename| in a |format|. |
+ |
+ Args: |
+ filename: A filename to be loaded. |
+ filetype: A string to specify a type of the file. Only 'json' is |
+ supported for now. |
+ |
+ Returns: |
+ A loaded Policy object. |
+ """ |
+ with open(os.path.join(BASE_PATH, filename)) as policy_f: |
+ return Policy.parse(policy_f, filetype) |
+ |
+ @staticmethod |
+ def parse(policy_f, filetype): |
+ """Parses a policy file content in a |format|. |
+ |
+ Args: |
+ policy_f: An IO object to be loaded. |
+ filetype: A string to specify a type of the file. Only 'json' is |
+ supported for now. |
+ |
+ Returns: |
+ A loaded Policy object. |
+ """ |
+ if filetype == 'json': |
+ return Policy._parse_json(policy_f) |
+ else: |
+ return None |
+ |
+ @staticmethod |
+ def _parse_json(policy_f): |
+ """Parses policy file in json format. |
+ |
+ A policy file contains component's names and their stacktrace pattern |
+ written in regular expression. Those patterns are matched against each |
+ symbols of each stacktraces in the order written in the policy file |
+ |
+ Args: |
+ policy_f: A File/IO object to read. |
+ |
+ Returns: |
+ A loaded policy object. |
+ """ |
+ policy = json.load(policy_f) |
+ |
+ rules = [] |
+ for rule in policy['rules']: |
+ stackfunction = rule.get('stackfunction') or rule.get('stacktrace') |
+ stacksourcefile = rule.get('stacksourcefile') |
+ rules.append(Rule( |
+ rule['name'], |
+ rule['allocator'], # allocator_type |
+ stackfunction, |
+ stacksourcefile, |
+ rule['typeinfo'] if 'typeinfo' in rule else None, |
+ rule.get('mappedpathname'), |
+ rule.get('mappedpermission'), |
+ rule.get('sharedwith'))) |
+ |
+ return Policy(rules, policy['version'], policy['components']) |
+ |
+ @staticmethod |
+ def _categorize_pageframe(pageframe, group_pfn_counts): |
+ """Categorizes a pageframe based on its sharing status. |
+ |
+ Returns: |
+ 'private' if |pageframe| is not shared with other processes. 'group' |
+ if |pageframe| is shared only with group (Chrome-related) processes. |
+ 'others' if |pageframe| is shared with non-group processes. |
+ """ |
+ if not pageframe: |
+ return 'private' |
+ |
+ if pageframe.pagecount: |
+ if pageframe.pagecount == 1: |
+ return 'private' |
+ elif pageframe.pagecount <= group_pfn_counts.get(pageframe.pfn, 0) + 1: |
+ return 'group' |
+ else: |
+ return 'others' |
+ else: |
+ if pageframe.pfn in group_pfn_counts: |
+ return 'group' |
+ else: |
+ return 'private' |
+ |
+ |
+class PolicySet(object): |
+ """Represents a set of policies.""" |
+ |
+ def __init__(self, policy_directory): |
+ self._policy_directory = policy_directory |
+ |
+ @staticmethod |
+ def load(labels=None): |
+ """Loads a set of policies via the "default policy directory". |
+ |
+ The "default policy directory" contains pairs of policies and their labels. |
+ For example, a policy "policy.l0.json" is labeled "l0" in the default |
+ policy directory "policies.json". |
+ |
+ All policies in the directory are loaded by default. Policies can be |
+ limited by |labels|. |
+ |
+ Args: |
+ labels: An array that contains policy labels to be loaded. |
+ |
+ Returns: |
+ A PolicySet object. |
+ """ |
+ default_policy_directory = PolicySet._load_default_policy_directory() |
+ if labels: |
+ specified_policy_directory = {} |
+ for label in labels: |
+ if label in default_policy_directory: |
+ specified_policy_directory[label] = default_policy_directory[label] |
+ # TODO(dmikurube): Load an un-labeled policy file. |
+ return PolicySet._load_policies(specified_policy_directory) |
+ else: |
+ return PolicySet._load_policies(default_policy_directory) |
+ |
+ def __len__(self): |
+ return len(self._policy_directory) |
+ |
+ def __iter__(self): |
+ for label in self._policy_directory: |
+ yield label |
+ |
+ def __getitem__(self, label): |
+ return self._policy_directory[label] |
+ |
+ @staticmethod |
+ def _load_default_policy_directory(): |
+ with open(POLICIES_JSON_PATH, mode='r') as policies_f: |
+ default_policy_directory = json.load(policies_f) |
+ return default_policy_directory |
+ |
+ @staticmethod |
+ def _load_policies(directory): |
+ LOGGER.info('Loading policy files.') |
+ policies = {} |
+ for label in directory: |
+ LOGGER.info(' %s: %s' % (label, directory[label]['file'])) |
+ loaded = Policy.load(directory[label]['file'], directory[label]['format']) |
+ if loaded: |
+ policies[label] = loaded |
+ return PolicySet(policies) |