/* $Id: VBoxManageMetrics.cpp 98103 2023-01-17 14:15:46Z vboxsync $ */ /** @file * VBoxManage - The 'metrics' command. */ /* * Copyright (C) 2006-2023 Oracle and/or its affiliates. * * This file is part of VirtualBox base platform packages, as * available from https://www.virtualbox.org. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, in version 3 of the * License. * * 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 * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * SPDX-License-Identifier: GPL-3.0-only */ /********************************************************************************************************************************* * Header Files * *********************************************************************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "VBoxManage.h" using namespace com; DECLARE_TRANSLATION_CONTEXT(Metrics); // funcs /////////////////////////////////////////////////////////////////////////////// static HRESULT parseFilterParameters(int argc, char *argv[], ComPtr aVirtualBox, ComSafeArrayOut(BSTR, outMetrics), ComSafeArrayOut(IUnknown *, outObjects)) { HRESULT hrc = S_OK; com::SafeArray retMetrics(1); com::SafeIfaceArray retObjects; Bstr metricNames, baseNames; /* Metric list */ if (argc > 1) metricNames = argv[1]; else { metricNames = L"*"; baseNames = L"*"; } metricNames.cloneTo(&retMetrics[0]); /* Object name */ if (argc > 0 && strcmp(argv[0], "*")) { if (!strcmp(argv[0], "host")) { ComPtr host; CHECK_ERROR(aVirtualBox, COMGETTER(Host)(host.asOutParam())); retObjects.reset(1); host.queryInterfaceTo(&retObjects[0]); } else { ComPtr machine; hrc = aVirtualBox->FindMachine(Bstr(argv[0]).raw(), machine.asOutParam()); if (SUCCEEDED(hrc)) { retObjects.reset(1); machine.queryInterfaceTo(&retObjects[0]); } else { errorArgument(Metrics::tr("Invalid machine name: '%s'"), argv[0]); return hrc; } } } retMetrics.detachTo(ComSafeArrayOutArg(outMetrics)); retObjects.detachTo(ComSafeArrayOutArg(outObjects)); return hrc; } static Bstr toBaseName(Utf8Str& aFullName) { char *pszRaw = aFullName.mutableRaw(); /* * Currently there are two metrics which base name is the same as the * sub-metric name: CPU/MHz and Net//LinkSpeed. */ if (pszRaw && strcmp(pszRaw, "CPU/MHz") && !RTStrSimplePatternMatch("Net/*/LinkSpeed", pszRaw)) { char *pszSlash = strrchr(pszRaw, '/'); if (pszSlash) { *pszSlash = 0; aFullName.jolt(); } } return Bstr(aFullName); } static Bstr getObjectName(ComPtr aObject) { HRESULT hrc; ComPtr host = aObject; if (!host.isNull()) return Bstr(Metrics::tr("host")); ComPtr machine = aObject; if (!machine.isNull()) { Bstr name; CHECK_ERROR(machine, COMGETTER(Name)(name.asOutParam())); if (SUCCEEDED(hrc)) return name; } return Bstr(Metrics::tr("unknown")); } static void listAffectedMetrics(ComSafeArrayIn(IPerformanceMetric*, aMetrics)) { HRESULT hrc; com::SafeIfaceArray metrics(ComSafeArrayInArg(aMetrics)); if (metrics.size()) { ComPtr object; Bstr metricName; RTPrintf(Metrics::tr("The following metrics were modified:\n\n" "Object Metric\n" "---------- --------------------\n")); for (size_t i = 0; i < metrics.size(); i++) { CHECK_ERROR(metrics[i], COMGETTER(Object)(object.asOutParam())); CHECK_ERROR(metrics[i], COMGETTER(MetricName)(metricName.asOutParam())); RTPrintf("%-10ls %-20ls\n", getObjectName(object).raw(), metricName.raw()); } RTPrintf("\n"); } else { RTMsgError(Metrics::tr("No metrics match the specified filter!")); } } /** * list */ static RTEXITCODE handleMetricsList(int argc, char *argv[], ComPtr aVirtualBox, ComPtr performanceCollector) { HRESULT hrc; com::SafeArray metrics; com::SafeIfaceArray objects; setCurrentSubcommand(HELP_SCOPE_METRICS_LIST); hrc = parseFilterParameters(argc - 1, &argv[1], aVirtualBox, ComSafeArrayAsOutParam(metrics), ComSafeArrayAsOutParam(objects)); if (FAILED(hrc)) return RTEXITCODE_FAILURE; com::SafeIfaceArray metricInfo; CHECK_ERROR(performanceCollector, GetMetrics(ComSafeArrayAsInParam(metrics), ComSafeArrayAsInParam(objects), ComSafeArrayAsOutParam(metricInfo))); ComPtr object; Bstr metricName, unit, description; ULONG period, count; LONG minimum, maximum; RTPrintf(Metrics::tr( "Object Metric Unit Minimum Maximum Period Count Description\n" "--------------- ---------------------------------------- ---- ---------- ---------- ---------- ---------- -----------\n")); for (size_t i = 0; i < metricInfo.size(); i++) { CHECK_ERROR(metricInfo[i], COMGETTER(Object)(object.asOutParam())); CHECK_ERROR(metricInfo[i], COMGETTER(MetricName)(metricName.asOutParam())); CHECK_ERROR(metricInfo[i], COMGETTER(Period)(&period)); CHECK_ERROR(metricInfo[i], COMGETTER(Count)(&count)); CHECK_ERROR(metricInfo[i], COMGETTER(MinimumValue)(&minimum)); CHECK_ERROR(metricInfo[i], COMGETTER(MaximumValue)(&maximum)); CHECK_ERROR(metricInfo[i], COMGETTER(Unit)(unit.asOutParam())); CHECK_ERROR(metricInfo[i], COMGETTER(Description)(description.asOutParam())); RTPrintf("%-15ls %-40ls %-4ls %10d %10d %10u %10u %ls\n", getObjectName(object).raw(), metricName.raw(), unit.raw(), minimum, maximum, period, count, description.raw()); } return RTEXITCODE_SUCCESS; } /** * Metrics setup */ static RTEXITCODE handleMetricsSetup(int argc, char *argv[], ComPtr aVirtualBox, ComPtr performanceCollector) { HRESULT hrc; com::SafeArray metrics; com::SafeIfaceArray objects; uint32_t period = 1, samples = 1; bool listMatches = false; int i; setCurrentSubcommand(HELP_SCOPE_METRICS_SETUP); for (i = 1; i < argc; i++) { if ( !strcmp(argv[i], "--period") || !strcmp(argv[i], "-period")) { if (argc <= i + 1) return errorArgument(Metrics::tr("Missing argument to '%s'"), argv[i]); if ( VINF_SUCCESS != RTStrToUInt32Full(argv[++i], 10, &period) || !period) return errorArgument(Metrics::tr("Invalid value for 'period' parameter: '%s'"), argv[i]); } else if ( !strcmp(argv[i], "--samples") || !strcmp(argv[i], "-samples")) { if (argc <= i + 1) return errorArgument(Metrics::tr("Missing argument to '%s'"), argv[i]); if ( VINF_SUCCESS != RTStrToUInt32Full(argv[++i], 10, &samples) || !samples) return errorArgument(Metrics::tr("Invalid value for 'samples' parameter: '%s'"), argv[i]); } else if ( !strcmp(argv[i], "--list") || !strcmp(argv[i], "-list")) listMatches = true; else break; /* The rest of params should define the filter */ } hrc = parseFilterParameters(argc - i, &argv[i], aVirtualBox, ComSafeArrayAsOutParam(metrics), ComSafeArrayAsOutParam(objects)); if (FAILED(hrc)) return RTEXITCODE_FAILURE; com::SafeIfaceArray affectedMetrics; CHECK_ERROR(performanceCollector, SetupMetrics(ComSafeArrayAsInParam(metrics), ComSafeArrayAsInParam(objects), period, samples, ComSafeArrayAsOutParam(affectedMetrics))); if (FAILED(hrc)) return RTEXITCODE_SYNTAX; /** @todo figure out why we must return 2 here. */ if (listMatches) listAffectedMetrics(ComSafeArrayAsInParam(affectedMetrics)); return RTEXITCODE_SUCCESS; } /** * metrics query */ static RTEXITCODE handleMetricsQuery(int argc, char *argv[], ComPtr aVirtualBox, ComPtr performanceCollector) { HRESULT hrc; com::SafeArray metrics; com::SafeIfaceArray objects; setCurrentSubcommand(HELP_SCOPE_METRICS_QUERY); hrc = parseFilterParameters(argc - 1, &argv[1], aVirtualBox, ComSafeArrayAsOutParam(metrics), ComSafeArrayAsOutParam(objects)); if (FAILED(hrc)) return RTEXITCODE_FAILURE; com::SafeArray retNames; com::SafeIfaceArray retObjects; com::SafeArray retUnits; com::SafeArray retScales; com::SafeArray retSequenceNumbers; com::SafeArray retIndices; com::SafeArray retLengths; com::SafeArray retData; CHECK_ERROR(performanceCollector, QueryMetricsData(ComSafeArrayAsInParam(metrics), ComSafeArrayAsInParam(objects), ComSafeArrayAsOutParam(retNames), ComSafeArrayAsOutParam(retObjects), ComSafeArrayAsOutParam(retUnits), ComSafeArrayAsOutParam(retScales), ComSafeArrayAsOutParam(retSequenceNumbers), ComSafeArrayAsOutParam(retIndices), ComSafeArrayAsOutParam(retLengths), ComSafeArrayAsOutParam(retData)) ); RTPrintf(Metrics::tr( "Object Metric Values\n" "--------------- ---------------------------------------- --------------------------------------------\n")); for (unsigned i = 0; i < retNames.size(); i++) { Bstr metricUnit(retUnits[i]); Bstr metricName(retNames[i]); RTPrintf("%-15ls %-40ls ", getObjectName(retObjects[i]).raw(), metricName.raw()); const char *separator = ""; for (unsigned j = 0; j < retLengths[i]; j++) { if (retScales[i] == 1) RTPrintf("%s%d %ls", separator, retData[retIndices[i] + j], metricUnit.raw()); else RTPrintf("%s%d.%02d%ls", separator, retData[retIndices[i] + j] / retScales[i], (retData[retIndices[i] + j] * 100 / retScales[i]) % 100, metricUnit.raw()); separator = ", "; } RTPrintf("\n"); } return RTEXITCODE_SUCCESS; } static void getTimestamp(char *pts, size_t tsSize) { *pts = 0; AssertReturnVoid(tsSize >= 13); /* 3+3+3+3+1 */ RTTIMESPEC TimeSpec; RTTIME Time; RTTimeExplode(&Time, RTTimeNow(&TimeSpec)); pts += RTStrFormatNumber(pts, Time.u8Hour, 10, 2, 0, RTSTR_F_ZEROPAD); *pts++ = ':'; pts += RTStrFormatNumber(pts, Time.u8Minute, 10, 2, 0, RTSTR_F_ZEROPAD); *pts++ = ':'; pts += RTStrFormatNumber(pts, Time.u8Second, 10, 2, 0, RTSTR_F_ZEROPAD); *pts++ = '.'; pts += RTStrFormatNumber(pts, Time.u32Nanosecond / 1000000, 10, 3, 0, RTSTR_F_ZEROPAD); *pts = 0; } /** Used by the handleMetricsCollect loop. */ static bool volatile g_fKeepGoing = true; #ifdef RT_OS_WINDOWS /** * Handler routine for catching Ctrl-C, Ctrl-Break and closing of * the console. * * @returns true if handled, false if not handled. * @param dwCtrlType The type of control signal. * * @remarks This is called on a new thread. */ static BOOL WINAPI ctrlHandler(DWORD dwCtrlType) RT_NOTHROW_DEF { switch (dwCtrlType) { /* Ctrl-C or Ctrl-Break or Close */ case CTRL_C_EVENT: case CTRL_BREAK_EVENT: case CTRL_CLOSE_EVENT: /* Let's shut down gracefully. */ ASMAtomicWriteBool(&g_fKeepGoing, false); return TRUE; } /* Don't care about the rest -- let it die a horrible death. */ return FALSE; } #endif /* RT_OS_WINDOWS */ /** * collect */ static RTEXITCODE handleMetricsCollect(int argc, char *argv[], ComPtr aVirtualBox, ComPtr performanceCollector) { HRESULT hrc; com::SafeArray metrics; com::SafeIfaceArray objects; uint32_t period = 1, samples = 1; bool isDetached = false, listMatches = false; int i; setCurrentSubcommand(HELP_SCOPE_METRICS_COLLECT); for (i = 1; i < argc; i++) { if ( !strcmp(argv[i], "--period") || !strcmp(argv[i], "-period")) { if (argc <= i + 1) return errorArgument(Metrics::tr("Missing argument to '%s'"), argv[i]); if ( VINF_SUCCESS != RTStrToUInt32Full(argv[++i], 10, &period) || !period) return errorArgument(Metrics::tr("Invalid value for 'period' parameter: '%s'"), argv[i]); } else if ( !strcmp(argv[i], "--samples") || !strcmp(argv[i], "-samples")) { if (argc <= i + 1) return errorArgument(Metrics::tr("Missing argument to '%s'"), argv[i]); if ( VINF_SUCCESS != RTStrToUInt32Full(argv[++i], 10, &samples) || !samples) return errorArgument(Metrics::tr("Invalid value for 'samples' parameter: '%s'"), argv[i]); } else if ( !strcmp(argv[i], "--list") || !strcmp(argv[i], "-list")) listMatches = true; else if ( !strcmp(argv[i], "--detach") || !strcmp(argv[i], "-detach")) isDetached = true; else break; /* The rest of params should define the filter */ } hrc = parseFilterParameters(argc - i, &argv[i], aVirtualBox, ComSafeArrayAsOutParam(metrics), ComSafeArrayAsOutParam(objects)); if (FAILED(hrc)) return RTEXITCODE_FAILURE; com::SafeIfaceArray metricInfo; CHECK_ERROR(performanceCollector, GetMetrics(ComSafeArrayAsInParam(metrics), ComSafeArrayAsInParam(objects), ComSafeArrayAsOutParam(metricInfo))); std::set,Bstr> > baseMetrics; ComPtr objectFiltered; Bstr metricNameFiltered; for (i = 0; i < (int)metricInfo.size(); i++) { CHECK_ERROR(metricInfo[i], COMGETTER(Object)(objectFiltered.asOutParam())); CHECK_ERROR(metricInfo[i], COMGETTER(MetricName)(metricNameFiltered.asOutParam())); Utf8Str baseMetricName(metricNameFiltered); baseMetrics.insert(std::make_pair(objectFiltered, toBaseName(baseMetricName))); } com::SafeArray baseMetricsFiltered(baseMetrics.size()); com::SafeIfaceArray objectsFiltered(baseMetrics.size()); std::set,Bstr> >::iterator it; i = 0; for (it = baseMetrics.begin(); it != baseMetrics.end(); ++it) { it->first.queryInterfaceTo(&objectsFiltered[i]); Bstr(it->second).detachTo(&baseMetricsFiltered[i++]); } com::SafeIfaceArray affectedMetrics; CHECK_ERROR(performanceCollector, SetupMetrics(ComSafeArrayAsInParam(baseMetricsFiltered), ComSafeArrayAsInParam(objectsFiltered), period, samples, ComSafeArrayAsOutParam(affectedMetrics))); if (FAILED(hrc)) return RTEXITCODE_SYNTAX; /** @todo figure out why we must return 2 here. */ if (listMatches) listAffectedMetrics(ComSafeArrayAsInParam(affectedMetrics)); if (!affectedMetrics.size()) return RTEXITCODE_FAILURE; if (isDetached) { RTMsgWarning(Metrics::tr("The background process holding collected metrics will shutdown\n" "in few seconds, discarding all collected data and parameters.")); return RTEXITCODE_SUCCESS; } #ifdef RT_OS_WINDOWS SetConsoleCtrlHandler(ctrlHandler, true); #endif /* RT_OS_WINDOWS */ RTPrintf(Metrics::tr("Time stamp Object Metric Value\n")); while (g_fKeepGoing) { RTPrintf("------------ ---------- -------------------- --------------------\n"); RTThreadSleep(period * 1000); // Sleep for 'period' seconds char ts[15]; getTimestamp(ts, sizeof(ts)); com::SafeArray retNames; com::SafeIfaceArray retObjects; com::SafeArray retUnits; com::SafeArray retScales; com::SafeArray retSequenceNumbers; com::SafeArray retIndices; com::SafeArray retLengths; com::SafeArray retData; CHECK_ERROR(performanceCollector, QueryMetricsData(ComSafeArrayAsInParam(metrics), ComSafeArrayAsInParam(objects), ComSafeArrayAsOutParam(retNames), ComSafeArrayAsOutParam(retObjects), ComSafeArrayAsOutParam(retUnits), ComSafeArrayAsOutParam(retScales), ComSafeArrayAsOutParam(retSequenceNumbers), ComSafeArrayAsOutParam(retIndices), ComSafeArrayAsOutParam(retLengths), ComSafeArrayAsOutParam(retData)) ); for (unsigned j = 0; j < retNames.size(); j++) { Bstr metricUnit(retUnits[j]); Bstr metricName(retNames[j]); RTPrintf("%-12s %-10ls %-20ls ", ts, getObjectName(retObjects[j]).raw(), metricName.raw()); const char *separator = ""; for (unsigned k = 0; k < retLengths[j]; k++) { if (retScales[j] == 1) RTPrintf("%s%d %ls", separator, retData[retIndices[j] + k], metricUnit.raw()); else RTPrintf("%s%d.%02d%ls", separator, retData[retIndices[j] + k] / retScales[j], (retData[retIndices[j] + k] * 100 / retScales[j]) % 100, metricUnit.raw()); separator = ", "; } RTPrintf("\n"); } RTStrmFlush(g_pStdOut); } #ifdef RT_OS_WINDOWS SetConsoleCtrlHandler(ctrlHandler, false); #endif /* RT_OS_WINDOWS */ return RTEXITCODE_SUCCESS; } /** * Enable metrics */ static RTEXITCODE handleMetricsEnable(int argc, char *argv[], ComPtr aVirtualBox, ComPtr performanceCollector) { HRESULT hrc; com::SafeArray metrics; com::SafeIfaceArray objects; bool listMatches = false; int i; setCurrentSubcommand(HELP_SCOPE_METRICS_ENABLE); for (i = 1; i < argc; i++) { if ( !strcmp(argv[i], "--list") || !strcmp(argv[i], "-list")) listMatches = true; else break; /* The rest of params should define the filter */ } hrc = parseFilterParameters(argc - i, &argv[i], aVirtualBox, ComSafeArrayAsOutParam(metrics), ComSafeArrayAsOutParam(objects)); if (FAILED(hrc)) return RTEXITCODE_FAILURE; com::SafeIfaceArray affectedMetrics; CHECK_ERROR(performanceCollector, EnableMetrics(ComSafeArrayAsInParam(metrics), ComSafeArrayAsInParam(objects), ComSafeArrayAsOutParam(affectedMetrics))); if (FAILED(hrc)) return RTEXITCODE_SYNTAX; /** @todo figure out why we must return 2 here. */ if (listMatches) listAffectedMetrics(ComSafeArrayAsInParam(affectedMetrics)); return RTEXITCODE_SUCCESS; } /** * Disable metrics */ static RTEXITCODE handleMetricsDisable(int argc, char *argv[], ComPtr aVirtualBox, ComPtr performanceCollector) { HRESULT hrc; com::SafeArray metrics; com::SafeIfaceArray objects; bool listMatches = false; int i; setCurrentSubcommand(HELP_SCOPE_METRICS_DISABLE); for (i = 1; i < argc; i++) { if ( !strcmp(argv[i], "--list") || !strcmp(argv[i], "-list")) listMatches = true; else break; /* The rest of params should define the filter */ } hrc = parseFilterParameters(argc - i, &argv[i], aVirtualBox, ComSafeArrayAsOutParam(metrics), ComSafeArrayAsOutParam(objects)); if (FAILED(hrc)) return RTEXITCODE_FAILURE; com::SafeIfaceArray affectedMetrics; CHECK_ERROR(performanceCollector, DisableMetrics(ComSafeArrayAsInParam(metrics), ComSafeArrayAsInParam(objects), ComSafeArrayAsOutParam(affectedMetrics))); if (FAILED(hrc)) return RTEXITCODE_SYNTAX; /** @todo figure out why we must return 2 here. */ if (listMatches) listAffectedMetrics(ComSafeArrayAsInParam(affectedMetrics)); return RTEXITCODE_SUCCESS; } RTEXITCODE handleMetrics(HandlerArg *a) { /* at least one option: subcommand name */ if (a->argc < 1) return errorSyntax(Metrics::tr("Subcommand missing")); ComPtr performanceCollector; CHECK_ERROR2I_RET(a->virtualBox, COMGETTER(PerformanceCollector)(performanceCollector.asOutParam()), RTEXITCODE_FAILURE); RTEXITCODE rcExit; if (!strcmp(a->argv[0], "list")) rcExit = handleMetricsList(a->argc, a->argv, a->virtualBox, performanceCollector); else if (!strcmp(a->argv[0], "setup")) rcExit = handleMetricsSetup(a->argc, a->argv, a->virtualBox, performanceCollector); else if (!strcmp(a->argv[0], "query")) rcExit = handleMetricsQuery(a->argc, a->argv, a->virtualBox, performanceCollector); else if (!strcmp(a->argv[0], "collect")) rcExit = handleMetricsCollect(a->argc, a->argv, a->virtualBox, performanceCollector); else if (!strcmp(a->argv[0], "enable")) rcExit = handleMetricsEnable(a->argc, a->argv, a->virtualBox, performanceCollector); else if (!strcmp(a->argv[0], "disable")) rcExit = handleMetricsDisable(a->argc, a->argv, a->virtualBox, performanceCollector); else return errorSyntax(Metrics::tr("Invalid subcommand '%s'"), a->argv[0]); return rcExit; }