/src/CMake/Source/cmInstrumentation.cxx
Line | Count | Source |
1 | | #include "cmInstrumentation.h" |
2 | | |
3 | | #include <algorithm> |
4 | | #include <chrono> |
5 | | #include <ctime> |
6 | | #include <iomanip> |
7 | | #include <iterator> |
8 | | #include <set> |
9 | | #include <sstream> |
10 | | #include <stdexcept> |
11 | | #include <utility> |
12 | | |
13 | | #include <cm/memory> |
14 | | #include <cm/optional> |
15 | | #include <cm/string_view> |
16 | | #include <cmext/algorithm> |
17 | | |
18 | | #include <cm3p/json/reader.h> |
19 | | #include <cm3p/json/version.h> |
20 | | #include <cm3p/json/writer.h> |
21 | | #include <cm3p/uv.h> |
22 | | |
23 | | #include "cmsys/Directory.hxx" |
24 | | #include "cmsys/FStream.hxx" |
25 | | #include "cmsys/RegularExpression.hxx" |
26 | | #include "cmsys/SystemInformation.hxx" |
27 | | |
28 | | #include "cmCMakePath.h" |
29 | | #include "cmCryptoHash.h" |
30 | | #include "cmFileLock.h" |
31 | | #include "cmFileLockResult.h" |
32 | | #include "cmGeneratedFileStream.h" |
33 | | #include "cmGeneratorTarget.h" |
34 | | #include "cmGlobalGenerator.h" |
35 | | #include "cmInstrumentationInterrupt.h" |
36 | | #include "cmInstrumentationQuery.h" |
37 | | #include "cmJSONState.h" |
38 | | #include "cmList.h" |
39 | | #include "cmLocalGenerator.h" |
40 | | #include "cmState.h" |
41 | | #include "cmStringAlgorithms.h" |
42 | | #include "cmSystemTools.h" |
43 | | #include "cmTargetTypes.h" |
44 | | #include "cmTimestamp.h" |
45 | | #include "cmUVProcessChain.h" |
46 | | #include "cmValue.h" |
47 | | #include "cmake.h" |
48 | | |
49 | | using LoadQueriesAfter = cmInstrumentation::LoadQueriesAfter; |
50 | | |
51 | | namespace { |
52 | | cmInstrumentationQuery::Version latestDataVersion = |
53 | | cmInstrumentationQuery::LatestDataVersion(); |
54 | | } |
55 | | |
56 | | std::map<std::string, std::string> cmInstrumentation::cdashSnippetsMap = { |
57 | | { |
58 | | "configure", |
59 | | "configure", |
60 | | }, |
61 | | { |
62 | | "generate", |
63 | | "configure", |
64 | | }, |
65 | | { |
66 | | "compile", |
67 | | "build", |
68 | | }, |
69 | | { |
70 | | "link", |
71 | | "build", |
72 | | }, |
73 | | { |
74 | | "custom", |
75 | | "build", |
76 | | }, |
77 | | { |
78 | | "build", |
79 | | "skip", |
80 | | }, |
81 | | { |
82 | | "cmakeBuild", |
83 | | "build", |
84 | | }, |
85 | | { |
86 | | "cmakeInstall", |
87 | | "build", |
88 | | }, |
89 | | { |
90 | | "install", |
91 | | "build", |
92 | | }, |
93 | | { |
94 | | "ctest", |
95 | | "build", |
96 | | }, |
97 | | { |
98 | | "test", |
99 | | "test", |
100 | | } |
101 | | }; |
102 | | |
103 | | cmInstrumentation::cmInstrumentation(std::string const& binary_dir, |
104 | | LoadQueriesAfter loadQueries) |
105 | 0 | { |
106 | 0 | this->binaryDir = binary_dir; |
107 | 0 | this->timingDirv1 = cmStrCat(this->binaryDir, "/.cmake/instrumentation/v1"); |
108 | 0 | this->cdashDir = cmStrCat(this->timingDirv1, "/cdash"); |
109 | 0 | this->dataDir = cmStrCat(this->timingDirv1, "/data"); |
110 | 0 | if (cm::optional<std::string> configDir = |
111 | 0 | cmSystemTools::GetCMakeConfigDirectory()) { |
112 | 0 | this->userTimingDirv1 = cmStrCat(configDir.value(), "/instrumentation/v1"); |
113 | 0 | } |
114 | 0 | if (loadQueries == LoadQueriesAfter::Yes) { |
115 | 0 | this->LoadQueries(); |
116 | 0 | } |
117 | 0 | } |
118 | | |
119 | | void cmInstrumentation::LoadQueries() |
120 | 0 | { |
121 | 0 | this->ResetQueries(); |
122 | 0 | auto const readJSONQueries = [this](std::string const& dir) { |
123 | 0 | if (cmSystemTools::FileIsDirectory(dir) && this->ReadJSONQueries(dir)) { |
124 | 0 | this->hasQuery = true; |
125 | 0 | } |
126 | 0 | }; |
127 | 0 | readJSONQueries(cmStrCat(this->timingDirv1, "/query")); |
128 | 0 | readJSONQueries(cmStrCat(this->timingDirv1, "/query/generated")); |
129 | 0 | if (!this->userTimingDirv1.empty()) { |
130 | 0 | readJSONQueries(cmStrCat(this->userTimingDirv1, "/query")); |
131 | 0 | } |
132 | 0 | } |
133 | | |
134 | | void cmInstrumentation::ResetQueries() |
135 | 0 | { |
136 | 0 | this->hasQuery = false; |
137 | 0 | this->options.clear(); |
138 | 0 | this->hooks.clear(); |
139 | 0 | this->callbacks.clear(); |
140 | 0 | this->queryFiles.clear(); |
141 | 0 | this->errorMsg.clear(); |
142 | 0 | } |
143 | | |
144 | | void cmInstrumentation::CheckCDashVariable() |
145 | 0 | { |
146 | 0 | std::string envVal; |
147 | 0 | if (cmSystemTools::GetEnv("CTEST_USE_INSTRUMENTATION", envVal) && |
148 | 0 | !cmIsOff(envVal)) { |
149 | 0 | std::set<cmInstrumentationQuery::Option> options_ = { |
150 | 0 | cmInstrumentationQuery::Option::CDashSubmit |
151 | 0 | }; |
152 | 0 | if (cmSystemTools::GetEnv("CTEST_USE_VERBOSE_INSTRUMENTATION", envVal) && |
153 | 0 | !cmIsOff(envVal)) { |
154 | 0 | options_.insert(cmInstrumentationQuery::Option::CDashVerbose); |
155 | 0 | } |
156 | 0 | std::set<cmInstrumentationQuery::Hook> hooks_; |
157 | 0 | this->WriteJSONQuery(latestDataVersion, options_, hooks_, {}); |
158 | 0 | } |
159 | 0 | } |
160 | | |
161 | | cmsys::SystemInformation& cmInstrumentation::GetSystemInformation() |
162 | 0 | { |
163 | 0 | if (!this->systemInformation) { |
164 | 0 | this->systemInformation = cm::make_unique<cmsys::SystemInformation>(); |
165 | 0 | } |
166 | 0 | return *this->systemInformation; |
167 | 0 | } |
168 | | |
169 | | bool cmInstrumentation::ReadJSONQueries(std::string const& directory) |
170 | 0 | { |
171 | 0 | cmsys::Directory d; |
172 | 0 | bool result = false; |
173 | 0 | if (d.Load(directory)) { |
174 | 0 | for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) { |
175 | 0 | std::string fpath = d.GetFilePath(i); |
176 | 0 | if (cmHasLiteralSuffix(fpath, ".json")) { |
177 | 0 | result = true; |
178 | 0 | this->ReadJSONQuery(fpath); |
179 | 0 | } |
180 | 0 | } |
181 | 0 | } |
182 | 0 | return result; |
183 | 0 | } |
184 | | |
185 | | void cmInstrumentation::ReadJSONQuery(std::string const& file) |
186 | 0 | { |
187 | 0 | auto query = cmInstrumentationQuery(); |
188 | 0 | query.ReadJSON(file, this->errorMsg, this->options, this->hooks, |
189 | 0 | this->callbacks); |
190 | 0 | if (this->HasOption(cmInstrumentationQuery::Option::CDashVerbose)) { |
191 | 0 | this->AddOption(cmInstrumentationQuery::Option::CDashSubmit); |
192 | 0 | } |
193 | 0 | if (this->HasOption(cmInstrumentationQuery::Option::CDashSubmit)) { |
194 | 0 | this->AddHook(cmInstrumentationQuery::Hook::PrepareForCDash); |
195 | 0 | this->AddOption(cmInstrumentationQuery::Option::DynamicSystemInformation); |
196 | 0 | } |
197 | 0 | if (!this->errorMsg.empty()) { |
198 | 0 | cmSystemTools::Error(cmStrCat( |
199 | 0 | "Could not load instrumentation queries from ", |
200 | 0 | cmSystemTools::GetParentDirectory(file), ":\n", this->errorMsg)); |
201 | 0 | } |
202 | 0 | } |
203 | | |
204 | | bool cmInstrumentation::HasErrors() const |
205 | 0 | { |
206 | 0 | return !this->errorMsg.empty(); |
207 | 0 | } |
208 | | |
209 | | void cmInstrumentation::WriteJSONQuery( |
210 | | cmInstrumentationQuery::Version dataVersion, |
211 | | std::set<cmInstrumentationQuery::Option> const& options_, |
212 | | std::set<cmInstrumentationQuery::Hook> const& hooks_, |
213 | | std::vector<std::vector<std::string>> const& callbacks_) |
214 | 0 | { |
215 | 0 | Json::Value root; |
216 | 0 | root["options"] = Json::arrayValue; |
217 | 0 | for (auto const& option : options_) { |
218 | 0 | root["options"].append(cmInstrumentationQuery::OptionString[option]); |
219 | 0 | } |
220 | 0 | root["hooks"] = Json::arrayValue; |
221 | 0 | for (auto const& hook : hooks_) { |
222 | 0 | root["hooks"].append(cmInstrumentationQuery::HookString[hook]); |
223 | 0 | } |
224 | 0 | root["callbacks"] = Json::arrayValue; |
225 | 0 | for (auto const& callback : callbacks_) { |
226 | 0 | root["callbacks"].append(cmInstrumentation::GetCommandStr(callback)); |
227 | 0 | } |
228 | 0 | this->WriteInstrumentationJson( |
229 | 0 | dataVersion, root, "query/generated", |
230 | 0 | cmStrCat("query-", this->writtenJsonQueries++, ".json")); |
231 | 0 | } |
232 | | |
233 | | void cmInstrumentation::AddCustomContent(std::string const& name, |
234 | | Json::Value const& contents) |
235 | 0 | { |
236 | 0 | this->customContent[name] = contents; |
237 | 0 | } |
238 | | |
239 | | void cmInstrumentation::WriteCMakeContent( |
240 | | std::unique_ptr<cmGlobalGenerator> const& gg) |
241 | 0 | { |
242 | 0 | Json::Value root; |
243 | 0 | root["targets"] = this->DumpTargets(gg); |
244 | 0 | root["custom"] = this->customContent; |
245 | 0 | root["project"] = |
246 | 0 | gg->GetCMakeInstance()->GetCacheDefinition("CMAKE_PROJECT_NAME").GetCStr(); |
247 | 0 | this->WriteInstrumentationJson( |
248 | 0 | latestDataVersion, root, "data/content", |
249 | 0 | cmStrCat("cmake-", this->ComputeSuffixTime(), ".json")); |
250 | 0 | } |
251 | | |
252 | | Json::Value cmInstrumentation::DumpTargets( |
253 | | std::unique_ptr<cmGlobalGenerator> const& gg) |
254 | 0 | { |
255 | 0 | Json::Value targets = Json::objectValue; |
256 | 0 | std::vector<cmGeneratorTarget*> targetList; |
257 | 0 | for (auto const& lg : gg->GetLocalGenerators()) { |
258 | 0 | cm::append(targetList, lg->GetGeneratorTargets()); |
259 | 0 | } |
260 | 0 | for (cmGeneratorTarget* gt : targetList) { |
261 | 0 | if (this->IsInstrumentableTargetType(gt->GetType())) { |
262 | 0 | Json::Value target = Json::objectValue; |
263 | 0 | auto labels = gt->GetSafeProperty("LABELS"); |
264 | 0 | target["labels"] = Json::arrayValue; |
265 | 0 | for (auto const& item : cmList(labels)) { |
266 | 0 | target["labels"].append(item); |
267 | 0 | } |
268 | 0 | target["type"] = cmState::GetTargetTypeName(gt->GetType()).c_str(); |
269 | 0 | targets[gt->GetName()] = target; |
270 | 0 | } |
271 | 0 | } |
272 | 0 | return targets; |
273 | 0 | } |
274 | | |
275 | | std::string cmInstrumentation::GetFileByTimestamp( |
276 | | cmInstrumentation::LatestOrOldest order, std::string const& dataSubdir, |
277 | | std::string const& exclude) |
278 | 0 | { |
279 | 0 | std::string fullDir = cmStrCat(this->dataDir, '/', dataSubdir); |
280 | 0 | std::string result; |
281 | 0 | if (cmSystemTools::FileExists(fullDir)) { |
282 | 0 | cmsys::Directory d; |
283 | 0 | if (d.Load(fullDir)) { |
284 | 0 | for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) { |
285 | 0 | std::string fname = d.GetFileName(i); |
286 | 0 | if (fname != "." && fname != ".." && fname != exclude && |
287 | 0 | (result.empty() || |
288 | 0 | (order == LatestOrOldest::Latest && fname > result) || |
289 | 0 | (order == LatestOrOldest::Oldest && fname < result))) { |
290 | 0 | result = fname; |
291 | 0 | } |
292 | 0 | } |
293 | 0 | } |
294 | 0 | } |
295 | 0 | return result; |
296 | 0 | } |
297 | | |
298 | | void cmInstrumentation::RemoveOldFiles(std::string const& dataSubdir) |
299 | 0 | { |
300 | 0 | std::string const dataSubdirPath = cmStrCat(this->dataDir, '/', dataSubdir); |
301 | 0 | std::string oldIndex = |
302 | 0 | this->GetFileByTimestamp(LatestOrOldest::Oldest, "index"); |
303 | 0 | if (!oldIndex.empty()) { |
304 | 0 | oldIndex = cmStrCat(this->dataDir, "/index/", oldIndex); |
305 | 0 | } |
306 | 0 | if (cmSystemTools::FileExists(dataSubdirPath)) { |
307 | 0 | std::string latestFile = |
308 | 0 | this->GetFileByTimestamp(LatestOrOldest::Latest, dataSubdir); |
309 | 0 | cmsys::Directory d; |
310 | 0 | if (d.Load(dataSubdirPath)) { |
311 | 0 | for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) { |
312 | 0 | std::string fname = d.GetFileName(i); |
313 | 0 | std::string fpath = d.GetFilePath(i); |
314 | 0 | if (fname != "." && fname != ".." && fname < latestFile) { |
315 | 0 | if (!oldIndex.empty()) { |
316 | 0 | int compare; |
317 | 0 | cmSystemTools::FileTimeCompare(oldIndex, fpath, &compare); |
318 | 0 | if (compare == 1) { |
319 | 0 | continue; |
320 | 0 | } |
321 | 0 | } |
322 | 0 | cmSystemTools::RemoveFile(fpath); |
323 | 0 | } |
324 | 0 | } |
325 | 0 | } |
326 | 0 | } |
327 | 0 | } |
328 | | |
329 | | void cmInstrumentation::ClearGeneratedQueries() |
330 | 0 | { |
331 | 0 | std::string dir = cmStrCat(this->timingDirv1, "/query/generated"); |
332 | 0 | if (cmSystemTools::FileIsDirectory(dir)) { |
333 | 0 | cmSystemTools::RemoveADirectory(dir); |
334 | 0 | } |
335 | 0 | this->writtenJsonQueries = 0; |
336 | 0 | } |
337 | | |
338 | | bool cmInstrumentation::HasQuery() const |
339 | 0 | { |
340 | 0 | return this->hasQuery; |
341 | 0 | } |
342 | | |
343 | | bool cmInstrumentation::HasOption(cmInstrumentationQuery::Option option) const |
344 | 0 | { |
345 | 0 | return (this->options.find(option) != this->options.end()); |
346 | 0 | } |
347 | | |
348 | | bool cmInstrumentation::HasHook(cmInstrumentationQuery::Hook hook) const |
349 | 0 | { |
350 | 0 | return (this->hooks.find(hook) != this->hooks.end()); |
351 | 0 | } |
352 | | |
353 | | int cmInstrumentation::CollectTimingData(cmInstrumentationQuery::Hook hook) |
354 | 0 | { |
355 | | // Don't run collection if hook is disabled |
356 | 0 | if (hook != cmInstrumentationQuery::Hook::Manual && !this->HasHook(hook)) { |
357 | 0 | return 0; |
358 | 0 | } |
359 | | |
360 | 0 | this->LockIndexing(); |
361 | | |
362 | | // Touch index file immediately to claim snippets |
363 | 0 | std::string suffix_time = ComputeSuffixTime(); |
364 | 0 | std::string const& index_name = cmStrCat("index-", suffix_time, ".json"); |
365 | 0 | std::string index_path = cmStrCat(this->dataDir, "/index/", index_name); |
366 | 0 | cmSystemTools::Touch(index_path, true); |
367 | | |
368 | | // Gather Snippets |
369 | 0 | using snippet = std::pair<std::string, std::string>; |
370 | 0 | std::vector<snippet> files; |
371 | 0 | cmsys::Directory d; |
372 | 0 | std::string last_index_name = |
373 | 0 | this->GetFileByTimestamp(LatestOrOldest::Latest, "index", index_name); |
374 | 0 | std::string last_index_path = |
375 | 0 | cmStrCat(this->dataDir, "/index/", last_index_name); |
376 | 0 | if (d.Load(this->dataDir)) { |
377 | 0 | for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) { |
378 | 0 | std::string fpath = d.GetFilePath(i); |
379 | 0 | std::string const& fname = d.GetFileName(i); |
380 | 0 | if (fname.rfind('.', 0) == 0 || d.FileIsDirectory(i)) { |
381 | 0 | continue; |
382 | 0 | } |
383 | 0 | files.push_back(snippet(fname, std::move(fpath))); |
384 | 0 | } |
385 | 0 | } |
386 | | |
387 | | // Build Json Object |
388 | 0 | Json::Value index(Json::objectValue); |
389 | 0 | index["snippets"] = Json::arrayValue; |
390 | 0 | index["hook"] = cmInstrumentationQuery::HookString[hook]; |
391 | 0 | index["dataDir"] = this->dataDir; |
392 | 0 | index["buildDir"] = this->binaryDir; |
393 | 0 | if (this->HasOption( |
394 | 0 | cmInstrumentationQuery::Option::StaticSystemInformation)) { |
395 | 0 | this->InsertStaticSystemInformation(index); |
396 | 0 | } |
397 | |
|
398 | 0 | for (auto const& file : files) { |
399 | 0 | if (last_index_name.empty()) { |
400 | 0 | index["snippets"].append(file.first); |
401 | 0 | } else { |
402 | 0 | int compare; |
403 | 0 | cmSystemTools::FileTimeCompare(file.second, last_index_path, &compare); |
404 | 0 | if (compare == 1) { |
405 | 0 | index["snippets"].append(file.first); |
406 | 0 | } |
407 | 0 | } |
408 | 0 | } |
409 | | |
410 | | // Parse snippets into the Google trace file |
411 | 0 | if (this->HasOption(cmInstrumentationQuery::Option::Trace)) { |
412 | 0 | std::string trace_name = cmStrCat("trace-", suffix_time, ".json"); |
413 | 0 | this->WriteTraceFile(index, trace_name); |
414 | 0 | index["trace"] = cmStrCat("trace/", trace_name); |
415 | 0 | } |
416 | | |
417 | | // Write index file |
418 | 0 | this->WriteInstrumentationJson(latestDataVersion, index, "data/index", |
419 | 0 | index_name); |
420 | | |
421 | | // Execute callbacks |
422 | 0 | for (auto const& cb : this->callbacks) { |
423 | 0 | cmSystemTools::RunSingleCommand( |
424 | 0 | cmStrCat(cb.Command, " \"", index_path, '"'), nullptr, nullptr, nullptr, |
425 | 0 | nullptr, cmSystemTools::OUTPUT_PASSTHROUGH); |
426 | 0 | } |
427 | | |
428 | | // Special case for CDash collation |
429 | 0 | if (this->HasOption(cmInstrumentationQuery::Option::CDashSubmit)) { |
430 | 0 | this->PrepareDataForCDash(this->dataDir, index_path); |
431 | 0 | } |
432 | | |
433 | | // Delete files |
434 | 0 | for (auto const& f : index["snippets"]) { |
435 | 0 | cmSystemTools::RemoveFile(cmStrCat(this->dataDir, '/', f.asString())); |
436 | 0 | } |
437 | 0 | cmSystemTools::RemoveFile(index_path); |
438 | | |
439 | | // Delete old content and trace files |
440 | 0 | this->RemoveOldFiles("content"); |
441 | 0 | this->RemoveOldFiles("compile-trace"); |
442 | 0 | this->RemoveOldFiles("trace"); |
443 | |
|
444 | 0 | this->indexLock.Release(); |
445 | |
|
446 | 0 | return 0; |
447 | 0 | } |
448 | | |
449 | | void cmInstrumentation::InsertDynamicSystemInformation( |
450 | | Json::Value& root, std::string const& prefix) |
451 | 0 | { |
452 | 0 | Json::Value data; |
453 | 0 | double memory; |
454 | 0 | double load; |
455 | 0 | this->GetDynamicSystemInformation(memory, load); |
456 | 0 | if (!root.isMember("dynamicSystemInformation")) { |
457 | 0 | root["dynamicSystemInformation"] = Json::objectValue; |
458 | 0 | } |
459 | 0 | root["dynamicSystemInformation"][cmStrCat(prefix, "HostMemoryUsed")] = |
460 | 0 | memory; |
461 | 0 | root["dynamicSystemInformation"][cmStrCat(prefix, "CPULoadAverage")] = |
462 | 0 | load > 0 ? Json::Value(load) : Json::nullValue; |
463 | 0 | } |
464 | | |
465 | | void cmInstrumentation::GetDynamicSystemInformation(double& memory, |
466 | | double& load) |
467 | 0 | { |
468 | 0 | cmsys::SystemInformation& info = this->GetSystemInformation(); |
469 | 0 | if (!this->ranSystemChecks) { |
470 | 0 | info.RunCPUCheck(); |
471 | 0 | info.RunMemoryCheck(); |
472 | 0 | this->ranSystemChecks = true; |
473 | 0 | } |
474 | 0 | memory = (double)info.GetHostMemoryUsed(); |
475 | 0 | load = info.GetLoadAverage(); |
476 | 0 | } |
477 | | |
478 | | void cmInstrumentation::InsertStaticSystemInformation(Json::Value& root) |
479 | 0 | { |
480 | 0 | cmsys::SystemInformation& info = this->GetSystemInformation(); |
481 | 0 | if (!this->ranOSCheck) { |
482 | 0 | info.RunOSCheck(); |
483 | 0 | this->ranOSCheck = true; |
484 | 0 | } |
485 | 0 | if (!this->ranSystemChecks) { |
486 | 0 | info.RunCPUCheck(); |
487 | 0 | info.RunMemoryCheck(); |
488 | 0 | this->ranSystemChecks = true; |
489 | 0 | } |
490 | 0 | Json::Value infoRoot; |
491 | 0 | infoRoot["familyId"] = info.GetFamilyID(); |
492 | 0 | infoRoot["hostname"] = info.GetHostname(); |
493 | 0 | infoRoot["is64Bits"] = info.Is64Bits(); |
494 | 0 | infoRoot["modelId"] = info.GetModelID(); |
495 | 0 | infoRoot["modelName"] = info.GetModelName(); |
496 | 0 | infoRoot["numberOfLogicalCPU"] = info.GetNumberOfLogicalCPU(); |
497 | 0 | infoRoot["numberOfPhysicalCPU"] = info.GetNumberOfPhysicalCPU(); |
498 | 0 | infoRoot["OSName"] = info.GetOSName(); |
499 | 0 | infoRoot["OSPlatform"] = info.GetOSPlatform(); |
500 | 0 | infoRoot["OSRelease"] = info.GetOSRelease(); |
501 | 0 | infoRoot["OSVersion"] = info.GetOSVersion(); |
502 | 0 | infoRoot["processorAPICID"] = info.GetProcessorAPICID(); |
503 | 0 | infoRoot["processorCacheSize"] = info.GetProcessorCacheSize(); |
504 | 0 | infoRoot["processorClockFrequency"] = |
505 | 0 | (double)info.GetProcessorClockFrequency(); |
506 | 0 | infoRoot["processorName"] = info.GetExtendedProcessorName(); |
507 | 0 | infoRoot["totalPhysicalMemory"] = |
508 | 0 | static_cast<Json::Value::UInt64>(info.GetTotalPhysicalMemory()); |
509 | 0 | infoRoot["totalVirtualMemory"] = |
510 | 0 | static_cast<Json::Value::UInt64>(info.GetTotalVirtualMemory()); |
511 | 0 | infoRoot["vendorID"] = info.GetVendorID(); |
512 | 0 | infoRoot["vendorString"] = info.GetVendorString(); |
513 | | |
514 | | // Record fields unable to be determined as null JSON objects. |
515 | 0 | for (std::string const& field : infoRoot.getMemberNames()) { |
516 | 0 | if ((infoRoot[field].isNumeric() && infoRoot[field].asInt64() <= 0) || |
517 | 0 | (infoRoot[field].isString() && infoRoot[field].asString().empty())) { |
518 | 0 | infoRoot[field] = Json::nullValue; |
519 | 0 | } |
520 | 0 | } |
521 | 0 | root["staticSystemInformation"] = infoRoot; |
522 | 0 | } |
523 | | |
524 | | void cmInstrumentation::InsertTimingData( |
525 | | Json::Value& root, std::chrono::steady_clock::time_point steadyStart, |
526 | | std::chrono::system_clock::time_point systemStart) |
527 | 0 | { |
528 | 0 | uint64_t timeStart = std::chrono::duration_cast<std::chrono::milliseconds>( |
529 | 0 | systemStart.time_since_epoch()) |
530 | 0 | .count(); |
531 | 0 | uint64_t duration = std::chrono::duration_cast<std::chrono::milliseconds>( |
532 | 0 | std::chrono::steady_clock::now() - steadyStart) |
533 | 0 | .count(); |
534 | 0 | root["timeStart"] = static_cast<Json::Value::UInt64>(timeStart); |
535 | 0 | root["duration"] = static_cast<Json::Value::UInt64>(duration); |
536 | 0 | } |
537 | | |
538 | | Json::Value cmInstrumentation::ReadJsonSnippet(std::string const& file_name) |
539 | 0 | { |
540 | 0 | Json::CharReaderBuilder builder; |
541 | 0 | builder["collectComments"] = false; |
542 | 0 | cmsys::ifstream ftmp( |
543 | 0 | cmStrCat(this->timingDirv1, "/data/", file_name).c_str()); |
544 | 0 | Json::Value snippetData; |
545 | 0 | builder["collectComments"] = false; |
546 | |
|
547 | 0 | if (!Json::parseFromStream(builder, ftmp, &snippetData, nullptr)) { |
548 | | #if JSONCPP_VERSION_HEXA < 0x01070300 |
549 | | snippetData = Json::Value::null; |
550 | | #else |
551 | 0 | snippetData = Json::Value::nullSingleton(); |
552 | 0 | #endif |
553 | 0 | } |
554 | |
|
555 | 0 | ftmp.close(); |
556 | 0 | return snippetData; |
557 | 0 | } |
558 | | |
559 | | void cmInstrumentation::WriteInstrumentationJson( |
560 | | cmInstrumentationQuery::Version version, Json::Value& root, |
561 | | std::string const& subdir, std::string const& file_name, Atomic atomic) |
562 | 0 | { |
563 | 0 | root["version"] = Json::objectValue; |
564 | 0 | root["version"]["major"] = version.Major; |
565 | 0 | root["version"]["minor"] = version.Minor; |
566 | |
|
567 | 0 | Json::StreamWriterBuilder wbuilder; |
568 | 0 | wbuilder["indentation"] = "\t"; |
569 | 0 | std::unique_ptr<Json::StreamWriter> JsonWriter = |
570 | 0 | std::unique_ptr<Json::StreamWriter>(wbuilder.newStreamWriter()); |
571 | 0 | std::string const& directory = cmStrCat(this->timingDirv1, '/', subdir); |
572 | 0 | cmSystemTools::MakeDirectory(directory); |
573 | 0 | std::string const file_path = cmStrCat(directory, '/', file_name); |
574 | |
|
575 | 0 | if (atomic == Atomic::Yes) { |
576 | | // Write to a temporary file and atomically rename it into place, so that |
577 | | // an interrupt during the write cannot leave a truncated snippet. |
578 | 0 | cmGeneratedFileStream ftmp(file_path); |
579 | 0 | if (!ftmp) { |
580 | 0 | throw std::runtime_error(std::string("Unable to open: ") + file_name); |
581 | 0 | } |
582 | 0 | try { |
583 | 0 | JsonWriter->write(root, &ftmp); |
584 | 0 | ftmp << "\n"; |
585 | | // The atomic rename happens when the stream is closed/destroyed. |
586 | 0 | } catch (std::ios_base::failure& fail) { |
587 | 0 | cmSystemTools::Error(cmStrCat("Failed to write JSON: ", fail.what())); |
588 | 0 | } catch (...) { |
589 | 0 | cmSystemTools::Error("Error writing JSON output for instrumentation."); |
590 | 0 | } |
591 | 0 | return; |
592 | 0 | } |
593 | | |
594 | 0 | cmsys::ofstream ftmp(file_path.c_str()); |
595 | 0 | if (!ftmp.good()) { |
596 | 0 | throw std::runtime_error(std::string("Unable to open: ") + file_name); |
597 | 0 | } |
598 | | |
599 | 0 | try { |
600 | 0 | JsonWriter->write(root, &ftmp); |
601 | 0 | ftmp << "\n"; |
602 | 0 | ftmp.close(); |
603 | 0 | } catch (std::ios_base::failure& fail) { |
604 | 0 | cmSystemTools::Error(cmStrCat("Failed to write JSON: ", fail.what())); |
605 | 0 | } catch (...) { |
606 | 0 | cmSystemTools::Error("Error writing JSON output for instrumentation."); |
607 | 0 | } |
608 | 0 | } |
609 | | |
610 | | std::string cmInstrumentation::InstrumentTest( |
611 | | std::string const& name, std::string const& command, |
612 | | std::vector<std::string> const& args, int64_t result, |
613 | | std::chrono::steady_clock::time_point steadyStart, |
614 | | std::chrono::system_clock::time_point systemStart, std::string config, |
615 | | cm::optional<std::string> output) |
616 | 0 | { |
617 | | // Store command info |
618 | 0 | Json::Value root(this->preTestStats); |
619 | 0 | std::string command_str = cmStrCat(command, ' ', GetCommandStr(args)); |
620 | 0 | root["command"] = command_str; |
621 | 0 | root["role"] = "test"; |
622 | 0 | root["testName"] = name; |
623 | 0 | root["result"] = static_cast<Json::Value::Int64>(result); |
624 | 0 | root["config"] = config; |
625 | 0 | root["workingDir"] = cmSystemTools::GetLogicalWorkingDirectory(); |
626 | 0 | if (this->HasOption(cmInstrumentationQuery::Option::CaptureOutput)) { |
627 | 0 | root["stdout"] = output ? *output : ""; |
628 | 0 | root["stderr"] = ""; |
629 | 0 | } |
630 | | |
631 | | // Post-Command |
632 | 0 | this->InsertTimingData(root, steadyStart, systemStart); |
633 | 0 | if (this->HasOption( |
634 | 0 | cmInstrumentationQuery::Option::DynamicSystemInformation)) { |
635 | 0 | this->InsertDynamicSystemInformation(root, "after"); |
636 | 0 | } |
637 | |
|
638 | 0 | cmsys::SystemInformation& info = this->GetSystemInformation(); |
639 | 0 | std::chrono::system_clock::time_point endTime = |
640 | 0 | systemStart + std::chrono::milliseconds(root["duration"].asUInt64()); |
641 | 0 | std::string file_name = cmStrCat( |
642 | 0 | "test-", |
643 | 0 | this->ComputeSuffixHash(cmStrCat(command_str, info.GetProcessId())), '-', |
644 | 0 | this->ComputeSuffixTime(endTime), ".json"); |
645 | 0 | this->WriteInstrumentationJson(latestDataVersion, root, "data", file_name); |
646 | 0 | return file_name; |
647 | 0 | } |
648 | | |
649 | | void cmInstrumentation::GetPreTestStats() |
650 | 0 | { |
651 | 0 | if (this->HasOption( |
652 | 0 | cmInstrumentationQuery::Option::DynamicSystemInformation)) { |
653 | 0 | this->InsertDynamicSystemInformation(this->preTestStats, "before"); |
654 | 0 | } |
655 | 0 | } |
656 | | |
657 | | int cmInstrumentation::InstrumentCommand( |
658 | | std::string command_type, std::vector<std::string> const& command, |
659 | | std::function<cmInstrumentation::CommandResult()> const& callback, |
660 | | cm::optional<std::map<std::string, std::string>> data, |
661 | | cm::optional<std::map<std::string, std::string>> arrayData, |
662 | | LoadQueriesAfter reloadQueriesAfterCommand) |
663 | 0 | { |
664 | | |
665 | | // Always begin gathering data for configure in case cmake_instrumentation |
666 | | // command creates a query |
667 | 0 | if (!this->hasQuery && reloadQueriesAfterCommand == LoadQueriesAfter::No) { |
668 | 0 | return callback().ExitCode; |
669 | 0 | } |
670 | | |
671 | | // Store command info |
672 | 0 | Json::Value root(Json::objectValue); |
673 | 0 | Json::Value commandInfo(Json::objectValue); |
674 | 0 | std::string command_str = GetCommandStr(command); |
675 | |
|
676 | 0 | if (!command_str.empty()) { |
677 | 0 | root["command"] = command_str; |
678 | 0 | } |
679 | 0 | root["role"] = command_type; |
680 | 0 | root["workingDir"] = cmSystemTools::GetLogicalWorkingDirectory(); |
681 | |
|
682 | 0 | if (data.has_value()) { |
683 | 0 | for (auto const& item : data.value()) { |
684 | 0 | if (item.first == "role" && !item.second.empty()) { |
685 | 0 | command_type = item.second; |
686 | 0 | root["role"] = command_type; |
687 | 0 | } else if (item.first == "showOnly") { |
688 | 0 | root[item.first] = item.second == "1" ? true : false; |
689 | 0 | } else if (!item.second.empty()) { |
690 | 0 | root[item.first] = item.second; |
691 | 0 | } |
692 | 0 | } |
693 | 0 | } |
694 | 0 | if (arrayData.has_value()) { |
695 | 0 | for (auto const& item : arrayData.value()) { |
696 | 0 | root[item.first] = Json::arrayValue; |
697 | 0 | std::stringstream ss(item.second); |
698 | 0 | std::string element; |
699 | 0 | while (getline(ss, element, ',')) { |
700 | 0 | root[item.first].append(element); |
701 | 0 | } |
702 | 0 | } |
703 | 0 | } |
704 | | // Create empty config entry if config not found |
705 | 0 | if (!root.isMember("config") && |
706 | 0 | (command_type == "compile" || command_type == "link" || |
707 | 0 | command_type == "custom" || command_type == "install")) { |
708 | 0 | root["config"] = ""; |
709 | 0 | } |
710 | | |
711 | | // Check existing compile trace json to check for modifications |
712 | 0 | std::string compileTraceFile; |
713 | 0 | if (this->HasOption(cmInstrumentationQuery::Option::CompileTrace) && |
714 | 0 | command_type == "compile") { |
715 | 0 | compileTraceFile = this->GetCompileTraceFile( |
716 | 0 | command, root["outputs"], root["workingDir"].asString()); |
717 | 0 | } |
718 | 0 | long int oldCompileTraceTimestamp = !compileTraceFile.empty() && |
719 | 0 | cmSystemTools::FileExists(compileTraceFile, true) |
720 | 0 | ? cmSystemTools::ModifiedTime(compileTraceFile) |
721 | 0 | : -1; |
722 | | |
723 | | // Pre-Command |
724 | 0 | auto steady_start = std::chrono::steady_clock::now(); |
725 | 0 | auto system_start = std::chrono::system_clock::now(); |
726 | 0 | double preConfigureMemory = 0; |
727 | 0 | double preConfigureLoad = 0; |
728 | 0 | if (this->HasOption( |
729 | 0 | cmInstrumentationQuery::Option::DynamicSystemInformation)) { |
730 | 0 | this->InsertDynamicSystemInformation(root, "before"); |
731 | 0 | } else if (reloadQueriesAfterCommand == LoadQueriesAfter::Yes) { |
732 | 0 | this->GetDynamicSystemInformation(preConfigureMemory, preConfigureLoad); |
733 | 0 | } |
734 | | |
735 | | // Execute Command |
736 | 0 | cmInstrumentation::CommandResult callbackResult = callback(); |
737 | 0 | int ret = callbackResult.ExitCode; |
738 | 0 | if (this->HasOption(cmInstrumentationQuery::Option::CaptureOutput)) { |
739 | 0 | if (callbackResult.StdOut) { |
740 | 0 | root["stdout"] = *callbackResult.StdOut; |
741 | 0 | } |
742 | 0 | if (callbackResult.StdErr) { |
743 | 0 | root["stderr"] = *callbackResult.StdErr; |
744 | 0 | } |
745 | 0 | } |
746 | | |
747 | | // Exit early if configure didn't generate a query |
748 | 0 | if (reloadQueriesAfterCommand == LoadQueriesAfter::Yes) { |
749 | 0 | this->LoadQueries(); |
750 | 0 | if (!this->HasQuery()) { |
751 | 0 | return ret; |
752 | 0 | } |
753 | 0 | if (this->HasOption( |
754 | 0 | cmInstrumentationQuery::Option::DynamicSystemInformation)) { |
755 | 0 | root["dynamicSystemInformation"] = Json::objectValue; |
756 | 0 | root["dynamicSystemInformation"]["beforeHostMemoryUsed"] = |
757 | 0 | preConfigureMemory; |
758 | 0 | root["dynamicSystemInformation"]["beforeCPULoadAverage"] = |
759 | 0 | preConfigureLoad; |
760 | 0 | } |
761 | 0 | } |
762 | | |
763 | | // Post-Command |
764 | 0 | this->InsertTimingData(root, steady_start, system_start); |
765 | 0 | if (this->HasOption( |
766 | 0 | cmInstrumentationQuery::Option::DynamicSystemInformation)) { |
767 | 0 | this->InsertDynamicSystemInformation(root, "after"); |
768 | 0 | } |
769 | | |
770 | | // See SpawnBuildDaemon(); this data is currently meaningless for build. |
771 | 0 | root["result"] = command_type == "build" ? Json::nullValue : ret; |
772 | | |
773 | | // If the command was interrupted (e.g. by Ctrl+C), record the signal number |
774 | | // that stopped it, so consumers can distinguish an interrupted command from |
775 | | // one that ran to completion. Omitted when no interrupt occurred; only a |
776 | | // command wrapped by HandleInterrupt can observe a pending signal here. |
777 | 0 | int sig = cmInstrumentationInterrupt::PendingInterruptSignal(); |
778 | 0 | if (sig != 0) { |
779 | 0 | root["interruptSignal"] = sig; |
780 | 0 | } |
781 | | |
782 | | // Output Sizes |
783 | 0 | if (root.isMember("outputs")) { |
784 | 0 | root["outputSizes"] = Json::arrayValue; |
785 | 0 | for (auto const& output : root["outputs"]) { |
786 | 0 | root["outputSizes"].append( |
787 | 0 | static_cast<Json::Value::UInt64>(cmSystemTools::FileLength( |
788 | 0 | cmStrCat(this->binaryDir, '/', output.asCString())))); |
789 | 0 | } |
790 | 0 | } |
791 | |
|
792 | 0 | auto addCMakeContent = [this](Json::Value& root_) -> void { |
793 | 0 | std::string contentFile = |
794 | 0 | this->GetFileByTimestamp(LatestOrOldest::Latest, "content"); |
795 | 0 | if (!contentFile.empty()) { |
796 | 0 | root_["cmakeContent"] = cmStrCat("content/", contentFile); |
797 | 0 | } |
798 | 0 | }; |
799 | | // Don't insert path to CMake content until generate time |
800 | 0 | if (command_type != "configure") { |
801 | 0 | addCMakeContent(root); |
802 | 0 | } |
803 | | |
804 | | // Compute file name properties |
805 | 0 | std::chrono::system_clock::time_point endTime = |
806 | 0 | system_start + std::chrono::milliseconds(root["duration"].asUInt64()); |
807 | 0 | cmsys::SystemInformation& info = this->GetSystemInformation(); |
808 | 0 | std::string const commandHash = |
809 | 0 | this->ComputeSuffixHash(cmStrCat(command_str, info.GetProcessId())); |
810 | 0 | std::string const suffixTime = this->ComputeSuffixTime(endTime); |
811 | | |
812 | | // Compile Trace |
813 | 0 | if (this->HasOption(cmInstrumentationQuery::Option::CompileTrace) && |
814 | 0 | command_type == "compile") { |
815 | 0 | this->CollectCompileTraceFile(root, compileTraceFile, |
816 | 0 | oldCompileTraceTimestamp, commandHash, |
817 | 0 | suffixTime); |
818 | 0 | } |
819 | | |
820 | | // Write JSON |
821 | 0 | std::string const& file_name = |
822 | 0 | cmStrCat(command_type, '-', commandHash, '-', suffixTime, ".json"); |
823 | | |
824 | | // Don't write configure snippet until generate time |
825 | 0 | if (command_type == "configure") { |
826 | 0 | this->configureSnippetData[file_name] = root; |
827 | 0 | } else { |
828 | | // Add reference to CMake content and write out configure snippet after |
829 | | // generate |
830 | 0 | if (command_type == "generate") { |
831 | 0 | for (auto it = this->configureSnippetData.begin(); |
832 | 0 | it != this->configureSnippetData.end(); ++it) { |
833 | 0 | if (std::next(it) != this->configureSnippetData.end()) { |
834 | 0 | it->second["cmakeContent"] = Json::nullValue; |
835 | 0 | } else { |
836 | 0 | addCMakeContent(it->second); |
837 | 0 | } |
838 | 0 | this->WriteInstrumentationJson(latestDataVersion, it->second, "data", |
839 | 0 | it->first); |
840 | 0 | } |
841 | 0 | this->configureSnippetData.clear(); |
842 | 0 | } |
843 | | // Write the cmakeBuild/cmakeInstall envelope atomically (temp file + |
844 | | // rename). This is the snippet flushed while unwinding from a user |
845 | | // interrupt, where a second Ctrl+C could otherwise truncate it mid-write; |
846 | | // the atomic write guarantees it is either absent or complete. Per-step |
847 | | // snippets are never flushed under interrupt and are left non-atomic. |
848 | 0 | bool const atomicEnvelope = |
849 | 0 | command_type == "cmakeBuild" || command_type == "cmakeInstall"; |
850 | 0 | this->WriteInstrumentationJson(latestDataVersion, root, "data", file_name, |
851 | 0 | atomicEnvelope ? Atomic::Yes : Atomic::No); |
852 | 0 | } |
853 | 0 | return ret; |
854 | 0 | } |
855 | | |
856 | | std::string cmInstrumentation::GetCommandStr( |
857 | | std::vector<std::string> const& args) |
858 | 0 | { |
859 | 0 | std::string command_str; |
860 | 0 | for (size_t i = 0; i < args.size(); ++i) { |
861 | 0 | if (args[i].find(' ') != std::string::npos) { |
862 | 0 | command_str = cmStrCat(command_str, '"', args[i], '"'); |
863 | 0 | } else { |
864 | 0 | command_str = cmStrCat(command_str, args[i]); |
865 | 0 | } |
866 | 0 | if (i < args.size() - 1) { |
867 | 0 | command_str = cmStrCat(command_str, ' '); |
868 | 0 | } |
869 | 0 | } |
870 | 0 | return command_str; |
871 | 0 | } |
872 | | |
873 | | std::string cmInstrumentation::ComputeSuffixHash( |
874 | | std::string const& command_str) |
875 | 0 | { |
876 | 0 | cmCryptoHash hasher(cmCryptoHash::AlgoSHA3_256); |
877 | 0 | std::string hash = hasher.HashString(command_str); |
878 | 0 | hash.resize(20, '0'); |
879 | 0 | return hash; |
880 | 0 | } |
881 | | |
882 | | std::string cmInstrumentation::ComputeSuffixTime( |
883 | | cm::optional<std::chrono::system_clock::time_point> time) |
884 | 0 | { |
885 | 0 | std::chrono::milliseconds ms = |
886 | 0 | std::chrono::duration_cast<std::chrono::milliseconds>( |
887 | 0 | (time.has_value() ? time.value() : std::chrono::system_clock::now()) |
888 | 0 | .time_since_epoch()); |
889 | 0 | std::chrono::seconds s = |
890 | 0 | std::chrono::duration_cast<std::chrono::seconds>(ms); |
891 | |
|
892 | 0 | std::time_t ts = s.count(); |
893 | 0 | std::size_t tms = ms.count() % 1000; |
894 | |
|
895 | 0 | cmTimestamp cmts; |
896 | 0 | std::ostringstream ss; |
897 | 0 | ss << cmts.CreateTimestampFromTimeT(ts, "%Y-%m-%dT%H-%M-%S", true) << '-' |
898 | 0 | << std::setfill('0') << std::setw(4) << tms; |
899 | 0 | return ss.str(); |
900 | 0 | } |
901 | | |
902 | | bool cmInstrumentation::IsInstrumentableTargetType(cm::TargetType type) |
903 | 0 | { |
904 | 0 | return type == cm::TargetType::EXECUTABLE || |
905 | 0 | type == cm::TargetType::SHARED_LIBRARY || |
906 | 0 | type == cm::TargetType::STATIC_LIBRARY || |
907 | 0 | type == cm::TargetType::MODULE_LIBRARY || |
908 | 0 | type == cm::TargetType::OBJECT_LIBRARY; |
909 | 0 | } |
910 | | |
911 | | /* |
912 | | * Called by ctest --start-instrumentation. |
913 | | * |
914 | | * This creates a detached process which waits for the parent process (i.e., |
915 | | * the build system) to die before running the postBuild hook. In this way, the |
916 | | * postBuild hook triggers after every invocation of the build system, |
917 | | * regardless of whether the build passed or failed. |
918 | | */ |
919 | | int cmInstrumentation::SpawnBuildDaemon() |
920 | 0 | { |
921 | | // Do not inherit handles from the parent process, so that the daemon is |
922 | | // fully detached. This helps prevent deadlock between the two. |
923 | 0 | uv_disable_stdio_inheritance(); |
924 | | |
925 | | // preBuild Hook |
926 | 0 | if (this->LockBuildDaemon()) { |
927 | | // Release lock before spawning the build daemon, to prevent blocking it. |
928 | 0 | this->buildLock.Release(); |
929 | 0 | this->CollectTimingData(cmInstrumentationQuery::Hook::PreBuild); |
930 | 0 | } |
931 | | |
932 | | // postBuild Hook |
933 | 0 | auto ppid = uv_os_getppid(); |
934 | 0 | if (ppid) { |
935 | 0 | std::vector<std::string> args; |
936 | 0 | args.push_back(cmSystemTools::GetCTestCommand()); |
937 | 0 | args.push_back("--wait-and-collect-instrumentation"); |
938 | 0 | args.push_back(this->binaryDir); |
939 | 0 | args.push_back(std::to_string(ppid)); |
940 | 0 | auto builder = cmUVProcessChainBuilder().SetDetached().AddCommand(args); |
941 | 0 | auto chain = builder.Start(); |
942 | 0 | uv_run(&chain.GetLoop(), UV_RUN_DEFAULT); |
943 | 0 | } |
944 | 0 | return 0; |
945 | 0 | } |
946 | | |
947 | | // Prevent multiple build daemons from running simultaneously |
948 | | bool cmInstrumentation::LockBuildDaemon() |
949 | 0 | { |
950 | | // 0 = non-blocking, 0s timeout |
951 | 0 | return this->AcquireLock(".build.lock", this->buildLock, 0); |
952 | 0 | } |
953 | | |
954 | | // Prevent multiple index processes from claiming snippets simultaneously |
955 | | bool cmInstrumentation::LockIndexing() |
956 | 0 | { |
957 | 0 | return this->AcquireLock(".index.lock", this->indexLock, |
958 | | // -1 = no timeout |
959 | 0 | static_cast<unsigned long>(-1)); |
960 | 0 | } |
961 | | |
962 | | bool cmInstrumentation::AcquireLock(std::string const& lock_file, |
963 | | cmFileLock& lock, unsigned long timeout) |
964 | 0 | { |
965 | 0 | std::string const lock_path = cmStrCat(this->timingDirv1, '/', lock_file); |
966 | 0 | if (!cmSystemTools::FileExists(lock_path)) { |
967 | 0 | cmSystemTools::Touch(lock_path, true); |
968 | 0 | } |
969 | 0 | return lock.Lock(lock_path, timeout).IsOk(); |
970 | 0 | } |
971 | | |
972 | | /* |
973 | | * Always called by ctest --wait-and-collect-instrumentation in a detached |
974 | | * process. Waits for the given PID to end before running the postBuild hook. |
975 | | * |
976 | | * See SpawnBuildDaemon() |
977 | | */ |
978 | | int cmInstrumentation::CollectTimingAfterBuild(int ppid) |
979 | 0 | { |
980 | | // Check if another process is already instrumenting the build. |
981 | | // This lock will be released when the process exits at the end of the build. |
982 | 0 | if (!this->LockBuildDaemon()) { |
983 | 0 | return 0; |
984 | 0 | } |
985 | 0 | std::function<int()> waitForBuild = [ppid]() -> int { |
986 | 0 | while (0 == uv_kill(ppid, 0)) { |
987 | 0 | cmSystemTools::Delay(100); |
988 | 0 | }; |
989 | | // FIXME(#27331): Investigate a cross-platform solution to obtain the exit |
990 | | // code given the `ppid` above. |
991 | 0 | return 0; |
992 | 0 | }; |
993 | 0 | int ret = this->InstrumentCommand( |
994 | 0 | "build", {}, |
995 | 0 | [waitForBuild]() -> cmInstrumentation::CommandResult { |
996 | 0 | return { waitForBuild(), cm::nullopt, cm::nullopt }; |
997 | 0 | }, |
998 | 0 | cm::nullopt, cm::nullopt, LoadQueriesAfter::Yes); |
999 | 0 | this->buildLock.Release(); |
1000 | 0 | this->CollectTimingData(cmInstrumentationQuery::Hook::PostBuild); |
1001 | 0 | return ret; |
1002 | 0 | } |
1003 | | |
1004 | | void cmInstrumentation::AddHook(cmInstrumentationQuery::Hook hook) |
1005 | 0 | { |
1006 | 0 | this->hooks.insert(hook); |
1007 | 0 | } |
1008 | | |
1009 | | void cmInstrumentation::AddOption(cmInstrumentationQuery::Option option) |
1010 | 0 | { |
1011 | 0 | this->options.insert(option); |
1012 | 0 | } |
1013 | | |
1014 | | std::string const& cmInstrumentation::GetCDashDir() const |
1015 | 0 | { |
1016 | 0 | return this->cdashDir; |
1017 | 0 | } |
1018 | | |
1019 | | std::string const& cmInstrumentation::GetDataDir() const |
1020 | 0 | { |
1021 | 0 | return this->dataDir; |
1022 | 0 | } |
1023 | | |
1024 | | /** Copy the snippets referred to by an index file to a separate |
1025 | | * directory where they will be parsed for submission to CDash. |
1026 | | **/ |
1027 | | void cmInstrumentation::PrepareDataForCDash(std::string const& data_dir, |
1028 | | std::string const& index_path) |
1029 | 0 | { |
1030 | 0 | cmSystemTools::MakeDirectory(this->cdashDir); |
1031 | 0 | cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/configure")); |
1032 | 0 | cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/build")); |
1033 | 0 | cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/build/commands")); |
1034 | 0 | cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/build/targets")); |
1035 | 0 | cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/test")); |
1036 | |
|
1037 | 0 | Json::Value root; |
1038 | 0 | std::string error_msg; |
1039 | 0 | cmJSONState parseState = cmJSONState(index_path, &root); |
1040 | 0 | if (!parseState.errors.empty()) { |
1041 | 0 | cmSystemTools::Error(parseState.GetErrorMessage(true)); |
1042 | 0 | return; |
1043 | 0 | } |
1044 | | |
1045 | 0 | if (!root.isObject()) { |
1046 | 0 | error_msg = |
1047 | 0 | cmStrCat("Expected index file ", index_path, " to contain an object"); |
1048 | 0 | cmSystemTools::Error(error_msg); |
1049 | 0 | return; |
1050 | 0 | } |
1051 | | |
1052 | 0 | if (!root.isMember("snippets")) { |
1053 | 0 | error_msg = cmStrCat("Expected index file ", index_path, |
1054 | 0 | " to have a key 'snippets'"); |
1055 | 0 | cmSystemTools::Error(error_msg); |
1056 | 0 | return; |
1057 | 0 | } |
1058 | | |
1059 | 0 | std::string dst_dir; |
1060 | 0 | Json::Value snippets = root["snippets"]; |
1061 | 0 | for (auto const& snippet : snippets) { |
1062 | | // Parse the role of this snippet. |
1063 | 0 | std::string snippet_str = snippet.asString(); |
1064 | 0 | std::string snippet_path = cmStrCat(data_dir, '/', snippet_str); |
1065 | 0 | Json::Value snippet_root; |
1066 | 0 | parseState = cmJSONState(snippet_path, &snippet_root); |
1067 | 0 | if (!parseState.errors.empty()) { |
1068 | 0 | cmSystemTools::Error(parseState.GetErrorMessage(true)); |
1069 | 0 | continue; |
1070 | 0 | } |
1071 | 0 | if (!snippet_root.isObject()) { |
1072 | 0 | error_msg = cmStrCat("Expected snippet file ", snippet_path, |
1073 | 0 | " to contain an object"); |
1074 | 0 | cmSystemTools::Error(error_msg); |
1075 | 0 | continue; |
1076 | 0 | } |
1077 | 0 | if (!snippet_root.isMember("role")) { |
1078 | 0 | error_msg = cmStrCat("Expected snippet file ", snippet_path, |
1079 | 0 | " to have a key 'role'"); |
1080 | 0 | cmSystemTools::Error(error_msg); |
1081 | 0 | continue; |
1082 | 0 | } |
1083 | | |
1084 | 0 | std::string snippet_role = snippet_root["role"].asString(); |
1085 | 0 | auto map_element = this->cdashSnippetsMap.find(snippet_role); |
1086 | 0 | if (map_element == this->cdashSnippetsMap.end()) { |
1087 | 0 | std::string message = |
1088 | 0 | "Unexpected snippet type encountered: " + snippet_role; |
1089 | 0 | cmSystemTools::Message(message, "Warning"); |
1090 | 0 | continue; |
1091 | 0 | } |
1092 | | |
1093 | 0 | if (map_element->second == "skip") { |
1094 | 0 | continue; |
1095 | 0 | } |
1096 | | |
1097 | 0 | if (map_element->second == "build") { |
1098 | | // We organize snippets on a per-target basis (when possible) |
1099 | | // for Build.xml. |
1100 | 0 | if (snippet_root.isMember("target")) { |
1101 | 0 | dst_dir = cmStrCat(this->cdashDir, "/build/targets/", |
1102 | 0 | snippet_root["target"].asString()); |
1103 | 0 | cmSystemTools::MakeDirectory(dst_dir); |
1104 | 0 | } else { |
1105 | 0 | dst_dir = cmStrCat(this->cdashDir, "/build/commands"); |
1106 | 0 | } |
1107 | 0 | } else { |
1108 | 0 | dst_dir = cmStrCat(this->cdashDir, '/', map_element->second); |
1109 | 0 | } |
1110 | |
|
1111 | 0 | std::string dst = cmStrCat(dst_dir, '/', snippet_str); |
1112 | 0 | if (!cmSystemTools::CopyFileAlways(snippet_path, dst)) { |
1113 | 0 | error_msg = cmStrCat("Failed to copy ", snippet_path, " to ", dst); |
1114 | 0 | cmSystemTools::Error(error_msg); |
1115 | 0 | } |
1116 | 0 | } |
1117 | 0 | } |
1118 | | |
1119 | | std::string cmInstrumentation::GetCompileTraceFile( |
1120 | | std::vector<std::string> const& command, Json::Value const& outputs, |
1121 | | std::string const& workingDir) |
1122 | 0 | { |
1123 | 0 | cm::string_view const prefix = "-ftime-trace="; |
1124 | 0 | std::string traceFile; |
1125 | 0 | std::vector<std::string> fullCommand = |
1126 | 0 | cmSystemTools::HandleResponseFile(command.cbegin(), command.cend()); |
1127 | 0 | for (auto it = fullCommand.rbegin(); it != fullCommand.rend(); ++it) { |
1128 | 0 | std::string const& arg = *it; |
1129 | 0 | if (cmHasPrefix(arg, prefix)) { |
1130 | 0 | traceFile = arg.substr(prefix.size()); |
1131 | 0 | } |
1132 | 0 | } |
1133 | 0 | if (traceFile.empty() && !outputs.empty()) { |
1134 | 0 | std::string outputPath = outputs[0].asString(); |
1135 | 0 | cm::string_view ext = |
1136 | 0 | cmSystemTools::GetFilenameLastExtensionView(outputPath); |
1137 | 0 | if (!outputPath.empty() && !ext.empty()) { |
1138 | 0 | traceFile = cmStrCat( |
1139 | 0 | outputPath.substr(0, outputPath.size() - ext.size()), ".json"); |
1140 | 0 | } |
1141 | 0 | } |
1142 | |
|
1143 | 0 | if (cmSystemTools::FileIsFullPath(traceFile)) { |
1144 | 0 | return traceFile; |
1145 | 0 | } |
1146 | 0 | if (cmSystemTools::FileExists(cmStrCat(workingDir, '/', traceFile), true)) { |
1147 | 0 | return cmStrCat(workingDir, '/', traceFile); |
1148 | 0 | } |
1149 | 0 | if (cmSystemTools::FileExists(cmStrCat(this->binaryDir, '/', traceFile), |
1150 | 0 | true)) { |
1151 | 0 | return cmStrCat(this->binaryDir, '/', traceFile); |
1152 | 0 | } |
1153 | | |
1154 | 0 | return traceFile; |
1155 | 0 | } |
1156 | | |
1157 | | void cmInstrumentation::CollectCompileTraceFile(Json::Value& root, |
1158 | | std::string traceFile, |
1159 | | long int oldTimestamp, |
1160 | | std::string const& commandHash, |
1161 | | std::string const& suffixTime) |
1162 | 0 | { |
1163 | 0 | if (traceFile.empty()) { |
1164 | 0 | root["traceFile"] = Json::nullValue; |
1165 | 0 | return; |
1166 | 0 | } |
1167 | 0 | if (!cmSystemTools::FileExists(traceFile, true) || |
1168 | 0 | cmSystemTools::ModifiedTime(traceFile) == oldTimestamp) { |
1169 | 0 | root["traceFile"] = Json::nullValue; |
1170 | 0 | return; |
1171 | 0 | } |
1172 | 0 | cm::string_view ext = cmSystemTools::GetFilenameLastExtensionView(traceFile); |
1173 | 0 | std::string candidateName = cmSystemTools::GetFilenameName(traceFile); |
1174 | 0 | std::string copiedName = |
1175 | 0 | cmStrCat(candidateName.substr(0, candidateName.size() - ext.size()), '-', |
1176 | 0 | commandHash, '-', suffixTime, ext); |
1177 | 0 | std::string const copiedFile = cmStrCat("compile-trace/", copiedName); |
1178 | 0 | std::string const destination = cmStrCat(this->dataDir, '/', copiedFile); |
1179 | 0 | cmSystemTools::MakeDirectory(cmSystemTools::GetFilenamePath(destination)); |
1180 | 0 | if (!cmSystemTools::CopyFileAlways(traceFile, destination)) { |
1181 | 0 | cmSystemTools::Error(cmStrCat("Failed to copy compile trace file ", |
1182 | 0 | traceFile, " to ", destination)); |
1183 | 0 | return; |
1184 | 0 | } |
1185 | 0 | root["traceFile"] = copiedFile; |
1186 | 0 | } |
1187 | | |
1188 | | void cmInstrumentation::WriteTraceFile(Json::Value const& index, |
1189 | | std::string const& trace_name) |
1190 | 0 | { |
1191 | 0 | std::vector<std::string> snippets = std::vector<std::string>(); |
1192 | 0 | for (auto const& f : index["snippets"]) { |
1193 | 0 | snippets.push_back(f.asString()); |
1194 | 0 | } |
1195 | | // Reverse-sort snippets by timeEnd (timeStart + duration) as a |
1196 | | // prerequisite for AssignTargetToTraceThread(). |
1197 | 0 | auto extractSnippetTimestamp = [](std::string file) -> std::string { |
1198 | 0 | cmsys::RegularExpression snippetTimeRegex( |
1199 | 0 | "[A-Za-z]+-[A-Za-z0-9]+-([0-9T\\-]+)\\.json"); |
1200 | 0 | cmsys::RegularExpressionMatch matchA; |
1201 | 0 | if (snippetTimeRegex.find(file.c_str(), matchA)) { |
1202 | 0 | return matchA.match(1); |
1203 | 0 | } |
1204 | 0 | return ""; |
1205 | 0 | }; |
1206 | 0 | std::sort( |
1207 | 0 | snippets.begin(), snippets.end(), |
1208 | 0 | [extractSnippetTimestamp](std::string snippetA, std::string snippetB) { |
1209 | 0 | return extractSnippetTimestamp(snippetA) > |
1210 | 0 | extractSnippetTimestamp(snippetB); |
1211 | 0 | }); |
1212 | |
|
1213 | 0 | std::string traceDir = cmStrCat(this->timingDirv1, "/data/trace/"); |
1214 | 0 | std::string traceFile = cmStrCat(traceDir, trace_name); |
1215 | 0 | cmSystemTools::MakeDirectory(traceDir); |
1216 | 0 | cmsys::ofstream traceStream; |
1217 | 0 | Json::StreamWriterBuilder wbuilder; |
1218 | 0 | wbuilder["indentation"] = "\t"; |
1219 | 0 | std::unique_ptr<Json::StreamWriter> jsonWriter = |
1220 | 0 | std::unique_ptr<Json::StreamWriter>(wbuilder.newStreamWriter()); |
1221 | 0 | traceStream.open(traceFile.c_str(), std::ios::out | std::ios::trunc); |
1222 | 0 | if (!traceStream.good()) { |
1223 | 0 | throw std::runtime_error(std::string("Unable to open: ") + traceFile); |
1224 | 0 | } |
1225 | 0 | traceStream << "["; |
1226 | | |
1227 | | // Append trace events from single snippets. Prefer writing to the output |
1228 | | // stream incrementally over building up a Json::arrayValue in memory for |
1229 | | // large traces. |
1230 | 0 | std::vector<uint64_t> workers = std::vector<uint64_t>(); |
1231 | 0 | Json::Value traceEvent; |
1232 | 0 | Json::Value snippetData; |
1233 | 0 | for (size_t i = 0; i < snippets.size(); i++) { |
1234 | 0 | snippetData = this->ReadJsonSnippet(snippets[i]); |
1235 | 0 | traceEvent = this->BuildTraceEvent(workers, snippetData); |
1236 | 0 | try { |
1237 | 0 | if (i > 0) { |
1238 | 0 | traceStream << ","; |
1239 | 0 | } |
1240 | 0 | jsonWriter->write(traceEvent, &traceStream); |
1241 | 0 | if (i % 50 == 0 || i == snippets.size() - 1) { |
1242 | 0 | traceStream.flush(); |
1243 | 0 | traceStream.clear(); |
1244 | 0 | } |
1245 | 0 | } catch (std::ios_base::failure& fail) { |
1246 | 0 | cmSystemTools::Error( |
1247 | 0 | cmStrCat("Failed to write to Google trace file: ", fail.what())); |
1248 | 0 | } catch (...) { |
1249 | 0 | cmSystemTools::Error("Error writing Google trace output."); |
1250 | 0 | } |
1251 | 0 | } |
1252 | |
|
1253 | 0 | try { |
1254 | 0 | traceStream << "]\n"; |
1255 | 0 | traceStream.close(); |
1256 | 0 | } catch (...) { |
1257 | 0 | cmSystemTools::Error("Error writing Google trace output."); |
1258 | 0 | } |
1259 | 0 | } |
1260 | | |
1261 | | Json::Value cmInstrumentation::BuildTraceEvent(std::vector<uint64_t>& workers, |
1262 | | Json::Value const& snippetData) |
1263 | 0 | { |
1264 | 0 | Json::Value snippetTraceEvent; |
1265 | | |
1266 | | // Provide a useful trace event name depending on what data is available |
1267 | | // from the snippet. |
1268 | 0 | std::string nameSuffix; |
1269 | 0 | if (snippetData["role"] == "compile") { |
1270 | 0 | nameSuffix = snippetData["source"].asString(); |
1271 | 0 | } else if (snippetData["role"] == "link") { |
1272 | 0 | nameSuffix = snippetData["target"].asString(); |
1273 | 0 | } else if (snippetData["role"] == "install") { |
1274 | 0 | cmCMakePath workingDir(snippetData["workingDir"].asCString()); |
1275 | 0 | nameSuffix = workingDir.GetFileName().String(); |
1276 | 0 | } else if (snippetData["role"] == "custom") { |
1277 | 0 | nameSuffix = snippetData["command"].asString(); |
1278 | 0 | } else if (snippetData["role"] == "test") { |
1279 | 0 | nameSuffix = snippetData["testName"].asString(); |
1280 | 0 | } |
1281 | 0 | if (!nameSuffix.empty()) { |
1282 | 0 | snippetTraceEvent["name"] = |
1283 | 0 | cmStrCat(snippetData["role"].asString(), ": ", nameSuffix); |
1284 | 0 | } else { |
1285 | 0 | snippetTraceEvent["name"] = snippetData["role"].asString(); |
1286 | 0 | } |
1287 | |
|
1288 | 0 | snippetTraceEvent["cat"] = snippetData["role"]; |
1289 | 0 | snippetTraceEvent["ph"] = "X"; |
1290 | 0 | snippetTraceEvent["args"] = snippetData; |
1291 | | |
1292 | | // Time in the Trace Event Format is stored in microseconds |
1293 | | // but the snippet files store time in milliseconds. |
1294 | 0 | snippetTraceEvent["ts"] = snippetData["timeStart"].asUInt64() * 1000; |
1295 | 0 | snippetTraceEvent["dur"] = snippetData["duration"].asUInt64() * 1000; |
1296 | | |
1297 | | // Assign an arbitrary PID, since this data isn't useful for the |
1298 | | // visualization in our case. |
1299 | 0 | snippetTraceEvent["pid"] = 0; |
1300 | | // Assign TID of 0 for snippets which will have other snippet data |
1301 | | // visualized "underneath" them. (For others, start from 1.) |
1302 | 0 | if (snippetData["role"] == "build" || snippetData["role"] == "cmakeBuild" || |
1303 | 0 | snippetData["role"] == "ctest" || |
1304 | 0 | snippetData["role"] == "cmakeInstall") { |
1305 | 0 | snippetTraceEvent["tid"] = 0; |
1306 | 0 | } else { |
1307 | 0 | snippetTraceEvent["tid"] = static_cast<Json::Value::UInt64>( |
1308 | 0 | AssignTargetToTraceThread(workers, snippetData["timeStart"].asUInt64(), |
1309 | 0 | snippetData["duration"].asUInt64())); |
1310 | 0 | } |
1311 | |
|
1312 | 0 | return snippetTraceEvent; |
1313 | 0 | } |
1314 | | |
1315 | | size_t cmInstrumentation::AssignTargetToTraceThread( |
1316 | | std::vector<uint64_t>& workers, uint64_t timeStart, uint64_t duration) |
1317 | 0 | { |
1318 | 0 | for (size_t i = 0; i < workers.size(); i++) { |
1319 | 0 | if (workers[i] >= timeStart + duration) { |
1320 | 0 | workers[i] = timeStart; |
1321 | 0 | return i + 1; |
1322 | 0 | } |
1323 | 0 | } |
1324 | 0 | workers.push_back(timeStart); |
1325 | 0 | return workers.size(); |
1326 | 0 | } |