diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/base/error_codes.err | 2 | ||||
-rw-r--r-- | src/mongo/util/SConscript | 20 | ||||
-rw-r--r-- | src/mongo/util/perfctr_collect.cpp | 506 | ||||
-rw-r--r-- | src/mongo/util/perfctr_collect.h | 284 | ||||
-rw-r--r-- | src/mongo/util/perfctr_collect_test.cpp | 422 |
5 files changed, 1234 insertions, 0 deletions
diff --git a/src/mongo/base/error_codes.err b/src/mongo/base/error_codes.err index 8b07c3ab8b6..e77c8dd2d94 100644 --- a/src/mongo/base/error_codes.err +++ b/src/mongo/base/error_codes.err @@ -175,6 +175,8 @@ error_code("QueryPlanKilled", 173) error_code("FileOpenFailed", 174) error_code("ZoneNotFound", 175) error_code("RangeOverlapConflict", 176) +error_code("WindowsPdhError", 177) +error_code("BadPerfCounterPath", 178) # Non-sequential error codes (for compatibility only) error_code("SocketException", 9001) diff --git a/src/mongo/util/SConscript b/src/mongo/util/SConscript index 0a4864eea40..83516dcc9da 100644 --- a/src/mongo/util/SConscript +++ b/src/mongo/util/SConscript @@ -548,5 +548,25 @@ if env.TargetOSIs('linux'): ], LIBDEPS=[ 'procparser', + ]) + +if env.TargetOSIs('windows'): + env.Library( + target='perfctr_collect', + source=[ + "perfctr_collect.cpp", + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/platform/platform', ], ) + + env.CppUnitTest( + target='perfctr_collect_test', + source=[ + 'perfctr_collect_test.cpp', + ], + LIBDEPS=[ + 'perfctr_collect', + ]) diff --git a/src/mongo/util/perfctr_collect.cpp b/src/mongo/util/perfctr_collect.cpp new file mode 100644 index 00000000000..da6667c9e77 --- /dev/null +++ b/src/mongo/util/perfctr_collect.cpp @@ -0,0 +1,506 @@ +/** + * Copyright (C) 2016 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kFTDC + +#include "mongo/platform/basic.h" + +#include "mongo/util/perfctr_collect.h" + +#include "mongo/base/init.h" +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/util/log.h" +#include "mongo/util/mongoutils/str.h" +#include "mongo/util/scopeguard.h" +#include "mongo/util/text.h" + +namespace mongo { + +namespace { + +// Handle to the PDH library so that we can format error messages. +HANDLE hPdhLibrary = nullptr; + +/** + * Load PDH.DLL for good error messages. + */ +MONGO_INITIALIZER(PdhInit)(InitializerContext* context) { + + hPdhLibrary = LoadLibraryW(L"pdh.dll"); + if (nullptr == hPdhLibrary) { + DWORD gle = GetLastError(); + return {ErrorCodes::WindowsPdhError, + str::stream() << "LoadLibrary of pdh.dll failed with " + << errnoWithDescription(gle)}; + } + + return Status::OK(); +} + +/** + * Output an error message for ether PDH or the system. + */ +std::string errnoWithPdhDescription(PDH_STATUS status) { + LPWSTR errorText = nullptr; + + if (!FormatMessageW( + FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_ALLOCATE_BUFFER, + hPdhLibrary, + status, + 0, + reinterpret_cast<LPWSTR>( + &errorText), // fudge the type so FormatMessageW uses it as an out parameter. + 0, + nullptr)) { + DWORD gle = GetLastError(); + return str::stream() << "Format message failed with " << gle << " for status " << status; + } + + ScopeGuard errorTextGuard = MakeGuard([errorText]() { LocalFree(errorText); }); + std::string utf8ErrorText = toUtf8String(errorText); + + auto size = utf8ErrorText.find_first_of("\r\n"); + if (size == std::string::npos) { + size = utf8ErrorText.length(); + } + + return utf8ErrorText.substr(0, size); +} + +/** + * Format an error message for a PDH function call failure. + */ +std::string formatFunctionCallError(StringData functionName, PDH_STATUS status) { + return str::stream() << functionName << " failed with '" << errnoWithPdhDescription(status) + << "'"; +} + +/** + * Transform a vector of string data into a vector of strings. + */ +void transformStringDataVector(const std::vector<StringData>& input, + std::vector<std::string>* output) { + output->reserve(input.size()); + for (const auto& str : input) { + output->emplace_back(str.toString()); + } +} + +/** + * Check if a counter depends on system ticks per second to compute its value from raw values. This + * is basically any counter that does not use 100NS as a base. FYI, if we query raw count counters, + * we will get the system ticks as a time base. + */ +bool counterHasTickBasedTimeBase(uint32_t type) { + return ((type & PERF_TYPE_COUNTER) == PERF_TYPE_COUNTER) && + ((type & PERF_TIMER_100NS) != PERF_TIMER_100NS); +} + +} // namespace + +StatusWith<std::vector<std::string>> PerfCounterCollection::checkCounters( + StringData name, const std::vector<StringData>& paths) { + + if (_counters.find(name.toString()) != _counters.end() || + _nestedCounters.find(name.toString()) != _nestedCounters.end()) { + return Status(ErrorCodes::DuplicateKeyValue, + str::stream() << "Duplicate group name for " << name); + } + + std::vector<std::string> stringPaths; + transformStringDataVector(paths, &stringPaths); + + // While duplicate counter paths are not a problem for PDH, they are a waste of time. + std::sort(stringPaths.begin(), stringPaths.end()); + + if (std::unique(stringPaths.begin(), stringPaths.end()) != stringPaths.end()) { + return Status(ErrorCodes::DuplicateKeyValue, + str::stream() << "Duplicate counters in paths specified"); + } + + return {stringPaths}; +} + +Status PerfCounterCollection::addCountersGroup(StringData name, + const std::vector<StringData>& paths) { + + auto swCounters = checkCounters(name, paths); + if (!swCounters.getStatus().isOK()) { + return swCounters.getStatus(); + } + + _counters.emplace(name.toString(), std::move(swCounters.getValue())); + + return Status::OK(); +} + +Status PerfCounterCollection::addCountersGroupedByInstanceName( + StringData name, const std::vector<StringData>& paths) { + + auto swCounters = checkCounters(name, paths); + if (!swCounters.getStatus().isOK()) { + return swCounters.getStatus(); + } + + _nestedCounters.emplace(name.toString(), std::move(swCounters.getValue())); + + return Status::OK(); +} + +StatusWith<std::unique_ptr<PerfCounterCollector>> PerfCounterCollector::create( + PerfCounterCollection builder) { + auto pcc = std::unique_ptr<PerfCounterCollector>(new PerfCounterCollector()); + + Status s = pcc->open(); + if (!s.isOK()) { + return s; + } + + for (const auto& kvp : builder._counters) { + s = pcc->addCountersGroup(kvp.first, kvp.second); + if (!s.isOK()) { + return s; + } + } + + // Sort to enforce predictable output in final document + std::sort(pcc->_counters.begin(), + pcc->_counters.end(), + [](const CounterGroup& a, const CounterGroup& b) { return a.name < b.name; }); + + for (const auto& kvp : builder._nestedCounters) { + s = pcc->addCountersGroupedByInstanceName(kvp.first, kvp.second); + if (!s.isOK()) { + return s; + } + } + + std::sort( + pcc->_nestedCounters.begin(), + pcc->_nestedCounters.end(), + [](const NestedCounterGroup& a, const NestedCounterGroup& b) { return a.name < b.name; }); + + pcc->checkForTicksTimeBase(); + + return {std::move(pcc)}; +} + +PerfCounterCollector::~PerfCounterCollector() { + /*ignore*/ PdhCloseQuery(_query); +} + +Status PerfCounterCollector::open() { + + PDH_STATUS status = PdhOpenQueryW(nullptr, NULL, &_query); + if (status != ERROR_SUCCESS) { + return {ErrorCodes::WindowsPdhError, formatFunctionCallError("PdhOpenQueryW", status)}; + } + + return Status::OK(); +} + +StatusWith<PerfCounterCollector::CounterInfo> PerfCounterCollector::addCounter(StringData path) { + + PDH_HCOUNTER counter{0}; + + PDH_STATUS status = + PdhAddCounterW(_query, toNativeString(path.toString().c_str()).c_str(), NULL, &counter); + + if (status != ERROR_SUCCESS) { + return {ErrorCodes::WindowsPdhError, formatFunctionCallError("PdhAddCounterW", status)}; + } + + DWORD bufferSize = 0; + + status = PdhGetCounterInfoW(counter, false, &bufferSize, nullptr); + + if (status != PDH_MORE_DATA) { + return {ErrorCodes::WindowsPdhError, formatFunctionCallError("PdhGetCounterInfoW", status)}; + } + + auto buf = std::make_unique<char[]>(bufferSize); + auto counterInfo = reinterpret_cast<PPDH_COUNTER_INFO>(buf.get()); + + status = PdhGetCounterInfoW(counter, false, &bufferSize, counterInfo); + + if (status != ERROR_SUCCESS) { + return {ErrorCodes::WindowsPdhError, formatFunctionCallError("PdhGetCounterInfoW", status)}; + } + + // A full qualified path is as such: + // "\\MYMACHINE\\Processor(0)\\% Idle Time" + // MachineName \\ Object Name (Instance Name) \\ CounterName + // Ex: + // MachineName: MYMACHINE + // Object Name: Processor + // InstanceName: 0 + // CounterName: % Idle Time + // We do not want to use Machine Name, but sometimes we want InstanceName + // + std::string firstName = str::stream() << '\\' << toUtf8String(counterInfo->szObjectName) << '\\' + << toUtf8String(counterInfo->szCounterName); + + // Compute a second name + std::string secondName(firstName); + + bool hasSecondValue = false; + + // Rate counters need time as a base + if ((counterInfo->dwType & PERF_COUNTER_DELTA) == PERF_COUNTER_DELTA || + (counterInfo->dwType & PERF_COUNTER_FRACTION) == PERF_COUNTER_FRACTION || + (counterInfo->dwType & PERF_ELAPSED_TIME) == PERF_ELAPSED_TIME) { + secondName += " Base"; + hasSecondValue = true; + } else { + invariant(counterInfo->dwType == PERF_COUNTER_RAWCOUNT || + counterInfo->dwType == PERF_COUNTER_LARGE_RAWCOUNT); + } + + // InstanceName is null for counters without instance names + return {CounterInfo{std::move(firstName), + std::move(secondName), + hasSecondValue, + counterInfo->szInstanceName ? toUtf8String(counterInfo->szInstanceName) + : std::string(), + counterInfo->dwType, + counter}}; +} + +StatusWith<std::vector<PerfCounterCollector::CounterInfo>> PerfCounterCollector::addCounters( + StringData path) { + std::wstring pathWide = toNativeString(path.toString().c_str()); + DWORD pathListLength = 0; + PDH_STATUS status = PdhExpandCounterPathW(pathWide.c_str(), nullptr, &pathListLength); + + if (status != PDH_MORE_DATA) { + return {ErrorCodes::WindowsPdhError, + str::stream() << formatFunctionCallError("PdhExpandCounterPathW", status) + << " for counter '" + << path + << "'"}; + } + + auto buf = std::make_unique<wchar_t[]>(pathListLength); + + status = PdhExpandCounterPathW(pathWide.c_str(), buf.get(), &pathListLength); + + if (status != ERROR_SUCCESS) { + return {ErrorCodes::WindowsPdhError, + formatFunctionCallError("PdhExpandCounterPathW", status)}; + } + + std::vector<CounterInfo> counters; + + // Windows' PdhExpandWildCardPathW returns a nullptr terminated array of nullptr separated + // strings. + std::vector<std::string> counterNames; + + const wchar_t* ptr = buf.get(); + while (ptr && *ptr) { + counterNames.emplace_back(toUtf8String(ptr)); + ptr += wcslen(ptr) + 1; + } + + // Sort to ensure we have a predictable ordering in the final BSON + std::sort(counterNames.begin(), counterNames.end()); + + for (const auto& name : counterNames) { + + auto swCounterInfo = addCounter(name); + if (!swCounterInfo.isOK()) { + return swCounterInfo.getStatus(); + } + + counters.emplace_back(std::move(swCounterInfo.getValue())); + } + + return {std::move(counters)}; +} + +Status PerfCounterCollector::addCountersGroup(StringData groupName, + const std::vector<std::string>& paths) { + CounterGroup group; + group.name = groupName.toString(); + + for (const auto& path : paths) { + auto swCounters = addCounters(path.c_str()); + if (!swCounters.isOK()) { + return swCounters.getStatus(); + } + + auto newCounters = swCounters.getValue(); + + std::copy(newCounters.begin(), newCounters.end(), std::back_inserter(group.counters)); + } + + _counters.emplace_back(group); + + return Status::OK(); +} + +Status PerfCounterCollector::addCountersGroupedByInstanceName( + StringData groupName, const std::vector<std::string>& paths) { + NestedCounterGroup group; + group.name = groupName.toString(); + + for (const auto& path : paths) { + auto swCounters = addCounters(path.c_str()); + if (!swCounters.isOK()) { + return swCounters.getStatus(); + } + + auto newCounters = swCounters.getValue(); + + for (const auto& counter : newCounters) { + // Verify the counter has an instance name. + if (counter.instanceName.empty()) { + return {ErrorCodes::BadValue, + str::stream() << "Counter '" << counter.firstName + << "' must be an instance specific counter."}; + } + + // Skip counters in the _Total instance category. + if (counter.instanceName == "_Total") { + continue; + } + + group.counters[counter.instanceName].emplace_back(std::move(counter)); + } + } + + _nestedCounters.emplace_back(group); + + return Status::OK(); +} + +Status PerfCounterCollector::collectCounters(const std::vector<CounterInfo>& counters, + BSONObjBuilder* builder) { + for (const auto& counterInfo : counters) { + + DWORD dwType = 0; + PDH_RAW_COUNTER rawCounter = {0}; + + PDH_STATUS status = PdhGetRawCounterValue(counterInfo.handle, &dwType, &rawCounter); + if (status != ERROR_SUCCESS) { + return {ErrorCodes::WindowsPdhError, + formatFunctionCallError("PdhGetRawCounterValue", status)}; + } + + if (counterInfo.hasSecondValue) { + // Delta, Rate, and Elapsed Time counters require the second value in the raw counter + // information + builder->append(counterInfo.firstName, rawCounter.FirstValue); + builder->append(counterInfo.secondName, rawCounter.SecondValue); + } else { + builder->append(counterInfo.firstName, rawCounter.FirstValue); + } + } + + return Status::OK(); +} + +void PerfCounterCollector::checkForTicksTimeBase() { + for (const auto& counterGroup : _counters) { + for (const auto& counter : counterGroup.counters) { + if (counterHasTickBasedTimeBase(counter.type)) { + _timeBaseTicksCounter = &counter; + return; + } + } + } + + for (const auto& counterGroup : _nestedCounters) { + for (const auto& instanceNamePair : counterGroup.counters) { + for (const auto& counter : instanceNamePair.second) { + if (counterHasTickBasedTimeBase(counter.type)) { + _timeBaseTicksCounter = &counter; + return; + } + } + } + } +} + +Status PerfCounterCollector::collect(BSONObjBuilder* builder) { + // Ask PDH to collect the counters + PDH_STATUS status = PdhCollectQueryData(_query); + if (status != ERROR_SUCCESS) { + return {ErrorCodes::WindowsPdhError, + formatFunctionCallError("PdhCollectQueryData", status)}; + } + + // Output timebase + // Counters that are based on time either use 100NS or System Ticks Per Second. + // We only need to output system ticks per second once if any counter depends on it. + // This is typically 3320310. + if (_timeBaseTicksCounter) { + int64_t timebase; + + status = PdhGetCounterTimeBase(_timeBaseTicksCounter->handle, &timebase); + if (status != ERROR_SUCCESS) { + return {ErrorCodes::WindowsPdhError, + formatFunctionCallError("PdhGetCounterTimeBase", status)}; + } + + builder->append("timebase", timebase); + } + + // Retrieve all the values that PDH collected for us. + for (const auto& counterGroup : _counters) { + BSONObjBuilder subObjBuilder(builder->subobjStart(counterGroup.name)); + + Status s = collectCounters(counterGroup.counters, &subObjBuilder); + if (!s.isOK()) { + return s; + } + + subObjBuilder.doneFast(); + } + + for (const auto& counterGroup : _nestedCounters) { + BSONObjBuilder subObjBuilder(builder->subobjStart(counterGroup.name)); + + for (const auto& instanceNamePair : counterGroup.counters) { + BSONObjBuilder instSubObjBuilder(builder->subobjStart(instanceNamePair.first)); + + Status s = collectCounters(instanceNamePair.second, &instSubObjBuilder); + if (!s.isOK()) { + return s; + } + + instSubObjBuilder.doneFast(); + } + + subObjBuilder.doneFast(); + } + + return Status::OK(); +} + +} // namespace mongo diff --git a/src/mongo/util/perfctr_collect.h b/src/mongo/util/perfctr_collect.h new file mode 100644 index 00000000000..72c74f9833a --- /dev/null +++ b/src/mongo/util/perfctr_collect.h @@ -0,0 +1,284 @@ +/** + * Copyright (C) 2016 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#pragma once + +#include <map> +#include <memory> +#include <pdh.h> +#include <pdhmsg.h> +#include <string> +#include <unordered_map> +#include <vector> + +#include "mongo/base/disallow_copying.h" +#include "mongo/base/status.h" +#include "mongo/base/status_with.h" +#include "mongo/base/string_data.h" +#include "mongo/stdx/memory.h" + +namespace mongo { + +class BSONObjBuilder; + +/** + * PerfCounterCollection contains a set of counters for PerfCounterCollector to collect. This class + * supports adding counters with wildcards. It also optionally supports grouping counters by + * instance name. + */ +class PerfCounterCollection { + MONGO_DISALLOW_COPYING(PerfCounterCollection); + + friend class PerfCounterCollector; + +public: + PerfCounterCollection() = default; + PerfCounterCollection(PerfCounterCollection&&) = default; + PerfCounterCollection& operator=(PerfCounterCollection&&) = default; + + /** + * Add vector of counters grouped under 'name'. + * + * group name - the name of the BSON document to add these counters into. + * paths - a vector of counter paths. These may contain wildcards. + * + * Errors if groupName duplicates an existing group or if paths has duplicate keys. Does not + * validate if the counters exist. + * + * Output document: + * For the following counters in "cpu": + * "\System\Processes" + * "\Processor(_Total)\% Idle Time" + * + * { + * "cpu" : { + * "\System\Processes" : 42, + * "\Processor\% Idle Time" : 12, + * "\Processor\% Idle Time Base" : 53, + * } + * } + */ + Status addCountersGroup(StringData groupName, const std::vector<StringData>& paths); + + /** + * Add vector of counters grouped under 'name', and grouped by instance name. + * + * group name - the name of the BSON document to add these counters into. + * paths - a vector of counter paths. These may contain wildcards. The '_Total' instance is + * automatically filtered since it can be computed by summing other instances. + * + * Errors if groupName duplicates an existing group or if paths has duplicate keys. Does not + * validate if the counters exist. + * + * Output document: + * For the following counters in "disks": + * "\PhysicalDisk(*)\% Disk Write Time" + * + * { + * "disks" : { + * "0 C:" : { + * "\PhysicalDisk\% Disk Write Time": 42, + * "\PhysicalDisk\% Disk Write Time Base": 32, + * }, + * "1 D:" : { + * "\PhysicalDisk\% Disk Write Time": 43, + * "\PhysicalDisk\% Disk Write Time Base": 37, + * } + * } + * } + */ + Status addCountersGroupedByInstanceName(StringData groupName, + const std::vector<StringData>& paths); + +private: + /** + * Check for duplicate group and counters. + */ + StatusWith<std::vector<std::string>> checkCounters(StringData groupName, + const std::vector<StringData>& paths); + +private: + // Vector of counters which are not sub-grouped by instance name. + std::unordered_map<std::string, std::vector<std::string>> _counters; + + // Vector of counters sub grouped by instance name. + std::unordered_map<std::string, std::vector<std::string>> _nestedCounters; +}; + +/** + * PerfCounterCollector collects a series of counters from a Performance Data Helper (PDH) Query and + * output the raw counter values to BSONObjBuilder. + */ +class PerfCounterCollector { + MONGO_DISALLOW_COPYING(PerfCounterCollector); + +public: + ~PerfCounterCollector(); + PerfCounterCollector(PerfCounterCollector&&) = default; + + /** + * Create a PerfCounterCollector to collect the performance counters in the specified + * PerfCounterCollection. + */ + static StatusWith<std::unique_ptr<PerfCounterCollector>> create(PerfCounterCollection builder); + + /** + * Collect the counters from PDH, and output their raw values into builder. + * + * For each counters, if the counter is a delta, rate, or fraction counter, the second value is + * output under the name "<counter> Base". Also, a single field is output called "timebase" if + * any counter depends on system ticks per second. + */ + Status collect(BSONObjBuilder* builder); + +private: + /** + * Describes a counter by querying PDH, and contains the necessary information to retrieve a + * counter from PDH. + */ + struct CounterInfo { + /** + * The name of the first value for a counter. This is output as: + * "\<Object Name>\<Counter Name>". + */ + std::string firstName; + + /** + * The name of the second value of a counter if the counter is a delta, rate, or fraction + * counter. This is output as: "\<Object Name>\<Counter Name> Base". + */ + std::string secondName; + + /** + * True if the counter is a delta, rate, or fraction counter, and its value should be output + * in the output BSON document. + */ + bool hasSecondValue; + + /** + * Instance name of the counter. Empty if the counter has no instance name. + */ + std::string instanceName; + + /** + * Counter Type. See PERF_* constants in winperf.h. + * https://technet.microsoft.com/en-us/library/cc785636(v=ws.10).aspx + */ + uint32_t type; + + /** + * Handle of counter to collect from. + */ + PDH_HCOUNTER handle; + }; + + /** + * A set of counters that are part of "name" in the final bson document. + */ + struct CounterGroup { + /** + * Name of the counter group. + */ + std::string name; + + /** + * Vector of counters in this group. + */ + std::vector<CounterInfo> counters; + }; + + /** + * A set of counters that are part of "name" and "instanceName" in the final bson document. + */ + struct NestedCounterGroup { + /** + * Name of the counter group. + */ + std::string name; + + /** + * A map of instance name to vector of counters to collect for each instance name. + * Ordered Map to ensure output is well-ordered. + */ + std::map<std::string, std::vector<CounterInfo>> counters; + }; + +private: + PerfCounterCollector() = default; + + /** + * Open the PDH Query. + */ + Status open(); + + /** + * Add the specified counter group to the PDH Query. + */ + Status addCountersGroup(StringData groupName, const std::vector<std::string>& paths); + + /** + * Add the specified counter group to the PDH Query grouped by instance name. + */ + Status addCountersGroupedByInstanceName(StringData groupName, + const std::vector<std::string>& paths); + + /** + * Add a counter to the PDH query and get a description of it. + */ + StatusWith<CounterInfo> addCounter(StringData path); + + /** + * Add a set of counters to the PDH query, and get descriptions of them. + */ + StatusWith<std::vector<CounterInfo>> addCounters(StringData path); + + /** + * Collect a vector of counters and output them to builder. + */ + Status collectCounters(const std::vector<CounterInfo>& counters, BSONObjBuilder* builder); + + /** + * Check if any of the counters we want depends on system ticks per second as a time base. + */ + void checkForTicksTimeBase(); + +private: + // PDH Query + HQUERY _query{INVALID_HANDLE_VALUE}; + + // Typically: CPU & Memory counters + std::vector<CounterGroup> _counters; + + // Typically: Disks counters + std::vector<NestedCounterGroup> _nestedCounters; + + // A counter that uses ticks as a timebase + const CounterInfo* _timeBaseTicksCounter{nullptr}; +}; + +} // namespace mongo diff --git a/src/mongo/util/perfctr_collect_test.cpp b/src/mongo/util/perfctr_collect_test.cpp new file mode 100644 index 00000000000..f9d6b2fda09 --- /dev/null +++ b/src/mongo/util/perfctr_collect_test.cpp @@ -0,0 +1,422 @@ +/** + * Copyright (C) 2016 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kFTDC + +#include "mongo/platform/basic.h" + +#include "mongo/util/perfctr_collect.h" + +#include <boost/filesystem.hpp> +#include <map> + +#include "mongo/bson/bsonobj.h" +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/unittest/unittest.h" +#include "mongo/util/log.h" + +namespace mongo { + +namespace { +using StringMap = std::map<std::string, uint64_t>; + +/** + * Convert BSON document nested up to 3 levels into a map where keys are dot separated paths to + * values. + */ +StringMap toNestedStringMap(BSONObj& obj) { + StringMap map; + + for (const auto& parent : obj) { + + if (parent.isABSONObj()) { + std::string parentNamePrefix = std::string(parent.fieldName()) + "."; + + for (const auto& child : parent.Obj()) { + + if (child.isABSONObj()) { + std::string childNamePrefix = parentNamePrefix + child.fieldName() + "."; + + for (const auto& grandChild : child.Obj()) { + map[childNamePrefix + grandChild.fieldName()] = grandChild.numberLong(); + } + + } else { + map[parentNamePrefix + child.fieldName()] = child.numberLong(); + } + } + } else { + map[parent.fieldName()] = parent.numberLong(); + } + } + + return map; +} + +#define ASSERT_KEY(_key) ASSERT_TRUE(stringMap.find(_key) != stringMap.end()); +#define ASSERT_NO_KEY(_key) ASSERT_TRUE(stringMap.find(_key) == stringMap.end()); + +#define ASSERT_TIMEBASE ASSERT_KEY("timebase"); +#define ASSERT_NO_TIMEBASE ASSERT_NO_KEY("timebase"); + +#define ASSERT_GROUP_AND_RAW_COUNTER(g, c) \ + ASSERT_KEY(g "." c); \ + ASSERT_NO_KEY(g "." c " Base"); + +#define ASSERT_GROUP_AND_NON_RAW_COUNTER(g, c) \ + ASSERT_KEY(g "." c); \ + ASSERT_KEY(g "." c " Base"); + +#define ASSERT_NESTED_GROUP_AND_NON_RAW_COUNTER(g, p, c) \ + ASSERT_KEY(g "." p "." c); \ + ASSERT_KEY(g "." p "." c " Base"); + +#define ASSERT_NO_NESTED_GROUP_AND_NON_RAW_COUNTER(g, p, c) \ + ASSERT_NO_KEY(g "." p "." c); \ + ASSERT_NO_KEY(g "." p "." c " Base"); + +#define COLLECT_COUNTERS_VERBOSE \ + BSONObjBuilder builder; \ + ASSERT_OK(collector->collect(&builder)); \ + auto obj = builder.obj(); \ + log() << "OBJ:" << obj; \ + auto stringMap = toNestedStringMap(obj); \ + for (const auto& kvp : stringMap) { \ + log() << "kvp " << kvp.first << " - " << kvp.second; \ + } + +#define COLLECT_COUNTERS_QUIET \ + BSONObjBuilder builder; \ + ASSERT_OK(collector->collect(&builder)); \ + auto obj = builder.obj(); \ + auto stringMap = toNestedStringMap(obj); + +#define COLLECT_COUNTERS COLLECT_COUNTERS_QUIET + +size_t kDefaultCollectionCount = 2; + +// Simple verification test +TEST(FTDCPerfCollector, TestSingleCounter) { + + PerfCounterCollection collection; + // PERF_100NSEC_TIMER + ASSERT_OK(collection.addCountersGroup("cpu", {"\\Processor(0)\\% Idle Time"})); + + auto swCollector = PerfCounterCollector::create(std::move(collection)); + ASSERT_OK(swCollector.getStatus()); + auto collector = std::move(swCollector.getValue()); + + for (size_t i = 0; i < kDefaultCollectionCount; i++) { + COLLECT_COUNTERS; + + ASSERT_NO_TIMEBASE; + ASSERT_GROUP_AND_NON_RAW_COUNTER("cpu", "\\Processor\\% Idle Time"); + } +} + + +// Simple verification test +TEST(FTDCPerfCollector, TestSingleRawCounter) { + + PerfCounterCollection collection; + // PERF_COUNTER_RAWCOUNT + ASSERT_OK(collection.addCountersGroup("cpu", {"\\System\\Processes"})); + + auto swCollector = PerfCounterCollector::create(std::move(collection)); + ASSERT_OK(swCollector.getStatus()); + auto collector = std::move(swCollector.getValue()); + + for (size_t i = 0; i < kDefaultCollectionCount; i++) { + COLLECT_COUNTERS; + + ASSERT_NO_TIMEBASE; + ASSERT_GROUP_AND_RAW_COUNTER("cpu", "\\System\\Processes"); + } +} + +// Test negative cases for collection +TEST(FTDCPerfCollector, TestBadCollectionInput) { + + PerfCounterCollection collection; + ASSERT_OK(collection.addCountersGroup("cpu", {"\\Processor(0)\\% Idle Time"})); + + // Duplicate group + ASSERT_NOT_OK(collection.addCountersGroup("cpu", {"\\Processor(0)\\% Idle Time"})); + + // Duplicate counter + ASSERT_NOT_OK(collection.addCountersGroup( + "cpu2", + { + "\\Processor(0)\\% Idle Time", "\\Processor(0)\\% Idle Time", + })); + + // Duplicate group + ASSERT_NOT_OK( + collection.addCountersGroupedByInstanceName("cpu", {"\\Processor(0)\\% Idle Time"})); + + // Duplicate counter + ASSERT_NOT_OK(collection.addCountersGroupedByInstanceName( + "cpu2", + { + "\\Processor(0)\\% Idle Time", "\\Processor(0)\\% Idle Time", + })); +} + +// Test negative collector input +TEST(FTDCPerfCollector, TestBadCollectorInput) { + // Bad counter name + { + PerfCounterCollection collection; + ASSERT_OK(collection.addCountersGroup("cpu", {"\\Processor(0)\\DOES NOT EXIST"})); + + auto swCollector = PerfCounterCollector::create(std::move(collection)); + ASSERT_NOT_OK(swCollector.getStatus()); + } + + // Bad wild card + { + PerfCounterCollection collection; + ASSERT_OK(collection.addCountersGroup("cpu", {"\\Processor(0)\\DOES*"})); + + auto swCollector = PerfCounterCollector::create(std::move(collection)); + ASSERT_NOT_OK(swCollector.getStatus()); + } + + // Use addCounterGroup with instance wildcard + { + PerfCounterCollection collection; + ASSERT_OK(collection.addCountersGroup("cpu", {"\\Processor(*)\\\\% Idle Time"})); + + auto swCollector = PerfCounterCollector::create(std::move(collection)); + ASSERT_NOT_OK(swCollector.getStatus()); + } + + // Use addCountersGroupedByInstanceName without instance name + { + PerfCounterCollection collection; + ASSERT_OK(collection.addCountersGroupedByInstanceName("cpu", {"\\System\\Processes"})); + + auto swCollector = PerfCounterCollector::create(std::move(collection)); + ASSERT_NOT_OK(swCollector.getStatus()); + } +} + +// Test all the different counter types we use in the MongoDB code +TEST(FTDCPerfCollector, TestCounterTypes) { + + PerfCounterCollection collection; + ASSERT_OK(collection.addCountersGroup( + "misc", + { + "\\Processor(0)\\% Idle Time", // PERF_100NSEC_TIMER + "\\Processor(0)\\% Processor Time", // PERF_100NSEC_TIMER_INV + "\\System\\Processes", // PERF_COUNTER_RAWCOUNT + "\\System\\System Up Time", // PERF_ELAPSED_TIME + "\\Memory\\Available Bytes", // PERF_COUNTER_LARGE_RAWCOUNT + "\\PhysicalDisk(_Total)\\% Disk Write Time", // PERF_PRECISION_100NS_TIMER + "\\PhysicalDisk(_Total)\\Avg. Disk Bytes/Write", // PERF_AVERAGE_BULK + "\\PhysicalDisk(_Total)\\Avg. Disk Read Queue Length", // PERF_COUNTER_LARGE_QUEUELEN_TYPE + "\\PhysicalDisk(_Total)\\Avg. Disk sec/Write", // PERF_AVERAGE_TIMER + "\\PhysicalDisk(_Total)\\Disk Write Bytes/sec", // PERF_COUNTER_BULK_COUNT + "\\PhysicalDisk(_Total)\\Disk Writes/sec", // PERF_COUNTER_COUNTER + })); + + auto swCollector = PerfCounterCollector::create(std::move(collection)); + ASSERT_OK(swCollector.getStatus()); + auto collector = std::move(swCollector.getValue()); + + for (size_t i = 0; i < kDefaultCollectionCount; i++) { + COLLECT_COUNTERS; + + ASSERT_TIMEBASE + ASSERT_GROUP_AND_NON_RAW_COUNTER("misc", "\\Processor\\% Idle Time"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("misc", "\\Processor\\% Processor Time"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("misc", "\\System\\System Up Time"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("misc", "\\PhysicalDisk\\% Disk Write Time"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("misc", "\\PhysicalDisk\\Avg. Disk Bytes/Write"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("misc", "\\PhysicalDisk\\Avg. Disk Read Queue Length"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("misc", "\\PhysicalDisk\\Avg. Disk sec/Write"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("misc", "\\PhysicalDisk\\Disk Write Bytes/sec"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("misc", "\\PhysicalDisk\\Disk Writes/sec"); + + ASSERT_GROUP_AND_RAW_COUNTER("misc", "\\System\\Processes"); + } +} + +// Test multiple counter groups +TEST(FTDCPerfCollector, TestMultipleCounterGroups) { + + PerfCounterCollection collection; + ASSERT_OK(collection.addCountersGroup( + "cpu", {"\\Processor(0)\\% Idle Time", "\\Processor(0)\\% Processor Time"})); + ASSERT_OK( + collection.addCountersGroup("sys", {"\\System\\Processes", "\\System\\System Up Time"})); + + auto swCollector = PerfCounterCollector::create(std::move(collection)); + ASSERT_OK(swCollector.getStatus()); + auto collector = std::move(swCollector.getValue()); + + for (size_t i = 0; i < kDefaultCollectionCount; i++) { + COLLECT_COUNTERS; + + ASSERT_TIMEBASE + ASSERT_GROUP_AND_NON_RAW_COUNTER("cpu", "\\Processor\\% Idle Time"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("cpu", "\\Processor\\% Processor Time"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("sys", "\\System\\System Up Time"); + + ASSERT_GROUP_AND_RAW_COUNTER("sys", "\\System\\Processes"); + } +} + +// Test multiple nested counter groups +TEST(FTDCPerfCollector, TestMultipleNestedCounterGroups) { + + PerfCounterCollection collection; + ASSERT_OK(collection.addCountersGroupedByInstanceName( + "cpu", {"\\Processor(*)\\% Idle Time", "\\Processor(*)\\% Processor Time"})); + ASSERT_OK( + collection.addCountersGroup("sys", {"\\System\\Processes", "\\System\\System Up Time"})); + + auto swCollector = PerfCounterCollector::create(std::move(collection)); + ASSERT_OK(swCollector.getStatus()); + auto collector = std::move(swCollector.getValue()); + + for (size_t i = 0; i < kDefaultCollectionCount; i++) { + COLLECT_COUNTERS; + ASSERT_TIMEBASE + + // We boldly assume that machines we test on have at least two processors + ASSERT_NESTED_GROUP_AND_NON_RAW_COUNTER("cpu", "0", "\\Processor\\% Idle Time"); + ASSERT_NESTED_GROUP_AND_NON_RAW_COUNTER("cpu", "0", "\\Processor\\% Processor Time"); + + ASSERT_NESTED_GROUP_AND_NON_RAW_COUNTER("cpu", "1", "\\Processor\\% Idle Time"); + ASSERT_NESTED_GROUP_AND_NON_RAW_COUNTER("cpu", "1", "\\Processor\\% Processor Time"); + + ASSERT_NO_NESTED_GROUP_AND_NON_RAW_COUNTER("cpu", "_Total", "\\Processor\\% Idle Time"); + ASSERT_NO_NESTED_GROUP_AND_NON_RAW_COUNTER( + "cpu", "_Total", "\\Processor\\% Processor Time"); + + ASSERT_GROUP_AND_NON_RAW_COUNTER("sys", "\\System\\System Up Time"); + ASSERT_GROUP_AND_RAW_COUNTER("sys", "\\System\\Processes"); + } +} + +// Test Counters we use in MongoDB +TEST(FTDCPerfCollector, TestLocalCounters) { + + PerfCounterCollection collection; + ASSERT_OK(collection.addCountersGroup("cpu", + { + "\\Processor(_Total)\\% Idle Time", + "\\Processor(_Total)\\% Interrupt Time", + "\\Processor(_Total)\\% Privileged Time", + "\\Processor(_Total)\\% Processor Time", + "\\Processor(_Total)\\% User Time", + "\\Processor(_Total)\\Interrupts/sec", + "\\System\\Context Switches/sec", + "\\System\\Processes", + "\\System\\Processor Queue Length", + "\\System\\System Up Time", + "\\System\\Threads", + })); + + // TODO: Should we capture the Heap Counters for the current process? + ASSERT_OK(collection.addCountersGroup("memory", + { + "\\Memory\\Available Bytes", + "\\Memory\\Cache Bytes", + "\\Memory\\Cache Faults/sec", + "\\Memory\\Committed Bytes", + "\\Memory\\Commit Limit", + "\\Memory\\Page Reads/sec", + "\\Memory\\Page Writes/sec", + "\\Memory\\Pages Input/sec", + "\\Memory\\Pages Output/sec", + "\\Memory\\Pool Nonpaged Bytes", + "\\Memory\\Pool Paged Bytes", + "\\Memory\\Pool Paged Resident Bytes", + "\\Memory\\System Cache Resident Bytes", + "\\Memory\\System Code Total Bytes", + })); + + ASSERT_OK(collection.addCountersGroupedByInstanceName( + "disks", + { + "\\PhysicalDisk(*)\\% Disk Read Time", + "\\PhysicalDisk(*)\\% Disk Write Time", + "\\PhysicalDisk(*)\\Avg. Disk Bytes/Read", + "\\PhysicalDisk(*)\\Avg. Disk Bytes/Write", + "\\PhysicalDisk(*)\\Avg. Disk Read Queue Length", + "\\PhysicalDisk(*)\\Avg. Disk Write Queue Length", + "\\PhysicalDisk(*)\\Avg. Disk sec/Read", + "\\PhysicalDisk(*)\\Avg. Disk sec/Write", + "\\PhysicalDisk(*)\\Disk Read Bytes/sec", + "\\PhysicalDisk(*)\\Disk Write Bytes/sec", + "\\PhysicalDisk(*)\\Disk Reads/sec", + "\\PhysicalDisk(*)\\Disk Writes/sec", + "\\PhysicalDisk(*)\\Current Disk Queue Length", + })); + + auto swCollector = PerfCounterCollector::create(std::move(collection)); + ASSERT_OK(swCollector.getStatus()); + auto collector = std::move(swCollector.getValue()); + + for (size_t i = 0; i < kDefaultCollectionCount; i++) { + COLLECT_COUNTERS; + ASSERT_TIMEBASE + + ASSERT_GROUP_AND_NON_RAW_COUNTER("cpu", "\\Processor\\% Idle Time"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("cpu", "\\Processor\\% Interrupt Time"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("cpu", "\\Processor\\% Privileged Time"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("cpu", "\\Processor\\% Processor Time"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("cpu", "\\Processor\\% User Time"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("cpu", "\\Processor\\Interrupts/sec"); + + ASSERT_GROUP_AND_NON_RAW_COUNTER("cpu", "\\System\\Context Switches/sec"); + ASSERT_GROUP_AND_RAW_COUNTER("cpu", "\\System\\Processes"); + ASSERT_GROUP_AND_RAW_COUNTER("cpu", "\\System\\Processor Queue Length"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("cpu", "\\System\\System Up Time"); + ASSERT_GROUP_AND_RAW_COUNTER("cpu", "\\System\\Threads"); + + ASSERT_GROUP_AND_RAW_COUNTER("memory", "\\Memory\\Available Bytes"); + ASSERT_GROUP_AND_RAW_COUNTER("memory", "\\Memory\\Cache Bytes"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("memory", "\\Memory\\Cache Faults/sec"); + ASSERT_GROUP_AND_RAW_COUNTER("memory", "\\Memory\\Commit Limit"); + ASSERT_GROUP_AND_RAW_COUNTER("memory", "\\Memory\\Committed Bytes"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("memory", "\\Memory\\Page Reads/sec"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("memory", "\\Memory\\Page Writes/sec"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("memory", "\\Memory\\Pages Input/sec"); + ASSERT_GROUP_AND_NON_RAW_COUNTER("memory", "\\Memory\\Pages Output/sec"); + ASSERT_GROUP_AND_RAW_COUNTER("memory", "\\Memory\\Pool Paged Resident Bytes"); + ASSERT_GROUP_AND_RAW_COUNTER("memory", "\\Memory\\System Cache Resident Bytes"); + ASSERT_GROUP_AND_RAW_COUNTER("memory", "\\Memory\\System Code Total Bytes"); + } +} + +} // namespace +} // namespace mongo |