# Copyright 2019 The TensorFlow Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== """A utility class to generate the report HTML based on a common template.""" from __future__ import absolute_import from __future__ import division from __future__ import print_function import io import os from tensorflow.lite.toco.logging import toco_conversion_log_pb2 as _toco_conversion_log_pb2 from tensorflow.python.lib.io import file_io as _file_io from tensorflow.python.platform import resource_loader as _resource_loader html_escape_table = { "&": "&", '"': """, "'": "'", ">": ">", "<": "<", } def html_escape(text): return "".join(html_escape_table.get(c, c) for c in text) def get_input_type_from_signature(op_signature): """Parses op_signature and returns a string denoting the input tensor type. Args: op_signature: a string specifying the signature of a particular operator. The signature of an operator contains the input tensor's shape and type, output tensor's shape and type, operator's name and its version. It has the following schema: INPUT:input_1_shape::input_1_type::input_2_shape::input_2_type::.. ::OUTPUT:output_1_shape::output_1_type::output_2_shape::output_2_type:: ..::NAME:operator_name ::VERSION:operator_version An example of an operator signature is: INPUT:[1,73,73,160]::float::[64,1,1,160]::float::[64]::float:: OUTPUT:[1,73,73,64]::float::NAME:Conv::VERSION:1 Returns: A string denoting the input tensors' type. In the form of shape/type separated by comma. For example: shape:[1,73,73,160],type:float,shape:[64,1,1,160],type:float,shape:[64], type:float """ start = op_signature.find(":") end = op_signature.find("::OUTPUT") inputs = op_signature[start + 1:end] lst = inputs.split("::") out_str = "" for i in range(len(lst)): if i % 2 == 0: out_str += "shape:" else: out_str += "type:" out_str += lst[i] out_str += "," return out_str[:-1] def get_operator_type(op_name, conversion_log): if op_name in conversion_log.built_in_ops: return "BUILT-IN" elif op_name in conversion_log.custom_ops: return "CUSTOM OP" else: return "SELECT OP" class HTMLGenerator(object): """Utility class to generate an HTML report.""" def __init__(self, html_template_path, export_report_path): """Reads the HTML template content. Args: html_template_path: A string, path to the template HTML file. export_report_path: A string, path to the generated HTML report. This path should point to a '.html' file with date and time in its name. e.g. 2019-01-01-10:05.toco_report.html. Raises: IOError: File doesn't exist. """ # Load the template HTML. if not _file_io.file_exists(html_template_path): raise IOError("File '{0}' does not exist.".format(html_template_path)) with _file_io.FileIO(html_template_path, "r") as f: self.html_template = f.read() _file_io.recursive_create_dir(os.path.dirname(export_report_path)) self.export_report_path = export_report_path def generate(self, toco_conversion_log_before, toco_conversion_log_after, post_training_quant_enabled, dot_before, dot_after, toco_err_log="", tflite_graph_path=""): """Generates the HTML report and writes it to local directory. This function uses the fields in `toco_conversion_log_before` and `toco_conversion_log_after` to populate the HTML content. Certain markers (placeholders) in the HTML template are then substituted with the fields from the protos. Once finished it will write the HTML file to the specified local file path. Args: toco_conversion_log_before: A `TocoConversionLog` protobuf generated before the model is converted by TOCO. toco_conversion_log_after: A `TocoConversionLog` protobuf generated after the model is converted by TOCO. post_training_quant_enabled: A boolean, whether post-training quantization is enabled. dot_before: A string, the dot representation of the model before the conversion. dot_after: A string, the dot representation of the model after the conversion. toco_err_log: A string, the logs emitted by TOCO during conversion. Caller need to ensure that this string is properly anonymized (any kind of user data should be eliminated). tflite_graph_path: A string, the filepath to the converted TFLite model. Raises: RuntimeError: When error occurs while generating the template. """ html_dict = {} html_dict[""] = ( r'Fail' ) if toco_err_log else r'Success' html_dict[""] = str( toco_conversion_log_before.model_size) html_dict[""] = str( toco_conversion_log_after.model_size) html_dict[""] = str( sum(toco_conversion_log_after.built_in_ops.values())) html_dict[""] = str( sum(toco_conversion_log_after.select_ops.values())) html_dict[""] = str( sum(toco_conversion_log_after.custom_ops.values())) html_dict[""] = ( "is" if post_training_quant_enabled else "isn't") pre_op_profile = "" post_op_profile = "" # Generate pre-conversion op profiles as a list of HTML table rows. for i in range(len(toco_conversion_log_before.op_list)): # Append operator name column. pre_op_profile += "" + toco_conversion_log_before.op_list[ i] + "" # Append input type column. if i < len(toco_conversion_log_before.op_signatures): pre_op_profile += "" + get_input_type_from_signature( toco_conversion_log_before.op_signatures[i]) + "" else: pre_op_profile += "" # Generate post-conversion op profiles as a list of HTML table rows. for op in toco_conversion_log_after.op_list: supported_type = get_operator_type(op, toco_conversion_log_after) post_op_profile += ("" + op + "" + supported_type + "") html_dict[""] = pre_op_profile html_dict[""] = post_op_profile html_dict[""] = dot_before html_dict[""] = dot_after if toco_err_log: html_dict[""] = html_escape(toco_err_log) else: success_info = ("TFLite graph conversion successful. You can preview the " "converted model at: ") + tflite_graph_path html_dict[""] = html_escape(success_info) # Replace each marker (as keys of html_dict) with the actual text (as values # of html_dict) in the HTML template string. template = self.html_template for marker in html_dict: template = template.replace(marker, html_dict[marker], 1) # Check that the marker text is replaced. if template.find(marker) != -1: raise RuntimeError("Could not populate marker text %r" % marker) with _file_io.FileIO(self.export_report_path, "w") as f: f.write(template) def gen_conversion_log_html(conversion_log_dir, quantization_enabled, tflite_graph_path): """Generates an HTML report about the conversion process. Args: conversion_log_dir: A string specifying the file directory of the conversion logs. It's required that before calling this function, the `conversion_log_dir` already contains the following files: `toco_log_before.pb`, `toco_log_after.pb`, `toco_tf_graph.dot`, `toco_tflite_graph.dot`. quantization_enabled: A boolean, passed from the tflite converter to indicate whether post-training quantization is enabled during conversion. tflite_graph_path: A string, the filepath to the converted TFLite model. Raises: IOError: When any of the required files doesn't exist. """ template_filename = _resource_loader.get_path_to_datafile("template.html") if not os.path.exists(template_filename): raise IOError("Failed to generate HTML: file '{0}' doesn't exist.".format( template_filename)) toco_log_before_path = os.path.join(conversion_log_dir, "toco_log_before.pb") toco_log_after_path = os.path.join(conversion_log_dir, "toco_log_after.pb") dot_before_path = os.path.join(conversion_log_dir, "toco_tf_graph.dot") dot_after_path = os.path.join(conversion_log_dir, "toco_tflite_graph.dot") if not os.path.exists(toco_log_before_path): raise IOError("Failed to generate HTML: file '{0}' doesn't exist.".format( toco_log_before_path)) if not os.path.exists(toco_log_after_path): raise IOError("Failed to generate HTML: file '{0}' doesn't exist.".format( toco_log_after_path)) if not os.path.exists(dot_before_path): raise IOError("Failed to generate HTML: file '{0}' doesn't exist.".format( dot_before_path)) if not os.path.exists(dot_after_path): raise IOError("Failed to generate HTML: file '{0}' doesn't exist.".format( dot_after_path)) html_generator = HTMLGenerator( template_filename, os.path.join(conversion_log_dir, "toco_conversion_summary.html")) # Parse the generated `TocoConversionLog`. toco_conversion_log_before = _toco_conversion_log_pb2.TocoConversionLog() toco_conversion_log_after = _toco_conversion_log_pb2.TocoConversionLog() with open(toco_log_before_path, "rb") as f: toco_conversion_log_before.ParseFromString(f.read()) with open(toco_log_after_path, "rb") as f: toco_conversion_log_after.ParseFromString(f.read()) # Read the dot file before/after the conversion. with io.open(dot_before_path, "r", encoding="utf-8") as f: dot_before = f.read().rstrip() with io.open(dot_after_path, "r", encoding="utf-8") as f: dot_after = f.read().rstrip() html_generator.generate(toco_conversion_log_before, toco_conversion_log_after, quantization_enabled, dot_before, dot_after, toco_conversion_log_after.toco_err_logs, tflite_graph_path)