Refactor string_output_buffer to support "Reserve".

On its own, it is 2% faster, but not statistically significant.

The main benefit here is that if we support reserve, we can implement
something akin to the following. Then we can, instead of allocating a
buffer for the escaped value, generating it, then copying it to the
buffer, write it directly.

struct NinjaEscaped {
  inner: &str
}

impl WriteBuf for NinjaEscaped {
  fn MaximumSize(&self) -> usize { inner.size() * 3 }
  fn Write(&self, dst: &mut [char], upto: &mut idx) {
    for c in self.inner.chars() {
      if needs_ninja_escape(c) {
        dst[upto++] = '\'
      }
      dst[upto++] = c
  }
}

Change-Id: I82469ade090787853e9c83085c7c1dfa6a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/27640
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Matt Stark <msta@google.com>
diff --git a/src/gn/string_output_buffer.cc b/src/gn/string_output_buffer.cc
index 2435fa1..d02f707 100644
--- a/src/gn/string_output_buffer.cc
+++ b/src/gn/string_output_buffer.cc
@@ -6,58 +6,50 @@
 
 #include <fstream>
 
+#include "base/compiler_specific.h"
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
 #include "gn/err.h"
 #include "gn/file_writer.h"
 #include "gn/filesystem_utils.h"
 
+void StringOutputBuffer::Reserve(size_t n) {
+  if (UNLIKELY(page_free_size() < n)) {
+    if (rw_.size > 0) {
+      ro_.push_back(std::move(rw_));
+      ro_size_ += rw_.size;
+    }
+
+    // Pick the minimum number of pages required to store n bytes.
+    // So technically, our "Page" could actually be multiple pages
+    // But unless you attempt to call append on a ridiculously large string,
+    // this should never happen in practice.
+    rw_capacity_ = (n + kPageSize - 1) & ~(kPageSize - 1);
+    rw_ = Page{
+        .buf = std::make_unique<char[]>(rw_capacity_),
+        .size = 0,
+    };
+  }
+}
+
 std::string StringOutputBuffer::str() const {
   std::string result;
-  size_t data_size = size();
-  result.reserve(data_size);
-  for (size_t nn = 0; nn < pages_.size(); ++nn) {
-    size_t wanted_size = std::min(kPageSize, data_size - nn * kPageSize);
-    result.append(pages_[nn]->data(), wanted_size);
+  result.reserve(size());
+  for (const auto& page : ro_) {
+    result.append(page.buf.get(), page.size);
+  }
+  if (rw_.size > 0) {
+    result.append(rw_.buf.get(), rw_.size);
   }
   return result;
 }
 
-void StringOutputBuffer::Append(const char* str, size_t len) {
-  Append(std::string_view(str, len));
-}
-
-void StringOutputBuffer::Append(std::string_view str) {
-  while (str.size() > 0) {
-    if (page_free_size() == 0) {
-      // Allocate a new page.
-      pages_.push_back(std::make_unique<Page>());
-      pos_ = 0;
-    }
-    size_t size = std::min(page_free_size(), str.size());
-    memcpy(pages_.back()->data() + pos_, str.data(), size);
-    pos_ += size;
-    str.remove_prefix(size);
-  }
-}
-
-void StringOutputBuffer::Append(char c) {
-  if (page_free_size() == 0) {
-    // Allocate a new page.
-    pages_.push_back(std::make_unique<Page>());
-    pos_ = 0;
-  }
-  pages_.back()->data()[pos_] = c;
-  pos_ += 1;
-}
-
 bool StringOutputBuffer::ContentsEqual(const base::FilePath& file_path) const {
   // Compare file and stream sizes first. Quick and will save us some time if
   // they are different sizes.
-  size_t data_size = size();
   int64_t file_size;
   if (!base::GetFileSize(file_path, &file_size) ||
-      static_cast<size_t>(file_size) != data_size) {
+      static_cast<size_t>(file_size) != size()) {
     return false;
   }
 
@@ -66,18 +58,26 @@
   if (!file.is_open())
     return false;
 
-  size_t page_count = pages_.size();
-  Page file_page;
-  for (size_t nn = 0; nn < page_count; ++nn) {
-    size_t wanted_size = std::min(data_size - nn * kPageSize, kPageSize);
-    file.read(file_page.data(), wanted_size);
+  std::vector<char> file_page;
+  file_page.resize(kPageSize);
+  auto check_page = [&](const Page& page) {
+    // Don't use reserve because if we do that, capacity is high enough but not
+    // size.
+    if (UNLIKELY(file_page.size() < page.size)) {
+      file_page.resize(page.size);
+    }
+    file.read(file_page.data(), page.size);
     if (!file.good())
       return false;
 
-    if (memcmp(file_page.data(), pages_[nn]->data(), wanted_size) != 0)
+    return memcmp(file_page.data(), page.buf.get(), page.size) == 0;
+  };
+
+  for (const auto& page : ro_) {
+    if (!check_page(page))
       return false;
   }
-  return true;
+  return rw_.size == 0 || check_page(rw_);
 }
 
 // Write the contents of this instance to a file at |file_path|.
@@ -93,18 +93,17 @@
     return false;
   }
 
-  size_t data_size = size();
-  size_t page_count = pages_.size();
-
   FileWriter writer;
   bool success = writer.Create(file_path);
   if (success) {
-    for (size_t nn = 0; nn < page_count; ++nn) {
-      size_t wanted_size = std::min(data_size - nn * kPageSize, kPageSize);
-      success = writer.Write(std::string_view(pages_[nn]->data(), wanted_size));
+    for (const auto& page : ro_) {
+      success = writer.Write(std::string_view(page.buf.get(), page.size));
       if (!success)
         break;
     }
+    if (success && rw_.size > 0) {
+      success = writer.Write(std::string_view(rw_.buf.get(), rw_.size));
+    }
   }
   if (!writer.Close())
     success = false;
diff --git a/src/gn/string_output_buffer.h b/src/gn/string_output_buffer.h
index 2338860..98c3b9a 100644
--- a/src/gn/string_output_buffer.h
+++ b/src/gn/string_output_buffer.h
@@ -5,7 +5,8 @@
 #ifndef TOOLS_GN_STRING_OUTPUT_BUFFER_H_
 #define TOOLS_GN_STRING_OUTPUT_BUFFER_H_
 
-#include <array>
+#include <concepts>
+#include <cstring>
 #include <memory>
 #include <streambuf>
 #include <string>
@@ -18,6 +19,27 @@
 
 class Err;
 
+inline size_t MaximumSize(std::string_view s) {
+  return s.size();
+}
+inline void WriteToBuf(std::string_view s, char buf[], size_t& upto) {
+  memcpy(&buf[upto], s.data(), s.size());
+  upto += s.size();
+}
+
+inline size_t MaximumSize(char) {
+  return 1;
+}
+inline void WriteToBuf(char c, char buf[], size_t& upto) {
+  buf[upto++] = c;
+}
+
+template <typename T>
+concept Writable = requires(const T& val, char buf[], size_t& upto) {
+  { MaximumSize(val) } -> std::convertible_to<size_t>;
+  WriteToBuf(val, buf, upto);
+};
+
 // An append-only very large storage area for string data. Useful for the parts
 // of GN that need to generate huge output files (e.g. --ide=json will create
 // a 139 MiB project.json file for the Fuchsia build).
@@ -48,15 +70,19 @@
   std::string str() const;
 
   // Return the number of characters stored in this instance.
-  size_t size() const { return (pages_.size() - 1u) * kPageSize + pos_; }
+  size_t size() const { return ro_size_ + rw_.size; }
 
-  // Append string to this instance.
-  void Append(const char* str, size_t len);
-  void Append(std::string_view str);
-  void Append(char c);
+  // Append a value to this instance.
+  void Append(const Writable auto& value) {
+    Reserve(MaximumSize(value));
+    WriteToBuf(value, rw_.buf.get(), rw_.size);
+  }
+  void Append(const char* str, size_t len) {
+    Append(std::string_view(str, len));
+  }
 
-  StringOutputBuffer& operator<<(std::string_view str) {
-    Append(str);
+  StringOutputBuffer& operator<<(const Writable auto& value) {
+    Append(value);
     return *this;
   }
 
@@ -87,13 +113,29 @@
 
  private:
   // Return the number of free bytes in the current page.
-  size_t page_free_size() const { return kPageSize - pos_; }
+  size_t page_free_size() const { return rw_capacity_ - rw_.size; }
+
+  // Ensures that the read-write section of the buffer has at least n bytes
+  // available.
+  void Reserve(size_t n);
 
   static constexpr size_t kPageSize = 65536;
-  using Page = std::array<char, kPageSize>;
 
-  size_t pos_ = kPageSize;
-  std::vector<std::unique_ptr<Page>> pages_;
+  struct Page {
+    std::unique_ptr<char[]> buf;
+    size_t size = 0;
+  };
+
+  // Because this is an extremely hot path, we avoid pointer indirection by
+  // storing the current page inline.
+  // The current page is read-write.
+  Page rw_;
+  size_t rw_capacity_ = 0;
+
+  // Completed read-only pages.
+  std::vector<Page> ro_;
+  // Contains sum([page.size for page in ro_])
+  size_t ro_size_ = 0;
 };
 
 #endif  // TOOLS_GN_STRING_OUTPUT_BUFFER_H_
diff --git a/src/gn/string_output_buffer_unittest.cc b/src/gn/string_output_buffer_unittest.cc
index 6dcb741..9067fd1 100644
--- a/src/gn/string_output_buffer_unittest.cc
+++ b/src/gn/string_output_buffer_unittest.cc
@@ -132,3 +132,38 @@
   ASSERT_TRUE(base::GetFileInfo(file_path, &file_info));
   ASSERT_TRUE(buffer.ContentsEqual(file_path));
 }
+
+namespace {
+
+struct CustomGreeting {
+  std::string_view name;
+};
+
+struct Unwritable {};
+
+size_t MaximumSize(const CustomGreeting& greeting) {
+  return greeting.name.size() + 7;
+}
+
+void WriteToBuf(const CustomGreeting& greeting, char* buf, size_t& size) {
+  constexpr std::string_view prefix = "Hello, ";
+  memcpy(buf + size, prefix.data(), prefix.size());
+  size += prefix.size();
+  memcpy(buf + size, greeting.name.data(), greeting.name.size());
+  size += greeting.name.size();
+}
+
+static_assert(Writable<CustomGreeting>);
+static_assert(Writable<std::string_view>);
+static_assert(Writable<char>);
+static_assert(Writable<std::string>);
+static_assert(Writable<const char*>);
+static_assert(!Writable<Unwritable>);
+
+}  // namespace
+
+TEST(StringOutputBuffer, CustomWritable) {
+  StringOutputBuffer buffer;
+  buffer << CustomGreeting{"world"} << '!';
+  EXPECT_EQ(buffer.str(), "Hello, world!");
+}