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 "<": "<", 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