• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2019 The TensorFlow Authors. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ==============================================================================
15"""A utility class to generate the report HTML based on a common template."""
16
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import print_function
20
21import io
22import os
23
24from tensorflow.lite.toco.logging import toco_conversion_log_pb2 as _toco_conversion_log_pb2
25from tensorflow.python.lib.io import file_io as _file_io
26from tensorflow.python.platform import resource_loader as _resource_loader
27
28html_escape_table = {
29    "&": "&",
30    '"': """,
31    "'": "'",
32    ">": ">",
33    "<": "&lt;",
34}
35
36
37def html_escape(text):
38  return "".join(html_escape_table.get(c, c) for c in text)
39
40
41def get_input_type_from_signature(op_signature):
42  """Parses op_signature and returns a string denoting the input tensor type.
43
44  Args:
45    op_signature: a string specifying the signature of a particular operator.
46      The signature of an operator contains the input tensor's shape and type,
47      output tensor's shape and type, operator's name and its version. It has
48      the following schema:
49      INPUT:input_1_shape::input_1_type::input_2_shape::input_2_type::..
50        ::OUTPUT:output_1_shape::output_1_type::output_2_shape::output_2_type::
51        ..::NAME:operator_name ::VERSION:operator_version
52     An example of an operator signature is:
53     INPUT:[1,73,73,160]::float::[64,1,1,160]::float::[64]::float::
54     OUTPUT:[1,73,73,64]::float::NAME:Conv::VERSION:1
55
56  Returns:
57    A string denoting the input tensors' type. In the form of shape/type
58    separated
59    by comma. For example:
60    shape:[1,73,73,160],type:float,shape:[64,1,1,160],type:float,shape:[64],
61    type:float
62  """
63  start = op_signature.find(":")
64  end = op_signature.find("::OUTPUT")
65  inputs = op_signature[start + 1:end]
66  lst = inputs.split("::")
67  out_str = ""
68  for i in range(len(lst)):
69    if i % 2 == 0:
70      out_str += "shape:"
71    else:
72      out_str += "type:"
73    out_str += lst[i]
74    out_str += ","
75  return out_str[:-1]
76
77
78def get_operator_type(op_name, conversion_log):
79  if op_name in conversion_log.built_in_ops:
80    return "BUILT-IN"
81  elif op_name in conversion_log.custom_ops:
82    return "CUSTOM OP"
83  else:
84    return "SELECT OP"
85
86
87class HTMLGenerator(object):
88  """Utility class to generate an HTML report."""
89
90  def __init__(self, html_template_path, export_report_path):
91    """Reads the HTML template content.
92
93    Args:
94      html_template_path: A string, path to the template HTML file.
95      export_report_path: A string, path to the generated HTML report. This path
96        should point to a '.html' file with date and time in its name.
97        e.g. 2019-01-01-10:05.toco_report.html.
98
99    Raises:
100      IOError: File doesn't exist.
101    """
102    # Load the template HTML.
103    if not _file_io.file_exists(html_template_path):
104      raise IOError("File '{0}' does not exist.".format(html_template_path))
105    with _file_io.FileIO(html_template_path, "r") as f:
106      self.html_template = f.read()
107
108    _file_io.recursive_create_dir(os.path.dirname(export_report_path))
109    self.export_report_path = export_report_path
110
111  def generate(self,
112               toco_conversion_log_before,
113               toco_conversion_log_after,
114               post_training_quant_enabled,
115               dot_before,
116               dot_after,
117               toco_err_log="",
118               tflite_graph_path=""):
119    """Generates the HTML report and writes it to local directory.
120
121    This function uses the fields in `toco_conversion_log_before` and
122    `toco_conversion_log_after` to populate the HTML content. Certain markers
123    (placeholders) in the HTML template are then substituted with the fields
124    from the protos. Once finished it will write the HTML file to the specified
125    local file path.
126
127    Args:
128      toco_conversion_log_before: A `TocoConversionLog` protobuf generated
129        before the model is converted by TOCO.
130      toco_conversion_log_after: A `TocoConversionLog` protobuf generated after
131        the model is converted by TOCO.
132      post_training_quant_enabled: A boolean, whether post-training quantization
133        is enabled.
134      dot_before: A string, the dot representation of the model
135        before the conversion.
136      dot_after: A string, the dot representation of the model after
137        the conversion.
138      toco_err_log: A string, the logs emitted by TOCO during conversion. Caller
139        need to ensure that this string is properly anonymized (any kind of
140        user data should be eliminated).
141      tflite_graph_path: A string, the filepath to the converted TFLite model.
142
143    Raises:
144      RuntimeError: When error occurs while generating the template.
145    """
146    html_dict = {}
147    html_dict["<!--CONVERSION_STATUS-->"] = (
148        r'<span class="label label-danger">Fail</span>'
149    ) if toco_err_log else r'<span class="label label-success">Success</span>'
150    html_dict["<!--TOTAL_OPS_BEFORE_CONVERT-->"] = str(
151        toco_conversion_log_before.model_size)
152    html_dict["<!--TOTAL_OPS_AFTER_CONVERT-->"] = str(
153        toco_conversion_log_after.model_size)
154    html_dict["<!--BUILT_IN_OPS_COUNT-->"] = str(
155        sum(toco_conversion_log_after.built_in_ops.values()))
156    html_dict["<!--SELECT_OPS_COUNT-->"] = str(
157        sum(toco_conversion_log_after.select_ops.values()))
158    html_dict["<!--CUSTOM_OPS_COUNT-->"] = str(
159        sum(toco_conversion_log_after.custom_ops.values()))
160    html_dict["<!--POST_TRAINING_QUANT_ENABLED-->"] = (
161        "is" if post_training_quant_enabled else "isn't")
162
163    pre_op_profile = ""
164    post_op_profile = ""
165
166    # Generate pre-conversion op profiles as a list of HTML table rows.
167    for i in range(len(toco_conversion_log_before.op_list)):
168      # Append operator name column.
169      pre_op_profile += "<tr><td>" + toco_conversion_log_before.op_list[
170          i] + "</td>"
171      # Append input type column.
172      if i < len(toco_conversion_log_before.op_signatures):
173        pre_op_profile += "<td>" + get_input_type_from_signature(
174            toco_conversion_log_before.op_signatures[i]) + "</td></tr>"
175      else:
176        pre_op_profile += "<td></td></tr>"
177
178    # Generate post-conversion op profiles as a list of HTML table rows.
179    for op in toco_conversion_log_after.op_list:
180      supported_type = get_operator_type(op, toco_conversion_log_after)
181      post_op_profile += ("<tr><td>" + op + "</td><td>" + supported_type +
182                          "</td></tr>")
183
184    html_dict["<!--REPEAT_TABLE1_ROWS-->"] = pre_op_profile
185    html_dict["<!--REPEAT_TABLE2_ROWS-->"] = post_op_profile
186    html_dict["<!--DOT_BEFORE_CONVERT-->"] = dot_before
187    html_dict["<!--DOT_AFTER_CONVERT-->"] = dot_after
188    if toco_err_log:
189      html_dict["<!--TOCO_INFO_LOG-->"] = html_escape(toco_err_log)
190    else:
191      success_info = ("TFLite graph conversion successful. You can preview the "
192                      "converted model at: ") + tflite_graph_path
193      html_dict["<!--TOCO_INFO_LOG-->"] = html_escape(success_info)
194
195    # Replace each marker (as keys of html_dict) with the actual text (as values
196    # of html_dict) in the HTML template string.
197    template = self.html_template
198    for marker in html_dict:
199      template = template.replace(marker, html_dict[marker], 1)
200      # Check that the marker text is replaced.
201      if template.find(marker) != -1:
202        raise RuntimeError("Could not populate marker text %r" % marker)
203
204    with _file_io.FileIO(self.export_report_path, "w") as f:
205      f.write(template)
206
207
208def gen_conversion_log_html(conversion_log_dir, quantization_enabled,
209                            tflite_graph_path):
210  """Generates an HTML report about the conversion process.
211
212  Args:
213    conversion_log_dir: A string specifying the file directory of the conversion
214      logs. It's required that before calling this function, the
215      `conversion_log_dir`
216      already contains the following files: `toco_log_before.pb`,
217        `toco_log_after.pb`, `toco_tf_graph.dot`,
218        `toco_tflite_graph.dot`.
219    quantization_enabled: A boolean, passed from the tflite converter to
220      indicate whether post-training quantization is enabled during conversion.
221    tflite_graph_path: A string, the filepath to the converted TFLite model.
222
223  Raises:
224    IOError: When any of the required files doesn't exist.
225  """
226  template_filename = _resource_loader.get_path_to_datafile("template.html")
227  if not os.path.exists(template_filename):
228    raise IOError("Failed to generate HTML: file '{0}' doesn't exist.".format(
229        template_filename))
230
231  toco_log_before_path = os.path.join(conversion_log_dir, "toco_log_before.pb")
232  toco_log_after_path = os.path.join(conversion_log_dir, "toco_log_after.pb")
233  dot_before_path = os.path.join(conversion_log_dir, "toco_tf_graph.dot")
234  dot_after_path = os.path.join(conversion_log_dir, "toco_tflite_graph.dot")
235  if not os.path.exists(toco_log_before_path):
236    raise IOError("Failed to generate HTML: file '{0}' doesn't exist.".format(
237        toco_log_before_path))
238  if not os.path.exists(toco_log_after_path):
239    raise IOError("Failed to generate HTML: file '{0}' doesn't exist.".format(
240        toco_log_after_path))
241  if not os.path.exists(dot_before_path):
242    raise IOError("Failed to generate HTML: file '{0}' doesn't exist.".format(
243        dot_before_path))
244  if not os.path.exists(dot_after_path):
245    raise IOError("Failed to generate HTML: file '{0}' doesn't exist.".format(
246        dot_after_path))
247
248  html_generator = HTMLGenerator(
249      template_filename,
250      os.path.join(conversion_log_dir, "toco_conversion_summary.html"))
251
252  # Parse the generated `TocoConversionLog`.
253  toco_conversion_log_before = _toco_conversion_log_pb2.TocoConversionLog()
254  toco_conversion_log_after = _toco_conversion_log_pb2.TocoConversionLog()
255  with open(toco_log_before_path, "rb") as f:
256    toco_conversion_log_before.ParseFromString(f.read())
257  with open(toco_log_after_path, "rb") as f:
258    toco_conversion_log_after.ParseFromString(f.read())
259
260  # Read the dot file before/after the conversion.
261  with io.open(dot_before_path, "r", encoding="utf-8") as f:
262    dot_before = f.read().rstrip()
263  with io.open(dot_after_path, "r", encoding="utf-8") as f:
264    dot_after = f.read().rstrip()
265
266  html_generator.generate(toco_conversion_log_before, toco_conversion_log_after,
267                          quantization_enabled, dot_before, dot_after,
268                          toco_conversion_log_after.toco_err_logs,
269                          tflite_graph_path)
270