cmInstrumentation.cxx 32 KB

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