cmInstrumentation.cxx 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002
  1. #include "cmInstrumentation.h"
  2. #include <algorithm>
  3. #include <chrono>
  4. #include <ctime>
  5. #include <iomanip>
  6. #include <set>
  7. #include <sstream>
  8. #include <utility>
  9. #include <cm/memory>
  10. #include <cm/optional>
  11. #include <cm3p/json/reader.h>
  12. #include <cm3p/json/version.h>
  13. #include <cm3p/json/writer.h>
  14. #include <cm3p/uv.h>
  15. #include "cmsys/Directory.hxx"
  16. #include "cmsys/FStream.hxx"
  17. #include "cmsys/SystemInformation.hxx"
  18. #include "cmCMakePath.h"
  19. #include "cmCryptoHash.h"
  20. #include "cmExperimental.h"
  21. #include "cmFileLock.h"
  22. #include "cmFileLockResult.h"
  23. #include "cmInstrumentationQuery.h"
  24. #include "cmJSONState.h"
  25. #include "cmStringAlgorithms.h"
  26. #include "cmSystemTools.h"
  27. #include "cmTimestamp.h"
  28. #include "cmUVProcessChain.h"
  29. #include "cmValue.h"
  30. using LoadQueriesAfter = cmInstrumentation::LoadQueriesAfter;
  31. std::map<std::string, std::string> cmInstrumentation::cdashSnippetsMap = {
  32. {
  33. "configure",
  34. "configure",
  35. },
  36. {
  37. "generate",
  38. "configure",
  39. },
  40. {
  41. "compile",
  42. "build",
  43. },
  44. {
  45. "link",
  46. "build",
  47. },
  48. {
  49. "custom",
  50. "build",
  51. },
  52. {
  53. "build",
  54. "skip",
  55. },
  56. {
  57. "cmakeBuild",
  58. "build",
  59. },
  60. {
  61. "cmakeInstall",
  62. "build",
  63. },
  64. {
  65. "install",
  66. "build",
  67. },
  68. {
  69. "ctest",
  70. "build",
  71. },
  72. {
  73. "test",
  74. "test",
  75. }
  76. };
  77. cmInstrumentation::cmInstrumentation(std::string const& binary_dir,
  78. LoadQueriesAfter loadQueries)
  79. {
  80. std::string const uuid =
  81. cmExperimental::DataForFeature(cmExperimental::Feature::Instrumentation)
  82. .Uuid;
  83. this->binaryDir = binary_dir;
  84. this->timingDirv1 =
  85. cmStrCat(this->binaryDir, "/.cmake/instrumentation-", uuid, "/v1");
  86. this->cdashDir = cmStrCat(this->timingDirv1, "/cdash");
  87. if (cm::optional<std::string> configDir =
  88. cmSystemTools::GetCMakeConfigDirectory()) {
  89. this->userTimingDirv1 =
  90. cmStrCat(configDir.value(), "/instrumentation-", uuid, "/v1");
  91. }
  92. if (loadQueries == LoadQueriesAfter::Yes) {
  93. this->LoadQueries();
  94. }
  95. }
  96. void cmInstrumentation::LoadQueries()
  97. {
  98. if (cmSystemTools::FileExists(cmStrCat(this->timingDirv1, "/query"))) {
  99. this->hasQuery =
  100. this->ReadJSONQueries(cmStrCat(this->timingDirv1, "/query")) ||
  101. this->ReadJSONQueries(cmStrCat(this->timingDirv1, "/query/generated"));
  102. }
  103. if (!this->userTimingDirv1.empty() &&
  104. cmSystemTools::FileExists(cmStrCat(this->userTimingDirv1, "/query"))) {
  105. this->hasQuery = this->hasQuery ||
  106. this->ReadJSONQueries(cmStrCat(this->userTimingDirv1, "/query"));
  107. }
  108. }
  109. void cmInstrumentation::CheckCDashVariable()
  110. {
  111. std::string envVal;
  112. if (cmSystemTools::GetEnv("CTEST_USE_INSTRUMENTATION", envVal) &&
  113. !cmIsOff(envVal)) {
  114. if (cmSystemTools::GetEnv("CTEST_EXPERIMENTAL_INSTRUMENTATION", envVal)) {
  115. std::string const uuid = cmExperimental::DataForFeature(
  116. cmExperimental::Feature::Instrumentation)
  117. .Uuid;
  118. if (envVal == uuid) {
  119. std::set<cmInstrumentationQuery::Option> options_ = {
  120. cmInstrumentationQuery::Option::CDashSubmit,
  121. cmInstrumentationQuery::Option::DynamicSystemInformation
  122. };
  123. if (cmSystemTools::GetEnv("CTEST_USE_VERBOSE_INSTRUMENTATION",
  124. envVal) &&
  125. !cmIsOff(envVal)) {
  126. options_.insert(cmInstrumentationQuery::Option::CDashVerbose);
  127. }
  128. for (auto const& option : options_) {
  129. this->AddOption(option);
  130. }
  131. std::set<cmInstrumentationQuery::Hook> hooks_ = {
  132. cmInstrumentationQuery::Hook::PrepareForCDash
  133. };
  134. this->AddHook(cmInstrumentationQuery::Hook::PrepareForCDash);
  135. this->WriteJSONQuery(options_, hooks_, {});
  136. }
  137. }
  138. }
  139. }
  140. cmsys::SystemInformation& cmInstrumentation::GetSystemInformation()
  141. {
  142. if (!this->systemInformation) {
  143. this->systemInformation = cm::make_unique<cmsys::SystemInformation>();
  144. }
  145. return *this->systemInformation;
  146. }
  147. bool cmInstrumentation::ReadJSONQueries(std::string const& directory)
  148. {
  149. cmsys::Directory d;
  150. std::string json = ".json";
  151. bool result = false;
  152. if (d.Load(directory)) {
  153. for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) {
  154. std::string fpath = d.GetFilePath(i);
  155. if (fpath.rfind(json) == (fpath.size() - json.size())) {
  156. result = true;
  157. this->ReadJSONQuery(fpath);
  158. }
  159. }
  160. }
  161. return result;
  162. }
  163. void cmInstrumentation::ReadJSONQuery(std::string const& file)
  164. {
  165. auto query = cmInstrumentationQuery();
  166. query.ReadJSON(file, this->errorMsg, this->options, this->hooks,
  167. this->callbacks);
  168. if (!this->errorMsg.empty()) {
  169. cmSystemTools::Error(cmStrCat(
  170. "Could not load instrumentation queries from ",
  171. cmSystemTools::GetParentDirectory(file), ":\n", this->errorMsg));
  172. }
  173. }
  174. bool cmInstrumentation::HasErrors() const
  175. {
  176. return !this->errorMsg.empty();
  177. }
  178. void cmInstrumentation::WriteJSONQuery(
  179. std::set<cmInstrumentationQuery::Option> const& options_,
  180. std::set<cmInstrumentationQuery::Hook> const& hooks_,
  181. std::vector<std::vector<std::string>> const& callbacks_)
  182. {
  183. Json::Value root;
  184. root["version"] = 1;
  185. root["options"] = Json::arrayValue;
  186. for (auto const& option : options_) {
  187. root["options"].append(cmInstrumentationQuery::OptionString[option]);
  188. }
  189. root["hooks"] = Json::arrayValue;
  190. for (auto const& hook : hooks_) {
  191. root["hooks"].append(cmInstrumentationQuery::HookString[hook]);
  192. }
  193. root["callbacks"] = Json::arrayValue;
  194. for (auto const& callback : callbacks_) {
  195. root["callbacks"].append(cmInstrumentation::GetCommandStr(callback));
  196. }
  197. this->WriteInstrumentationJson(
  198. root, "query/generated",
  199. cmStrCat("query-", this->writtenJsonQueries++, ".json"));
  200. }
  201. void cmInstrumentation::AddCustomContent(std::string const& name,
  202. Json::Value const& contents)
  203. {
  204. this->customContent[name] = contents;
  205. }
  206. void cmInstrumentation::WriteCustomContent()
  207. {
  208. if (!this->customContent.isNull()) {
  209. this->WriteInstrumentationJson(
  210. this->customContent, "data/content",
  211. cmStrCat("configure-", this->ComputeSuffixTime(), ".json"));
  212. }
  213. }
  214. std::string cmInstrumentation::GetFileByTimestamp(
  215. cmInstrumentation::LatestOrOldest order, std::string const& dataSubdir,
  216. std::string const& exclude)
  217. {
  218. std::string fullDir = cmStrCat(this->timingDirv1, "/data/", dataSubdir);
  219. std::string result;
  220. if (cmSystemTools::FileExists(fullDir)) {
  221. cmsys::Directory d;
  222. if (d.Load(fullDir)) {
  223. for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) {
  224. std::string fname = d.GetFileName(i);
  225. if (fname != "." && fname != ".." && fname != exclude &&
  226. (result.empty() ||
  227. (order == LatestOrOldest::Latest && fname > result) ||
  228. (order == LatestOrOldest::Oldest && fname < result))) {
  229. result = fname;
  230. }
  231. }
  232. }
  233. }
  234. return result;
  235. }
  236. void cmInstrumentation::RemoveOldFiles(std::string const& dataSubdir)
  237. {
  238. std::string const dataSubdirPath =
  239. cmStrCat(this->timingDirv1, "/data/", dataSubdir);
  240. std::string oldIndex =
  241. this->GetFileByTimestamp(LatestOrOldest::Oldest, "index");
  242. if (!oldIndex.empty()) {
  243. oldIndex = cmStrCat(this->timingDirv1, "/data/index/", oldIndex);
  244. }
  245. if (cmSystemTools::FileExists(dataSubdirPath)) {
  246. std::string latestFile =
  247. this->GetFileByTimestamp(LatestOrOldest::Latest, dataSubdir);
  248. cmsys::Directory d;
  249. if (d.Load(dataSubdirPath)) {
  250. for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) {
  251. std::string fname = d.GetFileName(i);
  252. std::string fpath = d.GetFilePath(i);
  253. if (fname != "." && fname != ".." && fname < latestFile) {
  254. if (!oldIndex.empty()) {
  255. int compare;
  256. cmSystemTools::FileTimeCompare(oldIndex, fpath, &compare);
  257. if (compare == 1) {
  258. continue;
  259. }
  260. }
  261. cmSystemTools::RemoveFile(fpath);
  262. }
  263. }
  264. }
  265. }
  266. }
  267. void cmInstrumentation::ClearGeneratedQueries()
  268. {
  269. std::string dir = cmStrCat(this->timingDirv1, "/query/generated");
  270. if (cmSystemTools::FileIsDirectory(dir)) {
  271. cmSystemTools::RemoveADirectory(dir);
  272. }
  273. }
  274. bool cmInstrumentation::HasQuery() const
  275. {
  276. return this->hasQuery;
  277. }
  278. bool cmInstrumentation::HasOption(cmInstrumentationQuery::Option option) const
  279. {
  280. return (this->options.find(option) != this->options.end());
  281. }
  282. bool cmInstrumentation::HasHook(cmInstrumentationQuery::Hook hook) const
  283. {
  284. return (this->hooks.find(hook) != this->hooks.end());
  285. }
  286. bool cmInstrumentation::HasPreOrPostBuildHook() const
  287. {
  288. return (this->HasHook(cmInstrumentationQuery::Hook::PreBuild) ||
  289. this->HasHook(cmInstrumentationQuery::Hook::PostBuild));
  290. }
  291. int cmInstrumentation::CollectTimingData(cmInstrumentationQuery::Hook hook)
  292. {
  293. // Don't run collection if hook is disabled
  294. if (hook != cmInstrumentationQuery::Hook::Manual && !this->HasHook(hook)) {
  295. return 0;
  296. }
  297. // Touch index file immediately to claim snippets
  298. std::string const& directory = cmStrCat(this->timingDirv1, "/data");
  299. std::string suffix_time = ComputeSuffixTime();
  300. std::string const& index_name = cmStrCat("index-", suffix_time, ".json");
  301. std::string index_path = cmStrCat(directory, "/index/", index_name);
  302. cmSystemTools::Touch(index_path, true);
  303. // Gather Snippets
  304. using snippet = std::pair<std::string, std::string>;
  305. std::vector<snippet> files;
  306. cmsys::Directory d;
  307. std::string last_index_name =
  308. this->GetFileByTimestamp(LatestOrOldest::Latest, "index", index_name);
  309. if (d.Load(directory)) {
  310. for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) {
  311. std::string fpath = d.GetFilePath(i);
  312. std::string fname = d.GetFile(i);
  313. if (fname.rfind('.', 0) == 0 || d.FileIsDirectory(i)) {
  314. continue;
  315. }
  316. files.push_back(snippet(std::move(fname), std::move(fpath)));
  317. }
  318. }
  319. // Build Json Object
  320. Json::Value index(Json::objectValue);
  321. index["snippets"] = Json::arrayValue;
  322. index["hook"] = cmInstrumentationQuery::HookString[hook];
  323. index["dataDir"] = directory;
  324. index["buildDir"] = this->binaryDir;
  325. index["version"] = 1;
  326. if (this->HasOption(
  327. cmInstrumentationQuery::Option::StaticSystemInformation)) {
  328. this->InsertStaticSystemInformation(index);
  329. }
  330. for (auto const& file : files) {
  331. if (last_index_name.empty()) {
  332. index["snippets"].append(file.first);
  333. } else {
  334. int compare;
  335. std::string last_index_path =
  336. cmStrCat(directory, "/index/", last_index_name);
  337. cmSystemTools::FileTimeCompare(file.second, last_index_path, &compare);
  338. if (compare == 1) {
  339. index["snippets"].append(file.first);
  340. }
  341. }
  342. }
  343. // Parse snippets into the Google trace file
  344. if (this->HasOption(cmInstrumentationQuery::Option::Trace)) {
  345. std::string trace_name = cmStrCat("trace-", suffix_time, ".json");
  346. this->WriteTraceFile(index, trace_name);
  347. index["trace"] = "trace/" + trace_name;
  348. }
  349. // Write index file
  350. this->WriteInstrumentationJson(index, "data/index", index_name);
  351. // Execute callbacks
  352. for (auto& cb : this->callbacks) {
  353. cmSystemTools::RunSingleCommand(cmStrCat(cb, " \"", index_path, '"'),
  354. nullptr, nullptr, nullptr, nullptr,
  355. cmSystemTools::OUTPUT_PASSTHROUGH);
  356. }
  357. // Special case for CDash collation
  358. if (this->HasOption(cmInstrumentationQuery::Option::CDashSubmit)) {
  359. this->PrepareDataForCDash(directory, index_path);
  360. }
  361. // Delete files
  362. for (auto const& f : index["snippets"]) {
  363. cmSystemTools::RemoveFile(cmStrCat(directory, '/', f.asString()));
  364. }
  365. cmSystemTools::RemoveFile(index_path);
  366. // Delete old content and trace files
  367. this->RemoveOldFiles("content");
  368. this->RemoveOldFiles("trace");
  369. return 0;
  370. }
  371. void cmInstrumentation::InsertDynamicSystemInformation(
  372. Json::Value& root, std::string const& prefix)
  373. {
  374. Json::Value data;
  375. double memory;
  376. double load;
  377. this->GetDynamicSystemInformation(memory, load);
  378. if (!root.isMember("dynamicSystemInformation")) {
  379. root["dynamicSystemInformation"] = Json::objectValue;
  380. }
  381. root["dynamicSystemInformation"][cmStrCat(prefix, "HostMemoryUsed")] =
  382. memory;
  383. root["dynamicSystemInformation"][cmStrCat(prefix, "CPULoadAverage")] =
  384. load > 0 ? Json::Value(load) : Json::nullValue;
  385. }
  386. void cmInstrumentation::GetDynamicSystemInformation(double& memory,
  387. double& load)
  388. {
  389. cmsys::SystemInformation& info = this->GetSystemInformation();
  390. if (!this->ranSystemChecks) {
  391. info.RunCPUCheck();
  392. info.RunMemoryCheck();
  393. this->ranSystemChecks = true;
  394. }
  395. memory = (double)info.GetHostMemoryUsed();
  396. load = info.GetLoadAverage();
  397. }
  398. void cmInstrumentation::InsertStaticSystemInformation(Json::Value& root)
  399. {
  400. cmsys::SystemInformation& info = this->GetSystemInformation();
  401. if (!this->ranOSCheck) {
  402. info.RunOSCheck();
  403. this->ranOSCheck = true;
  404. }
  405. Json::Value infoRoot;
  406. infoRoot["familyId"] = info.GetFamilyID();
  407. infoRoot["hostname"] = info.GetHostname();
  408. infoRoot["is64Bits"] = info.Is64Bits();
  409. infoRoot["modelId"] = info.GetModelID();
  410. infoRoot["numberOfLogicalCPU"] = info.GetNumberOfLogicalCPU();
  411. infoRoot["numberOfPhysicalCPU"] = info.GetNumberOfPhysicalCPU();
  412. infoRoot["OSName"] = info.GetOSName();
  413. infoRoot["OSPlatform"] = info.GetOSPlatform();
  414. infoRoot["OSRelease"] = info.GetOSRelease();
  415. infoRoot["OSVersion"] = info.GetOSVersion();
  416. infoRoot["processorAPICID"] = info.GetProcessorAPICID();
  417. infoRoot["processorCacheSize"] = info.GetProcessorCacheSize();
  418. infoRoot["processorClockFrequency"] =
  419. (double)info.GetProcessorClockFrequency();
  420. infoRoot["processorName"] = info.GetExtendedProcessorName();
  421. infoRoot["totalPhysicalMemory"] =
  422. static_cast<Json::Value::UInt64>(info.GetTotalPhysicalMemory());
  423. infoRoot["totalVirtualMemory"] =
  424. static_cast<Json::Value::UInt64>(info.GetTotalVirtualMemory());
  425. infoRoot["vendorID"] = info.GetVendorID();
  426. infoRoot["vendorString"] = info.GetVendorString();
  427. root["staticSystemInformation"] = infoRoot;
  428. }
  429. void cmInstrumentation::InsertTimingData(
  430. Json::Value& root, std::chrono::steady_clock::time_point steadyStart,
  431. std::chrono::system_clock::time_point systemStart)
  432. {
  433. uint64_t timeStart = std::chrono::duration_cast<std::chrono::milliseconds>(
  434. systemStart.time_since_epoch())
  435. .count();
  436. uint64_t duration = std::chrono::duration_cast<std::chrono::milliseconds>(
  437. std::chrono::steady_clock::now() - steadyStart)
  438. .count();
  439. root["timeStart"] = static_cast<Json::Value::UInt64>(timeStart);
  440. root["duration"] = static_cast<Json::Value::UInt64>(duration);
  441. }
  442. Json::Value cmInstrumentation::ReadJsonSnippet(std::string const& directory,
  443. std::string const& file_name)
  444. {
  445. Json::CharReaderBuilder builder;
  446. builder["collectComments"] = false;
  447. cmsys::ifstream ftmp(cmStrCat(directory, '/', file_name).c_str());
  448. Json::Value snippetData;
  449. builder["collectComments"] = false;
  450. if (!Json::parseFromStream(builder, ftmp, &snippetData, nullptr)) {
  451. #if JSONCPP_VERSION_HEXA < 0x01070300
  452. snippetData = Json::Value::null;
  453. #else
  454. snippetData = Json::Value::nullSingleton();
  455. #endif
  456. }
  457. ftmp.close();
  458. return snippetData;
  459. }
  460. void cmInstrumentation::WriteInstrumentationJson(Json::Value& root,
  461. std::string const& subdir,
  462. std::string const& file_name)
  463. {
  464. Json::StreamWriterBuilder wbuilder;
  465. wbuilder["indentation"] = "\t";
  466. std::unique_ptr<Json::StreamWriter> JsonWriter =
  467. std::unique_ptr<Json::StreamWriter>(wbuilder.newStreamWriter());
  468. std::string const& directory = cmStrCat(this->timingDirv1, '/', subdir);
  469. cmSystemTools::MakeDirectory(directory);
  470. cmsys::ofstream ftmp(cmStrCat(directory, '/', file_name).c_str());
  471. JsonWriter->write(root, &ftmp);
  472. ftmp << "\n";
  473. ftmp.close();
  474. }
  475. std::string cmInstrumentation::InstrumentTest(
  476. std::string const& name, std::string const& command,
  477. std::vector<std::string> const& args, int64_t result,
  478. std::chrono::steady_clock::time_point steadyStart,
  479. std::chrono::system_clock::time_point systemStart, std::string config)
  480. {
  481. // Store command info
  482. Json::Value root(this->preTestStats);
  483. std::string command_str = cmStrCat(command, ' ', GetCommandStr(args));
  484. root["version"] = 1;
  485. root["command"] = command_str;
  486. root["role"] = "test";
  487. root["testName"] = name;
  488. root["result"] = static_cast<Json::Value::Int64>(result);
  489. root["config"] = config;
  490. root["workingDir"] = cmSystemTools::GetLogicalWorkingDirectory();
  491. // Post-Command
  492. this->InsertTimingData(root, steadyStart, systemStart);
  493. if (this->HasOption(
  494. cmInstrumentationQuery::Option::DynamicSystemInformation)) {
  495. this->InsertDynamicSystemInformation(root, "after");
  496. }
  497. cmsys::SystemInformation& info = this->GetSystemInformation();
  498. std::string file_name = cmStrCat(
  499. "test-",
  500. this->ComputeSuffixHash(cmStrCat(command_str, info.GetProcessId())),
  501. this->ComputeSuffixTime(), ".json");
  502. this->WriteInstrumentationJson(root, "data", file_name);
  503. return file_name;
  504. }
  505. void cmInstrumentation::GetPreTestStats()
  506. {
  507. if (this->HasOption(
  508. cmInstrumentationQuery::Option::DynamicSystemInformation)) {
  509. this->InsertDynamicSystemInformation(this->preTestStats, "before");
  510. }
  511. }
  512. int cmInstrumentation::InstrumentCommand(
  513. std::string command_type, std::vector<std::string> const& command,
  514. std::function<int()> const& callback,
  515. cm::optional<std::map<std::string, std::string>> data,
  516. cm::optional<std::map<std::string, std::string>> arrayData,
  517. LoadQueriesAfter reloadQueriesAfterCommand)
  518. {
  519. // Always begin gathering data for configure in case cmake_instrumentation
  520. // command creates a query
  521. if (!this->hasQuery && reloadQueriesAfterCommand == LoadQueriesAfter::No) {
  522. return callback();
  523. }
  524. // Store command info
  525. Json::Value root(Json::objectValue);
  526. Json::Value commandInfo(Json::objectValue);
  527. std::string command_str = GetCommandStr(command);
  528. if (!command_str.empty()) {
  529. root["command"] = command_str;
  530. }
  531. root["version"] = 1;
  532. // Pre-Command
  533. auto steady_start = std::chrono::steady_clock::now();
  534. auto system_start = std::chrono::system_clock::now();
  535. double preConfigureMemory = 0;
  536. double preConfigureLoad = 0;
  537. if (this->HasOption(
  538. cmInstrumentationQuery::Option::DynamicSystemInformation)) {
  539. this->InsertDynamicSystemInformation(root, "before");
  540. } else if (reloadQueriesAfterCommand == LoadQueriesAfter::Yes) {
  541. this->GetDynamicSystemInformation(preConfigureMemory, preConfigureLoad);
  542. }
  543. // Execute Command
  544. int ret = callback();
  545. root["result"] = ret;
  546. // Write configure content if command was configure
  547. if (command_type == "configure") {
  548. this->WriteCustomContent();
  549. }
  550. // Exit early if configure didn't generate a query
  551. if (reloadQueriesAfterCommand == LoadQueriesAfter::Yes) {
  552. this->LoadQueries();
  553. if (!this->HasQuery()) {
  554. return ret;
  555. }
  556. if (this->HasOption(
  557. cmInstrumentationQuery::Option::DynamicSystemInformation)) {
  558. root["dynamicSystemInformation"] = Json::objectValue;
  559. root["dynamicSystemInformation"]["beforeHostMemoryUsed"] =
  560. preConfigureMemory;
  561. root["dynamicSystemInformation"]["beforeCPULoadAverage"] =
  562. preConfigureLoad;
  563. }
  564. }
  565. // Post-Command
  566. this->InsertTimingData(root, steady_start, system_start);
  567. if (this->HasOption(
  568. cmInstrumentationQuery::Option::DynamicSystemInformation)) {
  569. this->InsertDynamicSystemInformation(root, "after");
  570. }
  571. // Gather additional data
  572. if (data.has_value()) {
  573. for (auto const& item : data.value()) {
  574. if (item.first == "role" && !item.second.empty()) {
  575. command_type = item.second;
  576. } else if (!item.second.empty()) {
  577. root[item.first] = item.second;
  578. }
  579. }
  580. }
  581. // Create empty config entry if config not found
  582. if (!root.isMember("config") &&
  583. (command_type == "compile" || command_type == "link")) {
  584. root["config"] = "";
  585. }
  586. if (arrayData.has_value()) {
  587. for (auto const& item : arrayData.value()) {
  588. if (item.first == "targetLabels" && command_type != "link") {
  589. continue;
  590. }
  591. root[item.first] = Json::arrayValue;
  592. std::stringstream ss(item.second);
  593. std::string element;
  594. while (getline(ss, element, ',')) {
  595. root[item.first].append(element);
  596. }
  597. if (item.first == "outputs") {
  598. root["outputSizes"] = Json::arrayValue;
  599. for (auto const& output : root["outputs"]) {
  600. root["outputSizes"].append(
  601. static_cast<Json::Value::UInt64>(cmSystemTools::FileLength(
  602. cmStrCat(this->binaryDir, '/', output.asCString()))));
  603. }
  604. }
  605. }
  606. }
  607. root["role"] = command_type;
  608. root["workingDir"] = cmSystemTools::GetLogicalWorkingDirectory();
  609. // Add custom configure content
  610. std::string contentFile =
  611. this->GetFileByTimestamp(LatestOrOldest::Latest, "content");
  612. if (!contentFile.empty()) {
  613. root["configureContent"] = cmStrCat("content/", contentFile);
  614. }
  615. // Write Json
  616. cmsys::SystemInformation& info = this->GetSystemInformation();
  617. std::string const& file_name = cmStrCat(
  618. command_type, '-',
  619. this->ComputeSuffixHash(cmStrCat(command_str, info.GetProcessId())),
  620. this->ComputeSuffixTime(), ".json");
  621. this->WriteInstrumentationJson(root, "data", file_name);
  622. return ret;
  623. }
  624. std::string cmInstrumentation::GetCommandStr(
  625. std::vector<std::string> const& args)
  626. {
  627. std::string command_str;
  628. for (size_t i = 0; i < args.size(); ++i) {
  629. command_str = cmStrCat(command_str, '"', args[i], '"');
  630. if (i < args.size() - 1) {
  631. command_str = cmStrCat(command_str, ' ');
  632. }
  633. }
  634. return command_str;
  635. }
  636. std::string cmInstrumentation::ComputeSuffixHash(
  637. std::string const& command_str)
  638. {
  639. cmCryptoHash hasher(cmCryptoHash::AlgoSHA3_256);
  640. std::string hash = hasher.HashString(command_str);
  641. hash.resize(20, '0');
  642. return hash;
  643. }
  644. std::string cmInstrumentation::ComputeSuffixTime()
  645. {
  646. std::chrono::milliseconds ms =
  647. std::chrono::duration_cast<std::chrono::milliseconds>(
  648. std::chrono::system_clock::now().time_since_epoch());
  649. std::chrono::seconds s =
  650. std::chrono::duration_cast<std::chrono::seconds>(ms);
  651. std::time_t ts = s.count();
  652. std::size_t tms = ms.count() % 1000;
  653. cmTimestamp cmts;
  654. std::ostringstream ss;
  655. ss << cmts.CreateTimestampFromTimeT(ts, "%Y-%m-%dT%H-%M-%S", true) << '-'
  656. << std::setfill('0') << std::setw(4) << tms;
  657. return ss.str();
  658. }
  659. /*
  660. * Called by ctest --start-instrumentation.
  661. *
  662. * This creates a detached process which waits for the parent process (i.e.,
  663. * the build system) to die before running the postBuild hook. In this way, the
  664. * postBuild hook triggers after every invocation of the build system,
  665. * regardless of whether the build passed or failed.
  666. */
  667. int cmInstrumentation::SpawnBuildDaemon()
  668. {
  669. // Do not inherit handles from the parent process, so that the daemon is
  670. // fully detached. This helps prevent deadlock between the two.
  671. uv_disable_stdio_inheritance();
  672. // preBuild Hook
  673. if (this->LockBuildDaemon()) {
  674. // Release lock before spawning the build daemon, to prevent blocking it.
  675. this->lock.Release();
  676. this->CollectTimingData(cmInstrumentationQuery::Hook::PreBuild);
  677. }
  678. // postBuild Hook
  679. if (this->HasHook(cmInstrumentationQuery::Hook::PostBuild)) {
  680. auto ppid = uv_os_getppid();
  681. if (ppid) {
  682. std::vector<std::string> args;
  683. args.push_back(cmSystemTools::GetCTestCommand());
  684. args.push_back("--wait-and-collect-instrumentation");
  685. args.push_back(this->binaryDir);
  686. args.push_back(std::to_string(ppid));
  687. auto builder = cmUVProcessChainBuilder().SetDetached().AddCommand(args);
  688. auto chain = builder.Start();
  689. uv_run(&chain.GetLoop(), UV_RUN_DEFAULT);
  690. }
  691. }
  692. return 0;
  693. }
  694. // Prevent multiple build daemons from running simultaneously
  695. bool cmInstrumentation::LockBuildDaemon()
  696. {
  697. std::string const lockFile = cmStrCat(this->timingDirv1, "/.build.lock");
  698. if (!cmSystemTools::FileExists(lockFile)) {
  699. cmSystemTools::Touch(lockFile, true);
  700. }
  701. return this->lock.Lock(lockFile, 0).IsOk();
  702. }
  703. /*
  704. * Always called by ctest --wait-and-collect-instrumentation in a detached
  705. * process. Waits for the given PID to end before running the postBuild hook.
  706. *
  707. * See SpawnBuildDaemon()
  708. */
  709. int cmInstrumentation::CollectTimingAfterBuild(int ppid)
  710. {
  711. // Check if another process is already instrumenting the build.
  712. // This lock will be released when the process exits at the end of the build.
  713. if (!this->LockBuildDaemon()) {
  714. return 0;
  715. }
  716. std::function<int()> waitForBuild = [ppid]() -> int {
  717. while (0 == uv_kill(ppid, 0)) {
  718. cmSystemTools::Delay(100);
  719. };
  720. return 0;
  721. };
  722. int ret = this->InstrumentCommand(
  723. "build", {}, [waitForBuild]() { return waitForBuild(); }, cm::nullopt,
  724. cm::nullopt, LoadQueriesAfter::No);
  725. this->CollectTimingData(cmInstrumentationQuery::Hook::PostBuild);
  726. return ret;
  727. }
  728. void cmInstrumentation::AddHook(cmInstrumentationQuery::Hook hook)
  729. {
  730. this->hooks.insert(hook);
  731. }
  732. void cmInstrumentation::AddOption(cmInstrumentationQuery::Option option)
  733. {
  734. this->options.insert(option);
  735. }
  736. std::string const& cmInstrumentation::GetCDashDir()
  737. {
  738. return this->cdashDir;
  739. }
  740. /** Copy the snippets referred to by an index file to a separate
  741. * directory where they will be parsed for submission to CDash.
  742. **/
  743. void cmInstrumentation::PrepareDataForCDash(std::string const& data_dir,
  744. std::string const& index_path)
  745. {
  746. cmSystemTools::MakeDirectory(this->cdashDir);
  747. cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/configure"));
  748. cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/build"));
  749. cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/build/commands"));
  750. cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/build/targets"));
  751. cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/test"));
  752. Json::Value root;
  753. std::string error_msg;
  754. cmJSONState parseState = cmJSONState(index_path, &root);
  755. if (!parseState.errors.empty()) {
  756. cmSystemTools::Error(parseState.GetErrorMessage(true));
  757. return;
  758. }
  759. if (!root.isObject()) {
  760. error_msg =
  761. cmStrCat("Expected index file ", index_path, " to contain an object");
  762. cmSystemTools::Error(error_msg);
  763. return;
  764. }
  765. if (!root.isMember("snippets")) {
  766. error_msg = cmStrCat("Expected index file ", index_path,
  767. " to have a key 'snippets'");
  768. cmSystemTools::Error(error_msg);
  769. return;
  770. }
  771. std::string dst_dir;
  772. Json::Value snippets = root["snippets"];
  773. for (auto const& snippet : snippets) {
  774. // Parse the role of this snippet.
  775. std::string snippet_str = snippet.asString();
  776. std::string snippet_path = cmStrCat(data_dir, '/', snippet_str);
  777. Json::Value snippet_root;
  778. parseState = cmJSONState(snippet_path, &snippet_root);
  779. if (!parseState.errors.empty()) {
  780. cmSystemTools::Error(parseState.GetErrorMessage(true));
  781. continue;
  782. }
  783. if (!snippet_root.isObject()) {
  784. error_msg = cmStrCat("Expected snippet file ", snippet_path,
  785. " to contain an object");
  786. cmSystemTools::Error(error_msg);
  787. continue;
  788. }
  789. if (!snippet_root.isMember("role")) {
  790. error_msg = cmStrCat("Expected snippet file ", snippet_path,
  791. " to have a key 'role'");
  792. cmSystemTools::Error(error_msg);
  793. continue;
  794. }
  795. std::string snippet_role = snippet_root["role"].asString();
  796. auto map_element = this->cdashSnippetsMap.find(snippet_role);
  797. if (map_element == this->cdashSnippetsMap.end()) {
  798. std::string message =
  799. "Unexpected snippet type encountered: " + snippet_role;
  800. cmSystemTools::Message(message, "Warning");
  801. continue;
  802. }
  803. if (map_element->second == "skip") {
  804. continue;
  805. }
  806. if (map_element->second == "build") {
  807. // We organize snippets on a per-target basis (when possible)
  808. // for Build.xml.
  809. if (snippet_root.isMember("target")) {
  810. dst_dir = cmStrCat(this->cdashDir, "/build/targets/",
  811. snippet_root["target"].asString());
  812. cmSystemTools::MakeDirectory(dst_dir);
  813. } else {
  814. dst_dir = cmStrCat(this->cdashDir, "/build/commands");
  815. }
  816. } else {
  817. dst_dir = cmStrCat(this->cdashDir, '/', map_element->second);
  818. }
  819. std::string dst = cmStrCat(dst_dir, '/', snippet_str);
  820. cmsys::Status copied = cmSystemTools::CopyFileAlways(snippet_path, dst);
  821. if (!copied) {
  822. error_msg = cmStrCat("Failed to copy ", snippet_path, " to ", dst);
  823. cmSystemTools::Error(error_msg);
  824. }
  825. }
  826. }
  827. void cmInstrumentation::WriteTraceFile(Json::Value const& index,
  828. std::string const& trace_name)
  829. {
  830. std::string const& directory = cmStrCat(this->timingDirv1, "/data");
  831. std::vector<Json::Value> snippets = std::vector<Json::Value>();
  832. for (auto const& f : index["snippets"]) {
  833. Json::Value snippetData = this->ReadJsonSnippet(directory, f.asString());
  834. snippets.push_back(snippetData);
  835. }
  836. // Reverse-sort snippets by timeEnd (timeStart + duration) as a
  837. // prerequisite for AssignTargetToTraceThread().
  838. std::sort(snippets.begin(), snippets.end(),
  839. [](Json::Value snippetA, Json::Value snippetB) {
  840. uint64_t timeEndA = snippetA["timeStart"].asUInt64() +
  841. snippetA["duration"].asUInt64();
  842. uint64_t timeEndB = snippetB["timeStart"].asUInt64() +
  843. snippetB["duration"].asUInt64();
  844. return timeEndA > timeEndB;
  845. });
  846. Json::Value trace = Json::arrayValue;
  847. std::vector<uint64_t> workers = std::vector<uint64_t>();
  848. for (auto const& snippetData : snippets) {
  849. this->AppendTraceEvent(trace, workers, snippetData);
  850. }
  851. this->WriteInstrumentationJson(trace, "data/trace", trace_name);
  852. }
  853. void cmInstrumentation::AppendTraceEvent(Json::Value& trace,
  854. std::vector<uint64_t>& workers,
  855. Json::Value const& snippetData)
  856. {
  857. Json::Value snippetTraceEvent;
  858. // Provide a useful trace event name depending on what data is available
  859. // from the snippet.
  860. std::string name = cmStrCat(snippetData["role"].asString(), ": ");
  861. if (snippetData["role"] == "compile") {
  862. name.append(snippetData["source"].asString());
  863. } else if (snippetData["role"] == "link") {
  864. name.append(snippetData["target"].asString());
  865. } else if (snippetData["role"] == "install") {
  866. cmCMakePath workingDir(snippetData["workingDir"].asCString());
  867. std::string lastDirName = workingDir.GetFileName().String();
  868. name.append(lastDirName);
  869. } else if (snippetData["role"] == "custom") {
  870. name.append(snippetData["command"].asString());
  871. } else if (snippetData["role"] == "test") {
  872. name.append(snippetData["testName"].asString());
  873. }
  874. snippetTraceEvent["name"] = name;
  875. snippetTraceEvent["cat"] = snippetData["role"];
  876. snippetTraceEvent["ph"] = "X";
  877. snippetTraceEvent["args"] = snippetData;
  878. // Time in the Trace Event Format is stored in microseconds
  879. // but the snippet files store time in milliseconds.
  880. snippetTraceEvent["ts"] = snippetData["timeStart"].asUInt64() * 1000;
  881. snippetTraceEvent["dur"] = snippetData["duration"].asUInt64() * 1000;
  882. // Assign an arbitrary PID, since this data isn't useful for the
  883. // visualization in our case.
  884. snippetTraceEvent["pid"] = 0;
  885. // Assign TID of 0 for snippets which will have other snippet data
  886. // visualized "underneath" them. (For others, start from 1.)
  887. if (snippetData["role"] == "build" || snippetData["role"] == "cmakeBuild" ||
  888. snippetData["role"] == "ctest" ||
  889. snippetData["role"] == "cmakeInstall") {
  890. snippetTraceEvent["tid"] = 0;
  891. } else {
  892. snippetTraceEvent["tid"] = static_cast<Json::Value::UInt64>(
  893. AssignTargetToTraceThread(workers, snippetData["timeStart"].asUInt64(),
  894. snippetData["duration"].asUInt64()));
  895. }
  896. trace.append(snippetTraceEvent);
  897. }
  898. size_t cmInstrumentation::AssignTargetToTraceThread(
  899. std::vector<uint64_t>& workers, uint64_t timeStart, uint64_t duration)
  900. {
  901. for (size_t i = 0; i < workers.size(); i++) {
  902. if (workers[i] >= timeStart + duration) {
  903. workers[i] = timeStart;
  904. return i + 1;
  905. }
  906. }
  907. workers.push_back(timeStart);
  908. return workers.size();
  909. }