Improve flatbuffer reflection performance

Change-Id: I86b4ce21e8fe4ac2913dcd2c9e3c466c3f66a227
diff --git a/aos/BUILD b/aos/BUILD
index d05a086..294b218 100644
--- a/aos/BUILD
+++ b/aos/BUILD
@@ -381,6 +381,7 @@
     hdrs = ["json_to_flatbuffer.h"],
     visibility = ["//visibility:public"],
     deps = [
+        ":fast_string_builder",
         ":flatbuffer_utils",
         ":flatbuffers",
         ":json_tokenizer",
@@ -522,3 +523,18 @@
         "@com_github_google_glog//:glog",
     ],
 )
+
+cc_library(
+    name = "fast_string_builder",
+    srcs = [
+        "fast_string_builder.cc",
+    ],
+    hdrs = [
+        "fast_string_builder.h",
+    ],
+    deps = [
+        "@com_github_google_glog//:glog",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/strings:str_format",
+    ],
+)
diff --git a/aos/aos_dump.cc b/aos/aos_dump.cc
index 55ca57e..fe6cbd5 100644
--- a/aos/aos_dump.cc
+++ b/aos/aos_dump.cc
@@ -15,29 +15,26 @@
 
 namespace {
 
-void PrintMessage(const aos::Channel *channel, const aos::Context &context) {
+void PrintMessage(const aos::Channel *channel, const aos::Context &context,
+                  aos::FastStringBuilder *builder) {
   // Print the flatbuffer out to stdout, both to remove the
   // unnecessary cruft from glog and to allow the user to readily
   // redirect just the logged output independent of any debugging
   // information on stderr.
+
+  builder->Reset();
+  aos::FlatbufferToJson(builder, channel->schema(),
+                        static_cast<const uint8_t *>(context.data),
+                        {false, static_cast<size_t>(FLAGS_max_vector_size)});
+
   if (context.monotonic_remote_time != context.monotonic_event_time) {
     std::cout << context.realtime_remote_time << " ("
               << context.monotonic_remote_time << ") delivered "
               << context.realtime_event_time << " ("
-              << context.monotonic_event_time << "): "
-              << aos::FlatbufferToJson(
-                     channel->schema(),
-                     static_cast<const uint8_t *>(context.data),
-                     {false, static_cast<size_t>(FLAGS_max_vector_size)})
-              << '\n';
+              << context.monotonic_event_time << "): " << *builder << '\n';
   } else {
     std::cout << context.realtime_event_time << " ("
-              << context.monotonic_event_time << "): "
-              << aos::FlatbufferToJson(
-                     channel->schema(),
-                     static_cast<const uint8_t *>(context.data),
-                     {false, static_cast<size_t>(FLAGS_max_vector_size)})
-              << '\n';
+              << context.monotonic_event_time << "): " << *builder << '\n';
   }
 }
 
@@ -99,19 +96,22 @@
     LOG(FATAL) << "Multiple channels found with same type";
   }
 
+  aos::FastStringBuilder str_builder;
+
   for (const aos::Channel *channel : found_channels) {
     if (FLAGS_fetch) {
       const std::unique_ptr<aos::RawFetcher> fetcher =
           event_loop.MakeRawFetcher(channel);
       if (fetcher->Fetch()) {
-        PrintMessage(channel, fetcher->context());
+        PrintMessage(channel, fetcher->context(), &str_builder);
       }
     }
 
-    event_loop.MakeRawWatcher(channel, [channel](const aos::Context &context,
-                                                 const void * /*message*/) {
-      PrintMessage(channel, context);
-    });
+    event_loop.MakeRawWatcher(
+        channel, [channel, &str_builder](const aos::Context &context,
+                                         const void * /*message*/) {
+          PrintMessage(channel, context, &str_builder);
+        });
   }
 
   event_loop.Run();
diff --git a/aos/fast_string_builder.cc b/aos/fast_string_builder.cc
new file mode 100644
index 0000000..97d2fea
--- /dev/null
+++ b/aos/fast_string_builder.cc
@@ -0,0 +1,67 @@
+#include "fast_string_builder.h"
+
+namespace aos {
+
+FastStringBuilder::FastStringBuilder(std::size_t initial_size) {
+  str_.reserve(initial_size);
+}
+
+void FastStringBuilder::Reset() { str_.resize(0); }
+
+std::string_view FastStringBuilder::Result() const {
+  return std::string_view(str_);
+}
+
+std::string FastStringBuilder::MoveResult() {
+  std::string result;
+  result.swap(str_);
+  return result;
+}
+
+void FastStringBuilder::Resize(std::size_t chars_needed) {
+  str_.resize(str_.size() + chars_needed);
+}
+
+void FastStringBuilder::Append(std::string_view str) {
+  std::size_t index = str_.size();
+  Resize(str.size());
+  str_.replace(index, str.size(), str, 0);
+}
+
+void FastStringBuilder::Append(float val) {
+  std::size_t index = str_.size();
+  Resize(17);
+  int result = absl::SNPrintF(str_.data() + index, 17, "%.6g", val);
+  PCHECK(result != -1);
+  CHECK(result < 17);
+  str_.resize(index + result);
+}
+
+void FastStringBuilder::Append(double val) {
+  std::size_t index = str_.size();
+  Resize(25);
+  int result = absl::SNPrintF(str_.data() + index, 25, "%.15g", val);
+  PCHECK(result != -1);
+  CHECK(result < 25);
+  str_.resize(index + result);
+}
+
+void FastStringBuilder::AppendChar(char val) {
+  Resize(1);
+  str_[str_.size() - 1] = val;
+}
+
+void FastStringBuilder::AppendBool(bool val) {
+  if (bool_to_str_) {
+    Append(val ? kTrue : kFalse);
+  } else {
+    AppendChar(val ? '1' : '0');
+  }
+}
+
+std::ostream &operator<<(std::ostream &os, const FastStringBuilder &builder) {
+  os.write(builder.str_.data(), builder.str_.size());
+  return os;
+}
+
+}  // namespace aos
diff --git a/aos/fast_string_builder.h b/aos/fast_string_builder.h
new file mode 100644
index 0000000..9e111ff
--- /dev/null
+++ b/aos/fast_string_builder.h
@@ -0,0 +1,82 @@
+#ifndef AOS_FAST_STRING_BUILDER_H_
+#define AOS_FAST_STRING_BUILDER_H_
+
+#include <ostream>
+#include <string>
+#include <type_traits>
+
+#include "absl/strings/numbers.h"
+#include "absl/strings/str_format.h"
+#include "glog/logging.h"
+
+namespace aos {
+
+// Simplified string builder which is faster than standard classes
+// (std::string::append(), std::ostringstream). Reduces copies on insertion
+// and retrieval, uses fast number to string conversions, and allows reuse
+// of the internal buffer.
+class FastStringBuilder {
+ public:
+  FastStringBuilder(std::size_t initial_size = 64);
+
+  // Convert bools from integer representation to "true" and "false".
+  // Defaults on.
+  void set_bool_to_str(bool bool_to_str) { bool_to_str_ = bool_to_str; }
+
+  // Clears result, allows for reuse of builder without reallocation.
+  // Invalidates Result()
+  void Reset();
+
+  // Returns a temporary view to the string result. Invalidated on destruction
+  // of builder, after calling Reset(), or after calling MoveResult().
+  std::string_view Result() const;
+
+  // Release the string held by the builder. Resets builder and invalidates
+  // Result().
+  std::string MoveResult();
+
+  // Append integer to result, converted to string representation.
+  template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
+  void AppendInt(T val);
+
+  void Append(std::string_view);
+
+  void Append(const char *c) { Append(std::string_view(c)); }
+
+  // Append character to result as character.
+  void AppendChar(char);
+
+  // Append float or double to result, converted to string representation
+  void Append(float);
+
+  void Append(double);
+
+  // Append bool to result. set_bool_to_str() toggles between integer and string
+  // representation.
+  void AppendBool(bool);
+
+  // Prints the current result to output
+  friend std::ostream &operator<<(std::ostream &os,
+                                  const FastStringBuilder &builder);
+
+ private:
+  static const inline std::string kTrue = "true", kFalse = "false";
+
+  // Allocate additional space of at least chars_needed relative to index_.
+  void Resize(std::size_t chars_needed);
+
+  std::string str_;
+  bool bool_to_str_ = true;
+};
+
+template <typename T, typename>
+void FastStringBuilder::AppendInt(T val) {
+  std::size_t index = str_.size();
+  Resize(absl::numbers_internal::kFastToBufferSize);
+  char *end = absl::numbers_internal::FastIntToBuffer(val, str_.data() + index);
+  str_.resize(end - str_.data());
+}
+
+}  // namespace aos
+
+#endif  // AOS_FAST_STRING_BUIDLER_H_
diff --git a/aos/flatbuffer_introspection.cc b/aos/flatbuffer_introspection.cc
index 48200b7..0fc37a7 100644
--- a/aos/flatbuffer_introspection.cc
+++ b/aos/flatbuffer_introspection.cc
@@ -11,57 +11,55 @@
 using reflection::BaseType;
 
 void IntToString(int64_t val, reflection::BaseType type,
-                 std::stringstream *out) {
+                 FastStringBuilder *out) {
   switch (type) {
     case BaseType::Bool:
-      *out << (val ? "true" : "false");
+      out->AppendBool(static_cast<bool>(val));
       break;
     case BaseType::UByte:
-      *out << std::to_string(static_cast<uint8_t>(val));
+      out->AppendInt(static_cast<uint8_t>(val));
       break;
     case BaseType::Byte:
-      *out << std::to_string(static_cast<int8_t>(val));
+      out->AppendInt(static_cast<int8_t>(val));
       break;
     case BaseType::Short:
-      *out << static_cast<int16_t>(val);
+      out->AppendInt(static_cast<int16_t>(val));
       break;
     case BaseType::UShort:
-      *out << static_cast<uint16_t>(val);
+      out->AppendInt(static_cast<uint16_t>(val));
       break;
     case BaseType::Int:
-      *out << static_cast<int32_t>(val);
+      out->AppendInt(static_cast<int32_t>(val));
       break;
     case BaseType::UInt:
-      *out << static_cast<uint32_t>(val);
+      out->AppendInt(static_cast<uint32_t>(val));
       break;
     case BaseType::Long:
-      *out << static_cast<int64_t>(val);
+      out->AppendInt(static_cast<int64_t>(val));
       break;
     case BaseType::ULong:
-      *out << static_cast<uint64_t>(val);
+      out->AppendInt(static_cast<uint64_t>(val));
       break;
     default:
-      *out << "null";
+      out->Append("null");
   }
 }
 
 void FloatToString(double val, reflection::BaseType type,
-                   std::stringstream *out) {
+                   FastStringBuilder *out) {
   if (std::isnan(val)) {
-    *out << "null";
+    out->Append("null");
     return;
   }
   switch (type) {
     case BaseType::Float:
-      out->precision(std::numeric_limits<float>::digits10);
-      *out << static_cast<float>(val);
+      out->Append(static_cast<float>(val));
       break;
     case BaseType::Double:
-      out->precision(std::numeric_limits<double>::digits10);
-      *out << val;
+      out->Append(val);
       break;
     default:
-      *out << "null";
+      out->Append("null");
   }
 }
 
@@ -70,39 +68,41 @@
     const reflection::Object *obj,
     const flatbuffers::Vector<flatbuffers::Offset<reflection::Object>> *objects,
     const flatbuffers::Vector<flatbuffers::Offset<reflection::Enum>> *enums,
-    const ObjT *object, std::stringstream *out, JsonOptions json_options,
+    const ObjT *object, FastStringBuilder *out, JsonOptions json_options,
     int tree_depth = 0);
 
 // Get enum value name
-const char *EnumToString(
+std::string_view EnumToString(
     int64_t enum_value,
     const flatbuffers::Vector<flatbuffers::Offset<reflection::EnumVal>>
         *values) {
-  // Replace with binary search? Enum values are pre-sorted.
-  for (auto iter = values->begin(); iter != values->end(); iter++) {
-    if (enum_value == iter->value()) {
-      return iter->name()->c_str();
-    }
-  }
-  return nullptr;
+  auto result = std::find_if(values->begin(), values->end(),
+                             [enum_value](const reflection::EnumVal *a) {
+                               return a->value() == enum_value;
+                             });
+  return result != values->end() ? result->name()->string_view()
+                                 : std::string_view();
 }
 
 // Convert integer to string, checking if it is an enum.
 void IntOrEnumToString(
     int64_t val, const reflection::Type *type,
     const flatbuffers::Vector<flatbuffers::Offset<reflection::Enum>> *enums,
-    std::stringstream *out) {
+    FastStringBuilder *out) {
   // Check if integer is an enum and print string, otherwise fallback to
   // printing as int.
   if (type->index() > -1 && type->index() < (int32_t)enums->size()) {
     const reflection::Enum *enum_props = enums->Get(type->index());
     if (!enum_props->is_union()) {
-      const char *value_string = EnumToString(val, enum_props->values());
+      const std::string_view value_string =
+          EnumToString(val, enum_props->values());
 
-      if (value_string != nullptr) {
-        *out << '"' << value_string << '"';
+      if (value_string.data() != nullptr) {
+        out->AppendChar('"');
+        out->Append(value_string);
+        out->AppendChar('"');
       } else {
-        *out << val;
+        out->AppendInt(val);
       }
     }
   } else {
@@ -117,10 +117,10 @@
 
 // Adds a newline and indents
 // Every increment in tree depth is two spaces
-void AddWrapping(std::stringstream *out, int tree_depth) {
-  *out << "\n";
+void AddWrapping(FastStringBuilder *out, int tree_depth) {
+  out->Append("\n");
   for (int i = 0; i < tree_depth; i++) {
-    *out << "  ";
+    out->Append("  ");
   }
 }
 
@@ -141,7 +141,7 @@
     const ObjT *table, const reflection::Field *field,
     const flatbuffers::Vector<flatbuffers::Offset<reflection::Object>> *objects,
     const flatbuffers::Vector<flatbuffers::Offset<reflection::Enum>> *enums,
-    std::stringstream *out, JsonOptions json_options, int tree_depth) {
+    FastStringBuilder *out, JsonOptions json_options, int tree_depth) {
   const reflection::Type *type = field->type();
 
   switch (type->base_type()) {
@@ -162,40 +162,39 @@
       break;
     case BaseType::String:
       if constexpr (std::is_same<flatbuffers::Table, ObjT>()) {
-        std::string str = flatbuffers::GetFieldS(*table, *field)->str();
-        std::string out_str;
-        out_str.reserve(str.size());
+        flatbuffers::string_view str =
+            flatbuffers::GetFieldS(*table, *field)->string_view();
+        out->AppendChar('"');
         for (char c : str) {
-          // out_str += c;
           switch (c) {
             case '"':
-              out_str += "\\\"";
+              out->Append("\\\"");
               break;
             case '\\':
-              out_str += "\\\\";
+              out->Append("\\\\");
               break;
             case '\b':
-              out_str += "\\b";
+              out->Append("\\b");
               break;
             case '\f':
-              out_str += "\\f";
+              out->Append("\\f");
               break;
             case '\n':
-              out_str += "\\n";
+              out->Append("\\n");
               break;
             case '\r':
-              out_str += "\\r";
+              out->Append("\\r");
               break;
             case '\t':
-              out_str += "\\t";
+              out->Append("\\t");
               break;
             default:
-              out_str += c;
+              out->AppendChar(c);
           }
         }
-        *out << '"' << out_str << '"';
+        out->AppendChar('"');
       } else {
-        *out << "null";
+        out->Append("null");
       }
       break;
     case BaseType::Vector: {
@@ -205,7 +204,9 @@
         reflection::BaseType elem_type = type->element();
 
         if (vector->size() > json_options.max_vector_size) {
-          *out << "[ ... " << vector->size() << " elements ... ]";
+          out->Append("[ ... ");
+          out->AppendInt(vector->size());
+          out->Append(" elements ... ]");
           break;
         }
 
@@ -216,13 +217,13 @@
           wrap = ShouldCauseWrapping(elem_type);
         }
 
-        *out << '[';
+        out->AppendChar('[');
         for (flatbuffers::uoffset_t i = 0; i < vector->size(); ++i) {
           if (i != 0) {
             if (wrap) {
-              *out << ",";
+              out->Append(",");
             } else {
-              *out << ", ";
+              out->Append(", ");
             }
           }
           if (wrap) {
@@ -236,8 +237,9 @@
             FloatToString(flatbuffers::GetAnyVectorElemF(vector, elem_type, i),
                           elem_type, out);
           } else if (elem_type == BaseType::String) {
-            *out << '"' << flatbuffers::GetAnyVectorElemS(vector, elem_type, i)
-                 << '"';
+            out->AppendChar('"');
+            out->Append(flatbuffers::GetAnyVectorElemS(vector, elem_type, i));
+            out->AppendChar('"');
           } else if (elem_type == BaseType::Obj) {
             if (type->index() > -1 &&
                 type->index() < (int32_t)objects->size()) {
@@ -260,9 +262,9 @@
         if (wrap) {
           AddWrapping(out, tree_depth);
         }
-        *out << ']';
+        out->AppendChar(']');
       } else {
-        *out << "null";
+        out->Append("null");
       }
     } break;
     case BaseType::Obj: {
@@ -277,11 +279,11 @@
                          json_options, tree_depth);
         }
       } else {
-        *out << "null";
+        out->Append("null");
       }
     } break;
     default:
-      *out << "null";
+      out->Append("null");
   }
 }
 
@@ -292,7 +294,7 @@
     const reflection::Object *obj,
     const flatbuffers::Vector<flatbuffers::Offset<reflection::Object>> *objects,
     const flatbuffers::Vector<flatbuffers::Offset<reflection::Enum>> *enums,
-    const ObjT *object, std::stringstream *out, JsonOptions json_options,
+    const ObjT *object, FastStringBuilder *out, JsonOptions json_options,
     int tree_depth) {
   static_assert(std::is_same<flatbuffers::Table, ObjT>() ||
                     std::is_same<flatbuffers::Struct, ObjT>(),
@@ -312,16 +314,16 @@
     }
   }
 
-  *out << '{';
+  out->AppendChar('{');
   for (const reflection::Field *field : *obj->fields()) {
     // Check whether this object has the field populated (even for structs,
     // which should have all fields populated)
     if (object->GetAddressOf(field->offset())) {
       if (print_sep) {
         if (wrap) {
-          *out << ",";
+          out->Append(",");
         } else {
-          *out << ", ";
+          out->Append(", ");
         }
       } else {
         print_sep = true;
@@ -331,7 +333,9 @@
         AddWrapping(out, child_tree_depth);
       }
 
-      *out << '"' << field->name()->c_str() << "\": ";
+      out->AppendChar('"');
+      out->Append(field->name()->string_view());
+      out->Append("\": ");
       FieldToString(object, field, objects, enums, out, json_options,
                     child_tree_depth);
     }
@@ -341,30 +345,37 @@
     AddWrapping(out, tree_depth);
   }
 
-  *out << '}';
+  out->AppendChar('}');
 }
 
 }  // namespace
 
 std::string FlatbufferToJson(const reflection::Schema *schema,
                              const uint8_t *data, JsonOptions json_options) {
+  FastStringBuilder builder;
+  FlatbufferToJson(&builder, schema, data, json_options);
+  return builder.MoveResult();
+}
+
+void FlatbufferToJson(FastStringBuilder *builder,
+                      const reflection::Schema *schema, const uint8_t *data,
+                      JsonOptions json_options) {
   CHECK(schema != nullptr) << ": Need to provide a schema";
+  CHECK(builder != nullptr) << ": Need to provide an output builder";
 
   // It is pretty common to get passed in a nullptr when a test fails.  Rather
   // than CHECK, return a more user friendly result.
   if (data == nullptr) {
-    return "null";
+    builder->Append("null");
+    return;
   }
 
   const flatbuffers::Table *table = flatbuffers::GetAnyRoot(data);
 
   const reflection::Object *obj = schema->root_table();
 
-  std::stringstream out;
-
-  ObjectToString(obj, schema->objects(), schema->enums(), table, &out,
+  ObjectToString(obj, schema->objects(), schema->enums(), table, builder,
                  json_options);
-
-  return out.str();
 }
+
 }  // namespace aos
diff --git a/aos/json_to_flatbuffer.h b/aos/json_to_flatbuffer.h
index aebd069..9ba50f8 100644
--- a/aos/json_to_flatbuffer.h
+++ b/aos/json_to_flatbuffer.h
@@ -6,6 +6,7 @@
 #include <string>
 #include <string_view>
 
+#include "aos/fast_string_builder.h"
 #include "aos/flatbuffers.h"
 #include "flatbuffers/flatbuffers.h"
 #include "flatbuffers/reflection.h"
@@ -82,6 +83,10 @@
                              const uint8_t *const data,
                              JsonOptions json_options = {});
 
+void FlatbufferToJson(FastStringBuilder *builder,
+                      const reflection::Schema *const schema,
+                      const uint8_t *const data, JsonOptions json_options = {});
+
 // Writes a Flatbuffer to a file, or dies.
 template <typename T>
 inline void WriteFlatbufferToJson(const std::string_view filename,