Omit stamp when inputs are only used by a single build rule

When a target depends on a multiple inputs, but itself has only a
single build rule, omit the stamp and depend on inputs directly.
While it's not very common to have a target with only single
source file in hand written code, it's pretty common for generated
code such as various bindings, where this optimization can save
significant amount of stamping. For example, in Fuchsia x64.core
build this saves 2351 stamps.

Bug: gn:172
Change-Id: If58241f56bd1ee902b0d32909f0662003fc499ed
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/8960
Reviewed-by: Brett Wilson <brettw@chromium.org>
Commit-Queue: Petr Hosek <phosek@google.com>
diff --git a/src/gn/ninja_binary_target_writer.cc b/src/gn/ninja_binary_target_writer.cc
index 2afb3db..e12e836 100644
--- a/src/gn/ninja_binary_target_writer.cc
+++ b/src/gn/ninja_binary_target_writer.cc
@@ -50,7 +50,8 @@
   writer.Run();
 }
 
-OutputFile NinjaBinaryTargetWriter::WriteInputsStampAndGetDep() const {
+std::vector<OutputFile> NinjaBinaryTargetWriter::WriteInputsStampAndGetDep(
+    size_t num_stamp_uses) const {
   CHECK(target_->toolchain()) << "Toolchain not set on target "
                               << target_->label().GetUserVisibleName(true);
 
@@ -62,12 +63,23 @@
   }
 
   if (inputs.size() == 0)
-    return OutputFile();  // No inputs
+    return std::vector<OutputFile>();  // No inputs
 
   // If we only have one input, return it directly instead of writing a stamp
   // file for it.
-  if (inputs.size() == 1)
-    return OutputFile(settings_->build_settings(), *inputs[0]);
+  if (inputs.size() == 1) {
+    return std::vector<OutputFile>{
+      OutputFile(settings_->build_settings(), *inputs[0])};
+  }
+
+  std::vector<OutputFile> outs;
+  for (const SourceFile* source : inputs)
+    outs.push_back(OutputFile(settings_->build_settings(), *source));
+
+  // If there are multiple inputs, but the stamp file would be referenced only
+  // once, don't write it but depend on the inputs directly.
+  if (num_stamp_uses == 1u)
+    return outs;
 
   // Make a stamp file.
   OutputFile stamp_file =
@@ -87,7 +99,7 @@
   }
 
   out_ << std::endl;
-  return stamp_file;
+  return {stamp_file};
 }
 
 void NinjaBinaryTargetWriter::WriteSourceSetStamp(
diff --git a/src/gn/ninja_binary_target_writer.h b/src/gn/ninja_binary_target_writer.h
index 69f5b50..50d1151 100644
--- a/src/gn/ninja_binary_target_writer.h
+++ b/src/gn/ninja_binary_target_writer.h
@@ -26,9 +26,13 @@
  protected:
   // Writes to the output stream a stamp rule for inputs, and
   // returns the file to be appended to source rules that encodes the
-  // implicit dependencies for the current target. The returned OutputFile
-  // will be empty if there are no inputs.
-  OutputFile WriteInputsStampAndGetDep() const;
+  // implicit dependencies for the current target.
+  // If num_stamp_uses is small, this might return all input dependencies
+  // directly, without writing a stamp file.
+  // If there are no implicit dependencies and no extra target dependencies
+  // are passed in, this returns an empty vector.
+  std::vector<OutputFile> WriteInputsStampAndGetDep(
+      size_t num_stamp_uses) const;
 
   // Writes the stamp line for a source set. These are not linked.
   void WriteSourceSetStamp(const std::vector<OutputFile>& object_files);
diff --git a/src/gn/ninja_binary_target_writer_unittest.cc b/src/gn/ninja_binary_target_writer_unittest.cc
index 252fb43..970aa82 100644
--- a/src/gn/ninja_binary_target_writer_unittest.cc
+++ b/src/gn/ninja_binary_target_writer_unittest.cc
@@ -106,3 +106,78 @@
   std::string out_str = out.str();
   EXPECT_EQ(expected, out_str);
 }
+
+TEST_F(NinjaBinaryTargetWriterTest, Inputs) {
+  Err err;
+  TestWithScope setup;
+
+  {
+    Target target(setup.settings(), Label(SourceDir("//foo/"), "bar"));
+    target.set_output_type(Target::SOURCE_SET);
+    target.visibility().SetPublic();
+    target.sources().push_back(SourceFile("//foo/source1.cc"));
+    target.config_values().inputs().push_back(SourceFile("//foo/input1"));
+    target.config_values().inputs().push_back(SourceFile("//foo/input2"));
+    target.source_types_used().Set(SourceFile::SOURCE_CPP);
+    target.SetToolchain(setup.toolchain());
+    ASSERT_TRUE(target.OnResolved(&err));
+
+    std::ostringstream out;
+    NinjaBinaryTargetWriter writer(&target, out);
+    writer.Run();
+
+    const char expected[] =
+        "defines =\n"
+        "include_dirs =\n"
+        "cflags =\n"
+        "cflags_cc =\n"
+        "root_out_dir = .\n"
+        "target_out_dir = obj/foo\n"
+        "target_output_name = bar\n"
+        "\n"
+        "build obj/foo/bar.source1.o: cxx ../../foo/source1.cc | "
+        "../../foo/input1 ../../foo/input2\n"
+        "\n"
+        "build obj/foo/bar.stamp: stamp obj/foo/bar.source1.o\n";
+    std::string out_str = out.str();
+    EXPECT_EQ(expected, out_str) << expected << "\n" << out_str;
+  }
+
+  {
+    Target target(setup.settings(), Label(SourceDir("//foo/"), "bar"));
+    target.set_output_type(Target::SOURCE_SET);
+    target.visibility().SetPublic();
+    target.sources().push_back(SourceFile("//foo/source1.cc"));
+    target.sources().push_back(SourceFile("//foo/source2.cc"));
+    target.config_values().inputs().push_back(SourceFile("//foo/input1"));
+    target.config_values().inputs().push_back(SourceFile("//foo/input2"));
+    target.source_types_used().Set(SourceFile::SOURCE_CPP);
+    target.SetToolchain(setup.toolchain());
+    ASSERT_TRUE(target.OnResolved(&err));
+
+    std::ostringstream out;
+    NinjaBinaryTargetWriter writer(&target, out);
+    writer.Run();
+
+    const char expected[] =
+        "defines =\n"
+        "include_dirs =\n"
+        "cflags =\n"
+        "cflags_cc =\n"
+        "root_out_dir = .\n"
+        "target_out_dir = obj/foo\n"
+        "target_output_name = bar\n"
+        "\n"
+        "build obj/foo/bar.inputs.stamp: stamp "
+        "../../foo/input1 ../../foo/input2\n"
+        "build obj/foo/bar.source1.o: cxx ../../foo/source1.cc | "
+        "obj/foo/bar.inputs.stamp\n"
+        "build obj/foo/bar.source2.o: cxx ../../foo/source2.cc | "
+        "obj/foo/bar.inputs.stamp\n"
+        "\n"
+        "build obj/foo/bar.stamp: stamp obj/foo/bar.source1.o "
+        "obj/foo/bar.source2.o\n";
+    std::string out_str = out.str();
+    EXPECT_EQ(expected, out_str) << expected << "\n" << out_str;
+  }
+}
diff --git a/src/gn/ninja_c_binary_target_writer.cc b/src/gn/ninja_c_binary_target_writer.cc
index aba4b68..2773577 100644
--- a/src/gn/ninja_c_binary_target_writer.cc
+++ b/src/gn/ninja_c_binary_target_writer.cc
@@ -64,7 +64,10 @@
 void NinjaCBinaryTargetWriter::Run() {
   WriteCompilerVars();
 
-  OutputFile input_dep = WriteInputsStampAndGetDep();
+  size_t num_stamp_uses = target_->sources().size();
+
+  std::vector<OutputFile> input_deps = WriteInputsStampAndGetDep(
+      num_stamp_uses);
 
   // The input dependencies will be an order-only dependency. This will cause
   // Ninja to make sure the inputs are up to date before compiling this source,
@@ -96,7 +99,6 @@
   // that also use PCH files won't have a stamp file even though having
   // one would make output ninja file size a bit lower. That's ok, binary
   // targets with a single source are rare.
-  size_t num_stamp_uses = target_->sources().size();
   std::vector<OutputFile> order_only_deps = WriteInputDepsStampAndGetDep(
       std::vector<const Target*>(), num_stamp_uses);
 
@@ -105,7 +107,7 @@
   // |pch_other_files|. This is to prevent linking against them.
   std::vector<OutputFile> pch_obj_files;
   std::vector<OutputFile> pch_other_files;
-  WritePCHCommands(input_dep, order_only_deps, &pch_obj_files,
+  WritePCHCommands(input_deps, order_only_deps, &pch_obj_files,
                    &pch_other_files);
   std::vector<OutputFile>* pch_files =
       !pch_obj_files.empty() ? &pch_obj_files : &pch_other_files;
@@ -124,7 +126,7 @@
   //    object file list.
   std::vector<OutputFile> obj_files;
   std::vector<SourceFile> other_files;
-  WriteSources(*pch_files, input_dep, order_only_deps, &obj_files,
+  WriteSources(*pch_files, input_deps, order_only_deps, &obj_files,
                &other_files);
 
   // Link all MSVC pch object files. The vector will be empty on GCC toolchains.
@@ -144,7 +146,7 @@
       DCHECK_NE(static_cast<size_t>(-1), computed_obj.IndexOf(obj));
 #endif
   } else {
-    WriteLinkerStuff(obj_files, other_files, input_dep);
+    WriteLinkerStuff(obj_files, other_files, input_deps);
   }
 }
 
@@ -228,7 +230,7 @@
 }
 
 void NinjaCBinaryTargetWriter::WritePCHCommands(
-    const OutputFile& input_dep,
+    const std::vector<OutputFile>& input_deps,
     const std::vector<OutputFile>& order_only_deps,
     std::vector<OutputFile>* object_files,
     std::vector<OutputFile>* other_files) {
@@ -239,14 +241,14 @@
   if (tool_c && tool_c->precompiled_header_type() != CTool::PCH_NONE &&
       target_->source_types_used().Get(SourceFile::SOURCE_C)) {
     WritePCHCommand(&CSubstitutionCFlagsC, CTool::kCToolCc,
-                    tool_c->precompiled_header_type(), input_dep,
+                    tool_c->precompiled_header_type(), input_deps,
                     order_only_deps, object_files, other_files);
   }
   const CTool* tool_cxx = target_->toolchain()->GetToolAsC(CTool::kCToolCxx);
   if (tool_cxx && tool_cxx->precompiled_header_type() != CTool::PCH_NONE &&
       target_->source_types_used().Get(SourceFile::SOURCE_CPP)) {
     WritePCHCommand(&CSubstitutionCFlagsCc, CTool::kCToolCxx,
-                    tool_cxx->precompiled_header_type(), input_dep,
+                    tool_cxx->precompiled_header_type(), input_deps,
                     order_only_deps, object_files, other_files);
   }
 
@@ -254,7 +256,7 @@
   if (tool_objc && tool_objc->precompiled_header_type() == CTool::PCH_GCC &&
       target_->source_types_used().Get(SourceFile::SOURCE_M)) {
     WritePCHCommand(&CSubstitutionCFlagsObjC, CTool::kCToolObjC,
-                    tool_objc->precompiled_header_type(), input_dep,
+                    tool_objc->precompiled_header_type(), input_deps,
                     order_only_deps, object_files, other_files);
   }
 
@@ -263,7 +265,7 @@
   if (tool_objcxx && tool_objcxx->precompiled_header_type() == CTool::PCH_GCC &&
       target_->source_types_used().Get(SourceFile::SOURCE_MM)) {
     WritePCHCommand(&CSubstitutionCFlagsObjCc, CTool::kCToolObjCxx,
-                    tool_objcxx->precompiled_header_type(), input_dep,
+                    tool_objcxx->precompiled_header_type(), input_deps,
                     order_only_deps, object_files, other_files);
   }
 }
@@ -272,17 +274,17 @@
     const Substitution* flag_type,
     const char* tool_name,
     CTool::PrecompiledHeaderType header_type,
-    const OutputFile& input_dep,
+    const std::vector<OutputFile>& input_deps,
     const std::vector<OutputFile>& order_only_deps,
     std::vector<OutputFile>* object_files,
     std::vector<OutputFile>* other_files) {
   switch (header_type) {
     case CTool::PCH_MSVC:
-      WriteWindowsPCHCommand(flag_type, tool_name, input_dep, order_only_deps,
+      WriteWindowsPCHCommand(flag_type, tool_name, input_deps, order_only_deps,
                              object_files);
       break;
     case CTool::PCH_GCC:
-      WriteGCCPCHCommand(flag_type, tool_name, input_dep, order_only_deps,
+      WriteGCCPCHCommand(flag_type, tool_name, input_deps, order_only_deps,
                          other_files);
       break;
     case CTool::PCH_NONE:
@@ -294,7 +296,7 @@
 void NinjaCBinaryTargetWriter::WriteGCCPCHCommand(
     const Substitution* flag_type,
     const char* tool_name,
-    const OutputFile& input_dep,
+    const std::vector<OutputFile>& input_deps,
     const std::vector<OutputFile>& order_only_deps,
     std::vector<OutputFile>* gch_files) {
   // Compute the pch output file (it will be language-specific).
@@ -306,8 +308,8 @@
   gch_files->insert(gch_files->end(), outputs.begin(), outputs.end());
 
   std::vector<OutputFile> extra_deps;
-  if (!input_dep.value().empty())
-    extra_deps.push_back(input_dep);
+  std::copy(input_deps.begin(), input_deps.end(),
+            std::back_inserter(extra_deps));
 
   // Build line to compile the file.
   WriteCompilerBuildLine(target_->config_values().precompiled_source(),
@@ -346,7 +348,7 @@
 void NinjaCBinaryTargetWriter::WriteWindowsPCHCommand(
     const Substitution* flag_type,
     const char* tool_name,
-    const OutputFile& input_dep,
+    const std::vector<OutputFile>& input_deps,
     const std::vector<OutputFile>& order_only_deps,
     std::vector<OutputFile>* object_files) {
   // Compute the pch output file (it will be language-specific).
@@ -358,8 +360,8 @@
   object_files->insert(object_files->end(), outputs.begin(), outputs.end());
 
   std::vector<OutputFile> extra_deps;
-  if (!input_dep.value().empty())
-    extra_deps.push_back(input_dep);
+  std::copy(input_deps.begin(), input_deps.end(),
+            std::back_inserter(extra_deps));
 
   // Build line to compile the file.
   WriteCompilerBuildLine(target_->config_values().precompiled_source(),
@@ -381,7 +383,7 @@
 
 void NinjaCBinaryTargetWriter::WriteSources(
     const std::vector<OutputFile>& pch_deps,
-    const OutputFile& input_dep,
+    const std::vector<OutputFile>& input_deps,
     const std::vector<OutputFile>& order_only_deps,
     std::vector<OutputFile>* object_files,
     std::vector<SourceFile>* other_files) {
@@ -399,8 +401,8 @@
       continue;  // No output for this source.
     }
 
-    if (!input_dep.value().empty())
-      deps.push_back(input_dep);
+
+    std::copy(input_deps.begin(), input_deps.end(), std::back_inserter(deps));
 
     if (tool_name != Tool::kToolNone) {
       // Only include PCH deps that correspond to the tool type, for instance,
@@ -445,7 +447,7 @@
 void NinjaCBinaryTargetWriter::WriteLinkerStuff(
     const std::vector<OutputFile>& object_files,
     const std::vector<SourceFile>& other_files,
-    const OutputFile& input_dep) {
+    const std::vector<OutputFile>& input_deps) {
   std::vector<OutputFile> output_files;
   SubstitutionWriter::ApplyListToLinkerAsOutputFile(
       target_, tool_, tool_->outputs(), &output_files);
@@ -527,8 +529,8 @@
 
   // The input dependency is only needed if there are no object files, as the
   // dependency is normally provided transitively by the source files.
-  if (!input_dep.value().empty() && object_files.empty())
-    implicit_deps.push_back(input_dep);
+  std::copy(input_deps.begin(), input_deps.end(),
+            std::back_inserter(implicit_deps));
 
   // Any C++ target which depends on a Rust .rlib has to depend on its
   // entire tree of transitive rlibs.
diff --git a/src/gn/ninja_c_binary_target_writer.h b/src/gn/ninja_c_binary_target_writer.h
index d69baf2..1608af1 100644
--- a/src/gn/ninja_c_binary_target_writer.h
+++ b/src/gn/ninja_c_binary_target_writer.h
@@ -33,9 +33,9 @@
   // non-object files (for instance, .gch files from a GCC toolchain, are
   // appended to |other_files|).
   //
-  // input_dep is the stamp file collecting the dependencies required before
+  // input_deps is the stamp file collecting the dependencies required before
   // compiling this target. It will be empty if there are no input deps.
-  void WritePCHCommands(const OutputFile& input_dep,
+  void WritePCHCommands(const std::vector<OutputFile>& input_deps,
                         const std::vector<OutputFile>& order_only_deps,
                         std::vector<OutputFile>* object_files,
                         std::vector<OutputFile>* other_files);
@@ -44,20 +44,20 @@
   void WritePCHCommand(const Substitution* flag_type,
                        const char* tool_name,
                        CTool::PrecompiledHeaderType header_type,
-                       const OutputFile& input_dep,
+                       const std::vector<OutputFile>& input_deps,
                        const std::vector<OutputFile>& order_only_deps,
                        std::vector<OutputFile>* object_files,
                        std::vector<OutputFile>* other_files);
 
   void WriteGCCPCHCommand(const Substitution* flag_type,
                           const char* tool_name,
-                          const OutputFile& input_dep,
+                          const std::vector<OutputFile>& input_deps,
                           const std::vector<OutputFile>& order_only_deps,
                           std::vector<OutputFile>* gch_files);
 
   void WriteWindowsPCHCommand(const Substitution* flag_type,
                               const char* tool_name,
-                              const OutputFile& input_dep,
+                              const std::vector<OutputFile>& input_deps,
                               const std::vector<OutputFile>& order_only_deps,
                               std::vector<OutputFile>* object_files);
 
@@ -69,14 +69,14 @@
   //
   // The files produced by the compiler will be added to two output vectors.
   void WriteSources(const std::vector<OutputFile>& pch_deps,
-                    const OutputFile& input_dep,
+                    const std::vector<OutputFile>& input_deps,
                     const std::vector<OutputFile>& order_only_deps,
                     std::vector<OutputFile>* object_files,
                     std::vector<SourceFile>* other_files);
 
   void WriteLinkerStuff(const std::vector<OutputFile>& object_files,
                         const std::vector<SourceFile>& other_files,
-                        const OutputFile& input_dep);
+                        const std::vector<OutputFile>& input_deps);
   void WriteOutputSubstitutions();
   void WriteLibsList(const std::string& label,
                      const std::vector<OutputFile>& libs);
diff --git a/src/gn/ninja_rust_binary_target_writer.cc b/src/gn/ninja_rust_binary_target_writer.cc
index 983d92f..1584c44 100644
--- a/src/gn/ninja_rust_binary_target_writer.cc
+++ b/src/gn/ninja_rust_binary_target_writer.cc
@@ -110,7 +110,10 @@
 void NinjaRustBinaryTargetWriter::Run() {
   DCHECK(target_->output_type() != Target::SOURCE_SET);
 
-  OutputFile input_dep = WriteInputsStampAndGetDep();
+  size_t num_stamp_uses = target_->sources().size();
+
+  std::vector<OutputFile> input_deps = WriteInputsStampAndGetDep(
+      num_stamp_uses);
 
   WriteCompilerVars();
 
@@ -126,11 +129,10 @@
   // Ninja to make sure the inputs are up to date before compiling this source,
   // but changes in the inputs deps won't cause the file to be recompiled. See
   // the comment on NinjaCBinaryTargetWriter::Run for more detailed explanation.
-  size_t num_stamp_uses = target_->sources().size();
   std::vector<OutputFile> order_only_deps = WriteInputDepsStampAndGetDep(
       std::vector<const Target*>(), num_stamp_uses);
-  if (!input_dep.value().empty())
-    order_only_deps.push_back(input_dep);
+  std::copy(input_deps.begin(), input_deps.end(),
+            std::back_inserter(order_only_deps));
 
   // Build lists which will go into different bits of the rustc command line.
   // Public rust_library deps go in a --extern rlibs, public non-rust deps go in