15821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)#!/usr/bin/env python
25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# Copyright (c) 2011 The Chromium Authors. All rights reserved.
35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# Use of this source code is governed by a BSD-style license that can be
45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# found in the LICENSE file.
55821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
65821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)"""Process a History database and dump a .dot file suitable for GraphViz.
75821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
85821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)This is useful for debugging history redirect flows.
95821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)An example run of this program:
115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  python /src/history-viz.py History > foo.dot
125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  /c/Program\ Files/Graphviz2.18/bin/dot -Tpng foo.dot -o foo.png
135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)"""
145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import struct
165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import subprocess
175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import sys
185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import urlparse
195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# Some transition types, copied from page_transition_types.h.
225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)TRANS_TYPES = {
235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  0: 'link',
245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  1: 'typed',
255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  2: 'most-visited',
265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  3: 'auto subframe',
275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  7: 'form',
285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)class URL(object):
325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Represents a broken-down URL from our most visited database."""
335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  def __init__(self, id, url):
355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    """Initialize a new URL object.  |id| is the database id of the URL."""
365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.id = id
375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.url = url
385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    scheme, loc, path, query, fragment = urlparse.urlsplit(url)
395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if scheme == 'http':
405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      scheme = ''  # Shorten for display purposes.
415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if len(scheme) > 0:
425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      scheme += '://'
435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.host = scheme + loc
445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.path = path
455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    extra = ''
475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if len(query) > 0:
485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      extra += '?' + query
495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if len(fragment) > 0 or url.find('#') > 0:
505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      extra += '#' + fragment
515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.extra = extra
525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  def PrettyPrint(self, include_host=True, include_path=True):
545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    """Pretty-print this URL in a form more suitable for the graph.
555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    This will elide very long paths and potentially puts newlines between parts
575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    of long components.  include_host and include_path determine whether to
585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    include the host and path in the output.
595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    Returns: the pretty-printed string."""
615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    MAX_LEN = 30  # Maximum length of a line in the output.
625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    parts = []
635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if include_host:
645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      parts.append(self.host)
655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if include_path:
665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      parts.append(self.path)
675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    parts.append(self.extra)
685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    lines = []
695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    line = ''
705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    for part in parts:
715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if len(part) > MAX_LEN:
725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        part = part[0:MAX_LEN-3] + '...'
735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if len(line)+len(part) > MAX_LEN:
745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        lines.append(line)
755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        line = ''
765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      line += part
775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if len(line) > 0:
785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      lines.append(line)
795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return '\n'.join(lines)
805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)class Edge(object):
835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Represents an edge in the history graph, connecting two pages.
845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  If a link is traversed twice, it is one Edge with two entries in
865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  the .transitions array."""
875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  def __init__(self, src, dst):
885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.src = src
895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.dst = dst
905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.transitions = []
915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  def Transitions(self):
935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    """Return a dictionary mapping transition type -> occurences."""
945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    all = {}
955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    for trans in self.transitions:
965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      all[trans] = all.get(trans, 0) + 1
975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      # We currently don't use the chain type.
985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      # TODO(evanm): make this a command-line option.
995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      # if trans & 0x30000000 != 0:
1005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      #   chain = ''
1015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      #   if trans & 0x10000000:
1025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      #     chain = 'start'
1035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      #   if trans & 0x20000000:
1045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      #     if len(chain) == 0:
1055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      #       chain = 'end'
1065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      #     else:
1075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      #       chain = ''
1085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      #   if len(chain) > 0:
1095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      #     edge['chain'] = chain
1105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return all
1115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def ClusterBy(objs, pred):
1145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Group a list of objects by a predicate.
1155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Given a list of objects and a predicate over the objects, return a
1175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  dictionary mapping pred(obj) -> all objs with the same pred(obj)."""
1185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  clusters = {}
1195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  for obj in objs:
1205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    cluster = pred(obj)
1215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    clusters[cluster] = clusters.get(cluster, [])
1225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    clusters[cluster].append(obj)
1235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return clusters
1245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def EscapeDot(string):
1275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Escape a string suitable for embedding in a graphviz graph."""
1285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  # TODO(evanm): this is likely not sufficient.
1295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return string.replace('\n', '\\n')
1305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)class SQLite(object):
1335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  """Trivial interface to executing SQLite queries.
1345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  Spawns a new process with each call."""
1355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  def __init__(self, file=None):
1365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    self.file = file
1375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  def Run(self, sql):
1395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    """Execute |sql|, yielding each row of results as an array."""
1405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    subproc = subprocess.Popen(['sqlite', self.file],
1415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                               stdin=subprocess.PIPE,
1425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                               stdout=subprocess.PIPE)
1435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    subproc.stdin.write('.mode tabs\n')
1445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    subproc.stdin.write(sql + ';')
1455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    subproc.stdin.close()
1465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    for line in subproc.stdout:
1475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      row = line.strip().split('\t')
1485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      yield row
1495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def LoadHistory(filename):
1525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  db = SQLite(filename)
1535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  urls = {}  # Map of urlid => url.
1555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  urls['0'] = URL('0', 'start')  # Node name '0' is our special 'start' node.
1565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  for id, url in db.Run('SELECT id, url FROM urls'):
1575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    urls[id] = URL(id, url)
1585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  visiturlids = {}  # Map of visitid => urlid.
1605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  visiturlids['0'] = '0'  # '0' is our special 'start' node.
1615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  edges = {}  # Map of urlid->urlid->Edge.
1625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  for src, dst, url, trans in db.Run('SELECT from_visit, id, url, transition '
1635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                                     'FROM visits ORDER BY id'):
1645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    visiturlids[dst] = url
1655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    src = visiturlids[src]
1665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    dst = visiturlids[dst]
1675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    edges[src] = edges.get(src, {})
1685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    edge = edges[src][dst] = edges[src].get(dst, Edge(src, dst))
1695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    # SQLite outputs transition values as signed integers, but they're really
1705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    # a bitfield.  Below does "unsigned trans = static_cast<unsigned>(trans)".
1715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    trans = struct.unpack('I', struct.pack('i', int(trans)))[0]
1725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    edge.transitions.append(trans)
1735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return urls, edges
1755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def main():
1785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  urls, edges = LoadHistory(sys.argv[1])
1795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  print 'digraph G {'
1805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  print '  graph [rankdir=LR]'  # Display left to right.
1815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  print '  node [shape=box]'    # Display nodes as boxes.
1825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  print '  subgraph { rank=source; 0 [label="start"] }'
1835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  # Output all the nodes within graph clusters.
1855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  hosts = ClusterBy(urls.values(), lambda url: url.host)
1865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  for i, (host, urls) in enumerate(hosts.items()):
1875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    # Cluster all URLs under this host if it has more than one entry.
1885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    host_clustered = len(urls) > 1
1895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if host_clustered:
1905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      print 'subgraph clusterhost%d {' % i
1915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      print '  label="%s"' % host
1925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    paths = ClusterBy(urls, lambda url: url.path)
1935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    for j, (path, urls) in enumerate(paths.items()):
1945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      # Cluster all URLs under this host if it has more than one entry.
1955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      path_clustered = host_clustered and len(urls) > 1
1965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if path_clustered:
1975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        print '  subgraph cluster%d%d {' % (i, j)
1985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        print '    label="%s"' % path
1995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      for url in urls:
2005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if url.id == '0': continue  # We already output the special start node.
2015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        pretty = url.PrettyPrint(include_host=not host_clustered,
2025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                                include_path=not path_clustered)
2035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        print '    %s [label="%s"]' % (url.id, EscapeDot(pretty))
2045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if path_clustered:
2055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        print '  }'
2065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if host_clustered:
2075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      print '}'
2085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  # Output all the edges between nodes.
2105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  for src, dsts in edges.items():
2115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    for dst, edge in dsts.items():
2125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      # Gather up all the transitions into the label.
2135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      label = []      # Label for the edge.
2145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      transitions = edge.Transitions()
2155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      for trans, count in transitions.items():
2165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        text = ''
2175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if count > 1:
2185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          text = '%dx ' % count
2195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        base_type = trans & 0xFF
2205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        redir = (trans & 0xC0000000) != 0
2215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        start = (trans & 0x10000000) != 0
2225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        end = (trans & 0x20000000) != 0
2235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if start or end:
2245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          if start:
2255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            text += '<'
2265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          if end:
2275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            text += '>'
2285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          text += ' '
2295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        if redir:
2305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          text += 'R '
2315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        text += TRANS_TYPES.get(base_type, 'trans%d' % base_type)
2325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        label.append(text)
2335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if len(label) == 0:
2345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        continue
2355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      edgeattrs = []  # Graphviz attributes for the edge.
2375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      # If the edge is from the start and the transitions are fishy, make it
2385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      # display as a dotted line.
2395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if src == '0' and len(transitions.keys()) == 1 and transitions.has_key(0):
2405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        edgeattrs.append('style=dashed')
2415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if len(label) > 0:
2425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        edgeattrs.append('label="%s"' % EscapeDot('\n'.join(label)))
2435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      out = '%s -> %s' % (src, dst)
2455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if len(edgeattrs) > 0:
2465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        out += ' [%s]' % ','.join(edgeattrs)
2475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      print out
2485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  print '}'
2495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return 0
2505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
2525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)if __name__ == '__main__':
2535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  sys.exit(main())
254