text: #!/usr/bin/env python
  
  from __future__ import print_function, absolute_import
  
  # -*- coding: utf-8 -*-
  __author__ = "perfguru87@gmail.com"
  __copyright__ = "Copyright 2018, The PerfTracker project"
  __license__ = "MIT"
  
  """
  A browser Page helper
  """
  
  import time
  import copy
  import logging
  import sys
  import os
  import pickle
  import base64
  from collections import defaultdict
  
  from .utils import parse_url, get_common_url_prefix
  from ..helpers.texttable import TextTable
  
  
  class PageTimeline:
      types = ['navStrt', 'reqStrt', 'rspStrt', 'rspEnd', 'domEnd', 'onloadEnd', 'ajaxEnd']
      jstypes = {'navStrt': 'navigationStart',
                 'reqStrt': 'requestStart',
                 'rspStrt': 'responseStart',
                 'rspEnd': 'responseEnd',
                 'domEnd': 'domComplete',
                 'onloadEnd': 'loadEventEnd'
                 }
  
      def __init__(self, page, values=None):
          self.page = page
          self.values = {}
          self.deltas = []
          self.ajax_start = 0
          self.total_dur = 0
  
          for t in self.types:
              self.values[t] = values[t] if values and t in values else 0
  
          for n in range(0, len(self.types) - 1):
              self.deltas.append(0)
  
          if page.ts_end and not self.values['ajaxEnd']:
              self.values['ajaxEnd'] = page.ts_end - page.ts_start + self.values['navStrt']
  
          # fixup
  
          if self.values['ajaxEnd'] < self.values['onloadEnd']:
              self.values['ajaxEnd'] = self.values['onloadEnd']
  
          for n in range(0, len(self.types) - 1):
              self.deltas[n] = self.values[self.types[n + 1]] - self.values[self.types[n]]
  
          self.ajax_start = self.values['domEnd'] - self.values['navStrt']
          self.total_dur = self.values['onloadEnd'] - self.values['navStrt']
  
  
  class PageEvent:
      def __init__(self, name, params):
          self.name = name
          if type(params) == dict:
              self.params = params
          else:
              try:
                  self.params = json.loads(args)
              except ValueError:
                  logging.warning("Can't convert event '%s' parameters '%s' to Json" % (name, args))
  
  
  class PageRequestsGroup:
      def __init__(self, request, threshold_ms=10):
          self.threshold_ms = threshold_ms
          self.ts_start = request.ts_start
          self.ts_end = request.ts_start + request.dur if request.ts_start else None
          self.requests = [request]
          self.uncached_requests = [] if request.cached else [request]
  
      def _add(self, request):
          if self.ts_start is None:
              self.ts_start = 0
          if self.ts_end is None:
              self.ts_end = 0
          self.ts_start = min(self.ts_start, request.ts_start) if self.ts_start else None
          self.ts_end = max(self.ts_end, request.ts_start + request.dur) if self.ts_end else None
          self.requests.append(request)
          if not request.cached:
              self.uncached_requests.append(request)
  
      def add_request(self, request):
          if not self.ts_start:
              self._add(request)
              return True
          if self.ts_start <= request.ts_start and request.ts_start <= self.ts_end + self.threshold_ms:
              self._add(request)
              return True
          return False
  
      def get_uncached_reqs(self):
          return self.uncached_requests
  
  
  _page_req_id = 0
  
  
  class PageRequest:
      types = ["Image", "Stylesheet", "Script", "XHR", "Document", "Other"]
      doc_types = ["Document", "Other"]
      types_abbr = {"Image": "IMG", "Stylesheet": "CSS", "Script": "JS", "XHR": "XHR",
                    "Document": "Doc", "Other": "Oth", "Total": "Tot"}
  
      def __init__(self, page, id=None):
          global _page_req_id
          _page_req_id += 1
  
          self.page = page
          self.id = id if id else _page_req_id
          self.method = None
          self.url = None
          self.ts_start = None
          self.ts_end = None
          self.content_length = 0
          self.length = 0
          self.dur = 0
          self.connection_reused = False
          self.status = None
          self.type = 'Other'
          self.keepalive = False
          self.gzipped = False
          self.cached = False
          self.completed = False
  
          self.data = ""
          self.page_actions = None
  
          # properties below needed to re-execute request
          self.params = None
          self.header = {}
          self.validator = None
          self.valid_statuses = None
  
          self.longpoll = False
  
      def __str__(self):
          return str(self.url)
  
      def __unicode__(self):
          return str(self)
  
      def duplicate(self):
          result = copy.deepcopy(self)
  
          global _page_req_id
          _page_req_id += 1
          result.id = _page_req_id
  
          return result
  
      def get_url(self, domain):
          if self.url.startswith(domain):
              return self.url[len(domain):]
          return self.url
  
      def pretty_type(self, type):
          if not type:
              logging.warning("Empty request type for url '%s'" % self.url)
              return "Other"
          tail = self.url.lower()[-6:]
          if tail.endswith('.js'):
              return "Script"
          if tail.endswith('.css'):
              return "Stylesheet"
          if tail.endswith('.png') or tail.endswith('.gif') or tail.endswith('.jpg') or tail.endswith('.ico'):
              return "Image"
          if type in self.types:
              return type
          if type.startswith("text/html") or tail.endswith(".html"):
              return "Document"
          return "Other"
  
      def set_type(self, type):
          self.type = self.pretty_type(type)
  
      def start(self, ts=None):
          self.ts_start = ts if ts else int(time.time() * 1000)
          self.page.browser.log_debug(" req %s started   - %d %s %s %s %s" %
                                      (self.id, self.ts_start, self.method, self.url, self.header, self.params))
  
      def complete(self, ts=None):
          if not ts:
              ts = int(time.time() * 1000)
          self.ts_end = ts
          self.dur = int(round(self.ts_end - self.ts_start))
          self.completed = True
          self.page.browser.log_debug(" req %s completed - %d %s %s %s - %s, %sKA, %sGzip, %sCached,"
                                      " len %d, content-len %d, dur %d ms" %
                                      (self.id, ts, self.method, self.url, self.status, self.type,
                                       "+" if self.keepalive else "-",
                                       "+" if self.gzipped else "-",
                                       "+" if self.cached else "-",
                                       self.length, self.content_length, self.dur))
  
      def is_long_poll(self, longpolls=None):
          if self.longpoll:
              return True
  
          if not longpolls:
              return False
          for l in longpolls:
              if l in self.url:
                  self.longpoll = True
                  return True
          return False
  
      def is_ok(self):
          if self.longpoll:
              return True
          try:
              base = int(self.status / 100)
          except TypeError:
              return False
          return base in (0, 2, 3)
  
      def validate_response(self, data):
          if self.validator is not None:
              if data.find(self.validator) < 0:
                  data = data.decode('utf-8').encode('ascii', 'ignore')
                  from browser import BrowserExc
                  raise BrowserExc('%s %s validation failed\nvalidator: %s\ndata: %s' %
                                   (self.method, self.url, self.validator, data))
          return data
  
      def update_netloc(self, url):
          prot, netloc, _ = parse_url(url)
          self.url = "%s://%s%s" % (prot, netloc, self.url)
  
  
  class Page:
      def __init__(self, browser, url, cached=True, longpolls=None, name=None, real_navigation=True):
          self.browser = browser
          self.browser_pid = browser.pid if browser else 0  # denormalization required for faster serialization
          self.requests = []  # append in add_request
          self.requests_groups = []  # append on complete()
          self._id2request = {}
          self.longpolls = longpolls
  
          # FIXME: must be moved to cp_webdriver
          # FIXME: /gelf is a telemtry page on graylog.ap.int.zone/gelf that hangs sometime
          if not self.longpolls:
              self.longpolls = ["notifications?channel=", "notifications/channel", "/gelf", "/api/subscriptions"]
  
          self.ram_usage_kb = 0
  #        self.time_start_utc = int(time.time() * 1000)
          self.ts_start = None
          self.ts_end = None
          self.dur = 0
          self.url = url
          self.name = name
          self.cached = cached
          if url:
              self.domain = parse_url(url, server=True)
  
          self.length = 0
          self.timeline = PageTimeline(self)
          self.real_navigation = real_navigation  # True - we can trust browser navigation API, false - we can't
  
          if not self.real_navigation:
              self.cached = True  # FIXME: enforce cached=True for DOM-clicks, otherwise it looks strange in summary
  
          # page content
          self.data = ""
  
      def __str__(self):
          return self.url
  
      def __unicode__(self):
          return self.url
  
      def __deepcopy__(self, memo):
          # override deepcopy to:
          # 1. avoid 'browser' object cloning
          # 2. reset timelines
  
          cls = self.__class__
          result = cls(None, self.url, cached=self.cached, longpolls=self.longpolls, name=self.name)
          memo[id(self)] = result
          for k, v in self.__dict__.items():
              if k == "browser":
                  result.browser = v
              elif k == "timeline":
                  result.timeline = None
              else:
                  setattr(result, k, copy.deepcopy(v, memo))
          result.ts_start = None
          result.ts_end = None
          result.timeline = PageTimeline(result)
          return result
  
      def add_request(self, req):
          if req.id in self._id2request:
              self._id2request[req.id] = req
              for n in range(0, len(self.requests)):
                  if self.requests[n].id == req.id:
                      self.requests[n] = req
                      return
          else:
              self._id2request[req.id] = req
              self.requests.append(req)
  
      def del_request(self, req):
          if req.id in self._id2request:
              del self._id2request[req.id]
              for n in range(0, len(self.requests)):
                  if self.requests[n].id == req.id:
                      del self.requests[n]
                      return
  
      def get_request(self, id):
          if id in self._id2request:
              return self._id2request[id]
          return None
  
      def process_activity(self, name, timestamp):
          if not self.ts_start or self.ts_start > timestamp:
              self.ts_start = timestamp
              self.browser.log_debug("First detected activity '%s' timestamp: %d" % (name, self.ts_start))
  
          if not self.ts_end or self.ts_end < timestamp:
              self.ts_end = timestamp
              self.browser.log_debug("Last detected activity '%s' timestamp: %d" % (name, self.ts_end))
  
      def get_incomplete_reqs(self):
          return [r for r in self.requests if not r.completed and not r.is_long_poll(self.longpolls)]
  
      def get_uncached_reqs(self):
          return [r for r in self.requests if not r.cached]
  
      def get_error_reqs(self):
          return [r for r in self.requests if not r.is_ok()]
  
      def get_repeated_reqs_cnt(self):
          urls = [r.url for r in self.get_uncached_reqs()]
          return len(urls) - len(set(urls))
  
      def get_foreign_reqs(self):
          ret = []
          if not hasattr(self, '__page_netlocs'):
              _, netloc, _ = parse_url(self.url)
              self.__page_netlocs = [netloc, netloc[4:] if netloc.startswith("www.") else "www." + netloc]
  
          for r in self.get_uncached_reqs():
              _, netloc, _ = parse_url(r.url)
              if netloc not in self.__page_netlocs:
                  ret.append(r)
          return ret
  
      def print_page_requests_stats(self, title=True, description=None):
          print("")
          if title and not isinstance(title, str):
              title = "Navigation's network requests summary"
          if title:
              PageStats.print_title(title)
  
          PageStats.print_description(description)
  
          t = TextTable()
          t.add_row(["Type", "Requests", "non-200ok", "non-KA", "non-GZ", "Recv(KB)", "RecvAvg(KB)", "DurAvg(ms)"])
          t.add_row("-")
  
          uncached = self.get_uncached_reqs()
  
          for type in PageRequest.types + ["Total"]:
              if type == "Total":
                  t.add_row("-")
                  items = uncached
              else:
                  items = [r for r in uncached if r.type == type]
  
              if not items:
                  continue
  
              L = sum([r.length for r in items])
              d = sum([r.dur for r in items])
              t.add_row([PageRequest.types_abbr[type],
                         len(items),
                         len([r for r in items if r.status != 200]),
                         len([r for r in items if not r.keepalive]),
                         len([r for r in items if not r.gzipped]),
                         "%8s" % ("%.1f" % (float(L) / 1024)),
                         "%11s" % ("%.1f" % (float(L) / 1024 / len(items))),
                         "%10s" % ("%.0f" % (float(d) / len(items)))
                         ])
  
          print("  " + "\n  ".join(t.get_lines()))
  
      def print_page_req_groups_stats(self, title=True, description=None):
          print("")
          if title and not isinstance(title, str):
              title = "Navigation network activity grouped by independent requests"
          if title:
              PageStats.print_title(title)
              print("  Every *group* is separated by the ---- line, group includes independent requests only")
              print("  - Sta   - response HTTP status code")
              print("  - Recv  - length of received data")
              print("  - AX    - is Ajax request")
              print("  - KA    - keep-alive enabled")
              print("  - GZ    - response compressed by gzip\n")
  
          PageStats.print_description(description)
  
          t = TextTable(max_col_width=[0, 0, 0, 0, 0, 0, 0, 0, 0, 90], col_separator=" ", left_aligned=[0, 9])
          t.add_row(["Typ", "Sta", " Recv", "AX", "KA", "GZ", "Start", " Dur", "  End", "Url"])
          t.add_row(["", "", " (KB)", "", "", "", " (ms)", "(ms)", " (ms)", ""])
  
          for g in self.requests_groups:
              reqs = g.get_uncached_reqs()
              if not reqs:
                  continue
  
              t.add_row("-")
              for r in sorted(reqs, key=lambda x: x.ts_start):
                  t.add_row(["%s" % PageRequest.types_abbr[r.type],
                             r.status,
                             "%5s" % ("%.1f" % (r.length / 1024.0)),
                             "ax" if (r.ts_start - self.ts_start) > self.timeline.ajax_start else " -",
                             "ka" if r.keepalive else " -",
                             "gz" if r.gzipped else " -",
                             int(round(r.ts_start - self.ts_start)),
                             r.dur,
                             int(round(r.ts_start + r.dur - self.ts_start)),
                             r.get_url(self.domain)])
          print("  " + "\n  ".join(t.get_lines()))
  
      def start(self, ts=None):
          self.ts_start = ts if ts else int(time.time() * 1000)
  
      def complete(self, browser, ts=None):
          if not self.ts_end:
              ends = [req.ts_end for req in self.requests]
              self.ts_end = max(ends) if ends else self.timeline.values['ajaxEnd']
  
          self.ram_usage_kb = browser.browser_get_ram_usage_kb()
  
          if self.real_navigation:
              self.timeline = browser.browser_get_page_timeline(self)
              if self.timeline.values['navStrt']:
                  # Ok, it was real navigation request and we can trust browser navigation API
                  if len(self.requests):
                      if not self.ts_start:
                          self.browser.log_error("BUG: page %s load start time is empty" % str(self.url))
                      offt = self.timeline.values['navStrt'] - self.ts_start
                  else:
                      offt = 0
                      self.ts_start = self.timeline.values['navStrt']
  
                  self.browser.log_debug("Adjusting page/requests timestamp by %d ms" % round(offt))
  
                  self.ts_start += offt
                  self.ts_end += offt
                  for r in self.requests:
                      r.ts_start += offt
                      if r.is_long_poll(self.longpolls):
                          self.browser.log_debug("Skip '%s' longpoll request" % r.url)
                          continue
                      elif not r.ts_end:
                          self.browser.log_error("Can't determine '%s' request completion ts, ignored!" % r.url)
                          r.status = "timeout"
                      else:
                          r.ts_end += offt
                  for g in self.requests_groups:
                      g.ts_start += offt
                      g.ts_end += offt
  
          self.length = 0
          for r in self.requests:
              if r.content_length:
                  r.length = r.content_length
              if not r.cached:
                  self.length += r.length
  
          self.dur = max(self.ts_end - self.ts_start, self.timeline.total_dur)
  
          for r in sorted([r for r in self.requests if not r.is_long_poll(self.longpolls)], key=lambda x: x.ts_start):
              if not len(self.requests_groups) or not self.requests_groups[-1].add_request(r):
                  self.requests_groups.append(PageRequestsGroup(r))
  
          # self.data = "".join([req.data for req in self.requests if req.url == self.url])
          for req in self.requests:
              if req.url == self.url:
                  if type(req.data) == bytes:
                      self.data += req.data.decode('utf-8')
                  else:
                      self.data += req.data
  
      def get_full_name(self, url_prefix_to_remove=""):
          if self.name:
              return self.name
          return self.url[len(url_prefix_to_remove):]
  
      def get_key(self, name_priority=False):
          return (self.name if self.name else self.url.split("?bw_id")[0], self.cached)
  
      def serialize(self, use_pickle=True):
          self.browser.log_debug("serializing page: %s, %d, %s" % (str(int(self.ts_start)), int(self.dur), self.url))
          if use_pickle:
              p = copy.deepcopy(self)
              p.ts_start = self.ts_start  # it was zeroed by deepcopy()
              p.ts_end = self.ts_end  # it was zeroed by deepcopy()
              p.browser = None  # detach real browser object since it has open files
              return base64.urlsafe_b64encode(pickle.dumps(p)) + "\n"
  
          return "%s|%1.3f|%6d|%2d|%s|%s|%s|%d|%s\n" % \
                 (str(int(self.ts_start)), self.dur / 1000.0, self.length, len(self.get_uncached_reqs()),
                  "C" if self.cached else "U", self.name if self.name else "", self.browser_pid, str(self.id), self.url)
  
      @staticmethod
      def deserialize(line, browser_class, use_pickle=True):
  
          if use_pickle:
              p = pickle.loads(base64.urlsafe_b64decode(line.strip()))
              p.ts_start = int(p.ts_start)
              p.ts_end = int(p.ts_end)
              return p
  
          ar = line.split("|")
          if len(ar) < 7:
              raise Exception("can't parse line: %s" % line)
  
          url = ar[7].strip()
  
          p = Page(None, url)
          p.ts_start = int(ar[0])
          p.dur = float(ar[1].strip()) * 1000.0
          p.ts_end = int(p.ts_start) + int(round(p.dur, 0))
          p.length = int(ar[2])
          p.requests = [] * int(ar[3])
          p.cached = ar[4] == "C"
          p.browser_pid = ar[5].strip()
          p.name = ar[6].strip() if ar[5] else None
          p.url = url
  
          return p
  
  
  class PageStatsSummary:
      def __init__(self):
          self.common_prefix = ""
          self.page_stats = []
  
          def browser_dict():
              return defaultdict(url_dict)
  
          def url_dict():
              return defaultdict(int)
  
          def status_dict():
              return defaultdict(int)
  
          self.reqs = defaultdict(browser_dict)
          self.errs = {}
  
      def add_page_stats(self, page_stats):
          page_stats.summary = self
          self.page_stats.append(page_stats)
  
      def update_reqs(self):
          self.reqs = defaultdict(browser_dict)
  
          urls = set()
          for ps in self.page_stats:
              for p in ps.iterations:
                  for r in p.requests:
                      urls.add(r.url)
  
          self.common_prefix = get_common_url_prefix(urls)
  
          for ps in self.page_stats:
              for p in ps.iterations:
                  for r in p.requests:
                      reqs[ps.id][r.url]['all'] += 1
                      if not r.is_ok():
                          self.reqs[ps.id][r.url][r.status] += 1
  
          self.errs = {}
          for b, items in self.reqs.items():
              for url, statuses in items.items():
                  for status, count in reqs[b][url].items():
                      if status == 'all':
                          continue
                      total = reqs[b][url]['all']
                      if b not in errs:
                          errs[b] = []
                      errs[b].append(["  " + url[len(self.common_prefix):], status, count,
                                     "%.1f" % (100 * count / (1.0 * total))])
  
      def print_summary(self, title="Summary"):
          print("")
          if title and not isinstance(title, str):
              title = "Summary"
          if title:
              PageStats.print_title(title)
  
          t = TextTable(left_aligned=[0], max_col_width=[72])
          t.add_row(["Screen", "Iters", "   Requests per page   ", "RecvAvg", "Total", "MemUsg"])
          t.add_row(["", "", "Ntwrk  Rptd  Frgn  Errs", "   (KB)", " (ms)", "  (KB)"])
          t.add_row("-")
  
          prev_psid = ""
          for ps in self.page_stats:
  
              if prev_psid != ps.id:
                  t.add_row(str(ps.id) + ":")
                  prev_psid = ps.id
  
              t.add_row(["  " + ps.get_screen_title(self.common_prefix), len(ps.iterations),
                         ("%5s  %4s  %4s  %4s") %
                         ("%5.0f" % ps.uncached_reqs if ps.uncached_reqs else "-",
                          "%4.0f" % ps.repeated_reqs if ps.repeated_reqs else "-",
                          "%4.0f" % ps.foreign_reqs if ps.foreign_reqs else "-",
                          "%.1f!" % (ps.errs_cnt) if ps.errs_cnt else "-"),
                         "%.1f" % (ps.size_bytes / 1024.0),
                         "%.0f" % ps.dur_sec,
                         "%.0f" % ps.ram_usage_kb])
  
          print("  " + "\n  ".join(t.get_lines()))
  
          if len(self.errs):
              print("")
              PageStats.print_title("Warning: error network requests detected !!!")
  
              wt = TextTable(left_aligned=[0, 1], max_col_width=[80])
              wt.add_row(["URL", "Status", "Count", "% of total"])
              wt.add_row("-")
              for b, rows in self.errs.items():
                  wt.add_row(b + ":")
                  for row in rows:
                      wt.add_row(row)
              print("  " + "\n  ".join(wt.get_lines()))
  
  
  class PageStats:
      separator = "-->"
      width = 118
  
      def __init__(self, id=""):
          self.iterations = []
          self.id = id
          self.summary = None
  
          self.update()
  
      def update(self):
          self.size_bytes = 0
          self.errs_cnt = 0
          self.uncached_reqs = 0
          self.repeated_reqs = 0
          self.foreign_reqs = 0
          self.dur_sec = 0
          self.ram_usage_kb = 0
  
          if not len(self.iterations):
              return
  
          for i in self.iterations:
              self.size_bytes += i.length
              self.errs_cnt += len(i.get_error_reqs())
              self.uncached_reqs += len(i.get_uncached_reqs())
              self.repeated_reqs += i.get_repeated_reqs_cnt()
              self.foreign_reqs += len(i.get_foreign_reqs())
              self.dur_sec += i.dur
              self.ram_usage_kb += i.ram_usage_kb
  
          n = float(len(self.iterations))
  
          self.size_bytes /= n
          self.errs_cnt /= n
          self.uncached_reqs /= n
          self.repeated_reqs /= n
          self.foreign_reqs /= n
          self.dur_sec /= n
          self.ram_usage_kb /= n
  
      def add_iteration(self, page):
          self.iterations.append(page)
          self.update()
          if self.summary:
              self.summary.update_reqs()
  
      @staticmethod
      def print_title(title):
          print(title.upper())
          print("=" * len(title))
          print("")
  
      @staticmethod
      def print_description(description):
          if description:
              print("  " + "\n  ".join(description))
              print("")
  
      @staticmethod
      def print_page_timeline_header(title=True, description=None):
          print("")
          if title and not isinstance(title, str):
              title = "Page(s) timeline and memory usage"
          if title:
              PageStats.print_title(title)
  
              j = PageTimeline.jstypes
              print("  http://www.w3.org/TR/navigation-timing/timing-overview.png")
              print("  http://www.w3.org/TR/navigation-timing/#sec-navigation-timing-interface")
              print("  - navStrt    - %s - navigation start" % j['navStrt'])
              print("  - reqStrt    - %s - browser has resolved the domain (and redirects)"
                    " and started the request" % j['reqStrt'])
              print("  - rspStrt    - %s - server started response (TTFB)" % j['rspStrt'])
              print("  - rspEnd     - %s - browser received last byte of the document" % j['rspEnd'])
              print("  - domEnd     - %s - browser has downloaded all the CSS, JS and rendered the DOM model" %
                    j['domEnd'])
              print("  - onloadEnd  - %s - browser completed the onload() callbacks" % j['onloadEnd'])
              print("  - ajaxEnd    - last detected browser activity, all pending img/css/js and Ajax requests"
                    " have been completed")
              print("  - Total(ms)  - total time of page (TTLB) including all the img/css/js and Ajax requests")
              print("  - MemUsg(KB) - page memory usage (RSS delta between browser start and after page fully loaded)")
              print("")
  
          PageStats.print_description(description)
  
          print("  Iter # " + " |", end=" ")
          for n in range(0, len(PageTimeline.types) - 1):
              print("%s %s" % (PageTimeline.types[n], PageStats.separator), end=" ")
          print(PageTimeline.types[-1], end=" ")
          print("| Total(ms) | MemUsg(KB)")
          print("  " + "-" * PageStats.width)
  
      def print_page_timeline(self, p, title="", hr=False):
          if not p:
              return
  
          if hr:
              print("  " + "-" * self.width)
  
          print("  %7s |" % title, end=" ")
          t = p.timeline
          for n in range(0, len(t.deltas)):
              print("%s%5d" % (" " * (len(PageTimeline.types[n]) - 4 + len(self.separator)), t.deltas[n]), end=" ")
          print("%s" % (" " * len(PageTimeline.types[-1])), end=" ")
          print("| %9d | %10d" % (p.dur, p.ram_usage_kb))
  
      def get_avg(self, iterations=None):
          if not iterations:
              iterations = self.iterations
          if not iterations or len(iterations) < 2:
              return None
  
          avg = Page(None, "", None)
          avg.iterations = len(iterations)
          for p in iterations:
              t = p.timeline
              for d in range(0, len(t.deltas)):
                  avg.timeline.deltas[d] += t.deltas[d]
              avg.dur += p.dur
  
          for d in range(0, len(t.deltas)):
              avg.timeline.deltas[d] = int(avg.timeline.deltas[d] / len(iterations))
          avg.dur = int(avg.dur / len(iterations))
          avg.ram_usage_kb = sum([p.ram_usage_kb for p in iterations]) / len(iterations)
          return avg
  
      def get_screen_title(self, common_prefix=""):
          if len(self.iterations):
              return self.iterations[0].get_full_name(common_prefix)
          return ""
  
  
  # Represents some kind of the html page model with focus on actions (URLs)
  class PageWithActions:
      def __init__(self, actions, body, url):
          self.actions = actions  # actions (urls) found on page
          self.body = body  # page body (html text)
          self.url = url  # page source url
  