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