Write phony rules for harddeps
Write a rule for each target with at least two hard deps,
similar to what WritePublicInputsStampOrPhony() does for public_inputs:
build phony/foo/bar.harddeps: $
phony phony/foo/action phony/foo/dep.harddeps
It lists the target's direct hard deps. This mirrors
ResolvedTargetData::ComputeHardDeps(), so it depends on the same set
of hard deps, but its size is proportional to the number of direct deps.
A target's input deps then just refer to its own .harddeps rule.
Results for a Chromium build (mac, arm64, is_debug=false):
before after
size of all loaded .ninja files 1025 MB 427 MB
gn gen (hyperfine, 6 runs) 2.065s ± .048 1.947s ± .050
user / sys 12.0s / 4.3s 10.1s / 4.5s
Chromium no-op builds of base_unittests (target already built, hyperfine, M-series Mac):
before after
siso v1.6.0 (autoninja), -offline 735.5 ms ± 13.7 648.3 ms ± 22.8
user / sys 3.7s / 1.9s 2.3s / 2.2s
depot_tools ninja (1.12.1) 4.37s ± .02 2.25s ± .02
ninja v1.13.2 2.755s ± .009 1.532s ± .007
ninja with some local changes to tweak perf 535.8 ms ± 2.7 408.4 ms ± 8.1
(`siso -n` and `ninja -n` are within 2% of the no-op times.)
No intended behavior change.
Bug: chromium:40278737
Change-Id: I163e6bea334c2e626bf7c96cd8d942848a6766bf
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/26741
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Reviewed-by: Nico Weber <thakis@google.com>
Commit-Queue: Nico Weber <thakis@chromium.org>
diff --git a/src/gn/ninja_copy_target_writer_unittest.cc b/src/gn/ninja_copy_target_writer_unittest.cc
index 6fc5e43..c81fa0b 100644
--- a/src/gn/ninja_copy_target_writer_unittest.cc
+++ b/src/gn/ninja_copy_target_writer_unittest.cc
@@ -208,10 +208,8 @@
writer.Run();
const char expected_linux[] =
- "build phony/foo/bar.inputdeps: phony || phony/foo/action1 "
- "phony/foo/action2\n"
- "build action1.copy: copy action1.out | phony/foo/bar.inputdeps\n"
- "build action2.copy: copy action2.out | phony/foo/bar.inputdeps\n"
+ "build action1.copy: copy action1.out || phony/foo/bar.harddeps\n"
+ "build action2.copy: copy action2.out || phony/foo/bar.harddeps\n"
"\n"
"build phony/foo/bar: phony action1.copy action2.copy\n";
std::string out_str = out.str();
diff --git a/src/gn/ninja_target_writer.cc b/src/gn/ninja_target_writer.cc
index d3d1eb1..7e1ba0e 100644
--- a/src/gn/ninja_target_writer.cc
+++ b/src/gn/ninja_target_writer.cc
@@ -193,6 +193,7 @@
}
WritePublicInputsStampOrPhony(target, resolved, rules);
+ WriteHardDepsStampOrPhony(target, resolved, rules);
if (needs_file_write) {
// Write the ninja file.
@@ -256,6 +257,90 @@
out << std::endl << std::endl;
}
+namespace {
+
+// BUNDLE_DATA hard deps are only input deps of CREATE_BUNDLE targets, which
+// list them explicitly. See WriteInputDepsStampOrPhonyAndGetDep().
+bool IsListedHardDep(const Target* dep) {
+ return dep->output_type() != Target::BUNDLE_DATA &&
+ dep->has_dependency_output();
+}
+
+} // namespace
+
+// static
+void NinjaTargetWriter::AppendHardDepsOutputs(
+ const Target* target,
+ const ResolvedTargetData& resolved,
+ std::vector<OutputFile>* outputs) {
+ // The iteration order of GetHardDeps() isn't deterministic, but this only
+ // depends on the set's contents.
+ const Target* single_hard_dep = nullptr;
+ for (const Target* hard_dep : resolved.GetHardDeps(target)) {
+ if (!IsListedHardDep(hard_dep))
+ continue;
+ if (single_hard_dep) {
+ outputs->push_back(
+ GetHardDepsOutputFile(target, target->settings()->build_settings()));
+ return;
+ }
+ single_hard_dep = hard_dep;
+ }
+ if (single_hard_dep)
+ outputs->push_back(single_hard_dep->dependency_output());
+}
+
+// static
+void NinjaTargetWriter::WriteHardDepsStampOrPhony(const Target* target,
+ ResolvedTargetData* resolved,
+ std::ostream& out) {
+ std::unique_ptr<ResolvedTargetData> resolved_owned;
+ if (!resolved) {
+ resolved_owned = std::make_unique<ResolvedTargetData>();
+ resolved = resolved_owned.get();
+ }
+
+ const BuildSettings* build_settings = target->settings()->build_settings();
+ OutputFile output = GetHardDepsOutputFile(target, build_settings);
+ std::vector<OutputFile> self;
+ AppendHardDepsOutputs(target, *resolved, &self);
+ if (self.empty() || self[0] != output)
+ return; // Fewer than two hard deps, nothing will refer to this rule.
+
+ // Keep in sync with ResolvedTargetData::ComputeHardDeps(). This produces
+ // the same set of targets, but refers to the hard deps rules of deps
+ // instead of flattening them.
+ std::vector<OutputFile> deps;
+ for (const Target* dep : resolved->GetTargetDeps(target).linked_deps()) {
+ if (target->hard_dep() || dep->hard_dep()) {
+ if (IsListedHardDep(dep))
+ deps.push_back(dep->dependency_output());
+ } else if (ResolvedTargetData::ForwardsHardDeps(dep)) {
+ AppendHardDepsOutputs(dep, *resolved, &deps);
+ }
+ }
+ std::sort(deps.begin(), deps.end());
+ deps.erase(std::unique(deps.begin(), deps.end()), deps.end());
+
+ PathOutput path_output(build_settings->build_dir(),
+ build_settings->root_path_utf8(), ESCAPE_NINJA);
+
+ // The deps are regular inputs instead of order-only deps so that actions,
+ // which have an implicit dependency on their hard deps, become dirty if
+ // one of their recursive hard deps changes. Everything else refers to this
+ // rule through an order-only dep.
+ out << "build ";
+ path_output.WriteFile(out, output);
+ if (build_settings->no_stamp_files()) {
+ out << ": " << BuiltinTool::kBuiltinToolPhony;
+ } else {
+ out << ": " << GetNinjaRulePrefixForToolchain(target->settings())
+ << GeneralTool::kGeneralToolStamp;
+ }
+ path_output.WriteFiles(out, deps);
+ out << std::endl;
+}
+
void NinjaTargetWriter::WriteEscapedSubstitution(const Substitution* type) {
EscapeOptions opts;
opts.mode = ESCAPE_NINJA;
@@ -541,13 +626,19 @@
// Hard dependencies that are direct or indirect dependencies.
// These are large (up to 100s), hence why we check other
const TargetSet& hard_deps = resolved().GetHardDeps(target_);
- for (const Target* target : hard_deps) {
+ std::vector<OutputFile> hard_deps_outputs;
+ if (target_->output_type() == Target::CREATE_BUNDLE) {
// BUNDLE_DATA should normally be treated as a data-only dependency
// (see Target::IsDataOnly()). Only the CREATE_BUNDLE target, that actually
// consumes this data, needs to have the BUNDLE_DATA as an input dependency.
- if (target->output_type() != Target::BUNDLE_DATA ||
- target_->output_type() == Target::CREATE_BUNDLE)
+ // So list all hard deps directly for CREATE_BUNDLE targets.
+ for (const Target* target : hard_deps)
input_deps_targets.push_back(target);
+ } else {
+ // For everything else, depend on the (at most one) file that in turn
+ // depends on all hard deps that aren't BUNDLE_DATA, to not write
+ // thousands of hard deps for every target.
+ AppendHardDepsOutputs(target_, resolved(), &hard_deps_outputs);
}
// Additional hard dependencies passed in. These are usually empty or small,
@@ -575,7 +666,7 @@
// Write the outputs.
if (input_deps_sources.empty() && input_deps_targets.empty() &&
- toolchain_deps_targets.empty())
+ hard_deps_outputs.empty() && toolchain_deps_targets.empty())
return InputDeps{}; // No input dependencies.
InputDeps deps;
@@ -621,6 +712,8 @@
deps.push_back(dep->dependency_output());
}
};
+ deps.order_only.insert(deps.order_only.end(), hard_deps_outputs.begin(),
+ hard_deps_outputs.end());
add_target_deps(deps.order_only, input_deps_targets);
add_target_deps(deps.implicit, toolchain_deps_targets);
diff --git a/src/gn/ninja_target_writer.h b/src/gn/ninja_target_writer.h
index d7b2883..043e834 100644
--- a/src/gn/ninja_target_writer.h
+++ b/src/gn/ninja_target_writer.h
@@ -61,6 +61,25 @@
ResolvedTargetData* resolved,
std::ostream& out);
+ // Targets need to depend on all their recursive hard deps. Listing all of
+ // them on each target makes the generated ninja files very large, since
+ // targets near the top of the dependency graph tend to have thousands of
+ // recursive hard deps. Instead, every target with at least two hard deps
+ // gets a stamp or phony rule for its hard deps which depends on the target's
+ // direct hard deps, and on the hard deps rules of the direct deps that hard
+ // deps are inherited from. BUNDLE_DATA targets are never included (see
+ // WriteInputDepsStampOrPhonyAndGetDep()).
+ static void WriteHardDepsStampOrPhony(const Target* target,
+ ResolvedTargetData* resolved,
+ std::ostream& out);
+
+ // Appends what a build statement needs to depend on to depend on all hard
+ // deps of |target|: Nothing if it has no hard deps, the hard dep itself if
+ // it has just one, else the rule written by WriteHardDepsStampOrPhony().
+ static void AppendHardDepsOutputs(const Target* target,
+ const ResolvedTargetData& resolved,
+ std::vector<OutputFile>* outputs);
+
virtual void Run() = 0;
protected:
diff --git a/src/gn/ninja_target_writer_unittest.cc b/src/gn/ninja_target_writer_unittest.cc
index 53b1a4d..fb00320 100644
--- a/src/gn/ninja_target_writer_unittest.cc
+++ b/src/gn/ninja_target_writer_unittest.cc
@@ -760,3 +760,138 @@
EXPECT_FALSE(out.contains("../../foo/g.in")) << out;
}
}
+
+TEST(NinjaTargetWriter, HardDeps) {
+ TestWithScope setup;
+ Err err;
+
+ auto make_action = [&](Target* target) {
+ target->set_output_type(Target::ACTION);
+ target->visibility().SetPublic();
+ target->SetToolchain(setup.toolchain());
+ target->action_values().set_script(SourceFile("//foo/script.py"));
+ std::string output = "//out/Debug/" + target->label().name() + ".out";
+ target->action_values().outputs() =
+ SubstitutionList::MakeForTest(output.c_str());
+ };
+ auto make_source_set = [&](Target* target) {
+ target->set_output_type(Target::SOURCE_SET);
+ target->visibility().SetPublic();
+ target->sources().push_back(SourceFile("//foo/source.cc"));
+ target->source_types_used().Set(SourceFile::SOURCE_CPP);
+ target->SetToolchain(setup.toolchain());
+ };
+ auto hard_deps_rule = [](const Target* target) {
+ std::ostringstream out;
+ NinjaTargetWriter::WriteHardDepsStampOrPhony(target, nullptr, out);
+ return out.str();
+ };
+ auto hard_deps_outputs = [](const Target* target) {
+ ResolvedTargetData resolved;
+ std::vector<OutputFile> outputs;
+ NinjaTargetWriter::AppendHardDepsOutputs(target, resolved, &outputs);
+ std::string result;
+ for (const OutputFile& output : outputs)
+ result += std::string(output.value()) + " ";
+ return result;
+ };
+
+ Target action1(setup.settings(), Label(SourceDir("//foo/"), "action1"));
+ make_action(&action1);
+ ASSERT_TRUE(action1.OnResolved(&err));
+ Target action2(setup.settings(), Label(SourceDir("//foo/"), "action2"));
+ make_action(&action2);
+ ASSERT_TRUE(action2.OnResolved(&err));
+ Target action3(setup.settings(), Label(SourceDir("//foo/"), "action3"));
+ make_action(&action3);
+ ASSERT_TRUE(action3.OnResolved(&err));
+ Target bundle_data(setup.settings(), Label(SourceDir("//foo/"), "data"));
+ bundle_data.set_output_type(Target::BUNDLE_DATA);
+ bundle_data.visibility().SetPublic();
+ bundle_data.SetToolchain(setup.toolchain());
+ ASSERT_TRUE(bundle_data.OnResolved(&err));
+
+ // No hard deps: No rule, nothing to depend on.
+ EXPECT_EQ("", hard_deps_rule(&action1));
+ EXPECT_EQ("", hard_deps_outputs(&action1));
+
+ // A single hard dep: No rule, dependents depend on the hard dep itself.
+ // BUNDLE_DATA targets don't count.
+ Target single(setup.settings(), Label(SourceDir("//foo/"), "single"));
+ make_source_set(&single);
+ single.private_deps().push_back(LabelTargetPair(&action1));
+ single.private_deps().push_back(LabelTargetPair(&bundle_data));
+ ASSERT_TRUE(single.OnResolved(&err));
+ EXPECT_EQ("", hard_deps_rule(&single));
+ EXPECT_EQ("phony/foo/action1 ", hard_deps_outputs(&single));
+
+ // Several hard deps: The rule lists them.
+ Target mid(setup.settings(), Label(SourceDir("//foo/"), "mid"));
+ make_source_set(&mid);
+ mid.private_deps().push_back(LabelTargetPair(&action2));
+ mid.private_deps().push_back(LabelTargetPair(&action1));
+ mid.private_deps().push_back(LabelTargetPair(&bundle_data));
+ ASSERT_TRUE(mid.OnResolved(&err));
+ EXPECT_EQ(
+ "build phony/foo/mid.harddeps: phony phony/foo/action1 "
+ "phony/foo/action2\n",
+ hard_deps_rule(&mid));
+ EXPECT_EQ("phony/foo/mid.harddeps ", hard_deps_outputs(&mid));
+
+ // Hard deps of deps are referenced through the rule of the dep if there
+ // is one, and directly else. Duplicates are omitted.
+ Target top(setup.settings(), Label(SourceDir("//foo/"), "top"));
+ make_source_set(&top);
+ top.private_deps().push_back(LabelTargetPair(&mid));
+ top.private_deps().push_back(LabelTargetPair(&single));
+ top.private_deps().push_back(LabelTargetPair(&action3));
+ top.private_deps().push_back(LabelTargetPair(&action1));
+ ASSERT_TRUE(top.OnResolved(&err));
+ EXPECT_EQ(
+ "build phony/foo/top.harddeps: phony phony/foo/action1 "
+ "phony/foo/action3 phony/foo/mid.harddeps\n",
+ hard_deps_rule(&top));
+
+ // The input deps of a target just refer to its hard deps rule.
+ {
+ std::ostringstream stream;
+ TestingNinjaTargetWriter writer(&top, setup.toolchain(), stream);
+ auto deps = writer.WriteInputDepsStampOrPhonyAndGetDep({}, 10u);
+ EXPECT_EQ("", stream.str());
+ EXPECT_TRUE(deps.implicit.empty());
+ ASSERT_EQ(1u, deps.order_only.size());
+ EXPECT_EQ("phony/foo/top.harddeps", deps.order_only[0].value());
+ }
+
+ // Binary targets without public headers don't forward their hard deps.
+ Target no_public_headers(setup.settings(),
+ Label(SourceDir("//foo/"), "no_public_headers"));
+ make_source_set(&no_public_headers);
+ no_public_headers.set_all_headers_public(false);
+ no_public_headers.private_deps().push_back(LabelTargetPair(&action1));
+ no_public_headers.private_deps().push_back(LabelTargetPair(&action2));
+ ASSERT_TRUE(no_public_headers.OnResolved(&err));
+ EXPECT_EQ("phony/foo/no_public_headers.harddeps ",
+ hard_deps_outputs(&no_public_headers));
+ Target top2(setup.settings(), Label(SourceDir("//foo/"), "top2"));
+ make_source_set(&top2);
+ top2.private_deps().push_back(LabelTargetPair(&no_public_headers));
+ top2.private_deps().push_back(LabelTargetPair(&action3));
+ ASSERT_TRUE(top2.OnResolved(&err));
+ EXPECT_EQ("", hard_deps_rule(&top2));
+ EXPECT_EQ("phony/foo/action3 ", hard_deps_outputs(&top2));
+
+ // Targets that are hard deps themselves (here, an action) depend on all
+ // their deps, but don't inherit hard deps from them. Their input deps are
+ // implicit deps so that they re-run if a hard dep changes; that works
+ // through the hard deps rule since its deps aren't order-only.
+ Target action4(setup.settings(), Label(SourceDir("//foo/"), "action4"));
+ make_action(&action4);
+ action4.private_deps().push_back(LabelTargetPair(&mid));
+ action4.private_deps().push_back(LabelTargetPair(&action3));
+ ASSERT_TRUE(action4.OnResolved(&err));
+ EXPECT_EQ(
+ "build phony/foo/action4.harddeps: phony phony/foo/action3 "
+ "phony/foo/mid\n",
+ hard_deps_rule(&action4));
+}
diff --git a/src/gn/ninja_utils.cc b/src/gn/ninja_utils.cc
index bf30bfb..e8a046a 100644
--- a/src/gn/ninja_utils.cc
+++ b/src/gn/ninja_utils.cc
@@ -40,3 +40,14 @@
".public_inputs.stamp");
}
}
+
+OutputFile GetHardDepsOutputFile(const Target* target,
+ const BuildSettings* build_settings) {
+ if (build_settings->no_stamp_files()) {
+ return GetOutputFile(*target, BuildDirType::PHONY, target->label().name(),
+ ".harddeps");
+ } else {
+ return GetOutputFile(*target, BuildDirType::OBJ, target->label().name(),
+ ".harddeps.stamp");
+ }
+}
diff --git a/src/gn/ninja_utils.h b/src/gn/ninja_utils.h
index e629d91..1773038 100644
--- a/src/gn/ninja_utils.h
+++ b/src/gn/ninja_utils.h
@@ -24,6 +24,12 @@
// don't collide with rules from other toolchains.
std::string GetNinjaRulePrefixForToolchain(const Settings* settings);
+// Returns the name of the phony target (or stamp file) that depends on all
+// hard deps of |target|. Only written if |target| has at least two hard deps,
+// see NinjaTargetWriter::WriteHardDepsStampOrPhony().
+OutputFile GetHardDepsOutputFile(const Target* target,
+ const BuildSettings* build_settings);
+
// Returns the output file path for the target's public inputs stamp or phony
// target.
OutputFile GetPublicInputsOutputFile(const Target* target,
diff --git a/src/gn/resolved_target_data.cc b/src/gn/resolved_target_data.cc
index de936c5..b0ef9a6 100644
--- a/src/gn/resolved_target_data.cc
+++ b/src/gn/resolved_target_data.cc
@@ -58,6 +58,18 @@
info->has_framework_info = true;
}
+// static
+bool ResolvedTargetData::ForwardsHardDeps(const Target* dep) {
+ // If |dep| is binary target and |dep| has no public header,
+ // a target depending on it does not need to have |dep|'s hard_deps as its
+ // hard_deps to start compiles earlier. Unless the target compiles a
+ // Swift module (since they also generate a header that can be used
+ // by the current target).
+ return !(dep->IsBinary() && !dep->all_headers_public() &&
+ dep->public_headers().empty() && !dep->builds_swift_module());
+}
+
+// Keep in sync with NinjaTargetWriter::WriteHardDepsStampOrPhony().
void ResolvedTargetData::ComputeHardDeps(TargetInfo* info) const {
TargetSet all_hard_deps;
for (const Target* dep : info->deps.linked_deps()) {
@@ -66,15 +78,8 @@
all_hard_deps.insert(dep);
continue;
}
- // If |dep| is binary target and |dep| has no public header,
- // |this| target does not need to have |dep|'s hard_deps as its
- // hard_deps to start compiles earlier. Unless the target compiles a
- // Swift module (since they also generate a header that can be used
- // by the current target).
- if (dep->IsBinary() && !dep->all_headers_public() &&
- dep->public_headers().empty() && !dep->builds_swift_module()) {
+ if (!ForwardsHardDeps(dep))
continue;
- }
// Recursive hard dependencies of all dependencies.
const TargetInfo* dep_info = GetTargetHardDeps(dep);
diff --git a/src/gn/resolved_target_data.h b/src/gn/resolved_target_data.h
index 2c0f836..544872d 100644
--- a/src/gn/resolved_target_data.h
+++ b/src/gn/resolved_target_data.h
@@ -147,6 +147,11 @@
// Retrieves an ordered list of all order-only dependency outputs for this
// target.
+ // Returns true if a target depending on |dep| inherits the hard deps of
+ // |dep|. Used to compute GetHardDeps(), and by code that needs to mirror
+ // that computation.
+ static bool ForwardsHardDeps(const Target* dep);
+
const std::vector<OutputFile>& GetOrderOnlyDeps(const Target* target) const {
return GetTargetOrderOnlyDeps(target)->order_only_deps;
}