1effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch# Copyright 2014 The Chromium Authors. All rights reserved.
2a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)# Use of this source code is governed by a BSD-style license that can be
3a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)# found in the LICENSE file.
4a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
5a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)import HTMLParser
6a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)import json
7a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)import os
8a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)import string
9a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
10a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)from telemetry.web_components import web_components_project
11a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)from tvcm import generate
12a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
13a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
14effb81e5f8246d0db0270817048dc992db66e9fbBen Murdochclass WebComponent(object):
15a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  """An HTML-based viewer of data, built out of telemetry components.
16a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
17effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  A WebComponent is used to visualize complex data that is produced from a
18effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  telemtry benchmark. A WebComponent is a Javascript class that can be
19effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  data-bound, plus python-side bindings that let us write HTML files that
20effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  instantiate that class and bind it to specific data.
21a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
22effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  The primary job of the python side of a WebComponent is to implement the
23effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  WriteDataToFileAsJson. The data written here is passed to the
24effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  data_binding_property of the JS-side class.
25effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
26effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  The primary job of the javascript side of a WebComponent is visualization: it
27effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  takes the data from python and renders a UI for displaying that data in some
28effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  manner.
29a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
30a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  """
31a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  def __init__(self, tvcm_module_name, js_class_name, data_binding_property):
32a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    self._tvcm_module_name = tvcm_module_name
33a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    self._js_class_name = js_class_name
34a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    self._data_binding_property = data_binding_property
35effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    self._data_to_view = None
36effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
37effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  @property
38effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  def data_to_view(self):
39effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    return self._data_to_view
40effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
41effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  @data_to_view.setter
42effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  def data_to_view(self, data_to_view):
43effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    self._data_to_view = data_to_view
44a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
45a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  def WriteDataToFileAsJson(self, f):
46a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    raise NotImplementedError()
47a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
48effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  def GetDependentModuleNames(self):
49effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    return [self._tvcm_module_name]
50effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
51effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  def WriteWebComponentToFile(self, f):
52a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    project = web_components_project.WebComponentsProject()
53a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    load_sequence = project.CalcLoadSequenceForModuleNames(
54effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch      self.GetDependentModuleNames())
55a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
56a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    with open(os.path.join(os.path.dirname(__file__),
57effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                           'web_component_bootstrap.js')) as bfile:
58a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      bootstrap_js_template = string.Template(bfile.read())
59a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    bootstrap_js = bootstrap_js_template.substitute(
60a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      js_class_name=self._js_class_name,
61a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      data_binding_property=self._data_binding_property)
62a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
63a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    bootstrap_script = generate.ExtraScript(text_content=bootstrap_js)
64a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
65effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    class WebComponentDataScript(generate.ExtraScript):
66a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      def __init__(self, results_component):
67effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch        super(WebComponentDataScript, self).__init__()
68a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        self._results_component = results_component
69a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
70a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      def WriteToFile(self, output_file):
71effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch        output_file.write('<script id="telemetry-web-component-data" ' +
72effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch                          'type="application/json">\n')
73a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        self._results_component.WriteDataToFileAsJson(output_file)
74a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        output_file.write('</script>\n')
75a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
76a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
77a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    generate.GenerateStandaloneHTMLToFile(
78a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        f, load_sequence,
79a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        title='Telemetry results',
80effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch        extra_scripts=[bootstrap_script, WebComponentDataScript(self)])
81a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
82a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  @staticmethod
83effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch  def ReadDataObjectFromWebComponentFile(f):
84effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    """Reads the data inside a file written with WriteWebComponentToFile
85effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch
86effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    Returns None if the data wasn't found, the JSON.parse'd object on success.
87effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch    Raises exception if the HTML file was corrupt.
88a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
89a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    """
90a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    class MyHTMLParser(HTMLParser.HTMLParser):
91a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      def __init__(self):
92a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        HTMLParser.HTMLParser.__init__(self)
93a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        self._got_data_tag = False
94a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        self._in_data_tag = False
95a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        self._data_records = []
96a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
97a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      def handle_starttag(self, tag, attrs):
98a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        if tag != 'script':
99a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)          return
100a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        id_attr = dict(attrs).get('id', None)
101effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch        if id_attr == 'telemetry-web-component-data':
102a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)          assert not self._got_data_tag
103a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)          self._got_data_tag = True
104a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)          self._in_data_tag = True
105a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
106a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      def handle_endtag(self, tag):
107a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        self._in_data_tag = False
108a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
109a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      def handle_data(self, data):
110a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        if self._in_data_tag:
111a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)          self._data_records.append(data)
112a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
113a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      @property
114a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      def data(self):
115a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        if not self._got_data_tag:
116effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch          raise Exception('Missing <script> with #telemetry-web-component-data')
117a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        if self._in_data_tag:
118effb81e5f8246d0db0270817048dc992db66e9fbBen Murdoch          raise Exception('Missing </script> on #telemetry-web-component-data')
119a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)        return json.loads(''.join(self._data_records))
120a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)
121a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    parser = MyHTMLParser()
122a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    for line in f:
123a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)      parser.feed(line)
124a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)    return parser.data
125