diff --git a/DevHome.sln b/DevHome.sln index b842116a53..bb03d89b0a 100644 --- a/DevHome.sln +++ b/DevHome.sln @@ -80,6 +80,10 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DevHome.QuietBackgroundProc EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DevHome.QuietBackgroundProcesses.Common", "tools\QuietBackgroundProcesses\DevHome.QuietBackgroundProcesses.Common\DevHome.QuietBackgroundProcesses.Common.vcxproj", "{4B370E2F-FB1D-4887-90BF-3B72517485CE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevHome.QuietBackgroundProcesses.UI", "tools\QuietBackgroundProcesses\DevHome.QuietBackgroundProcesses.UI\DevHome.QuietBackgroundProcesses.UI.csproj", "{1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine", "tools\QuietBackgroundProcesses\DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine\DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj", "{AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{DCAF188B-60C3-4EDB-8049-BAA927FBCD7D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SampleTool", "SampleTool", "{E7C94F61-D6CF-464D-8D50-210488AF7A50}" @@ -516,6 +520,22 @@ Global {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|x64.Build.0 = Release|x64 {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|x86.ActiveCfg = Release|Win32 {4B370E2F-FB1D-4887-90BF-3B72517485CE}.Release|x86.Build.0 = Release|Win32 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|Any CPU.ActiveCfg = Debug|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|Any CPU.Build.0 = Debug|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|arm64.ActiveCfg = Debug|arm64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|arm64.Build.0 = Debug|arm64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|x64.ActiveCfg = Debug|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|x64.Build.0 = Debug|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|x86.ActiveCfg = Debug|x86 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Debug|x86.Build.0 = Debug|x86 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|Any CPU.ActiveCfg = Release|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|Any CPU.Build.0 = Release|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|arm64.ActiveCfg = Release|arm64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|arm64.Build.0 = Release|arm64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|x64.ActiveCfg = Release|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|x64.Build.0 = Release|x64 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|x86.ActiveCfg = Release|x86 + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE}.Release|x86.Build.0 = Release|x86 {CFD8A90D-8B6D-4ED6-BA35-FF894BEB46C0}.Debug|Any CPU.ActiveCfg = Debug|x64 {CFD8A90D-8B6D-4ED6-BA35-FF894BEB46C0}.Debug|Any CPU.Build.0 = Debug|x64 {CFD8A90D-8B6D-4ED6-BA35-FF894BEB46C0}.Debug|arm64.ActiveCfg = Debug|ARM64 @@ -740,6 +760,22 @@ Global {AF527EA4-6A24-4BD6-BC6E-A5863DC3489C}.Release|x64.Build.0 = Release|x64 {AF527EA4-6A24-4BD6-BC6E-A5863DC3489C}.Release|x86.ActiveCfg = Release|x86 {AF527EA4-6A24-4BD6-BC6E-A5863DC3489C}.Release|x86.Build.0 = Release|x86 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|Any CPU.ActiveCfg = Debug|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|Any CPU.Build.0 = Debug|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|arm64.ActiveCfg = Debug|ARM64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|arm64.Build.0 = Debug|ARM64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|x64.ActiveCfg = Debug|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|x64.Build.0 = Debug|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|x86.ActiveCfg = Debug|Win32 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Debug|x86.Build.0 = Debug|Win32 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|Any CPU.ActiveCfg = Release|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|Any CPU.Build.0 = Release|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|arm64.ActiveCfg = Release|ARM64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|arm64.Build.0 = Release|ARM64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|x64.ActiveCfg = Release|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|x64.Build.0 = Release|x64 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|x86.ActiveCfg = Release|Win32 + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -767,6 +803,8 @@ Global {092AC740-DA01-4872-8E93-B9557DAD6BE5} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} {80805B43-CE75-4C6E-92F8-F385C1039E53} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} {4B370E2F-FB1D-4887-90BF-3B72517485CE} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} + {1477F3EA-A9F6-4B4F-82F4-C2427F57EBEE} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} + {AF5A7FA0-E3E8-44C8-8830-31DD08F583E8} = {D04CD3A1-0B45-4CB3-925F-204F5F6AE2D8} {E7C94F61-D6CF-464D-8D50-210488AF7A50} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} {8FC9A04E-1FFD-42BA-B304-D1FA964D99CE} = {A972EC5B-FC61-4964-A6FF-F9633EB75DFD} {CFD8A90D-8B6D-4ED6-BA35-FF894BEB46C0} = {8FC9A04E-1FFD-42BA-B304-D1FA964D99CE} diff --git a/settings/DevHome.Settings/Strings/en-us/Resources.resw b/settings/DevHome.Settings/Strings/en-us/Resources.resw index d73581bd82..b078587296 100644 --- a/settings/DevHome.Settings/Strings/en-us/Resources.resw +++ b/settings/DevHome.Settings/Strings/en-us/Resources.resw @@ -559,12 +559,12 @@ Title text for the Environments configuration feature. - Quiet background processes experiment - Name of experimental feature ()'Quiet Background Processes') on the 'Settings -> Experiments' page where you enable it. + Quiet background processes + Name of experimental feature 'Quiet background processes' on the 'Settings -> Experiments' page where you enable it. - Silence and track background processes that may hinder device performance - Inline description of the Quiet Background Processes experimental feature on the 'Settings -> Experiments' page where you enable it. + Quiet background processes allows you to free up resources while developing + Inline description of the Quiet background processes experimental feature on the 'Settings -> Experiments' page where you enable it. Create a local or cloud machine from Dev Home diff --git a/src/DevHome.csproj b/src/DevHome.csproj index d749292754..039b6a841d 100644 --- a/src/DevHome.csproj +++ b/src/DevHome.csproj @@ -119,7 +119,7 @@ true - + <_DevHomeInternal_CppPlatform>$(Platform) <_DevHomeInternal_CppPlatform Condition="'$(Platform)' == 'x86'">Win32 @@ -138,6 +138,9 @@ PreserveNewest + + PreserveNewest + diff --git a/src/NavConfig.jsonc b/src/NavConfig.jsonc index 8aa0a372e2..9689941e21 100644 --- a/src/NavConfig.jsonc +++ b/src/NavConfig.jsonc @@ -86,18 +86,18 @@ "buildTypeOverrides": [ { "buildType": "dev", - "enabledByDefault": false, + "enabledByDefault": true, "visible": true }, { "buildType": "canary", - "enabledByDefault": false, - "visible": false + "enabledByDefault": true, + "visible": true }, { "buildType": "stable", "enabledByDefault": false, - "visible": false + "visible": true } ] }, diff --git a/src/Package.appxmanifest b/src/Package.appxmanifest index 3e44bb0bc3..0e9dc3da0a 100644 --- a/src/Package.appxmanifest +++ b/src/Package.appxmanifest @@ -22,6 +22,7 @@ DevHome.QuietBackgroundProcesses.ElevatedServer.exe singleInstance + diff --git a/tools/Customization/DevHome.Customization/DevHome.Customization.csproj b/tools/Customization/DevHome.Customization/DevHome.Customization.csproj index be0924d636..d06a565d6b 100644 --- a/tools/Customization/DevHome.Customization/DevHome.Customization.csproj +++ b/tools/Customization/DevHome.Customization/DevHome.Customization.csproj @@ -24,9 +24,9 @@ + - @@ -40,7 +40,6 @@ - @@ -60,9 +59,6 @@ MSBuild:Compile $(DefaultXamlRuntime) - - $(DefaultXamlRuntime) - diff --git a/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs b/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs index e3788796c5..1372f2b018 100644 --- a/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs +++ b/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs @@ -4,6 +4,7 @@ using DevHome.Customization.ViewModels; using DevHome.Customization.ViewModels.DevDriveInsights; using DevHome.Customization.Views; +using DevHome.QuietBackgroundProcesses.UI.ViewModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -19,7 +20,9 @@ public static IServiceCollection AddWindowsCustomization(this IServiceCollection services.AddSingleton(); services.AddTransient(); - services.AddSingleton(sp => (cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters) => ActivatorUtilities.CreateInstance(sp, cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters)); + services.AddSingleton(sp => + (cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters) => + ActivatorUtilities.CreateInstance(sp, cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters)); services.AddSingleton(); services.AddTransient(); diff --git a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw index 8c4f0d8428..ce4d1ddcbd 100644 --- a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw +++ b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw @@ -249,56 +249,4 @@ Windows developer settings The header for the Windows developer settings card - - - - Start session - Button that starts a quiet background session - - - Stop session - Button that stops a quiet background session - - - Silence and track background processes that may hinder device performance - Description of the Quiet Background Processes feature - - - Quiet background processes - Inline title of the Quiet Background Processes feature - - - This feature can be activated for 2 hours. - A description of the Quiet Background Processes time window - - - Link to docs - Link that launches documentation - - - Provide feedback - Link that launches feedback - - - Related links - Label for the doc links - - - - - Feature not supported on this version of Windows - Indicates that this OS isn't new enough to support the feature - - - Session Error - Something went wrong when running the session - - - Session ended - The quiet session was cancelled or the time expired - - - Unable to cancel session - Something went wrong when cancelling the session - \ No newline at end of file diff --git a/tools/Customization/DevHome.Customization/Views/MainPageView.xaml b/tools/Customization/DevHome.Customization/Views/MainPageView.xaml index cb805cb118..9e957a96f2 100644 --- a/tools/Customization/DevHome.Customization/Views/MainPageView.xaml +++ b/tools/Customization/DevHome.Customization/Views/MainPageView.xaml @@ -6,6 +6,7 @@ xmlns:converters="using:CommunityToolkit.WinUI.Converters" xmlns:ui="using:CommunityToolkit.WinUI" xmlns:views="using:DevHome.Customization.Views" + xmlns:quietviews="using:DevHome.QuietBackgroundProcesses.UI.Views" Loaded="UserControl_Loaded"> @@ -39,7 +40,7 @@ - + diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/Common.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/Common.h new file mode 100644 index 0000000000..e92ab65a15 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/Common.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +// Get temporary path for performance data +inline std::filesystem::path GetTemporaryPerformanceDataPath() +{ + auto tempDirectory = std::filesystem::temp_directory_path(); + return std::filesystem::path(tempDirectory) / L"DevHome.QuietMode.PerformanceData.dat"; +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.Common.vcxproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.Common.vcxproj index 54ea440e67..b790816874 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.Common.vcxproj +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.Common.vcxproj @@ -145,6 +145,7 @@ + diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.idl b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.idl index c64cb8d568..26e7ead7da 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.idl +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Common/DevHome.QuietBackgroundProcesses.idl @@ -2,15 +2,66 @@ // Licensed under the MIT License. import "inspectable.idl"; +import "windows.foundation.idl"; namespace DevHome.QuietBackgroundProcesses { + // Performance Engine types + enum ProcessCategory + { + Unknown, + User, + System, + Developer, + Background, + }; + + [default_interface] + runtimeclass ProcessRow + { + UInt32 Pid { get; }; + String Name { get; }; + String PackageFullName { get; }; + String Aumid { get; }; + String Path { get; }; + ProcessCategory Category { get; }; + Windows.Foundation.DateTime CreateTime { get; }; + Windows.Foundation.DateTime ExitTime { get; }; + + UInt64 SampleCount { get; }; + Double PercentCumulative { get; }; + Double VarianceCumulative { get; }; + Double Sigma4Cumulative { get; }; + Double MaxPercent { get; }; + UInt32 SamplesAboveThreshold { get; }; + + UInt64 TotalCpuTimeInMicroseconds { get; }; + } + + [default_interface] + runtimeclass ProcessPerformanceTable + { + ProcessRow[] Rows { get; }; + } + + [default_interface] + runtimeclass PerformanceRecorderEngine + { + static ProcessPerformanceTable TryGetLastPerformanceRecording(); + + PerformanceRecorderEngine(); + void Start(Windows.Foundation.TimeSpan periodInMs); + ProcessPerformanceTable Stop(); + } + + // Quiet background process types [default_interface] runtimeclass QuietBackgroundProcessesSession { static QuietBackgroundProcessesSession GetSingleton(); + Int64 Start(); - void Stop(); + ProcessPerformanceTable Stop(); Boolean IsActive { get; }; Int64 TimeLeftInSeconds { get; }; } @@ -21,5 +72,7 @@ namespace DevHome.QuietBackgroundProcesses static Boolean IsFeaturePresent(); static QuietBackgroundProcessesSession GetSession(); static QuietBackgroundProcessesSession TryGetSession(); + static Boolean HasLastPerformanceRecording(); + static ProcessPerformanceTable TryGetLastPerformanceRecording(); } } diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/DevHome.QuietBackgroundProcesses.ElevatedServer.vcxproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/DevHome.QuietBackgroundProcesses.ElevatedServer.vcxproj index 001ddcadaa..b2a6ac18a7 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/DevHome.QuietBackgroundProcesses.ElevatedServer.vcxproj +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/DevHome.QuietBackgroundProcesses.ElevatedServer.vcxproj @@ -112,8 +112,8 @@ - stdcpp17 - $(ProjectDir)..\DevHome.QuietBackgroundProcesses.Common\;$(ProjectDir)..\DevHome.QuietBackgroundProcesses.Common\$(GeneratedFilesDir)midl;%(AdditionalIncludeDirectories) + stdcpp20 + $(ProjectDir)..\DevHome.QuietBackgroundProcesses.Common\;$(ProjectDir)..\DevHome.QuietBackgroundProcesses.Common\$(GeneratedFilesDir)midl;$(ProjectDir)..\DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine\;%(AdditionalIncludeDirectories) onecore.lib;%(AdditionalDependencies) @@ -135,16 +135,19 @@ + + Create + @@ -158,6 +161,9 @@ {4b370e2f-fb1d-4887-90bf-3b72517485ce} + + {af5a7fa0-e3e8-44c8-8830-31dd08f583e8} + diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.cpp new file mode 100644 index 0000000000..1fe81e8cfb --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.cpp @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#include +#include +#include +#include + +#include "PerformanceRecorderEngine.h" +#include "Helpers.h" + +void WritePerformanceDataToDisk(_In_ PCWSTR path, const std::span& data) +{ + std::ofstream file(path, std::ios::binary); + if (!file.is_open()) + { + // Handle error + return; + } + + for (const auto& item : data) + { + file.write(reinterpret_cast(&item), sizeof(ProcessPerformanceSummary)); + } + + file.close(); +} + +std::vector ReadPerformanceDataFromDisk(_In_ PCWSTR path) +{ + std::vector data; + + std::ifstream file(path, std::ios::binary); + THROW_WIN32_IF(ERROR_SHARING_VIOLATION, !file.is_open()); + + ProcessPerformanceSummary item; + while (file.read(reinterpret_cast(&item), sizeof(ProcessPerformanceSummary))) + { + data.push_back(item); + } + + file.close(); + return data; +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.h new file mode 100644 index 0000000000..9045c9ae2f --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/Helpers.h @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include + +#include +#include + +#include "DevHome.QuietBackgroundProcesses.h" +#include "PerformanceRecorderEngine.h" + +struct com_ptr_deleter +{ + template + void operator()(_Pre_opt_valid_ _Frees_ptr_opt_ T p) const + { + if (p) + { + p.reset(); + } + } +}; + +template +using unique_comptr_array = wil::unique_any_array_ptr, ArrayDeleter, com_ptr_deleter>; + +template +unique_comptr_array make_unique_comptr_array(size_t numOfElements) +{ + auto list = unique_comptr_array(reinterpret_cast*>(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, numOfElements * sizeof(wil::com_ptr_nothrow))), numOfElements); + THROW_IF_NULL_ALLOC(list.get()); + return list; +} + +// Create a performance recorder engine +wil::com_ptr MakePerformanceRecorderEngine(); + +// Read/write the performance data to/from disk +void WritePerformanceDataToDisk(_In_ PCWSTR path, const std::span& data); +std::vector ReadPerformanceDataFromDisk(_In_ PCWSTR path); diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/PerformanceRecorderEngineWinrt.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/PerformanceRecorderEngineWinrt.cpp new file mode 100644 index 0000000000..df66eff75a --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/PerformanceRecorderEngineWinrt.cpp @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "Common.h" +#include "TimedQuietSession.h" +#include "DevHome.QuietBackgroundProcesses.h" +#include "PerformanceRecorderEngine.h" +#include "Helpers.h" + + +namespace ABI::DevHome::QuietBackgroundProcesses +{ + class ProcessRow : + public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, + IProcessRow, + Microsoft::WRL::FtmBase> + { + InspectableClass(RuntimeClass_DevHome_QuietBackgroundProcesses_ProcessRow, BaseTrust); + + public: + STDMETHODIMP RuntimeClassInitialize(ProcessPerformanceSummary summary) noexcept + { + m_summary = summary; + return S_OK; + } + + STDMETHODIMP get_Pid(unsigned int* value) noexcept override + try + { + *value = m_summary.pid; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_Name(HSTRING* value) noexcept override + try + { + Microsoft::WRL::Wrappers::HString str; + str.Set(m_summary.name); + *value = str.Detach(); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_PackageFullName(HSTRING* value) noexcept override + try + { + Microsoft::WRL::Wrappers::HString str; + str.Set(m_summary.packageFullName); + *value = str.Detach(); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_Aumid(HSTRING* value) noexcept override + try + { + Microsoft::WRL::Wrappers::HString str; + str.Set(m_summary.aumid); + *value = str.Detach(); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_Path(HSTRING* value) noexcept override + try + { + Microsoft::WRL::Wrappers::HString str; + str.Set(m_summary.path); + *value = str.Detach(); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_Category(ABI::DevHome::QuietBackgroundProcesses::ProcessCategory* value) noexcept override + try + { + *value = static_cast(m_summary.category); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_CreateTime(struct ABI::Windows::Foundation::DateTime* value) noexcept override + try + { + INT64 time = m_summary.createTime.dwLowDateTime + ((UINT64)m_summary.createTime.dwHighDateTime << 32); + *value = ABI::Windows::Foundation::DateTime{ time }; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_ExitTime(struct ABI::Windows::Foundation::DateTime* value) noexcept override + try + { + INT64 time = m_summary.exitTime.dwLowDateTime + ((UINT64)m_summary.exitTime.dwHighDateTime << 32); + *value = ABI::Windows::Foundation::DateTime{ time }; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_SampleCount(unsigned __int64* value) noexcept override + try + { + *value = m_summary.sampleCount; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_PercentCumulative(double* value) noexcept override + try + { + *value = m_summary.percentCumulative; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_VarianceCumulative(double* value) noexcept override + try + { + *value = m_summary.varianceCumulative; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_Sigma4Cumulative(double* value) noexcept override + try + { + *value = m_summary.sigma4Cumulative; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_MaxPercent(double* value) noexcept override + try + { + *value = m_summary.maxPercent; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_SamplesAboveThreshold(unsigned __int32* value) noexcept override + try + { + *value = m_summary.samplesAboveThreshold; + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP get_TotalCpuTimeInMicroseconds(unsigned __int64* value) noexcept override + try + { + *value = m_summary.totalCpuTimeInMicroseconds; + return S_OK; + } + CATCH_RETURN() + + private: + ProcessPerformanceSummary m_summary; + }; +} + +namespace ABI::DevHome::QuietBackgroundProcesses +{ + class ProcessPerformanceTable : + public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, + IProcessPerformanceTable, + Microsoft::WRL::FtmBase> + { + InspectableClass(RuntimeClass_DevHome_QuietBackgroundProcesses_ProcessPerformanceTable, BaseTrust); + + public: + STDMETHODIMP RuntimeClassInitialize(unique_process_utilization_monitoring_thread context) noexcept + { + m_context = std::move(context); + return S_OK; + } + + STDMETHODIMP get_Rows(unsigned int* valueLength, ABI::DevHome::QuietBackgroundProcesses::IProcessRow*** value) noexcept override + try + { + std::span span; + wil::unique_cotaskmem_array_ptr summariesCoarray; + std::vector summariesVector; + + if (m_context) + { + // We have a live context, read performance data from it + THROW_IF_FAILED(GetMonitoringProcessUtilization(m_context.get(), summariesCoarray.addressof(), summariesCoarray.size_address())); + + // Make span from cotaskmem_array + span = std::span{ summariesCoarray.get(), summariesCoarray.size() }; + } + else + { + // We don't have a live context. Let's try to read performance data from disk. + auto performanceDataFile = GetTemporaryPerformanceDataPath(); + THROW_HR_IF(E_FAIL, !std::filesystem::exists(performanceDataFile)); + + // Make span from vector + summariesVector = ReadPerformanceDataFromDisk(performanceDataFile.c_str()); + span = std::span{ summariesVector }; + } + + // Create IProcessRow entries + auto list = make_unique_comptr_array(span.size()); + for (uint32_t i = 0; i < span.size(); i++) + { + auto& summary = span[i]; + wil::com_ptr row; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&row, summary)); + list[i] = std::move(row); + } + *valueLength = static_cast(span.size()); + *value = (ABI::DevHome::QuietBackgroundProcesses::IProcessRow**)list.release(); + return S_OK; + } + CATCH_RETURN() + + private: + unique_process_utilization_monitoring_thread m_context; + }; +} + +namespace ABI::DevHome::QuietBackgroundProcesses +{ + class PerformanceRecorderEngine : + public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, + IPerformanceRecorderEngine, + Microsoft::WRL::FtmBase> + { + InspectableClass(RuntimeClass_DevHome_QuietBackgroundProcesses_PerformanceRecorderEngine, BaseTrust); + + public: + STDMETHODIMP RuntimeClassInitialize() noexcept + { + return S_OK; + } + + // IPerformanceRecorderEngine + STDMETHODIMP Start(ABI::Windows::Foundation::TimeSpan samplingPeriod) noexcept override + try + { + // Convert TimeSpan from 100ns to milliseconds + auto periodInMs = static_cast(samplingPeriod.Duration / 10000); + THROW_IF_FAILED(StartMonitoringProcessUtilization(periodInMs, &m_context)); + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP Stop(ABI::DevHome::QuietBackgroundProcesses::IProcessPerformanceTable** result) noexcept override + try + { + THROW_IF_FAILED(StopMonitoringProcessUtilization(m_context.get())); + + if (result) + { + wil::com_ptr performanceTable; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&performanceTable, std::move(m_context))); + *result = performanceTable.detach(); + } + else + { + // No one (no client) is currently asking for the performance data (presumably Dev Home is closed) so write it to disk + wil::unique_cotaskmem_array_ptr summaries; + THROW_IF_FAILED(GetMonitoringProcessUtilization(m_context.get(), summaries.addressof(), summaries.size_address())); + + // Write the performance .csv data to disk + std::span data(summaries.get(), summaries.size()); + try + { + auto performanceDataFile = GetTemporaryPerformanceDataPath(); + WritePerformanceDataToDisk(performanceDataFile.c_str(), data); + } + CATCH_LOG(); + + // Destroy the performance engine instance + m_context.reset(); + } + + return S_OK; + } + CATCH_RETURN() + + private: + unique_process_utilization_monitoring_thread m_context; + }; + + class PerformanceRecorderEngineStatics WrlFinal : + public Microsoft::WRL::AgileActivationFactory< + Microsoft::WRL::Implements> + { + InspectableClassStatic(RuntimeClass_DevHome_QuietBackgroundProcesses_PerformanceRecorderEngine, BaseTrust); + + public: + STDMETHODIMP ActivateInstance(_COM_Outptr_ IInspectable**) noexcept + { + // Disallow activation - must use GetSingleton() + return E_NOTIMPL; + } + + // IPerformanceRecorderEngineStatics + STDMETHODIMP TryGetLastPerformanceRecording(_COM_Outptr_ ABI::DevHome::QuietBackgroundProcesses::IProcessPerformanceTable** result) noexcept override + try + { + // Reconstruct a perform table from disk (passing nullptr for context) + wil::com_ptr performanceTable; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&performanceTable, nullptr)); + *result = performanceTable.detach(); + + return S_OK; + } + CATCH_RETURN() + }; + + ActivatableClassWithFactory(PerformanceRecorderEngine, PerformanceRecorderEngineStatics); +} + +wil::com_ptr MakePerformanceRecorderEngine() +{ + using namespace ABI::DevHome::QuietBackgroundProcesses; + wil::com_ptr result; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&result)); + return result; +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietBackgroundProcessesSession.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietBackgroundProcessesSession.cpp index fbd08ff74a..f0483e7b73 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietBackgroundProcessesSession.cpp +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/QuietBackgroundProcessesSession.cpp @@ -49,7 +49,7 @@ namespace ABI::DevHome::QuietBackgroundProcesses // Stop and discard the previous timer if (g_activeTimer) { - g_activeTimer->Cancel(); + g_activeTimer->Cancel(nullptr); } std::chrono::seconds duration = DEFAULT_QUIET_DURATION; @@ -67,14 +67,16 @@ namespace ABI::DevHome::QuietBackgroundProcesses } CATCH_RETURN() - STDMETHODIMP Stop() noexcept override try + STDMETHODIMP Stop(ABI::DevHome::QuietBackgroundProcesses::IProcessPerformanceTable** result) noexcept override + try { auto lock = std::scoped_lock(g_mutex); + *result = nullptr; // Turn off quiet mode and cancel timer if (g_activeTimer) { - g_activeTimer->Cancel(); + g_activeTimer->Cancel(result); g_activeTimer.reset(); } diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/TimedQuietSession.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/TimedQuietSession.h index 3dcc157e69..7e809cf045 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/TimedQuietSession.h +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer/TimedQuietSession.h @@ -20,6 +20,7 @@ #include "Timer.h" #include "QuietState.h" +#include "Helpers.h" using ElevatedServerReference = wrl_server_process_ref; @@ -39,7 +40,7 @@ struct UnelevatedServerReference }; -// TimedQuietSession is a 2 hour "Quiet Background Processes" timed window that disables quiet +// TimedQuietSession is a 2 hour "Quiet background processes" timed window that disables quiet // mode when the timer expires or when explicitly cancelled. It keeps also keeps the server alive. // // TimedQuietSession maintains, @@ -88,6 +89,12 @@ struct TimedQuietSession // Turn on quiet mode m_quietState = QuietState::TurnOn(); + + // Start performance recorder + ABI::Windows::Foundation::TimeSpan samplingPeriod; + samplingPeriod.Duration = 1000 * 10000; // 1 second + m_performanceRecorderEngine = MakePerformanceRecorderEngine(); + THROW_IF_FAILED(m_performanceRecorderEngine->Start(samplingPeriod)); } TimedQuietSession(TimedQuietSession&& other) noexcept = default; @@ -108,11 +115,11 @@ struct TimedQuietSession return (bool)m_quietState; } - void Cancel() + void Cancel(ABI::DevHome::QuietBackgroundProcesses::IProcessPerformanceTable** result) { auto lock = std::scoped_lock(m_mutex); - Deactivate(); + Deactivate(result); m_timer->Cancel(); // Destruct timer on another thread because it's destructor is blocking @@ -124,11 +131,20 @@ struct TimedQuietSession } private: - void Deactivate() + void Deactivate(ABI::DevHome::QuietBackgroundProcesses::IProcessPerformanceTable** result = nullptr) { // Turn off quiet mode m_quietState.reset(); + // Stop the performance recorder + if (m_performanceRecorderEngine) + { + LOG_IF_FAILED(m_performanceRecorderEngine->Stop(result)); + } + + // Disable performance recorder + m_performanceRecorderEngine.reset(); + // Release lifetime handles to this elevated server and unelevated client server m_unelevatedServer.reset(); m_elevatedServer.reset(); @@ -139,5 +155,6 @@ struct TimedQuietSession QuietState::unique_quietwindowclose_call m_quietState{ false }; std::unique_ptr m_timer; + wil::com_ptr m_performanceRecorderEngine; std::mutex m_mutex; }; diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj new file mode 100644 index 0000000000..6f47427600 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj @@ -0,0 +1,188 @@ + + + + C++ + + + + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + {af5a7fa0-e3e8-44c8-8830-31dd08f583e8} + DynamicLibrary + DevHome_QuietBackgroundProcesses_PerformanceRecorderEngine + en-US + 14.0 + false + Windows Store + 10.0.22621.0 + 10.0.17134.0 + 10.0 + $(CppOutDir) + + + + DynamicLibrary + true + v143 + + + DynamicLibrary + true + v143 + + + DynamicLibrary + true + v143 + + + DynamicLibrary + false + true + v143 + + + DynamicLibrary + false + true + v143 + + + DynamicLibrary + false + true + v143 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + false + + + false + false + + + false + false + + + false + false + + + false + false + + + false + false + + + + Use + false + stdcpp20 + + + Console + false + false + onecore.lib;onecoreuap_apiset.lib;%(AdditionalDependencies) + + + + + _DEBUG;%(PreprocessorDefinitions) + Disabled + + + + + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + + + + + Create + Create + Create + Create + Create + Create + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj.filters b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj.filters new file mode 100644 index 0000000000..e1d45a4ab6 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine.vcxproj.filters @@ -0,0 +1,29 @@ + + + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms + + + {a4747e26-49cd-412b-b5ad-2d499887d0d5} + + + + + + + + + + + inc + + + + + + + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.cpp new file mode 100644 index 0000000000..b182a1ff6f --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.cpp @@ -0,0 +1,674 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "PerformanceRecorderEngine.h" + +// 2 percent is the threshold for a process to be considered as a high CPU consumer +#define CPU_TIME_ABOVE_THRESHOLD_STRIKE_VALUE 0.02f + +enum class ProcessCategory +{ + Unknown, + User, + System, + Developer, + Background, +}; + +struct ProcessPerformanceInfo +{ + // Process id + wil::unique_process_handle process; + ULONG pid{}; + + // Process info + std::wstring name; + std::wstring path; + std::optional packageFullName; + std::optional aumid; + ProcessCategory category{}; + FILETIME createTime{}; + FILETIME exitTime{}; + + // CPU times + FILETIME startUserTime{}; + FILETIME startKernelTime{}; + FILETIME previousUserTime{}; + FILETIME currentUserTime{}; + FILETIME previousKernelTime{}; + FILETIME currentKernelTime{}; + + // Sampling + uint64_t sampleCount{}; + double percentCumulative{}; + double varianceCumulative{}; + double sigma4Cumulative{}; + double maxPercent{}; + uint32_t samplesAboveThreshold{}; +}; + +// Process Categories +std::set c_user = { + L"chrome.exe", + L"OUTLOOK.exe", + L"EXCEL.exe", + L"explorer.exe", + L"WINWORD.exe", + L"POWERPOINT.exe", + L"OfficeClickToRun.exe", + L"Microsoft.SharePoint.exe", + L"msedge.exe", + L"msedgewebview2.exe", + L"ShellExperienceHost.exe", + L"StartMenuExperienceHost.exe", + L"smartscreen.exe", + L"sihost.exe", + L"SystemSettings.exe", + L"electron.exe", + L"CrmSandbox.exe", + L"ms-teams.exe", + L"TextInputHost.exe", + L"UserOOBEBroker.exe", + L"WebViewHost.exe", + L"Widgets.exe", + L"WidgetService.exe", + L"XboxGameBarWidgets.exe", + L"teams.exe" +}; +std::set c_system = { + L"System", + L"Registry", + L"Secure System", + L"audiodg.exe", + L"ctfmon.exe", + L"LogonUI.exe", + L"MpDefenderCoreService.exe", + L"MpDlpService.exe", + L"ShellHost.exe", + L"smss.exe", + L"spoolsv.exe", + L"wininit.exe", + L"lsass.exe" +}; +std::set c_developer = { + L"cmd.exe", + L"conhost.exe", + L"console.exe", + L"OpenConsole.exe", + L"powershell.exe", + L"cl.exe", + L"link.exe", + L"devenv.exe", + L"DevHome.exe", + L"DevHomeGitHubExtension.exe", + L"python.exe", + L"build.exe", + L"msbuild.exe", + L"windbg.exe", + L"windbgx.exe", + L"EngHost.exe", + L"DbgX.Shell.exe", + L"GVFS.Mount.exe", + L"GVFS.Service.exe", + L"GVFS.ServiceUI.exe", + L"vscode.exe", + L"code.exe", + L"cpptools.exe", + L"notepad.exe", + L"notepad++.exe", + L"Wex.Services.exe", + L"Taskmgr.exe", + L"wpa.exe", + L"wpr.exe", + L"CalculatorApp.exe", + L"npm.exe", + L"winget.exe", + L"chocolatey.exe", + L"pip.exe", + L"vshost.exe", + L"VSSVC.exe", + L"VBCSCompiler.exe", + L"vcpkgsrv.exe", + L"WindowsTerminal.exe", + L"WindowsPackageManagerServer.exe", + L"reSearch.exe" +}; +std::set c_vms = { + L"vmmem", + L"vmwp.exe", // actual process name for 'vmmem' + L"vmcompute.exe", + L"vmconnect.exe", + L"vmwp.exe", + L"vmms.exe" +}; +std::set c_background = { + L"services.exe", + L"svchost.exe", + L"SCNotification.exe", + L"SecurityHealthyService.exe", + L"DevHome.QuietBackgroundProcesses.Server.exe", + L"DevHome.QuietBackgroundProcesses.ElevatedServer.exe", + L"OneDrive.exe", + L"MsMpEng.exe", + L"MsSense.exe", + L"NdrSetup.exe", + L"NisSrv.exe", + L"RuntimeBroker.exe", + L"rundll32.exe", + L"SearchHost.exe", + L"SenseCE.exe", + L"SenseNdr.exe", + L"SenseNdrX.exe", + L"SenseTVM.exe", + L"SearchIndexer.exe", + L"taskhostw.exe", + L"winlogon.exe", +}; + +ProcessCategory GetCategory(DWORD pid, std::wstring_view processName) +{ + auto search = [&](std::wstring_view processName, const auto& list) + { + auto it = std::find_if(list.begin(), list.end(), [&](const auto& elem) + { + return wil::compare_string_ordinal(processName, elem, true) == 0; + }); + auto found = (it != list.end()); + return found; + }; + + if (pid == 4) + { + // PID 4 is the System process + return ProcessCategory::System; + } + if (search(processName.data(), c_user)) + { + return ProcessCategory::User; + } + if (search(processName.data(), c_system)) + { + return ProcessCategory::System; + } + if (search(processName.data(), c_developer)) + { + return ProcessCategory::Developer; + } + if (search(processName.data(), c_vms)) + { + return ProcessCategory::Developer; + } + if (search(processName.data(), c_background)) + { + return ProcessCategory::Background; + } + return ProcessCategory::Unknown; +} + +template +void copystr(wchar_t(&dst)[N], const std::optional& src) +{ + wcscpy_s(dst, N, src.value_or(L"").substr(0, N - 1).c_str()); +} + +template +wil::unique_cotaskmem_array_ptr make_unique_cotaskmem_array_ptr(size_t numOfElements) +{ + wil::unique_cotaskmem_array_ptr result; + T* ptr = reinterpret_cast(CoTaskMemAlloc(sizeof(T) * numOfElements)); + THROW_IF_NULL_ALLOC(ptr); + *result.addressof() = ptr; + *result.size_address() = numOfElements; + return result; +} + +std::chrono::file_clock::time_point FileTimeToTimePoint(const FILETIME& fileTime) +{ + ULARGE_INTEGER uli; + uli.LowPart = fileTime.dwLowDateTime; + uli.HighPart = fileTime.dwHighDateTime; + std::chrono::file_clock::duration d{ (static_cast(fileTime.dwHighDateTime) << 32) | fileTime.dwLowDateTime }; + std::chrono::file_clock::time_point tp{ d }; + return tp; +} + +std::string FiletimeToString(const FILETIME& ft) +{ + std::chrono::file_clock::duration d{ (static_cast(ft.dwHighDateTime) << 32) | ft.dwLowDateTime }; + std::chrono::file_clock::time_point tp{ d }; + return std::format("{:%Y-%m-%d %H:%M}\n", tp); +} + +std::chrono::microseconds CpuTimeDuration(FILETIME previous, FILETIME current) +{ + if (CompareFileTime(&previous, ¤t) >= 0) + { + return std::chrono::microseconds(0); + } + + auto filetimeDeltaIn100ns = FileTimeToTimePoint(current) - FileTimeToTimePoint(previous); + auto durationMicroseconds = std::chrono::duration_cast(filetimeDeltaIn100ns); + return durationMicroseconds; +} + +int GetVirtualNumCpus() +{ + SYSTEM_INFO sysInfo; + GetSystemInfo(&sysInfo); + return sysInfo.dwNumberOfProcessors; +} + +template +std::span GetPids(DWORD (&pidArray)[size]) +{ + DWORD needed; + THROW_IF_WIN32_BOOL_FALSE(EnumProcesses(pidArray, sizeof(pidArray), &needed)); + return { &pidArray[0], needed / sizeof(DWORD) }; +} + +std::optional TryGetPackageFullNameFromTokenHelper(HANDLE token) +{ + wchar_t packageFullName[PACKAGE_FULL_NAME_MAX_LENGTH + 1]{}; + uint32_t packageFullNameLength = ARRAYSIZE(packageFullName); + if (GetPackageFullNameFromToken(token, &packageFullNameLength, packageFullName)) + { + return std::nullopt; + } + return std::wstring { packageFullName }; +} + +std::optional TryGetAppUserModelIdFromTokenHelper(HANDLE token) +{ + wchar_t aumid[APPLICATION_USER_MODEL_ID_MAX_LENGTH]{}; + uint32_t aumidLength = ARRAYSIZE(aumid); + if (GetApplicationUserModelIdFromToken(token, &aumidLength, aumid) != ERROR_SUCCESS) + { + return std::nullopt; + } + return std::wstring { aumid }; +} + +std::optional TryGetProcessName(HANDLE processHandle) +{ + static wchar_t s_buffer[MAX_PATH * 2]; + if (GetModuleFileNameExW(processHandle, nullptr, s_buffer, _countof(s_buffer)) > 0) + { + return s_buffer; + } + return std::nullopt; +} + +ProcessPerformanceInfo MakeProcessPerformanceInfo(DWORD processId) +{ + auto process = wil::unique_process_handle{ OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId) }; + if (!process) + { + // We can't open csrss.exe, so we'll just skip processes we can't open + auto info = ProcessPerformanceInfo{}; + info.process = nullptr; + info.pid = processId; + } + + auto processPathString = TryGetProcessName(process.get()); + + auto path = std::filesystem::path(processPathString.value_or(L"")); + + FILETIME createTime, exitTime, kernelTime, userTime; + THROW_IF_WIN32_BOOL_FALSE(GetProcessTimes(process.get(), &createTime, &exitTime, &kernelTime, &userTime)); + + std::optional packageFullName; + std::optional aumid; + wil::unique_handle processToken; + if (OpenProcessToken(process.get(), TOKEN_QUERY, &processToken)) + { + packageFullName = TryGetPackageFullNameFromTokenHelper(processToken.get()); + aumid = TryGetAppUserModelIdFromTokenHelper(processToken.get()); + } + + auto info = ProcessPerformanceInfo{}; + info.process = std::move(process); + info.pid = processId; + info.name = path.filename().wstring(); + info.path = path.parent_path().wstring(); + info.packageFullName = packageFullName; + info.aumid = aumid; + info.category = GetCategory(info.pid, info.name); + info.createTime = createTime; + + // Start times + info.startUserTime = userTime; + info.startKernelTime = kernelTime; + + info.previousUserTime = userTime; + info.currentUserTime = userTime; + info.previousKernelTime = kernelTime; + info.currentKernelTime = kernelTime; + + return info; +} + +bool UpdateProcessPerformanceInfo(ProcessPerformanceInfo& info) +{ + FILETIME createTime, exitTime, kernelTime, userTime; + THROW_IF_WIN32_BOOL_FALSE(GetProcessTimes(info.process.get(), &createTime, &exitTime, &kernelTime, &userTime)); + + if (exitTime.dwHighDateTime != 0 || exitTime.dwLowDateTime != 0) + { + info.exitTime = info.exitTime; + return false; + } + + info.previousUserTime = info.currentUserTime; + info.currentUserTime = userTime; + info.previousKernelTime = info.currentKernelTime; + info.currentKernelTime = kernelTime; + return true; +} + +struct cancellation_mechanism +{ + std::atomic m_cancelled{}; + std::mutex m_mutex; + std::condition_variable m_cancelCondition; + + void cancel() + { + auto lock = std::scoped_lock(m_mutex); + m_cancelled = true; + m_cancelCondition.notify_all(); + } + + bool wait_for_cancel(std::chrono::milliseconds duration) + { + auto lock = std::unique_lock(m_mutex); + auto cancelHappened = m_cancelCondition.wait_for(lock, duration, [this] { + return m_cancelled.load(); + }); + return cancelHappened; + } +}; + +struct MonitorThread +{ + cancellation_mechanism m_cancellationMechanism; + std::thread m_thread; + std::mutex m_dataMutex; + + // Tracking all our process infos + std::map m_runningProcesses; + std::vector m_terminatedProcesses; + + MonitorThread(std::chrono::milliseconds periodMs) + { + if (periodMs.count() <= 0) + { + THROW_HR(E_INVALIDARG); + } + + m_thread = std::thread([this, periodMs]() { + try + { + auto numCpus = GetVirtualNumCpus(); + + while (true) + { + if (m_cancellationMechanism.m_cancelled) + { + break; + } + + std::chrono::microseconds totalMicroseconds{}; + + // Check for new processes to track + DWORD pidArray[2048]; + auto pids = GetPids(pidArray); + + auto lock = std::scoped_lock(m_dataMutex); + for (auto& pid : pids) + { + // Ignore process "0" - the 'SYSTEM 'System' process + if (pid == 0) + { + continue; + } + + // Make a new entry + if (!m_runningProcesses.contains(pid)) + { + try + { + m_runningProcesses[pid] = MakeProcessPerformanceInfo(pid); + } + CATCH_LOG(); + } + } + + // Update counts for each tracked process + for (auto it = m_runningProcesses.begin(); it != m_runningProcesses.end(); ) + { + auto pid = it->first; + + // Get entry + auto& info = it->second; + + if (!info.process) + { + // The process couldn't be opened, so we'll skip this entry + ++it; + continue; + } + + // Update entry + try + { + if (!UpdateProcessPerformanceInfo(info)) + { + // The process terminated + + // Destroy the process handle + info.process.reset(); + + // Move from the map to the terminated list + m_terminatedProcesses.push_back(std::move(info)); + it = m_runningProcesses.erase(it); + continue; + } + } + catch (...) + { + ++it; + continue; + } + + // Collect cpuTime for process + auto cpuTime = CpuTimeDuration(info.previousUserTime, info.currentUserTime); + cpuTime += CpuTimeDuration(info.previousKernelTime, info.currentKernelTime); + + double percent = (double)cpuTime.count() / std::chrono::duration_cast(periodMs).count() / (double)numCpus * 100.0f; + double variance = (double)std::pow(percent, 2.0f); + double sigma4 = (double)std::pow(percent, 4.0f); + + info.sampleCount++; + info.percentCumulative += percent; + info.varianceCumulative += variance; + info.sigma4Cumulative += sigma4; + if (percent > info.maxPercent) + { + info.maxPercent = percent; + } + if (percent > CPU_TIME_ABOVE_THRESHOLD_STRIKE_VALUE) + { + info.samplesAboveThreshold++; + } + + totalMicroseconds += cpuTime; + + ++it; + } + + // Wait for interval period or user cancellation + if (m_cancellationMechanism.wait_for_cancel(periodMs)) + { + // User cancelled + break; + } + } + } + CATCH_LOG(); + }); + } + + void Cancel() + { + m_cancellationMechanism.cancel(); + if (m_thread.joinable()) + { + m_thread.join(); + } + } + + std::vector GetProcessPerformanceSummaries() + { + auto lock = std::scoped_lock(m_dataMutex); + + std::vector summaries; + auto MakeSummary = [](const ProcessPerformanceInfo& info) + { + auto summary = ProcessPerformanceSummary{}; + auto totalUserTime = CpuTimeDuration(info.startUserTime, info.currentUserTime); + auto totalKernelTime = CpuTimeDuration(info.startKernelTime, info.currentKernelTime); + + // Process info + summary.pid = info.pid; + if (summary.pid == 4) + { + copystr(summary.name, L"[System]"); + } + else if (info.name.empty()) + { + copystr(summary.name, L"[unk]"); + } + else + { + copystr(summary.name, info.name); + } + copystr(summary.packageFullName, info.packageFullName); + copystr(summary.aumid, info.aumid); + copystr(summary.path, info.path); + summary.category = static_cast(info.category); + summary.createTime = info.createTime; + summary.exitTime = info.exitTime; + + // Sampling + summary.sampleCount = info.sampleCount; + summary.percentCumulative = info.percentCumulative; + summary.varianceCumulative = info.varianceCumulative; + summary.sigma4Cumulative = info.sigma4Cumulative; + summary.maxPercent = info.maxPercent; + summary.samplesAboveThreshold = info.samplesAboveThreshold; + + // Other + summary.totalCpuTimeInMicroseconds = totalUserTime.count() + totalKernelTime.count(); + + if (summary.sampleCount <= 0) + { + summary.sampleCount = 0; + } + return summary; + }; + + // Add summaries for running processes + for (auto const& [key, info] : m_runningProcesses) + { + summaries.push_back(MakeSummary(info)); + } + + // Add summaries for terminated processes + for (auto const& info : m_terminatedProcesses) + { + summaries.push_back(MakeSummary(info)); + } + return summaries; + } +}; + +// +// Exports +// + +extern "C" __declspec(dllexport) HRESULT StartMonitoringProcessUtilization(uint32_t periodInMs, void** context) noexcept +try +{ + auto periodMs = std::chrono::milliseconds(periodInMs); + auto monitorThread = std::make_unique(periodMs); + *context = static_cast(monitorThread.release()); + return S_OK; +} +CATCH_RETURN() + +extern "C" __declspec(dllexport) HRESULT StopMonitoringProcessUtilization(void* context) noexcept +try +{ + auto monitorThread = reinterpret_cast(context); + monitorThread->Cancel(); + return S_OK; +} +CATCH_RETURN() + +extern "C" __declspec(dllexport) HRESULT DeleteMonitoringProcessUtilization(void* context) noexcept +try +{ + if (!context) + { + return S_OK; + } + auto monitorThread = std::unique_ptr(reinterpret_cast(context)); + monitorThread->Cancel(); + monitorThread.reset(); + return S_OK; +} +CATCH_RETURN() + +extern "C" __declspec(dllexport) HRESULT GetMonitoringProcessUtilization(void* context, ProcessPerformanceSummary** ppSummaries, size_t* summaryCount) noexcept +try +{ + auto monitorThread = reinterpret_cast(context); + auto summaries = monitorThread->GetProcessPerformanceSummaries(); + + // Alloc summaries block + auto ptrSummaries = make_unique_cotaskmem_array_ptr(summaries.size()); + auto i = 0; + for (auto const& summary : summaries) + { + auto& dst = ptrSummaries.get()[i++]; + dst = summary; + } + + *summaryCount = ptrSummaries.size(); + *ppSummaries = ptrSummaries.release(); + + return S_OK; +} +CATCH_RETURN() diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.h new file mode 100644 index 0000000000..82a73c612b --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/PerformanceRecorderEngine.h @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include + +struct ProcessPerformanceSummary +{ + // Process info + ULONG pid{}; + wchar_t name[64]; + wchar_t packageFullName[PACKAGE_FULL_NAME_MAX_LENGTH + 1]{}; + wchar_t aumid[APPLICATION_USER_MODEL_ID_MAX_LENGTH]{}; + wchar_t path[MAX_PATH * 2]{}; + uint32_t category{}; + FILETIME createTime{}; + FILETIME exitTime{}; + + // Sampling + uint64_t sampleCount{}; + double percentCumulative{}; + double varianceCumulative{}; + double sigma4Cumulative{}; + double maxPercent{}; + uint32_t samplesAboveThreshold{}; + + // Other + uint64_t totalCpuTimeInMicroseconds{}; +}; + +extern "C" __declspec(dllexport) HRESULT StartMonitoringProcessUtilization(uint32_t periodInMs, void** context) noexcept; +extern "C" __declspec(dllexport) HRESULT StopMonitoringProcessUtilization(void* context) noexcept; +extern "C" __declspec(dllexport) HRESULT GetMonitoringProcessUtilization(void* context, ProcessPerformanceSummary** ppSummaries, size_t* summaryCount) noexcept; +extern "C" __declspec(dllexport) HRESULT DeleteMonitoringProcessUtilization(void* context) noexcept; + +using unique_process_utilization_monitoring_thread = wil::unique_any; diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/dllmain.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/dllmain.cpp new file mode 100644 index 0000000000..b503a9ba44 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/dllmain.cpp @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" + +BOOL APIENTRY DllMain(HMODULE /* hModule */, DWORD ul_reason_for_call, LPVOID /* lpReserved */) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/packages.config b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/packages.config new file mode 100644 index 0000000000..09be25d9e4 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.cpp new file mode 100644 index 0000000000..40e691ba78 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.h b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.h new file mode 100644 index 0000000000..f4b4e69378 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.PerformanceRecorderEngine/pch.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#include diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/QuietBackgroundProcessesSessionManager.cpp b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/QuietBackgroundProcessesSessionManager.cpp index 8f5a76b7cd..3d74ecd239 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/QuietBackgroundProcessesSessionManager.cpp +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.Server/QuietBackgroundProcessesSessionManager.cpp @@ -3,6 +3,7 @@ #include +#include #include #include @@ -11,6 +12,8 @@ #include #include +#include + #include #include "DevHome.QuietBackgroundProcesses.h" @@ -80,6 +83,24 @@ namespace ABI::DevHome::QuietBackgroundProcesses } CATCH_RETURN() + STDMETHODIMP TryGetLastPerformanceRecording(_COM_Outptr_ ABI::DevHome::QuietBackgroundProcesses::IProcessPerformanceTable** result) noexcept override + try + { + auto factory = wil::GetActivationFactory(RuntimeClass_DevHome_QuietBackgroundProcesses_PerformanceRecorderEngine); + THROW_IF_FAILED(factory->TryGetLastPerformanceRecording(result)); + + return S_OK; + } + CATCH_RETURN() + + STDMETHODIMP HasLastPerformanceRecording(boolean* result) noexcept override + try + { + *result = std::filesystem::exists(GetTemporaryPerformanceDataPath()); + return S_OK; + } + CATCH_RETURN() + private: std::mutex m_mutex; wil::com_ptr m_sessionReference; diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/DevHome.QuietBackgroundProcesses.UI.csproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/DevHome.QuietBackgroundProcesses.UI.csproj new file mode 100644 index 0000000000..9d948baf63 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/DevHome.QuietBackgroundProcesses.UI.csproj @@ -0,0 +1,38 @@ + + + + + DevHome.QuietBackgroundProcesses.UI + x86;x64;arm64 + win-x86;win-x64;win-arm64 + true + enable + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + + + + $(DefaultXamlRuntime) + + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ProcessData.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ProcessData.cs new file mode 100644 index 0000000000..697a2dfeec --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ProcessData.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace DevHome.QuietBackgroundProcesses.UI; + +public class ProcessData +{ + public enum ProcessCategory + { + Unknown, + User, + System, + Developer, + Background, + } + + public ProcessData() + { + Name = string.Empty; + PackageFullName = string.Empty; + Aumid = string.Empty; + Path = string.Empty; + } + + public long Pid { get; set; } + + public string Name { get; set; } + + public string PackageFullName { get; set; } + + public string Aumid { get; set; } + + public string Path { get; set; } + + public ProcessCategory Category { get; set; } + + public DateTimeOffset CreateTime { get; set; } + + public DateTimeOffset ExitTime { get; set; } + + public ulong Samples { get; set; } + + public double Percent { get; set; } + + public double StandardDeviation { get; set; } + + public double Sigma4Deviation { get; set; } + + public double MaxPercent { get; set; } + + public TimeSpan TimeAboveThreshold { get; set; } + + public double TimeAboveThresholdInMinutes { get; set; } + + public ulong TotalCpuTimeInMicroseconds { get; set; } +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/QuietBackgroundProcessesEvent.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/QuietBackgroundProcessesEvent.cs new file mode 100644 index 0000000000..6a2507b760 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/QuietBackgroundProcessesEvent.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.Tracing; +using DevHome.Telemetry; +using Microsoft.Diagnostics.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; +using Windows.Foundation.Diagnostics; + +namespace DevHome.QuietBackgroundProcesses.UI; + +[EventData] +public class QuietBackgroundProcessesEvent : EventBase +{ + public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage; + + public LoggingOpcode Opcode { get; } + + public QuietBackgroundProcessesEvent(LoggingOpcode opcode = LoggingOpcode.Info) + { + Opcode = opcode; + } + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + } +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw new file mode 100644 index 0000000000..1a8948425b --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Strings/en-us/Resources.resw @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Quiet background processes + The name of the Quiet background processes page + + + Start session + Button that starts a quiet background session + + + End session + Button that stops a quiet background session + + + Last session length: + Length of the previously run quiet session + + + Enhance resource management while developing for a maximum of 2 hours + Description of the Quiet background processes feature + + + Quiet background processes + Inline title of the Quiet background processes feature + + + An analytic summary will be available after a session has completed. The summary will include an overview of process resource consumption. The analytic summary is not available while feature is active. + A description of the Quiet background processes time window + + + Link to docs + Link that launches documentation + + + Provide feedback + Link that launches feedback + + + Related links + Label for the doc links + + + Analytic summary + Button to open the analytic summary dialog + + + + + Analytic Summary + Title of the analytic summary page + + + Analysis of process CPU utilization while the session was in use. Calculated amount of time that the process used CPU above the designated threshold. + Desciption of the analytic summary page + + + Sort by + Label for the combo box for sorting + + + Process + Sortby dropdown option + + + Type + Sortby dropdown option + + + CPU above threshold + Sortby dropdown option + + + Search for a process + Label for analytic summary save report button + + + Save report + Label for analytic summary save report button + + + Close + Label for analytic summary close button + + + + + Process + Table heading for Process column + + + Type + Table heading for Type column + + + CPU above threshold + Table heading for 'CPU time above threshold' column + + + min + Abbreviation for the word minutes + + + + + Feature not supported on this version of Windows + Indicates that this OS isn't new enough to support the feature + + + Session Error + Something went wrong when running the session + + + Session ended + The quiet session was cancelled or the time expired + + + Unable to cancel session + Something went wrong when cancelling the session + + \ No newline at end of file diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/AnalyticSummaryPopupViewModel.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/AnalyticSummaryPopupViewModel.cs new file mode 100644 index 0000000000..0d19ddac0a --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/AnalyticSummaryPopupViewModel.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.WinUI.Collections; +using DevHome.Telemetry; +using Microsoft.UI.Xaml.Controls; +using Serilog; + +namespace DevHome.QuietBackgroundProcesses.UI.ViewModels; + +public partial class AnalyticSummaryPopupViewModel : ObservableObject +{ + // Enum for process category + public enum ProcessTableColumn + { + Process, + Type, + CPUAboveThreshold, + } + + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(AnalyticSummaryPopupViewModel)); + private readonly List _processDatas = new(); + + public int SortComboBoxIndex { get; set; } + + public AdvancedCollectionView ProcessDatasAd { get; private set; } + + private ProcessData.ProcessCategory ConvertProcessType(DevHome.QuietBackgroundProcesses.ProcessCategory inputType) + { + return (ProcessData.ProcessCategory)inputType; + } + + public AnalyticSummaryPopupViewModel(QuietBackgroundProcesses.ProcessPerformanceTable? performanceTable) + { + TelemetryFactory.Get().Log("QuietBackgroundProcesses_AnalyticSummary_Open", LogLevel.Info, new QuietBackgroundProcessesEvent()); + + try + { + if (performanceTable != null) + { + var rows = performanceTable.Rows; + foreach (var row in rows) + { + if (row != null) + { + var sampleCount = row.SampleCount; + var sampleDuration = 1; + + var entry = new ProcessData + { + Pid = row.Pid, + Name = row.Name, + PackageFullName = row.PackageFullName, + Aumid = row.Aumid, + Path = row.Path, + Category = ConvertProcessType(row.Category), + CreateTime = row.CreateTime, + ExitTime = row.ExitTime, + Samples = row.SampleCount, + Percent = row.PercentCumulative / sampleCount, + StandardDeviation = (float)Math.Sqrt(row.VarianceCumulative / sampleCount), + Sigma4Deviation = (float)Math.Sqrt(Math.Sqrt(row.Sigma4Cumulative / sampleCount)), + MaxPercent = row.MaxPercent, + TimeAboveThreshold = TimeSpan.FromSeconds(row.SamplesAboveThreshold * sampleDuration), + TotalCpuTimeInMicroseconds = row.TotalCpuTimeInMicroseconds, + }; + + entry.TimeAboveThresholdInMinutes = entry.TimeAboveThreshold.TotalMinutes; + _processDatas.Add(entry); + } + } + } + } + catch (Exception ex) + { + _log.Error("Error populating performance summary table", ex); + } + + ProcessDatasAd = new AdvancedCollectionView(_processDatas, true); + ProcessDatasAd.SortDescriptions.Add(new SortDescription("Pid", SortDirection.Descending)); + } + + private ProcessTableColumn GetProcessTableColumnFromString(string value) + { + if (string.Equals(value, "Process", StringComparison.Ordinal)) + { + return ProcessTableColumn.Process; + } + else if (string.Equals(value, "Type", StringComparison.Ordinal)) + { + return ProcessTableColumn.Type; + } + else if (string.Equals(value, "CPUAboveThreshold", StringComparison.Ordinal)) + { + return ProcessTableColumn.CPUAboveThreshold; + } + + throw new ArgumentException("Invalid value for ProcessTableColumn"); + } + + [RelayCommand] + public void FilterProcessesTextInputChanged(string filterExpression) + { + ProcessDatasAd.Filter = item => + { + try + { + if (item is DevHome.QuietBackgroundProcesses.UI.ProcessData process) + { + return + process.Name.Contains(filterExpression, StringComparison.OrdinalIgnoreCase) + || process.Category.ToString().Contains(filterExpression, StringComparison.OrdinalIgnoreCase) + || process.TimeAboveThreshold.Minutes.ToString(CultureInfo.InvariantCulture).Contains(filterExpression, StringComparison.OrdinalIgnoreCase); + } + + return false; + } + catch (Exception ex) + { + _log.Error("Filtering failed", ex); + } + + return true; + }; + + ProcessDatasAd.RefreshFilter(); + } + + [RelayCommand] + public void SortProcessesComboBoxChanged(string selectedValueString) + { + ProcessDatasAd.SortDescriptions.Clear(); + + var selectedValue = GetProcessTableColumnFromString(selectedValueString); + switch (selectedValue) + { + case ProcessTableColumn.Process: + ProcessDatasAd.SortDescriptions.Add(new SortDescription("Name", SortDirection.Ascending)); + break; + case ProcessTableColumn.Type: + ProcessDatasAd.SortDescriptions.Add(new SortDescription("Category", SortDirection.Descending)); + break; + case ProcessTableColumn.CPUAboveThreshold: + ProcessDatasAd.SortDescriptions.Add(new SortDescription("TimeAboveThreshold", SortDirection.Descending)); + break; + } + } + + public void SaveReport(string filePath) + { + // Save the report to a .csv + using (StreamWriter writer = new StreamWriter(filePath)) + { + // Write the .csv header + writer.WriteLine("Pid," + + "Name," + + "Samples," + + "Percent," + + "StandardDeviation," + + "Sigma4Deviation," + + "MaxPercent," + + "TimeAboveThreshold," + + "TotalCpuTimeInMicroseconds," + + "PackageFullName," + + "Aumid," + + "Path," + + "Category," + + "CreateTime," + + "ExitTime"); + + // Write each item from the list to the file + foreach (var data in this._processDatas) + { + var row = $"{data.Pid}," + + $"{data.Name}," + + $"{data.Samples}," + + $"{data.Percent}," + + $"{data.StandardDeviation}," + + $"{data.Sigma4Deviation}," + + $"{data.MaxPercent}," + + $"{data.TimeAboveThreshold}," + + $"{data.TotalCpuTimeInMicroseconds}," + + $"{data.PackageFullName}," + + $"{data.Aumid}," + + $"{data.Path}," + + $"{data.Category}," + + $"{data.CreateTime}," + + $"{data.ExitTime}"; + writer.WriteLine(row); + } + } + } +} diff --git a/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs similarity index 66% rename from tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs rename to tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs index 9df8d6d7a1..ff127e1c4a 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/ViewModels/QuietBackgroundProcessesViewModel.cs @@ -2,17 +2,19 @@ // Licensed under the MIT License. using System; +using System.Globalization; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.WinUI; using DevHome.Common.Services; -using DevHome.QuietBackgroundProcesses; +using DevHome.Telemetry; using Microsoft.UI.Xaml; using Serilog; +using Windows.Foundation.Diagnostics; using WinUIEx; -namespace DevHome.Customization.ViewModels; +namespace DevHome.QuietBackgroundProcesses.UI.ViewModels; public partial class QuietBackgroundProcessesViewModel : ObservableObject { @@ -22,16 +24,17 @@ public partial class QuietBackgroundProcessesViewModel : ObservableObject private readonly TimeSpan _zero = new(0, 0, 0); private readonly TimeSpan _oneSecond = new(0, 0, 1); - private readonly WindowEx _windowEx; - -#nullable enable + private TimeSpan _sessionDuration; private QuietBackgroundProcessesSession? _session; -#nullable disable + private ProcessPerformanceTable? _table; [ObservableProperty] private bool _isFeaturePresent; + [ObservableProperty] + private bool _isAnalyticSummaryAvailable; + [ObservableProperty] private string _sessionStateText; @@ -39,7 +42,7 @@ public partial class QuietBackgroundProcessesViewModel : ObservableObject private bool _quietButtonChecked; [ObservableProperty] - private string _quietButtonText; + private string? _quietButtonText; private QuietBackgroundProcessesSession GetSession() { @@ -53,7 +56,7 @@ private QuietBackgroundProcessesSession GetSession() private string GetString(string id) { - var stringResource = new StringResource("DevHome.Customization.pri", "DevHome.Customization/Resources"); + var stringResource = new StringResource("DevHome.QuietBackgroundProcesses.UI.pri", "DevHome.QuietBackgroundProcesses.UI/Resources"); return stringResource.GetLocalized(id); } @@ -70,6 +73,8 @@ public QuietBackgroundProcessesViewModel( { _experimentationService = experimentationService; _windowEx = windowEx; + _sessionStateText = string.Empty; + _dispatcherTimer = new DispatcherTimer(); } public async Task LoadViewModelContentAsync() @@ -81,7 +86,30 @@ await Task.Run(async () => return; } - var isFeaturePresent = QuietBackgroundProcessesSessionManager.IsFeaturePresent(); + var isFeaturePresent = false; + try + { + isFeaturePresent = QuietBackgroundProcessesSessionManager.IsFeaturePresent(); + } + catch (Exception ex) + { + _log.Error(ex, "COM error"); + } + + var isAvailable = false; + isAvailable = _table != null; + if (!isAvailable) + { + try + { + isAvailable = QuietBackgroundProcessesSessionManager.HasLastPerformanceRecording(); + } + catch (Exception ex) + { + _log.Error(ex, "COM error"); + } + } + var running = false; long? timeLeftInSeconds = null; if (isFeaturePresent) @@ -99,6 +127,7 @@ await Task.Run(async () => await _windowEx.DispatcherQueue.EnqueueAsync(() => { IsFeaturePresent = isFeaturePresent; + IsAnalyticSummaryAvailable = isAvailable; if (IsFeaturePresent) { // Resume countdown if there's an existing quiet window @@ -107,6 +136,7 @@ await _windowEx.DispatcherQueue.EnqueueAsync(() => else { SessionStateText = GetStatusString("FeatureNotSupported"); + QuietButtonText = GetString("QuietBackgroundProcesses_QuietButton_Start"); } }); }); @@ -119,11 +149,16 @@ private void SetQuietSessionRunningState(bool running, long? timeLeftInSeconds = var seconds = timeLeftInSeconds ?? GetTimeRemaining(); StartCountdownTimer(seconds); QuietButtonText = GetString("QuietBackgroundProcesses_QuietButton_Stop"); + IsAnalyticSummaryAvailable = false; } else { _dispatcherTimer?.Stop(); QuietButtonText = GetString("QuietBackgroundProcesses_QuietButton_Start"); + if (!IsAnalyticSummaryAvailable) + { + IsAnalyticSummaryAvailable = QuietBackgroundProcessesSessionManager.HasLastPerformanceRecording(); + } } QuietButtonChecked = !running; @@ -136,12 +171,17 @@ public void QuietButtonClicked() { try { + TelemetryFactory.Get().Log("QuietBackgroundProcesses_Session", LogLevel.Critical, new QuietBackgroundProcessesEvent(LoggingOpcode.Start)); + // Launch the server, which then elevates itself, showing a UAC prompt var timeLeftInSeconds = GetSession().Start(); + _sessionDuration = TimeSpan.FromSeconds(timeLeftInSeconds); SetQuietSessionRunningState(true, timeLeftInSeconds); } catch (Exception ex) { + TelemetryFactory.Get().Log("QuietBackgroundProcesses_SessionStartError", LogLevel.Critical, new QuietBackgroundProcessesEvent()); + SessionStateText = GetStatusString("SessionError"); _log.Error(ex, "QuietBackgroundProcessesSession::Start failed"); } @@ -150,12 +190,17 @@ public void QuietButtonClicked() { try { - GetSession().Stop(); + TelemetryFactory.Get().Log("QuietBackgroundProcesses_Session", LogLevel.Critical, new QuietBackgroundProcessesEvent(LoggingOpcode.Stop)); + + _table = GetSession().Stop(); + IsAnalyticSummaryAvailable = _table != null; SetQuietSessionRunningState(false); - SessionStateText = GetStatusString("SessionEnded"); + SessionStateText = GetLastSessionLengthString(_sessionDuration - _secondsLeft); } catch (Exception ex) { + TelemetryFactory.Get().Log("QuietBackgroundProcesses_SessionStopError", LogLevel.Critical, new QuietBackgroundProcessesEvent()); + SessionStateText = GetStatusString("UnableToCancelSession"); _log.Error(ex, "QuietBackgroundProcessesSession::Stop failed"); } @@ -214,7 +259,7 @@ private void StartCountdownTimer(long timeLeftInSeconds) SessionStateText = _secondsLeft.ToString(); } - private void DispatcherTimer_Tick(object sender, object e) + private void DispatcherTimer_Tick(object? sender, object e) { // Subtract 1 second _secondsLeft = _secondsLeft.Subtract(_oneSecond); @@ -238,12 +283,36 @@ private void DispatcherTimer_Tick(object sender, object e) if (sessionEnded) { SetQuietSessionRunningState(false); + var lastSessionLength = _sessionDuration - _secondsLeft; _secondsLeft = _zero; - SessionStateText = GetStatusString("SessionEnded"); + SessionStateText = GetLastSessionLengthString(lastSessionLength); } else { SessionStateText = _secondsLeft.ToString(); // CultureInfo.InvariantCulture } } + + private string GetLastSessionLengthString(TimeSpan lastSessionLength) + { + return GetString("QuietBackgroundProcesses_Time_LastSessionLength") + " " + lastSessionLength.ToString("g", CultureInfo.CurrentCulture); + } + + public ProcessPerformanceTable? GetProcessPerformanceTable() + { + if (_table == null) + { + try + { + _table = QuietBackgroundProcessesSessionManager.TryGetLastPerformanceRecording(); + } + catch (Exception ex) + { + SessionStateText = GetStatusString("SessionError"); + _log.Error(ex, "QuietBackgroundProcessesSessionManager.TryGetLastPerformanceRecording failed"); + } + } + + return _table; + } } diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml new file mode 100644 index 0000000000..9195fb6766 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml.cs new file mode 100644 index 0000000000..10c0608248 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/AnalyticSummaryPopup.xaml.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Extensions; +using DevHome.Common.Windows.FileDialog; +using DevHome.QuietBackgroundProcesses.UI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using WinUIEx; + +namespace DevHome.QuietBackgroundProcesses.UI.Views; + +public sealed partial class AnalyticSummaryPopup : ContentDialog +{ + private readonly WindowEx _mainWindow; + + public AnalyticSummaryPopupViewModel ViewModel + { + get; + } + + public AnalyticSummaryPopup(QuietBackgroundProcesses.ProcessPerformanceTable? performanceTable) + { + _mainWindow = Application.Current.GetService(); + + ViewModel = new AnalyticSummaryPopupViewModel(performanceTable); + + this.Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style; + this.DefaultButton = ContentDialogButton.Primary; + this.InitializeComponent(); + } + + private void SaveReportButtonClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + using var fileDialog = new WindowSaveFileDialog(); + fileDialog.AddFileType("CSV files", ".csv"); + + var filePath = fileDialog.Show(_mainWindow); + if (filePath != null) + { + ViewModel.SaveReport(filePath); + } + } +} diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml new file mode 100644 index 0000000000..14bd6c02f3 --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml.cs b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml.cs new file mode 100644 index 0000000000..364220422c --- /dev/null +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/ProcessPerformanceTableControl.xaml.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.ObjectModel; +using DevHome.QuietBackgroundProcesses.UI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.QuietBackgroundProcesses.UI.Views; + +public sealed partial class ProcessPerformanceTableControl : UserControl +{ + public ObservableCollection ProcessDatas { get; set; } = new ObservableCollection(); + + public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register( + nameof(ItemsSource), + typeof(object), + typeof(AnalyticSummaryPopupViewModel), + new PropertyMetadata(default)); + + public object ItemsSource + { + get => (object)GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + + public ProcessPerformanceTableControl() + { + this.InitializeComponent(); + } +} diff --git a/tools/Customization/DevHome.Customization/Views/QuietBackgroundProcessesView.xaml b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesView.xaml similarity index 72% rename from tools/Customization/DevHome.Customization/Views/QuietBackgroundProcessesView.xaml rename to tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesView.xaml index efe237d265..b92dfcbbe6 100644 --- a/tools/Customization/DevHome.Customization/Views/QuietBackgroundProcessesView.xaml +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.UI/Views/QuietBackgroundProcessesView.xaml @@ -1,6 +1,6 @@ - + @@ -35,7 +34,16 @@ - + + + +