/* Copyright 2016 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.
==============================================================================*/

#include "tensorflow/core/profiler/internal/tfprof_code.h"

#include <stdio.h>

#include <algorithm>
#include <iterator>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "tensorflow/core/lib/io/path.h"
#include "tensorflow/core/lib/io/zlib_compression_options.h"
#include "tensorflow/core/lib/io/zlib_outputbuffer.h"
#include "tensorflow/core/platform/regexp.h"
#include "tensorflow/core/profiler/internal/tfprof_constants.h"
#include "tensorflow/tsl/profiler/protobuf/profile.pb.h"

namespace tensorflow {
namespace tfprof {
namespace {

const char* const kGradientSuffix = " (gradient)";

// Convert to Trace proto into a short readable string.
std::string GetTraceString(const CallStack::Trace& trace) {
  std::string ntrace =
      absl::StrCat(io::Basename(trace.file()), ":", trace.lineno());
  if (trace.function().length() < 20) {
    absl::StrAppend(&ntrace, ":", trace.function());
  } else {
    absl::StrAppend(&ntrace, ":", trace.function().substr(0, 17), "...");
  }
  return ntrace;
}

bool IsGradNode(const string& name, string* forward_name) {
  // Given a forward operation with name op, its gradient op has the following
  // name: ...gradients/op_grad/...
  // TODO(xpan): This is hacky.
  auto grad_prefix = name.find("gradients/");
  auto grad_suffix = name.find("_grad/");
  if (grad_prefix == name.npos || grad_suffix == name.npos) {
    return false;
  }
  auto start = grad_prefix + string("gradients/").length();
  auto len = grad_suffix - start;
  if (len <= 0) {
    return false;
  }
  *forward_name = name.substr(start, len);
  return true;
}

// StringTable maps each string to an id.
class StringTable {
 public:
  StringTable() {
    // Pprof requires first entry in string_table to be ''.
    string_id_[""] = 0;
    all_strings_.push_back("");
  }

  // Returns the index of a string. If not found, inserts the string and
  // return the inserted index.
  uint64 GetIndex(const string& str) {
    auto idx = string_id_.find(str);
    if (idx != string_id_.end()) {
      return idx->second;
    }
    all_strings_.push_back(str);
    return string_id_.insert(std::pair<string, int64_t>(str, string_id_.size()))
        .first->second;
  }

  const std::vector<string>& strings() const { return all_strings_; }

 private:
  std::map<string, uint64> string_id_;
  std::vector<string> all_strings_;
};

// FunctionTable maps each function to an id.
class FunctionTable {
 public:
  explicit FunctionTable(StringTable* string_table)
      : string_table_(string_table) {}

  // Returns the index of a function. If not found, adds a function proto
  // and returns the function index.
  uint64 GetIndex(const string& file_path, const string& func_name,
                  uint64 func_start_line) {
    auto key = std::tuple<string, string, uint64>(file_path, func_name,
                                                  func_start_line);
    auto idx = function_table_.find(key);
    if (idx != function_table_.end()) {
      return idx->second.id();
    }
    pprof::Function* func_pb = &function_table_[key];
    // function index should start from 1.
    func_pb->set_id(function_table_.size());

    string file_base(io::Basename(file_path));
    file_base = file_base.substr(0, file_base.find_last_of('.'));
    func_pb->set_name(
        string_table_->GetIndex(absl::StrCat(file_base, ":", func_name)));
    func_pb->set_filename(string_table_->GetIndex(file_path));
    func_pb->set_start_line(func_start_line);
    return func_pb->id();
  }

  const std::map<std::tuple<string, string, uint64>, pprof::Function>&
  functions() const {
    return function_table_;
  }

 private:
  StringTable* string_table_;
  std::map<std::tuple<string, string, uint64>, pprof::Function> function_table_;
};

// LocationTable maps each function call to an id.
class LocationTable {
 public:
  explicit LocationTable(FunctionTable* function_table)
      : function_table_(function_table) {}

  // Returns the index of a function call location. If not found, adds a
  // location proto and returns the location index.
  uint64 GetIndex(const string& file_path, uint64 line_number,
                  const string& called_function_name,
                  const string& called_file_path,
                  uint64 called_func_start_line) {
    auto key = std::tuple<string, string, uint64>(
        file_path, called_function_name, line_number);

    auto idx = location_table_.find(key);
    if (idx != location_table_.end()) {
      return idx->second.id();
    }
    pprof::Location* location_pb = &location_table_[key];
    location_pb->set_id(location_table_.size());
    pprof::Line* line_pb = location_pb->add_line();
    line_pb->set_function_id(function_table_->GetIndex(
        called_file_path, called_function_name, called_func_start_line));
    line_pb->set_line(line_number);
    return location_pb->id();
  }

  const std::map<std::tuple<string, string, uint64>, pprof::Location>&
  locations() const {
    return location_table_;
  }

 private:
  FunctionTable* function_table_;
  std::map<std::tuple<string, string, uint64>, pprof::Location> location_table_;
};

// Samples stores samples of all calls. A sample is a single call trace,
// that is, the call path from top caller to the leaf callee.
class Samples {
 public:
  explicit Samples(StringTable* string_table, const Options* opts)
      : string_table_(string_table), opts_(opts) {}

  // 'node' is the leaf of the displayed trace. It includes all graph nodes
  // created by it. 'location_ids' contains
  // the call stack, from callee to caller.
  // This method adds the statistics of graph nodes created by the python
  // call.
  void Add(const CodeNode* node, const std::vector<uint64>& location_ids) {
    // displayed leaf might not be true leaf. Retrieve the true leaves for
    // stats.
    std::vector<const CodeNode*> all_leaf = FetchAllLeaf(node);
    CHECK(!all_leaf.empty()) << node->name();

    for (const CodeNode* cn : all_leaf) {
      for (const auto& gn_it : cn->node->graph_nodes()) {
        const TFGraphNode* gn = gn_it.second;
        string name = gn->name();
        // Generate a new trace name, in case the name is taken.
        while (sample_table_.find(name) != sample_table_.end()) {
          name += '@';
        }
        pprof::Sample* sample_pb = &sample_table_[name];
        for (uint64 id : location_ids) {
          sample_pb->mutable_location_id()->Add(id);
        }
        pprof::Label* label_pb = sample_pb->mutable_label()->Add();
        label_pb->set_key(string_table_->GetIndex("graph node:"));
        label_pb->set_str(string_table_->GetIndex(gn->name()));

        sample_pb->mutable_value()->Add(1);
        string type = *opts_->select.begin();
        if (type == kShown[1]) {
          sample_pb->mutable_value()->Add(gn->exec_micros(node->node->step()));
        } else if (type == kShown[9]) {
          sample_pb->mutable_value()->Add(
              gn->accelerator_exec_micros(node->node->step()));
        } else if (type == kShown[10]) {
          sample_pb->mutable_value()->Add(
              gn->cpu_exec_micros(node->node->step()));
        } else if (type == kShown[0]) {
          sample_pb->mutable_value()->Add(
              gn->requested_bytes(node->node->step()));
        } else if (type == kShown[11]) {
          sample_pb->mutable_value()->Add(gn->peak_bytes(node->node->step()));
        } else if (type == kShown[12]) {
          sample_pb->mutable_value()->Add(
              gn->residual_bytes(node->node->step()));
        } else if (type == kShown[13]) {
          sample_pb->mutable_value()->Add(gn->output_bytes(node->node->step()));
        } else if (type == kShown[2]) {
          sample_pb->mutable_value()->Add(gn->parameters());
        } else if (type == kShown[3]) {
          sample_pb->mutable_value()->Add(gn->float_ops(node->node->step()));
        } else {
          absl::FPrintF(stderr, "pprof doesn't support -select=%s\n", type);
        }
      }
    }
  }

  const std::map<string, pprof::Sample>& samples() const {
    return sample_table_;
  }

 private:
  std::vector<const CodeNode*> FetchAllLeaf(const CodeNode* root) {
    if (root->children.empty()) {
      return {root};
    }
    std::vector<const CodeNode*> ret;
    for (auto& n : root->children) {
      std::vector<const CodeNode*> nodes = FetchAllLeaf(n);
      ret.insert(ret.end(), nodes.begin(), nodes.end());
    }
    return ret;
  }

  StringTable* string_table_;
  const Options* opts_;
  std::map<string, pprof::Sample> sample_table_;
};

class PprofProfileImpl : public PprofProfile {
 public:
  explicit PprofProfileImpl(const Options* opts)
      : opts_(opts),
        func_table_(new FunctionTable(&string_table_)),
        loc_table_(new LocationTable(func_table_.get())),
        samples_(new Samples(&string_table_, opts)) {}

  uint64 AddLocation(const CodeNode* callee, const CodeNode* caller) override {
    const string& file_path = caller->file();
    uint64 lineno = caller->lineno();
    const string& callee_file_path = callee->file();
    const string& callee_function = callee->function();
    uint64 callee_func_start_line = callee->func_start_line();

    return loc_table_->GetIndex(file_path, lineno, callee_function,
                                callee_file_path, callee_func_start_line);
  }

  void AddSample(const CodeNode* leaf, std::vector<uint64>* call_ids) override {
    std::vector<uint64> reversed_call_ids;
    std::reverse_copy(call_ids->begin(), call_ids->end(),
                      std::back_inserter(reversed_call_ids));
    samples_->Add(leaf, reversed_call_ids);
  }

  Status WritePprofProfile(const string& filename) override {
    pprof::Profile profile_pb;
    Build(&profile_pb);

    std::unique_ptr<WritableFile> file;
    Status s = Env::Default()->NewWritableFile(filename, &file);
    if (!s.ok()) return s;

    int32_t buf_size = 1024 * 1024;
    io::ZlibOutputBuffer* zlib_output_buffer = new io::ZlibOutputBuffer(
        file.get(), buf_size, buf_size, io::ZlibCompressionOptions::GZIP());
    s = zlib_output_buffer->Init();
    if (!s.ok()) {
      delete zlib_output_buffer;
      return s;
    }
    s = zlib_output_buffer->Append(profile_pb.SerializeAsString());
    if (!s.ok()) {
      delete zlib_output_buffer;
      return s;
    }
    s = zlib_output_buffer->Close();
    if (!s.ok()) {
      delete zlib_output_buffer;
      return s;
    }
    absl::FPrintF(stdout,
                  "\nRun pprof -png --nodecount=100 --sample_index=1 <%s>\n",
                  filename);
    delete zlib_output_buffer;
    return s;
  }

 private:
  void Build(pprof::Profile* profile_pb) {
    string sample_type_description = "count";
    auto sample_type = profile_pb->mutable_sample_type()->Add();
    sample_type->set_type(string_table_.GetIndex(sample_type_description));
    sample_type->set_unit(string_table_.GetIndex("count"));

    string type = *opts_->select.begin();
    sample_type_description = type;
    sample_type = profile_pb->mutable_sample_type()->Add();
    sample_type->set_type(string_table_.GetIndex(sample_type_description));
    if (type == kShown[1] || type == kShown[9] || type == kShown[10]) {
      sample_type->set_unit(string_table_.GetIndex("microseconds"));
      if (type == kShown[1]) {
        profile_pb->mutable_comment()->Add(string_table_.GetIndex(
            "Sum of accelerator execution time and cpu execution time."));
      } else if (type == kShown[9]) {
        profile_pb->mutable_comment()->Add(
            string_table_.GetIndex("Accelerator execution time."));
      } else if (type == kShown[10]) {
        profile_pb->mutable_comment()->Add(
            string_table_.GetIndex("CPU execution time."));
      }
    } else if (type == kShown[0]) {
      sample_type->set_unit(string_table_.GetIndex("bytes"));
      profile_pb->mutable_comment()->Add(
          string_table_.GetIndex("Sum of operation total memory requests, "
                                 "excluding deallocations."));
    } else if (type == kShown[11]) {
      sample_type->set_unit(string_table_.GetIndex("bytes"));
      profile_pb->mutable_comment()->Add(
          string_table_.GetIndex("Sum of operation peak memory usage."));
    } else if (type == kShown[12]) {
      sample_type->set_unit(string_table_.GetIndex("bytes"));
      profile_pb->mutable_comment()->Add(string_table_.GetIndex(
          "Sum of operation allocated memory after finish."));
    } else if (type == kShown[13]) {
      sample_type->set_unit(string_table_.GetIndex("bytes"));
      profile_pb->mutable_comment()->Add(
          string_table_.GetIndex("Sum of operation output size."));
    } else if (type == kShown[2]) {
      sample_type->set_unit(string_table_.GetIndex("count"));
      profile_pb->mutable_comment()->Add(
          string_table_.GetIndex("Model parameters."));
    } else if (type == kShown[3]) {
      sample_type->set_unit(string_table_.GetIndex("count"));
      profile_pb->mutable_comment()->Add(string_table_.GetIndex(
          "Model float operations (Only available if defined)."));
    } else {
      absl::FPrintF(stderr, "pprof doesn't support selecting: %s\n", type);
    }

    for (const string& str : string_table_.strings()) {
      *profile_pb->mutable_string_table()->Add() = str;
    }
    for (const auto& sample_it : samples_->samples()) {
      // TODO(xpan): Consider swap.
      profile_pb->mutable_sample()->Add()->MergeFrom(sample_it.second);
    }
    for (const auto& function_it : func_table_->functions()) {
      profile_pb->mutable_function()->Add()->MergeFrom(function_it.second);
    }
    for (const auto& location_it : loc_table_->locations()) {
      profile_pb->mutable_location()->Add()->MergeFrom(location_it.second);
    }
  }

  const Options* opts_;
  StringTable string_table_;
  std::unique_ptr<FunctionTable> func_table_;
  std::unique_ptr<LocationTable> loc_table_;
  std::unique_ptr<Samples> samples_;
};
}  // namespace

void TFCode::AddNode(TFGraphNode* node) {
  if (!node->call_stack() || node->call_stack()->traces().empty()) {
    return;
  }
  // We infer the forward operation name from gradient op name. So, we can
  // map gradient op traces to forward op traces.
  // E.g. gradient node of 'inp_1/Conv2D' would be 'gradients/inp_1/Conv2D_grad.
  string forward_name;
  if (IsGradNode(node->name(), &forward_name)) {
    auto grad_nodes_it = grad_nodes_.find(forward_name);
    if (grad_nodes_it != grad_nodes_.end()) {
      grad_nodes_it->second.push_back(node);
    } else {
      grad_nodes_.insert(
          std::pair<string, std::vector<TFGraphNode*>>(forward_name, {node}));
    }
    return;
  } else {
    forward_nodes_[node->name()] = node;
  }

  if (!root_) {
    graph_root_ = std::make_unique<TFMultiGraphNode>(kTFProfRoot);
    root_ = std::make_unique<CodeNode>(graph_root_.get(), nullptr, "");
  }

  CodeNode* pre_code_node = root_.get();
  // TODO(xpan): Consider to release CodeDef after TFCode is built. It
  // takes a lot of memory.
  std::set<string> traces;
  for (int i = 0, end = node->call_stack()->traces().size(); i < end; ++i) {
    // Unlike op name, which is globally unique, trace name is only unique
    // w.r.t. it's parent.
    const string& trace = GetTraceString(node->call_stack()->traces().at(i));
    traces.insert(trace);
    pre_code_node = pre_code_node->AddChildren(
        trace, &node->call_stack()->traces().at(i), "");
    const int64_t last_index = node->call_stack()->traces().size() - 1;
    if (i == last_index) {
      pre_code_node->node->AddGraphNode(node);
    }
  }
}

void TFCode::Build() {
  int64_t unaccounted_nodes = 0;
  for (const auto& it : grad_nodes_) {
    const string& forward_name = it.first;
    auto forward_it = forward_nodes_.find(forward_name);
    if (forward_it == forward_nodes_.end()) {
      unaccounted_nodes += 1;
      continue;
    }
    TFGraphNode* fn = forward_it->second;
    CodeNode* leaf = nullptr;
    CodeNode* pre_code_node = root_.get();
    for (int i = 0, end = fn->call_stack()->traces().size(); i < end; ++i) {
      const string& trace =
          GetTraceString(fn->call_stack()->traces().at(i)) + kGradientSuffix;
      pre_code_node = pre_code_node->AddChildren(
          trace, &fn->call_stack()->traces().at(i), kGradientSuffix);
      const int64_t last_trace = fn->call_stack()->traces().size() - 1;
      if (i == last_trace) {
        leaf = pre_code_node;
      }
    }
    for (TFGraphNode* gn : it.second) {
      leaf->node->AddGraphNode(gn);
    }
  }
  if (unaccounted_nodes > 0) {
    absl::FPrintF(stderr, "%d gradient nodes not accounted\n",
                  unaccounted_nodes);
  }
}

const ShowMultiNode* TFCode::ShowInternal(const Options& opts,
                                          Timeline* timeline) {
  root_->ResetTotalStats();
  if (opts.output_type == kOutput[3]) {
    if (opts.select.size() != 1) {
      absl::FPrintF(stderr, "Can only select 1 attribute for pprof output.\n");
      return root_.get();
    }
    string select = *opts.select.begin();
    if (select != kShown[0] && select != kShown[1] && select != kShown[2] &&
        select != kShown[3] && select != kShown[9] && select != kShown[10] &&
        select != kShown[11] && select != kShown[12] && select != kShown[13]) {
      absl::FPrintF(stderr, "pprof doesn't support -select=%s\n", select);
      return root_.get();
    }
  }
  if (opts.account_displayed_op_only) {
    absl::FPrintF(stderr,
                  "Note: code view ignores account_displayed_op_only\n");
  }

  std::vector<CodeNode*> roots = Account(root_->children, opts);
  root_->show_children.clear();
  for (CodeNode* n : roots) {
    root_->AggregateTotalStats(n);
  }

  if (opts.start_name_regexes.size() != 1 ||
      opts.start_name_regexes[0] != ".*") {
    roots = SearchRoot(roots, opts.start_name_regexes);
  }

  root_->show_children.assign(roots.begin(), roots.end());

  CodeNode* root = PrintScope({root_.get()}, opts, 1, 0)[0];

  root->formatted_str = FormatLegend(opts) + root->formatted_str;

  if (opts.output_type == kOutput[3]) {
    std::vector<uint64> call_ids;
    pprof_profile_ = std::make_unique<PprofProfileImpl>(&opts);
    Format(root, root->show_children, opts, &root->formatted_str,
           root->mutable_proto(), &call_ids);
    Status s = pprof_profile_->WritePprofProfile(
        opts.output_options.at(kPprofOpts[0]));
    if (!s.ok()) {
      absl::FPrintF(stderr, "%s\n", s.ToString());
    }
  } else {
    Format(root, root->show_children, opts, &root->formatted_str,
           root->mutable_proto(), nullptr);
    if (timeline) {
      timeline->GenerateCodeTimeline(root);
    }
  }
  return root;
}

void TFCode::Format(const CodeNode* root, const std::vector<CodeNode*>& nodes,
                    const Options& opts, string* display_str,
                    MultiGraphNodeProto* proto, std::vector<uint64>* call_ids) {
  if (nodes.empty() && root->has_trace() && opts.output_type == kOutput[3]) {
    pprof_profile_->AddSample(root, call_ids);
  }

  for (CodeNode* node : nodes) {
    if (root->has_trace() && opts.output_type == kOutput[3]) {
      uint64 loc_id = pprof_profile_->AddLocation(node, root);
      call_ids->push_back(loc_id);
    }
    display_str->append(node->formatted_str);
    MultiGraphNodeProto* child = proto->add_children();
    child->MergeFrom(node->proto());
    Format(node, node->show_children, opts, display_str, child, call_ids);
    if (root->has_trace() && opts.output_type == kOutput[3]) {
      call_ids->pop_back();
    }
  }
}

std::vector<CodeNode*> TFCode::SearchRoot(std::vector<CodeNode*> roots,
                                          const std::vector<string>& regexes) {
  std::vector<CodeNode*> res;
  if (roots.empty()) {
    return res;
  }
  for (CodeNode* root : roots) {
    bool match_start_node = false;
    for (const string& regex : regexes) {
      if (RE2::FullMatch(root->name(), regex)) {
        res.push_back(root);
        match_start_node = true;
        break;
      }
    }
    if (match_start_node) {
      // Found a start node at this branch, no need to continue.
      continue;
    }
    std::vector<CodeNode*> nroots = SearchRoot(root->show_children, regexes);
    res.insert(res.end(), nroots.begin(), nroots.end());
  }
  return res;
}

std::vector<CodeNode*> TFCode::PrintScope(const std::vector<CodeNode*> roots,
                                          const Options& opts, int depth,
                                          int last_ident) {
  std::vector<CodeNode*> show_nodes;

  for (CodeNode* node : roots) {
    if (ShouldTrim(node, opts.trim_name_regexes) || depth > opts.max_depth) {
      continue;
    }
    int ident = last_ident;
    bool show = ShouldShow(node, opts, depth);
    if (show) ident += 2;

    std::vector<CodeNode*> show_cnodes =
        PrintScope(node->show_children, opts, depth + 1, ident);
    if (show) {
      node->show_children.clear();

      show_cnodes = SortNodes(show_cnodes, opts);
      for (CodeNode* sc : show_cnodes) {
        node->show_children.push_back(sc);
      }

      node->formatted_str = FormatNode(node, opts, last_ident);

      if (opts.select.find(kShown[4]) != opts.select.end()) {
        absl::FPrintF(stderr, "code view has no tensor value to show\n");
      }
      show_nodes.push_back(node);
    } else {
      show_nodes.insert(show_nodes.end(), show_cnodes.begin(),
                        show_cnodes.end());
    }
  }
  return show_nodes;
}

std::vector<CodeNode*> TFCode::Account(const std::vector<CodeNode*>& roots,
                                       const Options& opts) {
  std::vector<CodeNode*> act_nodes;

  for (CodeNode* node : roots) {
    node->ResetTotalStats();
    std::vector<CodeNode*> act_cnodes = Account(node->children, opts);
    node->account = ReAccount(node, opts);
    if (node->account || !act_cnodes.empty()) {
      node->show_children.clear();
      node->ResetTotalStats();
      node->AddSelfToTotalStats();
      for (CodeNode* c : act_cnodes) {
        node->AggregateTotalStats(c);
        node->show_children.push_back(c);
      }
      act_nodes.push_back(node);
    }
  }
  return act_nodes;
}

string TFCode::FormatNodeMemory(CodeNode* node, int64_t bytes,
                                int64_t total_bytes) const {
  string memory = FormatMemory(total_bytes);
  if (node->account) {
    memory = FormatMemory(bytes) + "/" + memory;
  } else {
    memory = "--/" + memory;
  }
  return memory;
}

string TFCode::FormatNode(CodeNode* node, const Options& opts,
                          int64_t indent) const {
  std::vector<string> attrs;
  if (opts.select.find(kShown[0]) != opts.select.end()) {
    attrs.push_back(FormatNodeMemory(node, node->proto().requested_bytes(),
                                     node->proto().total_requested_bytes()));
  }
  if (opts.select.find(kShown[11]) != opts.select.end()) {
    attrs.push_back(FormatNodeMemory(node, node->proto().peak_bytes(),
                                     node->proto().total_peak_bytes()));
  }
  if (opts.select.find(kShown[12]) != opts.select.end()) {
    attrs.push_back(FormatNodeMemory(node, node->proto().residual_bytes(),
                                     node->proto().total_residual_bytes()));
  }
  if (opts.select.find(kShown[13]) != opts.select.end()) {
    attrs.push_back(FormatNodeMemory(node, node->proto().output_bytes(),
                                     node->proto().total_output_bytes()));
  }

  std::vector<string> time_attrs = FormatTimes(node, opts);
  attrs.insert(attrs.end(), time_attrs.begin(), time_attrs.end());

  if (opts.select.find(kShown[2]) != opts.select.end()) {
    string params = FormatNumber(node->proto().total_parameters()) + " params";
    if (node->account) {
      params = FormatNumber(node->proto().parameters()) + "/" + params;
    } else {
      params = "--/" + params;
    }
    attrs.push_back(params);
  }

  if (opts.select.find(kShown[3]) != opts.select.end()) {
    string fops = FormatNumber(node->proto().total_float_ops()) + " flops";
    if (node->account) {
      fops = FormatNumber(node->proto().float_ops()) + "/" + fops;
    } else {
      fops = "--/" + fops;
    }
    attrs.push_back(fops);
  }

  if (opts.select.find(kShown[5]) != opts.select.end() &&
      !node->node->devices().empty()) {
    attrs.push_back(absl::StrJoin(node->node->devices(), "|"));
  }
  if (opts.select.find(kShown[6]) != opts.select.end()) {
    std::set<string> op_types = node->node->op_types();
    attrs.push_back(absl::StrJoin(op_types, "|"));
  }
  if (opts.select.find(kShown[7]) != opts.select.end()) {
    // TODO(xpan): Make op count available in code view?
    attrs.push_back(absl::StrFormat("%s N/A in code view", kShown[7]));
  }
  if (opts.select.find(kShown[8]) != opts.select.end()) {
    attrs.push_back(absl::StrFormat("%s N/A in code view", kShown[8]));
  }

  return absl::StrFormat("%s%s (%s)\n", std::string(indent, ' '), node->name(),
                         absl::StrJoin(attrs, ", "));
}
}  // namespace tfprof
}  // namespace tensorflow
