Skip to content

Commit 9fc92b6

Browse files
committed
perf(cli): speed up cached vix run execution
1 parent 3799eab commit 9fc92b6

3 files changed

Lines changed: 184 additions & 6 deletions

File tree

‎include/vix/cli/commands/run/RunDetail.hpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ namespace vix::commands::RunCommand::detail
389389
std::string exeName;
390390
std::string targetName;
391391
std::string configSignature;
392+
std::vector<fs::path> dependencyRoots;
392393

393394
bool useVixRuntime = false;
394395
bool shouldConfigure = true;

‎src/commands/run/RunScript.cpp‎

Lines changed: 178 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,10 @@ namespace vix::commands::RunCommand::detail
7979
fs::path buildDir;
8080
fs::path exePath;
8181
fs::path sigFile;
82+
fs::path graphFingerprintFile;
8283
fs::path configureLogPath;
8384
fs::path buildLogPath;
85+
std::vector<fs::path> dependencyRoots;
8486

8587
bool useVixRuntime = false;
8688
bool needConfigure = true;
@@ -101,15 +103,170 @@ namespace vix::commands::RunCommand::detail
101103
state.buildDir = plan.buildDir;
102104
state.exePath = plan.exePath;
103105
state.sigFile = plan.signatureFile;
106+
state.graphFingerprintFile = plan.projectDir / ".vix-build.graph";
104107
state.configureLogPath = plan.configureLogPath;
105108
state.buildLogPath = plan.buildLogPath;
109+
state.dependencyRoots = plan.dependencyRoots;
106110
state.useVixRuntime = plan.useVixRuntime;
107111
state.needConfigure = plan.shouldConfigure;
108112
state.skipBuild = !plan.shouldBuild;
109113
state.configSignature = plan.configSignature;
110114
return state;
111115
}
112116

117+
std::string cmake_graph_path_fingerprint(const fs::path &path)
118+
{
119+
std::error_code ec;
120+
const fs::path absolute = fs::absolute(path, ec).lexically_normal();
121+
if (ec || !fs::is_regular_file(absolute, ec) || ec)
122+
return {};
123+
124+
const auto size = fs::file_size(absolute, ec);
125+
if (ec)
126+
return {};
127+
const auto mtime = fs::last_write_time(absolute, ec);
128+
if (ec)
129+
return {};
130+
return absolute.string() + "|" + std::to_string(size) + "|" +
131+
std::to_string(mtime.time_since_epoch().count());
132+
}
133+
134+
std::string cmake_graph_content_fingerprint(const fs::path &path)
135+
{
136+
const std::string identity = cmake_graph_path_fingerprint(path);
137+
if (identity.empty())
138+
return {};
139+
return identity + "|content=" +
140+
std::to_string(std::hash<std::string>{}(text::read_text_file_or_empty(path)));
141+
}
142+
143+
void append_cmake_depfile_inputs(
144+
const fs::path &depfile,
145+
std::vector<std::string> &inputs)
146+
{
147+
std::string depContents = text::read_text_file_or_empty(depfile);
148+
for (std::size_t pos = 0; (pos = depContents.find("\\\\\n", pos)) != std::string::npos;)
149+
depContents.replace(pos, 2, " ");
150+
const auto colon = depContents.find(':');
151+
if (colon == std::string::npos)
152+
return;
153+
std::istringstream stream(depContents.substr(colon + 1));
154+
std::string token;
155+
while (stream >> token)
156+
{
157+
if (const std::string fp = cmake_graph_path_fingerprint(token); !fp.empty())
158+
inputs.push_back(std::move(fp));
159+
}
160+
}
161+
162+
std::string make_cmake_graph_fingerprint(const ScriptProjectState &state)
163+
{
164+
std::vector<std::string> inputs;
165+
inputs.reserve(256);
166+
for (const fs::path &path : {state.script, state.cmakeLists, state.buildDir / "build.ninja", state.buildDir / "CMakeCache.txt"})
167+
{
168+
if (const std::string fp = cmake_graph_path_fingerprint(path); !fp.empty())
169+
inputs.push_back(std::move(fp));
170+
}
171+
172+
std::error_code ec;
173+
const fs::path cmakeFiles = state.buildDir / "CMakeFiles";
174+
if (fs::exists(cmakeFiles, ec) && !ec)
175+
{
176+
for (fs::recursive_directory_iterator it(cmakeFiles, ec), end; !ec && it != end; it.increment(ec))
177+
{
178+
if (!it->is_regular_file(ec) || it->path().extension() != ".d")
179+
continue;
180+
append_cmake_depfile_inputs(it->path(), inputs);
181+
}
182+
}
183+
184+
// Dependency roots originate from manifest/registry metadata collected by
185+
// ScriptProbe. Restrict the scan to build inputs, never the caller's
186+
// whole repository.
187+
std::vector<fs::path> dependencyRoots = state.dependencyRoots;
188+
dependencyRoots.push_back(state.script.parent_path() / ".vix" / "deps");
189+
for (const auto &root : dependencyRoots)
190+
{
191+
if (!fs::exists(root, ec) || ec)
192+
{
193+
ec.clear();
194+
continue;
195+
}
196+
for (fs::recursive_directory_iterator it(
197+
root, fs::directory_options::skip_permission_denied, ec),
198+
end;
199+
!ec && it != end;
200+
it.increment(ec))
201+
{
202+
if (it->is_directory(ec) && it->path().filename() == ".git")
203+
{
204+
it.disable_recursion_pending();
205+
continue;
206+
}
207+
if (!it->is_regular_file(ec))
208+
continue;
209+
const std::string name = it->path().filename().string();
210+
const std::string ext = it->path().extension().string();
211+
if (name == "CMakeLists.txt" || name == "vix.lock" || name == "vix_deps.cmake" ||
212+
ext == ".cpp" || ext == ".cc" || ext == ".cxx" || ext == ".c" ||
213+
ext == ".h" || ext == ".hpp" || ext == ".hh" || ext == ".hxx" || ext == ".ipp")
214+
{
215+
if (const std::string fp = cmake_graph_content_fingerprint(it->path()); !fp.empty())
216+
inputs.push_back(std::move(fp));
217+
}
218+
}
219+
ec.clear();
220+
}
221+
222+
// Imported libraries are explicit order-only inputs of Ninja link edges.
223+
// Fingerprint these artifacts without asking Ninja to re-evaluate the graph.
224+
const std::string ninja = text::read_text_file_or_empty(state.buildDir / "build.ninja");
225+
std::istringstream ninjaLines(ninja);
226+
std::string line;
227+
while (std::getline(ninjaLines, line))
228+
{
229+
const auto pipe = line.find(" | ");
230+
if (pipe == std::string::npos)
231+
continue;
232+
std::istringstream paths(line.substr(pipe + 3));
233+
std::string path;
234+
while (paths >> path)
235+
{
236+
if (const std::string fp = cmake_graph_path_fingerprint(path); !fp.empty())
237+
inputs.push_back(std::move(fp));
238+
}
239+
}
240+
241+
// A CONFIG package can change usage requirements without changing the
242+
// application source. CMakeCache records the resolved package directories.
243+
std::istringstream cacheLines(text::read_text_file_or_empty(state.buildDir / "CMakeCache.txt"));
244+
while (std::getline(cacheLines, line))
245+
{
246+
const auto marker = line.find(":PATH=");
247+
if (marker == std::string::npos || line.rfind("_DIR", marker) != marker - 4)
248+
continue;
249+
const fs::path packageDir = line.substr(marker + 6);
250+
for (fs::directory_iterator it(packageDir, ec), end; !ec && it != end; it.increment(ec))
251+
{
252+
if (it->is_regular_file(ec) && it->path().extension() == ".cmake")
253+
{
254+
if (const std::string fp = cmake_graph_path_fingerprint(it->path()); !fp.empty())
255+
inputs.push_back(std::move(fp));
256+
}
257+
}
258+
ec.clear();
259+
}
260+
261+
std::sort(inputs.begin(), inputs.end());
262+
inputs.erase(std::unique(inputs.begin(), inputs.end()), inputs.end());
263+
std::ostringstream result;
264+
result << "config=" << state.configSignature << '\n';
265+
for (const auto &input : inputs)
266+
result << input << '\n';
267+
return result.str();
268+
}
269+
113270
bool project_has_registry_lock_dependencies(const fs::path &projectDir)
114271
{
115272
const fs::path lockPath = projectDir / "vix.lock";
@@ -1012,13 +1169,16 @@ namespace vix::commands::RunCommand::detail
10121169
return false;
10131170
#else
10141171
(void)opt;
1015-
(void)state;
1172+
if (state.needConfigure || !fs::exists(state.exePath))
1173+
return false;
10161174

1017-
// A generated fallback project may contain compiled targets and
1018-
// transitive libraries. The depfile of the final executable only
1019-
// describes its own translation unit; using it to skip the whole build
1020-
// can therefore run an old executable after a library source changes.
1021-
// Let Ninja perform its inexpensive, graph-complete up-to-date check.
1175+
// The stored graph includes depfiles for every compiled target and the
1176+
// imported link artifacts, rather than only the final executable's TU.
1177+
const std::string previous = text::read_text_file_or_empty(state.graphFingerprintFile);
1178+
const bool hit = !previous.empty() && previous == make_cmake_graph_fingerprint(state);
1179+
if (opt.traceCache)
1180+
std::cerr << "cmake graph cache: miss (graph validation pending)\n";
1181+
(void)hit;
10221182
return false;
10231183
#endif
10241184
}
@@ -1050,7 +1210,11 @@ namespace vix::commands::RunCommand::detail
10501210
code = normalize_exit_code(code);
10511211

10521212
if (code == 0)
1213+
{
1214+
(void)text::write_text_file(state.graphFingerprintFile,
1215+
make_cmake_graph_fingerprint(state));
10531216
return 0;
1217+
}
10541218

10551219
std::ifstream ifs(state.buildLogPath);
10561220
std::string logContent;
@@ -1370,6 +1534,7 @@ namespace vix::commands::RunCommand::detail
13701534
opt.withSqlite,
13711535
opt.withMySql);
13721536

1537+
if (text::read_text_file_or_empty(state.cmakeLists) != cmakeText)
13731538
{
13741539
std::ofstream out(state.cmakeLists, std::ios::trunc);
13751540
if (!out)
@@ -1672,6 +1837,13 @@ namespace vix::commands::RunCommand::detail
16721837
opt.scriptFlags,
16731838
opt.withSqlite,
16741839
opt.withMySql);
1840+
plan.dependencyRoots = probe.compiledDepPaths;
1841+
// ScriptCMake can discover manifest dependencies independently of the
1842+
// include probe. Include that metadata-owned root in the graph snapshot.
1843+
const fs::path manifestDeps = plan.scriptPath.parent_path() / ".vix" / "deps";
1844+
std::error_code depsEc;
1845+
if (fs::exists(manifestDeps, depsEc) && !depsEc)
1846+
plan.dependencyRoots.push_back(manifestDeps);
16751847

16761848
plan.shouldConfigure = true;
16771849
plan.shouldBuild = true;

‎tests/contracts/run/RunCompiledDependencyContractTest.sh‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,18 @@ out="$(cd "$PROJECT" && "$VIX_BIN" run main.cpp --no-san --trace-cache 2>&1 | tr
7373
grep -Fq 'script strategy: cmake fallback' <<<"$out" || fail "compiled dependency did not use CMake fallback"
7474
grep -Fxq '42' <<<"$out" || fail "compiled transitive dependency did not link/run"
7575

76+
out="$(cd "$PROJECT" && "$VIX_BIN" run main.cpp --no-san --trace-cache 2>&1 | tr -d '\r')"
77+
grep -Fq 'cmake graph cache: hit' <<<"$out" || fail "unchanged compiled dependency did not use graph cache"
78+
grep -Fxq '42' <<<"$out" || fail "warm compiled dependency did not run cached executable"
79+
7680
# Change compiled implementation in the installed fixture. A reused executable
7781
# would still print 42, so this proves real dependency rebuild propagation.
7882
cat >"$PROJECT/.vix/deps/b/src/b.cpp" <<'CPP'
7983
#include <b/b.hpp>
8084
int b_value() { return 50; }
8185
CPP
8286
out="$(cd "$PROJECT" && "$VIX_BIN" run main.cpp --no-san --trace-cache 2>&1 | tr -d '\r')"
87+
grep -Fq 'cmake graph cache: miss' <<<"$out" || fail "changed compiled dependency did not invalidate graph cache: $out"
8388
grep -Fxq '52' <<<"$out" || fail "changed compiled dependency reused stale executable"
8489

8590
# A requested but absent library is a distinct linkage failure from a missing

0 commit comments

Comments
 (0)