Mussa | 422993a | 2014-09-29 15:03:54 -0700 | [diff] [blame^] | 1 | # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | import re |
| 6 | from collections import namedtuple |
| 7 | |
| 8 | from autotest_lib.client.bin import utils |
| 9 | from autotest_lib.client.common_lib import error |
| 10 | |
| 11 | |
| 12 | BucketStats = namedtuple('BucketStats', 'value percent') |
| 13 | |
| 14 | |
| 15 | class HistogramParser(object): |
| 16 | """ |
| 17 | Parses a chrome histogram page and provide access to its values. |
| 18 | |
| 19 | Example usage: |
| 20 | parser = histogram_parser.HistogramParser('some_histogram_name') |
| 21 | |
| 22 | # Later access amazing magical values: |
| 23 | buckets = parser.buckets |
| 24 | |
| 25 | if buckets and buckets[1] == ??: |
| 26 | # do cool stuff |
| 27 | |
| 28 | """ |
| 29 | |
| 30 | def __init__(self, chrome, histogram, time_out_s=10): |
| 31 | """ |
| 32 | @param chrome: Chrome instance representing the browser in current test. |
| 33 | @param histogram: string, name of the histogram of interest. |
| 34 | @param time_out_s: int, max duration in secs to wait for specified |
| 35 | histogram to be loaded. |
| 36 | |
| 37 | """ |
| 38 | # This pattern was built by observing the chrome://histogram output |
| 39 | self._histogram_pattern = ('Histogram.*([0-9]+)' |
| 40 | 'samples.*average.*([0-9]+\.[0-9]+)') |
| 41 | |
| 42 | self._bucket_pattern = '(^[0-9]+).*\(([0-9]+)' |
| 43 | |
| 44 | """ |
| 45 | Match counts are based on the text that needs to be parsed. |
| 46 | E.g: "0 ---------------------------O (9 = 16.4%)" is a typical entry |
| 47 | in the list of buckets. In this case we want to match 0 and 9, |
| 48 | therefore the match count is 2. |
| 49 | |
| 50 | """ |
| 51 | |
| 52 | self._histogram_match_count = 2 |
| 53 | self._bucket_match_count = 2 |
| 54 | |
| 55 | self._histogram = histogram |
| 56 | self._time_out_s = time_out_s |
| 57 | self._raw_text = None |
| 58 | self._sample_count = None |
| 59 | self._average = None |
| 60 | self._buckets = {} |
| 61 | self.tab = chrome.browser.tabs.New() |
| 62 | self.wait_for_histogram_loaded() |
| 63 | self.parse() |
| 64 | |
| 65 | |
| 66 | @property |
| 67 | def buckets(self): |
| 68 | """ |
| 69 | @returns the dictionary containing buckets and their values. |
| 70 | |
| 71 | """ |
| 72 | return self._buckets |
| 73 | |
| 74 | |
| 75 | @property |
| 76 | def sample_count(self): |
| 77 | """ |
| 78 | @returns the count of all samples in histogram as int. |
| 79 | |
| 80 | """ |
| 81 | return self._sample_count |
| 82 | |
| 83 | |
| 84 | @property |
| 85 | def average(self): |
| 86 | """ |
| 87 | @returns the average of bucket values as float. |
| 88 | |
| 89 | """ |
| 90 | return self._average |
| 91 | |
| 92 | |
| 93 | def wait_for_histogram_loaded(self): |
| 94 | """ |
| 95 | Uses js to poll doc content until valid content is retrieved. |
| 96 | |
| 97 | """ |
| 98 | def loaded(): |
| 99 | """ |
| 100 | Checks if the histogram page has been fully loaded. |
| 101 | |
| 102 | """ |
| 103 | |
| 104 | self.tab.Navigate('chrome://histograms/%s' % self._histogram) |
| 105 | self.tab.WaitForDocumentReadyStateToBeComplete() |
| 106 | docEle = 'document.documentElement' |
| 107 | self._raw_text = self.tab.EvaluateJavaScript( |
| 108 | "{0} && {0}.innerText".format(docEle)) |
| 109 | return self._histogram in self._raw_text |
| 110 | |
| 111 | msg = "%s not loaded. Waited %ss" % (self._histogram, self._time_out_s) |
| 112 | |
| 113 | utils.poll_for_condition(condition=loaded, |
| 114 | exception=error.TestError(msg), |
| 115 | sleep_interval=1) |
| 116 | |
| 117 | def parse(self): |
| 118 | """ |
| 119 | Parses histogram text to retrieve useful properties. |
| 120 | |
| 121 | @raises whatever _check_match() raises. |
| 122 | |
| 123 | """ |
| 124 | |
| 125 | histogram_entries = self._raw_text.split('\n') |
| 126 | found_hist_title = False |
| 127 | |
| 128 | for entry in histogram_entries: |
| 129 | matches = self._check_match(self._histogram_pattern, |
| 130 | entry, |
| 131 | self._histogram_match_count) |
| 132 | |
| 133 | if matches: |
| 134 | if not found_hist_title: |
| 135 | self._sample_count = int(matches[0]) |
| 136 | self._average = matches[1] |
| 137 | found_hist_title = True |
| 138 | |
| 139 | else: # this is another histogram, bail out |
| 140 | return |
| 141 | |
| 142 | else: |
| 143 | matches = self._check_match(self._bucket_pattern, |
| 144 | entry, |
| 145 | self._bucket_match_count) |
| 146 | if matches: |
| 147 | self._buckets[int(matches[0])] = int(matches[1]) |
| 148 | |
| 149 | bucket_sum = sum(self._buckets.values()) |
| 150 | |
| 151 | for key, value in self._buckets.items(): |
| 152 | percent = (float(value) / bucket_sum) * 100 |
| 153 | percent = round(number=percent, ndigits=2) |
| 154 | self._buckets[key] = BucketStats(value, percent) |
| 155 | |
| 156 | |
| 157 | def _check_match(self, pattern, text, expected_match_count): |
| 158 | """ |
| 159 | Checks if provided text contains a pattern and if so expected number of |
| 160 | matches is found. |
| 161 | |
| 162 | @param pattern: string, regex pattern to search for. |
| 163 | @param text: string, text to search for patterns. |
| 164 | @param expected_match_count: int, number of matches expected. |
| 165 | |
| 166 | @returns: tuple, match groups, none if no match was found. |
| 167 | @raises TestError if a match was found but number of matches is not |
| 168 | equal to expected count. |
| 169 | |
| 170 | """ |
| 171 | m = re.match(pattern, text) |
| 172 | |
| 173 | if not m: |
| 174 | return m |
| 175 | |
| 176 | ln = len(m.groups()) |
| 177 | if ln != expected_match_count: |
| 178 | msg = ('Expected %d matches. Got %d. Pattern: %s. Text: %s' |
| 179 | % (expected_match_count, ln, pattern, text)) |
| 180 | raise error.TestError(msg) |
| 181 | |
| 182 | return m.groups() |
| 183 | |
| 184 | |
| 185 | def __str__(self): |
| 186 | return ("Histogram name: %s. Buckets: %s" |
| 187 | % (self._histogram, str(self._buckets))) |