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