#include "cmInstrumentation.h" #include #include #include #include #include #include #include #include #include #include #include "cmsys/Directory.hxx" #include "cmsys/FStream.hxx" #include #include "cmCryptoHash.h" #include "cmExperimental.h" #include "cmInstrumentationQuery.h" #include "cmJSONState.h" #include "cmStringAlgorithms.h" #include "cmSystemTools.h" #include "cmTimestamp.h" #include "cmUVProcessChain.h" #include "cmValue.h" using LoadQueriesAfter = cmInstrumentation::LoadQueriesAfter; std::map cmInstrumentation::cdashSnippetsMap = { { "configure", "configure", }, { "generate", "configure", }, { "compile", "build", }, { "link", "build", }, { "custom", "build", }, { "build", "skip", }, { "cmakeBuild", "build", }, { "cmakeInstall", "build", }, { "install", "build", }, { "ctest", "build", }, { "test", "test", } }; cmInstrumentation::cmInstrumentation(std::string const& binary_dir, LoadQueriesAfter loadQueries) { std::string const uuid = cmExperimental::DataForFeature(cmExperimental::Feature::Instrumentation) .Uuid; this->binaryDir = binary_dir; this->timingDirv1 = cmStrCat(this->binaryDir, "/.cmake/instrumentation-", uuid, "/v1"); this->cdashDir = cmStrCat(this->timingDirv1, "/cdash"); if (cm::optional configDir = cmSystemTools::GetCMakeConfigDirectory()) { this->userTimingDirv1 = cmStrCat(configDir.value(), "/instrumentation-", uuid, "/v1"); } if (loadQueries == LoadQueriesAfter::Yes) { this->LoadQueries(); } } void cmInstrumentation::LoadQueries() { if (cmSystemTools::FileExists(cmStrCat(this->timingDirv1, "/query"))) { this->hasQuery = this->ReadJSONQueries(cmStrCat(this->timingDirv1, "/query")) || this->ReadJSONQueries(cmStrCat(this->timingDirv1, "/query/generated")); } if (!this->userTimingDirv1.empty() && cmSystemTools::FileExists(cmStrCat(this->userTimingDirv1, "/query"))) { this->hasQuery = this->hasQuery || this->ReadJSONQueries(cmStrCat(this->userTimingDirv1, "/query")); } } void cmInstrumentation::CheckCDashVariable() { std::string envVal; if (cmSystemTools::GetEnv("CTEST_USE_INSTRUMENTATION", envVal) && !cmIsOff(envVal)) { if (cmSystemTools::GetEnv("CTEST_EXPERIMENTAL_INSTRUMENTATION", envVal)) { std::string const uuid = cmExperimental::DataForFeature( cmExperimental::Feature::Instrumentation) .Uuid; if (envVal == uuid) { std::set options_ = { cmInstrumentationQuery::Option::CDashSubmit, cmInstrumentationQuery::Option::DynamicSystemInformation }; if (cmSystemTools::GetEnv("CTEST_USE_VERBOSE_INSTRUMENTATION", envVal) && !cmIsOff(envVal)) { options_.insert(cmInstrumentationQuery::Option::CDashVerbose); } for (auto const& option : options_) { this->AddOption(option); } std::set hooks_ = { cmInstrumentationQuery::Hook::PrepareForCDash }; this->AddHook(cmInstrumentationQuery::Hook::PrepareForCDash); this->WriteJSONQuery(options_, hooks_, {}); } } } } cmsys::SystemInformation& cmInstrumentation::GetSystemInformation() { if (!this->systemInformation) { this->systemInformation = cm::make_unique(); } return *this->systemInformation; } bool cmInstrumentation::ReadJSONQueries(std::string const& directory) { cmsys::Directory d; std::string json = ".json"; bool result = false; if (d.Load(directory)) { for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) { std::string fpath = d.GetFilePath(i); if (fpath.rfind(json) == (fpath.size() - json.size())) { result = true; this->ReadJSONQuery(fpath); } } } return result; } void cmInstrumentation::ReadJSONQuery(std::string const& file) { auto query = cmInstrumentationQuery(); query.ReadJSON(file, this->errorMsg, this->options, this->hooks, this->callbacks); if (!this->errorMsg.empty()) { cmSystemTools::Error(cmStrCat( "Could not load instrumentation queries from ", cmSystemTools::GetParentDirectory(file), ":\n", this->errorMsg)); } } bool cmInstrumentation::HasErrors() const { return !this->errorMsg.empty(); } void cmInstrumentation::WriteJSONQuery( std::set const& options_, std::set const& hooks_, std::vector> const& callbacks_) { Json::Value root; root["version"] = 1; root["options"] = Json::arrayValue; for (auto const& option : options_) { root["options"].append(cmInstrumentationQuery::OptionString[option]); } root["hooks"] = Json::arrayValue; for (auto const& hook : hooks_) { root["hooks"].append(cmInstrumentationQuery::HookString[hook]); } root["callbacks"] = Json::arrayValue; for (auto const& callback : callbacks_) { root["callbacks"].append(cmInstrumentation::GetCommandStr(callback)); } this->WriteInstrumentationJson( root, "query/generated", cmStrCat("query-", this->writtenJsonQueries++, ".json")); } void cmInstrumentation::ClearGeneratedQueries() { std::string dir = cmStrCat(this->timingDirv1, "/query/generated"); if (cmSystemTools::FileIsDirectory(dir)) { cmSystemTools::RemoveADirectory(dir); } } bool cmInstrumentation::HasQuery() const { return this->hasQuery; } bool cmInstrumentation::HasOption(cmInstrumentationQuery::Option option) const { return (this->options.find(option) != this->options.end()); } bool cmInstrumentation::HasHook(cmInstrumentationQuery::Hook hook) const { return (this->hooks.find(hook) != this->hooks.end()); } bool cmInstrumentation::HasPreOrPostBuildHook() const { return (this->HasHook(cmInstrumentationQuery::Hook::PreBuild) || this->HasHook(cmInstrumentationQuery::Hook::PostBuild)); } int cmInstrumentation::CollectTimingData(cmInstrumentationQuery::Hook hook) { // Don't run collection if hook is disabled if (hook != cmInstrumentationQuery::Hook::Manual && !this->HasHook(hook)) { return 0; } // Touch index file immediately to claim snippets std::string const& directory = cmStrCat(this->timingDirv1, "/data"); std::string const& file_name = cmStrCat("index-", ComputeSuffixTime(), ".json"); std::string index_path = cmStrCat(directory, '/', file_name); cmSystemTools::Touch(index_path, true); // Gather Snippets using snippet = std::pair; std::vector files; cmsys::Directory d; std::string last_index; if (d.Load(directory)) { for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) { std::string fpath = d.GetFilePath(i); std::string fname = d.GetFile(i); if (fname.rfind('.', 0) == 0) { continue; } if (fname == file_name) { continue; } if (fname.rfind("index-", 0) == 0) { if (last_index.empty()) { last_index = fpath; } else { int compare; cmSystemTools::FileTimeCompare(fpath, last_index, &compare); if (compare == 1) { last_index = fpath; } } } files.push_back(snippet(std::move(fname), std::move(fpath))); } } // Build Json Object Json::Value index(Json::objectValue); index["snippets"] = Json::arrayValue; index["hook"] = cmInstrumentationQuery::HookString[hook]; index["dataDir"] = directory; index["buildDir"] = this->binaryDir; index["version"] = 1; if (this->HasOption( cmInstrumentationQuery::Option::StaticSystemInformation)) { this->InsertStaticSystemInformation(index); } for (auto const& file : files) { if (last_index.empty()) { index["snippets"].append(file.first); } else { int compare; cmSystemTools::FileTimeCompare(file.second, last_index, &compare); if (compare == 1) { index["snippets"].append(file.first); } } } this->WriteInstrumentationJson(index, "data", file_name); // Execute callbacks for (auto& cb : this->callbacks) { cmSystemTools::RunSingleCommand(cmStrCat(cb, " \"", index_path, '"'), nullptr, nullptr, nullptr, nullptr, cmSystemTools::OUTPUT_PASSTHROUGH); } // Special case for CDash collation if (this->HasOption(cmInstrumentationQuery::Option::CDashSubmit)) { this->PrepareDataForCDash(directory, index_path); } // Delete files for (auto const& f : index["snippets"]) { cmSystemTools::RemoveFile(cmStrCat(directory, '/', f.asString())); } cmSystemTools::RemoveFile(index_path); return 0; } void cmInstrumentation::InsertDynamicSystemInformation( Json::Value& root, std::string const& prefix) { Json::Value data; double memory; double load; this->GetDynamicSystemInformation(memory, load); if (!root.isMember("dynamicSystemInformation")) { root["dynamicSystemInformation"] = Json::objectValue; } root["dynamicSystemInformation"][cmStrCat(prefix, "HostMemoryUsed")] = memory; root["dynamicSystemInformation"][cmStrCat(prefix, "CPULoadAverage")] = load; } void cmInstrumentation::GetDynamicSystemInformation(double& memory, double& load) { cmsys::SystemInformation& info = this->GetSystemInformation(); if (!this->ranSystemChecks) { info.RunCPUCheck(); info.RunMemoryCheck(); this->ranSystemChecks = true; } memory = (double)info.GetHostMemoryUsed(); load = info.GetLoadAverage(); } void cmInstrumentation::InsertStaticSystemInformation(Json::Value& root) { cmsys::SystemInformation& info = this->GetSystemInformation(); if (!this->ranOSCheck) { info.RunOSCheck(); this->ranOSCheck = true; } Json::Value infoRoot; infoRoot["familyId"] = info.GetFamilyID(); infoRoot["hostname"] = info.GetHostname(); infoRoot["is64Bits"] = info.Is64Bits(); infoRoot["modelId"] = info.GetModelID(); infoRoot["numberOfLogicalCPU"] = info.GetNumberOfLogicalCPU(); infoRoot["numberOfPhysicalCPU"] = info.GetNumberOfPhysicalCPU(); infoRoot["OSName"] = info.GetOSName(); infoRoot["OSPlatform"] = info.GetOSPlatform(); infoRoot["OSRelease"] = info.GetOSRelease(); infoRoot["OSVersion"] = info.GetOSVersion(); infoRoot["processorAPICID"] = info.GetProcessorAPICID(); infoRoot["processorCacheSize"] = info.GetProcessorCacheSize(); infoRoot["processorClockFrequency"] = (double)info.GetProcessorClockFrequency(); infoRoot["processorName"] = info.GetExtendedProcessorName(); infoRoot["totalPhysicalMemory"] = static_cast(info.GetTotalPhysicalMemory()); infoRoot["totalVirtualMemory"] = static_cast(info.GetTotalVirtualMemory()); infoRoot["vendorID"] = info.GetVendorID(); infoRoot["vendorString"] = info.GetVendorString(); root["staticSystemInformation"] = infoRoot; } void cmInstrumentation::InsertTimingData( Json::Value& root, std::chrono::steady_clock::time_point steadyStart, std::chrono::system_clock::time_point systemStart) { uint64_t timeStart = std::chrono::duration_cast( systemStart.time_since_epoch()) .count(); uint64_t duration = std::chrono::duration_cast( std::chrono::steady_clock::now() - steadyStart) .count(); root["timeStart"] = static_cast(timeStart); root["duration"] = static_cast(duration); } void cmInstrumentation::WriteInstrumentationJson(Json::Value& root, std::string const& subdir, std::string const& file_name) { Json::StreamWriterBuilder wbuilder; wbuilder["indentation"] = "\t"; std::unique_ptr JsonWriter = std::unique_ptr(wbuilder.newStreamWriter()); std::string const& directory = cmStrCat(this->timingDirv1, '/', subdir); cmSystemTools::MakeDirectory(directory); cmsys::ofstream ftmp(cmStrCat(directory, '/', file_name).c_str()); JsonWriter->write(root, &ftmp); ftmp << "\n"; ftmp.close(); } std::string cmInstrumentation::InstrumentTest( std::string const& name, std::string const& command, std::vector const& args, int64_t result, std::chrono::steady_clock::time_point steadyStart, std::chrono::system_clock::time_point systemStart, std::string config) { // Store command info Json::Value root(this->preTestStats); std::string command_str = cmStrCat(command, ' ', GetCommandStr(args)); root["version"] = 1; root["command"] = command_str; root["role"] = "test"; root["testName"] = name; root["result"] = static_cast(result); root["config"] = config; root["workingDir"] = cmSystemTools::GetLogicalWorkingDirectory(); // Post-Command this->InsertTimingData(root, steadyStart, systemStart); if (this->HasOption( cmInstrumentationQuery::Option::DynamicSystemInformation)) { this->InsertDynamicSystemInformation(root, "after"); } cmsys::SystemInformation& info = this->GetSystemInformation(); std::string file_name = cmStrCat( "test-", this->ComputeSuffixHash(cmStrCat(command_str, info.GetProcessId())), this->ComputeSuffixTime(), ".json"); this->WriteInstrumentationJson(root, "data", file_name); return file_name; } void cmInstrumentation::GetPreTestStats() { if (this->HasOption( cmInstrumentationQuery::Option::DynamicSystemInformation)) { this->InsertDynamicSystemInformation(this->preTestStats, "before"); } } int cmInstrumentation::InstrumentCommand( std::string command_type, std::vector const& command, std::function const& callback, cm::optional> data, cm::optional> arrayData, LoadQueriesAfter reloadQueriesAfterCommand) { // Always begin gathering data for configure in case cmake_instrumentation // command creates a query if (!this->hasQuery && reloadQueriesAfterCommand == LoadQueriesAfter::No) { return callback(); } // Store command info Json::Value root(Json::objectValue); Json::Value commandInfo(Json::objectValue); std::string command_str = GetCommandStr(command); if (!command_str.empty()) { root["command"] = command_str; } root["version"] = 1; // Pre-Command auto steady_start = std::chrono::steady_clock::now(); auto system_start = std::chrono::system_clock::now(); double preConfigureMemory = 0; double preConfigureLoad = 0; if (this->HasOption( cmInstrumentationQuery::Option::DynamicSystemInformation)) { this->InsertDynamicSystemInformation(root, "before"); } else if (reloadQueriesAfterCommand == LoadQueriesAfter::Yes) { this->GetDynamicSystemInformation(preConfigureMemory, preConfigureLoad); } // Execute Command int ret = callback(); root["result"] = ret; // Exit early if configure didn't generate a query if (reloadQueriesAfterCommand == LoadQueriesAfter::Yes) { this->LoadQueries(); if (!this->HasQuery()) { return ret; } if (this->HasOption( cmInstrumentationQuery::Option::DynamicSystemInformation)) { root["dynamicSystemInformation"] = Json::objectValue; root["dynamicSystemInformation"]["beforeHostMemoryUsed"] = preConfigureMemory; root["dynamicSystemInformation"]["beforeCPULoadAverage"] = preConfigureLoad; } } // Post-Command this->InsertTimingData(root, steady_start, system_start); if (this->HasOption( cmInstrumentationQuery::Option::DynamicSystemInformation)) { this->InsertDynamicSystemInformation(root, "after"); } // Gather additional data if (data.has_value()) { for (auto const& item : data.value()) { if (item.first == "role" && !item.second.empty()) { command_type = item.second; } else if (!item.second.empty()) { root[item.first] = item.second; } } } // Create empty config entry if config not found if (!root.isMember("config") && (command_type == "compile" || command_type == "link")) { root["config"] = ""; } if (arrayData.has_value()) { for (auto const& item : arrayData.value()) { if (item.first == "targetLabels" && command_type != "link") { continue; } root[item.first] = Json::arrayValue; std::stringstream ss(item.second); std::string element; while (getline(ss, element, ',')) { root[item.first].append(element); } if (item.first == "outputs") { root["outputSizes"] = Json::arrayValue; for (auto const& output : root["outputs"]) { root["outputSizes"].append( static_cast(cmSystemTools::FileLength( cmStrCat(this->binaryDir, '/', output.asCString())))); } } } } root["role"] = command_type; root["workingDir"] = cmSystemTools::GetLogicalWorkingDirectory(); // Write Json cmsys::SystemInformation& info = this->GetSystemInformation(); std::string const& file_name = cmStrCat( command_type, '-', this->ComputeSuffixHash(cmStrCat(command_str, info.GetProcessId())), this->ComputeSuffixTime(), ".json"); this->WriteInstrumentationJson(root, "data", file_name); return ret; } std::string cmInstrumentation::GetCommandStr( std::vector const& args) { std::string command_str; for (size_t i = 0; i < args.size(); ++i) { command_str = cmStrCat(command_str, '"', args[i], '"'); if (i < args.size() - 1) { command_str = cmStrCat(command_str, ' '); } } return command_str; } std::string cmInstrumentation::ComputeSuffixHash( std::string const& command_str) { cmCryptoHash hasher(cmCryptoHash::AlgoSHA3_256); std::string hash = hasher.HashString(command_str); hash.resize(20, '0'); return hash; } std::string cmInstrumentation::ComputeSuffixTime() { std::chrono::milliseconds ms = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()); std::chrono::seconds s = std::chrono::duration_cast(ms); std::time_t ts = s.count(); std::size_t tms = ms.count() % 1000; cmTimestamp cmts; std::ostringstream ss; ss << cmts.CreateTimestampFromTimeT(ts, "%Y-%m-%dT%H-%M-%S", true) << '-' << std::setfill('0') << std::setw(4) << tms; return ss.str(); } /* * Called by ctest --start-instrumentation as part of the START_INSTRUMENTATION * rule when using the Ninja generator. * This creates a detached process which waits for the Ninja process to die * before running the postBuild hook. In this way, the postBuild hook triggers * after every ninja invocation, regardless of whether the build passed or * failed. */ int cmInstrumentation::SpawnBuildDaemon() { // preBuild Hook this->CollectTimingData(cmInstrumentationQuery::Hook::PreBuild); // postBuild Hook if (this->HasHook(cmInstrumentationQuery::Hook::PostBuild)) { auto ninja_pid = uv_os_getppid(); if (ninja_pid) { std::vector args; args.push_back(cmSystemTools::GetCTestCommand()); args.push_back("--wait-and-collect-instrumentation"); args.push_back(this->binaryDir); args.push_back(std::to_string(ninja_pid)); auto builder = cmUVProcessChainBuilder().SetDetached().AddCommand(args); auto chain = builder.Start(); uv_run(&chain.GetLoop(), UV_RUN_DEFAULT); } } return 0; } /* * Always called by ctest --wait-and-collect-instrumentation in a detached * process. Waits for the given PID to end before running the postBuild hook. * * See SpawnBuildDaemon() */ int cmInstrumentation::CollectTimingAfterBuild(int ppid) { std::function waitForBuild = [ppid]() -> int { while (0 == uv_kill(ppid, 0)) { cmSystemTools::Delay(100); }; return 0; }; int ret = this->InstrumentCommand( "build", {}, [waitForBuild]() { return waitForBuild(); }, cm::nullopt, cm::nullopt, LoadQueriesAfter::No); this->CollectTimingData(cmInstrumentationQuery::Hook::PostBuild); return ret; } void cmInstrumentation::AddHook(cmInstrumentationQuery::Hook hook) { this->hooks.insert(hook); } void cmInstrumentation::AddOption(cmInstrumentationQuery::Option option) { this->options.insert(option); } std::string const& cmInstrumentation::GetCDashDir() { return this->cdashDir; } /** Copy the snippets referred to by an index file to a separate * directory where they will be parsed for submission to CDash. **/ void cmInstrumentation::PrepareDataForCDash(std::string const& data_dir, std::string const& index_path) { cmSystemTools::MakeDirectory(this->cdashDir); cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/configure")); cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/build")); cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/build/commands")); cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/build/targets")); cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/test")); Json::Value root; std::string error_msg; cmJSONState parseState = cmJSONState(index_path, &root); if (!parseState.errors.empty()) { cmSystemTools::Error(parseState.GetErrorMessage(true)); return; } if (!root.isObject()) { error_msg = cmStrCat("Expected index file ", index_path, " to contain an object"); cmSystemTools::Error(error_msg); return; } if (!root.isMember("snippets")) { error_msg = cmStrCat("Expected index file ", index_path, " to have a key 'snippets'"); cmSystemTools::Error(error_msg); return; } std::string dst_dir; Json::Value snippets = root["snippets"]; for (auto const& snippet : snippets) { // Parse the role of this snippet. std::string snippet_str = snippet.asString(); std::string snippet_path = cmStrCat(data_dir, '/', snippet_str); Json::Value snippet_root; parseState = cmJSONState(snippet_path, &snippet_root); if (!parseState.errors.empty()) { cmSystemTools::Error(parseState.GetErrorMessage(true)); continue; } if (!snippet_root.isObject()) { error_msg = cmStrCat("Expected snippet file ", snippet_path, " to contain an object"); cmSystemTools::Error(error_msg); continue; } if (!snippet_root.isMember("role")) { error_msg = cmStrCat("Expected snippet file ", snippet_path, " to have a key 'role'"); cmSystemTools::Error(error_msg); continue; } std::string snippet_role = snippet_root["role"].asString(); auto map_element = this->cdashSnippetsMap.find(snippet_role); if (map_element == this->cdashSnippetsMap.end()) { std::string message = "Unexpected snippet type encountered: " + snippet_role; cmSystemTools::Message(message, "Warning"); continue; } if (map_element->second == "skip") { continue; } if (map_element->second == "build") { // We organize snippets on a per-target basis (when possible) // for Build.xml. if (snippet_root.isMember("target")) { dst_dir = cmStrCat(this->cdashDir, "/build/targets/", snippet_root["target"].asString()); cmSystemTools::MakeDirectory(dst_dir); } else { dst_dir = cmStrCat(this->cdashDir, "/build/commands"); } } else { dst_dir = cmStrCat(this->cdashDir, '/', map_element->second); } std::string dst = cmStrCat(dst_dir, '/', snippet_str); cmsys::Status copied = cmSystemTools::CopyFileAlways(snippet_path, dst); if (!copied) { error_msg = cmStrCat("Failed to copy ", snippet_path, " to ", dst); cmSystemTools::Error(error_msg); } } }