cmInstrumentation.cxx 32 KB

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