diff --git a/.gitignore b/.gitignore index b99e119c2..2aa77173d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -## Ignore Visual Studio temporary files, build results, and +## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # Build results @@ -22,6 +22,7 @@ PresentMon/nul # Visual Studio 2015 cache/options directory .vs/ +AGENTS.md # Visual C++ cache files ipch/ @@ -68,4 +69,4 @@ PresentMon/ddETWExternalEventsTEMP.BIN # Local runsettings files (user-specific test configurations) *.local.runsettings -Directory.Build.props \ No newline at end of file +Directory.Build.props diff --git a/IntelPresentMon/AppCef/ipm-ui-vue/presets/preset-0.json b/IntelPresentMon/AppCef/ipm-ui-vue/presets/preset-0.json index 49339e7e7..73079cc15 100644 --- a/IntelPresentMon/AppCef/ipm-ui-vue/presets/preset-0.json +++ b/IntelPresentMon/AppCef/ipm-ui-vue/presets/preset-0.json @@ -13,7 +13,7 @@ "metricId": 3, "arrayIndex": 0, "deviceId": 0, - "statId": 7, + "statId": 0, "desiredUnitId": 0 }, "lineColor": { @@ -217,4 +217,4 @@ "textSize": 11 } ] -} \ No newline at end of file +} diff --git a/IntelPresentMon/AppCef/ipm-ui-vue/presets/preset-1.json b/IntelPresentMon/AppCef/ipm-ui-vue/presets/preset-1.json index f401ad0ff..2ad29fe4e 100644 --- a/IntelPresentMon/AppCef/ipm-ui-vue/presets/preset-1.json +++ b/IntelPresentMon/AppCef/ipm-ui-vue/presets/preset-1.json @@ -13,7 +13,7 @@ "metricId": 3, "arrayIndex": 0, "deviceId": 0, - "statId": 7, + "statId": 0, "desiredUnitId": 0 }, "lineColor": { @@ -658,7 +658,7 @@ "metricId": 48, "arrayIndex": 0, "deviceId": 0, - "statId": 1, + "statId": 0, "desiredUnitId": 0 }, "lineColor": { @@ -733,4 +733,4 @@ "textSize": 11 } ] -} \ No newline at end of file +} diff --git a/IntelPresentMon/AppCef/ipm-ui-vue/presets/preset-2.json b/IntelPresentMon/AppCef/ipm-ui-vue/presets/preset-2.json index 57647473a..2ee0ef61c 100644 --- a/IntelPresentMon/AppCef/ipm-ui-vue/presets/preset-2.json +++ b/IntelPresentMon/AppCef/ipm-ui-vue/presets/preset-2.json @@ -13,7 +13,7 @@ "metricId": 3, "arrayIndex": 0, "deviceId": 0, - "statId": 7, + "statId": 0, "desiredUnitId": 0 }, "lineColor": { @@ -398,4 +398,4 @@ "textSize": 11 } ] -} \ No newline at end of file +} diff --git a/IntelPresentMon/AppCef/ipm-ui-vue/src/core/api.ts b/IntelPresentMon/AppCef/ipm-ui-vue/src/core/api.ts index b0627da4c..7a64534ca 100644 --- a/IntelPresentMon/AppCef/ipm-ui-vue/src/core/api.ts +++ b/IntelPresentMon/AppCef/ipm-ui-vue/src/core/api.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT import { type Metric } from '@/core/metric' import { type Stat } from './stat' @@ -54,11 +54,13 @@ export class Api { static async loadEnvVars(): Promise { return await this.invokeEndpointFuture('loadEnvVars', {}); } - static async introspect(): Promise<{metrics: Metric[], stats: Stat[], units: Unit[]}> { + static async introspect(): Promise<{metrics: Metric[], stats: Stat[], units: Unit[], systemDeviceId: number, defaultAdapterId: number}> { const introData = await this.invokeEndpointFuture('Introspect', {}); - if (!Array.isArray(introData.metrics) || !Array.isArray(introData.stats) || !Array.isArray(introData.units)) { + if (!Array.isArray(introData.metrics) || !Array.isArray(introData.stats) || + !Array.isArray(introData.units) || typeof introData.systemDeviceId !== 'number' || + typeof introData.defaultAdapterId !== 'number') { console.log("error intro call"); - throw new Error('Bad (non-array) member type returned from introspect'); + throw new Error('Bad member type returned from introspect'); } return introData; } @@ -152,4 +154,4 @@ export class Api { static registerStalePidHandler(callback: () => void) { this.core.registerSignalHandler('stalePid', callback); } -} \ No newline at end of file +} diff --git a/IntelPresentMon/AppCef/ipm-ui-vue/src/core/metric.ts b/IntelPresentMon/AppCef/ipm-ui-vue/src/core/metric.ts index 2135a1b46..414a1d080 100644 --- a/IntelPresentMon/AppCef/ipm-ui-vue/src/core/metric.ts +++ b/IntelPresentMon/AppCef/ipm-ui-vue/src/core/metric.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT export interface Metric { id: number, @@ -12,4 +12,4 @@ export interface Metric { // not used yet in frontend -> backend fills in selected adapter id IF metric is not universal availableDeviceIds: number[], numeric: boolean, -} \ No newline at end of file +} diff --git a/IntelPresentMon/AppCef/ipm-ui-vue/src/stores/introspection.ts b/IntelPresentMon/AppCef/ipm-ui-vue/src/stores/introspection.ts index fb7a11427..6ee78c82f 100644 --- a/IntelPresentMon/AppCef/ipm-ui-vue/src/stores/introspection.ts +++ b/IntelPresentMon/AppCef/ipm-ui-vue/src/stores/introspection.ts @@ -1,4 +1,4 @@ -import { ref, computed } from 'vue'; +import { ref, computed } from 'vue'; import { defineStore } from 'pinia'; import { Api } from '@/core/api'; import type { Metric } from '@/core/metric'; @@ -11,6 +11,8 @@ export const useIntrospectionStore = defineStore('introspection', () => { const metrics = ref([]); const stats = ref([]); const units = ref([]); + const systemDeviceId = ref(0); + const defaultAdapterId = ref(0); // === Computed === const metricOptions = computed(() => { @@ -25,11 +27,13 @@ export const useIntrospectionStore = defineStore('introspection', () => { // === Actions === async function load() { - if (metrics.value.length === 0) { + if (metrics.value.length === 0 || systemDeviceId.value === 0 || defaultAdapterId.value === 0) { const intro = await Api.introspect(); metrics.value = intro.metrics; stats.value = intro.stats; units.value = intro.units; + systemDeviceId.value = intro.systemDeviceId; + defaultAdapterId.value = intro.defaultAdapterId; } } @@ -38,7 +42,9 @@ export const useIntrospectionStore = defineStore('introspection', () => { metrics, stats, units, + systemDeviceId, + defaultAdapterId, metricOptions, load }; -}); \ No newline at end of file +}); diff --git a/IntelPresentMon/AppCef/ipm-ui-vue/src/stores/preferences.ts b/IntelPresentMon/AppCef/ipm-ui-vue/src/stores/preferences.ts index 1a98033c1..d8ed326f6 100644 --- a/IntelPresentMon/AppCef/ipm-ui-vue/src/stores/preferences.ts +++ b/IntelPresentMon/AppCef/ipm-ui-vue/src/stores/preferences.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT import { ref, computed } from 'vue'; import { defineStore } from 'pinia'; @@ -114,8 +114,11 @@ export const usePreferencesStore = defineStore('preferences', () => { } async function pushSpecification() { + await intro.load(); // we need to get a non-proxy object for the call const widgets = deepToRaw(loadout.widgets); + const systemDeviceId = intro.systemDeviceId; + const defaultAdapterId = intro.defaultAdapterId; for (const widget of widgets) { // Filter out the widgetMetrics that do not meet the condition, modify those that do widget.metrics = widget.metrics.filter(widgetMetric => { @@ -128,13 +131,18 @@ export const usePreferencesStore = defineStore('preferences', () => { widgetMetric.metric.deviceId = 0; // establish universal device id // Check whether metric is a gpu metric, then we need non-universal device id if (!metric.availableDeviceIds.includes(0)) { - // if no specific adapter id set, assume adapter id = 1 is active - const adapterId = preferences.value.adapterId !== null ? preferences.value.adapterId : 1; - // Set adapter id for this query element to the active one if available - if (metric.availableDeviceIds.includes(adapterId)) { - widgetMetric.metric.deviceId = adapterId; - } else { // if active adapter id is not available drop this widgetMetric - return false; + // if this is a system metric, deviceId needs to be set to fixed id + if (metric.availableDeviceIds.includes(systemDeviceId)) { + widgetMetric.metric.deviceId = systemDeviceId; + } + else { + const adapterId = preferences.value.adapterId !== null ? preferences.value.adapterId : defaultAdapterId; + // Set adapter id for this query element to the active one if available + if (adapterId !== 0 && metric.availableDeviceIds.includes(adapterId)) { + widgetMetric.metric.deviceId = adapterId; + } else { // if active adapter id is not available drop this widgetMetric + return false; + } } } // Fill out the unit @@ -178,4 +186,4 @@ export const usePreferencesStore = defineStore('preferences', () => { initPreferences, resetPreferences }; -}); \ No newline at end of file +}); diff --git a/IntelPresentMon/AppCef/source/util/KernelActionRegistration.cpp b/IntelPresentMon/AppCef/source/util/KernelActionRegistration.cpp index 51d092a48..d11dc669d 100644 --- a/IntelPresentMon/AppCef/source/util/KernelActionRegistration.cpp +++ b/IntelPresentMon/AppCef/source/util/KernelActionRegistration.cpp @@ -1,4 +1,4 @@ -#include "IpcInvocationManager.h" +#include "IpcInvocationManager.h" #include "../../../KernelProcess/kact/AllActions.h" namespace p2c::client::util::kact { template @@ -9,7 +9,6 @@ namespace p2c::client::util::kact { IpcActionRegistrator reg_ibind_EnumerateAdapters_; IpcActionRegistrator reg_ibind_Introspect_; IpcActionRegistrator reg_ibind_PushSpecification_; - IpcActionRegistrator reg_ibind_SetAdapter_; IpcActionRegistrator reg_ibind_SetCapture_; IpcActionRegistrator reg_ibind_BindHotkey_; IpcActionRegistrator reg_ibind_ClearHotkey_; diff --git a/IntelPresentMon/AppCef/source/winmain.cpp b/IntelPresentMon/AppCef/source/winmain.cpp index 46b2bad81..24b2c79a2 100644 --- a/IntelPresentMon/AppCef/source/winmain.cpp +++ b/IntelPresentMon/AppCef/source/winmain.cpp @@ -194,7 +194,7 @@ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi } // name this process / thread - log::IdentificationTable::AddThisProcess(opt.cefType.AsOptional().value_or("main-client")); + log::IdentificationTable::AddThisProcess("cef-" + opt.cefType.AsOptional().value_or("mclient")); log::IdentificationTable::AddThisThread("main"); // initialize the logging system diff --git a/IntelPresentMon/CliCore/CliCore.vcxproj b/IntelPresentMon/CliCore/CliCore.vcxproj deleted file mode 100644 index e70429fca..000000000 --- a/IntelPresentMon/CliCore/CliCore.vcxproj +++ /dev/null @@ -1,222 +0,0 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - Debug - x64 - - - Release - x64 - - - - 16.0 - Win32Proj - {31551c65-38ea-4e21-8c0d-4bc4a097993c} - CliCore - 10.0 - - - - StaticLibrary - true - v143 - Unicode - - - StaticLibrary - false - v143 - true - Unicode - - - StaticLibrary - true - v143 - Unicode - - - StaticLibrary - false - v143 - true - Unicode - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - - - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - - - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - - - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - - - - - - - - - - - - Level3 - true - true - NotUsing - stdcpplatest - MultiThreadedDebug - ..;%(AdditionalIncludeDirectories) - - - true - - - - - Level3 - true - true - true - NDEBUG;%(PreprocessorDefinitions) - true - NotUsing - stdcpplatest - MultiThreaded - ..;%(AdditionalIncludeDirectories) - - - true - true - true - - - - - Level3 - true - true - NotUsing - stdcpplatest - true - MultiThreadedDebug - ..;%(AdditionalIncludeDirectories) - - - true - - - - - Level3 - true - true - true - NDEBUG;%(PreprocessorDefinitions) - true - NotUsing - stdcpplatest - true - MultiThreaded - ..;%(AdditionalIncludeDirectories) - - - true - true - true - - - - - {808f5ea9-ea09-4d72-87b4-5397d43cba54} - - - {811adcf0-da1d-4a94-97c7-7aa6c33b5368} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/IntelPresentMon/CliCore/CliCore.vcxproj.filters b/IntelPresentMon/CliCore/CliCore.vcxproj.filters deleted file mode 100644 index 6015f4ca7..000000000 --- a/IntelPresentMon/CliCore/CliCore.vcxproj.filters +++ /dev/null @@ -1,47 +0,0 @@ - - - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/Entry.cpp b/IntelPresentMon/CliCore/source/Entry.cpp deleted file mode 100644 index 47935d74e..000000000 --- a/IntelPresentMon/CliCore/source/Entry.cpp +++ /dev/null @@ -1,347 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include "pmon/FrameDataStream.h" -#include "dat/CsvWriter.h" -#include "dat/FrameFilterPump.h" -#include "dat/FrameDemultiplexer.h" -#include -#include "opt/Options.h" -#include "cons/ConsoleWaitControl.h" -#include -#include -#include -#include -#include -#include -#include "svc/Boot.h" -#include "cons/OptionalConsoleOut.h" -#include "dat/MakeCsvName.h" -#include "pmon/Client.h" -#include "pmon/stream/LoggedFrameDataState.h" - - -namespace p2c::cli -{ - using infra::util::ToWide; - using infra::util::ToNarrow; - using namespace std::chrono_literals; - using namespace std::string_literals; - namespace rn = std::ranges; - namespace vi = rn::views; - - int Entry(int argc, char** argv) - { - // keep track of how long since cli app launch for the - // purposes of start/stop timing - infra::util::ChiliTimer launchTimer; - - // parse command line options - try { - opt::init(argc, argv); - } - catch (const CLI::ParseError& e) { - return e.get_exit_code(); - }; - - // shortcut for command line options - const auto& opts = opt::get(); - - // boot services - svc::Boot( - opts.log, - opts.logPath ? std::optional{ *opts.logPath } : std::nullopt, - opts.logLevel ? std::optional{ *opts.logLevel } : std::nullopt - ); - - try { - using WakeReason = cons::ConsoleWaitControl::WakeReason; - auto& waiter = cons::ConsoleWaitControl::Get(); - - std::shared_ptr pClient; - // stream used to make sure etl frames get processed when there are no other streams - // this is necessary because when processes are specified by name, we need to pull frames - // to generate the spawn events, but spawn events are only generated when there is a stream - // and a stream will only be created if there is a spawn event (chicken and egg problem) - const auto telemetryPeriod = opts.telemetryPeriod ? *opts.telemetryPeriod : 16; - std::shared_ptr pLogSpoolingStream; - if (opts.etlFile) { - pClient = std::make_shared(*opts.etlFile, waiter, telemetryPeriod); - pLogSpoolingStream = pClient->OpenStream(0); - } - else { - pClient = std::make_shared(telemetryPeriod); - } - - // list adapters - if (opts.listAdapters) { - const auto adapters = pClient->EnumerateAdapters(); - if (adapters.empty()) { - std::cout << "No adapters compatible with PresentMon GPU telemetry available." << std::endl; - } - else { - std::cout << "Available adapters:" << std::endl; - } - for (auto& a : adapters) { - std::cout << " " << a.id << ": [" << a.vendor << "] " << a.name << std::endl; - } - return 0; - } - - cons::OptionalConsoleOut out{ !opts.outputStdout }; - - // set adapter for gpu telemetry - if (opts.adapter) { - const auto adapters = pClient->EnumerateAdapters(); - if (const auto i = rn::find_if(adapters, [&opts](const auto& a) - {return a.id == *opts.adapter; }); i == adapters.end()) { - std::cout << "Adapter with ID [" << *opts.adapter << "] not found." << std::endl; - return -1; - } - else { - out << "Using for GPU telemetry adapter <" << i->id << ">: [" << i->vendor << "] " << i->name << std::endl; - } - pClient->SetAdapter(*opts.adapter); - } - - // count of the number of specifically-targetted processes (by name and pid) - const auto targetCount = (*opts.name).size() + (*opts.pid).size(); - // derived flag for whether we are capturing all processes - const auto captureAll = opts.captureAll || targetCount == 0; - // targets specified by name that have been detected and have had a stream created for them - std::set mappedProcessNames; - // frame pumps for each stream, which also own the sinks for their outgoing frame data - std::vector> pumps; - // count total number of processes being monitored for the purposes of exiting - int trackedProcessCount = 0; - // count number of monitored processes that have exited - int exitedProcessCount = 0; - // output file name option as std::optional - const auto outputFileName = opts.outputFile ? std::optional{ *opts.outputFile } : std::nullopt; - // setup csv column groups - auto csvGroups = opts.GetColumnGroups(); - // setup excludes - auto excludes = *opts.excludeName; - if (!opts.ignoreCase) { - for (auto& e : excludes) { - e = e | vi::transform([](char c) {return(char)std::tolower(c); }) - | rn::to(); - } - } - // function decides how streams should be added based on 2x2 matrix - // of options: single/multi-csv x omni/targeted-streams - const auto AddStream = [&](uint32_t pid, std::optional processName = {}) { - // non-omni stream usage cases: - // if we are targetting w/ multi-csv, add stream + writer - // if we are targetting single process (regardless of csv-mode), same - if (!captureAll && (opts.multiCsv || targetCount == 1)) { - auto name = dat::MakeCsvName(opts.noCsv, pid, std::move(processName), outputFileName); - auto pWriter = std::make_shared(std::move(name), csvGroups, opts.outputStdout); - pumps.push_back(std::make_shared( - pClient->OpenStream(pid), std::move(pWriter), - excludes, std::vector{}, opts.excludeDropped, opts.ignoreCase - )); - waiter.AddProcessDeathWatch(pid); - ++trackedProcessCount; - } - // omni stream usage cases: - // if we are doing omni capture x multi-csv, add omni stream w/ demux - else if (captureAll && opts.multiCsv) { - assert(pumps.empty()); - auto pDemux = std::make_shared(csvGroups, outputFileName); - pumps.push_back(std::make_shared( - pClient->OpenStream(0), std::move(pDemux), - excludes, std::vector{}, opts.excludeDropped, opts.ignoreCase - )); - } - // if we are doing omni capture x single-csv, add omni w/ writer - else if (captureAll && !opts.multiCsv) { - assert(pumps.empty()); - auto name = dat::MakeCsvName(opts.noCsv, {}, {}, outputFileName); - auto pWriter = std::make_shared(std::move(name), csvGroups, opts.outputStdout); - pumps.push_back(std::make_shared( - pClient->OpenStream(0), std::move(pWriter), - excludes, std::vector{}, opts.excludeDropped, opts.ignoreCase - )); - } - // if we are doing multi-target capture x single-csv, add omni w/ whitelist - // if one doesn't exist, or add whitelist to existing otherwise - else { - if (pumps.empty()) { - auto name = dat::MakeCsvName(opts.noCsv, {}, {}, outputFileName); - auto pWriter = std::make_shared(std::move(name), csvGroups, opts.outputStdout); - pumps.push_back(std::make_shared( - pClient->OpenStream(0), std::move(pWriter), excludes, - std::vector{ pid }, opts.excludeDropped, opts.ignoreCase - )); - } - else { - pumps.front()->AddInclude(pid); - } - waiter.AddProcessDeathWatch(pid); - ++trackedProcessCount; - } - }; - - // delay before starting any captures - if (opts.startAfter) { - // subtract time elapsed since launch from the desired start time to get wait time - const auto delaySeconds = std::max(0., *opts.startAfter - launchTimer.Peek()); - out << std::format("Waiting {:.3f}s for start delay.", *opts.startAfter) << std::endl; - const auto delayMs = 1ms * int(1000.f * delaySeconds); - waiter.SetTimer(delayMs); - if (waiter.Wait() == WakeReason::CtrlTermination) { - out << "Termination signal received during wait. Exiting..." << std::endl; - waiter.Exit(); - return 0; - } - } - - // immediately start streams for all processes specified by pid - if (opts.pid) { - for (auto pid : *opts.pid) { - out << "Capturing process with PID [" << pid << "]." << std::endl; - AddStream(pid); - } - } - - // try to start streams for all processes specified by name - // maintain a watch on all named processes not yet running so that a stream can be - // started as soon as a matching process spawns - if (opts.name) { - for (auto name : *opts.name) { - if (opts.ignoreCase) { - name = name - | vi::transform([](char c) {return(char)std::tolower(c); }) - | rn::to(); - } - // begin watching for process spawn before we check for pre-existence - // this guards against race conditions of spawn after check but before watch - waiter.AddProcessSpawnWatch(name); - - // now check for pre-existence and open stream if available (only for live captures) - if (!opts.etlFile) { - auto procMap = win::ProcessMapBuilder{}.AsNameMap(opts.ignoreCase); - if (const auto i = procMap.find(ToWide(name)); i != procMap.end()) { - // we found that our target process is already active! - const auto pid = i->second.pid; - out << "Capturing process [" << name << "] with PID [" << pid << "]." << std::endl; - AddStream(pid, name); - // we found by polling, so remove from the watch - waiter.RemoveProcessSpawnWatch(ToNarrow(i->first)); - // also add to set of opened process names so that we can ignore any - // spawn events if they were already emitted - mappedProcessNames.insert(i->first); - } - else { - // target process is not yet open - out << "Waiting for [" << name << "] to spawn." << std::endl; - } - } - } - } - - // start omni-stream capture - if (captureAll || (!opts.pid && !opts.name)) { - out << "Capturing all processes." << std::endl; - AddStream(0); - } - - // set stop timer - if (opts.captureFor) { - out << std::format("Setting capture timer to {:.3f}s.", *opts.captureFor) << std::endl; - const auto delayMs = 1ms * int(1000.f * *opts.captureFor); - waiter.SetTimer(delayMs); - } - - // periodically run FrameFilterPump Process() function for all active streams - // which reads in frame data and writes it to corresponding csv file(s) - // also respond to spawn events by adding new streams - // also respond to ctrl events by doing cleanup and shutting down - // also respond to timer events by doing cleanup and shutting down - // also respond to process death events by shutting down when all dead - // poll at high rate for offline etl processing to protect again race condition (early close) - const auto livePollPeriod = opts.pollPeriod ? 1ms * *opts.pollPeriod : 250ms; - const auto pollingPeriod = opts.etlFile ? 1ms : livePollPeriod; - // simulated timestamp used for pulling frames from streams - double timestamp = 0.; - for (WakeReason wakeReason = WakeReason::Timeout;; - wakeReason = waiter.WaitFor(pollingPeriod)) { - switch (wakeReason) { - case WakeReason::CtrlTermination: - out << "Received termination control signal. Exiting...." << std::endl; - // Exit() call unblocks the signal handler thread - waiter.Exit(); - return 0; - case WakeReason::EndOfLog: - out << "Reached end of ETW log. Exiting...." << std::endl; - waiter.Exit(); - return 0; - case WakeReason::TimerEvent: - out << "Requested stop time reached. Exiting...." << std::endl; - // Exit() call protects again race condition where if ctrl signal - // handler is call right after timer wake, we make sure it doesn't block - waiter.Exit(); - return 0; - case WakeReason::ProcessDeath: - while (auto p = waiter.GetProcessDeathEvent()) { - // non-omni target mode, remove process from stream set - if (!captureAll && (opts.multiCsv || targetCount == 1)) { - std::erase_if(pumps, [&](const auto& s) {return s->GetPid() == *p; }); - } - // keep track of how many processes have exited - out << "Detected that process with pid [" << *p << "] has exited." << std::endl; - exitedProcessCount++; - } - // if last target process dies, we will exit - if (exitedProcessCount >= trackedProcessCount && - exitedProcessCount >= targetCount) { - out << "All target processes have exited. Exiting...." << std::endl; - waiter.Exit(); - return 0; - } - break; - case WakeReason::ProcessSpawn: - while (auto p = waiter.GetSpawnEvent()) { - // ignore processes already opened - if (!mappedProcessNames.contains(p->name)) { - // add process to set of names to ignore - mappedProcessNames.insert(p->name); - // open stream - AddStream(p->pid, ToNarrow(p->name)); - out << "Detected process [" << ToNarrow(p->name) << "] with pid [" << p->pid << "]. Starting capture...." << std::endl; - } - } - break; - case WakeReason::Timeout: - // use this stream when processing etl file to make sure that frames are pulled - // and process spawn events are generated - if (pLogSpoolingStream) { - pLogSpoolingStream->Pull(timestamp); - } - // pump streams if there are no events pending - if (!waiter.EventPending()) { - // process all pumps if there was a polling timeout - for (auto& pump : pumps) { - pump->Process(timestamp); - } - // update timestamp so cache is refreshed - timestamp++; - } - break; - default: - assert(false && "Unknown WakeReason encountered in main stream processing loop"); - } - } - } - catch (const std::exception& e) { - std::cout << "Error: [" << e.what() << "].\n"; - std::cout << "Use --help to see a list of options supported by this program." << std::endl; - } - - return -1; - } -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/Entry.h b/IntelPresentMon/CliCore/source/Entry.h deleted file mode 100644 index 405d4fdb2..000000000 --- a/IntelPresentMon/CliCore/source/Entry.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -namespace p2c::cli -{ - int Entry(int argc, char** argv); -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/cons/ConsoleWaitControl.cpp b/IntelPresentMon/CliCore/source/cons/ConsoleWaitControl.cpp deleted file mode 100644 index c784c46f2..000000000 --- a/IntelPresentMon/CliCore/source/cons/ConsoleWaitControl.cpp +++ /dev/null @@ -1,340 +0,0 @@ -#include "ConsoleWaitControl.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include "../opt/Options.h" - - -namespace p2c::cli::cons -{ - using namespace std::chrono_literals; - namespace rn = std::ranges; - namespace vi = rn::views; - - class ProcessTracker : private win::com::ProcessSpawnSink::EventQueue - { - public: - ProcessTracker(std::function notify) - : - win::com::ProcessSpawnSink::EventQueue{ [this] { HandleEvents_(); } }, - notifyFunction_{ std::move(notify) }, - pSpawnListener_{ conn_.MakeListener( - static_cast(*this)) } - {} - void AddWatch(std::string watch) - { - std::lock_guard lk{ mtx_ }; - watchedNames_.insert(std::move(watch)); - } - void RemoveWatch(std::string watch) - { - std::lock_guard lk{ mtx_ }; - watchedNames_.erase(watch); - } - std::optional GetMatchEvent() - { - std::optional match; - std::lock_guard lk{ mtx_ }; - if (!matches_.empty()) { - match = std::move(matches_.back()); - matches_.pop_back(); - } - return match; - } - void SimulateSpawnEvent(p2c::win::Process process) - { - if (HandleOneEvent_(std::move(process))) { - notifyFunction_(); - } - } - private: - // functions - void HandleEvents_() - { - bool matchOccurred = false; - while (auto e = Pop()) { - if (HandleOneEvent_(std::move(*e))) { - matchOccurred = true; - } - } - if (matchOccurred) { - notifyFunction_(); - } - } - bool HandleOneEvent_(p2c::win::Process e) - { - bool matchOccurred = false; - auto moduleName = std::filesystem::path{ e.name }.filename().string(); - if (opt::get().ignoreCase) { - moduleName = moduleName - | vi::transform([](char c) {return(char)std::tolower(c); }) - | rn::to(); - } - std::lock_guard lk{ mtx_ }; - if (auto i = watchedNames_.find(moduleName); i != watchedNames_.end()) { - matches_.push_back(std::move(e)); - watchedNames_.erase(i); - matchOccurred = true; - } - return matchOccurred; - } - // data - win::com::WbemConnection conn_; - std::mutex mtx_; - std::set watchedNames_; - std::vector matches_; - std::function notifyFunction_; - std::unique_ptr pSpawnListener_; - }; - - class TargetDeathTracker - { - class Tracker - { - public: - Tracker(TargetDeathTracker* pParent, uint32_t pid) - : - pParent_{ pParent }, - processHandle_{ OpenProcess(SYNCHRONIZE, false, pid) }, - wakeEventHandle_{ CreateEventW(nullptr, true, false, nullptr) }, - thread_{ [this] {ThreadProc_(); } } - {} - ~Tracker() - { - SetEvent(wakeEventHandle_); - thread_.join(); - CloseHandle(processHandle_); - CloseHandle(wakeEventHandle_); - } - bool Died() const - { - return died_; - } - Tracker(const Tracker&) = delete; - Tracker& operator=(const Tracker&) = delete; - private: - // functions - void ThreadProc_() - { - const std::array handles{ processHandle_, wakeEventHandle_ }; - const auto wakeReason = WaitForMultipleObjects( - (DWORD)handles.size(), handles.data(), FALSE, INFINITE - ); - if (wakeReason == WAIT_OBJECT_0) { - died_ = true; - pParent_->SignalDeathEvent_(); - } - } - // data - TargetDeathTracker* pParent_; - HANDLE processHandle_; - HANDLE wakeEventHandle_; - std::atomic died_ = false; - std::thread thread_; - }; - public: - TargetDeathTracker(std::function notify) - : - notifyFunction_{ std::move(notify) } - {} - void Add(uint32_t pid) - { - std::lock_guard lk{ mtx_ }; - trackers_.emplace(pid, - std::make_unique(this, pid) - ); - } - void Remove(uint32_t pid) - { - std::lock_guard lk{ mtx_ }; - trackers_.erase(pid); - } - std::optional GetDeathEvent() - { - std::optional deathPid; - std::lock_guard lk{ mtx_ }; - if (auto i = rn::find_if(trackers_, [](const auto& t) - { return t.second->Died(); }); i != trackers_.end()) { - deathPid = i->first; - trackers_.erase(i); - } - return deathPid; - } - private: - // functions - void SignalDeathEvent_() - { - std::lock_guard lk{ mtx_ }; - notifyFunction_(); - } - // data - std::mutex mtx_; - std::unordered_map> trackers_; - std::function notifyFunction_; - }; - - - - ConsoleWaitControl::ConsoleWaitControl() - : - pProcessTracker_{ std::make_unique([this] { SignalSpawn(); }) }, - pTargetDeathTracker_{ std::make_unique([this] { SignalDeath(); }) } - { - // TODO: check return and log/throw - SetConsoleCtrlHandler(ConsoleWaitControl::CtrlHandler, TRUE); - } - ConsoleWaitControl::~ConsoleWaitControl() - { - SetConsoleCtrlHandler(ConsoleWaitControl::CtrlHandler, FALSE); - } - ConsoleWaitControl::WakeReason ConsoleWaitControl::WaitFor(std::chrono::milliseconds ms) - { - std::unique_lock lk{ mtx_ }; - cv_.wait_for(lk, ms, [this] { return EventPending(); }); - return ExtractWakeReason(); - } - ConsoleWaitControl::WakeReason ConsoleWaitControl::Wait() - { - std::unique_lock lk{ mtx_ }; - cv_.wait(lk, [this] { return EventPending(); }); - return ExtractWakeReason(); - } - void ConsoleWaitControl::AddProcessSpawnWatch(std::string processName) - { - pProcessTracker_->AddWatch(std::move(processName)); - } - void ConsoleWaitControl::RemoveProcessSpawnWatch(const std::string& processName) - { - pProcessTracker_->RemoveWatch(processName); - } - void ConsoleWaitControl::SetTimer(std::chrono::milliseconds ms) - { - if (!timerThread_.joinable()) { - timerThread_ = std::jthread{ &ConsoleWaitControl::TimerKernel, this, ms }; - } - else { - throw std::logic_error{ "Trying to set console wait control timer twice" }; - } - } - std::optional ConsoleWaitControl::GetSpawnEvent() - { - std::unique_lock lk{ mtx_ }; - return pProcessTracker_->GetMatchEvent(); - } - void ConsoleWaitControl::AddProcessDeathWatch(uint32_t pid) - { - std::unique_lock lk{ mtx_ }; - pTargetDeathTracker_->Add(pid); - } - std::optional ConsoleWaitControl::GetProcessDeathEvent() - { - std::unique_lock lk{ mtx_ }; - return pTargetDeathTracker_->GetDeathEvent(); - } - void ConsoleWaitControl::Exit() - { - exitSignal_.release(); - } - ConsoleWaitControl& ConsoleWaitControl::Get() - { - static ConsoleWaitControl singleton; - return singleton; - } - void ConsoleWaitControl::SimulateSpawnEvent(p2c::win::Process process) - { - pProcessTracker_->SimulateSpawnEvent(std::move(process)); - } - void ConsoleWaitControl::NotifyEndOfLogEvent() - { - { - std::lock_guard lk{ mtx_ }; - endOfLOgEventPending_ = true; - } - cv_.notify_all(); - } - int __stdcall ConsoleWaitControl::CtrlHandler(unsigned long type) - { - return Get().SignalCtrl(type); - } - int ConsoleWaitControl::SignalCtrl(unsigned long type) - { - { - std::lock_guard lk{ mtx_ }; - dying_ = true; - } - // unblock main thread with termination reason - cv_.notify_all(); - // wait 1s for ack from main thread - (void)exitSignal_.try_acquire_for(1s); - // returning here will kill process, either after main thread ack or 1s timeout - return TRUE; - } - void ConsoleWaitControl::SignalSpawn() - { - { - std::lock_guard lk{ mtx_ }; - processSpawnEventPending_ = true; - } - // unblock main thread with process spawn reason - cv_.notify_all(); - } - - void ConsoleWaitControl::SignalDeath() - { - { - std::lock_guard lk{ mtx_ }; - processDeathEventPending_ = true; - } - // unblock main thread with process spawn reason - cv_.notify_all(); - } - - void ConsoleWaitControl::TimerKernel(std::chrono::milliseconds ms) - { - auto st = timerThread_.get_stop_token(); - std::condition_variable_any cv; - std::unique_lock lk{ mtx_ }; - cv.wait_for(lk, st, ms, [] { return false; }); - if (!st.stop_requested()) { - timerEventPending_ = true; - lk.unlock(); - cv_.notify_all(); - } - } - ConsoleWaitControl::WakeReason ConsoleWaitControl::ExtractWakeReason() - { - if (dying_) { - return WakeReason::CtrlTermination; - } - else if (endOfLOgEventPending_) { - return WakeReason::EndOfLog; - } - else if (processSpawnEventPending_) { - processSpawnEventPending_ = false; - return WakeReason::ProcessSpawn; - } - else if (timerEventPending_) { - timerThread_.join(); - timerEventPending_ = false; - return WakeReason::TimerEvent; - } - else if (processDeathEventPending_) { - processDeathEventPending_ = false; - return WakeReason::ProcessDeath; - } - else { - return WakeReason::Timeout; - } - } - bool ConsoleWaitControl::EventPending() const - { - return dying_ || processSpawnEventPending_ || timerEventPending_ || - processDeathEventPending_ || endOfLOgEventPending_; - } -} - diff --git a/IntelPresentMon/CliCore/source/cons/ConsoleWaitControl.h b/IntelPresentMon/CliCore/source/cons/ConsoleWaitControl.h deleted file mode 100644 index 27af86b0f..000000000 --- a/IntelPresentMon/CliCore/source/cons/ConsoleWaitControl.h +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include - -namespace p2c::cli::cons -{ - class ProcessTracker; - class TargetDeathTracker; - - class ConsoleWaitControl - { - friend ProcessTracker; - friend TargetDeathTracker; - public: - // types - enum class WakeReason - { - Timeout, - ProcessSpawn, - CtrlTermination, - TimerEvent, - ProcessDeath, - EndOfLog, - }; - // functions - ConsoleWaitControl(const ConsoleWaitControl&) = delete; - ConsoleWaitControl& operator=(const ConsoleWaitControl&) = delete; - ~ConsoleWaitControl(); - WakeReason WaitFor(std::chrono::milliseconds ms); - WakeReason Wait(); - void AddProcessSpawnWatch(std::string processName); - void RemoveProcessSpawnWatch(const std::string& processName); - void SetTimer(std::chrono::milliseconds ms); - std::optional GetSpawnEvent(); - void AddProcessDeathWatch(uint32_t pid); - std::optional GetProcessDeathEvent(); - void Exit(); - static ConsoleWaitControl& Get(); - void SimulateSpawnEvent(p2c::win::Process process); - void NotifyEndOfLogEvent(); - bool EventPending() const; - private: - // functions - ConsoleWaitControl(); - // TODO: consider race condition where main thread exists normally - // just as ctrl command triggers handler (delayed wait to exit) - static int __stdcall CtrlHandler(unsigned long type); - int SignalCtrl(unsigned long type); - void SignalSpawn(); - void SignalDeath(); - void TimerKernel(std::chrono::milliseconds ms); - WakeReason ExtractWakeReason(); - // data - std::condition_variable cv_; - std::mutex mtx_; - bool dying_ = false; - bool processSpawnEventPending_ = false; - bool timerEventPending_ = false; - bool processDeathEventPending_ = false; - bool endOfLOgEventPending_ = false; - std::binary_semaphore exitSignal_{ 0 }; - std::unique_ptr pProcessTracker_; - std::unique_ptr pTargetDeathTracker_; - std::jthread timerThread_; - }; -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/cons/OptionalConsoleOut.h b/IntelPresentMon/CliCore/source/cons/OptionalConsoleOut.h deleted file mode 100644 index bbfe68cfc..000000000 --- a/IntelPresentMon/CliCore/source/cons/OptionalConsoleOut.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once -#include - - -namespace p2c::cli::cons -{ - class OptionalConsoleOut - { - public: - OptionalConsoleOut(bool enabled) - : - enabled_(enabled) - {} - template - OptionalConsoleOut& operator<<(T&& val) - { - if (enabled_) { - std::cout << std::forward(val); - } - return *this; - } - OptionalConsoleOut& operator<<(std::ostream& (*manip)(std::ostream&)) - { - if (enabled_) { - std::cout << manip; - } - return *this; - } - private: - bool enabled_; - }; -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/dat/ColumnGroups.h b/IntelPresentMon/CliCore/source/dat/ColumnGroups.h deleted file mode 100644 index 8ccccf677..000000000 --- a/IntelPresentMon/CliCore/source/dat/ColumnGroups.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#define COLUMN_GROUP_LIST \ -X_(track_gpu, "Duration of each process' GPU work performed between presents. No Win7 support.") \ -X_(track_gpu_video, "Duration of each process' video GPU work performed between presents. No Win7 support.") \ -X_(track_input, "Time of keyboard/mouse clicks that were used by each frame.") \ -X_(track_gpu_telemetry, "GPU telemetry relating to power, temperature, frequency, clock speed, etc.") \ -X_(track_vram_telemetry, "VRAM telemetry relating to power, temperature, frequency, etc.") \ -X_(track_gpu_memory, "GPU memory utilization.") \ -X_(track_gpu_fan, "GPU fanspeeds.") \ -X_(track_gpu_psu, "GPU PSU information.") \ -X_(track_perf_limit, "Flags denoting current reason for performance limitation.") \ -X_(track_cpu_telemetry, "CPU telemetry relating to power, temperature, frequency, clock speed, etc.") \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/dat/Columns.h b/IntelPresentMon/CliCore/source/dat/Columns.h deleted file mode 100644 index 626ea4976..000000000 --- a/IntelPresentMon/CliCore/source/dat/Columns.h +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once - -#define COLUMN_LIST \ -X_("Application", "", application,,, core) \ -X_("ProcessID", "", process_id,,, core) \ -X_("SwapChainAddress", "", swap_chain_address,,, core) \ -X_("Runtime", "", runtime,,, core) \ -X_("SyncInterval", "", sync_interval,,, core) \ -X_("PresentFlags", "", present_flags,,, core) \ -X_("Dropped", "", dropped,,, core) \ -X_("TimeInSeconds", "[s]", time_in_seconds,,, core) \ -X_("QPCTime", "", qpc_time,,, core) \ -X_("msInPresentAPI", "[ms]", ms_in_present_api,,, core) \ -X_("msBetweenPresents", "[ms]", ms_between_presents,,, core) \ -X_("AllowsTearing", "", allows_tearing,,, core) \ -X_("PresentMode", "", present_mode, TransformPresentMode,, core) \ -X_("msUntilRenderComplete", "[ms]", ms_until_render_complete,,, core) \ -X_("msUntilDisplayed", "[ms]", ms_until_displayed,,, core) \ -X_("msBetweenDisplayChange", "[ms]", ms_between_display_change,,, core) \ -X_("msUntilRenderStart", "[ms]", ms_until_render_start,,, track_gpu) \ -X_("msGPUActive", "[ms]", ms_gpu_active,,, track_gpu) \ -X_("msGPUVideoActive", "[ms]", ms_gpu_video_active,,, track_gpu_video) \ -X_("msSinceInput", "[ms]", ms_since_input,,, track_input) \ -X_("GPUPower", "[W]", gpu_power_w,,, track_gpu_telemetry) \ -X_("GPUSustainedPowerLimit", "[W]", gpu_sustained_power_limit_w,,, track_gpu_telemetry) \ -X_("GPUVoltage", "[V]", gpu_voltage_v,,, track_gpu_telemetry) \ -X_("GPUFrequency", "[MHz]", gpu_frequency_mhz,,, track_gpu_telemetry) \ -X_("GPUTemperature", "[C]", gpu_temperature_c,,, track_gpu_telemetry) \ -X_("GPUUtilization", "[%]", gpu_utilization,,, track_gpu_telemetry) \ -X_("GPURenderComputeUtilization", "[%]", gpu_render_compute_utilization,,, track_gpu_telemetry) \ -X_("GPUMediaUtilization", "[%]", gpu_media_utilization,,, track_gpu_telemetry) \ -X_("VRAMPower", "[W]", vram_power_w,,, track_vram_telemetry) \ -X_("VRAMVoltage", "[V]", vram_voltage_v,,, track_vram_telemetry) \ -X_("VRAMFrequency", "[MHz]", vram_frequency_mhz,,, track_vram_telemetry) \ -X_("VRAMEffectiveFrequency", "[Gbps]", vram_effective_frequency_gbs,,, track_vram_telemetry) \ -X_("VRAMTemperature", "[C]", vram_temperature_c,,, track_vram_telemetry) \ -X_("GPUMemTotalSize", "[B]", gpu_mem_total_size_b,,, track_gpu_memory) \ -X_("GPUMemUsed", "[B]", gpu_mem_used_b,,, track_gpu_memory) \ -X_("GPUMemMaxBandwidth", "[bps]", gpu_mem_max_bandwidth_bps,,, track_gpu_memory) \ -X_("GPUMemReadBandwidth", "[bps]", gpu_mem_read_bandwidth_bps,,, track_gpu_memory) \ -X_("GPUMemWriteBandwidth", "[bps]", gpu_mem_write_bandwidth_bps,,, track_gpu_memory) \ -X_("GPUFanSpeed0", "[rpm]", fan_speed_rpm,, [0], track_gpu_fan) \ -X_("GPUFanSpeed1", "[rpm]", fan_speed_rpm,, [1], track_gpu_fan) \ -X_("GPUFanSpeed2", "[rpm]", fan_speed_rpm,, [2], track_gpu_fan) \ -X_("GPUFanSpeed3", "[rpm]", fan_speed_rpm,, [3], track_gpu_fan) \ -X_("GPUFanSpeed4", "[rpm]", fan_speed_rpm,, [4], track_gpu_fan) \ -X_("PSUType0", "", psu_type,, [0], track_gpu_psu) \ -X_("PSUType1", "", psu_type,, [1], track_gpu_psu) \ -X_("PSUType2", "", psu_type,, [2], track_gpu_psu) \ -X_("PSUType3", "", psu_type,, [3], track_gpu_psu) \ -X_("PSUType4", "", psu_type,, [4], track_gpu_psu) \ -X_("PSUPower0", "[W]", psu_power,, [0], track_gpu_psu) \ -X_("PSUPower1", "[W]", psu_power,, [1], track_gpu_psu) \ -X_("PSUPower2", "[W]", psu_power,, [2], track_gpu_psu) \ -X_("PSUPower3", "[W]", psu_power,, [3], track_gpu_psu) \ -X_("PSUPower4", "[W]", psu_power,, [4], track_gpu_psu) \ -X_("PSUVoltage0", "[V]", psu_voltage,, [0], track_gpu_psu) \ -X_("PSUVoltage1", "[V]", psu_voltage,, [1], track_gpu_psu) \ -X_("PSUVoltage2", "[V]", psu_voltage,, [2], track_gpu_psu) \ -X_("PSUVoltage3", "[V]", psu_voltage,, [3], track_gpu_psu) \ -X_("PSUVoltage4", "[V]", psu_voltage,, [4], track_gpu_psu) \ -X_("GPUPowerLimited", "", gpu_power_limited,,, track_perf_limit) \ -X_("GPUTemperatureLimited", "", gpu_temperature_limited,,, track_perf_limit) \ -X_("GPUCurrentLimited", "", gpu_current_limited,,, track_perf_limit) \ -X_("GPUVoltageLimited", "", gpu_voltage_limited,,, track_perf_limit) \ -X_("GPUUtilizationLimited", "", gpu_utilization_limited,,, track_perf_limit) \ -X_("VRAMPowerLimited", "", vram_power_limited,,, track_perf_limit) \ -X_("VRAMTemperatureLimited", "", vram_temperature_limited,,, track_perf_limit) \ -X_("VRAMCurrentLimited", "", vram_current_limited,,, track_perf_limit) \ -X_("VRAMVoltageLimited", "", vram_voltage_limited,,, track_perf_limit) \ -X_("VRAMUtilizationLimited", "", vram_utilization_limited,,, track_perf_limit) \ -X_("CPUUtilization", "[%]", cpu_utilization,,, track_cpu_telemetry) \ -X_("CPUPower", "[W]", cpu_power_w,,, track_cpu_telemetry) \ -X_("CPUPowerLimit", "[W]", cpu_power_limit_w,,, track_cpu_telemetry) \ -X_("CPUTemperature", "[C]", cpu_temperature_c,,, track_cpu_telemetry) \ -X_("CPUFrequency", "[GHz]", cpu_frequency,,, track_cpu_telemetry) \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/dat/CsvWriter.cpp b/IntelPresentMon/CliCore/source/dat/CsvWriter.cpp deleted file mode 100644 index e03f75d2a..000000000 --- a/IntelPresentMon/CliCore/source/dat/CsvWriter.cpp +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#include "CsvWriter.h" -#include -#include -#include -#include -#include "Columns.h" -#include "ColumnGroups.h" - - -namespace p2c::cli::dat -{ - struct GroupFlags - { - bool core = true; -#define X_(name, description) bool name; - COLUMN_GROUP_LIST -#undef X_ - }; - - CsvWriter::~CsvWriter() = default; - CsvWriter::CsvWriter(CsvWriter&&) = default; - CsvWriter& CsvWriter::operator=(CsvWriter&&) = default; - - CsvWriter::CsvWriter(std::string path, const std::vector& groups, bool writeStdout) - : - writeStdout_{ writeStdout } - { - if (!path.empty()) { - file_.open(path, std::ios::trunc); - if (!file_) { - throw std::runtime_error{ "Failed to open file for writing: " + path }; - } - } - - // setup group flags - std::set groupSet{ groups.begin(), groups.end() }; - pGroupFlags_ = std::make_unique(); -#define X_(name, description) pGroupFlags_->name = groupSet.contains("all") || groupSet.contains(#name); - COLUMN_GROUP_LIST -#undef X_ - - int col = 0; - // write header -#define X_(name, unit, symbol, transform, index, group) if (pGroupFlags_->group) { if (col++) buffer_ << ','; buffer_ << name; } - COLUMN_LIST -#undef X_ - buffer_ << "\n"; - - Flush(); - } - - void CsvWriter::Process(const PM_FRAME_DATA& frame) - { - const auto TransformPresentMode = [](PM_PRESENT_MODE mode) { - return infra::util::ToNarrow(pmon::PresentModeToString(pmon::ConvertPresentMode(mode))); - }; - - int col = 0; -#define X_(name, unit, symbol, transform, index, group) if (pGroupFlags_->group) { if (col++) buffer_ << ','; buffer_ << transform(frame.symbol index); } - COLUMN_LIST -#undef X_ - buffer_ << "\n"; - - Flush(); - } - - void CsvWriter::Flush() - { - const auto string = buffer_.GetString(); - if (writeStdout_) { - std::cout << string; - } - if (file_) { - file_ << string; - } - buffer_.Clear(); - } -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/dat/CsvWriter.h b/IntelPresentMon/CliCore/source/dat/CsvWriter.h deleted file mode 100644 index 5868716a9..000000000 --- a/IntelPresentMon/CliCore/source/dat/CsvWriter.h +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once -#include -#include -#include -#include -#include "FrameSink.h" -#include - -namespace p2c::cli::dat -{ - struct GroupFlags; - - template - concept PresentMonOptionalStrict_ = - std::same_as || - std::same_as || - std::same_as || - std::same_as; - - template - concept PresentMonOptional = PresentMonOptionalStrict_>; - - class CsvWriter : public FrameSink - { - public: - CsvWriter(std::string path, const std::vector& groups, bool writeStdout = false); - CsvWriter(const CsvWriter&) = delete; - CsvWriter(CsvWriter&&); - CsvWriter& operator=(const CsvWriter&) = delete; - CsvWriter& operator=(CsvWriter&&); - ~CsvWriter(); - void Process(const struct PM_FRAME_DATA& frame) override; - private: - // types - class Buffer_ - { - public: - template - Buffer_& operator<<(const T& input) - { - if (!input.valid) { - stream_ << "NA"; - } - else { - stream_ << input.data; - } - return *this; - } - template - Buffer_& operator<<(T&& input) - { - stream_ << std::forward(input); - return *this; - } - std::string GetString() const - { - return stream_.str(); - } - void Clear() - { - stream_.str({}); - stream_.clear(); - } - private: - std::stringstream stream_; - }; - // functions - void Flush(); - // data - std::unique_ptr pGroupFlags_; - Buffer_ buffer_; - std::ofstream file_; - bool writeStdout_; - }; -} diff --git a/IntelPresentMon/CliCore/source/dat/FrameDemultiplexer.cpp b/IntelPresentMon/CliCore/source/dat/FrameDemultiplexer.cpp deleted file mode 100644 index 191a8a7ec..000000000 --- a/IntelPresentMon/CliCore/source/dat/FrameDemultiplexer.cpp +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#include "FrameDemultiplexer.h" -#include -#include -#include -#include "MakeCsvName.h" - -namespace p2c::cli::dat -{ - FrameDemultiplexer::FrameDemultiplexer(std::vector groups, std::optional customFileName) - : - customFileName_{ std::move(customFileName) }, - groups_{ std::move(groups) } - {} - void FrameDemultiplexer::Process(const PM_FRAME_DATA& frame) - { - if (auto i = procs_.find(frame.process_id); i != procs_.end()) { - i->second->Process(frame); - } - else { - auto processName = std::filesystem::path{ frame.application }.filename().string(); - auto fileName = MakeCsvName(false, frame.process_id, std::move(processName), customFileName_); - auto pWriter = std::make_shared(std::move(fileName), groups_); - pWriter->Process(frame); - procs_.emplace(frame.process_id, std::move(pWriter)); - } - } -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/dat/FrameDemultiplexer.h b/IntelPresentMon/CliCore/source/dat/FrameDemultiplexer.h deleted file mode 100644 index d1308fce2..000000000 --- a/IntelPresentMon/CliCore/source/dat/FrameDemultiplexer.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once -#include "FrameSink.h" -#include "CsvWriter.h" -#include -#include -#include -#include - -namespace p2c::cli::dat -{ - // TODO: add ability to configure file names better - class FrameDemultiplexer : public FrameSink - { - public: - FrameDemultiplexer(std::vector groups, std::optional customFileName); - void Process(const PM_FRAME_DATA& frame) override; - private: - std::unordered_map> procs_; - std::vector groups_; - std::optional customFileName_; - }; -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/dat/FrameFilterPump.cpp b/IntelPresentMon/CliCore/source/dat/FrameFilterPump.cpp deleted file mode 100644 index d4bda53c8..000000000 --- a/IntelPresentMon/CliCore/source/dat/FrameFilterPump.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "FrameFilterPump.h" -#include -#include -#include - -namespace p2c::cli::dat -{ - namespace rn = std::ranges; - namespace vi = rn::views; - - FrameFilterPump::FrameFilterPump( - std::shared_ptr pSource, - std::shared_ptr pSink, - const std::vector& excludes, - const std::vector& includes, - bool excludeDropped, - bool ignoreCase) - : - pSource_{ std::move(pSource) }, - pSink_{ std::move(pSink) }, - excludes_{ excludes.begin(), excludes.end() }, - includes_{ includes.begin(), includes.end() }, - excludeDropped_{ excludeDropped }, - ignoreCase_{ ignoreCase } - {} - void FrameFilterPump::AddInclude(uint32_t pid) - { - includes_.insert(pid); - } - void FrameFilterPump::Process(double timestamp) - { - for (auto& f : pSource_->Pull(timestamp)) { - // skip rows for applications in the exclude list - if (!excludes_.empty()) { - auto appName = std::filesystem::path{ f.application }.filename().string(); - if (ignoreCase_) { - appName = appName - | vi::transform([](char c) {return(char)std::tolower(c); }) - | rn::to(); - } - if (excludes_.contains(appName)) { - continue; - } - } - // skip rows for pid NOT in the include list - if (!includes_.empty()) { - if (!includes_.contains(f.process_id)) { - continue; - } - } - // skip dropped frames - if (excludeDropped_) { - if (f.dropped) { - continue; - } - } - pSink_->Process(f); - } - } - uint32_t FrameFilterPump::GetPid() const - { - return pSource_->GetPid(); - } -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/dat/FrameFilterPump.h b/IntelPresentMon/CliCore/source/dat/FrameFilterPump.h deleted file mode 100644 index d73a8bab6..000000000 --- a/IntelPresentMon/CliCore/source/dat/FrameFilterPump.h +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once -#include "FrameSink.h" -#include -#include -#include -#include - -namespace p2c::cli::pmon -{ - class FrameDataStream; -} - -namespace p2c::cli::dat -{ - class FrameFilterPump - { - public: - FrameFilterPump( - std::shared_ptr pSource, - std::shared_ptr pSink, - const std::vector& excludes, - const std::vector& includes, - bool excludeDropped, - bool ignoreCase); - void AddInclude(uint32_t pid); - void Process(double timestamp); - uint32_t GetPid() const; - private: - std::shared_ptr pSource_; - std::shared_ptr pSink_; - // TODO: add exclude-by-pid to speed up filtering - std::unordered_set excludes_; - // TODO: use includes for an early-pass in non-whitelist situations - std::unordered_set includes_; - bool ignoreCase_; - bool excludeDropped_; - }; -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/dat/FrameSink.h b/IntelPresentMon/CliCore/source/dat/FrameSink.h deleted file mode 100644 index 583f84384..000000000 --- a/IntelPresentMon/CliCore/source/dat/FrameSink.h +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once - -struct PM_FRAME_DATA; - -namespace p2c::cli::dat -{ - class FrameSink - { - public: - virtual void Process(const PM_FRAME_DATA&) = 0; - }; -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/dat/MakeCsvName.cpp b/IntelPresentMon/CliCore/source/dat/MakeCsvName.cpp deleted file mode 100644 index f183f6856..000000000 --- a/IntelPresentMon/CliCore/source/dat/MakeCsvName.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "MakeCsvName.h" -#include -#include - -using namespace std::string_literals; - -namespace p2c::cli::dat -{ - std::string MakeCsvName(bool noCsv, std::optional pid, std::optional processName, std::optional customName) - { - if (noCsv) { - return {}; - } - std::string customPrefix; - std::string customExtension; - if (customName) { - std::filesystem::path customPath{ *customName }; - if (customPath.has_parent_path()) { - customPrefix += customPath.parent_path().string() + "\\"; - } - if (customPath.has_stem()) { - customPrefix += customPath.stem().string(); - } - else { - customPrefix += "PresentMon"; - } - if (customPath.has_extension()) { - customExtension = customPath.extension().string(); - } - else { - customExtension = ".csv"s; - } - } - const std::chrono::zoned_time timestamp{ - std::chrono::current_zone(), - std::chrono::system_clock::now() - }; - std::stringstream buf; - if (customName) { - buf << customPrefix; - } - else { - buf << "PresentMon"; - } - if (processName) { - buf << "-" << *processName; - } - if (processName && pid) { - buf << std::format("[{}]", *pid); - } - else if (pid) { - buf << std::format("-[{}]", *pid); - } - buf << std::format("-{0:%y}{0:%m}{0:%d}-{0:%H}{0:%M}{0:%OS}", timestamp); - if (customName) { - buf << customExtension; - } - else { - buf << ".csv"; - } - return buf.str(); - } -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/dat/MakeCsvName.h b/IntelPresentMon/CliCore/source/dat/MakeCsvName.h deleted file mode 100644 index f52e64eca..000000000 --- a/IntelPresentMon/CliCore/source/dat/MakeCsvName.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once -#include -#include - -namespace p2c::cli::dat -{ - std::string MakeCsvName(bool noCsv, std::optional pid, std::optional processName, std::optional customName); -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/opt/Framework.cpp b/IntelPresentMon/CliCore/source/opt/Framework.cpp deleted file mode 100644 index 745ec8e43..000000000 --- a/IntelPresentMon/CliCore/source/opt/Framework.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "Framework.h" -#include - - -namespace p2c::cli::opt::impl -{ - std::optional AppBase_::app_; - std::optional AppBase_::line_; - const char* AppBase_::group_ = nullptr; - - void AppBase_::InitInternal_(int argc, char** argv) - { - assert(!line_); - line_.emplace(argc, argv); - assert(!app_); - app_.emplace(); - } - - CLI::App& AppBase_::GetApp_() - { - assert(app_); - return *app_; - } - - void AppBase_::Finalize_(const OptionStructBase_& options) - { - auto& app = GetApp_(); - assert(!app.parsed()); - - app.description(options.GetDescription()); - app.name(options.GetName()); - try { - app.parse(line_->argc, line_->argv); - } - catch (const CLI::ParseError& e) { - app.exit(e); - throw; - }; - } - - - Flag::Flag(std::string names, std::string description) - { - pOption_ = AppBase_::GetApp_().add_flag(std::move(names), value_, std::move(description)); - if (AppBase_::group_) { - pOption_->group(AppBase_::group_); - } - } - - Flag::operator bool() const - { - return value_; - } - - CLI::Option* Flag::opt() const { return pOption_; } - - Group::Group(const char* name) - { - AppBase_::group_ = name; - } -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/opt/Framework.h b/IntelPresentMon/CliCore/source/opt/Framework.h deleted file mode 100644 index 5543ecb90..000000000 --- a/IntelPresentMon/CliCore/source/opt/Framework.h +++ /dev/null @@ -1,147 +0,0 @@ -#pragma once -#include -#include -#include -#include - -namespace p2c::cli::opt::impl -{ - struct OptionStructBase_ - { - protected: - friend class AppBase_; - virtual std::string GetDescription() const = 0; - virtual std::string GetName() const = 0; - }; - - class AppBase_ - { - template - friend class Option; - friend class Flag; - friend class Group; - protected: - AppBase_() = default; - static void InitInternal_(int argc, char** argv); - static CLI::App& GetApp_(); - static void Finalize_(const OptionStructBase_&); - private: - // types - struct CommandLine - { - int argc; - char** argv; - }; - // data - static std::optional app_; - static std::optional line_; - static const char* group_; - }; - - template concept OptionStructable = std::derived_from; - - template - class App : public AppBase_ - { - public: - static void Init(int argc, char** argv) - { - InitInternal_(argc, argv); - GetOptions(); - } - static OptionStruct& GetOptions() - { - static App singleton; - return singleton.options_; - } - static bool CheckOptionPresence(const std::string& key) - { - const auto pOption = GetApp_()[key]; - return pOption && pOption->count() > 0; - } - private: - App() { Finalize_(options_); } - OptionStruct options_; - }; - - class Group - { - public: - Group(const char* name); - private: - OptionStructBase_* optionStruct_ = nullptr; - }; - - template - class Option - { - public: - template - Option(std::string names, std::string description, U&& customizer) - { - pOption_ = AppBase_::GetApp_().add_option(std::move(names), value_, std::move(description)); - if (AppBase_::group_) { - pOption_->group(AppBase_::group_); - } - // if the customizer returns a T, it is the default value - using R = std::invoke_result_t; - if constexpr (std::is_same_v) { - value_ = customizer(pOption_); - } - else { - customizer(pOption_); - } - } - Option(std::string names, std::string description) - { - pOption_ = AppBase_::GetApp_().add_option(std::move(names), value_, std::move(description)); - if (AppBase_::group_) { - pOption_->group(AppBase_::group_); - } - } - operator bool() const - { - return (bool)*pOption_; - } - bool operator!() const - { - return !bool(*this); - } - const T& operator*() const - { - return value_; - } - CLI::Option* opt() const { return pOption_; } - private: - T value_{}; - CLI::Option* pOption_ = nullptr; - }; - - class Flag - { - public: - template - Flag(std::string names, std::string description, U&& customizer) - { - pOption_ = AppBase_::GetApp_().add_flag(std::move(names), value_, std::move(description)); - if (AppBase_::group_) { - pOption_->group(AppBase_::group_); - } - // if the customizer returns a bool, it is the default value - using R = std::invoke_result_t; - if constexpr (std::is_same_v) { - value_ = customizer(pOption_); - } - else { - customizer(pOption_); - } - } - Flag(std::string names, std::string description); - operator bool() const; - CLI::Option* opt() const; - private: - // data - bool value_ = false; - CLI::Option* pOption_ = nullptr; - }; -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/opt/Options.cpp b/IntelPresentMon/CliCore/source/opt/Options.cpp deleted file mode 100644 index 92f9fa8ac..000000000 --- a/IntelPresentMon/CliCore/source/opt/Options.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once -#define PMCLI_OPTS_CPP -#include "Options.h" -#include - -namespace p2c::cli::opt::impl -{ - std::vector OptionsStruct::GetColumnGroups() const - { - std::vector groups{ -#define X_(name, desc) #name, - COLUMN_GROUP_LIST -#undef X_ - }; - std::erase_if(groups, [](const std::string& g) { - return !App::CheckOptionPresence("--" + g); - }); - return groups; - } -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/opt/Options.h b/IntelPresentMon/CliCore/source/opt/Options.h deleted file mode 100644 index 0ac97eab3..000000000 --- a/IntelPresentMon/CliCore/source/opt/Options.h +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once -#include "Framework.h" -#include -#include -#include - -namespace p2c::cli::opt -{ - namespace impl - { - struct OptionsStruct : public OptionStructBase_ - { - // add options and switches here to augment the CLI - private: Group gGeneric{ "Options" }; public: - Flag log{ "-l, --log", "Enable logging (off by default)" }; - Option logLevel{ "--log_level", "Level to log at from 4 (most logging) to 0 (least logging, default)", [this](CLI::Option* p) { - p->needs(log.opt()); } }; - Option logPath{ "--log_path", "Directory to write logs to (defaults to working dir)", [this](CLI::Option* p) { - p->needs(log.opt()); } }; - - private: Group gTarget{ "Capture Target Options" }; public: - Flag captureAll{ "-A,--captureall", "Capture all processes presenting on system. Default behavior." }; - Option> name{ "--process_name", "Name(s) of process(es) to capture" }; - Option> excludeName{ "--exclude", "Name(s) of process(es) to exclude", [this](CLI::Option* p) { - p->excludes(name.opt()); } }; - Option> pid{ "--process_id", "ID(s) of process(es) to capture", [this](CLI::Option* p) { - p->check(CLI::PositiveNumber)->excludes(captureAll.opt(), name.opt(), excludeName.opt()); }}; - Option etlFile{ "--etl_file", "Source frame data from an externally-captured ETL file instead of live ETW events" }; - Flag ignoreCase{ "-i,--ignore_case", "Ignore case when matching a process name" }; - - private: Group gOutput{ "Output Options" }; public: - Option outputFile{ "-o,--output_file", "Path, root name, and extension used for CSV file(s)" }; - Flag outputStdout{ "-s,--output_stdout", "Write frame data to stdout instead of status messages" }; - Flag multiCsv{ "-m,--multi_csv", "Write frame data from each process to a separate CSV file", [this](CLI::Option* p) { - p->excludes(outputStdout.opt()); } }; - Flag noCsv{ "-v,--no_csv", "Disable CSV file output", [this](CLI::Option* p) { - p->needs(outputStdout.opt()); } }; - - private: Group gRecording{ "Recording Options" }; public: - Option startAfter{ "--delay", "Time in seconds from launch to start of capture" }; - Option captureFor{ "--timed", "Time in seconds from start of capture to exit" }; - Flag excludeDropped{ "-d,--exclude_dropped", "Exclude dropped frames from capture" }; - - private: Group gColumns{ "CSV Column Options" }; public: -#define X_(name, desc) Flag name{ "--" #name, desc }; - COLUMN_GROUP_LIST -#undef X_ - - private: Group gAdapters{ "Graphics Adapter Options" }; public: - Flag listAdapters{ "-a,--list_adapters", "List adaptors available for selection as telemetry source" }; - Option adapter{ "--adapter", "Index of adapter to use as telemetry source", [this](CLI::Option* p) { - p->check(CLI::NonNegativeNumber); } }; - - private: Group gHidden{ "" }; public: - Option telemetryPeriod{ "--telemetry_period", "Period that the service uses to poll hardware telemetry", [this](CLI::Option* p) { - p->check(CLI::PositiveNumber); } }; - Option pollPeriod{ "--poll_period", "Period that this client uses to poll the service for frame data", [this](CLI::Option* p) { - p->check(CLI::PositiveNumber); } }; - - // used for getting vector of enabled CSV column groups - std::vector GetColumnGroups() const; - - protected: - // edit application name and description here - std::string GetName() const override - { return "PresentMonCli"; } - std::string GetDescription() const override - { return "Command line interface for capturing frame presentation data using the PresentMon service."; } - }; - } - - inline void init(int argc, char** argv) { impl::App::Init(argc, argv); } - inline auto& get() { return impl::App::GetOptions(); } -} - -#ifndef PMCLI_OPTS_CPP -#undef COLUMN_GROUP_LIST -#endif \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/pmon/AdapterInfo.h b/IntelPresentMon/CliCore/source/pmon/AdapterInfo.h deleted file mode 100644 index b89578bf4..000000000 --- a/IntelPresentMon/CliCore/source/pmon/AdapterInfo.h +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once -#include -#include - -namespace p2c::cli::pmon -{ - struct AdapterInfo - { - uint32_t id; - std::string vendor; - std::string name; - }; -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/pmon/Client.cpp b/IntelPresentMon/CliCore/source/pmon/Client.cpp deleted file mode 100644 index 6a6eadfde..000000000 --- a/IntelPresentMon/CliCore/source/pmon/Client.cpp +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#include "Client.h" -#include -#include -#include "stream/LiveFrameDataStream.h" -#include "stream/LoggedFrameDataStream.h" - -namespace p2c::cli::pmon -{ - Client::Client(uint32_t telemetrySamplePeriodMs) - { - if (auto sta = pmInitialize(nullptr); sta != PM_STATUS::PM_STATUS_SUCCESS) - { - p2clog.note(L"could not init pmon").code(sta).commit(); - } - SetGpuTelemetryPeriod(telemetrySamplePeriodMs); - } - Client::~Client() - { - if (auto sta = pmShutdown(); sta != PM_STATUS::PM_STATUS_SUCCESS) { - p2clog.warn(L"could not shutdown pmon").code(sta).commit(); - } - } - std::shared_ptr Client::OpenStream(uint32_t pid) - { - // if process exists in our map - if (auto i = streams_.find(pid); i != streams_.end()) { - // check if fresh - if (auto pStream = i->second.lock()) { - return pStream; - } - else { - // if stale, replace stale entry with new proc stream and return - pStream = MakeStream(pid); - i->second = pStream; - return pStream; - } - } - else { - auto pStream = MakeStream(pid); - streams_[pid] = pStream; - return pStream; - } - } - void Client::SetGpuTelemetryPeriod(uint32_t period) - { - if (auto sta = pmSetGPUTelemetryPeriod(period); sta != PM_STATUS::PM_STATUS_SUCCESS) - { - p2clog.warn(std::format(L"could not set gpu telemetry sample period to {}", period)).code(sta).commit(); - } - else - { - telemetrySamplePeriod_ = period; - } - } - uint32_t Client::GetGpuTelemetryPeriod() - { - return telemetrySamplePeriod_; - } - std::vector Client::EnumerateAdapters() const - { - uint32_t count = 0; - if (auto sta = pmEnumerateAdapters(nullptr, &count); sta != PM_STATUS::PM_STATUS_SUCCESS) - { - p2clog.note(L"could not query adapter count").code(sta).commit(); - } - std::vector buffer{ size_t(count) }; - if (auto sta = pmEnumerateAdapters(buffer.data(), &count); sta != PM_STATUS::PM_STATUS_SUCCESS) - { - p2clog.note(L"could not enumerate adapters").code(sta).commit(); - } - std::vector infos; - for (const auto& info : buffer) - { - const auto GetVendorName = [vendor = info.vendor] { - using namespace std::string_literals; - switch (vendor) { - case PM_GPU_VENDOR::PM_GPU_VENDOR_AMD: return "AMD"s; - case PM_GPU_VENDOR::PM_GPU_VENDOR_INTEL: return "Intel"s; - case PM_GPU_VENDOR::PM_GPU_VENDOR_NVIDIA: return "Nvidia"s; - default: return "Unknown"s; - } - }; - infos.push_back(AdapterInfo{ - .id = info.id, - .vendor = GetVendorName(), - .name = info.name, - }); - } - return infos; - } - void Client::SetAdapter(uint32_t id) - { - if (auto sta = pmSetActiveAdapter(id); sta != PM_STATUS::PM_STATUS_SUCCESS) - { - p2clog.note(L"could not set active adapter").code(sta).nox().commit(); - } - } - std::optional Client::GetSelectedAdapter() const - { - return selectedAdapter_; - } - - - - LiveClient::LiveClient(uint32_t telemetrySampleRateMs) - : - Client{ telemetrySampleRateMs } - {} - std::shared_ptr LiveClient::MakeStream(uint32_t pid) - { - return std::make_shared(pid); - } - - - - LoggedClient::LoggedClient(std::string filePath, cons::ConsoleWaitControl& waitControl, uint32_t telemetrySampleRateMs) - : - Client{ telemetrySampleRateMs }, - pLoggedState_{ std::make_shared(std::move(filePath), &waitControl) } - {} - std::shared_ptr LoggedClient::MakeStream(uint32_t pid) - { - return std::make_shared(pid, pLoggedState_); - } -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/pmon/Client.h b/IntelPresentMon/CliCore/source/pmon/Client.h deleted file mode 100644 index c3dff7a67..000000000 --- a/IntelPresentMon/CliCore/source/pmon/Client.h +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once -#include -#include -#include -#include -#include "AdapterInfo.h" -// adapters corresponding to pmapi structs/functions -#include "FrameDataStream.h" - -namespace p2c::cli::cons -{ - class ConsoleWaitControl; -} - -namespace p2c::cli::pmon -{ - namespace stream - { - class LoggedFrameDataState; - } - - class Client - { - public: - // types - using AdapterInfo = pmon::AdapterInfo; - // functions - Client(const Client&) = delete; - Client& operator=(const Client&) = delete; - virtual ~Client(); - std::shared_ptr OpenStream(uint32_t pid); - void SetGpuTelemetryPeriod(uint32_t period); - uint32_t GetGpuTelemetryPeriod(); - std::vector EnumerateAdapters() const; - void SetAdapter(uint32_t id); - std::optional GetSelectedAdapter() const; - protected: - Client(uint32_t telemetrySampleRateMs); - virtual std::shared_ptr MakeStream(uint32_t pid) = 0; - private: - std::unordered_map> streams_; - uint32_t telemetrySamplePeriod_ = 0; - std::optional selectedAdapter_; - }; - - class LiveClient : public Client - { - public: - LiveClient(uint32_t telemetrySampleRateMs = 16); - protected: - std::shared_ptr MakeStream(uint32_t pid) override; - }; - - class LoggedClient : public Client - { - public: - LoggedClient(std::string filePath, cons::ConsoleWaitControl& waitControl, uint32_t telemetrySampleRateMs = 16); - protected: - std::shared_ptr MakeStream(uint32_t pid) override; - private: - std::shared_ptr pLoggedState_; - }; -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/pmon/FrameDataStream.h b/IntelPresentMon/CliCore/source/pmon/FrameDataStream.h deleted file mode 100644 index 760df3e9e..000000000 --- a/IntelPresentMon/CliCore/source/pmon/FrameDataStream.h +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once -#include -#include - -namespace p2c::cli::pmon -{ - class FrameDataStream - { - public: - using Struct = PM_FRAME_DATA; - virtual std::span Pull(double timestamp) = 0; - virtual uint32_t GetPid() const = 0; - virtual ~FrameDataStream() = default; - }; -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/pmon/PresentMode.cpp b/IntelPresentMon/CliCore/source/pmon/PresentMode.cpp deleted file mode 100644 index acb80f6ad..000000000 --- a/IntelPresentMon/CliCore/source/pmon/PresentMode.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#define KEEP_PM_PRESENT_MODE_X_LIST -#include "PresentMode.h" - -namespace p2c::cli::pmon -{ - PresentMode ConvertPresentMode(PM_PRESENT_MODE mode) - { - return PresentMode(mode); - } - - std::wstring PresentModeToString(PresentMode mode) - { - switch (mode) - { -#define X_(a, b, c) case PresentMode::b: return c; - PM_PRESENT_MODE_X_LIST -#undef X_ - default: return L"Invalid"; - } - } -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/pmon/PresentMode.h b/IntelPresentMon/CliCore/source/pmon/PresentMode.h deleted file mode 100644 index 0ee7c1242..000000000 --- a/IntelPresentMon/CliCore/source/pmon/PresentMode.h +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once -#include -#include -#include - -#define PM_PRESENT_MODE_X_LIST \ - X_(PM_PRESENT_MODE_HARDWARE_LEGACY_FLIP, HardwareLegacyFlip, L"HW Legacy Flip") \ - X_(PM_PRESENT_MODE_HARDWARE_LEGACY_COPY_TO_FRONT_BUFFER, HardwareLegacyCopyToFrontBuffer, L"HW Legacy Copy To Front Buffer") \ - X_(PM_PRESENT_MODE_HARDWARE_INDEPENDENT_FLIP, HardwareIndependentFlip, L"HW Independent Flip") \ - X_(PM_PRESENT_MODE_COMPOSED_FLIP, ComposedFlip, L"Composed Flip") \ - X_(PM_PRESENT_MODE_HARDWARE_COMPOSED_INDEPENDENT_FLIP, HardwareComposedIndependentFlip, L"HW Composed Independent Flip") \ - X_(PM_PRESENT_MODE_COMPOSED_COPY_WITH_GPU_GDI, ComposedCopyWithGpuGdi, L"Composed Copy With GPU GDI") \ - X_(PM_PRESENT_MODE_COMPOSED_COPY_WITH_CPU_GDI, ComposedCopyWithCpuGdi, L"Composed Copy With CPU GDI") \ - X_(PM_PRESENT_MODE_UNKNOWN, Unknown, L"Unknown") - -namespace p2c::cli::pmon -{ - enum class PresentMode : uint32_t - { -#define X_(a, b, c) b = uint32_t(a), - PM_PRESENT_MODE_X_LIST -#undef X_ - }; - - PresentMode ConvertPresentMode(PM_PRESENT_MODE mode); - std::wstring PresentModeToString(PresentMode mode); -} - -#ifndef KEEP_PM_PRESENT_MODE_X_LIST -#undef PM_PRESENT_MODE_X_LIST -#endif \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/pmon/stream/LiveFrameDataStream.cpp b/IntelPresentMon/CliCore/source/pmon/stream/LiveFrameDataStream.cpp deleted file mode 100644 index 82ab3ceb2..000000000 --- a/IntelPresentMon/CliCore/source/pmon/stream/LiveFrameDataStream.cpp +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#include "LiveFrameDataStream.h" -#include -#include -#include - -namespace p2c::cli::pmon::stream -{ - LiveFrameDataStream::LiveFrameDataStream(uint32_t pid) - : - pid_(pid), - cache_(initialCacheSize_) - { - if (auto sta = pmStartStream(pid_); sta != PM_STATUS::PM_STATUS_SUCCESS) { - p2clog.note(std::format(L"could not start stream for pid {}", pid_)).code(sta).commit(); - } - p2clog.info(std::format(L"started pmon stream for pid {}", pid_)).commit(); - } - - LiveFrameDataStream::~LiveFrameDataStream() - { - if (auto sta = pmStopStream(pid_); sta != PM_STATUS::PM_STATUS_SUCCESS) { - p2clog.warn(std::format(L"could not stop stream for pid {}", pid_)).code(sta).commit(); - } - p2clog.info(std::format(L"stopped pmon stream for pid {}", pid_)).commit(); - } - - std::span LiveFrameDataStream::Pull(double timestamp) - { - if (!cacheTimestamp_ || cacheTimestamp_ != timestamp) - { - // in: max writeable buffer size, out: actual num frames written - auto frameCountInOut = (uint32_t)initialCacheSize_; - // cache buffer might have shrunk down real small, reset to initial size - cache_.resize(initialCacheSize_); - // where in vector to begin writing - uint32_t writePosition = 0; - while (true) { - if (auto sta = pmGetFrameData(pid_, &frameCountInOut, cache_.data() + writePosition); - sta == PM_STATUS::PM_STATUS_NO_DATA) { - // "shrink to fit" - cache_.resize(size_t(writePosition)); - break; - } - else if (sta != PM_STATUS::PM_STATUS_SUCCESS) { - p2clog.warn(std::format(L"failed to get raw frame data with error [{}]", pid_)).code(sta).commit(); - cache_.clear(); - break; - } - // case where there *might* be more data - // if the api writes to all available elements - // means there might have been exactly the needed number - // of elements in the buffer, or that there are more remaining - if (size_t(writePosition) + size_t(frameCountInOut) >= std::size(cache_)) { - // if we're full, we should have filled up to exact size of buffer - CORE_ASSERT(size_t(writePosition) + size_t(frameCountInOut) == std::size(cache_)); - // next frames write start just after end of current cache - writePosition = (uint32_t)std::size(cache_); - // double size of cache to accomodate more entries - cache_.resize(cache_.size() * 2); - // available space is total size of cache minus size already written - frameCountInOut = (uint32_t)std::size(cache_) - writePosition; - } - else { - // "shrink to fit" - cache_.resize(size_t(writePosition) + size_t(frameCountInOut)); - break; - } - } - cacheTimestamp_ = timestamp; - } - return cache_; - } - - uint32_t LiveFrameDataStream::GetPid() const - { - return pid_; - } -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/pmon/stream/LiveFrameDataStream.h b/IntelPresentMon/CliCore/source/pmon/stream/LiveFrameDataStream.h deleted file mode 100644 index 581a003af..000000000 --- a/IntelPresentMon/CliCore/source/pmon/stream/LiveFrameDataStream.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once -#include "../FrameDataStream.h" -#include -#include -#include -#include - -namespace p2c::cli::pmon -{ - class ProcessStream; -} - -namespace p2c::cli::pmon::stream -{ - class LiveFrameDataStream : public FrameDataStream - { - public: - LiveFrameDataStream(uint32_t pid); - std::span Pull(double timestamp) override; - uint32_t GetPid() const override; - LiveFrameDataStream(const LiveFrameDataStream&) = delete; - LiveFrameDataStream& operator=(const LiveFrameDataStream&) = delete; - ~LiveFrameDataStream() override; - private: - static constexpr size_t initialCacheSize_ = 120; - uint32_t pid_; - std::optional cacheTimestamp_; - std::vector cache_; - }; -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/pmon/stream/LoggedFrameDataState.cpp b/IntelPresentMon/CliCore/source/pmon/stream/LoggedFrameDataState.cpp deleted file mode 100644 index 96d4288a7..000000000 --- a/IntelPresentMon/CliCore/source/pmon/stream/LoggedFrameDataState.cpp +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#include "LoggedFrameDataState.h" -#include -#include -#include -#include -#include -#include -#include - -namespace rn = std::ranges; -namespace vi = rn::views; - -namespace p2c::cli::pmon::stream -{ - LoggedFrameDataState::LoggedFrameDataState(std::string filePath, cons::ConsoleWaitControl* pWaitControl) - : - cache_(initialCacheSize_), - pWaitControl_{ pWaitControl } - { - if (auto sta = pmStartStreamEtl(filePath.c_str()); sta != PM_STATUS::PM_STATUS_SUCCESS) - { - p2clog.note(std::format(L"failed to open etl log file {} with error", - infra::util::ToWide(filePath))).code(sta).commit(); - } - } - - std::vector LoggedFrameDataState::Pull(double timestamp, uint32_t pid) - { - if (!cacheTimestamp_ || cacheTimestamp_ != timestamp) - { - // in: max writeable buffer size, out: actual num frames written - auto frameCountInOut = (uint32_t)initialCacheSize_; - // cache buffer might have shrunk down real small, reset to initial size - cache_.resize(initialCacheSize_); - // where in vector to begin writing - uint32_t writePosition = 0; - while (true) { - if (auto sta = pmGetEtlFrameData(&frameCountInOut, cache_.data() + writePosition); - sta == PM_STATUS::PM_STATUS_NO_DATA) { - // "shrink to fit" - cache_.resize(size_t(writePosition)); - break; - } - else if (sta == PM_STATUS::PM_STATUS_PROCESS_NOT_EXIST) { - pWaitControl_->NotifyEndOfLogEvent(); - break; - } - else if (sta != PM_STATUS::PM_STATUS_SUCCESS) { - p2clog.warn(std::format(L"failed to get etl log frame data with error")).code(sta).commit(); - cache_.clear(); - break; - } - // case where there *might* be more data - // if the api writes to all available elements - // means there might have been exactly the needed number - // of elements in the buffer, or that there are more remaining - if (size_t(writePosition) + size_t(frameCountInOut) >= std::size(cache_)) { - // if we're full, we should have filled up to exact size of buffer - CORE_ASSERT(size_t(writePosition) + size_t(frameCountInOut) == std::size(cache_)); - // next frames write start just after end of current cache - writePosition = (uint32_t)std::size(cache_); - // double size of cache to accomodate more entries - cache_.resize(cache_.size() * 2); - // available space is total size of cache minus size already written - frameCountInOut = (uint32_t)std::size(cache_) - writePosition; - } - else { - // "shrink to fit" - cache_.resize(size_t(writePosition) + size_t(frameCountInOut)); - break; - } - } - cacheTimestamp_ = timestamp; - // process all frames for simulating process spawn events - for (const auto& frame : cache_) { - if (auto&&[i, fresh] = pids_.insert(frame.process_id); fresh) { - pWaitControl_->SimulateSpawnEvent({ - .pid = frame.process_id, - .name = infra::util::ToWide(frame.application), - }); - } - } - } - // if pid is 0, return all frames - if (pid == 0) { - return cache_; - } - // filter out frames not for this pid - return cache_ | - vi::filter([pid](const auto& frame){ return frame.process_id == pid; }) | - rn::to(); - } -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/pmon/stream/LoggedFrameDataState.h b/IntelPresentMon/CliCore/source/pmon/stream/LoggedFrameDataState.h deleted file mode 100644 index 7550f262e..000000000 --- a/IntelPresentMon/CliCore/source/pmon/stream/LoggedFrameDataState.h +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once -#include "../FrameDataStream.h" -#include -#include -#include -#include - -namespace p2c::cli::cons -{ - class ConsoleWaitControl; -} - -namespace p2c::cli::pmon::stream -{ - class LoggedFrameDataState - { - public: - LoggedFrameDataState(std::string filePath, cons::ConsoleWaitControl* pWaitControl); - std::vector Pull(double timestamp, uint32_t pid); - private: - static constexpr size_t initialCacheSize_ = 120; - std::optional cacheTimestamp_; - std::vector cache_; - cons::ConsoleWaitControl* pWaitControl_; - std::unordered_set pids_; - }; -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/pmon/stream/LoggedFrameDataStream.cpp b/IntelPresentMon/CliCore/source/pmon/stream/LoggedFrameDataStream.cpp deleted file mode 100644 index b9b410e1e..000000000 --- a/IntelPresentMon/CliCore/source/pmon/stream/LoggedFrameDataStream.cpp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#include "LoggedFrameDataStream.h" -#include -#include -#include - -namespace p2c::cli::pmon::stream -{ - LoggedFrameDataStream::LoggedFrameDataStream(uint32_t pid, std::shared_ptr pState) - : - pid_(pid), - cache_(initialCacheSize_), - pState_{ std::move(pState) } - {} - - std::span LoggedFrameDataStream::Pull(double timestamp) - { - if (!cacheTimestamp_ || cacheTimestamp_ != timestamp) - { - cache_ = pState_->Pull(timestamp, pid_); - cacheTimestamp_ = timestamp; - } - return cache_; - } - uint32_t LoggedFrameDataStream::GetPid() const - { - return pid_; - } -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/pmon/stream/LoggedFrameDataStream.h b/IntelPresentMon/CliCore/source/pmon/stream/LoggedFrameDataStream.h deleted file mode 100644 index 98924ed11..000000000 --- a/IntelPresentMon/CliCore/source/pmon/stream/LoggedFrameDataStream.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once -#include "../FrameDataStream.h" -#include "LoggedFrameDataState.h" -#include -#include -#include -#include -#include - -namespace p2c::cli::pmon -{ - class ProcessStream; -} - -namespace p2c::cli::pmon::stream -{ - class LoggedFrameDataStream : public FrameDataStream - { - public: - LoggedFrameDataStream(uint32_t pid, std::shared_ptr pState); - std::span Pull(double timestamp) override; - uint32_t GetPid() const override; - private: - static constexpr size_t initialCacheSize_ = 120; - uint32_t pid_; - std::shared_ptr pState_; - std::optional cacheTimestamp_; - std::vector cache_; - }; -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/svc/Boot.cpp b/IntelPresentMon/CliCore/source/svc/Boot.cpp deleted file mode 100644 index 617e456f2..000000000 --- a/IntelPresentMon/CliCore/source/svc/Boot.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include "Boot.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace p2c::cli::svc -{ - void Boot(bool logging, std::optional logPath, std::optional logLevel) - { - using Services = infra::svc::Services; - -#ifdef NDEBUG - constexpr bool is_debug = false; -#else - constexpr bool is_debug = true; -#endif - - // appfolder and docfolder resolve to cwd - Services::Bind( - [] { return std::make_shared(L"", L"", false); } - ); - - if (!is_debug) { - const auto level = infra::log::Level(std::clamp(logLevel.value_or(0), 0, 3)); - infra::log::SetDefaultChannelFactory([=] { - auto pDefaultChannel = std::make_shared(); - if (logging) { - auto logFilePath = logPath - .transform([](auto& p) {return p + "\\pm-cli.log"; }) - .value_or("pm-cli.log"); - pDefaultChannel->AddDriver(std::make_unique(std::move(logFilePath))); - pDefaultChannel->AddPolicy({ [=](infra::log::EntryOutputBase& entry) { - return (int)entry.data.level <= (int)level; - } }); - } - return pDefaultChannel; - }); - const auto glogLevel = [&] { - switch (level) { - case infra::log::Level::Error: return 2; - case infra::log::Level::Warning: return 1; - case infra::log::Level::Info: return 0; - case infra::log::Level::Debug: return 0; - default: return 2; - } - }(); - if (logging && logPath) { - std::filesystem::create_directories(*logPath); - } - if (logging) { - pmInitializeLogging( - logPath.transform(&std::string::c_str).value_or("."), - "pm-sdk", - ".log", - glogLevel - ); - } - else { - pmInitializeLogging(nullptr, nullptr, nullptr, glogLevel); - } - } - else { - std::filesystem::create_directories("logs"); - pmInitializeLogging("logs", "pm-sdk", ".log", 0); - } - - // error code translators - infra::util::errtl::HResult::RegisterService(); - infra::util::errtl::PMStatus::RegisterService(); - } -} \ No newline at end of file diff --git a/IntelPresentMon/CliCore/source/svc/Boot.h b/IntelPresentMon/CliCore/source/svc/Boot.h deleted file mode 100644 index 9fe895351..000000000 --- a/IntelPresentMon/CliCore/source/svc/Boot.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once -#include -#include - -namespace p2c::cli::svc -{ - void Boot(bool logging, std::optional logPath, std::optional logLevel); -} \ No newline at end of file diff --git a/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj b/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj index 97b538796..5be25df63 100644 --- a/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj +++ b/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj @@ -20,6 +20,7 @@ + @@ -68,6 +69,12 @@ + + + + + + @@ -150,6 +157,15 @@ + + + + + + + + + diff --git a/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj.filters b/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj.filters index 5abadad4f..29f9cbe0b 100644 --- a/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj.filters +++ b/IntelPresentMon/CommonUtilities/CommonUtilities.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -18,6 +18,9 @@ Header Files + + Header Files + Header Files @@ -294,6 +297,24 @@ Header Files + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Source Files + + + Header Files + @@ -470,6 +491,33 @@ Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + diff --git a/IntelPresentMon/CommonUtilities/Exception.h b/IntelPresentMon/CommonUtilities/Exception.h index beb8bf1f5..64f863b0f 100644 --- a/IntelPresentMon/CommonUtilities/Exception.h +++ b/IntelPresentMon/CommonUtilities/Exception.h @@ -3,7 +3,7 @@ #include #include #include -#include +#include "../PresentMonAPI2/PresentMonAPI.h" namespace pmon::util { @@ -34,7 +34,7 @@ namespace pmon::util void DoCapture_(Exception& e); - template + template auto Except(R&&...args) { E exception{ std::forward(args)... }; @@ -67,4 +67,5 @@ namespace pmon::util #define pmquell(stat) try { stat; } catch (...) {} #define pmcatch_report catch (...) { pmlog_error(::pmon::util::ReportException()); } -} \ No newline at end of file +#define pmcatch_report_diag(should_return) catch (...) { const auto code = ::pmon::util::GeneratePmStatus(); pmlog_error(::pmon::util::ReportException()).code(code).diag(); if constexpr (should_return) { return code; } } +} diff --git a/IntelPresentMon/CommonUtilities/IntervalWaiter.cpp b/IntelPresentMon/CommonUtilities/IntervalWaiter.cpp index 9d327530a..63749420c 100644 --- a/IntelPresentMon/CommonUtilities/IntervalWaiter.cpp +++ b/IntelPresentMon/CommonUtilities/IntervalWaiter.cpp @@ -3,6 +3,13 @@ namespace pmon::util { + IntervalWaiter::IntervalWaiter(double intervalSeconds, int64_t syncTimestamp, double waitBuffer) + : + intervalSeconds_{ intervalSeconds }, + waiter_{ waitBuffer }, + timer_{ syncTimestamp } + {} + IntervalWaiter::IntervalWaiter(double intervalSeconds, double waitBuffer) : intervalSeconds_{ intervalSeconds }, @@ -19,22 +26,30 @@ namespace pmon::util intervalSeconds_ = double(interval.count()) / 1'000'000'000.; } - void IntervalWaiter::Wait() + IntervalWaiter::WaitResult IntervalWaiter::Wait() { - const auto waitTimeSeconds = [=, this] { + WaitResult res{}; + const auto waitTimeSeconds = [=, this, &res] { const auto t = timer_.Peek(); - auto targetCandidate = lastTargetTime_ + intervalSeconds_; + res.targetSec = lastTargetTime_ + intervalSeconds_; // if we are on-time - if (t <= targetCandidate) { - lastTargetTime_ = targetCandidate; - return targetCandidate - t; + if (t <= res.targetSec) { + lastTargetTime_ = res.targetSec; + return res.targetSec - t; } // if we are late, reset target to NOW and do not wait lastTargetTime_ = t; + res.errorSec = res.targetSec - t; return 0.; }(); if (waitTimeSeconds > 0.) { - waiter_.Wait(waitTimeSeconds); + res.errorSec = waiter_.Wait(waitTimeSeconds); } + return res; + } + + int64_t IntervalWaiter::TargetTimeToTimestamp(double targetTime) const + { + return timer_.TimeToTimestamp(targetTime); } } diff --git a/IntelPresentMon/CommonUtilities/IntervalWaiter.h b/IntelPresentMon/CommonUtilities/IntervalWaiter.h index d202ed402..40b18c557 100644 --- a/IntelPresentMon/CommonUtilities/IntervalWaiter.h +++ b/IntelPresentMon/CommonUtilities/IntervalWaiter.h @@ -10,6 +10,14 @@ namespace pmon::util class IntervalWaiter { public: + // types + struct WaitResult + { + double targetSec; + double errorSec; + }; + // functions + IntervalWaiter(double intervalSeconds, int64_t syncTimestamp, double waitBuffer = PrecisionWaiter::standardWaitBuffer); IntervalWaiter(double intervalSeconds, double waitBuffer = PrecisionWaiter::standardWaitBuffer); IntervalWaiter(const IntervalWaiter&) = delete; IntervalWaiter & operator=(const IntervalWaiter&) = delete; @@ -18,7 +26,8 @@ namespace pmon::util ~IntervalWaiter() = default; void SetInterval(double intervalSeconds); void SetInterval(std::chrono::nanoseconds interval); - void Wait(); + WaitResult Wait(); + int64_t TargetTimeToTimestamp(double targetTime) const; private: double intervalSeconds_; double lastTargetTime_ = 0.; diff --git a/IntelPresentMon/CommonUtilities/Math.h b/IntelPresentMon/CommonUtilities/Math.h index 4730011e9..06a21fb7e 100644 --- a/IntelPresentMon/CommonUtilities/Math.h +++ b/IntelPresentMon/CommonUtilities/Math.h @@ -8,7 +8,7 @@ namespace pmon::util template T CommonEpsilonStrict(T a, T b) { - return std::numeric_limits::epsilon() * std::max(std::abs(a), std::abs(b)); + return std::numeric_limits::epsilon() * std::max(std::abs(a), std::abs(b)); } template diff --git a/IntelPresentMon/CommonUtilities/Memory.h b/IntelPresentMon/CommonUtilities/Memory.h index d3daf46b5..a2577d4f6 100644 --- a/IntelPresentMon/CommonUtilities/Memory.h +++ b/IntelPresentMon/CommonUtilities/Memory.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include namespace pmon::util @@ -16,6 +16,11 @@ namespace pmon::util return GetPadding(byteIndex, alignof(T)); } + inline constexpr size_t PadToAlignment(size_t bytes, size_t alignment) + { + return bytes + GetPadding(bytes, alignment); + } + template class CloningUptr : public std::unique_ptr { @@ -119,4 +124,4 @@ namespace pmon::util // unique_ptr that uses LocalFree. template using UniqueLocalPtr = std::unique_ptr>; -} \ No newline at end of file +} diff --git a/IntelPresentMon/CommonUtilities/Meta.h b/IntelPresentMon/CommonUtilities/Meta.h index 4334bb63d..8af96b6c9 100644 --- a/IntelPresentMon/CommonUtilities/Meta.h +++ b/IntelPresentMon/CommonUtilities/Meta.h @@ -1,4 +1,7 @@ #pragma once +#include +#include +#include #include #include @@ -29,6 +32,13 @@ namespace pmon::util template inline constexpr bool DependentFalse = DependentFalseT::value; + template + struct IsStdOptionalT : std::false_type {}; + template + struct IsStdOptionalT> : std::true_type {}; + template + inline constexpr bool IsStdOptional = IsStdOptionalT>::value; + // deconstruct member pointer into the object type the pointer works with and the member type template struct MemberPointerInfo; template @@ -36,6 +46,30 @@ namespace pmon::util using StructType = S; using MemberType = M; }; + template + struct MemberPointerInfo { + using StructType = S; + using MemberType = M; + }; + template + struct MemberPointerInfo { + using StructType = S; + using MemberType = M; + }; + template + struct MemberPointerInfo { + using StructType = S; + using MemberType = M; + }; + + template + std::size_t MemberPointerOffset(M S::*memberPtr) + { + const S sample{}; + const auto* base = reinterpret_cast(&sample); + const auto* member = reinterpret_cast(&(sample.*memberPtr)); + return static_cast(member - base); + } // get the type of the elements in any iterable type (typically containers) template @@ -83,4 +117,51 @@ namespace pmon::util } template struct FunctionPtrTraits : impl::FunctionPtrTraitsImpl_> {}; + + namespace impl { + template MaxValue, + std::underlying_type_t Value, typename Functor> + constexpr void ForEachEnumValueRecursive_(Functor& func) + { + if constexpr (Value < MaxValue) { + func.template operator()(Value)>(); + ForEachEnumValueRecursive_(func); + } + } + + template MaxValue, + std::underlying_type_t Value, typename Functor, typename ReturnT> + constexpr ReturnT DispatchEnumValueRecursive_(std::underlying_type_t target, + Functor& func, ReturnT&& defaultValue) + { + if constexpr (Value < MaxValue) { + if (target == Value) { + return func.template operator()(Value)>(); + } + return DispatchEnumValueRecursive_( + target, func, std::move(defaultValue)); + } + return std::move(defaultValue); + } + } + + // compile-time static to runtime dynamic TMP bridges (on enum) + + template MaxValue, typename Functor> + constexpr void ForEachEnumValue(Functor&& func) + { + auto& fn = func; + impl::ForEachEnumValueRecursive_(fn); + } + + template MaxValue, typename Functor, typename ReturnT> + constexpr std::remove_reference_t DispatchEnumValue(Enum value, Functor&& func, ReturnT&& defaultValue) + { + using Ret = std::remove_reference_t; + auto& fn = func; + Ret defaultValueCopy = std::forward(defaultValue); + return impl::DispatchEnumValueRecursive_( + static_cast>(value), + fn, std::move(defaultValueCopy)); + } } diff --git a/IntelPresentMon/CommonUtilities/PrecisionWaiter.cpp b/IntelPresentMon/CommonUtilities/PrecisionWaiter.cpp index 0df192f18..341ebe9b2 100644 --- a/IntelPresentMon/CommonUtilities/PrecisionWaiter.cpp +++ b/IntelPresentMon/CommonUtilities/PrecisionWaiter.cpp @@ -23,15 +23,15 @@ namespace pmon::util pmlog_error(ReportException("Failed creating timer")); } } - void PrecisionWaiter::Wait(double seconds, bool alertable) noexcept + double PrecisionWaiter::Wait(double seconds, bool alertable) noexcept { - WaitWithBuffer(seconds, defaultWaitBuffer_, alertable); + return WaitWithBuffer(seconds, defaultWaitBuffer_, alertable); } - void PrecisionWaiter::WaitUnbuffered(double seconds, bool alertable) noexcept + double PrecisionWaiter::WaitUnbuffered(double seconds, bool alertable) noexcept { - WaitWithBuffer(seconds, 0., alertable); + return WaitWithBuffer(seconds, 0., alertable); } - void PrecisionWaiter::WaitWithBuffer(double seconds, double buffer, bool alertable) noexcept + double PrecisionWaiter::WaitWithBuffer(double seconds, double buffer, bool alertable) noexcept { try { if (waitableTimer_) { @@ -61,10 +61,10 @@ namespace pmon::util } // spin wait for the remainder buffer time if (buffer > 0.) { - qpcTimer_.SpinWaitUntil(seconds); + return qpcTimer_.SpinWaitUntil(seconds); } // return so we don't do the fallback Sleep() wait - return; + return 0.; } } catch (...) { @@ -75,6 +75,7 @@ namespace pmon::util // fall through to the fallback } // fallback if we fail high performance timing - Sleep(DWORD(seconds * 1000.f)); + Sleep(DWORD(seconds * 1000.)); + return 0.; } } \ No newline at end of file diff --git a/IntelPresentMon/CommonUtilities/PrecisionWaiter.h b/IntelPresentMon/CommonUtilities/PrecisionWaiter.h index 3850d8fcd..3d46c820c 100644 --- a/IntelPresentMon/CommonUtilities/PrecisionWaiter.h +++ b/IntelPresentMon/CommonUtilities/PrecisionWaiter.h @@ -18,9 +18,9 @@ namespace pmon::util PrecisionWaiter(PrecisionWaiter&&) = delete; PrecisionWaiter& operator=(PrecisionWaiter&&) = delete; ~PrecisionWaiter() = default; - void Wait(double seconds, bool alertable = false) noexcept; - void WaitUnbuffered(double seconds, bool alertable = false) noexcept; - void WaitWithBuffer(double seconds, double buffer, bool alertable = false) noexcept; + double Wait(double seconds, bool alertable = false) noexcept; + double WaitUnbuffered(double seconds, bool alertable = false) noexcept; + double WaitWithBuffer(double seconds, double buffer, bool alertable = false) noexcept; // todo: add function to set wait and receive handle, make compatible with event wait functions private: // amount of time we should wake up early by to do hyper-accurate spin wait diff --git a/IntelPresentMon/CommonUtilities/Qpc.cpp b/IntelPresentMon/CommonUtilities/Qpc.cpp index de4421311..f18010596 100644 --- a/IntelPresentMon/CommonUtilities/Qpc.cpp +++ b/IntelPresentMon/CommonUtilities/Qpc.cpp @@ -1,4 +1,4 @@ -#include "Qpc.h" +#include "Qpc.h" #include "win/WinAPI.h" #include "log/Log.h" #include @@ -14,13 +14,23 @@ namespace pmon::util } return (int64_t)timestamp.QuadPart; } - double GetTimestampPeriodSeconds() noexcept + double GetTimestampFrequencyDouble() noexcept + { + return double(GetTimestampFrequencyUint64()); + } + uint64_t GetTimestampFrequencyUint64() noexcept { LARGE_INTEGER freq; if (!QueryPerformanceFrequency(&freq)) { pmlog_error("qpc frequency failed").hr().every(5); + return 0; } - return 1.0 / double(freq.QuadPart); + return (uint64_t)freq.QuadPart; + } + double GetTimestampPeriodSeconds() noexcept + { + const auto frequency = GetTimestampFrequencyDouble(); + return frequency == 0.0 ? 0.0 : 1.0 / frequency; } void SpinWaitUntilTimestamp(int64_t timestamp) noexcept { @@ -33,10 +43,36 @@ namespace pmon::util return double(end - start) * period; } + double TimestampDeltaToMilliSeconds(uint64_t duration, uint64_t qpcFrequency) noexcept + { + return qpcFrequency == 0 ? 0.0 : (duration * 1000.0) / double(qpcFrequency); + } + + double TimestampDeltaToMilliSeconds(uint64_t start, uint64_t end, uint64_t qpcFrequency) noexcept + { + return (end <= start || qpcFrequency == 0) ? 0.0 : TimestampDeltaToMilliSeconds(end - start, qpcFrequency); + } + + double TimestampDeltaToSignedMilliSeconds(uint64_t start, uint64_t end, uint64_t qpcFrequency) noexcept + { + if (qpcFrequency == 0 || start == 0 || end == 0 || start == end) { + return 0.0; + } + return end > start + ? TimestampDeltaToMilliSeconds(end - start, qpcFrequency) + : -TimestampDeltaToMilliSeconds(start - end, qpcFrequency); + } + + QpcTimer::QpcTimer(int64_t seededStartTimestamp) + : + performanceCounterPeriod_{ GetTimestampPeriodSeconds() }, + startTimestamp_{ seededStartTimestamp } + {} QpcTimer::QpcTimer() noexcept + : + performanceCounterPeriod_{ GetTimestampPeriodSeconds() } { - performanceCounterPeriod_ = GetTimestampPeriodSeconds(); Mark(); } double QpcTimer::Mark() noexcept @@ -56,10 +92,50 @@ namespace pmon::util { return startTimestamp_; } - void QpcTimer::SpinWaitUntil(double seconds) const noexcept + double QpcTimer::GetPerformanceCounterPeriod() const noexcept + { + return performanceCounterPeriod_; + } + double QpcTimer::SpinWaitUntil(double seconds) const noexcept { - while (Peek() < seconds) { + double t = Peek(); + for (; t < seconds; t = Peek()) { std::this_thread::yield(); } + return t - seconds; + } + int64_t QpcTimer::TimeToTimestamp(double seconds) const noexcept + { + return startTimestamp_ + int64_t(seconds / performanceCounterPeriod_); + } + + + + // Duration in ticks -> ms + double QpcConverter::TicksToMilliSeconds(uint64_t ticks) const noexcept + { + return ticks * msPerTick_; + } + + // Unsigned delta (0 if end <= start or either is 0) + double QpcConverter::DeltaUnsignedMilliSeconds(uint64_t start, uint64_t end) const noexcept + { + return (end <= start || start == 0 || end == 0) ? 0.0 : TicksToMilliSeconds(end - start); + } + + // Signed delta (positive if end > start; negative if end < start; 0 if invalid) + double QpcConverter::DeltaSignedMilliSeconds(uint64_t start, uint64_t end) const noexcept + { + if (start == 0 || end == 0 || start == end) { + return 0.0; + } + return end > start ? TicksToMilliSeconds(end - start) + : -TicksToMilliSeconds(start - end); + } + + // Convenience: raw duration already a tick count (e.g. TimeInPresent) + double QpcConverter::DurationMilliSeconds(uint64_t tickCount) const noexcept + { + return TicksToMilliSeconds(tickCount); } -} \ No newline at end of file +} diff --git a/IntelPresentMon/CommonUtilities/Qpc.h b/IntelPresentMon/CommonUtilities/Qpc.h index 9cacbe0a3..ecd308001 100644 --- a/IntelPresentMon/CommonUtilities/Qpc.h +++ b/IntelPresentMon/CommonUtilities/Qpc.h @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include @@ -6,20 +6,60 @@ namespace pmon::util { int64_t GetCurrentTimestamp() noexcept; + double GetTimestampFrequencyDouble() noexcept; + uint64_t GetTimestampFrequencyUint64() noexcept; double GetTimestampPeriodSeconds() noexcept; void SpinWaitUntilTimestamp(int64_t timestamp) noexcept; double TimestampDeltaToSeconds(int64_t start, int64_t end, double period) noexcept; + double TimestampDeltaToMilliSeconds(uint64_t duration, uint64_t qpcFrequency) noexcept; + double TimestampDeltaToMilliSeconds(uint64_t start, uint64_t end, uint64_t qpcFrequency) noexcept; + double TimestampDeltaToSignedMilliSeconds(uint64_t start, uint64_t end, uint64_t qpcFrequency) noexcept; class QpcTimer { public: + QpcTimer(int64_t seededStartTimestamp); QpcTimer() noexcept; double Mark() noexcept; double Peek() const noexcept; int64_t GetStartTimestamp() const noexcept; - void SpinWaitUntil(double seconds) const noexcept; + double GetPerformanceCounterPeriod() const noexcept; + double SpinWaitUntil(double seconds) const noexcept; + int64_t TimeToTimestamp(double seconds) const noexcept; private: double performanceCounterPeriod_; int64_t startTimestamp_ = 0; }; -} \ No newline at end of file + + class QpcConverter + { + public: + QpcConverter(uint64_t qpcFrequency, uint64_t sessionStartTimestamp = 0) noexcept + : qpcFrequency_(qpcFrequency) + , msPerTick_(qpcFrequency_ == 0 ? 0.0 : 1000.0 / double(qpcFrequency_)) + , sessionStartTimestamp_(sessionStartTimestamp) + { + } + + uint64_t GetFrequency() const noexcept { return qpcFrequency_; } + double GetMilliSecondsPerTick() const noexcept { return msPerTick_; } + uint64_t GetSessionStartTimestamp() const noexcept { return sessionStartTimestamp_; } + + // Duration in ticks -> ms + double TicksToMilliSeconds(uint64_t ticks) const noexcept; + + // Unsigned delta (0 if end <= start or either is 0) + double DeltaUnsignedMilliSeconds(uint64_t start, uint64_t end) const noexcept; + + // Signed delta (positive if end > start; negative if end < start; 0 if invalid) + double DeltaSignedMilliSeconds(uint64_t start, uint64_t end) const noexcept; + + // Convenience: raw duration already a tick count (e.g. TimeInPresent) + double DurationMilliSeconds(uint64_t tickCount) const noexcept; + + private: + uint64_t qpcFrequency_{ 0 }; + double msPerTick_{ 0.0 }; + uint64_t sessionStartTimestamp_{ 0 }; + }; +} diff --git a/IntelPresentMon/CommonUtilities/cnr/FixedVector.h b/IntelPresentMon/CommonUtilities/cnr/FixedVector.h new file mode 100644 index 000000000..a6fabeadd --- /dev/null +++ b/IntelPresentMon/CommonUtilities/cnr/FixedVector.h @@ -0,0 +1,382 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pmon::util::cnr +{ + template + class FixedVector + { + public: + using value_type = T; + using size_type = size_t; + using difference_type = std::ptrdiff_t; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; + using iterator = T*; + using const_iterator = const T*; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + FixedVector() noexcept = default; + + explicit FixedVector(size_type count) + { + Resize(count); + } + + FixedVector(size_type count, const T& value) + { + Resize(count, value); + } + + FixedVector(std::initializer_list init) + { + Assign(init); + } + + template, int> = 0> + FixedVector(It first, It last) + { + Assign(first, last); + } + + template + FixedVector(std::from_range_t, R&& range) + { + AssignRange_(std::forward(range)); + } + + FixedVector(const FixedVector& other) + { + CopyFrom_(other); + } + + FixedVector(FixedVector&& other) noexcept(std::is_nothrow_move_constructible_v) + { + MoveFrom_(std::move(other)); + } + + FixedVector& operator=(const FixedVector& other) + { + if (this != &other) { + Clear(); + CopyFrom_(other); + } + return *this; + } + + FixedVector& operator=(FixedVector&& other) noexcept(std::is_nothrow_move_constructible_v) + { + if (this != &other) { + Clear(); + MoveFrom_(std::move(other)); + } + return *this; + } + + FixedVector& operator=(std::initializer_list init) + { + Assign(init); + return *this; + } + + ~FixedVector() + { + Clear(); + } + + iterator begin() noexcept { return Data(); } + const_iterator begin() const noexcept { return Data(); } + const_iterator CBegin() const noexcept { return Data(); } + iterator end() noexcept { return Data() + size_; } + const_iterator end() const noexcept { return Data() + size_; } + const_iterator CEnd() const noexcept { return Data() + size_; } + + reverse_iterator RBegin() noexcept { return reverse_iterator(end()); } + const_reverse_iterator RBegin() const noexcept { return const_reverse_iterator(end()); } + const_reverse_iterator CRBegin() const noexcept { return const_reverse_iterator(CEnd()); } + reverse_iterator REnd() noexcept { return reverse_iterator(begin()); } + const_reverse_iterator REnd() const noexcept { return const_reverse_iterator(begin()); } + const_reverse_iterator CREnd() const noexcept { return const_reverse_iterator(CBegin()); } + + size_type Size() const noexcept { return size_; } + constexpr size_type Capacity() const noexcept { return MaxCapacity; } + constexpr size_type MaxSize() const noexcept { return MaxCapacity; } + bool Empty() const noexcept { return size_ == 0; } + + void Reserve(size_type count) + { + CheckCapacity_(count); + } + + void ShrinkToFit() noexcept {} + + reference operator[](size_type index) noexcept { return *Ptr_(index); } + const_reference operator[](size_type index) const noexcept { return *Ptr_(index); } + + reference At(size_type index) + { + if (index >= size_) { + throw std::out_of_range("FixedVector::At"); + } + return (*this)[index]; + } + + const_reference At(size_type index) const + { + if (index >= size_) { + throw std::out_of_range("FixedVector::At"); + } + return (*this)[index]; + } + + reference Front() noexcept + { + assert(size_ > 0); + return (*this)[0]; + } + + const_reference Front() const noexcept + { + assert(size_ > 0); + return (*this)[0]; + } + + reference Back() noexcept + { + assert(size_ > 0); + return (*this)[size_ - 1]; + } + + const_reference Back() const noexcept + { + assert(size_ > 0); + return (*this)[size_ - 1]; + } + + pointer Data() noexcept { return DataPtr_(); } + const_pointer Data() const noexcept { return DataPtr_(); } + + void Clear() noexcept + { + DestroyRange_(0, size_); + size_ = 0; + } + + void Resize(size_type count) + { + if (count < size_) { + DestroyRange_(count, size_); + size_ = count; + } + else if (count > size_) { + CheckCapacity_(count); + for (size_type i = size_; i < count; ++i) { + std::construct_at(Ptr_(i)); + ++size_; + } + } + } + + void Resize(size_type count, const T& value) + { + if (count < size_) { + DestroyRange_(count, size_); + size_ = count; + } + else if (count > size_) { + CheckCapacity_(count); + for (size_type i = size_; i < count; ++i) { + std::construct_at(Ptr_(i), value); + ++size_; + } + } + } + + void PushBack(const T& value) + { + EmplaceBack(value); + } + + void PushBack(T&& value) + { + EmplaceBack(std::move(value)); + } + + template + reference EmplaceBack(Args&&... args) + { + CheckCapacity_(size_ + 1); + pointer ptr = Ptr_(size_); + std::construct_at(ptr, std::forward(args)...); + ++size_; + return *ptr; + } + + void PopBack() noexcept + { + assert(size_ > 0); + if (size_ == 0) { + return; + } + DestroyRange_(size_ - 1, size_); + --size_; + } + + iterator Erase(const_iterator pos) + { + return Erase(pos, pos + 1); + } + + iterator Erase(const_iterator first, const_iterator last) + { + assert(first >= CBegin()); + assert(last <= CEnd()); + assert(first <= last); + + const size_type start = static_cast(first - CBegin()); + const size_type end = static_cast(last - CBegin()); + if (start == end) { + return begin() + start; + } + + const size_type count = end - start; + const size_type tail = size_ - end; + + if constexpr (std::is_move_assignable_v) { + for (size_type i = 0; i < tail; ++i) { + (*this)[start + i] = std::move((*this)[end + i]); + } + } + else { + for (size_type i = 0; i < tail; ++i) { + std::destroy_at(Ptr_(start + i)); + std::construct_at(Ptr_(start + i), std::move((*this)[end + i])); + } + } + + DestroyRange_(size_ - count, size_); + size_ -= count; + return begin() + start; + } + + void Assign(size_type count, const T& value) + { + Clear(); + Resize(count, value); + } + + void Assign(std::initializer_list init) + { + Clear(); + CheckCapacity_(init.size()); + for (const auto& value : init) { + EmplaceBack(value); + } + } + + template, int> = 0> + void Assign(It first, It last) + { + Clear(); + for (; first != last; ++first) { + EmplaceBack(*first); + } + } + + template + void Assign(std::from_range_t, R&& range) + { + AssignRange_(std::forward(range)); + } + + private: + using StorageByte = std::byte; + + template + void AssignRange_(R&& range) + { + Clear(); + if constexpr (std::ranges::sized_range) { + CheckCapacity_(static_cast(std::ranges::size(range))); + } + for (auto&& value : range) { + EmplaceBack(std::forward(value)); + } + } + + pointer DataPtr_() noexcept + { + if (size_ == 0) { + return reinterpret_cast(storage_.data()); + } + return Ptr_(0); + } + + const_pointer DataPtr_() const noexcept + { + if (size_ == 0) { + return reinterpret_cast(storage_.data()); + } + return Ptr_(0); + } + + pointer Ptr_(size_type index) noexcept + { + auto* raw = storage_.data() + (index * sizeof(T)); + return std::launder(reinterpret_cast(raw)); + } + + const_pointer Ptr_(size_type index) const noexcept + { + auto* raw = storage_.data() + (index * sizeof(T)); + return std::launder(reinterpret_cast(raw)); + } + + void DestroyRange_(size_type first, size_type last) noexcept + { + for (size_type i = first; i < last; ++i) { + std::destroy_at(Ptr_(i)); + } + } + + void CheckCapacity_(size_type count) const + { + if (count > MaxCapacity) { + throw std::length_error("FixedVector capacity exceeded"); + } + } + + void CopyFrom_(const FixedVector& other) + { + for (size_type i = 0; i < other.size_; ++i) { + std::construct_at(Ptr_(i), other[i]); + ++size_; + } + } + + void MoveFrom_(FixedVector&& other) + { + for (size_type i = 0; i < other.size_; ++i) { + std::construct_at(Ptr_(i), std::move(other[i])); + ++size_; + } + other.Clear(); + } + + alignas(T) std::array storage_{}; + size_type size_ = 0; + }; +} diff --git a/IntelPresentMon/CommonUtilities/file/SecureSubdirectory.cpp b/IntelPresentMon/CommonUtilities/file/SecureSubdirectory.cpp index 093f46791..44c1f1fc7 100644 --- a/IntelPresentMon/CommonUtilities/file/SecureSubdirectory.cpp +++ b/IntelPresentMon/CommonUtilities/file/SecureSubdirectory.cpp @@ -253,6 +253,7 @@ namespace pmon::util::file { if (!Empty()) { Clear(); + hDirectory_.Clear(); fs::remove(path_); path_.clear(); } diff --git a/IntelPresentMon/CommonUtilities/log/EntryBuilder.h b/IntelPresentMon/CommonUtilities/log/EntryBuilder.h index 136d8ab64..dc0f3597e 100644 --- a/IntelPresentMon/CommonUtilities/log/EntryBuilder.h +++ b/IntelPresentMon/CommonUtilities/log/EntryBuilder.h @@ -4,6 +4,14 @@ #include #include #include +#include +#if __has_include() +#include +#include +#define PMLOG_HAS_CEREAL_JSON 1 +#else +#define PMLOG_HAS_CEREAL_JSON 0 +#endif #include "TimePoint.h" #include "PanicLogger.h" @@ -54,6 +62,28 @@ namespace pmon::util::log catch (...) { pmlog_panic_("Failed to format watch in EntryBuilder"); } return *this; } + template + EntryBuilder& serialize(const char* symbol, const T& value) noexcept + { +#if PMLOG_HAS_CEREAL_JSON + try { + std::ostringstream os; + auto opts = cereal::JSONOutputArchive::Options::Default(); + cereal::JSONOutputArchive ar(os, opts); + ar(cereal::make_nvp(symbol, value)); + return watch(symbol, os.str()); + } + catch (const std::exception& e) { + return watch(symbol, std::string("JSON serialization failed: ") + e.what()); + } + catch (...) { + return watch(symbol, std::string("JSON serialization failed")); + } +#else + (void)value; + return watch(symbol, std::string("JSON serialization unavailable (cereal not built)")); +#endif + } template [[noreturn]] EntryBuilder& raise() { diff --git a/IntelPresentMon/CommonUtilities/log/GlobalPolicy.cpp b/IntelPresentMon/CommonUtilities/log/GlobalPolicy.cpp index 8d435e632..ac32fb516 100644 --- a/IntelPresentMon/CommonUtilities/log/GlobalPolicy.cpp +++ b/IntelPresentMon/CommonUtilities/log/GlobalPolicy.cpp @@ -7,12 +7,20 @@ #define PMLOG_GPOL_DEFAULT_LEVEL Level::Error #endif #endif +#ifndef PMLOG_GPOL_DEFAULT_TRACE_LEVEL +#ifndef NDEBUG +#define PMLOG_GPOL_DEFAULT_TRACE_LEVEL Level::Error +#else +#define PMLOG_GPOL_DEFAULT_TRACE_LEVEL Level::None +#endif +#endif namespace pmon::util::log { GlobalPolicy::GlobalPolicy() noexcept : - logLevel_{ PMLOG_GPOL_DEFAULT_LEVEL } + logLevel_{ PMLOG_GPOL_DEFAULT_LEVEL }, + traceLevel_{ PMLOG_GPOL_DEFAULT_TRACE_LEVEL } {} GlobalPolicy& GlobalPolicy::Get() noexcept { @@ -43,7 +51,7 @@ namespace pmon::util::log } void GlobalPolicy::SetTraceLevelDefault() noexcept { - SetTraceLevel(Level::Error); + SetTraceLevel(PMLOG_GPOL_DEFAULT_TRACE_LEVEL); } bool GlobalPolicy::GetResolveTraceInClientThread() const noexcept { @@ -97,4 +105,4 @@ namespace pmon::util::log { activeVerboseModules_.store(modset); } -} \ No newline at end of file +} diff --git a/IntelPresentMon/CommonUtilities/log/GlobalPolicy.h b/IntelPresentMon/CommonUtilities/log/GlobalPolicy.h index afc2f29bc..3ee116fbb 100644 --- a/IntelPresentMon/CommonUtilities/log/GlobalPolicy.h +++ b/IntelPresentMon/CommonUtilities/log/GlobalPolicy.h @@ -39,10 +39,10 @@ namespace pmon::util::log // data std::atomic logLevel_; std::atomic resolveTraceInClientThread_ = false; - std::atomic traceLevel_ = Level::Error; + std::atomic traceLevel_; std::atomic exceptionTracePolicy_ = false; std::atomic sehTraceOn_ = false; std::atomic subsystem_ = Subsystem::None; std::atomic activeVerboseModules_ = 0; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/CommonUtilities/log/Level.cpp b/IntelPresentMon/CommonUtilities/log/Level.cpp index 8eec750c8..16472880e 100644 --- a/IntelPresentMon/CommonUtilities/log/Level.cpp +++ b/IntelPresentMon/CommonUtilities/log/Level.cpp @@ -8,7 +8,7 @@ namespace pmon::util::log { std::string GetLevelName(Level lv) { - return std::string{ reflect::enum_name(lv)}; + return std::string{ reflect::enum_name(lv) }; } std::map GetLevelMapNarrow() @@ -18,7 +18,7 @@ namespace pmon::util::log for (int n = 0; n < (int)Level::EndOfEnumKeys; n++) { const auto lvl = Level(n); auto key = ToLower(GetLevelName(lvl)); - if (key != "Unknown") { + if (!key.starts_with("level")) { map[std::move(key)] = lvl; } } diff --git a/IntelPresentMon/CommonUtilities/log/Verbose.cpp b/IntelPresentMon/CommonUtilities/log/Verbose.cpp index a413d09fa..426b1707f 100644 --- a/IntelPresentMon/CommonUtilities/log/Verbose.cpp +++ b/IntelPresentMon/CommonUtilities/log/Verbose.cpp @@ -6,7 +6,7 @@ namespace pmon::util::log { std::string GetVerboseModuleName(V mod) { - return std::string{ reflect::enum_name(mod) }; + return std::string{ reflect::enum_name(mod) }; } std::map GetVerboseModuleMapNarrow() @@ -15,7 +15,8 @@ namespace pmon::util::log std::map map; for (int n = 0; n <= (int)V::Count; n++) { const auto lvl = V(n); - auto key = ToLower(GetVerboseModuleName(lvl)); + auto name = ToLower(GetVerboseModuleName(lvl)); + map[name] = lvl; } return map; } diff --git a/IntelPresentMon/CommonUtilities/log/Verbose.h b/IntelPresentMon/CommonUtilities/log/Verbose.h index 40d865b1f..ec70421df 100644 --- a/IntelPresentMon/CommonUtilities/log/Verbose.h +++ b/IntelPresentMon/CommonUtilities/log/Verbose.h @@ -13,9 +13,13 @@ namespace pmon::util::log core_hotkey, core_window, etwq, + kact, + ipc_sto, + ipc_ring, + met_use, Count }; std::string GetVerboseModuleName(V mod); std::map GetVerboseModuleMapNarrow(); -} \ No newline at end of file +} diff --git a/IntelPresentMon/CommonUtilities/mc/FrameMetricsMemberMap.h b/IntelPresentMon/CommonUtilities/mc/FrameMetricsMemberMap.h new file mode 100644 index 000000000..c3ef779e0 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/FrameMetricsMemberMap.h @@ -0,0 +1,86 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: MIT +#pragma once +#include +#include "MetricsTypes.h" +#include "../PresentMonAPI2/PresentMonAPI.h" + +namespace pmon::util::metrics +{ + templatestruct FrameMetricMember{}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::swapChainAddress;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::cpuStartMs;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msCPUTime;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::cpuStartQpc;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msCPUBusy;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msCPUWait;}; + template<>struct FrameMetricMember {static constexpr auto member = &FrameMetrics::msBetweenDisplayChange; + static constexpr auto reciprocationFactor = 1000.;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msBetweenPresents; + static constexpr auto reciprocationFactor = 1000.;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msGPULatency;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msGPUTime;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msGPUBusy;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msGPUWait;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::isDroppedFrame;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msDisplayedTime;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::syncInterval;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::presentFlags;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::presentMode;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::runtime;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::allowsTearing;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msDisplayLatency;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msAnimationError; + static constexpr bool needsDynamicAbs = true;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msClickToPhotonLatency;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msCPUTime; + static constexpr auto reciprocationFactor = 1000.;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::frameType;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msAllInputPhotonLatency;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msInstrumentedLatency;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msAnimationTime;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::presentStartMs;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::presentStartQpc;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msBetweenPresents;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msInPresentApi;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msBetweenDisplayChange;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msUntilDisplayed;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msUntilRenderComplete;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msBetweenSimStarts;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msPcLatency;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msBetweenDisplayChange;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msCPUTime;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msBetweenPresents;}; + template<>struct FrameMetricMember{static constexpr auto member=&FrameMetrics::msFlipDelay;}; + + template + inline constexpr bool HasFrameMetricMember = requires{ FrameMetricMember::member; }; + + template + inline constexpr bool HasReciprocationFactor = requires{ FrameMetricMember::reciprocationFactor; }; + + template + inline constexpr bool HasDynamicAbsRequirement = requires{ FrameMetricMember::needsDynamicAbs; }; + + template + constexpr std::optional GetReciprocationFactor() + { + if constexpr (HasReciprocationFactor) { + return FrameMetricMember::reciprocationFactor; + } + else { + return std::nullopt; + } + } + + template + constexpr bool NeedsDynamicAbs() + { + if constexpr (HasDynamicAbsRequirement) { + return FrameMetricMember::needsDynamicAbs; + } + else { + return false; + } + } +} diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.cpp new file mode 100644 index 000000000..e063e38b2 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.cpp @@ -0,0 +1,372 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#include "MetricsCalculator.h" +#include "MetricsCalculatorInternal.h" + +#include "../PresentData/PresentMonTraceConsumer.hpp" +#include "../Math.h" + +// Layout: internal helpers -> entry points -> metric assembly -> exported helpers + +namespace pmon::util::metrics +{ + // ============================================================================ + // 1) Internal helpers (file-local) + // ============================================================================ + namespace + { + // ---- Swap-chain state application ---- + void ApplyStateDeltas( + SwapChainCoreState& chainState, + const ComputedMetrics::StateDeltas& d) + { + // If we consumed pending input from a dropped frame, clear all + // "not displayed" input caches. + if (d.shouldResetInputTimes) + { + chainState.lastReceivedNotDisplayedAllInputTime = 0; + chainState.lastReceivedNotDisplayedMouseClickTime = 0; + chainState.lastReceivedNotDisplayedAppProviderInputTime = 0; + chainState.lastReceivedNotDisplayedPclSimStart = 0; + chainState.lastReceivedNotDisplayedPclInputTime = 0; + } + + // Dropped-frame input for all-input latency + if (d.lastReceivedNotDisplayedAllInputTime) + { + chainState.lastReceivedNotDisplayedAllInputTime = + *d.lastReceivedNotDisplayedAllInputTime; + } + + // Dropped-frame mouse click + if (d.lastReceivedNotDisplayedMouseClickTime) + { + chainState.lastReceivedNotDisplayedMouseClickTime = + *d.lastReceivedNotDisplayedMouseClickTime; + } + + // Dropped-frame app-provider input + if (d.lastReceivedNotDisplayedAppProviderInputTime) + { + chainState.lastReceivedNotDisplayedAppProviderInputTime = + *d.lastReceivedNotDisplayedAppProviderInputTime; + } + + // Dropped-frame PC Latency sim start/input + if (d.newLastReceivedPclSimStart) + { + chainState.lastReceivedNotDisplayedPclSimStart = + *d.newLastReceivedPclSimStart; + } + + // Accumulated PC latency input→frame-start time + if (d.newAccumulatedInput2FrameStart) + { + chainState.accumulatedInput2FrameStartTime = + *d.newAccumulatedInput2FrameStart; + } + + // Running EMA of PC latency input to frame-start time + if (d.newInput2FrameStartEma) + { + chainState.Input2FrameStartTimeEma = + *d.newInput2FrameStartEma; + } + } + + double ComputeCPUStartTimeMs( + const QpcConverter& qpc, + const uint64_t& CPUStartTimeQpc) + { + const auto startQpc = qpc.GetSessionStartTimestamp(); + return (startQpc != 0 && CPUStartTimeQpc != 0) + ? qpc.DeltaSignedMilliSeconds(startQpc, CPUStartTimeQpc) + : 0.0; + } + + } + + // 2) Public entry points + // ============================================================================ + std::vector ComputeMetricsForPresent( + const QpcConverter& qpc, + FrameData& present, + FrameData* nextDisplayed, + SwapChainCoreState& chainState, + MetricsVersion version) + { + std::vector results; + + const auto displayCount = present.displayed.Size(); + const bool isDisplayed = present.finalState == PresentResult::Presented && displayCount > 0; + + // Case 1: not displayed, return single not-displayed metrics + if (!isDisplayed || displayCount == 0) { + const uint64_t screenTime = 0; + const uint64_t nextScreenTime = 0; + const bool isDisplayed = false; + + // Legacy-equivalent attribution: compute displayIndex/appIndex and derive isAppFrame. + const auto indexing = DisplayIndexing::Calculate(present, nextDisplayed); + const size_t displayIndex = indexing.startIndex; // Case 1 => 0 + const size_t appIndex = indexing.appIndex; + + const bool isAppFrame = (displayIndex == appIndex); + const FrameType frameType = (displayCount > 0) + ? present.displayed[displayIndex].first + : FrameType::NotSet; + + auto metrics = ComputeFrameMetrics( + qpc, + present, + screenTime, + nextScreenTime, + isDisplayed, + isAppFrame, + frameType, + chainState); + + ApplyStateDeltas(chainState, metrics.stateDeltas); + + results.push_back(std::move(metrics)); + + chainState.UpdateAfterPresent(present); + + return results; + } + + // V1: displayed presents are computed immediately (no look-ahead / no postponing). + // Emit exactly one row per present (legacy V1 behavior). + if (version == MetricsVersion::V1) { + const size_t displayIndex = 0; + uint64_t screenTime = present.displayed[displayIndex].second; + uint64_t nextScreenTime = 0; + + AdjustScreenTimeForCollapsedPresentNV( + present, + nextDisplayed, + chainState.lastDisplayedFlipDelay, + chainState.lastDisplayedScreenTime, + screenTime, + nextScreenTime, + version); + + // This is so msDisplayedTime comes back as 0 instead of garbage for V1 single-row output. + // TODO: Better option is to have display metrics be optional. Update metrics struct accordingly. + nextScreenTime = screenTime; + const auto indexing = DisplayIndexing::Calculate(present, nullptr); + const bool isAppFrame = (displayIndex == indexing.appIndex); + const bool isDisplayedInstance = isDisplayed && screenTime != 0; + const FrameType frameType = isDisplayedInstance ? present.displayed[displayIndex].first : FrameType::NotSet; + + auto metrics = ComputeFrameMetrics( + qpc, + present, + screenTime, + nextScreenTime, + isDisplayedInstance, + isAppFrame, + frameType, + chainState); + + ApplyStateDeltas(chainState, metrics.stateDeltas); + results.push_back(std::move(metrics)); + + chainState.UpdateAfterPresent(present); + return results; + } + + // There is at least one displayed frame to process + const auto indexing = DisplayIndexing::Calculate(present, nextDisplayed); + + // Determine if we should update the swap chain based on nextDisplayed + const bool shouldUpdateSwapChain = (nextDisplayed != nullptr); + + for (size_t displayIndex = indexing.startIndex; displayIndex < indexing.endIndex; ++displayIndex) { + uint64_t screenTime = present.displayed[displayIndex].second; + uint64_t nextScreenTime = 0; + + if (displayIndex + 1 < displayCount) { + // Next display instance of the same present + nextScreenTime = present.displayed[displayIndex + 1].second; + } + else if (nextDisplayed != nullptr && !nextDisplayed->displayed.Empty()) { + // First display of the *next* presented frame + nextScreenTime = nextDisplayed->displayed[0].second; + } + else { + break; // No next screen time available + } + + AdjustScreenTimeForCollapsedPresentNV(present, nextDisplayed, 0, 0, screenTime, nextScreenTime, version); + + const bool isAppFrame = (displayIndex == indexing.appIndex); + const bool isDisplayedInstance = isDisplayed && screenTime != 0 && nextScreenTime != 0; + const FrameType frameType = isDisplayedInstance ? present.displayed[displayIndex].first : FrameType::NotSet; + + auto metrics = ComputeFrameMetrics( + qpc, + present, + screenTime, + nextScreenTime, + isDisplayedInstance, + isAppFrame, + frameType, + chainState); + + ApplyStateDeltas(chainState, metrics.stateDeltas); + + results.push_back(std::move(metrics)); + } + + // Matches old ReportMetricsHelper: + // - Case 2 (no nextDisplayed): no UpdateChain yet. + // - Case 3 (has nextDisplayed): this is the call that finally updates the chain. + if (shouldUpdateSwapChain) { + chainState.UpdateAfterPresent(present); + } + + return results; + } + + // ============================================================================ + // 3) Metric assembly helpers (ComputeFrameMetrics) + // ============================================================================ + ComputedMetrics ComputeFrameMetrics( + const QpcConverter& qpc, + const FrameData& present, + uint64_t screenTime, + uint64_t nextScreenTime, + bool isDisplayed, + bool isAppFrame, + FrameType frameType, + const SwapChainCoreState& chain) + { + + ComputedMetrics result{}; + FrameMetrics& metrics = result.metrics; + + metrics.frameType = frameType; + + CalculateBasePresentMetrics( + qpc, + present, + chain, + metrics); + + CalculateDisplayMetrics( + qpc, + present, + chain, + isDisplayed, + screenTime, + nextScreenTime, + metrics); + + CalculateCpuGpuMetrics( + qpc, + chain, + present, + isAppFrame, + metrics); + + CalculateAnimationMetrics( + qpc, + chain, + present, + isDisplayed, + isAppFrame, + screenTime, + metrics); + + CalculateInputLatencyMetrics( + qpc, + chain, + present, + isDisplayed, + isAppFrame, + metrics, + result.stateDeltas); + + metrics.msPcLatency = CalculatePcLatency( + qpc, + chain, + present, + isDisplayed, + screenTime, + result.stateDeltas); + + CalculateInstrumentedMetrics( + qpc, + chain, + present, + isDisplayed, + isAppFrame, + screenTime, + metrics); + + metrics.cpuStartQpc = CalculateCPUStart(chain, present); + metrics.cpuStartMs = ComputeCPUStartTimeMs(qpc, metrics.cpuStartQpc); + + return result; + } + + // ============================================================================ + // 4) Exported helper definitions (declared in MetricsCalculator.h) + // ============================================================================ + uint64_t CalculateCPUStart( + const SwapChainCoreState& chainState, + const FrameData& present) + { + uint64_t cpuStart = 0; + if (chainState.lastAppPresent.has_value()) { + const auto& lastAppPresent = chainState.lastAppPresent.value(); + if (lastAppPresent.appPropagatedPresentStartTime != 0) { + cpuStart = lastAppPresent.appPropagatedPresentStartTime + + lastAppPresent.appPropagatedTimeInPresent; + } + else { + cpuStart = lastAppPresent.presentStartTime + + lastAppPresent.timeInPresent; + } + } + else { + cpuStart = chainState.lastPresent.has_value() ? + chainState.lastPresent->presentStartTime + chainState.lastPresent->timeInPresent : 0; + } + return cpuStart; + } + + // Helper: Calculate simulation start time (for animation error) + uint64_t CalculateAnimationErrorSimStartTime( + const SwapChainCoreState& chainState, + const FrameData& present, + AnimationErrorSource source) + { + uint64_t simStartTime = 0; + if (source == AnimationErrorSource::CpuStart) { + simStartTime = CalculateCPUStart(chainState, present); + } + else if (source == AnimationErrorSource::AppProvider) { + simStartTime = present.appSimStartTime; + } + else if (source == AnimationErrorSource::PCLatency) { + simStartTime = present.pclSimStartTime; + } + return simStartTime; + } + + // Helper: Calculate animation time + double CalculateAnimationTime( + const QpcConverter& qpc, + uint64_t firstAppSimStartTime, + uint64_t currentSimTime) + { + double animationTime = 0.0; + uint64_t firstSimStartTime = firstAppSimStartTime != 0 ? firstAppSimStartTime : qpc.GetSessionStartTimestamp(); + if (currentSimTime > firstSimStartTime) { + animationTime = qpc.DeltaUnsignedMilliSeconds(firstSimStartTime, currentSimTime); + } + return animationTime; + } +} diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.h b/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.h new file mode 100644 index 000000000..720f4ac12 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculator.h @@ -0,0 +1,76 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#pragma once +#include +#include +#include +#include "../qpc.h" +#include "MetricsTypes.h" +#include "SwapChainState.h" + +namespace pmon::util::metrics +{ + // Result of metric calculation for one display index + struct ComputedMetrics { + FrameMetrics metrics; + + // State changes to apply to SwapChain + struct StateDeltas { + std::optional newInput2FrameStartEma; + std::optional newAccumulatedInput2FrameStart; + std::optional newLastReceivedPclSimStart; + std::optional lastReceivedNotDisplayedAllInputTime; + std::optional lastReceivedNotDisplayedMouseClickTime; + std::optional lastReceivedNotDisplayedAppProviderInputTime; + bool shouldResetInputTimes = false; + } stateDeltas; + }; + + // Index calculation helper + struct DisplayIndexing { + size_t startIndex; // First display index to process + size_t endIndex; // One past last index + size_t appIndex; // Index of app frame (or SIZE_MAX if none) + bool hasNextDisplayed; + + static DisplayIndexing Calculate( + const FrameData& present, + const FrameData* nextDisplayed); + }; + + std::vector ComputeMetricsForPresent( + const QpcConverter& qpc, + FrameData& present, + FrameData* nextDisplayed, + SwapChainCoreState& chainState, + MetricsVersion version = MetricsVersion::V2); + + // === Pure Calculation Functions === + + ComputedMetrics ComputeFrameMetrics( + const QpcConverter& qpc, + const FrameData& present, + uint64_t screenTime, + uint64_t nextScreenTime, + bool isDisplayed, + bool isAppFrame, + FrameType frameType, + const SwapChainCoreState& chain); + + // Helper: Calculate CPU start time + uint64_t CalculateCPUStart( + const SwapChainCoreState& chainState, + const FrameData& present); + + // Helper: Calculate simulation start time (for animation error) + uint64_t CalculateAnimationErrorSimStartTime( + const SwapChainCoreState& chainState, + const FrameData& present, + AnimationErrorSource source); + + // Helper: Calculate animation time + double CalculateAnimationTime( + const QpcConverter& qpc, + uint64_t firstAppSimStartTime, + uint64_t currentSimTime); +} \ No newline at end of file diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorAnimation.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorAnimation.cpp new file mode 100644 index 000000000..0e6e98d18 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorAnimation.cpp @@ -0,0 +1,152 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#include "MetricsCalculator.h" +#include "MetricsCalculatorInternal.h" + +#include "../PresentData/PresentMonTraceConsumer.hpp" +#include "../Math.h" + +namespace pmon::util::metrics +{ + namespace + { + // ---- Animation metrics ---- + std::optional ComputeAnimationError( + const QpcConverter& qpc, + const SwapChainCoreState& chain, + const FrameData& present, + bool isDisplayed, + bool isAppFrame, + uint64_t screenTime) + { + if (!isDisplayed || !isAppFrame) { + return std::nullopt; + } + + uint64_t currentSimStart = CalculateAnimationErrorSimStartTime(chain, present, chain.animationErrorSource); + + if (currentSimStart == 0 || + chain.lastDisplayedSimStartTime == 0 || + currentSimStart <= chain.lastDisplayedSimStartTime || + chain.lastDisplayedAppScreenTime == 0) { + return std::nullopt; + } + + double simElapsed = qpc.DeltaUnsignedMilliSeconds(chain.lastDisplayedSimStartTime, currentSimStart); + double displayElapsed = qpc.DeltaUnsignedMilliSeconds(chain.lastDisplayedAppScreenTime, screenTime); + + if (simElapsed == 0.0 || displayElapsed == 0.0) { + return std::nullopt; + } + + return simElapsed - displayElapsed; + } + + + std::optional ComputeAnimationTime( + const QpcConverter& qpc, + const SwapChainCoreState& chain, + const FrameData& present, + bool isDisplayed, + bool isAppFrame) + { + if (!isDisplayed || !isAppFrame) { + return std::nullopt; + } + + bool isFirstProviderSimTime = + chain.animationErrorSource == AnimationErrorSource::CpuStart && + (present.appSimStartTime != 0 || present.pclSimStartTime != 0); + if (isFirstProviderSimTime) { + // Seed only: no animation time yet. UpdateAfterPresent will flip us + // into AppProvider/PCL and latch firstAppSimStartTime. + return std::nullopt; + } + + uint64_t currentSimStart = CalculateAnimationErrorSimStartTime(chain, present, chain.animationErrorSource); + if (currentSimStart == 0) { + return std::nullopt; + } + + return CalculateAnimationTime(qpc, chain.firstAppSimStartTime, currentSimStart); + } + + double ComputePresentStartTimeMs( + const QpcConverter& qpc, + const FrameData& present) + { + const auto startQpc = qpc.GetSessionStartTimestamp(); + return startQpc != 0 && present.presentStartTime != 0 + ? qpc.DeltaSignedMilliSeconds(startQpc, present.presentStartTime) + : 0.0; + } + + } + + void CalculateBasePresentMetrics( + const QpcConverter& qpc, + const FrameData& present, + const SwapChainCoreState& swapChain, + FrameMetrics& out) + { + out.timeInSeconds = present.presentStartTime; + out.presentStartQpc = present.presentStartTime; + out.presentStartMs = ComputePresentStartTimeMs(qpc, present); + + // Calculate the delta from the previous present (if one exists) + // to the current present + if (swapChain.lastPresent.has_value()) { + out.msBetweenPresents = qpc.DeltaUnsignedMilliSeconds( + swapChain.lastPresent->presentStartTime, + present.presentStartTime); + } else { + out.msBetweenPresents = 0.0; + } + + out.msInPresentApi = qpc.DurationMilliSeconds(present.timeInPresent); + out.msUntilRenderStart = qpc.DeltaSignedMilliSeconds( + present.presentStartTime, + present.gpuStartTime); + out.msUntilRenderComplete = qpc.DeltaSignedMilliSeconds( + present.presentStartTime, + present.readyTime); + out.msGpuDuration = qpc.DurationMilliSeconds(present.gpuDuration); + out.msVideoDuration = qpc.DurationMilliSeconds(present.gpuVideoDuration); + out.msSinceInput = (present.inputTime == 0) + ? 0.0 + : qpc.DurationMilliSeconds(present.presentStartTime - present.inputTime); + + // Copy metadata + out.swapChainAddress = present.swapChainAddress; + out.runtime = present.runtime; + out.syncInterval = present.syncInterval; + out.presentFlags = present.presentFlags; + out.allowsTearing = present.supportsTearing; + out.presentMode = present.presentMode; + } + + void CalculateAnimationMetrics( + const QpcConverter& qpc, + const SwapChainCoreState& swapChain, + const FrameData& present, + bool isDisplayed, + bool isAppFrame, + uint64_t screenTime, + FrameMetrics& metrics) + { + metrics.msAnimationError = ComputeAnimationError( + qpc, + swapChain, + present, + isDisplayed, + isAppFrame, + screenTime); + + metrics.msAnimationTime = ComputeAnimationTime( + qpc, + swapChain, + present, + isDisplayed, + isAppFrame); + } +} diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorCpuGpu.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorCpuGpu.cpp new file mode 100644 index 000000000..b73325a45 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorCpuGpu.cpp @@ -0,0 +1,165 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#include "MetricsCalculator.h" +#include "MetricsCalculatorInternal.h" + +#include "../PresentData/PresentMonTraceConsumer.hpp" +#include "../Math.h" + +namespace pmon::util::metrics +{ + namespace + { + // ---- CPU/GPU metrics ---- + double ComputeMsCpuBusy( + const QpcConverter& qpc, + const SwapChainCoreState& swapChain, + const FrameData& present, + bool isAppPresent) + { + if (!isAppPresent) { + return 0.0; + } + + const auto cpuStart = CalculateCPUStart(swapChain, present); + if (cpuStart == 0) { + return 0.0; + } + + if (present.appPropagatedPresentStartTime != 0) { + return qpc.DeltaUnsignedMilliSeconds(cpuStart, present.appPropagatedPresentStartTime); + } + if (present.presentStartTime != 0) { + return qpc.DeltaUnsignedMilliSeconds(cpuStart, present.presentStartTime); + } + return 0.0; + } + + + double ComputeMsCpuWait( + const QpcConverter& qpc, + const FrameData& present, + bool isAppPresent) + { + if (!isAppPresent) { + return 0.0; + } + + if (present.appPropagatedTimeInPresent != 0) { + return qpc.DurationMilliSeconds(present.appPropagatedTimeInPresent); + } + if (present.timeInPresent != 0) { + return qpc.DurationMilliSeconds(present.timeInPresent); + } + return 0.0; + } + + + double ComputeMsGpuLatency( + const QpcConverter& qpc, + const SwapChainCoreState& swapChain, + const FrameData& present, + bool isAppPresent) + { + if (!isAppPresent) { + return 0.0; + } + + const auto cpuStart = CalculateCPUStart(swapChain, present); + if (cpuStart == 0) { + return 0.0; + } + + if (present.appPropagatedGPUStartTime != 0) { + return qpc.DeltaUnsignedMilliSeconds(cpuStart, present.appPropagatedGPUStartTime); + } + if (present.gpuStartTime != 0) { + return qpc.DeltaUnsignedMilliSeconds(cpuStart, present.gpuStartTime); + } + return 0.0; + } + + + double ComputeMsGpuBusy( + const QpcConverter& qpc, + const FrameData& present, + bool isAppPresent) + { + //out.msGPUBusy = std::nullopt; + double msGPUBusy = 0.0; + if (isAppPresent) { + if (present.appPropagatedGPUDuration != 0) { + msGPUBusy = qpc.DurationMilliSeconds(present.appPropagatedGPUDuration); + } + else if (present.gpuDuration != 0) { + msGPUBusy = qpc.DurationMilliSeconds(present.gpuDuration); + } + } + return msGPUBusy; + } + + double ComputeMsVideoBusy( + const QpcConverter& qpc, + const FrameData& present, + bool isAppPresent) + { + if (!isAppPresent) { + return 0.0; + } + + if (present.appPropagatedGPUVideoDuration != 0) { + return qpc.DurationMilliSeconds(present.appPropagatedGPUVideoDuration); + } + if (present.gpuVideoDuration != 0) { + return qpc.DurationMilliSeconds(present.gpuVideoDuration); + } + return 0.0; + } + + double ComputeMsGpuDuration( + const QpcConverter& qpc, + const FrameData& present, + bool isAppPresent) + { + //msGPUDuration = std::nullopt; + double msGPUDuration = 0.0; + if (isAppPresent) { + if (present.appPropagatedGPUStartTime != 0 || present.appPropagatedReadyTime != 0) { + msGPUDuration = qpc.DeltaUnsignedMilliSeconds(present.appPropagatedGPUStartTime, present.appPropagatedReadyTime); + } + else if (present.gpuStartTime != 0 || present.readyTime != 0) { + msGPUDuration = qpc.DeltaUnsignedMilliSeconds(present.gpuStartTime, present.readyTime); + } + } + return msGPUDuration; + } + + double ComputeMsGpuWait( + const QpcConverter& qpc, + const FrameData& present, + bool isAppPresent) + { + return std::max(0.0, + ComputeMsGpuDuration(qpc, present, isAppPresent) - + ComputeMsGpuBusy(qpc, present, isAppPresent)); + } + } + + void CalculateCpuGpuMetrics( + const QpcConverter& qpc, + const SwapChainCoreState& chainState, + const FrameData& present, + bool isAppFrame, + FrameMetrics& metrics) + { + metrics.msCPUBusy = ComputeMsCpuBusy(qpc, chainState, present, isAppFrame); + metrics.msCPUWait = ComputeMsCpuWait(qpc, present, isAppFrame); + metrics.msCPUTime = metrics.msCPUBusy + metrics.msCPUWait; + metrics.msGPULatency = ComputeMsGpuLatency(qpc, chainState, present, isAppFrame); + + metrics.msGPUBusy = ComputeMsGpuBusy(qpc, present, isAppFrame); + metrics.msVideoBusy = ComputeMsVideoBusy(qpc, present, isAppFrame); + metrics.msGPUWait = ComputeMsGpuWait(qpc, present, isAppFrame); + metrics.msGPUTime = metrics.msGPUBusy + metrics.msGPUWait; + } +} \ No newline at end of file diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorDisplay.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorDisplay.cpp new file mode 100644 index 000000000..268a82825 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorDisplay.cpp @@ -0,0 +1,206 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#include "MetricsCalculator.h" +#include "MetricsCalculatorInternal.h" + +#include "../PresentData/PresentMonTraceConsumer.hpp" +#include "../Math.h" + +namespace pmon::util::metrics +{ + namespace + { + // ---- Display metrics ---- + double ComputeMsUntilDisplayed( + const QpcConverter& qpc, + const FrameData& present, + bool isDisplayed, + uint64_t screenTime) + { + return isDisplayed + ? qpc.DeltaUnsignedMilliSeconds(present.presentStartTime, screenTime) + : 0.0; + } + + + // Helper dedicated to computing msBetweenDisplayChange, matching legacy ReportMetricsHelper behavior. + double ComputeMsBetweenDisplayChange( + const QpcConverter& qpc, + const SwapChainCoreState& chain, + bool isDisplayed, + uint64_t screenTime) + { + return isDisplayed + ? qpc.DeltaUnsignedMilliSeconds(chain.lastDisplayedScreenTime, screenTime) + : 0.0; + } + + + // Helper dedicated to computing msDisplayedTime, matching legacy ReportMetricsHelper behavior. + double ComputeMsDisplayedTime( + const QpcConverter& qpc, + bool isDisplayed, + uint64_t screenTime, + uint64_t nextScreenTime) + { + return isDisplayed + ? qpc.DeltaUnsignedMilliSeconds(screenTime, nextScreenTime) + : 0.0; + } + + + std::optional ComputeMsFlipDelay( + const QpcConverter& qpc, + const FrameData& present, + bool isDisplayed) + { + if (isDisplayed && present.flipDelay != 0) { + return qpc.DurationMilliSeconds(present.flipDelay); + } + return std::nullopt; + } + + + double ComputeMsDisplayLatency( + const QpcConverter& qpc, + const SwapChainCoreState& swapChain, + const FrameData& present, + bool isDisplayed, + uint64_t screenTime) + { + const auto cpuStart = CalculateCPUStart(swapChain, present); + return (isDisplayed && cpuStart != 0) + ? qpc.DeltaUnsignedMilliSeconds(cpuStart, screenTime) + : 0.0; + } + + + std::optional ComputeMsReadyTimeToDisplayLatency( + const QpcConverter& qpc, + const FrameData& present, + bool isDisplayed, + uint64_t screenTime) + { + if (isDisplayed && present.readyTime != 0) { + return qpc.DeltaUnsignedMilliSeconds(present.readyTime, screenTime); + } + return std::nullopt; + } + + + } + + // ---- NV collapsed/runt correction ---- + void AdjustScreenTimeForCollapsedPresentNV( + FrameData& present, + FrameData* nextDisplayedPresent, + const uint64_t& lastDisplayedFlipDelay, + const uint64_t& lastDisplayedScreenTime, + uint64_t& screenTime, + uint64_t& nextScreenTime, + MetricsVersion version) + { + if (version == MetricsVersion::V1) { + // NV1 collapsed/runt correction: legacy V1 adjusts the *current* present using the + // previous displayed state when the last displayed screen time (adjusted by flip delay) + // is greater than this present's screen time. + if (lastDisplayedFlipDelay > 0 && + lastDisplayedScreenTime > screenTime && + !present.displayed.Empty()) { + + const uint64_t diff = lastDisplayedScreenTime - screenTime; + present.flipDelay += diff; + present.displayed[0].second = lastDisplayedScreenTime; + screenTime = present.displayed[0].second; + } + return; + } + + // nextDisplayedPresent should always be non-null for NV GPU. + if (present.flipDelay && screenTime > nextScreenTime && nextDisplayedPresent) { + // If screenTime that is adjusted by flipDelay is larger than nextScreenTime, + // it implies this present is a collapsed present, or a runt frame. + // So we adjust the screenTime and flipDelay of nextDisplayedPresent, + // effectively making nextScreenTime equals to screenTime. + + nextDisplayedPresent->flipDelay += (screenTime - nextScreenTime); + nextScreenTime = screenTime; + nextDisplayedPresent->displayed[0].second = nextScreenTime; + } + } + + + DisplayIndexing DisplayIndexing::Calculate( + const FrameData& present, + const FrameData* nextDisplayed) + { + DisplayIndexing result{}; + + // Get display count + auto displayCount = present.displayed.Size(); // ConsoleAdapter/PresentSnapshot method + + // Check if displayed + bool displayed = present.finalState == PresentResult::Presented && displayCount > 0; + + // hasNextDisplayed + result.hasNextDisplayed = (nextDisplayed != nullptr); + + // Figure out range to process based on three cases: + // Case 1: Not displayed → empty range [0, 0) + // Case 2: Displayed, no next → process [0..N-2], postpone N-1 → range [0, N-1) + // Case 3: Displayed, with next → process postponed [N-1] → range [N-1, N) + + if (!displayed || displayCount == 0) { + // Case 1: Not displayed + result.startIndex = 0; + result.endIndex = 0; // Empty range + } + else if (nextDisplayed == nullptr) { + // Case 2: Postpone last display + result.startIndex = 0; + result.endIndex = displayCount - 1; // One past [N-2] = [N-1] (excludes last!) + } + else { + // Case 3: Process postponed last display + result.startIndex = displayCount - 1; + result.endIndex = displayCount; // One past [N-1] = [N] + } + + // appIndex - find first NotSet or Application frame + // Search from startIndex through ALL displays (not just the processing range) + result.appIndex = std::numeric_limits::max(); + if (displayCount > 0) { + for (size_t i = result.startIndex; i < displayCount; ++i) { + auto frameType = present.displayed[i].first; + if (frameType == FrameType::NotSet || frameType == FrameType::Application) { + result.appIndex = i; + break; + } + } + } + else { + result.appIndex = 0; + } + return result; + } + + + void CalculateDisplayMetrics( + const QpcConverter& qpc, + const FrameData& present, + const SwapChainCoreState& swapChain, + bool isDisplayed, + uint64_t screenTime, + uint64_t nextScreenTime, + FrameMetrics& metrics) + { + metrics.msUntilDisplayed = ComputeMsUntilDisplayed(qpc, present, isDisplayed, screenTime); + metrics.msBetweenDisplayChange = ComputeMsBetweenDisplayChange(qpc, swapChain, isDisplayed, screenTime); + metrics.msDisplayedTime = ComputeMsDisplayedTime(qpc, isDisplayed, screenTime, nextScreenTime); + metrics.msFlipDelay = ComputeMsFlipDelay(qpc, present, isDisplayed); + metrics.msDisplayLatency = ComputeMsDisplayLatency(qpc, swapChain, present, isDisplayed, screenTime); + metrics.msReadyTimeToDisplayLatency = ComputeMsReadyTimeToDisplayLatency(qpc, present, isDisplayed, screenTime); + metrics.isDroppedFrame = !isDisplayed; + metrics.screenTimeQpc = screenTime; + } +} diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInput.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInput.cpp new file mode 100644 index 000000000..63532729c --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInput.cpp @@ -0,0 +1,185 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#include "MetricsCalculator.h" +#include "MetricsCalculatorInternal.h" + +#include "../PresentData/PresentMonTraceConsumer.hpp" +#include "../Math.h" + +namespace pmon::util::metrics +{ + namespace + { + // ---- Input latency metrics ---- + std::optional ComputeClickToPhotonLatency( + const QpcConverter& qpc, + const SwapChainCoreState& chain, + const FrameData& present, + bool isDisplayed, + bool isAppFrame, + uint64_t screenTime, + ComputedMetrics::StateDeltas& stateDeltas) + { + // Only app frames participate in click-to-photon. + if (!isAppFrame) { + return std::nullopt; + } + + uint64_t inputTime = 0; + + // Case 1: This frame *has* a click. + if (present.mouseClickTime != 0) { + inputTime = present.mouseClickTime; + + if (!isDisplayed) { + // Not displayed: stash the click for a future displayed frame. + stateDeltas.lastReceivedNotDisplayedMouseClickTime = inputTime; + return std::nullopt; + } + else { + stateDeltas.shouldResetInputTimes = true; + } + } + // Case 2: No click on this frame, but frame is displayed: see if we can + // reuse a pending click from a previously dropped frame. + else if (isDisplayed && chain.lastReceivedNotDisplayedMouseClickTime != 0) { + inputTime = chain.lastReceivedNotDisplayedMouseClickTime; + stateDeltas.shouldResetInputTimes = true; + } + + // If we still have no inputTime, nothing to compute. + if (inputTime == 0) { + return std::nullopt; + } + + return qpc.DeltaUnsignedMilliSeconds(inputTime, screenTime); + } + + + std::optional ComputeAllInputToPhotonLatency( + const QpcConverter& qpc, + const SwapChainCoreState& chain, + const FrameData& present, + bool isDisplayed, + bool isAppFrame, + uint64_t screenTime, + ComputedMetrics::StateDeltas& stateDeltas) + { + // Only app frames participate in click-to-photon. + if (!isAppFrame) { + return std::nullopt; + } + + uint64_t inputTime = 0; + + // Case 1: This frame *has* a click. + if (present.inputTime != 0) { + inputTime = present.inputTime; + + if (!isDisplayed) { + // Not displayed: stash the click for a future displayed frame. + stateDeltas.lastReceivedNotDisplayedAllInputTime = inputTime; + return std::nullopt; + } + else { + stateDeltas.shouldResetInputTimes = true; + } + } + // Case 2: No click on this frame, but frame is displayed: see if we can + // reuse a pending click from a previously dropped frame. + else if (isDisplayed && chain.lastReceivedNotDisplayedAllInputTime != 0) { + inputTime = chain.lastReceivedNotDisplayedAllInputTime; + stateDeltas.shouldResetInputTimes = true; + } + + // If we still have no inputTime, nothing to compute. + if (inputTime == 0) { + return std::nullopt; + } + + return qpc.DeltaUnsignedMilliSeconds(inputTime, screenTime); + } + + std::optional ComputeInstrumentedInputToPhotonLatency( + const QpcConverter& qpc, + const SwapChainCoreState& chain, + const FrameData& present, + bool isDisplayed, + bool isAppFrame, + uint64_t screenTime, + ComputedMetrics::StateDeltas& stateDeltas) + { + // Only app frames participate in click-to-photon. + if (!isAppFrame) { + return std::nullopt; + } + + uint64_t inputTime = 0; + + // Case 1: This frame *has* a click. + if (present.appInputSample.first != 0) { + inputTime = present.appInputSample.first; + + if (!isDisplayed) { + // Not displayed: stash the click for a future displayed frame. + stateDeltas.lastReceivedNotDisplayedAppProviderInputTime = inputTime; + return std::nullopt; + } + else { + stateDeltas.shouldResetInputTimes = true; + } + } + // Case 2: No click on this frame, but frame is displayed: see if we can + // reuse a pending click from a previously dropped frame. + else if (isDisplayed && chain.lastReceivedNotDisplayedAppProviderInputTime != 0) { + inputTime = chain.lastReceivedNotDisplayedAppProviderInputTime; + stateDeltas.shouldResetInputTimes = true; + } + + // If we still have no inputTime, nothing to compute. + if (inputTime == 0) { + return std::nullopt; + } + + return qpc.DeltaUnsignedMilliSeconds(inputTime, screenTime); + } + } + + void CalculateInputLatencyMetrics( + const QpcConverter& qpc, + const SwapChainCoreState& swapChain, + const FrameData& present, + bool isDisplayed, + bool isAppFrame, + FrameMetrics& metrics, + ComputedMetrics::StateDeltas& stateDeltas) + { + const uint64_t screenTime = metrics.screenTimeQpc; + metrics.msClickToPhotonLatency = ComputeClickToPhotonLatency( + qpc, + swapChain, + present, + isDisplayed, + isAppFrame, + screenTime, + stateDeltas); + + metrics.msAllInputPhotonLatency = ComputeAllInputToPhotonLatency( + qpc, + swapChain, + present, + isDisplayed, + isAppFrame, + screenTime, + stateDeltas); + + metrics.msInstrumentedInputTime = ComputeInstrumentedInputToPhotonLatency( + qpc, + swapChain, + present, + isDisplayed, + isAppFrame, + screenTime, + stateDeltas); + } +} diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInstrumented.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInstrumented.cpp new file mode 100644 index 000000000..2b891feb0 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInstrumented.cpp @@ -0,0 +1,249 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#include "MetricsCalculator.h" +#include "MetricsCalculatorInternal.h" + +#include "../PresentData/PresentMonTraceConsumer.hpp" +#include "../Math.h" + +namespace pmon::util::metrics +{ + namespace + { + // ---- Instrumented metrics ---- + std::optional ComputeInstrumentedLatency( + const QpcConverter& qpc, + const FrameData& present, + bool isDisplayed, + bool isAppFrame, + uint64_t screenTime) + { + if (!isDisplayed || !isAppFrame) { + return std::nullopt; + } + + auto instrumentedStartTime = present.appSleepEndTime != 0 ? + present.appSleepEndTime : present.appSimStartTime; + + if (instrumentedStartTime == 0) { + // No instrumented start time: nothing to compute. + return std::nullopt; + } + + return qpc.DeltaUnsignedMilliSeconds(instrumentedStartTime, screenTime); + } + + std::optional ComputeInstrumentedRenderLatency( + const QpcConverter& qpc, + const FrameData& present, + bool isDisplayed, + bool isAppFrame, + uint64_t screenTime) + { + if (!isDisplayed || !isAppFrame) { + return std::nullopt; + } + + if (present.appRenderSubmitStartTime == 0) { + // No app provided render submit start time: nothing to compute. + return std::nullopt; + } + + return qpc.DeltaUnsignedMilliSeconds(present.appRenderSubmitStartTime, screenTime); + } + + std::optional ComputeInstrumentedSleep( + const QpcConverter& qpc, + const FrameData& present, + bool isAppFrame) + { + if (!isAppFrame) { + return std::nullopt; + } + + if (present.appSleepStartTime == 0 || present.appSleepEndTime == 0) { + // No app provided sleep times: nothing to compute. + return std::nullopt; + } + + return qpc.DeltaUnsignedMilliSeconds(present.appSleepStartTime, present.appSleepEndTime); + } + + std::optional ComputeInstrumentedGpuLatency( + const QpcConverter& qpc, + const FrameData& present, + bool isAppFrame) + { + if (!isAppFrame) { + return std::nullopt; + } + + auto instrumentedStartTime = present.appSleepEndTime != 0 ? + present.appSleepEndTime : present.appSimStartTime; + + if (instrumentedStartTime == 0) { + // No provider sleep end or sim start time: nothing to compute. + return std::nullopt; + } + + if (present.gpuStartTime == 0) { + // No GPU start time: nothing to compute. + return std::nullopt; + } + + return qpc.DeltaUnsignedMilliSeconds(instrumentedStartTime, present.gpuStartTime); + } + + // ---- Simulation metrics ---- + std::optional ComputeMsBetweenSimulationStarts( + const QpcConverter& qpc, + const SwapChainCoreState& chain, + const FrameData& present, + bool isAppFrame) + { + if (!isAppFrame) { + return std::nullopt; + } + + // The current sim start time is only dependent on the current frame's simulation start times. + // Preference is PCL, then App. + uint64_t currentSimStartTime = 0; + if (present.pclSimStartTime != 0) { + currentSimStartTime = present.pclSimStartTime; + } + else if (present.appSimStartTime != 0) { + currentSimStartTime = present.appSimStartTime; + } + if (chain.lastSimStartTime != 0 && currentSimStartTime != 0 && + currentSimStartTime > chain.lastSimStartTime) { + return qpc.DeltaUnsignedMilliSeconds( + chain.lastSimStartTime, + currentSimStartTime); + } + else { + return std::nullopt; + } + } + + } + + std::optional CalculatePcLatency( + const QpcConverter& qpc, + const SwapChainCoreState& chain, + const FrameData& present, + bool isDisplayed, + uint64_t screenTime, + ComputedMetrics::StateDeltas& stateDeltas) + { + if (!isDisplayed) { + if (present.pclSimStartTime != 0) { + if (present.pclInputPingTime != 0) { + // This frame was dropped but we have valid pc latency input and simulation start + // times. Calculate the initial input to sim start time + stateDeltas.newAccumulatedInput2FrameStart = qpc.DeltaUnsignedMilliSeconds( + present.pclInputPingTime, + present.pclSimStartTime); + } + else if (chain.accumulatedInput2FrameStartTime != 0.f) { + // This frame was also dropped and there is no pc latency input time. However, since we have + // accumulated time this means we have a pending input that has had multiple dropped frames + // and has not yet hit the screen. Calculate the time between the last not displayed sim start and + // this sim start and add it to our accumulated total + stateDeltas.newAccumulatedInput2FrameStart = chain.accumulatedInput2FrameStartTime + + qpc.DeltaUnsignedMilliSeconds( + chain.lastReceivedNotDisplayedPclSimStart, + present.pclSimStartTime); + } + stateDeltas.newLastReceivedPclSimStart = present.pclSimStartTime; + } + return std::nullopt; + } + + // Check to see if we have a valid PC Latency sim start time + if (present.pclSimStartTime != 0) { + if (present.pclInputPingTime != 0) { + // Both the pclSimStartTime and pclInputPingTime are valid, use them to update + // the Input to Frame Start EMA. Store in state deltas for later application. + + stateDeltas.newInput2FrameStartEma = pmon::util::CalculateEma( + chain.Input2FrameStartTimeEma, + qpc.DeltaUnsignedMilliSeconds(present.pclInputPingTime, present.pclSimStartTime), + 0.1); + + // Defensively clear the tracking variables for when we have a dropped frame with a pc latency input + stateDeltas.newAccumulatedInput2FrameStart = 0.0; + stateDeltas.newLastReceivedPclSimStart = 0; + } + else { + if (chain.accumulatedInput2FrameStartTime != 0.0) { + // This frame was displayed but we don't have a pc latency input time. However, there is accumulated time + // so there is a pending input that will now hit the screen. Add in the time from the last not + // displayed pc simulation start to this frame's pc simulation start. + stateDeltas.newAccumulatedInput2FrameStart = chain.accumulatedInput2FrameStartTime + + qpc.DeltaUnsignedMilliSeconds( + chain.lastReceivedNotDisplayedPclSimStart, + present.pclSimStartTime); + + stateDeltas.newInput2FrameStartEma = pmon::util::CalculateEma( + chain.Input2FrameStartTimeEma, + *stateDeltas.newAccumulatedInput2FrameStart, + 0.1); + + // Reset the tracking variables for when we have a dropped frame with a pc latency input + stateDeltas.newAccumulatedInput2FrameStart = 0.0; + stateDeltas.newLastReceivedPclSimStart = 0; + } + } + } + + auto simStartTime = present.pclSimStartTime != 0 ? present.pclSimStartTime : chain.lastSimStartTime; + double input2FrameStartEma = stateDeltas.newInput2FrameStartEma.has_value() ? + stateDeltas.newInput2FrameStartEma.value() : chain.Input2FrameStartTimeEma; + if (input2FrameStartEma != 0.0 && simStartTime != 0) { + return input2FrameStartEma + qpc.DeltaSignedMilliSeconds(simStartTime, screenTime); + } + else { + return std::nullopt; + } + } + + void CalculateInstrumentedMetrics( + const QpcConverter& qpc, + const SwapChainCoreState& chain, + const FrameData& present, + bool isDisplayed, + bool isAppFrame, + uint64_t screenTime, + FrameMetrics& metrics) { + + metrics.msInstrumentedLatency = ComputeInstrumentedLatency( + qpc, + present, + isDisplayed, + isAppFrame, + screenTime); + + metrics.msInstrumentedRenderLatency = ComputeInstrumentedRenderLatency( + qpc, + present, + isDisplayed, + isAppFrame, + screenTime); + + metrics.msInstrumentedSleep = ComputeInstrumentedSleep( + qpc, + present, + isAppFrame); + + metrics.msInstrumentedGpuLatency = ComputeInstrumentedGpuLatency( + qpc, + present, + isAppFrame); + + metrics.msBetweenSimStarts = ComputeMsBetweenSimulationStarts( + qpc, + chain, + present, + isAppFrame); + } +} diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInternal.h b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInternal.h new file mode 100644 index 000000000..26bc910aa --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/MetricsCalculatorInternal.h @@ -0,0 +1,76 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#pragma once + +#include "MetricsCalculator.h" + +namespace pmon::util::metrics +{ + // Category calculators implemented across MetricsCalculator_*.cpp translation units. + void CalculateBasePresentMetrics( + const QpcConverter& qpc, + const FrameData& present, + const SwapChainCoreState& swapChain, + FrameMetrics& out); + + void CalculateDisplayMetrics( + const QpcConverter& qpc, + const FrameData& present, + const SwapChainCoreState& swapChain, + bool isDisplayed, + uint64_t screenTime, + uint64_t nextScreenTime, + FrameMetrics& metrics); + + void CalculateCpuGpuMetrics( + const QpcConverter& qpc, + const SwapChainCoreState& chainState, + const FrameData& present, + bool isAppFrame, + FrameMetrics& metrics); + + void CalculateAnimationMetrics( + const QpcConverter& qpc, + const SwapChainCoreState& swapChain, + const FrameData& present, + bool isDisplayed, + bool isAppFrame, + uint64_t screenTime, + FrameMetrics& metrics); + + void CalculateInputLatencyMetrics( + const QpcConverter& qpc, + const SwapChainCoreState& swapChain, + const FrameData& present, + bool isDisplayed, + bool isAppFrame, + FrameMetrics& metrics, + ComputedMetrics::StateDeltas& stateDeltas); + + std::optional CalculatePcLatency( + const QpcConverter& qpc, + const SwapChainCoreState& chain, + const FrameData& present, + bool isDisplayed, + uint64_t screenTime, + ComputedMetrics::StateDeltas& stateDeltas); + + void CalculateInstrumentedMetrics( + const QpcConverter& qpc, + const SwapChainCoreState& chain, + const FrameData& present, + bool isDisplayed, + bool isAppFrame, + uint64_t screenTime, + FrameMetrics& metrics); + + // NVIDIA collapsed/runt correction helper used by ComputeMetricsForPresent. + void AdjustScreenTimeForCollapsedPresentNV( + FrameData& present, + FrameData* nextDisplayedPresent, + const uint64_t& lastDisplayedFlipDelay, + const uint64_t& lastDisplayedScreenTime, + uint64_t& screenTime, + uint64_t& nextScreenTime, + MetricsVersion version); +} \ No newline at end of file diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsTypes.cpp b/IntelPresentMon/CommonUtilities/mc/MetricsTypes.cpp new file mode 100644 index 000000000..9d0339e4f --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/MetricsTypes.cpp @@ -0,0 +1,72 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#include "MetricsTypes.h" + +#include "../PresentData/PresentMonTraceConsumer.hpp" + +namespace pmon::util::metrics { + + FrameData FrameData::CopyFrameData(const std::shared_ptr& p) + { + if (p) { + return CopyFrameData(*p); + } + pmlog_error("Tried to copy frame data from empty PresentEvent ptr"); + return {}; + } + + FrameData FrameData::CopyFrameData(const PresentEvent& p) + { + FrameData frame{}; + + frame.runtime = p.Runtime; + frame.presentMode = p.PresentMode; + frame.presentStartTime = p.PresentStartTime; + frame.readyTime = p.ReadyTime; + frame.timeInPresent = p.TimeInPresent; + frame.gpuStartTime = p.GPUStartTime; + frame.gpuDuration = p.GPUDuration; + frame.gpuVideoDuration = p.GPUVideoDuration; + + frame.appPropagatedPresentStartTime = p.AppPropagatedPresentStartTime; + frame.appPropagatedTimeInPresent = p.AppPropagatedTimeInPresent; + frame.appPropagatedGPUStartTime = p.AppPropagatedGPUStartTime; + frame.appPropagatedReadyTime = p.AppPropagatedReadyTime; + frame.appPropagatedGPUDuration = p.AppPropagatedGPUDuration; + frame.appPropagatedGPUVideoDuration = p.AppPropagatedGPUVideoDuration; + + frame.appSleepStartTime = p.AppSleepStartTime; + frame.appSleepEndTime = p.AppSleepEndTime; + frame.appSimStartTime = p.AppSimStartTime; + frame.appSimEndTime = p.AppSimEndTime; + frame.appRenderSubmitStartTime = p.AppRenderSubmitStartTime; + frame.appRenderSubmitEndTime = p.AppRenderSubmitEndTime; + frame.appPresentStartTime = p.AppPresentStartTime; + frame.appPresentEndTime = p.AppPresentEndTime; + frame.appInputSample = p.AppInputSample; + + frame.inputTime = p.InputTime; + frame.mouseClickTime = p.MouseClickTime; + + frame.pclSimStartTime = p.PclSimStartTime; + frame.pclInputPingTime = p.PclInputPingTime; + frame.flipDelay = p.FlipDelay; + frame.flipToken = p.FlipToken; + + frame.displayed.Assign(p.Displayed.begin(), p.Displayed.end()); + + frame.swapChainAddress = p.SwapChainAddress; + frame.syncInterval = p.SyncInterval; + frame.presentFlags = p.PresentFlags; + + frame.finalState = p.FinalState; + frame.supportsTearing = p.SupportsTearing; + frame.frameId = p.FrameId; + frame.processId = p.ProcessId; + frame.threadId = p.ThreadId; + frame.appFrameId = p.AppFrameId; + frame.pclFrameId = p.PclFrameId; + + return frame; + } +} diff --git a/IntelPresentMon/CommonUtilities/mc/MetricsTypes.h b/IntelPresentMon/CommonUtilities/mc/MetricsTypes.h new file mode 100644 index 000000000..e00f1ad11 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/MetricsTypes.h @@ -0,0 +1,162 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#pragma once +#include +#include +#include +#include "../cnr/FixedVector.h" +#include "../../../PresentData/PresentEventEnums.hpp" + +// Forward declarations for external types +struct PresentEvent; // From PresentMonTraceConsumer + +namespace pmon::util::metrics { + + // Metrics pipeline mode + enum class MetricsVersion { + V1, + V2, + }; + + // What the animation error calculation is based on + enum class AnimationErrorSource { + CpuStart, + AppProvider, + PCLatency, + }; + + using DisplayedEntry = std::pair; + using DisplayedVector = pmon::util::cnr::FixedVector; + + // Immutable snapshot - safe for both ownership models + struct FrameData { + Runtime runtime = {}; + PresentMode presentMode = {}; + + // Timing Data + uint64_t presentStartTime = 0; + uint64_t readyTime = 0; + uint64_t timeInPresent = 0; + uint64_t gpuStartTime = 0; + uint64_t gpuDuration = 0; + uint64_t gpuVideoDuration = 0; + + // Used to track the application work when Intel XeSS-FG is enabled + uint64_t appPropagatedPresentStartTime = 0; + uint64_t appPropagatedTimeInPresent = 0; + uint64_t appPropagatedGPUStartTime = 0; + uint64_t appPropagatedReadyTime = 0; + uint64_t appPropagatedGPUDuration = 0; + uint64_t appPropagatedGPUVideoDuration = 0; + + // Instrumented Timestamps + uint64_t appSimStartTime = 0; + uint64_t appSimEndTime = 0; + uint64_t appSleepStartTime = 0; + uint64_t appSleepEndTime = 0; + uint64_t appRenderSubmitStartTime = 0; + uint64_t appRenderSubmitEndTime = 0; + uint64_t appPresentStartTime = 0; + uint64_t appPresentEndTime = 0; + std::pair appInputSample; // time, input type + + // Input Device Timestamps + uint64_t inputTime = 0; // All input devices + uint64_t mouseClickTime = 0; // Mouse click specific + + DisplayedVector displayed; + + // PC Latency data + uint64_t pclSimStartTime = 0; + uint64_t pclInputPingTime = 0; + uint64_t flipDelay = 0; + uint32_t flipToken = 0; + + // Extra present parameters obtained through DXGI or D3D9 present + uint64_t swapChainAddress = 0; + int32_t syncInterval = 0; + uint32_t presentFlags = 0; + + // Metadata + PresentResult finalState = {}; + bool supportsTearing = 0; + bool isHybridPresent = false; + uint32_t processId = 0; + uint32_t threadId = 0; + uint32_t frameId = 0; + uint32_t appFrameId = 0; + uint32_t pclFrameId = 0; + + // Factory Methods + static FrameData CopyFrameData(const std::shared_ptr& p); + static FrameData CopyFrameData(const PresentEvent& p); + }; + + struct FrameMetrics { + // Core Timing (always computed) + uint64_t timeInSeconds = 0; + uint64_t presentStartQpc = 0; + double presentStartMs = 0; + uint64_t cpuStartQpc = 0; + double cpuStartMs = 0; + double msBetweenPresents = 0; + double msInPresentApi = 0; + double msUntilRenderStart = 0; + double msUntilRenderComplete = 0; + double msGpuDuration = 0; + double msVideoDuration = 0; + double msSinceInput = 0; + + // Display Metrics (displayed frames only) + double msDisplayLatency = 0; + double msDisplayedTime = 0; + double msUntilDisplayed = 0; + double msBetweenDisplayChange = 0; + uint64_t screenTimeQpc = 0; + std::optional msReadyTimeToDisplayLatency; + bool isDroppedFrame = false; + + // CPU Metrics (app frames only) + double msCPUBusy = 0; + double msCPUWait = 0; + double msCPUTime = 0; + + // GPU Metrics (app frames only) + double msGPULatency = 0; + double msGPUBusy = 0; + double msVideoBusy = 0; + double msGPUWait = 0; + double msGPUTime = 0; + + // Input Latency (optional, app+displayed only) + std::optional msClickToPhotonLatency = {}; + std::optional msAllInputPhotonLatency = {}; + std::optional msInstrumentedInputTime; + + // Animation (optional, app+displayed only) + std::optional msAnimationError = {}; + std::optional msAnimationTime = {}; + + // Instrumented Metrics (optional) + std::optional msInstrumentedLatency = {}; + std::optional msInstrumentedRenderLatency = {}; + std::optional msInstrumentedSleep = {}; + std::optional msInstrumentedGpuLatency = {}; + std::optional msPcLatency = {}; + std::optional msBetweenSimStarts = {}; + + // PCLatency (optional) + std::optional msFlipDelay = {}; // NVIDIA + + // Frame Classification + FrameType frameType = {}; + + // Present Metadata + uint64_t swapChainAddress = 0; + Runtime runtime = {}; + int32_t syncInterval = 0; + uint32_t presentFlags = 0; + bool allowsTearing = false; + PresentMode presentMode = {}; + }; +} diff --git a/IntelPresentMon/CommonUtilities/mc/SwapChainState.cpp b/IntelPresentMon/CommonUtilities/mc/SwapChainState.cpp new file mode 100644 index 000000000..964fd2821 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/SwapChainState.cpp @@ -0,0 +1,108 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#include "SwapChainState.h" +#include "../PresentData/PresentMonTraceConsumer.hpp" + +namespace pmon::util::metrics { + + void SwapChainCoreState::UpdateAfterPresent(const FrameData& present) + { + const auto finalState = present.finalState; + const size_t displayCnt = present.displayed.Size(); + + if (finalState == PresentResult::Presented) { + if (displayCnt > 0) { + const size_t lastIdx = displayCnt - 1; + const auto lastType = present.displayed[lastIdx].first; + const bool lastIsAppFrm = + (lastType == FrameType::NotSet || lastType == FrameType::Application); + + if (lastIsAppFrm) { + const uint64_t lastScreenTime = present.displayed[lastIdx].second; + + if (animationErrorSource == AnimationErrorSource::AppProvider) { + lastDisplayedSimStartTime = present.appSimStartTime; + if (firstAppSimStartTime == 0) { + firstAppSimStartTime = present.appSimStartTime; + } + lastDisplayedAppScreenTime = lastScreenTime; + } + else if (animationErrorSource == AnimationErrorSource::PCLatency) { + // In the case of PCLatency only set values if PCL sim start time is non-zero + if (present.pclSimStartTime != 0) { + lastDisplayedSimStartTime = present.pclSimStartTime; + if (firstAppSimStartTime == 0) { + firstAppSimStartTime = present.pclSimStartTime; + } + lastDisplayedAppScreenTime = lastScreenTime; + } + } + else { // AnimationErrorSource::CpuStart + // Check for PCL or App sim start and possibly change source. + if (present.pclSimStartTime != 0) { + animationErrorSource = AnimationErrorSource::PCLatency; + lastDisplayedSimStartTime = present.pclSimStartTime; + if (firstAppSimStartTime == 0) { + firstAppSimStartTime = present.pclSimStartTime; + } + lastDisplayedAppScreenTime = lastScreenTime; + } + else if (present.appSimStartTime != 0) { + animationErrorSource = AnimationErrorSource::AppProvider; + lastDisplayedSimStartTime = present.appSimStartTime; + if (firstAppSimStartTime == 0) { + firstAppSimStartTime = present.appSimStartTime; + } + lastDisplayedAppScreenTime = lastScreenTime; + } + else { + // Fall back to CPU start from last app present, if any. + if (lastAppPresent.has_value()) { + const FrameData& lastApp = lastAppPresent.value(); + lastDisplayedSimStartTime = + lastApp.presentStartTime + lastApp.timeInPresent; + } + lastDisplayedAppScreenTime = lastScreenTime; + } + } + } + } + + // Always track "last displayed" screen time + flip delay when presented. + if (displayCnt > 0) { + const size_t lastIdx = displayCnt - 1; + lastDisplayedScreenTime = present.displayed[lastIdx].second; + lastDisplayedFlipDelay = present.flipDelay; + } + else { + lastDisplayedScreenTime = 0; + lastDisplayedFlipDelay = 0; + } + } + + // Last app present selection (same logic as UpdateChain) + if (displayCnt > 0) { + const size_t lastIdx = displayCnt - 1; + const auto lastType = present.displayed[lastIdx].first; + if (lastType == FrameType::NotSet || lastType == FrameType::Application) { + lastAppPresent = present; + } + } + else { + // If not displayed at all, treat this as the last app present. + lastAppPresent = present; + } + + // Last simulation start time: PCL wins over App if both exist. + if (present.pclSimStartTime != 0) { + lastSimStartTime = present.pclSimStartTime; + } + else if (present.appSimStartTime != 0) { + lastSimStartTime = present.appSimStartTime; + } + + // Always advance lastPresent + lastPresent = present; + } + +} // namespace pmon::util::metrics diff --git a/IntelPresentMon/CommonUtilities/mc/SwapChainState.h b/IntelPresentMon/CommonUtilities/mc/SwapChainState.h new file mode 100644 index 000000000..74965d064 --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/SwapChainState.h @@ -0,0 +1,73 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#pragma once + +#include +#include "MetricsTypes.h" + +namespace pmon::util::metrics { + +struct SwapChainCoreState { + + // Historical Presents + + // The most recent present that has been processed (e.g., output into CSV and/or used for frame + // statistics). + std::optional lastPresent; + + // The most recent app present that has been processed (e.g., output into CSV and/or used for frame + // statistics). + std::optional lastAppPresent; + + // Timing State + + // QPC of the last simulation start time regardless of whether it was displayed or not + uint64_t lastSimStartTime = 0; + + // The simulation start time of the most recent displayed frame + uint64_t lastDisplayedSimStartTime = 0; + + // The screen time of the most recent displayed frame (any type) + uint64_t lastDisplayedScreenTime = 0; + + // The screen time of the most recent displayed application frame + uint64_t lastDisplayedAppScreenTime = 0; + + // QPC of the first received simulation start time from the application provider + uint64_t firstAppSimStartTime = 0; + + // Dropped Frame Input Tracking + + // QPC of last received all-input that did not make it to the screen (dropped Present) + uint64_t lastReceivedNotDisplayedAllInputTime = 0; + + // QPC of last received mouse-click input that did not make it to the screen (dropped Present) + uint64_t lastReceivedNotDisplayedMouseClickTime = 0; + + // QPC of the last received app provider input that did not make it to the screen (dropped Present) + uint64_t lastReceivedNotDisplayedAppProviderInputTime = 0; + + // QPC of last received PC Latency simulation start that did not make it to the screen (dropped Present) + uint64_t lastReceivedNotDisplayedPclSimStart = 0; + + // QPC of last received PC Latency input time that did not make it to the screen (dropped Present) + uint64_t lastReceivedNotDisplayedPclInputTime = 0; + + // Animation Error Configuration + AnimationErrorSource animationErrorSource = AnimationErrorSource::CpuStart; + + // PC Latency Accumulation + + // Accumulated PC latency input to frame start time due to dropped Present() calls + double accumulatedInput2FrameStartTime = 0.0; + + // Input to Frame Start EMA + double Input2FrameStartTimeEma = 0.0; + + // NVIDIA Specific Tracking + uint64_t lastDisplayedFlipDelay = 0; + + void UpdateAfterPresent(const FrameData& present); +}; + +} \ No newline at end of file diff --git a/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.cpp b/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.cpp new file mode 100644 index 000000000..bfcedf82c --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.cpp @@ -0,0 +1,113 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#include "MetricsTypes.h" +#include "UnifiedSwapChain.h" +#include "../PresentData/PresentMonTraceConsumer.hpp" + +#include + + +namespace pmon::util::metrics +{ + uint64_t UnifiedSwapChain::GetLastPresentQpc() const + { + return swapChain.lastPresent.has_value() ? swapChain.lastPresent->presentStartTime : 0; + } + + bool UnifiedSwapChain::IsPrunableBefore(uint64_t minTimestampQpc) const + { + auto const last = GetLastPresentQpc(); + return last != 0 && last < minTimestampQpc; + } + + void UnifiedSwapChain::SanitizeDisplayedRepeatedPresents(FrameData& present) + { + // Port of OutputThread.cpp::ReportMetrics() "Remove Repeated flips" pre-pass, + // but applied to FrameData (so we do not mutate PresentEvent). + auto& d = present.displayed; + for (size_t i = 0; i + 1 < d.Size(); ) { + const auto a = d[i].first; + const auto b = d[i + 1].first; + + const bool app_then_rep = (a == FrameType::Application && b == FrameType::Repeated); + const bool rep_then_app = (a == FrameType::Repeated && b == FrameType::Application); + + if (app_then_rep) { + d.Erase(d.begin() + i + 1); + } + else if (rep_then_app) { + d.Erase(d.begin() + i); + } + else { + ++i; + } + } + } + + void UnifiedSwapChain::SeedFromFirstPresent(FrameData present) + { + // Mirror console baseline behavior: + // first present just seeds history (no pending pipeline). + swapChain.UpdateAfterPresent(present); + } + + // UnifiedSwapChain.cpp + std::vector + UnifiedSwapChain::Enqueue(FrameData present, MetricsVersion version) + { + SanitizeDisplayedRepeatedPresents(present); + + std::vector out; + + // V1: FIFO (no buffering / no look-ahead). Every present is ready immediately. + if (version == MetricsVersion::V1) { + waitingDisplayed.reset(); + blocked.clear(); + out.push_back(ReadyItem{ std::move(present), nullptr, nullptr }); + return out; + } + + // Seed baseline + if (!swapChain.lastPresent.has_value()) { + SeedFromFirstPresent(std::move(present)); + return out; + } + + const bool isDisplayed = + (present.finalState == PresentResult::Presented) && + (!present.displayed.Empty()); + + if (isDisplayed) { + // 1) Finalize previously waiting displayed (if any), pointing at swapchain-owned next displayed. + if (waitingDisplayed.has_value()) { + FrameData prev = std::move(*waitingDisplayed); + waitingDisplayed = std::move(present); + + out.push_back(ReadyItem{ std::move(prev), nullptr, &*waitingDisplayed }); + } + else { + // First displayed: becomes the waitingDisplayed_. + waitingDisplayed = std::move(present); + } + + // 2) Release blocked not-displayed frames (owned, no look-ahead). + while (!blocked.empty()) { + out.push_back(ReadyItem{ std::move(blocked.front()), nullptr, nullptr }); + blocked.pop_front(); + } + + // 3) Current displayed is ready (all-but-last); provide a pointer so NV adjustments persist. + out.push_back(ReadyItem{ FrameData{}, &*waitingDisplayed, nullptr }); + return out; + } + + // Not displayed + if (waitingDisplayed.has_value()) { + blocked.push_back(std::move(present)); + return out; // nothing ready yet + } + + out.push_back(ReadyItem{ std::move(present), nullptr, nullptr }); + return out; + } +} diff --git a/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.h b/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.h new file mode 100644 index 000000000..71677df8b --- /dev/null +++ b/IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.h @@ -0,0 +1,49 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#pragma once + +#include +#include +#include +#include + +#include "SwapChainState.h" +#include "MetricsCalculator.h" // ComputeMetricsForPresent() + +namespace pmon::util::metrics +{ + struct UnifiedSwapChain + { + struct ReadyItem + { + FrameData present; // owned (used when presentPtr==nullptr) + FrameData* presentPtr = nullptr; // points into waitingDisplayed_ (optional) + FrameData* nextDisplayedPtr = nullptr; // points into waitingDisplayed_ (optional) + }; + + SwapChainCoreState swapChain; + + // Seed without needing a QPC converter (needed for console GetPresentProcessInfo() early-return). + void SeedFromFirstPresent(FrameData present); + + std::vector Enqueue(FrameData present, MetricsVersion version); + + uint64_t GetLastPresentQpc() const; + bool IsPrunableBefore(uint64_t minTimestampQpc) const; + + // Frame statistics + float avgCPUDuration = 0.f; + float avgGPUDuration = 0.f; + float avgDisplayLatency = 0.f; + float avgDisplayedTime = 0.f; + float avgMsUntilDisplayed = 0.f; + float avgMsBetweenDisplayChange = 0.f; + double emaInput2FrameStartTime = 0.f; + double accumulatedInput2FrameStartTime = 0.f; + + private: + static void SanitizeDisplayedRepeatedPresents(FrameData& present); + std::optional waitingDisplayed; + std::deque blocked; + }; +} diff --git a/IntelPresentMon/CommonUtilities/reg/Registry.h b/IntelPresentMon/CommonUtilities/reg/Registry.h index 98118106d..963fa0436 100644 --- a/IntelPresentMon/CommonUtilities/reg/Registry.h +++ b/IntelPresentMon/CommonUtilities/reg/Registry.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include @@ -29,19 +29,17 @@ namespace pmon::util::reg {} std::optional AsOptional() const noexcept { + if (!Exists()) { + return {}; + } try { return Get(); } catch (const RegistryNotOpen&) { pmlog_warn("Optional access of registry value when key not open"); } - catch (const winreg::RegException& e) { - if (e.code().value() == 2) { - pmlog_dbg("Optional access of absent value"); - } - else { - pmlog_error(ReportException("Error during optional access of registry value")); - } + catch (const winreg::RegException&) { + pmlog_error(ReportException("Error during optional access of registry value")); } catch (...) { pmlog_error(ReportException("Unknown error during optional access of registry value")); @@ -209,4 +207,4 @@ namespace pmon::util::reg private: inline static bool readonly_ = false; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/CommonUtilities/win/Event.h b/IntelPresentMon/CommonUtilities/win/Event.h index d0a149961..827f3a2bf 100644 --- a/IntelPresentMon/CommonUtilities/win/Event.h +++ b/IntelPresentMon/CommonUtilities/win/Event.h @@ -27,11 +27,12 @@ namespace pmon::util::win std::optional WaitOnMultipleEvents(std::span events, bool waitAll = false, uint32_t milli = 0xFFFF'FFFF); template concept Eventable = std::derived_from || std::same_as; + // TODO: this can just return a non-optional more simply template - std::optional WaitAnyEvent(const E&...events) + uint32_t WaitAnyEvent(const E&...events) { Event::HandleType handles[] = { events... }; - return WaitOnMultipleEvents(handles); + return *WaitOnMultipleEvents(handles); } template diff --git a/IntelPresentMon/CommonUtilities/win/WinAPI.h b/IntelPresentMon/CommonUtilities/win/WinAPI.h index f91ca947a..b9888c02a 100644 --- a/IntelPresentMon/CommonUtilities/win/WinAPI.h +++ b/IntelPresentMon/CommonUtilities/win/WinAPI.h @@ -1,9 +1,11 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #define _WIN32_WINNT 0x0603 #include #define WIN32_LEAN_AND_MEAN #define NOMINMAX +#ifndef STRICT #define STRICT -#include \ No newline at end of file +#endif +#include diff --git a/IntelPresentMon/ControlLib/AmdPowerTelemetryAdapter.cpp b/IntelPresentMon/ControlLib/AmdPowerTelemetryAdapter.cpp index 6f1e32aac..8521cae01 100644 --- a/IntelPresentMon/ControlLib/AmdPowerTelemetryAdapter.cpp +++ b/IntelPresentMon/ControlLib/AmdPowerTelemetryAdapter.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #include "AmdPowerTelemetryAdapter.h" #include "Logging.h" @@ -25,9 +25,10 @@ int operator>>(AmdResultGrabber g, AmdCheckerToken) noexcept { } AmdPowerTelemetryAdapter::AmdPowerTelemetryAdapter( - const Adl2Wrapper* adl2_wrapper, std::string adl_adapter_name, + uint32_t deviceId, const Adl2Wrapper* adl2_wrapper, std::string adl_adapter_name, int adl_adapter_index, int overdrive_version) - : adl2_(adl2_wrapper), + : PowerTelemetryAdapter(deviceId), + adl2_(adl2_wrapper), name_(std::move(adl_adapter_name)), adl_adapter_index_(adl_adapter_index), overdrive_version_(overdrive_version) {} @@ -53,6 +54,7 @@ bool AmdPowerTelemetryAdapter::GetVideoMemoryInfo(uint64_t& gpu_mem_size, uint64 return success; } + bool AmdPowerTelemetryAdapter::GetSustainedPowerLimit(double& sustainedPowerLimit) const noexcept { sustainedPowerLimit = 0.f; if (overdrive_version_ == 5) { @@ -114,7 +116,7 @@ double AmdPowerTelemetryAdapter::GetSustainedPowerLimit() const noexcept { return sustainedPowerLimit; } -bool AmdPowerTelemetryAdapter::Sample() noexcept { +PresentMonPowerTelemetryInfo AmdPowerTelemetryAdapter::Sample() noexcept { LARGE_INTEGER qpc; QueryPerformanceCounter(&qpc); @@ -154,11 +156,8 @@ bool AmdPowerTelemetryAdapter::Sample() noexcept { } } - // Insert telemetry into history - std::lock_guard lock{history_mutex_}; - history_.Push(info); - - return sample_return; + (void)sample_return; + return info; } bool AmdPowerTelemetryAdapter::Overdrive5Sample( @@ -461,12 +460,6 @@ bool AmdPowerTelemetryAdapter::Overdrive8Sample( } } -std::optional AmdPowerTelemetryAdapter::GetClosest( - uint64_t qpc) const noexcept { - std::lock_guard lock(history_mutex_); - return history_.GetNearest(qpc); -} - PM_DEVICE_VENDOR AmdPowerTelemetryAdapter::GetVendor() const noexcept { return PM_DEVICE_VENDOR::PM_DEVICE_VENDOR_AMD; } @@ -475,4 +468,4 @@ std::string AmdPowerTelemetryAdapter::GetName() const noexcept { return name_; } -} // namespace pwr::amd \ No newline at end of file +} // namespace pwr::amd diff --git a/IntelPresentMon/ControlLib/AmdPowerTelemetryAdapter.h b/IntelPresentMon/ControlLib/AmdPowerTelemetryAdapter.h index bd63d95ad..45361e5ee 100644 --- a/IntelPresentMon/ControlLib/AmdPowerTelemetryAdapter.h +++ b/IntelPresentMon/ControlLib/AmdPowerTelemetryAdapter.h @@ -1,10 +1,8 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once -#include #include #include "PowerTelemetryAdapter.h" -#include "TelemetryHistory.h" #include "Adl2Wrapper.h" namespace pwr::amd { @@ -20,11 +18,9 @@ int operator>>(AmdResultGrabber, AmdCheckerToken) noexcept; class AmdPowerTelemetryAdapter : public PowerTelemetryAdapter { public: - AmdPowerTelemetryAdapter(const Adl2Wrapper* adl_wrapper, std::string adl_adapter_name, + AmdPowerTelemetryAdapter(uint32_t deviceId, const Adl2Wrapper* adl_wrapper, std::string adl_adapter_name, int adl_adapter_index, int overdrive_version); - bool Sample() noexcept override; - std::optional GetClosest( - uint64_t qpc) const noexcept override; + PresentMonPowerTelemetryInfo Sample() noexcept override; PM_DEVICE_VENDOR GetVendor() const noexcept override; std::string GetName() const noexcept override; uint64_t GetDedicatedVideoMemory() const noexcept override; @@ -43,8 +39,5 @@ class AmdPowerTelemetryAdapter : public PowerTelemetryAdapter { int adl_adapter_index_ = 0; int overdrive_version_ = 0; std::string name_ = "Unknown Adapter Name"; - mutable std::mutex history_mutex_; - TelemetryHistory history_{ - PowerTelemetryAdapter::defaultHistorySize}; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/ControlLib/AmdPowerTelemetryProvider.cpp b/IntelPresentMon/ControlLib/AmdPowerTelemetryProvider.cpp index b15163ecc..8df1b950c 100644 --- a/IntelPresentMon/ControlLib/AmdPowerTelemetryProvider.cpp +++ b/IntelPresentMon/ControlLib/AmdPowerTelemetryProvider.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #include #include @@ -10,7 +10,7 @@ namespace pwr::amd { -AmdPowerTelemetryProvider::AmdPowerTelemetryProvider() { +AmdPowerTelemetryProvider::AmdPowerTelemetryProvider(DeviceIdAllocator& allocator) { int count = 0; if (!adl_.Ok(adl_.Adapter_NumberOfAdapters_Get(&count))) { throw std::runtime_error{"Failed to get number of amd gpus"}; @@ -67,7 +67,7 @@ AmdPowerTelemetryProvider::AmdPowerTelemetryProvider() { try { std::string adl_adapter_name = ai.strAdapterName; adapter_ptrs_.push_back(std::make_shared( - &adl_, adl_adapter_name, ai.iAdapterIndex, overdrive_version)); + allocator.Next(), &adl_, adl_adapter_name, ai.iAdapterIndex, overdrive_version)); } catch (const std::exception& e) { TELE_ERR(e.what()); } catch (...) { @@ -91,4 +91,4 @@ uint32_t AmdPowerTelemetryProvider::GetAdapterCount() const noexcept { return (uint32_t)adapter_ptrs_.size(); } -} // namespace pwr::amd \ No newline at end of file +} // namespace pwr::amd diff --git a/IntelPresentMon/ControlLib/AmdPowerTelemetryProvider.h b/IntelPresentMon/ControlLib/AmdPowerTelemetryProvider.h index 767dded22..c79fb500d 100644 --- a/IntelPresentMon/ControlLib/AmdPowerTelemetryProvider.h +++ b/IntelPresentMon/ControlLib/AmdPowerTelemetryProvider.h @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #define NOMINMAX @@ -8,13 +8,13 @@ #include #include "PresentMonPowerTelemetry.h" #include "PowerTelemetryProvider.h" -#include "TelemetryHistory.h" #include "Adl2Wrapper.h" +#include "DeviceIdAllocator.h" namespace pwr::amd { class AmdPowerTelemetryProvider : public PowerTelemetryProvider { public: - AmdPowerTelemetryProvider(); + explicit AmdPowerTelemetryProvider(DeviceIdAllocator& allocator); AmdPowerTelemetryProvider(const AmdPowerTelemetryProvider& t) = delete; AmdPowerTelemetryProvider& operator=(const AmdPowerTelemetryProvider& t) = delete; ~AmdPowerTelemetryProvider() override; @@ -27,4 +27,4 @@ class AmdPowerTelemetryProvider : public PowerTelemetryProvider { std::vector adl_adapter_infos_; std::vector> adapter_ptrs_; }; -} // namespace pwr::amd \ No newline at end of file +} // namespace pwr::amd diff --git a/IntelPresentMon/ControlLib/ControlLib.vcxproj b/IntelPresentMon/ControlLib/ControlLib.vcxproj index 80e6a3462..d1f621714 100644 --- a/IntelPresentMon/ControlLib/ControlLib.vcxproj +++ b/IntelPresentMon/ControlLib/ControlLib.vcxproj @@ -133,7 +133,6 @@ - @@ -156,11 +155,11 @@ - + {66e9f6c5-28db-4218-81b9-31e0e146ecc0} - \ No newline at end of file + diff --git a/IntelPresentMon/ControlLib/ControlLib.vcxproj.filters b/IntelPresentMon/ControlLib/ControlLib.vcxproj.filters index f69441acf..b2762a988 100644 --- a/IntelPresentMon/ControlLib/ControlLib.vcxproj.filters +++ b/IntelPresentMon/ControlLib/ControlLib.vcxproj.filters @@ -74,7 +74,6 @@ - Intel @@ -130,4 +129,4 @@ Intel - \ No newline at end of file + diff --git a/IntelPresentMon/ControlLib/CpuTelemetry.h b/IntelPresentMon/ControlLib/CpuTelemetry.h index 4da83714c..dcee0bd3c 100644 --- a/IntelPresentMon/ControlLib/CpuTelemetry.h +++ b/IntelPresentMon/ControlLib/CpuTelemetry.h @@ -1,8 +1,7 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once -#include #include #include #include @@ -16,9 +15,7 @@ namespace pwr::cpu { class CpuTelemetry { public: virtual ~CpuTelemetry() = default; - virtual bool Sample() noexcept = 0; - virtual std::optional GetClosest( - uint64_t qpc) const noexcept = 0; + virtual CpuTelemetryInfo Sample() noexcept = 0; void SetTelemetryCapBit(CpuTelemetryCapBits telemetryCapBit) noexcept { cpuTelemetryCapBits_.set(static_cast(telemetryCapBit)); @@ -31,9 +28,6 @@ class CpuTelemetry { std::string GetCpuName(); double GetCpuPowerLimit() { return 0.; } - // constants - static constexpr size_t defaultHistorySize = 300; - // data private: bool ExecuteWQLProcessorNameQuery(std::wstring& processor_name); @@ -42,4 +36,4 @@ class CpuTelemetry { cpuTelemetryCapBits_{}; std::string cpu_name_; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/ControlLib/CpuTelemetryInfo.h b/IntelPresentMon/ControlLib/CpuTelemetryInfo.h index b8a746e83..23c2fa475 100644 --- a/IntelPresentMon/ControlLib/CpuTelemetryInfo.h +++ b/IntelPresentMon/ControlLib/CpuTelemetryInfo.h @@ -21,6 +21,7 @@ enum class CpuTelemetryCapBits { cpu_power_limit, cpu_temperature, cpu_frequency, + cpu_core_utility, cpu_telemetry_count }; diff --git a/IntelPresentMon/ControlLib/DeviceIdAllocator.h b/IntelPresentMon/ControlLib/DeviceIdAllocator.h new file mode 100644 index 000000000..1e1e7f482 --- /dev/null +++ b/IntelPresentMon/ControlLib/DeviceIdAllocator.h @@ -0,0 +1,24 @@ +// Copyright (C) 2024 Intel Corporation +// SPDX-License-Identifier: MIT +#pragma once + +#include +#include + +namespace pwr +{ + class DeviceIdAllocator + { + public: + explicit DeviceIdAllocator(uint32_t startId = 1) noexcept + : nextId_{ startId } {} + + uint32_t Next() noexcept + { + return nextId_.fetch_add(1, std::memory_order_relaxed); + } + + private: + std::atomic nextId_; + }; +} diff --git a/IntelPresentMon/ControlLib/IntelPowerTelemetryAdapter.cpp b/IntelPresentMon/ControlLib/IntelPowerTelemetryAdapter.cpp index eea66b1af..d15805188 100644 --- a/IntelPresentMon/ControlLib/IntelPowerTelemetryAdapter.cpp +++ b/IntelPresentMon/ControlLib/IntelPowerTelemetryAdapter.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #include "IntelPowerTelemetryAdapter.h" #include "Logging.h" @@ -71,21 +71,23 @@ namespace pwr::intel // public interface functions - IntelPowerTelemetryAdapter::IntelPowerTelemetryAdapter(ctl_device_adapter_handle_t handle) + IntelPowerTelemetryAdapter::IntelPowerTelemetryAdapter(uint32_t deviceId, ctl_device_adapter_handle_t handle) : + PowerTelemetryAdapter(deviceId), deviceHandle{ handle } { properties = { .Size = sizeof(ctl_device_adapter_properties_t), - .pDeviceID = &deviceId, - .device_id_size = sizeof(deviceId), + .pDeviceID = &deviceLuid, + .device_id_size = sizeof(deviceLuid), }; if (auto result = ctlGetDeviceProperties(deviceHandle, &properties); result != CTL_RESULT_SUCCESS) { throw std::runtime_error{ "Failure to get device properties" }; } - pmlog_verb(v::tele_gpu)("Device properties").pmwatch(ref::DumpGenerated(properties)); + pmlog_verb(v::tele_gpu)("Device properties").pmwatch(ref::DumpGenerated(properties)) + .pmwatch(deviceLuid.HighPart).pmwatch(deviceLuid.LowPart); if (properties.device_type != CTL_DEVICE_TYPE_GRAPHICS) { throw NonGraphicsDeviceException{}; @@ -151,13 +153,16 @@ namespace pwr::intel } } - bool IntelPowerTelemetryAdapter::Sample() noexcept + PresentMonPowerTelemetryInfo IntelPowerTelemetryAdapter::Sample() noexcept { - pmlog_verb(v::tele_gpu)("Sample called").pmwatch(GetName()); + pmlog_verb(v::tele_gpu)("Telemetry update called").pmwatch(GetName()); LARGE_INTEGER qpc; QueryPerformanceCounter(&qpc); bool success = true; + PresentMonPowerTelemetryInfo sample{ + .qpc = (uint64_t)qpc.QuadPart, + }; decltype(previousSampleVariant) currentSampleVariant; @@ -253,7 +258,7 @@ namespace pwr::intel } else { success = GatherSampleData(*currentSample, memory_state, - memory_bandwidth, gpu_sustained_power_limit_mw, (uint64_t)qpc.QuadPart) && success; + memory_bandwidth, gpu_sustained_power_limit_mw, (uint64_t)qpc.QuadPart, sample) && success; } } else { @@ -265,26 +270,12 @@ namespace pwr::intel } else { success = GatherSampleData(*currentSample, memory_state, - memory_bandwidth, gpu_sustained_power_limit_mw, (uint64_t)qpc.QuadPart) && success; + memory_bandwidth, gpu_sustained_power_limit_mw, (uint64_t)qpc.QuadPart, sample) && success; } } - return success; - } - - std::optional IntelPowerTelemetryAdapter::GetClosest(uint64_t qpc) const noexcept - { - std::lock_guard lock(historyMutex); - const auto nearest = history.GetNearest(qpc); - if constexpr (PMLOG_BUILD_LEVEL_ >= pmon::util::log::Level::Verbose) { - if (!nearest) { - pmlog_verb(v::tele_gpu)("Empty telemetry info sample returned").pmwatch(GetName()).pmwatch(qpc); - } - else { - pmlog_verb(v::tele_gpu)("Nearest telemetry info sampled").pmwatch(GetName()).pmwatch(qpc).pmwatch(ref::DumpStatic(*nearest)); - } - } - return nearest; + (void)success; + return sample; } PM_DEVICE_VENDOR IntelPowerTelemetryAdapter::GetVendor() const noexcept @@ -360,8 +351,8 @@ namespace pwr::intel // LUID is a struct with LowPart (DWORD) and HighPart (LONG) // We pack it into a uint64_t uint64_t id = 0; - id |= (uint64_t)deviceId.LowPart; - id |= (uint64_t)deviceId.HighPart << 32; + id |= (uint64_t)deviceLuid.LowPart; + id |= (uint64_t)deviceLuid.HighPart << 32; return id; } // private implementation functions @@ -396,7 +387,8 @@ namespace pwr::intel ctl_mem_state_t& memory_state, ctl_mem_bandwidth_t& memory_bandwidth, std::optional gpu_sustained_power_limit_mw, - uint64_t qpc) + uint64_t qpc, + PresentMonPowerTelemetryInfo& sample) { bool success = true; @@ -407,33 +399,33 @@ namespace pwr::intel IGCL_ERR(result); } - PresentMonPowerTelemetryInfo pm_gpu_power_telemetry_info{ .qpc = qpc }; + sample.qpc = qpc; if (previousSampleVariant.index()) { if (const auto result = GetGPUPowerTelemetryData( - currentSample, pm_gpu_power_telemetry_info); result != CTL_RESULT_SUCCESS) + currentSample, sample); result != CTL_RESULT_SUCCESS) { success = false; IGCL_ERR(result); } if (const auto result = GetVramPowerTelemetryData( - currentSample, pm_gpu_power_telemetry_info); result != CTL_RESULT_SUCCESS) + currentSample, sample); result != CTL_RESULT_SUCCESS) { success = false; IGCL_ERR(result); } if (const auto result = GetFanPowerTelemetryData(currentSample, - pm_gpu_power_telemetry_info); result != CTL_RESULT_SUCCESS) + sample); result != CTL_RESULT_SUCCESS) { success = false; IGCL_ERR(result); } if (const auto result = GetPsuPowerTelemetryData( - currentSample, pm_gpu_power_telemetry_info); result != CTL_RESULT_SUCCESS) + currentSample, sample); result != CTL_RESULT_SUCCESS) { success = false; IGCL_ERR(result); @@ -442,21 +434,20 @@ namespace pwr::intel // Get memory state and bandwidth data if (memoryModules.size() > 0) { GetMemStateTelemetryData(memory_state, - pm_gpu_power_telemetry_info); + sample); GetMemBandwidthData(memory_bandwidth, - pm_gpu_power_telemetry_info); + sample); } // Save and convert the gpu sustained power limit - pm_gpu_power_telemetry_info.gpu_sustained_power_limit_w = + sample.gpu_sustained_power_limit_w = gpu_sustained_power_limit_mw.value_or(0.) / 1000.; if (gpu_sustained_power_limit_mw) { SetTelemetryCapBit(GpuTelemetryCapBits::gpu_sustained_power_limit); } - // Save off the calculated PresentMon power telemetry values. These are - // saved off for clients to extrace out timing information based on QPC - SavePmPowerTelemetryData(pm_gpu_power_telemetry_info); + pmlog_verb(v::tele_gpu)("Gathered telemetry info sample") + .pmwatch(GetName()).pmwatch(ref::DumpStatic(sample)); } // Save off the raw control library data for calculating time delta @@ -922,6 +913,7 @@ namespace pwr::intel return CTL_RESULT_SUCCESS; } + ctl_result_t IntelPowerTelemetryAdapter::GetPowerTelemetryItemUsage( const ctl_oc_telemetry_item_t& current_telemetry_item, const ctl_oc_telemetry_item_t& previous_telemetry_item, @@ -988,11 +980,4 @@ namespace pwr::intel return CTL_RESULT_SUCCESS; } - void IntelPowerTelemetryAdapter::SavePmPowerTelemetryData(PresentMonPowerTelemetryInfo& info) - { - pmlog_verb(v::tele_gpu)("Saving gathered telemetry info to history").pmwatch(GetName()).pmwatch(ref::DumpStatic(info)); - std::lock_guard lock(historyMutex); - history.Push(info); } - - } \ No newline at end of file diff --git a/IntelPresentMon/ControlLib/IntelPowerTelemetryAdapter.h b/IntelPresentMon/ControlLib/IntelPowerTelemetryAdapter.h index 4e6437912..3cc65e104 100644 --- a/IntelPresentMon/ControlLib/IntelPowerTelemetryAdapter.h +++ b/IntelPresentMon/ControlLib/IntelPowerTelemetryAdapter.h @@ -1,13 +1,11 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #define NOMINMAX #include #include "igcl_api.h" #include "PowerTelemetryAdapter.h" -#include "TelemetryHistory.h" #include "ctlpvttemp_api.h" -#include #include #include @@ -16,9 +14,8 @@ namespace pwr::intel class IntelPowerTelemetryAdapter : public PowerTelemetryAdapter { public: - IntelPowerTelemetryAdapter(ctl_device_adapter_handle_t handle); - bool Sample() noexcept override; - std::optional GetClosest(uint64_t qpc) const noexcept override; + IntelPowerTelemetryAdapter(uint32_t deviceId, ctl_device_adapter_handle_t handle); + PresentMonPowerTelemetryInfo Sample() noexcept override; PM_DEVICE_VENDOR GetVendor() const noexcept override; std::string GetName() const noexcept override; uint64_t GetDedicatedVideoMemory() const noexcept override; @@ -38,7 +35,8 @@ namespace pwr::intel ctl_mem_state_t& memory_state, ctl_mem_bandwidth_t& memory_bandwidth, std::optional gpu_sustained_power_limit_mw, - uint64_t qpc); + uint64_t qpc, + PresentMonPowerTelemetryInfo& sample); ctl_result_t EnumerateMemoryModules(); @@ -70,7 +68,6 @@ namespace pwr::intel const ctl_mem_bandwidth_t& mem_bandwidth, PresentMonPowerTelemetryInfo& pm_gpu_power_telemetry_info); - void SavePmPowerTelemetryData(PresentMonPowerTelemetryInfo& pm_gpu_power_telemetry_info); template ctl_result_t SaveTelemetry( const T& power_telemetry, @@ -93,12 +90,10 @@ namespace pwr::intel GpuTelemetryCapBits telemetry_cap_bit); // data ctl_device_adapter_handle_t deviceHandle = nullptr; - LUID deviceId; // pointed to by a device_adapter_properties member, written to by igcl api + LUID deviceLuid; // pointed to by a device_adapter_properties member, written to by igcl api ctl_device_adapter_properties_t properties{}; std::vector memoryModules; std::vector powerDomains; - mutable std::mutex historyMutex; - TelemetryHistory history{ PowerTelemetryAdapter::defaultHistorySize }; SampleVariantType previousSampleVariant; bool useV1PowerTelemetry = true; bool useNewBandwidthTelemetry = true; @@ -117,4 +112,4 @@ namespace pwr::intel // populated on init, used to calculate fan % std::vector maxFanSpeedsRpm_; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/ControlLib/IntelPowerTelemetryProvider.cpp b/IntelPresentMon/ControlLib/IntelPowerTelemetryProvider.cpp index 918cefece..4faeb99b4 100644 --- a/IntelPresentMon/ControlLib/IntelPowerTelemetryProvider.cpp +++ b/IntelPresentMon/ControlLib/IntelPowerTelemetryProvider.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #include "IntelPowerTelemetryProvider.h" #include "IntelPowerTelemetryAdapter.h" @@ -12,7 +12,7 @@ using v = log::V; namespace pwr::intel { - IntelPowerTelemetryProvider::IntelPowerTelemetryProvider() + IntelPowerTelemetryProvider::IntelPowerTelemetryProvider(DeviceIdAllocator& allocator) { // TODO(megalvan): Currently using the default Id of all zeros. Do we need // to obtain a legit application Id or is default fine? @@ -57,7 +57,7 @@ namespace pwr::intel for (auto& handle : handles) { try { - adapterPtrs.push_back(std::make_shared(handle)); + adapterPtrs.push_back(std::make_shared(allocator.Next(), handle)); } catch (const IntelPowerTelemetryAdapter::NonGraphicsDeviceException&) {} catch (const std::exception& e) { TELE_ERR(e.what()); } @@ -85,4 +85,4 @@ namespace pwr::intel { return (uint32_t)adapterPtrs.size(); } -} \ No newline at end of file +} diff --git a/IntelPresentMon/ControlLib/IntelPowerTelemetryProvider.h b/IntelPresentMon/ControlLib/IntelPowerTelemetryProvider.h index c69c5d0c5..b51d3f332 100644 --- a/IntelPresentMon/ControlLib/IntelPowerTelemetryProvider.h +++ b/IntelPresentMon/ControlLib/IntelPowerTelemetryProvider.h @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #define NOMINMAX @@ -10,14 +10,14 @@ #include "igcl_api.h" #include "PresentMonPowerTelemetry.h" #include "PowerTelemetryProvider.h" -#include "TelemetryHistory.h" +#include "DeviceIdAllocator.h" namespace pwr::intel { class IntelPowerTelemetryProvider : public PowerTelemetryProvider { public: - IntelPowerTelemetryProvider(); + explicit IntelPowerTelemetryProvider(DeviceIdAllocator& allocator); IntelPowerTelemetryProvider(const IntelPowerTelemetryProvider& t) = delete; IntelPowerTelemetryProvider& operator=(const IntelPowerTelemetryProvider& t) = delete; ~IntelPowerTelemetryProvider() override; @@ -28,4 +28,4 @@ namespace pwr::intel ctl_api_handle_t apiHandle = nullptr; std::vector> adapterPtrs; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/ControlLib/NvidiaPowerTelemetryAdapter.cpp b/IntelPresentMon/ControlLib/NvidiaPowerTelemetryAdapter.cpp index a9b4c0826..24861179b 100644 --- a/IntelPresentMon/ControlLib/NvidiaPowerTelemetryAdapter.cpp +++ b/IntelPresentMon/ControlLib/NvidiaPowerTelemetryAdapter.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #define NOMINMAX #include @@ -10,11 +10,13 @@ namespace pwr::nv { NvidiaPowerTelemetryAdapter::NvidiaPowerTelemetryAdapter( + uint32_t deviceId, const NvapiWrapper* pNvapi, const NvmlWrapper* pNvml, NvPhysicalGpuHandle hGpuNvapi, std::optional hGpuNvml) : + PowerTelemetryAdapter(deviceId), nvapi{ pNvapi }, nvml{ pNvml }, hNvapi{ hGpuNvapi }, @@ -27,6 +29,8 @@ namespace pwr::nv } } + + uint64_t NvidiaPowerTelemetryAdapter::GetDedicatedVideoMemory() const noexcept { uint64_t video_mem_size = 0; nvmlMemory_t mem{}; @@ -48,7 +52,7 @@ namespace pwr::nv return 0.f; } - bool NvidiaPowerTelemetryAdapter::Sample() noexcept + PresentMonPowerTelemetryInfo NvidiaPowerTelemetryAdapter::Sample() noexcept { LARGE_INTEGER qpc; QueryPerformanceCounter(&qpc); @@ -190,17 +194,7 @@ namespace pwr::nv } } - // insert telemetry into history - std::lock_guard lock{ historyMutex }; - history.Push(info); - - return true; - } - - std::optional NvidiaPowerTelemetryAdapter::GetClosest(uint64_t qpc) const noexcept - { - std::lock_guard lock{ historyMutex }; - return history.GetNearest(qpc); + return info; } PM_DEVICE_VENDOR NvidiaPowerTelemetryAdapter::GetVendor() const noexcept diff --git a/IntelPresentMon/ControlLib/NvidiaPowerTelemetryAdapter.h b/IntelPresentMon/ControlLib/NvidiaPowerTelemetryAdapter.h index 060c22c91..efe7297fd 100644 --- a/IntelPresentMon/ControlLib/NvidiaPowerTelemetryAdapter.h +++ b/IntelPresentMon/ControlLib/NvidiaPowerTelemetryAdapter.h @@ -1,9 +1,7 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include "PowerTelemetryAdapter.h" -#include "TelemetryHistory.h" -#include #include #include "NvapiWrapper.h" #include "NvmlWrapper.h" @@ -14,12 +12,12 @@ namespace pwr::nv { public: NvidiaPowerTelemetryAdapter( + uint32_t deviceId, const NvapiWrapper* pNvapi, const NvmlWrapper* pNvml, NvPhysicalGpuHandle hGpuNvapi, std::optional hGpuNvml); - bool Sample() noexcept override; - std::optional GetClosest(uint64_t qpc) const noexcept override; + PresentMonPowerTelemetryInfo Sample() noexcept override; PM_DEVICE_VENDOR GetVendor() const noexcept override; std::string GetName() const noexcept override; uint64_t GetDedicatedVideoMemory() const noexcept override; @@ -33,8 +31,6 @@ namespace pwr::nv NvPhysicalGpuHandle hNvapi; std::optional hNvml; std::string name = "Unknown Adapter Name"; - mutable std::mutex historyMutex; - TelemetryHistory history{ PowerTelemetryAdapter::defaultHistorySize }; bool useNvmlTemperature = false; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/ControlLib/NvidiaPowerTelemetryProvider.cpp b/IntelPresentMon/ControlLib/NvidiaPowerTelemetryProvider.cpp index 32feb622a..93f412bbd 100644 --- a/IntelPresentMon/ControlLib/NvidiaPowerTelemetryProvider.cpp +++ b/IntelPresentMon/ControlLib/NvidiaPowerTelemetryProvider.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #include "NvidiaPowerTelemetryProvider.h" #include "NvidiaPowerTelemetryAdapter.h" @@ -7,7 +7,7 @@ namespace pwr::nv { - NvidiaPowerTelemetryProvider::NvidiaPowerTelemetryProvider() + NvidiaPowerTelemetryProvider::NvidiaPowerTelemetryProvider(DeviceIdAllocator& allocator) { // enumerate nvapi gpu device handles std::vector> nvapiHandlePairs; @@ -62,10 +62,10 @@ namespace pwr::nv // create adaptor object for each api adapter handle pair // lambda to factor out repeated code pattern adapter creation and insertion into container, with exception quelling - const auto TryCreateAddAdapter = [this](NvPhysicalGpuHandle nvapiHandle, std::optional nvmlHandle) { + const auto TryCreateAddAdapter = [this, &allocator](NvPhysicalGpuHandle nvapiHandle, std::optional nvmlHandle) { try { adapterPtrs.push_back(std::make_shared( - &nvapi, &nvml, nvapiHandle, nvmlHandle + allocator.Next(), &nvapi, &nvml, nvapiHandle, nvmlHandle )); } catch (const std::exception& e) { TELE_ERR(e.what()); } @@ -108,4 +108,4 @@ namespace pwr::nv { return (uint32_t)adapterPtrs.size(); } -} \ No newline at end of file +} diff --git a/IntelPresentMon/ControlLib/NvidiaPowerTelemetryProvider.h b/IntelPresentMon/ControlLib/NvidiaPowerTelemetryProvider.h index 5150ebda4..a778b7525 100644 --- a/IntelPresentMon/ControlLib/NvidiaPowerTelemetryProvider.h +++ b/IntelPresentMon/ControlLib/NvidiaPowerTelemetryProvider.h @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #define NOMINMAX @@ -9,16 +9,16 @@ #include #include "PresentMonPowerTelemetry.h" #include "PowerTelemetryProvider.h" -#include "TelemetryHistory.h" #include "NvapiWrapper.h" #include "NvmlWrapper.h" +#include "DeviceIdAllocator.h" namespace pwr::nv { class NvidiaPowerTelemetryProvider : public PowerTelemetryProvider { public: - NvidiaPowerTelemetryProvider(); + explicit NvidiaPowerTelemetryProvider(DeviceIdAllocator& allocator); const std::vector>& GetAdapters() noexcept override; uint32_t GetAdapterCount() const noexcept override; @@ -27,4 +27,4 @@ namespace pwr::nv NvmlWrapper nvml; std::vector> adapterPtrs; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/ControlLib/PowerTelemetryAdapter.cpp b/IntelPresentMon/ControlLib/PowerTelemetryAdapter.cpp index 35c045f6e..f50567f65 100644 --- a/IntelPresentMon/ControlLib/PowerTelemetryAdapter.cpp +++ b/IntelPresentMon/ControlLib/PowerTelemetryAdapter.cpp @@ -1,4 +1,4 @@ -#include "PowerTelemetryAdapter.h" +#include "PowerTelemetryAdapter.h" #include "Logging.h" #include "../CommonUtilities/ref/WrapReflect.h" #include "../CommonUtilities/ref/StaticReflection.h" @@ -12,6 +12,14 @@ namespace pwr using v = ::pmon::util::log::V; using ::pmon::util::log::GlobalPolicy; + PowerTelemetryAdapter::PowerTelemetryAdapter(uint32_t deviceId) noexcept + : deviceId_{ deviceId } {} + + uint32_t PowerTelemetryAdapter::GetDeviceId() const noexcept + { + return deviceId_; + } + void PowerTelemetryAdapter::SetTelemetryCapBit(GpuTelemetryCapBits telemetryCapBit) noexcept { if (GlobalPolicy::VCheck(v::tele_gpu)) { @@ -34,4 +42,4 @@ namespace pwr .pmwatch(GetName()).pmwatch(gpuTelemetryCapBits_.test(size_t(bit))); return gpuTelemetryCapBits_.test(size_t(bit)); } -} \ No newline at end of file +} diff --git a/IntelPresentMon/ControlLib/PowerTelemetryAdapter.h b/IntelPresentMon/ControlLib/PowerTelemetryAdapter.h index 7e226e727..c2157f526 100644 --- a/IntelPresentMon/ControlLib/PowerTelemetryAdapter.h +++ b/IntelPresentMon/ControlLib/PowerTelemetryAdapter.h @@ -4,8 +4,8 @@ #include #include -#include #include +#include #include "PresentMonPowerTelemetry.h" #include "../PresentMonAPI2/PresentMonAPI.h" @@ -18,22 +18,23 @@ namespace pwr using SetTelemetryCapBitset = std::bitset(GpuTelemetryCapBits::gpu_telemetry_count)>; // functions virtual ~PowerTelemetryAdapter() = default; - virtual bool Sample() noexcept = 0; - virtual std::optional GetClosest(uint64_t qpc) const noexcept = 0; + virtual PresentMonPowerTelemetryInfo Sample() noexcept = 0; virtual PM_DEVICE_VENDOR GetVendor() const noexcept = 0; virtual std::string GetName() const noexcept = 0; virtual uint64_t GetDedicatedVideoMemory() const noexcept = 0; virtual uint64_t GetVideoMemoryMaxBandwidth() const noexcept = 0; virtual double GetSustainedPowerLimit() const noexcept = 0; virtual uint64_t GetAdapterId() const noexcept { return 0; } + uint32_t GetDeviceId() const noexcept; void SetTelemetryCapBit(GpuTelemetryCapBits telemetryCapBit) noexcept; SetTelemetryCapBitset GetPowerTelemetryCapBits(); bool HasTelemetryCapBit(GpuTelemetryCapBits bit) const; - // constants - static constexpr size_t defaultHistorySize = 300; + protected: + explicit PowerTelemetryAdapter(uint32_t deviceId) noexcept; - private: + private: // data SetTelemetryCapBitset gpuTelemetryCapBits_{}; + const uint32_t deviceId_ = 0; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/ControlLib/PowerTelemetryProviderFactory.cpp b/IntelPresentMon/ControlLib/PowerTelemetryProviderFactory.cpp index 3724dc15d..eec1143c1 100644 --- a/IntelPresentMon/ControlLib/PowerTelemetryProviderFactory.cpp +++ b/IntelPresentMon/ControlLib/PowerTelemetryProviderFactory.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #include "PowerTelemetryProviderFactory.h" #include "IntelPowerTelemetryProvider.h" @@ -7,13 +7,13 @@ namespace pwr { - std::unique_ptr PowerTelemetryProviderFactory::Make(PM_DEVICE_VENDOR vendor) + std::unique_ptr PowerTelemetryProviderFactory::Make(PM_DEVICE_VENDOR vendor, DeviceIdAllocator& allocator) { switch (vendor) { - case PM_DEVICE_VENDOR_INTEL: return std::make_unique(); - case PM_DEVICE_VENDOR_NVIDIA: return std::make_unique(); - case PM_DEVICE_VENDOR_AMD: return std::make_unique (); + case PM_DEVICE_VENDOR_INTEL: return std::make_unique(allocator); + case PM_DEVICE_VENDOR_NVIDIA: return std::make_unique(allocator); + case PM_DEVICE_VENDOR_AMD: return std::make_unique(allocator); } return {}; } -} \ No newline at end of file +} diff --git a/IntelPresentMon/ControlLib/PowerTelemetryProviderFactory.h b/IntelPresentMon/ControlLib/PowerTelemetryProviderFactory.h index dda0ec2d9..37fa9d364 100644 --- a/IntelPresentMon/ControlLib/PowerTelemetryProviderFactory.h +++ b/IntelPresentMon/ControlLib/PowerTelemetryProviderFactory.h @@ -1,7 +1,8 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include "PowerTelemetryProvider.h" +#include "DeviceIdAllocator.h" #include "../PresentMonAPI2/PresentMonAPI.h" #include @@ -10,6 +11,6 @@ namespace pwr class PowerTelemetryProviderFactory { public: - static std::unique_ptr Make(PM_DEVICE_VENDOR vendor); + static std::unique_ptr Make(PM_DEVICE_VENDOR vendor, DeviceIdAllocator& allocator); }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/ControlLib/TelemetryHistory.h b/IntelPresentMon/ControlLib/TelemetryHistory.h deleted file mode 100644 index e573772f8..000000000 --- a/IntelPresentMon/ControlLib/TelemetryHistory.h +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once -#include "PresentMonPowerTelemetry.h" -#include "PowerTelemetryProvider.h" -#include - -namespace pwr -{ - template - class TelemetryHistory - { - public: - class ConstIterator - { - public: - using iterator_category = std::random_access_iterator_tag; - using value_type = T; - using reference = value_type&; - using pointer = value_type*; - using difference_type = ptrdiff_t; - - ConstIterator() = delete; - ~ConstIterator() = default; - ConstIterator(const TelemetryHistory* pContainer, size_t unwrapped_offset) noexcept : pContainer{ pContainer }, unwrapped_offset{ unwrapped_offset } {} - ConstIterator(const ConstIterator& rhs) noexcept : pContainer{ rhs.pContainer }, unwrapped_offset{ rhs.unwrapped_offset } {} - ConstIterator& operator=(const ConstIterator& rhs) noexcept - { - // Self-assignment check - if (this != &rhs) { - pContainer = rhs.pContainer; - unwrapped_offset = rhs.unwrapped_offset; - } - return *this; - } - ConstIterator& operator+=(difference_type rhs) noexcept - { - unwrapped_offset += rhs; - return *this; - } - ConstIterator& operator-=(difference_type rhs) noexcept - { - unwrapped_offset -= rhs; - return *this; - } - const value_type& operator*() const noexcept - { - return pContainer->buffer[WrapIndex_(unwrapped_offset + pContainer->indexBegin)]; - } - const value_type* operator->() const noexcept - { - return &**this; - } - const value_type& operator[](size_t rhs) const noexcept - { - return pContainer->buffer[WrapIndex_(unwrapped_offset + rhs + pContainer->indexBegin)]; - } - - ConstIterator& operator++() noexcept - { - ++unwrapped_offset; - return *this; - } - ConstIterator& operator--() noexcept - { - --unwrapped_offset; - return *this; - } - ConstIterator operator++(int) noexcept { ConstIterator tmp(*this); ++(*this); return tmp; } - ConstIterator operator--(int) noexcept { ConstIterator tmp(*this); --(*this); return tmp; } - difference_type operator-(const ConstIterator& rhs) const noexcept - { - return difference_type(unwrapped_offset) - difference_type(rhs.unwrapped_offset); - } - ConstIterator operator+(difference_type rhs) const noexcept - { - auto dup = *this; - return dup += rhs; - } - ConstIterator operator-(difference_type rhs) const noexcept - { - auto dup = *this; - return dup -= rhs; - } - - bool operator==(const ConstIterator& rhs) const noexcept { return unwrapped_offset == rhs.unwrapped_offset; } - bool operator!=(const ConstIterator& rhs) const noexcept { return unwrapped_offset != rhs.unwrapped_offset; } - bool operator>(const ConstIterator& rhs) const noexcept { return unwrapped_offset > rhs.unwrapped_offset; } - bool operator<(const ConstIterator& rhs) const noexcept { return unwrapped_offset < rhs.unwrapped_offset; } - bool operator>=(const ConstIterator& rhs) const noexcept { return unwrapped_offset >= rhs.unwrapped_offset; } - bool operator<=(const ConstIterator& rhs) const noexcept { return unwrapped_offset <= rhs.unwrapped_offset; } - private: - // functions - size_t GetSize_() const noexcept - { - return pContainer->buffer.size(); - } - // only wraps positive excursions - size_t WrapIndex_(size_t unwrapped) const noexcept - { - return unwrapped % GetSize_(); - } - - // data - const TelemetryHistory* pContainer; - size_t unwrapped_offset; - }; - - TelemetryHistory(size_t size) noexcept; - void Push(const T& info) noexcept; - std::optional GetNearest(uint64_t qpc) const noexcept; - ConstIterator begin() const noexcept; - ConstIterator end() const noexcept; - private: - std::vector buffer; - // indexBegin==buffer.size initial condition is necessary to indicate an empty container - // (begin==end would normally indicate empty, but in a cyclic container this is ambiguous) - size_t indexBegin; - size_t indexEnd = 0; - }; - - template - TelemetryHistory::TelemetryHistory(size_t size) noexcept - : - indexBegin{ size } - { - buffer.resize(size); - } - - template - void TelemetryHistory::Push(const T& info) noexcept - { - // indexBegin set to buffer size only in initial state 0 entries pushed - if (indexBegin == buffer.size()) - { - buffer[0] = info; - indexBegin = 0; - indexEnd = 1; - } - else if (indexBegin == indexEnd) - { - // full circle buffer, newest overwrites oldest - buffer[indexEnd] = info; - if (++indexEnd >= buffer.size()) - { - indexEnd = 0; - } - indexBegin = indexEnd; - } - else - { - // buffer still filling - buffer[indexEnd] = info; - if (++indexEnd >= buffer.size()) - { - indexEnd = 0; - } - } - } - - template - std::optional TelemetryHistory::GetNearest(uint64_t qpc) const noexcept - { - const auto begin = this->begin(); - const auto end = this->end(); - - // return nothing if history empty - if (begin == end) return {}; - - const auto last = end - 1; - - // if outside the qpc history range return the closest values - if (qpc > last->qpc) { - return *last; - } else if (qpc < begin->qpc) { - return *begin; - } - - // find lowest not less than query qpc - const auto i = std::lower_bound(begin, end, qpc, [](const auto& a, auto b) { - return a.qpc < b; - }); - - // if we're right on the money, no need to find closest among 2 neighboring - if (i->qpc == qpc) return { *i }; - - const auto distanceToLower = qpc - std::prev(i)->qpc; - const auto distanceToUpper = i->qpc - qpc; - - return distanceToUpper <= distanceToLower ? - std::optional{ *i } : - std::optional{ *std::prev(i) }; - } - - template - typename TelemetryHistory::ConstIterator TelemetryHistory::begin() const noexcept - { - return ConstIterator{ this, 0 }; - } - - template - typename TelemetryHistory::ConstIterator TelemetryHistory::end() const noexcept - { - if (indexBegin == indexEnd) // begin/end indices locked mean container full - { - return ConstIterator{ this, buffer.size() }; - } - else if (indexBegin == buffer.size()) // special case for empty container - { - return ConstIterator{ this, 0 }; - } - else - { - return ConstIterator{ this, indexEnd }; // not yet cycling (not ful capacity) - } - } -} \ No newline at end of file diff --git a/IntelPresentMon/ControlLib/WmiCpu.cpp b/IntelPresentMon/ControlLib/WmiCpu.cpp index df44cc906..34edc5964 100644 --- a/IntelPresentMon/ControlLib/WmiCpu.cpp +++ b/IntelPresentMon/ControlLib/WmiCpu.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #include #include "WmiCpu.h" @@ -48,7 +48,7 @@ WmiCpu::WmiCpu() { // Most counters require two sample values to display a formatted value. // PDH stores the current sample value and the previously collected // sample value. This call retrieves the first value that will be used - // by PdhGetFormattedCounterValue in the Sample() call. + // by PdhGetFormattedCounterValue in the next data collection. if (const auto result = PdhCollectQueryData(query_.get()); result != ERROR_SUCCESS) { throw std::runtime_error{ @@ -72,22 +72,27 @@ WmiCpu::WmiCpu() { // next_sample_qpc_.QuadPart += frequency_.QuadPart; } -bool WmiCpu::Sample() noexcept { + +CpuTelemetryInfo WmiCpu::Sample() noexcept { DWORD counter_type; LARGE_INTEGER qpc; QueryPerformanceCounter(&qpc); - if (qpc.QuadPart < next_sample_qpc_.QuadPart) { - return true; - } + const bool should_collect = qpc.QuadPart >= next_sample_qpc_.QuadPart; CpuTelemetryInfo info{ .qpc = (uint64_t)qpc.QuadPart, }; - if (const auto result = PdhCollectQueryData(query_.get()); - result != ERROR_SUCCESS) { - return false; + if (should_collect) { + if (const auto result = PdhCollectQueryData(query_.get()); + result != ERROR_SUCCESS) { + LOG(INFO) << "PdhCollectQueryData failed. Result:" << result << std::endl; + } else { + // Update the next sample qpc based on the current sample qpc + // and adding in the frequency + next_sample_qpc_.QuadPart = qpc.QuadPart + frequency_.QuadPart; + } } // Sample cpu clock. This is an approximation using the frequency and then @@ -133,21 +138,7 @@ bool WmiCpu::Sample() noexcept { } } - // insert telemetry into history - std::lock_guard lock{history_mutex_}; - history_.Push(info); - - // Update the next sample qpc based on the current sample qpc - // and adding in the frequency - next_sample_qpc_.QuadPart = qpc.QuadPart + frequency_.QuadPart; - - return true; + return info; } -std::optional WmiCpu::GetClosest(uint64_t qpc) - const noexcept { - std::lock_guard lock{history_mutex_}; - return history_.GetNearest(qpc); } - -} \ No newline at end of file diff --git a/IntelPresentMon/ControlLib/WmiCpu.h b/IntelPresentMon/ControlLib/WmiCpu.h index 91d5ee9d5..1fb67b420 100644 --- a/IntelPresentMon/ControlLib/WmiCpu.h +++ b/IntelPresentMon/ControlLib/WmiCpu.h @@ -1,13 +1,10 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #define NOMINMAX #include #include #include "CpuTelemetry.h" -#include "TelemetryHistory.h" -#include -#include namespace pwr::cpu::wmi { @@ -18,14 +15,11 @@ struct PDHQueryDeleter { class WmiCpu : public CpuTelemetry { public: WmiCpu(); - bool Sample() noexcept override; - std::optional GetClosest( - uint64_t qpc) const noexcept override; + CpuTelemetryInfo Sample() noexcept override; // types class NonGraphicsDeviceException : public std::exception {}; private: - // data std::unique_ptr, PDHQueryDeleter> query_; HCOUNTER processor_frequency_counter_; @@ -34,9 +28,6 @@ class WmiCpu : public CpuTelemetry { LARGE_INTEGER next_sample_qpc_ = {}; LARGE_INTEGER frequency_ = {}; std::string cpu_name_; - - mutable std::mutex history_mutex_; - TelemetryHistory history_{CpuTelemetry::defaultHistorySize}; }; -} // namespace pwr::cpu::wmi \ No newline at end of file +} // namespace pwr::cpu::wmi diff --git a/IntelPresentMon/Core/source/cli/CliOptions.h b/IntelPresentMon/Core/source/cli/CliOptions.h index 078ce0f06..760f13976 100644 --- a/IntelPresentMon/Core/source/cli/CliOptions.h +++ b/IntelPresentMon/Core/source/cli/CliOptions.h @@ -21,7 +21,7 @@ namespace p2c::cli private: Group gd_{ this, "Debugging", "Aids in debugging this tool" }; public: Option controlPipe{ this, "--control-pipe", R"(\\.\pipe\pm-ctrl)", "Named pipe to connect to the service with" }; - Option shmName{ this, "--shm-name", "pm-intro-shm", "Shared memory to connect to the service with" }; + Option shmNamePrefix{ this, "--shm-name-prefix", "pm-child-shm", "Shared memory to connect to the service with" }; Option etwSessionName{ this, "--etw-session-name", "pm-child-etw-session", "ETW session name when lauching service as child" }; Flag svcAsChild{ this, "--svc-as-child", "Launch service as child console app" }; Flag traceExceptions{ this, "--trace-exceptions", "Add stack trace to all thrown exceptions (including SEH exceptions)" }; @@ -53,14 +53,15 @@ namespace p2c::cli Option capTargetName{ this, "--target-name", {}, "Main module name of the process to track" }; Option capDuration{ this, "--duration", 10., "How long to capture for in seconds" }; Option capTelemetryPeriod{ this, "--telemetry-period", 100, "Time between GPU/CPU telemetry samples in ms" }; + Option capDefaultAdapterId{ this, "--default-adapter", {}, "Default GPU adapter id used when no device id is specified in --metrics" }; Option capOutput{ this, "--output", {}, "Name of the output CSV file, optionally with absolute or relative path" }; - Option> capMetrics{ this, "--metrics", {}, "List of metrics to capture as columns in the output CSV file" }; + Option> capMetrics{ this, "--metrics", {}, "List of metrics to capture as columns in the output CSV file. Format: PM_METRIC_XXX[INDEX]:DEVICEID (index and device id optional)." }; Subcommand subcList{ this, "list", "List entities for use with PresentMon SDK/headless CLI" }; public: private: Group glists_{ this, "Standard", "Standard options for the list subcommand" }; public: Flag listMetrics{ this, "--metrics,-m", "Output a list of available metrics" }; Flag listMetricsStats{ this, "--stats,-s", "Output a list of available stats for each metric" }; - Flag listDevices{ this, "--devices,-d", "Output a list of available graphics adapters" }; + Flag listDevices{ this, "--devices,-d", "Output a list of available devices" }; Flag listFilterFrame{ this, "--filter-frame,-f", "Filter to only metrics available for use with frame event capture" }; Flag listFilterDynamic{ this, "--filter-dynamic,-y", "Filter to only metrics available for use with dynamic polling" }; Option listSearch{ this, "--search", {}, "Substring to filter metric results on (case-insensitive)" }; @@ -79,4 +80,4 @@ namespace p2c::cli Dependency inclListSearch_{ listSearch, listMetrics }; Dependency inclListMetricsStats_{ listMetricsStats, listMetrics }; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/Core/source/kernel/Kernel.cpp b/IntelPresentMon/Core/source/kernel/Kernel.cpp index e49dd304a..0c851df10 100644 --- a/IntelPresentMon/Core/source/kernel/Kernel.cpp +++ b/IntelPresentMon/Core/source/kernel/Kernel.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #include "Kernel.h" #include @@ -95,22 +95,21 @@ namespace p2c::kern cv.notify_one(); } - void Kernel::SetAdapter(uint32_t id) + const pmapi::intro::Root& Kernel::GetIntrospectionRoot() const { HandleMarshalledException_(); - std::lock_guard lk{ mtx }; - if (!pm) { - pmlog_warn("presentmon not initialized"); - return; - } - pm->SetAdapter(id); + std::lock_guard g{ mtx }; + return pm->GetIntrospectionRoot(); } - const pmapi::intro::Root& Kernel::GetIntrospectionRoot() const + uint32_t Kernel::GetDefaultGpuDeviceId() const { HandleMarshalledException_(); std::lock_guard g{ mtx }; - return pm->GetIntrospectionRoot(); + if (!pm) { + return 0; + } + return pm->GetDefaultGpuDeviceId(); } std::vector Kernel::EnumerateAdapters() const diff --git a/IntelPresentMon/Core/source/kernel/Kernel.h b/IntelPresentMon/Core/source/kernel/Kernel.h index 098fa7ced..5bab7fea2 100644 --- a/IntelPresentMon/Core/source/kernel/Kernel.h +++ b/IntelPresentMon/Core/source/kernel/Kernel.h @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include @@ -51,11 +51,11 @@ namespace p2c::kern std::optional overrideTargetName, const GfxLayer::Extension::OverlayConfig& cfg); void ClearOverlay(); - void SetAdapter(uint32_t id); std::vector EnumerateAdapters() const; void SetCapture(bool active); void SetEtlLogging(bool active); const pmapi::intro::Root& GetIntrospectionRoot() const; + uint32_t GetDefaultGpuDeviceId() const; private: // functions bool IsIdle_() const; @@ -84,4 +84,4 @@ namespace p2c::kern ::pmon::util::mt::Thread thread; bool headless; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/Core/source/kernel/Overlay.cpp b/IntelPresentMon/Core/source/kernel/Overlay.cpp index fc186f638..5c08599f2 100644 --- a/IntelPresentMon/Core/source/kernel/Overlay.cpp +++ b/IntelPresentMon/Core/source/kernel/Overlay.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #include "Overlay.h" #include @@ -450,7 +450,11 @@ namespace p2c::kern return std::nullopt; } }(); - pWriter = { pm->MakeRawFrameDataWriter(std::move(fullPath), std::move(fullStatsPath), proc.pid, proc.name) }; + const auto frameAdapterId = (pSpec->frameQueryAdapterId && *pSpec->frameQueryAdapterId > 0) + ? pSpec->frameQueryAdapterId + : std::optional{}; + pWriter = { pm->MakeRawFrameDataWriter(std::move(fullPath), std::move(fullStatsPath), proc.pid, + frameAdapterId) }; } else if (!active && pWriter) { pWriter.reset(); @@ -625,4 +629,4 @@ namespace p2c::kern { return remainings_[Trace_] == 0; } -} \ No newline at end of file +} diff --git a/IntelPresentMon/Core/source/kernel/OverlaySpec.h b/IntelPresentMon/Core/source/kernel/OverlaySpec.h index 7aa459298..98d98083d 100644 --- a/IntelPresentMon/Core/source/kernel/OverlaySpec.h +++ b/IntelPresentMon/Core/source/kernel/OverlaySpec.h @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include @@ -92,6 +92,7 @@ namespace p2c::kern bool independentKernelWindow; bool generateStats; bool enableFlashInjection; + std::optional frameQueryAdapterId; std::vector> sheets; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/Core/source/pmon/PresentMon.cpp b/IntelPresentMon/Core/source/pmon/PresentMon.cpp index 2dc9c62d4..18b0df3ab 100644 --- a/IntelPresentMon/Core/source/pmon/PresentMon.cpp +++ b/IntelPresentMon/Core/source/pmon/PresentMon.cpp @@ -1,10 +1,11 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #include "PresentMon.h" #include #include #include #include +#include #include #include "RawFrameDataWriter.h" @@ -12,25 +13,6 @@ namespace p2c::pmon { using namespace ::pmapi; - class FrameEventFlusher - { - public: - FrameEventFlusher(Session& sesh) - { - // register minimal query used for flushing frame events - std::array queryElements{ PM_QUERY_ELEMENT{ PM_METRIC_PRESENT_MODE, PM_STAT_MID_POINT } }; - query_ = sesh.RegisterFrameQuery(queryElements); - blobs_ = query_.MakeBlobContainer(50); - } - void Flush(ProcessTracker& tracker) - { - query_.ForEachConsume(tracker, blobs_, [](auto) {}); - } - private: - FrameQuery query_; - BlobContainer blobs_; - }; - PresentMon::PresentMon(std::optional namedPipeName, double window_in, double offset_in, uint32_t telemetrySamplePeriodMs_in) { const auto RemoveDoubleQuotes = [](std::string s) { @@ -56,8 +38,6 @@ namespace p2c::pmon SetGpuTelemetryPeriod(telemetrySamplePeriodMs_in); SetEtwFlushPeriod(std::nullopt); - // create flusher used to clear out piled-up old frame events before capture - pFlusher = std::make_unique(*pSession); } PresentMon::~PresentMon() = default; void PresentMon::StartTracking(uint32_t pid_) @@ -106,17 +86,6 @@ namespace p2c::pmon } return infos; } - void PresentMon::SetAdapter(uint32_t id) - { - pmlog_info(std::format("Set active adapter to [{}]", id)); - if (id == 0) { - pmlog_warn("Adapter was set to id 0; resetting"); - selectedAdapter.reset(); - } - else { - selectedAdapter = id; - } - } void PresentMon::SetEtlLogging(bool active) { pmlog_info("Setting etl logging").pmwatch(active); @@ -150,18 +119,16 @@ namespace p2c::pmon return processTracker; } std::shared_ptr PresentMon::MakeRawFrameDataWriter(std::wstring path, - std::optional statsPath, uint32_t pid, std::wstring procName) + std::optional statsPath, uint32_t pid, std::optional gpuDeviceIdOverride) { // flush any buffered present events before starting capture - pFlusher->Flush(processTracker); + processTracker.FlushFrames(); + constexpr bool omitUnavailableColumns = false; + const uint32_t activeDeviceId = gpuDeviceIdOverride.value_or(GetDefaultGpuDeviceId()); // make the frame data writer - return std::make_shared(std::move(path), processTracker, std::move(procName), - selectedAdapter.value_or(1), *pSession, std::move(statsPath), *pIntrospectionRoot); - } - std::optional PresentMon::GetSelectedAdapter() const - { - return selectedAdapter; + return std::make_shared(std::move(path), processTracker, activeDeviceId, + *pSession, std::move(statsPath), *pIntrospectionRoot, omitUnavailableColumns); } const pmapi::intro::Root& PresentMon::GetIntrospectionRoot() const { @@ -171,6 +138,18 @@ namespace p2c::pmon { return *pSession; } + + uint32_t PresentMon::GetDefaultGpuDeviceId() const + { + if (cachedDefaultGpuDeviceId_.has_value()) { + return *cachedDefaultGpuDeviceId_; + } + const auto deviceId = ComputeDefaultGpuDeviceId_(); + if (deviceId != 0) { + cachedDefaultGpuDeviceId_ = deviceId; + } + return deviceId; + } void PresentMon::SetEtwFlushPeriod(std::optional periodMs) { assert(pSession); @@ -181,4 +160,47 @@ namespace p2c::pmon { return etwFlushPeriodMs; } -} \ No newline at end of file + + uint32_t PresentMon::ComputeDefaultGpuDeviceId_() const + { + const auto& intro = *pIntrospectionRoot; + uint32_t bestId = 0; + uint64_t bestMem = 0; + + for (const auto& device : intro.GetDevices()) { + if (device.GetType() != PM_DEVICE_TYPE_GRAPHICS_ADAPTER) { + continue; + } + uint64_t memSize = 0; + bool hasValue = false; + try { + memSize = pmapi::PollStatic(*pSession, + PM_METRIC_GPU_MEM_SIZE, device.GetId(), 0).As(); + hasValue = true; + } + catch (...) { + hasValue = false; + } + + if (hasValue) { + if (memSize > bestMem) { + bestMem = memSize; + bestId = device.GetId(); + } + } + else if (bestId == 0) { + bestId = device.GetId(); + } + } + + if (bestId == 0) { + for (const auto& device : intro.GetDevices()) { + if (device.GetType() == PM_DEVICE_TYPE_GRAPHICS_ADAPTER) { + return device.GetId(); + } + } + } + + return bestId; + } +} diff --git a/IntelPresentMon/Core/source/pmon/PresentMon.h b/IntelPresentMon/Core/source/pmon/PresentMon.h index c8a0ea21f..cda36ef16 100644 --- a/IntelPresentMon/Core/source/pmon/PresentMon.h +++ b/IntelPresentMon/Core/source/pmon/PresentMon.h @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include @@ -20,7 +20,6 @@ namespace pmapi namespace p2c::pmon { class RawFrameDataWriter; - class FrameEventFlusher; class PresentMon { @@ -36,24 +35,23 @@ namespace p2c::pmon std::optional GetEtwFlushPeriod(); // std::wstring GetCpuName() const; std::vector EnumerateAdapters() const; - void SetAdapter(uint32_t id); void SetEtlLogging(bool active); std::optional GetPid() const; const pmapi::ProcessTracker& GetTracker() const; std::shared_ptr MakeRawFrameDataWriter(std::wstring path, std::optional statsPath, - uint32_t pid, std::wstring procName); - std::optional GetSelectedAdapter() const; + uint32_t pid, std::optional gpuDeviceIdOverride); + uint32_t GetDefaultGpuDeviceId() const; const pmapi::intro::Root& GetIntrospectionRoot() const; pmapi::Session& GetSession(); private: + uint32_t ComputeDefaultGpuDeviceId_() const; double window = -1.; uint32_t telemetrySamplePeriod = 0; std::optional etwFlushPeriodMs; + mutable std::optional cachedDefaultGpuDeviceId_; pmapi::EtlLogger etlLogger; std::unique_ptr pSession; - std::unique_ptr pFlusher; std::shared_ptr pIntrospectionRoot; pmapi::ProcessTracker processTracker; - std::optional selectedAdapter; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/Core/source/pmon/RawFrameDataMetricList.h b/IntelPresentMon/Core/source/pmon/RawFrameDataMetricList.h index 3a668ee32..136ac9c0a 100644 --- a/IntelPresentMon/Core/source/pmon/RawFrameDataMetricList.h +++ b/IntelPresentMon/Core/source/pmon/RawFrameDataMetricList.h @@ -1,4 +1,4 @@ -// Copyright (C) 2017-2024 Intel Corporation +// Copyright (C) 2017-2024 Intel Corporation // SPDX-License-Identifier: MIT #pragma once // must include PresentMonAPI.h before including this file @@ -6,6 +6,7 @@ #include #include #include +#include "../../../Interprocess/source/SystemDeviceId.h" namespace p2c::pmon { @@ -16,8 +17,6 @@ namespace p2c::pmon std::optional index; }; - constexpr int PM_METRIC_INTERNAL_PROCESS_ID_ = 4'021'373; - inline std::vector GetDefaultRawFrameDataMetricList(uint32_t activeDeviceId, bool enableTimestamp) { namespace rn = std::ranges; @@ -26,7 +25,7 @@ namespace p2c::pmon std::vector queryElements{ // these first 2 are special cases filled by the writer logic Element{.metricId = PM_METRIC_APPLICATION, .deviceId = 0 }, - Element{.metricId = (PM_METRIC)PM_METRIC_INTERNAL_PROCESS_ID_, .deviceId = 0 }, + Element{.metricId = PM_METRIC_PROCESS_ID, .deviceId = 0 }, Element{.metricId = PM_METRIC_SWAP_CHAIN_ADDRESS, .deviceId = 0 }, Element{.metricId = PM_METRIC_PRESENT_RUNTIME, .deviceId = 0 }, Element{.metricId = PM_METRIC_SYNC_INTERVAL, .deviceId = 0 }, @@ -42,7 +41,6 @@ namespace p2c::pmon Element{.metricId = PM_METRIC_IN_PRESENT_API, .deviceId = 0 }, Element{.metricId = PM_METRIC_RENDER_PRESENT_LATENCY, .deviceId = 0 }, Element{.metricId = PM_METRIC_UNTIL_DISPLAYED, .deviceId = 0 }, - Element{.metricId = PM_METRIC_PC_LATENCY, .deviceId = 0 }, Element{.metricId = PM_METRIC_CPU_START_TIME, .deviceId = 0 }, Element{.metricId = PM_METRIC_BETWEEN_APP_START, .deviceId = 0 }, Element{.metricId = PM_METRIC_CPU_BUSY, .deviceId = 0 }, @@ -91,10 +89,10 @@ namespace p2c::pmon Element{.metricId = PM_METRIC_GPU_MEM_VOLTAGE_LIMITED, .deviceId = activeDeviceId }, Element{.metricId = PM_METRIC_GPU_MEM_UTILIZATION_LIMITED, .deviceId = activeDeviceId }, - Element{.metricId = PM_METRIC_CPU_UTILIZATION }, - Element{.metricId = PM_METRIC_CPU_POWER }, - Element{.metricId = PM_METRIC_CPU_TEMPERATURE }, - Element{.metricId = PM_METRIC_CPU_FREQUENCY }, + Element{.metricId = PM_METRIC_CPU_UTILIZATION, .deviceId = ::pmon::ipc::kSystemDeviceId }, + Element{.metricId = PM_METRIC_CPU_POWER, .deviceId = ::pmon::ipc::kSystemDeviceId }, + Element{.metricId = PM_METRIC_CPU_TEMPERATURE, .deviceId = ::pmon::ipc::kSystemDeviceId }, + Element{.metricId = PM_METRIC_CPU_FREQUENCY, .deviceId = ::pmon::ipc::kSystemDeviceId }, }; if (enableTimestamp) { diff --git a/IntelPresentMon/Core/source/pmon/RawFrameDataWriter.cpp b/IntelPresentMon/Core/source/pmon/RawFrameDataWriter.cpp index 08a5184ef..276f61442 100644 --- a/IntelPresentMon/Core/source/pmon/RawFrameDataWriter.cpp +++ b/IntelPresentMon/Core/source/pmon/RawFrameDataWriter.cpp @@ -1,23 +1,116 @@ // Copyright (C) 2017-2024 Intel Corporation // SPDX-License-Identifier: MIT #include "RawFrameDataWriter.h" -#include #include #include #include #include #include +#include +#include #include "RawFrameDataMetricList.h" #include "../cli/CliOptions.h" namespace p2c::pmon { - using ::pmon::util::str::ToNarrow; namespace rn = std::ranges; namespace vi = rn::views; namespace { + struct MetricSpec_ + { + std::string symbol; + std::optional index; + std::optional deviceId; + }; + + bool TryParseUInt32_(std::string_view text, uint32_t& value) + { + if (text.empty()) { + return false; + } + const auto* start = text.data(); + const auto* end = start + text.size(); + auto [ptr, ec] = std::from_chars(start, end, value); + return ec == std::errc{} && ptr == end; + } + + bool TryParseMetricSpec_(std::string_view input, MetricSpec_& out, std::string& error) + { + out = {}; + if (input.empty()) { + error = "metric is empty"; + return false; + } + + const size_t colonPos = input.find(':'); + const size_t bracketPos = input.find('['); + + if (colonPos != std::string_view::npos && + bracketPos != std::string_view::npos && + colonPos < bracketPos) { + error = "device id must follow array index"; + return false; + } + + size_t symbolEnd = input.size(); + if (bracketPos != std::string_view::npos) { + symbolEnd = bracketPos; + } + else if (colonPos != std::string_view::npos) { + symbolEnd = colonPos; + } + + if (symbolEnd == 0) { + error = "missing metric symbol"; + return false; + } + + out.symbol.assign(input.substr(0, symbolEnd)); + + if (bracketPos != std::string_view::npos) { + const size_t closeBracket = input.find(']', bracketPos + 1); + if (closeBracket == std::string_view::npos) { + error = "missing closing bracket"; + return false; + } + if (colonPos != std::string_view::npos && closeBracket > colonPos) { + error = "device id must follow array index"; + return false; + } + const auto indexText = input.substr(bracketPos + 1, closeBracket - bracketPos - 1); + uint32_t indexValue = 0; + if (!TryParseUInt32_(indexText, indexValue)) { + error = "invalid array index"; + return false; + } + out.index = indexValue; + if (colonPos == std::string_view::npos) { + if (closeBracket + 1 != input.size()) { + error = "unexpected characters after array index"; + return false; + } + } + else if (closeBracket + 1 != colonPos) { + error = "unexpected characters after array index"; + return false; + } + } + + if (colonPos != std::string_view::npos) { + const auto devText = input.substr(colonPos + 1); + uint32_t devValue = 0; + if (!TryParseUInt32_(devText, devValue)) { + error = "invalid device id"; + return false; + } + out.deviceId = devValue; + } + + return true; + } + std::vector MakeMetricList_(std::vector metricSymbols, uint32_t activeDeviceId, const pmapi::intro::Root& introRoot) { @@ -33,36 +126,83 @@ namespace p2c::pmon std::vector elements; elements.reserve(metricSymbols.size()); for (auto& metricSymbol : metricSymbols) { + MetricSpec_ metricSpec{}; + std::string parseError; + if (!TryParseMetricSpec_(metricSymbol, metricSpec, parseError)) { + pmlog_error("Failed to parse metric spec") + .pmwatch(metricSymbol) + .pmwatch(parseError); + continue; + } try { - // special case for pid, which is not an api metric but needs to be available in headless - if (metricSymbol == "PM_METRIC_INTERNAL_PROCESS_ID_") { - elements.push_back(RawFrameQueryElementDefinition{ - .metricId = (PM_METRIC)PM_METRIC_INTERNAL_PROCESS_ID_, - }); + const auto metricIt = metricLookup.find(metricSpec.symbol); + if (metricIt == metricLookup.end()) { + pmlog_error("Unknown metric symbol").pmwatch(metricSpec.symbol); continue; } - const auto metricId = metricLookup.at(metricSymbol); + const auto metricId = metricIt->second; const auto& metric = introRoot.FindMetric(metricId); // make sure metric is valid for a frame query if (metric.GetType() == PM_METRIC_TYPE_DYNAMIC) { pmlog_error("Specified metric does not support frame query").raise<::pmon::util::Exception>(); } + const auto& deviceInfos = metric.GetDeviceMetricInfo(); + const bool isGraphicsAdapter = !deviceInfos.empty() && + deviceInfos.front().GetDevice().GetType() == PM_DEVICE_TYPE_GRAPHICS_ADAPTER; + const uint32_t deviceId = metricSpec.deviceId.value_or(isGraphicsAdapter ? activeDeviceId : 0); elements.push_back(RawFrameQueryElementDefinition{ - .metricId = metricLookup.at(metricSymbol), - .deviceId = metric.GetDeviceMetricInfo().front().GetDevice().GetType() == - PM_DEVICE_TYPE_GRAPHICS_ADAPTER ? activeDeviceId : 0, + .metricId = metricId, + .deviceId = deviceId, + .index = metricSpec.index, }); } catch (...) { pmlog_error("Failed to add metric").pmwatch(metricSymbol); } } - if (elements.empty()) { - pmlog_error("No valid metrics specified for frame event capture").raise<::pmon::util::Exception>(); - } return elements; } + struct MetricColumn_ + { + RawFrameQueryElementDefinition definition; + bool available = false; + }; + + std::vector BuildMetricColumns_(const std::vector& elements, + uint32_t activeDeviceId, const pmapi::intro::Root& introRoot, bool omitUnavailableColumns) + { + std::vector columns; + columns.reserve(elements.size()); + for (const auto& element : elements) { + const auto& metric = introRoot.FindMetric(element.metricId); + const auto checkDeviceId = element.deviceId != 0 ? element.deviceId : activeDeviceId; + bool available = false; + for (auto&& dmi : metric.GetDeviceMetricInfo()) { + if (auto devId = dmi.GetDevice().GetId(); devId == checkDeviceId || devId == 0) { + const auto arraySize = dmi.GetArraySize(); + if (dmi.IsAvailable() && arraySize > 0) { + if (!element.index.has_value() || + *element.index < arraySize) { + available = true; + break; + } + } + } + } + if (!available) { + pmlog_warn("Metric not available for active device") + .pmwatch(metric.Introspect().GetSymbol()) + .pmwatch(checkDeviceId); + if (omitUnavailableColumns) { + continue; + } + } + columns.push_back(MetricColumn_{ .definition = element, .available = available }); + } + return columns; + } + class StreamFlagPreserver_ { public: @@ -92,9 +232,9 @@ namespace p2c::pmon virtual ~Annotation_() = default; virtual void Write(std::ostream& out, const uint8_t* pBytes) const = 0; std::string columnName; - size_t queryElementIndex = 0; - static std::unique_ptr MakeTyped(PM_METRIC metricId, uint32_t deviceId, - const pmapi::intro::MetricView& metric); + std::optional queryElementIndex; + static std::unique_ptr MakeTyped(PM_METRIC metricId, const pmapi::intro::MetricView& metric, + bool available); protected: uint32_t flags_; @@ -133,30 +273,6 @@ namespace p2c::pmon } } }; - struct ProcessNameAnnotation_ : public Annotation_ - { - ProcessNameAnnotation_(std::string name) - : - name_{ std::move(name) } - {} - void Write(std::ostream& out, const uint8_t*) const override - { - out << name_; - } - std::string name_; - }; - struct PidAnnotation_ : public Annotation_ - { - PidAnnotation_(uint32_t pid) - : - pid_{ pid } - {} - void Write(std::ostream& out, const uint8_t*) const override - { - out << pid_; - } - uint32_t pid_; - }; template<> struct TypedAnnotation_ : public Annotation_ { @@ -190,15 +306,9 @@ namespace p2c::pmon } mutable std::optional startTime; }; - std::unique_ptr Annotation_::MakeTyped(PM_METRIC metricId, uint32_t deviceId, - const pmapi::intro::MetricView& metric) + std::unique_ptr Annotation_::MakeTyped(PM_METRIC metricId, const pmapi::intro::MetricView& metric, + bool available) { - // set availability (defaults to false, if we find matching device use its availability) - bool available = false; - for (auto di : metric.GetDeviceMetricInfo()) { - if (di.GetDevice().GetId() != deviceId) continue; - available = di.IsAvailable(); - } std::unique_ptr pAnnotation; if (available) { const auto typeId = metric.GetDataTypeInfo().GetFrameType(); @@ -257,47 +367,38 @@ namespace p2c::pmon class QueryElementContainer_ { public: - QueryElementContainer_(std::span elements, - pmapi::Session& session, const pmapi::intro::Root& introRoot, std::string procName, uint32_t pid) + QueryElementContainer_(std::span columns, + pmapi::Session& session, const pmapi::intro::Root& introRoot) { - for (auto& el : elements) { - // check for specific fill-in metrics - if (el.metricId == PM_METRIC_APPLICATION) { - annotationPtrs_.push_back(std::make_unique(procName)); - annotationPtrs_.back()->columnName = "Application"; - continue; - } - else if (el.metricId == (PM_METRIC)PM_METRIC_INTERNAL_PROCESS_ID_) { - annotationPtrs_.push_back(std::make_unique(pid)); - annotationPtrs_.back()->columnName = "ProcessID"; - continue; - } - const auto metric = introRoot.FindMetric(el.metricId); - annotationPtrs_.push_back(Annotation_::MakeTyped(el.metricId, el.deviceId, metric)); + for (auto& column : columns) { + const auto metric = introRoot.FindMetric(column.definition.metricId); + annotationPtrs_.push_back(Annotation_::MakeTyped(column.definition.metricId, metric, column.available)); // append metric array index to column name if array metric - if (el.index.has_value()) { - annotationPtrs_.back()->columnName += std::format("[{}]", *el.index); - } - // set index into query elements - annotationPtrs_.back()->queryElementIndex = queryElements_.size(); - // add to query elements - queryElements_.push_back(PM_QUERY_ELEMENT{ - .metric = el.metricId, - .stat = PM_STAT_NONE, - .deviceId = el.deviceId, - .arrayIndex = el.index.value_or(0), - }); - // check if metric is one of the specially-required fields - // these fields are required because they are used for summary stats - // we need pointers to these specific ones to read for generating those stats - if (el.metricId == PM_METRIC_CPU_START_TIME) { - totalTimeElementIdx_ = int(queryElements_.size() - 1); - } - else if (el.metricId == PM_METRIC_BETWEEN_PRESENTS) { - msBetweenPresentsElementIdx_ = int(queryElements_.size() - 1); + if (column.definition.index.has_value()) { + annotationPtrs_.back()->columnName += std::format("[{}]", *column.definition.index); } - else if (el.metricId == PM_METRIC_ANIMATION_ERROR) { - animationErrorElementIdx_ = int(queryElements_.size() - 1); + if (column.available) { + // set index into query elements + annotationPtrs_.back()->queryElementIndex = queryElements_.size(); + // add to query elements + queryElements_.push_back(PM_QUERY_ELEMENT{ + .metric = column.definition.metricId, + .stat = PM_STAT_NONE, + .deviceId = column.definition.deviceId, + .arrayIndex = column.definition.index.value_or(0), + }); + // check if metric is one of the specially-required fields + // these fields are required because they are used for summary stats + // we need pointers to these specific ones to read for generating those stats + if (column.definition.metricId == PM_METRIC_CPU_START_TIME) { + totalTimeElementIdx_ = int(queryElements_.size() - 1); + } + else if (column.definition.metricId == PM_METRIC_BETWEEN_PRESENTS) { + msBetweenPresentsElementIdx_ = int(queryElements_.size() - 1); + } + else if (column.definition.metricId == PM_METRIC_ANIMATION_ERROR) { + animationErrorElementIdx_ = int(queryElements_.size() - 1); + } } } // if any specially-required fields are missing, add to query (but not to annotations) @@ -352,16 +453,19 @@ namespace p2c::pmon { return reinterpret_cast(pBlob[queryElements_[animationErrorElementIdx_].dataOffset]); } - void WriteFrame(uint32_t pid, const std::string& procName, std::ostream& out, const uint8_t* pBlob) + void WriteFrame(std::ostream& out, const uint8_t* pBlob) { - // TODO: use metrics from procname and pid piped through middleware (after IPC upgrade) // loop over each element (column/field) in a frame of data for (auto&& [i, pAnno] : annotationPtrs_ | vi::enumerate) { if (i) { out << ','; } + if (!pAnno->queryElementIndex.has_value()) { + out << "NA"; + continue; + } // using output from the query registration of get offset of column's data - const auto pBytes = pBlob + queryElements_[pAnno->queryElementIndex].dataOffset; + const auto pBytes = pBlob + queryElements_[*pAnno->queryElementIndex].dataOffset; // annotation contains polymorphic info to reinterpret and convert bytes pAnno->Write(out, pBytes); } @@ -389,11 +493,11 @@ namespace p2c::pmon int animationErrorElementIdx_ = -1; }; - RawFrameDataWriter::RawFrameDataWriter(std::wstring path, const pmapi::ProcessTracker& procTrackerIn, std::wstring processName, uint32_t activeDeviceId, - pmapi::Session& session, std::optional frameStatsPathIn, const pmapi::intro::Root& introRoot) + RawFrameDataWriter::RawFrameDataWriter(std::wstring path, const pmapi::ProcessTracker& procTrackerIn, uint32_t activeDeviceId, + pmapi::Session& session, std::optional frameStatsPathIn, const pmapi::intro::Root& introRoot, + bool omitUnavailableColumns) : procTracker{ procTrackerIn }, - procName{ ToNarrow(processName) }, frameStatsPath{ std::move(frameStatsPathIn) }, pStatsTracker{ frameStatsPath ? std::make_unique() : nullptr }, pAnimationErrorTracker{ frameStatsPath ? std::make_unique() : nullptr }, @@ -407,8 +511,11 @@ namespace p2c::pmon else { elements = GetDefaultRawFrameDataMetricList(activeDeviceId, opt.enableTimestampColumn); } - pQueryElementContainer = std::make_unique(std::move(elements), session, - introRoot, ::pmon::util::str::ToNarrow(processName), procTracker.GetPid()); + const auto columns = BuildMetricColumns_(elements, activeDeviceId, introRoot, omitUnavailableColumns); + if (columns.empty()) { + pmlog_error("No valid metrics specified for frame event capture").raise<::pmon::util::Exception>(); + } + pQueryElementContainer = std::make_unique(columns, session, introRoot); blobs = pQueryElementContainer->MakeBlobs(numberOfBlobs); // write header pQueryElementContainer->WriteHeader(file); @@ -439,7 +546,7 @@ namespace p2c::pmon pAnimationErrorTracker->Push(std::abs(animationError)); } } - pQueryElementContainer->WriteFrame(procTracker.GetPid(), procName, file, pBlob); + pQueryElementContainer->WriteFrame(file, pBlob); } } while (blobs.AllBlobsPopulated()); // if container filled, means more might be left file << std::flush; diff --git a/IntelPresentMon/Core/source/pmon/RawFrameDataWriter.h b/IntelPresentMon/Core/source/pmon/RawFrameDataWriter.h index ff89039fd..5dba4bed3 100644 --- a/IntelPresentMon/Core/source/pmon/RawFrameDataWriter.h +++ b/IntelPresentMon/Core/source/pmon/RawFrameDataWriter.h @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include @@ -18,8 +18,9 @@ namespace p2c::pmon class RawFrameDataWriter { public: - RawFrameDataWriter(std::wstring path, const pmapi::ProcessTracker& procTracker, std::wstring processName, uint32_t activeDeviceId, - pmapi::Session& session, std::optional frameStatsPath, const pmapi::intro::Root& introRoot); + RawFrameDataWriter(std::wstring path, const pmapi::ProcessTracker& procTracker, uint32_t activeDeviceId, + pmapi::Session& session, std::optional frameStatsPath, const pmapi::intro::Root& introRoot, + bool omitUnavailableColumns); RawFrameDataWriter(const RawFrameDataWriter&) = delete; RawFrameDataWriter& operator=(const RawFrameDataWriter&) = delete; void Process(); @@ -31,7 +32,6 @@ namespace p2c::pmon // data static constexpr uint32_t numberOfBlobs = 150u; const pmapi::ProcessTracker& procTracker; - std::string procName; std::unique_ptr pQueryElementContainer; std::optional frameStatsPath; std::unique_ptr pStatsTracker; diff --git a/IntelPresentMon/Interprocess/Interprocess.vcxproj b/IntelPresentMon/Interprocess/Interprocess.vcxproj index 101c9d258..2da86bb5e 100644 --- a/IntelPresentMon/Interprocess/Interprocess.vcxproj +++ b/IntelPresentMon/Interprocess/Interprocess.vcxproj @@ -1,4 +1,4 @@ - + @@ -11,10 +11,14 @@ + + + + @@ -27,6 +31,9 @@ + + + @@ -55,6 +62,13 @@ + + + + + + + diff --git a/IntelPresentMon/Interprocess/Interprocess.vcxproj.filters b/IntelPresentMon/Interprocess/Interprocess.vcxproj.filters index 6187839f4..34a524306 100644 --- a/IntelPresentMon/Interprocess/Interprocess.vcxproj.filters +++ b/IntelPresentMon/Interprocess/Interprocess.vcxproj.filters @@ -15,6 +15,9 @@ + + Source Files + Source Files @@ -27,6 +30,15 @@ Source Files + + Source Files + + + Source Files + + + Source Files + @@ -143,5 +155,35 @@ Header Files + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + \ No newline at end of file diff --git a/IntelPresentMon/Interprocess/source/DataStores.cpp b/IntelPresentMon/Interprocess/source/DataStores.cpp new file mode 100644 index 000000000..bb0bf8eaf --- /dev/null +++ b/IntelPresentMon/Interprocess/source/DataStores.cpp @@ -0,0 +1,218 @@ +#include "DataStores.h" +#include "MetricCapabilities.h" +#include "IntrospectionTransfer.h" +#include "IntrospectionDataTypeMapping.h" +#include "PmStatusError.h" +#include "../../CommonUtilities/Meta.h" +#include "../../CommonUtilities/Memory.h" +#include "../../CommonUtilities/log/Verbose.h" +#include +#include +#include +#include + +namespace pmon::ipc +{ + namespace + { + constexpr size_t kSegmentAlignmentBytes_ = 64 * 1024; + constexpr size_t kFrameScaleMul_ = 3; + constexpr size_t kFrameScaleDiv_ = 2; + constexpr size_t kTelemetryScaleGpuMul_ = 3; + constexpr size_t kTelemetryScaleSystemMul_ = 2; + constexpr size_t kTelemetryScaleDiv_ = 1; + constexpr size_t kFixedLeewayBytes_ = 4 * 1024; + + size_t ScaleBytes_(size_t bytes, size_t numerator, size_t denominator) + { + return (bytes * numerator + denominator - 1) / denominator; + } + + template + struct DataTypeSizeBridger_ + { + static size_t Invoke() + { + return intro::DataTypeToStaticType_sz; + } + static size_t Default() + { + return 0ull; + } + }; + + size_t EstimateSampleBytes_(PM_DATA_TYPE type) + { + const size_t valueBytes = intro::BridgeDataType(type); + const size_t safeValueBytes = valueBytes > 0 ? valueBytes : sizeof(uint32_t); + const size_t pad = util::GetPadding(safeValueBytes, alignof(uint64_t)); + return safeValueBytes + pad + sizeof(uint64_t); + } + + bool ShouldAllocateTelemetryRing_(const intro::IntrospectionMetric& metric) + { + if (metric.GetMetricType() == PM_METRIC_TYPE_STATIC) { + return false; + } + return true; + } + + template + void ForEachTelemetryRing_(const DataStoreSizingInfo& sizing, + PM_DEVICE_TYPE deviceType, + Func&& func) + { + if (!sizing.pRoot || !sizing.pCaps) { + throw std::logic_error("DataStoreSizingInfo requires introspection root and caps"); + } + + std::unordered_map deviceTypeById; + for (const auto& pDevice : sizing.pRoot->GetDevices()) { + deviceTypeById.emplace(pDevice->GetId(), pDevice->GetType()); + } + + for (auto&& [metricId, count] : *sizing.pCaps) { + const auto& metric = sizing.pRoot->FindMetric(metricId); + bool matchesDeviceType = false; + for (const auto& pInfo : metric.GetDeviceMetricInfo()) { + const auto it = deviceTypeById.find(pInfo->GetDeviceId()); + if (it != deviceTypeById.end() && it->second == deviceType) { + matchesDeviceType = true; + break; + } + } + if (!matchesDeviceType) { + throw std::logic_error( + "DataStoreSizingInfo caps contain a metric outside the expected device type"); + } + if (!ShouldAllocateTelemetryRing_(metric)) { + continue; + } + const auto dataType = metric.GetDataTypeInfo().GetFrameType(); + func(metricId, count, dataType); + } + } + + size_t TelemetrySegmentBytes_(const DataStoreSizingInfo& sizing, PM_DEVICE_TYPE deviceType) + { + if (sizing.overrideBytes) { + return *sizing.overrideBytes; + } + + size_t ringCount = 0; + size_t payloadBytes = 0; + ForEachTelemetryRing_(sizing, deviceType, + [&](PM_METRIC metricId, size_t count, PM_DATA_TYPE dataType) { + const size_t sampleBytes = EstimateSampleBytes_(dataType); + const size_t metricBytes = count * sizing.ringSamples * sampleBytes; + payloadBytes += metricBytes; + ringCount += count; + pmlog_verb(util::log::V::ipc_sto)(std::format( + "ipc telem metric sizing | metric:{} count:{} ring_samples:{} sample_bytes:{} payload_bytes:{}", + static_cast(metricId), count, sizing.ringSamples, sampleBytes, metricBytes)); + }); + + const size_t scaleMul = (deviceType == PM_DEVICE_TYPE_SYSTEM) ? + kTelemetryScaleSystemMul_ : kTelemetryScaleGpuMul_; + size_t scaledBytes = + ScaleBytes_(payloadBytes, scaleMul, kTelemetryScaleDiv_); + if (scaledBytes < payloadBytes + kFixedLeewayBytes_) { + scaledBytes = payloadBytes + kFixedLeewayBytes_; + } + const size_t leewayBytes = scaledBytes - payloadBytes; + const size_t totalBytes = util::PadToAlignment(scaledBytes, kSegmentAlignmentBytes_); + pmlog_verb(util::log::V::ipc_sto)(std::format( + "ipc telem sizing | ring_samples:{} ring_count:{} payload_bytes:{} scaled_bytes:{} fixed_leeway_bytes:{} leeway_bytes:{} alignment:{} total_bytes:{}", + sizing.ringSamples, ringCount, payloadBytes, scaledBytes, kFixedLeewayBytes_, + leewayBytes, kSegmentAlignmentBytes_, totalBytes)); + return totalBytes; + } + } + + size_t FrameDataStore::CalculateSegmentBytes(const DataStoreSizingInfo& sizing) + { + const size_t payloadBytes = sizing.ringSamples * sizeof(FrameData); + size_t scaledBytes = + ScaleBytes_(payloadBytes, kFrameScaleMul_, kFrameScaleDiv_); + if (scaledBytes < payloadBytes + kFixedLeewayBytes_) { + scaledBytes = payloadBytes + kFixedLeewayBytes_; + } + const size_t leewayBytes = scaledBytes - payloadBytes; + const size_t totalBytes = util::PadToAlignment(scaledBytes, kSegmentAlignmentBytes_); + pmlog_verb(util::log::V::ipc_sto)(std::format( + "ipc frame sizing | ring_samples:{} payload_bytes:{} scaled_bytes:{} fixed_leeway_bytes:{} leeway_bytes:{} alignment:{} total_bytes:{}", + sizing.ringSamples, payloadBytes, scaledBytes, kFixedLeewayBytes_, + leewayBytes, kSegmentAlignmentBytes_, totalBytes)); + return totalBytes; + } + + StaticMetricValue FrameDataStore::FindStaticMetric(PM_METRIC metric) const + { + switch (metric) { + case PM_METRIC_APPLICATION: + return statics.applicationName.c_str(); + case PM_METRIC_PROCESS_ID: + return bookkeeping.processId; + case PM_METRIC_SESSION_START_QPC: + return bookkeeping.startQpc; + default: + throw util::Except(PM_STATUS_QUERY_MALFORMED, + "Static metric not handled by frame data store"); + } + } + + void PopulateTelemetryRings(TelemetryMap& telemetryData, + const DataStoreSizingInfo& sizing, + PM_DEVICE_TYPE deviceType) + { + ForEachTelemetryRing_(sizing, deviceType, + [&](PM_METRIC metricId, size_t count, PM_DATA_TYPE dataType) { + telemetryData.AddRing(metricId, sizing.ringSamples, count, dataType); + }); + } + + size_t GpuDataStore::CalculateSegmentBytes(const DataStoreSizingInfo& sizing) + { + return TelemetrySegmentBytes_(sizing, PM_DEVICE_TYPE_GRAPHICS_ADAPTER); + } + + StaticMetricValue GpuDataStore::FindStaticMetric(PM_METRIC metric) const + { + switch (metric) { + case PM_METRIC_GPU_VENDOR: + return int32_t(statics.vendor); + case PM_METRIC_GPU_NAME: + return statics.name.c_str(); + case PM_METRIC_GPU_SUSTAINED_POWER_LIMIT: + return statics.sustainedPowerLimit; + case PM_METRIC_GPU_MEM_SIZE: + return statics.memSize; + case PM_METRIC_GPU_MEM_MAX_BANDWIDTH: + return statics.maxMemBandwidth; + default: + throw util::Except(PM_STATUS_QUERY_MALFORMED, + "Static metric not handled by gpu data store"); + } + } + + size_t SystemDataStore::CalculateSegmentBytes(const DataStoreSizingInfo& sizing) + { + return TelemetrySegmentBytes_(sizing, PM_DEVICE_TYPE_SYSTEM); + } + + StaticMetricValue SystemDataStore::FindStaticMetric(PM_METRIC metric) const + { + switch (metric) { + case PM_METRIC_CPU_VENDOR: + return int32_t(statics.cpuVendor); + case PM_METRIC_CPU_NAME: + return statics.cpuName.c_str(); + case PM_METRIC_CPU_POWER_LIMIT: + return statics.cpuPowerLimit; + default: + throw util::Except(PM_STATUS_QUERY_MALFORMED, + "Static metric not handled by system data store"); + } + } +} + diff --git a/IntelPresentMon/Interprocess/source/DataStores.h b/IntelPresentMon/Interprocess/source/DataStores.h new file mode 100644 index 000000000..11d15cc99 --- /dev/null +++ b/IntelPresentMon/Interprocess/source/DataStores.h @@ -0,0 +1,136 @@ +#pragma once +#include "SharedMemoryTypes.h" +#include "HistoryRing.h" +#include "TelemetryMap.h" +#include "../../CommonUtilities/Exception.h" +#include "../../CommonUtilities/log/Log.h" +#include "../../CommonUtilities/mc/MetricsTypes.h" +#include "../../PresentMonAPI2/PresentMonAPI.h" +#include +#include + +// these data stores intended to be hosted within StreamedDataSegment instances via template +// they provide the interface that the middleware will use to access frame/telemetry data +// as well as the interface the service will use to publish same + +namespace pmon::ipc +{ + using FrameData = util::metrics::FrameData; + using FrameHistoryRing = HistoryRing; + + class MetricCapabilities; + namespace intro + { + struct IntrospectionRoot; + } + + struct DataStoreSizingInfo + { + // Telemetry-only: introspection root + capability map. + const intro::IntrospectionRoot* pRoot = nullptr; + const MetricCapabilities* pCaps = nullptr; + // Frame + telemetry: ring sample capacity and optional override size. + size_t ringSamples = 0; + std::optional overrideBytes; + // Frame-only: backpressure behavior for frame rings. + bool backpressured = false; + }; + + using StaticMetricValue = std::variant< + double, + uint64_t, + int32_t, + uint32_t, + bool, + int64_t, + const char*>; + + struct FrameDataStore + { + FrameDataStore(ShmSegmentManager& segMan, size_t cap, bool backpressured) + : + frameData{ cap, segMan.get_allocator(), backpressured }, + statics{ .applicationName{ segMan.get_allocator() } } + {} + FrameDataStore(ShmSegmentManager& segMan, const DataStoreSizingInfo& sizing) + : FrameDataStore(segMan, sizing.ringSamples, sizing.backpressured) + {} + // values that never change over the life of a target, available for use with metric queries + // often lazy initialized upon receipt of the first present/frame + struct Statics + { + ShmString applicationName; + } statics; + // values used for internal bookkeeping, often static (but not necessarily), typically not derived from frame data + // and typically initialized once on first aquisition of target; may also feed into metric queries + struct Bookkeeping + { + uint32_t processId; + int64_t startQpc; + bool staticInitComplete = false; + bool bookkeepingInitComplete = false; + bool isPlayback = false; + } bookkeeping{}; + FrameHistoryRing frameData; + + StaticMetricValue FindStaticMetric(PM_METRIC metric) const; + + static size_t CalculateSegmentBytes(const DataStoreSizingInfo& sizing); + }; + + struct GpuDataStore + { + GpuDataStore(ShmSegmentManager& segMan) + : + telemetryData{ segMan.get_allocator() }, + statics{ + .name{ segMan.get_allocator() }, + .maxFanSpeedRpm{ segMan.get_allocator() } } + {} + GpuDataStore(ShmSegmentManager& segMan, const DataStoreSizingInfo&) + : GpuDataStore(segMan) + {} + struct Statics + { + PM_DEVICE_VENDOR vendor; + ShmString name; + double sustainedPowerLimit; + uint64_t memSize; + uint64_t maxMemBandwidth; + ShmVector maxFanSpeedRpm; + } statics; + TelemetryMap telemetryData; + + StaticMetricValue FindStaticMetric(PM_METRIC metric) const; + + static size_t CalculateSegmentBytes(const DataStoreSizingInfo& sizing); + }; + + struct SystemDataStore + { + SystemDataStore(ShmSegmentManager& segMan) + : + telemetryData{ segMan.get_allocator() }, + statics{ .cpuName{ segMan.get_allocator() } } + {} + SystemDataStore(ShmSegmentManager& segMan, const DataStoreSizingInfo&) + : SystemDataStore(segMan) + {} + struct Statics + { + PM_DEVICE_VENDOR cpuVendor; + ShmString cpuName; + double cpuPowerLimit; + } statics; + TelemetryMap telemetryData; + + StaticMetricValue FindStaticMetric(PM_METRIC metric) const; + + static size_t CalculateSegmentBytes(const DataStoreSizingInfo& sizing); + }; + + void PopulateTelemetryRings(TelemetryMap& telemetryData, + const DataStoreSizingInfo& sizing, + PM_DEVICE_TYPE deviceType); +} + diff --git a/IntelPresentMon/Interprocess/source/HistoryRing.h b/IntelPresentMon/Interprocess/source/HistoryRing.h new file mode 100644 index 000000000..e31f8d413 --- /dev/null +++ b/IntelPresentMon/Interprocess/source/HistoryRing.h @@ -0,0 +1,247 @@ +#pragma once +#include "ShmRing.h" +#include "../../CommonUtilities/log/Verbose.h" +#include +#include +#include +#include +#include + +namespace pmon::ipc +{ + // wrapper around ShmRing that adds the ability to search/address by timestamp + // meant for use with telemetry data + template + class HistoryRing + { + public: + // functions + HistoryRing(size_t capacity, ShmVector::allocator_type alloc, bool backpressured = false) + : + samples_{ capacity, alloc, backpressured } + { + if (capacity < ReadBufferSize * 2) { + throw std::logic_error{ "The capacity of a ShmRing must be at least double its ReadBufferSize" }; + } + } + bool Push(const T& sample, std::optional timeoutMs = {}) + { + return samples_.Push(sample, timeoutMs); + } + template().value)>, + typename = std::enable_if_t>> + bool Push(const ValueT& value, uint64_t timestamp, std::optional timeoutMs = {}) + { + return samples_.Push(U{ value, timestamp }, timeoutMs); + } + const T& Newest() const + { + auto&&[first, last] = samples_.GetSerialRange(); + return At(last - 1); + } + const T& At(size_t serial) const + { + return samples_.At(serial); + } + const T& Nearest(uint64_t timestamp) const + { + return samples_.At(NearestSerial(timestamp)); + } + std::pair GetSerialRange() const + { + return samples_.GetSerialRange(); + } + bool Empty() const + { + return samples_.Empty(); + } + size_t Size() const + { + const auto range = GetSerialRange(); + return range.second - range.first; + } + void MarkNextRead(size_t serial) const + { + samples_.MarkNextRead(serial); + } + // First serial with timestamp >= given timestamp. + // If all samples have timestamp < given timestamp, returns last (one past end). + size_t LowerBoundSerial(uint64_t timestamp) const + { + return BoundSerial_(timestamp); + } + // First serial with timestamp > given timestamp. + // If all samples have timestamp <= given timestamp, returns last (one past end). + size_t UpperBoundSerial(uint64_t timestamp) const + { + return BoundSerial_(timestamp); + } + // Find the serial whose timestamp is closest to the given timestamp. + // If the timestamp is outside the stored range, clamps to first/last. + size_t NearestSerial(uint64_t timestamp) const + { + const auto range = samples_.GetSerialRange(); + // empty ring case + if (range.first == range.second) { + pmlog_verb(util::log::V::ipc_ring)("Reading from empty history ring"); + return range.first; + } + + // First serial with timestamp >= requested + size_t serial = LowerBoundSerial(timestamp); + + // case where requested timestamp is newer than newest sample + if (serial >= range.second) { + // log timing and dump ring contents in case where ring has insufficient samples + if (util::log::GlobalPolicy::VCheck(util::log::V::ipc_ring)) { + std::string recentSamples; + const size_t sampleCount = range.second - range.first; + const size_t maxSamples = 12; + const size_t dumpCount = sampleCount < maxSamples ? sampleCount : maxSamples; + const size_t startSerial = range.second - dumpCount; + for (size_t s = startSerial; s < range.second; ++s) { + const auto& sample = At(s); + if (!recentSamples.empty()) { + recentSamples += "\n"; + } + if constexpr (HasValueMember_::value) { + using ValueMemberT = std::decay_t().value)>; + if constexpr (IsFormattable_::value) { + recentSamples += std::format("ts={} value={}", TimestampOf_(sample), sample.value); + } + else { + recentSamples += std::format("ts={}", TimestampOf_(sample)); + } + } + else { + recentSamples += std::format("ts={}", TimestampOf_(sample)); + } + } + pmlog_verb(util::log::V::ipc_ring)("Target timestamp past end of history ring") + .pmwatch(timestamp) + .pmwatch(range.second) + .pmwatch(int64_t(TimestampOf_(At(serial - 1))) - int64_t(timestamp)) + .watch("recent_samples", recentSamples); + } + + return range.second - 1; + } + + // Check whether the previous sample is actually closer. + // but only if there is a sample available before this one + if (serial > range.first) { + const auto nextTimestamp = TimestampOf_(At(serial)); + const auto prevTimestamp = TimestampOf_(At(serial - 1)); + const uint64_t dPrev = timestamp - prevTimestamp; + const uint64_t dNext = nextTimestamp - timestamp; + if (dPrev <= dNext) { + --serial; + } + } + + pmlog_verb(util::log::V::ipc_ring)("Found nearest sample") + .pmwatch(timestamp) + .pmwatch(serial) + .pmwatch(int64_t(TimestampOf_(At(serial))) - int64_t(timestamp)); + return serial; + } + // Calls func(sample) for each sample whose timestamp is in [start, end]. + // Intended use case is calculation of stats (avg, min, %) + // Returns the number of samples visited. + template + size_t ForEachInTimestampRange(uint64_t start, uint64_t end, F&& func) const + { + const auto range = samples_.GetSerialRange(); + + // Find the first sample with timestamp >= start + size_t serial = LowerBoundSerial(start); + + size_t count = 0; + // Walk forward until we leave the [start, end] window or hit last + for (; serial < range.second; ++serial) { + const T& s = At(serial); + if (TimestampOf_(s) > end) { + break; + } + // s.timestamp is guaranteed >= start by LowerBoundSerial + std::forward(func)(s); + ++count; + } + + return count; + } + + private: + // types + enum class BoundKind_ + { + Lower, + Upper + }; + template + struct HasValueMember_ : std::false_type {}; + template + struct HasValueMember_().value)>> : std::true_type {}; + template + struct IsFormattable_ : std::false_type {}; + template + struct IsFormattable_()))>> : std::true_type {}; + // functions + static uint64_t TimestampOf_(const T& sample) + { + return sample.*TimestampMember; + } + // Shared binary search implementation for LowerBoundSerial / UpperBoundSerial. + template + size_t BoundSerial_(uint64_t timestamp) const + { + auto range = samples_.GetSerialRange(); + size_t first = range.first; + size_t last = range.second; // one past end + + size_t lo = first; + size_t hi = last; + + // Standard lower/upper bound style search over [first, last) + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + const T& s = At(mid); + + if constexpr (Kind == BoundKind_::Lower) { + // First with s.timestamp >= timestamp + if (TimestampOf_(s) < timestamp) { + lo = mid + 1; + } + else { + hi = mid; + } + } + else { + // First with s.timestamp > timestamp + if (TimestampOf_(s) <= timestamp) { + lo = mid + 1; + } + else { + hi = mid; + } + } + } + + return lo; // in [first, last] + } + // data + ShmRing samples_; + }; + + template + struct TelemetrySample + { + using value_type = TValue; + TValue value; + uint64_t timestamp; + }; + // Alias for telemetry convenience; matches the prior HistoryRing behavior. + template + using SampleHistoryRing = HistoryRing, &TelemetrySample::timestamp, ReadBufferSize>; +} diff --git a/IntelPresentMon/Interprocess/source/Interprocess.cpp b/IntelPresentMon/Interprocess/source/Interprocess.cpp index fcb1c4e1d..475491169 100644 --- a/IntelPresentMon/Interprocess/source/Interprocess.cpp +++ b/IntelPresentMon/Interprocess/source/Interprocess.cpp @@ -1,202 +1,479 @@ +#include "../../CommonUtilities/win/WinAPI.h" #include "Interprocess.h" #include "IntrospectionTransfer.h" #include "IntrospectionPopulators.h" #include "SharedMemoryTypes.h" +#include "OwnedDataSegment.h" +#include "ViewedDataSegment.h" #include "IntrospectionCloneAllocators.h" +#include "../../CommonUtilities/win/Security.h" #include #include #include +#include #include #include "../../PresentMonService/GlobalIdentifiers.h" #include #include +#include +#include namespace pmon::ipc { - namespace bip = boost::interprocess; + namespace bip = boost::interprocess; + namespace vi = std::views; + namespace rn = std::ranges; - namespace - { - class CommsBase_ - { - protected: - static constexpr const char* defaultSegmentName_ = pmon::gid::defaultIntrospectionNsmName; - static constexpr const char* introspectionRootName_ = "in-root"; - static constexpr const char* introspectionMutexName_ = "in-mtx"; - static constexpr const char* introspectionSemaphoreName_ = "in-sem"; - }; + namespace + { + class CommsBase_ + { + protected: + static constexpr size_t introShmSize_ = 0x10'0000; + static constexpr const char* introspectionRootName_ = "in-root"; + static constexpr const char* introspectionMutexName_ = "in-mtx"; + static constexpr const char* introspectionSemaphoreName_ = "in-sem"; + }; - class ServiceComms_ : public ServiceComms, CommsBase_ - { - public: - ServiceComms_(std::optional sharedMemoryName) - : - shm_{ bip::create_only, sharedMemoryName.value_or(defaultSegmentName_).c_str(), - 0x10'0000, nullptr, Permissions_{} }, - pIntroMutex_{ ShmMakeNamedUnique( - introspectionMutexName_, shm_.get_segment_manager()) }, - pIntroSemaphore_{ ShmMakeNamedUnique( - introspectionSemaphoreName_, shm_.get_segment_manager(), 0) }, - pRoot_{ ShmMakeNamedUnique(introspectionRootName_, - shm_.get_segment_manager(), shm_.get_segment_manager()) } - { - PreInitializeIntrospection_(); - } - intro::IntrospectionRoot& GetIntrospectionRoot() override - { - return *pRoot_; - } - void RegisterGpuDevice(PM_DEVICE_VENDOR vendor, std::string deviceName, const GpuTelemetryBitset& gpuCaps, std::span luidBytes) override - { - auto lck = LockIntrospectionMutexExclusive_(); - intro::PopulateGpuDevice(shm_.get_segment_manager(), *pRoot_, nextDeviceIndex_++, vendor, deviceName, gpuCaps, luidBytes); - } - void FinalizeGpuDevices() override - { - auto lck = LockIntrospectionMutexExclusive_(); - introGpuComplete_ = true; - if (introGpuComplete_ && introCpuComplete_) { - lck.unlock(); - FinalizeIntrospection_(); - } - } - void RegisterCpuDevice(PM_DEVICE_VENDOR vendor, std::string deviceName, const CpuTelemetryBitset& cpuCaps) override - { - auto lck = LockIntrospectionMutexExclusive_(); - intro::PopulateCpu(shm_.get_segment_manager(), *pRoot_, vendor, deviceName, cpuCaps); - introCpuComplete_ = true; - if (introGpuComplete_ && introCpuComplete_) { - lck.unlock(); - FinalizeIntrospection_(); - } - } - private: - // types - class Permissions_ - { - public: - Permissions_() - { - if (!ConvertStringSecurityDescriptorToSecurityDescriptorA( - "D:(A;OICI;GA;;;WD)", - SDDL_REVISION_1, &secAttr_.lpSecurityDescriptor, NULL)) { - throw std::runtime_error{ "Failed to create security descriptor for shared memory" }; - } - } - operator bip::permissions() - { - return bip::permissions{ &secAttr_ }; - } - private: - SECURITY_ATTRIBUTES secAttr_{ sizeof(secAttr_) }; - }; - // functions - void PreInitializeIntrospection_() - { - // populate introspection data structures at service-side - auto pSegmentManager = shm_.get_segment_manager(); - auto charAlloc = pSegmentManager->get_allocator(); - intro::PopulateEnums(pSegmentManager, *pRoot_); - intro::PopulateMetrics(pSegmentManager, *pRoot_); - intro::PopulateUnits(pSegmentManager, *pRoot_); + class ServiceComms_ : public ServiceComms, CommsBase_ + { + public: + ServiceComms_(std::string prefix, + size_t frameRingSamples, + size_t telemetryRingSamples) + : + namer_{ std::move(prefix) }, + frameRingSamples_{ std::max(frameRingSamples, kMinRingSamples_) }, + telemetryRingSamples_{ std::max(telemetryRingSamples, kMinRingSamples_) }, + shm_{ bip::create_only, namer_.MakeIntrospectionName().c_str(), + introShmSize_, nullptr, Permissions_{} }, + pIntroMutex_{ ShmMakeNamedUnique( + introspectionMutexName_, shm_.get_segment_manager()) }, + pIntroSemaphore_{ ShmMakeNamedUnique( + introspectionSemaphoreName_, shm_.get_segment_manager(), 0) }, + pRoot_{ ShmMakeNamedUnique(introspectionRootName_, + shm_.get_segment_manager(), shm_.get_segment_manager()) } + { + PreInitializeIntrospection_(); + } + intro::IntrospectionRoot& GetIntrospectionRoot() override + { + return *pRoot_; + } + void RegisterGpuDevice(uint32_t deviceId, PM_DEVICE_VENDOR vendor, + std::string deviceName, const MetricCapabilities& caps, + std::span luidBytes) override + { + auto lck = LockIntrospectionMutexExclusive_(); + pmlog_dbg("GPU metric capabilities") + .pmwatch(deviceId) + .pmwatch(deviceName) + .pmwatch(caps.ToString(26)); + intro::PopulateGpuDevice( + shm_.get_segment_manager(), *pRoot_, + deviceId, vendor, deviceName, caps, luidBytes + ); + const DataStoreSizingInfo sizing{ pRoot_.get().get(), &caps, telemetryRingSamples_ }; + const auto segmentName = namer_.MakeGpuName(deviceId); + // allocate map node and create shm segment + auto& gpuShm = gpuShms_.emplace( + std::piecewise_construct, + std::forward_as_tuple(deviceId), + std::forward_as_tuple(segmentName, + sizing, + static_cast(Permissions_{})) + ).first->second; + // populate rings based on caps + PopulateTelemetryRings(gpuShm.GetStore().telemetryData, + sizing, + PM_DEVICE_TYPE_GRAPHICS_ADAPTER); + pmlog_dbg("Shm segment populated (GPU)") + .pmwatch(segmentName) + .pmwatch(deviceId) + .pmwatch(gpuShm.GetBytesTotal()) + .pmwatch(gpuShm.GetBytesUsed()) + .pmwatch(gpuShm.GetBytesFree()); + } + void FinalizeGpuDevices() override + { + auto lck = LockIntrospectionMutexExclusive_(); + introGpuComplete_ = true; + if (introGpuComplete_ && introCpuComplete_) { + lck.unlock(); + FinalizeIntrospection_(); + } + } + void RegisterCpuDevice(PM_DEVICE_VENDOR vendor, + std::string deviceName, + const MetricCapabilities& caps) override + { + auto lck = LockIntrospectionMutexExclusive_(); + constexpr size_t kWatchIndent = 5; + constexpr size_t kCapsTextIndent = kWatchIndent + (sizeof("capsText => ") - 1); + const auto capsText = caps.ToString(kCapsTextIndent); + pmlog_dbg("CPU metric capabilities") + .pmwatch(deviceName) + .pmwatch(capsText); + intro::PopulateCpu( + shm_.get_segment_manager(), *pRoot_, vendor, std::move(deviceName), caps + ); + const DataStoreSizingInfo sizing{ pRoot_.get().get(), &caps, telemetryRingSamples_ }; + const auto segmentName = namer_.MakeSystemName(); + if (!systemShm_) { + systemShm_.emplace(segmentName, + sizing, + static_cast(Permissions_{})); + } + // populate rings based on caps + PopulateTelemetryRings(systemShm_->GetStore().telemetryData, + sizing, + PM_DEVICE_TYPE_SYSTEM); + pmlog_dbg("Shm segment populated (System)") + .pmwatch(segmentName) + .pmwatch(systemShm_->GetBytesTotal()) + .pmwatch(systemShm_->GetBytesUsed()) + .pmwatch(systemShm_->GetBytesFree()); + introCpuComplete_ = true; + if (introGpuComplete_ && introCpuComplete_) { + lck.unlock(); + FinalizeIntrospection_(); + } + } + const ShmNamer& GetNamer() const override + { + return namer_; + } + // data store access + std::shared_ptr> + CreateOrGetFrameDataSegment(uint32_t pid, bool backpressured) override + { + // resolve out existing or new weak ptr, try and lock + auto& pWeak = frameShmWeaks_[pid]; + auto pFrameData = pWeak.lock(); + if (!pFrameData) { + // if weak ptr was new (or expired), lock will not work and we need to construct + // make a frame data store as shared ptr + const auto segmentName = namer_.MakeFrameName(pid); + const DataStoreSizingInfo sizing{ + .ringSamples = frameRingSamples_, + .backpressured = backpressured, + }; + pFrameData = std::shared_ptr>( + new OwnedDataSegment( + segmentName, + sizing, + static_cast(Permissions_{})), + [pid, segmentName](OwnedDataSegment* pSegment) { + pmlog_dbg("Frame data segment destroyed") + .pmwatch(pid) + .pmwatch(segmentName); + delete pSegment; + }); + // store a weak reference + pWeak = pFrameData; + pmlog_dbg("Frame data segment created") + .pmwatch(pid) + .pmwatch(segmentName) + .pmwatch(backpressured); + } + // remove stale elements to keep map lean + for (auto it = frameShmWeaks_.begin(); it != frameShmWeaks_.end(); ) { + if (it->second.expired()) { + const auto segmentName = namer_.MakeFrameName(it->first); + pmlog_dbg("Frame data segment released") + .pmwatch(it->first) + .pmwatch(segmentName); + it = frameShmWeaks_.erase(it); + } + else { + ++it; + } + } + return pFrameData; + } + std::shared_ptr> + GetFrameDataSegment(uint32_t pid) override + { + if (auto i = frameShmWeaks_.find(pid); i != frameShmWeaks_.end()) { + if (auto pSegment = i->second.lock()) { + return pSegment; + } + // if weak ptr has expired, garbage collect from the map + const auto segmentName = namer_.MakeFrameName(pid); + pmlog_dbg("Frame data segment released") + .pmwatch(pid) + .pmwatch(segmentName); + frameShmWeaks_.erase(i); + } + return {}; + } + std::vector GetFramePids() const override + { + return frameShmWeaks_ | vi::filter([](auto&& p) {return !p.second.expired(); }) | + vi::keys | rn::to(); + } + GpuDataStore& GetGpuDataStore(uint32_t deviceId) override + { + const auto it = gpuShms_.find(deviceId); + if (it == gpuShms_.end()) { + pmlog_error("No gpu segment found").pmwatch(deviceId).raise(); + } + return it->second.GetStore(); + } + SystemDataStore& GetSystemDataStore() override + { + if (!systemShm_) { + throw std::runtime_error("System data segment not initialized"); + } + return systemShm_->GetStore(); + } + private: + // types + class Permissions_ + { + public: + Permissions_() + : + pSecDesc_{ util::win::MakeSecurityDescriptor("D:(A;OICI;GA;;;WD)") }, + secAttr_{ .nLength = sizeof(secAttr_), .lpSecurityDescriptor = pSecDesc_.get() } + {} + operator bip::permissions() + { + return bip::permissions{ &secAttr_ }; + } + private: + util::UniqueLocalPtr pSecDesc_; + SECURITY_ATTRIBUTES secAttr_{ sizeof(secAttr_) }; + }; + // functions + void PreInitializeIntrospection_() + { + // populate introspection data structures at service-side + auto pSegmentManager = shm_.get_segment_manager(); + auto charAlloc = pSegmentManager->get_allocator(); + intro::PopulateEnums(pSegmentManager, *pRoot_); + intro::PopulateMetrics(pSegmentManager, *pRoot_); + intro::PopulateUnits(pSegmentManager, *pRoot_); // construct empty LUID object (size = 0 means no LUID) auto pLuid = ShmMakeUnique(pSegmentManager, std::span{}, pSegmentManager); + pRoot_->AddDevice(ShmMakeUnique( + pSegmentManager, + 0, PM_DEVICE_TYPE_INDEPENDENT, PM_DEVICE_VENDOR_UNKNOWN, + ShmString{ "Device-independent", charAlloc }, std::move(pLuid) + )); + } + void FinalizeIntrospection_() + { + // sort all ordered introspection entities in their principal containers + pRoot_->Sort(); + // release semaphore holdoff once construction is complete + for (int i = 0; i < 8; i++) { pIntroSemaphore_->post(); } + } + bip::scoped_lock + LockIntrospectionMutexExclusive_() + { + const auto result = + shm_.find(introspectionMutexName_); + if (!result.first) { + throw std::runtime_error{ + "Failed to find introspection mutex in shared memory" + }; + } + return bip::scoped_lock{ *result.first }; + } - pRoot_->AddDevice(ShmMakeUnique(pSegmentManager, - 0, PM_DEVICE_TYPE_INDEPENDENT, PM_DEVICE_VENDOR_UNKNOWN, ShmString{ "Device-independent", charAlloc }, std::move(pLuid))); - } - void FinalizeIntrospection_() - { - // sort all ordered introspection entities in their pricipal containers - pRoot_->Sort(); - // release semaphore holdoff once construction is complete - for (int i = 0; i < 8; i++) { pIntroSemaphore_->post(); } - } - bip::scoped_lock LockIntrospectionMutexExclusive_() - { - const auto result = shm_.find(introspectionMutexName_); - if (!result.first) { - throw std::runtime_error{ "Failed to find introspection mutex in shared memory" }; - } - return bip::scoped_lock{ *result.first }; - } - // data - ShmSegment shm_; - ShmUniquePtr pIntroMutex_; - ShmUniquePtr pIntroSemaphore_; - ShmUniquePtr pRoot_; - uint32_t nextDeviceIndex_ = 1; - bool introGpuComplete_ = false; - bool introCpuComplete_ = false; - }; + // data + static constexpr size_t kMinRingSamples_ = 8; + ShmNamer namer_; + size_t frameRingSamples_; + size_t telemetryRingSamples_; + ShmSegment shm_; + ShmUniquePtr pIntroMutex_; + ShmUniquePtr pIntroSemaphore_; + ShmUniquePtr pRoot_; + bool introGpuComplete_ = false; + bool introCpuComplete_ = false; - class MiddlewareComms_ : public MiddlewareComms, CommsBase_ - { - public: - MiddlewareComms_(std::optional sharedMemoryName) - : - shm_{ bip::open_only, sharedMemoryName.value_or(defaultSegmentName_).c_str() } - {} - const PM_INTROSPECTION_ROOT* GetIntrospectionRoot(uint32_t timeoutMs) override - { - // make sure holdoff semaphore has been released - WaitOnIntrospectionHoldoff_(timeoutMs); - // acquire shared lock on introspection data - auto sharedLock = LockIntrospectionMutexForShare_(); - // find the introspection structure in shared memory - const auto result = shm_.find(introspectionRootName_); - if (!result.first) { - throw std::runtime_error{ "Failed to find introspection root in shared memory" }; - } - const auto& root = *result.first; - // probe allocator used to determine size of memory block required to hold the CAPI instrospection structure - intro::ProbeAllocator probeAllocator; - // this call to clone doesn't allocate of initialize any memory, the probe just determines required memory - root.ApiClone(probeAllocator); - // create actual allocator based on required size - ipc::intro::BlockAllocator blockAllocator{ probeAllocator.GetTotalSize() }; - // create the CAPI introspection struct on the heap, it is now the caller's responsibility to track this resource - return root.ApiClone(blockAllocator); - } - private: - // functions - void WaitOnIntrospectionHoldoff_(uint32_t timeoutMs) - { - using namespace std::chrono_literals; - using clock = std::chrono::high_resolution_clock; - const auto result = shm_.find(introspectionSemaphoreName_); - if (!result.first) { - throw std::runtime_error{ "Failed to find introspection semaphore in shared memory" }; - } - auto& sem = *result.first; - // wait for holdoff to be released (timeout after XXXms) - if (!sem.timed_wait(clock::now() + 1ms * timeoutMs)) { - throw std::runtime_error{ "timeout accessing introspection" }; - } - // return the slot we just took because holdoff should not limit entry once released - sem.post(); - } - bip::sharable_lock LockIntrospectionMutexForShare_() - { - const auto result = shm_.find(introspectionMutexName_); - if (!result.first) { - throw std::runtime_error{ "Failed to find introspection mutex in shared memory" }; - } - return bip::sharable_lock{ *result.first }; - } - // data - ShmSegment shm_; - }; - } + std::optional> systemShm_; + std::unordered_map>> frameShmWeaks_; + std::unordered_map> gpuShms_; + }; - std::unique_ptr MakeMiddlewareComms(std::optional sharedMemoryName) - { - return std::make_unique(std::move(sharedMemoryName)); - } + class MiddlewareComms_ : public MiddlewareComms, CommsBase_ + { + public: + MiddlewareComms_(std::string prefix, std::string salt) + : + namer_{ std::move(prefix), std::move(salt) }, + shm_{ bip::open_only, namer_.MakeIntrospectionName().c_str() } + { + WaitOnIntrospectionHoldoff_(1500); + systemShm_.emplace(namer_.MakeSystemName()); + // Eager-load all GPU segments based on introspection + auto ids = GetGpuDeviceIds_(); + for (auto id : ids) { + gpuShms_.emplace( + std::piecewise_construct, + std::forward_as_tuple(id), + std::forward_as_tuple(namer_.MakeGpuName(id)) + ); + } + } + const PM_INTROSPECTION_ROOT* GetIntrospectionRoot(uint32_t timeoutMs) override + { + // make sure holdoff semaphore has been released + WaitOnIntrospectionHoldoff_(timeoutMs); + // acquire shared lock on introspection data + auto sharedLock = LockIntrospectionMutexForShare_(); + // find the introspection structure in shared memory + const auto result = shm_.find(introspectionRootName_); + if (!result.first) { + throw std::runtime_error{ "Failed to find introspection root in shared memory" }; + } + const auto& root = *result.first; + // probe allocator used to determine size of memory block required to hold + // the CAPI introspection structure + intro::ProbeAllocator probeAllocator; + // this call to clone doesn't allocate or initialize any memory, + // the probe just determines required memory + root.ApiClone(probeAllocator); + // create actual allocator based on required size + ipc::intro::BlockAllocator blockAllocator{ probeAllocator.GetTotalSize() }; + // create the CAPI introspection struct on the heap, it is now the caller's + // responsibility to track this resource + return root.ApiClone(blockAllocator); + } + void OpenFrameDataStore(uint32_t pid) override + { + // If already open, nothing to do + if (frameShms_.find(pid) != frameShms_.end()) { + return; + } + + const auto segName = namer_.MakeFrameName(pid); + frameShms_.emplace( + std::piecewise_construct, + std::forward_as_tuple(pid), + std::forward_as_tuple(segName) + ); + pmlog_dbg("Frame data segment opened") + .pmwatch(pid) + .pmwatch(segName); + } + void CloseFrameDataStore(uint32_t pid) override + { + if (auto it = frameShms_.find(pid); it != frameShms_.end()) { + const auto segName = namer_.MakeFrameName(pid); + pmlog_dbg("Frame data segment closed") + .pmwatch(pid) + .pmwatch(segName); + frameShms_.erase(it); + } + } + // data store access + const FrameDataStore& GetFrameDataStore(uint32_t pid) const override + { + const auto it = frameShms_.find(pid); + if (it == frameShms_.end()) { + throw std::runtime_error{ "Frame data segment not open for this PID" }; + } + return it->second.GetStore(); + } + const GpuDataStore& GetGpuDataStore(uint32_t deviceId) const override + { + const auto it = gpuShms_.find(deviceId); + if (it == gpuShms_.end()) { + throw std::runtime_error{ "No GPU data segment found for this deviceId" }; + } + return it->second.GetStore(); + } + const SystemDataStore& GetSystemDataStore() const override + { + if (!systemShm_) { + throw std::runtime_error{ "System data segment not open" }; + } + return systemShm_->GetStore(); + } + + private: + // functions + std::vector GetGpuDeviceIds_() + { + // make sure holdoff semaphore has been released + WaitOnIntrospectionHoldoff_(1500); + // acquire shared lock on introspection data + auto sharedLock = LockIntrospectionMutexForShare_(); + // find the introspection structure in shared memory + const auto result = shm_.find(introspectionRootName_); + if (!result.first) { + throw std::runtime_error{ "Failed to find introspection root in shared memory" }; + } + std::vector ids; + for (auto& p : result.first->GetDevices()) { + // GPU device IDs live in the range (0, kSystemDeviceId) + if (auto id = p->GetId(); id > 0 && id < kSystemDeviceId) { + ids.push_back(id); + } + } + return ids; + } + void WaitOnIntrospectionHoldoff_(uint32_t timeoutMs) + { + using namespace std::chrono_literals; + using clock = std::chrono::high_resolution_clock; + const auto result = shm_.find(introspectionSemaphoreName_); + if (!result.first) { + throw std::runtime_error{ + "Failed to find introspection semaphore in shared memory" + }; + } + auto& sem = *result.first; + // wait for holdoff to be released (timeout after timeoutMs) + if (!sem.timed_wait(clock::now() + 1ms * timeoutMs)) { + throw std::runtime_error{ "timeout accessing introspection" }; + } + // return the slot we just took because holdoff should not limit entry once released + sem.post(); + } + bip::sharable_lock + LockIntrospectionMutexForShare_() + { + const auto result = + shm_.find(introspectionMutexName_); + if (!result.first) { + throw std::runtime_error{ + "Failed to find introspection mutex in shared memory" + }; + } + return bip::sharable_lock{ *result.first }; + } + + // data + ShmNamer namer_; + ShmSegment shm_; // introspection shm + + std::optional> systemShm_; + std::unordered_map> gpuShms_; + std::unordered_map> frameShms_; + }; + } + + std::unique_ptr + MakeServiceComms(std::string prefix, + size_t frameRingSamples, + size_t telemetryRingSamples) + { + return std::make_unique( + std::move(prefix), + frameRingSamples, + telemetryRingSamples); + } + + std::unique_ptr + MakeMiddlewareComms(std::string prefix, std::string salt) + { + return std::make_unique(std::move(prefix), std::move(salt)); + } +} - std::unique_ptr MakeServiceComms(std::optional sharedMemoryName) - { - return std::make_unique(std::move(sharedMemoryName)); - } -} \ No newline at end of file diff --git a/IntelPresentMon/Interprocess/source/Interprocess.h b/IntelPresentMon/Interprocess/source/Interprocess.h index 89d1e9c9f..3d67d92e8 100644 --- a/IntelPresentMon/Interprocess/source/Interprocess.h +++ b/IntelPresentMon/Interprocess/source/Interprocess.h @@ -2,12 +2,13 @@ #include #include #include -#include #include "../../ControlLib/PresentMonPowerTelemetry.h" #include "../../ControlLib/CpuTelemetryInfo.h" #include "../../PresentMonAPI2/PresentMonAPI.h" - -struct PM_INTROSPECTION_ROOT; +#include "DataStores.h" +#include "OwnedDataSegment.h" +#include "MetricCapabilities.h" +#include "ShmNamer.h" namespace pmon::ipc { @@ -21,9 +22,18 @@ namespace pmon::ipc public: virtual ~ServiceComms() = default; virtual intro::IntrospectionRoot& GetIntrospectionRoot() = 0; - virtual void RegisterGpuDevice(PM_DEVICE_VENDOR vendor, std::string deviceName, const GpuTelemetryBitset& gpuCaps, std::span luidBytes) = 0; + virtual void RegisterGpuDevice(uint32_t deviceId, PM_DEVICE_VENDOR vendor, std::string deviceName, const MetricCapabilities& caps, + std::span luidBytes) = 0; virtual void FinalizeGpuDevices() = 0; - virtual void RegisterCpuDevice(PM_DEVICE_VENDOR vendor, std::string deviceName, const CpuTelemetryBitset& cpuCaps) = 0; + virtual void RegisterCpuDevice(PM_DEVICE_VENDOR vendor, std::string deviceName, const MetricCapabilities& caps) = 0; + virtual const ShmNamer& GetNamer() const = 0; + + // data store access + virtual std::shared_ptr> CreateOrGetFrameDataSegment(uint32_t pid, bool backpressured) = 0; + virtual std::shared_ptr> GetFrameDataSegment(uint32_t pid) = 0; + virtual std::vector GetFramePids() const = 0; + virtual GpuDataStore& GetGpuDataStore(uint32_t deviceId) = 0; + virtual SystemDataStore& GetSystemDataStore() = 0; }; class MiddlewareComms @@ -31,8 +41,19 @@ namespace pmon::ipc public: virtual ~MiddlewareComms() = default; virtual const PM_INTROSPECTION_ROOT* GetIntrospectionRoot(uint32_t timeoutMs = 2000) = 0; + + // data store access + // not const because of the backpressure case + // TODO: consider more separation of backpressure and broadcast cases + virtual const FrameDataStore& GetFrameDataStore(uint32_t pid) const = 0; + virtual const GpuDataStore& GetGpuDataStore(uint32_t deviceId) const = 0; + virtual const SystemDataStore& GetSystemDataStore() const = 0; + virtual void OpenFrameDataStore(uint32_t pid) = 0; + virtual void CloseFrameDataStore(uint32_t pid) = 0; }; - std::unique_ptr MakeServiceComms(std::optional sharedMemoryName = {}); - std::unique_ptr MakeMiddlewareComms(std::optional sharedMemoryName = {}); + std::unique_ptr MakeServiceComms(std::string prefix, + size_t frameRingSamples, + size_t telemetryRingSamples); + std::unique_ptr MakeMiddlewareComms(std::string prefix, std::string salt); } \ No newline at end of file diff --git a/IntelPresentMon/Interprocess/source/IntrospectionCapsLookup.h b/IntelPresentMon/Interprocess/source/IntrospectionCapsLookup.h index 43e763e29..1062c9ab9 100644 --- a/IntelPresentMon/Interprocess/source/IntrospectionCapsLookup.h +++ b/IntelPresentMon/Interprocess/source/IntrospectionCapsLookup.h @@ -1,18 +1,18 @@ -#pragma once +#pragma once #include "IntrospectionMetadata.h" #include #include "../../ControlLib/PresentMonPowerTelemetry.h" #include "../../ControlLib/CpuTelemetryInfo.h" - namespace pmon::ipc::intro { // mapping of caps to metrics template struct IntrospectionCapsLookup { using Universal = std::true_type; }; + // sentinel metric used for enumeration only (no availability) + template<> struct IntrospectionCapsLookup {}; // GPU caps template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_power; }; - template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_sustained_power_limit; }; template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_voltage; }; template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_temperature; }; template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_frequency; }; @@ -36,15 +36,15 @@ namespace pmon::ipc::intro GpuTelemetryCapBits::fan_speed_2, GpuTelemetryCapBits::fan_speed_3, GpuTelemetryCapBits::fan_speed_4, }; }; template<> struct IntrospectionCapsLookup { + using Derived = std::true_type; static constexpr auto gpuCapBitArray = std::array{ GpuTelemetryCapBits::max_fan_speed_0, GpuTelemetryCapBits::max_fan_speed_1, GpuTelemetryCapBits::max_fan_speed_2, GpuTelemetryCapBits::max_fan_speed_3, GpuTelemetryCapBits::max_fan_speed_4, }; }; - //template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBitArray = std::array{ - // GpuTelemetryCapBits::psu_info_0, GpuTelemetryCapBits::psu_info_1, GpuTelemetryCapBits::psu_info_2, GpuTelemetryCapBits::psu_info_3, GpuTelemetryCapBits::psu_info_4, }; }; - template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_mem_size; }; template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_mem_used; }; - template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_mem_used; }; - template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_mem_max_bandwidth; }; + template<> struct IntrospectionCapsLookup { + using Derived = std::true_type; + static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_mem_used; + }; template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_mem_write_bandwidth; }; template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_mem_read_bandwidth; }; template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_power_limited; }; @@ -58,16 +58,25 @@ namespace pmon::ipc::intro template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::vram_voltage_limited; }; template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::vram_utilization_limited; }; // static GPU + // TODO: static and cap bit signalling are NOT mutually exclusive!! template<> struct IntrospectionCapsLookup { using GpuDeviceStatic = std::true_type; }; template<> struct IntrospectionCapsLookup { using GpuDeviceStatic = std::true_type; }; + template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_sustained_power_limit; }; + template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_mem_size; }; + template<> struct IntrospectionCapsLookup { static constexpr auto gpuCapBit = GpuTelemetryCapBits::gpu_mem_max_bandwidth; }; // CPU caps template<> struct IntrospectionCapsLookup { static constexpr auto cpuCapBit = CpuTelemetryCapBits::cpu_utilization; }; template<> struct IntrospectionCapsLookup { static constexpr auto cpuCapBit = CpuTelemetryCapBits::cpu_power; }; template<> struct IntrospectionCapsLookup { static constexpr auto cpuCapBit = CpuTelemetryCapBits::cpu_temperature; }; template<> struct IntrospectionCapsLookup { static constexpr auto cpuCapBit = CpuTelemetryCapBits::cpu_frequency; }; - template<> struct IntrospectionCapsLookup { using ManualDisable = std::true_type; }; + template<> struct IntrospectionCapsLookup { using ManualDisable = std::true_type; + static constexpr auto cpuCapBit = CpuTelemetryCapBits::cpu_core_utility; + }; // static CPU template<> struct IntrospectionCapsLookup { static constexpr auto cpuCapBit = CpuTelemetryCapBits::cpu_power_limit; }; + template<> struct IntrospectionCapsLookup { using CpuStatic = std::true_type; }; + template<> struct IntrospectionCapsLookup { using CpuStatic = std::true_type; }; + // TODO: consider additional static CPU metrics beyond name/vendor. // concepts to help determine device-metric mapping type @@ -76,7 +85,7 @@ namespace pmon::ipc::intro template concept IsGpuDeviceMetricArray = requires { T::gpuCapBitArray; }; template concept IsGpuDeviceStaticMetric = requires { typename T::GpuDeviceStatic; }; template concept IsCpuMetric = requires { T::cpuCapBit; }; + template concept IsCpuStaticMetric = requires { typename T::CpuStatic; }; template concept IsManualDisableMetric = requires { typename T::ManualDisable; }; - - // TODO: compile-time verify that all cap bits are covered (how?) -} \ No newline at end of file + template concept IsDerivedMetric = requires { typename T::Derived; }; +} diff --git a/IntelPresentMon/Interprocess/source/IntrospectionDataTypeMapping.h b/IntelPresentMon/Interprocess/source/IntrospectionDataTypeMapping.h index d7cca97ee..779bba894 100644 --- a/IntelPresentMon/Interprocess/source/IntrospectionDataTypeMapping.h +++ b/IntelPresentMon/Interprocess/source/IntrospectionDataTypeMapping.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "../../PresentMonAPI2/PresentMonAPI.h" #include "metadata/EnumDataType.h" #include "metadata/MasterEnumList.h" @@ -38,6 +38,11 @@ namespace pmon::ipc::intro { template<> constexpr size_t DataTypeToStaticType_sz = 0ull; + template + constexpr size_t DataTypeToStaticType_align = alignof(DataTypeToStaticType_t); + template<> + constexpr size_t DataTypeToStaticType_align = 1ull; + template class F, typename...P> auto BridgeDataType(PM_DATA_TYPE dataType, P&&...args) { @@ -87,4 +92,4 @@ namespace pmon::ipc::intro { } return F::Default(std::forward

(args)...); } -} \ No newline at end of file +} diff --git a/IntelPresentMon/Interprocess/source/IntrospectionHelpers.cpp b/IntelPresentMon/Interprocess/source/IntrospectionHelpers.cpp index 69fd5f43c..7c074706b 100644 --- a/IntelPresentMon/Interprocess/source/IntrospectionHelpers.cpp +++ b/IntelPresentMon/Interprocess/source/IntrospectionHelpers.cpp @@ -1,4 +1,4 @@ -#include "IntrospectionHelpers.h" +#include "IntrospectionHelpers.h" #include "metadata/EnumDataType.h" #include "IntrospectionMacroHelpers.h" #include "IntrospectionDataTypeMapping.h" @@ -11,8 +11,18 @@ namespace pmon::ipc::intro static size_t Default() { return 0ull; } }; + template struct DataTypeAlignmentBridger { + static size_t Invoke() { return DataTypeToStaticType_align

; } + static size_t Default() { return 1ull; } + }; + size_t GetDataTypeSize(PM_DATA_TYPE dt) { return BridgeDataType(dt); } -} \ No newline at end of file + + size_t GetDataTypeAlignment(PM_DATA_TYPE dt) + { + return BridgeDataType(dt); + } +} diff --git a/IntelPresentMon/Interprocess/source/IntrospectionHelpers.h b/IntelPresentMon/Interprocess/source/IntrospectionHelpers.h index 74db25c97..47d9078dd 100644 --- a/IntelPresentMon/Interprocess/source/IntrospectionHelpers.h +++ b/IntelPresentMon/Interprocess/source/IntrospectionHelpers.h @@ -1,7 +1,8 @@ -#pragma once +#pragma once #include "../../PresentMonAPI2/PresentMonAPI.h" namespace pmon::ipc::intro { size_t GetDataTypeSize(PM_DATA_TYPE v); -} \ No newline at end of file + size_t GetDataTypeAlignment(PM_DATA_TYPE v); +} diff --git a/IntelPresentMon/Interprocess/source/IntrospectionPopulators.cpp b/IntelPresentMon/Interprocess/source/IntrospectionPopulators.cpp index 1941989e2..fcc374306 100644 --- a/IntelPresentMon/Interprocess/source/IntrospectionPopulators.cpp +++ b/IntelPresentMon/Interprocess/source/IntrospectionPopulators.cpp @@ -2,6 +2,9 @@ #include "IntrospectionMetadata.h" #include "IntrospectionTransfer.h" #include "IntrospectionCapsLookup.h" +#include "MetricCapabilities.h" +#include "IntrospectionPopulators.h" +#include "../../CommonUtilities/log/Log.h" #include #include @@ -44,7 +47,7 @@ namespace pmon::ipc::intro PREFERRED_UNIT_LIST(X_PREF_UNIT) -#undef X_REG_METRIC +#undef X_PREF_UNIT #define X_REG_METRIC(metric, metric_type, unit, data_type_polled, data_type_frame, enum_id, device_type, ...) { \ auto pMetric = ShmMakeUnique(pSegmentManager, pSegmentManager, \ @@ -71,39 +74,29 @@ namespace pmon::ipc::intro #undef X_REG_UNIT } - template - void RegisterGpuMetricDeviceInfo_(ShmSegmentManager* pSegmentManager, IntrospectionRoot& root, - uint32_t deviceId, const GpuTelemetryBitset& gpuCaps) + static void PopulateDeviceMetrics_(IntrospectionRoot& root, + const MetricCapabilities& caps, uint32_t deviceId) { - using Lookup = IntrospectionCapsLookup; - std::optional info; - if constexpr (IsUniversalMetric) {} - else if constexpr (IsGpuDeviceMetric) { - const auto availability = gpuCaps[size_t(Lookup::gpuCapBit)] ? - PM_METRIC_AVAILABILITY_AVAILABLE : PM_METRIC_AVAILABILITY_UNAVAILABLE; - info.emplace(deviceId, availability, 1); - } - else if constexpr (IsGpuDeviceMetricArray) { - const auto nAvailable = (uint32_t)rn::count_if(Lookup::gpuCapBitArray, [&](auto bit){ return gpuCaps[size_t(bit)]; }); - const auto availability = nAvailable > 0 ? - PM_METRIC_AVAILABILITY_AVAILABLE : PM_METRIC_AVAILABILITY_UNAVAILABLE; - info.emplace(deviceId, availability, nAvailable); - } - else if constexpr (IsGpuDeviceStaticMetric) { - info.emplace(deviceId, PM_METRIC_AVAILABILITY_AVAILABLE, 1); - } - if (info) { - if (auto i = rn::find(root.GetMetrics(), metric, [](const ShmUniquePtr& pMetric) { + for (auto&& [metric, count] : caps) { + auto i = rn::find( + root.GetMetrics(), + metric, + [](const ShmUniquePtr& pMetric) { return pMetric->GetId(); - }); i != root.GetMetrics().end()) { - (*i)->AddDeviceMetricInfo(std::move(*info)); + }); + if (i != root.GetMetrics().end()) { + const auto availability = count ? PM_METRIC_AVAILABILITY_AVAILABLE : + PM_METRIC_AVAILABILITY_UNAVAILABLE; + (*i)->AddDeviceMetricInfo(IntrospectionDeviceMetricInfo{ deviceId, availability, (uint32_t)count }); + } + else { + pmlog_error("Metric ID not found").pmwatch((int)metric); } - // TODO: log metric not found } } void PopulateGpuDevice(ShmSegmentManager* pSegmentManager, IntrospectionRoot& root, uint32_t deviceId, - PM_DEVICE_VENDOR vendor, const std::string& deviceName, const GpuTelemetryBitset& gpuCaps, std::span luidBytes) + PM_DEVICE_VENDOR vendor, const std::string& deviceName, const MetricCapabilities& caps, std::span luidBytes) { // add the device auto charAlloc = pSegmentManager->get_allocator(); @@ -111,42 +104,20 @@ namespace pmon::ipc::intro root.AddDevice(ShmMakeUnique(pSegmentManager, deviceId, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, vendor, ShmString{ deviceName.c_str(), charAlloc }, std::move(pLuid))); - // populate device-metric info for this device -#define X_WALK_METRIC(metric, metric_type, unit, data_type, enum_id, device_type, ...) \ - RegisterGpuMetricDeviceInfo_(pSegmentManager, root, deviceId, gpuCaps); - - METRIC_LIST(X_WALK_METRIC) - -#undef X_WALK_METRIC - } - - template - void RegisterCpuMetricDeviceInfo_(ShmSegmentManager* pSegmentManager, IntrospectionRoot& root, - const CpuTelemetryBitset& cpuCaps) - { - using Lookup = IntrospectionCapsLookup; - if constexpr (IsCpuMetric) { - if (auto i = rn::find(root.GetMetrics(), metric, [](const ShmUniquePtr& pMetric) { - return pMetric->GetId(); - }); i != root.GetMetrics().end()) { - const auto availability = cpuCaps[size_t(Lookup::cpuCapBit)] ? - PM_METRIC_AVAILABILITY_AVAILABLE : PM_METRIC_AVAILABILITY_UNAVAILABLE; - (*i)->AddDeviceMetricInfo(IntrospectionDeviceMetricInfo{ 0, availability, 1 }); - } - } - - // TODO: log metric not found + // add the device metrics + PopulateDeviceMetrics_(root, caps, deviceId); } void PopulateCpu(ShmSegmentManager* pSegmentManager, IntrospectionRoot& root, - PM_DEVICE_VENDOR vendor, const std::string& deviceName, const CpuTelemetryBitset& cpuCaps) + PM_DEVICE_VENDOR vendor, const std::string& deviceName, const MetricCapabilities& caps) { - // populate device-metric info for the cpu -#define X_WALK_METRIC(metric, metric_type, unit, data_type, enum_id, device_type, ...) \ - RegisterCpuMetricDeviceInfo_(pSegmentManager, root, cpuCaps); - - METRIC_LIST(X_WALK_METRIC) - -#undef X_WALK_METRIC + // add the device + auto charAlloc = pSegmentManager->get_allocator(); + // construct empty LUID object (size = 0 means no LUID) + auto pLuid = ShmMakeUnique(pSegmentManager, std::span{}, pSegmentManager); + root.AddDevice(ShmMakeUnique(pSegmentManager, ::pmon::ipc::kSystemDeviceId, + PM_DEVICE_TYPE_SYSTEM, vendor, ShmString{ "System", charAlloc}, std::move(pLuid))); + PopulateDeviceMetrics_(root, caps, ::pmon::ipc::kSystemDeviceId); } -} \ No newline at end of file + +} diff --git a/IntelPresentMon/Interprocess/source/IntrospectionPopulators.h b/IntelPresentMon/Interprocess/source/IntrospectionPopulators.h index 967a7ef7e..e4493f075 100644 --- a/IntelPresentMon/Interprocess/source/IntrospectionPopulators.h +++ b/IntelPresentMon/Interprocess/source/IntrospectionPopulators.h @@ -1,8 +1,8 @@ #pragma once #include "../../PresentMonAPI2/PresentMonAPI.h" #include "SharedMemoryTypes.h" -#include "../../ControlLib/PresentMonPowerTelemetry.h" -#include "../../ControlLib/CpuTelemetryInfo.h" +#include "MetricCapabilities.h" +#include "SystemDeviceId.h" // TODO: forward declare the segment manager (or type erase) @@ -12,7 +12,7 @@ namespace pmon::ipc::intro void PopulateMetrics(ShmSegmentManager* pSegmentManager, struct IntrospectionRoot& root); void PopulateUnits(ShmSegmentManager* pSegmentManager, struct IntrospectionRoot& root); void PopulateGpuDevice(ShmSegmentManager* pSegmentManager, IntrospectionRoot& root, uint32_t deviceId, - PM_DEVICE_VENDOR vendor, const std::string& deviceName, const GpuTelemetryBitset& gpuCaps, std::span luidBytes); + PM_DEVICE_VENDOR vendor, const std::string& deviceName, const MetricCapabilities& caps, std::span luidBytes); void PopulateCpu(ShmSegmentManager* pSegmentManager, IntrospectionRoot& root, - PM_DEVICE_VENDOR vendor, const std::string& deviceName, const CpuTelemetryBitset& cpuCaps); -} \ No newline at end of file + PM_DEVICE_VENDOR vendor, const std::string& deviceName, const MetricCapabilities& caps); +} diff --git a/IntelPresentMon/Interprocess/source/IntrospectionTransfer.h b/IntelPresentMon/Interprocess/source/IntrospectionTransfer.h index df267b26a..6301246ce 100644 --- a/IntelPresentMon/Interprocess/source/IntrospectionTransfer.h +++ b/IntelPresentMon/Interprocess/source/IntrospectionTransfer.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include #include @@ -6,6 +6,7 @@ #include #include #include "../../PresentMonAPI2/PresentMonAPI.h" +#include "../../CommonUtilities/Exception.h" #include "SharedMemoryTypes.h" #include #include @@ -105,6 +106,10 @@ namespace pmon::ipc::intro { return { buffer_ }; } + std::span> GetElements() const + { + return { buffer_ }; + } void Sort() { if constexpr (LessThanComparable_) { @@ -306,6 +311,14 @@ namespace pmon::ipc::intro { return id_ < rhs.id_; } + uint32_t GetId() const + { + return id_; + } + PM_DEVICE_TYPE GetType() const + { + return type_; + } private: uint32_t id_; PM_DEVICE_TYPE type_; @@ -342,6 +355,10 @@ namespace pmon::ipc::intro } return pSelf; } + uint32_t GetDeviceId() const + { + return deviceId_; + } private: uint32_t deviceId_; PM_METRIC_AVAILABILITY availability_; @@ -376,6 +393,10 @@ namespace pmon::ipc::intro } return pSelf; } + PM_DATA_TYPE GetFrameType() const + { + return frameType_; + } private: PM_DATA_TYPE polledType_; PM_DATA_TYPE frameType_; @@ -509,6 +530,18 @@ namespace pmon::ipc::intro { return id_; } + const IntrospectionDataTypeInfo& GetDataTypeInfo() const + { + return *pTypeInfo_; + } + std::span> GetDeviceMetricInfo() const + { + return deviceMetricInfo_.GetElements(); + } + PM_METRIC_TYPE GetMetricType() const + { + return type_; + } bool operator<(const IntrospectionMetric& rhs) const { return id_ < rhs.id_; @@ -553,6 +586,37 @@ namespace pmon::ipc::intro { return metrics_.GetElements(); } + std::span> GetMetrics() const + { + return metrics_.GetElements(); + } + std::span> GetDevices() + { + return devices_.GetElements(); + } + std::span> GetDevices() const + { + return devices_.GetElements(); + } + IntrospectionMetric& FindMetric(PM_METRIC metric) + { + for (auto& p : GetMetrics()) { + if (p->GetId() == metric) { + return *p; + } + } + throw util::Except<>("Metric ID not found in introspection"); + } + const IntrospectionMetric& FindMetric(PM_METRIC metric) const + { + for (const auto& p : GetMetrics()) { + if (p->GetId() == metric) { + return *p; + } + } + throw util::Except<>("Metric ID not found in introspection"); + } + using ApiType = PM_INTROSPECTION_ROOT; template const ApiType* ApiClone(V voidAlloc) const @@ -590,4 +654,4 @@ namespace pmon::ipc::intro IntrospectionObjArray devices_; IntrospectionObjArray units_; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/Interprocess/source/MetricCapabilities.cpp b/IntelPresentMon/Interprocess/source/MetricCapabilities.cpp new file mode 100644 index 000000000..bafac4c8c --- /dev/null +++ b/IntelPresentMon/Interprocess/source/MetricCapabilities.cpp @@ -0,0 +1,59 @@ +#include "MetricCapabilities.h" +#include +#include + +namespace pmon::ipc +{ + void MetricCapabilities::Set(PM_METRIC metricId, size_t arraySize) + { + if (arraySize == 0) { + // Zero-sized capabilities are effectively "not available"; + // ignore rather than storing. + return; + } + + auto it = caps_.find(metricId); + if (it == caps_.end()) { + caps_.emplace(metricId, arraySize); + } + else { + it->second = arraySize; + } + } + + void MetricCapabilities::Merge(const MetricCapabilities& capsToMerge) + { + // Union of capabilities; for overlapping metrics overwrite + for (const auto& kv : capsToMerge.caps_) { + const auto metricId = kv.first; + const auto arraySize = kv.second; + Set(metricId, arraySize); + } + } + + size_t MetricCapabilities::Check(PM_METRIC metricId) const noexcept + { + auto it = caps_.find(metricId); + if (it == caps_.end()) { + return 0; // not present / not available + } + return it->second; + } + + std::string MetricCapabilities::ToString(size_t indentSpaces) const + { + std::ostringstream oss; + const std::string indent(indentSpaces, ' '); + bool first = true; + for (const auto& kv : caps_) { + if (!first) { + oss << "\r\n"; + oss << indent; + } + first = false; + oss << "metricId=" << static_cast>(kv.first) + << " arraySize=" << kv.second; + } + return oss.str(); + } +} diff --git a/IntelPresentMon/Interprocess/source/MetricCapabilities.h b/IntelPresentMon/Interprocess/source/MetricCapabilities.h new file mode 100644 index 000000000..1be3e8e11 --- /dev/null +++ b/IntelPresentMon/Interprocess/source/MetricCapabilities.h @@ -0,0 +1,29 @@ +#pragma once +#include "../../PresentMonAPI2/PresentMonAPI.h" +#include +#include + +namespace pmon::ipc +{ + // capabilities typically communicated from telemetry providers to be collected in here + // used to determine which metrics available for queries, which ShmRings need to be + // allocated elements, etc. + class MetricCapabilities + { + public: + void Set(PM_METRIC metricId, size_t arraySize); + void Merge(const MetricCapabilities& capsToMerge); + size_t Check(PM_METRIC metricId) const noexcept; + std::string ToString(size_t indentSpaces = 0) const; + auto begin() const noexcept + { + return caps_.begin(); + } + auto end() const noexcept + { + return caps_.end(); + } + private: + std::unordered_map caps_; + }; +} diff --git a/IntelPresentMon/Interprocess/source/MetricCapabilitiesShim.cpp b/IntelPresentMon/Interprocess/source/MetricCapabilitiesShim.cpp new file mode 100644 index 000000000..373abe982 --- /dev/null +++ b/IntelPresentMon/Interprocess/source/MetricCapabilitiesShim.cpp @@ -0,0 +1,124 @@ +#include "MetricCapabilitiesShim.h" +#include "IntrospectionCapsLookup.h" +#include "../../CommonUtilities/Meta.h" +#include + +namespace pmon::ipc::intro +{ + namespace detail + { + using MetricEnum = PM_METRIC; + // Probe underlying values in [0, COUNT) + constexpr auto MaxMetricUnderlying = int(PM_METRIC_COUNT_); + + // xxxCapBits is std::bitset + template + bool HasCap(const BitsType& bits, Index index) + { + return bits.test(static_cast(index)); + } + + template + size_t CountGpuCaps_(const GpuTelemetryBitset& bits, const ArrayT& caps) + { + size_t count = 0; + for (auto flag : caps) { + if (HasCap(bits, flag)) { + ++count; + } + } + return count; + } + + // GPU per-metric accumulation (only instantiated for valid enum values) + template + void AccumulateGpuCapability(MetricCapabilities& caps, const GpuTelemetryBitset& bits) + { + using Lookup = IntrospectionCapsLookup; + + if constexpr (IsDerivedMetric) { + return; + } + + // Single GPU capability bit -> metric present if bit set + if constexpr (IsGpuDeviceMetric) { + if (HasCap(bits, Lookup::gpuCapBit)) { + caps.Set(Metric, 1); + } + } + + // Array GPU capability bits (fan speeds, etc.) + if constexpr (IsGpuDeviceMetricArray && !IsManualDisableMetric) { + std::size_t count = 0; + for (auto flag : Lookup::gpuCapBitArray) { + if (HasCap(bits, flag)) { + ++count; + } + } + if (count > 0) { + caps.Set(Metric, count); + } + } + + // Static GPU metrics: name/vendor/etc. + if constexpr (IsGpuDeviceStaticMetric) { + caps.Set(Metric, 1); + } + } + + void AccumulateDerivedGpuCapabilities_(MetricCapabilities& caps, const GpuTelemetryBitset& bits) + { + const auto fanCount = caps.Check(PM_METRIC_GPU_FAN_SPEED); + const auto maxFanCount = CountGpuCaps_(bits, + IntrospectionCapsLookup::gpuCapBitArray); + const auto derivedFanCount = std::min(fanCount, maxFanCount); + if (derivedFanCount > 0) { + caps.Set(PM_METRIC_GPU_FAN_SPEED_PERCENT, derivedFanCount); + } + + if (caps.Check(PM_METRIC_GPU_MEM_USED) > 0 && + caps.Check(PM_METRIC_GPU_MEM_SIZE) > 0) { + caps.Set(PM_METRIC_GPU_MEM_UTILIZATION, 1); + } + } + + // CPU per-metric accumulation (only instantiated for valid enum values) + template + void AccumulateCpuCapability(MetricCapabilities& caps, const CpuTelemetryBitset& bits) + { + using Lookup = IntrospectionCapsLookup; + + // CPU metrics gated by a capability bit + if constexpr (IsCpuMetric && !IsManualDisableMetric) { + if (HasCap(bits, Lookup::cpuCapBit)) { + caps.Set(Metric, 1); + } + } + + if constexpr (IsCpuStaticMetric) { + caps.Set(Metric, 1); + } + } + } // namespace detail + + MetricCapabilities ConvertBitset(const GpuTelemetryBitset& bits) + { + MetricCapabilities caps; + util::ForEachEnumValue( + [&]() { + detail::AccumulateGpuCapability(caps, bits); + }); + detail::AccumulateDerivedGpuCapabilities_(caps, bits); + return caps; + } + + MetricCapabilities ConvertBitset(const CpuTelemetryBitset& bits) + { + MetricCapabilities caps; + util::ForEachEnumValue( + [&]() { + detail::AccumulateCpuCapability(caps, bits); + }); + return caps; + } +} diff --git a/IntelPresentMon/Interprocess/source/MetricCapabilitiesShim.h b/IntelPresentMon/Interprocess/source/MetricCapabilitiesShim.h new file mode 100644 index 000000000..d0fba7268 --- /dev/null +++ b/IntelPresentMon/Interprocess/source/MetricCapabilitiesShim.h @@ -0,0 +1,10 @@ +#pragma once +#include "MetricCapabilities.h" +#include "../../ControlLib/PresentMonPowerTelemetry.h" +#include "../../ControlLib/CpuTelemetryInfo.h" + +namespace pmon::ipc::intro +{ + MetricCapabilities ConvertBitset(const GpuTelemetryBitset& bits); + MetricCapabilities ConvertBitset(const CpuTelemetryBitset& bits); +} \ No newline at end of file diff --git a/IntelPresentMon/Interprocess/source/OwnedDataSegment.h b/IntelPresentMon/Interprocess/source/OwnedDataSegment.h new file mode 100644 index 000000000..bf37f524c --- /dev/null +++ b/IntelPresentMon/Interprocess/source/OwnedDataSegment.h @@ -0,0 +1,70 @@ +#pragma once +#include "SharedMemoryTypes.h" +#include "DataStores.h" +#include "../../CommonUtilities/log/Log.h" +#include + +namespace pmon::ipc +{ + // manages shared memory segment and hosts data store T + template + class OwnedDataSegment + { + public: + // Uses DataStoreSizingInfo for sizing across all stores; permissions are optional. + OwnedDataSegment(const std::string& segmentName, + const DataStoreSizingInfo& sizing, + const bip::permissions& perms = {}) + : + shm_{ bip::create_only, segmentName.c_str(), + ResolveSegmentBytes_(segmentName, sizing), + nullptr, perms }, + pData_{ MakeStore_(sizing) } + { + if constexpr (std::is_same_v) { + pmlog_dbg("Shm segment populated (Frame)") + .pmwatch(segmentName) + .pmwatch(GetBytesTotal()) + .pmwatch(GetBytesUsed()) + .pmwatch(GetBytesFree()); + } + } + + T& GetStore() { return *pData_; } + const T& GetStore() const { return *pData_; } + size_t GetBytesUsed() const { return shm_.get_size() - shm_.get_free_memory(); } + size_t GetBytesFree() const { return shm_.get_free_memory(); } + size_t GetBytesTotal() const { return shm_.get_size(); } + + private: + static constexpr const char* name_ = "seg-dat"; + + static size_t ResolveSegmentBytes_(const std::string& segmentName, + const DataStoreSizingInfo& sizing) + { + const auto calculatedSize = sizing.overrideBytes ? *sizing.overrideBytes : + T::CalculateSegmentBytes(sizing); + pmlog_dbg("Creating shm segment") + .pmwatch(segmentName) + .pmwatch(calculatedSize) + .pmwatch(sizing.ringSamples) + .pmwatch(sizing.overrideBytes.has_value()) + .pmwatch(sizing.backpressured); + return calculatedSize; + } + + ShmUniquePtr MakeStore_(const DataStoreSizingInfo& sizing) + { + return ShmMakeNamedUnique( + name_, + shm_.get_segment_manager(), + *shm_.get_segment_manager(), + sizing + ); + } + + ShmSegment shm_; + ShmUniquePtr pData_; + }; +} + diff --git a/IntelPresentMon/Interprocess/source/SharedMemoryTypes.h b/IntelPresentMon/Interprocess/source/SharedMemoryTypes.h index 16aaeea81..bbb5f6a07 100644 --- a/IntelPresentMon/Interprocess/source/SharedMemoryTypes.h +++ b/IntelPresentMon/Interprocess/source/SharedMemoryTypes.h @@ -2,6 +2,7 @@ #include #include #include +#include #include namespace pmon::ipc @@ -17,6 +18,8 @@ namespace pmon::ipc using ShmVector = bip::vector>; template using ShmUniquePtr = typename bip::managed_unique_ptr::type; + template + using ShmMap = bip::map, ShmAllocator>>; namespace impl { template @@ -38,4 +41,8 @@ namespace pmon::ipc { return impl::ShmMakeUnique_(name.c_str(), pSegmentManager, std::forward

(params)...); } + inline std::string_view ToStringView(const ShmString& value) + { + return std::string_view{ value.c_str(), value.size() }; + } } \ No newline at end of file diff --git a/IntelPresentMon/Interprocess/source/ShmNamer.cpp b/IntelPresentMon/Interprocess/source/ShmNamer.cpp new file mode 100644 index 000000000..b3ce2ba5d --- /dev/null +++ b/IntelPresentMon/Interprocess/source/ShmNamer.cpp @@ -0,0 +1,37 @@ +#pragma once +#include "ShmNamer.h" +#include +#include + +namespace pmon::ipc +{ + ShmNamer::ShmNamer(std::string customPrefix, std::optional salt) + : + prefix_{ customPrefix }, + salt_{ salt.value_or(std::format("{:08x}", std::random_device{}())) } + {} + std::string ShmNamer::MakeIntrospectionName() const + { + return std::format("{}_{}_int", prefix_, salt_); + } + std::string ShmNamer::MakeSystemName() const + { + return std::format("{}_{}_sys", prefix_, salt_); + } + std::string ShmNamer::MakeGpuName(uint32_t deviceId) const + { + return std::format("{}_{}_gpu_{}", prefix_, salt_, deviceId); + } + std::string ShmNamer::MakeFrameName(uint32_t pid) const + { + return std::format("{}_{}_tgt_{}", prefix_, salt_, pid); + } + const std::string& ShmNamer::GetSalt() const + { + return salt_; + } + const std::string& ShmNamer::GetPrefix() const + { + return prefix_; + } +} \ No newline at end of file diff --git a/IntelPresentMon/Interprocess/source/ShmNamer.h b/IntelPresentMon/Interprocess/source/ShmNamer.h new file mode 100644 index 000000000..1c341632d --- /dev/null +++ b/IntelPresentMon/Interprocess/source/ShmNamer.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include + + +namespace pmon::ipc +{ + // encodes the conventions used to name shared memory during creation and opening + class ShmNamer + { + public: + ShmNamer(std::string customPrefix, std::optional salt = {}); + std::string MakeIntrospectionName() const; + std::string MakeSystemName() const; + std::string MakeGpuName(uint32_t deviceId) const; + std::string MakeFrameName(uint32_t pid) const; + const std::string& GetSalt() const; + const std::string& GetPrefix() const; + private: + std::string prefix_; + std::string salt_; + }; +} \ No newline at end of file diff --git a/IntelPresentMon/Interprocess/source/ShmRing.h b/IntelPresentMon/Interprocess/source/ShmRing.h new file mode 100644 index 000000000..f24d78c54 --- /dev/null +++ b/IntelPresentMon/Interprocess/source/ShmRing.h @@ -0,0 +1,117 @@ +#pragma once +#include "SharedMemoryTypes.h" +#include "../../CommonUtilities/Exception.h" +#include "../../CommonUtilities/log/Log.h" +#include +#include +#include + +namespace pmon::ipc +{ + using namespace std::literals; + + // shared memory ring buffer for broadcast + template + class ShmRing + { + public: + ShmRing(size_t capacity, ShmVector::allocator_type alloc, bool backpressured = false) + : + backpressured_{ backpressured }, + data_{ capacity, alloc } + { + if (capacity < ReadBufferSize * 2) { + throw std::logic_error{ "The capacity of a ShmRing must be at least double its ReadBufferSize" }; + } + } + + + ShmRing(const ShmRing&) = delete; + ShmRing& operator=(const ShmRing&) = delete; + // we need to enable move for use inside vectors + // not enabled by default because of the atomic member + ShmRing(ShmRing&& other) noexcept + : + backpressured_{ other.backpressured_ }, + data_{ std::move(other.data_) }, + nextWriteSerial_{ other.nextWriteSerial_.load() } + { + } + ShmRing& operator=(ShmRing&& other) noexcept + { + if (this != &other) { + data_ = std::move(other.data_); + nextWriteSerial_ = other.nextWriteSerial_.load(); + } + return *this; + } + ~ShmRing() = default; + + + bool Push(const T& val, std::optional timeoutMs = {}) + { + using clock = std::chrono::high_resolution_clock; + if (backpressured_) { + const auto start = timeoutMs ? clock::now() : decltype(clock::now()){}; + while (nextWriteSerial_ >= nextReadSerial_ + data_.size() - ReadBufferSize) { + // bail with false to signal timeout without writing the pushed value + if (timeoutMs && (clock::now() - start >= *timeoutMs * 1ms)) { + return false; + } + std::this_thread::sleep_for(10ms); + } + } + data_[IndexFromSerial_(nextWriteSerial_)] = val; + nextWriteSerial_++; + return true; + } + const T& At(size_t serial) const + { + // adds a one element buffer to help prevent reading partially updated data + // note this is much less buffer than GetSerialRange because that range + // returns the "safe" range considering processing time, while this check + // determines whether actual stale data is being accessed at this instant + if (serial + data_.size() <= nextWriteSerial_) { + pmlog_warn("Reading stale serial").pmwatch(serial); + } + else if (nextWriteSerial_ <= serial) { + pmlog_warn("Reading nonexistent serial").pmwatch(serial); + } + return data_[IndexFromSerial_(serial)]; + } + std::pair GetSerialRange() const + { + if (nextWriteSerial_ < data_.size()) { + return { 0, nextWriteSerial_ }; + } + else { + // lock current next serial to keep the returned range logically consistent + const size_t serial = nextWriteSerial_; + // once we have looped around the vector, we need to maintain a buffer + // to help avoid the client reading partially-updated data (data race) + return { serial - data_.size() + ReadBufferSize, serial }; + } + } + void MarkNextRead(size_t serial) const + { + if (serial > nextReadSerial_) { + nextReadSerial_ = serial; + } + } + bool Empty() const + { + return nextWriteSerial_ == 0; + } + private: + // functions + size_t IndexFromSerial_(size_t serial) const + { + return serial % data_.size(); + } + // data + const bool backpressured_; + std::atomic nextWriteSerial_ = 0; + mutable std::atomic nextReadSerial_ = 0; + ShmVector data_; + }; +} diff --git a/IntelPresentMon/Interprocess/source/SystemDeviceId.h b/IntelPresentMon/Interprocess/source/SystemDeviceId.h new file mode 100644 index 000000000..3aa851ab9 --- /dev/null +++ b/IntelPresentMon/Interprocess/source/SystemDeviceId.h @@ -0,0 +1,8 @@ +#pragma once +#include + +namespace pmon::ipc +{ + inline constexpr uint32_t kUniversalDeviceId = 0; + inline constexpr uint32_t kSystemDeviceId = 65536; +} diff --git a/IntelPresentMon/Interprocess/source/TelemetryMap.h b/IntelPresentMon/Interprocess/source/TelemetryMap.h new file mode 100644 index 000000000..668c9b38d --- /dev/null +++ b/IntelPresentMon/Interprocess/source/TelemetryMap.h @@ -0,0 +1,124 @@ +#pragma once +#include "SharedMemoryTypes.h" +#include "HistoryRing.h" +#include "../../PresentMonAPI2/PresentMonAPI.h" +#include + +namespace pmon::ipc +{ + // container for multiple history rings organized by PM_METRIC x index + class TelemetryMap + { + public: + template + using HistoryRingVect = ShmVector>; + using MapValueType = std::variant< + HistoryRingVect, HistoryRingVect, HistoryRingVect, + HistoryRingVect, HistoryRingVect>; + using MapType = ShmMap; + using AllocatorType = MapType::allocator_type; + TelemetryMap(AllocatorType alloc) + : + ringMap_{ alloc } + {} + void AddRing(PM_METRIC id, size_t size, size_t count, PM_DATA_TYPE type) + { + switch (type) { + case PM_DATA_TYPE_DOUBLE: + AddRing(id, size, count); + break; + case PM_DATA_TYPE_UINT32: + AddRing(id, size, count); + break; + case PM_DATA_TYPE_UINT64: + AddRing(id, size, count); + break; + case PM_DATA_TYPE_BOOL: + AddRing(id, size, count); + break; + case PM_DATA_TYPE_ENUM: + AddRing(id, size, count); + break; + default: throw util::Except<>("Unsupported ring type for TelemetryMap"); + } + } + template + void AddRing(PM_METRIC id, size_t size, size_t count) + { + // extra guard of misuse at compile time + static_assert( + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v, + "Unsupported ring type for TelemetryMap" + ); + + using RingAlloc = typename MapType::allocator_type::template rebind>::other; + + // Construct an allocator for SampleHistoryRing from the map's allocator + RingAlloc ringAlloc(ringMap_.get_allocator()); + + // Insert (or get existing) entry for this id, constructing the correct variant alternative + auto [it, inserted] = ringMap_.emplace( + std::piecewise_construct, + std::forward_as_tuple(id), + std::forward_as_tuple( + std::in_place_type>, + ringAlloc + ) + ); + // don't allow AddRing when one already exists for the given id + if (!inserted) { + throw std::logic_error("TelemetryMap::AddRing: id already exists"); + } + + // Get the vector for this T + auto& rings = std::get>(it->second); + // construct (count) rings in the vector + rings.reserve(count); + for (size_t i = 0; i < count; ++i) { + rings.emplace_back(size, ringAlloc); + } + } + template + HistoryRingVect& FindRing(PM_METRIC id) + { + return std::get>(FindRingVariant(id)); + } + template + const HistoryRingVect& FindRing(PM_METRIC id) const + { + return std::get>(FindRingVariant(id)); + } + MapValueType& FindRingVariant(PM_METRIC id) + { + return ringMap_.at(id); + } + const MapValueType& FindRingVariant(PM_METRIC id) const + { + return ringMap_.at(id); + } + size_t ArraySize(PM_METRIC id) const + { + auto it = ringMap_.find(id); + if (it == ringMap_.end()) { + return 0; + } + return std::visit([](auto const& rings) -> size_t { + return rings.size(); + }, it->second); + } + auto Rings() + { + return std::ranges::subrange{ ringMap_.begin(), ringMap_.end() }; + } + auto Rings() const + { + return std::ranges::subrange{ ringMap_.begin(), ringMap_.end() }; + } + private: + MapType ringMap_; + }; +} diff --git a/IntelPresentMon/Interprocess/source/ViewedDataSegment.h b/IntelPresentMon/Interprocess/source/ViewedDataSegment.h new file mode 100644 index 000000000..ca370e17b --- /dev/null +++ b/IntelPresentMon/Interprocess/source/ViewedDataSegment.h @@ -0,0 +1,33 @@ +#pragma once +#include "SharedMemoryTypes.h" + +namespace pmon::ipc +{ + namespace bip = boost::interprocess; + + // Non-owning view over a shared memory segment that hosts a data store T. + // Opens an existing managed segment and finds the named T instance inside. + template + class ViewedDataSegment + { + public: + explicit ViewedDataSegment(const std::string& segmentName) + : + shm_{ bip::open_only, segmentName.c_str() } + { + auto result = shm_.find(name_); + if (!result.first) { + throw std::runtime_error("Failed to find data store in shared memory"); + } + pData_ = result.first; // non-owning + } + + const T& GetStore() const { return *pData_; } + + private: + static constexpr const char* name_ = "seg-dat"; + + ShmSegment shm_; + T* pData_ = nullptr; // non-owning + }; +} diff --git a/IntelPresentMon/Interprocess/source/act/ActionHelper.h b/IntelPresentMon/Interprocess/source/act/ActionHelper.h index 711d8eb56..bd4a1594f 100644 --- a/IntelPresentMon/Interprocess/source/act/ActionHelper.h +++ b/IntelPresentMon/Interprocess/source/act/ActionHelper.h @@ -5,6 +5,7 @@ #include #include #include +#include #ifndef PM_ASYNC_ACTION_CUSTOM_REG_ #define PM_ASYNC_ACTION_CUSTOM_REG_(name) diff --git a/IntelPresentMon/Interprocess/source/act/AsyncAction.h b/IntelPresentMon/Interprocess/source/act/AsyncAction.h index 23b1ffafb..bd9376f56 100644 --- a/IntelPresentMon/Interprocess/source/act/AsyncAction.h +++ b/IntelPresentMon/Interprocess/source/act/AsyncAction.h @@ -38,7 +38,7 @@ namespace pmon::ipc::act resHeader = MakeResponseHeader(header, TransportStatus::Success, PM_STATUS_SUCCESS); } catch (const ActionExecutionError& e) { - pmlog_error(std::format("Error in action [{}] execution", GetIdentifier())).code(e.GetCode()); + pmlog_error(std::format("Error in action [{}] execution", GetIdentifier())).code(e.GetCode()).diag(); resHeader = MakeResponseHeader(header, TransportStatus::ExecutionFailure, e.GetCode()); } catch (...) { @@ -78,7 +78,7 @@ namespace pmon::ipc::act T::Execute_(ctx, stx, pipe.ConsumePacketPayload()); } catch (const ActionExecutionError& e) { - pmlog_error(std::format("Error in action [{}] execution: {}", GetIdentifier(), e.what())).code(e.GetCode()); + pmlog_error(std::format("Error in action [{}] execution: {}", GetIdentifier(), e.what())).code(e.GetCode()).diag(); } catch (...) { pmlog_error(util::ReportException()); @@ -115,4 +115,4 @@ namespace pmon::ipc::act template using AwaitableFromParams = pipe::as::awaitable>; -} \ No newline at end of file +} diff --git a/IntelPresentMon/Interprocess/source/act/SymmetricActionConnector.h b/IntelPresentMon/Interprocess/source/act/SymmetricActionConnector.h index 4cf72268e..18b9e04e1 100644 --- a/IntelPresentMon/Interprocess/source/act/SymmetricActionConnector.h +++ b/IntelPresentMon/Interprocess/source/act/SymmetricActionConnector.h @@ -29,7 +29,7 @@ namespace pmon::ipc::act if (header.identifier != "OpenSession") { assert(bool(stx.remotePid)); if (!stx.remotePid) { - pmlog_warn("Received action without a valid session opened"); + pmlog_warn("Received action without a valid session opened").diag(); } } // lookup the command by identifier and execute it with remaining buffer contents @@ -144,4 +144,4 @@ namespace pmon::ipc::act std::unique_ptr pOutPipe_; std::unique_ptr pInPipe_; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/Interprocess/source/act/Transfer.h b/IntelPresentMon/Interprocess/source/act/Transfer.h index 679f8dc9b..e1d62dca0 100644 --- a/IntelPresentMon/Interprocess/source/act/Transfer.h +++ b/IntelPresentMon/Interprocess/source/act/Transfer.h @@ -28,11 +28,11 @@ namespace pmon::ipc::act pipe.ConsumePacketPayload(); if (resHeader.executionStatus) { const auto code = (PM_STATUS)resHeader.executionStatus; - pmlog_error("Execution error response to SyncRequest").code(code); + pmlog_error("Execution error response to SyncRequest").code(code).diag(); throw util::Except((PM_STATUS)resHeader.executionStatus); } else { - pmlog_error("Execution error response to SyncRequest").raise(); + pmlog_error("Execution error response to SyncRequest").diag().raise(); } } co_return pipe.ConsumePacketPayload(); @@ -51,4 +51,4 @@ namespace pmon::ipc::act }; co_await pipe.WritePacket(reqHeader, params, timeoutMs); } -} \ No newline at end of file +} diff --git a/IntelPresentMon/Interprocess/source/metadata/EnumDeviceType.h b/IntelPresentMon/Interprocess/source/metadata/EnumDeviceType.h index 713fd8325..6043eb481 100644 --- a/IntelPresentMon/Interprocess/source/metadata/EnumDeviceType.h +++ b/IntelPresentMon/Interprocess/source/metadata/EnumDeviceType.h @@ -3,5 +3,6 @@ // enum annotation (enum_name_fragment, key_name_fragment, name, short_name, description) #define ENUM_KEY_LIST_DEVICE_TYPE(X_) \ - X_(DEVICE_TYPE, INDEPENDENT, "Device Independent", "", "This device type is used for special device ID 0 which is reserved for metrics independent of any specific hardware device (e.g. FPS metrics)") \ - X_(DEVICE_TYPE, GRAPHICS_ADAPTER, "Graphics Adapter", "", "Graphics adapter or GPU device") \ No newline at end of file + X_(DEVICE_TYPE, INDEPENDENT, "Device Independent", "", "This device type is used for metrics independent of any specific hardware device (e.g. FPS metrics). Set device ID to 0.") \ + X_(DEVICE_TYPE, GRAPHICS_ADAPTER, "Graphics Adapter", "", "Graphics adapter or GPU device") \ + X_(DEVICE_TYPE, SYSTEM, "System", "", "Special virtual device represening core system components (such as the CPU). Set device ID to 0.") \ No newline at end of file diff --git a/IntelPresentMon/Interprocess/source/metadata/EnumStat.h b/IntelPresentMon/Interprocess/source/metadata/EnumStat.h index baa0ecb59..359974516 100644 --- a/IntelPresentMon/Interprocess/source/metadata/EnumStat.h +++ b/IntelPresentMon/Interprocess/source/metadata/EnumStat.h @@ -13,9 +13,9 @@ X_(STAT, PERCENTILE_10, "10th Percentile", "10%", "Value below which 10% of the observations within the sliding window fall") \ X_(STAT, MAX, "Maximum", "max", "Maximum value of observations within the sliding window") \ X_(STAT, MIN, "Minimum", "min", "Minimum value of observations within the sliding window") \ - X_(STAT, MID_POINT, "Midpoint", "raw", "Point sample of the observation nearest to the middle of the sliding window") \ + X_(STAT, MID_POINT, "Midpoint", "mpt", "Point sample of the observation nearest to the middle of the sliding window") \ X_(STAT, MID_LERP, "Mid Lerp", "mlp", "Linear interpolation between the two observations nearest to the middle of the sliding window") \ - X_(STAT, NEWEST_POINT, "Newest Point", "npt", "Value in the most recent observation in the sliding window") \ - X_(STAT, OLDEST_POINT, "Oldest Point", "opt", "Value in the least recent observation in the sliding window") \ + X_(STAT, NEWEST_POINT, "Newest Point", "raw", "Point sample of the most recent observation in the sliding window") \ + X_(STAT, OLDEST_POINT, "Oldest Point", "opt", "Point sample of the least recent observation in the sliding window") \ X_(STAT, COUNT, "Count", "cnt", "Count of observations in the sliding window matching a predicate (e.g. counting # of observations for which a field is boolean true)") \ X_(STAT, NON_ZERO_AVG, "Non-zero Average", (const char*)u8"øavg", "Average or mean of frame samples over the sliding window, excluding all zero values") \ No newline at end of file diff --git a/IntelPresentMon/Interprocess/source/metadata/EnumStatus.h b/IntelPresentMon/Interprocess/source/metadata/EnumStatus.h index bd3f23f10..6aa69d889 100644 --- a/IntelPresentMon/Interprocess/source/metadata/EnumStatus.h +++ b/IntelPresentMon/Interprocess/source/metadata/EnumStatus.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "../../../PresentMonAPI2/PresentMonAPI.h" // enum annotation (enum_name_fragment, key_name_fragment, name, short_name, description) @@ -23,4 +23,6 @@ X_(STATUS, MIDDLEWARE_MISSING_ENDPOINT, "Middleware Missing Endpoint", "", "A required endpoint function was not found in the Middleware DLL") \ X_(STATUS, MIDDLEWARE_VERSION_LOW, "Middleware Version Low", "", "Middleware DLL version was found to be too low for compatibility") \ X_(STATUS, MIDDLEWARE_VERSION_HIGH, "Middleware Version High", "", "Middleware DLL version was found to be too high for compatibility") \ - X_(STATUS, MIDDLEWARE_SERVICE_MISMATCH, "Middleware Service Mismatch", "", "Middleware DLL build ID does not match that of the service") \ No newline at end of file + X_(STATUS, MIDDLEWARE_SERVICE_MISMATCH, "Middleware Service Mismatch", "", "Middleware DLL build ID does not match that of the service") \ + X_(STATUS, QUERY_MALFORMED, "Query Malformed", "", "There are errors or inconsistencies in the elements of the query being registered") \ + X_(STATUS, MODE_MISMATCH, "Mode Mismatch", "", "Operation is not valid for the current service mode") diff --git a/IntelPresentMon/Interprocess/source/metadata/MetricList.h b/IntelPresentMon/Interprocess/source/metadata/MetricList.h index e945a12d9..c9daeec1d 100644 --- a/IntelPresentMon/Interprocess/source/metadata/MetricList.h +++ b/IntelPresentMon/Interprocess/source/metadata/MetricList.h @@ -1,4 +1,4 @@ -// Copyright (C) 2017-2024 Intel Corporation +// Copyright (C) 2017-2024 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include "../../../PresentMonAPI2/PresentMonAPI.h" @@ -6,34 +6,35 @@ #define METRIC_LIST(X_) \ X_(PM_METRIC_APPLICATION, PM_METRIC_TYPE_STATIC, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_STRING, PM_DATA_TYPE_STRING, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ - X_(PM_METRIC_SWAP_CHAIN_ADDRESS, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_UINT64, PM_DATA_TYPE_UINT64, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_MID_POINT) \ + X_(PM_METRIC_PROCESS_ID, PM_METRIC_TYPE_STATIC, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_UINT32, PM_DATA_TYPE_UINT32, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ + X_(PM_METRIC_SWAP_CHAIN_ADDRESS, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_UINT64, PM_DATA_TYPE_UINT64, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NEWEST_POINT) \ X_(PM_METRIC_GPU_VENDOR, PM_METRIC_TYPE_STATIC, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_ENUM, PM_DATA_TYPE_ENUM, PM_ENUM_DEVICE_VENDOR, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, PM_STAT_NONE) \ X_(PM_METRIC_GPU_NAME, PM_METRIC_TYPE_STATIC, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_STRING, PM_DATA_TYPE_STRING, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, PM_STAT_NONE) \ - X_(PM_METRIC_CPU_VENDOR, PM_METRIC_TYPE_STATIC, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_ENUM, PM_DATA_TYPE_ENUM, PM_ENUM_DEVICE_VENDOR, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ - X_(PM_METRIC_CPU_NAME, PM_METRIC_TYPE_STATIC, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_STRING, PM_DATA_TYPE_STRING, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ + X_(PM_METRIC_CPU_VENDOR, PM_METRIC_TYPE_STATIC, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_ENUM, PM_DATA_TYPE_ENUM, PM_ENUM_DEVICE_VENDOR, PM_DEVICE_TYPE_SYSTEM, PM_STAT_NONE) \ + X_(PM_METRIC_CPU_NAME, PM_METRIC_TYPE_STATIC, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_STRING, PM_DATA_TYPE_STRING, 0, PM_DEVICE_TYPE_SYSTEM, PM_STAT_NONE) \ X_(PM_METRIC_CPU_START_TIME, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_VOID, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ - X_(PM_METRIC_CPU_START_QPC, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_QPC, PM_DATA_TYPE_UINT64, PM_DATA_TYPE_UINT64, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ + X_(PM_METRIC_CPU_START_QPC, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_QPC, PM_DATA_TYPE_VOID, PM_DATA_TYPE_UINT64, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ X_(PM_METRIC_CPU_FRAME_TIME, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_CPU_BUSY, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_CPU_WAIT, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ - X_(PM_METRIC_DISPLAYED_FPS, PM_METRIC_TYPE_DYNAMIC, PM_UNIT_FPS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_VOID, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ - X_(PM_METRIC_APPLICATION_FPS, PM_METRIC_TYPE_DYNAMIC, PM_UNIT_FPS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_VOID, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ - X_(PM_METRIC_PRESENTED_FPS, PM_METRIC_TYPE_DYNAMIC, PM_UNIT_FPS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_VOID, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ + X_(PM_METRIC_DISPLAYED_FPS, PM_METRIC_TYPE_DYNAMIC, PM_UNIT_FPS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ + X_(PM_METRIC_APPLICATION_FPS, PM_METRIC_TYPE_DYNAMIC, PM_UNIT_FPS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ + X_(PM_METRIC_PRESENTED_FPS, PM_METRIC_TYPE_DYNAMIC, PM_UNIT_FPS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_GPU_TIME, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_GPU_BUSY, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_GPU_WAIT, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_DROPPED_FRAMES, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_BOOL, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ - X_(PM_METRIC_DISPLAYED_TIME, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ + X_(PM_METRIC_DISPLAYED_TIME, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_VOID, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_ANIMATION_ERROR, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ - X_(PM_METRIC_ANIMATION_TIME, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ - X_(PM_METRIC_SYNC_INTERVAL, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_VERTICAL_BLANKS, PM_DATA_TYPE_INT32, PM_DATA_TYPE_INT32, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_MID_POINT) \ - X_(PM_METRIC_PRESENT_FLAGS, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_UINT32, PM_DATA_TYPE_UINT32, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_MID_POINT) \ - X_(PM_METRIC_PRESENT_MODE, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_ENUM, PM_DATA_TYPE_ENUM, PM_ENUM_PRESENT_MODE, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_MID_POINT) \ - X_(PM_METRIC_PRESENT_RUNTIME, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_ENUM, PM_DATA_TYPE_ENUM, PM_ENUM_GRAPHICS_RUNTIME, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_MID_POINT) \ + X_(PM_METRIC_ANIMATION_TIME, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_VOID, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ + X_(PM_METRIC_SYNC_INTERVAL, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_VERTICAL_BLANKS, PM_DATA_TYPE_INT32, PM_DATA_TYPE_INT32, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NEWEST_POINT) \ + X_(PM_METRIC_PRESENT_FLAGS, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_UINT32, PM_DATA_TYPE_UINT32, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NEWEST_POINT) \ + X_(PM_METRIC_PRESENT_MODE, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_ENUM, PM_DATA_TYPE_ENUM, PM_ENUM_PRESENT_MODE, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NEWEST_POINT) \ + X_(PM_METRIC_PRESENT_RUNTIME, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_ENUM, PM_DATA_TYPE_ENUM, PM_ENUM_GRAPHICS_RUNTIME, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NEWEST_POINT) \ X_(PM_METRIC_ALLOWS_TEARING, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_BOOL, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ - X_(PM_METRIC_FRAME_TYPE, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_ENUM, PM_DATA_TYPE_ENUM, PM_ENUM_FRAME_TYPE, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ + X_(PM_METRIC_FRAME_TYPE, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_ENUM, PM_DATA_TYPE_ENUM, PM_ENUM_FRAME_TYPE, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NEWEST_POINT) \ X_(PM_METRIC_GPU_LATENCY, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ - X_(PM_METRIC_DISPLAY_LATENCY, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ + X_(PM_METRIC_DISPLAY_LATENCY, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_VOID, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_CLICK_TO_PHOTON_LATENCY, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NON_ZERO_AVG, PM_STAT_PERCENTILE_01, PM_STAT_PERCENTILE_05, PM_STAT_PERCENTILE_10, PM_STAT_MAX) \ X_(PM_METRIC_GPU_SUSTAINED_POWER_LIMIT, PM_METRIC_TYPE_STATIC, PM_UNIT_WATTS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, PM_STAT_NONE) \ X_(PM_METRIC_GPU_POWER, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_WATTS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ @@ -56,7 +57,7 @@ X_(PM_METRIC_GPU_MEM_TEMPERATURE, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_CELSIUS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ X_(PM_METRIC_GPU_MEM_SIZE, PM_METRIC_TYPE_STATIC, PM_UNIT_BYTES, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_UINT64, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, PM_STAT_NONE) \ X_(PM_METRIC_GPU_MEM_USED, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_BYTES, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_UINT64, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ - X_(PM_METRIC_GPU_MEM_UTILIZATION, PM_METRIC_TYPE_DYNAMIC, PM_UNIT_PERCENT, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_VOID, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ + X_(PM_METRIC_GPU_MEM_UTILIZATION, PM_METRIC_TYPE_DYNAMIC, PM_UNIT_PERCENT, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ X_(PM_METRIC_GPU_MEM_MAX_BANDWIDTH, PM_METRIC_TYPE_STATIC, PM_UNIT_BITS_PER_SECOND, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_UINT64, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, PM_STAT_NONE) \ X_(PM_METRIC_GPU_MEM_WRITE_BANDWIDTH, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_BITS_PER_SECOND, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ X_(PM_METRIC_GPU_MEM_READ_BANDWIDTH, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_BITS_PER_SECOND, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ @@ -65,12 +66,12 @@ X_(PM_METRIC_GPU_MEM_CURRENT_LIMITED, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_BOOLEAN, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_BOOL, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ X_(PM_METRIC_GPU_MEM_VOLTAGE_LIMITED, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_BOOLEAN, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_BOOL, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ X_(PM_METRIC_GPU_MEM_UTILIZATION_LIMITED, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_BOOLEAN, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_BOOL, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ - X_(PM_METRIC_CPU_UTILIZATION, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_PERCENT, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ - X_(PM_METRIC_CPU_POWER_LIMIT, PM_METRIC_TYPE_STATIC, PM_UNIT_WATTS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, PM_STAT_MID_POINT) \ - X_(PM_METRIC_CPU_POWER, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_WATTS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ - X_(PM_METRIC_CPU_TEMPERATURE, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_CELSIUS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ - X_(PM_METRIC_CPU_FREQUENCY, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MEGAHERTZ, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ - X_(PM_METRIC_CPU_CORE_UTILITY, PM_METRIC_TYPE_DYNAMIC, PM_UNIT_PERCENT, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_VOID, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ + X_(PM_METRIC_CPU_UTILIZATION, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_PERCENT, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_SYSTEM, FULL_STATS) \ + X_(PM_METRIC_CPU_POWER_LIMIT, PM_METRIC_TYPE_STATIC, PM_UNIT_WATTS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_SYSTEM, PM_STAT_NONE) \ + X_(PM_METRIC_CPU_POWER, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_WATTS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_SYSTEM, FULL_STATS) \ + X_(PM_METRIC_CPU_TEMPERATURE, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_CELSIUS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_SYSTEM, FULL_STATS) \ + X_(PM_METRIC_CPU_FREQUENCY, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MEGAHERTZ, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_SYSTEM, FULL_STATS) \ + X_(PM_METRIC_CPU_CORE_UTILITY, PM_METRIC_TYPE_DYNAMIC, PM_UNIT_PERCENT, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_SYSTEM, FULL_STATS) \ X_(PM_METRIC_ALL_INPUT_TO_PHOTON_LATENCY, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NON_ZERO_AVG, PM_STAT_PERCENTILE_01, PM_STAT_PERCENTILE_05, PM_STAT_PERCENTILE_10, PM_STAT_MAX) \ X_(PM_METRIC_INSTRUMENTED_LATENCY, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_GPU_EFFECTIVE_FREQUENCY, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MEGAHERTZ, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ @@ -82,15 +83,17 @@ X_(PM_METRIC_GPU_FAN_SPEED_PERCENT, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_PERCENT, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ X_(PM_METRIC_GPU_CARD_POWER, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_WATTS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, FULL_STATS) \ X_(PM_METRIC_PRESENT_START_TIME, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_VOID, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ - X_(PM_METRIC_PRESENT_START_QPC, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_QPC, PM_DATA_TYPE_UINT64, PM_DATA_TYPE_UINT64, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ - X_(PM_METRIC_BETWEEN_PRESENTS, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ - X_(PM_METRIC_IN_PRESENT_API, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ + X_(PM_METRIC_PRESENT_START_QPC, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_QPC, PM_DATA_TYPE_VOID, PM_DATA_TYPE_UINT64, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ + X_(PM_METRIC_BETWEEN_PRESENTS, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_VOID, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ + X_(PM_METRIC_IN_PRESENT_API, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_VOID, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_BETWEEN_DISPLAY_CHANGE, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_UNTIL_DISPLAYED, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ - X_(PM_METRIC_RENDER_PRESENT_LATENCY, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ - X_(PM_METRIC_BETWEEN_SIMULATION_START, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ + X_(PM_METRIC_RENDER_PRESENT_LATENCY, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_VOID, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ + X_(PM_METRIC_BETWEEN_SIMULATION_START, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_VOID, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_PC_LATENCY, PM_METRIC_TYPE_DYNAMIC_FRAME, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_DISPLAYED_FRAME_TIME, PM_METRIC_TYPE_DYNAMIC, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ X_(PM_METRIC_PRESENTED_FRAME_TIME, PM_METRIC_TYPE_DYNAMIC, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ - X_(PM_METRIC_BETWEEN_APP_START, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ - X_(PM_METRIC_FLIP_DELAY, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ + X_(PM_METRIC_BETWEEN_APP_START, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_VOID, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ + X_(PM_METRIC_FLIP_DELAY, PM_METRIC_TYPE_FRAME_EVENT, PM_UNIT_MILLISECONDS, PM_DATA_TYPE_VOID, PM_DATA_TYPE_DOUBLE, 0, PM_DEVICE_TYPE_INDEPENDENT, FULL_STATS) \ + X_(PM_METRIC_SESSION_START_QPC, PM_METRIC_TYPE_STATIC, PM_UNIT_QPC, PM_DATA_TYPE_UINT64, PM_DATA_TYPE_UINT64, 0, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ + X_(PM_METRIC_COUNT_, PM_METRIC_TYPE_STATIC, PM_UNIT_DIMENSIONLESS, PM_DATA_TYPE_VOID, PM_DATA_TYPE_VOID, PM_ENUM_NULL_ENUM, PM_DEVICE_TYPE_INDEPENDENT, PM_STAT_NONE) \ diff --git a/IntelPresentMon/Interprocess/source/metadata/StatsShortcuts.h b/IntelPresentMon/Interprocess/source/metadata/StatsShortcuts.h index 0099b8302..6d0d4ff1a 100644 --- a/IntelPresentMon/Interprocess/source/metadata/StatsShortcuts.h +++ b/IntelPresentMon/Interprocess/source/metadata/StatsShortcuts.h @@ -3,4 +3,4 @@ #define FULL_STATS PM_STAT_AVG, PM_STAT_PERCENTILE_99, PM_STAT_PERCENTILE_95, \ PM_STAT_PERCENTILE_90, PM_STAT_PERCENTILE_01, PM_STAT_PERCENTILE_05, \ - PM_STAT_PERCENTILE_10, PM_STAT_MAX, PM_STAT_MIN, PM_STAT_MID_POINT \ No newline at end of file + PM_STAT_PERCENTILE_10, PM_STAT_MAX, PM_STAT_MIN, PM_STAT_NEWEST_POINT \ No newline at end of file diff --git a/IntelPresentMon/InterprocessMock/InterprocessMock.vcxproj b/IntelPresentMon/InterprocessMock/InterprocessMock.vcxproj deleted file mode 100644 index b337349ce..000000000 --- a/IntelPresentMon/InterprocessMock/InterprocessMock.vcxproj +++ /dev/null @@ -1,98 +0,0 @@ - - - - - Debug - Win32 - - - Debug - x64 - - - - 17.0 - Win32Proj - {f612934b-9333-4628-9076-7dfe0b5c3e0c} - InterprocessMock - 10.0 - - - - Application - true - v143 - Unicode - - - Application - true - v143 - Unicode - - - - - - - - - - - - - - - - - - - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - - - - - Level3 - true - WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - stdcpplatest - - - Console - true - - - - - Level3 - true - _DEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - MultiThreadedDebug - stdcpplatest - - - Console - true - - - - - - - - {808f5ea9-ea09-4d72-87b4-5397d43cba54} - - - {ca23d648-daef-4f06-81d5-fe619bd31f0b} - - - - - - - - - \ No newline at end of file diff --git a/IntelPresentMon/InterprocessMock/InterprocessMock.vcxproj.filters b/IntelPresentMon/InterprocessMock/InterprocessMock.vcxproj.filters deleted file mode 100644 index e6a846e43..000000000 --- a/IntelPresentMon/InterprocessMock/InterprocessMock.vcxproj.filters +++ /dev/null @@ -1,27 +0,0 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - - - - Source Files - - - - - Header Files - - - \ No newline at end of file diff --git a/IntelPresentMon/InterprocessMock/Main.cpp b/IntelPresentMon/InterprocessMock/Main.cpp deleted file mode 100644 index d10948db7..000000000 --- a/IntelPresentMon/InterprocessMock/Main.cpp +++ /dev/null @@ -1,310 +0,0 @@ -#include -#include "../Interprocess/source/ExperimentalInterprocess.h" -#include "../Interprocess/source/Interprocess.h" -#include "Options.h" -#include -#include "../PresentMonMiddleware/MockCommon.h" - -int main(int argc, char** argv) -{ - using namespace pmon::ipc; - using namespace pmon::ipc::mock; - - // parse command line options - if (auto ecode = opt::Options::Init(argc, argv)) { - return *ecode; - } - - // shortcut for command line options - try - { - const auto& opts = opt::Options::Get(); - - if (opts.testF) { - std::cout << experimental::f() << std::endl; - } - else if (opts.basicMessage) { - std::string buffer; - std::cin >> buffer; - - auto pServer = experimental::IServer::Make(buffer); - pServer->MakeUptrToMessage(buffer); - - // send goahead signal to client - std::cout << "go" << std::endl; - - // wait until client has finished receiving - std::cin >> buffer; - } - else if (opts.sharedRootBasic) { - std::string buffer; - std::cin >> buffer; - - auto pServer = experimental::IServer::Make(buffer); - pServer->MakeUptrToMessage(buffer); - - // send result of allocator aware test to client - std::cout << pServer->RoundtripRootInShared() << std::endl; - // wait until client has finished receiving - std::cin >> buffer; - } - else if (opts.destroyUptr) { - std::string buffer; - - // waiting for client that contains input code - std::cin >> buffer; - - auto pServer = experimental::IServer::Make(buffer); - pServer->MakeUptrToMessage(buffer); - - // send goahead signal to client, checks the free memory - std::cout << "go" << std::endl; - - // wait until client has finishing checking free memory - std::cin >> buffer; - - pServer->FreeUptrToMessage(); - - // send goahead signal to client to check free memory again - std::cout << "go" << std::endl; - - // wait until client has finishing checking free memory - std::cin >> buffer; - } - else if (opts.makeDestroyUptr) { - std::string buffer; - - auto pServer = experimental::IServer::Make("dummy"); - - // signal to client that shm has been created - std::cout << "go" << std::endl; - - // wait for client signal that free memory has been checked, contains code string - std::cin >> buffer; - - // create the uptr and string in memory - pServer->MakeUptrToMessage(buffer); - - // send goahead signal to client, checks the free memory - std::cout << "go" << std::endl; - - // wait until client has finishing checking free memory - std::cin >> buffer; - - pServer->FreeUptrToMessage(); - - // send goahead signal to client to check free memory again - std::cout << "go" << std::endl; - - // wait until client has finishing checking free memory - std::cin >> buffer; - } - else if (opts.sharedRootRetained) { - std::string buffer; - - auto pServer = experimental::IServer::Make("dummy"); - - // signal to client that shm has been created - std::cout << "go" << std::endl; - - // wait for client signal that free memory has been checked, contains code string - std::cin >> buffer; - - // create the uptr and string in memory - pServer->MakeRoot(std::stoi(buffer)); - - // send goahead signal to client, checks the free memory - std::cout << "go" << std::endl; - - // wait until client has finishing checking free memory - std::cin >> buffer; - - pServer->FreeRoot(); - - // send goahead signal to client to check free memory again - std::cout << "go" << std::endl; - - // wait until client has finishing checking free memory - std::cin >> buffer; - } - else if (opts.clientFree) { - std::string buffer; - - auto pServer = experimental::IServer::Make("dummy"); - - // signal to client that shm has been created - std::cout << "go" << std::endl; - - // wait for client signal that free memory has been checked, contains code string - std::cin >> buffer; - - // create object in shm - pServer->CreateForClientFree(777, buffer); - - // send goahead signal to client, checks memory and values and frees - std::cout << "go" << std::endl; - - // wait until client has finished work - std::cin >> buffer; - } - else if (opts.deep) { - std::string buffer; - - auto pServer = experimental::IServer::Make("dummy"); - - // signal to client that shm has been created - std::cout << "go" << std::endl; - - // wait for client signal that free memory has been checked, client sends 2 ints - int n1, n2; - std::cin >> n1; - std::cin >> n2; - - // create object in shm - pServer->MakeDeep(n1, n2); - - // send goahead signal to client, checks memory and values - std::cout << "go" << std::endl; - - // wait for client ack - std::cin >> buffer; - - // free data - pServer->FreeDeep(); - - // send goahead signal to client, checks memory - std::cout << "go" << std::endl; - - // wait until client has finished work - std::cin >> buffer; - } - else if (opts.cloneHeap) { - std::string buffer; - - auto pServer = experimental::IServer::Make("dummy"); - - // signal to client that shm has been created - std::cout << "go" << std::endl; - - // wait for client signal that free memory has been checked, contains code string - std::cin >> buffer; - - // create the uptr and string in memory - pServer->MakeRootCloneHeap(std::stoi(buffer)); - - // send goahead signal to client, checks the free memory - std::cout << "go" << std::endl; - - // wait until client has finishing checking free memory - std::cin >> buffer; - - pServer->FreeRoot(); - - // send goahead signal to client to check free memory again - std::cout << "go" << std::endl; - - // wait until client has finishing checking free memory - std::cin >> buffer; - } - else if (opts.cloneHeapDeep) { - std::string buffer; - - auto pServer = experimental::IServer::Make("dummy"); - - // signal to client that shm has been created - std::cout << "go" << std::endl; - - // wait for client signal that free memory has been checked, 2 numbers - int n1, n2; - std::cin >> n1; - std::cin >> n2; - - // create the uptr and string in memory - pServer->MakeDeepCloneHeap(n1, n2); - - // send goahead signal to client, checks the free memory, makes a clone - std::cout << "go" << std::endl; - - // wait until client has finishing checking free memory / cloning - std::cin >> buffer; - - pServer->FreeDeep(); - - // send goahead signal to client to check free memory again - std::cout << "go" << std::endl; - - // wait until client has finishing checking free memory - std::cin >> buffer; - } - else if (opts.cloneHeapDeep2) { - std::string buffer; - - auto pServer = experimental::IServer::Make("dummy"); - - // signal to client that shm has been created - std::cout << "go" << std::endl; - - // wait for client signal that free memory has been checked, 2 numbers - int n1, n2; - std::cin >> n1; - std::cin >> n2; - - // create the uptr and string in memory - pServer->MakeDeepCloneHeap2(n1, n2); - - // send goahead signal to client, checks the free memory, makes a clone - std::cout << "go" << std::endl; - - // wait until client has finishing checking free memory / cloning - std::cin >> buffer; - - pServer->FreeDeep2(); - - // send goahead signal to client to check free memory again - std::cout << "go" << std::endl; - - // wait until client has finishing checking free memory - std::cin >> buffer; - } - else if (opts.basicIntro) { - std::string buffer; - - auto pServiceComms = pmon::ipc::MakeServiceComms(*opts.introNsm); - pmon::ipc::intro::RegisterMockIntrospectionDevices(*pServiceComms); - - // signal to client that shm has been created - std::cout << "ready" << std::endl; - - // wait for client signal that work is done - std::cin >> buffer; - } - else { - std::cout << "default-output" << std::endl; - } - } - catch (const CLI::CallForHelp& e) - { - return e.get_exit_code(); - } - catch (const boost::interprocess::interprocess_exception& e) - { - std::cout << "Interprocess Exception Occurred: " << e.what(); - return e.get_error_code(); - } - catch (const std::ios_base::failure& e) - { - std::cout << "Input/Output library Exception: " << e.what(); - return -1; - } - catch (const std::bad_cast& e) - { - std::cout << "Invalid Dynamic Cast Exception: " << e.what(); - return -1; - } - catch (const CLI::BadNameString& e) - { - return e.get_exit_code(); - } - - return 0; -} \ No newline at end of file diff --git a/IntelPresentMon/InterprocessMock/Options.h b/IntelPresentMon/InterprocessMock/Options.h deleted file mode 100644 index 1ed5da0be..000000000 --- a/IntelPresentMon/InterprocessMock/Options.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -#include "../CommonUtilities/cli/CliFramework.h" -#include -#include - -namespace pmon::ipc::mock::opt -{ - using namespace pmon::util; - using namespace pmon::util::cli; - struct Options : public OptionsBase - { - // add options and switches here to augment the CLI - Flag testF{ this, "--test-f", "Output test string from test function f() in ipc lib" }; - Flag basicMessage{ this, "--basic-message", "Do minimal IPC test transferring string across shared memory" }; - Flag destroyUptr{ this, "--destroy-uptr", "Test which destroys the uptr that is constructed by default" }; - Flag makeDestroyUptr{ this, "--make-destroy-uptr", "Test which makes and destroys the uptr such that leaks can be detected" }; - Flag sharedRootBasic{ this, "--shared-root-basic", "Basic test of creating root in shared memory" }; - Flag sharedRootRetained{ this, "--shared-root-retained", "Test retaining root in shared memory until ack" }; - Flag clientFree{ this, "--client-free", "Allocated in shm in server and free from client" }; - Flag deep{ this, "--deep", "Testing sequence for deeply nested structure with multiple container layers" }; - Flag cloneHeap{ this, "--clone-heap-to-shm", "Create simple root on heap, clone to shm" }; - Flag cloneHeapDeep{ this, "--clone-heap-deep-to-shm", "Create deep root on heap, clone to shm" }; - Flag cloneHeapDeep2{ this, "--clone-heap-deep-to-shm-2", "Create deep root on heap, clone to shm (vector of instead of )" }; - Flag basicIntro{ this, "--basic-intro", "Create introspection root so middleware code can access it" }; - Option introNsm{ this, "--intro-nsm", "", "Name to use when creating NSM for introspection data" }; - - static constexpr const char* description = "Application to mock interprocess communication endpoints for testing"; - static constexpr const char* name = "InterprocessMock.exe"; - private: - MutualExclusion ex_{ testF, basicMessage, destroyUptr, makeDestroyUptr, sharedRootBasic, - sharedRootRetained, clientFree, deep, cloneHeap, cloneHeapDeep, cloneHeapDeep2, basicIntro }; - }; -} \ No newline at end of file diff --git a/IntelPresentMon/KernelProcess/KernelProcess.args.json b/IntelPresentMon/KernelProcess/KernelProcess.args.json index 29e5f2b78..7a2a92d12 100644 --- a/IntelPresentMon/KernelProcess/KernelProcess.args.json +++ b/IntelPresentMon/KernelProcess/KernelProcess.args.json @@ -34,7 +34,7 @@ }, { "Id": "49b6fc1f-b969-4280-b0d8-17a213e80f16", - "Command": "--log-verbose-modules core_window v8async etwq" + "Command": "--log-verbose-modules ipc_ring" }, { "Id": "bab48d6d-3a48-4b3b-9ed9-903e381824c6", @@ -116,6 +116,10 @@ "Id": "5217575f-37d8-4f60-a2d2-a85992ce4341", "Command": "--ui-flag enable-chromium-debug" }, + { + "Id": "f4a7ccc2-978f-4bd0-af05-a70ce115b1eb", + "Command": "list --devices" + }, { "Id": "a82f2a21-c9de-460e-8f38-b10026e23809", "Command": "capture", diff --git a/IntelPresentMon/KernelProcess/KernelProcess.vcxproj b/IntelPresentMon/KernelProcess/KernelProcess.vcxproj index 0c2b0b35d..fc29dea1f 100644 --- a/IntelPresentMon/KernelProcess/KernelProcess.vcxproj +++ b/IntelPresentMon/KernelProcess/KernelProcess.vcxproj @@ -1,4 +1,4 @@ - + @@ -172,7 +172,6 @@ - @@ -195,4 +194,4 @@ - \ No newline at end of file + diff --git a/IntelPresentMon/KernelProcess/KernelProcess.vcxproj.filters b/IntelPresentMon/KernelProcess/KernelProcess.vcxproj.filters index 36c866017..311ebca46 100644 --- a/IntelPresentMon/KernelProcess/KernelProcess.vcxproj.filters +++ b/IntelPresentMon/KernelProcess/KernelProcess.vcxproj.filters @@ -44,9 +44,6 @@ Header Files - - Header Files - Header Files @@ -79,4 +76,4 @@ - \ No newline at end of file + diff --git a/IntelPresentMon/KernelProcess/MakeOverlaySpec.cpp b/IntelPresentMon/KernelProcess/MakeOverlaySpec.cpp index 6dc541d06..00eee1c8c 100644 --- a/IntelPresentMon/KernelProcess/MakeOverlaySpec.cpp +++ b/IntelPresentMon/KernelProcess/MakeOverlaySpec.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #include "MakeOverlaySpec.h" #include "../Core/source/gfx/layout/style/RawAttributeHelpers.h" @@ -45,6 +45,9 @@ namespace kproc .generateStats = pref.generateStats, .enableFlashInjection = pref.enableFlashInjection, }); + if (pref.adapterId && *pref.adapterId > 0) { + pSpec->frameQueryAdapterId = uint32_t(*pref.adapterId); + } // style sheets { diff --git a/IntelPresentMon/KernelProcess/kact/AllActions.h b/IntelPresentMon/KernelProcess/kact/AllActions.h index d07b3cfe6..f51d9b798 100644 --- a/IntelPresentMon/KernelProcess/kact/AllActions.h +++ b/IntelPresentMon/KernelProcess/kact/AllActions.h @@ -1,10 +1,9 @@ -#pragma once +#pragma once #include "OpenSession.h" -#include "SetAdapter.h" #include "SetCapture.h" #include "EnumerateAdapters.h" #include "Introspect.h" #include "PushSpecification.h" #include "BindHotkey.h" #include "ClearHotkey.h" -#include "SetEtlLogging.h" \ No newline at end of file +#include "SetEtlLogging.h" diff --git a/IntelPresentMon/KernelProcess/kact/Introspect.h b/IntelPresentMon/KernelProcess/kact/Introspect.h index 0b8cb07d6..86794279b 100644 --- a/IntelPresentMon/KernelProcess/kact/Introspect.h +++ b/IntelPresentMon/KernelProcess/kact/Introspect.h @@ -4,98 +4,144 @@ #include #include "../../Core/source/kernel/Kernel.h" #include "../../PresentMonAPIWrapper/PresentMonAPIWrapper.h" +#include "../../Interprocess/source/SystemDeviceId.h" #include #include +// cereal JSON dump + NVP macro +#include +#include + #define ACT_NAME Introspect #define ACT_EXEC_CTX KernelExecutionContext #define ACT_TYPE AsyncActionBase_ #define ACT_NS kproc::kact - namespace ACT_NS { - using namespace ::pmon::ipc::act; - - class ACT_NAME : public ACT_TYPE - { - public: - static constexpr const char* Identifier = STRINGIFY(ACT_NAME); - struct Params - { - template void serialize(A& ar) { - } - }; - struct Metric { - PM_METRIC id; - std::string name; - std::string description; - std::vector availableDeviceIds; - PM_UNIT preferredUnitId; - int arraySize; - std::vector availableStatIds; - bool numeric; - - template void serialize(A& ar) { - ar(id, name, description, availableDeviceIds, preferredUnitId, - arraySize, availableStatIds, numeric); - } - }; - struct Stat { - PM_STAT id; - std::string name; - std::string shortName; - std::string description; - - template void serialize(A& ar) { - ar(id, name, shortName, description); - } - }; - struct Unit { - template void serialize(A& ar) { - } - }; - struct Response { + using namespace ::pmon::ipc::act; + + class ACT_NAME : public ACT_TYPE + { + public: + static constexpr const char* Identifier = STRINGIFY(ACT_NAME); + + struct Params + { + template void serialize(A& ar) { + // no params + } + }; + + struct Metric + { + PM_METRIC id; + std::string name; + std::string description; + std::vector availableDeviceIds; + PM_UNIT preferredUnitId; + int arraySize; + std::vector availableStatIds; + bool numeric; + + template void serialize(A& ar) { + ar(CEREAL_NVP(id), + CEREAL_NVP(name), + CEREAL_NVP(description), + CEREAL_NVP(availableDeviceIds), + CEREAL_NVP(preferredUnitId), + CEREAL_NVP(arraySize), + CEREAL_NVP(availableStatIds), + CEREAL_NVP(numeric)); + } + }; + + struct Stat + { + PM_STAT id; + std::string name; + std::string shortName; + std::string description; + + template void serialize(A& ar) { + ar(CEREAL_NVP(id), + CEREAL_NVP(name), + CEREAL_NVP(shortName), + CEREAL_NVP(description)); + } + }; + + struct Unit + { + template void serialize(A& ar) { + // TODO: populate when units are implemented + } + }; + + struct Response + { std::vector metrics; std::vector stats; std::vector units; + uint32_t systemDeviceId; + uint32_t defaultAdapterId; + + template void serialize(A& ar) { + ar(CEREAL_NVP(metrics), + CEREAL_NVP(stats), + CEREAL_NVP(units), + CEREAL_NVP(systemDeviceId), + CEREAL_NVP(defaultAdapterId)); + } + }; + + private: + friend class ACT_TYPE; - template void serialize(A& ar) { - ar(metrics, stats, units); - } - }; - private: - friend class ACT_TYPE; - static Response Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in) - { + static Response Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in) + { using namespace std::string_literals; namespace vi = std::views; namespace rn = std::ranges; + using v = pmon::util::log::V; auto& intro = (*ctx.ppKernel)->GetIntrospectionRoot(); - // --- metrics --- + // --- metrics --- // set of types that are numeric, used to generate numeric flag that the frontend uses const std::array numericTypes{ PM_DATA_TYPE_DOUBLE, PM_DATA_TYPE_UINT32, PM_DATA_TYPE_INT32, PM_DATA_TYPE_UINT64 }; + // filter predicate to only pick up metrics usable in dynamic queries (plus hardcoded blacklist) - const auto filterPred = [](const pmapi::intro::MetricView& m) { const auto type = m.GetType(); - return - ( m.GetId() != PM_METRIC_GPU_LATENCY) - && - ( type == PM_METRIC_TYPE_DYNAMIC || - type == PM_METRIC_TYPE_DYNAMIC_FRAME || - type == PM_METRIC_TYPE_STATIC); + const auto filterPred = [](const pmapi::intro::MetricView& m) { + const auto id = m.GetId(); + const auto type = m.GetType(); + return + (id != PM_METRIC_GPU_LATENCY) && + (id != PM_METRIC_SESSION_START_QPC) && + (id != PM_METRIC_SWAP_CHAIN_ADDRESS) + && + ( + type == PM_METRIC_TYPE_DYNAMIC || + type == PM_METRIC_TYPE_DYNAMIC_FRAME || + type == PM_METRIC_TYPE_STATIC + ); }; + // generate the response Response res; + res.systemDeviceId = ::pmon::ipc::kSystemDeviceId; + res.defaultAdapterId = (*ctx.ppKernel)->GetDefaultGpuDeviceId(); + // reserve space for the actual number of metrics res.metrics.reserve(rn::distance(intro.GetMetrics() | vi::filter(filterPred))); + // now process each applicable metric, filtering ones not usable for dynamic queries for (auto&& [i, m] : intro.GetMetrics() | vi::filter(filterPred) | vi::enumerate) { // array size: stopgap measure to use largest among all available devices // will replace this with per-device size when loadout per-line device selection // and per-line array index selection is implemented int arraySize = 0; + // generate device list std::vector devices; for (auto&& d : m.GetDeviceMetricInfo()) { @@ -103,10 +149,12 @@ namespace ACT_NS arraySize = std::max(arraySize, (int)d.GetArraySize()); devices.push_back(d.GetDevice().GetId()); } + // generate stat list auto stats = m.GetStatInfo() - | vi::transform([](auto&& s) {return s.GetStat(); }) + | vi::transform([](auto&& s) { return s.GetStat(); }) | rn::to(); + // add metric res.metrics.push_back(Metric{ .id = m.GetId(), @@ -118,8 +166,9 @@ namespace ACT_NS .arraySize = arraySize, .availableStatIds = std::move(stats), .numeric = rn::contains(numericTypes, m.GetDataTypeInfo().GetPolledType()), - }); + }); } + // --- stats --- auto&& statRange = intro.FindEnum(PM_ENUM_STAT).GetKeys(); for (auto&& s : statRange) { @@ -128,14 +177,17 @@ namespace ACT_NS .name = s.GetName(), .shortName = s.GetShortName(), .description = s.GetDescription(), - }); + }); } + pmlog_verb(v::kact)("Introspect action") + .serialize("introspect", res); + return res; - } - }; + } + }; - ACTION_REG(); + ACTION_REG(); } ACTION_TRAITS_DEF(); @@ -143,4 +195,4 @@ ACTION_TRAITS_DEF(); #undef ACT_NAME #undef ACT_EXEC_CTX #undef ACT_NS -#undef ACT_TYPE \ No newline at end of file +#undef ACT_TYPE diff --git a/IntelPresentMon/KernelProcess/kact/PushSpecification.h b/IntelPresentMon/KernelProcess/kact/PushSpecification.h index 83a009736..2477814f4 100644 --- a/IntelPresentMon/KernelProcess/kact/PushSpecification.h +++ b/IntelPresentMon/KernelProcess/kact/PushSpecification.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "../../Interprocess/source/act/ActionHelper.h" #include "KernelExecutionContext.h" #include "../MakeOverlaySpec.h" @@ -9,6 +9,12 @@ #include #include +// cereal JSON dump + NVP macro +#include +#include +#include +#include + #define ACT_NAME PushSpecification #define ACT_EXEC_CTX KernelExecutionContext #define ACT_TYPE AsyncActionBase_ @@ -16,9 +22,9 @@ namespace ACT_NS { - using namespace ::pmon::ipc::act; + using namespace ::pmon::ipc::act; using ::p2c::gfx::Color; - + namespace push_spec_impl { struct Metric @@ -30,7 +36,11 @@ namespace ACT_NS PM_UNIT desiredUnitId; template void serialize(A& ar) { - ar(metricId, arrayIndex, deviceId, statId, desiredUnitId); + ar(CEREAL_NVP(metricId), + CEREAL_NVP(arrayIndex), + CEREAL_NVP(deviceId), + CEREAL_NVP(statId), + CEREAL_NVP(desiredUnitId)); } }; @@ -42,19 +52,25 @@ namespace ACT_NS ::p2c::gfx::lay::AxisAffinity axisAffinity; template void serialize(A& ar) { - ar(metric, lineColor, fillColor, axisAffinity); + ar(CEREAL_NVP(metric), + CEREAL_NVP(lineColor), + CEREAL_NVP(fillColor), + CEREAL_NVP(axisAffinity)); } }; // Graph widget type. - struct Graph { + struct Graph + { std::vector metrics; uint32_t height; uint32_t vDivs; uint32_t hDivs; bool showBottomAxis; - struct GraphType { + + struct GraphType + { std::string name; std::array range; std::array rangeRight; @@ -65,9 +81,17 @@ namespace ACT_NS bool autoCount; template void serialize(A& ar) { - ar(name, range, rangeRight, binCount, countRange, autoLeft, autoRight, autoCount); + ar(CEREAL_NVP(name), + CEREAL_NVP(range), + CEREAL_NVP(rangeRight), + CEREAL_NVP(binCount), + CEREAL_NVP(countRange), + CEREAL_NVP(autoLeft), + CEREAL_NVP(autoRight), + CEREAL_NVP(autoCount)); } } graphType; + Color gridColor; Color dividerColor; Color backgroundColor; @@ -76,12 +100,23 @@ namespace ACT_NS float textSize; template void serialize(A& ar) { - ar(metrics, height, vDivs, hDivs, showBottomAxis, graphType, - gridColor, dividerColor, backgroundColor, borderColor, textColor, textSize); + ar(CEREAL_NVP(metrics), + CEREAL_NVP(height), + CEREAL_NVP(vDivs), + CEREAL_NVP(hDivs), + CEREAL_NVP(showBottomAxis), + CEREAL_NVP(graphType), + CEREAL_NVP(gridColor), + CEREAL_NVP(dividerColor), + CEREAL_NVP(backgroundColor), + CEREAL_NVP(borderColor), + CEREAL_NVP(textColor), + CEREAL_NVP(textSize)); } }; - struct Readout { + struct Readout + { std::vector metrics; bool showLabel; @@ -90,16 +125,22 @@ namespace ACT_NS Color backgroundColor; template void serialize(A& ar) { - ar(metrics, showLabel, fontSize, fontColor, backgroundColor); + ar(CEREAL_NVP(metrics), + CEREAL_NVP(showLabel), + CEREAL_NVP(fontSize), + CEREAL_NVP(fontColor), + CEREAL_NVP(backgroundColor)); } }; using Widget = std::variant; - struct Params { + struct Params + { std::optional pid; - struct Preferences { + struct Preferences + { std::string capturePath; uint32_t captureDelay; bool enableCaptureDelay; @@ -126,11 +167,14 @@ namespace ACT_NS Color overlayBorderColor; Color overlayBackgroundColor; - struct GraphFont { + struct GraphFont + { std::string name; float axisSize; + template void serialize(A& ar) { - ar(name, axisSize); + ar(CEREAL_NVP(name), + CEREAL_NVP(axisSize)); } } graphFont; @@ -155,18 +199,50 @@ namespace ACT_NS float flashInjectionBackgroundSize; template void serialize(A& ar) { - ar(capturePath, captureDelay, enableCaptureDelay, - captureDuration, enableCaptureDuration, hideDuringCapture, hideAlways, - independentWindow, metricPollRate, overlayDrawRate, telemetrySamplingPeriodMs, - etwFlushPeriod, manualEtwFlush, metricsOffset, metricsWindow, overlayPosition, - timeRange, overlayMargin, overlayBorder, overlayPadding, graphMargin, - graphBorder, graphPadding, overlayBorderColor, overlayBackgroundColor, - graphFont, overlayWidth, upscale, generateStats, enableTargetBlocklist, - enableAutotargetting, upscaleFactor, adapterId, enableFlashInjection, - flashInjectionSize, flashInjectionEnableTargetOverride, flashInjectionTargetOverride, - flashInjectionColor, flashInjectionBackgroundEnable, flashInjectionBackgroundColor, - flashInjectionRightShift, flashInjectionFlashDuration, flashInjectionUseRainbow, - flashInjectionBackgroundSize); + ar(CEREAL_NVP(capturePath), + CEREAL_NVP(captureDelay), + CEREAL_NVP(enableCaptureDelay), + CEREAL_NVP(captureDuration), + CEREAL_NVP(enableCaptureDuration), + CEREAL_NVP(hideDuringCapture), + CEREAL_NVP(hideAlways), + CEREAL_NVP(independentWindow), + CEREAL_NVP(metricPollRate), + CEREAL_NVP(overlayDrawRate), + CEREAL_NVP(telemetrySamplingPeriodMs), + CEREAL_NVP(etwFlushPeriod), + CEREAL_NVP(manualEtwFlush), + CEREAL_NVP(metricsOffset), + CEREAL_NVP(metricsWindow), + CEREAL_NVP(overlayPosition), + CEREAL_NVP(timeRange), + CEREAL_NVP(overlayMargin), + CEREAL_NVP(overlayBorder), + CEREAL_NVP(overlayPadding), + CEREAL_NVP(graphMargin), + CEREAL_NVP(graphBorder), + CEREAL_NVP(graphPadding), + CEREAL_NVP(overlayBorderColor), + CEREAL_NVP(overlayBackgroundColor), + CEREAL_NVP(graphFont), + CEREAL_NVP(overlayWidth), + CEREAL_NVP(upscale), + CEREAL_NVP(generateStats), + CEREAL_NVP(enableTargetBlocklist), + CEREAL_NVP(enableAutotargetting), + CEREAL_NVP(upscaleFactor), + CEREAL_NVP(adapterId), + CEREAL_NVP(enableFlashInjection), + CEREAL_NVP(flashInjectionEnableTargetOverride), + CEREAL_NVP(flashInjectionTargetOverride), + CEREAL_NVP(flashInjectionSize), + CEREAL_NVP(flashInjectionColor), + CEREAL_NVP(flashInjectionBackgroundEnable), + CEREAL_NVP(flashInjectionBackgroundColor), + CEREAL_NVP(flashInjectionRightShift), + CEREAL_NVP(flashInjectionFlashDuration), + CEREAL_NVP(flashInjectionUseRainbow), + CEREAL_NVP(flashInjectionBackgroundSize)); } } preferences; @@ -174,22 +250,28 @@ namespace ACT_NS std::vector widgets; template void serialize(A& ar) { - ar(pid, preferences, widgets); + ar(CEREAL_NVP(pid), + CEREAL_NVP(preferences), + CEREAL_NVP(widgets)); } }; } - class ACT_NAME : public ACT_TYPE - { - public: - static constexpr const char* Identifier = STRINGIFY(ACT_NAME); + class ACT_NAME : public ACT_TYPE + { + public: + static constexpr const char* Identifier = STRINGIFY(ACT_NAME); using Params = push_spec_impl::Params; - struct Response { - template void serialize(A& ar) { - } - }; - private: - friend class ACT_TYPE; + + struct Response { + template void serialize(A& ar) { + // no response fields + } + }; + + private: + friend class ACT_TYPE; + static Response Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in) { const GfxLayer::Extension::OverlayConfig cfg{ @@ -202,20 +284,29 @@ namespace ACT_NS .UseRainbow = in.preferences.flashInjectionUseRainbow, .BackgroundSize = in.preferences.flashInjectionBackgroundSize, }; + const auto flashTgtOverride = in.preferences.flashInjectionEnableTargetOverride ? std::optional{ in.preferences.flashInjectionTargetOverride } : std::nullopt; + (*ctx.ppKernel)->UpdateInjection(in.preferences.enableFlashInjection, in.pid, flashTgtOverride, cfg); + if (!in.pid) { (*ctx.ppKernel)->ClearOverlay(); } else { (*ctx.ppKernel)->PushSpec(MakeOverlaySpec(in)); } + + // (No useful response fields; logging the request is typically what you want here.) + using v = pmon::util::log::V; + pmlog_verb(v::kact)("PushSpecification action") + .serialize("pushSpecification", in); + return {}; } - }; + }; - ACTION_REG(); + ACTION_REG(); } namespace cereal @@ -223,7 +314,10 @@ namespace cereal template void serialize(Archive& archive, p2c::gfx::Color& s) { - archive(s.r, s.g, s.b, s.a); + archive(CEREAL_NVP(s.r), + CEREAL_NVP(s.g), + CEREAL_NVP(s.b), + CEREAL_NVP(s.a)); } } @@ -232,4 +326,4 @@ ACTION_TRAITS_DEF(); #undef ACT_NAME #undef ACT_EXEC_CTX #undef ACT_NS -#undef ACT_TYPE \ No newline at end of file +#undef ACT_TYPE diff --git a/IntelPresentMon/KernelProcess/kact/SetAdapter.h b/IntelPresentMon/KernelProcess/kact/SetAdapter.h deleted file mode 100644 index 38e80b67b..000000000 --- a/IntelPresentMon/KernelProcess/kact/SetAdapter.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once -#include "../../Interprocess/source/act/ActionHelper.h" -#include "KernelExecutionContext.h" -#include -#include "../../Core/source/kernel/Kernel.h" - -#define ACT_NAME SetAdapter -#define ACT_EXEC_CTX KernelExecutionContext -#define ACT_TYPE AsyncActionBase_ -#define ACT_NS kproc::kact - -namespace ACT_NS -{ - using namespace ::pmon::ipc::act; - - class ACT_NAME : public ACT_TYPE - { - public: - static constexpr const char* Identifier = STRINGIFY(ACT_NAME); - struct Params - { - uint32_t id; - - template void serialize(A& ar) { - ar(id); - } - }; - struct Response { - template void serialize(A& ar) { - } - }; - private: - friend class ACT_TYPE; - static Response Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in) - { - (*ctx.ppKernel)->SetAdapter(in.id); - return {}; - } - }; - -#ifdef PM_ASYNC_ACTION_REGISTRATION_ - ACTION_REG(); -#endif -} - -ACTION_TRAITS_DEF(); - -#undef ACT_NAME -#undef ACT_EXEC_CTX -#undef ACT_NS -#undef ACT_TYPE \ No newline at end of file diff --git a/IntelPresentMon/KernelProcess/winmain.cpp b/IntelPresentMon/KernelProcess/winmain.cpp index 8bb1d5dca..995c29fc8 100644 --- a/IntelPresentMon/KernelProcess/winmain.cpp +++ b/IntelPresentMon/KernelProcess/winmain.cpp @@ -1,6 +1,7 @@ -#include "../CommonUtilities/win/WinAPI.h" +#include "../CommonUtilities/win/WinAPI.h" #include "../Core/source/kernel/Kernel.h" #include "../Core/source/infra/util/FolderResolver.h" +#include "../CommonUtilities/log/IdentificationTable.h" #include "../Interprocess/source/act/SymmetricActionServer.h" #include "kact/KernelExecutionContext.h" #include "../AppCef/source/util/cact/TargetLostAction.h" @@ -124,6 +125,8 @@ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi #endif try { + util::log::IdentificationTable::AddThisProcess("kproc"); + util::log::IdentificationTable::AddThisThread("main"); // if we were run from a parent with a console (terminal?), try to attach there const bool fromTerminal = TryAttachToParentConsole_(); // parse the command line arguments and make them globally available @@ -213,8 +216,7 @@ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi // compile fixed CLI options auto args = std::vector{ "--control-pipe"s, *opt.controlPipe, - "--nsm-prefix"s, "pm-frame-nsm"s, - "--intro-nsm"s, *opt.shmName, + "--shm-name-prefix"s, *opt.shmNamePrefix, "--etw-session-name"s, *opt.etwSessionName, "--log-level"s, util::log::GetLevelName(util::log::GlobalPolicy::Get().GetLogLevel()), "--log-pipe-name"s, logSvcPipe, @@ -228,7 +230,7 @@ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi // launch service child process svcChild = bp2::windows::default_launcher{}(ioctx, "PresentMonService.exe"s, std::move(args)); // wait for pipe availability of service api - if (!::pmon::util::win::WaitForNamedPipe(*opt.controlPipe + "-in", 1500)) { + if (!::pmon::util::win::WaitForNamedPipe(*opt.controlPipe + "-in", 1500000)) { pmlog_error("timeout waiting for child service control pipe to go online"); return -1; } @@ -311,10 +313,11 @@ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi } // list adapter devices if (opt.listDevices) { - std::cout << "List of graphics adapters:\n"; + std::cout << "List of queryable devices:\n"; for (auto&& d : pIntro->GetDevices()) { - if (!d.GetId()) continue; - std::cout << d.GetName() << " [" << d.GetId() << "] (" << d.IntrospectVendor().GetName() << ")\n"; + std::cout << d.GetName() << " [" << d.GetId() << "] " + << d.IntrospectVendor().GetName() + << " (" << d.IntrospectType().GetName() << ")\n"; } } return 0; @@ -410,6 +413,7 @@ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi // don't exit this process until the CEF control panel exits cefChild.wait(); } + // TODO: organize headless CLI code into own source modules else if (opt.subcCapture.Active()) { DWORD pid; if (opt.capTargetPid) { @@ -448,6 +452,9 @@ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi .telemetrySamplingPeriodMs = *opt.capTelemetryPeriod, .hideAlways = true, }); + if (opt.capDefaultAdapterId && *opt.capDefaultAdapterId > 0) { + pSpec->frameQueryAdapterId = *opt.capDefaultAdapterId; + } std::cout << "Starting capture..." << std::endl; kernel.PushSpec(std::move(pSpec)); kernel.SetCapture(true); @@ -481,10 +488,9 @@ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi } } else { // wait indefinitely until %q command received or kernel signal - auto res = util::win::WaitAnyEvent( + if (util::win::WaitAnyEvent( dynamic_cast(pKernelHandler.get())->stopEvent_, - stopCommandEvent); - if (res && *res == 1) { + stopCommandEvent) == 1) { std::cerr << "Capture terminated by %q command." << std::endl; } else { @@ -502,4 +508,4 @@ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi } return 0; -} \ No newline at end of file +} diff --git a/IntelPresentMon/PresentMonAPI2/Internal.h b/IntelPresentMon/PresentMonAPI2/Internal.h index a230062ab..7bcc6abd8 100644 --- a/IntelPresentMon/PresentMonAPI2/Internal.h +++ b/IntelPresentMon/PresentMonAPI2/Internal.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "PresentMonAPI.h" #include "PresentMonDiagnostics.h" #include "../CommonUtilities/log/Log.h" @@ -43,5 +43,4 @@ PRESENTMON_API2_EXPORT void pmSetupODSLogging_(PM_DIAGNOSTIC_LEVEL logLevel, PRESENTMON_API2_EXPORT PM_STATUS pmSetupFileLogging_(const char* filename, PM_DIAGNOSTIC_LEVEL logLevel, PM_DIAGNOSTIC_LEVEL stackTraceLevel, bool exceptionTrace); -// set middleware to log using OutputDebugString PRESENTMON_API2_EXPORT PM_STATUS pmStopPlayback_(PM_SESSION_HANDLE handle); diff --git a/IntelPresentMon/PresentMonAPI2/PresentMonAPI.cpp b/IntelPresentMon/PresentMonAPI2/PresentMonAPI.cpp index 57b2c4870..f80be2db4 100644 --- a/IntelPresentMon/PresentMonAPI2/PresentMonAPI.cpp +++ b/IntelPresentMon/PresentMonAPI2/PresentMonAPI.cpp @@ -1,7 +1,7 @@ #include #include #include -#include "../PresentMonMiddleware/ConcreteMiddleware.h" +#include "../PresentMonMiddleware/Middleware.h" #include "../Interprocess/source/PmStatusError.h" #include "Internal.h" #include "PresentMonAPI.h" @@ -136,17 +136,13 @@ PRESENTMON_API2_EXPORT PM_STATUS pmOpenSessionWithPipe(PM_SESSION_HANDLE* pHandl return PM_STATUS_BAD_ARGUMENT; } std::shared_ptr pMiddleware; - pMiddleware = std::make_shared(pipe ? std::optional{ pipe } : std::nullopt); - *pHandle = pMiddleware.get(); + pMiddleware = std::make_shared(pipe ? std::optional{ pipe } : std::nullopt); + *pHandle = reinterpret_cast(pMiddleware.get()); handleMap_[*pHandle] = std::move(pMiddleware); pmlog_info("Middleware successfully opened session with service"); return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; - } + pmcatch_report_diag(true); } // public endpoints @@ -161,39 +157,34 @@ PRESENTMON_API2_EXPORT PM_STATUS pmCloseSession(PM_SESSION_HANDLE handle) DestroyMiddleware_(handle); return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; - } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmStartTrackingProcess(PM_SESSION_HANDLE handle, uint32_t processId) { try { - // TODO: consider tracking resource usage for process tracking to validate Start/Stop pairing - // TODO: middleware (StartStreaming) should not return status codes - return LookupMiddleware_(handle).StartStreaming(processId); + LookupMiddleware_(handle).StartTracking(processId); + return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; + pmcatch_report_diag(true); +} + +PRESENTMON_API2_EXPORT PM_STATUS pmStartPlaybackTracking(PM_SESSION_HANDLE handle, uint32_t processId, uint32_t isBackpressured) +{ + try { + LookupMiddleware_(handle).StartPlaybackTracking(processId, isBackpressured != 0); + return PM_STATUS_SUCCESS; } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmStopTrackingProcess(PM_SESSION_HANDLE handle, uint32_t processId) { try { - // TODO: consider tracking resource usage for process tracking to validate Start/Stop pairing - // TODO: middleware should not return status codes - return LookupMiddleware_(handle).StopStreaming(processId); - } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; + LookupMiddleware_(handle).StopTracking(processId); + return PM_STATUS_SUCCESS; } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmGetIntrospectionRoot(PM_SESSION_HANDLE handle, const PM_INTROSPECTION_ROOT** ppInterface) @@ -213,11 +204,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmGetIntrospectionRoot(PM_SESSION_HANDLE handle *ppInterface = pIntro; return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; - } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmFreeIntrospectionRoot(const PM_INTROSPECTION_ROOT* pInterface) @@ -234,35 +221,34 @@ PRESENTMON_API2_EXPORT PM_STATUS pmFreeIntrospectionRoot(const PM_INTROSPECTION_ free(const_cast(pInterface)); return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; - } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmSetTelemetryPollingPeriod(PM_SESSION_HANDLE handle, uint32_t deviceId, uint32_t timeMs) { try { - return LookupMiddleware_(handle).SetTelemetryPollingPeriod(deviceId, timeMs); - } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; + LookupMiddleware_(handle).SetTelemetryPollingPeriod(deviceId, timeMs); + return PM_STATUS_SUCCESS; } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmSetEtwFlushPeriod(PM_SESSION_HANDLE handle, uint32_t periodMs) { try { - return LookupMiddleware_(handle).SetEtwFlushPeriod(periodMs ? std::optional{ periodMs } : std::nullopt); + LookupMiddleware_(handle).SetEtwFlushPeriod(periodMs ? std::optional{ periodMs } : std::nullopt); + return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; + pmcatch_report_diag(true); +} + +PRESENTMON_API2_EXPORT PM_STATUS pmFlushFrames(PM_SESSION_HANDLE handle, uint32_t processId) +{ + try { + LookupMiddleware_(handle).FlushFrames(processId); + return PM_STATUS_SUCCESS; } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmRegisterDynamicQuery(PM_SESSION_HANDLE sessionHandle, PM_DYNAMIC_QUERY_HANDLE* pQueryHandle, @@ -283,11 +269,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmRegisterDynamicQuery(PM_SESSION_HANDLE sessio *pQueryHandle = queryHandle; return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; - } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmFreeDynamicQuery(PM_DYNAMIC_QUERY_HANDLE handle) @@ -302,11 +284,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmFreeDynamicQuery(PM_DYNAMIC_QUERY_HANDLE hand mid.FreeDynamicQuery(handle); return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; - } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmPollDynamicQuery(PM_DYNAMIC_QUERY_HANDLE handle, uint32_t processId, uint8_t* pBlob, uint32_t* numSwapChains) @@ -327,11 +305,28 @@ PRESENTMON_API2_EXPORT PM_STATUS pmPollDynamicQuery(PM_DYNAMIC_QUERY_HANDLE hand LookupMiddlewareCheckDropped_(handle).PollDynamicQuery(handle, processId, pBlob, numSwapChains); return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; + pmcatch_report_diag(true); +} + +PRESENTMON_API2_EXPORT PM_STATUS pmPollDynamicQueryWithTimestamp(PM_DYNAMIC_QUERY_HANDLE handle, uint32_t processId, uint8_t* pBlob, uint32_t* numSwapChains, uint64_t nowTimestamp) +{ + try { + if (!pBlob) { + pmlog_error("null blob ptr").diag(); + return PM_STATUS_BAD_ARGUMENT; + } + if (!numSwapChains) { + pmlog_error("null swap chain inoutptr").diag(); + return PM_STATUS_BAD_ARGUMENT; + } + if (!*numSwapChains) { + pmlog_error("swap chain in count is zero").diag(); + return PM_STATUS_BAD_ARGUMENT; + } + LookupMiddleware_(handle).PollDynamicQuery(handle, processId, pBlob, numSwapChains, nowTimestamp); + return PM_STATUS_SUCCESS; } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmPollStaticQuery(PM_SESSION_HANDLE sessionHandle, const PM_QUERY_ELEMENT* pElement, uint32_t processId, uint8_t* pBlob) @@ -348,11 +343,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmPollStaticQuery(PM_SESSION_HANDLE sessionHand LookupMiddlewareCheckDropped_(sessionHandle).PollStaticQuery(*pElement, processId, pBlob); return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; - } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmRegisterFrameQuery(PM_SESSION_HANDLE sessionHandle, PM_FRAME_QUERY_HANDLE* pQueryHandle, PM_QUERY_ELEMENT* pElements, uint64_t numElements, uint32_t* pBlobSize) @@ -379,11 +370,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmRegisterFrameQuery(PM_SESSION_HANDLE sessionH *pQueryHandle = queryHandle; return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; - } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmConsumeFrames(PM_FRAME_QUERY_HANDLE handle, uint32_t processId, uint8_t* pBlob, uint32_t* pNumFramesToRead) @@ -404,10 +391,10 @@ PRESENTMON_API2_EXPORT PM_STATUS pmConsumeFrames(PM_FRAME_QUERY_HANDLE handle, u const auto code = util::GeneratePmStatus(); if (code == PM_STATUS_INVALID_PID) { // invalid pid is an exception that happens at the end of a normal workflow, so don't flag as error - pmlog_info(util::ReportException()).code(code); + pmlog_info(util::ReportException()).code(code).diag(); } else { - pmlog_error(util::ReportException()).code(code); + pmlog_error(util::ReportException()).code(code).diag(); } return code; } @@ -421,11 +408,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmFreeFrameQuery(PM_FRAME_QUERY_HANDLE handle) mid.FreeFrameEventQuery(handle); return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; - } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmGetApiVersion(PM_VERSION* pVersion) @@ -445,11 +428,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmStopPlayback_(PM_SESSION_HANDLE handle) mid.StopPlayback(); return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; - } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmStartEtlLogging(PM_SESSION_HANDLE session, PM_ETL_HANDLE* pEtlHandle, @@ -460,11 +439,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmStartEtlLogging(PM_SESSION_HANDLE session, PM *pEtlHandle = mid.StartEtlLogging(); return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; - } + pmcatch_report_diag(true); } PRESENTMON_API2_EXPORT PM_STATUS pmFinishEtlLogging(PM_SESSION_HANDLE session, PM_ETL_HANDLE etlHandle, @@ -485,9 +460,5 @@ PRESENTMON_API2_EXPORT PM_STATUS pmFinishEtlLogging(PM_SESSION_HANDLE session, P pOutputFilePathBuffer[path.size()] = '\0'; return PM_STATUS_SUCCESS; } - catch (...) { - const auto code = util::GeneratePmStatus(); - pmlog_error(util::ReportException()).code(code); - return code; - } -} \ No newline at end of file + pmcatch_report_diag(true); +} diff --git a/IntelPresentMon/PresentMonAPI2/PresentMonAPI.h b/IntelPresentMon/PresentMonAPI2/PresentMonAPI.h index ed6a17760..ff9cdf421 100644 --- a/IntelPresentMon/PresentMonAPI2/PresentMonAPI.h +++ b/IntelPresentMon/PresentMonAPI2/PresentMonAPI.h @@ -1,4 +1,4 @@ -// Copyright (C) 2017-2024 Intel Corporation +// Copyright (C) 2017-2024 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #ifdef PRESENTMONAPI2_EXPORTS @@ -10,7 +10,7 @@ #include #define PM_API_VERSION_MAJOR 3 -#define PM_API_VERSION_MINOR 2 +#define PM_API_VERSION_MINOR 3 #define PM_MAX_PATH 260 @@ -42,6 +42,8 @@ extern "C" { PM_STATUS_MIDDLEWARE_VERSION_LOW, PM_STATUS_MIDDLEWARE_VERSION_HIGH, PM_STATUS_MIDDLEWARE_SERVICE_MISMATCH, + PM_STATUS_QUERY_MALFORMED, + PM_STATUS_MODE_MISMATCH, }; enum PM_METRIC @@ -135,6 +137,9 @@ extern "C" { PM_METRIC_BETWEEN_APP_START, PM_METRIC_PRESENTED_FRAME_TIME, PM_METRIC_FLIP_DELAY, + PM_METRIC_PROCESS_ID, + PM_METRIC_SESSION_START_QPC, + PM_METRIC_COUNT_, // sentry to mark end of metric list; not an actual query metric }; enum PM_METRIC_TYPE @@ -253,6 +258,7 @@ extern "C" { { PM_DEVICE_TYPE_INDEPENDENT, PM_DEVICE_TYPE_GRAPHICS_ADAPTER, + PM_DEVICE_TYPE_SYSTEM, }; enum PM_METRIC_AVAILABILITY @@ -403,6 +409,9 @@ extern "C" { PRESENTMON_API2_EXPORT PM_STATUS pmCloseSession(PM_SESSION_HANDLE handle); // command the service to process and make available frame/metric data for the specified process id PRESENTMON_API2_EXPORT PM_STATUS pmStartTrackingProcess(PM_SESSION_HANDLE handle, uint32_t process_id); + // command the service to process and make available frame/metric data for playback of the specified process id + // isBackpressured controls whether playback uses backpressured frame rings + PRESENTMON_API2_EXPORT PM_STATUS pmStartPlaybackTracking(PM_SESSION_HANDLE handle, uint32_t process_id, uint32_t isBackpressured); // command the service to cease processing and exporting frame/metric data for the specified process id PRESENTMON_API2_EXPORT PM_STATUS pmStopTrackingProcess(PM_SESSION_HANDLE handle, uint32_t process_id); // allocate and populate a tree data structure describing the available metrics, devices, etc. @@ -417,12 +426,16 @@ extern "C" { // a value of zero indicates to use current service setting (default or value requested by other client) PRESENTMON_API2_EXPORT PM_STATUS pmSetEtwFlushPeriod(PM_SESSION_HANDLE handle, uint32_t periodMs); #define PM_ETW_FLUSH_PERIOD_MAX 1000 + // flush any buffered frame event data for the specified process on this session + PRESENTMON_API2_EXPORT PM_STATUS pmFlushFrames(PM_SESSION_HANDLE handle, uint32_t processId); // register a dynamic query used for polling metric data with (optional) statistic processing such as average or percentile PRESENTMON_API2_EXPORT PM_STATUS pmRegisterDynamicQuery(PM_SESSION_HANDLE sessionHandle, PM_DYNAMIC_QUERY_HANDLE* pHandle, PM_QUERY_ELEMENT* pElements, uint64_t numElements, double windowSizeMs, double metricOffsetMs); // free the resources associated with a registered dynamic query PRESENTMON_API2_EXPORT PM_STATUS pmFreeDynamicQuery(PM_DYNAMIC_QUERY_HANDLE handle); // poll a dynamic query, writing the query poll results into the specified memory blob (byte buffer) PRESENTMON_API2_EXPORT PM_STATUS pmPollDynamicQuery(PM_DYNAMIC_QUERY_HANDLE handle, uint32_t processId, uint8_t* pBlob, uint32_t* numSwapChains); + // poll a dynamic query, writing the query poll results into the specified memory blob (byte buffer) using the provided now timestamp + PRESENTMON_API2_EXPORT PM_STATUS pmPollDynamicQueryWithTimestamp(PM_DYNAMIC_QUERY_HANDLE handle, uint32_t processId, uint8_t* pBlob, uint32_t* numSwapChains, uint64_t nowTimestamp); // query a static metric immediately, writing the result into the specified memory blob (byte buffer) PRESENTMON_API2_EXPORT PM_STATUS pmPollStaticQuery(PM_SESSION_HANDLE sessionHandle, const PM_QUERY_ELEMENT* pElement, uint32_t processId, uint8_t* pBlob); // register a frame query used for consuming desired metrics from a queue of frame events diff --git a/IntelPresentMon/PresentMonAPI2/PresentMonDiagnostics.cpp b/IntelPresentMon/PresentMonAPI2/PresentMonDiagnostics.cpp index 029df99eb..cc2665fc6 100644 --- a/IntelPresentMon/PresentMonAPI2/PresentMonDiagnostics.cpp +++ b/IntelPresentMon/PresentMonAPI2/PresentMonDiagnostics.cpp @@ -20,7 +20,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmDiagnosticSetup(const PM_DIAGNOSTIC_CONFIGURA try { log::SetupDiagnosticChannel(pConfig); return PM_STATUS_SUCCESS; - } pmcatch_report; + } pmcatch_report_diag(true); return PM_STATUS_FAILURE; } @@ -30,8 +30,8 @@ PRESENTMON_API2_EXPORT uint32_t pmDiagnosticGetQueuedMessageCount() if (auto pDiag = log::GetDiagnostics()) { return pDiag->GetQueuedMessageCount(); } - } pmcatch_report; - return PM_STATUS_FAILURE; + } pmcatch_report_diag(false); + return 0; } PRESENTMON_API2_EXPORT uint32_t pmDiagnosticGetMaxQueuedMessages() @@ -40,7 +40,7 @@ PRESENTMON_API2_EXPORT uint32_t pmDiagnosticGetMaxQueuedMessages() if (auto pDiag = log::GetDiagnostics()) { return pDiag->GetMaxQueuedMessages(); } - } pmcatch_report; + } pmcatch_report_diag(false); return 0; } @@ -51,7 +51,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmDiagnosticSetMaxQueuedMessages(uint32_t max) pDiag->SetMaxQueuedMessages(max); return PM_STATUS_SUCCESS; } - } pmcatch_report; + } pmcatch_report_diag(true); return PM_STATUS_FAILURE; } @@ -61,7 +61,7 @@ PRESENTMON_API2_EXPORT uint32_t pmDiagnosticGetDiscardedMessageCount() if (auto pDiag = log::GetDiagnostics()) { return pDiag->GetDiscardedMessageCount(); } - } pmcatch_report; + } pmcatch_report_diag(false); return 0; } @@ -79,7 +79,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmDiagnosticDequeueMessage(PM_DIAGNOSTIC_MESSAG return PM_STATUS_SUCCESS; } } - } pmcatch_report; + } pmcatch_report_diag(true); return PM_STATUS_FAILURE; } @@ -92,7 +92,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmDiagnosticEnqueueMessage(const PM_DIAGNOSTIC_ return PM_STATUS_SUCCESS; } } - } pmcatch_report; + } pmcatch_report_diag(true); return PM_STATUS_FAILURE; } @@ -103,7 +103,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmDiagnosticFreeMessage(PM_DIAGNOSTIC_MESSAGE* delete pMessage; return PM_STATUS_SUCCESS; } - } pmcatch_report; + } pmcatch_report_diag(true); return PM_STATUS_FAILURE; } @@ -113,7 +113,7 @@ PRESENTMON_API2_EXPORT PM_DIAGNOSTIC_WAKE_REASON pmDiagnosticWaitForMessage(uint if (auto pDiag = log::GetDiagnostics()) { return pDiag->WaitForMessage(timeoutMs); } - } pmcatch_report; + } pmcatch_report_diag(false); return PM_DIAGNOSTIC_WAKE_REASON_ERROR; } @@ -124,7 +124,7 @@ PRESENTMON_API2_EXPORT PM_STATUS pmDiagnosticUnblockWaitingThread() pDiag->UnblockWaitingThread(); return PM_STATUS_SUCCESS; } - } pmcatch_report; + } pmcatch_report_diag(true); return PM_STATUS_FAILURE; } diff --git a/IntelPresentMon/PresentMonAPI2Loader/Implementation.cpp b/IntelPresentMon/PresentMonAPI2Loader/Implementation.cpp index 68531c5fa..00b1cd4a3 100644 --- a/IntelPresentMon/PresentMonAPI2Loader/Implementation.cpp +++ b/IntelPresentMon/PresentMonAPI2Loader/Implementation.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -27,14 +27,17 @@ PM_STATUS(*pFunc_pmOpenSession_)(PM_SESSION_HANDLE*) = nullptr; PM_STATUS(*pFunc_pmOpenSessionWithPipe_)(PM_SESSION_HANDLE* pHandle, const char*) = nullptr; PM_STATUS(*pFunc_pmCloseSession_)(PM_SESSION_HANDLE) = nullptr; PM_STATUS(*pFunc_pmStartTrackingProcess_)(PM_SESSION_HANDLE, uint32_t) = nullptr; +PM_STATUS(*pFunc_pmStartPlaybackTracking_)(PM_SESSION_HANDLE, uint32_t, uint32_t) = nullptr; PM_STATUS(*pFunc_pmStopTrackingProcess_)(PM_SESSION_HANDLE, uint32_t) = nullptr; PM_STATUS(*pFunc_pmGetIntrospectionRoot_)(PM_SESSION_HANDLE, const PM_INTROSPECTION_ROOT**) = nullptr; PM_STATUS(*pFunc_pmFreeIntrospectionRoot_)(const PM_INTROSPECTION_ROOT*) = nullptr; PM_STATUS(*pFunc_pmSetTelemetryPollingPeriod_)(PM_SESSION_HANDLE, uint32_t, uint32_t) = nullptr; PM_STATUS(*pFunc_pmSetEtwFlushPeriod_)(PM_SESSION_HANDLE, uint32_t) = nullptr; +PM_STATUS(*pFunc_pmFlushFrames_)(PM_SESSION_HANDLE, uint32_t) = nullptr; PM_STATUS(*pFunc_pmRegisterDynamicQuery_)(PM_SESSION_HANDLE, PM_DYNAMIC_QUERY_HANDLE*, PM_QUERY_ELEMENT*, uint64_t, double, double) = nullptr; PM_STATUS(*pFunc_pmFreeDynamicQuery_)(PM_DYNAMIC_QUERY_HANDLE) = nullptr; PM_STATUS(*pFunc_pmPollDynamicQuery_)(PM_DYNAMIC_QUERY_HANDLE, uint32_t, uint8_t*, uint32_t*) = nullptr; +PM_STATUS(*pFunc_pmPollDynamicQueryWithTimestamp_)(PM_DYNAMIC_QUERY_HANDLE, uint32_t, uint8_t*, uint32_t*, uint64_t) = nullptr; PM_STATUS(*pFunc_pmPollStaticQuery_)(PM_SESSION_HANDLE, const PM_QUERY_ELEMENT*, uint32_t, uint8_t*) = nullptr; PM_STATUS(*pFunc_pmRegisterFrameQuery_)(PM_SESSION_HANDLE, PM_FRAME_QUERY_HANDLE*, PM_QUERY_ELEMENT*, uint64_t, uint32_t*) = nullptr; PM_STATUS(*pFunc_pmConsumeFrames_)(PM_FRAME_QUERY_HANDLE, uint32_t, uint8_t*, uint32_t*) = nullptr; @@ -156,14 +159,17 @@ PRESENTMON_API2_EXPORT PM_STATUS LoadLibrary_(bool versionOnly = false) RESOLVE(pmOpenSessionWithPipe); RESOLVE(pmCloseSession); RESOLVE(pmStartTrackingProcess); + RESOLVE(pmStartPlaybackTracking); RESOLVE(pmStopTrackingProcess); RESOLVE(pmGetIntrospectionRoot); RESOLVE(pmFreeIntrospectionRoot); RESOLVE(pmSetTelemetryPollingPeriod); RESOLVE(pmSetEtwFlushPeriod); + RESOLVE(pmFlushFrames); RESOLVE(pmRegisterDynamicQuery); RESOLVE(pmFreeDynamicQuery); RESOLVE(pmPollDynamicQuery); + RESOLVE(pmPollDynamicQueryWithTimestamp); RESOLVE(pmPollStaticQuery); RESOLVE(pmRegisterFrameQuery); RESOLVE(pmConsumeFrames); @@ -228,6 +234,11 @@ PRESENTMON_API2_EXPORT PM_STATUS pmStartTrackingProcess(PM_SESSION_HANDLE handle LoadEndpointsIfEmpty_(); return pFunc_pmStartTrackingProcess_(handle, process_id); } +PRESENTMON_API2_EXPORT PM_STATUS pmStartPlaybackTracking(PM_SESSION_HANDLE handle, uint32_t process_id, uint32_t isBackpressured) +{ + LoadEndpointsIfEmpty_(); + return pFunc_pmStartPlaybackTracking_(handle, process_id, isBackpressured); +} PRESENTMON_API2_EXPORT PM_STATUS pmStopTrackingProcess(PM_SESSION_HANDLE handle, uint32_t process_id) { LoadEndpointsIfEmpty_(); @@ -253,6 +264,11 @@ PRESENTMON_API2_EXPORT PM_STATUS pmSetEtwFlushPeriod(PM_SESSION_HANDLE handle, u LoadEndpointsIfEmpty_(); return pFunc_pmSetEtwFlushPeriod_(handle, periodMs); } +PRESENTMON_API2_EXPORT PM_STATUS pmFlushFrames(PM_SESSION_HANDLE handle, uint32_t processId) +{ + LoadEndpointsIfEmpty_(); + return pFunc_pmFlushFrames_(handle, processId); +} PRESENTMON_API2_EXPORT PM_STATUS pmRegisterDynamicQuery(PM_SESSION_HANDLE sessionHandle, PM_DYNAMIC_QUERY_HANDLE* pHandle, PM_QUERY_ELEMENT* pElements, uint64_t numElements, double windowSizeMs, double metricOffsetMs) { LoadEndpointsIfEmpty_(); @@ -268,6 +284,11 @@ PRESENTMON_API2_EXPORT PM_STATUS pmPollDynamicQuery(PM_DYNAMIC_QUERY_HANDLE hand LoadEndpointsIfEmpty_(); return pFunc_pmPollDynamicQuery_(handle, processId, pBlob, numSwapChains); } +PRESENTMON_API2_EXPORT PM_STATUS pmPollDynamicQueryWithTimestamp(PM_DYNAMIC_QUERY_HANDLE handle, uint32_t processId, uint8_t* pBlob, uint32_t* numSwapChains, uint64_t nowTimestamp) +{ + LoadEndpointsIfEmpty_(); + return pFunc_pmPollDynamicQueryWithTimestamp_(handle, processId, pBlob, numSwapChains, nowTimestamp); +} PRESENTMON_API2_EXPORT PM_STATUS pmPollStaticQuery(PM_SESSION_HANDLE sessionHandle, const PM_QUERY_ELEMENT* pElement, uint32_t processId, uint8_t* pBlob) { LoadEndpointsIfEmpty_(); @@ -432,4 +453,4 @@ namespace pmon::util::log { return {}; } -} \ No newline at end of file +} diff --git a/IntelPresentMon/PresentMonAPI2Tests/EtlLoggerTests.cpp b/IntelPresentMon/PresentMonAPI2Tests/EtlLoggerTests.cpp index 86f090ed3..2d8498207 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/EtlLoggerTests.cpp +++ b/IntelPresentMon/PresentMonAPI2Tests/EtlLoggerTests.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2023 Intel Corporation // SPDX-License-Identifier: MIT #include "../CommonUtilities/win/WinAPI.h" #include "CppUnitTest.h" @@ -25,8 +25,7 @@ namespace EtlLoggerTests { static CommonProcessArgs args{ .ctrlPipe = R"(\\.\pipe\pm-etllog-test-ctrl)", - .introNsm = "pm_etllog_test_intro", - .frameNsm = "pm_etllog_test_nsm", + .shmNamePrefix = "pm_etllog_test_intro", .logLevel = "debug", .logFolder = logFolder_, .sampleClientMode = "EtlLogger", @@ -53,7 +52,8 @@ namespace EtlLoggerTests { // verify initial status const auto status = fixture_.service->QueryStatus(); - Assert::AreEqual(0ull, status.nsmStreamedPids.size()); + Assert::AreEqual(0ull, status.trackedPids.size()); + Assert::AreEqual(0ull, status.frameStorePids.size()); Assert::AreEqual(16u, status.telemetryPeriodMs); Assert::IsTrue((bool)status.etwFlushPeriodMs); Assert::AreEqual(1000u, *status.etwFlushPeriodMs); @@ -107,4 +107,4 @@ namespace EtlLoggerTests Assert::IsTrue(std::filesystem::file_size(csvFilePath) > 10'000); } }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/PresentMonAPI2Tests/EtlTests.cpp b/IntelPresentMon/PresentMonAPI2Tests/EtlTests.cpp index 4f8549745..6acb00911 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/EtlTests.cpp +++ b/IntelPresentMon/PresentMonAPI2Tests/EtlTests.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2023 Intel Corporation // SPDX-License-Identifier: MIT #include "../CommonUtilities/win/WinAPI.h" #include @@ -62,7 +62,7 @@ namespace EtlTests auto frameQuery = pSession->RegisterFrameQuery(queryElements); auto blobs = frameQuery.MakeBlobContainer(numberOfBlobs); - processTracker = pSession->TrackProcess(processId); + processTracker = pSession->TrackProcess(processId, true, true); using Clock = std::chrono::high_resolution_clock; const auto start = Clock::now(); @@ -125,15 +125,14 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_0.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_0.csv"; oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -150,14 +149,13 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_0.etl"; oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -193,14 +191,13 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_0.etl"; oChild.emplace("PresentMonService.exe"s, //"--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -232,7 +229,7 @@ namespace EtlTests auto frameQuery = pSession->RegisterFrameQuery(queryElements); auto blobs = frameQuery.MakeBlobContainer(8); - processTracker = pSession->TrackProcess(processId); + processTracker = pSession->TrackProcess(processId, true, true); using Clock = std::chrono::high_resolution_clock; const auto start = Clock::now(); @@ -264,7 +261,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_0.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_0.csv"; @@ -274,8 +271,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, //"--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -313,7 +309,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_0.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_0.csv"; @@ -323,8 +319,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -362,7 +357,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_0.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_0.csv"; @@ -372,8 +367,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -411,7 +405,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_0.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_0.csv"; @@ -421,8 +415,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -460,7 +453,7 @@ namespace EtlTests std::optional debugCsv; // Empty optional const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_0.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_0.csv"; @@ -470,8 +463,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -509,7 +501,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_0.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_0.csv"; @@ -519,8 +511,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -558,7 +549,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_0.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_0.csv"; @@ -568,8 +559,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -607,7 +597,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_0.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_0.csv"; @@ -617,8 +607,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -663,7 +652,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_0.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_0.csv"; @@ -673,8 +662,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -712,7 +700,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_0.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_0.csv"; @@ -722,8 +710,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -767,7 +754,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_1.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_1.csv"; @@ -777,8 +764,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -816,7 +802,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_1.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_1.csv"; @@ -826,8 +812,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -865,7 +850,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_1.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_1.csv"; @@ -875,8 +860,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -914,7 +898,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_2.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_2.csv"; @@ -924,8 +908,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -963,7 +946,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_2.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_2.csv"; @@ -973,8 +956,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1012,7 +994,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_2.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_2.csv"; @@ -1022,8 +1004,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1061,7 +1042,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_2.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_2.csv"; @@ -1071,8 +1052,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1110,7 +1090,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_3.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_3.csv"; @@ -1120,8 +1100,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1159,7 +1138,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_3.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_3.csv"; @@ -1169,8 +1148,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1208,7 +1186,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_3.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_3.csv"; @@ -1218,8 +1196,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1264,7 +1241,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_3.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_3.csv"; @@ -1274,8 +1251,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1313,7 +1289,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_4.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_4.csv"; @@ -1323,8 +1299,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1364,7 +1339,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_4.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_4.csv"; @@ -1374,8 +1349,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1413,7 +1387,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_4.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_4.csv"; @@ -1423,8 +1397,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1469,7 +1442,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_4.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_4.csv"; @@ -1479,8 +1452,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1520,7 +1492,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_4.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_4.csv"; @@ -1530,8 +1502,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1571,7 +1542,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "..\\..\\tests\\gold\\test_case_5.etl"; const auto goldCsvName = L"..\\..\\tests\\gold\\test_case_5.csv"; @@ -1581,8 +1552,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1620,7 +1590,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "F:\\EtlTesting\\test_case_6.etl"; const auto goldCsvName = L"F:\\EtlTesting\\test_case_6.csv"; @@ -1632,8 +1602,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "60000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1671,7 +1640,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "F:\\EtlTesting\\test_case_7.etl"; const auto goldCsvName = L"F:\\EtlTesting\\test_case_7.csv"; @@ -1683,8 +1652,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "60000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1727,7 +1695,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "F:\\EtlTesting\\test_case_8.etl"; const auto goldCsvName = L"F:\\EtlTesting\\test_case_8.csv"; @@ -1739,8 +1707,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "60000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1778,7 +1745,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "F:\\EtlTesting\\test_case_9.etl"; const auto goldCsvName = L"F:\\EtlTesting\\test_case_9.csv"; @@ -1789,8 +1756,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "60000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1831,7 +1797,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "F:\\EtlTesting\\test_case_10.etl"; const auto goldCsvName = L"F:\\EtlTesting\\test_case_10.csv"; @@ -1843,8 +1809,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "60000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1885,7 +1850,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "F:\\EtlTesting\\test_case_11.etl"; const auto goldCsvName = L"F:\\EtlTesting\\test_case_11.csv"; @@ -1897,8 +1862,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, "--timed-stop"s, "60000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1938,7 +1902,7 @@ namespace EtlTests bp::opstream in; // Stream for writing to the process's input const auto pipeName = R"(\\.\pipe\test-pipe-pmsvc-2)"s; - const auto introName = "PM_intro_test_nsm_2"s; + const auto shmNamePrefix = "pm_etl_test_shm"s; const auto etlName = "F:\\EtlTesting\\test_case_12.etl"; const auto goldCsvName = L"F:\\EtlTesting\\test_case_12.csv"; @@ -1954,8 +1918,7 @@ namespace EtlTests oChild.emplace("PresentMonService.exe"s, //"--timed-stop"s, "60000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_utest_"s, - "--intro-nsm"s, introName, + "--shm-name-prefix"s, shmNamePrefix, "--etl-test-file"s, etlName, bp::std_out > out, bp::std_in < in); @@ -1983,4 +1946,4 @@ namespace EtlTests } } }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/PresentMonAPI2Tests/Folders.h b/IntelPresentMon/PresentMonAPI2Tests/Folders.h index 8b7b6a9d2..619abefbf 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/Folders.h +++ b/IntelPresentMon/PresentMonAPI2Tests/Folders.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once namespace MultiClientTests { @@ -15,4 +15,27 @@ namespace PacedPolling { static constexpr const char* logFolder_ = "TestLogs\\PacedPolling"; static constexpr const char* outFolder_ = "TestOutput\\PacedPolling"; -} \ No newline at end of file +} + +namespace PacedFrame +{ + static constexpr const char* logFolder_ = "TestLogs\\PacedFrame"; + static constexpr const char* outFolder_ = "TestOutput\\PacedFrame"; +} + +namespace InterimBroadcasterTests +{ + static constexpr const char* logFolder_ = "TestLogs\\InterimBroadcaster"; + static constexpr const char* outFolder_ = "TestOutput\\InterimBroadcaster"; +} + +namespace IpcComponentTests +{ + static constexpr const char* logFolder_ = "TestLogs\\IpcComponent"; + static constexpr const char* outFolder_ = "TestOutput\\IpcComponent"; +} + +namespace IpcMcIntegrationTests +{ + static constexpr const char* logFolder_ = "TestLogs\\IpcMcIntegration"; +} diff --git a/IntelPresentMon/PresentMonAPI2Tests/InterimBroadcasterTests.cpp b/IntelPresentMon/PresentMonAPI2Tests/InterimBroadcasterTests.cpp new file mode 100644 index 000000000..ac50e2fb4 --- /dev/null +++ b/IntelPresentMon/PresentMonAPI2Tests/InterimBroadcasterTests.cpp @@ -0,0 +1,1329 @@ +// Copyright (C) 2022-2023 Intel Corporation +// SPDX-License-Identifier: MIT +#include "../CommonUtilities/win/WinAPI.h" +#include "CppUnitTest.h" +#include "StatusComparison.h" +#include "TestProcess.h" +#include +#include +#include +#include "Folders.h" +#include "JobManager.h" + +#include "../PresentMonMiddleware/ActionClient.h" +#include "../Interprocess/source/Interprocess.h" +#include "../Interprocess/source/SystemDeviceId.h" +#include "../PresentMonAPIWrapperCommon/EnumMap.h" +#include "../PresentMonAPIWrapper/PresentMonAPIWrapper.h" +#include "../PresentMonAPIWrapper/FixedQuery.h" +#include "../PresentMonService/AllActions.h" + +#include +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +namespace vi = std::views; +namespace rn = std::ranges; +using namespace std::literals; +using namespace pmon; + +namespace InterimBroadcasterTests +{ + static std::string DumpRing_(const ipc::SampleHistoryRing& ring, size_t maxSamples = 8) + { + std::ostringstream oss; + const auto [first, last] = ring.GetSerialRange(); + const size_t count = last - first; + + oss << "serial range [" << first << ", " << last << "), count=" << count << "\n"; + + if (count == 0) { + return oss.str(); + } + + const size_t n = (count < maxSamples) ? count : maxSamples; + for (size_t i = 0; i < n; ++i) { + const auto& s = ring.At(first + i); + oss << " [" << (first + i) << "] ts=" << s.timestamp << " val=" << s.value << "\n"; + } + + if (count > n) { + oss << " ...\n"; + const auto& sLast = ring.At(last - 1); + oss << " [" << (last - 1) << "] ts=" << sLast.timestamp << " val=" << sLast.value << "\n"; + } + + // Try to include Newest() summary for debugging + try { + const auto& newest = ring.Newest(); + oss << "newest: ts=" << newest.timestamp << " val=" << newest.value << "\n"; + } + catch (...) { + // If Newest() throws on empty in some impls, ignore here. + } + + return oss.str(); + } + + class TestFixture : public CommonTestFixture + { + public: + const CommonProcessArgs& GetCommonArgs() const override + { + static CommonProcessArgs args{ + .ctrlPipe = R"(\\.\pipe\pm-intbroad-test-ctrl)", + .shmNamePrefix = "pm_intbroad_test", + .logLevel = "verbose", + .logVerboseModules = "ipc_sto met_use", + .logFolder = logFolder_, + .sampleClientMode = "NONE", + }; + return args; + } + }; + + TEST_CLASS(CommonFixtureTests) + { + TestFixture fixture_; + + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup(); + } + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + // verify service lifetime and status command functionality + TEST_METHOD(ServiceStatusTest) + { + // verify initial status + const auto status = fixture_.service->QueryStatus(); + Assert::AreEqual(0ull, status.trackedPids.size()); + Assert::AreEqual(0ull, status.frameStorePids.size()); + Assert::AreEqual(16u, status.telemetryPeriodMs); + Assert::IsTrue((bool)status.etwFlushPeriodMs); + Assert::AreEqual(1000u, *status.etwFlushPeriodMs); + } + // verify action system can connect + TEST_METHOD(ActionConnect) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + Assert::IsFalse(client.GetShmPrefix().empty()); + // there is a bit of a race condition on creating a service, immediately connecting + // and then immediately terminating it via the test control module + // not a concern for normal operation and is entirely synthetic; don't waste + // effort on trying to rework this, just add a little wait for odd tests like this + std::this_thread::sleep_for(150ms); + } + // verify comms work with introspection (no wrapper) + TEST_METHOD(IntrospectionConnect) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + auto pIntro = pComms->GetIntrospectionRoot(); + Assert::AreEqual(3ull, pIntro->pDevices->size); + auto pDevice = static_cast(pIntro->pDevices->pData[1]); + Assert::AreEqual("NVIDIA GeForce RTX 2080 Ti", pDevice->pName->pData); + } + }; + + TEST_CLASS(SystemStoreTests) + { + TestFixture fixture_; + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup(); + } + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + // trying to use a store without reporting use + TEST_METHOD(NoReport) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + // acquire introspection with enhanced wrapper interface + auto pIntro = pComms->GetIntrospectionRoot(); + pmapi::intro::Root intro{ pIntro, [](auto* p) {delete p; } }; + pmapi::EnumMap::Refresh(intro); + auto pMetricMap = pmapi::EnumMap::GetKeyMap(PM_ENUM_METRIC); + + // set telemetry period so we have a known baseline + client.DispatchSync(svc::acts::SetTelemetryPeriod::Params{ .telemetrySamplePeriodMs = 100 }); + + // get the store containing system-wide telemetry (cpu etc.) + auto& sys = pComms->GetSystemDataStore(); + for (auto&& [met, r] : sys.telemetryData.Rings()) { + Logger::WriteMessage(std::format(" TeleRing@{}\n", pMetricMap->at(met).narrowName).c_str()); + // TODO: understand the disconnect between CPU Core Utility showing up here + // and not showing up in the UI (update: it is blacklisted manually in UI introspection) + } + Assert::AreEqual(2ull, (size_t)rn::distance(sys.telemetryData.Rings())); + + // allow warm-up period + std::this_thread::sleep_for(650ms); + + // we expect 0 data point in the rings for the system since it does not populate on init + Assert::AreEqual(0ull, sys.telemetryData.FindRing(PM_METRIC_CPU_UTILIZATION).at(0).Size()); + } + // static store + TEST_METHOD(StaticData) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + // get the store containing system-wide telemetry (cpu etc.) + auto& sys = pComms->GetSystemDataStore(); + Assert::AreEqual((int)PM_DEVICE_VENDOR_AMD, (int)sys.statics.cpuVendor); + Assert::AreEqual("AMD Ryzen 7 5800X 8-Core Processor", sys.statics.cpuName.c_str()); + Assert::AreEqual(0., sys.statics.cpuPowerLimit); + } + // polled store + TEST_METHOD(PolledData) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + // acquire introspection with enhanced wrapper interface + auto pIntro = pComms->GetIntrospectionRoot(); + pmapi::intro::Root intro{ pIntro, [](auto* p) {delete p; } }; + pmapi::EnumMap::Refresh(intro); + auto pMetricMap = pmapi::EnumMap::GetKeyMap(PM_ENUM_METRIC); + + // set telemetry period so we have a known baseline + client.DispatchSync(svc::acts::SetTelemetryPeriod::Params{ .telemetrySamplePeriodMs = 100 }); + + // get the store containing system-wide telemetry (cpu etc.) + auto& sys = pComms->GetSystemDataStore(); + for (auto&& [met, r] : sys.telemetryData.Rings()) { + Logger::WriteMessage(std::format(" TeleRing@{}\n", pMetricMap->at(met).narrowName).c_str()); + // TODO: understand the disconnect between CPU Core Utility showing up here + // and not showing up in the UI (update: it is blacklisted manually in UI introspection) + } + Assert::AreEqual(2ull, (size_t)rn::distance(sys.telemetryData.Rings())); + + // update server with metric/device usage information + // this will trigger system telemetry collection + client.DispatchSync(svc::acts::ReportMetricUse::Params{ { + { PM_METRIC_CPU_UTILIZATION, ipc::kSystemDeviceId, 0 }, + { PM_METRIC_CPU_FREQUENCY, ipc::kSystemDeviceId, 0 }, + } }); + + // allow warm-up period + std::this_thread::sleep_for(150ms); + + // check that we have data for frequency and utilization + std::vector> utilizSamples; + std::vector> freqSamples; + for (int i = 0; i < 10; i++) { + std::this_thread::sleep_for(250ms); + { + constexpr auto m = PM_METRIC_CPU_UTILIZATION; + auto& r = sys.telemetryData.FindRing(m).at(0); + Assert::IsFalse(r.Empty()); + if (i == 0 || i == 9) { + Logger::WriteMessage(DumpRing_(r).c_str()); + } + auto sample = r.Newest(); + utilizSamples.push_back(sample); + Logger::WriteMessage(std::format("({}) {}: {}\n", + i, pMetricMap->at(m).narrowName, sample.value).c_str()); + Assert::IsTrue(sample.value > 1.); + } + { + constexpr auto m = PM_METRIC_CPU_FREQUENCY; + auto& r = sys.telemetryData.FindRing(m).at(0); + Assert::IsFalse(r.Empty()); + if (i == 0 || i == 9) { + Logger::WriteMessage(DumpRing_(r).c_str()); + } + auto sample = r.Newest(); + freqSamples.push_back(sample); + Logger::WriteMessage(std::format("({}) {}: {}\n", + i, pMetricMap->at(m).narrowName, sample.value).c_str()); + Assert::IsTrue(sample.value > 1500.); + } + } + + // make sure samples actually change over time + Assert::AreNotEqual(utilizSamples.front().timestamp, utilizSamples.back().timestamp); + Assert::AreNotEqual(utilizSamples.front().value, utilizSamples.back().value); + Assert::AreNotEqual(freqSamples.front().timestamp, freqSamples.back().timestamp); + Assert::AreNotEqual(freqSamples.front().value, freqSamples.back().value); + } + // full 1:1 correspondence between ring creation, ring population, and introspection availability + TEST_METHOD(RingUtilization) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + // acquire introspection with enhanced wrapper interface + auto pIntro = pComms->GetIntrospectionRoot(); + pmapi::intro::Root intro{ pIntro, [](auto* p) { delete p; } }; + pmapi::EnumMap::Refresh(intro); + auto pMetricMap = pmapi::EnumMap::GetKeyMap(PM_ENUM_METRIC); + + // set telemetry period so we have a known baseline + client.DispatchSync(svc::acts::SetTelemetryPeriod::Params{ .telemetrySamplePeriodMs = 40 }); + + // get the store containing adapter telemetry + auto& sys = pComms->GetSystemDataStore(); + + // allow a short warmup + std::this_thread::sleep_for(500ms); + + // build the set of expected rings from the store, and cross-check against introspection + Logger::WriteMessage("Store Metrics\n=============\n"); + std::map storeRings; + for (auto&& [met, r] : sys.telemetryData.Rings()) { + const auto storeArraySize = sys.telemetryData.ArraySize(met); + storeRings[met] = storeArraySize; + + // dump for review in output pane + Logger::WriteMessage(std::format("[{}] {}\n", storeArraySize, + pMetricMap->at(met).narrowName).c_str()); + + // validate introspection says the metric is available for the system device + auto&& m = intro.FindMetric(met); + bool matchedDevice = false; + size_t introArraySize = 0; + for (auto&& di : m.GetDeviceMetricInfo()) { + if (di.GetDevice().GetId() != ipc::kSystemDeviceId) { + // skip over non-matching devices + continue; + } + matchedDevice = true; + if (di.GetAvailability() == PM_METRIC_AVAILABILITY_AVAILABLE) { + introArraySize = di.GetArraySize(); + } + // either way, if we get here, device matched so no need to continue + break; + } + Assert::IsTrue(matchedDevice, pMetricMap->at(met).wideName.c_str()); + Assert::AreEqual(storeArraySize, introArraySize, pMetricMap->at(met).wideName.c_str()); + } + Logger::WriteMessage(std::format("Total: {}\n", storeRings.size()).c_str()); + + // validate that the expected number of rings sets are present in the store + Assert::AreEqual(storeRings.size(), (size_t)rn::distance(sys.telemetryData.Rings())); + + { + // build metric use set from above store results + std::unordered_set uses; + for (auto&& [met, siz] : storeRings) { + if (siz > 0) { + uses.insert({ met, ipc::kSystemDeviceId, 0 }); + } + } + // update server with metric/device usage information + // this will trigger system telemetry collection + client.DispatchSync(svc::acts::ReportMetricUse::Params{ std::move(uses) }); + } + + // allow a short warmup + std::this_thread::sleep_for(150ms); + + // validate that exepected rings are populated with samples and have correct dimensions + for (auto&& [met, size] : storeRings) { + // array sizes should match + Assert::AreEqual(size, sys.telemetryData.ArraySize(met), + pMetricMap->at(met).wideName.c_str()); + std::visit([&](auto const& rings) { + // for each history ring in set, make sure it has at least more than one sample in it + for (size_t i = 0; i < size; i++) { + auto& name = pMetricMap->at(met).wideName; + Assert::IsTrue(rings[i].Size() > 1, + std::format(L"{}[{}]", name, i).c_str()); + auto& sample = rings[i].Newest(); + Logger::WriteMessage(std::format(L"{}[{}]: {}@{}\n", name, i, + sample.value, sample.timestamp).c_str()); + } + }, sys.telemetryData.FindRingVariant(met)); + } + } + }; + + TEST_CLASS(GpuStoreTests) + { + TestFixture fixture_; + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup(); + } + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + // trying to use a store without reporting use + TEST_METHOD(NoReport) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + // acquire introspection with enhanced wrapper interface + auto pIntro = pComms->GetIntrospectionRoot(); + pmapi::intro::Root intro{ pIntro, [](auto* p) { delete p; } }; + pmapi::EnumMap::Refresh(intro); + auto pMetricMap = pmapi::EnumMap::GetKeyMap(PM_ENUM_METRIC); + + // set telemetry period so we have a known baseline + client.DispatchSync(svc::acts::SetTelemetryPeriod::Params{ .telemetrySamplePeriodMs = 100 }); + + // target gpu device 1 (hardcoded for test) + const uint32_t TargetDeviceID = 1; + + // get the store containing adapter telemetry + auto& gpu = pComms->GetGpuDataStore(TargetDeviceID); + for (auto&& [met, r] : gpu.telemetryData.Rings()) { + Logger::WriteMessage(std::format(" TeleRing@{}\n", pMetricMap->at(met).narrowName).c_str()); + } + Assert::IsTrue((size_t)rn::distance(gpu.telemetryData.Rings()) > 0); + + // allow warm-up period + std::this_thread::sleep_for(650ms); + + // we expect 0 data points in the rings for the gpu since it does not populate on init + Assert::AreEqual(0ull, gpu.telemetryData.FindRing(PM_METRIC_GPU_TEMPERATURE).at(0).Size()); + } + // static store + TEST_METHOD(StaticData) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + // get the store containing gpu telemetry + auto& gpu = pComms->GetGpuDataStore(1); + Assert::AreEqual((int)PM_DEVICE_VENDOR_NVIDIA, (int)gpu.statics.vendor); + Assert::AreEqual("NVIDIA GeForce RTX 2080 Ti", gpu.statics.name.c_str()); + Assert::AreEqual(260., gpu.statics.sustainedPowerLimit); + Assert::AreEqual(11811160064ull, gpu.statics.memSize); + } + // polled store + TEST_METHOD(PolledData) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + // acquire introspection with enhanced wrapper interface + auto pIntro = pComms->GetIntrospectionRoot(); + pmapi::intro::Root intro{ pIntro, [](auto* p) { delete p; } }; + pmapi::EnumMap::Refresh(intro); + auto pMetricMap = pmapi::EnumMap::GetKeyMap(PM_ENUM_METRIC); + + // set telemetry period so we have a known baseline + client.DispatchSync(svc::acts::SetTelemetryPeriod::Params{ .telemetrySamplePeriodMs = 100 }); + + // target gpu device 1 (hardcoded for test) + const uint32_t TargetDeviceID = 1; + + // update server with metric/device usage information + // this will trigger gpu telemetry collection + client.DispatchSync(svc::acts::ReportMetricUse::Params{ { + { PM_METRIC_GPU_TEMPERATURE, TargetDeviceID, 0 }, + { PM_METRIC_GPU_POWER, TargetDeviceID, 0 }, + } }); + + // get the store containing adapter telemetry + auto& gpu = pComms->GetGpuDataStore(TargetDeviceID); + + // allow a short warmup + std::this_thread::sleep_for(150ms); + + std::vector> tempSamples; + std::vector> powerSamples; + + for (int i = 0; i < 10; i++) { + std::this_thread::sleep_for(250ms); + + { + constexpr auto m = PM_METRIC_GPU_TEMPERATURE; + auto& r = gpu.telemetryData.FindRing(m).at(0); + Assert::IsFalse(r.Empty()); + + if (i == 0 || i == 9) { + Logger::WriteMessage(DumpRing_(r).c_str()); + } + + auto sample = r.Newest(); + tempSamples.push_back(sample); + + Logger::WriteMessage(std::format("({}) {}: {}\n", + i, pMetricMap->at(m).narrowName, sample.value).c_str()); + + // loose sanity check to avoid flakiness + Assert::IsTrue(sample.value > 10.); + } + + { + constexpr auto m = PM_METRIC_GPU_POWER; + auto& r = gpu.telemetryData.FindRing(m).at(0); + Assert::IsFalse(r.Empty()); + + if (i == 0 || i == 9) { + Logger::WriteMessage(DumpRing_(r).c_str()); + } + + auto sample = r.Newest(); + powerSamples.push_back(sample); + + Logger::WriteMessage(std::format("({}) {}: {}\n", + i, pMetricMap->at(m).narrowName, sample.value).c_str()); + + // loose sanity check to avoid flakiness + Assert::IsTrue(sample.value > 1.); + } + } + + // make sure samples actually change over time + Assert::AreNotEqual(tempSamples.front().timestamp, tempSamples.back().timestamp); + Assert::AreNotEqual(powerSamples.front().timestamp, powerSamples.back().timestamp); + Assert::AreNotEqual(powerSamples.front().value, powerSamples.back().value); + } + // full 1:1 correspondence between ring creation, ring population, and introspection availability + TEST_METHOD(RingUtilization) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + // acquire introspection with enhanced wrapper interface + auto pIntro = pComms->GetIntrospectionRoot(); + pmapi::intro::Root intro{ pIntro, [](auto* p) { delete p; } }; + pmapi::EnumMap::Refresh(intro); + auto pMetricMap = pmapi::EnumMap::GetKeyMap(PM_ENUM_METRIC); + + // set telemetry period so we have a known baseline + client.DispatchSync(svc::acts::SetTelemetryPeriod::Params{ .telemetrySamplePeriodMs = 40 }); + + // target gpu device 1 (hardcoded for test) + const uint32_t TargetDeviceID = 1; + + // get the store containing adapter telemetry + auto& gpu = pComms->GetGpuDataStore(TargetDeviceID); + + // build the set of expected rings from introspection + Logger::WriteMessage("Introspection Metrics\n=====================\n"); + std::map introspectionAvailability; + for (auto&& m : intro.GetMetrics()) { + // only consider metrics that are polled + if (m.GetType() != PM_METRIC_TYPE_DYNAMIC && + m.GetType() != PM_METRIC_TYPE_DYNAMIC_FRAME) { + continue; + } + // check availability for target gpu + size_t arraySize = 0; + for (auto&& di : m.GetDeviceMetricInfo()) { + if (di.GetDevice().GetId() != TargetDeviceID) { + // skip over non-matching devices + continue; + } + if (di.GetAvailability() == PM_METRIC_AVAILABILITY_AVAILABLE) { + // if available get size (otherwise leave at 0 default) + arraySize = di.GetArraySize(); + } + // either way, if we get here, device matched so no need to continue + break; + } + // only consider metrics associated with and available for target gpu + if (arraySize > 0) { + introspectionAvailability[m.GetId()] = arraySize; + // dump for review in output pane + Logger::WriteMessage(std::format("[{}] {}\n", arraySize, + pMetricMap->at(m.GetId()).narrowName).c_str()); + } + } + Logger::WriteMessage(std::format("Total: {}\n", introspectionAvailability.size()).c_str()); + + // validate that the expected number of rings sets are present in the store + Assert::AreEqual(introspectionAvailability.size(), (size_t)rn::distance(gpu.telemetryData.Rings())); + + { + // build metric use set from above introspection results + std::unordered_set uses; + for (auto&& [met, siz] : introspectionAvailability) { + if (siz > 0) { + uses.insert({ met, TargetDeviceID, 0 }); + } + } + // update server with metric/device usage information + // this will trigger gpu telemetry collection + client.DispatchSync(svc::acts::ReportMetricUse::Params{ std::move(uses) }); + } + + // allow a short warmup + std::this_thread::sleep_for(150ms); + + // validate that exepected rings are populated with samples and have correct dimensions + for (auto&& [met, size] : introspectionAvailability) { + // array sizes should match + Assert::AreEqual(size, gpu.telemetryData.ArraySize(met), + pMetricMap->at(met).wideName.c_str()); + std::visit([&](auto const& rings) { + // for each history ring in set, make sure it has at least more than one sample in it + for (size_t i = 0; i < size; i++) { + auto& name = pMetricMap->at(met).wideName; + Assert::IsTrue(rings[i].Size() > 1, + std::format(L"{}[{}]", name, i).c_str()); + auto& sample = rings[i].Newest(); + Logger::WriteMessage(std::format(L"{}[{}]: {}@{}\n", name, i, + sample.value, sample.timestamp).c_str()); + } + }, gpu.telemetryData.FindRingVariant(met)); + } + } + }; + + TEST_CLASS(NewActivationIsolationTests) + { + TestFixture fixture_; + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup(); + } + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + TEST_METHOD(SystemOnlyLeavesGpuEmpty) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + client.DispatchSync(svc::acts::SetTelemetryPeriod::Params{ .telemetrySamplePeriodMs = 350 }); + Logger::WriteMessage("SystemOnlyLeavesGpuEmpty: telemetry period set to 350ms\n"); + + const uint32_t TargetDeviceID = 1; + + client.DispatchSync(svc::acts::ReportMetricUse::Params{ { + { PM_METRIC_CPU_UTILIZATION, ipc::kSystemDeviceId, 0 }, + { PM_METRIC_CPU_FREQUENCY, ipc::kSystemDeviceId, 0 }, + } }); + Logger::WriteMessage("SystemOnlyLeavesGpuEmpty: reported CPU utilization/frequency usage\n"); + + auto& sys = pComms->GetSystemDataStore(); + auto& gpu = pComms->GetGpuDataStore(TargetDeviceID); + + auto& sysRing = sys.telemetryData.FindRing(PM_METRIC_CPU_UTILIZATION).at(0); + auto& sysFreqRing = sys.telemetryData.FindRing(PM_METRIC_CPU_FREQUENCY).at(0); + auto& gpuRing = gpu.telemetryData.FindRing(PM_METRIC_GPU_TEMPERATURE).at(0); + std::this_thread::sleep_for(1500ms); + const auto logRing = [](const char* label, const ipc::SampleHistoryRing& ring) { + const auto range = ring.GetSerialRange(); + Logger::WriteMessage(std::format( + "{}: serial [{}, {}) count={}\n", + label, range.first, range.second, range.second - range.first).c_str()); + for (size_t s = range.first; s < range.second; ++s) { + const auto& sample = ring.At(s); + Logger::WriteMessage(std::format( + "{}[{}]: val={} ts={}\n", + label, s, sample.value, sample.timestamp).c_str()); + } + }; + Logger::WriteMessage(std::format( + "SystemOnlyLeavesGpuEmpty: sizes cpu_util={} cpu_freq={} gpu_temp={}\n", + sysRing.Size(), sysFreqRing.Size(), gpuRing.Size()).c_str()); + logRing("cpu_util", sysRing); + logRing("cpu_freq", sysFreqRing); + logRing("gpu_temp", gpuRing); + Assert::IsTrue(sysRing.Size() >= 3, + std::format(L"Expected cpu utilization ring to have >= 3 samples, got {}", + sysRing.Size()).c_str()); + Assert::IsTrue(sysFreqRing.Size() >= 3, + std::format(L"Expected cpu frequency ring to have >= 3 samples, got {}", + sysFreqRing.Size()).c_str()); + const auto sysRange = sysRing.GetSerialRange(); + const auto sysFreqRange = sysFreqRing.GetSerialRange(); + const auto sysSample = sysRing.At(sysRange.second - 1); + const auto sysFreqSample = sysFreqRing.At(sysFreqRange.second - 1); + Logger::WriteMessage(std::format( + "SystemOnlyLeavesGpuEmpty: cpu_util val={} ts={} cpu_freq val={} ts={}\n", + sysSample.value, sysSample.timestamp, sysFreqSample.value, sysFreqSample.timestamp).c_str()); + Assert::IsTrue(sysSample.value > 1., + std::format(L"Expected cpu utilization > 1, got {}", sysSample.value).c_str()); + Assert::IsTrue(sysSample.value < 100., + std::format(L"Expected cpu utilization < 100, got {}", sysSample.value).c_str()); + Assert::IsTrue(sysFreqSample.value > 1500., + std::format(L"Expected cpu frequency > 1500, got {}", sysFreqSample.value).c_str()); + Assert::IsTrue(sysFreqSample.value < 6000., + std::format(L"Expected cpu frequency < 6000, got {}", sysFreqSample.value).c_str()); + Assert::AreEqual(0ull, gpuRing.Size(), + std::format(L"Expected gpu temperature ring size == 0, got {}", gpuRing.Size()).c_str()); + } + TEST_METHOD(GpuOnlyLeavesSystemEmpty) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + client.DispatchSync(svc::acts::SetTelemetryPeriod::Params{ .telemetrySamplePeriodMs = 350 }); + Logger::WriteMessage("GpuOnlyLeavesSystemEmpty: telemetry period set to 350ms\n"); + + const uint32_t TargetDeviceID = 1; + + client.DispatchSync(svc::acts::ReportMetricUse::Params{ { + { PM_METRIC_GPU_TEMPERATURE, TargetDeviceID, 0 }, + { PM_METRIC_GPU_POWER, TargetDeviceID, 0 }, + } }); + Logger::WriteMessage("GpuOnlyLeavesSystemEmpty: reported GPU temperature/power usage\n"); + + auto& sys = pComms->GetSystemDataStore(); + auto& gpu = pComms->GetGpuDataStore(TargetDeviceID); + + auto& gpuRing = gpu.telemetryData.FindRing(PM_METRIC_GPU_TEMPERATURE).at(0); + auto& gpuPowerRing = gpu.telemetryData.FindRing(PM_METRIC_GPU_POWER).at(0); + auto& sysRing = sys.telemetryData.FindRing(PM_METRIC_CPU_UTILIZATION).at(0); + std::this_thread::sleep_for(1500ms); + const auto logRing = [](const char* label, const ipc::SampleHistoryRing& ring) { + const auto range = ring.GetSerialRange(); + Logger::WriteMessage(std::format( + "{}: serial [{}, {}) count={}\n", + label, range.first, range.second, range.second - range.first).c_str()); + for (size_t s = range.first; s < range.second; ++s) { + const auto& sample = ring.At(s); + Logger::WriteMessage(std::format( + "{}[{}]: val={} ts={}\n", + label, s, sample.value, sample.timestamp).c_str()); + } + }; + Logger::WriteMessage(std::format( + "GpuOnlyLeavesSystemEmpty: sizes gpu_temp={} gpu_power={} cpu_util={}\n", + gpuRing.Size(), gpuPowerRing.Size(), sysRing.Size()).c_str()); + logRing("gpu_temp", gpuRing); + logRing("gpu_power", gpuPowerRing); + logRing("cpu_util", sysRing); + Assert::IsTrue(gpuRing.Size() >= 3, + std::format(L"Expected gpu temperature ring to have >= 3 samples, got {}", + gpuRing.Size()).c_str()); + Assert::IsTrue(gpuPowerRing.Size() >= 3, + std::format(L"Expected gpu power ring to have >= 3 samples, got {}", + gpuPowerRing.Size()).c_str()); + const auto gpuRange = gpuRing.GetSerialRange(); + const auto gpuPowerRange = gpuPowerRing.GetSerialRange(); + const auto gpuSample = gpuRing.At(gpuRange.second - 1); + const auto gpuPowerSample = gpuPowerRing.At(gpuPowerRange.second - 1); + Logger::WriteMessage(std::format( + "GpuOnlyLeavesSystemEmpty: gpu_temp val={} ts={} gpu_power val={} ts={}\n", + gpuSample.value, gpuSample.timestamp, gpuPowerSample.value, gpuPowerSample.timestamp).c_str()); + Assert::IsTrue(gpuSample.value > 10., + std::format(L"Expected gpu temperature > 10, got {}", gpuSample.value).c_str()); + Assert::IsTrue(gpuSample.value < 120., + std::format(L"Expected gpu temperature < 120, got {}", gpuSample.value).c_str()); + Assert::IsTrue(gpuPowerSample.value > 1., + std::format(L"Expected gpu power > 1, got {}", gpuPowerSample.value).c_str()); + Assert::IsTrue(gpuPowerSample.value < 600., + std::format(L"Expected gpu power < 600, got {}", gpuPowerSample.value).c_str()); + Assert::AreEqual(0ull, sysRing.Size(), + std::format(L"Expected cpu utilization ring size == 0, got {}", sysRing.Size()).c_str()); + } + }; + + TEST_CLASS(FrameStoreRealtimeTests) + { + TestFixture fixture_; + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup(); + } + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + // static store + TEST_METHOD(StaticData) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + // launch target and track it + auto pres = fixture_.LaunchPresenter(); + client.DispatchSync(svc::acts::StartTracking::Params{ .targetPid = pres.GetId() }); + + // open the store + pComms->OpenFrameDataStore(pres.GetId()); + + // verify static data + auto& store = pComms->GetFrameDataStore(pres.GetId()); + Assert::AreEqual(pres.GetId(), store.bookkeeping.processId); + const std::string staticAppName = store.statics.applicationName.c_str(); + Assert::AreEqual("PresentBench.exe"s, staticAppName); + } + TEST_METHOD(TrackUntrack) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + // launch target and track it + auto pres = fixture_.LaunchPresenter(); + client.DispatchSync(svc::acts::StartTracking::Params{ .targetPid = pres.GetId() }); + + // verify the store exists + pComms->OpenFrameDataStore(pres.GetId()); + + // verify the service tracking, as expected + { + const auto sta = fixture_.service->QueryStatus(); + Assert::AreEqual(1ull, sta.trackedPids.size()); + Assert::IsTrue(sta.trackedPids.contains(pres.GetId())); + Assert::AreEqual(1ull, sta.frameStorePids.size()); + Assert::IsTrue(sta.frameStorePids.contains(pres.GetId())); + } + + // stop tracking + client.DispatchSync(svc::acts::StopTracking::Params{ .targetPid = pres.GetId() }); + + // close the segment + pComms->CloseFrameDataStore(pres.GetId()); + + // verify the service not tracking, as expected + { + const auto sta = fixture_.service->QueryStatus(); + Assert::AreEqual(0ull, sta.trackedPids.size()); + Assert::AreEqual(0ull, sta.frameStorePids.size()); + } + + // verify segment can no longer be opened + Assert::ExpectException([&] {pComms->OpenFrameDataStore(pres.GetId()); }); + } + // make sure we get frames over time + TEST_METHOD(ReadFrames) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + // launch target and track it + auto pres = fixture_.LaunchPresenter(); + client.DispatchSync(svc::acts::SetEtwFlushPeriod::Params{ .etwFlushPeriodMs = 8 }); + // make sure the flush period propagates to the flusher thread + std::this_thread::sleep_for(1ms); + client.DispatchSync(svc::acts::StartTracking::Params{ .targetPid = pres.GetId() }); + + // open the store + pComms->OpenFrameDataStore(pres.GetId()); + auto& frames = pComms->GetFrameDataStore(pres.GetId()).frameData; + + // sleep here to let the presenter init, etw system warm up, and frames propagate + std::this_thread::sleep_for(550ms); + + // verify that frames are added over time + const auto range1 = frames.GetSerialRange(); + Logger::WriteMessage(std::format("range [{},{})\n", range1.first, range1.second).c_str()); + std::this_thread::sleep_for(100ms); + const auto range2 = frames.GetSerialRange(); + Logger::WriteMessage(std::format("range [{},{})\n", range2.first, range2.second).c_str()); + std::this_thread::sleep_for(100ms); + const auto range3 = frames.GetSerialRange(); + Logger::WriteMessage(std::format("range [{},{})\n", range3.first, range3.second).c_str()); + + Assert::IsTrue(range1.second - range1.first < range2.second - range2.first); + Assert::IsTrue(range2.second - range2.first < range3.second - range3.first); + } + }; + + TEST_CLASS(FrameStoreRealtimeWrapTests) + { + TestFixture fixture_; + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup({ + "--frame-ring-samples"s, "16"s, + }); + } + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + TEST_METHOD(WrapNoMissingFrames) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + auto pres = fixture_.LaunchPresenter(); + client.DispatchSync(svc::acts::SetEtwFlushPeriod::Params{ .etwFlushPeriodMs = 8 }); + std::this_thread::sleep_for(1ms); + client.DispatchSync(svc::acts::StartTracking::Params{ .targetPid = pres.GetId() }); + + pComms->OpenFrameDataStore(pres.GetId()); + auto& ring = pComms->GetFrameDataStore(pres.GetId()).frameData; + + std::this_thread::sleep_for(200ms); + + size_t lastProcessed = 0; + bool missed = false; + bool sawWrap = false; + bool hasTimestamp = false; + uint64_t lastTimestamp = 0; + + for (size_t i = 0; i < 60; ++i) { + std::this_thread::sleep_for(25ms); + const auto range = ring.GetSerialRange(); + Logger::WriteMessage(std::format( + "rt-wrap-no-miss: range [{}, {}), lastProcessed={}\n", + range.first, range.second, lastProcessed).c_str()); + if (range.first > 0) { + sawWrap = true; + } + if (range.first > lastProcessed) { + missed = true; + } + const size_t start = (std::max)(lastProcessed, range.first); + for (size_t s = start; s < range.second; ++s) { + const auto& frame = ring.At(s); + const uint64_t stamp = frame.presentStartTime + frame.timeInPresent; + if (hasTimestamp) { + Assert::IsTrue(stamp >= lastTimestamp); + } + lastTimestamp = stamp; + hasTimestamp = true; + } + lastProcessed = range.second; + } + + Assert::IsTrue(sawWrap, L"Expected ring to wrap"); + Assert::IsFalse(missed, L"Expected no missing frames with frequent reads"); + Assert::IsTrue(lastProcessed > 0); + } + TEST_METHOD(WrapMissingFrames) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + auto pres = fixture_.LaunchPresenter(); + client.DispatchSync(svc::acts::SetEtwFlushPeriod::Params{ .etwFlushPeriodMs = 8 }); + std::this_thread::sleep_for(1ms); + client.DispatchSync(svc::acts::StartTracking::Params{ .targetPid = pres.GetId() }); + + pComms->OpenFrameDataStore(pres.GetId()); + auto& ring = pComms->GetFrameDataStore(pres.GetId()).frameData; + + auto range = ring.GetSerialRange(); + for (size_t i = 0; i < 20 && range.first == 0; ++i) { + std::this_thread::sleep_for(100ms); + range = ring.GetSerialRange(); + } + Logger::WriteMessage(std::format( + "rt-wrap-miss: range [{}, {})\n", range.first, range.second).c_str()); + + Assert::IsTrue(range.first > 0, L"Expected missing frames after delay"); + Assert::IsTrue(range.second > range.first); + } + }; + + TEST_CLASS(FrameStorePacedPlaybackTests) + { + TestFixture fixture_; + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup({ + "--etl-test-file"s, R"(..\..\Tests\AuxData\Data\P00HeaWin2080.etl)", + "--pace-playback"s, + }); + } + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + // static store + TEST_METHOD(StaticData) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + // track known target + const uint32_t pid = 12820; + client.DispatchSync(svc::acts::StartTracking::Params{ .targetPid = pid, .isPlayback = true }); + + // open the store + pComms->OpenFrameDataStore(pid); + + // wait for population of frame data-initialized statics + std::this_thread::sleep_for(500ms); + + // verify static data + auto& store = pComms->GetFrameDataStore(pid); + Assert::AreEqual(pid, store.bookkeeping.processId); + const std::string staticAppName = store.statics.applicationName.c_str(); + Assert::AreEqual("Heaven.exe"s, staticAppName); + } + // make sure we get frames over time + TEST_METHOD(ReadFrames) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + // set up a fast flush + client.DispatchSync(svc::acts::SetEtwFlushPeriod::Params{ .etwFlushPeriodMs = 8 }); + // make sure the flush period propagates to the flusher thread + std::this_thread::sleep_for(1ms); + // we know the pid of interest in this etl file, track it + const uint32_t pid = 12820; + client.DispatchSync(svc::acts::StartTracking::Params{ .targetPid = pid, .isPlayback = true }); + + // open the store + pComms->OpenFrameDataStore(pid); + auto& frames = pComms->GetFrameDataStore(pid).frameData; + + // sleep here to let the etw system warm up, and frames propagate + std::this_thread::sleep_for(450ms); + + // verify that frames are added over time + const auto range1 = frames.GetSerialRange(); + Logger::WriteMessage(std::format("range [{},{})\n", range1.first, range1.second).c_str()); + std::this_thread::sleep_for(100ms); + const auto range2 = frames.GetSerialRange(); + Logger::WriteMessage(std::format("range [{},{})\n", range2.first, range2.second).c_str()); + std::this_thread::sleep_for(100ms); + const auto range3 = frames.GetSerialRange(); + Logger::WriteMessage(std::format("range [{},{})\n", range3.first, range3.second).c_str()); + + Assert::IsTrue(range1.second - range1.first < range2.second - range2.first); + Assert::IsTrue(range2.second - range2.first < range3.second - range3.first); + } + }; + + TEST_CLASS(FrameStoreBackpressuredPlaybackTests) + { + TestFixture fixture_; + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup({ + "--etl-test-file"s, R"(..\..\Tests\AuxData\Data\P00HeaWin2080.etl)"s + }); + } + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + // static store + TEST_METHOD(StaticData) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + // track known target + const uint32_t pid = 12820; + client.DispatchSync(svc::acts::StartTracking::Params{ .targetPid = pid, .isPlayback = true }); + + // open the store + pComms->OpenFrameDataStore(pid); + + // wait for population of frame data-initialized statics + std::this_thread::sleep_for(500ms); + + // verify static data + auto& store = pComms->GetFrameDataStore(pid); + Assert::AreEqual(pid, store.bookkeeping.processId); + const std::string staticAppName = store.statics.applicationName.c_str(); + Assert::AreEqual("Heaven.exe"s, staticAppName); + } + // make sure we get frames over time + TEST_METHOD(ReadFrames) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + // set up a fast flush + client.DispatchSync(svc::acts::SetEtwFlushPeriod::Params{ .etwFlushPeriodMs = 8 }); + // make sure the flush period propagates to the flusher thread + std::this_thread::sleep_for(1ms); + + // we know the pid of interest in this etl file, track it + const uint32_t pid = 12820; + client.DispatchSync(svc::acts::StartTracking::Params{ + .targetPid = pid, .isPlayback = true, .isBackpressured = true }); + + // open the store + pComms->OpenFrameDataStore(pid); + auto& ring = pComms->GetFrameDataStore(pid).frameData; + + // sleep here to let the etw system warm up, and frames propagate + std::this_thread::sleep_for(300ms); + + struct Row { uint64_t timestamp; uint64_t timeInPresent; }; + std::vector frames; + uint64_t lastProcessed = 0; + + const auto appendRange = [&](std::pair range) { + for (uint64_t s = (std::max)(lastProcessed, range.first); s < range.second; ++s) { + auto& p = ring.At(s); + frames.push_back(Row{ + .timestamp = p.presentStartTime + p.timeInPresent, + .timeInPresent = p.timeInPresent, + }); + } + lastProcessed = range.second; + }; + + const auto range1 = ring.GetSerialRange(); + ring.MarkNextRead(range1.second); + Logger::WriteMessage(std::format("range [{},{})\n", range1.first, range1.second).c_str()); + appendRange(range1); + + std::this_thread::sleep_for(300ms); + + const auto range2 = ring.GetSerialRange(); + ring.MarkNextRead(range2.second); + Logger::WriteMessage(std::format("range [{},{})\n", range2.first, range2.second).c_str()); + appendRange(range2); + + std::this_thread::sleep_for(500ms); + + const auto range3 = ring.GetSerialRange(); + ring.MarkNextRead(range3.second); + Logger::WriteMessage(std::format("range [{},{})\n", range3.first, range3.second).c_str()); + appendRange(range3); + + // output timestamp of each frame + const auto outpath = fs::path{ outFolder_ } / + std::format("broadcaster-frames-{:%Y%m%d-%H%M%S}.csv", std::chrono::system_clock::now()); + Logger::WriteMessage(std::format("Writing output to: {}\n", + fs::absolute(outpath).string()).c_str()); + + std::ofstream frameFile{ outpath }; + frameFile << "timestamp,timeInPresent\n"; + for (const auto& r : frames) { + frameFile << r.timestamp << ',' << r.timeInPresent << "\n"; + } + + Assert::AreEqual(0ull, range1.first); + Assert::IsTrue(range2.first <= range1.second); + Assert::IsTrue(range3.first <= range2.second); + // known issue with PresentData is that it sometimes outputs 24 rogue frames at + // the end for P00; we can ignore these for the time being, issue added to board + Assert::IsTrue(range3.second == 1905ull || range3.second == 1929ull); + } + }; + + TEST_CLASS(FrameStorePlaybackBackpressureWrapTests) + { + TestFixture fixture_; + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup({ + "--etl-test-file"s, R"(..\..\Tests\AuxData\Data\P01TimeSpyDemoFS2080.etl)"s, + "--frame-ring-samples"s, "32"s, + }); + } + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + TEST_METHOD(BackpressurePreventsMissingFrames) + { + mid::ActionClient client{ fixture_.GetCommonArgs().ctrlPipe }; + auto pComms = ipc::MakeMiddlewareComms(client.GetShmPrefix(), client.GetShmSalt()); + + client.DispatchSync(svc::acts::SetEtwFlushPeriod::Params{ .etwFlushPeriodMs = 8 }); + std::this_thread::sleep_for(1ms); + + const uint32_t pid = 19736; + client.DispatchSync(svc::acts::StartTracking::Params{ + .targetPid = pid, .isPlayback = true, .isBackpressured = true }); + + pComms->OpenFrameDataStore(pid); + auto& ring = pComms->GetFrameDataStore(pid).frameData; + + size_t lastProcessed = 0; + bool missed = false; + bool sawWrap = false; + + for (size_t i = 0; i < 10; ++i) { + std::this_thread::sleep_for(300ms); + const auto range = ring.GetSerialRange(); + Logger::WriteMessage(std::format( + "pb-wrap-backpressure: range [{}, {}), lastProcessed={}\n", + range.first, range.second, lastProcessed).c_str()); + if (range.first > 0) { + sawWrap = true; + } + if (range.first > lastProcessed) { + missed = true; + } + const size_t start = (std::max)(lastProcessed, range.first); + for (size_t s = start; s < range.second; ++s) { + (void)ring.At(s); + } + ring.MarkNextRead(range.second); + lastProcessed = range.second; + } + + Assert::IsTrue(sawWrap, L"Expected ring to wrap during playback"); + Assert::IsFalse(missed, L"Expected backpressure to prevent missing frames"); + Assert::IsTrue(lastProcessed > 0); + } + }; + + TEST_CLASS(FrameStoreBackpressuredPlayback3DMTests) + { + TestFixture fixture_; + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup({ + "--etl-test-file"s, R"(..\..\Tests\AuxData\Data\P01TimeSpyDemoFS2080.etl)"s + }); + } + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + TEST_METHOD(ReadFrames) + { + pmapi::Session session{ fixture_.GetCommonArgs().ctrlPipe }; + + // set up a fast flush + session.SetEtwFlushPeriod(8); + // make sure the flush period propagates to the flusher thread + std::this_thread::sleep_for(1ms); + + // setup query + PM_BEGIN_FIXED_FRAME_QUERY(FQ) + pmapi::FixedQueryElement timestamp{ this, PM_METRIC_CPU_START_QPC }; + pmapi::FixedQueryElement timeInPres{ this, PM_METRIC_IN_PRESENT_API }; + PM_END_FIXED_QUERY query{ session, 1'000 }; + + struct Row { uint64_t timestamp; double timeInPresent; }; + std::vector frames; + + // we know the pid of interest in this etl file, track it + const uint32_t pid = 19736; + auto tracker = query.TrackProcess(pid, true, true); + + // sleep here to let the etw system warm up, and frames propagate + std::this_thread::sleep_for(300ms); + + const auto consume = [&] { + query.ForEachConsume(tracker, [&] { + frames.push_back(Row{ + .timestamp = query.timestamp, + .timeInPresent = query.timeInPres, + }); + }); + }; + + // verify that backpressure works correctly to ensure no frames are lost + consume(); + const auto count1 = query.PeekBlobContainer().GetNumBlobsPopulated(); + Logger::WriteMessage(std::format("count [{}]\n", count1).c_str()); + + std::this_thread::sleep_for(300ms); + + consume(); + const auto count2 = query.PeekBlobContainer().GetNumBlobsPopulated(); + Logger::WriteMessage(std::format("count [{}]\n", count2).c_str()); + + std::this_thread::sleep_for(500ms); + + consume(); + const auto count3 = query.PeekBlobContainer().GetNumBlobsPopulated(); + Logger::WriteMessage(std::format("count [{}]\n", count3).c_str()); + + // output timestamp of each frame + const auto outpath = fs::path{ outFolder_ } / + std::format("legacy-frames-32m-{:%Y%m%d-%H%M%S}.csv", std::chrono::system_clock::now()); + Logger::WriteMessage(std::format("Writing output to: {}\n", + fs::absolute(outpath).string()).c_str()); + + std::ofstream frameFile{ outpath }; + frameFile << "timestamp,timeInPresent\n"; + for (const auto& r : frames) { + frameFile << r.timestamp << ',' << r.timeInPresent << "\n"; + } + + Assert::AreEqual(2037u, count1 + count2 + count3); + } + }; + + TEST_CLASS(LegacyBackpressuredPlaybackTests) + { + TestFixture fixture_; + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup({ + "--etl-test-file"s, R"(..\..\Tests\AuxData\Data\P00HeaWin2080.etl)"s, + }); + } + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + // make sure we get frames over time + TEST_METHOD(ReadFrames) + { + pmapi::Session session{ fixture_.GetCommonArgs().ctrlPipe }; + + // set up a fast flush + session.SetEtwFlushPeriod(8); + // make sure the flush period propagates to the flusher thread + std::this_thread::sleep_for(1ms); + + // setup query + PM_BEGIN_FIXED_FRAME_QUERY(FQ) + pmapi::FixedQueryElement timestamp{ this, PM_METRIC_CPU_START_QPC }; + pmapi::FixedQueryElement timeInPres{ this, PM_METRIC_IN_PRESENT_API }; + PM_END_FIXED_QUERY query{ session, 1'000 }; + + struct Row { uint64_t timestamp; double timeInPresent; }; + std::vector frames; + + // we know the pid of interest in this etl file, track it + const uint32_t pid = 12820; + auto tracker = query.TrackProcess(pid, true, true); + + // sleep here to let the etw system warm up, and frames propagate + std::this_thread::sleep_for(300ms); + + const auto consume = [&] { + query.ForEachConsume(tracker, [&] { + frames.push_back(Row{ + .timestamp = query.timestamp, + .timeInPresent = query.timeInPres, + }); + }); + }; + + // verify that backpressure works correctly to ensure no frames are lost + consume(); + const auto count1 = query.PeekBlobContainer().GetNumBlobsPopulated(); + Logger::WriteMessage(std::format("count [{}]\n", count1).c_str()); + + std::this_thread::sleep_for(300ms); + + consume(); + const auto count2 = query.PeekBlobContainer().GetNumBlobsPopulated(); + Logger::WriteMessage(std::format("count [{}]\n", count2).c_str()); + + std::this_thread::sleep_for(500ms); + + consume(); + const auto count3 = query.PeekBlobContainer().GetNumBlobsPopulated(); + Logger::WriteMessage(std::format("count [{}]\n", count3).c_str()); + + // output timestamp of each frame + const auto outpath = fs::path{ outFolder_ } / + std::format("legacy-frames-{:%Y%m%d-%H%M%S}.csv", std::chrono::system_clock::now()); + Logger::WriteMessage(std::format("Writing output to: {}\n", + fs::absolute(outpath).string()).c_str()); + + std::ofstream frameFile{ outpath }; + frameFile << "timestamp,timeInPresent\n"; + for (const auto& r : frames) { + frameFile << r.timestamp << ',' << r.timeInPresent << "\n"; + } + + const auto total = count1 + count2 + count3; + // known issue with PresentData is that it sometimes outputs 24 rogue frames at + // the end for P00; we can ignore these for the time being, issue added to board + Logger::WriteMessage(std::format("Total frames: {}\n", total).c_str()); + Assert::IsTrue(total == 1902u); + } + }; +} diff --git a/IntelPresentMon/PresentMonAPI2Tests/IpcComponentTests.cpp b/IntelPresentMon/PresentMonAPI2Tests/IpcComponentTests.cpp new file mode 100644 index 000000000..4c7db296a --- /dev/null +++ b/IntelPresentMon/PresentMonAPI2Tests/IpcComponentTests.cpp @@ -0,0 +1,525 @@ +// Copyright (C) 2022-2025 Intel Corporation +// SPDX-License-Identifier: MIT + +#include "../CommonUtilities/win/WinAPI.h" +#include "CppUnitTest.h" +#include "TestProcess.h" +#include "Folders.h" +#include "JobManager.h" + +#include "../Interprocess/source/ViewedDataSegment.h" +#include "../Interprocess/source/OwnedDataSegment.h" +#include "../Interprocess/source/DataStores.h" + +#include "../PresentMonAPI2/PresentMonAPI.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace std::literals; + +namespace IpcComponentTests +{ + namespace ipc = pmon::ipc; + + // Must match the server submode constant. + static constexpr const char* kSystemSegName = "pm_ipc_system_store_test_seg"; + + static constexpr PM_METRIC kScalarMetric = PM_METRIC_CPU_FREQUENCY; + static constexpr PM_METRIC kArrayMetric = PM_METRIC_CPU_UTILIZATION; + + // Expected test child patterns + static constexpr uint64_t kBaseTs = 10'000ull; + static constexpr size_t kSampleCount = 12; + + class TestFixture : public CommonTestFixture + { + protected: + const CommonProcessArgs& GetCommonArgs() const override + { + static CommonProcessArgs args{ + .ctrlPipe = R"(\\.\pipe\pm-ipc-sys-store-test-ctrl)", + .shmNamePrefix = "pm_ipc_sys_store_unused_prefix", + .logLevel = "debug", + .logFolder = logFolder_, + .sampleClientMode = "IpcComponentServer", + .suppressService = true, + }; + return args; + } + }; + + static std::string DumpRing_(const ipc::SampleHistoryRing& ring, size_t maxSamples = 8) + { + std::ostringstream oss; + const auto [first, last] = ring.GetSerialRange(); + const size_t count = last - first; + + oss << "serial range [" << first << ", " << last << "), count=" << count << "\n"; + + if (count == 0) { + return oss.str(); + } + + const size_t n = (count < maxSamples) ? count : maxSamples; + for (size_t i = 0; i < n; ++i) { + const auto& s = ring.At(first + i); + oss << " [" << (first + i) << "] ts=" << s.timestamp << " val=" << s.value << "\n"; + } + + if (count > n) { + oss << " ...\n"; + const auto& sLast = ring.At(last - 1); + oss << " [" << (last - 1) << "] ts=" << sLast.timestamp << " val=" << sLast.value << "\n"; + } + + // Try to include Newest() summary for debugging + try { + const auto& newest = ring.Newest(); + oss << "newest: ts=" << newest.timestamp << " val=" << newest.value << "\n"; + } + catch (...) { + // If Newest() throws on empty in some impls, ignore here. + } + + return oss.str(); + } + + static void LogRing_(const char* label, const ipc::SampleHistoryRing& ring) + { + std::ostringstream oss; + oss << label << "\n" << DumpRing_(ring); + Logger::WriteMessage(oss.str().c_str()); + } + + static double ExpectedScalarValue_(uint64_t timestamp) + { + const size_t i = static_cast(timestamp - kBaseTs); + return 3000.0 + 10.0 * static_cast(i); + } + + static double ExpectedArray0Value_(uint64_t timestamp) + { + const size_t i = static_cast(timestamp - kBaseTs); + return 5.0 + static_cast(i); + } + + static double ExpectedArray1Value_(uint64_t timestamp) + { + const size_t i = static_cast(timestamp - kBaseTs); + return 50.0 + 2.0 * static_cast(i); + } + + static void AssertScalarSampleMatches_(const ipc::SampleHistoryRing& ring, size_t serial) + { + const auto& sample = ring.At(serial); + const uint64_t expectedTs = kBaseTs + static_cast(serial); + Assert::AreEqual(expectedTs, sample.timestamp); + Assert::AreEqual(ExpectedScalarValue_(expectedTs), sample.value, 1e-9); + } + + TEST_CLASS(HistoryRingStoreBasicAccessTests) + { + TestFixture fixture_; + + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup(); + } + + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + + TEST_METHOD(RingsArePresentAndSized) + { + auto server = fixture_.LaunchClient(); + std::this_thread::sleep_for(25ms); + + ipc::ViewedDataSegment view{ kSystemSegName }; + const auto& store = view.GetStore(); + + const auto& scalarVect = store.telemetryData.FindRing(kScalarMetric); + const auto& arrayVect = store.telemetryData.FindRing(kArrayMetric); + + Logger::WriteMessage("Checking ring vector sizes...\n"); + + Assert::AreEqual(1, scalarVect.size(), L"Scalar metric should have 1 ring"); + Assert::AreEqual(2, arrayVect.size(), L"Array metric should have 2 rings"); + } + + TEST_METHOD(EmptyRangeAndNewestWorkForScalar) + { + auto server = fixture_.LaunchClient(); + std::this_thread::sleep_for(25ms); + + ipc::ViewedDataSegment view{ kSystemSegName }; + const auto& store = view.GetStore(); + + const auto& ring = store.telemetryData.FindRing(kScalarMetric).at(0); + + Logger::WriteMessage("Validating Empty/GetSerialRange/Newest for scalar ring\n"); + LogRing_("Scalar ring dump:", ring); + + Assert::IsFalse(ring.Empty(), L"Ring should not be empty after server push"); + + const auto [first, last] = ring.GetSerialRange(); + Assert::IsTrue(last >= first, L"Serial range should be valid"); + Assert::IsTrue((last - first) >= kSampleCount, L"Expected at least 12 samples"); + + const uint64_t expectedNewestTs = kBaseTs + static_cast(kSampleCount - 1); + + const auto& newest = ring.Newest(); + const auto& atLast = ring.At(last - 1); + + Assert::AreEqual(atLast.timestamp, newest.timestamp); + Assert::AreEqual(atLast.value, newest.value, 1e-9); + + Assert::AreEqual(expectedNewestTs, newest.timestamp); + Assert::AreEqual(ExpectedScalarValue_(expectedNewestTs), newest.value, 1e-9); + } + + TEST_METHOD(AtReadsExpectedValuesForArrayElements) + { + auto server = fixture_.LaunchClient(); + std::this_thread::sleep_for(25ms); + + ipc::ViewedDataSegment view{ kSystemSegName }; + const auto& store = view.GetStore(); + + const auto& arrVect = store.telemetryData.FindRing(kArrayMetric); + + const auto& ring0 = arrVect.at(0); + const auto& ring1 = arrVect.at(1); + + Logger::WriteMessage("Validating At() value mapping for array rings\n"); + LogRing_("Array ring[0] dump:", ring0); + LogRing_("Array ring[1] dump:", ring1); + + const auto [f0, l0] = ring0.GetSerialRange(); + const auto [f1, l1] = ring1.GetSerialRange(); + + Assert::IsTrue((l0 - f0) >= kSampleCount); + Assert::IsTrue((l1 - f1) >= kSampleCount); + + // Check a few specific timestamps + for (uint64_t ts : { kBaseTs, kBaseTs + 5, kBaseTs + 11 }) { + const size_t i = static_cast(ts - kBaseTs); + + const auto& s0 = ring0.At(f0 + i); + const auto& s1 = ring1.At(f1 + i); + + Assert::AreEqual(ts, s0.timestamp); + Assert::AreEqual(ts, s1.timestamp); + + Assert::AreEqual(ExpectedArray0Value_(ts), s0.value, 1e-9); + Assert::AreEqual(ExpectedArray1Value_(ts), s1.value, 1e-9); + } + } + + TEST_METHOD(LowerBoundSerialEdgeAndExactCases) + { + auto server = fixture_.LaunchClient(); + std::this_thread::sleep_for(25ms); + + ipc::ViewedDataSegment view{ kSystemSegName }; + const auto& store = view.GetStore(); + + const auto& ring = store.telemetryData.FindRing(kScalarMetric).at(0); + + Logger::WriteMessage("Validating LowerBoundSerial cases\n"); + + const auto [first, last] = ring.GetSerialRange(); + + // Before first timestamp -> should return first + { + const size_t s = ring.LowerBoundSerial(kBaseTs - 1); + Assert::AreEqual(first, s); + } + + // Exact timestamp match + { + const uint64_t ts = kBaseTs + 5; + const size_t s = ring.LowerBoundSerial(ts); + const auto& sample = ring.At(s); + + Assert::AreEqual(ts, sample.timestamp); + Assert::AreEqual(ExpectedScalarValue_(ts), sample.value, 1e-9); + } + + // After last timestamp -> should return last (one past end) + { + const size_t s = ring.LowerBoundSerial(kBaseTs + static_cast(kSampleCount)); + Assert::AreEqual(last, s); + } + } + + TEST_METHOD(UpperBoundSerialEdgeAndExactCases) + { + auto server = fixture_.LaunchClient(); + std::this_thread::sleep_for(25ms); + + ipc::ViewedDataSegment view{ kSystemSegName }; + const auto& store = view.GetStore(); + + const auto& ring = store.telemetryData.FindRing(kScalarMetric).at(0); + + Logger::WriteMessage("Validating UpperBoundSerial cases\n"); + + const auto [first, last] = ring.GetSerialRange(); + + // Before first timestamp -> should return first + { + const size_t s = ring.UpperBoundSerial(kBaseTs - 1); + Assert::AreEqual(first, s); + } + + // Upper bound of first sample timestamp -> should point to second sample + { + const size_t s = ring.UpperBoundSerial(kBaseTs); + Assert::IsTrue(s > first); + const auto& sample = ring.At(s); + Assert::AreEqual(kBaseTs + 1, sample.timestamp); + } + + // Upper bound of last sample timestamp -> should return last + { + const uint64_t lastTs = kBaseTs + static_cast(kSampleCount - 1); + const size_t s = ring.UpperBoundSerial(lastTs); + Assert::AreEqual(last, s); + } + } + + TEST_METHOD(NearestSerialClampsAndExact) + { + auto server = fixture_.LaunchClient(); + std::this_thread::sleep_for(25ms); + + ipc::ViewedDataSegment view{ kSystemSegName }; + const auto& store = view.GetStore(); + + const auto& ring = store.telemetryData.FindRing(kScalarMetric).at(0); + + Logger::WriteMessage("Validating NearestSerial cases\n"); + + const auto [first, last] = ring.GetSerialRange(); + + // Before first -> clamp to first + { + const size_t s = ring.NearestSerial(kBaseTs - 500); + Assert::AreEqual(first, s); + Assert::AreEqual(kBaseTs, ring.At(s).timestamp); + } + + // After last -> clamp to last-1 + { + const size_t s = ring.NearestSerial(kBaseTs + 500); + Assert::AreEqual(last - 1, s); + Assert::AreEqual(kBaseTs + static_cast(kSampleCount - 1), + ring.At(s).timestamp); + } + + // Exact timestamp -> should return that sample + { + const uint64_t ts = kBaseTs + 7; + const size_t s = ring.NearestSerial(ts); + const auto& sample = ring.At(s); + + Assert::AreEqual(ts, sample.timestamp); + Assert::AreEqual(ExpectedScalarValue_(ts), sample.value, 1e-9); + } + } + + TEST_METHOD(ForEachInTimestampRangeVisitsExpectedSamples) + { + auto server = fixture_.LaunchClient(); + std::this_thread::sleep_for(25ms); + + ipc::ViewedDataSegment view{ kSystemSegName }; + const auto& store = view.GetStore(); + + const auto& ring = store.telemetryData.FindRing(kScalarMetric).at(0); + + Logger::WriteMessage("Validating ForEachInTimestampRange\n"); + LogRing_("Scalar ring dump:", ring); + + const uint64_t start = kBaseTs + 3; + const uint64_t end = kBaseTs + 6; + + size_t visited = 0; + double sum = 0.0; + + const size_t count = ring.ForEachInTimestampRange(start, end, [&](const auto& s) { + ++visited; + sum += s.value; + }); + + // Timestamps are contiguous and inclusive + // Expected: 10003, 10004, 10005, 10006 -> 4 samples + Assert::AreEqual(4, count); + Assert::AreEqual(4, visited); + + const double expectedSum = + ExpectedScalarValue_(start) + + ExpectedScalarValue_(start + 1) + + ExpectedScalarValue_(start + 2) + + ExpectedScalarValue_(end); + + Logger::WriteMessage(std::format("ForEach visited={}, sum={}\n", visited, sum).c_str()); + + Assert::AreEqual(expectedSum, sum, 1e-9); + } + }; + + TEST_CLASS(HistoryRingStoreWrappingTests) + { + TestFixture fixture_; + + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup(); + } + + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + + TEST_METHOD(RingWrapNoMissingFrames) + { + constexpr size_t ringCapacity = 16; + constexpr size_t samplesPerPush = 10; + auto server = fixture_.LaunchClient({ + "--ipc-system-ring-capacity", std::to_string(ringCapacity), + "--ipc-system-samples-per-push", std::to_string(samplesPerPush), + }); + std::this_thread::sleep_for(25ms); + + ipc::ViewedDataSegment view{ kSystemSegName }; + const auto& store = view.GetStore(); + const auto& ring = store.telemetryData.FindRing(kScalarMetric).at(0); + + size_t lastProcessed = 0; + size_t totalPushed = samplesPerPush; + + const auto consumeRange = [&](const std::pair& range) { + const size_t start = (std::max)(lastProcessed, range.first); + for (size_t serial = start; serial < range.second; ++serial) { + AssertScalarSampleMatches_(ring, serial); + } + lastProcessed = range.second; + }; + + const auto range1 = ring.GetSerialRange(); + Logger::WriteMessage(std::format("wrap-no-miss: range1 [{}, {})\n", + range1.first, range1.second).c_str()); + consumeRange(range1); + Assert::AreEqual(0, range1.first); + Assert::AreEqual(samplesPerPush, range1.second); + + Assert::AreEqual("push-more-ok"s, server.Command("push-more")); + totalPushed += samplesPerPush; + + const auto range2 = ring.GetSerialRange(); + Logger::WriteMessage(std::format("wrap-no-miss: range2 [{}, {}), lastProcessed={}\n", + range2.first, range2.second, lastProcessed).c_str()); + Assert::IsTrue(range2.first > 0); + Assert::IsTrue(range2.first <= lastProcessed); + consumeRange(range2); + + Logger::WriteMessage(std::format("wrap-no-miss: processed={}, newest-ts={}\n", + lastProcessed, ring.Newest().timestamp).c_str()); + Assert::AreEqual(totalPushed, lastProcessed); + const auto& newest = ring.Newest(); + Assert::AreEqual(kBaseTs + static_cast(totalPushed - 1), + newest.timestamp); + } + + TEST_METHOD(RingWrapMissingFrames) + { + constexpr size_t ringCapacity = 16; + constexpr size_t samplesPerPush = 10; + auto server = fixture_.LaunchClient({ + "--ipc-system-ring-capacity", std::to_string(ringCapacity), + "--ipc-system-samples-per-push", std::to_string(samplesPerPush), + }); + std::this_thread::sleep_for(25ms); + + ipc::ViewedDataSegment view{ kSystemSegName }; + const auto& store = view.GetStore(); + const auto& ring = store.telemetryData.FindRing(kScalarMetric).at(0); + + Assert::AreEqual("push-more-ok"s, server.Command("push-more")); + const size_t totalPushed = samplesPerPush * 2; + + const auto range = ring.GetSerialRange(); + Logger::WriteMessage(std::format("wrap-miss: range [{}, {}), totalPushed={}\n", + range.first, range.second, totalPushed).c_str()); + Assert::AreEqual(totalPushed, range.second); + Assert::IsTrue(range.first > 0); + + const size_t missed = range.first; + Logger::WriteMessage(std::format("wrap-miss: missed={} samples\n", missed).c_str()); + Assert::IsTrue(missed > 0); + + const auto& firstSample = ring.At(range.first); + Assert::AreEqual(kBaseTs + static_cast(range.first), + firstSample.timestamp); + + for (size_t serial = range.first; serial < range.second; ++serial) { + AssertScalarSampleMatches_(ring, serial); + } + + Logger::WriteMessage(std::format("wrap-miss: newest-ts={}\n", + ring.Newest().timestamp).c_str()); + const auto& newest = ring.Newest(); + Assert::AreEqual(kBaseTs + static_cast(totalPushed - 1), + newest.timestamp); + } + + TEST_METHOD(RingBackpressureBlocksAndResumes) + { + constexpr size_t ringSamples = 8; + ipc::DataStoreSizingInfo sizing{}; + sizing.ringSamples = ringSamples; + sizing.backpressured = true; + sizing.overrideBytes = 256 * 1024; + + const auto segName = std::format("pm_ipc_backpressure_test_seg_{}", + static_cast(::GetCurrentProcessId())); + ipc::OwnedDataSegment seg{ segName, sizing }; + auto& ring = seg.GetStore().frameData; + + ipc::FrameData sample{}; + size_t pushed = 0; + bool sawTimeout = false; + for (size_t i = 0; i < ringSamples + 4; ++i) { + if (!ring.Push(sample, 30)) { + sawTimeout = true; + break; + } + ++pushed; + } + + Assert::IsTrue(sawTimeout, L"Expected backpressure to block writes when full"); + const auto rangeBefore = ring.GetSerialRange(); + Assert::AreEqual(pushed, rangeBefore.second); + Assert::AreEqual(0, rangeBefore.first); + + ring.MarkNextRead(rangeBefore.second); + + Assert::IsTrue(ring.Push(sample, 30), L"Expected push after MarkNextRead"); + const auto rangeAfter = ring.GetSerialRange(); + Assert::AreEqual(pushed + 1, rangeAfter.second); + } + }; +} diff --git a/IntelPresentMon/PresentMonAPI2Tests/IpcMcIntegrationTests.cpp b/IntelPresentMon/PresentMonAPI2Tests/IpcMcIntegrationTests.cpp new file mode 100644 index 000000000..2394bff22 --- /dev/null +++ b/IntelPresentMon/PresentMonAPI2Tests/IpcMcIntegrationTests.cpp @@ -0,0 +1,414 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#include "../CommonUtilities/win/WinAPI.h" +#include "CppUnitTest.h" +#include "TestProcess.h" +#include "Folders.h" + +#include "../PresentMonAPIWrapper/PresentMonAPIWrapper.h" +#include "../Interprocess/source/SystemDeviceId.h" +#include "../CommonUtilities/Meta.h" +#include "../CommonUtilities/mc/FrameMetricsMemberMap.h" + +#include +#include +#include +#include +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace std::chrono_literals; + +namespace IpcMcIntegrationTests +{ + namespace ipc = pmon::ipc; + namespace util = pmon::util; + + class TestFixture : public CommonTestFixture + { + public: + const CommonProcessArgs& GetCommonArgs() const override + { + static CommonProcessArgs args{ + .ctrlPipe = R"(\\.\pipe\pm-ipcmc-int-ctrl)", + .shmNamePrefix = "pm_ipcmc_int", + .logLevel = "verbose", + .logVerboseModules = "ipc_sto met_use", + .logFolder = logFolder_, + .sampleClientMode = "NONE", + }; + return args; + } + }; + + static std::vector BuildUniversalFrameQueryElements_(const pmapi::intro::Root& intro) + { + std::vector elements; + for (auto metric : intro.GetMetrics()) { + if (!pmapi::intro::MetricTypeIsFrameEvent(metric.GetType())) { + continue; + } + for (auto info : metric.GetDeviceMetricInfo()) { + if (info.GetDevice().GetId() != ipc::kUniversalDeviceId) { + continue; + } + if (!info.IsAvailable()) { + break; + } + const uint32_t arraySize = info.GetArraySize(); + for (uint32_t index = 0; index < arraySize; ++index) { + elements.push_back(PM_QUERY_ELEMENT{ + .metric = metric.GetId(), + .stat = PM_STAT_NONE, + .deviceId = ipc::kUniversalDeviceId, + .arrayIndex = index, + .dataOffset = 0, + .dataSize = 0, + }); + } + break; + } + } + return elements; + } + + static std::optional FindFirstGpuDeviceId_(const pmapi::intro::Root& intro) + { + for (auto device : intro.GetDevices()) { + if (device.GetType() == PM_DEVICE_TYPE_GRAPHICS_ADAPTER) { + return device.GetId(); + } + } + return std::nullopt; + } + + static std::string GetVendorName_(const pmapi::intro::Root& intro, PM_DEVICE_VENDOR vendor) + { + return intro.FindEnumKey(PM_ENUM_DEVICE_VENDOR, (int)vendor).GetName(); + } + + static std::vector BuildStaticFrameQueryElements_(uint32_t gpuDeviceId) + { + return { + PM_QUERY_ELEMENT{ + .metric = PM_METRIC_CPU_NAME, + .stat = PM_STAT_NONE, + .deviceId = ipc::kSystemDeviceId, + .arrayIndex = 0, + .dataOffset = 0, + .dataSize = 0, + }, + PM_QUERY_ELEMENT{ + .metric = PM_METRIC_CPU_VENDOR, + .stat = PM_STAT_NONE, + .deviceId = ipc::kSystemDeviceId, + .arrayIndex = 0, + .dataOffset = 0, + .dataSize = 0, + }, + PM_QUERY_ELEMENT{ + .metric = PM_METRIC_GPU_NAME, + .stat = PM_STAT_NONE, + .deviceId = gpuDeviceId, + .arrayIndex = 0, + .dataOffset = 0, + .dataSize = 0, + }, + PM_QUERY_ELEMENT{ + .metric = PM_METRIC_GPU_VENDOR, + .stat = PM_STAT_NONE, + .deviceId = gpuDeviceId, + .arrayIndex = 0, + .dataOffset = 0, + .dataSize = 0, + }, + PM_QUERY_ELEMENT{ + .metric = PM_METRIC_GPU_MEM_SIZE, + .stat = PM_STAT_NONE, + .deviceId = gpuDeviceId, + .arrayIndex = 0, + .dataOffset = 0, + .dataSize = 0, + }, + PM_QUERY_ELEMENT{ + .metric = PM_METRIC_APPLICATION, + .stat = PM_STAT_NONE, + .deviceId = ipc::kUniversalDeviceId, + .arrayIndex = 0, + .dataOffset = 0, + .dataSize = 0, + }, + }; + } + + static const PM_QUERY_ELEMENT* FindQueryElement_(const std::vector& elements, PM_METRIC metric, uint32_t deviceId) + { + for (const auto& element : elements) { + if (element.metric == metric && element.deviceId == deviceId) { + return &element; + } + } + return nullptr; + } + + static std::string FormatQueryValue_(const pmapi::intro::Root& intro, const PM_QUERY_ELEMENT& element, const uint8_t* pBlob) + { + const auto metricView = intro.FindMetric(element.metric); + const auto dataType = metricView.GetDataTypeInfo().GetFrameType(); + const uint8_t* pData = pBlob + (size_t)element.dataOffset; + + switch (dataType) { + case PM_DATA_TYPE_UINT64: + return std::format("{}", *reinterpret_cast(pData)); + case PM_DATA_TYPE_INT32: + return std::format("{}", *reinterpret_cast(pData)); + case PM_DATA_TYPE_UINT32: + return std::format("{}", *reinterpret_cast(pData)); + case PM_DATA_TYPE_DOUBLE: + return std::format("{:.6f}", *reinterpret_cast(pData)); + case PM_DATA_TYPE_ENUM: + { + const int enumValue = *reinterpret_cast(pData); + std::string enumName = "Unknown"; + try { + enumName = intro.FindEnumKey(metricView.GetDataTypeInfo().GetEnumId(), enumValue).GetName(); + } + catch (...) { + } + return std::format("{} ({})", enumName, enumValue); + } + case PM_DATA_TYPE_BOOL: + return *reinterpret_cast(pData) ? "true" : "false"; + case PM_DATA_TYPE_STRING: + return std::string(reinterpret_cast(pData)); + case PM_DATA_TYPE_VOID: + default: + return "void"; + } + } + + static void LogFrameQueryResults_(const pmapi::intro::Root& intro, const std::vector& elements, const uint8_t* pBlob) + { + for (const auto& element : elements) { + const auto metricView = intro.FindMetric(element.metric); + const auto value = FormatQueryValue_(intro, element, pBlob); + Logger::WriteMessage(std::format("{}, {}\n", + metricView.Introspect().GetSymbol(), + value).c_str()); + } + } + + TEST_CLASS(IpcMcIntegrationTests) + { + TestFixture fixture_; + + public: + TEST_METHOD_INITIALIZE(Setup) + { + fixture_.Setup(); + } + + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + + TEST_METHOD(UniversalFrameQueryConsumesPresenterFrames) + { + pmapi::Session session{ fixture_.GetCommonArgs().ctrlPipe }; + auto intro = session.GetIntrospectionRoot(); + Assert::IsTrue((bool)intro); + + auto elements = BuildUniversalFrameQueryElements_(*intro); + Logger::WriteMessage(std::format("Universal frame metrics: {}\n", elements.size()).c_str()); + Assert::IsTrue(!elements.empty(), L"No universal frame metrics found"); + + auto query = session.RegisterFrameQuery(elements); + + auto presenter = fixture_.LaunchPresenter(); + session.SetEtwFlushPeriod(8); + std::this_thread::sleep_for(1ms); + auto tracker = session.TrackProcess(presenter.GetId()); + + auto blobs = query.MakeBlobContainer(16); + bool gotFrames = false; + const auto deadline = std::chrono::steady_clock::now() + 2s; + while (std::chrono::steady_clock::now() < deadline) { + query.Consume(tracker, blobs); + if (blobs.GetNumBlobsPopulated() > 0) { + gotFrames = true; + Logger::WriteMessage("Universal frame query results:\n"); + LogFrameQueryResults_(*intro, elements, blobs[0]); + break; + } + std::this_thread::sleep_for(25ms); + } + Assert::IsTrue(gotFrames, L"Expected frame query to consume frames"); + } + + TEST_METHOD(FrameQueryStaticMetricsAreFilled) + { + pmapi::Session session{ fixture_.GetCommonArgs().ctrlPipe }; + auto intro = session.GetIntrospectionRoot(); + Assert::IsTrue((bool)intro); + + const auto gpuDeviceId = FindFirstGpuDeviceId_(*intro); + Assert::IsTrue(gpuDeviceId.has_value(), L"No GPU device found"); + + auto elements = BuildUniversalFrameQueryElements_(*intro); + auto staticElements = BuildStaticFrameQueryElements_(*gpuDeviceId); + elements.insert(elements.end(), staticElements.begin(), staticElements.end()); + Logger::WriteMessage(std::format("Frame query metrics (with statics): {}\n", elements.size()).c_str()); + + auto query = session.RegisterFrameQuery(elements); + + auto presenter = fixture_.LaunchPresenter(); + session.SetEtwFlushPeriod(8); + std::this_thread::sleep_for(1ms); + auto tracker = session.TrackProcess(presenter.GetId()); + + auto blobs = query.MakeBlobContainer(16); + bool gotFrames = false; + const auto deadline = std::chrono::steady_clock::now() + 2s; + while (std::chrono::steady_clock::now() < deadline) { + query.Consume(tracker, blobs); + if (blobs.GetNumBlobsPopulated() > 0) { + gotFrames = true; + Logger::WriteMessage("Frame query results with static metrics:\n"); + LogFrameQueryResults_(*intro, elements, blobs[0]); + break; + } + std::this_thread::sleep_for(25ms); + } + Assert::IsTrue(gotFrames, L"Expected frame query to consume frames"); + + const auto* cpuNameElement = FindQueryElement_(elements, PM_METRIC_CPU_NAME, ipc::kSystemDeviceId); + Assert::IsTrue(cpuNameElement != nullptr, L"CPU name element missing"); + const auto* cpuVendorElement = FindQueryElement_(elements, PM_METRIC_CPU_VENDOR, ipc::kSystemDeviceId); + Assert::IsTrue(cpuVendorElement != nullptr, L"CPU vendor element missing"); + const auto* gpuNameElement = FindQueryElement_(elements, PM_METRIC_GPU_NAME, *gpuDeviceId); + Assert::IsTrue(gpuNameElement != nullptr, L"GPU name element missing"); + const auto* gpuVendorElement = FindQueryElement_(elements, PM_METRIC_GPU_VENDOR, *gpuDeviceId); + Assert::IsTrue(gpuVendorElement != nullptr, L"GPU vendor element missing"); + const auto* gpuMemSizeElement = FindQueryElement_(elements, PM_METRIC_GPU_MEM_SIZE, *gpuDeviceId); + Assert::IsTrue(gpuMemSizeElement != nullptr, L"GPU memory size element missing"); + const auto* appNameElement = FindQueryElement_(elements, PM_METRIC_APPLICATION, ipc::kUniversalDeviceId); + Assert::IsTrue(appNameElement != nullptr, L"Application element missing"); + + const uint8_t* firstBlob = blobs[0]; + const auto cpuName = std::string(reinterpret_cast(firstBlob + (size_t)cpuNameElement->dataOffset)); + const int cpuVendorValue = *reinterpret_cast(firstBlob + (size_t)cpuVendorElement->dataOffset); + const auto cpuVendorName = GetVendorName_(*intro, (PM_DEVICE_VENDOR)cpuVendorValue); + const auto gpuName = std::string(reinterpret_cast(firstBlob + (size_t)gpuNameElement->dataOffset)); + const int gpuVendorValue = *reinterpret_cast(firstBlob + (size_t)gpuVendorElement->dataOffset); + const auto gpuVendorName = GetVendorName_(*intro, (PM_DEVICE_VENDOR)gpuVendorValue); + const auto gpuMemSize = *reinterpret_cast(firstBlob + (size_t)gpuMemSizeElement->dataOffset); + const auto appName = std::string(reinterpret_cast(firstBlob + (size_t)appNameElement->dataOffset)); + + Assert::IsTrue(!cpuName.empty(), L"CPU name empty"); + Assert::IsTrue(!cpuVendorName.empty(), L"CPU vendor name empty"); + Assert::IsTrue(!gpuName.empty(), L"GPU name empty"); + Assert::IsTrue(!gpuVendorName.empty(), L"GPU vendor name empty"); + Assert::IsTrue(gpuMemSize > 0, L"GPU memory size not available"); + Assert::IsTrue(appName == "PresentBench.exe", L"Unexpected application name"); + } + + TEST_METHOD(StaticQueryReturnsExpectedValues) + { + pmapi::Session session{ fixture_.GetCommonArgs().ctrlPipe }; + auto intro = session.GetIntrospectionRoot(); + Assert::IsTrue((bool)intro); + + const auto gpuDeviceId = FindFirstGpuDeviceId_(*intro); + Assert::IsTrue(gpuDeviceId.has_value(), L"No GPU device found"); + + auto presenter = fixture_.LaunchPresenter(); + auto tracker = session.TrackProcess(presenter.GetId()); + + const auto cpuName = pmapi::PollStatic(session, tracker, PM_METRIC_CPU_NAME, ipc::kSystemDeviceId) + .As(); + Logger::WriteMessage(std::format("CPU name: {}\n", cpuName).c_str()); + Assert::IsTrue(!cpuName.empty(), L"CPU name empty"); + + const auto cpuVendor = pmapi::PollStatic(session, tracker, PM_METRIC_CPU_VENDOR, ipc::kSystemDeviceId) + .As(); + const auto cpuVendorName = GetVendorName_(*intro, cpuVendor); + Logger::WriteMessage(std::format("CPU vendor: {}\n", cpuVendorName).c_str()); + Assert::IsTrue(!cpuVendorName.empty(), L"CPU vendor name empty"); + + const auto gpuName = pmapi::PollStatic(session, tracker, PM_METRIC_GPU_NAME, *gpuDeviceId) + .As(); + const auto gpuVendor = pmapi::PollStatic(session, tracker, PM_METRIC_GPU_VENDOR, *gpuDeviceId) + .As(); + const auto gpuMemSize = pmapi::PollStatic(session, tracker, PM_METRIC_GPU_MEM_SIZE, *gpuDeviceId) + .As(); + Logger::WriteMessage(std::format("GPU name: {}\n", gpuName).c_str()); + Assert::IsTrue(!gpuName.empty(), L"GPU name empty"); + const auto gpuVendorName = GetVendorName_(*intro, gpuVendor); + Logger::WriteMessage(std::format("GPU vendor: {}\n", gpuVendorName).c_str()); + Assert::IsTrue(!gpuVendorName.empty(), L"GPU vendor name empty"); + Logger::WriteMessage(std::format("GPU memory size: {}\n", gpuMemSize).c_str()); + Assert::IsTrue(gpuMemSize > 0, L"GPU memory size not available"); + + const auto appName = pmapi::PollStatic(session, tracker, PM_METRIC_APPLICATION).As(); + Logger::WriteMessage(std::format("Application name: {}\n", appName).c_str()); + Assert::IsTrue(appName == "PresentBench.exe", L"Unexpected application name"); + } + + TEST_METHOD(UniversalNonStaticMetricsMapToFrameMetrics) + { + pmapi::Session session{ fixture_.GetCommonArgs().ctrlPipe }; + auto intro = session.GetIntrospectionRoot(); + Assert::IsTrue((bool)intro); + + std::vector metricsToCheck; + for (auto metricView : intro->GetMetrics()) { + if (metricView.GetType() == PM_METRIC_TYPE_STATIC) { + continue; + } + + bool hasUniversalDevice = false; + for (auto info : metricView.GetDeviceMetricInfo()) { + if (info.GetDevice().GetId() == ipc::kUniversalDeviceId) { + hasUniversalDevice = true; + break; + } + } + + if (hasUniversalDevice) { + metricsToCheck.push_back(metricView.GetId()); + } + } + + Logger::WriteMessage(std::format("Universal non-static metrics to map: {}\n", metricsToCheck.size()).c_str()); + Assert::IsTrue(!metricsToCheck.empty(), L"No universal non-static metrics found"); + + size_t failedMappings = 0; + for (auto metricId : metricsToCheck) { + const auto metricView = intro->FindMetric(metricId); + const auto symbol = metricView.Introspect().GetSymbol(); + const bool mapped = util::DispatchEnumValue( + metricId, + [&]() -> bool { + if constexpr (util::metrics::HasFrameMetricMember) { + constexpr auto memberPtr = util::metrics::FrameMetricMember::member; + using MemberInfo = util::MemberPointerInfo; + return std::is_same_v; + } + return false; + }, + false); + Logger::WriteMessage(std::format("Metric {}: {}\n", symbol, mapped ? "ok" : "*FAIL !! MISSING").c_str()); + if (!mapped) { + ++failedMappings; + } + } + + if (failedMappings > 0) { + Logger::WriteMessage(std::format("Unmapped universal non-static metrics: {}\n", failedMappings).c_str()); + } + + Assert::IsTrue(failedMappings == 0, L"FrameMetricsMemberMap missing universal non-static metrics"); + } + }; +} diff --git a/IntelPresentMon/PresentMonAPI2Tests/Logging.cpp b/IntelPresentMon/PresentMonAPI2Tests/Logging.cpp index 4b29c5ecc..7614d2be8 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/Logging.cpp +++ b/IntelPresentMon/PresentMonAPI2Tests/Logging.cpp @@ -1,11 +1,205 @@ +#include "Logging.h" + +#include "../CommonUtilities/log/Log.h" +#include "../CommonUtilities/log/Channel.h" +#include "../CommonUtilities/log/MsvcDebugDriver.h" +#include "../CommonUtilities/log/BasicFileDriver.h" +#include "../CommonUtilities/log/TextFormatter.h" +#include "../CommonUtilities/log/SimpleFileStrategy.h" +#include "../CommonUtilities/log/LinePolicy.h" +#include "../CommonUtilities/log/ErrorCodeResolvePolicy.h" +#include "../CommonUtilities/log/ErrorCodeResolver.h" +#include "../CommonUtilities/log/IdentificationTable.h" +#include "../CommonUtilities/log/Verbose.h" +#include "../CommonUtilities/str/String.h" +#include "../CommonUtilities/win/HrErrorCodeProvider.h" +#include "../CommonUtilities/win/WinAPI.h" +#include "../CommonUtilities/Exception.h" +#include "../PresentMonAPIWrapperCommon/PmErrorCodeProvider.h" +#include "../PresentMonAPI2/Internal.h" + +#include +#include +#include #include +#include namespace pmon::util::log { + namespace + { + std::shared_ptr MakeChannel_() + { + // channel (use custom deleter to ensure deletion in this module's heap) + auto pChannel = std::shared_ptr{ new Channel{}, [](Channel* p) { delete p; } }; + // error resolver + auto pErrorResolver = std::make_shared(); + pErrorResolver->AddProvider(std::make_unique()); + pErrorResolver->AddProvider(std::make_unique()); + // error resolving policy + auto pErrPolicy = std::make_shared(); + pErrPolicy->SetResolver(std::move(pErrorResolver)); + pChannel->AttachComponent(std::move(pErrPolicy)); + // make and add the line-tracking policy + pChannel->AttachComponent(std::make_shared()); + // attach debugger output by default + const auto pFormatter = std::make_shared(); + pChannel->AttachComponent(std::make_shared(pFormatter), "drv:dbg"); + return pChannel; + } + } + // this is injected into to util::log namespace and hooks into that system - // not using logging yet in the tests so just return empty pointer - std::shared_ptr GetDefaultChannel() noexcept + std::shared_ptr GetDefaultChannel() noexcept + { + return GetDefaultChannelWithFactory(MakeChannel_); + } +} + +namespace pmon::test +{ + namespace + { + struct LogLinkState + { + std::mutex mtx; + bool linked = false; + LoggingSingletons getters{}; + }; + + LogLinkState& GetLogLinkState_() + { + static LogLinkState state; + return state; + } + + util::log::Level ParseLogLevel_(const std::string& logLevel) + { + if (logLevel.empty()) { + return util::log::Level::Debug; + } + const auto levelMap = util::log::GetLevelMapNarrow(); + const auto key = util::str::ToLower(logLevel); + if (auto it = levelMap.find(key); it != levelMap.end()) { + return it->second; + } + return util::log::Level::Debug; + } + + std::string BuildLogFileName_(const std::filesystem::path& folder) + { + return std::format("test-harness-{}.txt", GetCurrentProcessId()); + } + + std::vector SplitVerboseModules_(const std::string& raw) + { + std::vector tokens; + std::string token; + for (unsigned char ch : raw) { + if (ch == ',' || std::isspace(ch)) { + if (!token.empty()) { + tokens.push_back(token); + token.clear(); + } + continue; + } + token.push_back(static_cast(ch)); + } + if (!token.empty()) { + tokens.push_back(token); + } + return tokens; + } + + std::vector ParseVerboseModules_(const std::string& raw) + { + std::vector modules; + if (raw.empty()) { + return modules; + } + const auto map = util::log::GetVerboseModuleMapNarrow(); + for (const auto& token : SplitVerboseModules_(raw)) { + const auto key = util::str::ToLower(token); + if (auto it = map.find(key); it != map.end()) { + modules.push_back(it->second); + } + } + return modules; + } + } + + void SetupTestLogging(const std::string& logFolder, + const std::string& logLevel, + const std::optional& logVerboseModules) noexcept + { + try { + util::log::IdentificationTable::AddThisProcess("ms-test"); + util::log::IdentificationTable::AddThisThread("exec"); + + const auto verboseModules = + logVerboseModules ? ParseVerboseModules_(*logVerboseModules) : std::vector{}; + auto pChannel = util::log::GetDefaultChannel(); + if (!pChannel) { + return; + } + + const auto level = ParseLogLevel_(logLevel); + auto& policy = util::log::GlobalPolicy::Get(); + policy.SetLogLevel(level); + policy.SetTraceLevel(util::log::Level::Error); + policy.SetExceptionTrace(false); + for (auto mod : verboseModules) { + policy.ActivateVerboseModule(mod); + } + + if (!logFolder.empty()) { + std::filesystem::path folderPath{ logFolder }; + std::error_code ec; + std::filesystem::create_directories(folderPath, ec); + const auto filePath = folderPath / BuildLogFileName_(folderPath); + auto pFormatter = std::make_shared(); + pChannel->AttachComponent(std::make_shared( + pFormatter, + std::make_shared(std::move(filePath))), + "drv:file"); + } + + auto& linkState = GetLogLinkState_(); + LoggingSingletons gettersCopy{}; + { + std::lock_guard lock{ linkState.mtx }; + if (!linkState.linked) { + linkState.getters = pmLinkLogging_(pChannel, []() -> util::log::IdentificationTable& { + return util::log::IdentificationTable::Get_(); + }); + linkState.linked = true; + } + gettersCopy = linkState.getters; + } + + if (gettersCopy) { + auto& dllPolicy = gettersCopy.getGlobalPolicy(); + dllPolicy.SetLogLevel(level); + dllPolicy.SetTraceLevel(util::log::Level::Error); + dllPolicy.SetExceptionTrace(false); + for (auto mod : verboseModules) { + dllPolicy.ActivateVerboseModule(mod); + } + } + } + catch (...) { + } + } + + LogChannelManager::LogChannelManager() noexcept + { + util::InstallSehTranslator(); + util::log::BootDefaultChannelEager(); + } + + LogChannelManager::~LogChannelManager() { - return {}; + pmFlushEntryPoint_(); + util::log::FlushEntryPoint(); } -} \ No newline at end of file +} diff --git a/IntelPresentMon/PresentMonAPI2Tests/Logging.h b/IntelPresentMon/PresentMonAPI2Tests/Logging.h new file mode 100644 index 000000000..ac7ab98d9 --- /dev/null +++ b/IntelPresentMon/PresentMonAPI2Tests/Logging.h @@ -0,0 +1,16 @@ +#pragma once +#include +#include + +namespace pmon::test +{ + void SetupTestLogging(const std::string& logFolder, + const std::string& logLevel, + const std::optional& logVerboseModules) noexcept; + + struct LogChannelManager + { + LogChannelManager() noexcept; + ~LogChannelManager(); + }; +} diff --git a/IntelPresentMon/PresentMonAPI2Tests/ModuleInit.cpp b/IntelPresentMon/PresentMonAPI2Tests/ModuleInit.cpp index 2aa0045f2..4999900b7 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/ModuleInit.cpp +++ b/IntelPresentMon/PresentMonAPI2Tests/ModuleInit.cpp @@ -1,4 +1,4 @@ -#include "../CommonUtilities/win/WinAPI.h" +#include "../CommonUtilities/win/WinAPI.h" #include "CppUnitTest.h" #include #include "Folders.h" @@ -34,4 +34,9 @@ TEST_MODULE_INITIALIZE(Api2TestModuleInit) WipeAndRecreate(EtlLoggerTests::outFolder_); WipeAndRecreate(PacedPolling::logFolder_); WipeAndRecreate(PacedPolling::outFolder_); -} \ No newline at end of file + WipeAndRecreate(PacedFrame::logFolder_); + WipeAndRecreate(PacedFrame::outFolder_); + WipeAndRecreate(InterimBroadcasterTests::logFolder_); + WipeAndRecreate(InterimBroadcasterTests::outFolder_); + WipeAndRecreate(IpcMcIntegrationTests::logFolder_); +} diff --git a/IntelPresentMon/PresentMonAPI2Tests/MultiClientTests.cpp b/IntelPresentMon/PresentMonAPI2Tests/MultiClientTests.cpp index b47d6e6e2..418a240c6 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/MultiClientTests.cpp +++ b/IntelPresentMon/PresentMonAPI2Tests/MultiClientTests.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2023 Intel Corporation // SPDX-License-Identifier: MIT #include "../CommonUtilities/win/WinAPI.h" #include "CppUnitTest.h" @@ -24,8 +24,7 @@ namespace MultiClientTests { static CommonProcessArgs args{ .ctrlPipe = R"(\\.\pipe\pm-multi-test-ctrl)", - .introNsm = "pm_multi_test_intro", - .frameNsm = "pm_multi_test_nsm", + .shmNamePrefix = "pm_multi_test_intro", .logLevel = "debug", .logFolder = logFolder_, .sampleClientMode = "MultiClient", @@ -52,7 +51,8 @@ namespace MultiClientTests { // verify initial status const auto status = fixture_.service->QueryStatus(); - Assert::AreEqual(0ull, status.nsmStreamedPids.size()); + Assert::AreEqual(0ull, status.trackedPids.size()); + Assert::AreEqual(0ull, status.frameStorePids.size()); Assert::AreEqual(16u, status.telemetryPeriodMs); Assert::IsTrue((bool)status.etwFlushPeriodMs); Assert::AreEqual(1000u, *status.etwFlushPeriodMs); @@ -476,21 +476,24 @@ namespace MultiClientTests // verify tracking status at service { const auto status = fixture_.service->QueryStatus(); - Assert::AreEqual(1ull, status.nsmStreamedPids.size()); + Assert::AreEqual(1ull, status.trackedPids.size()); + Assert::AreEqual(1ull, status.frameStorePids.size()); } // one client quits client1.Quit(); // verify tracking status at service { const auto status = fixture_.service->QueryStatus(); - Assert::AreEqual(1ull, status.nsmStreamedPids.size()); + Assert::AreEqual(1ull, status.trackedPids.size()); + Assert::AreEqual(1ull, status.frameStorePids.size()); } // other client quits client2.Quit(); // verify tracking stopped at service { const auto status = fixture_.service->QueryStatus(); - Assert::AreEqual(0ull, status.nsmStreamedPids.size()); + Assert::AreEqual(0ull, status.trackedPids.size()); + Assert::AreEqual(0ull, status.frameStorePids.size()); } } // verify process untrack (stream stop) when clients die suddenly @@ -509,7 +512,8 @@ namespace MultiClientTests // verify tracking status at service { const auto status = fixture_.service->QueryStatus(); - Assert::AreEqual(1ull, status.nsmStreamedPids.size()); + Assert::AreEqual(1ull, status.trackedPids.size()); + Assert::AreEqual(1ull, status.frameStorePids.size()); } // one client dies client1.Murder(); @@ -517,7 +521,8 @@ namespace MultiClientTests // verify tracking status at service { const auto status = fixture_.service->QueryStatus(); - Assert::AreEqual(1ull, status.nsmStreamedPids.size()); + Assert::AreEqual(1ull, status.trackedPids.size()); + Assert::AreEqual(1ull, status.frameStorePids.size()); } // other client dies client2.Murder(); @@ -525,7 +530,8 @@ namespace MultiClientTests // verify tracking stopped at service { const auto status = fixture_.service->QueryStatus(); - Assert::AreEqual(0ull, status.nsmStreamedPids.size()); + Assert::AreEqual(0ull, status.trackedPids.size()); + Assert::AreEqual(0ull, status.frameStorePids.size()); } } // test a large number of clients running @@ -563,8 +569,7 @@ namespace MultiClientTests { static CommonProcessArgs args{ .ctrlPipe = R"(\\.\pipe\pm-multi-test-ctrl)", - .introNsm = "pm_multi_test_intro", - .frameNsm = "pm_multi_test_nsm", + .shmNamePrefix = "pm_multi_test_intro", .logLevel = "debug", .logFolder = logFolder_, .sampleClientMode = "ServiceCrashClient", diff --git a/IntelPresentMon/PresentMonAPI2Tests/PacedFrameTests.cpp b/IntelPresentMon/PresentMonAPI2Tests/PacedFrameTests.cpp new file mode 100644 index 000000000..2b13de63e --- /dev/null +++ b/IntelPresentMon/PresentMonAPI2Tests/PacedFrameTests.cpp @@ -0,0 +1,563 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#include "../CommonUtilities/win/WinAPI.h" +#include "../CommonUtilities/str/String.h" +#include "CppUnitTest.h" +#include "TestProcess.h" +#include "Folders.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +namespace fs = std::filesystem; +using namespace std::literals; +using namespace pmon; + +namespace PacedFrame +{ + class TestFixture : public CommonTestFixture + { + public: + const CommonProcessArgs& GetCommonArgs() const override + { + static CommonProcessArgs args{ + .ctrlPipe = R"(\\.\pipe\pm-paced-frame-test-ctrl)", + .shmNamePrefix = "pm_paced_frame_test_intro", + .logLevel = "debug", + .logFolder = logFolder_, + .sampleClientMode = "PacedFramePlayback", + }; + return args; + } + }; + + enum class ColumnIndex : size_t + { + Application = 0, + ProcessID = 1, + SwapChainAddress = 2, + PresentRuntime = 3, + SyncInterval = 4, + PresentFlags = 5, + AllowsTearing = 6, + PresentMode = 7, + FrameType = 8, + CPUStartTime = 9, + MsBetweenSimulationStart = 10, + MsBetweenPresents = 11, + MsBetweenDisplayChange = 12, + MsInPresentAPI = 13, + MsRenderPresentLatency = 14, + MsUntilDisplayed = 15, + MsPCLatency = 16, + MsBetweenAppStart = 17, + MsCPUBusy = 18, + MsCPUWait = 19, + MsGPULatency = 20, + MsGPUTime = 21, + MsGPUBusy = 22, + MsGPUWait = 23, + MsVideoBusy = 24, + MsAnimationError = 25, + AnimationTime = 26, + MsFlipDelay = 27, + MsAllInputToPhotonLatency = 28, + MsClickToPhotonLatency = 29, + MsInstrumentedLatency = 30, + }; + + const std::array kFrameCsvHeader{ + "Application", + "ProcessID", + "SwapChainAddress", + "PresentRuntime", + "SyncInterval", + "PresentFlags", + "AllowsTearing", + "PresentMode", + "FrameType", + "CPUStartTime", + "MsBetweenSimulationStart", + "MsBetweenPresents", + "MsBetweenDisplayChange", + "MsInPresentAPI", + "MsRenderPresentLatency", + "MsUntilDisplayed", + "MsPCLatency", + "MsBetweenAppStart", + "MsCPUBusy", + "MsCPUWait", + "MsGPULatency", + "MsGPUTime", + "MsGPUBusy", + "MsGPUWait", + "MsVideoBusy", + "MsAnimationError", + "AnimationTime", + "MsFlipDelay", + "MsAllInputToPhotonLatency", + "MsClickToPhotonLatency", + "MsInstrumentedLatency", + }; + + struct FrameCsvRow + { + std::string application; + uint32_t processId = 0; + uint64_t swapChainAddress = 0; + std::string presentRuntime; + int32_t syncInterval = 0; + uint32_t presentFlags = 0; + uint32_t allowsTearing = 0; + std::string presentMode; + std::string frameType; + std::optional cpuStartTime; + std::optional msBetweenSimulationStart; + double msBetweenPresents = 0.0; + std::optional msBetweenDisplayChange; + double msInPresentApi = 0.0; + double msRenderPresentLatency = 0.0; + std::optional msUntilDisplayed; + std::optional msPcLatency; + double msBetweenAppStart = 0.0; + double msCpuBusy = 0.0; + double msCpuWait = 0.0; + double msGpuLatency = 0.0; + double msGpuTime = 0.0; + double msGpuBusy = 0.0; + double msGpuWait = 0.0; + double msVideoBusy = 0.0; + std::optional msAnimationError; + std::optional animationTime; + std::optional msFlipDelay; + std::optional msAllInputToPhotonLatency; + std::optional msClickToPhotonLatency; + std::optional msInstrumentedLatency; + }; + + std::wstring MakeFailMessage(size_t row, const char* column, const std::string& expected, + const std::string& actual) + { + return pmon::util::str::ToWide(std::format( + "Row {} column {} expected [{}] got [{}]", row, column, expected, actual)); + } + + std::wstring MakeFailMessage(size_t row, const char* column) + { + return pmon::util::str::ToWide(std::format("Row {} column {} mismatch", row, column)); + } + + void StripUtf8Bom(std::string& value) + { + const char bom[] = { char(0xEF), char(0xBB), char(0xBF), 0 }; + if (value.rfind(bom, 0) == 0) { + value.erase(0, 3); + } + } + + bool IsMissingToken(const std::string& value) + { + return value == "NA" || value == "NaN" || value == "nan"; + } + + uint64_t ParseUint64(const std::string& value, size_t row, const char* column) + { + try { + return std::stoull(value, nullptr, 0); + } + catch (...) { + Assert::Fail(MakeFailMessage(row, column).c_str()); + } + return 0; + } + + uint32_t ParseUint32(const std::string& value, size_t row, const char* column) + { + try { + return static_cast(std::stoul(value, nullptr, 0)); + } + catch (...) { + Assert::Fail(MakeFailMessage(row, column).c_str()); + } + return 0; + } + + int32_t ParseInt32(const std::string& value, size_t row, const char* column) + { + try { + return std::stoi(value, nullptr, 0); + } + catch (...) { + Assert::Fail(MakeFailMessage(row, column).c_str()); + } + return 0; + } + + double ParseDouble(const std::string& value, size_t row, const char* column) + { + try { + return std::stod(value); + } + catch (...) { + Assert::Fail(MakeFailMessage(row, column).c_str()); + } + return 0.0; + } + + std::optional ParseOptionalDouble(const std::string& value, size_t row, const char* column) + { + if (IsMissingToken(value)) { + return std::nullopt; + } + return ParseDouble(value, row, column); + } + + void ValidateHeader(const std::vector& header) + { + Assert::IsTrue(header.size() == kFrameCsvHeader.size(), L"Unexpected header column count"); + for (size_t i = 0; i < kFrameCsvHeader.size(); ++i) { + Assert::IsTrue(header[i] == kFrameCsvHeader[i], + MakeFailMessage(0, "Header", kFrameCsvHeader[i], header[i]).c_str()); + } + } + + FrameCsvRow ParseFrameRow(const std::vector& row, size_t rowIndex) + { + FrameCsvRow parsed; + parsed.application = row[static_cast(ColumnIndex::Application)]; + parsed.processId = ParseUint32(row[static_cast(ColumnIndex::ProcessID)], rowIndex, "ProcessID"); + parsed.swapChainAddress = ParseUint64(row[static_cast(ColumnIndex::SwapChainAddress)], rowIndex, "SwapChainAddress"); + parsed.presentRuntime = row[static_cast(ColumnIndex::PresentRuntime)]; + parsed.syncInterval = ParseInt32(row[static_cast(ColumnIndex::SyncInterval)], rowIndex, "SyncInterval"); + parsed.presentFlags = ParseUint32(row[static_cast(ColumnIndex::PresentFlags)], rowIndex, "PresentFlags"); + parsed.allowsTearing = ParseUint32(row[static_cast(ColumnIndex::AllowsTearing)], rowIndex, "AllowsTearing"); + parsed.presentMode = row[static_cast(ColumnIndex::PresentMode)]; + parsed.frameType = row[static_cast(ColumnIndex::FrameType)]; + parsed.cpuStartTime = ParseOptionalDouble( + row[static_cast(ColumnIndex::CPUStartTime)], rowIndex, "CPUStartTime"); + parsed.msBetweenSimulationStart = ParseOptionalDouble( + row[static_cast(ColumnIndex::MsBetweenSimulationStart)], rowIndex, "MsBetweenSimulationStart"); + parsed.msBetweenPresents = ParseDouble( + row[static_cast(ColumnIndex::MsBetweenPresents)], rowIndex, "MsBetweenPresents"); + parsed.msBetweenDisplayChange = ParseOptionalDouble( + row[static_cast(ColumnIndex::MsBetweenDisplayChange)], rowIndex, "MsBetweenDisplayChange"); + parsed.msInPresentApi = ParseDouble( + row[static_cast(ColumnIndex::MsInPresentAPI)], rowIndex, "MsInPresentAPI"); + parsed.msRenderPresentLatency = ParseDouble( + row[static_cast(ColumnIndex::MsRenderPresentLatency)], rowIndex, "MsRenderPresentLatency"); + parsed.msUntilDisplayed = ParseOptionalDouble( + row[static_cast(ColumnIndex::MsUntilDisplayed)], rowIndex, "MsUntilDisplayed"); + parsed.msPcLatency = ParseOptionalDouble( + row[static_cast(ColumnIndex::MsPCLatency)], rowIndex, "MsPCLatency"); + parsed.msBetweenAppStart = ParseDouble( + row[static_cast(ColumnIndex::MsBetweenAppStart)], rowIndex, "MsBetweenAppStart"); + parsed.msCpuBusy = ParseDouble( + row[static_cast(ColumnIndex::MsCPUBusy)], rowIndex, "MsCPUBusy"); + parsed.msCpuWait = ParseDouble( + row[static_cast(ColumnIndex::MsCPUWait)], rowIndex, "MsCPUWait"); + parsed.msGpuLatency = ParseDouble( + row[static_cast(ColumnIndex::MsGPULatency)], rowIndex, "MsGPULatency"); + parsed.msGpuTime = ParseDouble( + row[static_cast(ColumnIndex::MsGPUTime)], rowIndex, "MsGPUTime"); + parsed.msGpuBusy = ParseDouble( + row[static_cast(ColumnIndex::MsGPUBusy)], rowIndex, "MsGPUBusy"); + parsed.msGpuWait = ParseDouble( + row[static_cast(ColumnIndex::MsGPUWait)], rowIndex, "MsGPUWait"); + parsed.msVideoBusy = ParseDouble( + row[static_cast(ColumnIndex::MsVideoBusy)], rowIndex, "MsVideoBusy"); + parsed.msAnimationError = ParseOptionalDouble( + row[static_cast(ColumnIndex::MsAnimationError)], rowIndex, "MsAnimationError"); + parsed.animationTime = ParseOptionalDouble( + row[static_cast(ColumnIndex::AnimationTime)], rowIndex, "AnimationTime"); + parsed.msFlipDelay = ParseOptionalDouble( + row[static_cast(ColumnIndex::MsFlipDelay)], rowIndex, "MsFlipDelay"); + parsed.msAllInputToPhotonLatency = ParseOptionalDouble( + row[static_cast(ColumnIndex::MsAllInputToPhotonLatency)], rowIndex, "MsAllInputToPhotonLatency"); + parsed.msClickToPhotonLatency = ParseOptionalDouble( + row[static_cast(ColumnIndex::MsClickToPhotonLatency)], rowIndex, "MsClickToPhotonLatency"); + parsed.msInstrumentedLatency = ParseOptionalDouble( + row[static_cast(ColumnIndex::MsInstrumentedLatency)], rowIndex, "MsInstrumentedLatency"); + return parsed; + } + + std::vector LoadCsvRows(const std::string& path, uint32_t targetPid) + { + csv::CSVReader reader{ path }; + auto header = reader.get_col_names(); + if (!header.empty()) { + StripUtf8Bom(header.front()); + } + ValidateHeader(header); + + std::vector rows; + std::vector values; + size_t rowIndex = 0; + for (auto& row : reader) { + values.clear(); + values.reserve(row.size()); + for (auto& field : row) { + values.push_back(field.get()); + } + if (values.size() < kFrameCsvHeader.size()) { + ++rowIndex; + continue; + } + auto rowPid = ParseUint32(values[static_cast(ColumnIndex::ProcessID)], + rowIndex, "ProcessID"); + if (rowPid != targetPid) { + ++rowIndex; + continue; + } + auto parsed = ParseFrameRow(values, rowIndex); + rows.push_back(std::move(parsed)); + ++rowIndex; + } + return rows; + } + + std::optional FindProcessNameInCsv(const std::string& path, uint32_t targetPid) + { + if (!fs::exists(path)) { + return std::nullopt; + } + csv::CSVReader reader{ path }; + auto header = reader.get_col_names(); + if (header.empty()) { + return std::nullopt; + } + StripUtf8Bom(header.front()); + + size_t pidIndex = SIZE_MAX; + size_t appIndex = SIZE_MAX; + for (size_t i = 0; i < header.size(); ++i) { + if (header[i] == "ProcessID") { + pidIndex = i; + } + else if (header[i] == "Application") { + appIndex = i; + } + } + if (pidIndex == SIZE_MAX || appIndex == SIZE_MAX) { + return std::nullopt; + } + + for (auto& row : reader) { + if (row.size() <= pidIndex || row.size() <= appIndex) { + continue; + } + auto pidValue = row[pidIndex].get(); + if (ParseUint32(pidValue, 0, "ProcessID") == targetPid) { + return row[appIndex].get(); + } + } + return std::nullopt; + } + + void CompareOptionalDouble(const std::optional& expected, const std::optional& actual, + size_t rowIndex, const char* column) + { + if (expected.has_value() != actual.has_value()) { + Assert::Fail(MakeFailMessage(rowIndex, column).c_str()); + } + if (expected && actual && *expected != *actual) { + Assert::Fail(MakeFailMessage(rowIndex, column).c_str()); + } + } + + void CompareRows(const FrameCsvRow& expected, const FrameCsvRow& actual, size_t rowIndex) + { + if (expected.application != actual.application) { + Assert::Fail(MakeFailMessage(rowIndex, "Application", expected.application, actual.application).c_str()); + } + if (expected.processId != actual.processId) { + Assert::Fail(MakeFailMessage(rowIndex, "ProcessID").c_str()); + } + if (expected.swapChainAddress != actual.swapChainAddress) { + Assert::Fail(MakeFailMessage(rowIndex, "SwapChainAddress").c_str()); + } + if (expected.presentRuntime != actual.presentRuntime) { + Assert::Fail(MakeFailMessage(rowIndex, "PresentRuntime", + expected.presentRuntime, actual.presentRuntime).c_str()); + } + if (expected.syncInterval != actual.syncInterval) { + Assert::Fail(MakeFailMessage(rowIndex, "SyncInterval").c_str()); + } + if (expected.presentFlags != actual.presentFlags) { + Assert::Fail(MakeFailMessage(rowIndex, "PresentFlags").c_str()); + } + if (expected.allowsTearing != actual.allowsTearing) { + Assert::Fail(MakeFailMessage(rowIndex, "AllowsTearing").c_str()); + } + if (expected.presentMode != actual.presentMode) { + Assert::Fail(MakeFailMessage(rowIndex, "PresentMode", + expected.presentMode, actual.presentMode).c_str()); + } + if (expected.frameType != actual.frameType) { + Assert::Fail(MakeFailMessage(rowIndex, "FrameType", + expected.frameType, actual.frameType).c_str()); + } + CompareOptionalDouble(expected.cpuStartTime, actual.cpuStartTime, + rowIndex, "CPUStartTime"); + CompareOptionalDouble(expected.msBetweenSimulationStart, actual.msBetweenSimulationStart, + rowIndex, "MsBetweenSimulationStart"); + if (expected.msBetweenPresents != actual.msBetweenPresents) { + Assert::Fail(MakeFailMessage(rowIndex, "MsBetweenPresents").c_str()); + } + CompareOptionalDouble(expected.msBetweenDisplayChange, actual.msBetweenDisplayChange, + rowIndex, "MsBetweenDisplayChange"); + if (expected.msInPresentApi != actual.msInPresentApi) { + Assert::Fail(MakeFailMessage(rowIndex, "MsInPresentAPI").c_str()); + } + if (expected.msRenderPresentLatency != actual.msRenderPresentLatency) { + Assert::Fail(MakeFailMessage(rowIndex, "MsRenderPresentLatency").c_str()); + } + CompareOptionalDouble(expected.msUntilDisplayed, actual.msUntilDisplayed, + rowIndex, "MsUntilDisplayed"); + CompareOptionalDouble(expected.msPcLatency, actual.msPcLatency, + rowIndex, "MsPCLatency"); + if (expected.msBetweenAppStart != actual.msBetweenAppStart) { + Assert::Fail(MakeFailMessage(rowIndex, "MsBetweenAppStart").c_str()); + } + if (expected.msCpuBusy != actual.msCpuBusy) { + Assert::Fail(MakeFailMessage(rowIndex, "MsCPUBusy").c_str()); + } + if (expected.msCpuWait != actual.msCpuWait) { + Assert::Fail(MakeFailMessage(rowIndex, "MsCPUWait").c_str()); + } + if (expected.msGpuLatency != actual.msGpuLatency) { + Assert::Fail(MakeFailMessage(rowIndex, "MsGPULatency").c_str()); + } + if (expected.msGpuTime != actual.msGpuTime) { + Assert::Fail(MakeFailMessage(rowIndex, "MsGPUTime").c_str()); + } + if (expected.msGpuBusy != actual.msGpuBusy) { + Assert::Fail(MakeFailMessage(rowIndex, "MsGPUBusy").c_str()); + } + if (expected.msGpuWait != actual.msGpuWait) { + Assert::Fail(MakeFailMessage(rowIndex, "MsGPUWait").c_str()); + } + if (expected.msVideoBusy != actual.msVideoBusy) { + Assert::Fail(MakeFailMessage(rowIndex, "MsVideoBusy").c_str()); + } + CompareOptionalDouble(expected.msAnimationError, actual.msAnimationError, + rowIndex, "MsAnimationError"); + CompareOptionalDouble(expected.animationTime, actual.animationTime, + rowIndex, "AnimationTime"); + CompareOptionalDouble(expected.msFlipDelay, actual.msFlipDelay, + rowIndex, "MsFlipDelay"); + CompareOptionalDouble(expected.msAllInputToPhotonLatency, actual.msAllInputToPhotonLatency, + rowIndex, "MsAllInputToPhotonLatency"); + CompareOptionalDouble(expected.msClickToPhotonLatency, actual.msClickToPhotonLatency, + rowIndex, "MsClickToPhotonLatency"); + CompareOptionalDouble(expected.msInstrumentedLatency, actual.msInstrumentedLatency, + rowIndex, "MsInstrumentedLatency"); + } + + void CompareCsvFiles(const std::string& goldPath, const std::string& runPath, uint32_t targetPid) + { + auto goldRows = LoadCsvRows(goldPath, targetPid); + auto runRows = LoadCsvRows(runPath, targetPid); + + Assert::IsTrue(!goldRows.empty(), L"No gold rows found for target pid"); + Assert::IsTrue(goldRows.size() == runRows.size(), L"Row count mismatch"); + for (size_t i = 0; i < goldRows.size(); ++i) { + CompareRows(goldRows[i], runRows[i], i); + } + } + + void ExecutePacedFrameTest(const std::string& testName, uint32_t targetPid, + size_t frameLimit, TestFixture& fixture) + { + const auto goldCsvPath = std::format(R"(..\..\Tests\AuxData\Data\{}_gold.csv)", testName); + const auto outCsvPath = std::format("{}\\{}.csv", outFolder_, testName); + Logger::WriteMessage(std::format("Frame test output csv: {}\n", + fs::absolute(outCsvPath).string()).c_str()); + Logger::WriteMessage(std::format("Frame test gold csv search path: {}\n", + fs::absolute(goldCsvPath).string()).c_str()); + + std::optional processName; + if (fs::exists(goldCsvPath)) { + processName = FindProcessNameInCsv(goldCsvPath, targetPid); + } + + std::vector args{ + "--process-id"s, std::to_string(targetPid), + "--output-path"s, outCsvPath, + "--frame-limit"s, std::to_string(frameLimit), + }; + if (processName) { + args.push_back("--process-name"s); + args.push_back(*processName); + } + + fixture.LaunchClient(args); + + Assert::IsTrue(fs::exists(outCsvPath), L"Output CSV not created"); + Assert::IsTrue(fs::exists(goldCsvPath), L"Gold CSV missing"); + CompareCsvFiles(goldCsvPath, outCsvPath, targetPid); + } + +#define TEST_NAME F00HeaWin2080 +#define ETL_NAME P00HeaWin2080 + TEST_CLASS(TEST_NAME) + { + TestFixture fixture_; + public: + TEST_METHOD_INITIALIZE(Setup) + { + Logger::WriteMessage(std::format("Frame test etl path: {}\n", + fs::absolute(std::format(R"(..\..\Tests\AuxData\Data\{}.etl)", STRINGIFY(ETL_NAME))).string()).c_str()); + fixture_.Setup({ + "--etl-test-file"s, std::format(R"(..\..\Tests\AuxData\Data\{}.etl)", STRINGIFY(ETL_NAME)), + "--pace-playback"s, + }); + } + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + TEST_METHOD(PollFrame) + { + const uint32_t targetPid = 12820; + const size_t frameLimit = 1903; + ExecutePacedFrameTest(STRINGIFY(TEST_NAME), targetPid, frameLimit, fixture_); + } + }; +#undef TEST_NAME +#undef ETL_NAME + +#define TEST_NAME F01TimeSpyDemoFS2080 +#define ETL_NAME P01TimeSpyDemoFS2080 + TEST_CLASS(TEST_NAME) + { + TestFixture fixture_; + public: + TEST_METHOD_INITIALIZE(Setup) + { + Logger::WriteMessage(std::format("Frame test etl path: {}\n", + fs::absolute(std::format(R"(..\..\Tests\AuxData\Data\{}.etl)", STRINGIFY(ETL_NAME))).string()).c_str()); + fixture_.Setup({ + "--etl-test-file"s, std::format(R"(..\..\Tests\AuxData\Data\{}.etl)", STRINGIFY(ETL_NAME)), + "--pace-playback"s, + }); + } + TEST_METHOD_CLEANUP(Cleanup) + { + fixture_.Cleanup(); + } + TEST_METHOD(PollFrame) + { + const uint32_t targetPid = 19736; + const size_t frameLimit = 0; + ExecutePacedFrameTest(STRINGIFY(TEST_NAME), targetPid, frameLimit, fixture_); + } + }; +#undef TEST_NAME +#undef ETL_NAME +} diff --git a/IntelPresentMon/PresentMonAPI2Tests/PacedPollingTests.cpp b/IntelPresentMon/PresentMonAPI2Tests/PacedPollingTests.cpp index 34e51c94d..0d99826d0 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/PacedPollingTests.cpp +++ b/IntelPresentMon/PresentMonAPI2Tests/PacedPollingTests.cpp @@ -34,8 +34,7 @@ namespace PacedPolling { static CommonProcessArgs args{ .ctrlPipe = R"(\\.\pipe\pm-paced-polling-test-ctrl)", - .introNsm = "pm_paced_polling_test_intro", - .frameNsm = "pm_paced_polling_test_nsm", + .shmNamePrefix = "pm_paced_polling_test_intro", .logLevel = "debug", .logFolder = logFolder_, .sampleClientMode = "PacedPlayback", @@ -253,7 +252,7 @@ namespace PacedPolling "--run-time"s, std::to_string(recordingStop - recordingStart), "--run-start"s, std::to_string(recordingStart), "--poll-period"s, std::to_string(pollPeriod), - "--metric-offset"s, "64"s, + "--metric-offset"s, "100"s, "--window-size"s, "1000"s, }); // load up result @@ -486,8 +485,8 @@ namespace PacedPolling const auto recordingStart = 1.; const auto recordingStop = 14.; const auto pollPeriod = 0.1; - const auto toleranceFactor = 0.005; - const auto fullFailRatio = 0.667; + const auto toleranceFactor = 0.01; + const auto fullFailRatio = 0.95; // run test ExecutePacedPollingTest(STRINGIFY(TEST_NAME), targetPid, recordingStart, recordingStop, pollPeriod, toleranceFactor, fullFailRatio, fixture_); diff --git a/IntelPresentMon/PresentMonAPI2Tests/PresentMonAPI2Tests.vcxproj b/IntelPresentMon/PresentMonAPI2Tests/PresentMonAPI2Tests.vcxproj index 5ce3f9ec0..9754fb5fb 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/PresentMonAPI2Tests.vcxproj +++ b/IntelPresentMon/PresentMonAPI2Tests/PresentMonAPI2Tests.vcxproj @@ -1,4 +1,4 @@ - + @@ -97,8 +97,12 @@ + + + + @@ -109,9 +113,15 @@ {08a704d8-ca1c-45e9-8ede-542a1a43b53e} + + {ca23d648-daef-4f06-81d5-fe619bd31f0b} + {8f86d067-2437-46fc-8f82-4d7155ceced7} + + {5ddba061-53a0-4835-8aaf-943f403f924f} + {cee032ed-b0d3-47f8-bdae-d46757b0061b} @@ -129,6 +139,7 @@ + diff --git a/IntelPresentMon/PresentMonAPI2Tests/PresentMonAPI2Tests.vcxproj.filters b/IntelPresentMon/PresentMonAPI2Tests/PresentMonAPI2Tests.vcxproj.filters index a8ea4a3dd..18d0d13c7 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/PresentMonAPI2Tests.vcxproj.filters +++ b/IntelPresentMon/PresentMonAPI2Tests/PresentMonAPI2Tests.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -39,6 +39,18 @@ Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + @@ -62,5 +74,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/IntelPresentMon/PresentMonAPI2Tests/TestCommands.h b/IntelPresentMon/PresentMonAPI2Tests/TestCommands.h index 21b079f95..e7fe932c1 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/TestCommands.h +++ b/IntelPresentMon/PresentMonAPI2Tests/TestCommands.h @@ -13,15 +13,16 @@ namespace pmon::test { struct Status { - std::set nsmStreamedPids; - uint32_t activeAdapterId; + // new ipc tracking + std::set trackedPids; + std::set frameStorePids; uint32_t telemetryPeriodMs; std::optional etwFlushPeriodMs; template void serialize(Archive& ar) { - ar(nsmStreamedPids, activeAdapterId, telemetryPeriodMs, etwFlushPeriodMs); + ar(trackedPids, frameStorePids, telemetryPeriodMs, etwFlushPeriodMs); } }; } diff --git a/IntelPresentMon/PresentMonAPI2Tests/TestProcess.h b/IntelPresentMon/PresentMonAPI2Tests/TestProcess.h index 2f1b93003..d5374ccbf 100644 --- a/IntelPresentMon/PresentMonAPI2Tests/TestProcess.h +++ b/IntelPresentMon/PresentMonAPI2Tests/TestProcess.h @@ -1,16 +1,19 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2023 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include "../CommonUtilities/win/WinAPI.h" #include "CppUnitTest.h" #include "JobManager.h" +#include "Logging.h" #include "TestCommands.h" #include "../CommonUtilities/file/FileUtils.h" #include "../CommonUtilities/pipe/Pipe.h" #include #include +#include #include #include +#include #include #include #include @@ -26,13 +29,49 @@ using namespace pmon; struct CommonProcessArgs { std::string ctrlPipe; - std::string introNsm; - std::string frameNsm; + std::string shmNamePrefix; std::string logLevel; + std::optional logVerboseModules; std::string logFolder; std::string sampleClientMode; + bool suppressService = false; }; +inline std::vector SplitVerboseModulesArgs_(const std::string& raw) +{ + std::vector modules; + std::string token; + for (unsigned char ch : raw) { + if (ch == ',' || std::isspace(ch)) { + if (!token.empty()) { + modules.push_back(token); + token.clear(); + } + continue; + } + token.push_back(static_cast(ch)); + } + if (!token.empty()) { + modules.push_back(token); + } + return modules; +} + +inline void AppendVerboseModulesArgs_(std::vector& args, + const std::optional& modules, + const char* flag) +{ + if (!modules || modules->empty()) { + return; + } + const auto values = SplitVerboseModulesArgs_(*modules); + if (values.empty()) { + return; + } + args.push_back(flag); + args.insert(args.end(), values.begin(), values.end()); +} + // base class to represent child processes launched by test cases class TestProcess { @@ -180,13 +219,14 @@ class ServiceProcess : public ConnectedTestProcess { std::vector allArgs{ "--control-pipe"s, common.ctrlPipe, - "--nsm-prefix"s, common.frameNsm, - "--intro-nsm"s, common.introNsm, + "--shm-name-prefix"s, common.shmNamePrefix, "--enable-test-control"s, "--log-dir"s, common.logFolder, "--log-name-pid"s, "--log-level"s, common.logLevel, + "--enable-debugger-log"s, }; + AppendVerboseModulesArgs_(allArgs, common.logVerboseModules, "--log-verbose-modules"); allArgs.append_range(customArgs); return allArgs; } @@ -216,13 +256,13 @@ class ClientProcess : public ConnectedTestProcess { std::vector allArgs{ "--control-pipe"s, common.ctrlPipe, - "--intro-nsm"s, common.introNsm, "--middleware-dll-path"s, "PresentMonAPI2.dll"s, "--log-folder"s, common.logFolder, "--log-name-pid"s, "--log-level"s, common.logLevel, "--mode"s, common.sampleClientMode, }; + AppendVerboseModulesArgs_(allArgs, common.logVerboseModules, "--log-verbose-modules"); allArgs.append_range(customArgs); return allArgs; } @@ -276,13 +316,30 @@ class CommonTestFixture void Setup(std::vector args = {}) { - StartService_(args, GetCommonArgs()); + if (!logManager_) { + logManager_.emplace(); + } + pmon::test::SetupTestLogging(GetCommonArgs().logFolder, GetCommonArgs().logLevel, + GetCommonArgs().logVerboseModules); svcArgs_ = std::move(args); + if (!GetCommonArgs().suppressService) { + StartService_(svcArgs_, GetCommonArgs()); + serviceStarted_ = true; + } + else { + serviceStarted_ = false; + } } void Cleanup() { - StopService_(GetCommonArgs()); - ioctxRunThread_.join(); + if (serviceStarted_) { + StopService_(GetCommonArgs()); + serviceStarted_ = false; + } + if (ioctxRunThread_.joinable()) { + ioctxRunThread_.join(); + } + logManager_.reset(); } void StopService() { @@ -291,10 +348,14 @@ class CommonTestFixture void RebootService(std::optional> newArgs = {}) { auto& common = GetCommonArgs(); + if (common.suppressService) { + return; + } auto& svcArgs = newArgs ? *newArgs : svcArgs_; StopService_(common); StartService_(svcArgs, common); svcArgs_ = std::move(svcArgs); + serviceStarted_ = true; } ClientProcess LaunchClient(const std::vector& args = {}) { @@ -344,6 +405,8 @@ class CommonTestFixture static constexpr int svcPipeTimeout_ = 250; std::vector svcArgs_; JobManager jobMan_; + bool serviceStarted_ = false; std::thread ioctxRunThread_; as::io_context ioctx_; + std::optional logManager_; }; diff --git a/IntelPresentMon/PresentMonAPIWrapper/DynamicQuery.cpp b/IntelPresentMon/PresentMonAPIWrapper/DynamicQuery.cpp index 3db871610..53441ea2a 100644 --- a/IntelPresentMon/PresentMonAPIWrapper/DynamicQuery.cpp +++ b/IntelPresentMon/PresentMonAPIWrapper/DynamicQuery.cpp @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "DynamicQuery.h" #include #include @@ -42,6 +42,30 @@ namespace pmapi } } + void DynamicQuery::PollWithTimestamp(const ProcessTracker& tracker, uint8_t* pBlob, uint32_t& numSwapChains, uint64_t nowTimestamp) const + { + if (auto sta = pmPollDynamicQueryWithTimestamp(hQuery_, tracker.GetPid(), pBlob, &numSwapChains, nowTimestamp); + sta != PM_STATUS_SUCCESS) { + throw ApiErrorException{ sta, "dynamic poll with timestamp call failed" }; + } + } + + void DynamicQuery::Poll(uint8_t* pBlob, uint32_t& numSwapChains) const + { + if (auto sta = pmPollDynamicQuery(hQuery_, 0u, pBlob, &numSwapChains); + sta != PM_STATUS_SUCCESS) { + throw ApiErrorException{ sta, "dynamic poll call failed" }; + } + } + + void DynamicQuery::PollWithTimestamp(uint8_t* pBlob, uint32_t& numSwapChains, uint64_t nowTimestamp) const + { + if (auto sta = pmPollDynamicQueryWithTimestamp(hQuery_, 0u, pBlob, &numSwapChains, nowTimestamp); + sta != PM_STATUS_SUCCESS) { + throw ApiErrorException{ sta, "dynamic poll with timestamp call failed" }; + } + } + void DynamicQuery::Poll(const ProcessTracker& tracker, BlobContainer& blobs) const { assert(!Empty()); @@ -49,6 +73,27 @@ namespace pmapi Poll(tracker, blobs.GetFirst(), blobs.AcquireNumBlobsInRef_()); } + void DynamicQuery::PollWithTimestamp(const ProcessTracker& tracker, BlobContainer& blobs, uint64_t nowTimestamp) const + { + assert(!Empty()); + assert(blobs.CheckHandle(hQuery_)); + PollWithTimestamp(tracker, blobs.GetFirst(), blobs.AcquireNumBlobsInRef_(), nowTimestamp); + } + + void DynamicQuery::Poll(BlobContainer& blobs) const + { + assert(!Empty()); + assert(blobs.CheckHandle(hQuery_)); + Poll(blobs.GetFirst(), blobs.AcquireNumBlobsInRef_()); + } + + void DynamicQuery::PollWithTimestamp(BlobContainer& blobs, uint64_t nowTimestamp) const + { + assert(!Empty()); + assert(blobs.CheckHandle(hQuery_)); + PollWithTimestamp(blobs.GetFirst(), blobs.AcquireNumBlobsInRef_(), nowTimestamp); + } + BlobContainer DynamicQuery::MakeBlobContainer(uint32_t nBlobs) const { assert(!Empty()); diff --git a/IntelPresentMon/PresentMonAPIWrapper/DynamicQuery.h b/IntelPresentMon/PresentMonAPIWrapper/DynamicQuery.h index 6224c1dfd..ad197eeba 100644 --- a/IntelPresentMon/PresentMonAPIWrapper/DynamicQuery.h +++ b/IntelPresentMon/PresentMonAPIWrapper/DynamicQuery.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include "BlobContainer.h" #include "ProcessTracker.h" @@ -33,6 +33,28 @@ namespace pmapi // numSwapChains: input indicates to API how many blobs available, output indicates how many were written // if the target process has multiple swap chains, will poll data for as many swaps as there are blobs available void Poll(const ProcessTracker& tracker, uint8_t* pBlob, uint32_t& numSwapChains) const; + // poll the specified process using this query, using an explicit timestamp for the "now" time + // polling processes frame event data and generates metrics for the time point at which this query was polled + // numSwapChains: input indicates to API how many blobs available, output indicates how many were written + // if the target process has multiple swap chains, will poll data for as many swaps as there are blobs available + void PollWithTimestamp(const ProcessTracker& tracker, uint8_t* pBlob, uint32_t& numSwapChains, uint64_t nowTimestamp) const; + // poll this query without a target process (processId = 0) + // only valid for queries that do not require frame metrics + void Poll(uint8_t* pBlob, uint32_t& numSwapChains) const; + // poll this query without a target process (processId = 0) + // only valid for queries that do not require frame metrics + void PollWithTimestamp(uint8_t* pBlob, uint32_t& numSwapChains, uint64_t nowTimestamp) const; + // poll the specified process using this query, using an explicit timestamp for the "now" time + // polling processes frame event data and generates metrics for the time point at which this query was polled + // makes use of a blob container to manage the output data + // if the target process has multiple swap chains, will poll data for as many swaps has the container has room for + void PollWithTimestamp(const ProcessTracker& tracker, BlobContainer& blobs, uint64_t nowTimestamp) const; + // poll this query without a target process (processId = 0) + // only valid for queries that do not require frame metrics + void Poll(BlobContainer& blobs) const; + // poll this query without a target process (processId = 0) + // only valid for queries that do not require frame metrics + void PollWithTimestamp(BlobContainer& blobs, uint64_t nowTimestamp) const; // create a blob container sized suited for this query // nBlobs parameter will control how many swaps can be polled maximum using the container BlobContainer MakeBlobContainer(uint32_t nBlobs) const; diff --git a/IntelPresentMon/PresentMonAPIWrapper/FixedQuery.cpp b/IntelPresentMon/PresentMonAPIWrapper/FixedQuery.cpp index 14ba552ab..be59823dc 100644 --- a/IntelPresentMon/PresentMonAPIWrapper/FixedQuery.cpp +++ b/IntelPresentMon/PresentMonAPIWrapper/FixedQuery.cpp @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "FixedQuery.h" #include #include @@ -46,6 +46,12 @@ namespace pmapi return activeBlobIndex_; } + ProcessTracker FixedQueryContainer_::TrackProcess(uint32_t pid, bool isPlayback, bool isBackpressured) + { + assert(pSession_); + return pSession_->TrackProcess(pid, isPlayback, isBackpressured); + } + void FixedQueryContainer_::FinalizationPreprocess_() { // replace slot indexes with device ids @@ -77,7 +83,6 @@ namespace pmapi smartElements_.clear(); rawElements_.clear(); slotDeviceIds_.clear(); - pSession_ = nullptr; } @@ -182,4 +187,4 @@ namespace pmapi { return dataType_ != PM_DATA_TYPE_VOID; } -} \ No newline at end of file +} diff --git a/IntelPresentMon/PresentMonAPIWrapper/FixedQuery.h b/IntelPresentMon/PresentMonAPIWrapper/FixedQuery.h index 885f1b010..ea764cac9 100644 --- a/IntelPresentMon/PresentMonAPIWrapper/FixedQuery.h +++ b/IntelPresentMon/PresentMonAPIWrapper/FixedQuery.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include #include @@ -52,6 +52,9 @@ namespace pmapi const uint8_t* PeekActiveBlob() const; // get index of the current active blob size_t GetActiveBlobIndex() const; + // begin tracking a process using the session associated with this fixed query + // set isPlayback and isBackpressured when tracking ETL playback + ProcessTracker TrackProcess(uint32_t pid, bool isPlayback = false, bool isBackpressured = false); protected: friend class FixedQueryElement; // functions @@ -296,4 +299,4 @@ namespace pmapi // begin a fixed frame query #define PM_BEGIN_FIXED_FRAME_QUERY(type) struct type : pmapi::FixedFrameQueryContainer { using FixedFrameQueryContainer::FixedFrameQueryContainer; // end a fixed query (dynamic or frame) -#define PM_END_FIXED_QUERY private: pmapi::FinalizingElement finalizer{ this }; } \ No newline at end of file +#define PM_END_FIXED_QUERY private: pmapi::FinalizingElement finalizer{ this }; } diff --git a/IntelPresentMon/PresentMonAPIWrapper/PresentMonAPIWrapper.h b/IntelPresentMon/PresentMonAPIWrapper/PresentMonAPIWrapper.h index 9ca269d99..9ff79d867 100644 --- a/IntelPresentMon/PresentMonAPIWrapper/PresentMonAPIWrapper.h +++ b/IntelPresentMon/PresentMonAPIWrapper/PresentMonAPIWrapper.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "Session.h" #include "ProcessTracker.h" #include "FrameQuery.h" diff --git a/IntelPresentMon/PresentMonAPIWrapper/PresentMonAPIWrapper.vcxproj b/IntelPresentMon/PresentMonAPIWrapper/PresentMonAPIWrapper.vcxproj index b49d557ee..bdb8e3ae2 100644 --- a/IntelPresentMon/PresentMonAPIWrapper/PresentMonAPIWrapper.vcxproj +++ b/IntelPresentMon/PresentMonAPIWrapper/PresentMonAPIWrapper.vcxproj @@ -1,4 +1,4 @@ - + @@ -136,4 +136,4 @@ - \ No newline at end of file + diff --git a/IntelPresentMon/PresentMonAPIWrapper/PresentMonAPIWrapper.vcxproj.filters b/IntelPresentMon/PresentMonAPIWrapper/PresentMonAPIWrapper.vcxproj.filters index f1cf4eb1a..6e8699635 100644 --- a/IntelPresentMon/PresentMonAPIWrapper/PresentMonAPIWrapper.vcxproj.filters +++ b/IntelPresentMon/PresentMonAPIWrapper/PresentMonAPIWrapper.vcxproj.filters @@ -75,4 +75,4 @@ Header Files - \ No newline at end of file + diff --git a/IntelPresentMon/PresentMonAPIWrapper/ProcessTracker.cpp b/IntelPresentMon/PresentMonAPIWrapper/ProcessTracker.cpp index b65a1cd92..4a4bbebe5 100644 --- a/IntelPresentMon/PresentMonAPIWrapper/ProcessTracker.cpp +++ b/IntelPresentMon/PresentMonAPIWrapper/ProcessTracker.cpp @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "ProcessTracker.h" #include #include @@ -34,6 +34,14 @@ namespace pmapi return pid_; } + void ProcessTracker::FlushFrames() + { + assert(!Empty()); + if (auto sta = pmFlushFrames(hSession_, pid_); sta != PM_STATUS_SUCCESS) { + throw ApiErrorException{ sta, "flush frames call failed" }; + } + } + void ProcessTracker::Reset() noexcept { if (!Empty()) { @@ -50,12 +58,19 @@ namespace pmapi ProcessTracker::operator bool() const { return !Empty(); } - ProcessTracker::ProcessTracker(PM_SESSION_HANDLE hSession, uint32_t pid) + ProcessTracker::ProcessTracker(PM_SESSION_HANDLE hSession, uint32_t pid, bool isPlayback, bool isBackpressured) : pid_{ pid }, hSession_{ hSession } { - if (auto sta = pmStartTrackingProcess(hSession_, pid_); sta != PM_STATUS_SUCCESS) { + PM_STATUS sta = PM_STATUS_SUCCESS; + if (isPlayback) { + sta = pmStartPlaybackTracking(hSession_, pid_, isBackpressured ? 1u : 0u); + } + else { + sta = pmStartTrackingProcess(hSession_, pid_); + } + if (sta != PM_STATUS_SUCCESS) { throw ApiErrorException{ sta, "start process tracking call failed" }; } } @@ -65,4 +80,4 @@ namespace pmapi pid_ = 0; hSession_ = nullptr; } -} \ No newline at end of file +} diff --git a/IntelPresentMon/PresentMonAPIWrapper/ProcessTracker.h b/IntelPresentMon/PresentMonAPIWrapper/ProcessTracker.h index da8286997..7771d7e1b 100644 --- a/IntelPresentMon/PresentMonAPIWrapper/ProcessTracker.h +++ b/IntelPresentMon/PresentMonAPIWrapper/ProcessTracker.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include namespace pmapi @@ -20,6 +20,8 @@ namespace pmapi ProcessTracker& operator=(ProcessTracker&& rhs) noexcept; // get the id of process being tracked uint32_t GetPid() const; + // flush any buffered frame event data for this process + void FlushFrames(); // empty this tracker (stop tracking process if any) void Reset() noexcept; // check if tracker is empty @@ -28,7 +30,7 @@ namespace pmapi operator bool() const; private: // functions - ProcessTracker(PM_SESSION_HANDLE hSession, uint32_t pid); + ProcessTracker(PM_SESSION_HANDLE hSession, uint32_t pid, bool isPlayback, bool isBackpressured); // zero out members, useful after emptying via move or reset void Clear_() noexcept; // data diff --git a/IntelPresentMon/PresentMonAPIWrapper/Session.cpp b/IntelPresentMon/PresentMonAPIWrapper/Session.cpp index 355bff977..8cabc43e9 100644 --- a/IntelPresentMon/PresentMonAPIWrapper/Session.cpp +++ b/IntelPresentMon/PresentMonAPIWrapper/Session.cpp @@ -1,4 +1,4 @@ -#include "Session.h" +#include "Session.h" #include #include #include @@ -80,10 +80,10 @@ namespace pmapi return pIntrospectionRootCache_ = std::make_shared(pRoot, [](const PM_INTROSPECTION_ROOT* ptr) { pmFreeIntrospectionRoot(ptr); }); } - ProcessTracker Session::TrackProcess(uint32_t pid) + ProcessTracker Session::TrackProcess(uint32_t pid, bool isPlayback, bool isBackpressured) { assert(handle_); - return { handle_, pid }; + return { handle_, pid, isPlayback, isBackpressured }; } DynamicQuery Session::RegisterDynamicQuery(std::span elements, double winSizeMs, double metricOffsetMs) @@ -134,4 +134,4 @@ namespace pmapi // initialize the enum map so that it doesn't need to be initialized explicitly EnumMap::Refresh(*GetIntrospectionRoot()); } -} \ No newline at end of file +} diff --git a/IntelPresentMon/PresentMonAPIWrapper/Session.h b/IntelPresentMon/PresentMonAPIWrapper/Session.h index 3df3ccfad..c4279cbb2 100644 --- a/IntelPresentMon/PresentMonAPIWrapper/Session.h +++ b/IntelPresentMon/PresentMonAPIWrapper/Session.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include "ProcessTracker.h" #include "FrameQuery.h" @@ -51,7 +51,8 @@ namespace pmapi // see PresentMonAPIWrapperCommon/Introspection.h for details about introspection data available std::shared_ptr GetIntrospectionRoot(bool forceRefresh = false) const; // begin tracking a process, necessary to consume frame data or query metrics involving that process - ProcessTracker TrackProcess(uint32_t pid); + // set isPlayback and isBackpressured when tracking ETL playback + ProcessTracker TrackProcess(uint32_t pid, bool isPlayback = false, bool isBackpressured = false); // register (build/compile) a dynamic query used to poll metrics DynamicQuery RegisterDynamicQuery(std::span elements, double winSizeMs = 1000, double metricOffsetMs = 1020); // register (build/compile) a frame query used to consume frame events diff --git a/IntelPresentMon/PresentMonAPIWrapper/StaticQuery.cpp b/IntelPresentMon/PresentMonAPIWrapper/StaticQuery.cpp index 07a266e2d..2d706a851 100644 --- a/IntelPresentMon/PresentMonAPIWrapper/StaticQuery.cpp +++ b/IntelPresentMon/PresentMonAPIWrapper/StaticQuery.cpp @@ -1,10 +1,10 @@ -#pragma once +#pragma once #include "StaticQuery.h" #include namespace pmapi { - StaticQueryResult PollStatic(const Session& session, const ProcessTracker& process, + StaticQueryResult StaticQueryResult::PollStatic_(const Session& session, uint32_t pid, PM_METRIC metric, uint32_t deviceId, uint32_t arrayIndex) { const auto pIntro = session.GetIntrospectionRoot(); @@ -17,10 +17,22 @@ namespace pmapi .deviceId = deviceId, .arrayIndex = arrayIndex, }; - if (const auto err = pmPollStaticQuery(session.GetHandle(), &element, process.GetPid(), result.blob_.data()); + if (const auto err = pmPollStaticQuery(session.GetHandle(), &element, pid, result.blob_.data()); err != PM_STATUS_SUCCESS) { throw ApiErrorException{ err, "Error polling static query" }; } return result; } + + StaticQueryResult PollStatic(const Session& session, const ProcessTracker& process, + PM_METRIC metric, uint32_t deviceId, uint32_t arrayIndex) + { + return StaticQueryResult::PollStatic_(session, process.GetPid(), metric, deviceId, arrayIndex); + } + + StaticQueryResult PollStatic(const Session& session, + PM_METRIC metric, uint32_t deviceId, uint32_t arrayIndex) + { + return StaticQueryResult::PollStatic_(session, 0, metric, deviceId, arrayIndex); + } } diff --git a/IntelPresentMon/PresentMonAPIWrapper/StaticQuery.h b/IntelPresentMon/PresentMonAPIWrapper/StaticQuery.h index 4d08177af..c7184b91d 100644 --- a/IntelPresentMon/PresentMonAPIWrapper/StaticQuery.h +++ b/IntelPresentMon/PresentMonAPIWrapper/StaticQuery.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include #include @@ -98,6 +98,8 @@ namespace pmapi { friend StaticQueryResult PollStatic(const Session& session, const ProcessTracker& process, PM_METRIC metric, uint32_t deviceId, uint32_t arrayIndex); + friend StaticQueryResult PollStatic(const Session& session, + PM_METRIC metric, uint32_t deviceId, uint32_t arrayIndex); public: // access this result as a specific static data type // this will perform the conversion based on the runtime information about the metric's type @@ -127,6 +129,8 @@ namespace pmapi // although NRVO should prevent this from being called, it's not standard guarantee // so we still need the copy ctor StaticQueryResult(const StaticQueryResult&) = default; + static StaticQueryResult PollStatic_(const Session& session, uint32_t pid, + PM_METRIC metric, uint32_t deviceId, uint32_t arrayIndex); // data // 260 bytes is the maximum possible size for query element data static constexpr size_t blobSize_ = 260; @@ -141,4 +145,8 @@ namespace pmapi // query result data blobs and convert to the desired type when conversion is possible StaticQueryResult PollStatic(const Session& session, const ProcessTracker& process, PM_METRIC metric, uint32_t deviceId = 0, uint32_t arrayIndex = 0); + + // overload without process tracker (uses pid 0) + StaticQueryResult PollStatic(const Session& session, + PM_METRIC metric, uint32_t deviceId = 0, uint32_t arrayIndex = 0); } diff --git a/IntelPresentMon/PresentMonCli/PresentMonCli.cpp b/IntelPresentMon/PresentMonCli/PresentMonCli.cpp deleted file mode 100644 index 44ba1427c..000000000 --- a/IntelPresentMon/PresentMonCli/PresentMonCli.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include - -int main(int argc, char** argv) -{ - try { - return p2c::cli::Entry(argc, argv); - } - catch (...) { - puts("\nError: Unhandled exception.\n"); - return -1; - } -} \ No newline at end of file diff --git a/IntelPresentMon/PresentMonCli/PresentMonCli.vcxproj b/IntelPresentMon/PresentMonCli/PresentMonCli.vcxproj deleted file mode 100644 index 17a202ba6..000000000 --- a/IntelPresentMon/PresentMonCli/PresentMonCli.vcxproj +++ /dev/null @@ -1,184 +0,0 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - Debug - x64 - - - Release - x64 - - - - 16.0 - Win32Proj - {4197c787-370f-4886-a357-f8a5e1357a9c} - PresentMonCli - 10.0 - - - - Application - true - v143 - Unicode - - - Application - false - v143 - true - Unicode - - - Application - true - v143 - Unicode - - - Application - false - v143 - true - Unicode - - - - - - - - - - - - - - - - - - - - - - - - - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - ..\..\build\$(Configuration)\ - $(ProjectName)-x86 - - - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - ..\..\build\$(Configuration)\ - $(ProjectName)-x86 - - - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - ..\..\build\$(Configuration)\ - - - ..\..\build\obj\$(ProjectName)-$(Platform)-$(Configuration)\ - ..\..\build\$(Configuration)\ - - - false - - - false - - - false - - - false - - - - Level3 - true - true - stdcpplatest - MultiThreadedDebug - ..;%(AdditionalIncludeDirectories) - - - Console - true - - - - - Level3 - true - true - true - true - stdcpplatest - MultiThreaded - ..;%(AdditionalIncludeDirectories) - NDEBUG;%(PreprocessorDefinitions) - - - Console - true - true - true - - - - - Level3 - true - true - stdcpplatest - MultiThreadedDebug - ..;%(AdditionalIncludeDirectories) - - - Console - true - - - - - Level3 - true - true - true - true - stdcpplatest - MultiThreaded - ..;%(AdditionalIncludeDirectories) - NDEBUG;%(PreprocessorDefinitions) - - - Console - true - true - true - - - - - {31551c65-38ea-4e21-8c0d-4bc4a097993c} - - - - - - - - - \ No newline at end of file diff --git a/IntelPresentMon/PresentMonCli/README.md b/IntelPresentMon/PresentMonCli/README.md deleted file mode 100644 index 272b5ec72..000000000 --- a/IntelPresentMon/PresentMonCli/README.md +++ /dev/null @@ -1,282 +0,0 @@ -# PresentMon CLI - -The PresentMon CLI uses the PresentMon Service to report out ETW based frame metrics and sampled CPU/GPU telemetry. It shares many of the command line arguments as the original PresentMon application with additional arguments related to the sampled GPU/CPU telemetry. - -## License - -Copyright (C) 2017-2023 Intel Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -## Command line options - -| Capture Target Options | | -| ---------------------- | ---------------------------------------------------------------------------------------------------------------- | -| `-A, --captureall` | Record all processes (default). | -| `--process_name name` | Record only processes with the provided exe name. This argument can be repeated to capture multiple processes. | -| `--exclude name` | Don't record processes with the provided exe name. This argument can be repeated to exclude multiple processes. | -| `--process_id id` | Record only the process specified by ID. | -| `--etl_file path` | Consume events from an ETW log file instead of running processes. | -| -i, --ignore_case | Ignore case when matching a process name | - -| Output Options | | -| ------------------- | ------------------------------------------------------------------------ | -| `-o, --output_file path` | Write CSV output to the provided path. | -| `-s, --output_stdout` | Write CSV output to STDOUT. | -| `-m, --multi_csv` | Create a separate CSV file for each captured process. | -| `-v, --no_csv` | Do not create any output file. | - -| Recording Options | | -| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| `--delay seconds` | Wait for provided time before starting to record. | -| `--timed seconds` | Stop recording after the provided amount of time. | -| `-d, --exclude_dropped` | Exclude dropped presents from the csv output. | - -| GPU/CPU Options | | -|-----------------|----------------------------------------------------------------------------------------------| -| `--track_gpu_telemetry` | Tracks GPU telemetry relating to power, temperature, frequency, clock speed, etc. | -| `--track_vram_telemetry` | Tracks VRAM telemetry relating to power, temperature, frequency, etc. | -| `--track_gpu_memory` | Tracks GPU memory utilization | -| `--track_gpu_fan` | Tracks GPU fanspeeds | -| `--track_gpu_psu` | Tracks GPU PSU information | -| `--track_gpu_psu` | Tracks GPU PSU information | -| `--track_perf_limit` | Tracks flags denoting current reason for performance limitation | -| `--track_cpu_telemetry` | Tracks CPU telemetry relating to power, temperature, frequency, clock speed, etc. | -| `--track_powershare_bias` | Tracks powershare bias information. | - -| Beta Options | | -| ------------------------- | ------------------------------------------------------------------------------------------------------- | -| `--track_gpu` | Tracks the duration of each process' GPU work performed between presents. Not supported on Win7. | -| `--track_gpu_video` | Track the video encode/decode portion of GPU work separately from other engines. Not supported on Win7. | -| `--track_input` | Tracks the time of keyboard/mouse clicks that were used by each frame. | -| `-track_memory_residency` | Capture CPU time spent in memory residency and paging operations during each frame. | - -| Internal Options | | -| --------------------- | ---------------------------------------------------------- | -| `--track_queue_timers` | Capture Intel D3D11 driver producer/consumer queue timers. | -| `--track_cpu_gpu_sync` | Capture Intel D3D11 driver CPU/GPU syncs. | -| `--track_shader_compilation` | Capture Intel D3D11 driver shader compilation. | - -Note: internal options require a release-internal driver, with the GfxEvents manifest installed from its corresponding TestTools package. Some options may also require specific driver feature branches. - -## Comma-separated value (CSV) file output - -### CSV file names - -By default, PresentMon creates a CSV file named "PresentMon-\

::type; + if constexpr (IsTelemetryRingValue_) { + using SampleType = ipc::TelemetrySample; + return std::make_unique>(qel); + } + else { + assert(false); + return {}; + } + } + + static std::unique_ptr Default(PM_QUERY_ELEMENT&) + { + assert(false); + return {}; + } + }; + } + + std::unique_ptr MakeFrameMetricBinding(PM_QUERY_ELEMENT& qel) + { + return std::make_unique(qel); + } + + std::unique_ptr MakeTelemetryMetricBinding(PM_QUERY_ELEMENT& qel, const pmapi::intro::Root& intro) + { + const auto metricView = intro.FindMetric(qel.metric); + const auto typeInfo = metricView.GetDataTypeInfo(); + return ipc::intro::BridgeDataTypeWithEnum( + typeInfo.GetFrameType(), typeInfo.GetEnumId(), qel); + } + + std::unique_ptr MakeStaticMetricBinding(PM_QUERY_ELEMENT& qel, Middleware& middleware) + { + return std::make_unique(middleware, qel); + } +} diff --git a/IntelPresentMon/PresentMonMiddleware/MetricBinding.h b/IntelPresentMon/PresentMonMiddleware/MetricBinding.h new file mode 100644 index 000000000..4e9d1b8ae --- /dev/null +++ b/IntelPresentMon/PresentMonMiddleware/MetricBinding.h @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include "DynamicMetric.h" +#include "DynamicQueryWindow.h" +#include "../CommonUtilities/Exception.h" +#include "../CommonUtilities/Meta.h" +#include "../CommonUtilities/mc/FrameMetricsMemberMap.h" +#include "../Interprocess/source/HistoryRing.h" +#include "../Interprocess/source/PmStatusError.h" + +namespace pmon::ipc +{ + class MiddlewareComms; +} + +namespace pmon::mid +{ + class SwapChainState; + class Middleware; + + // container to bind and type erase a metric source type to one or more metrics + // (telemetry rings are always 1 metric per ring, but the frame ring serves many metrics) + class MetricBinding + { + public: + virtual ~MetricBinding() = default; + + virtual void Poll(const DynamicQueryWindow& window, uint8_t* pBlobBase, ipc::MiddlewareComms& comms, + const SwapChainState* pSwapChain, uint32_t processId) const = 0; + virtual void Finalize() = 0; + virtual void AddMetricStat(PM_QUERY_ELEMENT& qel, const pmapi::intro::Root& intro) = 0; + }; + + std::unique_ptr MakeFrameMetricBinding(PM_QUERY_ELEMENT& qel); + std::unique_ptr MakeTelemetryMetricBinding(PM_QUERY_ELEMENT& qel, const pmapi::intro::Root& intro); + std::unique_ptr MakeStaticMetricBinding(PM_QUERY_ELEMENT& qel, Middleware& middleware); +} diff --git a/IntelPresentMon/PresentMonMiddleware/Middleware.cpp b/IntelPresentMon/PresentMonMiddleware/Middleware.cpp new file mode 100644 index 000000000..b25467e79 --- /dev/null +++ b/IntelPresentMon/PresentMonMiddleware/Middleware.cpp @@ -0,0 +1,336 @@ +// Copyright (C) 2017-2024 Intel Corporation +// SPDX-License-Identifier: MIT +#include "Middleware.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "../CommonUtilities/mt/Thread.h" +#include "../CommonUtilities/log/Log.h" +#include "../CommonUtilities/Qpc.h" +#include "../Interprocess/source/IntrospectionTransfer.h" +#include "../Interprocess/source/IntrospectionHelpers.h" +#include "../Interprocess/source/IntrospectionCloneAllocators.h" +#include "../Interprocess/source/SystemDeviceId.h" +#include "../Interprocess/source/PmStatusError.h" +#include "../PresentMonAPI2/Internal.h" +#include "../PresentMonAPIWrapperCommon/Introspection.h" +#include "../PresentMonService/GlobalIdentifiers.h" +#include "FrameMetricsSource.h" +#include "FrameEventQuery.h" +#include "DynamicQuery.h" +#include "QueryValidation.h" +#include "ActionClient.h" + +namespace pmon::mid +{ + using namespace ipc::intro; + using namespace util; + namespace rn = std::ranges; + namespace vi = std::views; + + static constexpr size_t kFrameMetricsPerSwapChainCapacity = 4096u; + + Middleware::Middleware(std::optional pipeNameOverride) + { + const auto pipeName = pipeNameOverride.transform(&std::string::c_str) + .value_or(pmon::gid::defaultControlPipeName); + + // Try to open a named pipe to action server; wait for it, if necessary + if (!pipe::DuplexPipe::WaitForAvailability(pipeName, 500)) { + throw util::Except(PM_STATUS_PIPE_ERROR, + "Timeout waiting for service action pipe to become available"); + } + pActionClient_ = std::make_shared(pipeName); + + // connect to the shm server + pComms_ = ipc::MakeMiddlewareComms(pActionClient_->GetShmPrefix(), pActionClient_->GetShmSalt()); + + // Get and cache the introspection data + (void)GetIntrospectionRoot_(); + } + + Middleware::~Middleware() = default; + + const PM_INTROSPECTION_ROOT* Middleware::GetIntrospectionData() + { + // TODO: consider updating cache or otherwise connecting to middleware intro cache here + return pComms_->GetIntrospectionRoot(); + } + + void Middleware::FreeIntrospectionData(const PM_INTROSPECTION_ROOT* pRoot) + { + free(const_cast(pRoot)); + } + + void Middleware::StartTracking(uint32_t targetPid) + { + if (frameMetricsSources_.contains(targetPid)) { + throw util::Except(PM_STATUS_ALREADY_TRACKING_PROCESS, + std::format("Process [{}] is already being tracked", targetPid)); + } + pActionClient_->DispatchSync(StartTracking::Params{ targetPid }); + frameMetricsSources_.emplace(targetPid, + std::make_unique(*pComms_, targetPid, kFrameMetricsPerSwapChainCapacity)); + + pmlog_info(std::format("Started tracking pid [{}]", targetPid)).diag(); + } + + void Middleware::StartPlaybackTracking(uint32_t targetPid, bool isBackpressured) + { + if (frameMetricsSources_.contains(targetPid)) { + throw util::Except(PM_STATUS_ALREADY_TRACKING_PROCESS, + std::format("Process [{}] is already being tracked", targetPid)); + } + pActionClient_->DispatchSync(StartTracking::Params{ + .targetPid = targetPid, + .isPlayback = true, + .isBackpressured = isBackpressured + }); + frameMetricsSources_.emplace(targetPid, + std::make_unique(*pComms_, targetPid, kFrameMetricsPerSwapChainCapacity)); + + pmlog_info(std::format("Started playback tracking pid [{}]", targetPid)).diag(); + } + + void Middleware::StopTracking(uint32_t targetPid) + { + auto it = frameMetricsSources_.find(targetPid); + if (it == frameMetricsSources_.end()) { + throw util::Except(PM_STATUS_INVALID_PID, + std::format("Process [{}] is not currently being tracked", targetPid)); + } + pActionClient_->DispatchSync(StopTracking::Params{ targetPid }); + frameMetricsSources_.erase(it); + + pmlog_info(std::format("Stopped tracking pid [{}]", targetPid)).diag(); + } + + const pmapi::intro::Root& mid::Middleware::GetIntrospectionRoot_() + { + if (!pIntroRoot_) { + pmlog_info("Creating and cacheing introspection root object").diag(); + pIntroRoot_ = std::make_unique(GetIntrospectionData(), [this](auto p){FreeIntrospectionData(p);}); + } + return *pIntroRoot_; + } + + void Middleware::SetTelemetryPollingPeriod(uint32_t deviceId, uint32_t timeMs) + { + // note: deviceId is being ignored for the time being, but might be used in the future + pActionClient_->DispatchSync(SetTelemetryPeriod::Params{ timeMs }); + } + + void Middleware::SetEtwFlushPeriod(std::optional periodMs) + { + pActionClient_->DispatchSync(acts::SetEtwFlushPeriod::Params{ periodMs }); + } + + void Middleware::FlushFrames(uint32_t processId) + { + if (auto it = frameMetricsSources_.find(processId); it != frameMetricsSources_.end() && it->second) { + it->second->Flush(); + } + } + + PM_DYNAMIC_QUERY* Middleware::RegisterDynamicQuery(std::span queryElements, + double windowSizeMs, double metricOffsetMs) + { + pmlog_dbg("Registering dynamic query").pmwatch(queryElements.size()).pmwatch(windowSizeMs).pmwatch(metricOffsetMs); + const auto qpcPeriod = util::GetTimestampPeriodSeconds(); + auto* query = new PM_DYNAMIC_QUERY{ queryElements, windowSizeMs, metricOffsetMs, qpcPeriod, *pComms_, *this }; + RegisterMetricUsage_(query, queryElements); + return query; + } + + void Middleware::FreeDynamicQuery(const PM_DYNAMIC_QUERY* pQuery) + { + if (pQuery == nullptr) { + return; + } + UnregisterMetricUsage_(pQuery); + delete pQuery; + } + + void Middleware::PollDynamicQuery(const PM_DYNAMIC_QUERY* pQuery, uint32_t processId, + uint8_t* pBlob, uint32_t* numSwapChains, std::optional nowTimestamp) + { + if (pQuery == nullptr) { + throw Except(PM_STATUS_BAD_ARGUMENT, "pQuery pointer is null."); + } + if (numSwapChains == nullptr) { + throw Except(PM_STATUS_BAD_ARGUMENT, "numSwapChains pointer is null."); + } + + const uint32_t maxSwapChains = *numSwapChains; + if (maxSwapChains == 0) { + throw Except(PM_STATUS_BAD_ARGUMENT, "numSwapChains is zero."); + } + if (pBlob == nullptr) { + throw Except(PM_STATUS_BAD_ARGUMENT, "pBlob pointer is null."); + } + if (processId == 0 && pQuery->HasFrameMetrics()) { + throw Except(PM_STATUS_BAD_ARGUMENT, + "processId is zero but query requires frame metrics."); + } + + *numSwapChains = 0; + + FrameMetricsSource* pFrameSource = nullptr; + if (processId != 0) { + pFrameSource = &GetFrameMetricSource_(processId); + pFrameSource->Update(); + } + + const auto now = nowTimestamp.value_or((uint64_t)util::GetCurrentTimestamp()); + *numSwapChains = pQuery->Poll(pBlob, *pComms_, now, pFrameSource, processId, maxSwapChains); + } + + void Middleware::PollStaticQuery(const PM_QUERY_ELEMENT& element, uint32_t processId, uint8_t* pBlob) + { + if (pBlob == nullptr) { + throw Except(PM_STATUS_BAD_ARGUMENT, "pBlob pointer is null."); + } + const ipc::StaticMetricValue value = [&]() { + if (element.deviceId == ipc::kSystemDeviceId) { + return pComms_->GetSystemDataStore().FindStaticMetric(element.metric); + } + if (element.deviceId == ipc::kUniversalDeviceId) { + return pComms_->GetFrameDataStore(processId).FindStaticMetric(element.metric); + } + return pComms_->GetGpuDataStore(element.deviceId).FindStaticMetric(element.metric); + }(); + + std::visit([&](auto&& v) { + using T = std::decay_t; + // need stringcopy instead of memcpy for string type data (null terminator) + if constexpr (std::is_same_v) { + strncpy_s(reinterpret_cast(pBlob), PM_MAX_PATH, v, _TRUNCATE); + } + else { + std::memcpy(pBlob, &v, sizeof(v)); + } + }, value); + } + + PM_FRAME_QUERY* mid::Middleware::RegisterFrameEventQuery(std::span queryElements, uint32_t& blobSize) + { + auto pQuery = new PM_FRAME_QUERY{ queryElements, *this, *pComms_, GetIntrospectionRoot_() }; + blobSize = (uint32_t)pQuery->GetBlobSize(); + RegisterMetricUsage_(pQuery, queryElements); + return pQuery; + } + + void mid::Middleware::FreeFrameEventQuery(const PM_FRAME_QUERY* pQuery) + { + UnregisterMetricUsage_(pQuery); + delete const_cast(pQuery); + } + + void mid::Middleware::ConsumeFrameEvents(const PM_FRAME_QUERY* pQuery, uint32_t processId, uint8_t* pBlob, uint32_t& numFrames) + { + if (pQuery == nullptr) { + throw Except(PM_STATUS_BAD_ARGUMENT, "pQuery pointer is null."); + } + if (numFrames > 0 && pBlob == nullptr) { + throw Except(PM_STATUS_BAD_ARGUMENT, "pBlob pointer is null."); + } + const auto framesToCopy = numFrames; + numFrames = 0; + if (framesToCopy == 0) { + return; + } + + // TODO: consider making consume return one frame at a time (eliminate need for heap alloc) + auto frames = GetFrameMetricSource_(processId).Consume(framesToCopy); + assert(frames.size() <= framesToCopy); + for (const auto& frameMetrics : frames) { + pQuery->GatherToBlob(pBlob, processId, frameMetrics); + pBlob += pQuery->GetBlobSize(); + } + + numFrames = uint32_t(frames.size()); + } + + void Middleware::StopPlayback() + { + pActionClient_->DispatchSync(StopPlayback::Params{}); + } + + uint32_t Middleware::StartEtlLogging() + { + return pActionClient_->DispatchSync(StartEtlLogging::Params{}).etwLogSessionHandle; + } + + std::string Middleware::FinishEtlLogging(uint32_t etlLogSessionHandle) + { + return pActionClient_->DispatchSync(FinishEtlLogging::Params{ etlLogSessionHandle }).etlFilePath; + } + + bool Middleware::ServiceConnected() const + { + return pActionClient_->IsRunning(); + } + + FrameMetricsSource& Middleware::GetFrameMetricSource_(uint32_t pid) const + { + if (auto it = frameMetricsSources_.find(pid); + it == frameMetricsSources_.end() || it->second == nullptr) { + pmlog_error("Frame metrics source for process {} doesn't exist. Call pmStartTracking to initialize the client.").diag(); + throw Except(std::format("Failed to find frame metrics source for pid {}", pid)); + } + else { + return *it->second; + } + } + + void Middleware::RegisterMetricUsage_(const void* queryHandle, std::span queryElements) + { + if (queryHandle == nullptr) { + pmlog_warn("Attempting to register metric usage with null query handle"); + return; + } + std::vector keys; + keys.reserve(queryElements.size()); + for (const auto& element : queryElements) { + keys.push_back(QueryMetricKey{ + .metric = element.metric, + .deviceId = element.deviceId, + .arrayIndex = element.arrayIndex, + }); + } + queryMetricUsage_[queryHandle] = std::move(keys); + UpdateMetricUsage_(); + } + + void Middleware::UnregisterMetricUsage_(const void* queryHandle) + { + if (queryHandle == nullptr) { + pmlog_warn("Attempting to unregister metric usage with null query handle"); + return; + } + if (queryMetricUsage_.erase(queryHandle) > 0) { + UpdateMetricUsage_(); + } + } + + void Middleware::UpdateMetricUsage_() + { + std::unordered_set usage; + const auto& introRoot = GetIntrospectionRoot_(); + for (const auto& [handle, elements] : queryMetricUsage_) { + for (const auto& element : elements) { + usage.insert(svc::acts::MetricUse{ + .metricId = element.metric, + .deviceId = element.deviceId, + .arrayIdx = element.arrayIndex, + }); + } + } + pActionClient_->DispatchSync(svc::acts::ReportMetricUse::Params{ std::move(usage) }); + } +} diff --git a/IntelPresentMon/PresentMonMiddleware/Middleware.h b/IntelPresentMon/PresentMonMiddleware/Middleware.h index 39517b807..65e12e96b 100644 --- a/IntelPresentMon/PresentMonMiddleware/Middleware.h +++ b/IntelPresentMon/PresentMonMiddleware/Middleware.h @@ -1,32 +1,73 @@ -#pragma once +#pragma once +#include "../CommonUtilities/win/WinAPI.h" +#include "../Interprocess/source/Interprocess.h" #include "../PresentMonAPI2/PresentMonAPI.h" -#include +#include +#include #include +#include #include +#include +#include +#include "../IntelPresentMon/CommonUtilities/mc/SwapChainState.h" -struct PM_SESSION { virtual ~PM_SESSION() = default; }; +namespace pmapi::intro +{ + class Root; +} namespace pmon::mid { - class Middleware : public PM_SESSION + class FrameMetricsSource; + + class Middleware { public: - virtual const PM_INTROSPECTION_ROOT* GetIntrospectionData() = 0; - virtual void FreeIntrospectionData(const PM_INTROSPECTION_ROOT* pRoot) = 0; - virtual PM_STATUS StartStreaming(uint32_t processId) = 0; - virtual PM_STATUS StopStreaming(uint32_t processId) = 0; - virtual PM_STATUS SetTelemetryPollingPeriod(uint32_t deviceId, uint32_t timeMs) = 0; - virtual PM_STATUS SetEtwFlushPeriod(std::optional periodMs) = 0; - virtual PM_DYNAMIC_QUERY* RegisterDynamicQuery(std::span queryElements, double windowSizeMs, double metricOffsetMs) = 0; - virtual void FreeDynamicQuery(const PM_DYNAMIC_QUERY* pQuery) = 0; - virtual void PollDynamicQuery(const PM_DYNAMIC_QUERY* pQuery, uint32_t processId, uint8_t* pBlob, uint32_t* numSwapChains) = 0; - virtual void PollStaticQuery(const PM_QUERY_ELEMENT& element, uint32_t processId, uint8_t* pBlob) = 0; - virtual PM_FRAME_QUERY* RegisterFrameEventQuery(std::span queryElements, uint32_t& blobSize) = 0; - virtual void FreeFrameEventQuery(const PM_FRAME_QUERY* pQuery) = 0; - virtual void ConsumeFrameEvents(const PM_FRAME_QUERY* pQuery, uint32_t processId, uint8_t* pBlob, uint32_t& numFrames) = 0; - virtual void StopPlayback() = 0; - virtual uint32_t StartEtlLogging() = 0; - virtual std::string FinishEtlLogging(uint32_t etlLogSessionHandle) = 0; - virtual bool ServiceConnected() const { return false; } + Middleware(std::optional pipeNameOverride = {}); + ~Middleware(); + const PM_INTROSPECTION_ROOT* GetIntrospectionData(); + void FreeIntrospectionData(const PM_INTROSPECTION_ROOT* pRoot); + void StartTracking(uint32_t processId); + void StartPlaybackTracking(uint32_t processId, bool isBackpressured); + void StopTracking(uint32_t processId); + void SetTelemetryPollingPeriod(uint32_t deviceId, uint32_t timeMs); + void SetEtwFlushPeriod(std::optional periodMs); + void FlushFrames(uint32_t processId); + PM_DYNAMIC_QUERY* RegisterDynamicQuery(std::span queryElements, double windowSizeMs, double metricOffsetMs); + void FreeDynamicQuery(const PM_DYNAMIC_QUERY* pQuery); + void PollDynamicQuery(const PM_DYNAMIC_QUERY* pQuery, uint32_t processId, uint8_t* pBlob, + uint32_t* numSwapChains, std::optional nowTimestamp = {}); + void PollStaticQuery(const PM_QUERY_ELEMENT& element, uint32_t processId, uint8_t* pBlob); + PM_FRAME_QUERY* RegisterFrameEventQuery(std::span queryElements, uint32_t& blobSize); + void FreeFrameEventQuery(const PM_FRAME_QUERY* pQuery); + void ConsumeFrameEvents(const PM_FRAME_QUERY* pQuery, uint32_t processId, uint8_t* pBlob, uint32_t& numFrames); + void StopPlayback(); + uint32_t StartEtlLogging(); + std::string FinishEtlLogging(uint32_t etlLogSessionHandle); + bool ServiceConnected() const; + private: + struct QueryMetricKey + { + PM_METRIC metric; + uint32_t deviceId; + uint32_t arrayIndex; + }; + // functions + const pmapi::intro::Root& GetIntrospectionRoot_(); + FrameMetricsSource& GetFrameMetricSource_(uint32_t pid) const; + void RegisterMetricUsage_(const void* queryHandle, std::span queryElements); + void UnregisterMetricUsage_(const void* queryHandle); + void UpdateMetricUsage_(); + // data + // action client connection to service RPC + std::shared_ptr pActionClient_; + // ipc shared memory for frame data, telemetry, and introspection + std::unique_ptr pComms_; + // cache of marshalled introspection data + std::unique_ptr pIntroRoot_; + // Frame metrics sources mapped to process id + std::map> frameMetricsSources_; + // Query handles mapped to their metric usage keys + std::unordered_map> queryMetricUsage_; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/PresentMonMiddleware/MockCommon.h b/IntelPresentMon/PresentMonMiddleware/MockCommon.h deleted file mode 100644 index 230ad50b6..000000000 --- a/IntelPresentMon/PresentMonMiddleware/MockCommon.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once -#include "../Interprocess/source/Interprocess.h" - -namespace pmon::ipc::intro -{ - inline void RegisterMockIntrospectionDevices(ServiceComms& comms) - { - using namespace std::string_literals; - { - CpuTelemetryBitset caps; - caps.set(size_t(CpuTelemetryCapBits::cpu_utilization)); - comms.RegisterCpuDevice(PM_DEVICE_VENDOR_INTEL, "Core i7 4770k"s, caps); - } - { - GpuTelemetryBitset caps; - caps.set(size_t(GpuTelemetryCapBits::gpu_power)); - caps.set(size_t(GpuTelemetryCapBits::fan_speed_0)); - comms.RegisterGpuDevice(PM_DEVICE_VENDOR_INTEL, "Arc 750"s, caps); - } - { - GpuTelemetryBitset caps; - caps.set(size_t(GpuTelemetryCapBits::gpu_power)); - caps.set(size_t(GpuTelemetryCapBits::fan_speed_0)); - caps.set(size_t(GpuTelemetryCapBits::fan_speed_1)); - comms.RegisterGpuDevice(PM_DEVICE_VENDOR_NVIDIA, "GeForce RTX 2080 ti"s, caps); - } - comms.FinalizeGpuDevices(); - } -} \ No newline at end of file diff --git a/IntelPresentMon/PresentMonMiddleware/PresentMonMiddleware.vcxproj b/IntelPresentMon/PresentMonMiddleware/PresentMonMiddleware.vcxproj index 1404d9ac0..484a8017c 100644 --- a/IntelPresentMon/PresentMonMiddleware/PresentMonMiddleware.vcxproj +++ b/IntelPresentMon/PresentMonMiddleware/PresentMonMiddleware.vcxproj @@ -1,4 +1,4 @@ - + @@ -12,20 +12,28 @@ - - + + + + + - + - - /bigobj %(AdditionalOptions) - + + + + + + /bigobj /we4062 %(AdditionalOptions) + + @@ -40,12 +48,6 @@ {3a848e5b-a376-4a22-bbac-e9b3c01bd385} - - {66e9f6c5-28db-4218-81b9-31e0e146ecc0} - - - {bf43064b-01f0-4c69-91fb-c2122baf621d} - {c73aa532-e532-4d93-9279-905444653c08} diff --git a/IntelPresentMon/PresentMonMiddleware/PresentMonMiddleware.vcxproj.filters b/IntelPresentMon/PresentMonMiddleware/PresentMonMiddleware.vcxproj.filters index 76c84a6af..a8691c60c 100644 --- a/IntelPresentMon/PresentMonMiddleware/PresentMonMiddleware.vcxproj.filters +++ b/IntelPresentMon/PresentMonMiddleware/PresentMonMiddleware.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -18,13 +18,16 @@ Header Files - + Header Files - + Header Files - + + Header Files + + Header Files @@ -36,22 +39,43 @@ Header Files - + + Header Files + + + Header Files + + Header Files - + + Source Files + + + Source Files + + + Source Files + + Source Files Source Files + + Source Files + Source Files Source Files + + Source Files + \ No newline at end of file diff --git a/IntelPresentMon/PresentMonMiddleware/QueryValidation.cpp b/IntelPresentMon/PresentMonMiddleware/QueryValidation.cpp new file mode 100644 index 000000000..ea3c43c3b --- /dev/null +++ b/IntelPresentMon/PresentMonMiddleware/QueryValidation.cpp @@ -0,0 +1,391 @@ +// Copyright (C) 2017-2024 Intel Corporation +#include "QueryValidation.h" +#include "../PresentMonAPIWrapperCommon/Introspection.h" +#include "../PresentMonAPIWrapperCommon/Exception.h" +#include "../CommonUtilities/Exception.h" +#include "../CommonUtilities/Meta.h" +#include "../CommonUtilities/mc/FrameMetricsMemberMap.h" +#include "../CommonUtilities/log/Log.h" +#include "../Interprocess/source/Interprocess.h" +#include "../Interprocess/source/IntrospectionHelpers.h" +#include "../Interprocess/source/IntrospectionDataTypeMapping.h" +#include "../Interprocess/source/PmStatusError.h" +#include "../Interprocess/source/SystemDeviceId.h" +#include +#include +#include +#include + +namespace ipc = pmon::ipc; +namespace util = pmon::util; + +namespace +{ + void ThrowMalformed_(const char* msg) + { + throw util::Except(PM_STATUS_QUERY_MALFORMED, msg); + } + + bool IsStatSupported_(PM_STAT stat, const pmapi::intro::MetricView& metricView) + { + for (auto statInfo : metricView.GetStatInfo()) { + if (statInfo.GetStat() == stat) { + return true; + } + } + return false; + } + + bool IsDynamicStatSupported_(PM_STAT stat) + { + switch (stat) { + case PM_STAT_AVG: + case PM_STAT_NON_ZERO_AVG: + case PM_STAT_PERCENTILE_99: + case PM_STAT_PERCENTILE_95: + case PM_STAT_PERCENTILE_90: + case PM_STAT_PERCENTILE_01: + case PM_STAT_PERCENTILE_05: + case PM_STAT_PERCENTILE_10: + case PM_STAT_MAX: + case PM_STAT_MIN: + case PM_STAT_MID_POINT: + case PM_STAT_NEWEST_POINT: + case PM_STAT_OLDEST_POINT: + return true; + case PM_STAT_NONE: + case PM_STAT_MID_LERP: + case PM_STAT_COUNT: + default: + return false; + } + } + + bool IsAvgStat_(PM_STAT stat) + { + return stat == PM_STAT_AVG || stat == PM_STAT_NON_ZERO_AVG; + } + + bool IsSupportedDynamicInputType_(PM_DATA_TYPE inType) + { + switch (inType) { + case PM_DATA_TYPE_DOUBLE: + case PM_DATA_TYPE_INT32: + case PM_DATA_TYPE_ENUM: + case PM_DATA_TYPE_UINT32: + case PM_DATA_TYPE_UINT64: + case PM_DATA_TYPE_BOOL: + return true; + default: + return false; + } + } + + bool IsSupportedDynamicOutputType_(PM_DATA_TYPE outType, bool allowBool, bool allowUint32, bool allowUint64) + { + switch (outType) { + case PM_DATA_TYPE_DOUBLE: + case PM_DATA_TYPE_INT32: + case PM_DATA_TYPE_ENUM: + return true; + case PM_DATA_TYPE_BOOL: + return allowBool; + case PM_DATA_TYPE_UINT32: + return allowUint32; + case PM_DATA_TYPE_UINT64: + return allowUint64; + default: + return false; + } + } + + const char* ValidateDynamicStatTypes_(PM_STAT stat, PM_DATA_TYPE inType, PM_DATA_TYPE outType) + { + if (IsAvgStat_(stat)) { + if (outType != PM_DATA_TYPE_DOUBLE) { + return "Dynamic stat average expects double output value"; + } + return nullptr; + } + + const bool allowBool = inType == PM_DATA_TYPE_BOOL; + const bool allowUint32 = inType == PM_DATA_TYPE_UINT32; + const bool allowUint64 = inType == PM_DATA_TYPE_UINT64; + if (!IsSupportedDynamicOutputType_(outType, allowBool, allowUint32, allowUint64)) { + return "Unsupported dynamic stat output data type"; + } + return nullptr; + } + + PM_DATA_TYPE SelectDynamicOutputType_(PM_STAT stat, PM_DATA_TYPE metricOutType) + { + if (IsAvgStat_(stat)) { + return PM_DATA_TYPE_DOUBLE; + } + return metricOutType; + } + + bool IsFrameMetricMapped_(PM_METRIC metric) + { + return util::DispatchEnumValue( + metric, + [&]() -> bool { + return util::metrics::HasFrameMetricMember; + }, + false); + } + + template + struct TelemetryRingValueChecker_ + { + static bool Invoke(PM_ENUM) + { + using ValueType = typename ipc::intro::DataTypeToStaticType::type; + return std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v; + } + static bool Default() + { + return false; + } + }; + + struct QueryKey_ + { + PM_METRIC metric; + uint32_t arrayIndex; + PM_STAT stat; + uint32_t deviceId; + }; + + struct QueryKeyHasher_ + { + size_t operator()(const QueryKey_& key) const noexcept + { + uint64_t h = (uint64_t)key.metric; + h = (h * 1315423911u) ^ (uint64_t)key.arrayIndex; + h = (h * 1315423911u) ^ (uint64_t)key.stat; + h = (h * 1315423911u) ^ (uint64_t)key.deviceId; + return (size_t)h; + } + }; + + struct QueryKeyEqual_ + { + bool operator()(const QueryKey_& lhs, const QueryKey_& rhs) const noexcept + { + return lhs.metric == rhs.metric && + lhs.arrayIndex == rhs.arrayIndex && + lhs.stat == rhs.stat && + lhs.deviceId == rhs.deviceId; + } + }; + + std::string GetStatSymbol_(const pmapi::intro::Root& introRoot, PM_STAT stat) + { + try { + return introRoot.FindEnumKey(PM_ENUM_STAT, (int)stat).GetSymbol(); + } + catch (...) { + return "UnknownStat"; + } + } + + bool IsSupportedTelemetryRingType_(PM_DATA_TYPE dataType, PM_ENUM enumId) + { + return ipc::intro::BridgeDataTypeWithEnum(dataType, enumId); + } +} + +namespace pmon::mid +{ + void ValidateQueryElements(std::span queryElements, PM_METRIC_TYPE queryType, + const pmapi::intro::Root& introRoot, const ipc::MiddlewareComms& comms) + { + const bool isDynamicQuery = queryType == PM_METRIC_TYPE_DYNAMIC; + const bool isFrameQuery = queryType == PM_METRIC_TYPE_FRAME_EVENT; + if (!isDynamicQuery && !isFrameQuery) { + ThrowMalformed_("Invalid query type for validation"); + } + + if (queryElements.empty()) { + pmlog_error("Query requires at least one query element").diag(); + ThrowMalformed_("Empty query"); + } + + std::unordered_map seenKeys; + for (size_t elementIndex = 0; elementIndex < queryElements.size(); ++elementIndex) { + const auto& q = queryElements[elementIndex]; + const auto metricView = introRoot.FindMetric(q.metric); + const auto statSymbol = GetStatSymbol_(introRoot, q.stat); + const auto LogAndThrow = [&](const char* msg) { + pmlog_error(msg) + .pmwatch(metricView.Introspect().GetSymbol()) + .pmwatch(statSymbol) + .pmwatch((int)q.stat) + .pmwatch(q.arrayIndex) + .pmwatch(q.deviceId) + .pmwatch((uint64_t)elementIndex).diag(); + ThrowMalformed_(msg); + }; + + const QueryKey_ key{ + .metric = q.metric, + .arrayIndex = q.arrayIndex, + .stat = q.stat, + .deviceId = q.deviceId, + }; + if (auto it = seenKeys.find(key); it != seenKeys.end()) { + pmlog_error("Duplicate query element") + .pmwatch(metricView.Introspect().GetSymbol()) + .pmwatch(statSymbol) + .pmwatch((int)q.stat) + .pmwatch(q.arrayIndex) + .pmwatch(q.deviceId) + .pmwatch((uint64_t)it->second) + .pmwatch((uint64_t)elementIndex).diag(); + ThrowMalformed_("Duplicate query element"); + } + else { + seenKeys.emplace(key, elementIndex); + } + + const auto metricType = metricView.GetType(); + const bool isStaticMetric = metricType == PM_METRIC_TYPE_STATIC; + + const bool metricTypeOk = isStaticMetric || (isDynamicQuery ? + pmapi::intro::MetricTypeIsDynamic(metricType) : + pmapi::intro::MetricTypeIsFrameEvent(metricType)); + if (!metricTypeOk) { + if (isDynamicQuery) { + LogAndThrow("Dynamic query contains non-dynamic metric"); + } + LogAndThrow("Frame query contains non-frame metric"); + } + + if (isFrameQuery) { + if (q.stat != PM_STAT_NONE) { + pmlog_warn("Frame query stat should be NONE") + .pmwatch(metricView.Introspect().GetSymbol()) + .pmwatch((int)q.stat).diag(); + } + } + else { + if (isStaticMetric) { + if (q.stat != PM_STAT_NONE) { + LogAndThrow("Static metric in dynamic query requires NONE stat"); + } + } + else { + if (!IsStatSupported_(q.stat, metricView)) { + LogAndThrow("Dynamic metric stat not supported by metric"); + } + if (!IsDynamicStatSupported_(q.stat)) { + LogAndThrow("Dynamic metric stat not supported by implementation"); + } + } + } + + const auto typeInfo = metricView.GetDataTypeInfo(); + const auto frameType = typeInfo.GetFrameType(); + const auto polledType = typeInfo.GetPolledType(); + const auto queryDataType = isFrameQuery ? frameType : polledType; + if (ipc::intro::GetDataTypeSize(queryDataType) == 0) { + LogAndThrow("Unsupported query data type"); + } + + if (q.deviceId != ipc::kUniversalDeviceId) { + try { + introRoot.FindDevice(q.deviceId); + } + catch (const pmapi::LookupException&) { + LogAndThrow("Invalid device ID"); + } + } + + const auto deviceMetricInfo = [&]() -> std::optional { + for (auto info : metricView.GetDeviceMetricInfo()) { + if (info.GetDevice().GetId() == q.deviceId) { + return info; + } + } + return std::nullopt; + }(); + + if (!deviceMetricInfo.has_value()) { + if (!isStaticMetric || q.deviceId != ipc::kSystemDeviceId) { + LogAndThrow("Metric not supported by device in query"); + } + if (q.arrayIndex != 0) { + pmlog_error("Query array index out of bounds") + .pmwatch(metricView.Introspect().GetSymbol()) + .pmwatch(statSymbol) + .pmwatch((int)q.stat) + .pmwatch(q.arrayIndex) + .pmwatch(q.deviceId) + .pmwatch((uint64_t)elementIndex) + .pmwatch(1).diag(); + ThrowMalformed_("Query array index out of bounds"); + } + } + else { + if (!deviceMetricInfo->IsAvailable()) { + LogAndThrow("Metric not supported by device in query"); + } + + const auto arraySize = deviceMetricInfo->GetArraySize(); + if (q.arrayIndex >= arraySize) { + pmlog_error("Query array index out of bounds") + .pmwatch(metricView.Introspect().GetSymbol()) + .pmwatch(statSymbol) + .pmwatch((int)q.stat) + .pmwatch(q.arrayIndex) + .pmwatch(q.deviceId) + .pmwatch((uint64_t)elementIndex) + .pmwatch(arraySize).diag(); + ThrowMalformed_("Query array index out of bounds"); + } + } + + if (isFrameQuery && !isStaticMetric && q.deviceId == ipc::kUniversalDeviceId) { + if (!IsFrameMetricMapped_(q.metric)) { + LogAndThrow("Unexpected frame metric in frame query"); + } + } + + if (!isStaticMetric && q.deviceId != ipc::kUniversalDeviceId) { + if (q.deviceId > 0 && q.deviceId <= ipc::kSystemDeviceId) { + const auto& teleMap = q.deviceId == ipc::kSystemDeviceId ? + comms.GetSystemDataStore().telemetryData : + comms.GetGpuDataStore(q.deviceId).telemetryData; + if (teleMap.ArraySize(q.metric) == 0) { + LogAndThrow("Telemetry ring missing for metric in query"); + } + } + else { + LogAndThrow("Invalid device id in query"); + } + } + + if (isDynamicQuery && !isStaticMetric) { + if (!IsSupportedDynamicInputType_(frameType)) { + LogAndThrow("Unsupported dynamic stat input data type"); + } + + const auto outType = SelectDynamicOutputType_(q.stat, polledType); + if (const auto err = ValidateDynamicStatTypes_(q.stat, frameType, outType)) { + LogAndThrow(err); + } + + if (q.deviceId != ipc::kUniversalDeviceId) { + if (!IsSupportedTelemetryRingType_(frameType, typeInfo.GetEnumId())) { + LogAndThrow("Unsupported telemetry ring data type for dynamic query"); + } + } + } + } + } +} diff --git a/IntelPresentMon/PresentMonMiddleware/QueryValidation.h b/IntelPresentMon/PresentMonMiddleware/QueryValidation.h new file mode 100644 index 000000000..58c42f098 --- /dev/null +++ b/IntelPresentMon/PresentMonMiddleware/QueryValidation.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include + +namespace pmapi::intro +{ + class Root; +} + +namespace pmon::ipc +{ + class MiddlewareComms; +} + +namespace pmon::mid +{ + void ValidateQueryElements(std::span queryElements, PM_METRIC_TYPE queryType, + const pmapi::intro::Root& introRoot, const ipc::MiddlewareComms& comms); +} diff --git a/IntelPresentMon/PresentMonService/ActionExecutionContext.cpp b/IntelPresentMon/PresentMonService/ActionExecutionContext.cpp index 44ac3ff04..fec3cd328 100644 --- a/IntelPresentMon/PresentMonService/ActionExecutionContext.cpp +++ b/IntelPresentMon/PresentMonService/ActionExecutionContext.cpp @@ -1,25 +1,36 @@ -#include "ActionExecutionContext.h" +#include "ActionExecutionContext.h" #include #include #include "../Interprocess/source/act/ActionHelper.h" +#include +#include namespace pmon::svc::acts { void ActionExecutionContext::Dispose(SessionContextType& stx) { + // etw log trace cleanup auto& etw = pPmon->GetEtwLogger(); for (auto id : stx.etwLogSessionIds) { if (etw.HasActiveSession(id)) { etw.CancelLogSession(id); } } - for (auto& tracked : stx.trackedPids) { - pPmon->StopStreaming(stx.remotePid, tracked); - } + // tracked pids cleanup + stx.trackedPids.clear(); + pPmon->UpdateTracking(GetTrackedPidSet()); + // telemetry period cleanup stx.requestedTelemetryPeriodMs.reset(); UpdateTelemetryPeriod(); + // etw flush cleanup stx.requestedEtwFlushPeriodMs.reset(); UpdateEtwFlushPeriod(); + // metric use cleanup + pmlog_verb(pmon::util::log::V::met_use)("Session closing, removing metric usage") + .pmwatch(stx.remotePid) + .serialize("sessionMetricUsage", stx.metricUsage); + stx.metricUsage.clear(); + UpdateMetricUsage(); } void ActionExecutionContext::UpdateTelemetryPeriod() const { @@ -45,4 +56,37 @@ namespace pmon::svc::acts throw util::Except(sta); } } -} \ No newline at end of file + void ActionExecutionContext::UpdateMetricUsage() const + { + std::unordered_set aggregateMetricUsage; + std::unordered_set deviceMetricUsage; + auto&& allUsageSets = util::rng::MemberSlice(*pSessionMap, &SessionContextType::metricUsage); + for (auto&& clientUsageSet : allUsageSets) { + for (auto&& usage : clientUsageSet) { + aggregateMetricUsage.insert(usage); + deviceMetricUsage.insert(usage.deviceId); + } + } + if (!hasLastAggregateMetricUsage || aggregateMetricUsage != lastAggregateMetricUsage) { + pmlog_verb(pmon::util::log::V::met_use)("Aggregate metric usage updated") + .serialize("aggregateMetricUsage", aggregateMetricUsage); + lastAggregateMetricUsage = aggregateMetricUsage; + hasLastAggregateMetricUsage = true; + } + pPmon->SetDeviceMetricUsage(std::move(deviceMetricUsage)); + } + + std::unordered_set ActionExecutionContext::GetTrackedPidSet() const + { + std::unordered_set trackedPids; + if (pSessionMap == nullptr) { + return trackedPids; + } + for (auto const& [sid, session] : *pSessionMap) { + for (auto const& [pid, target] : session.trackedPids) { + trackedPids.emplace(pid); + } + } + return trackedPids; + } +} diff --git a/IntelPresentMon/PresentMonService/ActionExecutionContext.h b/IntelPresentMon/PresentMonService/ActionExecutionContext.h index 5f70f595f..5af9e3522 100644 --- a/IntelPresentMon/PresentMonService/ActionExecutionContext.h +++ b/IntelPresentMon/PresentMonService/ActionExecutionContext.h @@ -1,38 +1,98 @@ -#pragma once +#pragma once #include "../Interprocess/source/act/SymmetricActionConnector.h" +#include "../Interprocess/source/ShmNamer.h" +#include "../CommonUtilities/Hash.h" +#include "../CommonUtilities/win/Handle.h" #include #include #include +#include #include #include #include #include +#include +#include +#include +#include + #include "PresentMon.h" #include "Service.h" - +#include "FrameBroadcaster.h" namespace pmon::svc::acts { struct ActionExecutionContext; + // TODO: move this struct into its own header + struct MetricUse + { + PM_METRIC metricId; + uint32_t deviceId; + uint32_t arrayIdx; + + template + void serialize(A& ar) + { + ar(CEREAL_NVP(metricId), + CEREAL_NVP(deviceId), + CEREAL_NVP(arrayIdx)); + } + + bool operator==(const MetricUse& rhs) const + { + return metricId == rhs.metricId && + deviceId == rhs.deviceId && + arrayIdx == rhs.arrayIdx; + } + }; +} + +// Must be visible before any std::unordered_set instantiation +namespace std +{ + template<> + struct hash + { + size_t operator()(const pmon::svc::acts::MetricUse& mu) const noexcept + { + const uint64_t devIdx = + (uint64_t(mu.deviceId) << 32) | uint64_t(mu.arrayIdx); + + // Avoid depending on std::hash existing: + using Under = std::underlying_type_t; + const Under mid = static_cast(mu.metricId); + + return pmon::util::hash::DualHash(mid, devIdx); + } + }; +} +namespace pmon::svc::acts +{ struct ActionSessionContext { // common session context items std::unique_ptr> pConn; uint32_t remotePid = 0; uint32_t nextCommandToken = 0; + // custom items - std::set trackedPids; + struct TrackedTarget + { + std::shared_ptr pSegment; + util::win::Handle processHandle; + }; + std::map trackedPids; + // etl recording functionality support std::set etwLogSessionIds; - std::optional requestedAdapterId; std::optional requestedTelemetryPeriodMs; std::optional requestedEtwFlushPeriodMs; std::string clientBuildId; + std::unordered_set metricUsage; }; struct ActionExecutionContext { - // types using SessionContextType = ActionSessionContext; // data @@ -40,10 +100,16 @@ namespace pmon::svc::acts PresentMon* pPmon = nullptr; const std::unordered_map* pSessionMap = nullptr; std::optional responseWriteTimeoutMs; + mutable std::unordered_set lastAggregateMetricUsage; + mutable bool hasLastAggregateMetricUsage = false; // functions void Dispose(SessionContextType& stx); + + // TODO: refactor so that these functions need not be const void UpdateTelemetryPeriod() const; void UpdateEtwFlushPeriod() const; + void UpdateMetricUsage() const; + std::unordered_set GetTrackedPidSet() const; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/PresentMonService/ActionServer.h b/IntelPresentMon/PresentMonService/ActionServer.h index 3fe41c434..b478d6eb0 100644 --- a/IntelPresentMon/PresentMonService/ActionServer.h +++ b/IntelPresentMon/PresentMonService/ActionServer.h @@ -6,6 +6,7 @@ #include #include "PresentMon.h" #include "Service.h" +#include "FrameBroadcaster.h" namespace pmon::svc { diff --git a/IntelPresentMon/PresentMonService/AllActions.h b/IntelPresentMon/PresentMonService/AllActions.h index b0ddb7db9..5f8805471 100644 --- a/IntelPresentMon/PresentMonService/AllActions.h +++ b/IntelPresentMon/PresentMonService/AllActions.h @@ -2,10 +2,9 @@ #pragma once #include "acts/EnumerateAdapters.h" #include "acts/FinishEtlLogging.h" -#include "acts/GetIntrospectionShmName.h" #include "acts/GetStaticCpuMetrics.h" #include "acts/OpenSession.h" -#include "acts/SelectAdapter.h" +#include "acts/ReportMetricUse.h" #include "acts/SetEtwFlushPeriod.h" #include "acts/SetTelemetryPeriod.h" #include "acts/StartEtlLogging.h" diff --git a/IntelPresentMon/PresentMonService/CliOptions.h b/IntelPresentMon/PresentMonService/CliOptions.h index 9cbd6f834..0ea7e0918 100644 --- a/IntelPresentMon/PresentMonService/CliOptions.h +++ b/IntelPresentMon/PresentMonService/CliOptions.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "../CommonUtilities/cli/CliFramework.h" #include "../CommonUtilities/log/Level.h" #include "../CommonUtilities/log/Verbose.h" @@ -17,8 +17,11 @@ namespace clio private: Group gc_{ this, "Connection", "Control client connection" }; public: Option etwSessionName{ this, "--etw-session-name", "PMService", "Name to use when creating the ETW session" }; Option controlPipe{ this, "--control-pipe", "", "Name of the named pipe to use for the client-service control channel" }; - Option nsmPrefix{ this, "--nsm-prefix", "", "Prefix to use when naming named shared memory segments created for frame data circular buffers" }; - Option introNsm{ this, "--intro-nsm", "", "Name of the NSM used for introspection data" }; + Option shmNamePrefix{ this, "--shm-name-prefix", R"(Global\pm_svc_shm)", "Prefix to use when naming shared memory segments" }; + + private: Group gs_{ this, "Shared Memory", "Shared memory ring sizing" }; public: + Option frameRingSamples{ this, "--frame-ring-samples", 1000, "Number of frame samples to retain per target" }; + Option telemetryRingSamples{ this, "--telemetry-ring-samples", 1500, "Number of telemetry samples to retain per ring" }; private: Group gd_{ this, "Debugging", "Aids in debugging this tool" }; public: Flag debug{ this, "--debug,-d", "Stall service by running in a loop after startup waiting for debugger to connect" }; @@ -44,4 +47,4 @@ namespace clio static constexpr const char* description = "Intel PresentMon service for frame and system performance measurement"; static constexpr const char* name = "PresentMonService.exe"; }; -} \ No newline at end of file +} diff --git a/IntelPresentMon/PresentMonService/FrameBroadcaster.h b/IntelPresentMon/PresentMonService/FrameBroadcaster.h new file mode 100644 index 000000000..635eba3c5 --- /dev/null +++ b/IntelPresentMon/PresentMonService/FrameBroadcaster.h @@ -0,0 +1,111 @@ +#pragma once +#include "../Interprocess/source/Interprocess.h" +#include "../../PresentData/PresentMonTraceConsumer.hpp" +#include "../CommonUtilities/win/Utilities.h" +#include "../CommonUtilities/str/String.h" +#include +#include + +namespace pmon::svc +{ + namespace vi = std::views; + namespace rn = std::ranges; + using ipc::FrameData; + using namespace std::literals; + + class FrameBroadcaster + { + public: + using Segment = ipc::OwnedDataSegment; + FrameBroadcaster(ipc::ServiceComms& comms) : comms_{ comms } {} + std::shared_ptr RegisterTarget(uint32_t pid, bool isPlayback, bool isBackpressured) + { + std::lock_guard lk{ mtx_ }; + auto pSegment = comms_.CreateOrGetFrameDataSegment(pid, isBackpressured); + auto& store = pSegment->GetStore(); + auto& book = store.bookkeeping; + // init bookkeeping only once and only here + if (!book.bookkeepingInitComplete) { + book.processId = pid; // we can also init this static here + book.isPlayback = isPlayback; + book.startQpc = startQpc_; // this member is optionally lazy initialized + book.bookkeepingInitComplete = true; + } + else { // sanity checks for subsequent opens + if (book.processId != pid || book.isPlayback != isPlayback) { + pmlog_error("Mismatch in bookkeeping data") + .pmwatch(book.processId).pmwatch(pid) + .pmwatch(book.isPlayback).pmwatch(isPlayback); + } + } + // initialize name/pid statics on new store segment creation + // for playback, this needs to be deferred to when processing 1st process event + if (!book.staticInitComplete && !isPlayback) { + book.staticInitComplete = true; + try { + store.statics.applicationName = util::win::GetExecutableModulePath( + util::win::OpenProcess(pid) + ).filename().string().c_str(); + } + catch (...) { + pmlog_warn("Process exited right as it was being initialized").pmwatch(pid); + } + } + return pSegment; + } + void Broadcast(const PresentEvent& present, std::optional timeoutMs = {}) + { + std::lock_guard lk{ mtx_ }; + if (auto pSegment = comms_.GetFrameDataSegment(present.ProcessId)) { + pSegment->GetStore().frameData.Push(FrameData::CopyFrameData(present)); + } + } + void HandleTargetProcessEvent(const ProcessEvent& targetProcessEvent) + { + std::lock_guard lk{ mtx_ }; + if (auto pSegment = comms_.GetFrameDataSegment(targetProcessEvent.ProcessId)) { + auto& store = pSegment->GetStore(); + if (!store.bookkeeping.staticInitComplete && store.bookkeeping.isPlayback) { + store.bookkeeping.staticInitComplete = true; + store.statics.applicationName = + util::str::ToNarrow(targetProcessEvent.ImageFileName).c_str(); + } + } + } + + std::vector GetPids() const + { + std::lock_guard lk{ mtx_ }; + return comms_.GetFramePids(); + } + const ipc::ShmNamer& GetNamer() const + { + return comms_.GetNamer(); + } + // TODO: consider how this works when multiple trace sessions (realtime and playback) + // are able to be in flight simultaneously + void SetStartQpc(int64_t startQpc) + { + if (startQpc_ == 0) { + std::lock_guard lk{ mtx_ }; + // set qpc here so that future stores are initialized with it + startQpc_ = startQpc; + // make sure all existing stores get updated right now + for (auto pid : comms_.GetFramePids()) { + try { + auto pSeg = comms_.GetFrameDataSegment(pid); + pSeg->GetStore().bookkeeping.startQpc = startQpc_; + } + catch (...) { + pmlog_warn("Failed getting store for pid, might just be closure race").pmwatch(pid); + } + } + } + } + private: + // data + ipc::ServiceComms& comms_; + mutable std::mutex mtx_; + int64_t startQpc_ = 0; + }; +} diff --git a/IntelPresentMon/PresentMonService/GlobalIdentifiers.h b/IntelPresentMon/PresentMonService/GlobalIdentifiers.h index 943f3dd1b..de192e341 100644 --- a/IntelPresentMon/PresentMonService/GlobalIdentifiers.h +++ b/IntelPresentMon/PresentMonService/GlobalIdentifiers.h @@ -3,7 +3,6 @@ namespace pmon::gid { inline constexpr const char* defaultControlPipeName = R"(\\.\pipe\sharedpresentmonsvcnamedpipe)"; - inline constexpr const char* defaultIntrospectionNsmName = R"(Global\shared_pm2_bip_shm)"; inline constexpr const char* defaultLogPipeBaseName = "shared-pm2-svc-log"; inline constexpr const wchar_t* registryPath = LR"(SOFTWARE\INTEL\PresentMon\Service)"; inline constexpr const char* middlewarePathKey = "sharedMiddlewarePath"; diff --git a/IntelPresentMon/PresentMonService/MockPresentMonSession.cpp b/IntelPresentMon/PresentMonService/MockPresentMonSession.cpp index 420c35489..72ddf78ed 100644 --- a/IntelPresentMon/PresentMonService/MockPresentMonSession.cpp +++ b/IntelPresentMon/PresentMonService/MockPresentMonSession.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2023 Intel Corporation // SPDX-License-Identifier: MIT #include "MockPresentMonSession.h" #include "CliOptions.h" @@ -9,8 +9,9 @@ static const std::wstring kMockEtwSessionName = L"MockETWSession"; using namespace std::literals; -MockPresentMonSession::MockPresentMonSession() +MockPresentMonSession::MockPresentMonSession(svc::FrameBroadcaster& broadcaster) { + pBroadcaster = &broadcaster; ResetEtwFlushPeriod(); } @@ -18,9 +19,7 @@ bool MockPresentMonSession::IsTraceSessionActive() { return session_active_.load(std::memory_order_acquire); } -PM_STATUS MockPresentMonSession::StartStreaming(uint32_t client_process_id, - uint32_t target_process_id, - std::string& nsmFileName) { +PM_STATUS MockPresentMonSession::UpdateTracking(const std::unordered_set& trackedPids) { auto& opt = clio::Options::Get(); @@ -31,13 +30,6 @@ PM_STATUS MockPresentMonSession::StartStreaming(uint32_t client_process_id, return PM_STATUS::PM_STATUS_FAILURE; } - // TODO: hook up all cli options - PM_STATUS status = streamer_.StartStreaming(client_process_id, - target_process_id, nsmFileName, true, opt.pacePlayback, opt.pacePlayback, !opt.pacePlayback, true); - if (status != PM_STATUS::PM_STATUS_SUCCESS) { - return status; - } - std::wstring sessionName; if (opt.etwSessionName.AsOptional().has_value()) { sessionName = pmon::util::str::ToWide(opt.etwSessionName.AsOptional().value()); @@ -46,25 +38,54 @@ PM_STATUS MockPresentMonSession::StartStreaming(uint32_t client_process_id, sessionName = kMockEtwSessionName; } - // TODO: hook up all cli options - status = StartTraceSession(target_process_id, *opt.etlTestFile, sessionName, - true, opt.pacePlayback, opt.pacePlayback, !opt.pacePlayback, true); - if (status == PM_STATUS_FAILURE) { - // Unable to start a trace session. Destroy the NSM and - // return status - streamer_.StopStreaming(target_process_id); - return status; + const bool wasActive = HasLiveTargets(); + std::unordered_map previousState; + { + std::lock_guard lock(tracked_processes_mutex_); + previousState = tracked_pid_live_; + } + SyncTrackedPidState(trackedPids); + const bool isActive = HasLiveTargets(); + if (isActive && (!wasActive || !IsTraceSessionActive())) { + uint32_t target_process_id = 0; + if (!trackedPids.empty()) { + target_process_id = *trackedPids.begin(); + } + // TODO: hook up all cli options + auto status = StartTraceSession(target_process_id, *opt.etlTestFile, sessionName, + true, opt.pacePlayback, opt.pacePlayback, !opt.pacePlayback, true); + if (status != PM_STATUS_SUCCESS) { + { + std::lock_guard lock(tracked_processes_mutex_); + tracked_pid_live_ = std::move(previousState); + } + return status; + } + if (evtStreamingStarted_) { + evtStreamingStarted_.Set(); + } } - return PM_STATUS::PM_STATUS_SUCCESS; -} + { + std::lock_guard lock(tracked_processes_mutex_); + for (auto it = started_processes_.begin(); it != started_processes_.end(); ) { + if (tracked_pid_live_.find(*it) == tracked_pid_live_.end()) { + it = started_processes_.erase(it); + } + else { + ++it; + } + } + } -void MockPresentMonSession::StopStreaming(uint32_t client_process_id, - uint32_t target_process_id) { - streamer_.StopStreaming(client_process_id, target_process_id); - if (streamer_.NumActiveStreams() == 0) { + if (!isActive) { + if (evtStreamingStarted_) { + evtStreamingStarted_.Reset(); + } StopTraceSession(); } + + return PM_STATUS::PM_STATUS_SUCCESS; } bool MockPresentMonSession::CheckTraceSessions(bool forceTerminate) { @@ -75,6 +96,7 @@ bool MockPresentMonSession::CheckTraceSessions(bool forceTerminate) { if (forceTerminate) { StopTraceSession(); + ClearTrackedProcesses(); return true; } return false; @@ -183,24 +205,28 @@ PM_STATUS MockPresentMonSession::StartTraceSession(uint32_t processId, const std void MockPresentMonSession::StopTraceSession() { // PHASE 1: Signal shutdown and wait for threads to observe it - session_active_.store(false, std::memory_order_release); + // also enforce only_once semantics with atomic flag + if (session_active_.exchange(false, std::memory_order_acq_rel)) { - // Stop the trace session. - trace_session_.Stop(); + // Stop the trace session. + trace_session_.Stop(); - // Wait for the consumer and output threads to end (which are using the - // consumers). - WaitForConsumerThreadToExit(); - StopOutputThread(); + // Wait for the consumer and output threads to end (which are using the + // consumers). + WaitForConsumerThreadToExit(); + StopOutputThread(); - // PHASE 2: Safe cleanup after threads have finished - std::lock_guard lock(session_mutex_); + // PHASE 2: Safe cleanup after threads have finished + std::lock_guard lock(session_mutex_); - // Stop all streams - streamer_.StopAllStreams(); + if (evtStreamingStarted_) { + evtStreamingStarted_.Reset(); + } - if (pm_consumer_) { - pm_consumer_.reset(); + if (pm_consumer_) { + pm_consumer_.reset(); + } + started_processes_.clear(); } } @@ -231,10 +257,12 @@ void MockPresentMonSession::AddPresents( auto i = *presentEventIndex; // If session is active and mStartTimestamp contains a value, an etl file is being processed. - // Set this value in the streamer to have the correct start time (atomic guard). + // Set this value in the broadcaster bookkeeping to have the correct start time (atomic guard). if (session_active_.load(std::memory_order_acquire)) { assert(trace_session_.mStartTimestamp.QuadPart != 0); - streamer_.SetStartQpc(trace_session_.mStartTimestamp.QuadPart); + if (pBroadcaster) { + pBroadcaster->SetStartQpc(trace_session_.mStartTimestamp.QuadPart); + } } for (auto n = presentEvents.size(); i < n; ++i) { @@ -252,49 +280,10 @@ void MockPresentMonSession::AddPresents( break; } - // Look up the swapchain this present belongs to. - auto processInfo = GetProcessInfo(presentEvent->ProcessId); - if (!processInfo->mTargetProcess) { + if (!IsProcessTracked(presentEvent->ProcessId)) { continue; } - PresentMonPowerTelemetryInfo power_telemetry = {}; - std::bitset(GpuTelemetryCapBits::gpu_telemetry_count)> - gpu_telemetry_cap_bits = {}; - if (telemetry_container_) { - auto current_adapters = telemetry_container_->GetPowerTelemetryAdapters(); - if (current_adapters.size() != 0 && - current_telemetry_adapter_id_ < current_adapters.size()) { - auto current_telemetry_adapter = - current_adapters.at(current_telemetry_adapter_id_).get(); - if (auto data = current_telemetry_adapter->GetClosest( - presentEvent->PresentStartTime)) { - power_telemetry = *data; - } - gpu_telemetry_cap_bits = current_telemetry_adapter - ->GetPowerTelemetryCapBits(); - } - } - - CpuTelemetryInfo cpu_telemetry = {}; - std::bitset(CpuTelemetryCapBits::cpu_telemetry_count)> - cpu_telemetry_cap_bits = {}; - if (cpu_) { - if (auto data = cpu_->GetClosest(presentEvent->PresentStartTime)) { - cpu_telemetry = *data; - } - cpu_telemetry_cap_bits = cpu_->GetCpuTelemetryCapBits(); - } - - auto result = processInfo->mSwapChain.emplace( - presentEvent->SwapChainAddress, SwapChainData()); - auto chain = &result.first->second; - if (result.second) { - chain->mPresentHistoryCount = 0; - chain->mLastPresentQPC = 0; - chain->mLastDisplayedPresentQPC = 0; - } - // Remove Repeated flips if they are in Application->Repeated or Repeated->Application sequences. for (size_t i = 0, n = presentEvent->Displayed.size(); i + 1 < n; ) { if (presentEvent->Displayed[i].first == FrameType::Application && @@ -312,23 +301,9 @@ void MockPresentMonSession::AddPresents( } } - // Last producer and last consumer are internal fields - // Remove for public build - // Send data to streamer if we have more than single present event - streamer_.ProcessPresentEvent( - presentEvent.get(), &power_telemetry, &cpu_telemetry, - chain->mLastPresentQPC, chain->mLastDisplayedPresentQPC, - processInfo->mModuleName, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - + // timeout set for 1000 ms + pBroadcaster->Broadcast(*presentEvent, 1000); - chain->mLastPresentQPC = presentEvent->PresentStartTime; - if (presentEvent->FinalState == PresentResult::Presented) { - chain->mLastDisplayedPresentQPC = presentEvent->Displayed.empty() ? 0 : presentEvent->Displayed[0].second; - } - else if (chain->mLastDisplayedPresentQPC == chain->mLastPresentQPC) { - chain->mLastDisplayedPresentQPC = 0; - } } *presentEventIndex = i; @@ -471,44 +446,25 @@ void MockPresentMonSession::StopOutputThread() { } } -ProcessInfo* MockPresentMonSession::GetProcessInfo(uint32_t processId) { - auto result = processes_.emplace(processId, ProcessInfo()); - auto processInfo = &result.first->second; - auto newProcess = result.second; - - if (newProcess) { - InitProcessInfo(processInfo, processId, INVALID_HANDLE_VALUE, L"MockProcess.exe"); - } - return processInfo; -} - -void MockPresentMonSession::InitProcessInfo(ProcessInfo* processInfo, uint32_t processId, - HANDLE handle, std::wstring const& processName) { - - processInfo->mHandle = handle; - processInfo->mModuleName = processName; - processInfo->mTargetProcess = true; - - target_process_count_ += 1; -} - void MockPresentMonSession::UpdateProcesses( std::vector const& processEvents, std::vector>* terminatedProcesses) { + (void)terminatedProcesses; for (auto const& processEvent : processEvents) { + if (!IsProcessTracked(processEvent.ProcessId)) { + continue; + } if (processEvent.IsStartEvent) { - // This event is a new process starting, the pid should not already be - // in processes_. - auto result = processes_.emplace(processEvent.ProcessId, ProcessInfo()); - auto processInfo = &result.first->second; - auto newProcess = result.second; - if (newProcess) { - InitProcessInfo(processInfo, processEvent.ProcessId, NULL, - processEvent.ImageFileName); + if (started_processes_.insert(processEvent.ProcessId).second) { + pBroadcaster->HandleTargetProcessEvent(processEvent); } } } } void MockPresentMonSession::HandleTerminatedProcess(uint32_t processId) { -} \ No newline at end of file + MarkProcessExited(processId); + if (!HasLiveTrackedProcesses() && evtStreamingStarted_) { + evtStreamingStarted_.Reset(); + } +} diff --git a/IntelPresentMon/PresentMonService/MockPresentMonSession.h b/IntelPresentMon/PresentMonService/MockPresentMonSession.h index 5b2ffbd02..945686890 100644 --- a/IntelPresentMon/PresentMonService/MockPresentMonSession.h +++ b/IntelPresentMon/PresentMonService/MockPresentMonSession.h @@ -1,21 +1,23 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2023 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include "PresentMonSession.h" #include "../CommonUtilities/win/Event.h" +#include + +using namespace pmon; class MockPresentMonSession : public PresentMonSession { public: // functions - MockPresentMonSession(); + MockPresentMonSession(svc::FrameBroadcaster& broadcaster); MockPresentMonSession(const MockPresentMonSession& t) = delete; MockPresentMonSession& operator=(const MockPresentMonSession& t) = delete; ~MockPresentMonSession() override = default; bool IsTraceSessionActive() override; - PM_STATUS StartStreaming(uint32_t client_process_id, uint32_t target_process_id, std::string& nsmFileName) override; - void StopStreaming(uint32_t client_process_id, uint32_t target_process_id) override; + PM_STATUS UpdateTracking(const std::unordered_set& trackedPids) override; bool CheckTraceSessions(bool forceTerminate) override; HANDLE GetStreamingStartHandle() override; void ResetEtwFlushPeriod() override; @@ -54,9 +56,6 @@ class MockPresentMonSession : public PresentMonSession void Consume(TRACEHANDLE traceHandle); void Output(); - ProcessInfo* GetProcessInfo(uint32_t processId); - void InitProcessInfo(ProcessInfo* processInfo, uint32_t processId, - HANDLE handle, std::wstring const& processName); void UpdateProcesses( std::vector const& processEvents, std::vector>* terminatedProcesses); @@ -73,17 +72,15 @@ class MockPresentMonSession : public PresentMonSession std::atomic quit_output_thread_ = false; std::atomic stop_playback_requested_ = false; - std::unordered_map processes_; + std::unordered_set started_processes_; // Note we only support a single ETL session at a time uint32_t etlProcessId_ = 0; - // TODO: Determine if this actually does anything! - uint32_t target_process_count_ = 0; - // Event for when streaming has started (needed to satisfy virtual interface) pmon::util::win::Event evtStreamingStarted_; mutable std::mutex session_mutex_; + // TODO: evaluate necessity/improvements on this construct std::atomic session_active_{false}; // Lock-free session state for hot path queries -}; \ No newline at end of file +}; diff --git a/IntelPresentMon/PresentMonService/PMMainThread.cpp b/IntelPresentMon/PresentMonService/PMMainThread.cpp index fb01e5158..15bf5d8c7 100644 --- a/IntelPresentMon/PresentMonService/PMMainThread.cpp +++ b/IntelPresentMon/PresentMonService/PMMainThread.cpp @@ -5,16 +5,22 @@ #include "ActionServer.h" #include "PresentMon.h" #include "PowerTelemetryContainer.h" +#include "FrameBroadcaster.h" #include "..\ControlLib\WmiCpu.h" -#include "..\PresentMonUtils\StringUtils.h" #include #include "../Interprocess/source/Interprocess.h" +#include "../Interprocess/source/ShmNamer.h" +#include "../Interprocess/source/MetricCapabilitiesShim.h" +#include "../Interprocess/source/SystemDeviceId.h" #include "CliOptions.h" +#include "Registry.h" #include "GlobalIdentifiers.h" +#include #include #include "../CommonUtilities/IntervalWaiter.h" #include "../CommonUtilities/PrecisionWaiter.h" #include "../CommonUtilities/win/Event.h" +#include "../CommonUtilities/log/IdentificationTable.h" #include "../CommonUtilities/log/GlogShim.h" #include "testing/TestControl.h" @@ -25,13 +31,9 @@ using namespace pmon; using namespace svc; using namespace util; using v = log::V; +namespace vi = std::views; -std::string GetIntrospectionShmName() -{ - return clio::Options::Get().introNsm.AsOptional().value_or(gid::defaultIntrospectionNsmName); -} - void EventFlushThreadEntry_(Service* const srv, PresentMon* const pm) { if (srv == nullptr || pm == nullptr) { @@ -42,160 +44,504 @@ void EventFlushThreadEntry_(Service* const srv, PresentMon* const pm) // this is the interval to wait when manual flush is disabled // we still want to run the inner loop to poll in case it gets enabled const uint32_t disabledIntervalMs = 250u; - double currentInterval = (double)pm->GetEtwFlushPeriod().value_or(disabledIntervalMs) / 1000.; - IntervalWaiter waiter{ currentInterval }; - + // waiter interval will be set later before it is actually used; set 0 placeholder + IntervalWaiter waiter{ 0. }; // outer dormant loop waits for either start of process tracking or service exit while (true) { pmlog_verb(v::etwq)("Begin idle ETW flush wait"); // if event index 0 is signalled that means we are stopping - if (*win::WaitAnyEvent(srv->GetServiceStopHandle(), pm->GetStreamingStartHandle()) == 0) { + if (win::WaitAnyEvent(srv->GetServiceStopHandle(), pm->GetStreamingStartHandle()) == 0) { pmlog_dbg("exiting ETW flush thread due to stop handle"); return; } pmlog_verb(v::etwq)("Entering ETW flush inner active loop"); + // read flush period here right before first wait + auto currentInterval = (double)pm->GetEtwFlushPeriod().value_or(disabledIntervalMs) / 1000.; // otherwise we assume streaming has started and we begin the flushing loop, checking for stop signal while (!win::WaitAnyEventFor(0s, srv->GetServiceStopHandle())) { // use interval wait to time flushes as a fixed cadence + pmlog_verb(v::etwq)("Wait on ETW flush interval (period)").pmwatch(currentInterval); waiter.SetInterval(currentInterval); waiter.Wait(); // go dormant if there are no active streams left // TODO: GetActiveStreams is not technically thread-safe, reconsider fixing this stuff in Service - if (pm->GetActiveStreams() == 0) { + if (!pm->HasLiveTargets()) { pmlog_dbg("ETW flush loop entering dormancy due to 0 active streams"); break; } - // check to see if manual flush is enabled if (auto flushPeriodMs = pm->GetEtwFlushPeriod()) { // flush events manually to reduce latency pmlog_verb(v::etwq)("Manual ETW flush").pmwatch(*flushPeriodMs); pm->FlushEvents(); - currentInterval = double(*flushPeriodMs) / 1000.; + // update flush period + currentInterval = *flushPeriodMs / 1000.; } else { pmlog_verb(v::etwq)("Detected disabled ETW flush, using idle poll period"); + // set a period here (low rate) to check for changes to the etw flush setting currentInterval = disabledIntervalMs / 1000.; } } } } +struct DerivedGpuTelemetry_ +{ + double memUtilization = 0.0; + std::array fanSpeedPercent{}; + size_t fanSpeedPercentCount = 0; +}; + +static DerivedGpuTelemetry_ CalculateDerivedGpuTelemetry_( + const ipc::GpuDataStore& store, + const PresentMonPowerTelemetryInfo& s) noexcept +{ + DerivedGpuTelemetry_ out{}; + if (store.statics.memSize > 0) { + out.memUtilization = 100.0 * (static_cast(s.gpu_mem_used_b) / + static_cast(store.statics.memSize)); + } + + const size_t maxFanCount = store.statics.maxFanSpeedRpm.size(); + const size_t fanCount = std::min(maxFanCount, s.fan_speed_rpm.size()); + out.fanSpeedPercentCount = fanCount; + for (size_t i = 0; i < fanCount; ++i) { + const auto maxRpm = store.statics.maxFanSpeedRpm[i]; + if (maxRpm > 0) { + out.fanSpeedPercent[i] = s.fan_speed_rpm[i] / static_cast(maxRpm); + } + else { + out.fanSpeedPercent[i] = 0.0; + } + } + + return out; +} + +// Translate a single power-telemetry sample into the rings for one GPU store. +// This mirrors the CPU placeholder approach but handles double/uint64/bool rings. +static void PopulateGpuTelemetryRings_( + ipc::GpuDataStore& store, + const PresentMonPowerTelemetryInfo& s) noexcept +{ + const auto derived = CalculateDerivedGpuTelemetry_(store, s); + for (auto&& [metric, ringVariant] : store.telemetryData.Rings()) { + switch (metric) { + + // -------- double metrics -------- + + case PM_METRIC_GPU_POWER: + std::get>(ringVariant)[0] + .Push(s.gpu_power_w, s.qpc); + break; + + case PM_METRIC_GPU_VOLTAGE: + std::get>(ringVariant)[0] + .Push(s.gpu_voltage_v, s.qpc); + break; + + case PM_METRIC_GPU_FREQUENCY: + std::get>(ringVariant)[0] + .Push(s.gpu_frequency_mhz, s.qpc); + break; + + case PM_METRIC_GPU_EFFECTIVE_FREQUENCY: + std::get>(ringVariant)[0] + .Push(s.gpu_effective_frequency_mhz, s.qpc); + break; + + case PM_METRIC_GPU_TEMPERATURE: + std::get>(ringVariant)[0] + .Push(s.gpu_temperature_c, s.qpc); + break; + + case PM_METRIC_GPU_VOLTAGE_REGULATOR_TEMPERATURE: + std::get>(ringVariant)[0] + .Push(s.gpu_voltage_regulator_temperature_c, s.qpc); + break; + + case PM_METRIC_GPU_UTILIZATION: + std::get>(ringVariant)[0] + .Push(s.gpu_utilization, s.qpc); + break; + + case PM_METRIC_GPU_RENDER_COMPUTE_UTILIZATION: + std::get>(ringVariant)[0] + .Push(s.gpu_render_compute_utilization, s.qpc); + break; + + case PM_METRIC_GPU_MEDIA_UTILIZATION: + std::get>(ringVariant)[0] + .Push(s.gpu_media_utilization, s.qpc); + break; + + case PM_METRIC_GPU_MEM_EFFECTIVE_BANDWIDTH: + std::get>(ringVariant)[0] + .Push(s.gpu_mem_effective_bandwidth_gbps, s.qpc); + break; + + case PM_METRIC_GPU_OVERVOLTAGE_PERCENT: + std::get>(ringVariant)[0] + .Push(s.gpu_overvoltage_percent, s.qpc); + break; + + case PM_METRIC_GPU_TEMPERATURE_PERCENT: + std::get>(ringVariant)[0] + .Push(s.gpu_temperature_percent, s.qpc); + break; + + case PM_METRIC_GPU_POWER_PERCENT: + std::get>(ringVariant)[0] + .Push(s.gpu_power_percent, s.qpc); + break; + + case PM_METRIC_GPU_CARD_POWER: + std::get>(ringVariant)[0] + .Push(s.gpu_card_power_w, s.qpc); + break; + + case PM_METRIC_GPU_FAN_SPEED: + { + auto& ringVect = + std::get>(ringVariant); + + const size_t n = std::min(ringVect.size(), s.fan_speed_rpm.size()); + for (size_t i = 0; i < n; ++i) { + ringVect[i].Push(s.fan_speed_rpm[i], s.qpc); + } + break; + } + + case PM_METRIC_GPU_FAN_SPEED_PERCENT: + { + auto& ringVect = + std::get>(ringVariant); + const size_t n = std::min(ringVect.size(), derived.fanSpeedPercentCount); + for (size_t i = 0; i < n; ++i) { + ringVect[i].Push(derived.fanSpeedPercent[i], s.qpc); + } + break; + } + + // VRAM-related doubles + case PM_METRIC_GPU_MEM_POWER: + std::get>(ringVariant)[0] + .Push(s.vram_power_w, s.qpc); + break; + + case PM_METRIC_GPU_MEM_VOLTAGE: + std::get>(ringVariant)[0] + .Push(s.vram_voltage_v, s.qpc); + break; + + case PM_METRIC_GPU_MEM_FREQUENCY: + std::get>(ringVariant)[0] + .Push(s.vram_frequency_mhz, s.qpc); + break; + + case PM_METRIC_GPU_MEM_EFFECTIVE_FREQUENCY: + std::get>(ringVariant)[0] + .Push(s.vram_effective_frequency_gbps, s.qpc); + break; + + case PM_METRIC_GPU_MEM_TEMPERATURE: + std::get>(ringVariant)[0] + .Push(s.vram_temperature_c, s.qpc); + break; + + // Memory bandwidth doubles + case PM_METRIC_GPU_MEM_WRITE_BANDWIDTH: + std::get>(ringVariant)[0] + .Push(s.gpu_mem_write_bandwidth_bps, s.qpc); + break; + + case PM_METRIC_GPU_MEM_READ_BANDWIDTH: + std::get>(ringVariant)[0] + .Push(s.gpu_mem_read_bandwidth_bps, s.qpc); + break; + + case PM_METRIC_GPU_MEM_UTILIZATION: + std::get>(ringVariant)[0] + .Push(derived.memUtilization, s.qpc); + break; + + // -------- uint64 metrics -------- + case PM_METRIC_GPU_MEM_USED: + std::get>(ringVariant)[0] + .Push(s.gpu_mem_used_b, s.qpc); + break; + + // -------- bool metrics -------- + case PM_METRIC_GPU_POWER_LIMITED: + std::get>(ringVariant)[0] + .Push(s.gpu_power_limited, s.qpc); + break; + + case PM_METRIC_GPU_TEMPERATURE_LIMITED: + std::get>(ringVariant)[0] + .Push(s.gpu_temperature_limited, s.qpc); + break; + + case PM_METRIC_GPU_CURRENT_LIMITED: + std::get>(ringVariant)[0] + .Push(s.gpu_current_limited, s.qpc); + break; + + case PM_METRIC_GPU_VOLTAGE_LIMITED: + std::get>(ringVariant)[0] + .Push(s.gpu_voltage_limited, s.qpc); + break; + + case PM_METRIC_GPU_UTILIZATION_LIMITED: + std::get>(ringVariant)[0] + .Push(s.gpu_utilization_limited, s.qpc); + break; + + case PM_METRIC_GPU_MEM_POWER_LIMITED: + std::get>(ringVariant)[0] + .Push(s.vram_power_limited, s.qpc); + break; + + case PM_METRIC_GPU_MEM_TEMPERATURE_LIMITED: + std::get>(ringVariant)[0] + .Push(s.vram_temperature_limited, s.qpc); + break; + + case PM_METRIC_GPU_MEM_CURRENT_LIMITED: + std::get>(ringVariant)[0] + .Push(s.vram_current_limited, s.qpc); + break; + + case PM_METRIC_GPU_MEM_VOLTAGE_LIMITED: + std::get>(ringVariant)[0] + .Push(s.vram_voltage_limited, s.qpc); + break; + + case PM_METRIC_GPU_MEM_UTILIZATION_LIMITED: + std::get>(ringVariant)[0] + .Push(s.vram_utilization_limited, s.qpc); + break; + + default: + pmlog_warn("Unhandled metric").pmwatch((int)metric); + break; + } + } +} + + void PowerTelemetryThreadEntry_(Service* const srv, PresentMon* const pm, PowerTelemetryContainer* const ptc, ipc::ServiceComms* const pComms) { - if (srv == nullptr || pm == nullptr || ptc == nullptr) { - // TODO: log error here - return; - } - - // we first wait for a client control connection before populating telemetry container - // after populating, we sample each adapter to gather availability information - // this is deferred until client connection in order to increase the probability that - // telemetry metric availability is accurately assessed - { - const HANDLE events[]{ - srv->GetClientSessionHandle(), - srv->GetServiceStopHandle(), - }; - const auto waitResult = WaitForMultipleObjects((DWORD)std::size(events), events, FALSE, INFINITE); - // TODO: check for wait result error - // if events[1] was signalled, that means service is stopping so exit thread - if ((waitResult - WAIT_OBJECT_0) == 1) { + using util::win::WaitAnyEvent; + using util::win::WaitAnyEventFor; + try { + util::log::IdentificationTable::AddThisThread("tele-gpu"); + pmlog_dbg("Starting gpu telemetry thread"); + if (srv == nullptr || pm == nullptr || ptc == nullptr || pComms == nullptr) { + pmlog_error("Required parameter was null"); return; } - pmon::util::QpcTimer timer; - ptc->Repopulate(); - for (auto& adapter : ptc->GetPowerTelemetryAdapters()) { - // sample 2x here as workaround/kludge because Intel provider misreports 1st sample - adapter->Sample(); - adapter->Sample(); - uint64_t luid = adapter->GetAdapterId(); - std::span bytes; - if (luid == 0) { - // if we have no LUID, send empty span - bytes = std::span{}; + + // we first wait for a client control connection before populating telemetry container + // after populating, we refresh each adapter to gather availability information + // this is deferred until client connection in order to increase the probability that + // telemetry metric availability is accurately assessed + { + if (WaitAnyEvent(srv->GetClientSessionHandle(), srv->GetServiceStopHandle()) == 1) { + // if events[1] was signalled, that means service is stopping so exit thread + pmlog_dbg("Exiting gpu telemetry thread before initialization"); + return; } - else { - bytes = std::span(reinterpret_cast(&luid), sizeof(luid)); + pmon::util::QpcTimer timer; + ptc->Repopulate(); + for (auto&& adapter : ptc->GetPowerTelemetryAdapters()) { + const auto deviceId = adapter->GetDeviceId(); + // refresh 2x here as workaround/kludge because Intel provider misreports 1st sample + adapter->Sample(); + const auto sample = adapter->Sample(); + // populate luid information for adapter (currently intel-only) + uint64_t luid = adapter->GetAdapterId(); + std::span luidBytes; + if (luid == 0) { + // if we have no LUID, send empty span + luidBytes = std::span{}; + } + else { + luidBytes = std::span(reinterpret_cast(&luid), sizeof(luid)); + } + pComms->RegisterGpuDevice(deviceId, adapter->GetVendor(), adapter->GetName(), + ipc::intro::ConvertBitset(adapter->GetPowerTelemetryCapBits()), luidBytes); + // after registering, we know that at least the store is available even + // if the introspection itself is not complete + auto& gpuStore = pComms->GetGpuDataStore(deviceId); + // TODO: replace this placeholder routine for populating statics + gpuStore.statics.name = adapter->GetName().c_str(); + gpuStore.statics.vendor = adapter->GetVendor(); + gpuStore.statics.memSize = adapter->GetDedicatedVideoMemory(); + gpuStore.statics.maxMemBandwidth = adapter->GetVideoMemoryMaxBandwidth(); + gpuStore.statics.sustainedPowerLimit = adapter->GetSustainedPowerLimit(); + // max fanspeed is polled in old system but static in new system, shim here + // TODO: make this fully static + // infer number of fans by the size of the telemetry ring array for fan speed + const auto nFans = gpuStore.telemetryData.ArraySize(PM_METRIC_GPU_FAN_SPEED); + for (size_t i = 0; i < nFans; i++) { + gpuStore.statics.maxFanSpeedRpm.push_back(sample.max_fan_speed_rpm[i]); + } + } + pComms->FinalizeGpuDevices(); + pmlog_info(std::format("Finished populating GPU telemetry introspection, {} seconds elapsed", timer.Mark())); + } + + // only start periodic polling when streaming starts + // exit polling loop and this thread when service is stopping + { + IntervalWaiter waiter{ 0.016 }; + while (true) { + pmlog_dbg("(re)starting gpu idle wait"); + if (WaitAnyEvent(pm->GetDeviceUsageEvent(), srv->GetServiceStopHandle()) == 1) { + pmlog_dbg("gpu telemetry received exit code, thread exiting"); + return; + } + else { + // if any of our gpu telemetry devices are active enter polling loop + bool hasActive = false; + for (auto&& adapter : ptc->GetPowerTelemetryAdapters()) { + const auto deviceId = adapter->GetDeviceId(); + if (pm->CheckDeviceMetricUsage(deviceId)) { + pmlog_dbg("detected gpu active").pmwatch(deviceId); + hasActive = true; + break; + } + } + if (!hasActive) { + pmlog_dbg("received device usage event, but no gpu tele device was active"); + continue; + } + } + // otherwise we assume streaming has started and we begin the polling loop + // TODO: consider poll loop per device architecture instead of loop all + pmlog_dbg("entering gpu tele active poll loop"); + while (!WaitAnyEventFor(0ms, srv->GetServiceStopHandle())) { + // if device was reset (driver installed etc.) we need to repopulate telemetry + if (WaitAnyEventFor(0ms, srv->GetResetPowerTelemetryHandle())) { + // TODO: log error here or inside of repopulate + ptc->Repopulate(); + } + // poll all gpu adapter devices, skipping inactive devices + auto& adapters = ptc->GetPowerTelemetryAdapters(); + for (auto&& adapter : adapters) { + const auto deviceId = adapter->GetDeviceId(); + if (!pm->CheckDeviceMetricUsage(deviceId)) { + continue; + } + // Get the newest sample from the provider + const auto sample = adapter->Sample(); + + // Retrieve the matching GPU store. + auto& store = pComms->GetGpuDataStore(deviceId); + + PopulateGpuTelemetryRings_(store, sample); + } + // Convert from the ms to seconds as GetTelemetryPeriod returns back + // ms and SetInterval expects seconds. + waiter.SetInterval(pm->GetGpuTelemetryPeriod() / 1000.); + waiter.Wait(); + // conditions for ending active poll and returning to idle state + // go dormant if no gpu devices are in use + const bool anyUsed = std::ranges::any_of(adapters, + [&](const auto& adapter) { return pm->CheckDeviceMetricUsage(adapter->GetDeviceId()); }); + if (!anyUsed) { + break; + } + } } - pComms->RegisterGpuDevice(adapter->GetVendor(), adapter->GetName(), adapter->GetPowerTelemetryCapBits(), bytes); } - pComms->FinalizeGpuDevices(); - pmlog_info(std::format("Finished populating GPU telemetry introspection, {} seconds elapsed", timer.Mark())); } + catch (...) { + pmlog_error(util::ReportException("Exception leaked to top level of gpu telemetry thread")); + } +} + +void CpuTelemetryThreadEntry_(Service* const srv, PresentMon* const pm, ipc::ServiceComms* pComms, + pwr::cpu::CpuTelemetry* const cpu) noexcept +{ + using util::win::WaitAnyEvent; + using util::win::WaitAnyEventFor; + // we don't expect any exceptions in this system during normal operation + // (maybe during initialization at most, not during polling) + // but if they do happen, it is a halting condition for the system telemetry + // don't let this thread crash the process, just exit with an error for later + // diagnosis + try { + util::log::IdentificationTable::AddThisThread("tele-sys"); + pmlog_dbg("Starting system telemetry thread"); + if (srv == nullptr || pm == nullptr || pComms == nullptr || cpu == nullptr) { + pmlog_error("Required parameter was null"); + return; + } - // only start periodic polling when streaming starts - // exit polling loop and this thread when service is stopping - { IntervalWaiter waiter{ 0.016 }; - const HANDLE events[]{ - pm->GetStreamingStartHandle(), - srv->GetServiceStopHandle(), - }; - while (1) { - auto waitResult = WaitForMultipleObjects((DWORD)std::size(events), events, FALSE, INFINITE); - // TODO: check for wait result error - // if events[1] was signalled, that means service is stopping so exit thread - if ((waitResult - WAIT_OBJECT_0) == 1) { + while (true) { + pmlog_dbg("(re)starting system idle wait"); + if (WaitAnyEvent(pm->GetDeviceUsageEvent(), srv->GetServiceStopHandle()) == 1) { + pmlog_dbg("system telemetry received exit code, thread exiting"); return; } - // otherwise we assume streaming has started and we begin the polling loop - while (WaitForSingleObject(srv->GetServiceStopHandle(), 0) != WAIT_OBJECT_0) { - // if device was reset (driver installed etc.) we need to repopulate telemetry - if (WaitForSingleObject(srv->GetResetPowerTelemetryHandle(), 0) == WAIT_OBJECT_0) { - // TODO: log error here or inside of repopulate - ptc->Repopulate(); + else { + // if system telemetry metrics active enter active polling loop + if (pm->CheckDeviceMetricUsage(ipc::kSystemDeviceId)) { + pmlog_dbg("detected system active"); + } + else { + pmlog_dbg("received device usage event, but system tele device was not active"); + continue; } - for (auto& adapter : ptc->GetPowerTelemetryAdapters()) { - adapter->Sample(); + } + while (!WaitAnyEventFor(0ms, srv->GetServiceStopHandle())) { + // TODO:streamer replace this flow with a call that populates rings of a store + // placeholder routine shim to translate cpu tranfer struct into rings + // replace with a direct mapping on PM_METRIC that obviates the switch + auto& store = pComms->GetSystemDataStore(); + const auto sample = cpu->Sample(); + for (auto&& [metric, ringVariant] : store.telemetryData.Rings()) { + // all cpu telemetry is double + auto& ringVect = std::get>(ringVariant); + switch (metric) { + case PM_METRIC_CPU_FREQUENCY: + ringVect[0].Push(sample.cpu_frequency, sample.qpc); + break; + case PM_METRIC_CPU_UTILIZATION: + ringVect[0].Push(sample.cpu_utilization, sample.qpc); + break; + case PM_METRIC_CPU_POWER: + ringVect[0].Push(sample.cpu_power_w, sample.qpc); + break; + case PM_METRIC_CPU_POWER_LIMIT: + ringVect[0].Push(sample.cpu_power_limit_w, sample.qpc); + break; + case PM_METRIC_CPU_TEMPERATURE: + ringVect[0].Push(sample.cpu_temperature, sample.qpc); + break; + default: + pmlog_warn("Unhandled metric ring").pmwatch((int)metric); + break; + } } // Convert from the ms to seconds as GetTelemetryPeriod returns back // ms and SetInterval expects seconds. - waiter.SetInterval(pm->GetGpuTelemetryPeriod()/1000.); + waiter.SetInterval(pm->GetGpuTelemetryPeriod() / 1000.); waiter.Wait(); - // go dormant if there are no active streams left - // TODO: consider race condition here if client stops and starts streams rapidly - if (pm->GetActiveStreams() == 0) { + // conditions for ending active poll and returning to idle state + if (!pm->CheckDeviceMetricUsage(ipc::kSystemDeviceId)) { break; } } } } -} - -void CpuTelemetryThreadEntry_(Service* const srv, PresentMon* const pm, - pwr::cpu::CpuTelemetry* const cpu) -{ - IntervalWaiter waiter{ 0.016 }; - if (srv == nullptr || pm == nullptr) { - // TODO: log error on this condition - return; - } - - const HANDLE events[] { - pm->GetStreamingStartHandle(), - srv->GetServiceStopHandle(), - }; - - while (1) { - auto waitResult = WaitForMultipleObjects((DWORD)std::size(events), events, FALSE, INFINITE); - auto i = waitResult - WAIT_OBJECT_0; - if (i == 1) { - return; - } - while (WaitForSingleObject(srv->GetServiceStopHandle(), 0) != WAIT_OBJECT_0) { - cpu->Sample(); - // Convert from the ms to seconds as GetTelemetryPeriod returns back - // ms and SetInterval expects seconds. - waiter.SetInterval(pm->GetGpuTelemetryPeriod() / 1000.); - waiter.Wait(); - // Get the number of currently active streams - auto num_active_streams = pm->GetActiveStreams(); - if (num_active_streams == 0) { - break; - } - } - } + catch (...) { + pmlog_error(util::ReportException("Failure in telemetry loop")); + } } @@ -215,6 +561,15 @@ void PresentMonMainThread(Service* const pSvc) try { // alias for options auto& opt = clio::Options::Get(); + const auto& reg = Reg::Get(); + const auto frameRingSamples = opt.frameRingSamples.AsOptional().value_or( + reg.frameRingSamples.AsOptional() + .transform([](auto val) { return static_cast(val); }) + .value_or(*opt.frameRingSamples)); + const auto telemetryRingSamples = opt.telemetryRingSamples.AsOptional().value_or( + reg.telemetryRingSamples.AsOptional() + .transform([](auto val) { return static_cast(val); }) + .value_or(*opt.telemetryRingSamples)); // spin here waiting for debugger to attach, after which debugger should set // debug_service to false in order to proceed @@ -245,21 +600,28 @@ void PresentMonMainThread(Service* const pSvc) } } - PresentMon pm{ !opt.etlTestFile }; - PowerTelemetryContainer ptc; - // create service-side comms object for transmitting introspection data to clients std::unique_ptr pComms; try { - const auto introNsmName = GetIntrospectionShmName(); - LOG(INFO) << "Creating comms with NSM name: " << introNsmName; - pComms = ipc::MakeServiceComms(std::move(introNsmName)); + pmlog_dbg("Creating comms with shm prefix: ").pmwatch(*opt.shmNamePrefix); + pComms = ipc::MakeServiceComms(*opt.shmNamePrefix, + frameRingSamples, + telemetryRingSamples); + pmlog_info("Created comms with introspection shm name: ") + .pmwatch(pComms->GetNamer().MakeIntrospectionName()); } catch (const std::exception& e) { LOG(ERROR) << "Failed making service comms> " << e.what() << std::endl; pSvc->SignalServiceStop(-1); return; } + // create comms wrapper for managing frame data segments + FrameBroadcaster frameBroadcaster{ *pComms }; + + // container for session object + PresentMon pm{ frameBroadcaster, !opt.etlTestFile }; + // container for all GPU telemetry providers + PowerTelemetryContainer ptc; // Set the created power telemetry container pm.SetPowerTelemetryContainer(&ptc); @@ -268,7 +630,8 @@ void PresentMonMainThread(Service* const pSvc) auto pActionServer = std::make_unique(pSvc, &pm, opt.controlPipe.AsOptional()); try { - gpuTelemetryThread = std::jthread{ PowerTelemetryThreadEntry_, pSvc, &pm, &ptc, pComms.get() }; + gpuTelemetryThread = std::jthread{ PowerTelemetryThreadEntry_, pSvc, &pm, &ptc, + pComms.get() }; } catch (...) { LOG(ERROR) << "failed creating gpu(power) telemetry thread" << std::endl; @@ -288,18 +651,19 @@ void PresentMonMainThread(Service* const pSvc) } if (cpu) { - cpuTelemetryThread = std::jthread{ CpuTelemetryThreadEntry_, pSvc, &pm, cpu.get() }; pm.SetCpu(cpu); - // sample once to populate the cap bits + // refresh once to populate the cap bits cpu->Sample(); // determine vendor based on device name + // TODO: move this logic either into system (CPU) provider or + // into the ipc components const auto vendor = [&] { const auto lowerNameRn = cpu->GetCpuName() | vi::transform(tolower); const std::string lowerName{ lowerNameRn.begin(), lowerNameRn.end() }; - if (rn::search(lowerName, "intel")) { + if (lowerName.contains("intel")) { return PM_DEVICE_VENDOR_INTEL; } - else if (rn::search(lowerName, "amd")) { + else if (lowerName.contains("amd")) { return PM_DEVICE_VENDOR_AMD; } else { @@ -307,12 +671,21 @@ void PresentMonMainThread(Service* const pSvc) } }(); // register cpu - pComms->RegisterCpuDevice(vendor, cpu->GetCpuName(), cpu->GetCpuTelemetryCapBits()); + pComms->RegisterCpuDevice(vendor, cpu->GetCpuName(), + ipc::intro::ConvertBitset(cpu->GetCpuTelemetryCapBits())); + // after registering, we know that at least the store is available even + // if the introspection itself is not complete + auto& systemStore = pComms->GetSystemDataStore(); + // TODO: replace this placeholder routine for populating statics + systemStore.statics.cpuName = cpu->GetCpuName().c_str(); + systemStore.statics.cpuPowerLimit = cpu->GetCpuPowerLimit(); + systemStore.statics.cpuVendor = vendor; + cpuTelemetryThread = std::jthread{ CpuTelemetryThreadEntry_, pSvc, &pm, pComms.get(), + cpu.get() }; } else { // We were unable to determine the cpu. - std::bitset(CpuTelemetryCapBits::cpu_telemetry_count)> - cpuTelemetryCapBits_{}; - pComms->RegisterCpuDevice(PM_DEVICE_VENDOR_UNKNOWN, "UNKNOWN_CPU", cpuTelemetryCapBits_); + pComms->RegisterCpuDevice(PM_DEVICE_VENDOR_UNKNOWN, "UNKNOWN_CPU", + ipc::intro::ConvertBitset(CpuTelemetryBitset{})); } // start thread for manual ETW event buffer flushing @@ -330,7 +703,14 @@ void PresentMonMainThread(Service* const pSvc) } // Stop the PresentMon sessions - pm.StopTraceSessions(); + pm.StopTraceSessions(); + // wait for the telemetry threads to exit + if (gpuTelemetryThread.joinable()) { + gpuTelemetryThread.join(); + } + if (cpuTelemetryThread.joinable()) { + cpuTelemetryThread.join(); + } } catch (...) { LOG(ERROR) << "Exception in PMMainThread, bailing" << std::endl; diff --git a/IntelPresentMon/PresentMonService/PowerTelemetryContainer.cpp b/IntelPresentMon/PresentMonService/PowerTelemetryContainer.cpp index eae1a82d8..2bbb12827 100644 --- a/IntelPresentMon/PresentMonService/PowerTelemetryContainer.cpp +++ b/IntelPresentMon/PresentMonService/PowerTelemetryContainer.cpp @@ -1,4 +1,4 @@ -#include "PowerTelemetryContainer.h" +#include "PowerTelemetryContainer.h" #include "../ControlLib/Exceptions.h" #include #include @@ -15,7 +15,7 @@ bool PowerTelemetryContainer::Repopulate() { for (int iVendor = 0; iVendor < int(PM_DEVICE_VENDOR_UNKNOWN); iVendor++) { try { if (auto pProvider = pwr::PowerTelemetryProviderFactory::Make( - PM_DEVICE_VENDOR(iVendor))) { + PM_DEVICE_VENDOR(iVendor), device_id_allocator_)) { telemetry_providers_.push_back(std::move(pProvider)); } } @@ -48,4 +48,4 @@ bool PowerTelemetryContainer::Repopulate() { catch (...) { return false; } -} \ No newline at end of file +} diff --git a/IntelPresentMon/PresentMonService/PowerTelemetryContainer.h b/IntelPresentMon/PresentMonService/PowerTelemetryContainer.h index e75d4e763..75e199a69 100644 --- a/IntelPresentMon/PresentMonService/PowerTelemetryContainer.h +++ b/IntelPresentMon/PresentMonService/PowerTelemetryContainer.h @@ -1,7 +1,8 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include "../ControlLib/PowerTelemetryProviderFactory.h" +#include "../ControlLib/DeviceIdAllocator.h" class PowerTelemetryContainer { public: @@ -13,4 +14,5 @@ class PowerTelemetryContainer { private: std::vector> telemetry_providers_; std::vector> telemetry_adapters_; -}; \ No newline at end of file + pwr::DeviceIdAllocator device_id_allocator_; +}; diff --git a/IntelPresentMon/PresentMonService/PresentMon.cpp b/IntelPresentMon/PresentMonService/PresentMon.cpp index 2ac636328..8d6bdedff 100644 --- a/IntelPresentMon/PresentMonService/PresentMon.cpp +++ b/IntelPresentMon/PresentMonService/PresentMon.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2023 Intel Corporation // SPDX-License-Identifier: MIT #include "PresentMon.h" @@ -12,32 +12,30 @@ #include #include "../CommonUtilities/win/Privileges.h" -PresentMon::PresentMon(bool isRealtime) +PresentMon::PresentMon(svc::FrameBroadcaster& broadcaster, + bool isRealtime) : - etwLogger_{ util::win::WeAreElevated() } + broadcaster_{ broadcaster }, + etwLogger_{ util::win::WeAreElevated() }, + isRealtime_{ isRealtime } { if (isRealtime) { - pSession_ = std::make_unique(); + pSession_ = std::make_unique(broadcaster); } else { - pSession_ = std::make_unique(); + pSession_ = std::make_unique(broadcaster); } } PresentMon::~PresentMon() { pSession_->CheckTraceSessions(true); + pmlog_dbg("PresentMon object destructor finishing"); } -PM_STATUS PresentMon::StartStreaming(uint32_t client_process_id, uint32_t target_process_id, - std::string& nsm_file_name) +PM_STATUS PresentMon::UpdateTracking(const std::unordered_set& trackedPids) { - return pSession_->StartStreaming(client_process_id, target_process_id, nsm_file_name); -} - -void PresentMon::StopStreaming(uint32_t client_process_id, uint32_t target_process_id) -{ - return pSession_->StopStreaming(client_process_id, target_process_id); + return pSession_->UpdateTracking(trackedPids); } std::vector> PresentMon::EnumerateAdapters() @@ -46,12 +44,6 @@ std::vector> PresentMon::EnumerateAd return pSession_->EnumerateAdapters(); } -PM_STATUS PresentMon::SelectAdapter(uint32_t adapter_id) -{ - // Only the real time trace uses the control libary interface - return pSession_->SelectAdapter(adapter_id); -} - void PresentMon::StartPlayback() { if (auto pPlaybackSession = dynamic_cast(pSession_.get())) { diff --git a/IntelPresentMon/PresentMonService/PresentMon.h b/IntelPresentMon/PresentMonService/PresentMon.h index c10276d8d..fbe3a2f6d 100644 --- a/IntelPresentMon/PresentMonService/PresentMon.h +++ b/IntelPresentMon/PresentMonService/PresentMon.h @@ -1,17 +1,26 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2023 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include "PresentMonSession.h" #include "EtwLogger.h" +#include "FrameBroadcaster.h" +#include "../CommonUtilities/win/Event.h" +#include "../CommonUtilities/Hash.h" #include #include +#include +#include +#include +#include +#include +#include using namespace pmon; class PresentMon { public: - PresentMon(bool isRealtime); + PresentMon(svc::FrameBroadcaster& broadcaster, bool isRealtime); ~PresentMon(); // Check the status of both ETW logfile and real time trace sessions. @@ -23,14 +32,10 @@ class PresentMon void CheckTraceSessions(); // Force stop trace sessions void StopTraceSessions(); - PM_STATUS StartStreaming(uint32_t client_process_id, - uint32_t target_process_id, - std::string& nsm_file_name); - void StopStreaming(uint32_t client_process_id, uint32_t target_process_id); + PM_STATUS UpdateTracking(const std::unordered_set& trackedPids); std::vector> EnumerateAdapters(); std::string GetCpuName() { return pSession_->GetCpuName(); } double GetCpuPowerLimit() { return pSession_->GetCpuPowerLimit(); } - PM_STATUS SelectAdapter(uint32_t adapter_id); PM_STATUS SetGpuTelemetryPeriod(std::optional telemetryPeriodRequestsMs) { return pSession_->SetGpuTelemetryPeriod(telemetryPeriodRequestsMs); @@ -59,10 +64,9 @@ class PresentMon { return pSession_->GetStreamingStartHandle(); } - int GetActiveStreams() + bool HasLiveTargets() const { - // Only the real time trace uses the control libary interface - return pSession_->GetActiveStreams(); + return pSession_->HasLiveTargets(); } void SetPowerTelemetryContainer(PowerTelemetryContainer* ptc) { @@ -81,9 +85,56 @@ class PresentMon { return etwLogger_; } + auto& GetBroadcaster() + { + return broadcaster_; + } + bool IsPlayback() const + { + return !isRealtime_; + } + bool CheckDeviceMetricUsage(uint32_t deviceId) const + { + std::shared_lock lk{ metricDeviceUsageMtx_ }; + return metricDeviceUsage_.contains(deviceId); + } + void SetDeviceMetricUsage(std::unordered_set usage) + { + // we need exclusive lock to prevent concurrent access to usage data while being modified + { + std::lock_guard lk{ metricDeviceUsageMtx_ }; + metricDeviceUsage_ = std::move(usage); + } + // keep shared lock now to prevent modification to event set while we are iterating it + // if this were non-shared, it would cause the listeners to block immediately on wake + std::shared_lock lk2{ metricDeviceUsageMtx_ }; + for (auto& kv : deviceUsageEvts_) { + kv.second.Set(); + } + } + HANDLE GetDeviceUsageEvent(std::source_location loc = std::source_location::current()) const + { + const DeviceUsageEvtKey key{ loc.file_name(), (uint32_t)loc.line() }; + { + std::shared_lock lk{ metricDeviceUsageMtx_ }; + if (auto it = deviceUsageEvts_.find(key); it != deviceUsageEvts_.end()) { + return it->second.Get(); + } + } + // get non-shared lock for modification purposes (add new event) + std::lock_guard lk2{ metricDeviceUsageMtx_ }; + auto it = deviceUsageEvts_.emplace(key, util::win::Event{ false, false }).first; + return it->second.Get(); + } void StartPlayback(); void StopPlayback(); private: + svc::FrameBroadcaster& broadcaster_; svc::EtwLogger etwLogger_; std::unique_ptr pSession_; -}; \ No newline at end of file + bool isRealtime_ = true; + mutable std::shared_mutex metricDeviceUsageMtx_; + std::unordered_set metricDeviceUsage_; + using DeviceUsageEvtKey = std::pair; + mutable std::unordered_map deviceUsageEvts_; +}; diff --git a/IntelPresentMon/PresentMonService/PresentMonService.vcxproj b/IntelPresentMon/PresentMonService/PresentMonService.vcxproj index 026e06b2c..8993545e8 100644 --- a/IntelPresentMon/PresentMonService/PresentMonService.vcxproj +++ b/IntelPresentMon/PresentMonService/PresentMonService.vcxproj @@ -134,9 +134,8 @@ - - + @@ -147,6 +146,7 @@ + @@ -180,12 +180,9 @@ {2b343210-86ab-4153-9a6c-945e4af54c7c} - + {66e9f6c5-28db-4218-81b9-31e0e146ecc0} - - {bf43064b-01f0-4c69-91fb-c2122baf621d} - {c73aa532-e532-4d93-9279-905444653c08} @@ -200,4 +197,4 @@ - \ No newline at end of file + diff --git a/IntelPresentMon/PresentMonService/PresentMonService.vcxproj.filters b/IntelPresentMon/PresentMonService/PresentMonService.vcxproj.filters index 5b0f4debe..aafdd0cd9 100644 --- a/IntelPresentMon/PresentMonService/PresentMonService.vcxproj.filters +++ b/IntelPresentMon/PresentMonService/PresentMonService.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -36,20 +36,20 @@ - - + + @@ -57,4 +57,4 @@ - \ No newline at end of file + diff --git a/IntelPresentMon/PresentMonService/PresentMonSession.cpp b/IntelPresentMon/PresentMonService/PresentMonSession.cpp index a76d7e90f..f05410ef4 100644 --- a/IntelPresentMon/PresentMonService/PresentMonSession.cpp +++ b/IntelPresentMon/PresentMonService/PresentMonSession.cpp @@ -1,12 +1,26 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2023 Intel Corporation // SPDX-License-Identifier: MIT #include "PresentMonSession.h" +#include pmon::test::service::Status PresentMonSession::GetTestingStatus() const { + std::set trackedPids; + { + std::lock_guard lock(tracked_processes_mutex_); + for (auto const& entry : tracked_pid_live_) { + trackedPids.emplace(entry.first); + } + } + std::set frameStorePids; + if (pBroadcaster) { + for (auto pid : pBroadcaster->GetPids()) { + frameStorePids.emplace(pid); + } + } return pmon::test::service::Status{ - .nsmStreamedPids = streamer_.GetActiveStreamPids(), - .activeAdapterId = current_telemetry_adapter_id_, + .trackedPids = std::move(trackedPids), + .frameStorePids = std::move(frameStorePids), .telemetryPeriodMs = gpu_telemetry_period_ms_, .etwFlushPeriodMs = etw_flush_period_ms_, }; @@ -43,16 +57,6 @@ double PresentMonSession::GetCpuPowerLimit() { } } -PM_STATUS PresentMonSession::SelectAdapter(uint32_t adapter_id) { - if (telemetry_container_) { - if (adapter_id > telemetry_container_->GetPowerTelemetryAdapters().size()) { - return PM_STATUS_INVALID_ADAPTER_ID; - } - current_telemetry_adapter_id_ = adapter_id; - } - return PM_STATUS::PM_STATUS_SUCCESS; -} - PM_STATUS PresentMonSession::SetGpuTelemetryPeriod(std::optional period_ms) { gpu_telemetry_period_ms_ = period_ms.value_or(default_gpu_telemetry_period_ms_); @@ -79,10 +83,57 @@ std::optional PresentMonSession::GetEtwFlushPeriod() return etw_flush_period_ms_; } -int PresentMonSession::GetActiveStreams() { - return streamer_.NumActiveStreams(); +bool PresentMonSession::HasLiveTargets() const { + return HasLiveTrackedProcesses(); } void PresentMonSession::SetPowerTelemetryContainer(PowerTelemetryContainer* ptc) { telemetry_container_ = ptc; -} \ No newline at end of file +} + +void PresentMonSession::SyncTrackedPidState(const std::unordered_set& trackedPids) +{ + // TODO: consider theoretical rare race condition where exited process is added and never gets + // marked "dead" while action client maintains session and nevers stops tracking + std::lock_guard lock(tracked_processes_mutex_); + std::erase_if(tracked_pid_live_, [&](auto const& entry) { + return !trackedPids.contains(entry.first); + }); + for (auto pid : trackedPids) { + if (!tracked_pid_live_.contains(pid)) { + tracked_pid_live_.emplace(pid, true); + } + } +} + +void PresentMonSession::MarkProcessExited(uint32_t pid) +{ + std::lock_guard lock(tracked_processes_mutex_); + if (auto it = tracked_pid_live_.find(pid); it != tracked_pid_live_.end()) { + it->second = false; + } +} + +bool PresentMonSession::IsProcessTracked(uint32_t pid) const +{ + std::lock_guard lock(tracked_processes_mutex_); + return tracked_pid_live_.find(pid) != tracked_pid_live_.end(); +} + +bool PresentMonSession::HasTrackedProcesses() const +{ + std::lock_guard lock(tracked_processes_mutex_); + return std::ranges::any_of(tracked_pid_live_, [](auto const&) { return true; }); +} + +bool PresentMonSession::HasLiveTrackedProcesses() const +{ + std::lock_guard lock(tracked_processes_mutex_); + return std::ranges::any_of(tracked_pid_live_, [](auto const& entry) { return entry.second; }); +} + +void PresentMonSession::ClearTrackedProcesses() +{ + std::lock_guard lock(tracked_processes_mutex_); + tracked_pid_live_.clear(); +} diff --git a/IntelPresentMon/PresentMonService/PresentMonSession.h b/IntelPresentMon/PresentMonService/PresentMonSession.h index 1415a2f37..5ea82856a 100644 --- a/IntelPresentMon/PresentMonService/PresentMonSession.h +++ b/IntelPresentMon/PresentMonService/PresentMonSession.h @@ -1,42 +1,41 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2023 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include "../CommonUtilities/win/WinAPI.h" - +#include #include #include +#include #include +#include +#include +#include +#include +#include #include #include "../ControlLib/PowerTelemetryProvider.h" #include "../ControlLib/CpuTelemetry.h" -#include "../Streamer/Streamer.h" #include "../../PresentData/PresentMonTraceConsumer.hpp" #include "../../PresentData/PresentMonTraceSession.hpp" #include "PowerTelemetryContainer.h" +#include "FrameBroadcaster.h" #include "../PresentMonAPI2Tests/TestCommands.h" -struct SwapChainData { - uint32_t mPresentHistoryCount; - uint64_t mLastPresentQPC; - uint64_t mLastDisplayedPresentQPC; -}; - struct ProcessInfo { std::wstring mModuleName; - std::unordered_map mSwapChain; HANDLE mHandle = INVALID_HANDLE_VALUE; - bool mTargetProcess = false; }; +using namespace pmon; + class PresentMonSession { public: virtual ~PresentMonSession() = default; virtual bool IsTraceSessionActive() = 0; - virtual PM_STATUS StartStreaming(uint32_t client_process_id, uint32_t target_process_id, std::string& nsmFileName) = 0; - virtual void StopStreaming(uint32_t client_process_id, uint32_t target_process_id) = 0; + virtual PM_STATUS UpdateTracking(const std::unordered_set& trackedPids) = 0; virtual bool CheckTraceSessions(bool forceTerminate) = 0; virtual HANDLE GetStreamingStartHandle() = 0; virtual void FlushEvents() {} @@ -47,15 +46,21 @@ class PresentMonSession { std::string GetCpuName(); double GetCpuPowerLimit(); pmon::test::service::Status GetTestingStatus() const; - - PM_STATUS SelectAdapter(uint32_t adapter_id); PM_STATUS SetGpuTelemetryPeriod(std::optional period_ms); PM_STATUS SetEtwFlushPeriod(std::optional periodMs); std::optional GetEtwFlushPeriod(); uint32_t GetGpuTelemetryPeriod(); - int GetActiveStreams(); + bool HasLiveTargets() const; void SetPowerTelemetryContainer(PowerTelemetryContainer* ptc); + void MarkProcessExited(uint32_t pid); + bool IsProcessTracked(uint32_t pid) const; + bool HasTrackedProcesses() const; + bool HasLiveTrackedProcesses() const; + void ClearTrackedProcesses(); + +protected: + // TODO: review all of these members and consider fixing the unsound thread safety aspects // data std::wstring pm_session_name_; @@ -63,8 +68,6 @@ class PresentMonSession { pwr::cpu::CpuTelemetry* cpu_ = nullptr; PowerTelemetryContainer* telemetry_container_ = nullptr; - uint32_t current_telemetry_adapter_id_ = 0; - // Set the initial telemetry period to 16ms static constexpr uint32_t default_gpu_telemetry_period_ms_ = 16; uint32_t gpu_telemetry_period_ms_ = default_gpu_telemetry_period_ms_; @@ -74,6 +77,11 @@ class PresentMonSession { // empty optional means automatic flushing active std::atomic> etw_flush_period_ms_; - Streamer streamer_; + svc::FrameBroadcaster* pBroadcaster = nullptr; + + void SyncTrackedPidState(const std::unordered_set& trackedPids); + + mutable std::mutex tracked_processes_mutex_; + std::unordered_map tracked_pid_live_; }; diff --git a/IntelPresentMon/PresentMonService/RealtimePresentMonSession.cpp b/IntelPresentMon/PresentMonService/RealtimePresentMonSession.cpp index 43ee0de75..7a3d56e18 100644 --- a/IntelPresentMon/PresentMonService/RealtimePresentMonSession.cpp +++ b/IntelPresentMon/PresentMonService/RealtimePresentMonSession.cpp @@ -1,20 +1,21 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2023 Intel Corporation // SPDX-License-Identifier: MIT #include "Logging.h" #include "RealtimePresentMonSession.h" #include "CliOptions.h" #include "../CommonUtilities/str/String.h" #include "../CommonUtilities/win/Event.h" +#include "../CommonUtilities/win/Utilities.h" #include "../CommonUtilities/Qpc.h" #include "../CommonUtilities/Exception.h" -#include using namespace pmon; using namespace std::literals; using v = util::log::V; -RealtimePresentMonSession::RealtimePresentMonSession() +RealtimePresentMonSession::RealtimePresentMonSession(svc::FrameBroadcaster& broadcaster) { + pBroadcaster = &broadcaster; ResetEtwFlushPeriod(); } @@ -22,57 +23,44 @@ bool RealtimePresentMonSession::IsTraceSessionActive() { return session_active_.load(std::memory_order_acquire); } -PM_STATUS RealtimePresentMonSession::StartStreaming(uint32_t client_process_id, - uint32_t target_process_id, - std::string& nsmFileName) { - - // Check to see if the target process is valid - HANDLE target_process_handle = OpenProcess( - PROCESS_QUERY_LIMITED_INFORMATION, FALSE, target_process_id); - if (target_process_handle == nullptr) { - return PM_STATUS::PM_STATUS_INVALID_PID; - } - CloseHandle(target_process_handle); - - PM_STATUS status = streamer_.StartStreaming(client_process_id, - target_process_id, nsmFileName, false, false, false, false, false); - if (status != PM_STATUS::PM_STATUS_SUCCESS) { - return status; +PM_STATUS RealtimePresentMonSession::UpdateTracking(const std::unordered_set& trackedPids) { + const bool wasActive = HasLiveTargets(); + std::unordered_map previousState; + { + std::lock_guard lock(tracked_processes_mutex_); + previousState = tracked_pid_live_; } - else { - // Add the client process id to be monitored - GetProcessInfo(client_process_id); + SyncTrackedPidState(trackedPids); + const bool isActive = HasLiveTargets(); + if (isActive && (!wasActive || !IsTraceSessionActive())) { auto status = StartTraceSession(); - if (status == PM_STATUS_FAILURE) { - // Unable to start a trace session. Destroy the NSM and - // return status - streamer_.StopStreaming(target_process_id); + if (status != PM_STATUS_SUCCESS) { + { + std::lock_guard lock(tracked_processes_mutex_); + tracked_pid_live_ = std::move(previousState); + } return status; } - // Only signal the streaming started event when we have - // exactly one stream after returning from StartStreaming. - if ((streamer_.NumActiveStreams() == 1) && evtStreamingStarted_) { - evtStreamingStarted_.Set(); - } - // Also monitor the target process id - GetProcessInfo(target_process_id); - - return PM_STATUS::PM_STATUS_SUCCESS; } -} - -void RealtimePresentMonSession::StopStreaming(uint32_t client_process_id, - uint32_t target_process_id) { - streamer_.StopStreaming(client_process_id, target_process_id); - if ((streamer_.NumActiveStreams() == 0) && evtStreamingStarted_) { - evtStreamingStarted_.Reset(); + if (isActive && evtStreamingStarted_) { + evtStreamingStarted_.Set(); + } + if (!isActive) { + if (evtStreamingStarted_) { + evtStreamingStarted_.Reset(); + } StopTraceSession(); } + return PM_STATUS::PM_STATUS_SUCCESS; } bool RealtimePresentMonSession::CheckTraceSessions(bool forceTerminate) { - if (((GetActiveStreams() == 0) && (IsTraceSessionActive() == true)) || - forceTerminate) { + if (forceTerminate) { + StopTraceSession(); + ClearTrackedProcesses(); + return true; + } + if (!HasLiveTargets() && (IsTraceSessionActive() == true)) { StopTraceSession(); return true; } @@ -104,7 +92,7 @@ void RealtimePresentMonSession::ResetEtwFlushPeriod() } PM_STATUS RealtimePresentMonSession::StartTraceSession() { - std::lock_guard lock(session_mutex_); + std::lock_guard lock(session_mutex_); if (pm_consumer_) { return PM_STATUS::PM_STATUS_SERVICE_ERROR; @@ -181,26 +169,26 @@ PM_STATUS RealtimePresentMonSession::StartTraceSession() { void RealtimePresentMonSession::StopTraceSession() { // PHASE 1: Signal shutdown and wait for threads to observe it - session_active_.store(false, std::memory_order_release); - - // Stop the trace session to stop new events from coming in - trace_session_.Stop(); - - // Wait for threads to exit their critical sections and finish - WaitForConsumerThreadToExit(); - StopOutputThread(); - - // PHASE 2: Safe cleanup after threads have finished - std::lock_guard lock(session_mutex_); - - // Stop all streams - streamer_.StopAllStreams(); - if (evtStreamingStarted_) { - evtStreamingStarted_.Reset(); - } + // this also enforces "only_once" semantics for multiple stop callers + if (session_active_.exchange(false, std::memory_order_acq_rel)) { - if (pm_consumer_) { - pm_consumer_.reset(); + // Stop the trace session to stop new events from coming in + trace_session_.Stop(); + + // Wait for threads to exit their critical sections and finish + WaitForConsumerThreadToExit(); + StopOutputThread(); + + // PHASE 2: Safe cleanup after threads have finished + std::lock_guard lock(session_mutex_); + + if (evtStreamingStarted_) { + evtStreamingStarted_.Reset(); + } + + if (pm_consumer_) { + pm_consumer_.reset(); + } } } @@ -232,7 +220,9 @@ void RealtimePresentMonSession::AddPresents( if (session_active_.load(std::memory_order_acquire)) { if (trace_session_.mStartTimestamp.QuadPart != 0) { - streamer_.SetStartQpc(trace_session_.mStartTimestamp.QuadPart); + if (pBroadcaster) { + pBroadcaster->SetStartQpc(trace_session_.mStartTimestamp.QuadPart); + } } } @@ -257,6 +247,7 @@ void RealtimePresentMonSession::AddPresents( // Ignore failed and lost presents. if (presentEvent->IsLost || presentEvent->PresentFailed) { + // TODO: log these continue; } @@ -266,49 +257,10 @@ void RealtimePresentMonSession::AddPresents( break; } - // Look up the swapchain this present belongs to. - auto processInfo = GetProcessInfo(presentEvent->ProcessId); - if (!processInfo->mTargetProcess) { + if (!IsProcessTracked(presentEvent->ProcessId)) { continue; } - PresentMonPowerTelemetryInfo power_telemetry = {}; - std::bitset(GpuTelemetryCapBits::gpu_telemetry_count)> - gpu_telemetry_cap_bits = {}; - if (telemetry_container_) { - auto current_adapters = telemetry_container_->GetPowerTelemetryAdapters(); - if (current_adapters.size() != 0 && - current_telemetry_adapter_id_ < current_adapters.size()) { - auto current_telemetry_adapter = - current_adapters.at(current_telemetry_adapter_id_).get(); - if (auto data = current_telemetry_adapter->GetClosest( - presentEvent->PresentStartTime)) { - power_telemetry = *data; - } - gpu_telemetry_cap_bits = current_telemetry_adapter - ->GetPowerTelemetryCapBits(); - } - } - - CpuTelemetryInfo cpu_telemetry = {}; - std::bitset(CpuTelemetryCapBits::cpu_telemetry_count)> - cpu_telemetry_cap_bits = {}; - if (cpu_) { - if (auto data = cpu_->GetClosest(presentEvent->PresentStartTime)) { - cpu_telemetry = *data; - } - cpu_telemetry_cap_bits = cpu_->GetCpuTelemetryCapBits(); - } - - auto result = processInfo->mSwapChain.emplace( - presentEvent->SwapChainAddress, SwapChainData()); - auto chain = &result.first->second; - if (result.second) { - chain->mPresentHistoryCount = 0; - chain->mLastPresentQPC = 0; - chain->mLastDisplayedPresentQPC = 0; - } - // Remove Repeated flips if they are in Application->Repeated or Repeated->Application sequences. for (size_t i = 0, n = presentEvent->Displayed.size(); i + 1 < n; ) { if (presentEvent->Displayed[i].first == FrameType::Application && @@ -326,21 +278,8 @@ void RealtimePresentMonSession::AddPresents( } } - streamer_.ProcessPresentEvent( - presentEvent.get(), &power_telemetry, &cpu_telemetry, - chain->mLastPresentQPC, chain->mLastDisplayedPresentQPC, - processInfo->mModuleName, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); + pBroadcaster->Broadcast(*presentEvent); - chain->mLastPresentQPC = presentEvent->PresentStartTime; - if (presentEvent->FinalState == PresentResult::Presented) { - chain->mLastDisplayedPresentQPC = presentEvent->Displayed.empty() ? 0 : presentEvent->Displayed[0].second; - } - else if (chain->mLastDisplayedPresentQPC == chain->mLastPresentQPC) { - chain->mLastDisplayedPresentQPC = 0; - } - - chain->mPresentHistoryCount += 1; } *presentEventIndex = i; @@ -487,7 +426,7 @@ void RealtimePresentMonSession::Output() { // wait for either events to process or periodic polling timer while (auto idx = util::win::WaitAnyEvent(pm_consumer_->hEventsReadyEvent, hTimer)) { // events are ready so we should process them - if (*idx == 0) { + if (idx == 0) { pmlog_verb(v::etwq)("Event(s) ready"); break; } @@ -503,15 +442,6 @@ void RealtimePresentMonSession::Output() { } } - // Process handles - std::lock_guard lock(process_mutex_); - for (auto& pair : processes_) { - auto processInfo = &pair.second; - if (processInfo->mHandle != NULL) { - CloseHandle(processInfo->mHandle); - } - } - processes_.clear(); } catch (...) { pmlog_error(util::ReportException()); @@ -530,62 +460,18 @@ void RealtimePresentMonSession::StopOutputThread() { } } -ProcessInfo* RealtimePresentMonSession::GetProcessInfo(uint32_t processId) { - std::lock_guard lock(process_mutex_); - auto result = processes_.emplace(processId, ProcessInfo()); - auto processInfo = &result.first->second; - auto newProcess = result.second; - - if (newProcess) { - // Try to open a limited handle into the process in order to query its - // name and also periodically check if it has terminated. This will - // fail (with GetLastError() == ERROR_ACCESS_DENIED) if the process was - // run on another account, unless we're running with SeDebugPrivilege. - auto pProcessName = L""; - wchar_t path[MAX_PATH]; - const auto handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId); - if (handle) { - auto numChars = (DWORD)std::size(path); - if (QueryFullProcessImageNameW(handle, 0, path, &numChars)) { - pProcessName = PathFindFileNameW(path); - } - } - - InitProcessInfo(processInfo, processId, handle, pProcessName); - } - - return processInfo; -} - -void RealtimePresentMonSession::InitProcessInfo(ProcessInfo* processInfo, uint32_t processId, - HANDLE handle, - std::wstring const& processName) { - processInfo->mHandle = handle; - processInfo->mModuleName = processName; - processInfo->mTargetProcess = true; - - target_process_count_ += 1; -} void RealtimePresentMonSession::UpdateProcesses( std::vector const& processEvents, std::vector>* terminatedProcesses) { for (auto const& processEvent : processEvents) { - if (processEvent.IsStartEvent) { - // This event is a new process starting, the pid should not already be - // in processes_. - std::lock_guard lock(process_mutex_); - auto result = processes_.emplace(processEvent.ProcessId, ProcessInfo()); - auto processInfo = &result.first->second; - auto newProcess = result.second; - if (newProcess) { - InitProcessInfo(processInfo, processEvent.ProcessId, NULL, - processEvent.ImageFileName); - } + if (!IsProcessTracked(processEvent.ProcessId)) { + continue; } - else { + if (!processEvent.IsStartEvent) { // Note any process termination in terminatedProcess, to be handled // once the present event stream catches up to the termination time. + MarkProcessExited(processEvent.ProcessId); terminatedProcesses->emplace_back(processEvent.ProcessId, processEvent.QpcTime); } @@ -593,53 +479,21 @@ void RealtimePresentMonSession::UpdateProcesses( } void RealtimePresentMonSession::HandleTerminatedProcess(uint32_t processId) { - std::lock_guard lock(process_mutex_); - auto iter = processes_.find(processId); - if (iter == processes_.end()) { - return; // shouldn't happen. - } - - auto processInfo = &iter->second; - if (processInfo->mTargetProcess) { - // TODO(megalvan): Need to figure this out - // Close this process' CSV. - // CloseOutputCsv(processInfo); - - target_process_count_ -= 1; + // TODO(megalvan): Need to figure this out + // Close this process' CSV. + // CloseOutputCsv(processInfo); + MarkProcessExited(processId); + if (!HasLiveTrackedProcesses() && evtStreamingStarted_) { + evtStreamingStarted_.Reset(); } - processes_.erase(std::move(iter)); - streamer_.StopStreaming(processId); } // Check if any realtime processes terminated and add them to the terminated // list. // -// We assume that the process terminated now, which is wrong but conservative -// and functionally ok because no other process should start with the same PID -// as long as we're still holding a handle to it. +// Note: handle-based polling of target process lifetime is intentionally +// handled outside of the session layer. void RealtimePresentMonSession::CheckForTerminatedRealtimeProcesses( std::vector>* terminatedProcesses) { - std::lock_guard lock(process_mutex_); - for (auto& pair : processes_) { - auto processId = pair.first; - auto processInfo = &pair.second; - - DWORD exitCode = 0; - if (processInfo->mHandle != NULL && - GetExitCodeProcess(processInfo->mHandle, &exitCode) && - exitCode != STILL_ACTIVE) { - uint64_t qpc = 0; - QueryPerformanceCounter(reinterpret_cast(&qpc)); - terminatedProcesses->emplace_back(processId, qpc); - CloseHandle(processInfo->mHandle); - processInfo->mHandle = NULL; - // The tracked process has terminated. As multiple clients could be - // tracking this process call stop streaming until the streamer - // returns false and no longer holds an NSM for the process. - streamer_.StopStreaming(processId); - if ((streamer_.NumActiveStreams() == 0) && evtStreamingStarted_) { - evtStreamingStarted_.Reset(); - } - } - } + (void)terminatedProcesses; } diff --git a/IntelPresentMon/PresentMonService/RealtimePresentMonSession.h b/IntelPresentMon/PresentMonService/RealtimePresentMonSession.h index e02a4b617..54be31357 100644 --- a/IntelPresentMon/PresentMonService/RealtimePresentMonSession.h +++ b/IntelPresentMon/PresentMonService/RealtimePresentMonSession.h @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2023 Intel Corporation // SPDX-License-Identifier: MIT #pragma once #include "PresentMonSession.h" @@ -8,14 +8,13 @@ class RealtimePresentMonSession : public PresentMonSession { public: // functions - RealtimePresentMonSession(); + RealtimePresentMonSession(svc::FrameBroadcaster& broadcaster); RealtimePresentMonSession(const RealtimePresentMonSession& t) = delete; RealtimePresentMonSession& operator=(const RealtimePresentMonSession& t) = delete; ~RealtimePresentMonSession() override = default; bool IsTraceSessionActive() override; - PM_STATUS StartStreaming(uint32_t client_process_id, uint32_t target_process_id, std::string& nsmFileName) override; - void StopStreaming(uint32_t client_process_id, uint32_t target_process_id) override; + PM_STATUS UpdateTracking(const std::unordered_set& trackedPids) override; bool CheckTraceSessions(bool forceTerminate) override; HANDLE GetStreamingStartHandle() override; void FlushEvents() override; @@ -45,9 +44,6 @@ class RealtimePresentMonSession : public PresentMonSession void Consume(TRACEHANDLE traceHandle); void Output(); - ProcessInfo* GetProcessInfo(uint32_t processId); - void InitProcessInfo(ProcessInfo* processInfo, uint32_t processId, - HANDLE handle, std::wstring const& processName); void UpdateProcesses( std::vector const& processEvents, std::vector>* terminatedProcesses); @@ -66,13 +62,9 @@ class RealtimePresentMonSession : public PresentMonSession std::atomic quit_output_thread_ = false; - std::unordered_map processes_; - uint32_t target_process_count_ = 0; - // Event for when streaming has started pmon::util::win::Event evtStreamingStarted_; mutable std::mutex session_mutex_; - mutable std::mutex process_mutex_; std::atomic session_active_{false}; // Lock-free session state for hot path queries -}; \ No newline at end of file +}; diff --git a/IntelPresentMon/PresentMonService/Registry.h b/IntelPresentMon/PresentMonService/Registry.h index e29769921..96e0f151d 100644 --- a/IntelPresentMon/PresentMonService/Registry.h +++ b/IntelPresentMon/PresentMonService/Registry.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "../CommonUtilities/reg/Registry.h" #include "../CommonUtilities/log/Level.h" #include "GlobalIdentifiers.h" @@ -10,6 +10,8 @@ struct Reg : public reg::RegistryBase Value logDir{ this, "logDir" }; Value middlewarePath{ this, pmon::gid::middlewarePathKey }; Value logVerboseModules{ this, "logVerboseModules" }; + Value frameRingSamples{ this, "frameRingSamples" }; + Value telemetryRingSamples{ this, "telemetryRingSamples" }; static constexpr const wchar_t* keyPath_ = pmon::gid::registryPath; -}; \ No newline at end of file +}; diff --git a/IntelPresentMon/PresentMonService/ServiceMain.cpp b/IntelPresentMon/PresentMonService/ServiceMain.cpp index ac2571dfa..12f0dad69 100644 --- a/IntelPresentMon/PresentMonService/ServiceMain.cpp +++ b/IntelPresentMon/PresentMonService/ServiceMain.cpp @@ -10,6 +10,7 @@ #include "Registry.h" #include "../Versioning/BuildId.h" #include "../CommonUtilities/log/GlobalPolicy.h" +#include "../CommonUtilities/log/IdentificationTable.h" TCHAR serviceName[MaxBufferLength] = TEXT("Intel PresentMon Service"); using namespace pmon; @@ -17,6 +18,8 @@ using namespace pmon; // common entry point whether invoked as service or as app int CommonEntry(DWORD argc, LPTSTR* argv, bool asApp) { + util::log::IdentificationTable::AddThisProcess("service"); + util::log::IdentificationTable::AddThisThread("main"); logsetup::LogChannelManager logMan_; // parse command line, return with error code from CLI11 if running as app if (auto e = clio::Options::Init(argc, argv); e && asApp) { diff --git a/IntelPresentMon/PresentMonService/acts/EnumerateAdapters.h b/IntelPresentMon/PresentMonService/acts/EnumerateAdapters.h index ef7d19a3a..a7de1174e 100644 --- a/IntelPresentMon/PresentMonService/acts/EnumerateAdapters.h +++ b/IntelPresentMon/PresentMonService/acts/EnumerateAdapters.h @@ -1,8 +1,7 @@ -#pragma once +#pragma once #include "../../Interprocess/source/act/ActionHelper.h" #include "../ActionExecutionContext.h" #include -#include #define ACT_NAME EnumerateAdapters #define ACT_EXEC_CTX ActionExecutionContext @@ -12,9 +11,8 @@ namespace pmon::svc::acts { using namespace ipc::act; - namespace rn = std::ranges; - namespace vi = rn::views; + // TODO: remove this action, it is redundant in light of Introspection class ACT_NAME : public ACT_TYPE { public: @@ -46,9 +44,9 @@ namespace pmon::svc::acts static Response Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in) { Response out; - for (auto&&[i, adapter] : ctx.pPmon->EnumerateAdapters() | vi::enumerate) { + for (auto&& adapter : ctx.pPmon->EnumerateAdapters()) { out.adapters.push_back(Response::Adapter{ - .id = (uint32_t)i, + .id = adapter->GetDeviceId(), .vendor = adapter->GetVendor(), .name = adapter->GetName(), .gpuSustainedPowerLimit = adapter->GetSustainedPowerLimit(), diff --git a/IntelPresentMon/PresentMonService/acts/GetIntrospectionShmName.h b/IntelPresentMon/PresentMonService/acts/GetIntrospectionShmName.h deleted file mode 100644 index 8411f754a..000000000 --- a/IntelPresentMon/PresentMonService/acts/GetIntrospectionShmName.h +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once -#include "../../Interprocess/source/act/ActionHelper.h" -#include "../PMMainThread.h" -#include - -#define ACT_NAME GetIntrospectionShmName -#define ACT_EXEC_CTX ActionExecutionContext -#define ACT_NS ::pmon::svc::acts -#define ACT_TYPE AsyncActionBase_ - -namespace pmon::svc::acts -{ - using namespace ipc::act; - - class ACT_NAME : public ACT_TYPE - { - public: - static constexpr const char* Identifier = STRINGIFY(ACT_NAME); - struct Params {}; - struct Response - { - std::string name; - - template void serialize(A& ar) { - ar(name); - } - }; - private: - friend class ACT_TYPE; - static Response Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in) - { - const Response out{ - .name = ::GetIntrospectionShmName(), - }; - pmlog_dbg(std::format("Reported introspection shm name: {}", out.name)); - return out; - } - }; - -#ifdef PM_ASYNC_ACTION_REGISTRATION_ - ACTION_REG(); -#endif -} - -ACTION_TRAITS_DEF(); - -#undef ACT_NAME -#undef ACT_EXEC_CTX -#undef ACT_NS -#undef ACT_TYPE \ No newline at end of file diff --git a/IntelPresentMon/PresentMonService/acts/OpenSession.h b/IntelPresentMon/PresentMonService/acts/OpenSession.h index bd4e2a868..0e2831a8f 100644 --- a/IntelPresentMon/PresentMonService/acts/OpenSession.h +++ b/IntelPresentMon/PresentMonService/acts/OpenSession.h @@ -30,9 +30,11 @@ namespace pmon::svc::acts uint32_t servicePid; std::string serviceBuildId; std::string serviceBuildConfig; + std::string shmPrefix; + std::string shmSalt; template void serialize(A& ar) { - ar(serviceBuildId, serviceBuildConfig); + ar(servicePid, serviceBuildId, serviceBuildConfig, shmPrefix, shmSalt); } }; private: @@ -48,7 +50,9 @@ namespace pmon::svc::acts return Response{ .servicePid = GetCurrentProcessId(), .serviceBuildId = bid::BuildIdShortHash(), - .serviceBuildConfig = bid::BuildIdConfig() + .serviceBuildConfig = bid::BuildIdConfig(), + .shmPrefix = ctx.pPmon->GetBroadcaster().GetNamer().GetPrefix(), + .shmSalt = ctx.pPmon->GetBroadcaster().GetNamer().GetSalt(), }; } }; diff --git a/IntelPresentMon/PresentMonService/acts/SelectAdapter.h b/IntelPresentMon/PresentMonService/acts/ReportMetricUse.h similarity index 61% rename from IntelPresentMon/PresentMonService/acts/SelectAdapter.h rename to IntelPresentMon/PresentMonService/acts/ReportMetricUse.h index 4046ae462..6d42ff1a3 100644 --- a/IntelPresentMon/PresentMonService/acts/SelectAdapter.h +++ b/IntelPresentMon/PresentMonService/acts/ReportMetricUse.h @@ -1,9 +1,9 @@ -#pragma once +#pragma once #include "../../Interprocess/source/act/ActionHelper.h" #include -#include +#include -#define ACT_NAME SelectAdapter +#define ACT_NAME ReportMetricUse #define ACT_EXEC_CTX ActionExecutionContext #define ACT_NS ::pmon::svc::acts #define ACT_TYPE AsyncActionBase_ @@ -11,8 +11,6 @@ namespace pmon::svc::acts { using namespace ipc::act; - namespace rn = std::ranges; - namespace vi = rn::views; class ACT_NAME : public ACT_TYPE { @@ -20,23 +18,25 @@ namespace pmon::svc::acts static constexpr const char* Identifier = STRINGIFY(ACT_NAME); struct Params { - uint32_t adapterId; - + std::unordered_set metricUsage; template void serialize(A& ar) { - ar(adapterId); + ar(CEREAL_NVP(metricUsage)); } }; - struct Response {}; + struct Response + { + template void serialize(A& ar) {} + }; private: friend class ACT_TYPE; static Response Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in) { - if (auto sta = ctx.pPmon->SelectAdapter(in.adapterId); sta != PM_STATUS_SUCCESS) { - pmlog_error("Select adapter failed").code(sta); - throw util::Except(sta); - } - stx.requestedAdapterId = in.adapterId; - pmlog_dbg(std::format("selecting adapter id [{}]", in.adapterId)); + pmlog_verb(pmon::util::log::V::met_use)("ReportMetricUse action payload") + .pmwatch(stx.remotePid) + .serialize("reportMetricUse", in); + + stx.metricUsage = std::move(in.metricUsage); + ctx.UpdateMetricUsage(); return {}; } }; diff --git a/IntelPresentMon/PresentMonService/acts/StartTracking.h b/IntelPresentMon/PresentMonService/acts/StartTracking.h index c980fee58..e2046fa75 100644 --- a/IntelPresentMon/PresentMonService/acts/StartTracking.h +++ b/IntelPresentMon/PresentMonService/acts/StartTracking.h @@ -1,6 +1,8 @@ -#pragma once +#pragma once #include "../../Interprocess/source/act/ActionHelper.h" +#include "../../CommonUtilities/win/Utilities.h" #include +#include #define ACT_NAME StartTracking #define ACT_EXEC_CTX ActionExecutionContext @@ -18,33 +20,58 @@ namespace pmon::svc::acts struct Params { uint32_t targetPid; + bool isPlayback = false; + bool isBackpressured = false; template void serialize(A& ar) { - ar(targetPid); + ar(targetPid, isPlayback, isBackpressured); } }; struct Response { - std::string nsmFileName; - template void serialize(A& ar) { - ar(nsmFileName); } }; private: friend class ACT_TYPE; static Response Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in) { - std::string nsmFileName; - if (auto sta = ctx.pPmon->StartStreaming(stx.remotePid, in.targetPid, nsmFileName); sta != PM_STATUS_SUCCESS) { - pmlog_error("Start stream failed").code(sta); + // playback mode compatibility check + const bool serviceIsPlayback = ctx.pPmon->IsPlayback(); + if (serviceIsPlayback != in.isPlayback) { + pmlog_error("StartTracking playback mode mismatch") + .pmwatch(serviceIsPlayback).pmwatch(in.isPlayback); + throw util::Except(PM_STATUS_MODE_MISMATCH); + } + // check if this session already tracking requested pid + if (stx.trackedPids.find(in.targetPid) != stx.trackedPids.end()) { + pmlog_error("StartTracking called for already tracked pid").pmwatch(in.targetPid); + throw util::Except(PM_STATUS_ALREADY_TRACKING_PROCESS); + } + // lock in handle to process for pid parking purposes + ActionSessionContext::TrackedTarget target{}; + if (!in.isPlayback) { + target.processHandle = util::win::OpenProcess(in.targetPid); + if (!target.processHandle) { + pmlog_error("StartTracking called for invalid pid").pmwatch(in.targetPid); + throw util::Except(PM_STATUS_INVALID_PID); + } + } + // build full tracking state for session sync + auto trackedPids = ctx.GetTrackedPidSet(); + trackedPids.emplace(in.targetPid); + // get the (possibly shared) segment (new or find operation in broadcaster) + auto pSegment = ctx.pPmon->GetBroadcaster().RegisterTarget( + in.targetPid, in.isPlayback, in.isBackpressured); + if (auto sta = ctx.pPmon->UpdateTracking(trackedPids); sta != PM_STATUS_SUCCESS) { + pmlog_error("Start tracking call failed").code(sta); throw util::Except(sta); } - stx.trackedPids.insert(in.targetPid); - const Response out{ .nsmFileName = std::move(nsmFileName) }; - pmlog_info(std::format("StartTracking action from [{}] targeting [{}] assigned nsm [{}]", - stx.remotePid, in.targetPid, out.nsmFileName)); - return out; + target.pSegment = std::move(pSegment); + stx.trackedPids.emplace(in.targetPid, std::move(target)); + pmlog_info(std::format("StartTracking action from [{}] targeting [{}]", + stx.remotePid, in.targetPid)); + return {}; } }; @@ -58,4 +85,4 @@ ACTION_TRAITS_DEF(); #undef ACT_NAME #undef ACT_EXEC_CTX #undef ACT_NS -#undef ACT_TYPE \ No newline at end of file +#undef ACT_TYPE diff --git a/IntelPresentMon/PresentMonService/acts/StopTracking.h b/IntelPresentMon/PresentMonService/acts/StopTracking.h index fd041ba5f..b6f3bd907 100644 --- a/IntelPresentMon/PresentMonService/acts/StopTracking.h +++ b/IntelPresentMon/PresentMonService/acts/StopTracking.h @@ -1,6 +1,7 @@ -#pragma once +#pragma once #include "../../Interprocess/source/act/ActionHelper.h" #include +#include #define ACT_NAME StopTracking #define ACT_EXEC_CTX ActionExecutionContext @@ -28,8 +29,11 @@ namespace pmon::svc::acts friend class ACT_TYPE; static Response Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in) { - ctx.pPmon->StopStreaming(stx.remotePid, in.targetPid); - stx.trackedPids.erase(in.targetPid); + if (stx.trackedPids.erase(in.targetPid) == 0) { + pmlog_error("StopTracking called for untracked pid").pmwatch(in.targetPid); + throw util::Except(PM_STATUS_INVALID_PID); + } + ctx.pPmon->UpdateTracking(ctx.GetTrackedPidSet()); pmlog_info(std::format("StopTracking action from [{}] un-targeting [{}]", stx.remotePid, in.targetPid)); return {}; } @@ -45,4 +49,4 @@ ACTION_TRAITS_DEF(); #undef ACT_NAME #undef ACT_EXEC_CTX #undef ACT_NS -#undef ACT_TYPE \ No newline at end of file +#undef ACT_TYPE diff --git a/IntelPresentMon/PresentMonUtils/LegacyAPIDefines.h b/IntelPresentMon/PresentMonUtils/LegacyAPIDefines.h deleted file mode 100644 index 67c6a8f9e..000000000 --- a/IntelPresentMon/PresentMonUtils/LegacyAPIDefines.h +++ /dev/null @@ -1,155 +0,0 @@ -#pragma once -#include - -// TODO: review these definitions; they are legacy code copied from old PresentMonAPI header. -// Replace with more suitable structures that align better with new API or work better without -// the extern "C" restrictions, as necessary/appropriate/convenient -#define MAX_PM_ADAPTER_NAME 64 -#define MAX_PM_CPU_NAME 256 -#define MAX_PM_PATH 260 -#define MAX_RUNTIME_LENGTH 7 -#define MAX_PM_FAN_COUNT 5 -#define MAX_PM_PSU_COUNT 5 -enum PM_GPU_VENDOR { - PM_GPU_VENDOR_INTEL, - PM_GPU_VENDOR_NVIDIA, - PM_GPU_VENDOR_AMD, - PM_GPU_VENDOR_UNKNOWN -}; -enum PM_PSU_TYPE { - PM_PSU_TYPE_NONE, - PM_PSU_TYPE_PCIE, - PM_PSU_TYPE_6PIN, - PM_PSU_TYPE_8PIN -}; -struct PM_ADAPTER_INFO -{ - uint32_t id; - PM_GPU_VENDOR vendor; - char name[MAX_PM_ADAPTER_NAME]; - double gpuSustainedPowerLimit; - uint64_t gpuMemorySize; - uint64_t gpuMemoryMaxBandwidth; -}; -struct PM_FRAME_DATA_OPT_DOUBLE { - double data; - bool valid; -}; -struct PM_FRAME_DATA_OPT_UINT64 { - uint64_t data; - bool valid; -}; -struct PM_FRAME_DATA_OPT_INT { - int data; - bool valid; -}; -struct PM_FRAME_DATA_OPT_PSU_TYPE { - PM_PSU_TYPE data; - bool valid; -}; -struct PM_FRAME_DATA -{ - // @brief The name of the process that called Present(). - char application[MAX_PM_PATH]; - - // @brief The process ID of the process that called Present(). - uint32_t process_id; - // @brief The address of the swap chain that was presented into. - uint64_t swap_chain_address; - // @brief The runtime used to present (e.g., D3D9 or DXGI). - char runtime[MAX_RUNTIME_LENGTH]; - // @brief The sync interval provided by the application in the Present() - // call. This value may be modified later by the driver, e.g., based on - // control panel overrides. - int32_t sync_interval; - // @brief Flags used in the Present() call. - uint32_t present_flags; - // @brief Whether the frame was dropped (1) or displayed (0). Note, if - // dropped, msUntilDisplayed will be 0. - uint32_t dropped; - // @brief The time of the Present() call, in seconds, relative to when the - // PresentMon started recording. - double time_in_seconds; - // @brief The time spent inside the Present() call, in milliseconds. - double ms_in_present_api; - // @brief The time between this Present() call and the previous one, in - // milliseconds. - double ms_between_presents; - // @brief Whether tearing is possible (1) or not (0). - uint32_t allows_tearing; - // @brief The presentation mode used by the system for this Present(). - PM_PRESENT_MODE present_mode; - // @brief The time between the Present() call and when the GPU work - // completed, in milliseconds. - double ms_until_render_complete; - // @brief The time between the Present() call and when the frame was - // displayed, in milliseconds. - double ms_until_displayed; - // @brief How long the previous frame was displayed before this Present() - // was displayed, in milliseconds. - double ms_between_display_change; - // @brief The time between the Present() call and when the GPU work - // started, in milliseconds. - double ms_until_render_start; - // @brief The time of the Present() call, as a performance counter value. - uint64_t qpc_time; - - // @brief The time between the Present() call and the earliest keyboard or - // mouse interaction that contributed to this frame. - double ms_since_input; - // @brief The time that any GPU engine was active working on this frame, - // in milliseconds. Not supported on Win7 - double ms_gpu_active; - // @brief The time video encode/decode was active separate from the other - // engines in milliseconds. Not supported on Win7 - double ms_gpu_video_active; - - // Power telemetry - PM_FRAME_DATA_OPT_DOUBLE gpu_power_w; - PM_FRAME_DATA_OPT_DOUBLE gpu_sustained_power_limit_w; - PM_FRAME_DATA_OPT_DOUBLE gpu_voltage_v; - PM_FRAME_DATA_OPT_DOUBLE gpu_frequency_mhz; - PM_FRAME_DATA_OPT_DOUBLE gpu_temperature_c; - PM_FRAME_DATA_OPT_DOUBLE gpu_utilization; - PM_FRAME_DATA_OPT_DOUBLE gpu_render_compute_utilization; - PM_FRAME_DATA_OPT_DOUBLE gpu_media_utilization; - - PM_FRAME_DATA_OPT_DOUBLE vram_power_w; - PM_FRAME_DATA_OPT_DOUBLE vram_voltage_v; - PM_FRAME_DATA_OPT_DOUBLE vram_frequency_mhz; - PM_FRAME_DATA_OPT_DOUBLE vram_effective_frequency_gbs; - PM_FRAME_DATA_OPT_DOUBLE vram_temperature_c; - - PM_FRAME_DATA_OPT_DOUBLE fan_speed_rpm[MAX_PM_FAN_COUNT]; - - PM_FRAME_DATA_OPT_PSU_TYPE psu_type[MAX_PM_PSU_COUNT]; - PM_FRAME_DATA_OPT_DOUBLE psu_power[MAX_PM_PSU_COUNT]; - PM_FRAME_DATA_OPT_DOUBLE psu_voltage[MAX_PM_PSU_COUNT]; - - // Gpu memory telemetry - PM_FRAME_DATA_OPT_UINT64 gpu_mem_total_size_b; - PM_FRAME_DATA_OPT_UINT64 gpu_mem_used_b; - PM_FRAME_DATA_OPT_UINT64 gpu_mem_max_bandwidth_bps; - PM_FRAME_DATA_OPT_DOUBLE gpu_mem_read_bandwidth_bps; - PM_FRAME_DATA_OPT_DOUBLE gpu_mem_write_bandwidth_bps; - - // Throttling flags - PM_FRAME_DATA_OPT_INT gpu_power_limited; - PM_FRAME_DATA_OPT_INT gpu_temperature_limited; - PM_FRAME_DATA_OPT_INT gpu_current_limited; - PM_FRAME_DATA_OPT_INT gpu_voltage_limited; - PM_FRAME_DATA_OPT_INT gpu_utilization_limited; - - PM_FRAME_DATA_OPT_INT vram_power_limited; - PM_FRAME_DATA_OPT_INT vram_temperature_limited; - PM_FRAME_DATA_OPT_INT vram_current_limited; - PM_FRAME_DATA_OPT_INT vram_voltage_limited; - PM_FRAME_DATA_OPT_INT vram_utilization_limited; - - // Cpu Telemetry - PM_FRAME_DATA_OPT_DOUBLE cpu_utilization; - PM_FRAME_DATA_OPT_DOUBLE cpu_power_w; - PM_FRAME_DATA_OPT_DOUBLE cpu_power_limit_w; - PM_FRAME_DATA_OPT_DOUBLE cpu_temperature_c; - PM_FRAME_DATA_OPT_DOUBLE cpu_frequency; -}; \ No newline at end of file diff --git a/IntelPresentMon/PresentMonUtils/PresentDataUtils.h b/IntelPresentMon/PresentMonUtils/PresentDataUtils.h deleted file mode 100644 index ce80829ae..000000000 --- a/IntelPresentMon/PresentMonUtils/PresentDataUtils.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once -#include -#include "../../PresentData/PresentMonTraceConsumer.hpp" -#include "../PresentMonAPI2/PresentMonAPI.h" -#include "../ControlLib/PresentMonPowerTelemetry.h" - -inline const char* PresentModeToString(PresentMode mode) { - switch (mode) { - case PresentMode::Hardware_Legacy_Flip: - return "Hardware: Legacy Flip"; - case PresentMode::Hardware_Legacy_Copy_To_Front_Buffer: - return "Hardware: Legacy Copy to front buffer"; - case PresentMode::Hardware_Independent_Flip: - return "Hardware: Independent Flip"; - case PresentMode::Composed_Flip: - return "Composed: Flip"; - case PresentMode::Composed_Copy_GPU_GDI: - return "Composed: Copy with GPU GDI"; - case PresentMode::Composed_Copy_CPU_GDI: - return "Composed: Copy with CPU GDI"; - case PresentMode::Hardware_Composed_Independent_Flip: - return "Hardware Composed: Independent Flip"; - default: - return "Other"; - } -} - -inline const char* RuntimeToString(Runtime rt) { - switch (rt) { - case Runtime::DXGI: - return "DXGI"; - case Runtime::D3D9: - return "D3D9"; - default: - return "Other"; - } -} - -inline PM_PSU_TYPE TranslatePsuType(PresentMonPsuType psu_type_in) { - switch (psu_type_in) { - case PresentMonPsuType::Pcie: - return PM_PSU_TYPE::PM_PSU_TYPE_PCIE; - case PresentMonPsuType::Pin6: - return PM_PSU_TYPE::PM_PSU_TYPE_6PIN; - case PresentMonPsuType::Pin8: - return PM_PSU_TYPE::PM_PSU_TYPE_8PIN; - default: - return PM_PSU_TYPE::PM_PSU_TYPE_NONE; - } -} - -inline PM_PRESENT_MODE TranslatePresentMode( - PresentMode present_mode_in) { - switch (present_mode_in) { - case PresentMode::Hardware_Legacy_Flip: - return PM_PRESENT_MODE::PM_PRESENT_MODE_HARDWARE_LEGACY_FLIP; - case PresentMode::Hardware_Legacy_Copy_To_Front_Buffer: - return PM_PRESENT_MODE:: - PM_PRESENT_MODE_HARDWARE_LEGACY_COPY_TO_FRONT_BUFFER; - case PresentMode::Hardware_Independent_Flip: - return PM_PRESENT_MODE::PM_PRESENT_MODE_HARDWARE_INDEPENDENT_FLIP; - case PresentMode::Composed_Flip: - return PM_PRESENT_MODE::PM_PRESENT_MODE_COMPOSED_FLIP; - case PresentMode::Hardware_Composed_Independent_Flip: - return PM_PRESENT_MODE:: - PM_PRESENT_MODE_HARDWARE_COMPOSED_INDEPENDENT_FLIP; - case PresentMode::Composed_Copy_GPU_GDI: - return PM_PRESENT_MODE::PM_PRESENT_MODE_COMPOSED_COPY_WITH_GPU_GDI; - case PresentMode::Composed_Copy_CPU_GDI: - return PM_PRESENT_MODE::PM_PRESENT_MODE_COMPOSED_COPY_WITH_CPU_GDI; - default: - return PM_PRESENT_MODE::PM_PRESENT_MODE_UNKNOWN; - } -} \ No newline at end of file diff --git a/IntelPresentMon/PresentMonUtils/PresentMonUtils.vcxproj b/IntelPresentMon/PresentMonUtils/PresentMonUtils.vcxproj deleted file mode 100644 index 12786394e..000000000 --- a/IntelPresentMon/PresentMonUtils/PresentMonUtils.vcxproj +++ /dev/null @@ -1,119 +0,0 @@ - - - - - Debug - x64 - - - Release - x64 - - - - 16.0 - Win32Proj - {66e9f6c5-28db-4218-81b9-31e0e146ecc0} - PresentMonUtils - 10.0 - - - - StaticLibrary - true - v143 - Unicode - x64 - - - StaticLibrary - false - v143 - true - Unicode - x64 - - - - - - - - - - - - - - - - - - - true - $(SolutionDir)PresentMonAPI;$(SolutionDir)PresentMonService;$(SolutionDir)ControlLib;$(VC_IncludePath);$(WindowsSDK_IncludePath); - --checks==clang-analyzer-*,google-* - false - false - - - false - $(SolutionDir)PresentMonAPI;$(SolutionDir)PresentMonService;$(SolutionDir)ControlLib;$(VC_IncludePath);$(WindowsSDK_IncludePath); - --checks==clang-analyzer-*,google-* - false - false - - - - - - - - Level3 - true - true - MultiThreadedDebug - EditAndContinue - stdcpp17 - - - Console - true - - - - - Level3 - true - true - true - true - MaxSpeed - MultiThreaded - stdcpp17 - NDEBUG;%(PreprocessorDefinitions) - Guard - - - Console - true - true - true - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/IntelPresentMon/PresentMonUtils/QPCUtils.cpp b/IntelPresentMon/PresentMonUtils/QPCUtils.cpp deleted file mode 100644 index d856fb30c..000000000 --- a/IntelPresentMon/PresentMonUtils/QPCUtils.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "QPCUtils.h" - -double QpcDeltaToSeconds(uint64_t qpcDelta, LARGE_INTEGER qpcFrequency) -{ - return static_cast(qpcDelta) / qpcFrequency.QuadPart; -} - -uint64_t SecondsDeltaToQpc(double secondsDelta, LARGE_INTEGER qpcFrequency) -{ - return static_cast((secondsDelta * qpcFrequency.QuadPart)); -} - -double QpcToSeconds(uint64_t qpc, LARGE_INTEGER qpcFrequency, LARGE_INTEGER startQpc) -{ - return QpcDeltaToSeconds(qpc - startQpc.QuadPart, qpcFrequency); -} - -void QpcToLocalSystemTime(uint64_t qpc, LARGE_INTEGER startQpc, LARGE_INTEGER qpcFrequency, FILETIME startTime, SYSTEMTIME* st, uint64_t* ns) -{ - auto tns = (qpc - startQpc.QuadPart) * 1000000000ull / qpcFrequency.QuadPart; - auto t100ns = tns / 100; - auto ft = (*reinterpret_cast(&startTime)) + t100ns; - - FileTimeToSystemTime(reinterpret_cast(&ft), st); - *ns = (ft - (ft / 10000000ull) * 10000000ull) * 100ull + (tns - t100ns * 100ull); -} - -double QpcDeltaToMs(uint64_t qpc_data, LARGE_INTEGER qpc_frequency) { - return 1000.0f * (static_cast(qpc_data)/ qpc_frequency.QuadPart); -} \ No newline at end of file diff --git a/IntelPresentMon/PresentMonUtils/QPCUtils.h b/IntelPresentMon/PresentMonUtils/QPCUtils.h deleted file mode 100644 index bf97ff325..000000000 --- a/IntelPresentMon/PresentMonUtils/QPCUtils.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include -#include - -double QpcDeltaToSeconds(uint64_t qpcDelta, LARGE_INTEGER qpcFrequency); -uint64_t SecondsDeltaToQpc(double secondsDelta, LARGE_INTEGER qpcFrequency); -double QpcToSeconds(uint64_t qpc, LARGE_INTEGER qpcFrequency, LARGE_INTEGER startQpc); -void QpcToLocalSystemTime( - uint64_t qpc, - LARGE_INTEGER startQpc, - LARGE_INTEGER qpcFrequency, - FILETIME startTime, - SYSTEMTIME* st, - uint64_t* ns); -double QpcDeltaToMs(uint64_t qpc_data, LARGE_INTEGER qpc_frequency); \ No newline at end of file diff --git a/IntelPresentMon/PresentMonUtils/StreamFormat.h b/IntelPresentMon/PresentMonUtils/StreamFormat.h deleted file mode 100644 index 408ddf9fa..000000000 --- a/IntelPresentMon/PresentMonUtils/StreamFormat.h +++ /dev/null @@ -1,170 +0,0 @@ -/* - * - * Copyright (C) 2021,2023 Intel Corporation - * - * SPDX-License-Identifier: MIT - * - * - * File Name: StreamFormat.h - * - * Abstract: Header for Intel PresentMon Named Shared Memory IPC - * - */ -#pragma once -#include -#include -#include -#include "../../PresentData/PresentMonTraceConsumer.hpp" -#include "../PresentMonAPI2/PresentMonAPI.h" -#include "../ControlLib/PowerTelemetryProvider.h" -#include "../ControlLib/CpuTelemetryInfo.h" -#include "LegacyAPIDefines.h" - - // We use system reserved pid (0: System Idle Process, 4: System) that will - // never show up in present mon for StreamAll and ETL PIDs -enum class StreamPidOverride : uint32_t { kStreamAllPid = 0, kEtlPid = 4 }; - -struct NamedSharedMemoryHeader -{ - NamedSharedMemoryHeader() - : start_qpc(0), - last_displayed_qpc(0), - buf_size(0), - max_entries(0), - current_write_offset(0), - num_frames_written(0), - head_idx(0), - tail_idx(0), - process_active(true) {} - // start QPC time of the very first frame recorderd after PmStartStream - char application[MAX_PATH] = {}; - uint64_t start_qpc; - LARGE_INTEGER qpc_frequency = {}; - uint64_t last_displayed_qpc; - uint64_t buf_size; - uint64_t max_entries; - uint64_t current_write_offset; - uint64_t num_frames_written; - uint64_t head_idx; - uint64_t tail_idx; - bool process_active; - std::bitset(GpuTelemetryCapBits::gpu_telemetry_count)> - gpuTelemetryCapBits{}; - std::bitset(CpuTelemetryCapBits::cpu_telemetry_count)> - cpuTelemetryCapBits{}; - bool isPlayback = false; - bool isPlaybackPaced = false; - bool isPlaybackRetimed = false; - bool isPlaybackBackpressured = false; - bool isPlaybackResetOldest = false; -}; - -struct PmNsmPresentEvent -{ - uint64_t PresentStartTime; // QPC value of the first event related to the - // Present (D3D9, DXGI, or DXGK Present_Start) - uint32_t ProcessId; // ID of the process that presented - uint32_t ThreadId; // ID of the thread that presented - uint64_t TimeInPresent; // QPC duration between runtime present start and end - uint64_t GPUStartTime; // QPC value when the frame's first DMA packet started - uint64_t ReadyTime; // QPC value when the frame's last DMA packet completed - - uint64_t GPUDuration; // QPC duration during which a frame's DMA packet was - // running on any node - uint64_t GPUVideoDuration; // QPC duration during which a frame's DMA packet was - // running on a video node (if mTrackGPUVideo==true) - uint64_t InputTime; // Earliest QPC value for all keyboard/mouse input used by this frame - uint64_t MouseClickTime; // Earliest QPC value when the mouse was clicked and used by this frame - - // Used to track the application work when Intel XeSS-FG is enabled - uint64_t AppPropagatedPresentStartTime; // Propogated QPC value of the first event related to the Present (D3D9, DXGI, or DXGK Present_Start) - uint64_t AppPropagatedTimeInPresent; // Propogated QPC duration of the Present call (only applicable for D3D9/DXGI) - uint64_t AppPropagatedGPUStartTime; // Propogated QPC value when the frame's first DMA packet started - uint64_t AppPropagatedReadyTime; // Propogated QPC value when the frame's last DMA packet completed - uint64_t AppPropagatedGPUDuration; // Propogated QPC duration during which a frame's DMA packet was running on - uint64_t AppPropagatedGPUVideoDuration; // Propogated QPC duration during which a frame's DMA packet was running on - // a video node (if mTrackGPUVideo==true) - - uint64_t AppSleepStartTime; // QPC value of app sleep start time provided by Intel App Provider - uint64_t AppSleepEndTime; // QPC value of app sleep end time provided by Intel App Provider - uint64_t AppSimStartTime; // QPC value of app sim start time provided by Intel App Provider - uint64_t AppSimEndTime; // QPC value of app sim end time provided by Intel App Provider - uint64_t AppRenderSubmitStartTime; // QPC value of app render submit start time provided by Intel App Provider - uint64_t AppRenderSubmitEndTime; // QPC value of app render submit end time provided by Intel App Provider - uint64_t AppPresentStartTime; // QPC value of app present start time provided by Intel App Provider - uint64_t AppPresentEndTime; // QPC value of app present end time provided by Intel App Provider - uint64_t AppInputTime; // QPC value of app input time provided by Intel App Provider - InputDeviceType AppInputType; // Input type provided by Intel App Provider - - uint64_t PclInputPingTime; // QPC value of input ping time provided by the PC Latency ETW event - uint64_t PclSimStartTime; // QPC value of app sim start time provided by the PC Latency ETW event - uint64_t FlipDelay; // QPC timestamp delta of FlipDelay calculated using the NV DisplayDriver FlipRequest event. - uint32_t FlipToken; // Flip token from the NV DisplayDriver FlipRequest event. - - // Extra present parameters obtained through DXGI or D3D9 present - uint64_t SwapChainAddress; - int32_t SyncInterval; - uint32_t PresentFlags; - - // (FrameType, DisplayedQPC) for each time the frame was displayed - uint64_t Displayed_ScreenTime[16]; - FrameType Displayed_FrameType[16]; - uint32_t DisplayedCount; - - // Keys used to index into PMTraceConsumer's tracking data structures: - uint64_t CompositionSurfaceLuid; // mPresentByWin32KPresentHistoryToken - uint64_t Win32KPresentCount; // mPresentByWin32KPresentHistoryToken - uint64_t Win32KBindId; // mPresentByWin32KPresentHistoryToken - uint64_t DxgkPresentHistoryToken; // mPresentByDxgkPresentHistoryToken - uint64_t - DxgkPresentHistoryTokenData; // mPresentByDxgkPresentHistoryTokenData - uint64_t DxgkContext; // mPresentByDxgkContext - uint64_t Hwnd; // mLastPresentByWindow - uint32_t QueueSubmitSequence; // mPresentBySubmitSequence - uint32_t RingIndex; // mTrackedPresents and mCompletedPresents - - // Properties deduced by watching events through present pipeline - uint32_t DestWidth; - uint32_t DestHeight; - uint32_t DriverThreadId; - - uint32_t FrameId; // ID for the logical frame that this Present is associated with. - - Runtime Runtime; - PresentMode PresentMode; - PresentResult FinalState; - InputDeviceType InputType; - FrameType FrameType; - - bool SupportsTearing; - bool WaitForFlipEvent; - bool WaitForMPOFlipEvent; - bool SeenDxgkPresent; - bool SeenWin32KEvents; - bool SeenInFrameEvent; // This present has gotten a Win32k TokenStateChanged - // event into InFrame state - bool GpuFrameCompleted; // This present has already seen an event that caused - // GpuTrace::CompleteFrame() to be called. - bool IsCompleted; // All expected events have been observed - bool IsLost; // This PresentEvent was found in an unexpected state or is too - // old - - // Whether this PresentEvent is currently stored in - // PMTraceConsumer::mPresentsWaitingForDWM - bool PresentInDwmWaitingStruct; - - // QPC time of last presented frame - uint64_t last_present_qpc; - // QPC time of the last displayed frame - uint64_t last_displayed_qpc; - - // Application name - char application[MAX_PATH]; -}; - -struct PmNsmFrameData -{ - PmNsmPresentEvent present_event; - PresentMonPowerTelemetryInfo power_telemetry; - CpuTelemetryInfo cpu_telemetry; -}; diff --git a/IntelPresentMon/PresentMonUtils/StringUtils.cpp b/IntelPresentMon/PresentMonUtils/StringUtils.cpp deleted file mode 100644 index eea1e5d3d..000000000 --- a/IntelPresentMon/PresentMonUtils/StringUtils.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include -//#include -//#include -#include "StringUtils.h" - -std::string ConvertFromWideString(std::wstring wide_string) { - // Determine the required size of the buffer - int bufferSize = WideCharToMultiByte(CP_UTF8, 0, wide_string.c_str(), -1, - nullptr, 0, nullptr, nullptr); - // Convert the wide string to UTF-8 - std::string str(bufferSize, '\0'); - WideCharToMultiByte(CP_UTF8, 0, wide_string.c_str(), -1, &str[0], bufferSize, - nullptr, nullptr); - // Remove the null-terminator from the resulting string - str.pop_back(); - - return str; -} diff --git a/IntelPresentMon/PresentMonUtils/StringUtils.h b/IntelPresentMon/PresentMonUtils/StringUtils.h deleted file mode 100644 index 6b5511a5c..000000000 --- a/IntelPresentMon/PresentMonUtils/StringUtils.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once -#include - -std::string ConvertFromWideString(std::wstring wide_string); \ No newline at end of file diff --git a/IntelPresentMon/SampleClient/CliOptions.h b/IntelPresentMon/SampleClient/CliOptions.h index 0f1b04157..2fbb8f5d5 100644 --- a/IntelPresentMon/SampleClient/CliOptions.h +++ b/IntelPresentMon/SampleClient/CliOptions.h @@ -1,7 +1,9 @@ -#pragma once +#pragma once #include "../CommonUtilities/cli/CliFramework.h" #include "../CommonUtilities/log/Level.h" +#include "../CommonUtilities/log/Verbose.h" #include "../CommonUtilities/ref/StaticReflection.h" +#include #include namespace clio @@ -10,6 +12,7 @@ namespace clio { Introspection, DynamicQuery, + DynamicQueryNoTargetAll, FrameQuery, CsvFrameQuery, CheckMetric, @@ -25,6 +28,8 @@ namespace clio ServiceCrashClient, EtlLogger, PacedPlayback, + PacedFramePlayback, + IpcComponentServer, IntrospectionDevices, Count, }; @@ -38,7 +43,6 @@ namespace clio Option submode{ this, "--submode", 0, "Which submode option to run for the given mode" }; private: Group gc_{ this, "Connection", "Control client connection" }; public: Option controlPipe{ this, "--control-pipe", "", "Name of the named pipe to use for the client-service control channel" }; - Option introNsm{ this, "--intro-nsm", "", "Name of the NSM used for introspection data" }; Option middlewareDllPath{ this, "--middleware-dll-path", "", "Override middleware DLL path discovery with custom path" }; private: Group gs_{ this, "Sampling", "Control sampling / targeting behavior" }; public: Option metricOffset{ this, "--metric-offset", 1064., "Offset from top for frame data. Used in --dynamic-query-sample" }; @@ -46,12 +50,14 @@ namespace clio Option processId{ this, "--process-id", 0, "Process Id to use for polling or frame data capture" }; Option processName{ this, "--process-name", "", "Name of process to use for polling or frame data capture" }; Option metric{ this, "--metric", "", "PM_METRIC, ex. PM_METRIC_PRESENTED_FPS" }; + Option defaultAdapterId{ this, "--default-adapter-id", 0, "GPU device id to use for system-wide dynamic polling" }; Option telemetryPeriodMs{ this, "--telemetry-period-ms", {}, "Telemetry period in milliseconds" }; Option etwFlushPeriodMs{ this, "--etw-flush-period-ms", {}, "ETW manual flush period in milliseconds" }; Option runTime{ this, "--run-time", 10., "How long to capture for, in seconds" }; Option runStart{ this, "--run-start", 1., "How many seconds to delay before beginning run" }; Option pollPeriod{ this, "--poll-period", 0.1, "Period in seconds for polling the API query" }; Option outputPath{ this, "--output-path", {}, "Full path to output to" }; + Option frameLimit{ this, "--frame-limit", 0, "Maximum number of frames to capture (0 for unlimited)" }; private: Group gl_{ this, "Logging", "Control logging behavior" }; public: Option logLevel{ this, "--log-level", log::Level::Error, "Severity to log at", CLI::CheckedTransformer{ log::GetLevelMapNarrow(), CLI::ignore_case } }; Option logTraceLevel{ this, "--log-trace-level", log::Level::Error, "Severity to print stacktrace at", CLI::CheckedTransformer{ log::GetLevelMapNarrow(), CLI::ignore_case } }; @@ -60,11 +66,16 @@ namespace clio Option logAllowList{ this, "--log-allow-list", "", "Path to log allow list (with trace overrides)", CLI::ExistingFile }; Option logFolder{ this, "--log-folder", "", "Folder to create log file(s) in" }; Flag logNamePid{ this, "--log-name-pid", "Append PID to the log file name" }; + Option> logVerboseModules{ this, "--log-verbose-modules", {}, "Verbose logging modules to enable", CLI::CheckedTransformer{ log::GetVerboseModuleMapNarrow(), CLI::ignore_case } }; private: Group gv_{ this, "Service", "Control service options" }; public: Flag servicePacePlayback{ this, "--service-pace-playback", "Pace ETL playback on the service" }; Option serviceEtlPath{ this, "--service-etl-path", "", "Path of the ETL file to pass to the service for playback" }; private: Group gt_{ this, "Testing", "Control testing support options" }; public: Flag testExpectError{ this, "--test-expect-error", "Indicates to test modes that fail state is being tested" }; + Option ipcSystemRingCapacity{ this, "--ipc-system-ring-capacity", 32, + "System telemetry ring capacity for the IPC component server" }; + Option ipcSystemSamplesPerPush{ this, "--ipc-system-samples-per-push", 12, + "Samples per push batch for the IPC component server" }; static constexpr const char* description = "Minimal Sample Client for Intel PresentMon service"; static constexpr const char* name = "SampleClient.exe"; diff --git a/IntelPresentMon/SampleClient/DynamicQueryNoTargetSample.h b/IntelPresentMon/SampleClient/DynamicQueryNoTargetSample.h new file mode 100644 index 000000000..6801c4172 --- /dev/null +++ b/IntelPresentMon/SampleClient/DynamicQueryNoTargetSample.h @@ -0,0 +1,359 @@ +#pragma once +#include "../PresentMonAPIWrapper/PresentMonAPIWrapper.h" +#include "../PresentMonAPIWrapperCommon/Introspection.h" +#include "../Interprocess/source/SystemDeviceId.h" +#include "../CommonUtilities/IntervalWaiter.h" +#include "CliOptions.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + struct QueryKey_ + { + PM_METRIC metric; + uint32_t arrayIndex; + PM_STAT stat; + }; + + struct QueryKeyHasher_ + { + size_t operator()(const QueryKey_& key) const noexcept + { + uint64_t h = (uint64_t)key.metric; + h = (h * 1315423911u) ^ (uint64_t)key.arrayIndex; + h = (h * 1315423911u) ^ (uint64_t)key.stat; + return (size_t)h; + } + }; + + struct QueryKeyEqual_ + { + bool operator()(const QueryKey_& lhs, const QueryKey_& rhs) const noexcept + { + return lhs.metric == rhs.metric && + lhs.arrayIndex == rhs.arrayIndex && + lhs.stat == rhs.stat; + } + }; + + struct QueryItem_ + { + size_t elementIndex = 0; + PM_DATA_TYPE outputType = PM_DATA_TYPE_VOID; + PM_ENUM enumId = PM_ENUM_NULL_ENUM; + std::string label; + uint32_t arrayIndex = 0; + uint32_t arraySize = 1; + }; + + bool HasStat_(const pmapi::intro::MetricView& metric, PM_STAT stat) + { + for (auto s : metric.GetStatInfo()) { + if (s.GetStat() == stat) { + return true; + } + } + return false; + } + + PM_STAT ChooseDynamicStat_(const pmapi::intro::MetricView& metric) + { + if (HasStat_(metric, PM_STAT_AVG)) { + return PM_STAT_AVG; + } + if (HasStat_(metric, PM_STAT_NON_ZERO_AVG)) { + return PM_STAT_NON_ZERO_AVG; + } + if (HasStat_(metric, PM_STAT_NEWEST_POINT)) { + return PM_STAT_NEWEST_POINT; + } + if (HasStat_(metric, PM_STAT_MID_POINT)) { + return PM_STAT_MID_POINT; + } + if (HasStat_(metric, PM_STAT_OLDEST_POINT)) { + return PM_STAT_OLDEST_POINT; + } + return PM_STAT_NONE; + } + + PM_DATA_TYPE SelectDynamicOutputType_(PM_STAT stat, PM_DATA_TYPE polledType) + { + if (stat == PM_STAT_AVG || stat == PM_STAT_NON_ZERO_AVG) { + return PM_DATA_TYPE_DOUBLE; + } + return polledType; + } + + std::optional FindDeviceMetricInfo_( + const pmapi::intro::MetricView& metric, uint32_t deviceId) + { + for (auto info : metric.GetDeviceMetricInfo()) { + if (info.GetDevice().GetId() == deviceId) { + return info; + } + } + return {}; + } + + std::optional SelectGpuDeviceId_( + const pmapi::intro::Root& intro, const clio::Options& opt, std::string& outName, std::string& outError) + { + if (opt.defaultAdapterId) { + const uint32_t requestedId = *opt.defaultAdapterId; + if (requestedId == 0 || requestedId == pmon::ipc::kSystemDeviceId) { + outError = "Invalid --default-adapter-id (must be a GPU device id)."; + return {}; + } + try { + const auto dev = intro.FindDevice(requestedId); + if (dev.GetType() != PM_DEVICE_TYPE_GRAPHICS_ADAPTER) { + outError = "Requested adapter id is not a graphics adapter."; + return {}; + } + outName = dev.GetName(); + return requestedId; + } + catch (...) { + outError = "Requested adapter id not found in introspection."; + return {}; + } + } + + for (auto dev : intro.GetDevices()) { + if (dev.GetType() == PM_DEVICE_TYPE_GRAPHICS_ADAPTER) { + outName = dev.GetName(); + return dev.GetId(); + } + } + return {}; + } + + std::string FormatValue_( + const QueryItem_& item, + const std::vector& elements, + const uint8_t* pBlob, + const pmapi::intro::Root& intro) + { + if (item.elementIndex >= elements.size()) { + return "n/a"; + } + const auto& el = elements[item.elementIndex]; + const uint8_t* pData = pBlob + el.dataOffset; + switch (item.outputType) { + case PM_DATA_TYPE_DOUBLE: + return std::format("{}", *reinterpret_cast(pData)); + case PM_DATA_TYPE_INT32: + return std::format("{}", *reinterpret_cast(pData)); + case PM_DATA_TYPE_UINT32: + return std::format("{}", *reinterpret_cast(pData)); + case PM_DATA_TYPE_UINT64: + return std::format("{}", *reinterpret_cast(pData)); + case PM_DATA_TYPE_BOOL: + return *reinterpret_cast(pData) ? "true" : "false"; + case PM_DATA_TYPE_ENUM: + { + const int32_t value = *reinterpret_cast(pData); + if (item.enumId != PM_ENUM_NULL_ENUM) { + try { + return intro.FindEnumKey(item.enumId, value).GetSymbol(); + } + catch (...) { + return std::format("{}", value); + } + } + return std::format("{}", value); + } + case PM_DATA_TYPE_STRING: + return std::string{ reinterpret_cast(pData) }; + default: + return "n/a"; + } + } +} + +int DynamicQueryNoTargetSample(std::unique_ptr&& pSession, double windowSize, double metricOffset) +{ + using Clock = std::chrono::steady_clock; + + try { + auto& opt = clio::Options::Get(); + + if (opt.telemetryPeriodMs) { + pSession->SetTelemetryPollingPeriod(0, *opt.telemetryPeriodMs); + } + if (opt.etwFlushPeriodMs) { + pSession->SetEtwFlushPeriod(*opt.etwFlushPeriodMs); + } + + auto pIntro = pSession->GetIntrospectionRoot(); + const auto& intro = *pIntro; + + std::string gpuName; + std::string gpuError; + std::optional gpuDeviceId = SelectGpuDeviceId_(intro, opt, gpuName, gpuError); + if (opt.defaultAdapterId && !gpuDeviceId.has_value()) { + std::cout << "Error: " << gpuError << std::endl; + return -1; + } + if (gpuDeviceId.has_value()) { + std::cout << "Using GPU device id " << *gpuDeviceId << " (" << gpuName << ")" << std::endl; + } + else { + std::cout << "No GPU device found, using system device only." << std::endl; + } + + std::vector elements; + std::vector items; + std::unordered_set seen; + + for (auto metric : intro.GetMetrics()) { + const auto metricType = metric.GetType(); + const bool isStatic = metricType == PM_METRIC_TYPE_STATIC; + if (!isStatic && !pmapi::intro::MetricTypeIsDynamic(metricType)) { + continue; + } + + PM_STAT stat = isStatic ? PM_STAT_NONE : ChooseDynamicStat_(metric); + if (!isStatic && stat == PM_STAT_NONE) { + continue; + } + + std::optional chosenInfo; + const auto systemInfo = FindDeviceMetricInfo_(metric, pmon::ipc::kSystemDeviceId); + if (systemInfo.has_value() && systemInfo->IsAvailable()) { + chosenInfo = systemInfo; + } + else if (gpuDeviceId.has_value()) { + const auto gpuInfo = FindDeviceMetricInfo_(metric, *gpuDeviceId); + if (gpuInfo.has_value() && gpuInfo->IsAvailable()) { + chosenInfo = gpuInfo; + } + } + + if (!chosenInfo.has_value()) { + continue; + } + + const uint32_t deviceId = chosenInfo->GetDevice().GetId(); + if (deviceId == 0) { + continue; + } + + const uint32_t arraySize = chosenInfo->GetArraySize(); + if (arraySize == 0) { + continue; + } + + const auto typeInfo = metric.GetDataTypeInfo(); + const PM_DATA_TYPE outputType = isStatic + ? typeInfo.GetPolledType() + : SelectDynamicOutputType_(stat, typeInfo.GetPolledType()); + + std::string statSymbol; + try { + statSymbol = intro.FindEnumKey(PM_ENUM_STAT, (int)stat).GetSymbol(); + } + catch (...) { + statSymbol = "PM_STAT_UNKNOWN"; + } + + const std::string metricSymbol = metric.Introspect().GetSymbol(); + + for (uint32_t arrayIndex = 0; arrayIndex < arraySize; ++arrayIndex) { + const QueryKey_ key{ + .metric = metric.GetId(), + .arrayIndex = arrayIndex, + .stat = stat, + }; + if (seen.find(key) != seen.end()) { + continue; + } + seen.insert(key); + + PM_QUERY_ELEMENT element{ + .metric = metric.GetId(), + .stat = stat, + .deviceId = deviceId, + .arrayIndex = arrayIndex, + .dataOffset = 0, + .dataSize = 0, + }; + const size_t elementIndex = elements.size(); + elements.push_back(element); + + std::string label = metricSymbol; + if (arraySize > 1) { + label += "#"; + label += std::to_string(arrayIndex); + } + label += "["; + label += statSymbol; + label += "]"; + + items.push_back(QueryItem_{ + .elementIndex = elementIndex, + .outputType = outputType, + .enumId = typeInfo.GetEnumId(), + .label = std::move(label), + .arrayIndex = arrayIndex, + .arraySize = arraySize, + }); + } + } + + if (elements.empty()) { + std::cout << "No eligible metrics found for system or GPU devices." << std::endl; + return -1; + } + + auto query = pSession->RegisterDynamicQuery(elements, windowSize, metricOffset); + auto blobs = query.MakeBlobContainer(1u); + + if (*opt.runStart > 0.0) { + std::this_thread::sleep_for(std::chrono::duration(*opt.runStart)); + } + + const double pollPeriod = *opt.pollPeriod > 0.0 ? *opt.pollPeriod : 0.1; + pmon::util::IntervalWaiter waiter{ pollPeriod }; + + auto PrintPoll_ = [&](size_t pollIndex) { + std::cout << "======= poll " << pollIndex << " =======" << std::endl; + const uint8_t* pBlob = blobs.GetFirst(); + for (const auto& item : items) { + std::cout << item.label << ":" << FormatValue_(item, elements, pBlob, intro) << std::endl; + } + }; + + const double runTime = *opt.runTime; + if (runTime <= 0.0) { + query.Poll(blobs); + PrintPoll_(1); + return 0; + } + + const auto endTime = Clock::now() + std::chrono::duration(runTime); + size_t pollIndex = 1; + while (Clock::now() < endTime) { + query.Poll(blobs); + PrintPoll_(pollIndex++); + waiter.Wait(); + } + } + catch (const std::exception& e) { + std::cout << "Error: " << e.what() << std::endl; + return -1; + } + catch (...) { + std::cout << "Unknown Error" << std::endl; + return -1; + } + + return 0; +} diff --git a/IntelPresentMon/SampleClient/IpcComponentServer.cpp b/IntelPresentMon/SampleClient/IpcComponentServer.cpp new file mode 100644 index 000000000..433f1e1d5 --- /dev/null +++ b/IntelPresentMon/SampleClient/IpcComponentServer.cpp @@ -0,0 +1,132 @@ +// Copyright (C) 2022-2025 Intel Corporation +// SPDX-License-Identifier: MIT + +#include "../Interprocess/source/OwnedDataSegment.h" +#include "../Interprocess/source/DataStores.h" + +#include "../CommonUtilities/Exception.h" +#include "../CommonUtilities/log/Log.h" +#include "../PresentMonAPI2/PresentMonAPI.h" +#include "CliOptions.h" + +#include +#include +#include +#include +#include + +using namespace std::literals; + +namespace ipc = pmon::ipc; + +// Hardcoded segment name shared with the test. +static constexpr const char* kSystemSegName = "pm_ipc_system_store_test_seg"; + +// We only create two metrics: +// 1) A scalar metric with 1 element (count = 1) +// 2) An "array-like" metric with 2 elements (count = 2) +// +// The test goal is ring push/read plumbing, not capability validation. +static constexpr PM_METRIC kScalarMetric = PM_METRIC_CPU_FREQUENCY; +static constexpr PM_METRIC kArrayMetric = PM_METRIC_CPU_UTILIZATION; +static constexpr size_t kDefaultSystemRingCapacity = 32; +static constexpr size_t kDefaultSamplesPerPush = 12; +static constexpr uint64_t kBaseTimestamp = 10'000ull; +static constexpr size_t kSystemSegmentBytes = 512 * 1024; + +static void BuildRings_(ipc::SystemDataStore& store, size_t ringCapacity) +{ + // Scalar metric + store.telemetryData.AddRing(kScalarMetric, ringCapacity, 1, PM_DATA_TYPE_DOUBLE); + + // Array metric with 2 elements + store.telemetryData.AddRing(kArrayMetric, ringCapacity, 2, PM_DATA_TYPE_DOUBLE); +} + +static void PushDeterministicSamples_(ipc::SystemDataStore& store, size_t sampleCount, + size_t& nextIndex) +{ + auto& scalar = store.telemetryData.FindRing(kScalarMetric); + + auto& array = store.telemetryData.FindRing(kArrayMetric); + + // Expect sizes: scalar = 1 ring, array = 2 rings + if (scalar.size() != 1 || array.size() != 2) { + throw std::logic_error("IpcSystemServer: ring vectors not sized as expected"); + } + + auto& scalarRing = scalar.at(0); + auto& arr0 = array.at(0); + auto& arr1 = array.at(1); + + for (size_t i = 0; i < sampleCount; ++i) { + const size_t sampleIndex = nextIndex + i; + const uint64_t ts = kBaseTimestamp + static_cast(sampleIndex); + + // Scalar sequence + const double freq = 3000.0 + 10.0 * static_cast(sampleIndex); + scalarRing.Push(freq, ts); + + // Array element 0 sequence + const double util0 = 5.0 + static_cast(sampleIndex); + arr0.Push(util0, ts); + + // Array element 1 sequence (offset so we can tell them apart) + const double util1 = 50.0 + static_cast(sampleIndex) * 2.0; + arr1.Push(util1, ts); + } + + nextIndex += sampleCount; +} + +// Submode entry point. +int IpcComponentServer() +{ + const auto& opt = clio::Options::Get(); + const size_t ringCapacity = *opt.ipcSystemRingCapacity; + const size_t samplesPerPush = *opt.ipcSystemSamplesPerPush; + ipc::DataStoreSizingInfo sizing{}; + sizing.overrideBytes = kSystemSegmentBytes; + + // Create the shared memory segment hosting SystemDataStore. + ipc::OwnedDataSegment seg{ + kSystemSegName, + sizing + }; + auto& store = seg.GetStore(); + + // Only build the two test rings. + BuildRings_(store, ringCapacity); + + // Ping gate to sync "server ready" with the test harness. + std::string line; + std::getline(std::cin, line); + if (line != "%ping") { + std::cout << "%%{ping-error}%%" << std::endl; + return -1; + } + std::cout << "%%{ping-ok}%%" << std::endl; + + // Push a deterministic batch after ping. + size_t nextIndex = 0; + PushDeterministicSamples_(store, samplesPerPush, nextIndex); + + // Command loop. + while (std::getline(std::cin, line)) { + if (line == "%quit") { + std::cout << "%%{quit-ok}%%" << std::endl; + std::this_thread::sleep_for(25ms); + return 0; + } + else if (line == "%push-more") { + PushDeterministicSamples_(store, samplesPerPush, nextIndex); + std::cout << "%%{push-more-ok}%%" << std::endl; + } + else { + std::cout << "%%{err-bad-command}%%" << std::endl; + } + } + + return -1; +} + diff --git a/IntelPresentMon/SampleClient/IpcComponentServer.h b/IntelPresentMon/SampleClient/IpcComponentServer.h new file mode 100644 index 000000000..0d82228a6 --- /dev/null +++ b/IntelPresentMon/SampleClient/IpcComponentServer.h @@ -0,0 +1,2 @@ +#pragma once +int IpcComponentServer(); \ No newline at end of file diff --git a/IntelPresentMon/SampleClient/LogSetup.cpp b/IntelPresentMon/SampleClient/LogSetup.cpp index b3ae3eaa0..8135c51e7 100644 --- a/IntelPresentMon/SampleClient/LogSetup.cpp +++ b/IntelPresentMon/SampleClient/LogSetup.cpp @@ -1,4 +1,4 @@ -#include "../CommonUtilities/log/Log.h" +#include "../CommonUtilities/log/Log.h" #include "../CommonUtilities/log/Channel.h" #include "../CommonUtilities/log/MsvcDebugDriver.h" #include "../CommonUtilities/log/BasicFileDriver.h" @@ -83,6 +83,12 @@ namespace p2sam GlobalPolicy::Get().SetLogLevel(*opt.logLevel); getters.getGlobalPolicy().SetLogLevel(*opt.logLevel); } + if (opt.logVerboseModules) { + for (auto mod : *opt.logVerboseModules) { + GlobalPolicy::Get().ActivateVerboseModule(mod); + getters.getGlobalPolicy().ActivateVerboseModule(mod); + } + } if (opt.logTraceLevel) { GlobalPolicy::Get().SetTraceLevel(*opt.logTraceLevel); getters.getGlobalPolicy().SetTraceLevel(*opt.logTraceLevel); @@ -114,4 +120,4 @@ namespace p2sam pmFlushEntryPoint_(); FlushEntryPoint(); } -} \ No newline at end of file +} diff --git a/IntelPresentMon/SampleClient/PacedFramePlayback.cpp b/IntelPresentMon/SampleClient/PacedFramePlayback.cpp new file mode 100644 index 000000000..cfdeee551 --- /dev/null +++ b/IntelPresentMon/SampleClient/PacedFramePlayback.cpp @@ -0,0 +1,343 @@ +#include "PacedFramePlayback.h" +#include "CliOptions.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pmon; +using namespace std::literals; + +namespace +{ + const std::array kFrameCsvHeader{ + "Application", + "ProcessID", + "SwapChainAddress", + "PresentRuntime", + "SyncInterval", + "PresentFlags", + "AllowsTearing", + "PresentMode", + "FrameType", + "CPUStartTime", + "MsBetweenSimulationStart", + "MsBetweenPresents", + "MsBetweenDisplayChange", + "MsInPresentAPI", + "MsRenderPresentLatency", + "MsUntilDisplayed", + "MsPCLatency", + "MsBetweenAppStart", + "MsCPUBusy", + "MsCPUWait", + "MsGPULatency", + "MsGPUTime", + "MsGPUBusy", + "MsGPUWait", + "MsVideoBusy", + "MsAnimationError", + "AnimationTime", + "MsFlipDelay", + "MsAllInputToPhotonLatency", + "MsClickToPhotonLatency", + "MsInstrumentedLatency", + }; + + std::string TranslateGraphicsRuntime(PM_GRAPHICS_RUNTIME runtime) + { + switch (runtime) { + case PM_GRAPHICS_RUNTIME_DXGI: + return "DXGI"; + case PM_GRAPHICS_RUNTIME_D3D9: + return "D3D9"; + default: + return "Other"; + } + } + + std::string TranslatePresentMode(PM_PRESENT_MODE presentMode) + { + switch (presentMode) { + case PM_PRESENT_MODE_HARDWARE_LEGACY_FLIP: + return "Hardware: Legacy Flip"; + case PM_PRESENT_MODE_HARDWARE_LEGACY_COPY_TO_FRONT_BUFFER: + return "Hardware: Legacy Copy to front buffer"; + case PM_PRESENT_MODE_HARDWARE_INDEPENDENT_FLIP: + return "Hardware: Independent Flip"; + case PM_PRESENT_MODE_COMPOSED_FLIP: + return "Composed: Flip"; + case PM_PRESENT_MODE_HARDWARE_COMPOSED_INDEPENDENT_FLIP: + return "Hardware Composed: Independent Flip"; + case PM_PRESENT_MODE_COMPOSED_COPY_WITH_GPU_GDI: + return "Composed: Copy with GPU GDI"; + case PM_PRESENT_MODE_COMPOSED_COPY_WITH_CPU_GDI: + return "Composed: Copy with CPU GDI"; + default: + return "Other"; + } + } + + std::string TranslateFrameType(PM_FRAME_TYPE frameType) + { + switch (frameType) { + case PM_FRAME_TYPE_NOT_SET: + case PM_FRAME_TYPE_UNSPECIFIED: + case PM_FRAME_TYPE_APPLICATION: + return "Application"; + case PM_FRAME_TYPE_AMD_AFMF: + return "AMD_AFMF"; + case PM_FRAME_TYPE_INTEL_XEFG: + return "Intel XeSS-FG"; + default: + return "Other"; + } + } + + void WriteHeader(std::ofstream& csv) + { + for (size_t i = 0; i < kFrameCsvHeader.size(); ++i) { + if (i > 0) { + csv << ","; + } + csv << kFrameCsvHeader[i]; + } + csv << "\n"; + } + + void WriteOptionalDouble(std::ofstream& csv, double value) + { + if (std::isnan(value)) { + csv << "NA"; + return; + } + csv << value; + } + + void WriteOptionalElement(std::ofstream& csv, const pmapi::FixedQueryElement& element) + { + if (!element.IsAvailable()) { + csv << "NA"; + return; + } + WriteOptionalDouble(csv, element.As()); + } +} + +int PacedFramePlaybackTest(std::unique_ptr pSession) +{ + auto& opt = clio::Options::Get(); + + std::optional errorStatus; + + try { + if (!opt.processId) { + pmlog_error("need pid"); + } + if (!opt.outputPath) { + pmlog_error("need output path"); + } + + if (opt.etwFlushPeriodMs) { + pSession->SetEtwFlushPeriod(*opt.etwFlushPeriodMs); + } + if (opt.telemetryPeriodMs) { + pSession->SetTelemetryPollingPeriod(0, *opt.telemetryPeriodMs); + } + + std::string line; + std::getline(std::cin, line); + if (line != "%ping") { + std::cout << "%%{ping-error}%%" << std::endl; + return -1; + } + std::cout << "%%{ping-ok}%%" << std::endl; + + const auto processName = opt.processName.AsOptional().value_or("unknown"s); + const auto frameLimit = static_cast(*opt.frameLimit); + + PM_BEGIN_FIXED_FRAME_QUERY(FrameQuery) + pmapi::FixedQueryElement applicationName{ this, PM_METRIC_APPLICATION, PM_STAT_NONE }; + pmapi::FixedQueryElement processId{ this, PM_METRIC_PROCESS_ID, PM_STAT_NONE }; + pmapi::FixedQueryElement swapChain{ this, PM_METRIC_SWAP_CHAIN_ADDRESS, PM_STAT_NONE }; + pmapi::FixedQueryElement presentRuntime{ this, PM_METRIC_PRESENT_RUNTIME, PM_STAT_NONE }; + pmapi::FixedQueryElement syncInterval{ this, PM_METRIC_SYNC_INTERVAL, PM_STAT_NONE }; + pmapi::FixedQueryElement presentFlags{ this, PM_METRIC_PRESENT_FLAGS, PM_STAT_NONE }; + pmapi::FixedQueryElement allowsTearing{ this, PM_METRIC_ALLOWS_TEARING, PM_STAT_NONE }; + pmapi::FixedQueryElement presentMode{ this, PM_METRIC_PRESENT_MODE, PM_STAT_NONE }; + pmapi::FixedQueryElement frameType{ this, PM_METRIC_FRAME_TYPE, PM_STAT_NONE }; + pmapi::FixedQueryElement cpuStartTime{ this, PM_METRIC_CPU_START_TIME, PM_STAT_NONE }; + pmapi::FixedQueryElement msBetweenSimStart{ this, PM_METRIC_BETWEEN_SIMULATION_START, PM_STAT_NONE }; + pmapi::FixedQueryElement msBetweenPresents{ this, PM_METRIC_BETWEEN_PRESENTS, PM_STAT_NONE }; + pmapi::FixedQueryElement msBetweenDisplayChange{ this, PM_METRIC_BETWEEN_DISPLAY_CHANGE, PM_STAT_NONE }; + pmapi::FixedQueryElement msInPresentApi{ this, PM_METRIC_IN_PRESENT_API, PM_STAT_NONE }; + pmapi::FixedQueryElement msRenderPresentLatency{ this, PM_METRIC_RENDER_PRESENT_LATENCY, PM_STAT_NONE }; + pmapi::FixedQueryElement msUntilDisplayed{ this, PM_METRIC_UNTIL_DISPLAYED, PM_STAT_NONE }; + pmapi::FixedQueryElement msPcLatency{ this, PM_METRIC_PC_LATENCY, PM_STAT_NONE }; + pmapi::FixedQueryElement msBetweenAppStart{ this, PM_METRIC_BETWEEN_APP_START, PM_STAT_NONE }; + pmapi::FixedQueryElement msCpuBusy{ this, PM_METRIC_CPU_BUSY, PM_STAT_NONE }; + pmapi::FixedQueryElement msCpuWait{ this, PM_METRIC_CPU_WAIT, PM_STAT_NONE }; + pmapi::FixedQueryElement msGpuLatency{ this, PM_METRIC_GPU_LATENCY, PM_STAT_NONE }; + pmapi::FixedQueryElement msGpuTime{ this, PM_METRIC_GPU_TIME, PM_STAT_NONE }; + pmapi::FixedQueryElement msGpuBusy{ this, PM_METRIC_GPU_BUSY, PM_STAT_NONE }; + pmapi::FixedQueryElement msGpuWait{ this, PM_METRIC_GPU_WAIT, PM_STAT_NONE }; + pmapi::FixedQueryElement msAnimationError{ this, PM_METRIC_ANIMATION_ERROR, PM_STAT_NONE }; + pmapi::FixedQueryElement animationTime{ this, PM_METRIC_ANIMATION_TIME, PM_STAT_NONE }; + pmapi::FixedQueryElement msFlipDelay{ this, PM_METRIC_FLIP_DELAY, PM_STAT_NONE }; + pmapi::FixedQueryElement msAllInputToPhotonLatency{ this, PM_METRIC_ALL_INPUT_TO_PHOTON_LATENCY, PM_STAT_NONE }; + pmapi::FixedQueryElement msClickToPhotonLatency{ this, PM_METRIC_CLICK_TO_PHOTON_LATENCY, PM_STAT_NONE }; + pmapi::FixedQueryElement msInstrumentedLatency{ this, PM_METRIC_INSTRUMENTED_LATENCY, PM_STAT_NONE }; + PM_END_FIXED_QUERY query{ *pSession, 512 }; + + auto tracker = pSession->TrackProcess(*opt.processId, true, false); + + std::ofstream csv{ *opt.outputPath }; + if (!csv.is_open()) { + pmlog_error("failed to open output file"); + return -1; + } + csv << std::fixed << std::setprecision(14); + WriteHeader(csv); + + using Clock = std::chrono::high_resolution_clock; + const auto start = Clock::now(); + size_t emptyPollCount = 0; + size_t totalRecorded = 0; + const size_t emptyLimit = 10; + + while (true) { + const auto elapsed = std::chrono::duration(Clock::now() - start).count(); + const auto processed = query.ForEachConsume(tracker, [&] { + if (frameLimit > 0 && totalRecorded >= frameLimit) { + return; + } + std::string appName = processName; + if (query.applicationName.IsAvailable()) { + appName = query.applicationName.As(); + } + csv << appName << ","; + if (query.processId.IsAvailable()) { + csv << query.processId.As() << ","; + } + else { + csv << *opt.processId << ","; + } + csv << std::hex << std::uppercase << "0x" << query.swapChain.As() + << std::dec << std::nouppercase << ","; + csv << TranslateGraphicsRuntime(query.presentRuntime.As()) << ","; + csv << query.syncInterval.As() << ","; + csv << query.presentFlags.As() << ","; + csv << (query.allowsTearing.As() ? 1 : 0) << ","; + csv << TranslatePresentMode(query.presentMode.As()) << ","; + csv << TranslateFrameType(query.frameType.As()) << ","; + WriteOptionalElement(csv, query.cpuStartTime); + csv << ","; + WriteOptionalElement(csv, query.msBetweenSimStart); + csv << ","; + WriteOptionalElement(csv, query.msBetweenPresents); + csv << ","; + WriteOptionalElement(csv, query.msBetweenDisplayChange); + csv << ","; + WriteOptionalElement(csv, query.msInPresentApi); + csv << ","; + WriteOptionalElement(csv, query.msRenderPresentLatency); + csv << ","; + WriteOptionalElement(csv, query.msUntilDisplayed); + csv << ","; + WriteOptionalElement(csv, query.msPcLatency); + csv << ","; + WriteOptionalElement(csv, query.msBetweenAppStart); + csv << ","; + WriteOptionalElement(csv, query.msCpuBusy); + csv << ","; + WriteOptionalElement(csv, query.msCpuWait); + csv << ","; + WriteOptionalElement(csv, query.msGpuLatency); + csv << ","; + WriteOptionalElement(csv, query.msGpuTime); + csv << ","; + WriteOptionalElement(csv, query.msGpuBusy); + csv << ","; + WriteOptionalElement(csv, query.msGpuWait); + csv << ","; + WriteOptionalDouble(csv, 0.0); + csv << ","; + WriteOptionalElement(csv, query.msAnimationError); + csv << ","; + WriteOptionalElement(csv, query.animationTime); + csv << ","; + WriteOptionalElement(csv, query.msFlipDelay); + csv << ","; + WriteOptionalElement(csv, query.msAllInputToPhotonLatency); + csv << ","; + WriteOptionalElement(csv, query.msClickToPhotonLatency); + csv << ","; + WriteOptionalElement(csv, query.msInstrumentedLatency); + csv << "\n"; + ++totalRecorded; + }); + + if (frameLimit > 0 && totalRecorded >= frameLimit) { + break; + } + + if (processed == 0) { + if (totalRecorded > 0) { + if (++emptyPollCount >= emptyLimit) { + break; + } + } + else if (elapsed >= 1.0) { + break; + } + } + else { + emptyPollCount = 0; + } + + if (processed == 0) { + std::this_thread::sleep_for(8ms); + } + } + } + catch (const pmapi::ApiErrorException& e) { + if (!opt.testExpectError) { + throw; + } + errorStatus = e.GetCode(); + } + + std::string line; + + if (errorStatus) { + std::getline(std::cin, line); + if (line != "%err-check") { + std::cout << "%%{err-check-error}%%" << std::endl; + return -1; + } + auto&& err = pmapi::EnumMap::GetKeyMap(PM_ENUM_STATUS)->at(*errorStatus).narrowSymbol; + std::cout << "%%{err-check-ok:" << err << "}%%" << std::endl; + } + + while (std::getline(std::cin, line)) { + if (line == "%quit") { + std::cout << "%%{quit-ok}%%" << std::endl; + std::this_thread::sleep_for(25ms); + return 0; + } + else { + std::cout << "%%{err-bad-command}%%" << std::endl; + } + } + + return -1; +} diff --git a/IntelPresentMon/SampleClient/PacedFramePlayback.h b/IntelPresentMon/SampleClient/PacedFramePlayback.h new file mode 100644 index 000000000..398da2415 --- /dev/null +++ b/IntelPresentMon/SampleClient/PacedFramePlayback.h @@ -0,0 +1,5 @@ +#pragma once +#include +#include "../PresentMonAPIWrapper/PresentMonAPIWrapper.h" + +int PacedFramePlaybackTest(std::unique_ptr pSession); diff --git a/IntelPresentMon/SampleClient/PacedPlayback.cpp b/IntelPresentMon/SampleClient/PacedPlayback.cpp index 766d55cd0..0ad4e5803 100644 --- a/IntelPresentMon/SampleClient/PacedPlayback.cpp +++ b/IntelPresentMon/SampleClient/PacedPlayback.cpp @@ -1,4 +1,4 @@ -#include "MultiClient.h" +#include "MultiClient.h" #include "CliOptions.h" #include #include @@ -88,20 +88,6 @@ std::vector BuildQueryElementSet(const pmapi::intro::Root& int { std::vector qels; for (const auto& m : intro.GetMetrics()) { - // there is no reliable way of distinguishing CPU telemetry metrics from PresentData-based metrics via introspection - // adding CPU device type is an idea, however that would require changing device id of the cpu metrics from 0 to - // whatever id is assigned to cpu (probably an upper range like 1024+) and this might break existing code that just - // hardcodes device id for the CPU metrics; for the time being use a hardcoded blacklist here - if (rn::contains(std::array{ - PM_METRIC_CPU_UTILIZATION, - PM_METRIC_CPU_POWER_LIMIT, - PM_METRIC_CPU_POWER, - PM_METRIC_CPU_TEMPERATURE, - PM_METRIC_CPU_FREQUENCY, - PM_METRIC_CPU_CORE_UTILITY, - }, m.GetId())) { - continue; - } // only allow dynamic metrics if (m.GetType() != PM_METRIC_TYPE_DYNAMIC && m.GetType() != PM_METRIC_TYPE_DYNAMIC_FRAME) { continue; @@ -120,10 +106,6 @@ std::vector BuildQueryElementSet(const pmapi::intro::Root& int continue; } for (const auto& s : m.GetStatInfo()) { - // skip displayed fps (max) as it is broken now - if (m.GetId() == PM_METRIC_DISPLAYED_FPS && s.GetStat() == PM_STAT_MAX) { - continue; - } qels.push_back(PM_QUERY_ELEMENT{ m.GetId(), s.GetStat() }); } } @@ -161,34 +143,41 @@ class TestClientModule std::vector> RecordPolling(uint32_t targetPid, double recordingStartSec, double recordingStopSec, double pollInterval) { + if (recordingStartSec < 1.) { + pmlog_error("Insufficient recording start leeway").pmwatch(recordingStartSec).no_trace(); + } // start tracking target - auto tracker = pSession_->TrackProcess(targetPid); + auto tracker = pSession_->TrackProcess(targetPid, true, false); // get the waiter and the timer clocks ready using Clock = std::chrono::high_resolution_clock; - const auto startTime = Clock::now(); - util::IntervalWaiter waiter{ pollInterval, 0.001 }; + // wait to give time for the static data (startQpc specifically) to propagate to the shm + // wait until 500ms (buffer time) before the requested recordingStart + util::PrecisionWaiter{}.Wait(recordingStartSec - 0.5); + // capture session startQpc and setup interval waiter to sync with session start + const auto startQpc = pmapi::PollStatic(*pSession_, tracker, PM_METRIC_SESSION_START_QPC).As(); + util::IntervalWaiter waiter{ pollInterval, (int64_t)startQpc, 0.0015 }; // run polling loop and poll into vector std::vector> rows; std::vector cells; BlobReader br{ qels_, pIntro_ }; br.Target(blobs_); - const auto recordingStart = recordingStartSec * 1s; - const auto recordingStop = recordingStopSec * 1s; - for (auto now = Clock::now(), start = Clock::now(); - now - start <= recordingStop; now = Clock::now()) { + while (true) { + const auto wr = waiter.Wait(); // skip recording while time has not reached start time - if (now - start >= recordingStart) { + if (wr.targetSec >= recordingStartSec) { cells.reserve(qels_.size() + 1); - query_.Poll(tracker, blobs_); + query_.PollWithTimestamp(tracker, blobs_, (uint64_t)waiter.TargetTimeToTimestamp(wr.targetSec)); // first column is the time as measured in polling loop - cells.push_back(std::chrono::duration(now - start).count()); + cells.push_back(wr.targetSec + wr.errorSec); // remaining columns are from the query for (size_t i = 0; i < qels_.size(); i++) { cells.push_back(br.At(i)); } rows.push_back(std::move(cells)); } - waiter.Wait(); + if (wr.targetSec >= recordingStopSec) { + break; + } } return rows; } @@ -280,4 +269,4 @@ int PacedPlaybackTest(std::unique_ptr pSession) } return -1; -} \ No newline at end of file +} diff --git a/IntelPresentMon/SampleClient/SampleClient.cpp b/IntelPresentMon/SampleClient/SampleClient.cpp index e01b6b1da..a71236f79 100644 --- a/IntelPresentMon/SampleClient/SampleClient.cpp +++ b/IntelPresentMon/SampleClient/SampleClient.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2022 Intel Corporation +// Copyright (C) 2022 Intel Corporation // SPDX-License-Identifier: MIT #include "../CommonUtilities/win/WinAPI.h" #include @@ -26,6 +26,7 @@ #include "../PresentMonAPI2Loader/Loader.h" #include "Utils.h" #include "DynamicQuerySample.h" +#include "DynamicQueryNoTargetSample.h" #include "FrameQuerySample.h" #include "IntrospectionSample.h" #include "CheckMetricSample.h" @@ -34,7 +35,9 @@ #include "MultiClient.h" #include "ServiceCrashClient.h" #include "EtlLogger.h" +#include "IpcComponentServer.h" #include "PacedPlayback.h" +#include "PacedFramePlayback.h" #include "LogDemo.h" #include "DiagnosticDemo.h" #include "LogSetup.h" @@ -62,8 +65,7 @@ void RunPlaybackFrameQuery() "PresentMonService.exe"s, // "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_tt_"s, - "--intro-nsm"s, "svc-intro-tt"s, + "--shm-name-prefix"s, "pm-tt-shm"s, "--etw-session-name"s, "svc-sesh-tt"s, bp::args(dargs), }; @@ -92,7 +94,7 @@ void RunPlaybackFrameQuery() // track the pid we know to be active in the ETL (1268 for dwm in gold_0) pid = opt.processId.AsOptional().value_or(1268); } - auto tracker = api.TrackProcess(pid); + auto tracker = api.TrackProcess(pid, true, false); std::this_thread::sleep_for(500ms); uint32_t frameCount = 0; @@ -148,8 +150,7 @@ void RunPlaybackDynamicQuery() "PresentMonService.exe"s, // "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_tt_"s, - "--intro-nsm"s, "svc-intro-tt"s, + "--shm-name-prefix"s, "pm-tt-shm"s, "--etw-session-name"s, "svc-sesh-tt"s, bp::args(dargs), }; @@ -172,7 +173,7 @@ void RunPlaybackDynamicQuery() PM_END_FIXED_QUERY query{ api, 200., 50., 1, 1 }; // track the pid we know to be active in the ETL (1268 for dwm in gold_0) - auto tracker = api.TrackProcess(opt.processId.AsOptional().value_or(1268)); + auto tracker = api.TrackProcess(opt.processId.AsOptional().value_or(1268), true, false); pmon::util::IntervalWaiter waiter{ 0.1 }; @@ -223,8 +224,7 @@ void RunPlaybackDynamicQueryN() "PresentMonService.exe"s, // "--timed-stop"s, "10000"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_tt_"s, - "--intro-nsm"s, "svc-intro-tt"s, + "--shm-name-prefix"s, "pm-tt-shm"s, "--etw-session-name"s, "svc-sesh-tt"s, bp::args(dargs), }; @@ -247,7 +247,7 @@ void RunPlaybackDynamicQueryN() PM_END_FIXED_QUERY query{ api, 200., 50., 1, 1 }; // track the pid we know to be active in the ETL (1268 for dwm in gold_0) - auto tracker = api.TrackProcess(opt.processId.AsOptional().value_or(1268)); + auto tracker = api.TrackProcess(opt.processId.AsOptional().value_or(1268), true, false); pmon::util::IntervalWaiter waiter{ 0.1 }; @@ -288,8 +288,7 @@ void IntrospectAllDynamicOptions() bp::child svc{ "PresentMonService.exe"s, "--control-pipe"s, pipeName, - "--nsm-prefix"s, "pmon_nsm_tt_"s, - "--intro-nsm"s, "svc-intro-tt"s, + "--shm-name-prefix"s, "pm-tt-shm"s, "--etw-session-name"s, "svc-sesh-tt"s, }; @@ -343,9 +342,7 @@ int main(int argc, char* argv[]) if (opt.controlPipe) { return std::make_unique(*opt.controlPipe); } - else { - return std::make_unique(); - } + return std::make_unique(); }; // determine requested mode to run the sample app in @@ -362,6 +359,8 @@ int main(int argc, char* argv[]) return DynamicQuerySample(ConnectSession(), *opt.windowSize, *opt.metricOffset, false); case clio::Mode::AddGpuMetric: return DynamicQuerySample(ConnectSession(), *opt.windowSize, *opt.metricOffset, true); + case clio::Mode::DynamicQueryNoTargetAll: + return DynamicQueryNoTargetSample(ConnectSession(), *opt.windowSize, *opt.metricOffset); case clio::Mode::WrapperStaticQuery: return WrapperStaticQuerySample(ConnectSession()); case clio::Mode::MetricList: @@ -378,12 +377,16 @@ int main(int argc, char* argv[]) return EtlLoggerTest(ConnectSession()); case clio::Mode::PacedPlayback: return PacedPlaybackTest(ConnectSession()); + case clio::Mode::PacedFramePlayback: + return PacedFramePlaybackTest(ConnectSession()); case clio::Mode::PlaybackDynamicQuery: RunPlaybackDynamicQueryN(); break; case clio::Mode::PlaybackFrameQuery: RunPlaybackFrameQuery(); break; case clio::Mode::IntrospectAllDynamicOptions: IntrospectAllDynamicOptions(); break; + case clio::Mode::IpcComponentServer: + IpcComponentServer(); break; case clio::Mode::IntrospectionDevices: IntrospectAllDevices(ConnectSession()); break; default: diff --git a/IntelPresentMon/SampleClient/SampleClient.vcxproj b/IntelPresentMon/SampleClient/SampleClient.vcxproj index bac3618b0..2a76dccee 100644 --- a/IntelPresentMon/SampleClient/SampleClient.vcxproj +++ b/IntelPresentMon/SampleClient/SampleClient.vcxproj @@ -1,4 +1,4 @@ - + @@ -110,9 +110,11 @@ + + @@ -126,10 +128,12 @@ + + @@ -139,6 +143,9 @@ {08a704d8-ca1c-45e9-8ede-542a1a43b53e} + + {ca23d648-daef-4f06-81d5-fe619bd31f0b} + {8f86d067-2437-46fc-8f82-4d7155ceced7} @@ -149,4 +156,4 @@ - \ No newline at end of file + diff --git a/IntelPresentMon/SampleClient/SampleClient.vcxproj.filters b/IntelPresentMon/SampleClient/SampleClient.vcxproj.filters index 96c9847b3..07c7673e9 100644 --- a/IntelPresentMon/SampleClient/SampleClient.vcxproj.filters +++ b/IntelPresentMon/SampleClient/SampleClient.vcxproj.filters @@ -10,6 +10,7 @@ + @@ -28,5 +29,6 @@ + diff --git a/IntelPresentMon/SampleStreamerClient/Logging.cpp b/IntelPresentMon/SampleStreamerClient/Logging.cpp deleted file mode 100644 index b93912285..000000000 --- a/IntelPresentMon/SampleStreamerClient/Logging.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include - -namespace pmon::util::log -{ - // this is injected into to util::log namespace and hooks into that system - // not using logging yet in the SampleStreamClient so just return empty pointer - std::shared_ptr GetDefaultChannel() noexcept - { - return {}; - } -} \ No newline at end of file diff --git a/IntelPresentMon/SampleStreamerClient/SampleStreamerClient.cpp b/IntelPresentMon/SampleStreamerClient/SampleStreamerClient.cpp deleted file mode 100644 index 710c0adb6..000000000 --- a/IntelPresentMon/SampleStreamerClient/SampleStreamerClient.cpp +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -// SampleStreamerClient.cpp : This file contains the 'main' function. Program execution begins and ends there. - -#include -#include -#include -#include - -#include "..\Streamer\StreamClient.h" - -#include "../CommonUtilities/log/GlogShim.h" - -uint32_t kClientLoopCount = 50; - -int main(int argc, TCHAR* argv[]) -{ - if (argc < 2) { - LOG(ERROR) << "Mapfile name needed."; - } - - std::string mapfile_name = argv[1]; - LOG(INFO) << "Mapfile name is " << mapfile_name; - - StreamClient client; - - try{ - client.Initialize(std::move(mapfile_name)); - } catch (const std::exception& e) { - LOG(ERROR) << " a standard exception was caught, with message '" - << e.what() << "'\n"; - return 0; - } - - uint32_t count = 0; - - while (count < kClientLoopCount) { - - PmNsmFrameData* data; - - data = client.ReadLatestFrame(); - if (data != nullptr) { - try { - LOG(INFO) - << "\nSampleStreamerClient read out ...\n" - << data->present_event.ProcessId << ", " - << data->present_event.SyncInterval << ", " - << data->present_event.PresentFlags << ", " << std::hex - << data->present_event.SwapChainAddress; - } catch (const std::exception& e) { - LOG(ERROR) - << " a standard exception was caught, with message '" - << e.what() << "'\n"; - } - } - count++; - } -} \ No newline at end of file diff --git a/IntelPresentMon/SampleStreamerClient/SampleStreamerClient.vcxproj b/IntelPresentMon/SampleStreamerClient/SampleStreamerClient.vcxproj deleted file mode 100644 index 12d79e6ef..000000000 --- a/IntelPresentMon/SampleStreamerClient/SampleStreamerClient.vcxproj +++ /dev/null @@ -1,117 +0,0 @@ - - - - - Debug - x64 - - - Release - x64 - - - - 16.0 - Win32Proj - {57abaf22-837a-4da3-9c92-1ba37ccfa550} - SampleStreamerClient - 10.0 - - - - Application - true - v143 - MultiByte - x64 - - - Application - false - v143 - true - MultiByte - x64 - - - - - - - - - - - - - - - - - - - true - false - false - --checks=clang-analyzer-*,google-* - - - false - false - false - --checks=clang-analyzer-*,google-* - - - - - - - - Level3 - true - true - MultiThreadedDebug - None - stdcpplatest - - - Console - true - shlwapi.lib;%(AdditionalDependencies) - - - - - Level3 - true - true - true - true - MultiThreaded - stdcpplatest - Guard - - - Console - true - true - true - shlwapi.lib;%(AdditionalDependencies) - - - - - - - - - {08a704d8-ca1c-45e9-8ede-542a1a43b53e} - - - {bf43064b-01f0-4c69-91fb-c2122baf621d} - - - - - - \ No newline at end of file diff --git a/IntelPresentMon/SampleStreamerClient/SampleStreamerClient.vcxproj.filters b/IntelPresentMon/SampleStreamerClient/SampleStreamerClient.vcxproj.filters deleted file mode 100644 index f81c34646..000000000 --- a/IntelPresentMon/SampleStreamerClient/SampleStreamerClient.vcxproj.filters +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/IntelPresentMon/Streamer/NamedSharedMemory.cpp b/IntelPresentMon/Streamer/NamedSharedMemory.cpp deleted file mode 100644 index 0669f573b..000000000 --- a/IntelPresentMon/Streamer/NamedSharedMemory.cpp +++ /dev/null @@ -1,364 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#include -#include "NamedSharedMemory.h" -#include - -#include "../CommonUtilities/log/GlogShim.h" - -#define PAGE 4096 - -template -constexpr T align(T what, U to) { - return (what + to - 1) & ~(to - 1); -} - -NamedSharedMem::NamedSharedMem() - : mapfile_handle_(NULL), - data_offset_base_(sizeof(NamedSharedMemoryHeader)), - header_(NULL), - buf_(NULL), - alloc_granularity_(0), - refcount_(0), - buf_created_(false), - buf_size_(0){}; - - -NamedSharedMem::NamedSharedMem(std::string mapfile_name, uint64_t buf_size, - bool isPlayback, - bool isPlaybackPaced, - bool isPlaybackRetimed, - bool isPlaybackBackpressured, - bool isPlaybackResetOldest) - : mapfile_handle_(NULL), - data_offset_base_(sizeof(NamedSharedMemoryHeader)), - header_(NULL), - buf_(NULL), - alloc_granularity_(0), - refcount_(0), - buf_created_(false), - buf_size_(0) { - - SYSTEM_INFO system_info; - GetSystemInfo(&system_info); - - alloc_granularity_ = system_info.dwAllocationGranularity; - - CreateSharedMem(std::move(mapfile_name), buf_size); - - header_->isPlayback = isPlayback; - header_->isPlaybackPaced = isPlaybackPaced; - header_->isPlaybackRetimed = isPlaybackRetimed; - header_->isPlaybackBackpressured = isPlaybackBackpressured; - header_->isPlaybackResetOldest = isPlaybackResetOldest; -}; - -void NamedSharedMem::OutputErrorLog(const char* error_string, - DWORD last_error) { - try { - if (last_error != 0) { - LOG(ERROR) << error_string << last_error << ""; - } - } catch (...) { - LOG(ERROR) << error_string << "unknown last error\n"; - } -} - -HRESULT NamedSharedMem::CreateSharedMem(std::string mapfile_name, uint64_t buf_size) -{ - HRESULT hr = S_OK; - - LOG(INFO) << "Creating NSM: " << mapfile_name; - - if (buf_size == 0) { - LOG(ERROR) << " CreateSharedMem failed with zero buf_size."; - return E_FAIL; - } - - mapfile_name_ = std::move(mapfile_name); - - DWORD buf_size_low = buf_size & 0xFFFFFFFF; - DWORD buf_size_high = (buf_size & 0xFFFFFFFF00000000ULL) >> 32; - - SECURITY_ATTRIBUTES sa = { sizeof(sa) }; - if (ConvertStringSecurityDescriptorToSecurityDescriptorW(L"D:PNO_ACCESS_CONTROLS:(ML;;NW;;;LW)", - SDDL_REVISION_1, &sa.lpSecurityDescriptor, NULL)) - { - - mapfile_handle_ = CreateFileMappingA( - INVALID_HANDLE_VALUE, // use paging file - &sa, // default security - PAGE_READWRITE, // read/write access - buf_size_high, // maximum object size (high-order DWORD) - buf_size_low, // maximum object size (low-order DWORD) - mapfile_name_.c_str()); // name of mapping object - - LocalFree(sa.lpSecurityDescriptor); - } - else { - OutputErrorLog("Failed to set security. Error code: ", GetLastError()); - return E_FAIL; - } - - if (mapfile_handle_ == NULL) { - OutputErrorLog("Could not create file mapping object. Error code: ", - GetLastError()); - return E_FAIL; - } - - buf_ = static_cast(MapViewOfFile(mapfile_handle_, // handle to map object - FILE_MAP_ALL_ACCESS, // read/write permission - 0, - 0, - buf_size)); - - if (buf_ == NULL) { - OutputErrorLog("Could not map view of file. Error code: ", - GetLastError()); - CloseHandle(mapfile_handle_); - return E_FAIL; - } - - - // Populate header info - memset(buf_, 0, buf_size); - - UnmapViewOfFile(buf_); - buf_ = NULL; - - // Always map header - header_ = static_cast(MapViewOfFile(mapfile_handle_, // handle to map object - FILE_MAP_ALL_ACCESS, // write permission - 0, - 0, - PAGE)); - - if (header_ == NULL) { - OutputErrorLog("Could not map view of file. Error code: ", - GetLastError()); - return E_FAIL; - } - - header_->max_entries = (buf_size - sizeof(NamedSharedMemoryHeader)) / sizeof(PmNsmFrameData); - header_->current_write_offset = data_offset_base_; - header_->buf_size = buf_size; - header_->process_active = true; - header_->num_frames_written = 0; - - // Query qpc frequency - if (!QueryPerformanceFrequency(&header_->qpc_frequency)) { - OutputErrorLog("QueryPerformanceFrequency failed with error: ", - GetLastError()); - } - - try { - LOG(INFO) << std::format("Shared mem initialized with size {} bytes.", buf_size); - } catch (...) { - LOG(INFO) << "Shared mem initialized"; - } - - refcount_++; - buf_created_ = true; - buf_size_ = buf_size; - return hr; -} - -void NamedSharedMem::OpenSharedMemView(std::string mapfile_name) -{ - mapfile_name_ = mapfile_name; - - mapfile_handle_ = OpenFileMapping( - FILE_MAP_READ | FILE_MAP_WRITE, // read/write access - FALSE, // do not inherit the name - mapfile_name.c_str()); // name of mapping object - - if (mapfile_handle_ == NULL) - { - OutputErrorLog("Could not open file mapping object. Error code: ", - GetLastError()); - throw std::runtime_error{"failed open file mapping object"}; - } - else { - try { - LOG(INFO) << std::format("Client opened mapfile from {}", - mapfile_name); - } catch (...) { - LOG(INFO) << "Client opened mapfile\n"; - } - } - - // Map header - header_ = static_cast(MapViewOfFile(mapfile_handle_, // handle to map object - FILE_MAP_READ | FILE_MAP_WRITE , // read permission - 0, - 0, - PAGE)); - - if (header_ == NULL) { - OutputErrorLog("Could not map view of file. Error code: ", - GetLastError()); - return; - } - - if (header_->buf_size > kBufSize) { - OutputErrorLog("Named Shared Memory header is incorrect.", - 0); - return; - } - - buf_ = static_cast(MapViewOfFile(mapfile_handle_, // handle to map object - FILE_MAP_READ, // read permission - 0, - 0, - header_->buf_size)); - - if (buf_ == NULL) { - OutputErrorLog("Could not map view of file. Error code: ", - GetLastError()); - } -} - - -NamedSharedMem::~NamedSharedMem() { - if (buf_ != NULL) { - UnmapViewOfFile(buf_); - buf_ = NULL; - } - - if (header_ != NULL) { - UnmapViewOfFile(header_); - header_ = NULL; - } - - if (mapfile_handle_ != NULL) { - CloseHandle(mapfile_handle_); - mapfile_handle_ = NULL; - } -} - -void NamedSharedMem::WriteFrameData(PmNsmFrameData* data) { - uint64_t data_size_bytes = sizeof(PmNsmFrameData); - - uint64_t write_to_offset = header_->current_write_offset; - if (write_to_offset + data_size_bytes >= header_->buf_size) { - // not enough space, move to front of the buffer - write_to_offset = data_offset_base_; - } - - uint64_t map_offset = (write_to_offset / alloc_granularity_) * alloc_granularity_; - DWORD map_offset_low = map_offset & 0xFFFFFFFF; - DWORD map_offset_high = (map_offset & 0xFFFFFFFF00000000ULL) >> 32; - - uint64_t in_map_offset = write_to_offset % alloc_granularity_; - - SIZE_T num_bytes_to_map = ((in_map_offset + sizeof(PmNsmFrameData)) > alloc_granularity_) ? 2 * alloc_granularity_ : alloc_granularity_; - - num_bytes_to_map = num_bytes_to_map > header_->buf_size ? header_->buf_size - : num_bytes_to_map; - - // Map data region - buf_ = static_cast(MapViewOfFile(mapfile_handle_, // handle to map object - FILE_MAP_WRITE, // write permission - map_offset_high, - map_offset_low, - num_bytes_to_map)); - - if (buf_ == NULL) { - OutputErrorLog("Could not map view of file. Error code: ", - GetLastError()); - return; - } - - std::memcpy(static_cast(static_cast(buf_) + in_map_offset), - static_cast(data), sizeof(PmNsmFrameData)); - - if (IsFull()) { - header_->head_idx = (header_->head_idx + 1) % header_->max_entries; - } - - header_->tail_idx = (header_->tail_idx + 1) % header_->max_entries; - header_->current_write_offset = write_to_offset + sizeof(PmNsmFrameData); - header_->num_frames_written++; - - LARGE_INTEGER qpc; - QueryPerformanceCounter(&qpc); - //LOG(INFO) << data->present_event.ProcessId << "," << qpc.QuadPart - // << "," << data->present_event.SwapChainAddress << "," - // << "," << data->present_event.PresentStartTime << "," - // << (int)data->present_event.FinalState << "," - // << data->present_event.ScreenTime << "," << header_->tail_idx - // << "," << header_->head_idx << "," << header_->num_frames_written; - - UnmapViewOfFile(buf_); - buf_ = NULL; -} - -// Pop the first frame and move the head_idx -void NamedSharedMem::DequeueFrameData() { - if (!IsEmpty()) { - header_->head_idx = (header_->head_idx + 1) % header_->max_entries; - } -} - -uint64_t NamedSharedMem::GetNumServiceWrittenFrames() { - return header_->num_frames_written; -} - -bool NamedSharedMem::IsFull() { - if (header_ == nullptr) { - return false; - } - - if (((header_->tail_idx+1) % header_->max_entries) == (header_->head_idx)) { - return true; - } - - return false; -} - -bool NamedSharedMem::IsEmpty() { - if ((header_ == nullptr) || - (header_->head_idx == header_->tail_idx)) { - return true; - } - - return false; -} - -bool NamedSharedMem::HasUninitializedFrames() -{ - if (header_ == nullptr) { - return true; - } - return header_->max_entries > header_->num_frames_written; -} - -void NamedSharedMem::NotifyProcessKilled() { - header_->process_active = false; - FlushViewOfFile(header_, sizeof(NamedSharedMemoryHeader)); -} - -void NamedSharedMem::RecordFirstFrameTime(uint64_t start_qpc) { - header_->start_qpc = start_qpc; - // Query qpc frequency - if (!QueryPerformanceFrequency(&header_->qpc_frequency)) { - OutputErrorLog("QueryPerformanceFrequency failed with error: ", - GetLastError()); - } -} - -uint64_t NamedSharedMem::RecordAndGetLastDisplayedQpc(uint64_t qpc) { - uint64_t last_qpc = ULLONG_MAX; - last_qpc = header_->last_displayed_qpc; - header_->last_displayed_qpc = qpc; - return last_qpc; -} - -void NamedSharedMem::WriteTelemetryCapBits( - std::bitset(GpuTelemetryCapBits::gpu_telemetry_count)> - gpu_telemetry_cap_bits, - std::bitset(CpuTelemetryCapBits::cpu_telemetry_count)> - cpu_telemetry_cap_bits) { - header_->gpuTelemetryCapBits = gpu_telemetry_cap_bits; - header_->cpuTelemetryCapBits = cpu_telemetry_cap_bits; -} \ No newline at end of file diff --git a/IntelPresentMon/Streamer/NamedSharedMemory.h b/IntelPresentMon/Streamer/NamedSharedMemory.h deleted file mode 100644 index f0b9aa2f7..000000000 --- a/IntelPresentMon/Streamer/NamedSharedMemory.h +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once -#include - -#include "../PresentMonUtils/StreamFormat.h" - -static const uint64_t kBufSize = 65536 * 60; -static const std::string kGlobalPrefix = "Global\\NamedSharedMem_"; - -class NamedSharedMem { - public: - NamedSharedMem(); - NamedSharedMem(std::string mapfile_name, uint64_t buf_size, - bool isPlayback, - bool isPlaybackPaced, - bool isPlaybackRetimed, - bool isPlaybackBackpressured, - bool isPlaybackResetOldest); - ~NamedSharedMem(); - NamedSharedMem(const NamedSharedMem& t) = delete; - NamedSharedMem& operator=(const NamedSharedMem& t) = delete; - - std::string GetMapFileName() { return mapfile_name_; } - HANDLE GetMapFileHandle() { return mapfile_handle_; }; - // Get base offset of the frame data in shared memory. Normally this is - // sizeof(NamedSharedMemoryHeader) - uint32_t GetBaseOffset() { return data_offset_base_; }; - void* GetBuffer() { return buf_; }; - // Server only method to write frame data - void WriteFrameData(PmNsmFrameData* data); - // Server only method to write the telemetry bit caps to - // the header - void WriteTelemetryCapBits( - std::bitset(GpuTelemetryCapBits::gpu_telemetry_count)> - gpu_telemetry_cap_bits, - std::bitset(CpuTelemetryCapBits::cpu_telemetry_count)> - cpu_telemetry_cap_bits); - - // Client only method to pop already read frame data - void DequeueFrameData(); - // Client only method to get the number of frames written by the - // service - uint64_t GetNumServiceWrittenFrames(); - // Client method to open a view into the shared mem - void OpenSharedMemView(std::string mapfile_name); - void NotifyProcessKilled(); - // TODO(jtseng2): header_ is client used only. Separate GetHeader() API from - // server. - const NamedSharedMemoryHeader* GetHeader() { return header_; }; - // indicates whether the ring buffer has available write space - // only meaninful in a backpressured (typically replay) scenario - bool IsFull(); - // indicates whether the ring buffer has any unconsumed frames - // only meaninful in a backpressured (typically replay) scenario - bool IsEmpty(); - // indicates whether every element in the ring buffer contains valid frame information - // typically, this is only false until the buffer has wrapped once - bool HasUninitializedFrames(); - bool IsNSMCreated() { return buf_created_; }; - - // Helper frunctions for generating frame statistics - // Cache first frame's qpc time as the start time - void RecordFirstFrameTime(uint64_t start_qpc); - // Cache last displayed frame id during WriteFrameData - uint64_t RecordAndGetLastDisplayedQpc(uint64_t qpc); - void IncrementRefcount() { refcount_++;}; - void DecrementRefcount() { refcount_--; }; - int GetRefCount() { return refcount_; }; - uint64_t GetBufSize() { return buf_size_; }; - - private: - // Server method to create a shared mem in buf_size bytes - HRESULT CreateSharedMem(std::string mapfile_name, uint64_t buf_size); - void OutputErrorLog(const char* error_string, DWORD last_error); - std::string mapfile_name_; - HANDLE mapfile_handle_; - uint32_t data_offset_base_; - NamedSharedMemoryHeader* header_; - void* buf_; - // allocation granularity obtained from GetSystemInfo. MapViewOfFile offset - // must be multiple of alloc_granularity - size_t alloc_granularity_; - int refcount_; - bool buf_created_; - uint64_t buf_size_; -}; \ No newline at end of file diff --git a/IntelPresentMon/Streamer/StreamClient.cpp b/IntelPresentMon/Streamer/StreamClient.cpp deleted file mode 100644 index 13dd765b4..000000000 --- a/IntelPresentMon/Streamer/StreamClient.cpp +++ /dev/null @@ -1,785 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#include "StreamClient.h" -#include "../PresentMonUtils/QPCUtils.h" -#include "../PresentMonUtils/PresentDataUtils.h" - -#include "../CommonUtilities/log/GlogShim.h" - -StreamClient::StreamClient() - : initialized_(false), - next_dequeue_idx_(0), - recording_frame_data_(false), - current_dequeue_frame_num_(0) {} - -StreamClient::StreamClient(std::string mapfile_name, bool is_etl_stream_client) - : next_dequeue_idx_(0), - recording_frame_data_(false), - current_dequeue_frame_num_(0) { - Initialize(std::move(mapfile_name)); -} - -StreamClient::~StreamClient() { -} - -void StreamClient::Initialize(std::string mapfile_name) { - shared_mem_view_ = std::make_unique(); - shared_mem_view_->OpenSharedMemView(mapfile_name); - mapfile_name_ = std::move(mapfile_name); - - // Query qpc frequency - if (!QueryPerformanceFrequency(&qpcFrequency_)) { - pmlog_error("QueryPerformanceFrequency failed").hr(); - } - - initialized_ = true; - LOG(INFO) << "Stream client initialized."; -} - -void StreamClient::CloseSharedMemView() { shared_mem_view_.reset(nullptr); } - -PmNsmFrameData* StreamClient::ReadLatestFrame() { - PmNsmFrameData* data = nullptr; - - if (shared_mem_view_ == nullptr) { - LOG(ERROR) - << "Shared mem view is null. Initialze client with mapfile name."; - return data; - } - - if (shared_mem_view_->IsEmpty()) { - LOG(INFO) << "Shared mem view is empty. start ETW tracing or wait for " - "more data"; - return data; - } - - uint64_t index = GetLatestFrameIndex(); - - return ReadFrameByIdx(index, true); -} - -PmNsmFrameData* StreamClient::ReadFrameByIdx(uint64_t frame_idx, bool checked) { - PmNsmFrameData* data = nullptr; - uint64_t read_offset = 0; - - if (shared_mem_view_ == nullptr) { - LOG(ERROR) - << "Shared mem view is null. Initialze client with mapfile name."; - return nullptr; - } - - if (shared_mem_view_->IsEmpty()) { - if (checked) { - pmlog_warn("Trying to read from empty nsm ring").pmwatch(frame_idx); - } - return nullptr; - } - - auto p_header = shared_mem_view_->GetHeader(); - - if (!p_header->process_active) { - pmlog_warn("Process is not active. Shared mem view to be destroyed."); - CloseSharedMemView(); - return nullptr; - } - - if (frame_idx > p_header->max_entries - 1) { - pmlog_warn("Bad out of bounds index in circular buffer").pmwatch(frame_idx); - return nullptr; - } - - if (checked) { - // when ring is full, every frame is valid for reading - // in realtime usage (head not updated on read), this is always the case once the ring wraps once - if (!shared_mem_view_->IsFull()) { - bool invalid = false; - // when ring is not full, there are two cases (head before tail, or vice versa) - // head before tail (invalid range potentially non-contiguous/wrapping) - if (p_header->tail_idx > p_header->head_idx) { - if (frame_idx >= p_header->tail_idx || frame_idx < p_header->head_idx) { - invalid = true; - } - } - else { // tail before head (invalid range contiguous in middle) - if (frame_idx >= p_header->tail_idx && frame_idx < p_header->head_idx) { - invalid = true; - } - } - if (invalid) { - pmlog_warn("Invalid frame idx").pmwatch(frame_idx); - return nullptr; - } - } - } - - read_offset = frame_idx * sizeof(PmNsmFrameData) + shared_mem_view_->GetBaseOffset(); - - data = reinterpret_cast( - static_cast((shared_mem_view_->GetBuffer())) + read_offset); - - return data; -} - -void StreamClient::PeekNextFrames(const PmNsmFrameData** pNextFrame, - const PmNsmFrameData** pNextDisplayedFrame) -{ - *pNextFrame = nullptr; - *pNextDisplayedFrame = nullptr; - if (recording_frame_data_) { - auto nsm_view = GetNamedSharedMemView(); - auto nsm_hdr = nsm_view->GetHeader(); - if (!nsm_hdr->process_active) { - // Service destroyed the named shared memory. - return; - } - - uint64_t peekIndex{ next_dequeue_idx_ }; - auto pTempFrameData = ReadFrameByIdx(peekIndex, false); - *pNextFrame = pTempFrameData; - while (pTempFrameData) { - if (pTempFrameData->present_event.FinalState == PresentResult::Presented && - pTempFrameData->present_event.DisplayedCount > 0) { - *pNextDisplayedFrame = pTempFrameData; - return; - } - // advance to next frame with circular buffer wrapping behavior - peekIndex = (peekIndex + 1) % nsm_view->GetHeader()->max_entries; - pTempFrameData = ReadFrameByIdx(peekIndex, false); - } - } - return; -} - -bool StreamClient::IsAppPresentedFrame(const PmNsmFrameData* frame) const { - if (!frame || frame->present_event.DisplayedCount == 0) { - return false; - } - size_t lastDisplayedIndex = frame->present_event.DisplayedCount - 1; - return frame->present_event.Displayed_FrameType[lastDisplayedIndex] == FrameType::NotSet || - frame->present_event.Displayed_FrameType[lastDisplayedIndex] == FrameType::Application; -} - -bool StreamClient::IsDisplayedFrame(const PmNsmFrameData* frame) const { - return frame && frame->present_event.FinalState == PresentResult::Presented && - frame->present_event.DisplayedCount > 0; -} - -bool StreamClient::IsAppDisplayedFrame(const PmNsmFrameData* frame) const { - if (!frame || frame->present_event.DisplayedCount == 0) { - return false; - } - size_t lastDisplayedIndex = frame->present_event.DisplayedCount - 1; - return frame->present_event.FinalState == PresentResult::Presented && - (frame->present_event.Displayed_FrameType[lastDisplayedIndex] == FrameType::NotSet || - frame->present_event.Displayed_FrameType[lastDisplayedIndex] == FrameType::Application); -} - -// Function to peek previous frames from the current frame -void StreamClient::PeekPreviousFrames(const PmNsmFrameData** pFrameDataOfLastPresented, - const PmNsmFrameData** pFrameDataOfLastAppPresented, - const PmNsmFrameData** pFrameDataOfLastDisplayed, - const PmNsmFrameData** pFrameDataOfLastAppDisplayed, - const PmNsmFrameData** pFrameDataOfPreviousAppFrameOfLastAppDisplayed) -{ - *pFrameDataOfLastPresented = nullptr; - *pFrameDataOfLastAppPresented = nullptr; - *pFrameDataOfLastDisplayed = nullptr; - *pFrameDataOfLastAppDisplayed = nullptr; - *pFrameDataOfPreviousAppFrameOfLastAppDisplayed = nullptr; - if (recording_frame_data_) { - auto nsm_view = GetNamedSharedMemView(); - auto nsm_hdr = nsm_view->GetHeader(); - if (!nsm_hdr->process_active) { - // Service destroyed the named shared memory. - return; - } - - // if nsm is not fully initialized, last frame to read is at idx 0, so stop condition is when we wrap to last idx - // otherwise, last frame to read is the one that follows the latest one (which is the oldest one, next one to be overwritten) - const auto stopIdx = nsm_view->HasUninitializedFrames() ? nsm_hdr->max_entries - 1 : this->GetLatestFrameIndex(); - // here, peek index is starting at the frame which follows the current one - // this frame is guaranteed to be valid since peekNext call prior to this would exit early if it were not - uint64_t peekIndex{ next_dequeue_idx_ }; - uint32_t numFramesTraversed = 0; - while (true) { - // seek back one frame with ring buffer cycling behavior - peekIndex = peekIndex == 0 ? (nsm_hdr->max_entries - 1) : (peekIndex - 1); - // exit when we hit stopIdx (which is the first frame encountered we should *not* use) - if (peekIndex == stopIdx) { - return; - } - numFramesTraversed++; - // TODO: we can improve checking by independently enabling for forward and backward cases - auto pTempFrameData = ReadFrameByIdx(peekIndex, false); - // We need to traverse back two frames from the next_dequeue_idx to - // get to the start of the previous frames - if (numFramesTraversed > 1) { - if (pTempFrameData) { - if (numFramesTraversed == 2) { - // If we made it here we were able to go back two frames - // from the current next_deque_index. This is the frame - // data of the last presented frame before the current frame. - *pFrameDataOfLastPresented = pTempFrameData; - } - if (*pFrameDataOfLastAppPresented == nullptr) { - // If we haven't found the last app presented frame check to see if - // the current one is presented. This could point to the same frame - // as the last presented one above. - auto displayedCount = pTempFrameData->present_event.DisplayedCount; - if (displayedCount > 0) { - if (pTempFrameData->present_event.Displayed_FrameType[displayedCount - 1] == FrameType::NotSet || - pTempFrameData->present_event.Displayed_FrameType[displayedCount - 1] == FrameType::Application) { - *pFrameDataOfLastAppPresented = pTempFrameData; - } - } else { - // If the displayed count is 0 we assume this is an app frame - *pFrameDataOfLastAppPresented = pTempFrameData; - } - } - if (*pFrameDataOfLastDisplayed == nullptr) { - // If we haven't found the last displayed frame check to see if - // the current one is presented. This could point to the same frame - // as the last presented one above. - if (pTempFrameData->present_event.FinalState == PresentResult::Presented && - pTempFrameData->present_event.DisplayedCount > 0) { - *pFrameDataOfLastDisplayed = pTempFrameData; - } - } - if (*pFrameDataOfLastAppDisplayed == nullptr) { - // If we haven't found the last displayed app frame check to see if - // the current one is presented. This could point to the same frame - // as the last displayed frame above. - if (pTempFrameData->present_event.DisplayedCount > 0) { - size_t lastDisplayedIndex = pTempFrameData->present_event.DisplayedCount - 1; - if (pTempFrameData->present_event.FinalState == PresentResult::Presented && - (pTempFrameData->present_event.Displayed_FrameType[lastDisplayedIndex] == FrameType::NotSet || - pTempFrameData->present_event.Displayed_FrameType[lastDisplayedIndex] == FrameType::Application)) { - *pFrameDataOfLastAppDisplayed = pTempFrameData; - } - } - } else { - // If we have found the last displayed app frame we now need to find the previously - // presented frame to determine the cpu start time. The frame does not need to be - // displayed but must be from the application - if (pTempFrameData->present_event.DisplayedCount > 0) { - size_t lastDisplayedIndex = pTempFrameData->present_event.DisplayedCount - 1; - if (pTempFrameData->present_event.Displayed_FrameType[lastDisplayedIndex] == FrameType::NotSet || - pTempFrameData->present_event.Displayed_FrameType[lastDisplayedIndex] == FrameType::Application) { - *pFrameDataOfPreviousAppFrameOfLastAppDisplayed = pTempFrameData; - } - } else { - // If the displayed count is 0 we assume this is an app frame - *pFrameDataOfPreviousAppFrameOfLastAppDisplayed = pTempFrameData; - } - } - if (*pFrameDataOfLastPresented != nullptr && - *pFrameDataOfLastAppPresented != nullptr && - *pFrameDataOfLastDisplayed != nullptr && - *pFrameDataOfPreviousAppFrameOfLastAppDisplayed != nullptr && - *pFrameDataOfLastAppDisplayed != nullptr) { - // We have found all the frames we need to find. We can exit early. - return; - } - } - else { - // The read frame data is null. - // TODO log this as the index is supposed to be valid! - return; - } - } - } - } - return; -} - - -PM_STATUS StreamClient::ConsumePtrToNextNsmFrameData(const PmNsmFrameData** pNsmData, - const PmNsmFrameData** pNextFrame, - const PmNsmFrameData** pFrameDataOfNextDisplayed, - const PmNsmFrameData** pFrameDataOfLastPresented, - const PmNsmFrameData** pFrameDataOfLastAppPresented, - const PmNsmFrameData** pFrameDataOfLastDisplayed, - const PmNsmFrameData** pFrameDataOfLastAppDisplayed, - const PmNsmFrameData** pFrameDataOfPreviousAppFrameOfLastAppDisplayed) -{ - if (pNsmData == nullptr || pNextFrame == nullptr || pFrameDataOfNextDisplayed == nullptr || - pFrameDataOfLastPresented == nullptr || pFrameDataOfLastAppPresented == nullptr || - pFrameDataOfLastDisplayed == nullptr || pFrameDataOfLastAppDisplayed == nullptr || - pFrameDataOfPreviousAppFrameOfLastAppDisplayed == nullptr) { - return PM_STATUS::PM_STATUS_FAILURE; - } - - // nullify point so that if we exit early it will be null - *pNsmData = nullptr; - - auto nsm_view = GetNamedSharedMemView(); - auto nsm_hdr = nsm_view->GetHeader(); - if (!nsm_hdr->process_active) { - // Service destroyed the named shared memory. - return PM_STATUS::PM_STATUS_INVALID_PID; - } - - if (recording_frame_data_ == false) { - // Get the current number of frames written and set it as the current - // dequeue frame number. This will be used to track data overruns if - // the client does not read data fast enough. - recording_frame_data_ = true; - if (nsm_hdr->isPlaybackResetOldest) { - // during playback it is desirable to start at the very first frame even if we start consuming late - if (nsm_view->IsFull()) { - // if nsm is full, we have to dequeue all frames currently in the buffer - current_dequeue_frame_num_ = nsm_hdr->num_frames_written - nsm_hdr->max_entries; - } - else { - // if nsm is not full, then we must be in the initial state so set to zero (start from the beginning) - current_dequeue_frame_num_ = 0; - } - // always start from the head (oldest frame) - next_dequeue_idx_ = nsm_hdr->head_idx; - } - else { - // at start or after overrun, reset to most recent frame data - current_dequeue_frame_num_ = nsm_hdr->num_frames_written; - next_dequeue_idx_ = (nsm_hdr->tail_idx + 1) % nsm_hdr->max_entries; - } - } - - // Check to see if the number of pending read frames is greater - // than the maximum number of entries. If so we have lost frame - // data - uint64_t num_pending_frames = CheckPendingReadFrames(); - if (num_pending_frames > nsm_hdr->max_entries) { - recording_frame_data_ = false; - return PM_STATUS::PM_STATUS_SUCCESS; - } - else if (num_pending_frames < 2) { - // we need at least 2 pending frames to enable peek-ahead - return PM_STATUS::PM_STATUS_SUCCESS; - } - - // Set the rest of the incoming frame pointers in - // preparation for the various frame data peeks and - // reads - *pNextFrame = nullptr; - *pFrameDataOfNextDisplayed = nullptr; - *pFrameDataOfLastPresented = nullptr; - *pFrameDataOfLastAppPresented = nullptr; - *pFrameDataOfLastDisplayed = nullptr; - *pFrameDataOfLastAppDisplayed = nullptr; - *pFrameDataOfPreviousAppFrameOfLastAppDisplayed = nullptr; - - // First read the current frame. next_dequeue_idx_ sits - // at next frame we need to dequeue. - *pNsmData = ReadFrameByIdx(next_dequeue_idx_, true); - if (*pNsmData) { - // Good so far. Save off the queue index in case - // we need to reset - auto previous_dequeue_idx = next_dequeue_idx_; - // Increment the index to the next frame to dequeue as PeekNextDisplayedFrame expects - // the frame to be incremented. Can change this when done debugging so we don't have to - // reset the dequeue index. - next_dequeue_idx_ = (next_dequeue_idx_ + 1) % nsm_hdr->max_entries; - PeekNextFrames(pNextFrame, pFrameDataOfNextDisplayed); - if (*pFrameDataOfNextDisplayed == nullptr) { - // We were unable to get the next displayed frame. It might not have been displayed - // yet. Reset the next_dequeue_idx back to where we first started. - next_dequeue_idx_ = previous_dequeue_idx; - // Also reset the current and next frame data pointers - *pNsmData = nullptr; - *pNextFrame = nullptr; - return PM_STATUS::PM_STATUS_SUCCESS; - } - PeekPreviousFrames( - pFrameDataOfLastPresented, - pFrameDataOfLastAppPresented, - pFrameDataOfLastDisplayed, - pFrameDataOfLastAppDisplayed, - pFrameDataOfPreviousAppFrameOfLastAppDisplayed); - - current_dequeue_frame_num_++; - if (nsm_hdr->isPlaybackBackpressured) { - nsm_view->DequeueFrameData(); - } - return PM_STATUS::PM_STATUS_SUCCESS; - } - else { - return PM_STATUS::PM_STATUS_FAILURE; - } -} - -void StreamClient::CopyFrameData(uint64_t start_qpc, - const PmNsmFrameData* src_frame, - GpuTelemetryBitset gpu_telemetry_cap_bits, - CpuTelemetryBitset cpu_telemetry_cap_bits, - PM_FRAME_DATA* dst_frame) { - memset(dst_frame, 0, sizeof(PM_FRAME_DATA)); - dst_frame->qpc_time = src_frame->present_event.PresentStartTime; - - dst_frame->ms_between_presents = - QpcDeltaToMs(src_frame->present_event.PresentStartTime - - src_frame->present_event.last_present_qpc, - GetQpcFrequency()); - - strncpy_s(dst_frame->application, src_frame->present_event.application, _TRUNCATE); - - dst_frame->process_id = src_frame->present_event.ProcessId; - dst_frame->swap_chain_address = src_frame->present_event.SwapChainAddress; - strncpy_s(dst_frame->runtime, - RuntimeToString(src_frame->present_event.Runtime), _TRUNCATE); - dst_frame->sync_interval = src_frame->present_event.SyncInterval; - dst_frame->present_flags = src_frame->present_event.PresentFlags; - - if (src_frame->present_event.FinalState == PresentResult::Presented) { - dst_frame->dropped = false; - // If the last_displayed_qpc is zero this means there has not been - // displayed frame. - if (src_frame->present_event.last_displayed_qpc > 0) { - dst_frame->ms_between_display_change = - QpcDeltaToMs(src_frame->present_event.Displayed_ScreenTime[0] - - src_frame->present_event.last_displayed_qpc, - GetQpcFrequency()); - } - dst_frame->ms_until_displayed = QpcDeltaToMs(src_frame->present_event.Displayed_ScreenTime[0] - - src_frame->present_event.PresentStartTime, - GetQpcFrequency()); - } else { - dst_frame->dropped = true; - } - - dst_frame->time_in_seconds = QpcDeltaToSeconds( - src_frame->present_event.PresentStartTime - start_qpc, GetQpcFrequency()); - - dst_frame->ms_in_present_api = - QpcDeltaToMs(src_frame->present_event.TimeInPresent, - GetQpcFrequency()); - - dst_frame->allows_tearing = src_frame->present_event.SupportsTearing; - dst_frame->present_mode = - TranslatePresentMode(src_frame->present_event.PresentMode); - - // If the ReadyTime is BEFORE the present event start time denote this - // with a negative render complete - if (src_frame->present_event.ReadyTime == 0) { - dst_frame->ms_until_render_complete = 0.0; - } else { - if (src_frame->present_event.ReadyTime < - src_frame->present_event.PresentStartTime) { - dst_frame->ms_until_render_complete = - -1.0 * QpcDeltaToMs(src_frame->present_event.PresentStartTime - - src_frame->present_event.ReadyTime, - GetQpcFrequency()); - } else { - dst_frame->ms_until_render_complete = - QpcDeltaToMs(src_frame->present_event.ReadyTime - - src_frame->present_event.PresentStartTime, - GetQpcFrequency()); - } - } - // If the GPUStartTime is BEFORE the present event start time denote this - // with a negative render start - if (src_frame->present_event.GPUStartTime == 0) { - dst_frame->ms_until_render_start = 0.0; - } else { - if (src_frame->present_event.GPUStartTime < - src_frame->present_event.PresentStartTime) { - dst_frame->ms_until_render_start = - -1.0 * QpcDeltaToMs(src_frame->present_event.PresentStartTime - - src_frame->present_event.GPUStartTime, - GetQpcFrequency()); - - } else { - dst_frame->ms_until_render_start = - QpcDeltaToMs(src_frame->present_event.GPUStartTime - - src_frame->present_event.PresentStartTime, - GetQpcFrequency()); - } - } - dst_frame->ms_gpu_active = - QpcDeltaToMs(src_frame->present_event.GPUDuration, GetQpcFrequency()); - dst_frame->ms_gpu_video_active = QpcDeltaToMs( - src_frame->present_event.GPUVideoDuration, GetQpcFrequency()); - dst_frame->ms_since_input = 0.; - if (src_frame->present_event.InputTime != 0) { - dst_frame->ms_since_input = - QpcDeltaToMs(src_frame->present_event.PresentStartTime - - src_frame->present_event.InputTime, - GetQpcFrequency()); - } - - // power telemetry - dst_frame->gpu_power_w.data = src_frame->power_telemetry.gpu_power_w; - dst_frame->gpu_power_w.valid = gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_power)]; - - dst_frame->gpu_sustained_power_limit_w.data = - src_frame->power_telemetry.gpu_sustained_power_limit_w; - dst_frame->gpu_sustained_power_limit_w.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_sustained_power_limit)]; - - dst_frame->gpu_voltage_v.data = - src_frame->power_telemetry.gpu_voltage_v; - dst_frame->gpu_voltage_v.valid = gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_voltage)]; - - dst_frame->gpu_frequency_mhz.data = - src_frame->power_telemetry.gpu_frequency_mhz; - dst_frame->gpu_frequency_mhz.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_frequency)]; - - dst_frame->gpu_temperature_c.data = - src_frame->power_telemetry.gpu_temperature_c; - dst_frame->gpu_temperature_c.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_temperature)]; - - dst_frame->gpu_utilization.data = - src_frame->power_telemetry.gpu_utilization; - dst_frame->gpu_utilization.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_utilization)]; - - dst_frame->gpu_render_compute_utilization.data = - src_frame->power_telemetry.gpu_render_compute_utilization; - dst_frame->gpu_render_compute_utilization.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_render_compute_utilization)]; - - dst_frame->gpu_media_utilization.data = - src_frame->power_telemetry.gpu_media_utilization; - dst_frame->gpu_media_utilization.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_media_utilization)]; - - dst_frame->vram_power_w.data = src_frame->power_telemetry.vram_power_w; - dst_frame->vram_power_w.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_power)]; - - dst_frame->vram_voltage_v.data = src_frame->power_telemetry.vram_voltage_v; - dst_frame->vram_voltage_v.valid = gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_voltage)]; - - dst_frame->vram_frequency_mhz.data = src_frame->power_telemetry.vram_frequency_mhz; - dst_frame->vram_frequency_mhz.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_frequency)]; - - dst_frame->vram_effective_frequency_gbs.data = - src_frame->power_telemetry.vram_effective_frequency_gbps; - dst_frame->vram_effective_frequency_gbs.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_effective_frequency)]; - - dst_frame->vram_temperature_c.data = src_frame->power_telemetry.vram_temperature_c; - dst_frame->vram_temperature_c.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_temperature)]; - - for (size_t i = 0; i < MAX_PM_FAN_COUNT; i++) { - dst_frame->fan_speed_rpm[i].data = src_frame->power_telemetry.fan_speed_rpm[i]; - dst_frame->fan_speed_rpm[i].valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::fan_speed_0) + i]; - } - - for (size_t i = 0; i < MAX_PM_PSU_COUNT; i++) { - dst_frame->psu_type[i].data = - TranslatePsuType(src_frame->power_telemetry.psu[i].psu_type); - dst_frame->psu_type[i].valid = dst_frame->psu_power[i].valid = - gpu_telemetry_cap_bits - [static_cast(GpuTelemetryCapBits::psu_info_0) + i]; - - dst_frame->psu_power[i].data = src_frame->power_telemetry.psu[i].psu_power; - dst_frame->psu_power[i].valid = gpu_telemetry_cap_bits - [static_cast(GpuTelemetryCapBits::psu_info_0) + i]; - - dst_frame->psu_voltage[i].data = src_frame->power_telemetry.psu[i].psu_voltage; - dst_frame->psu_voltage[i].valid = gpu_telemetry_cap_bits - [static_cast(GpuTelemetryCapBits::psu_info_0) + i]; - } - - // Gpu memory telemetry - dst_frame->gpu_mem_total_size_b.data = - src_frame->power_telemetry.gpu_mem_total_size_b; - dst_frame->gpu_mem_total_size_b.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_size)]; - - dst_frame->gpu_mem_used_b.data = - src_frame->power_telemetry.gpu_mem_used_b; - dst_frame->gpu_mem_used_b.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_used)]; - - dst_frame->gpu_mem_max_bandwidth_bps.data = - src_frame->power_telemetry.gpu_mem_max_bandwidth_bps; - dst_frame->gpu_mem_max_bandwidth_bps.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_max_bandwidth)]; - - dst_frame->gpu_mem_read_bandwidth_bps.data = - src_frame->power_telemetry.gpu_mem_read_bandwidth_bps; - dst_frame->gpu_mem_read_bandwidth_bps.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_read_bandwidth)]; - - dst_frame->gpu_mem_write_bandwidth_bps.data = - src_frame->power_telemetry.gpu_mem_write_bandwidth_bps; - dst_frame->gpu_mem_write_bandwidth_bps.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_write_bandwidth)]; - - // Throttling flags - dst_frame->gpu_power_limited.data = src_frame->power_telemetry.gpu_power_limited; - dst_frame->gpu_power_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_power_limited)]; - - dst_frame->gpu_temperature_limited.data = - src_frame->power_telemetry.gpu_temperature_limited; - dst_frame->gpu_temperature_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_temperature_limited)]; - - dst_frame->gpu_current_limited.data = - src_frame->power_telemetry.gpu_current_limited; - dst_frame->gpu_current_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_current_limited)]; - - dst_frame->gpu_voltage_limited.data = - src_frame->power_telemetry.gpu_voltage_limited; - dst_frame->gpu_voltage_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_voltage_limited)]; - - dst_frame->gpu_utilization_limited.data = - src_frame->power_telemetry.gpu_utilization_limited; - dst_frame->gpu_utilization_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_utilization_limited)]; - - dst_frame->vram_power_limited.data = - src_frame->power_telemetry.vram_power_limited; - dst_frame->vram_power_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_utilization_limited)]; - - dst_frame->vram_temperature_limited.data = - src_frame->power_telemetry.vram_temperature_limited; - dst_frame->vram_temperature_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_temperature_limited)]; - - dst_frame->vram_current_limited.data = - src_frame->power_telemetry.vram_current_limited; - dst_frame->vram_current_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_current_limited)]; - - dst_frame->vram_voltage_limited.data = - src_frame->power_telemetry.vram_voltage_limited; - dst_frame->vram_voltage_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_voltage_limited)]; - - dst_frame->vram_utilization_limited.data = - src_frame->power_telemetry.vram_utilization_limited; - dst_frame->vram_utilization_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_utilization_limited)]; - - // cpu telemetry - only available in INTERNAL builds - dst_frame->cpu_utilization.data = src_frame->cpu_telemetry.cpu_utilization; - dst_frame->cpu_utilization.valid = - cpu_telemetry_cap_bits[static_cast( - CpuTelemetryCapBits::cpu_utilization)]; - - dst_frame->cpu_power_w.data = src_frame->cpu_telemetry.cpu_power_w; - dst_frame->cpu_power_w.valid = cpu_telemetry_cap_bits[static_cast( - CpuTelemetryCapBits::cpu_power)]; - - dst_frame->cpu_power_limit_w.data = src_frame->cpu_telemetry.cpu_power_limit_w; - dst_frame->cpu_power_limit_w.valid = - cpu_telemetry_cap_bits[static_cast( - CpuTelemetryCapBits::cpu_power_limit)]; - - dst_frame->cpu_temperature_c.data = src_frame->cpu_telemetry.cpu_temperature; - dst_frame->cpu_temperature_c.valid = - cpu_telemetry_cap_bits[static_cast( - CpuTelemetryCapBits::cpu_temperature)]; - - dst_frame->cpu_frequency.data = src_frame->cpu_telemetry.cpu_frequency; - dst_frame->cpu_frequency.valid = - cpu_telemetry_cap_bits[static_cast( - CpuTelemetryCapBits::cpu_frequency)]; -} - -uint64_t StreamClient::GetLatestFrameIndex() { - if (shared_mem_view_->IsEmpty()) { - return UINT_MAX; - } - - auto p_header = shared_mem_view_->GetHeader(); - - if (!p_header->process_active) { - LOG(ERROR) << "\n Process is not active. Shared mem view to be destroyed."; - CloseSharedMemView(); - return UINT_MAX; - } - - if (p_header->tail_idx == 0) { - return p_header->max_entries - 1; - } else { - return (p_header->tail_idx - 1); - } -} - -// Calculate the number of frames written since the last dequue -uint64_t StreamClient::CheckPendingReadFrames() { - uint64_t num_pending_read_frames = 0; - - auto p_header = shared_mem_view_->GetHeader(); - if (p_header->num_frames_written < current_dequeue_frame_num_) { - // Wrap case where p_header->num_frames_written has wrapped to zero - num_pending_read_frames = ULLONG_MAX - current_dequeue_frame_num_; - num_pending_read_frames += p_header->num_frames_written; - } else { - num_pending_read_frames = - p_header->num_frames_written - current_dequeue_frame_num_; - } - - return num_pending_read_frames; -} - -std::optional< - std::bitset(GpuTelemetryCapBits::gpu_telemetry_count)>> -StreamClient::GetGpuTelemetryCaps() { - if (shared_mem_view_->IsEmpty()) { - return {}; - } - - auto p_header = shared_mem_view_->GetHeader(); - if (!p_header->process_active) { - return {}; - } - - return p_header->gpuTelemetryCapBits; -} - -std::optional< - std::bitset(CpuTelemetryCapBits::cpu_telemetry_count)>> -StreamClient::GetCpuTelemetryCaps() { - if (shared_mem_view_->IsEmpty()) { - return {}; - } - - auto p_header = shared_mem_view_->GetHeader(); - if (!p_header->process_active) { - return {}; - } - - return p_header->cpuTelemetryCapBits; -} diff --git a/IntelPresentMon/Streamer/StreamClient.h b/IntelPresentMon/Streamer/StreamClient.h deleted file mode 100644 index 304359c73..000000000 --- a/IntelPresentMon/Streamer/StreamClient.h +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (C) 2022 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once -#include -#include -#include -#include -#include "../PresentMonUtils/StreamFormat.h" -#include "../PresentMonUtils/LegacyAPIDefines.h" -#include "NamedSharedMemory.h" - -class StreamClient { - public: - StreamClient(); - StreamClient(std::string mapfile_name, bool is_etl_stream_client); - ~StreamClient(); - - void Initialize(std::string mapfile_name); - bool IsInitialized() { return initialized_; }; - // Read the latest frame from shared memory. - PmNsmFrameData* ReadLatestFrame(); - PmNsmFrameData* ReadFrameByIdx(uint64_t frame_id, bool checked); - // Dequeue a frame of data from shared mem and update the last_read_idx (just get pointer to NsmData) - PM_STATUS ConsumePtrToNextNsmFrameData(const PmNsmFrameData** pNsmData, - const PmNsmFrameData** pNextFrame, - const PmNsmFrameData** pFrameDataOfNextDisplayed, - const PmNsmFrameData** pFrameDataOfLastPresented, - const PmNsmFrameData** pFrameDataOfLastAppPresented, - const PmNsmFrameData** pFrameDataOfLastDisplayed, - const PmNsmFrameData** pFrameDataOfLastAppDisplayed, - const PmNsmFrameData** pFrameDataOfPreviousAppFrameOfLastAppDisplayed); - // Return the last frame id that holds valid data - uint64_t GetLatestFrameIndex(); - NamedSharedMem* GetNamedSharedMemView() { return shared_mem_view_.get(); } - void CloseSharedMemView(); - LARGE_INTEGER GetQpcFrequency() { return qpcFrequency_; }; - void CopyFrameData(uint64_t start_qpc, const PmNsmFrameData* src_frame, - GpuTelemetryBitset gpu_telemetry_cap_bits, - CpuTelemetryBitset cpu_telemetry_cap_bits, - PM_FRAME_DATA* dst_frame); - - std::optional(GpuTelemetryCapBits::gpu_telemetry_count)>> - GetGpuTelemetryCaps(); - std::optional(CpuTelemetryCapBits::cpu_telemetry_count)>> - GetCpuTelemetryCaps(); - - private: - uint64_t CheckPendingReadFrames(); - - // Functions to peek at the next and previous frames - void PeekNextFrames(const PmNsmFrameData** pNextFrame, - const PmNsmFrameData** pNextDisplayedFrame); - void PeekPreviousFrames(const PmNsmFrameData** pFrameDataOfLastPresented, - const PmNsmFrameData** pFrameDataOfLastAppPresented, - const PmNsmFrameData** pFrameDataOfLastDisplayed, - const PmNsmFrameData** pFrameDataOfLastAppDisplayed, - const PmNsmFrameData** pFrameDataOfPreviousAppFrameOfLastAppDisplayed); - - // Helper functions evaluater various frame types - bool IsAppPresentedFrame(const PmNsmFrameData* frame) const; - bool IsDisplayedFrame(const PmNsmFrameData* frame) const; - bool IsAppDisplayedFrame(const PmNsmFrameData* frame) const; - - // Shared memory view that the client opened into based on mapfile name - std::unique_ptr shared_mem_view_; - // mapfile name the client has for named shared memory - std::string mapfile_name_; - // Last read offset from the shared_mem_view_ - bool initialized_; - LARGE_INTEGER qpcFrequency_ = {}; - uint64_t next_dequeue_idx_; - bool recording_frame_data_; - uint64_t current_dequeue_frame_num_; -}; \ No newline at end of file diff --git a/IntelPresentMon/Streamer/Streamer.cpp b/IntelPresentMon/Streamer/Streamer.cpp deleted file mode 100644 index bce2846bc..000000000 --- a/IntelPresentMon/Streamer/Streamer.cpp +++ /dev/null @@ -1,480 +0,0 @@ -// Copyright (C) 2022-2023 Intel Corporation -// SPDX-License-Identifier: MIT -// Streamer.cpp : Defines the functions for the static library. - -#include -#include -#include -#include - -#include "Streamer.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include "../PresentMonService/CliOptions.h" -#include "../CommonUtilities/str/String.h" -#include "../CommonUtilities/log/GlogShim.h" - -namespace vi = std::views; -namespace rn = std::ranges; - -static const std::chrono::milliseconds kTimeoutLimitMs = - std::chrono::milliseconds(500); - -Streamer::Streamer() - : shared_mem_size_(kBufSize), - start_qpc_(0), - write_timedout_(false), - mapfileNamePrefix_{ kGlobalPrefix } -{ - if (clio::Options::IsInitialized()) { - mapfileNamePrefix_ = clio::Options::Get().nsmPrefix.AsOptional().value_or(mapfileNamePrefix_); - } -} - -void Streamer::WriteFrameData( - uint32_t process_id, PmNsmFrameData* data, - std::bitset(GpuTelemetryCapBits::gpu_telemetry_count)> - gpu_telemetry_cap_bits, - std::bitset(CpuTelemetryCapBits::cpu_telemetry_count)> - cpu_telemetry_cap_bits) { - if (data == NULL) { - LOG(ERROR) << "Invalid data."; - return; - } - - // Lock the nsm mutex as stop streaming calls can occur at any time - // and destory the named shared memory during writing of frame data. - std::lock_guard lock(nsm_map_mutex_); - auto iter = process_shared_mem_map_.find(process_id); - if (iter == process_shared_mem_map_.end()) { - LOG(INFO) << "Corresponding named shared memory doesn't exist. Please call StartStreaming(process_id) first."; - - return; - } - - auto shared_mem = iter->second.get(); - - // Record start time if it's the first frame. - if (shared_mem->IsEmpty()) { - shared_mem->RecordFirstFrameTime(data->present_event.PresentStartTime); - } - shared_mem->WriteTelemetryCapBits(gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - shared_mem->WriteFrameData(data); -} - -void Streamer::CopyFromPresentMonPresentEvent( - PresentEvent* present_event, PmNsmPresentEvent* nsm_present_event) { - if (present_event == nullptr || nsm_present_event == nullptr) { - return; - } - - nsm_present_event->PresentStartTime = present_event->PresentStartTime; - nsm_present_event->ProcessId = present_event->ProcessId; - nsm_present_event->ThreadId = present_event->ThreadId; - nsm_present_event->TimeInPresent = present_event->TimeInPresent; - nsm_present_event->GPUStartTime = present_event->GPUStartTime; - nsm_present_event->ReadyTime = present_event->ReadyTime; - nsm_present_event->GPUDuration = present_event->GPUDuration; - nsm_present_event->GPUVideoDuration = present_event->GPUVideoDuration; - nsm_present_event->InputTime = present_event->InputTime; - nsm_present_event->MouseClickTime = present_event->MouseClickTime; - - nsm_present_event->AppPropagatedPresentStartTime = present_event->AppPropagatedPresentStartTime; - nsm_present_event->AppPropagatedTimeInPresent = present_event->AppPropagatedTimeInPresent; - nsm_present_event->AppPropagatedGPUStartTime = present_event->AppPropagatedGPUStartTime; - nsm_present_event->AppPropagatedReadyTime = present_event->AppPropagatedReadyTime; - nsm_present_event->AppPropagatedGPUDuration = present_event->AppPropagatedGPUDuration; - nsm_present_event->AppPropagatedGPUVideoDuration = present_event->AppPropagatedGPUVideoDuration; - - nsm_present_event->AppSleepStartTime = present_event->AppSleepStartTime; - nsm_present_event->AppSleepEndTime = present_event->AppSleepEndTime; - nsm_present_event->AppSimStartTime = present_event->AppSimStartTime; - nsm_present_event->AppSimEndTime = present_event->AppSimEndTime; - nsm_present_event->AppRenderSubmitStartTime = present_event->AppRenderSubmitStartTime; - nsm_present_event->AppRenderSubmitEndTime = present_event->AppRenderSubmitEndTime; - nsm_present_event->AppPresentStartTime = present_event->AppPresentStartTime; - nsm_present_event->AppPresentEndTime = present_event->AppPresentEndTime; - nsm_present_event->AppInputTime = present_event->AppInputSample.first; - nsm_present_event->AppInputType = present_event->AppInputSample.second; - - nsm_present_event->PclInputPingTime = present_event->PclInputPingTime; - nsm_present_event->PclSimStartTime = present_event->PclSimStartTime; - - nsm_present_event->FlipDelay = present_event->FlipDelay; - nsm_present_event->FlipToken = present_event->FlipToken; - - nsm_present_event->SwapChainAddress = present_event->SwapChainAddress; - nsm_present_event->SyncInterval = present_event->SyncInterval; - nsm_present_event->PresentFlags = present_event->PresentFlags; - - nsm_present_event->DisplayedCount = (uint32_t) min(present_event->Displayed.size(), _countof(nsm_present_event->Displayed_ScreenTime)); - for (uint32_t i = 0; i < nsm_present_event->DisplayedCount; ++i) { - nsm_present_event->Displayed_ScreenTime[i] = present_event->Displayed[i].second; - nsm_present_event->Displayed_FrameType[i] = present_event->Displayed[i].first; - } - - nsm_present_event->CompositionSurfaceLuid = - present_event->CompositionSurfaceLuid; - nsm_present_event->Win32KPresentCount = present_event->Win32KPresentCount; - nsm_present_event->Win32KBindId = present_event->Win32KBindId; - nsm_present_event->DxgkPresentHistoryToken = - present_event->DxgkPresentHistoryToken; - nsm_present_event->DxgkPresentHistoryTokenData = - present_event->DxgkPresentHistoryTokenData; - nsm_present_event->DxgkContext = present_event->DxgkContext; - nsm_present_event->Hwnd = present_event->Hwnd; - nsm_present_event->QueueSubmitSequence = present_event->QueueSubmitSequence; - nsm_present_event->RingIndex = - present_event->RingIndex; - - nsm_present_event->DestWidth = present_event->DestWidth; - nsm_present_event->DestHeight = present_event->DestHeight; - nsm_present_event->DriverThreadId = present_event->DriverThreadId; - - nsm_present_event->FrameId = present_event->FrameId; - - nsm_present_event->Runtime = present_event->Runtime; - nsm_present_event->PresentMode = present_event->PresentMode; - nsm_present_event->FinalState = present_event->FinalState; - nsm_present_event->InputType = present_event->InputType; - - nsm_present_event->SupportsTearing = present_event->SupportsTearing; - nsm_present_event->WaitForFlipEvent = present_event->WaitForFlipEvent; - nsm_present_event->WaitForMPOFlipEvent = present_event->WaitForMPOFlipEvent; - nsm_present_event->SeenDxgkPresent = present_event->SeenDxgkPresent; - nsm_present_event->SeenWin32KEvents = present_event->SeenWin32KEvents; - nsm_present_event->SeenInFrameEvent = present_event->SeenInFrameEvent; - nsm_present_event->GpuFrameCompleted = present_event->GpuFrameCompleted; - nsm_present_event->IsCompleted = present_event->IsCompleted; - nsm_present_event->IsLost = present_event->IsLost; - nsm_present_event->PresentInDwmWaitingStruct = - present_event->PresentInDwmWaitingStruct; - - return; -} - -void Streamer::ProcessPresentEvent( - PresentEvent* present_event, - PresentMonPowerTelemetryInfo* power_telemetry_info, - CpuTelemetryInfo* cpu_telemetry_info, uint64_t last_present_qpc, - uint64_t last_displayed_qpc, std::wstring app_name, - std::bitset(GpuTelemetryCapBits::gpu_telemetry_count)> - gpu_telemetry_cap_bits, - std::bitset(CpuTelemetryCapBits::cpu_telemetry_count)> - cpu_telemetry_cap_bits) { - uint32_t process_id = present_event->ProcessId; - - // Lock the nsm mutex as stop streaming calls can occur at any time - // and destroy the named shared memory during writing of frame data. - std::lock_guard lock(nsm_map_mutex_); - - // Search for the requested process - auto iter = process_shared_mem_map_.find(process_id); - NamedSharedMem* process_nsm = nullptr; - if (iter != process_shared_mem_map_.end()) { - process_nsm = iter->second.get(); - // Record start time if it's the first frame. - if (process_nsm->IsEmpty()) { - if (start_qpc_ && process_nsm->GetHeader()->isPlayback) { - process_nsm->RecordFirstFrameTime(start_qpc_); - } - else { - process_nsm->RecordFirstFrameTime(present_event->PresentStartTime); - } - } - } - - // In addition search for the stream all process - auto stream_all_iter = process_shared_mem_map_.find( - (uint32_t)StreamPidOverride::kStreamAllPid); - NamedSharedMem* stream_all_nsm = nullptr; - if (stream_all_iter != process_shared_mem_map_.end()) { - stream_all_nsm = stream_all_iter->second.get(); - // Record start time if it's the first frame. - if (stream_all_nsm->IsEmpty()) { - if (start_qpc_ && stream_all_nsm->GetHeader()->isPlayback) { - stream_all_nsm->RecordFirstFrameTime(start_qpc_); - } - else { - stream_all_nsm->RecordFirstFrameTime(present_event->PresentStartTime); - } - } - } - - if ((process_nsm == nullptr) && (stream_all_nsm == nullptr)) { - // process is not being monitored. Skip. - return; - } - - PmNsmFrameData data = {}; - // Copy the passed in PresentEvent data into the PmNsmFrameData - // structure. - CopyFromPresentMonPresentEvent(present_event, &data.present_event); - // Now update the necessary qpcs and application name which - // reside AFTER the PresentEvent members and hence were not - // updated in the copy above. - data.present_event.last_present_qpc = last_present_qpc; - data.present_event.last_displayed_qpc = last_displayed_qpc; - auto appNameNarrow = pmon::util::str::ToNarrow(app_name); - std::size_t length = appNameNarrow.copy(data.present_event.application, appNameNarrow.size()); - data.present_event.application[length] = '\0'; - // Now copy the power telemetry data - memcpy_s(&data.power_telemetry, sizeof(PresentMonPowerTelemetryInfo), - power_telemetry_info, sizeof(PresentMonPowerTelemetryInfo)); - // Finally copy the cpu telemetry data - memcpy_s(&data.cpu_telemetry, sizeof(CpuTelemetryInfo), cpu_telemetry_info, - sizeof(CpuTelemetryInfo)); - - if (process_nsm) { - auto pHdr = process_nsm->GetHeader(); - // block here if nsm is full and backpressure is enabled (only in playback modes) - if (pHdr->isPlaybackBackpressured && process_nsm->IsFull()) { - const auto start = std::chrono::high_resolution_clock::now(); - do { - const auto now = std::chrono::high_resolution_clock::now(); - const auto time_elapsed = now - start; - LOG(INFO) << "NSM is full ..."; - using namespace std::literals; - std::this_thread::sleep_for(25ms); - - if (time_elapsed >= kTimeoutLimitMs) { - LOG(ERROR) << "\nServer data write timed out."; - write_timedout_ = true; - return; - } - } while (process_nsm->IsFull()); - } - process_nsm->WriteTelemetryCapBits(gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - process_nsm->WriteFrameData(&data); - } - - if (stream_all_nsm) { - stream_all_nsm->WriteTelemetryCapBits(gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - stream_all_nsm->WriteFrameData(&data); - } -} - - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -/// @brief StartStreaming with target process_id -/// @return Returns corresponding mapfile_name string. If mapfile_name is empty, it means process is not found. -/// - PM_STATUS Streamer::StartStreaming(uint32_t client_process_id, - uint32_t target_process_id, - std::string& mapfile_name, - bool isPlayback, - bool isPlaybackPaced, - bool isPlaybackRetimed, - bool isPlaybackBackpressured, - bool isPlaybackResetOldest) { - auto target_range = client_map_.equal_range(client_process_id); - auto found = std::any_of(target_range.first, target_range.second, - [target_process_id](auto client_entry) { - return target_process_id == client_entry.second; - }); - if (found) { - return PM_STATUS::PM_STATUS_ALREADY_TRACKING_PROCESS; - } - - uint64_t mem_size = kBufSize; - #define _CRT_SECURE_NO_WARNINGS - const char* name = "PM2_NSM_SIZE"; - char* pValue = nullptr; - size_t len; - errno_t err = _dupenv_s(&pValue, &len, name); - if (err || pValue == nullptr) { - LOG(INFO) << "Use default NSM size : " << kBufSize; - } else { - LOG(INFO) << "PM2_NSM_SIZE = " << pValue; - char* pEnd; - mem_size = strtoull(pValue, &pEnd,10); - } - free(pValue); - - // create new shared mem for particular process id - if (CreateNamedSharedMemory(target_process_id, mem_size, - isPlayback, - isPlaybackPaced, - isPlaybackRetimed, - isPlaybackBackpressured, - isPlaybackResetOldest) == false) { - return PM_STATUS::PM_STATUS_UNABLE_TO_CREATE_NSM; - } - - client_map_.insert(std::make_pair(client_process_id, target_process_id)); - LOG(INFO) << "Started streaming for process id:" << target_process_id; - mapfile_name = GetMapFileName(target_process_id); - - return PM_STATUS::PM_STATUS_SUCCESS; -} - -// Update the named shared memory process attachments. If the number of -// attachments drops to zero release the NSM. Function assumes the -// NSM map mutex has been called PRIOR to calling this function. -bool Streamer::UpdateNSMAttachments(uint32_t process_id, int& ref_count) { - ref_count = 0; - auto iter = process_shared_mem_map_.find(process_id); - if (iter != process_shared_mem_map_.end()) { - // Check if refcount is going to be zero - if (iter->second->GetRefCount() > 1) { - iter->second->DecrementRefcount(); - ref_count = iter->second->GetRefCount(); - } else { - iter->second->NotifyProcessKilled(); - process_shared_mem_map_.erase(std::move(iter)); - ref_count = 0; - } - return true; - } - return false; -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -/// @brief Stop streaming with target process_id. -/// Decrement refcount if more than one client is attached to it. -/// Otherwise destroy corresponding mapfilename -/// -void Streamer::StopStreaming(uint32_t process_id) { - // Lock the nsm map mutex as stop streaming calls can occur at any time - // from both the client and the output thread when it detects processes - // have terminated - std::lock_guard lock(nsm_map_mutex_); - - int ref_count = 0; - // Check to see if the incoming process id is a client process id - if (client_map_.contains(process_id) == false) { - // Not a client process id, assume a target process. Need to remove - // the NSM. - bool status = UpdateNSMAttachments(process_id, ref_count); - while ((status == true) && (ref_count > 0)) { - status = UpdateNSMAttachments(process_id, ref_count); - } - if ((status == true) && (ref_count == 0)) { - // If the passed in target process id resulted in a destruction of the - // named shared memory then go through the client maps and remove - // any references to the target process. - for (auto i = client_map_.begin(); i != client_map_.end();) { - if (i->second == process_id) { - i = client_map_.erase(i); - } else { - ++i; - } - } - } - } else { - auto client_range = client_map_.equal_range(process_id); - for (auto i = client_range.first; i != client_range.second; ++i) { - UpdateNSMAttachments(i->second, ref_count); - } - client_map_.erase(client_range.first, client_range.second); - } - return; -} - -void Streamer::StopStreaming(uint32_t client_process_id, - uint32_t target_process_id) { - // Lock the nsm map mutex as stop streaming calls can occur at any time - // from both the client and the output thread when it detects processes - // have terminated - std::lock_guard lock(nsm_map_mutex_); - - int ref_count = 0; - bool status = UpdateNSMAttachments(target_process_id, ref_count); - if ((status == true) && (ref_count == 0)) { - // If the passed in target process id resulted in the destruction of the - // named shared memory then go through the client maps and remove - // any references to the target process. - for (auto i = client_map_.begin(); i != client_map_.end();) { - if (i->second == target_process_id) { - i = client_map_.erase(i); - } else { - ++i; - } - } - } else if ((status == true) && (ref_count > 0)) { - // Succesfully found the NSM of the target process id but other clients - // are still monitoring it. Only remove it from this clients mapping. - for (auto i = client_map_.begin(); i != client_map_.end();) { - if ((i->first == client_process_id) && (i->second == target_process_id)) { - i = client_map_.erase(i); - } else { - ++i; - } - } - } - return; -} - -void Streamer::StopAllStreams() { - // Lock the nsm map mutex to ensure we don't destroy the NSMs - // while writing frame data. - std::lock_guard lock(nsm_map_mutex_); - for (auto const& it : process_shared_mem_map_) { - it.second->NotifyProcessKilled(); - } - process_shared_mem_map_.clear(); - client_map_.clear(); - write_timedout_ = false; -} - -bool Streamer::CreateNamedSharedMemory(DWORD process_id, - uint64_t nsm_size_in_bytes, - bool isPlayback, - bool isPlaybackPaced, - bool isPlaybackRetimed, - bool isPlaybackBackpressured, - bool isPlaybackResetOldest) { - std::string mapfile_name = mapfileNamePrefix_ + std::to_string(process_id); - - std::lock_guard lock(nsm_map_mutex_); - auto iter = process_shared_mem_map_.find(process_id); - if (iter == process_shared_mem_map_.end()) { - auto nsm = - std::make_unique(std::move(mapfile_name), nsm_size_in_bytes, - isPlayback, - isPlaybackPaced, - isPlaybackRetimed, - isPlaybackBackpressured, - isPlaybackResetOldest); - if (nsm->IsNSMCreated()) { - process_shared_mem_map_.emplace(process_id, std::move(nsm)); - return true; - } else { - LOG(INFO) << "Unabled to create NSM for process id:" << process_id; - return false; - } - - } else { - LOG(INFO) << "Shared mem for process(" << process_id - << ") already exists. Increment recount."; - iter->second->IncrementRefcount(); - return true; - } -} - -std::string Streamer::GetMapFileName(DWORD process_id) { - std::lock_guard lock(nsm_map_mutex_); - auto iter = process_shared_mem_map_.find(process_id); - std::string mapfile_name; - - if (iter != process_shared_mem_map_.end()) { - mapfile_name = iter->second->GetMapFileName(); - } - - return mapfile_name; -} - -std::set Streamer::GetActiveStreamPids() const -{ - std::lock_guard lk{ nsm_map_mutex_ }; - return { std::from_range, process_shared_mem_map_ | vi::keys }; -} \ No newline at end of file diff --git a/IntelPresentMon/Streamer/Streamer.h b/IntelPresentMon/Streamer/Streamer.h deleted file mode 100644 index 9332d27a3..000000000 --- a/IntelPresentMon/Streamer/Streamer.h +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (C) 2022-2023 Intel Corporation -// SPDX-License-Identifier: MIT -#pragma once - -#include -#include -#include -#include -#include - -#include "../PresentMonUtils/StreamFormat.h" -#include "gtest/gtest.h" -#include "NamedSharedMemory.h" - -class Streamer { - public: - Streamer(); - ~Streamer() = default; - - // Client API, start streaming data for process by name - PM_STATUS StartStreaming(uint32_t client_process_id, - uint32_t target_process_id, - std::string& mapfile_name, - bool isPlayback, - bool isPlaybackPaced, - bool isPlaybackRetimed, - bool isPlaybackBackpressured, - bool isPlaybackResetOldest); - - // Set streaming mode. Default value is real time streaming for single process. - void StopStreaming(uint32_t client_process_id, uint32_t target_process_id); - void StopStreaming(uint32_t process_id); - void StopAllStreams(); - // Last producer and last consumer are internal fields - // Remove for public build - void ProcessPresentEvent( - PresentEvent* present_event, - PresentMonPowerTelemetryInfo* power_telemetry_info, - CpuTelemetryInfo* cpu_telemetry_info, uint64_t last_present_qpc, - uint64_t last_displayed_qpc, std::wstring app_name, - std::bitset(GpuTelemetryCapBits::gpu_telemetry_count)> - gpu_telemetry_cap_bits, - std::bitset(CpuTelemetryCapBits::cpu_telemetry_count)> - cpu_telemetry_cap_bits); - - void WriteFrameData( - uint32_t process_id, PmNsmFrameData* data, - std::bitset(GpuTelemetryCapBits::gpu_telemetry_count)> - gpu_telemetry_cap_bits, - std::bitset(CpuTelemetryCapBits::cpu_telemetry_count)> - cpu_telemetry_cap_bits); - std::string GetMapFileName(DWORD process_id); - std::set GetActiveStreamPids() const; - void SetStartQpc(uint64_t start_qpc) { start_qpc_ = start_qpc; }; - bool IsTimedOut() { return write_timedout_; }; - int NumActiveStreams() { return (int)process_shared_mem_map_.size(); } - - private: - FRIEND_TEST(NamedSharedMemoryTest, CreateNamedSharedMemory); - FRIEND_TEST(NamedSharedMemoryTestCustomSize, CreateNamedSharedMemory); - bool CreateNamedSharedMemory(DWORD process_id, uint64_t nsm_size_in_bytes, - bool isPlayback, - bool isPlaybackPaced, - bool isPlaybackRetimed, - bool isPlaybackBackpressured, - bool isPlaybackResetOldest); - void CopyFromPresentMonPresentEvent(PresentEvent* present_event, - PmNsmPresentEvent* nsm_present_event); - bool UpdateNSMAttachments(uint32_t process_id, int& ref_count); - std::string mapfileNamePrefix_; - // Shared mem buffer map of process id and share mem handle - std::map> process_shared_mem_map_; - std::multimap client_map_; - uint64_t shared_mem_size_; - uint64_t start_qpc_; - // This flag is currently used during etl processing. If the client - // misbehaved or died such that streamer is blocked to write frame data, - // write_timedout_ would be set to true and etl_session_ of PresentMon would - // stop the trace session. - bool write_timedout_; - mutable std::mutex nsm_map_mutex_; -}; diff --git a/IntelPresentMon/Streamer/Streamer.vcxproj b/IntelPresentMon/Streamer/Streamer.vcxproj deleted file mode 100644 index b6b857204..000000000 --- a/IntelPresentMon/Streamer/Streamer.vcxproj +++ /dev/null @@ -1,134 +0,0 @@ - - - - - Debug - x64 - - - Release - x64 - - - - 16.0 - Win32Proj - {bf43064b-01f0-4c69-91fb-c2122baf621d} - Streamer - 10.0 - - - - StaticLibrary - true - v143 - MultiByte - x64 - - - StaticLibrary - false - v143 - true - MultiByte - x64 - - - - - - - - - - - - - - - - - - - true - --checks=clang-analyzer-*,google-* - false - false - true - - - false - --checks=clang-analyzer-*,google-* - false - false - true - - - - - - - - Level3 - true - true - NotUsing - MultiThreadedDebug - stdcpplatest - - - - - true - - - - - Level3 - true - true - true - true - NotUsing - MultiThreaded - stdcpplatest - NDEBUG;%(PreprocessorDefinitions) - Guard - - - - - true - true - true - - - - - - - - - - - - - - - - - - {892028e5-32f6-45fc-8ab2-90fcbcac4bf6} - - - {08a704d8-ca1c-45e9-8ede-542a1a43b53e} - - - {66e9f6c5-28db-4218-81b9-31e0e146ecc0} - - - - - - - \ No newline at end of file diff --git a/IntelPresentMon/Streamer/Streamer.vcxproj.filters b/IntelPresentMon/Streamer/Streamer.vcxproj.filters deleted file mode 100644 index cae7383e9..000000000 --- a/IntelPresentMon/Streamer/Streamer.vcxproj.filters +++ /dev/null @@ -1,23 +0,0 @@ - - - - - {88626460-8f37-486c-9c5b-7a47e38ff975} - - - - - - - - - - - - - - - proto - - - \ No newline at end of file diff --git a/IntelPresentMon/ULT/MemBufferTests.cpp b/IntelPresentMon/ULT/MemBufferTests.cpp deleted file mode 100644 index 4c774b6b9..000000000 --- a/IntelPresentMon/ULT/MemBufferTests.cpp +++ /dev/null @@ -1,174 +0,0 @@ -#include -#include -#include - -#include "../PresentMonUtils/MemBuffer.h" - -class MemBufferULT : public ::testing::Test { - public: - void SetUp() override { mMemBuffer = std::make_unique(); } - std::unique_ptr mMemBuffer; -}; - -TEST_F(MemBufferULT, MemBufferInitialization) { - EXPECT_TRUE((mMemBuffer->GetCurrentSize() == 0)); - EXPECT_TRUE((mMemBuffer->AccessMem() == nullptr)); -} - -TEST_F(MemBufferULT, MemBufferSingleItemAdd) { - double testData[2]; - testData[0] = 60.0f; - testData[1] = 120.34f; - size_t testDataSize = sizeof(testData); - mMemBuffer->AddItem(testData, testDataSize); - EXPECT_TRUE((mMemBuffer->GetCurrentSize() == testDataSize)); - - double* tempTestData = (double*)mMemBuffer->AccessMem(); - EXPECT_TRUE(tempTestData != nullptr); - EXPECT_TRUE(*tempTestData++ == 60.0f); - EXPECT_TRUE(*tempTestData++ == 120.34f); -} - -TEST_F(MemBufferULT, MemBufferMultiItemAddOne) { - - uint32_t testData1 = 0x1645; - size_t testData1Size = sizeof(testData1); - uint64_t testData2 = 0xba5eball; - size_t testData2Size = sizeof(testData2); - double testData3[2]; - testData3[0] = 60.0f; - testData3[1] = 120.34f; - size_t testData3Size = sizeof(testData3); - - mMemBuffer->AddItem(&testData1, testData1Size); - mMemBuffer->AddItem(&testData2, testData2Size); - mMemBuffer->AddItem(&testData3, testData3Size); - EXPECT_TRUE((mMemBuffer->GetCurrentSize() == testData1Size + testData2Size + testData3Size)); - - LPVOID testData = mMemBuffer->AccessMem(); - EXPECT_TRUE(testData != nullptr); - - uint32_t* dataItem1 = (uint32_t*)testData; - EXPECT_TRUE(*dataItem1 == testData1); - - uint64_t* dataItem2 = (uint64_t*)((BYTE*)testData + testData1Size); - EXPECT_TRUE(*dataItem2 == testData2); - - double* dataItem3 = (double*)((BYTE*)testData + (testData1Size + testData2Size)); - EXPECT_TRUE(*dataItem3++ == 60.0f); - EXPECT_TRUE(*dataItem3++ == 120.34f); -} - -TEST_F(MemBufferULT, MemBufferMultiItemAddTwo) { - - uint32_t testData1 = 0x1645; - size_t testData1Size = sizeof(testData1); - uint64_t testData2 = 0xba5eball; - size_t testData2Size = sizeof(testData2); - double testData3[2]; - testData3[0] = 60.0f; - testData3[1] = 120.34f; - size_t testData3Size = sizeof(testData3); - uint64_t testData4[10]; - size_t testData4Size = sizeof(testData4); - - BCRYPT_ALG_HANDLE crypto_algo; - auto status = BCryptOpenAlgorithmProvider( - &crypto_algo, BCRYPT_RNG_ALGORITHM, NULL, 0); - EXPECT_TRUE(status >= 0); - - for (uint32_t i = 0; i < 10; i++) { - ULONG test_data_size = sizeof(uint64_t); - auto rand_gen_success = - BCryptGenRandom(crypto_algo, (BYTE*)&testData4[i], test_data_size, 0); - EXPECT_TRUE(rand_gen_success >= 0); - } - status = BCryptCloseAlgorithmProvider(crypto_algo, 0); - EXPECT_TRUE(status >= 0); - - mMemBuffer->AddItem(&testData1, testData1Size); - mMemBuffer->AddItem(&testData2, testData2Size); - mMemBuffer->AddItem(&testData3, testData3Size); - mMemBuffer->AddItem(&testData4, testData4Size); - EXPECT_TRUE((mMemBuffer->GetCurrentSize() == testData1Size + testData2Size + testData3Size + testData4Size)); - - LPVOID testData = mMemBuffer->AccessMem(); - EXPECT_TRUE(testData != nullptr); - - uint32_t* dataItem1 = (uint32_t*)testData; - EXPECT_TRUE(*dataItem1 == testData1); - - uint64_t* dataItem2 = (uint64_t*)((BYTE*)testData + testData1Size); - EXPECT_TRUE(*dataItem2 == testData2); - - double* dataItem3 = (double*)((BYTE*)testData + (testData1Size + testData2Size)); - EXPECT_TRUE(*dataItem3++ == testData3[0]); - EXPECT_TRUE(*dataItem3++ == testData3[1]); - - uint64_t* dataItem4 = (uint64_t*)((BYTE*)testData + (testData1Size + testData2Size + testData3Size)); - for (uint32_t i = 0; i < 10; i++) { - EXPECT_TRUE(*dataItem4++ == testData4[i]); - } -} - -TEST_F(MemBufferULT, MemBufferAddAfterAccess) { - - uint32_t testData1 = 0x1645; - size_t testData1Size = sizeof(testData1); - uint64_t testData2 = 0xba5eball; - size_t testData2Size = sizeof(testData2); - double testData3[2]; - testData3[0] = 60.0f; - testData3[1] = 120.34f; - size_t testData3Size = sizeof(testData3); - - mMemBuffer->AddItem(&testData1, testData1Size); - mMemBuffer->AddItem(&testData2, testData2Size); - mMemBuffer->AddItem(&testData3, testData3Size); - EXPECT_TRUE((mMemBuffer->GetCurrentSize() == testData1Size + testData2Size + testData3Size)); - - LPVOID testData = mMemBuffer->AccessMem(); - EXPECT_TRUE(testData != nullptr); - - uint32_t* dataItem1 = (uint32_t*)testData; - EXPECT_TRUE(*dataItem1 == testData1); - - uint64_t* dataItem2 = (uint64_t*)((BYTE*)testData + testData1Size); - EXPECT_TRUE(*dataItem2 == testData2); - - double* dataItem3 = (double*)((BYTE*)testData + (testData1Size + testData2Size)); - EXPECT_TRUE(*dataItem3++ == 60.0f); - EXPECT_TRUE(*dataItem3++ == 120.34f); - - uint64_t testData4[10]; - size_t testData4Size = sizeof(testData4); - - BCRYPT_ALG_HANDLE crypto_algo; - auto status = BCryptOpenAlgorithmProvider( - &crypto_algo, BCRYPT_RNG_ALGORITHM, NULL, 0); - EXPECT_TRUE(status >= 0); - - for (uint32_t i = 0; i < 10; i++) { - ULONG test_data_size = sizeof(uint64_t); - auto rand_gen_success = - BCryptGenRandom(crypto_algo, (BYTE*)&testData4[i], test_data_size, 0); - EXPECT_TRUE(rand_gen_success >= 0); - } - status = BCryptCloseAlgorithmProvider(crypto_algo, 0); - EXPECT_TRUE(status >= 0); - - mMemBuffer->AddItem(&testData4, testData4Size); - - testData = mMemBuffer->AccessMem(); - - EXPECT_TRUE((mMemBuffer->GetCurrentSize() == testData1Size + testData2Size + testData3Size + testData4Size)); - uint64_t* dataItem4 = (uint64_t*)((BYTE*)testData + (testData1Size + testData2Size + testData3Size)); - for (uint32_t i = 0; i < 10; i++) { - EXPECT_TRUE(*dataItem4++ == testData4[i]); - } -} - -TEST_F(MemBufferULT, MemBufferNullInputBuffer) { - bool status = mMemBuffer->AddItem(nullptr, 0); - EXPECT_TRUE(status == false); -} \ No newline at end of file diff --git a/IntelPresentMon/ULT/PMApiTests.cpp b/IntelPresentMon/ULT/PMApiTests.cpp deleted file mode 100644 index 39e73d0cc..000000000 --- a/IntelPresentMon/ULT/PMApiTests.cpp +++ /dev/null @@ -1,1219 +0,0 @@ -#include "../PresentMonUtils/MemBuffer.h" -#include "../PresentMonUtils/PresentMonNamedPipe.h" -#include "../Streamer/StreamClient.h" -#include "../Streamer/Streamer.h" -#include "PmFrameGenerator.h" -#include "gtest/gtest.h" -#include "utils.h" - -#include "../CommonUtilities/log/GlogShim.h" - -const uint32_t kPid = 10; -const double kRunWindowSize = 1000.0f; -const uint32_t kNumFrames = 150; - -// Error tolerance -const double kErrorTolerance = 0.2; - -static bool fltCmp(double val1, double val2, double tolerance) { - if (fabs(val1 - val2) < tolerance) { - return true; - } else { - return false; - } -} - -class PresentMonApiULT : public ::testing::Test {}; - -bool Equal(const PM_METRIC_DOUBLE_DATA &data1, const PM_METRIC_DOUBLE_DATA& data2) { - - if (!fltCmp(data1.avg, data2.avg, kErrorTolerance)) { - return false; - } - if (!fltCmp(data1.high, data2.high, kErrorTolerance)) { - return false; - } - if (!fltCmp(data1.low, data2.low, kErrorTolerance)) { - return false; - } - if (!fltCmp(data1.percentile_90, data2.percentile_90, kErrorTolerance)) { - return false; - } - if (!fltCmp(data1.percentile_95, data2.percentile_95, kErrorTolerance)) { - return false; - } - if (!fltCmp(data1.percentile_95, data2.percentile_95, kErrorTolerance)) { - return false; - } - if (!fltCmp(data1.raw, data2.raw, kErrorTolerance)) { - return false; - } - return true; -} - -template -bool CompareTelemetry(T telemetry_a, T telemetry_b) { - if constexpr (std::is_floating_point::value) { - return (fltCmp(telemetry_a, telemetry_b, kErrorTolerance)); - } else { - return (telemetry_a == telemetry_b); - } -} - -template -bool CompareOptionalTelemetry(T telemetry_a, T telemetry_b) { - - if (telemetry_a.valid == telemetry_b.valid) { - return CompareTelemetry(telemetry_a.data, telemetry_b.data); - return true; - } else { - return false; - } -} - -bool CompareFrameData(const PM_FRAME_DATA &data1, const PM_FRAME_DATA &data2) { - std::string data1_string = data1.application; - std::string data2_string = data2.application; - if (data1_string != data2_string) { - return false; - } - if (data1.swap_chain_address != data2.swap_chain_address) { - return false; - } - data1_string = data1.runtime; - data2_string = data2.runtime; - if (data1_string != data2_string) { - return false; - } - if (data1.sync_interval != data2.sync_interval) { - return false; - } - if (data1.present_flags != data2.present_flags) { - return false; - } - if (data1.dropped != data2.dropped) { - return false; - } - if (!fltCmp(data1.time_in_seconds, data2.time_in_seconds, kErrorTolerance)) { - return false; - } - if (!fltCmp(data1.ms_in_present_api, data2.ms_in_present_api, - kErrorTolerance)) { - return false; - } - if (!fltCmp(data1.ms_between_presents, data2.ms_between_presents, - kErrorTolerance)) { - return false; - } - if (data1.allows_tearing != data2.allows_tearing) { - return false; - } - if (data1.present_mode != data2.present_mode) { - return false; - } - if (!fltCmp(data1.ms_until_render_complete, data2.ms_until_render_complete, - kErrorTolerance)) { - return false; - } - if (!fltCmp(data1.ms_until_displayed, data2.ms_until_displayed, - kErrorTolerance)) { - return false; - } - if (!fltCmp(data1.ms_between_display_change, data2.ms_between_display_change, - kErrorTolerance)) { - return false; - } - if (!fltCmp(data1.ms_until_render_start, data2.ms_until_render_start, - kErrorTolerance)) { - return false; - } - if (data1.qpc_time != data2.qpc_time) { - return false; - } - - if (!fltCmp(data1.ms_since_input, data2.ms_since_input, kErrorTolerance)) { - return false; - } - if (!fltCmp(data1.ms_gpu_active, data2.ms_gpu_active, kErrorTolerance)) { - return false; - } - if (!fltCmp(data1.ms_gpu_video_active, data2.ms_gpu_video_active, kErrorTolerance)) { - return false; - } - - // Power Telemetry data - if (!CompareOptionalTelemetry(data1.gpu_power_w, data2.gpu_power_w)){ - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_sustained_power_limit_w, - data2.gpu_sustained_power_limit_w)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_voltage_v, data2.gpu_voltage_v)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_frequency_mhz, data2.gpu_frequency_mhz)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_temperature_c, data2.gpu_temperature_c)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_utilization, data2.gpu_utilization)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_render_compute_utilization, - data2.gpu_render_compute_utilization)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_media_utilization, data2.gpu_media_utilization)) { - return false; - } - if (!CompareOptionalTelemetry(data1.vram_power_w, data2.vram_power_w)) { - return false; - } - if (!CompareOptionalTelemetry(data1.vram_voltage_v, data2.vram_voltage_v)) { - return false; - } - if (!CompareOptionalTelemetry(data1.vram_frequency_mhz, data2.vram_frequency_mhz)) { - return false; - } - if (!CompareOptionalTelemetry(data1.vram_effective_frequency_gbs, - data2.vram_effective_frequency_gbs)) { - return false; - } - if (!CompareOptionalTelemetry(data1.vram_temperature_c, data2.vram_temperature_c)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_mem_total_size_b, data2.gpu_mem_total_size_b)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_mem_used_b, data2.gpu_mem_used_b)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_mem_max_bandwidth_bps, data2.gpu_mem_max_bandwidth_bps)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_mem_read_bandwidth_bps, - data2.gpu_mem_read_bandwidth_bps)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_mem_write_bandwidth_bps, - data2.gpu_mem_write_bandwidth_bps)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_power_limited, data2.gpu_power_limited)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_temperature_limited, data2.gpu_temperature_limited)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_current_limited, data2.gpu_current_limited)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_voltage_limited, data2.gpu_voltage_limited)) { - return false; - } - if (!CompareOptionalTelemetry(data1.gpu_utilization_limited, data2.gpu_utilization_limited)) { - return false; - } - if (!CompareOptionalTelemetry(data1.vram_power_limited, data2.vram_power_limited)) { - return false; - } - if (!CompareOptionalTelemetry(data1.vram_temperature_limited, data2.vram_temperature_limited)) { - return false; - } - if (!CompareOptionalTelemetry(data1.vram_current_limited, data2.vram_current_limited)) { - return false; - } - if (!CompareOptionalTelemetry(data1.vram_voltage_limited, data2.vram_voltage_limited)) { - return false; - } - if (!CompareOptionalTelemetry(data1.vram_utilization_limited, data2.vram_utilization_limited)) { - return false; - } - if (!CompareOptionalTelemetry(data1.cpu_utilization, data2.cpu_utilization)) { - return false; - } - if (!CompareOptionalTelemetry(data1.cpu_frequency, data2.cpu_frequency)) { - return false; - } - - return true; -} - -TEST_F(PresentMonApiULT, TestFpsMetrics) { - - PmFrameGenerator frame_gen{PmFrameGenerator::FrameParams{}}; - LOG(INFO) << "\nSeed used for TestFpsMetrics test: " - << frame_gen.GetSeed(); - frame_gen.SetFps(60.); - frame_gen.GenerateFrames(kNumFrames); - DWORD client_process_id = GetCurrentProcessId(); - std::string nsm_name; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - { - Streamer test_streamer; - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - for (int i = 0; i <= frame_gen.GetNumFrames(); i++) { - auto frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - pm_client.SetupClientCaches(kPid); - - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - std::vector calculated_fps_data; - frame_gen.CalculateFpsMetrics((uint32_t)frame_gen.GetNumFrames(), - kRunWindowSize, calculated_fps_data); - - PM_FPS_DATA fps_data{}; - uint32_t num_swap_chains = 1; - pm_client.GetFramesPerSecondData(kPid, &fps_data, kRunWindowSize, - &num_swap_chains); - EXPECT_TRUE(1 == num_swap_chains); - EXPECT_TRUE((num_swap_chains == (uint32_t)calculated_fps_data.size())); - - EXPECT_TRUE( - Equal(fps_data.presented_fps, calculated_fps_data[0].presented_fps)); - EXPECT_TRUE( - Equal(fps_data.displayed_fps, calculated_fps_data[0].displayed_fps)); - EXPECT_TRUE( - Equal(fps_data.frame_time_ms, calculated_fps_data[0].frame_time_ms)); - EXPECT_TRUE(Equal(fps_data.gpu_busy, calculated_fps_data[0].gpu_busy)); - EXPECT_TRUE(Equal(fps_data.percent_dropped_frames, - calculated_fps_data[0].percent_dropped_frames)); - - EXPECT_TRUE((fps_data.sync_interval == 0)); - EXPECT_TRUE(fps_data.present_mode == calculated_fps_data[0].present_mode); - - EXPECT_TRUE((fps_data.num_presents == calculated_fps_data[0].num_presents)); - } - - frame_gen.SetFps(300.); - frame_gen.GenerateFrames(kNumFrames); - { - Streamer test_streamer; - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - for (int i = 0; i <= frame_gen.GetNumFrames(); i++) { - auto frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - pm_client.SetupClientCaches(kPid); - - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - std::vector calculated_fps_data; - frame_gen.CalculateFpsMetrics((uint32_t)frame_gen.GetNumFrames(), - kRunWindowSize, calculated_fps_data); - - PM_FPS_DATA fps_data{}; - uint32_t num_swap_chains = 1; - pm_client.GetFramesPerSecondData(kPid, &fps_data, kRunWindowSize, - &num_swap_chains); - EXPECT_TRUE(1 == num_swap_chains); - EXPECT_TRUE((num_swap_chains == (uint32_t)calculated_fps_data.size())); - - EXPECT_TRUE( - Equal(fps_data.presented_fps, calculated_fps_data[0].presented_fps)); - EXPECT_TRUE( - Equal(fps_data.displayed_fps, calculated_fps_data[0].displayed_fps)); - EXPECT_TRUE( - Equal(fps_data.frame_time_ms, calculated_fps_data[0].frame_time_ms)); - - EXPECT_TRUE(Equal(fps_data.gpu_busy, calculated_fps_data[0].gpu_busy)); - EXPECT_TRUE(Equal(fps_data.percent_dropped_frames, - calculated_fps_data[0].percent_dropped_frames)); - - EXPECT_TRUE((fps_data.sync_interval == 0)); - EXPECT_TRUE(fps_data.present_mode == calculated_fps_data[0].present_mode); - - EXPECT_TRUE((fps_data.num_presents == calculated_fps_data[0].num_presents)); - } -} - -TEST_F(PresentMonApiULT, TestFpsMetricsMultiSwapChains) { - PmFrameGenerator frame_gen{PmFrameGenerator::FrameParams{}}; - LOG(INFO) << "\nSeed used for TestFpsMetricsMultiSwapChains test: " - << frame_gen.GetSeed(); - frame_gen.SetFps(180.); - frame_gen.SetNumberSwapChains(3); - frame_gen.GenerateFrames(kNumFrames); - - DWORD client_process_id = GetCurrentProcessId(); - std::string nsm_name; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - Streamer test_streamer; - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - for (int i = 0; i <= frame_gen.GetNumFrames(); i++) { - auto frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - pm_client.SetupClientCaches(kPid); - - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - std::vector calculated_fps_data; - frame_gen.CalculateFpsMetrics((uint32_t)frame_gen.GetNumFrames(), - kRunWindowSize, calculated_fps_data); - - PM_FPS_DATA fps_data[3]{}; - uint32_t num_swap_chains = 3; - pm_client.GetFramesPerSecondData(kPid, fps_data, kRunWindowSize, - &num_swap_chains); - EXPECT_TRUE(3 == num_swap_chains); - EXPECT_TRUE((num_swap_chains == (uint32_t)calculated_fps_data.size())); - - for (uint32_t i = 0; i < num_swap_chains; i++) { - EXPECT_TRUE( - Equal(fps_data[i].presented_fps, calculated_fps_data[i].presented_fps)); - EXPECT_TRUE( - Equal(fps_data[i].displayed_fps, calculated_fps_data[i].displayed_fps)); - EXPECT_TRUE( - Equal(fps_data[i].frame_time_ms, calculated_fps_data[i].frame_time_ms)); - EXPECT_TRUE(Equal(fps_data[i].gpu_busy, calculated_fps_data[i].gpu_busy)); - EXPECT_TRUE(Equal(fps_data[i].percent_dropped_frames, - calculated_fps_data[i].percent_dropped_frames)); - - EXPECT_TRUE((fps_data[i].sync_interval == 0)); - EXPECT_TRUE(fps_data[i].present_mode == - calculated_fps_data[i].present_mode); - - EXPECT_TRUE( - (fps_data[i].num_presents == calculated_fps_data[i].num_presents)); - } -} - -TEST_F(PresentMonApiULT, TestGfxLatencyMultiSwapChains) { - PmFrameGenerator frame_gen{PmFrameGenerator::FrameParams{}}; - LOG(INFO) << "\nSeed used for GetFramesPerSecondData test: " - << frame_gen.GetSeed(); - frame_gen.SetFps(60.); - frame_gen.SetNumberSwapChains(4); - frame_gen.GenerateFrames(kNumFrames); - DWORD client_process_id = GetCurrentProcessId(); - std::string nsm_name; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - { - Streamer test_streamer; - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - for (int i = 0; i <= frame_gen.GetNumFrames(); i++) { - auto frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - pm_client.SetupClientCaches(kPid); - - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - std::vector calculated_latency_data; - frame_gen.CalculateLatencyMetrics((uint32_t)frame_gen.GetNumFrames(), - kRunWindowSize, calculated_latency_data); - - PM_GFX_LATENCY_DATA gfx_latency_data[4]{}; - uint32_t num_swap_chains = 4; - pm_client.GetGfxLatencyData(kPid, gfx_latency_data, kRunWindowSize, - &num_swap_chains); - EXPECT_TRUE(4 == num_swap_chains); - EXPECT_TRUE((num_swap_chains == (uint32_t)calculated_latency_data.size())); - - for (uint32_t i = 0; i < num_swap_chains; i++) { - EXPECT_TRUE(Equal(gfx_latency_data[i].display_latency_ms, - calculated_latency_data[i].display_latency_ms)); - EXPECT_TRUE(Equal(gfx_latency_data[i].render_latency_ms, - calculated_latency_data[i].render_latency_ms)); - } - } -} - -TEST_F(PresentMonApiULT, TestFpsDroppedFrames) { - PmFrameGenerator frame_gen{ - PmFrameGenerator::FrameParams{.percent_dropped = 50.}}; - LOG(INFO) << "\nSeed used for GetFramesPerSecondData test: " - << frame_gen.GetSeed(); - frame_gen.SetFps(302.); - frame_gen.GenerateFrames(kNumFrames); - DWORD client_process_id = GetCurrentProcessId(); - std::string nsm_name; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - { - Streamer test_streamer; - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - for (int i = 0; i <= frame_gen.GetNumFrames(); i++) { - auto frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - pm_client.SetupClientCaches(kPid); - - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - std::vector calculated_fps_data; - frame_gen.CalculateFpsMetrics((uint32_t)frame_gen.GetNumFrames(), - kRunWindowSize, calculated_fps_data); - - PM_FPS_DATA fps_data{}; - uint32_t num_swap_chains = 1; - pm_client.GetFramesPerSecondData(kPid, &fps_data, kRunWindowSize, - &num_swap_chains); - EXPECT_TRUE(1 == num_swap_chains); - EXPECT_TRUE((num_swap_chains == (uint32_t)calculated_fps_data.size())); - - EXPECT_TRUE( - Equal(fps_data.presented_fps, calculated_fps_data[0].presented_fps)); - EXPECT_TRUE( - Equal(fps_data.displayed_fps, calculated_fps_data[0].displayed_fps)); - EXPECT_TRUE( - Equal(fps_data.frame_time_ms, calculated_fps_data[0].frame_time_ms)); - EXPECT_TRUE(Equal(fps_data.gpu_busy, calculated_fps_data[0].gpu_busy)); - EXPECT_TRUE(Equal(fps_data.percent_dropped_frames, - calculated_fps_data[0].percent_dropped_frames)); - - EXPECT_TRUE((fps_data.sync_interval == 0)); - EXPECT_TRUE(fps_data.present_mode == calculated_fps_data[0].present_mode); - - EXPECT_TRUE((fps_data.num_presents == calculated_fps_data[0].num_presents)); - } -} - -TEST_F(PresentMonApiULT, TestGfxLatency) { - PmFrameGenerator frame_gen{PmFrameGenerator::FrameParams{}}; - LOG(INFO) << "\nSeed used for GetGfxLatencyData test: " - << frame_gen.GetSeed(); - frame_gen.GenerateFrames(kNumFrames); - - DWORD client_process_id = GetCurrentProcessId(); - std::string nsm_name; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - Streamer test_streamer; - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - for (int i = 0; i <= frame_gen.GetNumFrames(); i++) { - auto frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - pm_client.SetupClientCaches(kPid); - - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - std::vector calculated_latency_data; - frame_gen.CalculateLatencyMetrics((uint32_t)frame_gen.GetNumFrames(), - kRunWindowSize, - calculated_latency_data); - - PM_GFX_LATENCY_DATA gfx_latency_data{}; - uint32_t num_swap_chains = 1; - pm_client.GetGfxLatencyData(kPid, &gfx_latency_data, - kRunWindowSize, &num_swap_chains); - EXPECT_TRUE(1 == num_swap_chains); - EXPECT_TRUE(num_swap_chains == (uint32_t)calculated_latency_data.size()); - EXPECT_TRUE(Equal(gfx_latency_data.display_latency_ms, - calculated_latency_data[0].display_latency_ms)); - EXPECT_TRUE(Equal(gfx_latency_data.render_latency_ms, - calculated_latency_data[0].render_latency_ms)); -} - -TEST_F(PresentMonApiULT, TestGfxLatencyDroppedFrames) { - PmFrameGenerator frame_gen{PmFrameGenerator::FrameParams{.percent_dropped = 50.}}; - LOG(INFO) << "\nSeed used for GetFramesPerSecondData test: " - << frame_gen.GetSeed(); - frame_gen.SetFps(302.); - frame_gen.GenerateFrames(kNumFrames); - - DWORD client_process_id = GetCurrentProcessId(); - std::string nsm_name; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - Streamer test_streamer; - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - for (int i = 0; i <= frame_gen.GetNumFrames(); i++) { - auto frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - pm_client.SetupClientCaches(kPid); - - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - std::vector calculated_latency_data; - frame_gen.CalculateLatencyMetrics((uint32_t)frame_gen.GetNumFrames(), - kRunWindowSize, calculated_latency_data); - - PM_GFX_LATENCY_DATA gfx_latency_data{}; - uint32_t num_swap_chains = 1; - pm_client.GetGfxLatencyData(kPid, &gfx_latency_data, kRunWindowSize, - &num_swap_chains); - EXPECT_TRUE(1 == num_swap_chains); - EXPECT_TRUE(num_swap_chains == (uint32_t)calculated_latency_data.size()); - EXPECT_TRUE(Equal(gfx_latency_data.display_latency_ms, - calculated_latency_data[0].display_latency_ms)); - EXPECT_TRUE(Equal(gfx_latency_data.render_latency_ms, - calculated_latency_data[0].render_latency_ms)); -} - - -TEST_F(PresentMonApiULT, TestSingleEntryFpsPercentiles) { - PmFrameGenerator frame_gen{PmFrameGenerator::FrameParams{}}; - LOG(INFO) << "\nSeed used for TestSingleEntryFpsPercentiles test: " - << frame_gen.GetSeed(); - frame_gen.GenerateFrames(kNumFrames); - - DWORD client_process_id = GetCurrentProcessId(); - std::string nsm_name; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - Streamer test_streamer; - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - for (int i = 0; i <= frame_gen.GetNumFrames(); i++) { - auto frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - pm_client.SetupClientCaches(kPid); - - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - std::vector calculated_fps_data; - frame_gen.CalculateFpsMetrics((uint32_t)frame_gen.GetNumFrames(), 8, - calculated_fps_data); - - PM_FPS_DATA fps_data; - uint32_t num_swap_chains = 1; - pm_client.GetFramesPerSecondData(10, &fps_data, 8, &num_swap_chains); - EXPECT_TRUE(1 == num_swap_chains); - EXPECT_TRUE((num_swap_chains == (uint32_t)calculated_fps_data.size())); - - EXPECT_TRUE(fltCmp(fps_data.presented_fps.percentile_99, - calculated_fps_data[0].presented_fps.percentile_99, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(fps_data.presented_fps.percentile_95, - calculated_fps_data[0].presented_fps.percentile_95, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(fps_data.presented_fps.percentile_90, - calculated_fps_data[0].presented_fps.percentile_90, - kErrorTolerance)); - - EXPECT_TRUE(fltCmp(fps_data.displayed_fps.percentile_99, - calculated_fps_data[0].displayed_fps.percentile_99, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(fps_data.displayed_fps.percentile_95, - calculated_fps_data[0].displayed_fps.percentile_95, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(fps_data.displayed_fps.percentile_90, - calculated_fps_data[0].displayed_fps.percentile_90, - kErrorTolerance)); -} - -TEST_F(PresentMonApiULT, TestSingleEntryGfxLatencyPercentiles) { - PmFrameGenerator frame_gen{PmFrameGenerator::FrameParams{}}; - LOG(INFO) << "\nSeed used for TestSingleEntryLatencyPercentiles test: " - << frame_gen.GetSeed(); - frame_gen.GenerateFrames(20); - - DWORD client_process_id = GetCurrentProcessId(); - std::string nsm_name; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - Streamer test_streamer; - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - for (int i = 0; i <= frame_gen.GetNumFrames(); i++) { - auto frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - pm_client.SetupClientCaches(kPid); - - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - std::vector calculated_latency_data; - frame_gen.CalculateLatencyMetrics((uint32_t)frame_gen.GetNumFrames(), - 8, - calculated_latency_data); - - PM_GFX_LATENCY_DATA gfx_latency_data; - uint32_t num_swap_chains = 1; - pm_client.GetGfxLatencyData(kPid, &gfx_latency_data, 8, - &num_swap_chains); - EXPECT_EQ(1, num_swap_chains); - EXPECT_EQ(num_swap_chains, (uint32_t)calculated_latency_data.size()); - EXPECT_TRUE(fltCmp(gfx_latency_data.display_latency_ms.avg, - calculated_latency_data[0].display_latency_ms.avg, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(gfx_latency_data.display_latency_ms.percentile_99, - calculated_latency_data[0].display_latency_ms.percentile_99, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(gfx_latency_data.display_latency_ms.percentile_95, - calculated_latency_data[0].display_latency_ms.percentile_95, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(gfx_latency_data.display_latency_ms.percentile_90, - calculated_latency_data[0].display_latency_ms.percentile_90, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(gfx_latency_data.display_latency_ms.high, - calculated_latency_data[0].display_latency_ms.high, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(gfx_latency_data.display_latency_ms.low, - calculated_latency_data[0].display_latency_ms.low, - kErrorTolerance)); - - EXPECT_TRUE(fltCmp(gfx_latency_data.render_latency_ms.avg, - calculated_latency_data[0].render_latency_ms.avg, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(gfx_latency_data.render_latency_ms.percentile_99, - calculated_latency_data[0].render_latency_ms.percentile_99, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(gfx_latency_data.render_latency_ms.percentile_95, - calculated_latency_data[0].render_latency_ms.percentile_95, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(gfx_latency_data.render_latency_ms.percentile_90, - calculated_latency_data[0].render_latency_ms.percentile_90, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(gfx_latency_data.render_latency_ms.high, - calculated_latency_data[0].render_latency_ms.high, - kErrorTolerance)); - EXPECT_TRUE(fltCmp(gfx_latency_data.render_latency_ms.low, - calculated_latency_data[0].render_latency_ms.low, - kErrorTolerance)); -} - -TEST_F(PresentMonApiULT, TestGpuMetrics) { - PmFrameGenerator frame_gen{PmFrameGenerator::FrameParams{.gpu_power_limited_percent = 50.}}; - LOG(INFO) << "\nSeed used for TestGpuMetrics test: " << frame_gen.GetSeed(); - frame_gen.GenerateFrames(kNumFrames); - - DWORD client_process_id = GetCurrentProcessId(); - std::string nsm_name; - Streamer test_streamer; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - for (int i = 0; i <= frame_gen.GetNumFrames(); i++) { - auto frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - pm_client.SetupClientCaches(kPid); - - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - PM_GPU_DATA calculated_gpu_data{}; - frame_gen.CalculateGpuMetrics((uint32_t)frame_gen.GetNumFrames(), - kRunWindowSize, gpu_telemetry_cap_bits, - calculated_gpu_data); - - PM_GPU_DATA gfx_gpu_data{}; - pm_client.GetGpuData(kPid, &gfx_gpu_data, kRunWindowSize); - - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_power_w, gfx_gpu_data.gpu_power_w)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_sustained_power_limit_w, - gfx_gpu_data.gpu_sustained_power_limit_w)); - EXPECT_TRUE( - Equal(calculated_gpu_data.gpu_voltage_v, gfx_gpu_data.gpu_voltage_v)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_frequency_mhz, - gfx_gpu_data.gpu_frequency_mhz)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_temperature_c, - gfx_gpu_data.gpu_temperature_c)); - EXPECT_TRUE( - Equal(calculated_gpu_data.gpu_utilization, gfx_gpu_data.gpu_utilization)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_render_compute_utilization, - gfx_gpu_data.gpu_render_compute_utilization)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_media_utilization, - gfx_gpu_data.gpu_media_utilization)); - EXPECT_TRUE( - Equal(calculated_gpu_data.vram_power_w, gfx_gpu_data.vram_power_w)); - EXPECT_TRUE( - Equal(calculated_gpu_data.vram_voltage_v, gfx_gpu_data.vram_voltage_v)); - EXPECT_TRUE(Equal(calculated_gpu_data.vram_frequency_mhz, - gfx_gpu_data.vram_frequency_mhz)); - EXPECT_TRUE(Equal(calculated_gpu_data.vram_effective_frequency_gbps, - gfx_gpu_data.vram_effective_frequency_gbps)); - EXPECT_TRUE(Equal(calculated_gpu_data.vram_read_bandwidth_bps, - gfx_gpu_data.vram_read_bandwidth_bps)); - EXPECT_TRUE(Equal(calculated_gpu_data.vram_write_bandwidth_bps, - gfx_gpu_data.vram_write_bandwidth_bps)); - EXPECT_TRUE(Equal(calculated_gpu_data.vram_temperature_c, - gfx_gpu_data.vram_temperature_c)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_mem_total_size_b, - gfx_gpu_data.gpu_mem_total_size_b)); - EXPECT_TRUE( - Equal(calculated_gpu_data.gpu_mem_used_b, gfx_gpu_data.gpu_mem_used_b)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_mem_utilization, - gfx_gpu_data.gpu_mem_utilization)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_mem_max_bandwidth_bps, - gfx_gpu_data.gpu_mem_max_bandwidth_bps)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_mem_read_bandwidth_bps, - gfx_gpu_data.gpu_mem_read_bandwidth_bps)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_mem_write_bandwidth_bps, - gfx_gpu_data.gpu_mem_write_bandwidth_bps)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_power_limited, - gfx_gpu_data.gpu_power_limited)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_temperature_limited, - gfx_gpu_data.gpu_temperature_limited)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_current_limited, - gfx_gpu_data.gpu_current_limited)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_voltage_limited, - gfx_gpu_data.gpu_voltage_limited)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_utilization_limited, - gfx_gpu_data.gpu_utilization_limited)); - EXPECT_TRUE(Equal(calculated_gpu_data.vram_power_limited, - gfx_gpu_data.vram_power_limited)); - EXPECT_TRUE(Equal(calculated_gpu_data.vram_temperature_limited, - gfx_gpu_data.vram_temperature_limited)); - EXPECT_TRUE(Equal(calculated_gpu_data.vram_current_limited, - gfx_gpu_data.vram_current_limited)); - EXPECT_TRUE(Equal(calculated_gpu_data.vram_voltage_limited, - gfx_gpu_data.vram_voltage_limited)); - EXPECT_TRUE(Equal(calculated_gpu_data.vram_utilization_limited, - gfx_gpu_data.vram_utilization_limited)); - for (int i = 0; i < MAX_PM_FAN_COUNT; i++) { - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_fan_speed_rpm[i], - gfx_gpu_data.gpu_fan_speed_rpm[i])); - } -} - -TEST_F(PresentMonApiULT, TestTelemetryCapBits) { - PmFrameGenerator frame_gen{ - PmFrameGenerator::FrameParams{}}; - LOG(INFO) << "\nSeed used for TestTelemetryCapBits test: " << frame_gen.GetSeed(); - frame_gen.GenerateFrames(kNumFrames); - - DWORD client_process_id = GetCurrentProcessId(); - std::string nsm_name; - Streamer test_streamer; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - { - // Both the GPU and CPU metrics tests enable all of the telemetry bits. - // In this test only enable a single GPU and CPU telemetry item - gpu_telemetry_cap_bits.set( - static_cast(GpuTelemetryCapBits::gpu_utilization), true); - cpu_telemetry_cap_bits.set( - static_cast(CpuTelemetryCapBits::cpu_utilization), true); - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - for (int i = 0; i <= frame_gen.GetNumFrames(); i++) { - auto frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - pm_client.SetupClientCaches(kPid); - - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - PM_GPU_DATA calculated_gpu_data{}; - frame_gen.CalculateGpuMetrics((uint32_t)frame_gen.GetNumFrames(), - kRunWindowSize, gpu_telemetry_cap_bits, - calculated_gpu_data); - PM_GPU_DATA gfx_gpu_data{}; - pm_client.GetGpuData(kPid, &gfx_gpu_data, kRunWindowSize); - - // Check to make sure only GPU Utilization is valid, has the correct values - // and that all other telemetry items are not valid - EXPECT_EQ(gfx_gpu_data.gpu_utilization.valid, true); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_utilization, - gfx_gpu_data.gpu_utilization)); - EXPECT_EQ(gfx_gpu_data.gpu_power_w.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_sustained_power_limit_w.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_voltage_v.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_frequency_mhz.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_temperature_c.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_render_compute_utilization.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_media_utilization.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_power_w.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_voltage_v.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_frequency_mhz.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_effective_frequency_gbps.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_read_bandwidth_bps.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_write_bandwidth_bps.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_temperature_c.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_mem_total_size_b.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_mem_used_b.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_mem_utilization.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_mem_max_bandwidth_bps.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_mem_read_bandwidth_bps.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_mem_write_bandwidth_bps.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_power_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_temperature_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_current_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_voltage_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_utilization_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_power_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_temperature_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_current_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_voltage_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_utilization_limited.valid, false); - for (int i = 0; i < MAX_PM_FAN_COUNT; i++) { - EXPECT_EQ(gfx_gpu_data.gpu_fan_speed_rpm[i].valid, false); - } - // Now inspect CPU telemetry. Again a single telemetry should - // be valid (CPU Utilization). - PM_CPU_DATA calculated_cpu_data{}; - frame_gen.CalculateCpuMetrics((uint32_t)frame_gen.GetNumFrames(), - kRunWindowSize, cpu_telemetry_cap_bits, - calculated_cpu_data); - PM_CPU_DATA cpu_data{}; - pm_client.GetCpuData(kPid, &cpu_data, kRunWindowSize); - EXPECT_EQ(cpu_data.cpu_utilization.valid, true); - EXPECT_TRUE( - Equal(calculated_cpu_data.cpu_utilization, cpu_data.cpu_utilization)); - EXPECT_EQ(cpu_data.cpu_power_w.valid, false); - EXPECT_EQ(cpu_data.cpu_power_limit_w.valid, false); - EXPECT_EQ(cpu_data.cpu_temperature_c.valid, false); - EXPECT_EQ(cpu_data.cpu_frequency.valid, false); - } - - { - gpu_telemetry_cap_bits.reset(); - cpu_telemetry_cap_bits.reset(); - - gpu_telemetry_cap_bits.set( - static_cast(GpuTelemetryCapBits::gpu_utilization), true); - gpu_telemetry_cap_bits.set( - static_cast( - GpuTelemetryCapBits::gpu_render_compute_utilization), - true); - gpu_telemetry_cap_bits.set( - static_cast(GpuTelemetryCapBits::gpu_media_utilization), - true); - gpu_telemetry_cap_bits.set( - static_cast(GpuTelemetryCapBits::fan_speed_2), true); - gpu_telemetry_cap_bits.set( - static_cast(GpuTelemetryCapBits::vram_power_limited), true); - cpu_telemetry_cap_bits.set( - static_cast(CpuTelemetryCapBits::cpu_frequency), true); - - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - for (int i = 0; i <= frame_gen.GetNumFrames(); i++) { - auto frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - pm_client.SetupClientCaches(kPid); - - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - PM_GPU_DATA calculated_gpu_data{}; - frame_gen.CalculateGpuMetrics((uint32_t)frame_gen.GetNumFrames(), - kRunWindowSize, gpu_telemetry_cap_bits, - calculated_gpu_data); - PM_GPU_DATA gfx_gpu_data{}; - pm_client.GetGpuData(kPid, &gfx_gpu_data, kRunWindowSize); - - EXPECT_EQ(gfx_gpu_data.gpu_utilization.valid, true); - EXPECT_EQ(gfx_gpu_data.gpu_render_compute_utilization.valid, true); - EXPECT_EQ(gfx_gpu_data.gpu_media_utilization.valid, true); - EXPECT_EQ(gfx_gpu_data.gpu_fan_speed_rpm[2].valid, true); - EXPECT_EQ(gfx_gpu_data.vram_power_limited.valid, true); - - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_utilization, - gfx_gpu_data.gpu_utilization)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_render_compute_utilization, - gfx_gpu_data.gpu_render_compute_utilization)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_media_utilization, - gfx_gpu_data.gpu_media_utilization)); - EXPECT_TRUE(Equal(calculated_gpu_data.gpu_fan_speed_rpm[2], - gfx_gpu_data.gpu_fan_speed_rpm[2])); - EXPECT_TRUE(Equal(calculated_gpu_data.vram_power_limited, - gfx_gpu_data.vram_power_limited)); - - EXPECT_EQ(gfx_gpu_data.gpu_power_w.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_sustained_power_limit_w.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_voltage_v.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_frequency_mhz.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_temperature_c.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_power_w.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_voltage_v.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_frequency_mhz.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_effective_frequency_gbps.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_read_bandwidth_bps.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_write_bandwidth_bps.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_temperature_c.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_mem_total_size_b.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_mem_used_b.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_mem_utilization.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_mem_max_bandwidth_bps.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_mem_read_bandwidth_bps.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_mem_write_bandwidth_bps.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_power_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_temperature_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_current_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_voltage_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_utilization_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_temperature_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_current_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_voltage_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.vram_utilization_limited.valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_fan_speed_rpm[0].valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_fan_speed_rpm[1].valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_fan_speed_rpm[3].valid, false); - EXPECT_EQ(gfx_gpu_data.gpu_fan_speed_rpm[4].valid, false); - - // Now inspect CPU telemetry. Again a single telemetry should - // be valid (CPU Utilization). - PM_CPU_DATA calculated_cpu_data{}; - frame_gen.CalculateCpuMetrics((uint32_t)frame_gen.GetNumFrames(), - kRunWindowSize, cpu_telemetry_cap_bits, - calculated_cpu_data); - PM_CPU_DATA cpu_data{}; - pm_client.GetCpuData(kPid, &cpu_data, kRunWindowSize); - EXPECT_EQ(cpu_data.cpu_frequency.valid, true); - EXPECT_TRUE( - Equal(calculated_cpu_data.cpu_frequency, cpu_data.cpu_frequency)); - EXPECT_EQ(cpu_data.cpu_power_w.valid, false); - EXPECT_EQ(cpu_data.cpu_power_limit_w.valid, false); - EXPECT_EQ(cpu_data.cpu_temperature_c.valid, false); - EXPECT_EQ(cpu_data.cpu_utilization.valid, false); - } -} - - -TEST_F(PresentMonApiULT, TestCpuMetrics) { - PmFrameGenerator frame_gen{PmFrameGenerator::FrameParams{}}; - LOG(INFO) << "\nSeed used for TestCpuMetrics test: " << frame_gen.GetSeed(); - frame_gen.GenerateFrames(kNumFrames); - - DWORD client_process_id = GetCurrentProcessId(); - std::string nsm_name; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - Streamer test_streamer; - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - for (int i = 0; i <= frame_gen.GetNumFrames(); i++) { - auto frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - pm_client.SetupClientCaches(kPid); - - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - PM_CPU_DATA calculated_cpu_data{}; - frame_gen.CalculateCpuMetrics((uint32_t)frame_gen.GetNumFrames(), - kRunWindowSize, cpu_telemetry_cap_bits, - calculated_cpu_data); - - PM_CPU_DATA cpu_data{}; - pm_client.GetCpuData(kPid, &cpu_data, kRunWindowSize); - - EXPECT_TRUE(Equal(calculated_cpu_data.cpu_utilization, cpu_data.cpu_utilization)); - EXPECT_TRUE(Equal(calculated_cpu_data.cpu_frequency, cpu_data.cpu_frequency)); -} - -TEST_F(PresentMonApiULT, TestFrameCapture) { - PmFrameGenerator frame_gen{PmFrameGenerator::FrameParams{}}; - LOG(INFO) << "\nSeed used for TestFrameCapture test: " << frame_gen.GetSeed(); - frame_gen.GenerateFrames(kNumFrames); - - DWORD client_process_id = GetCurrentProcessId(); - std::string nsm_name; - - Streamer test_streamer; - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - auto frame_data = std::make_unique(kNumFrames); - uint32_t num_frames = kNumFrames; - auto pm_status = pm_client.GetFrameData(kPid, false, &num_frames, frame_data.get()); - EXPECT_TRUE(num_frames == 0); - EXPECT_TRUE(pm_status == PM_STATUS::PM_STATUS_NO_DATA); - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - - // Stream a single frame. - auto frame = frame_gen.GetFrameData(0); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - num_frames = kNumFrames; - // This first call initializes the frame data capture system on the pm - // client side. No frames will be captured on the first call - pm_status = pm_client.GetFrameData(kPid, false, &num_frames, frame_data.get()); - EXPECT_TRUE(num_frames == 0); - EXPECT_TRUE(pm_status == PM_STATUS::PM_STATUS_NO_DATA); - - // Stream the rest of the generated frames - for (int i = 1; i <= frame_gen.GetNumFrames(); i++) { - frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - num_frames = kNumFrames; - pm_status = pm_client.GetFrameData(kPid, false, &num_frames, frame_data.get()); - EXPECT_TRUE(num_frames == kNumFrames - 1); - EXPECT_TRUE(num_frames == (uint32_t)frame_gen.GetNumFrames()); - EXPECT_TRUE(pm_status == PM_STATUS::PM_STATUS_SUCCESS); - // Compare all recorded frames - for (uint32_t i = 0; i <= ((uint32_t)frame_gen.GetNumFrames() - 1); i++) { - EXPECT_TRUE(CompareFrameData( - frame_data[i], frame_gen.GetPmFrameData(i, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits))); - } - -} - -TEST_F(PresentMonApiULT, TestFrameCaptureCapBits) { - PmFrameGenerator frame_gen{PmFrameGenerator::FrameParams{}}; - LOG(INFO) << "\nSeed used for TestFrameCapture test: " << frame_gen.GetSeed(); - frame_gen.GenerateFrames(kNumFrames); - - DWORD client_process_id = GetCurrentProcessId(); - std::string nsm_name; - - Streamer test_streamer; - test_streamer.StartStreaming(client_process_id, kPid, nsm_name); - - PresentMonClient pm_client; - std::unique_ptr client = std::make_unique(); - client->Initialize(test_streamer.GetMapFileName(kPid)); - pm_client.clients_.emplace(kPid, std::move(client)); - EXPECT_NE(pm_client.clients_[kPid]->GetNamedSharedMemView(), nullptr); - - auto frame_data = std::make_unique(kNumFrames); - uint32_t num_frames = kNumFrames; - auto pm_status = - pm_client.GetFrameData(kPid, false, &num_frames, frame_data.get()); - EXPECT_TRUE(num_frames == 0); - EXPECT_TRUE(pm_status == PM_STATUS::PM_STATUS_NO_DATA); - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - // Both the GPU and CPU metrics tests enable all of the telemetry bits. - // In this test only enable a few metrics - gpu_telemetry_cap_bits.set( - static_cast(GpuTelemetryCapBits::gpu_power), true); - gpu_telemetry_cap_bits.set( - static_cast(GpuTelemetryCapBits::gpu_frequency), true); - gpu_telemetry_cap_bits.set( - static_cast(GpuTelemetryCapBits::gpu_utilization), true); - gpu_telemetry_cap_bits.set( - static_cast(GpuTelemetryCapBits::gpu_mem_size), true); - gpu_telemetry_cap_bits.set( - static_cast(GpuTelemetryCapBits::gpu_mem_used), true); - cpu_telemetry_cap_bits.set( - static_cast(CpuTelemetryCapBits::cpu_utilization), true); - - // Stream a single frame. - auto frame = frame_gen.GetFrameData(0); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - num_frames = kNumFrames; - // This first call initializes the frame data capture system on the pm - // client side. No frames will be captured on the first call - pm_status = - pm_client.GetFrameData(kPid, false, &num_frames, frame_data.get()); - EXPECT_TRUE(num_frames == 0); - EXPECT_TRUE(pm_status == PM_STATUS::PM_STATUS_NO_DATA); - - // Stream the rest of the generated frames - for (int i = 1; i <= frame_gen.GetNumFrames(); i++) { - frame = frame_gen.GetFrameData(i); - test_streamer.WriteFrameData(kPid, &frame, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - } - num_frames = kNumFrames; - pm_status = - pm_client.GetFrameData(kPid, false, &num_frames, frame_data.get()); - EXPECT_TRUE(num_frames == kNumFrames - 1); - EXPECT_TRUE(num_frames == (uint32_t)frame_gen.GetNumFrames()); - EXPECT_TRUE(pm_status == PM_STATUS::PM_STATUS_SUCCESS); - // Compare all recorded frames - for (uint32_t i = 0; i <= ((uint32_t)frame_gen.GetNumFrames() - 1); i++) { - EXPECT_TRUE(CompareFrameData( - frame_data[i], frame_gen.GetPmFrameData(i, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits))); - } -} \ No newline at end of file diff --git a/IntelPresentMon/ULT/PmFrameGenerator.cpp b/IntelPresentMon/ULT/PmFrameGenerator.cpp deleted file mode 100644 index 8ba91c173..000000000 --- a/IntelPresentMon/ULT/PmFrameGenerator.cpp +++ /dev/null @@ -1,1243 +0,0 @@ -#include "PmFrameGenerator.h" - -PmFrameGenerator::PmFrameGenerator(const FrameParams& frame_params) - : app_name_{frame_params.app_name.value_or("test_app")}, - process_id_{frame_params.process_id.value_or(10)}, - runtime_{frame_params.runtime.value_or(Runtime::D3D9)}, - sync_interval_{frame_params.sync_interval.value_or(0)}, - present_flags_{frame_params.present_flags.value_or(512)}, - percent_dropped_{frame_params.percent_dropped.value_or(0.)}, - in_present_api_ms_{frame_params.in_present_api_ms.value_or(1.3707)}, - in_present_api_variation_ms_{ - frame_params.in_present_api_variation_ms.value_or(1.3)}, - between_presents_ms_{frame_params.between_presents_ms.value_or(7.15)}, - between_presents_variation_ms_{ - frame_params.between_presents_variation_ms.value_or(0.15)}, - percent_tearing_{frame_params.percent_tearing.value_or(0.)}, - present_mode_{frame_params.present_mode.value_or( - PresentMode::Hardware_Independent_Flip)}, - until_render_complete_ms_{ - frame_params.until_render_complete_ms.value_or(6.2663)}, - until_render_complete_variation_ms_{ - frame_params.until_render_complete_variation_ms.value_or(0.)}, - until_displayed_ms_{frame_params.until_displayed_ms.value_or(11.35)}, - until_displayed_variation_ms_{ - frame_params.until_displayed_ms.value_or(0.2)}, - until_display_change_ms_{ - frame_params.until_display_change_ms.value_or(7.15)}, - until_display_change_variation_ms_{ - frame_params.until_display_change_variation_ms.value_or(0.15)}, - until_render_start_ms_{ - frame_params.until_render_start_ms.value_or(-10.96)}, - until_render_start_variation_ms_{ - frame_params.until_render_start_variation_ms.value_or(0.)}, - qpc_time_{frame_params.qpc_time.value_or(0)}, - since_input_ms_{frame_params.since_input_ms.value_or(15.2)}, - since_input_variation_ms_{frame_params.since_input_ms.value_or(3.)}, - gpu_active_ms_{frame_params.gpu_active_ms.value_or(7.05)}, - gpu_active_variation_ms_{ - frame_params.gpu_active_variation_ms.value_or(0.0)}, - gpu_video_active_ms_{frame_params.gpu_video_active_ms.value_or(7.05)}, - gpu_video_active_variation_ms_{ - frame_params.gpu_video_active_variation_ms.value_or(0.0)}, - gpu_power_w_{frame_params.gpu_power_w.value_or(135.34)}, - gpu_power_variation_w_{frame_params.gpu_power_variation_w.value_or(37.)}, - gpu_sustained_power_limit_w_{ - frame_params.gpu_sustained_power_limit_w.value_or(190.)}, - gpu_sustained_power_limit_variation_w_{ - frame_params.gpu_sustained_power_limit_variation_w.value_or(0.)}, - gpu_voltage_v_{frame_params.gpu_voltage_v.value_or(1.032)}, - gpu_voltage_variation_v_{ - frame_params.gpu_voltage_variation_v.value_or(0.)}, - gpu_frequency_mhz_{frame_params.gpu_frequency_mhz.value_or(2400.)}, - gpu_frequency_variation_mhz_{ - frame_params.gpu_frequency_variation_mhz.value_or(0.)}, - gpu_temp_c_{frame_params.gpu_temp_c.value_or(62.7)}, - gpu_temp_variation_c_{ - frame_params.gpu_frequency_variation_mhz.value_or(1.01)}, - gpu_util_percent_{frame_params.gpu_util_percent.value_or(98.3)}, - gpu_util_variation_percent_{ - frame_params.gpu_util_variation_percent.value_or(5.9)}, - gpu_render_compute_util_percent_{ - frame_params.gpu_render_compute_util_percent.value_or(96.4)}, - gpu_render_compute_util_variation_percent_{ - frame_params.gpu_render_compute_util_variation_percent.value_or(7.)}, - gpu_media_util_percent_{frame_params.gpu_media_util_percent.value_or(0.)}, - gpu_media_util_variation_percent_{ - frame_params.gpu_media_util_variation_percent.value_or(0.)}, - vram_power_w_{frame_params.vram_power_w.value_or(0.)}, - vram_power_variation_w_{frame_params.vram_power_variation_w.value_or(0.)}, - vram_voltage_v_{frame_params.vram_voltage_v.value_or(0.)}, - vram_voltage_variation_v_{ - frame_params.vram_voltage_variation_v.value_or(0.)}, - vram_frequency_mhz_{frame_params.vram_frequency_mhz.value_or(2000.)}, - vram_frequency_variation_mhz_{ - frame_params.vram_frequency_variation_mhz.value_or(0.)}, - vram_effective_frequency_gbps_{ - frame_params.vram_effective_frequency_gbps.value_or(16000.)}, - vram_effective_frequency_variation_gbps_{ - frame_params.vram_effective_frequency_variation_gbps.value_or(0.)}, - vram_temp_c_{frame_params.vram_temp_c.value_or(71.)}, - vram_temp_variation_c_{frame_params.vram_temp_variation_c.value_or(1.2)}, - gpu_mem_total_size_b_{ - frame_params.gpu_mem_total_size_b.value_or(8589934592)}, - gpu_mem_total_size_variation_b_{ - frame_params.gpu_mem_total_size_variation_b.value_or(0)}, - gpu_mem_used_b_{frame_params.gpu_mem_total_size_b.value_or(2192377540)}, - gpu_mem_used_variation_b_{ - frame_params.gpu_mem_total_size_variation_b.value_or(3016908)}, - gpu_mem_max_bw_gbps_{ - frame_params.gpu_mem_max_bw_gbps.value_or(512000000000)}, - gpu_mem_max_bw_variation_gbps_{ - frame_params.gpu_mem_max_bw_variation_gbps.value_or(0)}, - gpu_mem_read_bw_bps_{ - frame_params.gpu_mem_read_bw_bps.value_or(55754930711)}, - gpu_mem_read_bw_variation_bps_{ - frame_params.gpu_mem_read_bw_variation_bps.value_or(4938578793)}, - gpu_mem_write_bw_bps_{ - frame_params.gpu_mem_max_bw_gbps.value_or(35272691238)}, - gpu_mem_write_bw_variation_bps_{ - frame_params.gpu_mem_write_bw_variation_bps.value_or(3051695995)}, - gpu_fan_speed_rpm_{ - frame_params.gpu_fan_speed_rpm.value_or(1070.2)}, - gpu_fan_speed_rpm_variation_rpm_{ - frame_params.gpu_fan_speed_rpm.value_or(100.2)}, - gpu_power_limited_percent_{ - frame_params.gpu_power_limited_percent.value_or(0.)}, - gpu_temp_limited_percent_{ - frame_params.gpu_temp_limited_percent.value_or(0.)}, - gpu_current_limited_percent_{ - frame_params.gpu_current_limited_percent.value_or(0.)}, - gpu_voltage_limited_percent_{ - frame_params.gpu_voltage_limited_percent.value_or(0.)}, - gpu_util_limited_percent_{ - frame_params.gpu_util_limited_percent.value_or(0.)}, - vram_power_limited_percent_{ - frame_params.vram_power_limited_percent.value_or(0.)}, - vram_temp_limited_percent_{ - frame_params.vram_temp_limited_percent.value_or(0.)}, - vram_current_limited_percent_{ - frame_params.vram_current_limited_percent.value_or(0.)}, - vram_voltage_limited_percent_{ - frame_params.vram_voltage_limited_percent.value_or(0.)}, - vram_util_limited_percent_{ - frame_params.vram_util_limited_percent.value_or(0.)}, - cpu_util_percent_{frame_params.cpu_util_percent.value_or(19.4)}, - cpu_util_variation_percent_{ - frame_params.cpu_util_variation_percent.value_or(5.86)}, - cpu_frequency_mhz_{frame_params.cpu_frequency_mhz.value_or(4212.9)}, - cpu_frequency_variation_mhz_{ - frame_params.cpu_frequency_variation_mhz.value_or(1070.2)} { - - QueryPerformanceFrequency(&qpc_frequency_); - QueryPerformanceCounter(&start_qpc_); - - swap_chains_.resize(1); - swap_chains_[0] = (uint64_t)1; -} - -void PmFrameGenerator::GenerateFrames(int num_frames) { - frames_.clear(); - frames_.resize(num_frames); - pmft_frames_.clear(); - pmft_frames_.resize(num_frames); - GeneratePresentData(); - GenerateGPUData(); - GenerateCPUData(); - return; -} - -size_t PmFrameGenerator::GetNumFrames() { - if (frames_.size() > 0) { - return frames_.size() - 1; - } else { - return frames_.size(); - } -} - -PmNsmFrameData PmFrameGenerator::GetFrameData(int frame_num) { - PmNsmFrameData temp_frame{}; - if (frame_num >= 0 && frame_num < frames_.size()) { - temp_frame = frames_[frame_num]; - } - return temp_frame; -} - -PM_FRAME_DATA PmFrameGenerator::GetPmFrameData( - int frame_num, GpuTelemetryBitset gpu_telemetry_cap_bits, - CpuTelemetryBitset cpu_telemetry_cap_bits) { - PM_FRAME_DATA temp_frame{}; - if (frame_num >= 0 && - (frame_num < pmft_frames_.size() && (frame_num < frames_.size()))) { - std::string temp_string = frames_[frame_num].present_event.application; - if (temp_string.size() < sizeof(temp_frame.application)) { - temp_string.copy(temp_frame.application, sizeof(temp_frame.application)); - } - temp_frame.process_id = frames_[frame_num].present_event.ProcessId; - temp_frame.swap_chain_address = - frames_[frame_num].present_event.SwapChainAddress; - temp_string = RuntimeToString(frames_[frame_num].present_event.Runtime); - if (temp_string.size() < sizeof(temp_frame.runtime)) { - temp_string.copy(temp_frame.runtime, sizeof(temp_frame.runtime)); - } - temp_frame.sync_interval = frames_[frame_num].present_event.SyncInterval; - temp_frame.present_flags = frames_[frame_num].present_event.PresentFlags; - temp_frame.dropped = pmft_frames_[frame_num].dropped; - temp_frame.time_in_seconds = pmft_frames_[frame_num].time_in_seconds; - temp_frame.ms_in_present_api = pmft_frames_[frame_num].ms_in_present_api; - temp_frame.ms_between_presents = - pmft_frames_[frame_num].ms_between_presents; - temp_frame.allows_tearing = - frames_[frame_num].present_event.SupportsTearing; - temp_frame.present_mode = pmft_frames_[frame_num].present_mode; - temp_frame.ms_until_render_complete = - pmft_frames_[frame_num].ms_until_render_complete; - temp_frame.ms_until_displayed = pmft_frames_[frame_num].ms_until_displayed; - temp_frame.ms_between_display_change = - pmft_frames_[frame_num].ms_between_display_change; - temp_frame.ms_until_render_start = - pmft_frames_[frame_num].ms_until_render_start; - temp_frame.qpc_time = pmft_frames_[frame_num].qpc_time; - temp_frame.ms_since_input = pmft_frames_[frame_num].ms_until_input; - temp_frame.ms_gpu_active = pmft_frames_[frame_num].ms_gpu_active; - temp_frame.ms_gpu_video_active = pmft_frames_[frame_num].ms_gpu_video_active; - - // Copy power telemetry - temp_frame.gpu_power_w.data = frames_[frame_num].power_telemetry.gpu_power_w; - temp_frame.gpu_power_w.valid = gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_power)]; - - temp_frame.gpu_sustained_power_limit_w.data = - frames_[frame_num].power_telemetry.gpu_sustained_power_limit_w; - temp_frame.gpu_sustained_power_limit_w.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_sustained_power_limit)]; - - temp_frame.gpu_voltage_v.data = frames_[frame_num].power_telemetry.gpu_voltage_v; - temp_frame.gpu_voltage_v.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_voltage)]; - - temp_frame.gpu_frequency_mhz.data = - frames_[frame_num].power_telemetry.gpu_frequency_mhz; - temp_frame.gpu_frequency_mhz.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_frequency)]; - - temp_frame.gpu_temperature_c.data = - frames_[frame_num].power_telemetry.gpu_temperature_c; - temp_frame.gpu_temperature_c.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_temperature)]; - - temp_frame.gpu_utilization.data = - frames_[frame_num].power_telemetry.gpu_utilization; - temp_frame.gpu_utilization.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_utilization)]; - - temp_frame.gpu_render_compute_utilization.data = - frames_[frame_num].power_telemetry.gpu_render_compute_utilization; - temp_frame.gpu_render_compute_utilization.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_render_compute_utilization)]; - - temp_frame.gpu_media_utilization.data = - frames_[frame_num].power_telemetry.gpu_media_utilization; - temp_frame.gpu_media_utilization.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_media_utilization)]; - - temp_frame.vram_power_w.data = - frames_[frame_num].power_telemetry.vram_power_w; - temp_frame.vram_power_w.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_power)]; - - temp_frame.vram_voltage_v.data = - frames_[frame_num].power_telemetry.vram_voltage_v; - temp_frame.vram_voltage_v.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_voltage)]; - - temp_frame.vram_frequency_mhz.data = - frames_[frame_num].power_telemetry.vram_frequency_mhz; - temp_frame.vram_frequency_mhz.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_frequency)]; - - temp_frame.vram_effective_frequency_gbs.data = - frames_[frame_num].power_telemetry.vram_effective_frequency_gbps; - temp_frame.vram_effective_frequency_gbs.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_effective_frequency)]; - - temp_frame.vram_temperature_c.data = - frames_[frame_num].power_telemetry.vram_temperature_c; - temp_frame.vram_temperature_c.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_temperature)]; - - for (size_t i = 0; i < MAX_PM_FAN_COUNT; i++) { - temp_frame.fan_speed_rpm[i].data = - frames_[frame_num].power_telemetry.fan_speed_rpm[i]; - temp_frame.fan_speed_rpm[i].valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::fan_speed_0) + i]; - } - - for (uint32_t i = 0; i < MAX_PM_PSU_COUNT; i++) { - temp_frame.psu_type[i].data = - TranslatePsuType(frames_[frame_num].power_telemetry.psu[i].psu_type); - temp_frame.psu_type[i].valid = gpu_telemetry_cap_bits - [static_cast(GpuTelemetryCapBits::psu_info_0) + i]; - - temp_frame.psu_power[i].data = - frames_[frame_num].power_telemetry.psu[i].psu_power; - temp_frame.psu_power[i].valid = gpu_telemetry_cap_bits - [static_cast(GpuTelemetryCapBits::psu_info_0) + i]; - - temp_frame.psu_voltage[i].data = - frames_[frame_num].power_telemetry.psu[i].psu_voltage; - temp_frame.psu_voltage[i].valid = gpu_telemetry_cap_bits - [static_cast(GpuTelemetryCapBits::psu_info_0) + i]; - } - - temp_frame.gpu_mem_total_size_b.data = - frames_[frame_num].power_telemetry.gpu_mem_total_size_b; - temp_frame.gpu_mem_total_size_b.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_size)]; - - temp_frame.gpu_mem_used_b.data = - frames_[frame_num].power_telemetry.gpu_mem_used_b; - temp_frame.gpu_mem_used_b.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_used)]; - - temp_frame.gpu_mem_max_bandwidth_bps.data = - frames_[frame_num].power_telemetry.gpu_mem_max_bandwidth_bps; - temp_frame.gpu_mem_max_bandwidth_bps.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_max_bandwidth)]; - - temp_frame.gpu_mem_read_bandwidth_bps.data = - frames_[frame_num].power_telemetry.gpu_mem_read_bandwidth_bps; - temp_frame.gpu_mem_read_bandwidth_bps.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_read_bandwidth)]; - - temp_frame.gpu_mem_write_bandwidth_bps.data = - frames_[frame_num].power_telemetry.gpu_mem_write_bandwidth_bps; - temp_frame.gpu_mem_write_bandwidth_bps.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_write_bandwidth)]; - - temp_frame.gpu_power_limited.data = - frames_[frame_num].power_telemetry.gpu_power_limited; - temp_frame.gpu_power_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_power_limited)]; - - temp_frame.gpu_temperature_limited.data = - frames_[frame_num].power_telemetry.gpu_temperature_limited; - temp_frame.gpu_temperature_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_temperature_limited)]; - - temp_frame.gpu_current_limited.data = - frames_[frame_num].power_telemetry.gpu_current_limited; - temp_frame.gpu_current_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_current_limited)]; - - temp_frame.gpu_voltage_limited.data = - frames_[frame_num].power_telemetry.gpu_voltage_limited; - temp_frame.gpu_voltage_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_voltage_limited)]; - - temp_frame.gpu_utilization_limited.data = - frames_[frame_num].power_telemetry.gpu_utilization_limited; - temp_frame.gpu_utilization_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_utilization_limited)]; - - temp_frame.vram_power_limited.data = - frames_[frame_num].power_telemetry.vram_power_limited; - temp_frame.vram_power_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_power_limited)]; - - temp_frame.vram_temperature_limited.data = - frames_[frame_num].power_telemetry.vram_temperature_limited; - temp_frame.vram_temperature_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_temperature_limited)]; - - temp_frame.vram_current_limited.data = - frames_[frame_num].power_telemetry.vram_current_limited; - temp_frame.vram_current_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_current_limited)]; - - temp_frame.vram_voltage_limited.data = - frames_[frame_num].power_telemetry.vram_voltage_limited; - temp_frame.vram_voltage_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_voltage_limited)]; - - temp_frame.vram_utilization_limited.data = - frames_[frame_num].power_telemetry.vram_utilization_limited; - temp_frame.vram_utilization_limited.valid = - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_utilization_limited)]; - - // Cpu telemetry - temp_frame.cpu_utilization.data = - frames_[frame_num].cpu_telemetry.cpu_utilization; - temp_frame.cpu_utilization.valid = - cpu_telemetry_cap_bits[static_cast( - CpuTelemetryCapBits::cpu_utilization)]; - - temp_frame.cpu_frequency.data = - frames_[frame_num].cpu_telemetry.cpu_frequency; - temp_frame.cpu_frequency.valid = - cpu_telemetry_cap_bits[static_cast( - CpuTelemetryCapBits::cpu_frequency)]; - } - return temp_frame; -} - -void PmFrameGenerator::GeneratePresentData() { - if (frames_.size() != pmft_frames_.size()) { - return; - } - - int swap_chain_idx = 0; - uint64_t last_displayed_screen_time = 0; - for (int i = 0; i < (int)frames_.size(); i++) { - if (app_name_.size() < sizeof(frames_[i].present_event.application)) { - app_name_.copy(frames_[i].present_event.application, - sizeof(frames_[i].present_event.application)); - } - frames_[i].present_event.ProcessId = process_id_; - frames_[i].present_event.SwapChainAddress = swap_chains_[swap_chain_idx]; - pmft_frames_[i].swap_chain = swap_chains_[swap_chain_idx]; - // Set the next swap chain index - swap_chain_idx += 1; - swap_chain_idx = swap_chain_idx % swap_chains_.size(); - frames_[i].present_event.Runtime = runtime_; - frames_[i].present_event.SyncInterval = sync_interval_; - frames_[i].present_event.PresentFlags = present_flags_; - // Using the specified percent dropped set the final state of the - // present - if (uniform_random_gen_.Generate(0., 100.) < percent_dropped_) { - frames_[i].present_event.FinalState = PresentResult::Discarded; - } else { - frames_[i].present_event.FinalState = PresentResult::Presented; - } - - frames_[i].present_event.PresentMode = present_mode_; - pmft_frames_[i].present_mode = TranslatePresentMode(present_mode_); - // Calculate the ms between presents - pmft_frames_[i].ms_between_presents = GetAlteredTimingValue( - between_presents_ms_, between_presents_variation_ms_); - // Convert ms between presents to qpc ticks - auto qpc_ticks_between_presents = SecondsDeltaToQpc( - pmft_frames_[i].ms_between_presents / 1000., qpc_frequency_); - // Using the ms between presents calculate the frame qpc times. - if (i == 0) { - // Set the first frame to the QPC of when the frame generator was - // initialized. - frames_[i].present_event.PresentStartTime = start_qpc_.QuadPart; - // Also on the first frame calculate a qpc for the last presented qpc - // using the created ms_between_presents and the start qpc. - frames_[i].present_event.last_present_qpc = - frames_[i].present_event.PresentStartTime - - qpc_ticks_between_presents; - - } else { - frames_[i].present_event.PresentStartTime = - frames_[i - 1].present_event.PresentStartTime + - qpc_ticks_between_presents; - frames_[i].present_event.last_present_qpc = - frames_[i - 1].present_event.PresentStartTime; - } - pmft_frames_[i].qpc_time = frames_[i].present_event.PresentStartTime; - pmft_frames_[i].time_in_seconds = QpcDeltaToSeconds( - pmft_frames_[i].qpc_time - pmft_frames_[0].qpc_time, qpc_frequency_); - - pmft_frames_[i].ms_in_present_api = - GetAlteredTimingValue( - in_present_api_ms_, in_present_api_variation_ms_); - frames_[i].present_event.TimeInPresent = - SecondsDeltaToQpc(pmft_frames_[i].ms_in_present_api / 1000., - qpc_frequency_); - pmft_frames_[i].ms_until_render_start = GetAlteredTimingValue( - until_render_start_ms_, until_render_start_variation_ms_); - if (pmft_frames_[i].ms_until_render_start > 0.) { - // If positive this means GPU start time occurred after present - // start. Convert the render start time from ms to qpc ticks - frames_[i].present_event.GPUStartTime = SecondsDeltaToQpc( - pmft_frames_[i].ms_until_render_start / 1000., qpc_frequency_); - // Add in the start time of the present to get the correct gpu - // start time - frames_[i].present_event.GPUStartTime = - frames_[i].present_event.PresentStartTime + - frames_[i].present_event.GPUStartTime; - } else { - // GPUStartTime is an unsigned int 64 so we need to convert to - // positive. - frames_[i].present_event.GPUStartTime = SecondsDeltaToQpc( - pmft_frames_[i].ms_until_render_start / -1000., qpc_frequency_); - // SUBTRACT the start time of the present to get the correct gpu - // start time - frames_[i].present_event.GPUStartTime = - frames_[i].present_event.PresentStartTime - - frames_[i].present_event.GPUStartTime; - } - - pmft_frames_[i].ms_until_render_complete = GetAlteredTimingValue( - until_render_complete_ms_, until_render_complete_variation_ms_); - if (pmft_frames_[i].ms_until_render_complete > 0.0) { - frames_[i].present_event.ReadyTime = SecondsDeltaToQpc( - pmft_frames_[i].ms_until_render_complete / 1000., qpc_frequency_); - frames_[i].present_event.ReadyTime = - frames_[i].present_event.PresentStartTime + - frames_[i].present_event.ReadyTime; - } else { - // ReadyTime is also an unsigned int 64, same as above. - frames_[i].present_event.ReadyTime = SecondsDeltaToQpc( - pmft_frames_[i].ms_until_render_complete / -1000., qpc_frequency_); - frames_[i].present_event.ReadyTime = - frames_[i].present_event.PresentStartTime - - frames_[i].present_event.ReadyTime; - } - - if (frames_[i].present_event.FinalState == PresentResult::Presented) { - pmft_frames_[i].dropped = false; - pmft_frames_[i].ms_until_displayed = GetAlteredTimingValue( - until_displayed_ms_, until_displayed_variation_ms_); - frames_[i].present_event.ScreenTime = SecondsDeltaToQpc( - pmft_frames_[i].ms_until_displayed / 1000., qpc_frequency_); - frames_[i].present_event.ScreenTime += - frames_[i].present_event.PresentStartTime; - if (last_displayed_screen_time != 0) { - pmft_frames_[i].ms_between_display_change = QpcDeltaToMs( - frames_[i].present_event.ScreenTime - last_displayed_screen_time, - qpc_frequency_); - frames_[i].present_event.last_displayed_qpc = - last_displayed_screen_time; - - } else { - pmft_frames_[i].ms_between_display_change = 0.; - frames_[i].present_event.last_displayed_qpc = 0; - } - last_displayed_screen_time = frames_[i].present_event.ScreenTime; - } else { - pmft_frames_[i].ms_until_displayed = 0; - pmft_frames_[i].ms_between_display_change = 0.; - pmft_frames_[i].dropped = true; - frames_[i].present_event.ScreenTime = 0; - } - - // Internal frame data fields; Remove for public build - pmft_frames_[i].ms_until_input = GetAlteredTimingValue(since_input_ms_, - since_input_variation_ms_); - if (pmft_frames_[i].ms_until_input != 0.) { - frames_[i].present_event.InputTime = SecondsDeltaToQpc( - pmft_frames_[i].ms_until_input / 1000., qpc_frequency_); - frames_[i].present_event.InputTime = - frames_[i].present_event.PresentStartTime - - frames_[i].present_event.InputTime; - } else { - frames_[i].present_event.InputTime = 0; - } - pmft_frames_[i].ms_gpu_active = - GetAlteredTimingValue(gpu_active_ms_, gpu_active_variation_ms_); - frames_[i].present_event.GPUDuration = SecondsDeltaToQpc( - pmft_frames_[i].ms_gpu_active / 1000., qpc_frequency_); - pmft_frames_[i].ms_gpu_video_active = - GetAlteredTimingValue(gpu_video_active_ms_, gpu_video_active_variation_ms_); - frames_[i].present_event.GPUVideoDuration = SecondsDeltaToQpc( - pmft_frames_[i].ms_gpu_video_active / 1000., qpc_frequency_); - } -} - -void PmFrameGenerator::GenerateGPUData() { - for (int i = 0; i < (int)frames_.size(); i++) { - frames_[i].power_telemetry.gpu_power_w = - GetAlteredTimingValue(gpu_power_w_, gpu_power_variation_w_); - frames_[i].power_telemetry.gpu_sustained_power_limit_w = - GetAlteredTimingValue(gpu_sustained_power_limit_w_, - gpu_sustained_power_limit_variation_w_); - frames_[i].power_telemetry.gpu_voltage_v = - GetAlteredTimingValue(gpu_voltage_v_, gpu_voltage_variation_v_); - frames_[i].power_telemetry.gpu_frequency_mhz = - GetAlteredTimingValue( - gpu_frequency_mhz_, gpu_frequency_variation_mhz_); - frames_[i].power_telemetry.gpu_temperature_c = - GetAlteredTimingValue(gpu_temp_c_, gpu_temp_variation_c_); - frames_[i].power_telemetry.gpu_utilization = GetAlteredTimingValue( - gpu_util_percent_, gpu_util_variation_percent_); - frames_[i].power_telemetry.gpu_render_compute_utilization = - GetAlteredTimingValue(gpu_render_compute_util_percent_, - gpu_render_compute_util_variation_percent_); - frames_[i].power_telemetry.gpu_media_utilization = - GetAlteredTimingValue(gpu_media_util_percent_, - gpu_media_util_variation_percent_); - frames_[i].power_telemetry.vram_power_w = - GetAlteredTimingValue(vram_power_w_, vram_power_variation_w_); - frames_[i].power_telemetry.vram_voltage_v = - GetAlteredTimingValue(vram_voltage_v_, vram_voltage_variation_v_); - frames_[i].power_telemetry.vram_frequency_mhz = GetAlteredTimingValue( - vram_frequency_mhz_, vram_frequency_variation_mhz_); - frames_[i].power_telemetry.vram_effective_frequency_gbps = - GetAlteredTimingValue(vram_effective_frequency_gbps_, - vram_effective_frequency_variation_gbps_); - frames_[i].power_telemetry.vram_temperature_c = - GetAlteredTimingValue(vram_temp_c_, vram_temp_variation_c_); - frames_[i].power_telemetry.gpu_mem_total_size_b = GetAlteredTimingValue( - gpu_mem_total_size_b_, gpu_mem_total_size_variation_b_); - frames_[i].power_telemetry.gpu_mem_used_b = - GetAlteredTimingValue(gpu_mem_used_b_, gpu_mem_used_variation_b_); - frames_[i].power_telemetry.gpu_mem_max_bandwidth_bps = - GetAlteredTimingValue(gpu_mem_max_bw_gbps_, - gpu_mem_max_bw_variation_gbps_); - frames_[i].power_telemetry.gpu_mem_read_bandwidth_bps = - (double)GetAlteredTimingValue(gpu_mem_read_bw_bps_, - gpu_mem_read_bw_variation_bps_); - frames_[i].power_telemetry.gpu_mem_write_bandwidth_bps = - (double)GetAlteredTimingValue(gpu_mem_write_bw_bps_, - gpu_mem_write_bw_variation_bps_); - frames_[i].power_telemetry.fan_speed_rpm[0] = GetAlteredTimingValue( - gpu_fan_speed_rpm_, gpu_fan_speed_rpm_variation_rpm_); - frames_[i].power_telemetry.gpu_power_limited = - IsLimited(gpu_power_limited_percent_); - frames_[i].power_telemetry.gpu_temperature_limited = - IsLimited(gpu_util_limited_percent_); - frames_[i].power_telemetry.gpu_current_limited = - IsLimited(gpu_current_limited_percent_); - frames_[i].power_telemetry.gpu_voltage_limited = - IsLimited(gpu_voltage_limited_percent_); - frames_[i].power_telemetry.gpu_utilization_limited = - IsLimited(gpu_util_limited_percent_); - frames_[i].power_telemetry.vram_power_limited = - IsLimited(vram_power_limited_percent_); - frames_[i].power_telemetry.vram_temperature_limited = - IsLimited(vram_util_limited_percent_); - frames_[i].power_telemetry.vram_current_limited = - IsLimited(vram_current_limited_percent_); - frames_[i].power_telemetry.vram_voltage_limited = - IsLimited(vram_voltage_limited_percent_); - frames_[i].power_telemetry.vram_utilization_limited = - IsLimited(vram_util_limited_percent_); - } -} - -void PmFrameGenerator::GenerateCPUData() { - for (int i = 0; i < (int)frames_.size(); i++) { - frames_[i].cpu_telemetry.cpu_utilization = GetAlteredTimingValue( - cpu_util_percent_, cpu_util_variation_percent_); - frames_[i].cpu_telemetry.cpu_frequency = GetAlteredTimingValue( - cpu_frequency_mhz_, cpu_frequency_variation_mhz_); - } -} - -void PmFrameGenerator::CalcMetricStats(std::vector& data, - PM_METRIC_DOUBLE_DATA& metric, - bool valid) { - if (data.size() > 1) { - // Before sorting, grab the raw data. - size_t middle_index = data.size() / 2; - metric.raw = data[middle_index]; - std::sort(data.begin(), data.end()); - metric.low = data[0]; - metric.high = data[data.size() - 1]; - - int window_size = (int)data.size(); - auto sum = std::accumulate(data.begin(), data.end(), 0.); - if (sum != 0) { - metric.avg = sum / window_size; - } - - metric.percentile_90 = GetPercentile(data, 0.1); - metric.percentile_95 = GetPercentile(data, 0.05); - metric.percentile_99 = GetPercentile(data, 0.01); - } else if (data.size() == 1) { - metric.low = data[0]; - metric.high = data[0]; - metric.avg = data[0]; - metric.percentile_90 = data[0]; - metric.percentile_95 = data[0]; - metric.percentile_99 = data[0]; - } else { - metric.low = 0.; - metric.high = 0.; - metric.avg = 0.; - metric.percentile_90 = 0.; - metric.percentile_95 = 0.; - metric.percentile_99 = 0.; - } - metric.valid = valid; - return; -} - -// Calculate percentile using linear interpolation between the closet ranks -// method -double PmFrameGenerator::GetPercentile(std::vector& data, - double percentile) { - double integral_part_as_double; - double fractpart = - modf(((percentile * (static_cast(data.size() - 1))) + 1), - &integral_part_as_double); - - // Subtract off one from the integral_part as we are zero based and the - // calculation above is based one based - uint32_t integral_part = static_cast(integral_part_as_double) - 1; - uint32_t next_idx = integral_part + 1; - // Before we access the vector data ensure that our calculated index values - // are not out of range - if (integral_part < data.size() || next_idx < data.size()) { - return data[integral_part] + - (fractpart * (data[next_idx] - data[integral_part])); - } else { - return 0.0f; - } -} - -bool PmFrameGenerator::CalculateFpsMetrics( - uint32_t start_frame, double window_size_in_ms, - std::vector& fps_metrics) { - std::unordered_map swap_chain_data; - if (start_frame > pmft_frames_.size()) { - return false; - } - - // Get the qpc for the start frame - uint64_t start_frame_qpc = pmft_frames_[start_frame].qpc_time; - - // Calculate number of ticks based on the passed in window size - uint64_t window_size_in_ticks = - SecondsDeltaToQpc(window_size_in_ms / 1000., qpc_frequency_); - uint64_t calculated_end_frame_qpc = start_frame_qpc - window_size_in_ticks; - - for (uint32_t current_frame_number = start_frame; current_frame_number > 0; - current_frame_number--) { - auto result = swap_chain_data.emplace( - pmft_frames_[current_frame_number].swap_chain, fps_swap_chain_data()); - auto swap_chain = &result.first->second; - if (result.second) { - swap_chain->num_presents = 1; - // TODO Need to define sync interval in pmft structure - // swap_chain->sync_interval - swap_chain->present_mode = - pmft_frames_[current_frame_number].present_mode; - swap_chain->gpu_sum_ms.push_back( - pmft_frames_[current_frame_number].ms_gpu_active); - swap_chain->time_in_s = 0.; - swap_chain->cpu_n_time = - pmft_frames_[current_frame_number].time_in_seconds; - swap_chain->cpu_0_time = swap_chain->cpu_n_time; - if (pmft_frames_[current_frame_number].dropped == false) { - swap_chain->mLastDisplayedScreenTime = - pmft_frames_[current_frame_number].time_in_seconds + - (pmft_frames_[current_frame_number].ms_until_displayed / 1000.); - swap_chain->display_0_screen_time = swap_chain->mLastDisplayedScreenTime; - swap_chain->dropped.push_back(0); - } else { - swap_chain->dropped.push_back(1); - } - } else { - if (pmft_frames_[current_frame_number].qpc_time > - calculated_end_frame_qpc) { - swap_chain->num_presents++; - swap_chain->present_mode = - pmft_frames_[current_frame_number].present_mode; - swap_chain->gpu_sum_ms.push_back( - pmft_frames_[current_frame_number].ms_gpu_active); - swap_chain->time_in_s = - swap_chain->cpu_0_time - - pmft_frames_[current_frame_number].time_in_seconds; - swap_chain->cpu_0_time = - pmft_frames_[current_frame_number].time_in_seconds; - if (swap_chain->time_in_s != 0.) { - // Convert to ms and store frametime - swap_chain->frame_times_ms.push_back(swap_chain->time_in_s * 1000.); - swap_chain->presented_fps.push_back(1. / swap_chain->time_in_s); - } - if (pmft_frames_[current_frame_number].dropped == false) { - auto current_display_screen_time_s = - pmft_frames_[current_frame_number].time_in_seconds + - (pmft_frames_[current_frame_number].ms_until_displayed / 1000.); - if (swap_chain->display_0_screen_time != 0.) { - swap_chain->display_fps.push_back( - 1. / (swap_chain->display_0_screen_time - - current_display_screen_time_s)); - } else { - swap_chain->mLastDisplayedScreenTime = current_display_screen_time_s; - } - swap_chain->display_0_screen_time = current_display_screen_time_s; - swap_chain->dropped.push_back(0); - } else { - swap_chain->dropped.push_back(1); - } - } else { - break; - } - } - } - - fps_metrics.clear(); - PM_FPS_DATA temp_fps_data{}; - for (auto pair : swap_chain_data) { - temp_fps_data.swap_chain = pair.first; - auto swap_chain = pair.second; - CalcMetricStats(swap_chain.display_fps, temp_fps_data.displayed_fps); - CalcMetricStats(swap_chain.presented_fps, temp_fps_data.presented_fps); - CalcMetricStats(swap_chain.frame_times_ms, temp_fps_data.frame_time_ms); - CalcMetricStats(swap_chain.gpu_sum_ms, temp_fps_data.gpu_busy); - // Overwrite the average both the display and cpu average fps. - auto avg_fps = - swap_chain.mLastDisplayedScreenTime - swap_chain.display_0_screen_time; - avg_fps /= swap_chain.display_fps.size(); - avg_fps = 1. / avg_fps; - temp_fps_data.displayed_fps.avg = avg_fps; - avg_fps = swap_chain.cpu_n_time - swap_chain.cpu_0_time; - avg_fps /= swap_chain.presented_fps.size(); - avg_fps = 1. / avg_fps; - temp_fps_data.presented_fps.avg = avg_fps; - temp_fps_data.present_mode = swap_chain.present_mode; - temp_fps_data.num_presents = swap_chain.num_presents; - temp_fps_data.sync_interval = swap_chain.sync_interval; - CalcMetricStats(swap_chain.dropped, temp_fps_data.percent_dropped_frames); - temp_fps_data.percent_dropped_frames.avg *= 100.; - - fps_metrics.emplace_back(temp_fps_data); - temp_fps_data = {}; - } - return true; -} - -void PmFrameGenerator::CalculateLatencyMetrics( - uint32_t start_frame, double window_size_in_ms, - std::vector& latency_metrics) { - - std::unordered_map swap_chain_data; - - if (start_frame > pmft_frames_.size()) { - return; - } - - // Get the qpc for the start frame - uint64_t start_frame_qpc = pmft_frames_[start_frame].qpc_time; - - // Calculate number of ticks based on the passed in window size - uint64_t window_size_in_ticks = - SecondsDeltaToQpc(window_size_in_ms / 1000., qpc_frequency_); - uint64_t calculated_end_frame_qpc = start_frame_qpc - window_size_in_ticks; - - for (uint32_t current_frame_number = start_frame; current_frame_number > 0; - current_frame_number--) { - auto result = - swap_chain_data.emplace(pmft_frames_[current_frame_number].swap_chain, - latency_swap_chain_data()); - auto swap_chain = &result.first->second; - if (result.second) { - swap_chain->render_latency_ms.clear(); - swap_chain->display_latency_ms.clear(); - } else { - if (pmft_frames_[current_frame_number].qpc_time > - calculated_end_frame_qpc) { - if (pmft_frames_[current_frame_number].ms_between_display_change != - 0.) { - swap_chain->render_latency_ms.push_back( - pmft_frames_[current_frame_number].ms_until_displayed); - swap_chain->display_latency_ms.push_back( - pmft_frames_[current_frame_number].ms_until_displayed - - pmft_frames_[current_frame_number].ms_until_render_complete); - } - } else { - break; - } - } - } - - latency_metrics.clear(); - PM_GFX_LATENCY_DATA temp_latency_data{}; - for (auto pair : swap_chain_data) { - temp_latency_data.swap_chain = pair.first; - auto swap_chain = pair.second; - CalcMetricStats(swap_chain.render_latency_ms, - temp_latency_data.render_latency_ms); - CalcMetricStats(swap_chain.display_latency_ms, - temp_latency_data.display_latency_ms); - latency_metrics.emplace_back(temp_latency_data); - temp_latency_data = {}; - } - return; -} - -void PmFrameGenerator::CalculateGpuMetrics( - uint32_t start_frame, double window_size_in_ms, - GpuTelemetryBitset gpu_telemetry_cap_bits, PM_GPU_DATA& gpu_metrics) { - if (start_frame > pmft_frames_.size()) { - return; - } - - // Get the qpc for the start frame - uint64_t start_frame_qpc = pmft_frames_[start_frame].qpc_time; - - bool gpu_mem_util_enabled = (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_size)] && - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_used)]); - - // Calculate number of ticks based on the passed in window size - uint64_t window_size_in_ticks = - SecondsDeltaToQpc(window_size_in_ms / 1000., qpc_frequency_); - uint64_t calculated_end_frame_qpc = start_frame_qpc - window_size_in_ticks; - gpu_data calculated_gpu_metrics{}; - for (uint32_t current_frame_number = start_frame; current_frame_number > 0; - current_frame_number--) { - if (pmft_frames_[current_frame_number].qpc_time > - calculated_end_frame_qpc) { - if (gpu_telemetry_cap_bits[static_cast(GpuTelemetryCapBits::gpu_power)]) { - calculated_gpu_metrics.gpu_power_w.push_back( - frames_[current_frame_number].power_telemetry.gpu_power_w); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_sustained_power_limit)]) { - calculated_gpu_metrics.gpu_sustained_power_limit_w.push_back( - frames_[current_frame_number] - .power_telemetry.gpu_sustained_power_limit_w); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_voltage)]) { - calculated_gpu_metrics.gpu_voltage_v.push_back( - frames_[current_frame_number].power_telemetry.gpu_voltage_v); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_frequency)]) { - calculated_gpu_metrics.gpu_frequency_mhz.push_back( - frames_[current_frame_number].power_telemetry.gpu_frequency_mhz); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_temperature)]) { - calculated_gpu_metrics.gpu_temp_c.push_back( - frames_[current_frame_number].power_telemetry.gpu_temperature_c); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_utilization)]) { - calculated_gpu_metrics.gpu_util_percent.push_back( - frames_[current_frame_number].power_telemetry.gpu_utilization); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_render_compute_utilization)]) { - calculated_gpu_metrics.gpu_render_compute_util_percent.push_back( - frames_[current_frame_number] - .power_telemetry.gpu_render_compute_utilization); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_media_utilization)]) { - calculated_gpu_metrics.gpu_media_util_percent.push_back( - frames_[current_frame_number] - .power_telemetry.gpu_media_utilization); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_power)]) { - calculated_gpu_metrics.vram_power_w.push_back( - frames_[current_frame_number].power_telemetry.vram_power_w); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_voltage)]) { - calculated_gpu_metrics.vram_voltage_v.push_back( - frames_[current_frame_number].power_telemetry.vram_voltage_v); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_frequency)]) { - calculated_gpu_metrics.vram_frequency_mhz.push_back( - frames_[current_frame_number].power_telemetry.vram_frequency_mhz); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_effective_frequency)]) { - calculated_gpu_metrics.vram_effective_frequency_gbps.push_back( - frames_[current_frame_number] - .power_telemetry.vram_effective_frequency_gbps); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_temperature)]) { - calculated_gpu_metrics.vram_temp_c.push_back( - frames_[current_frame_number].power_telemetry.vram_temperature_c); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_size)]) { - calculated_gpu_metrics.gpu_mem_total_size_b.push_back( - (double)frames_[current_frame_number] - .power_telemetry.gpu_mem_total_size_b); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_used)]) { - calculated_gpu_metrics.gpu_mem_used_b.push_back( - (double)frames_[current_frame_number] - .power_telemetry.gpu_mem_used_b); - } - // gpu mem utilization is calculated from the total gpu memory - // and the used gpu memory - if (gpu_mem_util_enabled) { - if (frames_[current_frame_number] - .power_telemetry.gpu_mem_total_size_b != 0.) { - calculated_gpu_metrics.gpu_mem_util_percent.push_back( - 100. * - double(frames_[current_frame_number] - .power_telemetry.gpu_mem_used_b) / - frames_[current_frame_number] - .power_telemetry.gpu_mem_total_size_b); - } else { - calculated_gpu_metrics.gpu_mem_util_percent.push_back(0.); - } - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_max_bandwidth)]) { - calculated_gpu_metrics.gpu_mem_max_bw_gbps.push_back( - (double)frames_[current_frame_number] - .power_telemetry.gpu_mem_max_bandwidth_bps); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_read_bandwidth)]) { - calculated_gpu_metrics.gpu_mem_read_bw_bps.push_back( - frames_[current_frame_number] - .power_telemetry.gpu_mem_read_bandwidth_bps); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_write_bandwidth)]) { - calculated_gpu_metrics.gpu_mem_write_bw_bps.push_back( - frames_[current_frame_number] - .power_telemetry.gpu_mem_write_bandwidth_bps); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::fan_speed_0)]) { - calculated_gpu_metrics.gpu_fan_speed_rpm.push_back( - frames_[current_frame_number].power_telemetry.fan_speed_rpm[0]); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_power_limited)]) { - calculated_gpu_metrics.gpu_power_limited_percent.push_back( - frames_[current_frame_number].power_telemetry.gpu_power_limited); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_temperature_limited)]) { - calculated_gpu_metrics.gpu_temp_limited_percent.push_back( - frames_[current_frame_number] - .power_telemetry.gpu_temperature_limited); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_current_limited)]) { - calculated_gpu_metrics.gpu_current_limited_percent.push_back( - frames_[current_frame_number].power_telemetry.gpu_current_limited); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_voltage_limited)]) { - calculated_gpu_metrics.gpu_voltage_limited_percent.push_back( - frames_[current_frame_number].power_telemetry.gpu_voltage_limited); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_utilization_limited)]) { - calculated_gpu_metrics.gpu_util_limited_percent.push_back( - frames_[current_frame_number] - .power_telemetry.gpu_utilization_limited); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_power_limited)]) { - calculated_gpu_metrics.vram_power_limited_percent.push_back( - frames_[current_frame_number].power_telemetry.vram_power_limited); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_temperature_limited)]) { - calculated_gpu_metrics.vram_temp_limited_percent.push_back( - frames_[current_frame_number] - .power_telemetry.vram_temperature_limited); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_current_limited)]) { - calculated_gpu_metrics.vram_current_limited_percent.push_back( - frames_[current_frame_number].power_telemetry.vram_current_limited); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_voltage_limited)]) { - calculated_gpu_metrics.vram_voltage_limited_percent.push_back( - frames_[current_frame_number].power_telemetry.vram_voltage_limited); - } - if (gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_utilization_limited)]) { - calculated_gpu_metrics.vram_util_limited_percent.push_back( - frames_[current_frame_number] - .power_telemetry.vram_utilization_limited); - } - - } else { - break; - } - } - - CalcMetricStats(calculated_gpu_metrics.gpu_power_w, gpu_metrics.gpu_power_w, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_power)]); - CalcMetricStats(calculated_gpu_metrics.gpu_sustained_power_limit_w, - gpu_metrics.gpu_sustained_power_limit_w, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_sustained_power_limit)]); - CalcMetricStats(calculated_gpu_metrics.gpu_voltage_v, - gpu_metrics.gpu_voltage_v, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_voltage)]); - CalcMetricStats(calculated_gpu_metrics.gpu_frequency_mhz, - gpu_metrics.gpu_frequency_mhz, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_frequency)]); - CalcMetricStats(calculated_gpu_metrics.gpu_temp_c, - gpu_metrics.gpu_temperature_c, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_temperature)]); - CalcMetricStats(calculated_gpu_metrics.gpu_fan_speed_rpm, - gpu_metrics.gpu_fan_speed_rpm[0], - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::fan_speed_0)]); - CalcMetricStats(calculated_gpu_metrics.gpu_util_percent, - gpu_metrics.gpu_utilization, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_utilization)]); - CalcMetricStats(calculated_gpu_metrics.gpu_render_compute_util_percent, - gpu_metrics.gpu_render_compute_utilization, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_render_compute_utilization)]); - CalcMetricStats(calculated_gpu_metrics.gpu_media_util_percent, - gpu_metrics.gpu_media_utilization, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_media_utilization)]); - CalcMetricStats(calculated_gpu_metrics.vram_power_w, gpu_metrics.vram_power_w, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_power)]); - CalcMetricStats(calculated_gpu_metrics.vram_voltage_v, - gpu_metrics.vram_voltage_v, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_voltage)]); - CalcMetricStats(calculated_gpu_metrics.vram_frequency_mhz, - gpu_metrics.vram_frequency_mhz, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_frequency)]); - CalcMetricStats(calculated_gpu_metrics.vram_effective_frequency_gbps, - gpu_metrics.vram_effective_frequency_gbps, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_effective_frequency)]); - CalcMetricStats(calculated_gpu_metrics.vram_temp_c, - gpu_metrics.vram_temperature_c, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_temperature)]); - CalcMetricStats(calculated_gpu_metrics.gpu_mem_total_size_b, - gpu_metrics.gpu_mem_total_size_b, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_size)]); - CalcMetricStats(calculated_gpu_metrics.gpu_mem_used_b, - gpu_metrics.gpu_mem_used_b, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_used)]); - CalcMetricStats(calculated_gpu_metrics.gpu_mem_util_percent, - gpu_metrics.gpu_mem_utilization, gpu_mem_util_enabled); - CalcMetricStats(calculated_gpu_metrics.gpu_mem_max_bw_gbps, - gpu_metrics.gpu_mem_max_bandwidth_bps, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_max_bandwidth)]); - CalcMetricStats(calculated_gpu_metrics.gpu_mem_read_bw_bps, - gpu_metrics.gpu_mem_read_bandwidth_bps, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_max_bandwidth)]); - CalcMetricStats(calculated_gpu_metrics.gpu_mem_write_bw_bps, - gpu_metrics.gpu_mem_write_bandwidth_bps, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_mem_write_bandwidth)]); - CalcMetricStats(calculated_gpu_metrics.gpu_power_limited_percent, - gpu_metrics.gpu_power_limited, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_power_limited)]); - CalcMetricStats(calculated_gpu_metrics.gpu_temp_limited_percent, - gpu_metrics.gpu_temperature_limited, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_temperature_limited)]); - CalcMetricStats(calculated_gpu_metrics.gpu_current_limited_percent, - gpu_metrics.gpu_current_limited, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_current_limited)]); - CalcMetricStats(calculated_gpu_metrics.gpu_voltage_limited_percent, - gpu_metrics.gpu_voltage_limited, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_voltage_limited)]); - CalcMetricStats(calculated_gpu_metrics.gpu_util_limited_percent, - gpu_metrics.gpu_utilization_limited, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::gpu_utilization_limited)]); - CalcMetricStats(calculated_gpu_metrics.vram_power_limited_percent, - gpu_metrics.vram_power_limited, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_power_limited)]); - CalcMetricStats(calculated_gpu_metrics.vram_temp_limited_percent, - gpu_metrics.vram_temperature_limited, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_temperature_limited)]); - CalcMetricStats(calculated_gpu_metrics.vram_current_limited_percent, - gpu_metrics.vram_current_limited, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_current_limited)]); - CalcMetricStats(calculated_gpu_metrics.vram_voltage_limited_percent, - gpu_metrics.vram_voltage_limited, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_voltage_limited)]); - CalcMetricStats(calculated_gpu_metrics.vram_util_limited_percent, - gpu_metrics.vram_utilization_limited, - gpu_telemetry_cap_bits[static_cast( - GpuTelemetryCapBits::vram_utilization_limited)]); -} - -void PmFrameGenerator::CalculateCpuMetrics( - uint32_t start_frame, double window_size_in_ms, - CpuTelemetryBitset cpu_telemetry_cap_bits, - PM_CPU_DATA& cpu_metrics) { - if (start_frame > pmft_frames_.size()) { - return; - } - - // Get the qpc for the start frame - uint64_t start_frame_qpc = pmft_frames_[start_frame].qpc_time; - - // Calculate number of ticks based on the passed in window size - uint64_t window_size_in_ticks = - SecondsDeltaToQpc(window_size_in_ms / 1000., qpc_frequency_); - uint64_t calculated_end_frame_qpc = start_frame_qpc - window_size_in_ticks; - cpu_data calculated_cpu_metrics{}; - for (uint32_t current_frame_number = start_frame; current_frame_number > 0; - current_frame_number--) { - if (pmft_frames_[current_frame_number].qpc_time > - calculated_end_frame_qpc) { - if (cpu_telemetry_cap_bits[static_cast( - CpuTelemetryCapBits::cpu_utilization)]) { - calculated_cpu_metrics.cpu_util_percent.push_back( - frames_[current_frame_number].cpu_telemetry.cpu_utilization); - } - if (cpu_telemetry_cap_bits[static_cast( - CpuTelemetryCapBits::cpu_frequency)]) { - calculated_cpu_metrics.cpu_frequency_mhz.push_back( - frames_[current_frame_number].cpu_telemetry.cpu_frequency); - } - } - } - - CalcMetricStats(calculated_cpu_metrics.cpu_util_percent, - cpu_metrics.cpu_utilization, - cpu_telemetry_cap_bits[static_cast( - CpuTelemetryCapBits::cpu_utilization)]); - CalcMetricStats(calculated_cpu_metrics.cpu_frequency_mhz, - cpu_metrics.cpu_frequency, - cpu_telemetry_cap_bits[static_cast( - CpuTelemetryCapBits::cpu_frequency)]); -} \ No newline at end of file diff --git a/IntelPresentMon/ULT/PmFrameGenerator.h b/IntelPresentMon/ULT/PmFrameGenerator.h deleted file mode 100644 index 7bb5925f2..000000000 --- a/IntelPresentMon/ULT/PmFrameGenerator.h +++ /dev/null @@ -1,371 +0,0 @@ -#pragma once - -#include -#include - -#include "../PresentMonUtils/MemBuffer.h" -#include "../PresentMonUtils/PresentDataUtils.h" -#include "../PresentMonUtils/PresentMonNamedPipe.h" -#include "../PresentMonUtils/QPCUtils.h" - -struct PMFrameTimingInformation { - uint64_t swap_chain = 0; - bool dropped = false; - double time_in_seconds = 0.; - double ms_between_presents = 0.; - double ms_in_present_api = 0.; - double ms_until_render_start = 0.; - double ms_until_render_complete = 0.; - double ms_until_displayed = 0.; - double ms_between_display_change = 0.; - uint64_t qpc_time = 0; - PM_PRESENT_MODE present_mode = - PM_PRESENT_MODE::PM_PRESENT_MODE_HARDWARE_INDEPENDENT_FLIP; - double ms_until_input = 0.; - double ms_gpu_active = 0.; - double ms_gpu_video_active = 0.; -}; - -template -concept Numeric = (std::is_integral_v || - std::is_floating_point_v)&&!std::same_as; - -class PmFrameGenerator { - public: - struct FrameParams { - std::optional app_name; - std::optional process_id; - std::optional runtime; - std::optional sync_interval; - std::optional present_flags; - std::optional percent_dropped; - std::optional in_present_api_ms; - std::optional in_present_api_variation_ms; - std::optional between_presents_ms; - std::optional between_presents_variation_ms; - std::optional percent_tearing; - std::optional present_mode; - std::optional until_render_complete_ms; - std::optional until_render_complete_variation_ms; - std::optional until_displayed_ms; - std::optional until_displayed_variation_ms; - std::optional until_display_change_ms; - std::optional until_display_change_variation_ms; - std::optional until_render_start_ms; - std::optional until_render_start_variation_ms; - std::optional qpc_time; - - // Internal frame data fields; Remove for public build - std::optional since_input_ms; - std::optional since_input_variation_ms; - std::optional gpu_active_ms; - std::optional gpu_active_variation_ms; - std::optional gpu_video_active_ms; - std::optional gpu_video_active_variation_ms; - - std::optional gpu_power_w; - std::optional gpu_power_variation_w; - std::optional gpu_sustained_power_limit_w; - std::optional gpu_sustained_power_limit_variation_w; - std::optional gpu_voltage_v; - std::optional gpu_voltage_variation_v; - std::optional gpu_frequency_mhz; - std::optional gpu_frequency_variation_mhz; - std::optional gpu_temp_c; - std::optional gpu_temp_variation_c; - std::optional gpu_util_percent; - std::optional gpu_util_variation_percent; - std::optional gpu_render_compute_util_percent; - std::optional gpu_render_compute_util_variation_percent; - std::optional gpu_media_util_percent; - std::optional gpu_media_util_variation_percent; - std::optional vram_power_w; - std::optional vram_power_variation_w; - std::optional vram_voltage_v; - std::optional vram_voltage_variation_v; - std::optional vram_frequency_mhz; - std::optional vram_frequency_variation_mhz; - std::optional vram_effective_frequency_gbps; - std::optional vram_effective_frequency_variation_gbps; - std::optional vram_read_bw_gbps; - std::optional vram_read_bw_variation_gbps; - std::optional vram_write_bw_gbps; - std::optional vram_write_bw_variation_gbps; - std::optional vram_temp_c; - std::optional vram_temp_variation_c; - std::optional gpu_mem_total_size_b; - std::optional gpu_mem_total_size_variation_b; - std::optional gpu_mem_used_b; - std::optional gpu_mem_used_variation_b; - std::optional gpu_mem_max_bw_gbps; - std::optional gpu_mem_max_bw_variation_gbps; - std::optional gpu_mem_read_bw_bps; - std::optional gpu_mem_read_bw_variation_bps; - std::optional gpu_mem_write_bw_bps; - std::optional gpu_mem_write_bw_variation_bps; - std::optional gpu_fan_speed_rpm; - std::optional gpu_fan_speed_rpm_variation_rpm; - std::optional gpu_power_limited_percent; - std::optional gpu_temp_limited_percent; - std::optional gpu_current_limited_percent; - std::optional gpu_voltage_limited_percent; - std::optional gpu_util_limited_percent; - std::optional vram_power_limited_percent; - std::optional vram_temp_limited_percent; - std::optional vram_current_limited_percent; - std::optional vram_voltage_limited_percent; - std::optional vram_util_limited_percent; - std::optional cpu_util_percent; - std::optional cpu_util_variation_percent; - std::optional cpu_frequency_mhz; - std::optional cpu_frequency_variation_mhz; - }; - - PmFrameGenerator(const FrameParams& frame_params); - - void GenerateFrames(int num_frames); - size_t GetNumFrames(); - PmNsmFrameData GetFrameData(int frame_num); - PM_FRAME_DATA GetPmFrameData(int frame_num, - GpuTelemetryBitset gpu_telemetry_cap_bits, - CpuTelemetryBitset cpu_telemetry_cap_bits); - - // Calculate the Fps metrics using the PresentMon frame data and not - // the raw qpc data. - bool CalculateFpsMetrics(uint32_t start_frame, double window_size_in_ms, - std::vector& fps_metrics); - void CalculateLatencyMetrics( - uint32_t start_frame, double window_size_in_ms, - std::vector& latency_metrics); - void CalculateGpuMetrics(uint32_t start_frame, double window_size_in_ms, - GpuTelemetryBitset gpu_telemetry_cap_bits, - PM_GPU_DATA& gpu_metrics); - void CalculateCpuMetrics(uint32_t start_frame, double window_size_in_ms, - CpuTelemetryBitset cpu_telemetry_cap_bits, - PM_CPU_DATA& cpu_metrics); - - - // Simple function to set the number of swap chains to be created - // when generating // frame data. If set AFTER frame data generation it - // will have no affect on current data. Frames must be re-generated after - // setting the number of swapchains. When more than one swap chain is - // specified the swap chain will be divided as equal as possible among - // the frames. - void SetNumberSwapChains(uint32_t num_swap_chains) { - swap_chains_.clear(); - swap_chains_.resize(num_swap_chains); - for (uint32_t i = 0; i < swap_chains_.size(); i++) { - swap_chains_[i] = (uint64_t)i; - } - } - void SetFps(double fps) { between_presents_ms_ = 1000. / fps; }; - size_t GetNumberOfSwapChains() { return swap_chains_.size(); } - double GetWindowSize() { - return QpcDeltaToMs( - (frames_[frames_.size() - 1].present_event.PresentStartTime - - frames_[0].present_event.PresentStartTime), - qpc_frequency_); - } - uint32_t GetSeed() { return uniform_random_gen_.GetSeed(); } - - private: - class UniformRandomGenerator { - public: - UniformRandomGenerator(uint32_t seed = std::random_device{}()) - : seed_{seed}, engine_{seed} {} - template - T Generate(T low, T high) { - if constexpr (std::is_integral_v) { - return std::uniform_int_distribution{low, high}(engine_); - } else { - return std::uniform_real_distribution{low, high}(engine_); - } - } - uint32_t GetSeed() const { return seed_; } - - private: - std::mt19937 engine_; - uint32_t seed_; - }; // note: no need to provide virtual dtor; this is not a polymorphic type - // (no virtual functions) - - template - T GetAlteredTimingValue(T metric_in, T metric_variation_value) { - if constexpr (std::is_unsigned::value) { - return metric_in + - uniform_random_gen_.Generate((T)0, metric_variation_value); - } else { - return metric_in + - uniform_random_gen_.Generate(-1. * metric_variation_value, - metric_variation_value); - } - } - - struct fps_swap_chain_data { - std::vector presented_fps; - std::vector display_fps; - std::vector frame_times_ms; - std::vector gpu_sum_ms; - std::vector dropped; - double mLastDisplayedScreenTime = 0.; - double display_0_screen_time = 0.; - double cpu_n_time = 0.; - double cpu_0_time = 0.; - double time_in_s = 0.; - uint32_t num_presents = 0; - int32_t sync_interval = 0; - PM_PRESENT_MODE present_mode = PM_PRESENT_MODE_UNKNOWN; - }; - - struct latency_swap_chain_data { - std::vector render_latency_ms; - std::vector display_latency_ms; - }; - - struct gpu_data { - std::vector gpu_power_w; - std::vector gpu_sustained_power_limit_w; - std::vector gpu_voltage_v; - std::vector gpu_frequency_mhz; - std::vector gpu_temp_c; - std::vector gpu_util_percent; - std::vector gpu_render_compute_util_percent; - std::vector gpu_media_util_percent; - std::vector vram_power_w; - std::vector vram_voltage_v; - std::vector vram_frequency_mhz; - std::vector vram_effective_frequency_gbps; - std::vector vram_read_bw_gbps; - std::vector vram_write_bw_gbps; - std::vector vram_temp_c; - std::vector gpu_mem_total_size_b; - std::vector gpu_mem_used_b; - std::vector gpu_mem_util_percent; - std::vector gpu_mem_max_bw_gbps; - std::vector gpu_mem_read_bw_bps; - std::vector gpu_mem_write_bw_bps; - std::vector gpu_fan_speed_rpm; - std::vector gpu_power_limited_percent; - std::vector gpu_temp_limited_percent; - std::vector gpu_current_limited_percent; - std::vector gpu_voltage_limited_percent; - std::vector gpu_util_limited_percent; - std::vector vram_power_limited_percent; - std::vector vram_temp_limited_percent; - std::vector vram_current_limited_percent; - std::vector vram_voltage_limited_percent; - std::vector vram_util_limited_percent; - }; - - struct cpu_data { - std::vector cpu_util_percent; - std::vector cpu_frequency_mhz; - }; - - void GeneratePresentData(); - void GenerateGPUData(); - void GenerateCPUData(); - - void CalcMetricStats(std::vector& data, - PM_METRIC_DOUBLE_DATA& metric, - bool valid=true); - - // Calculate percentile using linear interpolation between the closet ranks - // method - double GetPercentile(std::vector& data, double percentile); - bool IsLimited(const double& limited_percentage) { - if (uniform_random_gen_.Generate(0., 100.) < limited_percentage) { - return true; - } else { - return false; - } - } - - std::string app_name_; - std::vector swap_chains_; - uint32_t process_id_; - Runtime runtime_; - int32_t sync_interval_; - uint32_t present_flags_; - double percent_dropped_; - double in_present_api_ms_; - double in_present_api_variation_ms_; - double between_presents_ms_; - double between_presents_variation_ms_; - double percent_tearing_; - PresentMode present_mode_; - double until_render_complete_ms_; - double until_render_complete_variation_ms_; - double until_displayed_ms_; - double until_displayed_variation_ms_; - double until_display_change_ms_; - double until_display_change_variation_ms_; - double until_render_start_ms_; - double until_render_start_variation_ms_; - double qpc_time_; - double since_input_ms_; - double since_input_variation_ms_; - double gpu_active_ms_; - double gpu_active_variation_ms_; - double gpu_video_active_ms_; - double gpu_video_active_variation_ms_; - - double gpu_power_w_; - double gpu_power_variation_w_; - double gpu_sustained_power_limit_w_; - double gpu_sustained_power_limit_variation_w_; - double gpu_voltage_v_; - double gpu_voltage_variation_v_; - double gpu_frequency_mhz_; - double gpu_frequency_variation_mhz_; - double gpu_temp_c_; - double gpu_temp_variation_c_; - double gpu_util_percent_; - double gpu_util_variation_percent_; - double gpu_render_compute_util_percent_; - double gpu_render_compute_util_variation_percent_; - double gpu_media_util_percent_; - double gpu_media_util_variation_percent_; - double vram_power_w_; - double vram_power_variation_w_; - double vram_voltage_v_; - double vram_voltage_variation_v_; - double vram_frequency_mhz_; - double vram_frequency_variation_mhz_; - double vram_effective_frequency_gbps_; - double vram_effective_frequency_variation_gbps_; - double vram_temp_c_; - double vram_temp_variation_c_; - uint64_t gpu_mem_total_size_b_; - uint64_t gpu_mem_total_size_variation_b_; - uint64_t gpu_mem_used_b_; - uint64_t gpu_mem_used_variation_b_; - uint64_t gpu_mem_max_bw_gbps_; - uint64_t gpu_mem_max_bw_variation_gbps_; - uint64_t gpu_mem_read_bw_bps_; - uint64_t gpu_mem_read_bw_variation_bps_; - uint64_t gpu_mem_write_bw_bps_; - uint64_t gpu_mem_write_bw_variation_bps_; - double gpu_fan_speed_rpm_; - double gpu_fan_speed_rpm_variation_rpm_; - double gpu_power_limited_percent_; - double gpu_temp_limited_percent_; - double gpu_current_limited_percent_; - double gpu_voltage_limited_percent_; - double gpu_util_limited_percent_; - double vram_power_limited_percent_; - double vram_temp_limited_percent_; - double vram_current_limited_percent_; - double vram_voltage_limited_percent_; - double vram_util_limited_percent_; - double cpu_util_percent_; - double cpu_util_variation_percent_; - double cpu_frequency_mhz_; - double cpu_frequency_variation_mhz_; - LARGE_INTEGER qpc_frequency_; - LARGE_INTEGER start_qpc_; - - std::vector frames_; - std::vector pmft_frames_; - UniformRandomGenerator uniform_random_gen_; -}; \ No newline at end of file diff --git a/IntelPresentMon/ULT/StreamerTests.cpp b/IntelPresentMon/ULT/StreamerTests.cpp deleted file mode 100644 index 30979accb..000000000 --- a/IntelPresentMon/ULT/StreamerTests.cpp +++ /dev/null @@ -1,306 +0,0 @@ -#include "gtest/gtest.h" -#include "..\Streamer\Streamer.h" -#include "..\Streamer\StreamClient.h" -#include "utils.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../CommonUtilities/log/GlogShim.h" - -static const string kClientEXE = "SampleStreamerClient.exe"; -static const string kMapFileName = "Global\\MyFileMappingObject"; -static const string kSampleTestFile = "C:\\temp\\test.txt"; -static const uint32_t kServerUpdateIntervalInMs = 10; -static const uint32_t kClientReadIntervalsInMs = 20; -static const string kSamplePresentMonFile = "testdata\\hitman3_presentmon_2.log"; -static const string kSamplePresentMonFileSmall = "testdata\\hitman3_presentmon_1.log"; -static const uint32_t kLoopCount = 20; -static const uint32_t kClientLoopCount = 20; -static const string sample_test_data = "HITMAN3.exe,1166288,0x000000004474CBE0,DXGI,1,0,1,0.04376850000000,0.39320000000000,16.52640000000000,0,Composed: Flip,32.33330000000000,0.00000000000000,0.00000000000000,785981.74005040002521"; -static const uint32_t kNumFramesInBuf = 20; -static const uint64_t kNsmBufSize = 65535; -static const uint64_t kNsmBufSizeLarge = -1; - - -void ParsePresentMonCsvData(string line, PmNsmFrameData& data) { - const std::regex delimiter(","); // whitespace - std::sregex_token_iterator iter(line.begin(), line.end(), delimiter, -1); - std::sregex_token_iterator end; - - // Skipping Application - ++iter; - data.present_event.ProcessId = atoi(static_cast(*iter).c_str()); - ++iter; - data.present_event.SwapChainAddress = std::stoull(static_cast(*iter).c_str(), NULL, 16); - // Skipping Runtime - ++iter; - ++iter; - data.present_event.SyncInterval = atoi(static_cast(*iter).c_str()); - ++iter; - //PresentFlags - data.present_event.PresentFlags = atoi(static_cast(*iter).c_str()); -} - - -class StreamerULT : public ::testing::Test { -public: - void SetUp() override { - } - - void TearDown() override { - LOG(INFO) << "Tear down ..."; - } - - Streamer streamer_; - StreamClient client_; - - ~StreamerULT() { - } - - void ServerRead(string file_name); - bool reading_from_file_; -}; - - -void StreamerULT::ServerRead(string file_name) { - DWORD proc_id = GetCurrentProcessId(); - static const int kPathSize = 1024; - - string mapfile_name; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - - streamer_.StartStreaming(proc_id, proc_id, mapfile_name); - EXPECT_FALSE(mapfile_name.empty()); - - std::ifstream test_read_file; - test_read_file.open(file_name, std::fstream::in); - - - if (test_read_file.is_open()) { - LOG(INFO) << "\nFile successfully opened."; - - // skip the first line - string line; - getline(test_read_file, line); - - while (getline(test_read_file, line) && reading_from_file_) { - LOG(INFO) << "\nWriting data...\n"<< line << std::endl; - PmNsmFrameData data = { 0 }; - - ParsePresentMonCsvData(line, data); - - streamer_.WriteFrameData(proc_id, &data, - gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - LOG(INFO) << "\nspin for " << kServerUpdateIntervalInMs << " ms"; - std::this_thread::sleep_for(std::chrono::milliseconds(kServerUpdateIntervalInMs)); - } - } - else { - LOG(ERROR) << "\n Failed to open " << kSamplePresentMonFile; - } - - test_read_file.close(); -} - -TEST_F(StreamerULT, StartStreaming) { - DWORD proc_id = GetCurrentProcessId(); - auto status = pmStartStream(proc_id); - EXPECT_EQ(status, PM_STATUS::PM_STATUS_SERVICE_NOT_INITIALIZED); -} - -TEST_F(StreamerULT, StopStreaming) { - DWORD proc_id = GetCurrentProcessId(); - - string mapfile_name; - streamer_.StartStreaming(proc_id, proc_id, mapfile_name); - EXPECT_FALSE(mapfile_name.empty()); - - // Stop with the same proc_id again - streamer_.StopStreaming(proc_id); - mapfile_name = streamer_.GetMapFileName(proc_id); - // NSM should no longer exist - EXPECT_TRUE(mapfile_name.empty()); -} - -TEST_F(StreamerULT, ClientOpenMappedFile) { - std::thread serverthread(&StreamerULT::ServerRead, this, kSamplePresentMonFile); - std::this_thread::sleep_for(std::chrono::milliseconds(kClientReadIntervalsInMs)); - - STARTUPINFO si; - PROCESS_INFORMATION pi; - - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - ZeroMemory(&pi, sizeof(pi)); - - DWORD proc_id = GetCurrentProcessId(); - - LOG(INFO) << "\nStreamer map file name for pid(" << proc_id << "):" << streamer_.GetMapFileName(proc_id); - - string cmdline_str = kClientEXE + " "+ streamer_.GetMapFileName(proc_id); - - LOG(INFO) << "\nCmdLine to start client process: " << cmdline_str; - - LPSTR cmdLine = const_cast(cmdline_str.c_str()); - - // Start the child process. - if (!CreateProcess(NULL, // No module name (use command line) - cmdLine, // Command line - NULL, // Process handle not inheritable - NULL, // Thread handle not inheritable - FALSE, // Set handle inheritance to FALSE - 0, // No creation flags - NULL, // Use parent's environment block - NULL, // Use parent's starting directory - &si, // Pointer to STARTUPINFO structure - &pi) // Pointer to PROCESS_INFORMATION structure - ) - { - printf("CreateProcess failed (%lu).\n", GetLastError()); - return; - } - - - // Wait until child process exits. - WaitForSingleObject(pi.hProcess, INFINITE); - - // Close process and thread handles. - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); - - reading_from_file_ = false; - serverthread.join(); - - SUCCEED(); -} - - -TEST_F(StreamerULT, ServerWriteData) { - DWORD proc_id = GetCurrentProcessId(); - - string mapfile_name; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - - streamer_.StartStreaming(proc_id, proc_id, mapfile_name); - EXPECT_FALSE(mapfile_name.empty()); - PmNsmFrameData data = {}; - - ParsePresentMonCsvData(sample_test_data, data); - - streamer_.WriteFrameData(proc_id, &data, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - - std::this_thread::sleep_for(std::chrono::milliseconds(kClientReadIntervalsInMs)); - - StreamClient client(std::move(mapfile_name), false); - - PmNsmFrameData* client_read_data = nullptr; - client_read_data = client.ReadLatestFrame(); - EXPECT_NE(client_read_data, nullptr); - - EXPECT_EQ(client_read_data->present_event.ProcessId, data.present_event.ProcessId); - EXPECT_EQ(client_read_data->present_event.SwapChainAddress, data.present_event.SwapChainAddress); - EXPECT_EQ(client_read_data->present_event.SyncInterval, data.present_event.SyncInterval); - - SUCCEED(); -} - -TEST(NamedSharedMemoryTest, CreateNamedSharedMemory) { - DWORD proc_id = GetCurrentProcessId(); - EXPECT_NE(proc_id, 0); - - std::unique_ptr streamer = std::make_unique(); - streamer->CreateNamedSharedMemory(proc_id); - EXPECT_NE(streamer->process_shared_mem_map_.find(proc_id), - streamer->process_shared_mem_map_.end()); - string mapfilename = streamer->GetMapFileName(proc_id); - EXPECT_EQ(mapfilename, kGlobalPrefix + std::to_string(proc_id)); - - // Test standard etl case - streamer->SetStreamMode(StreamMode::kOfflineEtl); - streamer->CreateNamedSharedMemory(static_cast(StreamPidOverride::kEtlPid)); - mapfilename = streamer->GetMapFileName( - static_cast(StreamPidOverride::kEtlPid)); - - EXPECT_EQ(mapfilename, kGlobalPrefix + std::to_string(static_cast( - StreamPidOverride::kEtlPid))); -} - -TEST(NamedSharedMemoryTestCustomSize, CreateNamedSharedMemory) { - DWORD proc_id = GetCurrentProcessId(); - EXPECT_NE(proc_id, 0); - - std::unique_ptr streamer = std::make_unique(); - // Buf size 0 - streamer->CreateNamedSharedMemory(proc_id, 0); - EXPECT_EQ(streamer->process_shared_mem_map_.find(proc_id), - streamer->process_shared_mem_map_.end()); - - // Normal buf size - streamer->CreateNamedSharedMemory(proc_id, kNsmBufSize); - EXPECT_NE(streamer->process_shared_mem_map_.find(proc_id), - streamer->process_shared_mem_map_.end()); - string mapfilename = streamer->GetMapFileName(proc_id); - EXPECT_EQ(mapfilename, kGlobalPrefix + std::to_string(proc_id)); - streamer->StopAllStreams(); - - // Oversized buf size - streamer->CreateNamedSharedMemory(proc_id, kNsmBufSizeLarge); - EXPECT_EQ(streamer->process_shared_mem_map_.find(proc_id), - streamer->process_shared_mem_map_.end()); - mapfilename = streamer->GetMapFileName(proc_id); -} - -TEST_F(StreamerULT, ServerWriteDataOverflow) { - // There are enough data to ensure write overflow - ServerRead(kSamplePresentMonFileSmall); - DWORD proc_id = GetCurrentProcessId(); - - string mapfile_name = streamer_.GetMapFileName(proc_id); - EXPECT_FALSE(mapfile_name.empty()); - PmNsmFrameData data = {}; - GpuTelemetryBitset gpu_telemetry_cap_bits; - CpuTelemetryBitset cpu_telemetry_cap_bits; - gpu_telemetry_cap_bits.set(); - cpu_telemetry_cap_bits.set(); - - ParsePresentMonCsvData(sample_test_data, data); - - streamer_.WriteFrameData(proc_id, &data, gpu_telemetry_cap_bits, - cpu_telemetry_cap_bits); - - std::this_thread::sleep_for(std::chrono::milliseconds(kClientReadIntervalsInMs)); - - StreamClient client(std::move(mapfile_name), false); - - PmNsmFrameData* client_read_data = nullptr; - client_read_data = client.ReadLatestFrame(); - EXPECT_NE(client_read_data, nullptr); - - EXPECT_EQ(client_read_data->present_event.ProcessId, data.present_event.ProcessId); - EXPECT_EQ(client_read_data->present_event.SwapChainAddress, data.present_event.SwapChainAddress); - EXPECT_EQ(client_read_data->present_event.SyncInterval, data.present_event.SyncInterval); -} - -TEST_F(StreamerULT, ReadBeforeWrite) { - StreamClient client; - - PmNsmFrameData* client_read_data = nullptr; - client_read_data = client.ReadLatestFrame(); - EXPECT_EQ(client_read_data, nullptr); -} \ No newline at end of file diff --git a/IntelPresentMon/ULT/TelemetryHistory.cpp b/IntelPresentMon/ULT/TelemetryHistory.cpp deleted file mode 100644 index 136d618b3..000000000 --- a/IntelPresentMon/ULT/TelemetryHistory.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "gtest/gtest.h" -#include "../ControlLib/TelemetryHistory.h" -#include -#include -#include -#include - -TEST(TelemetryHistory, iterationEmpty) -{ - pwr::TelemetryHistory hist(5); - EXPECT_EQ(0, hist.end() - hist.begin()); -} - -TEST(TelemetryHistory, iterationPartial) -{ - pwr::TelemetryHistory hist(5); - hist.Push({ .qpc = 10 }); - hist.Push({ .qpc = 20 }); - hist.Push({ .qpc = 30 }); - EXPECT_EQ(3, hist.end() - hist.begin()); - EXPECT_EQ(10, (*(hist.begin() + 0)).qpc); - EXPECT_EQ(20, (hist.begin() += 1)->qpc); - EXPECT_EQ(30, hist.begin()[2].qpc); -} - -TEST(TelemetryHistory, iterationFull) -{ - pwr::TelemetryHistory hist(5); - hist.Push({ .qpc = 10 }); - hist.Push({ .qpc = 20 }); - hist.Push({ .qpc = 30 }); - hist.Push({ .qpc = 40 }); - hist.Push({ .qpc = 50 }); - EXPECT_EQ(5, hist.end() - hist.begin()); - - std::vector plucked; - std::transform(hist.begin(), hist.end(), std::back_inserter(plucked), - std::mem_fn(&PresentMonPowerTelemetryInfo::qpc)); - - std::vector expected{ 10, 20, 30, 40, 50 }; - EXPECT_TRUE(plucked == expected); -} - -TEST(TelemetryHistory, iterationExcursioned) -{ - pwr::TelemetryHistory hist(5); - hist.Push({ .qpc = 10 }); - hist.Push({ .qpc = 20 }); - hist.Push({ .qpc = 30 }); - hist.Push({ .qpc = 40 }); - hist.Push({ .qpc = 50 }); - hist.Push({ .qpc = 60 }); - hist.Push({ .qpc = 70 }); - EXPECT_EQ(5, hist.end() - hist.begin()); - - std::vector plucked; - std::transform(hist.begin(), hist.end(), std::back_inserter(plucked), - std::mem_fn(&PresentMonPowerTelemetryInfo::qpc)); - - std::vector expected{ 30, 40, 50, 60, 70 }; - EXPECT_TRUE(plucked == expected); -} - -TEST(TelemetryHistory, nearestInside) -{ - pwr::TelemetryHistory hist(5); - hist.Push({ .qpc = 10 }); - hist.Push({ .qpc = 20 }); - hist.Push({ .qpc = 30 }); - hist.Push({ .qpc = 40 }); - hist.Push({ .qpc = 50 }); - hist.Push({ .qpc = 60 }); - hist.Push({ .qpc = 70 }); - - const auto nearest = hist.GetNearest(50); - EXPECT_TRUE(bool(nearest)); - EXPECT_EQ(50, nearest->qpc); -} - -TEST(TelemetryHistory, nearestCloseLower) -{ - pwr::TelemetryHistory hist(5); - hist.Push({ .qpc = 10 }); - hist.Push({ .qpc = 20 }); - hist.Push({ .qpc = 30 }); - hist.Push({ .qpc = 40 }); - hist.Push({ .qpc = 50 }); - hist.Push({ .qpc = 60 }); - hist.Push({ .qpc = 70 }); - - const auto nearest = hist.GetNearest(33); - EXPECT_TRUE(bool(nearest)); - EXPECT_EQ(30, nearest->qpc); -} - -TEST(TelemetryHistory, nearestCloseHigher) -{ - pwr::TelemetryHistory hist(5); - hist.Push({ .qpc = 10 }); - hist.Push({ .qpc = 20 }); - hist.Push({ .qpc = 30 }); - hist.Push({ .qpc = 40 }); - hist.Push({ .qpc = 50 }); - hist.Push({ .qpc = 60 }); - hist.Push({ .qpc = 70 }); - - const auto nearest = hist.GetNearest(58); - EXPECT_TRUE(bool(nearest)); - EXPECT_EQ(60, nearest->qpc); -} \ No newline at end of file diff --git a/IntelPresentMon/ULT/ULT.vcxproj b/IntelPresentMon/ULT/ULT.vcxproj deleted file mode 100644 index 521134037..000000000 --- a/IntelPresentMon/ULT/ULT.vcxproj +++ /dev/null @@ -1,115 +0,0 @@ - - - - - Debug - x64 - - - Release - x64 - - - - 16.0 - Win32Proj - {f7d9e1cd-298d-465a-82d2-8778c860bc46} - ULT - PresentMonULT - 10.0 - - - - Application - true - v143 - MultiByte - - - Application - false - v143 - true - MultiByte - - - - - - - - - - - - - - - - - - - - - - - - - - Level3 - true - true - MultiThreadedDebug - stdcpplatest - - - Console - true - manual-link\gtest_main.lib;bcrypt.lib;shlwapi.lib;%(AdditionalDependencies) - - - - - Level3 - true - true - true - true - MultiThreaded - stdcpplatest - - - Console - true - true - true - manual-link\gtest_main.lib;bcrypt.lib;shlwapi.lib;%(AdditionalDependencies) - - - - - {3c39c9bc-0e85-42c0-894c-3561bb93e87f} - - - {66e9f6c5-28db-4218-81b9-31e0e146ecc0} - - - {bf43064b-01f0-4c69-91fb-c2122baf621d} - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/IntelPresentMon/ULT/ULT.vcxproj.filters b/IntelPresentMon/ULT/ULT.vcxproj.filters deleted file mode 100644 index 2f9d5dad3..000000000 --- a/IntelPresentMon/ULT/ULT.vcxproj.filters +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/IntelPresentMon/ULT/utils.cpp b/IntelPresentMon/ULT/utils.cpp deleted file mode 100644 index 1ba2bdb02..000000000 --- a/IntelPresentMon/ULT/utils.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "utils.h" - -#include - -string GetCurrentProcessName(){ - static const int kPathSize = 1024; - char currpath[kPathSize]; - string exename; - string exepath; - if (GetModuleFileNameA(NULL, currpath, kPathSize) > 0) { - exename = exepath = currpath; - size_t sep = exename.find_last_of("\\"); - if (sep != std::string::npos) { - exename = exename.substr(sep + 1, exename.size() - sep - 1); - exepath = exepath.substr(0, sep); - } - } - - return exename; -} diff --git a/IntelPresentMon/ULT/utils.h b/IntelPresentMon/ULT/utils.h deleted file mode 100644 index 542e65b2d..000000000 --- a/IntelPresentMon/ULT/utils.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once -#include - -using std::string; - -string GetCurrentProcessName(); \ No newline at end of file diff --git a/IntelPresentMon/UnitTests/FixedVectorTests.cpp b/IntelPresentMon/UnitTests/FixedVectorTests.cpp new file mode 100644 index 000000000..558073fdf --- /dev/null +++ b/IntelPresentMon/UnitTests/FixedVectorTests.cpp @@ -0,0 +1,312 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: MIT + +#include + +#include + +#include +#include +#include +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace UtilityTests +{ + struct ValueType + { + int value = 0; + ValueType() : value(7) {} + explicit ValueType(int v) : value(v) {} + bool operator==(const ValueType& other) const { return value == other.value; } + }; + + struct CountingType + { + static int Alive; + static int Constructions; + static int Destructions; + + int value = 0; + CountingType() : value(0) { ++Alive; ++Constructions; } + explicit CountingType(int v) : value(v) { ++Alive; ++Constructions; } + CountingType(const CountingType& other) : value(other.value) { ++Alive; ++Constructions; } + CountingType(CountingType&& other) noexcept : value(other.value) { other.value = -1; ++Alive; ++Constructions; } + CountingType& operator=(const CountingType& other) { value = other.value; return *this; } + CountingType& operator=(CountingType&& other) noexcept { value = other.value; other.value = -1; return *this; } + ~CountingType() { --Alive; ++Destructions; } + + static void Reset() + { + Alive = 0; + Constructions = 0; + Destructions = 0; + } + }; + + int CountingType::Alive = 0; + int CountingType::Constructions = 0; + int CountingType::Destructions = 0; + + struct CounterRange + { + struct Iterator + { + using iterator_concept = std::input_iterator_tag; + using iterator_category = std::input_iterator_tag; + using value_type = int; + using difference_type = std::ptrdiff_t; + + int value = 0; + int finish = 0; + + int operator*() const { return value; } + Iterator& operator++() { ++value; return *this; } + void operator++(int) { ++value; } + bool operator==(const Iterator& other) const { return value == other.value; } + bool operator!=(const Iterator& other) const { return value != other.value; } + }; + + int start = 0; + int finish = 0; + + Iterator begin() const { return Iterator{ start, finish }; } + Iterator end() const { return Iterator{ finish, finish }; } + }; + + TEST_CLASS(TestFixedVector) + { + public: + TEST_METHOD(DefaultState) + { + pmon::util::cnr::FixedVector vec; + Assert::IsTrue(vec.Empty()); + Assert::AreEqual(0u, vec.Size()); + Assert::AreEqual(4u, vec.Capacity()); + Assert::AreEqual(4u, vec.MaxSize()); + Assert::IsTrue(vec.begin() == vec.end()); + Assert::IsTrue(vec.CBegin() == vec.CEnd()); + Assert::IsTrue(vec.Data() != nullptr); + } + + TEST_METHOD(CountConstructors) + { + pmon::util::cnr::FixedVector vecDefault(3); + Assert::AreEqual(3u, vecDefault.Size()); + Assert::AreEqual(7, vecDefault[0].value); + Assert::AreEqual(7, vecDefault[1].value); + Assert::AreEqual(7, vecDefault[2].value); + + pmon::util::cnr::FixedVector vecFill(2, ValueType{ 9 }); + Assert::AreEqual(2u, vecFill.Size()); + Assert::AreEqual(9, vecFill[0].value); + Assert::AreEqual(9, vecFill[1].value); + } + + TEST_METHOD(InitializerListAndIteratorConstructors) + { + pmon::util::cnr::FixedVector initVec{ 1, 2, 3 }; + Assert::AreEqual(3u, initVec.Size()); + Assert::AreEqual(1, initVec[0]); + Assert::AreEqual(2, initVec[1]); + Assert::AreEqual(3, initVec[2]); + + std::array source{ 4, 5, 6 }; + pmon::util::cnr::FixedVector iterVec(source.begin(), source.end()); + Assert::AreEqual(3u, iterVec.Size()); + Assert::AreEqual(4, iterVec[0]); + Assert::AreEqual(5, iterVec[1]); + Assert::AreEqual(6, iterVec[2]); + } + + TEST_METHOD(FromRangeConstructorAndAssign) + { + std::vector source{ 7, 8, 9 }; + pmon::util::cnr::FixedVector rangeVec(std::from_range, source); + Assert::AreEqual(3u, rangeVec.Size()); + Assert::AreEqual(7, rangeVec[0]); + Assert::AreEqual(8, rangeVec[1]); + Assert::AreEqual(9, rangeVec[2]); + + pmon::util::cnr::FixedVector assigned; + assigned.Assign(std::from_range, source); + Assert::AreEqual(3u, assigned.Size()); + Assert::AreEqual(7, assigned[0]); + Assert::AreEqual(8, assigned[1]); + Assert::AreEqual(9, assigned[2]); + } + + TEST_METHOD(CopyAndMove) + { + pmon::util::cnr::FixedVector src{ 1, 2, 3 }; + pmon::util::cnr::FixedVector copy(src); + Assert::AreEqual(3u, copy.Size()); + Assert::AreEqual(1, copy[0]); + Assert::AreEqual(2, copy[1]); + Assert::AreEqual(3, copy[2]); + + pmon::util::cnr::FixedVector assigned; + assigned = src; + Assert::AreEqual(3u, assigned.Size()); + Assert::AreEqual(1, assigned[0]); + Assert::AreEqual(2, assigned[1]); + Assert::AreEqual(3, assigned[2]); + + pmon::util::cnr::FixedVector moved(std::move(src)); + Assert::AreEqual(3u, moved.Size()); + Assert::AreEqual(1, moved[0]); + Assert::AreEqual(2, moved[1]); + Assert::AreEqual(3, moved[2]); + Assert::IsTrue(src.Empty()); + + pmon::util::cnr::FixedVector moveAssign; + moveAssign = std::move(moved); + Assert::AreEqual(3u, moveAssign.Size()); + Assert::AreEqual(1, moveAssign[0]); + Assert::AreEqual(2, moveAssign[1]); + Assert::AreEqual(3, moveAssign[2]); + Assert::IsTrue(moved.Empty()); + } + + TEST_METHOD(PushPopEmplace) + { + pmon::util::cnr::FixedVector vec; + ValueType first{ 1 }; + vec.PushBack(first); + vec.PushBack(ValueType{ 2 }); + auto& ref = vec.EmplaceBack(3); + + Assert::AreEqual(3u, vec.Size()); + Assert::AreEqual(1, vec[0].value); + Assert::AreEqual(2, vec[1].value); + Assert::AreEqual(3, vec[2].value); + Assert::IsTrue(&ref == &vec.Back()); + + vec.PopBack(); + Assert::AreEqual(2u, vec.Size()); + Assert::AreEqual(2, vec.Back().value); + } + + TEST_METHOD(ResizeClearAndLifetime) + { + CountingType::Reset(); + { + pmon::util::cnr::FixedVector vec; + vec.Resize(3); + Assert::AreEqual(3, CountingType::Alive); + vec.Resize(1); + Assert::AreEqual(1, CountingType::Alive); + vec.Clear(); + Assert::AreEqual(0, CountingType::Alive); + } + Assert::AreEqual(0, CountingType::Alive); + } + + TEST_METHOD(AssignOverloads) + { + pmon::util::cnr::FixedVector vec; + vec.Assign(3, 42); + Assert::AreEqual(3u, vec.Size()); + Assert::AreEqual(42, vec[0]); + Assert::AreEqual(42, vec[2]); + + vec.Assign({ 1, 2 }); + Assert::AreEqual(2u, vec.Size()); + Assert::AreEqual(1, vec[0]); + Assert::AreEqual(2, vec[1]); + + std::array source{ 9, 8, 7 }; + vec.Assign(source.begin(), source.end()); + Assert::AreEqual(3u, vec.Size()); + Assert::AreEqual(9, vec[0]); + Assert::AreEqual(8, vec[1]); + Assert::AreEqual(7, vec[2]); + } + + TEST_METHOD(ElementAccessAndIterators) + { + pmon::util::cnr::FixedVector vec{ 1, 2, 3 }; + const auto& cvec = vec; + + Assert::AreEqual(1, vec.Front()); + Assert::AreEqual(3, vec.Back()); + Assert::AreEqual(2, vec[1]); + Assert::AreEqual(2, cvec[1]); + Assert::AreEqual(1, cvec.At(0)); + Assert::AreEqual(3, cvec.At(2)); + + Assert::IsTrue(vec.Data() == &vec[0]); + Assert::IsTrue(vec.Data() + 2 == &vec[2]); + + int sum = 0; + for (int value : vec) { + sum += value; + } + Assert::AreEqual(6, sum); + + std::vector reversed; + for (auto it = vec.RBegin(); it != vec.REnd(); ++it) { + reversed.push_back(*it); + } + Assert::AreEqual(3u, reversed.size()); + Assert::AreEqual(3, reversed[0]); + Assert::AreEqual(2, reversed[1]); + Assert::AreEqual(1, reversed[2]); + } + + TEST_METHOD(ExceptionsAndCapacity) + { + pmon::util::cnr::FixedVector vec; + vec.PushBack(1); + vec.PushBack(2); + vec.PushBack(3); + + Assert::ExpectException([&]() { + vec.At(3); + }); + + Assert::ExpectException([&]() { + vec.Reserve(4); + }); + + Assert::ExpectException([&]() { + vec.Resize(4); + }); + + Assert::ExpectException([&]() { + vec.PushBack(4); + }); + + pmon::util::cnr::FixedVector fromInit; + Assert::ExpectException([&]() { + fromInit.Assign({ 1, 2, 3, 4 }); + }); + + std::vector sizedRange{ 1, 2, 3, 4 }; + pmon::util::cnr::FixedVector fromSized; + Assert::ExpectException([&]() { + fromSized.Assign(std::from_range, sizedRange); + }); + + CounterRange unsized{ 0, 5 }; + pmon::util::cnr::FixedVector fromUnsized; + Assert::ExpectException([&]() { + fromUnsized.Assign(std::from_range, unsized); + }); + Assert::AreEqual(3u, fromUnsized.Size()); + Assert::AreEqual(0, fromUnsized[0]); + Assert::AreEqual(1, fromUnsized[1]); + Assert::AreEqual(2, fromUnsized[2]); + } + + TEST_METHOD(ReserveAndShrinkNoOp) + { + pmon::util::cnr::FixedVector vec{ 1, 2 }; + vec.Reserve(4); + vec.ShrinkToFit(); + Assert::AreEqual(2u, vec.Size()); + Assert::AreEqual(2, vec.Back()); + } + }; +} diff --git a/IntelPresentMon/UnitTests/MetricsCore.cpp b/IntelPresentMon/UnitTests/MetricsCore.cpp new file mode 100644 index 000000000..a1927c0b3 --- /dev/null +++ b/IntelPresentMon/UnitTests/MetricsCore.cpp @@ -0,0 +1,8866 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace pmon::util::metrics; +using namespace pmon::util; + +namespace MetricsCoreTests +{ + // ============================================================================ + // SECTION 1: Core Types & Foundation + // ============================================================================ + + + // ConsoleAdapter tests are skipped in unit tests because they require PresentData + // which has ETW dependencies. These will be tested during Console integration. + /* + TEST_CLASS(ConsoleAdapterTests) + { + public: + TEST_METHOD(Constructor_AcceptsSharedPtr) + { + auto event = std::make_shared(); + event->PresentStartTime = 1234; + + ConsoleAdapter adapter(event); + + Assert::AreEqual(1234ull, adapter.getPresentStartTime()); + } + + TEST_METHOD(Constructor_AcceptsRawPointer) + { + PresentEvent event{}; + event.ReadyTime = 5678; + + ConsoleAdapter adapter(&event); + + Assert::AreEqual(5678ull, adapter.getReadyTime()); + } + + TEST_METHOD(GettersProvideAccessToAllTimingFields) + { + auto event = std::make_shared(); + event->PresentStartTime = 100; + event->ReadyTime = 200; + event->TimeInPresent = 50; + event->GPUStartTime = 150; + event->GPUDuration = 75; + event->GPUVideoDuration = 25; + + ConsoleAdapter adapter(event); + + Assert::AreEqual(100ull, adapter.getPresentStartTime()); + Assert::AreEqual(200ull, adapter.getReadyTime()); + Assert::AreEqual(50ull, adapter.getTimeInPresent()); + Assert::AreEqual(150ull, adapter.getGPUStartTime()); + Assert::AreEqual(75ull, adapter.getGPUDuration()); + Assert::AreEqual(25ull, adapter.getGPUVideoDuration()); + } + + TEST_METHOD(GettersProvideAccessToAppPropagatedData) + { + auto event = std::make_shared(); + event->AppPropagatedPresentStartTime = 300; + event->AppPropagatedGPUDuration = 100; + + ConsoleAdapter adapter(event); + + Assert::AreEqual(300ull, adapter.getAppPropagatedPresentStartTime()); + Assert::AreEqual(100ull, adapter.getAppPropagatedGPUDuration()); + } + + TEST_METHOD(GettersProvideAccessToPcLatencyData) + { + auto event = std::make_shared(); + event->PclSimStartTime = 400; + event->PclInputPingTime = 350; + + ConsoleAdapter adapter(event); + + Assert::AreEqual(400ull, adapter.getPclSimStartTime()); + Assert::AreEqual(350ull, adapter.getPclInputPingTime()); + } + + TEST_METHOD(GetDisplayedCount_ReturnsVectorSize) + { + auto event = std::make_shared(); + event->Displayed.push_back({FrameType::Application, 100}); + event->Displayed.push_back({FrameType::Repeated, 200}); + + ConsoleAdapter adapter(event); + + Assert::AreEqual(size_t(2), adapter.getDisplayedCount()); + } + + TEST_METHOD(GetDisplayed_ProvidesIndexedAccess) + { + auto event = std::make_shared(); + event->Displayed.push_back({FrameType::Application, 1000}); + event->Displayed.push_back({FrameType::Repeated, 2000}); + + ConsoleAdapter adapter(event); + + Assert::IsTrue(adapter.getDisplayedFrameType(0) == FrameType::Application); + Assert::AreEqual(1000ull, adapter.getDisplayedScreenTime(0)); + Assert::IsTrue(adapter.getDisplayedFrameType(1) == FrameType::Repeated); + Assert::AreEqual(2000ull, adapter.getDisplayedScreenTime(1)); + } + + TEST_METHOD(HasAppPropagatedData_ReturnsTrueWhenPresent) + { + auto event = std::make_shared(); + event->AppPropagatedPresentStartTime = 123; + + ConsoleAdapter adapter(event); + + Assert::IsTrue(adapter.hasAppPropagatedData()); + } + + TEST_METHOD(HasAppPropagatedData_ReturnsFalseWhenZero) + { + auto event = std::make_shared(); + event->AppPropagatedPresentStartTime = 0; + + ConsoleAdapter adapter(event); + + Assert::IsFalse(adapter.hasAppPropagatedData()); + } + + TEST_METHOD(HasPclSimStartTime_ReturnsTrueWhenPresent) + { + auto event = std::make_shared(); + event->PclSimStartTime = 456; + + ConsoleAdapter adapter(event); + + Assert::IsTrue(adapter.hasPclSimStartTime()); + } + + TEST_METHOD(HasPclInputPingTime_ReturnsTrueWhenPresent) + { + auto event = std::make_shared(); + event->PclInputPingTime = 789; + + ConsoleAdapter adapter(event); + + Assert::IsTrue(adapter.hasPclInputPingTime()); + } + }; + */ + + // ============================================================================ + // SECTION 2: SwapChainCoreState + // ============================================================================ + + TEST_CLASS(SwapChainCoreStateTests) + { + public: + // Simple mock type for testing - just needs to be storable + struct MockPresent { + uint64_t presentStartTime = 0; + }; + + TEST_METHOD(DefaultConstruction_InitializesTimestampsToZero) + { + SwapChainCoreState swapChain; + + Assert::AreEqual(0ull, swapChain.lastSimStartTime); + Assert::AreEqual(0ull, swapChain.lastDisplayedSimStartTime); + Assert::AreEqual(0ull, swapChain.lastDisplayedScreenTime); + Assert::AreEqual(0ull, swapChain.firstAppSimStartTime); + } + + TEST_METHOD(DefaultConstruction_InitializesOptionalPresentToEmpty) + { + SwapChainCoreState swapChain; + + Assert::IsFalse(swapChain.lastPresent.has_value()); + Assert::IsFalse(swapChain.lastAppPresent.has_value()); + } + + TEST_METHOD(LastPresent_CanBeAssigned) + { + SwapChainCoreState swapChain; + FrameData p1{}; + p1.presentStartTime = 12345; + swapChain.lastPresent = p1; + + Assert::IsTrue(swapChain.lastPresent.has_value()); + Assert::AreEqual(12345ull, swapChain.lastPresent.value().presentStartTime); + } + + TEST_METHOD(DroppedInputTracking_InitializesToZero) + { + SwapChainCoreState swapChain; + + Assert::AreEqual(0ull, swapChain.lastReceivedNotDisplayedAllInputTime); + Assert::AreEqual(0ull, swapChain.lastReceivedNotDisplayedMouseClickTime); + Assert::AreEqual(0ull, swapChain.lastReceivedNotDisplayedAppProviderInputTime); + } + + TEST_METHOD(DroppedInputTracking_CanBeUpdated) + { + SwapChainCoreState swapChain; + + swapChain.lastReceivedNotDisplayedAllInputTime = 1000; + swapChain.lastReceivedNotDisplayedMouseClickTime = 2000; + swapChain.lastReceivedNotDisplayedAppProviderInputTime = 3000; + + Assert::AreEqual(1000ull, swapChain.lastReceivedNotDisplayedAllInputTime); + Assert::AreEqual(2000ull, swapChain.lastReceivedNotDisplayedMouseClickTime); + Assert::AreEqual(3000ull, swapChain.lastReceivedNotDisplayedAppProviderInputTime); + } + + TEST_METHOD(PcLatencyAccumulation_InitializesToZero) + { + SwapChainCoreState swapChain; + + Assert::AreEqual(0.0, swapChain.accumulatedInput2FrameStartTime); + } + + TEST_METHOD(PcLatencyAccumulation_CanAccumulateTime) + { + SwapChainCoreState swapChain; + + // Simulate accumulating 3 dropped frames at 16.666ms each + swapChain.accumulatedInput2FrameStartTime += 16.666; + swapChain.accumulatedInput2FrameStartTime += 16.666; + swapChain.accumulatedInput2FrameStartTime += 16.666; + + Assert::AreEqual(49.998, swapChain.accumulatedInput2FrameStartTime, 0.001); + } + + TEST_METHOD(AnimationErrorSource_DefaultsToCpuStart) + { + SwapChainCoreState swapChain; + + Assert::IsTrue(swapChain.animationErrorSource == AnimationErrorSource::CpuStart); + } + + TEST_METHOD(AnimationErrorSource_CanBeChanged) + { + SwapChainCoreState swapChain; + + swapChain.animationErrorSource = AnimationErrorSource::PCLatency; + Assert::IsTrue(swapChain.animationErrorSource == AnimationErrorSource::PCLatency); + + swapChain.animationErrorSource = AnimationErrorSource::AppProvider; + Assert::IsTrue(swapChain.animationErrorSource == AnimationErrorSource::AppProvider); + } + }; + + // ============================================================================ + // SECTION 2: DisplayIndexing Calculator + // ============================================================================ + + TEST_CLASS(DisplayIndexingTests) + { + public: + TEST_METHOD(Calculate_NoDisplayedFrames_ReturnsEmptyRange) + { + FrameData present{}; + // No displayed frames + present.displayed.Clear(); + + auto result = DisplayIndexing::Calculate(present, nullptr); + + Assert::AreEqual(size_t(0), result.startIndex); + Assert::AreEqual(size_t(0), result.endIndex); + Assert::AreEqual(size_t(0), result.appIndex); // No displays → appIndex = 0 + Assert::IsFalse(result.hasNextDisplayed); + } + + TEST_METHOD(Calculate_SingleDisplay_NoNext_Postponed) + { + FrameData present{}; + present.displayed.PushBack({ FrameType::Application, 1000 }); + present.finalState = PresentResult::Presented; + + auto result = DisplayIndexing::Calculate(present, nullptr); + + // Single display with no next = postponed (empty range) + Assert::AreEqual(size_t(0), result.startIndex); + Assert::AreEqual(size_t(0), result.endIndex); // Empty! Postponed + Assert::AreEqual(size_t(0), result.appIndex); // Would be 0 if processed + Assert::IsFalse(result.hasNextDisplayed); + } + + TEST_METHOD(Calculate_MultipleDisplays_NoNext_PostponeLast) + { + FrameData present{}; + present.displayed.PushBack({ FrameType::Application, 1000 }); + present.displayed.PushBack({ FrameType::Repeated, 2000 }); + present.displayed.PushBack({ FrameType::Repeated, 3000 }); + present.finalState = PresentResult::Presented; + + auto result = DisplayIndexing::Calculate(present, nullptr); + + // Process [0..1], postpone [2] + Assert::AreEqual(size_t(0), result.startIndex); + Assert::AreEqual(size_t(2), result.endIndex); // Excludes last! + Assert::AreEqual(size_t(0), result.appIndex); // App frame at index 0 + Assert::IsFalse(result.hasNextDisplayed); + } + + TEST_METHOD(Calculate_MultipleDisplays_WithNext_ProcessPostponed) + { + FrameData present{}; + present.displayed.PushBack({ FrameType::Application, 1000 }); + present.displayed.PushBack({ FrameType::Repeated, 2000 }); + present.displayed.PushBack({ FrameType::Repeated, 3000 }); + present.finalState = PresentResult::Presented; + + FrameData next{}; + next.displayed.PushBack({ FrameType::Application, 4000 }); + + auto result = DisplayIndexing::Calculate(present, &next); + + // Process only postponed last display [2] + Assert::AreEqual(size_t(2), result.startIndex); + Assert::AreEqual(size_t(3), result.endIndex); + Assert::AreEqual(SIZE_MAX, result.appIndex); // No app frame at [2], it's Repeated + Assert::IsTrue(result.hasNextDisplayed); + } + + TEST_METHOD(Calculate_NotDisplayed_ReturnsEmptyRange) + { + FrameData present{}; + present.displayed.PushBack({ FrameType::Application, 1000 }); + present.displayed.PushBack({ FrameType::Repeated, 2000 }); + // Don't set finalState = Presented, so displayed = false + + auto result = DisplayIndexing::Calculate(present, nullptr); + + // Not displayed → empty range + Assert::AreEqual(size_t(0), result.startIndex); + Assert::AreEqual(size_t(0), result.endIndex); + Assert::AreEqual(size_t(0), result.appIndex); // Fallback when displayCount > 0 but not displayed + Assert::IsFalse(result.hasNextDisplayed); + } + + TEST_METHOD(Calculate_FindsAppFrameIndex_Displayed) + { + FrameData present{}; + present.displayed.PushBack({ FrameType::Repeated, 1000 }); + present.displayed.PushBack({ FrameType::Application, 2000 }); + present.displayed.PushBack({ FrameType::Repeated, 3000 }); + present.finalState = PresentResult::Presented; + + auto result = DisplayIndexing::Calculate(present, nullptr); + + // Process [0..1], postpone [2] + Assert::AreEqual(size_t(0), result.startIndex); + Assert::AreEqual(size_t(2), result.endIndex); + Assert::AreEqual(size_t(1), result.appIndex); // App at index 1 + } + + TEST_METHOD(Calculate_FindsAppFrameIndex_NotDisplayed) + { + FrameData present{}; + present.displayed.PushBack({ FrameType::Repeated, 1000 }); + present.displayed.PushBack({ FrameType::Application, 2000 }); + present.displayed.PushBack({ FrameType::Repeated, 3000 }); + // Not displayed + + auto result = DisplayIndexing::Calculate(present, nullptr); + + // Not displayed → empty range + Assert::AreEqual(size_t(0), result.startIndex); + Assert::AreEqual(size_t(0), result.endIndex); + } + + TEST_METHOD(Calculate_AllRepeatedFrames_AppIndexInvalid) + { + FrameData present{}; + present.displayed.PushBack({ FrameType::Repeated, 1000 }); + present.displayed.PushBack({ FrameType::Repeated, 2000 }); + present.displayed.PushBack({ FrameType::Repeated, 3000 }); + present.finalState = PresentResult::Presented; + + auto result = DisplayIndexing::Calculate(present, nullptr); + + // Process [0..1], postpone [2] + Assert::AreEqual(size_t(0), result.startIndex); + Assert::AreEqual(size_t(2), result.endIndex); + Assert::AreEqual(SIZE_MAX, result.appIndex); // No app frame found + } + + TEST_METHOD(Calculate_MultipleAppFrames_FindsFirst) + { + FrameData present{}; + present.displayed.PushBack({ FrameType::Application, 1000 }); + present.displayed.PushBack({ FrameType::Application, 2000 }); + present.displayed.PushBack({ FrameType::Repeated, 3000 }); + present.finalState = PresentResult::Presented; + + auto result = DisplayIndexing::Calculate(present, nullptr); + + // Process [0..1], postpone [2] + Assert::AreEqual(size_t(0), result.startIndex); + Assert::AreEqual(size_t(2), result.endIndex); + Assert::AreEqual(size_t(0), result.appIndex); // First app frame + } + + TEST_METHOD(Calculate_WorksWithFrameData) + { + // Verify template works with FrameData + FrameData present{}; + present.displayed.PushBack({ FrameType::Application, 1000 }); + present.finalState = PresentResult::Presented; + + auto result = DisplayIndexing::Calculate(present, nullptr); + + Assert::AreEqual(size_t(0), result.startIndex); + Assert::AreEqual(size_t(0), result.endIndex); // Postponed [0], nothing processed + Assert::IsTrue(result.appIndex == 0); + } + }; + + // ============================================================================ + // SECTION 3: Helper Functions + // ============================================================================ + + TEST_CLASS(CalculateCPUStartTests) + { + public: + TEST_METHOD(UsesAppPropagatedWhenAvailable) + { + // Setup: swapchain with lastAppPresent that has AppPropagated data + SwapChainCoreState swapChain{}; + FrameData lastApp{}; + lastApp.appPropagatedPresentStartTime = 1000; + lastApp.appPropagatedTimeInPresent = 50; + swapChain.lastAppPresent = lastApp; // std::optional assignment + + FrameData current{}; + current.presentStartTime = 2000; + + auto result = CalculateCPUStart(swapChain, current); + + // Should use appPropagated: 1000 + 50 = 1050 + Assert::AreEqual(1050ull, result); + } + + TEST_METHOD(FallsBackToRegularPresentStart) + { + // Setup: swapchain with lastAppPresent but NO appPropagated data + SwapChainCoreState swapChain{}; + FrameData lastApp{}; + lastApp.appPropagatedPresentStartTime = 0; // No propagated data + lastApp.presentStartTime = 1000; + lastApp.timeInPresent = 50; + swapChain.lastAppPresent = lastApp; + + FrameData current{}; + + auto result = CalculateCPUStart(swapChain, current); + + // Should use regular: 1000 + 50 = 1050 + Assert::AreEqual(1050ull, result); + } + + TEST_METHOD(UsesLastPresentWhenNoAppPresent) + { + // Setup: swapchain with lastPresent but NO lastAppPresent + SwapChainCoreState swapChain{}; + // lastAppPresent is std::nullopt by default + + FrameData lastPresent{}; + lastPresent.presentStartTime = 1000; + lastPresent.timeInPresent = 50; + swapChain.lastPresent = lastPresent; + + FrameData current{}; + current.timeInPresent = 30; + + auto result = CalculateCPUStart(swapChain, current); + + // Should use lastPresents values: 1000 + 50 (last presents start time and the + // time it spent in that present). This would equal the last presents + // stop time which is the earliest the application can start the next frame. + Assert::AreEqual(1050ull, result); + } + + TEST_METHOD(ReturnsZeroWhenNoHistory) + { + // Setup: empty chain (both optionals are std::nullopt) + SwapChainCoreState swapChain{}; + + FrameData current{}; + current.presentStartTime = 2000; + + auto result = CalculateCPUStart(swapChain, current); + + // Should return 0 when no history + Assert::AreEqual(0ull, result); + } + }; + + TEST_CLASS(CalculateAnimationErrorSimStartTimeTests) + { + public: + TEST_METHOD(UsesCpuStartSource) + { + QpcConverter qpc{ 10000000, 0 }; // 10 MHz for easy math + + SwapChainCoreState swapChain{}; + FrameData lastApp{}; + lastApp.presentStartTime = 1000; + lastApp.timeInPresent = 50; + swapChain.lastAppPresent = lastApp; + + FrameData current{}; + current.appSimStartTime = 5000; // Has appSim, but source is CpuStart + + auto result = CalculateAnimationErrorSimStartTime(swapChain, current, AnimationErrorSource::CpuStart); + + // Should use CPU start calculation: 1000 + 50 = 1050 + Assert::AreEqual(1050ull, result); + } + + TEST_METHOD(UsesAppProviderSource) + { + QpcConverter qpc{ 10000000, 0 }; + + SwapChainCoreState swapChain{}; + FrameData lastApp{}; + lastApp.presentStartTime = 1000; + lastApp.timeInPresent = 50; + swapChain.lastAppPresent = lastApp; + + FrameData current{}; + current.appSimStartTime = 5000; + + auto result = CalculateAnimationErrorSimStartTime(swapChain, current, AnimationErrorSource::AppProvider); + + // Should use appSimStartTime + Assert::AreEqual(5000ull, result); + } + + TEST_METHOD(UsesPCLatencySource) + { + QpcConverter qpc{ 10000000, 0 }; + + SwapChainCoreState swapChain{}; + FrameData lastApp{}; + lastApp.presentStartTime = 1000; + lastApp.timeInPresent = 50; + swapChain.lastAppPresent = lastApp; + + FrameData current{}; + current.pclSimStartTime = 6000; + + auto result = CalculateAnimationErrorSimStartTime(swapChain, current, AnimationErrorSource::PCLatency); + + // Should use pclSimStartTime + Assert::AreEqual(6000ull, result); + } + }; + + TEST_CLASS(CalculateAnimationTimeTests) + { + public: + TEST_METHOD(ComputesRelativeTime) + { + QpcConverter qpc{ 10000000, 0 }; // 10 MHz QPC frequency + + uint64_t firstSimStart = 1000; + uint64_t currentSimStart = 1500; // 500 ticks later + + auto result = CalculateAnimationTime(qpc, firstSimStart, currentSimStart); + + // 500 ticks at 10 MHz = 0.05 ms + Assert::AreEqual(0.05, result, 0.001); + } + + TEST_METHOD(HandlesZeroFirst) + { + QpcConverter qpc{ 10000000, 0 }; + + uint64_t firstSimStart = 0; // Not initialized yet + uint64_t currentSimStart = 1500; + + auto result = CalculateAnimationTime(qpc, firstSimStart, currentSimStart); + + // When first is 0, should return 0 + Assert::AreEqual(0.0, result, 0.001); + } + + TEST_METHOD(HandlesSameTimestamp) + { + QpcConverter qpc{ 10000000, 0 }; + + uint64_t firstSimStart = 1000; + uint64_t currentSimStart = 1000; // Same as first + + auto result = CalculateAnimationTime(qpc, firstSimStart, currentSimStart); + + // Same timestamp = 0 ms elapsed + Assert::AreEqual(0.0, result, 0.001); + } + + TEST_METHOD(HandlesLargeTimespan) + { + QpcConverter qpc{ 10000000, 0 }; // 10 MHz + + uint64_t firstSimStart = 1000; + uint64_t currentSimStart = 1000 + (10000000 * 5); // +5 seconds in ticks + + auto result = CalculateAnimationTime(qpc, firstSimStart, currentSimStart); + + // 5 seconds = 5000 ms + Assert::AreEqual(5000.0, result, 0.1); + } + + TEST_METHOD(HandlesBackwardsTime) + { + QpcConverter qpc{ 10000000, 0 }; + + uint64_t firstSimStart = 2000; + uint64_t currentSimStart = 1000; // Earlier than first (unusual but possible) + + auto result = CalculateAnimationTime(qpc, firstSimStart, currentSimStart); + + // Should handle gracefully - returns negative or 0 depending on implementation + // This tests error handling + Assert::IsTrue(result <= 0.0); + } + }; + + // TEST HELPERS FOR METRICS UNIFICATION + // ============================================================================ + + using namespace pmon::util::metrics; + + // Simple helper to construct FrameData for metrics tests. + static FrameData MakeFrame( + PresentResult finalState, + uint64_t presentStartTime, + uint64_t timeInPresent, + uint64_t readyTime, + const DisplayedVector& displayed, + uint64_t appSimStartTime = 0, + uint64_t pclSimStartTime = 0, + uint64_t flipDelay = 0) + { + FrameData f{}; + f.presentStartTime = presentStartTime; + f.timeInPresent = timeInPresent; + f.readyTime = readyTime; + f.displayed = displayed; + f.appSimStartTime = appSimStartTime; + f.pclSimStartTime = pclSimStartTime; + f.flipDelay = flipDelay; + f.finalState = finalState; + return f; + } + + +TEST_CLASS(UnifiedSwapChainTests) +{ +public: + TEST_METHOD(Enqueue_V2_SeedsFirstPresent_ReturnsNoReady) + { + UnifiedSwapChain u{}; + + // First present seeds baseline (no pipeline output). + auto seed = MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}); + auto out = u.Enqueue(std::move(seed), MetricsVersion::V2); + + Assert::AreEqual(size_t(0), out.size()); + Assert::IsTrue(u.swapChain.lastPresent.has_value()); + Assert::AreEqual(uint64_t(1'000'000), u.GetLastPresentQpc()); + } + + TEST_METHOD(Enqueue_V2_NotDisplayed_NoWaiting_ReturnsSingleOwnedItem) + { + UnifiedSwapChain u{}; + (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed + + auto p = MakeFrame(static_cast(9999), 2'000'000, 10, 2'000'010, {}); // not displayed + auto out = u.Enqueue(std::move(p), MetricsVersion::V2); + + Assert::AreEqual(size_t(1), out.size()); + Assert::IsNull(out[0].presentPtr); + Assert::IsNull(out[0].nextDisplayedPtr); + Assert::AreEqual(uint64_t(2'000'000), out[0].present.presentStartTime); + } + + TEST_METHOD(Enqueue_V2_Displayed_FirstDisplayed_ReturnsCurrentDisplayedPtrItemOnly) + { + UnifiedSwapChain u{}; + (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed + + auto displayed = MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, + { { FrameType::Application, 2'500'000 } }); + + auto out = u.Enqueue(std::move(displayed), MetricsVersion::V2); + + Assert::AreEqual(size_t(1), out.size()); + Assert::IsNotNull(out[0].presentPtr); + Assert::IsNull(out[0].nextDisplayedPtr); + Assert::AreEqual(uint64_t(2'000'000), out[0].presentPtr->presentStartTime); + } + + TEST_METHOD(Enqueue_V2_NotDisplayed_WithWaiting_IsBufferedUntilNextDisplayed) + { + UnifiedSwapChain u{}; + (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed + + // First displayed => enters waitingDisplayed, produces current item (but may postpone metrics). + (void)u.Enqueue(MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, + { { FrameType::Application, 2'500'000 } }), MetricsVersion::V2); + + // Not displayed while waiting => no ready work. + auto out1 = u.Enqueue(MakeFrame(static_cast(9999), 2'200'000, 10, 2'200'010, {}), MetricsVersion::V2); + Assert::AreEqual(size_t(0), out1.size()); + + // Next displayed => releases blocked. + auto out2 = u.Enqueue(MakeFrame(PresentResult::Presented, 3'000'000, 10, 3'000'010, + { { FrameType::Application, 3'500'000 } }), MetricsVersion::V2); + + // finalize previous + blocked + current + Assert::AreEqual(size_t(3), out2.size()); + Assert::AreEqual(uint64_t(2'000'000), out2[0].present.presentStartTime); // finalize previous + Assert::AreEqual(uint64_t(2'200'000), out2[1].present.presentStartTime); // released blocked + Assert::IsNotNull(out2[2].presentPtr); // current displayed + Assert::AreEqual(uint64_t(3'000'000), out2[2].presentPtr->presentStartTime); + } + + TEST_METHOD(Enqueue_V2_Displayed_WithWaiting_OrdersFinalizeThenBlockedThenCurrent) + { + UnifiedSwapChain u{}; + (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed + + // Displayed A enters waiting + (void)u.Enqueue(MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, + { { FrameType::Application, 2'500'000 } }), MetricsVersion::V2); + + // Buffer B and C + (void)u.Enqueue(MakeFrame(static_cast(9999), 2'100'000, 10, 2'100'010, {}), MetricsVersion::V2); + (void)u.Enqueue(MakeFrame(static_cast(9999), 2'200'000, 10, 2'200'010, {}), MetricsVersion::V2); + + // Next displayed D triggers: finalize A, then B,C, then current D + auto out = u.Enqueue(MakeFrame(PresentResult::Presented, 3'000'000, 10, 3'000'010, + { { FrameType::Application, 3'500'000 } }), MetricsVersion::V2); + + Assert::AreEqual(size_t(4), out.size()); + Assert::AreEqual(uint64_t(2'000'000), out[0].present.presentStartTime); + Assert::IsNotNull(out[0].nextDisplayedPtr); + Assert::AreEqual(uint64_t(3'000'000), out[0].nextDisplayedPtr->presentStartTime); + + Assert::AreEqual(uint64_t(2'100'000), out[1].present.presentStartTime); + Assert::AreEqual(uint64_t(2'200'000), out[2].present.presentStartTime); + + Assert::IsNotNull(out[3].presentPtr); + Assert::AreEqual(uint64_t(3'000'000), out[3].presentPtr->presentStartTime); + } + + TEST_METHOD(Enqueue_V2_SanitizeDisplayed_RemovesAppThenRepeated) + { + UnifiedSwapChain u{}; + (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed + + auto p = MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, + { { FrameType::Application, 2'500'000 }, + { FrameType::Repeated, 2'700'000 } }); + + auto out = u.Enqueue(std::move(p), MetricsVersion::V2); + + Assert::AreEqual(size_t(1), out.size()); + Assert::IsNotNull(out[0].presentPtr); + + Assert::AreEqual(size_t(1), out[0].presentPtr->displayed.Size()); + Assert::AreEqual((int)FrameType::Application, (int)out[0].presentPtr->displayed[0].first); + } + + TEST_METHOD(Enqueue_V2_SanitizeDisplayed_RemovesRepeatedThenApp) + { + UnifiedSwapChain u{}; + (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed + + auto p = MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, + { { FrameType::Repeated, 2'400'000 }, + { FrameType::Application, 2'500'000 } }); + + auto out = u.Enqueue(std::move(p), MetricsVersion::V2); + + Assert::AreEqual(size_t(1), out.size()); + Assert::IsNotNull(out[0].presentPtr); + + Assert::AreEqual(size_t(1), out[0].presentPtr->displayed.Size()); + Assert::AreEqual((int)FrameType::Application, (int)out[0].presentPtr->displayed[0].first); + } + + TEST_METHOD(Pipeline_V2_PostponedLastDisplayInstance_EmittedOnFinalize) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain u{}; + + // Seed history + (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); + + // First displayed has two instances: app then repeated. + auto outA = u.Enqueue(MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, + { { FrameType::Intel_XEFG, 2'500'000 }, + { FrameType::Application, 2'700'000 } }), + MetricsVersion::V2); + + Assert::AreEqual(size_t(1), outA.size()); + Assert::IsNotNull(outA[0].presentPtr); + + // Processing current displayed without next: should produce all-but-last => one result at 2'500'000. + auto resA = ComputeMetricsForPresent(qpc, *outA[0].presentPtr, nullptr, u.swapChain, MetricsVersion::V2); + Assert::AreEqual(size_t(1), resA.size()); + Assert::AreEqual(uint64_t(2'500'000), resA[0].metrics.screenTimeQpc); + + // Next displayed triggers finalize of the previous displayed (postponed last instance at 2'700'000). + auto outB = u.Enqueue(MakeFrame(PresentResult::Presented, 3'000'000, 10, 3'000'010, + { { FrameType::Application, 3'500'000 } }), + MetricsVersion::V2); + + Assert::AreEqual(size_t(2), outB.size()); + Assert::IsNotNull(outB[0].nextDisplayedPtr); + + auto resFinalize = ComputeMetricsForPresent(qpc, outB[0].present, outB[0].nextDisplayedPtr, u.swapChain, MetricsVersion::V2); + Assert::AreEqual(size_t(1), resFinalize.size()); + Assert::AreEqual(uint64_t(2'700'000), resFinalize[0].metrics.screenTimeQpc); + } + + TEST_METHOD(Pipeline_V2_NvCollapsed_AdjustmentPersistsViaNextDisplayedPtr) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain u{}; + + (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); + + // First displayed: collapsed/runt-style (flipDelay set), screenTime is later than next's raw screenTime. + (void)u.Enqueue(MakeFrame(PresentResult::Presented, 4'000'000, 50'000, 4'100'000, + { { FrameType::Application, 5'500'000 } }, + 0, 0, 200'000), + MetricsVersion::V2); + + // Second displayed: raw screenTime earlier -> should be adjusted upward by NV2 when finalizing first. + auto out = u.Enqueue(MakeFrame(PresentResult::Presented, 5'000'000, 40'000, 5'100'000, + { { FrameType::Application, 5'000'000 } }, + 0, 0, 100'000), + MetricsVersion::V2); + + // Expect: finalize previous + current displayed + Assert::AreEqual(size_t(2), out.size()); + Assert::IsNotNull(out[0].nextDisplayedPtr); + Assert::IsNotNull(out[1].presentPtr); + + // Finalize first with look-ahead to second => mutates second via pointer. + (void)ComputeMetricsForPresent(qpc, out[0].present, out[0].nextDisplayedPtr, u.swapChain, MetricsVersion::V2); + + // Mutation must persist on swapchain-owned second frame. + Assert::AreEqual(uint64_t(5'500'000), out[1].presentPtr->displayed[0].second); + Assert::AreEqual(uint64_t(100'000 + (5'500'000 - 5'000'000)), out[1].presentPtr->flipDelay); + } + + TEST_METHOD(Pipeline_V1_NvCollapsed_AdjustsCurrentPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain u{}; + + // Establish previous displayed state (lastDisplayedScreenTime/flipDelay) via first displayed. + auto out1 = u.Enqueue(MakeFrame(PresentResult::Presented, 4'000'000, 50'000, 4'100'000, + { { FrameType::Application, 5'500'000 } }, + 0, 0, 200'000), + MetricsVersion::V1); + + Assert::AreEqual(size_t(1), out1.size()); + + (void)ComputeMetricsForPresent(qpc, out1[0].present, nullptr, u.swapChain, MetricsVersion::V1); + + // Second present has earlier raw screenTime; NV1 should adjust *current* present to lastDisplayedScreenTime. + auto out2 = u.Enqueue(MakeFrame(PresentResult::Presented, 5'000'000, 40'000, 5'100'000, + { { FrameType::Application, 5'000'000 } }, + 0, 0, 100'000), + MetricsVersion::V1); + + Assert::AreEqual(size_t(1), out2.size()); + + (void)ComputeMetricsForPresent(qpc, out2[0].present, nullptr, u.swapChain, MetricsVersion::V1); + + Assert::AreEqual(uint64_t(5'500'000), out2[0].present.displayed[0].second); + Assert::AreEqual(uint64_t(100'000 + (5'500'000 - 5'000'000)), out2[0].present.flipDelay); + } + + TEST_METHOD(Pipeline_V1_NoNvCollapse_DoesNotModifyCurrentPresent) + { + QpcConverter qpc(10'000'000, 0); + UnifiedSwapChain u{}; + + // Prior displayed state via first displayed. + auto out1 = u.Enqueue(MakeFrame(PresentResult::Presented, 4'000'000, 50'000, 4'100'000, + { { FrameType::Application, 5'000'000 } }, + 0, 0, 200'000), + MetricsVersion::V1); + (void)ComputeMetricsForPresent(qpc, out1[0].present, nullptr, u.swapChain, MetricsVersion::V1); + + // Current has later screenTime => no collapse. + auto out2 = u.Enqueue(MakeFrame(PresentResult::Presented, 5'000'000, 40'000, 5'100'000, + { { FrameType::Application, 6'000'000 } }, + 0, 0, 100'000), + MetricsVersion::V1); + + // Preserve originals for comparison. + const uint64_t origScreen = out2[0].present.displayed[0].second; + const uint64_t origFlipDelay = out2[0].present.flipDelay; + + (void)ComputeMetricsForPresent(qpc, out2[0].present, nullptr, u.swapChain, MetricsVersion::V1); + + Assert::AreEqual(origScreen, out2[0].present.displayed[0].second); + Assert::AreEqual(origFlipDelay, out2[0].present.flipDelay); + } + + TEST_METHOD(Enqueue_V1_ClearsV2Buffers_AndIsAlwaysReady) + { + UnifiedSwapChain u{}; + (void)u.Enqueue(MakeFrame(PresentResult::Presented, 1'000'000, 10, 1'000'010, {}), MetricsVersion::V2); // seed + + // Create V2 waitingDisplayed + blocked. + (void)u.Enqueue(MakeFrame(PresentResult::Presented, 2'000'000, 10, 2'000'010, + { { FrameType::Application, 2'500'000 } }), MetricsVersion::V2); + (void)u.Enqueue(MakeFrame(static_cast(9999), 2'200'000, 10, 2'200'010, {}), MetricsVersion::V2); + + // V1 enqueue must clear V2 buffers and return one ready item. + auto outV1 = u.Enqueue(MakeFrame(static_cast(9999), 2'300'000, 10, 2'300'010, {}), MetricsVersion::V1); + Assert::AreEqual(size_t(1), outV1.size()); + + // Next V2 displayed should behave as "no waiting/no blocked": returns only current displayed item. + auto outV2 = u.Enqueue(MakeFrame(PresentResult::Presented, 3'000'000, 10, 3'000'010, + { { FrameType::Application, 3'500'000 } }), MetricsVersion::V2); + + Assert::AreEqual(size_t(1), outV2.size()); + Assert::IsNotNull(outV2[0].presentPtr); + Assert::IsNull(outV2[0].nextDisplayedPtr); + } +}; + + +TEST_CLASS(ComputeMetricsForPresentTests) + { + public: + TEST_METHOD(ComputeMetricsForPresent_NotDisplayed_NoDisplays_ProducesSingleMetricsAndUpdatesChain) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + auto frame = MakeFrame(PresentResult::Presented, 10'000, 500, 10'500, {}); // Presented but no displays => not displayed path + auto metrics = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + + Assert::AreEqual(size_t(1), metrics.size(), L"Should produce exactly one metrics entry."); + Assert::IsTrue(chain.lastPresent.has_value(), L"Chain should be updated for not displayed."); + Assert::IsTrue(chain.lastAppPresent.has_value(), L"Not displayed frames become lastAppPresent."); + Assert::AreEqual(uint64_t(0), chain.lastDisplayedScreenTime); + Assert::AreEqual(uint64_t(0), chain.lastDisplayedFlipDelay); + } + + TEST_METHOD(ComputeMetricsForPresent_NotDisplayed_WithDisplaysButNotPresented_ProducesSingleMetricsAndUpdatesChain) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Simulate a frame with 'displayed' entries but finalState != Presented (treat as not displayed). + auto frame = MakeFrame(static_cast(9999), 1'000, 100, 1'200, + { { FrameType::Application, 2'000 } }); + + auto metrics = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(1), metrics.size()); + Assert::IsTrue(chain.lastPresent.has_value()); + Assert::IsTrue(chain.lastAppPresent.has_value()); + Assert::AreEqual(uint64_t(0), chain.lastDisplayedScreenTime, L"Not displayed path should not update displayed screen time."); + } + + TEST_METHOD(ComputeMetricsForPresent_DisplayedNoNext_SingleDisplay_PostponedChainNotUpdated) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + auto frame = MakeFrame(PresentResult::Presented, 5'000, 200, 5'500, + { { FrameType::Application, 6'000 } }); + + auto metrics = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + + Assert::AreEqual(size_t(0), metrics.size(), L"Single display is postponed => zero metrics now."); + Assert::IsFalse(chain.lastPresent.has_value(), L"Chain should NOT be updated yet."); + Assert::IsFalse(chain.lastAppPresent.has_value()); + } + + TEST_METHOD(ComputeMetricsForPresent_DisplayedNoNext_MultipleDisplays_ProcessesAllButLast) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + auto frame = MakeFrame(PresentResult::Presented, 10'000, 300, 10'800, + { + { FrameType::Application, 11'000 }, + { FrameType::Repeated, 11'500 }, + { FrameType::Repeated, 12'000 } // postponed + }); + + auto metrics = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + + Assert::AreEqual(size_t(2), metrics.size(), L"Should process all but last display."); + Assert::IsFalse(chain.lastPresent.has_value()); + Assert::IsFalse(chain.lastAppPresent.has_value()); + } + + TEST_METHOD(ComputeMetricsForPresent_DisplayedWithNext_ProcessesPostponedLastAndUpdatesChain) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + auto frame = MakeFrame(PresentResult::Presented, 10'000, 300, 10'800, + { + { FrameType::Application, 11'000 }, + { FrameType::Repeated, 11'500 }, + { FrameType::Repeated, 12'000 } + }, + 0, 0, 777); + + auto nextDisplayed = MakeFrame(PresentResult::Presented, 13'000, 250, 13'600, + { { FrameType::Application, 14'000 } }); + + // First call without nextDisplayed: postpone last + auto preMetrics = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(2), preMetrics.size()); + Assert::IsFalse(chain.lastPresent.has_value()); + + // Second call with nextDisplayed: process postponed last + update chain + auto postMetrics = ComputeMetricsForPresent(qpc, frame, &nextDisplayed, chain); + Assert::AreEqual(size_t(1), postMetrics.size(), L"Should process only the postponed last display this time."); + Assert::IsTrue(chain.lastPresent.has_value()); + Assert::AreEqual(uint64_t(12'000), chain.lastDisplayedScreenTime); + Assert::AreEqual(uint64_t(777), chain.lastDisplayedFlipDelay); + } + + TEST_METHOD(ComputeMetricsForPresent_DisplayedWithNext_LastDisplayIsRepeated_DoesNotUpdateLastAppPresent) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Previous app present for fallback usage. + FrameData prevApp = MakeFrame(PresentResult::Presented, 2'000, 100, 2'300, + { { FrameType::Application, 2'800 } }); + chain.lastAppPresent = prevApp; + + auto frame = MakeFrame(PresentResult::Presented, 4'000, 120, 4'300, + { + { FrameType::Application, 4'500 }, + { FrameType::Repeated, 4'900 } // last (Repeated) + }); + + auto nextDisplayed = MakeFrame(PresentResult::Presented, 5'000, 110, 5'250, + { { FrameType::Application, 5'600 } }); + + auto metrics = ComputeMetricsForPresent(qpc, frame, &nextDisplayed, chain); + Assert::AreEqual(size_t(1), metrics.size()); + + Assert::IsTrue(chain.lastPresent.has_value()); + // lastAppPresent should remain previous since last display was Repeated + Assert::IsTrue(chain.lastAppPresent.has_value()); + Assert::AreEqual(uint64_t(2'000), chain.lastAppPresent->presentStartTime); + } + }; + + TEST_CLASS(UpdateAfterPresentAnimationErrorSourceTests) + { + public: + TEST_METHOD(UpdateAfterPresent_AnimationSource_AppProvider_UpdatesSimStartAndFirstAppSim) + { + SwapChainCoreState chain{}; + chain.animationErrorSource = AnimationErrorSource::AppProvider; + + auto frame = MakeFrame(PresentResult::Presented, 1'000, 50, 1'200, + { { FrameType::Application, 1'500 } }, + 10'000 /* appSimStartTime */); + + chain.UpdateAfterPresent(frame); + + Assert::AreEqual(uint64_t(10'000), chain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(10'000), chain.firstAppSimStartTime); + Assert::AreEqual(uint64_t(1'500), chain.lastDisplayedAppScreenTime); + } + + TEST_METHOD(UpdateAfterPresent_AnimationSource_PCLatency_UpdatesSimStartAndFirstAppSim) + { + SwapChainCoreState chain{}; + chain.animationErrorSource = AnimationErrorSource::PCLatency; + + auto frame = MakeFrame(PresentResult::Presented, 2'000, 40, 2'300, + { { FrameType::Application, 2'700 } }, + 0 /* appSimStartTime */, 12'345 /* pclSimStart */); + + chain.UpdateAfterPresent(frame); + + Assert::AreEqual(uint64_t(12'345), chain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(12'345), chain.firstAppSimStartTime); + Assert::AreEqual(uint64_t(2'700), chain.lastDisplayedAppScreenTime); + } + + TEST_METHOD(UpdateAfterPresent_AnimationSource_CpuStart_FallbackToPreviousAppPresent) + { + SwapChainCoreState chain{}; + chain.animationErrorSource = AnimationErrorSource::CpuStart; + + FrameData previousApp = MakeFrame(PresentResult::Presented, 5'000, 80, 5'300, + { { FrameType::Application, 5'800 } }); + chain.lastAppPresent = previousApp; + + auto frame = MakeFrame(PresentResult::Presented, 6'000, 60, 6'250, + { { FrameType::Application, 6'700 } }, + 0, 0); + + chain.UpdateAfterPresent(frame); + + // No appSimStartTime or pclSimStartTime, fallback uses previous app present CPU end: + // 5'000 + 80 = 5'080 + Assert::AreEqual(uint64_t(5'080), chain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(0), chain.firstAppSimStartTime); // Not set yet + Assert::AreEqual(uint64_t(6'700), chain.lastDisplayedAppScreenTime); + } + + TEST_METHOD(UpdateAfterPresent_AnimationSource_CpuStart_TransitionsToAppProvider) + { + SwapChainCoreState chain{}; + chain.animationErrorSource = AnimationErrorSource::CpuStart; + + auto frame = MakeFrame(PresentResult::Presented, 7'000, 70, 7'400, + { { FrameType::Application, 7'900 } }, + 20'000 /* appSimStartTime */); + + chain.UpdateAfterPresent(frame); + + Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::AppProvider); + Assert::AreEqual(uint64_t(20'000), chain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(20'000), chain.firstAppSimStartTime); + } + + TEST_METHOD(UpdateAfterPresent_AnimationSource_CpuStart_TransitionsToPCLatency) + { + SwapChainCoreState chain{}; + chain.animationErrorSource = AnimationErrorSource::CpuStart; + + auto frame = MakeFrame(PresentResult::Presented, 8'000, 80, 8'400, + { { FrameType::Application, 8'950 } }, + 0 /* appSim */, 30'000 /* pclSim */); + + chain.UpdateAfterPresent(frame); + + Assert::IsTrue(chain.animationErrorSource == AnimationErrorSource::PCLatency); + Assert::AreEqual(uint64_t(30'000), chain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(30'000), chain.firstAppSimStartTime); + } + }; + + TEST_CLASS(UpdateAfterPresentFlipDelayTests) + { + public: + TEST_METHOD(UpdateAfterPresent_FlipDelayTracking_PresentedWithDisplays_SetsFlipDelayAndScreenTime) + { + SwapChainCoreState chain{}; + auto frame = MakeFrame(PresentResult::Presented, 10'000, 50, 10'300, + { + { FrameType::Application, 10'800 }, + { FrameType::Repeated, 11'000 } + }, + 0, 0, 1234 /* flipDelay */); + + chain.UpdateAfterPresent(frame); + + Assert::AreEqual(uint64_t(11'000), chain.lastDisplayedScreenTime); + Assert::AreEqual(uint64_t(1234), chain.lastDisplayedFlipDelay); + } + + TEST_METHOD(UpdateAfterPresent_FlipDelayTracking_PresentedNoDisplays_ZeroesFlipDelayAndScreenTime) + { + SwapChainCoreState chain{}; + auto frame = MakeFrame(PresentResult::Presented, 12'000, 40, 12'300, + {}, 0, 0, 9999); + + chain.UpdateAfterPresent(frame); + + Assert::AreEqual(uint64_t(0), chain.lastDisplayedScreenTime); + Assert::AreEqual(uint64_t(0), chain.lastDisplayedFlipDelay); + } + + TEST_METHOD(UpdateAfterPresent_NotPresented_DoesNotChangeLastDisplayedScreenTime) + { + SwapChainCoreState chain{}; + // Seed previous displayed state + FrameData prev = MakeFrame(PresentResult::Presented, 1'000, 30, 1'200, + { { FrameType::Application, 1'500 } }); + chain.UpdateAfterPresent(prev); + Assert::AreEqual(uint64_t(1'500), chain.lastDisplayedScreenTime); + + // Not presented frame with displays (ignored for displayed tracking) + auto frame = MakeFrame(static_cast(7777), 2'000, 25, 2'150, + { { FrameType::Application, 2'600 } }); + + chain.UpdateAfterPresent(frame); + + Assert::AreEqual(uint64_t(1'500), chain.lastDisplayedScreenTime, L"Should remain unchanged."); + } + }; + TEST_CLASS(FrameTypeXefgAfmfIndexingTests) + { + public: + TEST_METHOD(DisplayIndexing_IntelXefg_Multi_NoNext_AppIndexIsLast) + { + // 3x Intel_XEFG then a single Application + FrameData present = MakeFrame( + PresentResult::Presented, + 10'000, 500, 20'000, + { + { FrameType::Intel_XEFG, 11'000 }, + { FrameType::Intel_XEFG, 11'500 }, + { FrameType::Intel_XEFG, 12'000 }, + { FrameType::Application, 12'500 }, + }); + + auto idx = DisplayIndexing::Calculate(present, nullptr); + + // No nextDisplayed: process [0..N-2] => [0..3) + Assert::AreEqual(size_t(0), idx.startIndex); + Assert::AreEqual(size_t(3), idx.endIndex); + // App frame is at index 3 (outside processing range, postponed) + Assert::AreEqual(size_t(3), idx.appIndex); + Assert::IsFalse(idx.hasNextDisplayed); + } + + TEST_METHOD(DisplayIndexing_AmdAfmf_Multi_WithNext_AppIndexProcessed) + { + // 3x AMD_AFMF then a single Application + FrameData present = MakeFrame( + PresentResult::Presented, + 20'000, 600, 30'000, + { + { FrameType::AMD_AFMF, 21'000 }, + { FrameType::AMD_AFMF, 21'500 }, + { FrameType::AMD_AFMF, 22'000 }, + { FrameType::Application, 22'500 }, + }); + + FrameData nextDisplayed = MakeFrame( + PresentResult::Presented, + 23'000, 400, 30'500, + { { FrameType::Application, 24'000 } }); + + auto idx = DisplayIndexing::Calculate(present, &nextDisplayed); + + // With nextDisplayed: process postponed last only => [N-1, N) => [3, 4) + Assert::AreEqual(size_t(3), idx.startIndex); + Assert::AreEqual(size_t(4), idx.endIndex); + Assert::AreEqual(size_t(3), idx.appIndex); + Assert::IsTrue(idx.hasNextDisplayed); + } + }; + + TEST_CLASS(FrameTypeXefgAfmfMetricsTests) + { + public: + TEST_METHOD(ComputeMetricsForPresent_IntelXefg_NoNext_AppNotProcessed_ChainNotUpdated) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // 3x Intel_XEFG then 1 Application; no nextDisplayed + FrameData present = MakeFrame( + PresentResult::Presented, + 30'000, 700, 40'000, + { + { FrameType::Intel_XEFG, 31'000 }, + { FrameType::Intel_XEFG, 31'500 }, + { FrameType::Intel_XEFG, 32'000 }, + { FrameType::Application, 32'500 }, + }); + + auto metrics = ComputeMetricsForPresent(qpc, present, nullptr, chain); + + // Should process all but last => 3 metrics + Assert::AreEqual(size_t(3), metrics.size()); + // Chain update postponed until nextDisplayed + Assert::IsFalse(chain.lastPresent.has_value()); + Assert::IsFalse(chain.lastAppPresent.has_value()); + Assert::AreEqual(uint64_t(0), chain.lastDisplayedScreenTime); + Assert::AreEqual(uint64_t(0), chain.lastDisplayedFlipDelay); + } + + TEST_METHOD(ComputeMetricsForPresent_IntelXefg_Discarded_NoNext_ChainNotUpdated) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // 3x Intel_XEFG then 1 Application; no nextDisplayed + FrameData present = MakeFrame( + PresentResult::Discarded, + 30'000, 700, 40'000, + { + { FrameType::Intel_XEFG, 0 }, + }); + + auto metrics = ComputeMetricsForPresent(qpc, present, nullptr, chain); + + // Should process 1 + Assert::AreEqual(size_t(1), metrics.size()); + // Chain update postponed until nextDisplayed + const auto& m = metrics[0].metrics; + Assert::IsTrue(FrameType::Intel_XEFG == m.frameType, L"FrameType should be Intel_XEFG"); + } + TEST_METHOD(ComputeMetricsForPresent_AmdAfmf_WithNext_AppProcessedAndUpdatesChain) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // 3x AMD_AFMF then 1 Application; with nextDisplayed provided + FrameData present = MakeFrame( + PresentResult::Presented, + 40'000, 650, 50'000, + { + { FrameType::AMD_AFMF, 41'000 }, + { FrameType::AMD_AFMF, 41'400 }, + { FrameType::AMD_AFMF, 41'800 }, + { FrameType::Application, 42'200 }, + }, + 39'500, /* appSimStartTime*/ + 0, /* pclSimStartTime*/ + 999 /* flipDelay*/); + + FrameData nextDisplayed = MakeFrame( + PresentResult::Presented, + 43'000, 500, 50'500, + { { FrameType::Application, 44'000 } }); + + auto metrics = ComputeMetricsForPresent(qpc, present, &nextDisplayed, chain); + + // Should process only postponed last => 1 metrics + Assert::AreEqual(size_t(1), metrics.size()); + + // UpdateAfterPresent has run + Assert::IsTrue(chain.lastPresent.has_value()); + Assert::IsTrue(chain.lastAppPresent.has_value(), L"Last displayed is Application; lastAppPresent should be updated."); + Assert::AreEqual(uint64_t(42'200), chain.lastDisplayedScreenTime); + Assert::AreEqual(uint64_t(999), chain.lastDisplayedFlipDelay); + } + }; + TEST_CLASS(DisplayedDroppedDisplayedSequenceTests) + { + public: + TEST_METHOD(Displayed_Dropped_Displayed_Sequence_IsHandledAcrossCalls) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // A: displayed once, but no nextDisplayed yet => postponed + FrameData A = MakeFrame( + PresentResult::Presented, + 50'000, 400, 50'500, + { { FrameType::Application, 51'000 } }); + + auto mA_pre = ComputeMetricsForPresent(qpc, A, nullptr, chain); + Assert::AreEqual(size_t(0), mA_pre.size(), L"Single display postponed."); + Assert::IsFalse(chain.lastPresent.has_value(), L"Chain is not updated without nextDisplayed."); + + // B: dropped (not presented/displayed) + FrameData B = MakeFrame( + PresentResult::Discarded, + 52'000, 300, 52'400, + {} /* no displayed */); + + auto mB = ComputeMetricsForPresent(qpc, B, nullptr, chain); + Assert::AreEqual(size_t(1), mB.size(), L"Dropped frame goes through not-displayed path."); + Assert::IsTrue(chain.lastPresent.has_value(), L"Not-displayed path updates chain."); + Assert::IsTrue(chain.lastAppPresent.has_value(), L"Not-displayed frame becomes lastAppPresent."); + Assert::AreEqual(uint64_t(0), chain.lastDisplayedScreenTime, L"Not-displayed should leave lastDisplayedScreenTime at 0."); + + // C: displayed next; use it to process A's postponed last + FrameData C = MakeFrame( + PresentResult::Presented, + 53'000, 350, 53'400, + { { FrameType::Application, 54'000 } }); + + auto mA_post = ComputeMetricsForPresent(qpc, A, &C, chain); + Assert::AreEqual(size_t(1), mA_post.size(), L"Postponed last display of A processed with nextDisplayed."); + + // Chain updated based on A (last display instance) + Assert::IsTrue(chain.lastPresent.has_value()); + Assert::AreEqual(uint64_t(51'000), chain.lastDisplayedScreenTime); + } + }; + + TEST_CLASS(MetricsValueTests) + { + TEST_METHOD(ComputeMetricsForPresent_NotDisplayed_msBetweenPresents_UsesLastPresentDelta) + { + // 10MHz QPC frequency + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // First frame: not displayed path (Presented but no Displayed entries) + auto first = MakeFrame( + PresentResult::Presented, + /*presentStartTime*/ 1'000'000, + /*timeInPresent*/ 10'000, + /*readyTime*/ 1'020'000, + /*displayed*/{}); // no displayed frames => not-displayed path + + auto firstMetrics = ComputeMetricsForPresent(qpc, first, nullptr, chain); + + // We should get exactly one metrics entry + Assert::AreEqual(size_t(1), firstMetrics.size(), L"First not-displayed frame should produce one metrics entry."); + + // With no prior lastPresent, msBetweenPresents should be zero + Assert::AreEqual( + 0.0, + firstMetrics[0].metrics.msBetweenPresents, + 0.0001, + L"First frame should have msBetweenPresents == 0."); + + // Chain should now treat this as lastPresent / lastAppPresent + Assert::IsTrue(chain.lastPresent.has_value()); + if (!chain.lastPresent.has_value()) + { + Assert::Fail(L"lastPresent was unexpectedly empty."); + return; + } + const auto& last = chain.lastPresent.value(); + Assert::AreEqual(uint64_t(1'000'000), last.presentStartTime); + + // Second frame: also not displayed, later in time + auto second = MakeFrame( + PresentResult::Presented, + /*presentStartTime*/ 1'016'660, // ~16.666 ms later at 10MHz + /*timeInPresent*/ 10'000, + /*readyTime*/ 1'036'660, + /*displayed*/{}); + + auto secondMetrics = ComputeMetricsForPresent(qpc, second, nullptr, chain); + + Assert::AreEqual( + size_t(1), + secondMetrics.size(), + L"Second not-displayed frame should also produce one metrics entry."); + + // Expected delta: use the same converter the implementation uses + double expectedDelta = qpc.DeltaUnsignedMilliSeconds( + first.presentStartTime, + second.presentStartTime); + + Assert::AreEqual( + expectedDelta, + secondMetrics[0].metrics.msBetweenPresents, + 0.0001, + L"msBetweenPresents should equal the unsigned delta between lastPresent and current presentStartTime."); + + } + TEST_METHOD(ComputeMetricsForPresent_NotDisplayed_BaseTimingAndCpuStart_AreCorrect) + { + // 10 MHz QPC: 10,000,000 ticks per second + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // First frame: not displayed, becomes the baseline lastPresent/lastAppPresent. + FrameData first = MakeFrame( + PresentResult::Presented, + /*presentStartTime*/ 1'000'000, // 0.1s + /*timeInPresent*/ 200'000, // 0.02s + /*readyTime*/ 1'500'000, // 0.15s → 50 ms after start + /*displayed*/{} // no displays => "not displayed" path + ); + first.gpuStartTime = 1'200'000; // 0.12s + + auto firstMetricsList = ComputeMetricsForPresent(qpc, first, nullptr, chain); + Assert::AreEqual( + size_t(1), + firstMetricsList.size(), + L"First not-displayed frame should produce one metrics entry."); + + const auto& firstMetrics = firstMetricsList[0].metrics; + + uint64_t expectedTimeInSecondsFirst = first.presentStartTime; + Assert::AreEqual( + expectedTimeInSecondsFirst, + firstMetrics.timeInSeconds, + L"timeInSeconds should come from QpcToSeconds(presentStartTime)."); + + // No prior lastPresent → msBetweenPresents should be 0 + Assert::AreEqual( + 0.0, + firstMetrics.msBetweenPresents, + 0.0001, + L"First frame should have msBetweenPresents == 0."); + + // msInPresentApi = delta for TimeInPresent + double expectedMsInPresentFirst = qpc.DurationMilliSeconds(first.timeInPresent); + Assert::AreEqual( + expectedMsInPresentFirst, + firstMetrics.msInPresentApi, + 0.0001, + L"msInPresentApi should equal QpcDeltaToMilliSeconds(timeInPresent)."); + + // msUntilRenderComplete = delta between PresentStart and Ready + double expectedMsUntilRenderCompleteFirst = + qpc.DeltaUnsignedMilliSeconds(first.presentStartTime, first.readyTime); + Assert::AreEqual( + expectedMsUntilRenderCompleteFirst, + firstMetrics.msUntilRenderComplete, + 0.0001, + L"msUntilRenderComplete should equal delta from PresentStartTime to ReadyTime."); + + // msUntilRenderStart = delta between PresentStart and GPU start + double expectedMsUntilRenderStart = + qpc.DeltaUnsignedMilliSeconds(first.presentStartTime, first.gpuStartTime); + Assert::AreEqual( + expectedMsUntilRenderStart, + firstMetrics.msUntilRenderStart, + 0.0001, + L"msUntilRenderStart should equal delta from PresentStartTime to GPUStartTime."); + + // With no prior present, CalculateCPUStart should return 0 → cpuStartQpc == 0 + Assert::AreEqual( + uint64_t(0), + firstMetrics.cpuStartQpc, + L"First frame with no history should have cpuStartQpc == 0."); + + // Chain must now have lastPresent/lastAppPresent set to 'first' + Assert::IsTrue(chain.lastPresent.has_value(), L"Expected lastPresent to be set."); + if (!chain.lastPresent.has_value()) { + Assert::Fail(L"lastPresent was unexpectedly empty."); + return; + } + const auto& lastAfterFirst = chain.lastPresent.value(); + Assert::AreEqual(first.presentStartTime, lastAfterFirst.presentStartTime); + + // ------------------------------------------------------------------------- + // Second frame: also not displayed, later in time. + // This should: + // - compute msBetweenPresents based on first→second start times + // - keep msInPresentApi/msUntilRenderComplete consistent + // - use CalculateCPUStart based on 'first' as lastAppPresent + // ------------------------------------------------------------------------- + + FrameData second = MakeFrame( + PresentResult::Presented, + /*presentStartTime*/ 1'016'000, // slightly later than first + /*timeInPresent*/ 300'000, // 0.03s + /*readyTime*/ 1'516'000, // 0.5s after first start + /*displayed*/{} // still "not displayed" path + ); + second.gpuStartTime = 1'220'000; // 0.122s + + auto secondMetricsList = ComputeMetricsForPresent(qpc, second, nullptr, chain); + Assert::AreEqual( + size_t(1), + secondMetricsList.size(), + L"Second not-displayed frame should produce one metrics entry."); + + const auto& secondMetrics = secondMetricsList[0].metrics; + + // msBetweenPresents should be based on lastPresent.start -> second.start + double expectedBetween = + qpc.DeltaUnsignedMilliSeconds(first.presentStartTime, second.presentStartTime); + Assert::AreEqual( + expectedBetween, + secondMetrics.msBetweenPresents, + 0.0001, + L"msBetweenPresents should equal delta between lastPresent and current presentStart."); + + // msInPresentApi / msUntilRenderComplete / msUntilRenderStart for second + double expectedMsInPresentSecond = qpc.DurationMilliSeconds(second.timeInPresent); + double expectedMsUntilRenderCompleteSecond = + qpc.DeltaUnsignedMilliSeconds(second.presentStartTime, second.readyTime); + double expectedMsUntilRenderStartSecond = + qpc.DeltaUnsignedMilliSeconds(second.presentStartTime, second.gpuStartTime); + + Assert::AreEqual( + expectedMsInPresentSecond, + secondMetrics.msInPresentApi, + 0.0001, + L"Second frame msInPresentApi should match timeInPresent."); + Assert::AreEqual( + expectedMsUntilRenderCompleteSecond, + secondMetrics.msUntilRenderComplete, + 0.0001, + L"Second frame msUntilRenderComplete should match start→ready delta."); + Assert::AreEqual( + expectedMsUntilRenderStartSecond, + secondMetrics.msUntilRenderStart, + 0.0001, + L"Second frame msUntilRenderStart should match start→GPU start delta."); + + // cpuStartQpc for second should come from CalculateCPUStart: + // lastAppPresent == first (no propagated times) → first.start + first.timeInPresent + uint64_t expectedCpuStartSecond = first.presentStartTime + first.timeInPresent; + Assert::AreEqual( + expectedCpuStartSecond, + secondMetrics.cpuStartQpc, + L"cpuStartQpc should match CalculateCPUStart from lastAppPresent."); + } + TEST_METHOD(ComputeMetricsForPresent_DisplayedWithNext_BaseTimingAndCpuStart_AreCorrect) + { + // 10 MHz QPC + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Baseline frame: Presented but not displayed → not-displayed path + FrameData first = MakeFrame( + PresentResult::Presented, + /*presentStartTime*/ 1'000'000, + /*timeInPresent*/ 200'000, + /*readyTime*/ 1'500'000, + /*displayed*/{}); // no displays + + auto firstMetricsList = ComputeMetricsForPresent(qpc, first, nullptr, chain); + Assert::AreEqual( + size_t(1), + firstMetricsList.size(), + L"Baseline not-displayed frame should produce one metrics entry."); + + // Chain should now have lastPresent/lastAppPresent == first + Assert::IsTrue(chain.lastPresent.has_value(), L"Expected lastPresent to be set after baseline frame."); + if (!chain.lastPresent.has_value()) { + Assert::Fail(L"lastPresent was unexpectedly empty after baseline frame."); + return; + } + + // Second frame: Presented + one displayed instance, processed with a nextDisplayed + FrameData second = MakeFrame( + PresentResult::Presented, + /*presentStartTime*/ 1'016'000, // slightly later than first + /*timeInPresent*/ 300'000, + /*readyTime*/ 1'616'000, + DisplayedVector{ + { FrameType::Application, 2'000'000 } // one displayed instance + }); + second.gpuStartTime = 1'200'000; + + // Dummy nextDisplayed with at least one display so the "with next" path is taken + FrameData nextDisplayed = MakeFrame( + PresentResult::Presented, + /*presentStartTime*/ 2'100'000, + /*timeInPresent*/ 100'000, + /*readyTime*/ 2'200'000, + DisplayedVector{ + { FrameType::Application, 2'300'000 } + }); + + auto secondMetricsList = ComputeMetricsForPresent(qpc, second, &nextDisplayed, chain); + + Assert::AreEqual( + size_t(1), + secondMetricsList.size(), + L"Displayed-with-next frame should produce one metrics entry (postponed last display)."); + + const auto& secondMetrics = secondMetricsList[0].metrics; + + // timeInSeconds from presentStartTime + auto expectedTimeInSecondsSecond = second.presentStartTime; + Assert::AreEqual( + expectedTimeInSecondsSecond, + secondMetrics.timeInSeconds, + L"timeInSeconds should match QpcToSeconds(presentStartTime) for displayed frame."); + + // msBetweenPresents: lastPresent.start (first) → second.start + double expectedBetween = + qpc.DeltaUnsignedMilliSeconds(first.presentStartTime, second.presentStartTime); + Assert::AreEqual( + expectedBetween, + secondMetrics.msBetweenPresents, + 0.0001, + L"msBetweenPresents should match delta between lastPresent and current presentStart for displayed frame."); + + // msInPresentApi from timeInPresent + double expectedMsInPresentSecond = qpc.DurationMilliSeconds(second.timeInPresent); + Assert::AreEqual( + expectedMsInPresentSecond, + secondMetrics.msInPresentApi, + 0.0001, + L"msInPresentApi should match QpcDeltaToMilliSeconds(timeInPresent) for displayed frame."); + + // msUntilRenderComplete from start → ready + double expectedMsUntilRenderCompleteSecond = + qpc.DeltaUnsignedMilliSeconds(second.presentStartTime, second.readyTime); + Assert::AreEqual( + expectedMsUntilRenderCompleteSecond, + secondMetrics.msUntilRenderComplete, + 0.0001, + L"msUntilRenderComplete should match start→ready delta for displayed frame."); + + // msUntilRenderStart from start → GPU start + double expectedMsUntilRenderStartSecond = + qpc.DeltaUnsignedMilliSeconds(second.presentStartTime, second.gpuStartTime); + Assert::AreEqual( + expectedMsUntilRenderStartSecond, + secondMetrics.msUntilRenderStart, + 0.0001, + L"msUntilRenderStart should match start→GPU start delta for displayed frame."); + + // cpuStartQpc should come from CalculateCPUStart using baseline frame as lastAppPresent: + // (no propagated times) → first.start + first.timeInPresent + uint64_t expectedCpuStartSecond = first.presentStartTime + first.timeInPresent; + Assert::AreEqual( + expectedCpuStartSecond, + secondMetrics.cpuStartQpc, + L"cpuStartQpc for displayed frame should match CalculateCPUStart based on lastAppPresent."); + } + }; + TEST_CLASS(MsUntilDisplayedTests) { + public: + TEST_METHOD(NotDisplayed_ReturnsZero) + { + QpcConverter qpc(10'000'000, 0); SwapChainCoreState chain{}; + // Not displayed: Presented but no displayed entries + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 10'000; + frame.readyTime = 1'010'000; + frame.finalState = PresentResult::Presented; + // No displayed entries + + auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + Assert::AreEqual(0.0, m.msUntilDisplayed, 0.0001); + } + TEST_METHOD(Displayed_ReturnsDeltaFromPresentStartToScreenTime) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 2'000'000; // start + frame.timeInPresent = 20'000; + frame.readyTime = 2'050'000; + frame.finalState = PresentResult::Presented; + // Single displayed; will be postponed unless nextDisplayed provided + frame.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + FrameData next{}; // provide nextDisplayed to process postponed + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 3'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + double expected = qpc.DeltaUnsignedMilliSeconds(frame.presentStartTime, frame.displayed[0].second); + Assert::AreEqual(expected, m.msUntilDisplayed, 0.0001); + } + TEST_METHOD(DisplayedGeneratedFrame_AlsoReturnsDelta) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 5'000'000; + frame.timeInPresent = 15'000; + frame.readyTime = 5'030'000; + frame.finalState = PresentResult::Presented; + // Displayed generated frame (e.g., Repeated/Composed/Desktop depending on enum) + frame.displayed.PushBack({ FrameType::Intel_XEFG, 5'100'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 6'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + double expected = qpc.DeltaUnsignedMilliSeconds(frame.presentStartTime, frame.displayed[0].second); + Assert::AreEqual(expected, m.msUntilDisplayed, 0.0001); + } + }; + TEST_CLASS(MsDisplayedTimeTests) + { + public: + TEST_METHOD(NotDisplayed_ReturnsZero) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 10'000; + frame.readyTime = 1'010'000; + frame.finalState = PresentResult::Presented; + + auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + Assert::AreEqual(0.0, m.msDisplayedTime, 0.0001); + } + + TEST_METHOD(DisplayedSingleDisplay_WithNextDisplay_ReturnsDeltaToNextScreenTime) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 2'000'000; + frame.timeInPresent = 20'000; + frame.readyTime = 2'050'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'800'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + double expected = qpc.DeltaUnsignedMilliSeconds(2'500'000, 2'800'000); + Assert::AreEqual(expected, m.msDisplayedTime, 0.0001); + } + + TEST_METHOD(DisplayedMultipleDisplays_ProcessEachWithNextScreenTime) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 3'000'000; + frame.timeInPresent = 30'000; + frame.readyTime = 3'050'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 3'100'000 }); + frame.displayed.PushBack({ FrameType::Repeated, 3'400'000 }); + frame.displayed.PushBack({ FrameType::Repeated, 3'700'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 4'000'000 }); + + auto results1 = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(2), results1.size()); + + double expected0 = qpc.DeltaUnsignedMilliSeconds(3'100'000, 3'400'000); + Assert::AreEqual(expected0, results1[0].metrics.msDisplayedTime, 0.0001); + + double expected1 = qpc.DeltaUnsignedMilliSeconds(3'400'000, 3'700'000); + Assert::AreEqual(expected1, results1[1].metrics.msDisplayedTime, 0.0001); + + auto results2 = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results2.size()); + + double expected2 = qpc.DeltaUnsignedMilliSeconds(3'700'000, 4'000'000); + Assert::AreEqual(expected2, results2[0].metrics.msDisplayedTime, 0.0001); + } + }; + + TEST_CLASS(MsBetweenDisplayChangeTests) + { + public: + TEST_METHOD(FirstDisplayedFrame_NoChainHistory_ReturnsZero) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 5'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 5'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 5'500'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 6'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + Assert::AreEqual(0.0, m.msBetweenDisplayChange, 0.0001); + } + + TEST_METHOD(SubsequentDisplayedFrame_UsesChainLastDisplayedScreenTime) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + chain.lastDisplayedScreenTime = 4'000'000; + + FrameData frame{}; + frame.presentStartTime = 5'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 5'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 5'500'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 6'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + double expected = qpc.DeltaUnsignedMilliSeconds(4'000'000, 5'500'000); + Assert::AreEqual(expected, m.msBetweenDisplayChange, 0.0001); + } + + TEST_METHOD(NotDisplayed_ReturnsZero) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + chain.lastDisplayedScreenTime = 4'000'000; + + FrameData frame{}; + frame.presentStartTime = 5'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 5'100'000; + frame.finalState = PresentResult::Presented; + + auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + Assert::AreEqual(0.0, m.msBetweenDisplayChange, 0.0001); + } + + TEST_METHOD(MultipleDisplays_EachComputesDeltaFromPrior) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + chain.lastDisplayedScreenTime = 3'000'000; + + FrameData frame{}; + frame.presentStartTime = 5'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 5'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 5'500'000 }); + frame.displayed.PushBack({ FrameType::Repeated, 5'800'000 }); + frame.displayed.PushBack({ FrameType::Repeated, 6'100'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 6'400'000 }); + + auto results1 = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(2), results1.size()); + + double expected0 = qpc.DeltaUnsignedMilliSeconds(3'000'000, 5'500'000); + Assert::AreEqual(expected0, results1[0].metrics.msBetweenDisplayChange, 0.0001); + + double expected1 = qpc.DeltaUnsignedMilliSeconds(3'000'000, 5'800'000); + Assert::AreEqual(expected1, results1[1].metrics.msBetweenDisplayChange, 0.0001); + + auto results2 = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results2.size()); + + double expected2 = qpc.DeltaUnsignedMilliSeconds(3'000'000, 6'100'000); + Assert::AreEqual(expected2, results2[0].metrics.msBetweenDisplayChange, 0.0001); + } + }; + + TEST_CLASS(MsFlipDelayTests) + { + public: + TEST_METHOD(NotDisplayed_ReturnsZero) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 7'000'000; + frame.timeInPresent = 70'000; + frame.readyTime = 7'100'000; + frame.flipDelay = 5'000; + frame.finalState = PresentResult::Presented; + + auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + if (m.msFlipDelay.has_value()) { + Assert::AreEqual(0.0, m.msFlipDelay.value(), 0.0001); + } + } + + TEST_METHOD(Displayed_WithFlipDelay_ReturnsFlipDelayInMs) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 7'000'000; + frame.timeInPresent = 70'000; + frame.readyTime = 7'100'000; + frame.flipDelay = 100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 7'500'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 8'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + if (m.msFlipDelay.has_value()) { + double expected = qpc.DurationMilliSeconds(100'000); + Assert::AreEqual(expected, m.msFlipDelay.value(), 0.0001); + } + } + + TEST_METHOD(Displayed_WithoutFlipDelay_ReturnsZero) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 7'000'000; + frame.timeInPresent = 70'000; + frame.readyTime = 7'100'000; + frame.flipDelay = 0; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 7'500'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 8'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + if (m.msFlipDelay.has_value()) { + Assert::AreEqual(0.0, m.msFlipDelay.value(), 0.0001); + } + } + + TEST_METHOD(DisplayedWithGeneratedFrame_AlsoIncludesFlipDelay) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 7'000'000; + frame.timeInPresent = 70'000; + frame.readyTime = 7'100'000; + frame.flipDelay = 50'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Repeated, 7'500'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 8'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + if (m.msFlipDelay.has_value()) { + double expected = qpc.DurationMilliSeconds(50'000); + Assert::AreEqual(expected, m.msFlipDelay.value(), 0.0001); + } + } + }; + + TEST_CLASS(ScreenTimeQpcTests) + { + public: + TEST_METHOD(NotDisplayed_ReturnsZero) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 9'000'000; + frame.timeInPresent = 90'000; + frame.readyTime = 9'100'000; + frame.finalState = PresentResult::Presented; + + auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + Assert::AreEqual(uint64_t(0), m.screenTimeQpc); + } + + TEST_METHOD(DisplayedSingleFrame_EqualsScreenTime) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 9'000'000; + frame.timeInPresent = 90'000; + frame.readyTime = 9'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 9'500'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 10'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + Assert::AreEqual(uint64_t(9'500'000), m.screenTimeQpc); + } + + TEST_METHOD(DisplayedMultipleFrames_EachHasOwnScreenTime) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 9'000'000; + frame.timeInPresent = 90'000; + frame.readyTime = 9'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 9'500'000 }); + frame.displayed.PushBack({ FrameType::Repeated, 9'800'000 }); + frame.displayed.PushBack({ FrameType::Repeated, 10'100'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 10'400'000 }); + + auto results1 = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(2), results1.size()); + Assert::AreEqual(uint64_t(9'500'000), results1[0].metrics.screenTimeQpc); + Assert::AreEqual(uint64_t(9'800'000), results1[1].metrics.screenTimeQpc); + + auto results2 = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results2.size()); + Assert::AreEqual(uint64_t(10'100'000), results2[0].metrics.screenTimeQpc); + } + + TEST_METHOD(DisplayedGeneratedFrame_EqualsGeneratedFrameScreenTime) + { + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 9'000'000; + frame.timeInPresent = 90'000; + frame.readyTime = 9'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Repeated, 9'700'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 10'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + Assert::AreEqual(uint64_t(9'700'000), m.screenTimeQpc); + } + }; + TEST_CLASS(NvCollapsedPresentTests) + { + public: + TEST_METHOD(NvCollapsedPresent_AdjustsNextScreenTimeAndFlipDelay) + { + // Mirrors AdjustScreenTimeForCollapsedPresentNV behavior: + // When current frame's screenTime > nextFrame's screenTime and current has flipDelay, + // the next frame's screenTime and flipDelay are adjusted upward. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // First frame: collapsed present with significant flipDelay + // Its adjusted screenTime will be later than the next frame's raw screenTime + FrameData first{}; + first.presentStartTime = 4'000'000; + first.timeInPresent = 50'000; + first.readyTime = 4'100'000; + first.flipDelay = 200'000; // 0.02ms at 10MHz + first.finalState = PresentResult::Presented; + // First's screen time is 5'500'000 + first.displayed.PushBack({ FrameType::Application, 5'500'000 }); + + // Second frame (next displayed) + FrameData second{}; + second.presentStartTime = 5'000'000; + second.timeInPresent = 40'000; + second.readyTime = 5'100'000; + second.flipDelay = 100'000; // Original flip delay for second frame + second.finalState = PresentResult::Presented; + // Second's raw screen time is 5'000'000, which is EARLIER than first's (5'500'000) + // This triggers NV2 adjustment + second.displayed.PushBack({ FrameType::Application, 5'000'000 }); + + // Process first frame with second as nextDisplayed + auto resultsFirst = ComputeMetricsForPresent(qpc, first, &second, chain); + Assert::AreEqual(size_t(1), resultsFirst.size()); + + // Now process second frame (which should have been adjusted by NV2) + FrameData third{}; + third.finalState = PresentResult::Presented; + third.displayed.PushBack({ FrameType::Application, 6'000'000 }); + + auto resultsSecond = ComputeMetricsForPresent(qpc, second, &third, chain); + Assert::AreEqual(size_t(1), resultsSecond.size()); + const auto& secondMetrics = resultsSecond[0].metrics; + + // NV2 adjustment: second's screenTime should be raised to first's screenTime + // when first.screenTime (5'500'000) > second.screenTime (5'000'000) + Assert::AreEqual(uint64_t(5'500'000), secondMetrics.screenTimeQpc, + L"NV2 should adjust second's screenTime to first's screenTime (5'500'000)"); + + // NV2 adjustment: second's flipDelay should be increased by the difference + // effectiveSecondFlipDelay = 100'000 + (5'500'000 - 5'000'000) = 100'000 + 500'000 = 600'000 + uint64_t expectedEffectiveFlipDelaySecond = 100'000 + (5'500'000 - 5'000'000); + double expectedMsFlipDelaySecond = qpc.DurationMilliSeconds(expectedEffectiveFlipDelaySecond); + + Assert::IsTrue(secondMetrics.msFlipDelay.has_value(), + L"msFlipDelay should be set for displayed frame"); + if (secondMetrics.msFlipDelay.has_value()) { + Assert::AreEqual(expectedMsFlipDelaySecond, secondMetrics.msFlipDelay.value(), 0.0001, + L"NV2 should adjust second's flipDelay to account for screenTime catch-up"); + } + } + + TEST_METHOD(NvCollapsedPresent_NoCollapse_ScreenTimesAndFlipDelaysUnchanged) + { + // Sanity check: when there is NO collapsed present condition, + // screen times and flip delays should pass through unchanged. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior displayed frame with screen time and flip delay + chain.lastDisplayedScreenTime = 3'000'000; + chain.lastDisplayedFlipDelay = 50'000; + + // Current frame with LATER screen time (no collapse) + FrameData current{}; + current.presentStartTime = 4'000'000; + current.timeInPresent = 50'000; + current.readyTime = 4'100'000; + current.flipDelay = 75'000; + current.finalState = PresentResult::Presented; + // Current screen time is LATER than lastDisplayedScreenTime, so no NV1 adjustment + current.displayed.PushBack({ FrameType::Application, 4'000'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 5'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, current, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& metrics = results[0].metrics; + + // No NV1 adjustment: screenTime should remain unchanged + Assert::AreEqual(uint64_t(4'000'000), metrics.screenTimeQpc, + L"No collapse: screenTime should remain at original value"); + + // No adjustment to flipDelay: should use original 75'000 + double expectedMsFlipDelay = qpc.DurationMilliSeconds(75'000); + + Assert::IsTrue(metrics.msFlipDelay.has_value(), + L"msFlipDelay should be set for displayed frame"); + if (metrics.msFlipDelay.has_value()) { + Assert::AreEqual(expectedMsFlipDelay, metrics.msFlipDelay.value(), 0.0001, + L"No collapse: flipDelay should remain at original value"); + } + } + + TEST_METHOD(NvCollapsedPresent_OnlyAdjustsWhenFirstScreenTimeGreaterThanSecond) + { + // NV2 should only adjust when first.screenTime > second.screenTime. + // This test verifies that when second.screenTime >= first.screenTime, + // no adjustment occurs. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // First frame with flip delay + FrameData first{}; + first.presentStartTime = 4'000'000; + first.timeInPresent = 50'000; + first.readyTime = 4'100'000; + first.flipDelay = 100'000; + first.finalState = PresentResult::Presented; + // First screen time is 5'000'000 + first.displayed.PushBack({ FrameType::Application, 5'000'000 }); + + // Second frame with screen time >= first (no collapse condition) + FrameData second{}; + second.presentStartTime = 5'000'000; + second.timeInPresent = 40'000; + second.readyTime = 5'100'000; + second.flipDelay = 50'000; + second.finalState = PresentResult::Presented; + // Second screen time is equal to first (5'000'000), so NV2 should NOT adjust + second.displayed.PushBack({ FrameType::Application, 5'000'000 }); + + auto resultsFirst = ComputeMetricsForPresent(qpc, first, &second, chain); + Assert::AreEqual(size_t(1), resultsFirst.size()); + + FrameData third{}; + third.finalState = PresentResult::Presented; + third.displayed.PushBack({ FrameType::Application, 6'000'000 }); + + auto resultsSecond = ComputeMetricsForPresent(qpc, second, &third, chain); + Assert::AreEqual(size_t(1), resultsSecond.size()); + const auto& secondMetrics = resultsSecond[0].metrics; + + // NV2 should NOT adjust: second's screenTime should remain at 5'000'000 + Assert::AreEqual(uint64_t(5'000'000), secondMetrics.screenTimeQpc, + L"NV2: when second.screenTime >= first.screenTime, no adjustment should occur"); + + // flipDelay should remain at original 50'000 + double expectedMsFlipDelay = qpc.DurationMilliSeconds(50'000); + + Assert::IsTrue(secondMetrics.msFlipDelay.has_value(), + L"msFlipDelay should be set for displayed frame"); + if (secondMetrics.msFlipDelay.has_value()) { + Assert::AreEqual(expectedMsFlipDelay, secondMetrics.msFlipDelay.value(), 0.0001, + L"NV2: when no collapse, flipDelay should remain unchanged"); + } + } + + + TEST_METHOD(NvCollapsedPresent_V1_AdjustsCurrentScreenTimeAndFlipDelay) + { + // Legacy PresentMon V1 behavior: when the previous displayed screen time (adjusted by flipDelay) + // is greater than the current present's screen time, treat the current as a collapsed/runt frame + // and adjust *this* present's screen time + flipDelay. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + chain.lastDisplayedScreenTime = 5'500'000; + chain.lastDisplayedFlipDelay = 50'000; + + FrameData current{}; + current.presentStartTime = 4'000'000; + current.timeInPresent = 50'000; + current.readyTime = 4'100'000; + current.flipDelay = 100'000; + current.finalState = PresentResult::Presented; + current.displayed.PushBack({ FrameType::Application, 5'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, current, nullptr, chain, MetricsVersion::V1); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + Assert::AreEqual(uint64_t(5'500'000), m.screenTimeQpc, + L"NV1 should adjust current screenTime to lastDisplayedScreenTime"); + + const uint64_t expectedFlipDelay = 100'000 + (5'500'000 - 5'000'000); + Assert::IsTrue(m.msFlipDelay.has_value(), L"msFlipDelay should be set for displayed frame"); + if (m.msFlipDelay.has_value()) { + Assert::AreEqual(qpc.DurationMilliSeconds(expectedFlipDelay), m.msFlipDelay.value(), 0.0001, + L"NV1 should adjust current flipDelay to account for screenTime catch-up"); + } + + // Validate the legacy-style mutation of the current present and that chain advanced using adjusted values. + Assert::AreEqual(uint64_t(5'500'000), current.displayed[0].second, + L"NV1 should update current.displayed[0].second"); + Assert::AreEqual(expectedFlipDelay, current.flipDelay, + L"NV1 should update current.flipDelay"); + Assert::AreEqual(uint64_t(5'500'000), chain.lastDisplayedScreenTime, + L"Chain should latch adjusted screenTime"); + } + + }; + TEST_CLASS(DisplayLatencyTests) + { + public: + TEST_METHOD(DisplayLatency_SimpleCase_PositiveDelta) + { + // Scenario: Single displayed frame with well-separated timestamps. + // cpuStart = 1'000'000, screenTime = 2'000'000 + // QPC frequency 10 MHz → 1'000'000 ticks = 0.1 ms + // Expected: msDisplayLatency ≈ 0.1 ms + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 1'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + // Set up chain with prior app present to establish cpuStart + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + chain.lastAppPresent = priorApp; + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + // cpuStart = 800'000 + 200'000 = 1'000'000 + // msDisplayLatency = screenTime - cpuStart = 2'000'000 - 1'000'000 = 1'000'000 ticks = 0.1 ms + double expected = qpc.DeltaUnsignedMilliSeconds(1'000'000, 2'000'000); + Assert::AreEqual(expected, m.msDisplayLatency, 0.0001); + } + + TEST_METHOD(DisplayLatency_CpuStartEqualsScreenTime) + { + // Scenario: CPU work finishes exactly when frame displays (degenerate case). + // cpuStart = screenTime = 2'000'000 + // Expected: msDisplayLatency = 0.0 ms + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 1'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + FrameData priorApp{}; + priorApp.presentStartTime = 1'700'000; + priorApp.timeInPresent = 300'000; + chain.lastAppPresent = priorApp; + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + // cpuStart = 1'700'000 + 300'000 = 2'000'000 + // msDisplayLatency = 2'000'000 - 2'000'000 = 0 + Assert::AreEqual(0.0, m.msDisplayLatency, 0.0001); + } + + TEST_METHOD(DisplayLatency_NotDisplayed_ReturnsZero) + { + // Scenario: Frame with no displayed entries (not displayed). + // Expected: msDisplayLatency = 0.0 ms + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 1'100'000; + frame.finalState = PresentResult::Presented; + // No displayed entries + + auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + Assert::AreEqual(0.0, m.msDisplayLatency, 0.0001); + } + + TEST_METHOD(DisplayLatency_ZeroCpuStart) + { + // Scenario: No prior chain history; cpuStart defaults to 0. + // cpuStart = 0, screenTime = 3'000'000 + // Expected: msDisplayLatency ≈ 0.3 ms + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + // No prior app present set + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 1'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 3'000'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 3'500'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + // cpuStart = 0 (no prior app present) + // msDisplayLatency = 3'000'000 - 0 = 3'000'000 ticks = 0.3 ms + double expected = qpc.DeltaUnsignedMilliSeconds(0, 3'000'000); + Assert::AreEqual(expected, m.msDisplayLatency, 0.0001); + } + }; + + TEST_CLASS(ReadyTimeToDisplayLatencyTests) + { + public: + TEST_METHOD(ReadyTimeToDisplay_SimpleCase_PositiveDelta) + { + // Scenario: Single displayed frame with GPU ready time before screen time. + // readyTime = 1'500'000, screenTime = 2'000'000 + // QPC 10 MHz: 500'000 ticks = 0.05 ms + // Expected: msReadyTimeToDisplayLatency ≈ 0.05 ms + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 1'500'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + // msReadyTimeToDisplayLatency = screenTime - readyTime = 2'000'000 - 1'500'000 = 500'000 ticks = 0.05 ms + double expected = qpc.DeltaUnsignedMilliSeconds(1'500'000, 2'000'000); + Assert::IsTrue(m.msReadyTimeToDisplayLatency.has_value()); + Assert::AreEqual(expected, m.msReadyTimeToDisplayLatency.value(), 0.0001); + } + + TEST_METHOD(ReadyTimeToDisplay_ReadyTimeEqualsScreenTime) + { + // Scenario: GPU finishes exactly when frame displays. + // readyTime = screenTime = 2'000'000 + // Expected: msReadyTimeToDisplayLatency = 0.0 ms + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 2'000'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + Assert::IsTrue(m.msReadyTimeToDisplayLatency.has_value()); + Assert::AreEqual(0.0, m.msReadyTimeToDisplayLatency.value(), 0.0001); + } + + TEST_METHOD(ReadyTimeToDisplay_NotDisplayed_ReturnsZero) + { + // Scenario: Frame with no displayed entries. + // Expected: msReadyTimeToDisplayLatency = 0.0 ms + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 1'500'000; + frame.finalState = PresentResult::Presented; + // No displayed entries + + auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + Assert::IsFalse(m.msReadyTimeToDisplayLatency.has_value()); + } + + TEST_METHOD(ReadyTimeToDisplay_ReadyTimeZero) + { + // Scenario: Ready time not set (edge case, readyTime = 0). + // readyTime = 70'000, screenTime = 2'000'000 + // Expected: msReadyTimeToDisplayLatency ≈ 0.2 ms (2'000'000 ticks) + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 70'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + // msReadyTimeToDisplayLatency = 2'000'000 - 70'000 = 1'930'000 ticks = 0.193 ms + double expected = qpc.DeltaUnsignedMilliSeconds(70'000, 2'000'000); + Assert::IsTrue(m.msReadyTimeToDisplayLatency.has_value()); + Assert::AreEqual(expected, m.msReadyTimeToDisplayLatency.value(), 0.0001); + } + }; + + TEST_CLASS(MultiDisplayLatencyTests) + { + public: + TEST_METHOD(DisplayLatency_MultipleDisplays_EachComputesIndependently) + { + // Scenario: Single FrameData with 3 displayed instances (e.g., frame interpolation). + // Display 0: screenTime = 2'000'000 + // Display 1: screenTime = 2'100'000 + // Display 2: screenTime = 2'200'000 + // cpuStart = 1'000'000 (same for all) + // QPC 10 MHz + // Expected: + // Metrics[0].msDisplayLatency ≈ 0.1 ms + // Metrics[1].msDisplayLatency ≈ 0.11 ms + // Metrics[2].msDisplayLatency ≈ 0.12 ms + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 1'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 2'000'000 }); + frame.displayed.PushBack({ FrameType::Repeated, 2'100'000 }); + frame.displayed.PushBack({ FrameType::Repeated, 2'200'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + chain.lastAppPresent = priorApp; + + // First call without next: process [0..1] + auto results1 = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(2), results1.size()); + + // First display: cpuStart = 1'000'000, screenTime = 2'000'000 → 0.1 ms + double expected0 = qpc.DeltaUnsignedMilliSeconds(1'000'000, 2'000'000); + Assert::AreEqual(expected0, results1[0].metrics.msDisplayLatency, 0.0001); + + // Second display: cpuStart = 1'000'000, screenTime = 2'100'000 → 0.11 ms + double expected1 = qpc.DeltaUnsignedMilliSeconds(1'000'000, 2'100'000); + Assert::AreEqual(expected1, results1[1].metrics.msDisplayLatency, 0.0001); + + // Second call with next: process [2] + auto results2 = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results2.size()); + + // Third display: cpuStart = 1'000'000, screenTime = 2'200'000 → 0.12 ms + double expected2 = qpc.DeltaUnsignedMilliSeconds(1'000'000, 2'200'000); + Assert::AreEqual(expected2, results2[0].metrics.msDisplayLatency, 0.0001); + } + + TEST_METHOD(ReadyTimeToDisplay_MultipleDisplays_IndependentDeltas) + { + // Scenario: Multiple displays, each with different screenTime, but same readyTime. + // readyTime = 1'500'000 (single value for the frame) + // Display 0: screenTime = 2'000'000 + // Display 1: screenTime = 2'100'000 + // Display 2: screenTime = 2'200'000 + // QPC 10 MHz + // Expected: + // Metrics[0].msReadyTimeToDisplayLatency ≈ 0.05 ms (500'000 ticks) + // Metrics[1].msReadyTimeToDisplayLatency ≈ 0.06 ms (600'000 ticks) + // Metrics[2].msReadyTimeToDisplayLatency ≈ 0.07 ms (700'000 ticks) + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 1'500'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 2'000'000 }); + frame.displayed.PushBack({ FrameType::Intel_XEFG, 2'100'000 }); + frame.displayed.PushBack({ FrameType::Intel_XEFG, 2'200'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + chain.lastAppPresent = priorApp; + + auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(2), results.size()); + + // Display 0: readyTime = 1'500'000, screenTime = 2'000'000 → 0.05 ms + double expected0 = qpc.DeltaUnsignedMilliSeconds(1'500'000, 2'000'000); + Assert::IsTrue(results[0].metrics.msReadyTimeToDisplayLatency.has_value()); + Assert::AreEqual(expected0, results[0].metrics.msReadyTimeToDisplayLatency.value(), 0.0001); + + // Display 0: readyTime = 1'500'000, screenTime = 2'000'000 → 0.05 ms + double expected1 = qpc.DeltaUnsignedMilliSeconds(1'500'000, 2'100'000); + Assert::IsTrue(results[1].metrics.msReadyTimeToDisplayLatency.has_value()); + Assert::AreEqual(expected1, results[1].metrics.msReadyTimeToDisplayLatency.value(), 0.0001); + + // Second call with next: process [2] + auto results2 = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results2.size()); + double expected2 = qpc.DeltaUnsignedMilliSeconds(1'500'000, 2'200'000); + Assert::IsTrue(results2[0].metrics.msReadyTimeToDisplayLatency.has_value()); + Assert::AreEqual(expected2, results2[0].metrics.msReadyTimeToDisplayLatency.value(), 0.0001); + } + }; + + TEST_CLASS(NvCollapsedPresentLatencyTests) + { + public: + TEST_METHOD(DisplayLatency_NvCollapsed_AdjustedScreenTime) + { + // Scenario: NV collapse adjustment modifies screenTime before metric computation. + // cpuStart = 1'000'000 + // Display 0: screenTime = 4'000'000 + // Display 1: screenTime = 3'000'000 + // QPC 10 MHz + // Assume the unified code applies NV adjustment + // Expected: msDisplayLatency ≈ 0.295 ms (using adjusted screenTime 4'000'000 − 1'050'000) + // Expected: msFlipDelay ≈ 0.103 ms (original 30'000 + adjustment) + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 1'100'000; + frame.flipDelay = 50'000; + frame.finalState = PresentResult::Presented; + // Raw screen time is 4'000'000, greater than next screen time + frame.displayed.PushBack({ FrameType::Application, 4'000'000 }); + + FrameData next1{}; + next1.presentStartTime = 2'000'000; + next1.timeInPresent = 50'000; + next1.readyTime = 2'100'000; + next1.flipDelay = 30'000; + next1.finalState = PresentResult::Presented; + next1.displayed.PushBack({ FrameType::Application, 3'000'000 }); + + FrameData next2{}; + next2.presentStartTime = 3'000'000; + next2.timeInPresent = 50'000; + next2.readyTime = 3'100'000; + next2.finalState = PresentResult::Presented; + next2.displayed.PushBack({ FrameType::Application, 5'000'000 }); + + // Set up chain with prior app present to establish cpuStart + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + chain.lastAppPresent = priorApp; + + auto results1 = ComputeMetricsForPresent(qpc, frame, &next1, chain); + Assert::AreEqual(size_t(1), results1.size()); + + // No adjust of first frame msDisplayLatency = 4'000'000 - 1'000'000 = 3'000'000 ticks = 0.3 ms + double expectedDisplayLatency = qpc.DeltaUnsignedMilliSeconds(1'000'000, 4'000'000); + Assert::AreEqual(expectedDisplayLatency, results1[0].metrics.msDisplayLatency, 0.0001); + double expectedFlipDelay = qpc.DurationMilliSeconds(frame.flipDelay); + Assert::IsTrue(results1[0].metrics.msFlipDelay.has_value()); + Assert::AreEqual(expectedFlipDelay, results1[0].metrics.msFlipDelay.value(), 0.0001); + + auto results2 = ComputeMetricsForPresent(qpc, next1, &next2, chain); + Assert::AreEqual(size_t(1), results1.size()); + + // After NV adjustment: screenTime = 4'000'000 -> set from NV FlipDelay adjustment + // msDisplayLatency = 4'000'000 - 1'050'000 = 2'950'000 ticks = 0.295 ms + // msFlipDelay = original 30'000 + (4'000'000 - 3'000'000) = 1'030'000 ticks = 0.103 ms + double expectedDisplayLatency2 = qpc.DeltaUnsignedMilliSeconds(1'050'000, 4'000'000); + Assert::AreEqual(expectedDisplayLatency2, results2[0].metrics.msDisplayLatency, 0.0001); + double expectedFlipDelay2 = qpc.DurationMilliSeconds(1'030'000); + Assert::IsTrue(results2[0].metrics.msFlipDelay.has_value()); + Assert::AreEqual(expectedFlipDelay2, results2[0].metrics.msFlipDelay.value(), 0.0001); + } + + TEST_METHOD(ReadyTimeToDisplay_NvCollapsed_UsesAdjustedScreenTime) + { + // Scenario: NV collapse adjustment affects ReadyTimeToDisplay metric. + // Adjusted screenTime = 4'000'000 + // readyTime = 2'100'000 + // QPC 10 MHz + // Expected: msReadyTimeToDisplayLatency ≈ 0.19 ms (4'000'000 - 2'100'000 = 1'900'000 ticks) + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 1'100'000; + frame.flipDelay = 50'000; + frame.finalState = PresentResult::Presented; + // Raw screen time is 4'000'000, greater than next screen time + frame.displayed.PushBack({ FrameType::Application, 4'000'000 }); + + FrameData next1{}; + next1.presentStartTime = 2'000'000; + next1.timeInPresent = 50'000; + next1.readyTime = 2'100'000; + next1.flipDelay = 30'000; + next1.finalState = PresentResult::Presented; + next1.displayed.PushBack({ FrameType::Application, 3'000'000 }); + + FrameData next2{}; + next2.presentStartTime = 3'000'000; + next2.timeInPresent = 50'000; + next2.readyTime = 3'100'000; + next2.finalState = PresentResult::Presented; + next2.displayed.PushBack({ FrameType::Application, 5'000'000 }); + + // Set up chain with prior app present to establish cpuStart + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + chain.lastAppPresent = priorApp; + + auto results1 = ComputeMetricsForPresent(qpc, frame, &next1, chain); + Assert::AreEqual(size_t(1), results1.size()); + + // No adjust of first frame ready time = 4'000'000 - 1'100'000 = 2'900'000 ticks = 0.29 ms + double expectedReadyTimeLatency = qpc.DeltaUnsignedMilliSeconds(1'100'000, 4'000'000); + Assert::IsTrue(results1[0].metrics.msReadyTimeToDisplayLatency.has_value()); + Assert::AreEqual(expectedReadyTimeLatency, results1[0].metrics.msReadyTimeToDisplayLatency.value(), 0.0001); + + auto results2 = ComputeMetricsForPresent(qpc, next1, &next2, chain); + Assert::AreEqual(size_t(1), results1.size()); + + // After NV adjustment: ready time latency = 4'000'000 - 2'100'000 = 1'900'000 ticks = 0.19 ms + double expectedReadyTimeLatency2 = qpc.DeltaUnsignedMilliSeconds(2'100'000, 4'000'000); + Assert::IsTrue(results2[0].metrics.msReadyTimeToDisplayLatency.has_value()); + Assert::AreEqual(expectedReadyTimeLatency2, results2[0].metrics.msReadyTimeToDisplayLatency.value(), 0.0001); + } + }; + + TEST_CLASS(DisplayLatencyEdgeCasesTests) + { + public: + TEST_METHOD(DisplayLatency_ScreenTimeBeforeCpuStart) + { + // Scenario: Timestamps appear out-of-order (screen time earlier than CPU start). + // cpuStart = 3'000'000 + // screenTime = 2'000'000 (earlier) + // This is unusual but should be handled gracefully (likely as 0 or negative value). + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 1'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + FrameData priorApp{}; + priorApp.presentStartTime = 2'500'000; + priorApp.timeInPresent = 500'000; + chain.lastAppPresent = priorApp; + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + // cpuStart = 2'500'000 + 500'000 = 3'000'000 + // screenTime = 2'000'000 (earlier than cpuStart) + // Result should be 0 or negative (implementation dependent) + Assert::IsTrue(m.msDisplayLatency <= 0.0 || m.msDisplayLatency == 0.0); + } + + TEST_METHOD(ReadyTimeToDisplay_ScreenTimeBeforeReadyTime) + { + // Scenario: Frame displays before GPU finishes (should not happen in practice). + // readyTime = 3'000'000 + // screenTime = 2'000'000 (earlier) + // Expected: 0 or negative value (defensive behavior) + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 3'000'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + // screenTime = 2'000'000, readyTime = 3'000'000 + // Result should be 0 or negative + Assert::IsTrue(m.msReadyTimeToDisplayLatency <= 0.0 || m.msReadyTimeToDisplayLatency == 0.0); + } + + TEST_METHOD(DisplayLatency_FirstFrame_NoPriorAppPresent) + { + // Scenario: First frame in swapchain; no prior lastAppPresent in chain state. + // cpuStart derived from lastPresent only (fallback). + // Single display with screenTime = 2'000'000 + // Expected: msDisplayLatency computed correctly using fallback CPU start + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + // No lastAppPresent set + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 1'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + // No prior present, so cpuStart = 0 + // msDisplayLatency = 2'000'000 - 0 = 2'000'000 ticks = 0.2 ms + double expected = qpc.DeltaUnsignedMilliSeconds(0, 2'000'000); + Assert::AreEqual(expected, m.msDisplayLatency, 0.0001); + } + + TEST_METHOD(DisplayLatency_FrameWithAppPropagatedData) + { + // Scenario: lastAppPresent has appPropagatedPresentStartTime and appPropagatedTimeInPresent set. + // CPU start should use these. + // appPropagatedPresentStartTime = 800'000 + // appPropagatedTimeInPresent = 150'000 + // screenTime = 2'000'000 + // Expected cpuStart = 800'000 + 150'000 = 950'000 + // Expected msDisplayLatency ≈ 0.1055 ms (2'000'000 − 950'000 = 1'050'000 ticks) + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 50'000; + frame.readyTime = 1'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + FrameData priorApp{}; + priorApp.presentStartTime = 1'000'000; + priorApp.timeInPresent = 200'000; + priorApp.appPropagatedPresentStartTime = 800'000; + priorApp.appPropagatedTimeInPresent = 150'000; + chain.lastAppPresent = priorApp; + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + // cpuStart = 800'000 + 150'000 = 950'000 + // msDisplayLatency = 2'000'000 - 950'000 = 1'050'000 ticks = 0.105 ms + double expected = qpc.DeltaUnsignedMilliSeconds(950'000, 2'000'000); + Assert::AreEqual(expected, m.msDisplayLatency, 0.0001); + } + }; + TEST_CLASS(CPUMetricsTests) + { + public: + TEST_METHOD(CPUBusy_BasicCase_StandardPath) + { + // No propagated data in lastAppPresent + // cpuStart = 1'000'000 (prior frame start + timeInPresent) + // presentStartTime = 1'100'000 + // QPC frequency: 10 MHz + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData priorFrame{}; + priorFrame.presentStartTime = 800'000; + priorFrame.timeInPresent = 200'000; + priorFrame.readyTime = 1'100'000; + priorFrame.finalState = PresentResult::Presented; + priorFrame.displayed.PushBack({ FrameType::Application, 1'200'000 }); + + chain.lastAppPresent = priorFrame; + + FrameData frame{}; + frame.presentStartTime = 1'100'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'200'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'300'000 }); + + FrameData next{}; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'400'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // cpuStart = 800'000 + 200'000 = 1'000'000 + // msCPUBusy = 1'100'000 - 1'000'000 = 100'000 ticks = 10 ms + double expected = qpc.DeltaUnsignedMilliSeconds(1'000'000, 1'100'000); + Assert::AreEqual(expected, m.msCPUBusy, 0.0001); + } + + TEST_METHOD(CPUBusy_WithAppPropagatedData) + { + // lastAppPresent has appPropagatedPresentStartTime set + // We need to ensure the frame is displayed so CPU metrics are computed + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame with propagated data + FrameData priorApp{}; + priorApp.presentStartTime = 1'000'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'200'000; + priorApp.appPropagatedPresentStartTime = 800'000; + priorApp.appPropagatedTimeInPresent = 200'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'300'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame (app frame, displayed) + FrameData frame{}; + frame.presentStartTime = 1'500'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'600'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 2'000'000; + next.timeInPresent = 80'000; + next.readyTime = 2'100'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'200'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // cpuStart = 800'000 + 200'000 = 1'000'000 (uses appPropagated from priorApp) + // msCPUBusy = 1'500'000 - 1'000'000 = 500'000 ticks = 50 ms + double expected = qpc.DeltaUnsignedMilliSeconds(1'000'000, 1'500'000); + Assert::AreEqual(expected, m.msCPUBusy, 0.0001); + } + + TEST_METHOD(CPUBusy_FirstFrameNoPriorAppPresent) + { + // No lastAppPresent in chain state + // cpuStart = 0 (default fallback) + // presentStartTime = 5'000'000 + // Expected msCPUBusy = 5'000'000 ticks = 500 ms + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + // No prior app present set + + // Current frame (app frame, displayed) + FrameData frame{}; + frame.presentStartTime = 5'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 5'200'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 5'500'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 6'000'000; + next.timeInPresent = 80'000; + next.readyTime = 6'100'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 6'300'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // cpuStart = 0 (no prior app present) + // msCPUBusy = 5'000'000 - 0 = 5'000'000 ticks = 500 ms + double expected = qpc.DeltaUnsignedMilliSeconds(0, 5'000'000); + Assert::AreEqual(expected, m.msCPUBusy, 0.0001); + } + + TEST_METHOD(CPUBusy_ZeroTimeInPresent) + { + // cpuStart = 1'000'000 + // presentStartTime = 1'000'000 (same as cpuStart) + // timeInPresent = 0 (zero present duration) + // Expected msCPUBusy = 0 + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorFrame{}; + priorFrame.presentStartTime = 800'000; + priorFrame.timeInPresent = 200'000; + priorFrame.readyTime = 1'100'000; + priorFrame.finalState = PresentResult::Presented; + priorFrame.displayed.PushBack({ FrameType::Application, 1'200'000 }); + + chain.lastAppPresent = priorFrame; + + // Current frame with zero timeInPresent + FrameData frame{}; + frame.presentStartTime = 1'000'000; // Same as cpuStart + frame.timeInPresent = 0; // Zero present duration + frame.readyTime = 1'000'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'500'000; + next.timeInPresent = 50'000; + next.readyTime = 1'600'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // cpuStart = 800'000 + 200'000 = 1'000'000 + // presentStartTime = 1'000'000 (same as cpuStart) + // msCPUBusy = 1'000'000 - 1'000'000 = 0 ticks = 0 ms + Assert::AreEqual(0.0, m.msCPUBusy, 0.0001); + } + + TEST_METHOD(CPUWait_BasicCase_StandardPath) + { + // timeInPresent = 200'000 ticks + // Expected msCPUWait = 200'000 / 10'000'000 = 0.02 ms = 20 ms + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorFrame{}; + priorFrame.presentStartTime = 800'000; + priorFrame.timeInPresent = 100'000; + priorFrame.readyTime = 1'100'000; + priorFrame.finalState = PresentResult::Presented; + priorFrame.displayed.PushBack({ FrameType::Application, 1'200'000 }); + + chain.lastAppPresent = priorFrame; + + // Current frame with timeInPresent = 200'000 + FrameData frame{}; + frame.presentStartTime = 1'100'000; + frame.timeInPresent = 200'000; // 20 ms + frame.readyTime = 1'300'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'400'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'800'000; + next.timeInPresent = 50'000; + next.readyTime = 1'900'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // msCPUWait = 200'000 ticks = 20 ms + double expected = qpc.DurationMilliSeconds(200'000); + Assert::AreEqual(expected, m.msCPUWait, 0.0001); + } + + TEST_METHOD(CPUWait_WithAppPropagatedTimeInPresent) + { + // appPropagatedTimeInPresent = 150'000 ticks + // Expected msCPUWait = 150'000 / 10'000'000 = 0.015 ms = 15 ms + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorFrame{}; + priorFrame.presentStartTime = 800'000; + priorFrame.timeInPresent = 100'000; + priorFrame.readyTime = 1'100'000; + priorFrame.finalState = PresentResult::Presented; + priorFrame.displayed.PushBack({ FrameType::Application, 1'200'000 }); + + chain.lastAppPresent = priorFrame; + + // Current frame with appPropagatedTimeInPresent = 150'000 + FrameData frame{}; + frame.presentStartTime = 1'100'000; + frame.timeInPresent = 200'000; // Regular time (not used when propagated available) + frame.readyTime = 1'300'000; + frame.appPropagatedTimeInPresent = 150'000; // 15 ms (propagated value) + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'400'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'800'000; + next.timeInPresent = 50'000; + next.readyTime = 1'900'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // When appPropagated is available, use it instead of regular timeInPresent + // msCPUWait = 150'000 ticks = 15 ms + double expected = qpc.DurationMilliSeconds(150'000); + Assert::AreEqual(expected, m.msCPUWait, 0.0001); + } + + TEST_METHOD(CPUWait_ZeroDuration) + { + // timeInPresent = 0 + // Expected msCPUWait = 0 / 10'000'000 = 0 ms + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorFrame{}; + priorFrame.presentStartTime = 800'000; + priorFrame.timeInPresent = 100'000; + priorFrame.readyTime = 1'100'000; + priorFrame.finalState = PresentResult::Presented; + priorFrame.displayed.PushBack({ FrameType::Application, 1'200'000 }); + + chain.lastAppPresent = priorFrame; + + // Current frame with zero timeInPresent + FrameData frame{}; + frame.presentStartTime = 1'100'000; + frame.timeInPresent = 0; // Zero CPU wait time + frame.readyTime = 1'100'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'200'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'600'000; + next.timeInPresent = 50'000; + next.readyTime = 1'700'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'800'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // msCPUWait = 0 ticks = 0 ms + Assert::AreEqual(0.0, m.msCPUWait, 0.0001); + } + + TEST_METHOD(CPUTime_IsDerivedCorrectly) + { + // Verify msCPUTime = msCPUBusy + msCPUWait. + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame sets cpuStart = 1'000'000. + FrameData priorFrame{}; + priorFrame.presentStartTime = 900'000; + priorFrame.timeInPresent = 100'000; + priorFrame.readyTime = 1'050'000; + priorFrame.finalState = PresentResult::Presented; + priorFrame.displayed.PushBack({ FrameType::Application, 1'100'000 }); + chain.lastAppPresent = priorFrame; + + // Current frame: presentStartTime=1'100'000 => msCPUBusy=10ms; timeInPresent=200'000 => msCPUWait=20ms. + FrameData frame{}; + frame.presentStartTime = 1'100'000; + frame.timeInPresent = 200'000; + frame.readyTime = 1'350'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'400'000 }); + + // Next displayed frame required to process current frame's display. + FrameData next{}; + next.presentStartTime = 1'600'000; + next.timeInPresent = 50'000; + next.readyTime = 1'700'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'800'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + const auto& m = results[0].metrics; + + double expectedBusy = qpc.DeltaUnsignedMilliSeconds(1'000'000, 1'100'000); + double expectedWait = qpc.DurationMilliSeconds(200'000); + double expectedCpuTime = expectedBusy + expectedWait; + + Assert::AreEqual(expectedBusy, m.msCPUBusy, 0.0001); + Assert::AreEqual(expectedWait, m.msCPUWait, 0.0001); + Assert::AreEqual(expectedCpuTime, m.msCPUTime, 0.0001); + } + }; + + // ============================================================================ + // GROUP B: CORE GPU METRICS (NON-VIDEO) + // ============================================================================ + + TEST_CLASS(GPUMetricsNonVideoTests) + { + public: + TEST_METHOD(GPULatency_BasicCase_StandardPath) + { + // cpuStart = 1'000'000 + // gpuStartTime = 1'050'000 + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame + FrameData frame{}; + frame.presentStartTime = 1'200'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'300'000; + frame.gpuStartTime = 1'050'000; + frame.gpuDuration = 200'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'400'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'600'000; + next.timeInPresent = 50'000; + next.readyTime = 1'700'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'800'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // cpuStart = 800'000 + 200'000 = 1'000'000 + // msGPULatency = 1'050'000 - 1'000'000 = 50'000 ticks = 5 ms + double expected = qpc.DeltaUnsignedMilliSeconds(1'000'000, 1'050'000); + Assert::AreEqual(expected, m.msGPULatency, 0.0001); + } + + TEST_METHOD(GPULatency_WithAppPropagatedGPUStart) + { + // appPropagatedGPUStartTime = 1'080'000 + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame with app propagated GPU start + FrameData frame{}; + frame.presentStartTime = 1'200'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'300'000; + frame.gpuStartTime = 1'050'000; // Not used when propagated available + frame.gpuDuration = 200'000; + frame.appPropagatedGPUStartTime = 1'080'000; + frame.appPropagatedGPUDuration = 200'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'400'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'600'000; + next.timeInPresent = 50'000; + next.readyTime = 1'700'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'800'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // cpuStart = 1'000'000 + // msGPULatency = 1'080'000 - 1'000'000 = 80'000 ticks = 8 ms + double expected = qpc.DeltaUnsignedMilliSeconds(1'000'000, 1'080'000); + Assert::AreEqual(expected, m.msGPULatency, 0.0001); + } + + TEST_METHOD(GPULatency_GPUStartBeforeCpuStart) + { + // cpuStart = 2'000'000 + // gpuStartTime = 1'900'000 (impossible but defensive) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData priorApp{}; + priorApp.presentStartTime = 1'500'000; + priorApp.timeInPresent = 500'000; // cpuStart = 2'000'000 + priorApp.readyTime = 2'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 2'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame with GPU start before CPU start + FrameData frame{}; + frame.presentStartTime = 2'200'000; + frame.timeInPresent = 100'000; + frame.readyTime = 2'300'000; + frame.gpuStartTime = 1'900'000; // Earlier than cpuStart + frame.gpuDuration = 300'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 2'400'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 2'600'000; + next.timeInPresent = 50'000; + next.readyTime = 2'700'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'800'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // Result should be 0 or negative (defensive clamping) + Assert::IsTrue(m.msGPULatency <= 0.0 || m.msGPULatency == 0.0); + } + + TEST_METHOD(GPUBusy_BasicCase_StandardPath) + { + // gpuDuration = 500'000 ticks + // Expected msGPUBusy = 500'000 / 10'000'000 = 0.05 ms = 50 ms + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'200'000; + frame.gpuStartTime = 1'050'000; + frame.gpuDuration = 500'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'300'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'500'000; + next.timeInPresent = 50'000; + next.readyTime = 1'600'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // msGPUBusy = 500'000 ticks = 50 ms + double expected = qpc.DurationMilliSeconds(500'000); + Assert::AreEqual(expected, m.msGPUBusy, 0.0001); + } + + TEST_METHOD(GPUBusy_ZeroDuration) + { + // gpuDuration = 0 + // Expected msGPUBusy = 0 ms + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame with zero GPU duration + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'200'000; + frame.gpuStartTime = 1'050'000; + frame.gpuDuration = 0; // No GPU work + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'300'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'500'000; + next.timeInPresent = 50'000; + next.readyTime = 1'600'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // msGPUBusy = 0 ticks = 0 ms + Assert::AreEqual(0.0, m.msGPUBusy, 0.0001); + } + + TEST_METHOD(GPUBusy_WithAppPropagatedDuration) + { + // appPropagatedGPUDuration = 450'000 ticks + // Expected msGPUBusy = 450'000 / 10'000'000 = 0.045 ms = 45 ms + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame with app propagated GPU duration + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'200'000; + frame.gpuStartTime = 1'050'000; + frame.gpuDuration = 500'000; // Not used when propagated available + frame.appPropagatedGPUStartTime = 1'050'000; + frame.appPropagatedGPUDuration = 450'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'300'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'500'000; + next.timeInPresent = 50'000; + next.readyTime = 1'600'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // Uses appPropagated: 450'000 ticks = 45 ms + double expected = qpc.DurationMilliSeconds(450'000); + Assert::AreEqual(expected, m.msGPUBusy, 0.0001); + } + + TEST_METHOD(GPUWait_BasicCase_BusyLessThanTotal) + { + // gpuStartTime = 1'000'000, readyTime = 1'600'000 → total = 600'000 + // gpuDuration (busy) = 500'000 + // msGPUWait should be 100'000 ticks = 10 ms + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'600'000; + frame.gpuStartTime = 1'000'000; + frame.gpuDuration = 500'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'900'000; + next.timeInPresent = 50'000; + next.readyTime = 2'000'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'100'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // Total = 1'600'000 - 1'000'000 = 600'000 + // msGPUBusy = 500'000 ticks = 50 ms + // msGPUWait = 600'000 - 500'000 = 100'000 ticks = 10 ms + double expectedTotal = qpc.DeltaUnsignedMilliSeconds(1'000'000, 1'600'000); + double expectedBusy = qpc.DurationMilliSeconds(500'000); + double expectedWait = std::max(0.0, expectedTotal - expectedBusy); + Assert::AreEqual(expectedWait, m.msGPUWait, 0.0001); + } + + TEST_METHOD(GPUWait_BusyEqualsTotal) + { + // gpuStartTime = 1'000'000, readyTime = 1'600'000 → total = 600'000 + // gpuDuration = 600'000 (fully busy) + // msGPUWait should be 0 + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame with GPU duration equal to total time + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'600'000; + frame.gpuStartTime = 1'000'000; + frame.gpuDuration = 600'000; // Equal to total + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'900'000; + next.timeInPresent = 50'000; + next.readyTime = 2'000'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'100'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // Total = 1'600'000 - 1'000'000 = 600'000 + // msGPUBusy = 600'000 ticks = 60 ms (equal to total) + // msGPUWait = 600'000 - 600'000 = 0 ms + Assert::AreEqual(0.0, m.msGPUWait, 0.0001); + } + + TEST_METHOD(GPUWait_BusyGreaterThanTotal) + { + // gpuStartTime = 1'000'000, readyTime = 1'600'000 → total = 600'000 + // gpuDuration = 700'000 (impossible, but defensive) + // msGPUWait should clamp to 0 + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame with GPU duration greater than total (impossible case) + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'600'000; + frame.gpuStartTime = 1'000'000; + frame.gpuDuration = 700'000; // Greater than total (impossible) + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'900'000; + next.timeInPresent = 50'000; + next.readyTime = 2'000'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'100'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // Total = 1'600'000 - 1'000'000 = 600'000 + // msGPUBusy = 700'000 ticks = 70 ms (greater than total) + // msGPUWait should clamp to 0 + Assert::AreEqual(0.0, m.msGPUWait, 0.0001); + } + + TEST_METHOD(GPUWait_WithAppPropagatedData) + { + // appPropagatedGPUStartTime = 1'000'000, appPropagatedReadyTime = 1'550'000 → total = 550'000 + // appPropagatedGPUDuration = 450'000 + // msGPUWait should be 100'000 ticks = 10 ms + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame with app propagated GPU data + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'600'000; + frame.gpuStartTime = 1'000'000; + frame.gpuDuration = 600'000; + frame.appPropagatedGPUStartTime = 1'000'000; + frame.appPropagatedReadyTime = 1'550'000; + frame.appPropagatedGPUDuration = 450'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'900'000; + next.timeInPresent = 50'000; + next.readyTime = 2'000'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'100'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // Total = 1'550'000 - 1'000'000 = 550'000 + // msGPUBusy = 450'000 ticks = 45 ms + // msGPUWait = 550'000 - 450'000 = 100'000 ticks = 10 ms + double expectedTotal = qpc.DeltaUnsignedMilliSeconds(1'000'000, 1'550'000); + double expectedBusy = qpc.DurationMilliSeconds(450'000); + double expectedWait = std::max(0.0, expectedTotal - expectedBusy); + Assert::AreEqual(expectedWait, m.msGPUWait, 0.0001); + } + }; + + TEST_CLASS(GPUMetricsVideoTests) + { + public: + TEST_METHOD(VideoBusy_BasicCase_StandardPath) + { + // gpuVideoDuration = 200'000 ticks + // Expected msVideoBusy = 200'000 / 10'000'000 = 0.02 ms = 20 ms + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'200'000; + frame.gpuStartTime = 1'050'000; + frame.gpuDuration = 500'000; + frame.gpuVideoDuration = 200'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'300'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'500'000; + next.timeInPresent = 50'000; + next.readyTime = 1'600'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // msVideoBusy = 200'000 ticks = 20 ms + double expected = qpc.DurationMilliSeconds(200'000); + Assert::AreEqual(expected, m.msVideoBusy, 0.0001); + } + + TEST_METHOD(VideoBusy_ZeroDuration) + { + // gpuVideoDuration = 0 + // Expected msVideoBusy = 0 ms + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame with zero video duration + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'200'000; + frame.gpuStartTime = 1'050'000; + frame.gpuDuration = 500'000; + frame.gpuVideoDuration = 0; // No video work + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'300'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'500'000; + next.timeInPresent = 50'000; + next.readyTime = 1'600'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + Assert::AreEqual(0.0, m.msVideoBusy, 0.0001); + } + + TEST_METHOD(VideoBusy_WithAppPropagatedData) + { + // appPropagatedGPUVideoDuration = 180'000 ticks + // Expected msVideoBusy = 180'000 / 10'000'000 = 0.018 ms = 18 ms + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame with app propagated GPU video duration + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'200'000; + frame.gpuStartTime = 1'050'000; + frame.gpuDuration = 500'000; + frame.gpuVideoDuration = 200'000; // Not used when propagated available + frame.appPropagatedGPUStartTime = 1'050'000; + frame.appPropagatedGPUDuration = 450'000; + frame.appPropagatedGPUVideoDuration = 180'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'300'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'500'000; + next.timeInPresent = 50'000; + next.readyTime = 1'600'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // Uses appPropagated: 180'000 ticks = 18 ms + double expected = qpc.DurationMilliSeconds(180'000); + Assert::AreEqual(expected, m.msVideoBusy, 0.0001); + } + + TEST_METHOD(VideoBusy_OverlapWithGPUBusy) + { + // msGPUBusy = 50 ms (500'000 ticks) + // msVideoBusy = 20 ms (200'000 ticks) + // Verify both are independently computed (no constraint) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'200'000; + frame.gpuStartTime = 1'050'000; + frame.gpuDuration = 500'000; + frame.gpuVideoDuration = 200'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'300'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'500'000; + next.timeInPresent = 50'000; + next.readyTime = 1'600'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + double expectedGpuBusy = qpc.DurationMilliSeconds(500'000); + double expectedVideoBusy = qpc.DurationMilliSeconds(200'000); + + Assert::AreEqual(expectedGpuBusy, m.msGPUBusy, 0.0001); + Assert::AreEqual(expectedVideoBusy, m.msVideoBusy, 0.0001); + } + + TEST_METHOD(VideoBusy_LargerThanGPUBusy) + { + // msGPUBusy = 30 ms (computed from gpuDuration) + // msVideoBusy = 50 ms (computed from gpuVideoDuration) + // Verify independent computation (no implicit constraints) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame where video duration > GPU duration + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'200'000; + frame.gpuStartTime = 1'050'000; + frame.gpuDuration = 300'000; // 30 ms + frame.gpuVideoDuration = 500'000; // 50 ms (larger than gpuDuration) + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'300'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'500'000; + next.timeInPresent = 50'000; + next.readyTime = 1'600'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // Verify both are computed independently + Assert::IsTrue(m.msVideoBusy > m.msGPUBusy); + } + }; + + TEST_CLASS(EdgeCasesAndMissingData) + { + public: + TEST_METHOD(AllMetrics_NoGPUData_GPUMetricsZero) + { + // Frame with no GPU data (all GPU fields = 0) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData priorApp = MakeFrame( + PresentResult::Presented, + /*presentStartTime*/ 800'000, + /*timeInPresent*/ 200'000, + /*readyTime*/ 1'000'000, + /*displayed*/{}); + + chain.lastAppPresent = priorApp; + + // Current frame with no GPU data + FrameData frame{}; + frame.presentStartTime = 1'100'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'200'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'300'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'500'000; + next.timeInPresent = 50'000; + next.readyTime = 1'600'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // CPU metrics should be non-zero + Assert::IsTrue(m.msCPUBusy > 0); + // GPU metrics should be zero + Assert::AreEqual(0.0, m.msGPULatency, 0.0001); + Assert::AreEqual(0.0, m.msGPUBusy, 0.0001); + Assert::AreEqual(0.0, m.msGPUWait, 0.0001); + Assert::AreEqual(0.0, m.msVideoBusy, 0.0001); + } + + TEST_METHOD(GeneratedFrameMetrics_NotAppFrame_CPUGPUMetricsZero) + { + // Frame with only Repeated display type (not Application) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData priorApp = MakeFrame( + PresentResult::Presented, + /*presentStartTime*/ 800'000, + /*timeInPresent*/ 200'000, + /*readyTime*/ 1'000'000, + /*displayed*/{}); + + chain.lastAppPresent = priorApp; + + // Current frame with only Repeated display (not Application) + FrameData frame{}; + frame.presentStartTime = 1'100'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'200'000; + frame.gpuStartTime = 1'150'000; + frame.gpuDuration = 200'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Repeated, 1'300'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'500'000; + next.timeInPresent = 50'000; + next.readyTime = 1'600'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // Generated frames have no CPU/GPU work attribution + Assert::AreEqual(0.0, m.msCPUBusy, 0.0001); + Assert::AreEqual(0.0, m.msCPUWait, 0.0001); + Assert::AreEqual(0.0, m.msGPULatency, 0.0001); + Assert::AreEqual(0.0, m.msGPUBusy, 0.0001); + Assert::AreEqual(0.0, m.msGPUWait, 0.0001); + } + + TEST_METHOD(NotDisplayedFrame_AppFrameMetrics_Computed) + { + // Frame is not displayed (Discarded) but has CPU/GPU work + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame is not displayed (Discarded) + FrameData frame{}; + frame.presentStartTime = 1'100'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'200'000; + frame.gpuStartTime = 1'150'000; + frame.gpuDuration = 200'000; + frame.finalState = PresentResult::Discarded; + // No displayed entries + + auto results = ComputeMetricsForPresent(qpc, frame, nullptr, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // CPU/GPU metrics should still be computed even for dropped frames + Assert::IsTrue(m.msCPUBusy > 0); + Assert::IsTrue(m.msGPUBusy > 0); + } + }; + + TEST_CLASS(StateAndHistory) + { + public: + TEST_METHOD(CPUStart_UsesLastAppPresent_WhenAvailable) + { + // chain.lastAppPresent set; current is app frame + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData lastApp{}; + lastApp.presentStartTime = 800'000; + lastApp.timeInPresent = 200'000; + lastApp.readyTime = 1'000'000; + lastApp.finalState = PresentResult::Presented; + lastApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = lastApp; + + // Current frame + FrameData frame{}; + frame.presentStartTime = 1'200'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'300'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'400'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'600'000; + next.timeInPresent = 50'000; + next.readyTime = 1'700'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'800'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // cpuStart should be 800'000 + 200'000 = 1'000'000 + uint64_t expectedCpuStart = 800'000 + 200'000; + Assert::AreEqual(expectedCpuStart, m.cpuStartQpc); + } + + TEST_METHOD(CPUStart_FallsBackToLastPresent_WhenNoAppPresent) + { + // chain.lastAppPresent is empty; chain.lastPresent is set + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData lastPresent{}; + lastPresent.presentStartTime = 800'000; + lastPresent.timeInPresent = 200'000; + lastPresent.readyTime = 1'000'000; + lastPresent.finalState = PresentResult::Presented; + lastPresent.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastPresent = lastPresent; + // lastAppPresent remains unset + + // Current frame + FrameData frame{}; + frame.presentStartTime = 1'200'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'300'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'400'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'600'000; + next.timeInPresent = 50'000; + next.readyTime = 1'700'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'800'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // cpuStart falls back to lastPresent: 800'000 + 200'000 = 1'000'000 + uint64_t expectedCpuStart = 800'000 + 200'000; + Assert::AreEqual(expectedCpuStart, m.cpuStartQpc); + } + + TEST_METHOD(CPUStart_ReturnsZero_WhenNoChainHistory) + { + // No lastAppPresent; no lastPresent + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; // Empty chain + + // Current frame + FrameData frame{}; + frame.presentStartTime = 5'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 5'200'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 5'500'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 6'000'000; + next.timeInPresent = 50'000; + next.readyTime = 6'100'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 6'300'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // cpuStart = 0 (no history) + Assert::AreEqual(uint64_t(0), m.cpuStartQpc); + } + + TEST_METHOD(ChainState_UpdatedAfterPresent_SingleDisplay) + { + // Process a displayed app frame; verify chain state is updated + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Current frame + FrameData frame{}; + frame.presentStartTime = 5'000'000; + frame.timeInPresent = 100'000; + frame.readyTime = 5'200'000; + frame.flipDelay = 777; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 5'500'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 6'000'000; + next.timeInPresent = 50'000; + next.readyTime = 6'100'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 6'300'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + // Verify chain state was updated + Assert::IsTrue(chain.lastPresent.has_value()); + Assert::IsTrue(chain.lastAppPresent.has_value()); + Assert::AreEqual(uint64_t(5'500'000), chain.lastDisplayedScreenTime); + Assert::AreEqual(uint64_t(777), chain.lastDisplayedFlipDelay); + } + }; + + TEST_CLASS(NumericAndPrecision) + { + public: + TEST_METHOD(CPUBusy_LargeValues_DoesNotOverflow) + { + // cpuStart = 1'000'000'000 (large QPC value) + // presentStartTime = 1'100'000'000 + // Expected msCPUBusy = 100'000'000 ticks = 10'000 ms (10 seconds) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Prior app frame with large values + FrameData priorApp{}; + priorApp.presentStartTime = 900'000'000; + priorApp.timeInPresent = 100'000'000; + priorApp.readyTime = 1'000'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame with large values + FrameData frame{}; + frame.presentStartTime = 1'100'000'000; + frame.timeInPresent = 100'000'000; + frame.readyTime = 1'200'000'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'300'000'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'600'000'000; + next.timeInPresent = 50'000'000; + next.readyTime = 1'700'000'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'800'000'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // cpuStart = 900'000'000 + 100'000'000 = 1'000'000'000 + // msCPUBusy = 1'100'000'000 - 1'000'000'000 = 100'000'000 ticks = 10'000 ms (10 seconds) + double expected = qpc.DeltaUnsignedMilliSeconds(1'000'000'000, 1'100'000'000); + Assert::AreEqual(expected, m.msCPUBusy, 0.0001); + // Verify large value is reasonable (10 seconds) + Assert::IsTrue(m.msCPUBusy > 9000 && m.msCPUBusy < 11000); + } + + TEST_METHOD(GPULatency_SmallDelta_HighPrecision) + { + // cpuStart = 1'000'000 + // gpuStartTime = 1'000'001 (1 tick delta; tiny latency) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + chain.lastAppPresent = priorApp; + + // Current frame with small GPU latency + FrameData frame{}; + frame.presentStartTime = 1'100'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'200'000; + frame.gpuStartTime = 1'000'001; // Only 1 tick later than cpuStart + frame.gpuDuration = 200'000; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'300'000 }); + + // Next displayed frame (required to process current frame's display) + FrameData next{}; + next.presentStartTime = 1'500'000; + next.timeInPresent = 50'000; + next.readyTime = 1'600'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'700'000 }); + + auto results = ComputeMetricsForPresent(qpc, frame, &next, chain); + Assert::AreEqual(size_t(1), results.size()); + + const auto& m = results[0].metrics; + // msGPULatency = 1 tick at 10 MHz = 0.0001 ms (very small but non-zero) + double expected = qpc.DeltaUnsignedMilliSeconds(1'000'000, 1'000'001); + Assert::AreEqual(expected, m.msGPULatency, 0.00001); + Assert::IsTrue(m.msGPULatency > 0.0 && m.msGPULatency < 0.001); + } + + TEST_METHOD(VideoBusy_ZeroAndNonzeroInSequence) + { + // Frame A: no video work + // Frame B: with video work + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Frame A: zero video + FrameData frameA{}; + frameA.presentStartTime = 1'000'000; + frameA.timeInPresent = 100'000; + frameA.readyTime = 1'200'000; + frameA.gpuStartTime = 1'050'000; + frameA.gpuDuration = 400'000; + frameA.gpuVideoDuration = 0; // No video + frameA.finalState = PresentResult::Presented; + frameA.displayed.PushBack({ FrameType::Application, 1'500'000 }); + + FrameData nextA{}; + nextA.presentStartTime = 2'000'000; + nextA.timeInPresent = 50'000; + nextA.readyTime = 2'100'000; + nextA.finalState = PresentResult::Presented; + nextA.displayed.PushBack({ FrameType::Application, 2'200'000 }); + + auto resultsA = ComputeMetricsForPresent(qpc, frameA, &nextA, chain); + Assert::AreEqual(size_t(1), resultsA.size()); + Assert::AreEqual(0.0, resultsA[0].metrics.msVideoBusy, 0.0001); + + // Frame B: with video + FrameData frameB{}; + frameB.presentStartTime = 2'100'000; + frameB.timeInPresent = 100'000; + frameB.readyTime = 2'300'000; + frameB.gpuStartTime = 2'150'000; + frameB.gpuDuration = 400'000; + frameB.gpuVideoDuration = 300'000; // 30 ms of video + frameB.finalState = PresentResult::Presented; + frameB.displayed.PushBack({ FrameType::Application, 2'600'000 }); + + FrameData nextB{}; + nextB.presentStartTime = 3'000'000; + nextB.timeInPresent = 50'000; + nextB.readyTime = 3'100'000; + nextB.finalState = PresentResult::Presented; + nextB.displayed.PushBack({ FrameType::Application, 3'200'000 }); + + auto resultsB = ComputeMetricsForPresent(qpc, frameB, &nextB, chain); + Assert::AreEqual(size_t(1), resultsB.size()); + double expectedVideoBusy = qpc.DurationMilliSeconds(300'000); + Assert::AreEqual(expectedVideoBusy, resultsB[0].metrics.msVideoBusy, 0.0001); + } + }; + + TEST_CLASS(AnimationTime) + { + public: + // ======================================================================== + // A1: AnimationTime_AppProvider_FirstFrame_ZeroWithoutAppSimStartTime + // ======================================================================== + TEST_METHOD(AnimationTime_AppProvider_FirstFrame_ZeroWithoutAppSimStartTime) + { + // Scenario: + // - SwapChainCoreState starts with CpuStart (default) + // - Current frame: displayed, displayIndex == appIndex, but appSimStartTime == 0 + // - No app data means source stays CpuStart + // + // Expected Outcome: + // - msAnimationTime = std::nullopt (no valid sim start time, no history) + // - firstAppSimStartTime remains 0 in state + // - Source remains CpuStart + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + // state.animationErrorSource defaults to CpuStart + + // Verify initial state + Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime); + Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime); + + // Create a displayed app frame WITHOUT appSimStartTime + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 500; + frame.readyTime = 1'500'000; + frame.appSimStartTime = 0; // No app instrumentation yet + frame.pclSimStartTime = 0; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + // Verify frame setup + Assert::AreEqual(uint64_t(0), frame.appSimStartTime); + Assert::AreEqual(size_t(1), frame.displayed.Size()); + + // Create nextDisplayed to allow processing + FrameData next{}; + next.presentStartTime = 2'000'000; + next.timeInPresent = 400; + next.readyTime = 2'500'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + // Action: Compute metrics for this frame + auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + + // Assert: Should have one computed metric + Assert::AreEqual(size_t(1), metricsVector.size()); + + const ComputedMetrics& result = metricsVector[0]; + + // Assert: msAnimationTime should be nullopt + Assert::IsFalse(result.metrics.msAnimationTime.has_value(), + L"msAnimationTime should be nullopt when appSimStartTime is 0 on first frame"); + + // Assert: firstAppSimStartTime in state should remain 0 + Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, + L"State: firstAppSimStartTime should remain 0 (no valid app sim start time detected)"); + + // Assert: lastDisplayedSimStartTime should remain 0 + Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime, + L"State: lastDisplayedSimStartTime should remain 0 (no valid app sim start time detected)"); + } + + // ======================================================================== + // A2: AnimationTime_AppProvider_TransitionFrame_FirstValidAppSimStart + // ======================================================================== + TEST_METHOD(AnimationTime_AppProvider_TransitionFrame_FirstValidAppSimStart) + { + // Scenario: + // - Start with CpuStart source (default) + // - Frame 1: App data arrives, triggers source switch to AppProvider + // - Expected: msAnimationTime = 0 (first frame with valid app sim start) + // + // Expected Outcome: + // - msAnimationTime = 0 (first frame with app instrumentation) + // - firstAppSimStartTime is set to qpc(100) + // - Source switches to AppProvider after UpdateChain + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + // state.animationErrorSource defaults to CpuStart + + // Create a displayed app frame WITH appSimStartTime for the first time + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 500; + frame.readyTime = 1'500'000; + frame.appSimStartTime = 100; // First valid app sim start + frame.pclSimStartTime = 0; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + // Create nextDisplayed + FrameData frame2{}; + frame2.presentStartTime = 2'000'000; + frame2.timeInPresent = 400; + frame2.readyTime = 2'500'000; + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + // Action: Compute metrics + auto metricsVector = ComputeMetricsForPresent(qpc, frame, &frame2, state); + + // Assert + Assert::AreEqual(size_t(1), metricsVector.size()); + const ComputedMetrics& result = metricsVector[0]; + + // Assert: msAnimationTime shouldn't have a value (first frame with valid app sim start) + Assert::IsFalse(result.metrics.msAnimationTime.has_value(), + L"msAnimationTime should not have a value on first valid app sim start"); + + // Assert: State should be updated + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, + L"State: firstAppSimStartTime should be set to first valid app sim start"); + Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime, + L"State: lastDisplayedSimStartTime should be set to current frame's app sim start"); + Assert::IsTrue( + state.animationErrorSource == AnimationErrorSource::AppProvider, + L"Animation source should transition to AppProvider after first appSimStartTime."); + } + + // ======================================================================== + // A3: AnimationTime_AppProvider_SecondFrame_IncrementsCorrectly + // ======================================================================== + TEST_METHOD(AnimationTime_AppProvider_SecondFrame_IncrementsCorrectly) + { + // Scenario: + // - Frame 1: First app data, establishes firstAppSimStartTime = 100, switches to AppProvider + // - Frame 2: appSimStartTime = 150 (50 QPC ticks later) + // - QPC frequency = 10 MHz → 50 ticks = 5 µs = 0.005 ms + // + // Expected Outcome: + // - msAnimationTime ≈ 0.005 ms (elapsed sim time from first to current) + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + // Start with CpuStart, will switch to AppProvider after frame1 + + // Frame 1: First app data + FrameData frame1{}; + frame1.presentStartTime = 500'000; + frame1.timeInPresent = 300; + frame1.appSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 900'000 }); + + FrameData next1{}; + next1.presentStartTime = 1'500'000; + next1.finalState = PresentResult::Presented; + next1.displayed.PushBack({ FrameType::Application, 1'500'000 }); + + ComputeMetricsForPresent(qpc, frame1, &next1, state); + // After this, source is AppProvider, firstAppSimStartTime = 100 + + // Frame 2: Second app frame with incremented sim start + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 500; + frame.readyTime = 1'500'000; + frame.appSimStartTime = 150; // 50 ticks later + frame.pclSimStartTime = 0; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + // Create nextDisplayed + FrameData next{}; + next.presentStartTime = 2'000'000; + next.timeInPresent = 400; + next.readyTime = 2'500'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + // Action: Compute metrics + auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + + Assert::AreEqual(size_t(1), metricsVector.size()); + const ComputedMetrics& result = metricsVector[0]; + + // Assert: msAnimationTime should be 0.005 ms + Assert::IsTrue(result.metrics.msAnimationTime.has_value()); + double expectedMs = qpc.DeltaUnsignedMilliSeconds(100, 150); + Assert::AreEqual(expectedMs, result.metrics.msAnimationTime.value(), 0.0001, + L"msAnimationTime should reflect elapsed time from first app sim start"); + + // Assert: firstAppSimStartTime unchanged + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, + L"firstAppSimStartTime should remain at first value"); + + // Assert: lastDisplayedSimStartTime updated + Assert::AreEqual(uint64_t(150), state.lastDisplayedSimStartTime, + L"lastDisplayedSimStartTime should be updated to current frame's app sim start"); + } + // ======================================================================== + // A4: AnimationTime_AppProvider_ThreeFrames_CumulativeElapsedTime + // ======================================================================== + TEST_METHOD(AnimationTime_AppProvider_ThreeFrames_CumulativeElapsedTime) + { + // Scenario: + // - Start with CpuStart as the animation source. + // - Frame 1: first displayed app frame with appSimStartTime = 100. + // This SEEDS animation state (firstAppSimStartTime), but + // does not yet emit msAnimationTime. + // - Frame 2: displayed app frame with appSimStartTime = 150. + // - Frame 3: displayed app frame with appSimStartTime = 250. + // + // QPC frequency = 10 MHz. + // + // Expected outcome: + // - firstAppSimStartTime is latched to 100 and never changes. + // - Frame 1: msAnimationTime == nullopt (just seeds state). + // - Frame 2: msAnimationTime = (150 - 100) / 10e6. + // - Frame 3: msAnimationTime = (250 - 100) / 10e6. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + // animationErrorSource defaults to CpuStart; will transition to AppProvider + // when the first displayed frame with appSimStartTime arrives. + + // -------------------------------------------------------------------- + // Frame 1: first valid app sim start, displayed + // -------------------------------------------------------------------- + FrameData frame1{}; + frame1.presentStartTime = 1'000'000; + frame1.timeInPresent = 500; + frame1.readyTime = 1'500'000; + frame1.appSimStartTime = 100; + frame1.pclSimStartTime = 0; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + FrameData next1{}; + next1.presentStartTime = 2'000'000; + next1.timeInPresent = 400; + next1.readyTime = 2'500'000; + next1.finalState = PresentResult::Presented; + next1.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + auto metrics1 = ComputeMetricsForPresent(qpc, frame1, &next1, state); + Assert::AreEqual(size_t(1), metrics1.size()); + + // First displayed app frame seeds state; animation time is not reported yet. + Assert::IsFalse( + metrics1[0].metrics.msAnimationTime.has_value(), + L"First AppProvider frame should seed firstAppSimStartTime but not report msAnimationTime yet."); + + // After processing frame1, the chain should have latched sim start and + // switched to AppProvider. + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime); + Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime); + Assert::IsTrue( + state.animationErrorSource == AnimationErrorSource::AppProvider, + L"Animation source should transition to AppProvider after first appSimStartTime."); + + // -------------------------------------------------------------------- + // Frame 2: second displayed app frame + // -------------------------------------------------------------------- + FrameData frame2{}; + frame2.presentStartTime = 3'000'000; + frame2.timeInPresent = 500; + frame2.readyTime = 3'500'000; + frame2.appSimStartTime = 150; + frame2.pclSimStartTime = 0; + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 3'000'000 }); + + FrameData next2{}; + next2.presentStartTime = 4'000'000; + next2.timeInPresent = 400; + next2.readyTime = 4'500'000; + next2.finalState = PresentResult::Presented; + next2.displayed.PushBack({ FrameType::Application, 4'000'000 }); + + auto metrics2 = ComputeMetricsForPresent(qpc, frame2, &next2, state); + Assert::AreEqual(size_t(1), metrics2.size()); + Assert::IsTrue( + metrics2[0].metrics.msAnimationTime.has_value(), + L"Second displayed app frame should report msAnimationTime."); + + double expected2 = qpc.DeltaUnsignedMilliSeconds(100, 150); + Assert::AreEqual( + expected2, + metrics2[0].metrics.msAnimationTime.value(), + 0.0001, + L"Frame 2's msAnimationTime should be relative to firstAppSimStartTime (100 → 150)."); + + // firstAppSimStartTime should stay anchored at 100 + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, L"firstAppSimStartTime should not change."); + // lastDisplayedSimStartTime should now reflect frame2 + Assert::AreEqual(uint64_t(150), state.lastDisplayedSimStartTime); + + // -------------------------------------------------------------------- + // Frame 3: third displayed app frame + // -------------------------------------------------------------------- + FrameData frame3{}; + frame3.presentStartTime = 5'000'000; + frame3.timeInPresent = 500; + frame3.readyTime = 5'500'000; + frame3.appSimStartTime = 250; + frame3.pclSimStartTime = 0; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 5'000'000 }); + + FrameData next3{}; + next3.presentStartTime = 6'000'000; + next3.timeInPresent = 400; + next3.readyTime = 6'500'000; + next3.finalState = PresentResult::Presented; + next3.displayed.PushBack({ FrameType::Application, 6'000'000 }); + + auto metrics3 = ComputeMetricsForPresent(qpc, frame3, &next3, state); + Assert::AreEqual(size_t(1), metrics3.size()); + Assert::IsTrue( + metrics3[0].metrics.msAnimationTime.has_value(), + L"Third displayed app frame should report msAnimationTime."); + + double expected3 = qpc.DeltaUnsignedMilliSeconds(100, 250); + Assert::AreEqual( + expected3, + metrics3[0].metrics.msAnimationTime.value(), + 0.0001, + L"Frame 3's msAnimationTime should be relative to original firstAppSimStartTime (100 → 250)."); + + // firstAppSimStartTime remains the original seed + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, L"firstAppSimStartTime should remain at 100."); + // lastDisplayedSimStartTime should now reflect frame3 + Assert::AreEqual(uint64_t(250), state.lastDisplayedSimStartTime); + } + // ======================================================================== + // A5: AnimationTime_AppProvider_SkippedFrame_StaysConsistent + // ======================================================================== + TEST_METHOD(AnimationTime_AppProvider_SkippedFrame_StaysConsistent) + { + // We want to verify: + // - Animation source is AppProvider (AppSimStartTime-based). + // - A discarded (not displayed) frame with AppSimStartTime: + // * Produces no animation metrics. + // * Does NOT change firstAppSimStartTime / lastDisplayedSimStartTime / + // lastDisplayedAppScreenTime. + // - A later displayed app frame still computes msAnimationTime correctly + // based on the original firstAppSimStartTime. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + // Seed state as if we already had one displayed app frame at sim = 100. + state.animationErrorSource = AnimationErrorSource::AppProvider; + state.firstAppSimStartTime = 100; + state.lastDisplayedSimStartTime = 100; + state.lastDisplayedAppScreenTime = 1'000'000; // prior displayed screen time + + // -------------------------------------------------------------------- + // Frame 1: Discarded / not displayed, but with AppSimStartTime = 150 + // -------------------------------------------------------------------- + FrameData frameDropped{}; + frameDropped.presentStartTime = 2'000'000; + frameDropped.timeInPresent = 50'000; + frameDropped.readyTime = 2'050'000; + frameDropped.appSimStartTime = 150; + frameDropped.finalState = PresentResult::Discarded; + // No displayed entries -> not displayed + + auto droppedResults = ComputeMetricsForPresent(qpc, frameDropped, nullptr, state); + Assert::AreEqual(size_t(1), droppedResults.size()); + const auto& droppedMetrics = droppedResults[0].metrics; + + // Discarded / not-displayed frame must NOT produce animation metrics. + Assert::IsFalse(droppedMetrics.msAnimationTime.has_value(), + L"Discarded frame should not have msAnimationTime"); + Assert::IsFalse(droppedMetrics.msAnimationError.has_value(), + L"Discarded frame should not have msAnimationError"); + + // And it must NOT disturb the animation anchors from prior displayed frames. + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, + L"firstAppSimStartTime should be unchanged by discarded frame"); + Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime, + L"lastDisplayedSimStartTime should be unchanged by discarded frame"); + Assert::AreEqual(uint64_t(1'000'000), state.lastDisplayedAppScreenTime, + L"lastDisplayedAppScreenTime should be unchanged by discarded frame"); + + // -------------------------------------------------------------------- + // Frame 2: Next displayed app frame with AppSimStartTime = 200 + // -------------------------------------------------------------------- + FrameData frameDisplayed{}; + frameDisplayed.presentStartTime = 3'000'000; + frameDisplayed.timeInPresent = 50'000; + frameDisplayed.readyTime = 3'050'000; + frameDisplayed.appSimStartTime = 200; + frameDisplayed.finalState = PresentResult::Presented; + frameDisplayed.displayed.PushBack({ FrameType::Application, 3'500'000 }); + + // Dummy "next" frame – just to exercise the Case 3 path so that + // UpdateAfterPresent() is called for frameDisplayed. + FrameData frameNext{}; + frameNext.presentStartTime = 4'000'000; + frameNext.timeInPresent = 50'000; + frameNext.readyTime = 4'050'000; + frameNext.appSimStartTime = 250; + frameNext.finalState = PresentResult::Presented; + frameNext.displayed.PushBack({ FrameType::Application, 4'500'000 }); + + auto displayedResults = ComputeMetricsForPresent(qpc, frameDisplayed, &frameNext, state); + Assert::AreEqual(size_t(1), displayedResults.size()); + const auto& displayedMetrics = displayedResults[0].metrics; + + Assert::IsTrue(displayedMetrics.msAnimationTime.has_value(), + L"Displayed app frame should have msAnimationTime"); + + // Animation time should be based purely on the AppProvider sim times: + // firstAppSimStartTime = 100, currentSim = 200. + const double expected = qpc.DeltaUnsignedMilliSeconds(100, 200); + Assert::AreEqual(expected, displayedMetrics.msAnimationTime.value(), 0.0001); + + // After processing a displayed app frame via the Case 3 path, + // state should now reflect that frame as the last displayed. + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, + L"firstAppSimStartTime should remain the first displayed AppSimStartTime"); + Assert::AreEqual(uint64_t(200), state.lastDisplayedSimStartTime, + L"lastDisplayedSimStartTime should track the most recent DISPLAYED AppSimStartTime"); + Assert::AreEqual(uint64_t(3'500'000), state.lastDisplayedAppScreenTime, + L"lastDisplayedAppScreenTime should track the most recent displayed screen time"); + } + // ======================================================================== + // B1: AnimationTime_PCLatency_FirstFrame_ZeroWithoutPclSimStartTime + // ======================================================================== + TEST_METHOD(AnimationTime_PCLatency_FirstFrame_ZeroWithoutPclSimStartTime) + { + // Scenario: + // - SwapChainCoreState starts with CpuStart (default) + // - Current frame: displayed, displayIndex == appIndex, but pclSimStartTime == 0 + // - No PCL data means source stays CpuStart + // + // Expected Outcome: + // - msAnimationTime = std::nullopt (no valid sim start time, no history) + // - firstAppSimStartTime remains 0 in state + // - Source remains CpuStart + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + // state.animationErrorSource defaults to CpuStart + + // Verify initial state + Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime); + Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime); + + // Create a displayed app frame WITHOUT pclSimStartTime + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 500; + frame.readyTime = 1'500'000; + frame.appSimStartTime = 0; + frame.pclSimStartTime = 0; // No PC latency instrumentation yet + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + // Verify frame setup + Assert::AreEqual(uint64_t(0), frame.pclSimStartTime); + Assert::AreEqual(size_t(1), frame.displayed.Size()); + + // Create nextDisplayed to allow processing + FrameData next{}; + next.presentStartTime = 2'000'000; + next.timeInPresent = 400; + next.readyTime = 2'500'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + // Action: Compute metrics for this frame + auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + + // Assert: Should have one computed metric + Assert::AreEqual(size_t(1), metricsVector.size()); + + const ComputedMetrics& result = metricsVector[0]; + + // Assert: msAnimationTime should be nullopt + Assert::IsFalse(result.metrics.msAnimationTime.has_value(), + L"msAnimationTime should be nullopt when pclSimStartTime is 0 on first frame"); + + // Assert: firstAppSimStartTime in state should remain 0 + Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, + L"State: firstAppSimStartTime should remain 0 (no valid pcl sim start time detected)"); + + // Assert: lastDisplayedSimStartTime should remain 0 + Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime, + L"State: lastDisplayedSimStartTime should remain 0 (no valid pcl sim start time detected)"); + } + + // ======================================================================== + // B2: AnimationTime_PCLatency_TransitionFrame_FirstValidPclSimStart + // ======================================================================== + TEST_METHOD(AnimationTime_PCLatency_TransitionFrame_FirstValidPclSimStart) + { + // Scenario: + // - Start with CpuStart source (default) + // - Frame 1: PCL data arrives, triggers source switch to PCLatency + // - Expected: msAnimationTime = 0 (first frame with valid pcl sim start) + // + // Expected Outcome: + // - msAnimationTime = 0 (first frame with PCL instrumentation) + // - firstAppSimStartTime is set to qpc(100) + // - Source switches to PCLatency after UpdateChain + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + // state.animationErrorSource defaults to CpuStart + + // Create a displayed app frame WITH pclSimStartTime for the first time + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 500; + frame.readyTime = 1'500'000; + frame.appSimStartTime = 0; + frame.pclSimStartTime = 100; // First valid pcl sim start + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + // Create nextDisplayed + FrameData frame2{}; + frame2.presentStartTime = 2'000'000; + frame2.timeInPresent = 400; + frame2.readyTime = 2'500'000; + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + // Action: Compute metrics + auto metricsVector = ComputeMetricsForPresent(qpc, frame, &frame2, state); + + // Assert + Assert::AreEqual(size_t(1), metricsVector.size()); + const ComputedMetrics& result = metricsVector[0]; + + // Assert: msAnimationTime shouldn't have a value (first frame with valid pcl sim start) + Assert::IsFalse(result.metrics.msAnimationTime.has_value(), + L"msAnimationTime should not have a value on first valid pcl sim start"); + + // Assert: State should be updated + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, + L"State: firstAppSimStartTime should be set to first valid pcl sim start"); + Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime, + L"State: lastDisplayedSimStartTime should be set to current frame's pcl sim start"); + Assert::IsTrue( + state.animationErrorSource == AnimationErrorSource::PCLatency, + L"Animation source should transition to PCLatency after first appSimStartTime."); + } + + // ======================================================================== + // B3: AnimationTime_PCLatency_SecondFrame_IncrementsCorrectly + // ======================================================================== + TEST_METHOD(AnimationTime_PCLatency_SecondFrame_IncrementsCorrectly) + { + // Scenario: + // - Frame 2: pclSimStartTime = 200 (100 QPC ticks later) + // - QPC frequency = 10 MHz → 100 ticks = 10 µs = 0.01 ms + // + // Expected Outcome: + // - msAnimationTime ≈ 0.01 ms (elapsed sim time from first to current) + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + // Start with CpuStart, will switch to PCLatency after frame1 + + // Frame 1: First PCL data + FrameData frame1{}; + frame1.presentStartTime = 500'000; + frame1.timeInPresent = 300; + frame1.pclSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 900'000 }); + + FrameData next1{}; + next1.presentStartTime = 1'500'000; + next1.finalState = PresentResult::Presented; + next1.displayed.PushBack({ FrameType::Application, 1'500'000 }); + + ComputeMetricsForPresent(qpc, frame1, &next1, state); + // After this, source is PCLatency, firstAppSimStartTime = 100 + + // Frame 2: Second PCL frame with incremented sim start + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 500; + frame.readyTime = 1'500'000; + frame.appSimStartTime = 0; + frame.pclSimStartTime = 200; // 100 ticks later + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + // Create nextDisplayed + FrameData next{}; + next.presentStartTime = 2'000'000; + next.timeInPresent = 400; + next.readyTime = 2'500'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + // Action: Compute metrics + auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + + Assert::AreEqual(size_t(1), metricsVector.size()); + const ComputedMetrics& result = metricsVector[0]; + + // Assert: msAnimationTime should be 0.01 ms + Assert::IsTrue(result.metrics.msAnimationTime.has_value()); + double expectedMs = qpc.DeltaUnsignedMilliSeconds(100, 200); + Assert::AreEqual(expectedMs, result.metrics.msAnimationTime.value(), 0.0001, + L"msAnimationTime should reflect elapsed time from first pcl sim start"); + + // Assert: firstAppSimStartTime unchanged + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, + L"firstAppSimStartTime should remain at first value"); + + // Assert: lastDisplayedSimStartTime updated + Assert::AreEqual(uint64_t(200), state.lastDisplayedSimStartTime, + L"lastDisplayedSimStartTime should be updated to current frame's pcl sim start"); + } + + // ======================================================================== + // B4: AnimationTime_PCLatency_ThreeFrames_CumulativeElapsedTime + // ======================================================================== + TEST_METHOD(AnimationTime_PCLatency_ThreeFrames_CumulativeElapsedTime) + { + // Scenario: + // - Start with CpuStart as the animation source. + // - Frame 1: first displayed app frame with appSimStartTime = 100. + // This SEEDS animation state (firstAppSimStartTime), but + // does not yet emit msAnimationTime. + // - Frame 2: displayed app frame with appSimStartTime = 150. + // - Frame 3: displayed app frame with appSimStartTime = 250. + // + // QPC frequency = 10 MHz. + // + // Expected outcome: + // - firstAppSimStartTime is latched to 100 and never changes. + // - Frame 1: msAnimationTime == nullopt (just seeds state). + // - Frame 2: msAnimationTime = (150 - 100) / 10e6. + // - Frame 3: msAnimationTime = (250 - 100) / 10e6. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + // animationErrorSource defaults to CpuStart; will transition to AppProvider + // when the first displayed frame with appSimStartTime arrives. + + // -------------------------------------------------------------------- + // Frame 1: first valid app sim start, displayed + // -------------------------------------------------------------------- + FrameData frame1{}; + frame1.presentStartTime = 1'000'000; + frame1.timeInPresent = 500; + frame1.readyTime = 1'500'000; + frame1.appSimStartTime = 0; + frame1.pclSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + FrameData next1{}; + next1.presentStartTime = 2'000'000; + next1.timeInPresent = 400; + next1.readyTime = 2'500'000; + next1.finalState = PresentResult::Presented; + next1.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + auto metrics1 = ComputeMetricsForPresent(qpc, frame1, &next1, state); + Assert::AreEqual(size_t(1), metrics1.size()); + + // First displayed app frame seeds state; animation time is not reported yet. + Assert::IsFalse( + metrics1[0].metrics.msAnimationTime.has_value(), + L"First PCLatecny frame should seed firstAppSimStartTime but not report msAnimationTime yet."); + + // After processing frame1, the chain should have latched sim start and + // switched to PCLatency. + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime); + Assert::AreEqual(uint64_t(100), state.lastDisplayedSimStartTime); + Assert::IsTrue( + state.animationErrorSource == AnimationErrorSource::PCLatency, + L"Animation source should transition to PCLatency after first appSimStartTime."); + + // -------------------------------------------------------------------- + // Frame 2: second displayed app frame + // -------------------------------------------------------------------- + FrameData frame2{}; + frame2.presentStartTime = 3'000'000; + frame2.timeInPresent = 500; + frame2.readyTime = 3'500'000; + frame2.appSimStartTime = 0; + frame2.pclSimStartTime = 150; + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 3'000'000 }); + + FrameData next2{}; + next2.presentStartTime = 4'000'000; + next2.timeInPresent = 400; + next2.readyTime = 4'500'000; + next2.finalState = PresentResult::Presented; + next2.displayed.PushBack({ FrameType::Application, 4'000'000 }); + + auto metrics2 = ComputeMetricsForPresent(qpc, frame2, &next2, state); + Assert::AreEqual(size_t(1), metrics2.size()); + Assert::IsTrue( + metrics2[0].metrics.msAnimationTime.has_value(), + L"Second displayed app frame should report msAnimationTime."); + + double expected2 = qpc.DeltaUnsignedMilliSeconds(100, 150); + Assert::AreEqual( + expected2, + metrics2[0].metrics.msAnimationTime.value(), + 0.0001, + L"Frame 2's msAnimationTime should be relative to firstAppSimStartTime (100 → 150)."); + + // firstAppSimStartTime should stay anchored at 100 + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, L"firstAppSimStartTime should not change."); + // lastDisplayedSimStartTime should now reflect frame2 + Assert::AreEqual(uint64_t(150), state.lastDisplayedSimStartTime); + + // -------------------------------------------------------------------- + // Frame 3: third displayed app frame + // -------------------------------------------------------------------- + FrameData frame3{}; + frame3.presentStartTime = 5'000'000; + frame3.timeInPresent = 500; + frame3.readyTime = 5'500'000; + frame3.appSimStartTime = 0; + frame3.pclSimStartTime = 250; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 5'000'000 }); + + FrameData next3{}; + next3.presentStartTime = 6'000'000; + next3.timeInPresent = 400; + next3.readyTime = 6'500'000; + next3.finalState = PresentResult::Presented; + next3.displayed.PushBack({ FrameType::Application, 6'000'000 }); + + auto metrics3 = ComputeMetricsForPresent(qpc, frame3, &next3, state); + Assert::AreEqual(size_t(1), metrics3.size()); + Assert::IsTrue( + metrics3[0].metrics.msAnimationTime.has_value(), + L"Third displayed app frame should report msAnimationTime."); + + double expected3 = qpc.DeltaUnsignedMilliSeconds(100, 250); + Assert::AreEqual( + expected3, + metrics3[0].metrics.msAnimationTime.value(), + 0.0001, + L"Frame 3's msAnimationTime should be relative to original firstAppSimStartTime (100 → 250)."); + + // firstAppSimStartTime remains the original seed + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, L"firstAppSimStartTime should remain at 100."); + // lastDisplayedSimStartTime should now reflect frame3 + Assert::AreEqual(uint64_t(250), state.lastDisplayedSimStartTime); + } + // ======================================================================== + // B5: AnimationTime_PCLatency_SkippedFrame_StaysConsistent + // ======================================================================== + TEST_METHOD(AnimationTime_PCLatency_SkippedFrame_StaysConsistent) + { + // Scenario: + // - Chain is configured to use PCLatency as the animation source. + // - Frame 1: displayed, pclSimStartTime = 100 → seeds animation state. + // - Frame 2: discarded (not displayed), pclSimStartTime = 200 → should NOT + // produce animation time and should NOT advance animation state. + // - Frame 3: displayed again, pclSimStartTime = 300 → animation time should be + // measured from the original 100, skipping the discarded frame. + // + // Expectations: + // - Frame 1: msAnimationTime is std::nullopt, but firstAppSimStartTime and + // lastDisplayedSimStartTime are set to 100. + // - Frame 2: msAnimationTime is std::nullopt and state remains 100/100. + // - Frame 3: msAnimationTime == Delta(100, 300) and lastDisplayedSimStartTime == 300. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // + // Frame 1: first displayed PCL frame + // + FrameData frame1{}; + frame1.presentStartTime = 1'000'000; + frame1.timeInPresent = 10'000; + frame1.readyTime = 1'010'000; + frame1.pclSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + FrameData next1{}; + next1.presentStartTime = 3'000'000; + next1.timeInPresent = 10'000; + next1.readyTime = 3'010'000; + next1.finalState = PresentResult::Presented; + next1.displayed.PushBack({ FrameType::Application, 4'000'000 }); + + auto metrics1 = ComputeMetricsForPresent(qpc, frame1, &next1, chain); + Assert::AreEqual(size_t(1), metrics1.size()); + + // First displayed frame just seeds animation state; no animation time yet + Assert::IsFalse( + metrics1[0].metrics.msAnimationTime.has_value(), + L"First PCL-driven displayed frame should only seed state; no animation time reported yet."); + + Assert::AreEqual(uint64_t(100), chain.firstAppSimStartTime); + Assert::AreEqual(uint64_t(100), chain.lastDisplayedSimStartTime); + Assert::IsTrue(AnimationErrorSource::PCLatency == chain.animationErrorSource); + + // + // Frame 2: discarded (not displayed) but has a PCL sim start + // + FrameData frame2{}; + frame2.presentStartTime = 5'000'000; + frame2.timeInPresent = 10'000; + frame2.readyTime = 5'010'000; + frame2.pclSimStartTime = 200; // Has PCL sim start but not displayed + frame2.finalState = PresentResult::Discarded; // Not presented → not displayed + + auto metrics2 = ComputeMetricsForPresent(qpc, frame2, nullptr, chain); + Assert::AreEqual(size_t(1), metrics2.size()); + + // Not displayed → no animation time, and animation state should not advance + Assert::IsFalse( + metrics2[0].metrics.msAnimationTime.has_value(), + L"Non-displayed frame should not report animation time."); + + Assert::AreEqual(uint64_t(100), chain.firstAppSimStartTime, + L"firstAppSimStartTime must remain anchored to Frame 1 after skipped frame."); + Assert::AreEqual(uint64_t(100), chain.lastDisplayedSimStartTime, + L"lastDisplayedSimStartTime must remain anchored to Frame 1 after skipped frame."); + + // + // Frame 3: displayed again after the skipped frame + // + FrameData frame3{}; + frame3.presentStartTime = 6'000'000; + frame3.timeInPresent = 10'000; + frame3.readyTime = 6'010'000; + frame3.pclSimStartTime = 300; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 7'000'000 }); + + FrameData next3{}; + next3.presentStartTime = 8'000'000; + next3.timeInPresent = 10'000; + next3.readyTime = 8'010'000; + next3.finalState = PresentResult::Presented; + next3.displayed.PushBack({ FrameType::Application, 9'000'000 }); + + auto metrics3 = ComputeMetricsForPresent(qpc, frame3, &next3, chain); + Assert::AreEqual(size_t(1), metrics3.size()); + Assert::IsTrue( + metrics3[0].metrics.msAnimationTime.has_value(), + L"Displayed frame with valid PCL sim start should report animation time."); + + double expected3 = qpc.DeltaUnsignedMilliSeconds(100, 300); + Assert::AreEqual( + expected3, + metrics3[0].metrics.msAnimationTime.value(), + 0.0001, + L"Frame 3's msAnimationTime should be measured from Frame 1's PCL sim start, skipping Frame 2."); + + Assert::AreEqual(uint64_t(100), chain.firstAppSimStartTime, + L"firstAppSimStartTime should remain at Frame 1's value."); + Assert::AreEqual(uint64_t(300), chain.lastDisplayedSimStartTime, + L"lastDisplayedSimStartTime should advance to Frame 3's PCL sim start."); + } + // ======================================================================== + // B6: AnimationTime_PCLatency_FallsBackToCpuStart_WhenPclSimStartTimeZero + // ======================================================================== + TEST_METHOD(AnimationTime_PCLatency_FallsBackToCpuStart_WhenPclSimStartTimeZero) + { + // Scenario: + // - Start with CpuStart source + // - Frame 1: PCL data establishes PCLatency source + // - Frame 2: pclSimStartTime = 0 (PCL data disappears) + // - Source should stay PCLatency (no fallback), msAnimationTime = nullopt + // + // Expected Outcome: + // - msAnimationTime = nullopt (PCL data missing) + // - animationErrorSource remains PCLatency (no fallback) + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + // Frame 1: First PCL data + FrameData frame1{}; + frame1.presentStartTime = 500'000; + frame1.timeInPresent = 300; + frame1.pclSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 900'000 }); + + FrameData next1{}; + next1.presentStartTime = 1'500'000; + next1.finalState = PresentResult::Presented; + next1.displayed.PushBack({ FrameType::Application, 1'500'000 }); + + auto metrics1 = ComputeMetricsForPresent(qpc, frame1, &next1, state); + Assert::AreEqual(size_t(1), metrics1.size()); + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime); + Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::PCLatency); + + // Frame 2: PCL data missing (pclSimStartTime = 0) + FrameData frame{}; + frame.presentStartTime = 1'200'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'300'000; + frame.appSimStartTime = 0; + frame.pclSimStartTime = 0; // PCL data disappeared + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'400'000 }); + + // Create nextDisplayed + FrameData next{}; + next.presentStartTime = 1'600'000; + next.timeInPresent = 50'000; + next.readyTime = 1'700'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'800'000 }); + + // Action: Compute metrics + auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + + // Assert: Should have one computed metric + Assert::AreEqual(size_t(1), metricsVector.size()); + + const ComputedMetrics& result = metricsVector[0]; + + // Assert: msAnimationTime should be nullopt (no PCL data, no fallback) + Assert::IsFalse(result.metrics.msAnimationTime.has_value(), + L"msAnimationTime should be nullopt when pclSimStartTime is 0"); + + // Assert: State should remain PCLatency + Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::PCLatency, + L"animationErrorSource should remain PCLatency (no fallback)"); + + // Assert: firstAppSimStartTime unchanged + Assert::AreEqual(uint64_t(100), state.firstAppSimStartTime, + L"firstAppSimStartTime should remain unchanged"); + } + // ======================================================================== + // D1: AnimationTime_CpuStart_FirstFrame_ZeroWithoutHistory + // ======================================================================== + TEST_METHOD(AnimationTime_CpuStart_FirstFrame_ZeroWithoutHistory) + { + // Scenario: + // - SwapChainCoreState is in initial state (no prior frames) + // - Current frame: displayed, displayIndex == appIndex + // - animationErrorSource = CpuStart + // - No lastAppPresent or lastPresent in chain + // + // Expected Outcome: + // - msAnimationTime = std::nullopt (cpuStart = 0, cannot initialize firstAppSimStartTime) + // - firstAppSimStartTime remains 0 in state + // - lastDisplayedSimStartTime remains 0 in state + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + // state.animationErrorSource defaults to CpuStart + + // Verify initial state + Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime); + Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime); + + // Create a displayed app frame with CpuStart source + FrameData frame{}; + frame.presentStartTime = 1'000'000; + frame.timeInPresent = 500; + frame.readyTime = 1'500'000; + frame.appSimStartTime = 0; + frame.pclSimStartTime = 0; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + // Verify frame setup + Assert::AreEqual(size_t(1), frame.displayed.Size()); + + // Create nextDisplayed to allow processing + FrameData next{}; + next.presentStartTime = 2'000'000; + next.timeInPresent = 400; + next.readyTime = 2'500'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 2'000'000 }); + + // Action: Compute metrics for this frame + auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + + // Assert: Should have one computed metric + Assert::AreEqual(size_t(1), metricsVector.size()); + + const ComputedMetrics& result = metricsVector[0]; + + // Assert: msAnimationTime should be nullopt (cpuStart = 0) + Assert::IsFalse(result.metrics.msAnimationTime.has_value(), + L"msAnimationTime should be nullopt when no prior frame history exists"); + + // Assert: State should not be updated + Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, + L"State: firstAppSimStartTime should remain 0 (no valid CPU start available)"); + + // Assert: lastDisplayedSimStartTime should remain 0 + Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime, + L"State: lastDisplayedSimStartTime should remain 0"); + } + + // ======================================================================== + // D2: AnimationTime_CpuStart_TransitionFrame_FirstValidCpuStart + // ======================================================================== + TEST_METHOD(AnimationTime_CpuStart_TransitionFrame_FirstValidCpuStart) + { + // Scenario: + // - Prior state: firstAppSimStartTime == 0 (no prior CPU start) + // - Chain has lastAppPresent with valid timing data + // - Current frame: displayed, displayIndex == appIndex + // - cpuStart = lastAppPresent.presentStartTime + lastAppPresent.timeInPresent = 1'000'000 + // - animationErrorSource = CpuStart + // + // Expected Outcome: + // - msAnimationTime = 0 (first frame with valid CPU start) + // - lastDisplayedSimStartTime is updated to cpuStart (1'000'000) + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + // state.animationErrorSource defaults to CpuStart + + // Set up prior app present for CPU start calculation + FrameData priorApp{}; + priorApp.presentStartTime = 800'000; + priorApp.timeInPresent = 200'000; + priorApp.readyTime = 1'000'000; + priorApp.finalState = PresentResult::Presented; + priorApp.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + state.lastAppPresent = priorApp; + + // Create a displayed app frame with CpuStart source + FrameData frame{}; + frame.presentStartTime = 1'200'000; + frame.timeInPresent = 100'000; + frame.readyTime = 1'300'000; + frame.appSimStartTime = 0; + frame.pclSimStartTime = 0; + frame.finalState = PresentResult::Presented; + frame.displayed.PushBack({ FrameType::Application, 1'400'000 }); + + // Create nextDisplayed + FrameData next{}; + next.presentStartTime = 1'600'000; + next.timeInPresent = 50'000; + next.readyTime = 1'700'000; + next.finalState = PresentResult::Presented; + next.displayed.PushBack({ FrameType::Application, 1'800'000 }); + + // Action: Compute metrics + auto metricsVector = ComputeMetricsForPresent(qpc, frame, &next, state); + + // Assert + Assert::AreEqual(size_t(1), metricsVector.size()); + const ComputedMetrics& result = metricsVector[0]; + + // Assert: msAnimationTime should be 0 (first transition frame) + Assert::IsTrue(result.metrics.msAnimationTime.has_value(), + L"msAnimationTime should have a value on first valid CPU start"); + Assert::AreEqual(0.0, result.metrics.msAnimationTime.value(), 0.0001, + L"msAnimationTime should be 0 on first transition frame"); + + // Assert: State should be updated with CPU start + // cpuStart = 800'000 + 200'000 = 1'000'000 + uint64_t expectedCpuStart = 800'000 + 200'000; + Assert::AreEqual(expectedCpuStart, state.lastDisplayedSimStartTime, + L"State: lastDisplayedSimStartTime should be set to CPU start value"); + } + + // ======================================================================== + // D3: AnimationTime_CpuStart_IncreasesAcrossFramesWithoutProvider + // ======================================================================== + TEST_METHOD(AnimationTime_CpuStart_IncreasesAcrossFramesWithoutProvider) + { + // Scenario: + // - animationErrorSource = CpuStart + // - No App or PCL sim start timestamps on any frame. + // - We seed lastAppPresent so the first tested frame already has + // a non-zero CpuStart. + // - We then process two displayed frames and expect: + // * Both report msAnimationTime (has_value()). + // * Frame 2's msAnimationTime > Frame 1's msAnimationTime. + // * firstAppSimStartTime stays 0 (no provider events yet). + + QpcConverter qpc(10'000'000, 500'000); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::CpuStart; + + // Seed lastAppPresent so CalculateCPUStart() for frame1 is non-zero. + // cpuStart1 = prior.presentStartTime + prior.timeInPresent + FrameData prior{}; + prior.presentStartTime = 1'000'000; + prior.timeInPresent = 100'000; // cpuStart1 = 1'100'000 + prior.readyTime = 1'200'000; + prior.finalState = PresentResult::Presented; + prior.displayed.PushBack({ FrameType::Application, 1'300'000 }); + state.lastAppPresent = prior; + + // Sanity: no provider sim-start yet. + state.firstAppSimStartTime = 0; + state.lastDisplayedSimStartTime = 0; + + // -------------------------------------------------------------------- + // Frame 1: Presented + displayed, no App/PCL sim start + // -------------------------------------------------------------------- + FrameData frame1{}; + frame1.presentStartTime = 2'000'000; + frame1.timeInPresent = 80'000; + frame1.readyTime = 2'100'000; + frame1.appSimStartTime = 0; + frame1.pclSimStartTime = 0; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 2'500'000 }); + + FrameData next1{}; + next1.presentStartTime = 3'000'000; + next1.timeInPresent = 50'000; + next1.readyTime = 3'100'000; + next1.finalState = PresentResult::Presented; + next1.displayed.PushBack({ FrameType::Application, 3'500'000 }); + + auto metrics1 = ComputeMetricsForPresent(qpc, frame1, &next1, state); + Assert::AreEqual(size_t(1), metrics1.size()); + const auto& m1 = metrics1[0].metrics; + + Assert::IsTrue(m1.msAnimationTime.has_value(), + L"CpuStart animation should report msAnimationTime even without App/PCL provider."); + const double anim1 = m1.msAnimationTime.value(); + Assert::IsTrue(anim1 > 0.0, + L"First CpuStart-driven frame should have a positive animation time relative to session/start anchor."); + + // No provider yet → firstAppSimStartTime must remain 0. + Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, + L"firstAppSimStartTime should not be set until App/PCL provider events arrive."); + + // After frame1, lastAppPresent is now frame1; CpuStart for frame2 will be later. + // -------------------------------------------------------------------- + // Frame 2: later Presented + displayed frame, still no App/PCL provider + // -------------------------------------------------------------------- + FrameData frame2{}; + frame2.presentStartTime = 4'000'000; + frame2.timeInPresent = 120'000; + frame2.readyTime = 4'200'000; + frame2.appSimStartTime = 0; + frame2.pclSimStartTime = 0; + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 4'600'000 }); + + FrameData next2{}; + next2.presentStartTime = 5'000'000; + next2.timeInPresent = 50'000; + next2.readyTime = 5'100'000; + next2.finalState = PresentResult::Presented; + next2.displayed.PushBack({ FrameType::Application, 5'500'000 }); + + auto metrics2 = ComputeMetricsForPresent(qpc, frame2, &next2, state); + Assert::AreEqual(size_t(1), metrics2.size()); + const auto& m2 = metrics2[0].metrics; + + Assert::IsTrue(m2.msAnimationTime.has_value(), + L"Second CpuStart-driven frame should also report msAnimationTime."); + const double anim2 = m2.msAnimationTime.value(); + + Assert::IsTrue(anim2 > anim1, + L"CpuStart-based animation time should increase across frames as CpuStart advances."); + + // Still no provider events → anchor remains "non-provider", so firstAppSimStartTime should be 0. + Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, + L"firstAppSimStartTime should still be 0 without App/PCL provider data."); + } + }; + + // ============================================================================ + // SECTION: Animation Error Tests + // ============================================================================ + + TEST_CLASS(AnimationErrorTests) + { + public: + // Section B: Animation Error – AppProvider Source + + TEST_METHOD(AnimationError_AppProvider_NoLastDisplayedFrame_Nullopt) + { + // Scenario: First app-displayed frame with appSimStartTime + // Expected: msAnimationError = std::nullopt (no prior frame data) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::AppProvider; + + FrameData present{}; + present.presentStartTime = 1000; + present.timeInPresent = 100; + present.appSimStartTime = 150; + present.finalState = PresentResult::Presented; + present.displayed.PushBack({ FrameType::Application, 200 }); + + FrameData nextPresent{}; + nextPresent.presentStartTime = 2000; + nextPresent.finalState = PresentResult::Presented; + nextPresent.displayed.PushBack({ FrameType::Application, 2100 }); + + auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); + + Assert::AreEqual(size_t(1), results.size()); + Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + L"msAnimationError should be nullopt without prior displayed frame"); + } + + TEST_METHOD(AnimationError_AppProvider_TwoFrames_PositiveError) + { + // Scenario: Two frames with sim elapsed = 50, display elapsed = 50 + // Expected: msAnimationError = 0 ms (cadences match) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::AppProvider; + + // Frame 1 setup + FrameData frame1{}; + frame1.presentStartTime = 1000; + frame1.timeInPresent = 100; + frame1.appSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1000 }); + + FrameData frame2{}; + frame2.presentStartTime = 2000; + frame2.timeInPresent = 100; + frame2.appSimStartTime = 150; // sim elapsed = 50 + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 1050 }); // display elapsed = 50 + + // Process frame 1 + FrameData dummyNext{}; + dummyNext.finalState = PresentResult::Presented; + dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); + auto results1 = ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + + // Process frame 2 + FrameData frame3{}; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 3000 }); + auto results2 = ComputeMetricsForPresent(qpc, frame2, &frame3, state); + + Assert::AreEqual(size_t(1), results2.size()); + Assert::IsTrue(results2[0].metrics.msAnimationError.has_value()); + Assert::AreEqual(0.0, results2[0].metrics.msAnimationError.value(), 0.0001, + L"msAnimationError should be 0 when sim and display cadences match"); + } + + TEST_METHOD(AnimationError_AppProvider_TwoFrames_SimSlowerThanDisplay) + { + // Scenario: sim elapsed = 40 ticks, display elapsed = 50 ticks + // Expected: msAnimationError = -0.001 ms (sim slower) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::AppProvider; + + FrameData frame1{}; + frame1.presentStartTime = 1000; + frame1.timeInPresent = 100; + frame1.appSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1000 }); + + FrameData frame2{}; + frame2.presentStartTime = 2000; + frame2.timeInPresent = 100; + frame2.appSimStartTime = 140; // sim elapsed = 40 + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 1050 }); // display elapsed = 50 + + FrameData dummyNext{}; + dummyNext.finalState = PresentResult::Presented; + dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); + ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + + FrameData frame3{}; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 3000 }); + auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); + + Assert::IsTrue(results[0].metrics.msAnimationError.has_value()); + double simElapsed = qpc.DeltaUnsignedMilliSeconds(100, 140); // 0.004 ms + double displayElapsed = qpc.DeltaUnsignedMilliSeconds(1000, 1050); // 0.005 ms + double expected = simElapsed - displayElapsed; // -0.001 ms + Assert::AreEqual(expected, results[0].metrics.msAnimationError.value(), 0.0001); + } + + TEST_METHOD(AnimationError_AppProvider_TwoFrames_SimFasterThanDisplay) + { + // Scenario: sim elapsed = 60 ticks, display elapsed = 50 ticks + // Expected: msAnimationError = +0.001 ms (sim faster) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::AppProvider; + + FrameData frame1{}; + frame1.presentStartTime = 1000; + frame1.timeInPresent = 100; + frame1.appSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1000 }); + + FrameData frame2{}; + frame2.presentStartTime = 2000; + frame2.timeInPresent = 100; + frame2.appSimStartTime = 160; // sim elapsed = 60 + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 1050 }); // display elapsed = 50 + + FrameData dummyNext{}; + dummyNext.finalState = PresentResult::Presented; + dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); + ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + + FrameData frame3{}; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 3000 }); + auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); + + Assert::IsTrue(results[0].metrics.msAnimationError.has_value()); + double simElapsed = qpc.DeltaUnsignedMilliSeconds(100, 160); // 0.006 ms + double displayElapsed = qpc.DeltaUnsignedMilliSeconds(1000, 1050); // 0.005 ms + double expected = simElapsed - displayElapsed; // +0.001 ms + Assert::AreEqual(expected, results[0].metrics.msAnimationError.value(), 0.0001); + } + + TEST_METHOD(AnimationError_AppProvider_BackwardsSimStartTime_Nullopt) + { + // Scenario: Current sim start goes backward in time + // Expected: msAnimationError = std::nullopt + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::AppProvider; + + FrameData frame1{}; + frame1.presentStartTime = 1000; + frame1.timeInPresent = 100; + frame1.appSimStartTime = 150; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1050 }); + + FrameData frame2{}; + frame2.presentStartTime = 2000; + frame2.timeInPresent = 100; + frame2.appSimStartTime = 140; // backwards! + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 1100 }); + + FrameData dummyNext{}; + dummyNext.finalState = PresentResult::Presented; + dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); + ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + + FrameData frame3{}; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 3000 }); + auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); + + Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + L"msAnimationError should be nullopt when sim start goes backward"); + } + + TEST_METHOD(AnimationError_AppProvider_CurrentSimStartTimeZero_Nullopt) + { + // Scenario: Current frame has no app instrumentation (appSimStartTime = 0) + // Expected: msAnimationError = std::nullopt + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState swapChain{}; + swapChain.animationErrorSource = AnimationErrorSource::AppProvider; + + FrameData frame1{}; + frame1.presentStartTime = 1000; + frame1.timeInPresent = 100; + frame1.appSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1000 }); + + FrameData frame2{}; + frame2.presentStartTime = 2000; + frame2.timeInPresent = 100; + frame2.appSimStartTime = 0; // no instrumentation + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 1050 }); + + FrameData dummyNext{}; + dummyNext.finalState = PresentResult::Presented; + dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); + ComputeMetricsForPresent(qpc, frame1, &dummyNext, swapChain); + + FrameData frame3{}; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 3000 }); + auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, swapChain); + + Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + L"msAnimationError should be nullopt without valid sim start time"); + } + + TEST_METHOD(AnimationError_AppProvider_ZeroDisplayDelta_ErrorIsSimElapsed) + { + // Scenario: Display elapsed = 0 (same screen time) + // Expected: msAnimationError = simElapsed - 0 + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::AppProvider; + + FrameData frame1{}; + frame1.presentStartTime = 1000; + frame1.timeInPresent = 100; + frame1.appSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1000 }); + + FrameData frame2{}; + frame2.presentStartTime = 2000; + frame2.timeInPresent = 100; + frame2.appSimStartTime = 150; // sim elapsed = 50 + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 1000 }); // same screen time! + + FrameData dummyNext{}; + dummyNext.finalState = PresentResult::Presented; + dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); + ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + + FrameData frame3{}; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 3000 }); + auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); + + Assert::IsFalse(results[0].metrics.msAnimationError.has_value()); + } + + // Section C: Animation Error – PCLatency Source + + TEST_METHOD(AnimationError_PCLatency_TwoFrames_ValidPclSimStart) + { + // Scenario: PCL source, sim elapsed = 40, display elapsed = 50 + // Expected: msAnimationError = -0.001 ms + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::PCLatency; + + FrameData frame1{}; + frame1.presentStartTime = 1000; + frame1.timeInPresent = 100; + frame1.pclSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1000 }); + + FrameData frame2{}; + frame2.presentStartTime = 2000; + frame2.timeInPresent = 100; + frame2.pclSimStartTime = 140; // PCL sim elapsed = 40 + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 1050 }); // display elapsed = 50 + + FrameData dummyNext{}; + dummyNext.finalState = PresentResult::Presented; + dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); + ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + + FrameData frame3{}; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 3000 }); + auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); + + Assert::IsTrue(results[0].metrics.msAnimationError.has_value()); + double simElapsed = qpc.DeltaUnsignedMilliSeconds(100, 140); // 0.004 ms + double displayElapsed = qpc.DeltaUnsignedMilliSeconds(1000, 1050); // 0.005 ms + double expected = simElapsed - displayElapsed; // -0.001 ms + Assert::AreEqual(expected, results[0].metrics.msAnimationError.value(), 0.0001); + } + + TEST_METHOD(AnimationError_PCLatency_CurrentPclSimStartZero_Nullopt) + { + // Scenario: PCL source, but current frame has pclSimStartTime = 0 + // Expected: msAnimationError = std::nullopt (no fallback to app in PCL mode) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::PCLatency; + + FrameData frame1{}; + frame1.presentStartTime = 1000; + frame1.timeInPresent = 100; + frame1.pclSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1000 }); + + FrameData frame2{}; + frame2.presentStartTime = 2000; + frame2.timeInPresent = 100; + frame2.pclSimStartTime = 0; // PCL unavailable + frame2.appSimStartTime = 150; // app available, but should not be used + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 1050 }); + + FrameData dummyNext{}; + dummyNext.finalState = PresentResult::Presented; + dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); + ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + + FrameData frame3{}; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 3000 }); + auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); + + Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + L"msAnimationError should be nullopt when PCL source unavailable"); + } + + TEST_METHOD(AnimationError_PCLatency_TransitionFromZero_FirstValidPclSimStart) + { + // Scenario: First frame with valid PCL sim start + // Expected: msAnimationError = std::nullopt (no prior PCL frame) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::PCLatency; + + FrameData present{}; + present.presentStartTime = 1000; + present.timeInPresent = 100; + present.pclSimStartTime = 100; // first valid PCL + present.finalState = PresentResult::Presented; + present.displayed.PushBack({ FrameType::Application, 1000 }); + + FrameData nextPresent{}; + nextPresent.finalState = PresentResult::Presented; + nextPresent.displayed.PushBack({ FrameType::Application, 2000 }); + + auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); + + Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + L"msAnimationError should be nullopt on first valid PCL frame"); + } + + TEST_METHOD(AnimationError_PCLatency_TransitionFromAppToPcl_SourceSwitches) + { + // Scenario: Switching from CpuStart source to PCL + // Expected: Source auto-switches when PCL data arrives + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::CpuStart; + + // Frame 1: Establish baseline + FrameData frame1{}; + frame1.presentStartTime = 800; + frame1.timeInPresent = 100; // CPU end = 900 + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1800 }); + + // Frame 2: Continue with CPU source + FrameData frame2{}; + frame2.presentStartTime = 1000; + frame2.timeInPresent = 100; // CPU end = 1100 + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 2000 }); + + // Frame 3: PCL becomes available, triggers source switch + FrameData frame3{}; + frame3.presentStartTime = 1200; + frame3.timeInPresent = 100; + frame3.pclSimStartTime = 150; // PCL data present + frame3.appSimStartTime = 150; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 2100 }); + + // Frame 4: Next frame for completion + FrameData frame4{}; + frame4.presentStartTime = 1400; + frame4.timeInPresent = 100; + frame4.finalState = PresentResult::Presented; + frame4.displayed.PushBack({ FrameType::Application, 2200 }); + + ComputeMetricsForPresent(qpc, frame1, &frame2, state); + ComputeMetricsForPresent(qpc, frame2, &frame3, state); + + // Frame 3 processes with CpuStart source, then UpdateChain switches to PCLatency + auto results = ComputeMetricsForPresent(qpc, frame3, &frame4, state); + + // Animation error computed using CPU start (source still CpuStart during calculation) + Assert::IsTrue(results[0].metrics.msAnimationError.has_value(), + L"Animation error should be computed with CPU start before source switch"); + + // After UpdateChain, source should have switched to PCLatency + Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::PCLatency, + L"Source should auto-switch to PCLatency after UpdateChain"); + } + + TEST_METHOD(AnimationError_PCLatency_SourcePriority_PclWinsOverApp) + { + // Scenario: Both PCL and App sim start present + // Expected: Use PCL (source priority) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::PCLatency; + + FrameData frame1{}; + frame1.presentStartTime = 1000; + frame1.timeInPresent = 100; + frame1.pclSimStartTime = 100; + frame1.appSimStartTime = 200; // different value + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1000 }); + + FrameData frame2{}; + frame2.presentStartTime = 2000; + frame2.timeInPresent = 100; + frame2.pclSimStartTime = 150; // PCL elapsed = 50 + frame2.appSimStartTime = 300; // app elapsed = 100 (should be ignored) + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 1050 }); // display elapsed = 50 + + FrameData dummyNext{}; + dummyNext.finalState = PresentResult::Presented; + dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); + ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + + FrameData frame3{}; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 3000 }); + auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); + + Assert::IsTrue(results[0].metrics.msAnimationError.has_value()); + // Should use PCL: sim=50, display=50, error=0 + double simElapsed = qpc.DeltaUnsignedMilliSeconds(100, 150); // PCL: 0.005 ms + double displayElapsed = qpc.DeltaUnsignedMilliSeconds(1000, 1050); + double expected = simElapsed - displayElapsed; // 0.0 ms + Assert::AreEqual(expected, results[0].metrics.msAnimationError.value(), 0.0001, + L"Should use PCL source, not app source"); + } + + // Section D: Animation Error – CpuStart Source + + TEST_METHOD(AnimationError_CpuStart_ComputedFromCpuPresent) + { + // Scenario: CpuStart source, CPU-derived sim times + // Expected: Error computed from CPU present end times + // Note: Need baseline frame to establish lastDisplayedSimStartTime + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::CpuStart; + + // Baseline frame to establish initial state + FrameData frame1{}; + frame1.presentStartTime = 800; + frame1.timeInPresent = 100; // CPU sim start for next frame = 900 + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1900 }); + + FrameData frame2{}; + frame2.presentStartTime = 1000; + frame2.timeInPresent = 100; // CPU sim start for next frame = 1100 + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 2000 }); + + FrameData frame3{}; + frame3.presentStartTime = 1200; + frame3.timeInPresent = 100; // CPU sim start = 1300, elapsed from 1100 = 200 + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 2050 }); // display elapsed from 2000 = 50 + + FrameData dummyNext1{}; + dummyNext1.finalState = PresentResult::Presented; + dummyNext1.displayed.PushBack({ FrameType::Application, 2500 }); + ComputeMetricsForPresent(qpc, frame1, &dummyNext1, state); + + FrameData dummyNext2{}; + dummyNext2.finalState = PresentResult::Presented; + dummyNext2.displayed.PushBack({ FrameType::Application, 3000 }); + ComputeMetricsForPresent(qpc, frame2, &dummyNext2, state); + + FrameData frame4{}; + frame4.finalState = PresentResult::Presented; + frame4.displayed.PushBack({ FrameType::Application, 4000 }); + auto results = ComputeMetricsForPresent(qpc, frame3, &frame4, state); + + Assert::IsTrue(results[0].metrics.msAnimationError.has_value()); + double simElapsed = qpc.DeltaUnsignedMilliSeconds(1100, 1300); // 0.020 ms + double displayElapsed = qpc.DeltaUnsignedMilliSeconds(2000, 2050); // 0.005 ms + double expected = simElapsed - displayElapsed; // 0.015 ms + Assert::AreEqual(expected, results[0].metrics.msAnimationError.value(), 0.0001); + } + + TEST_METHOD(AnimationError_CpuStart_Frame2DisplayIsGreaterThanFrame1Display) + { + // Scenario: CpuStart source, CPU-derived sim times + // Frame 2 has a display time earlier than Frame 1 + // Expected: NA reported for animation error + // Note: Need baseline frame to establish lastDisplayedSimStartTime + // and lastDisplayedScreenTime + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::CpuStart; + state.lastDisplayedScreenTime = 55454524262; + state.lastDisplayedSimStartTime = 55454168764; + state.lastDisplayedAppScreenTime = 55454524262; + FrameData frame3{}; + frame3.presentStartTime = 55454299820; + frame3.timeInPresent = 24537; + state.lastPresent = frame3; + + FrameData frame1{}; + frame1.presentStartTime = 55454457377; + frame1.timeInPresent = 2411; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 55454512384 }); + + FrameData frame2{}; + frame2.presentStartTime = 55454612236; + frame2.timeInPresent = 3056; + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 55454615330 }); + + ComputeMetricsForPresent(qpc, frame1, nullptr, state); + auto results = ComputeMetricsForPresent(qpc, frame1, &frame2, state); + + Assert::IsFalse(results[0].metrics.msAnimationError.has_value()); + } + + TEST_METHOD(AnimationError_CpuStart_TransitionToAppProvider_Nullopt) + { + // Scenario: Source switches from CpuStart to AppProvider mid-stream + // Expected: msAnimationError = std::nullopt (first frame of new source) + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::CpuStart; + + FrameData frame1{}; + frame1.presentStartTime = 1000; + frame1.timeInPresent = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 2000 }); + + FrameData frame2{}; + frame2.presentStartTime = 2000; + frame2.timeInPresent = 100; + frame2.appSimStartTime = 100; // app instrumentation appears + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 2050 }); + + FrameData dummyNext{}; + dummyNext.finalState = PresentResult::Presented; + dummyNext.displayed.PushBack({ FrameType::Application, 3000 }); + ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + + FrameData frame3{}; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 4000 }); + auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); + + Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + L"msAnimationError should be nullopt on source transition"); + Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, + L"Source should auto-switch to AppProvider"); + } + + // Section E: Disabled or Edge Cases + + TEST_METHOD(AnimationError_NotAppDisplayed_BothNullopt) + { + // Scenario: Frame not app-displayed (wrong displayIndex) + // Expected: Both animation metrics = std::nullopt + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::AppProvider; + state.firstAppSimStartTime = 100; + state.lastDisplayedSimStartTime = 100; + + FrameData present{}; + present.presentStartTime = 1000; + present.timeInPresent = 100; + present.appSimStartTime = 150; + present.finalState = PresentResult::Presented; + present.displayed.PushBack({ FrameType::Repeated, 2000 }); // Not Application! + + FrameData nextPresent{}; + nextPresent.finalState = PresentResult::Presented; + nextPresent.displayed.PushBack({ FrameType::Application, 3000 }); + + auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); + + Assert::AreEqual(size_t(1), results.size()); + Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + L"msAnimationError should be nullopt for non-app frames"); + Assert::IsFalse(results[0].metrics.msAnimationTime.has_value(), + L"msAnimationTime should be nullopt for non-app frames"); + } + + TEST_METHOD(AnimationError_FirstFrameEver_BothNullopt) + { + // Scenario: Very first frame, no prior state + // Expected: Both animation metrics = std::nullopt + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; // all zeros + state.animationErrorSource = AnimationErrorSource::AppProvider; + + FrameData present{}; + present.presentStartTime = 1000; + present.timeInPresent = 100; + present.appSimStartTime = 0; // no instrumentation + present.pclSimStartTime = 0; + present.finalState = PresentResult::Presented; + present.displayed.PushBack({ FrameType::Application, 2000 }); + + FrameData nextPresent{}; + nextPresent.finalState = PresentResult::Presented; + nextPresent.displayed.PushBack({ FrameType::Application, 3000 }); + + auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); + + Assert::IsFalse(results[0].metrics.msAnimationError.has_value()); + Assert::IsFalse(results[0].metrics.msAnimationTime.has_value()); + } + + TEST_METHOD(AnimationError_BackwardsScreenTime_ErrorStillComputed) + { + // Scenario: Screen time goes backward + // Expected: nullopt for animation error + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::AppProvider; + + FrameData frame1{}; + frame1.presentStartTime = 1000; + frame1.timeInPresent = 100; + frame1.appSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1100 }); + + FrameData frame2{}; + frame2.presentStartTime = 2000; + frame2.timeInPresent = 100; + frame2.appSimStartTime = 150; // sim elapsed = 50 + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 1050 }); // screen time backward! + + FrameData dummyNext{}; + dummyNext.finalState = PresentResult::Presented; + dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); + ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + + FrameData frame3{}; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 3000 }); + auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); + + Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + L"Error should be nullopt with backwards screen time"); + } + + TEST_METHOD(AnimationError_VeryLargeCadenceMismatch_LargeError) + { + // Scenario: Sim running much faster than display + // Expected: Large positive error + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::AppProvider; + + FrameData frame1{}; + frame1.presentStartTime = 1000; + frame1.timeInPresent = 100; + frame1.appSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1000 }); + + FrameData frame2{}; + frame2.presentStartTime = 2000; + frame2.timeInPresent = 100; + frame2.appSimStartTime = 500; // sim elapsed = 400 ticks + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Application, 1010 }); // display elapsed = 10 ticks + + FrameData dummyNext{}; + dummyNext.finalState = PresentResult::Presented; + dummyNext.displayed.PushBack({ FrameType::Application, 2000 }); + ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + + FrameData frame3{}; + frame3.finalState = PresentResult::Presented; + frame3.displayed.PushBack({ FrameType::Application, 3000 }); + auto results = ComputeMetricsForPresent(qpc, frame2, &frame3, state); + + Assert::IsTrue(results[0].metrics.msAnimationError.has_value()); + double simElapsed = qpc.DeltaUnsignedMilliSeconds(100, 500); // 0.040 ms + double displayElapsed = qpc.DeltaUnsignedMilliSeconds(1000, 1010); // 0.001 ms + double expected = simElapsed - displayElapsed; // 0.039 ms + Assert::AreEqual(expected, results[0].metrics.msAnimationError.value(), 0.0001, + L"Large cadence mismatch should produce large positive error"); + } + + TEST_METHOD(AnimationError_RepeatedFrameType_BothNullopt) + { + // Scenario: Frame displayed but type is Repeated, not Application + // Expected: Animation metrics should be nullopt + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::AppProvider; + state.firstAppSimStartTime = 100; + state.lastDisplayedSimStartTime = 100; + + FrameData present{}; + present.presentStartTime = 1000; + present.timeInPresent = 100; + present.appSimStartTime = 150; + present.finalState = PresentResult::Presented; + present.displayed.PushBack({ FrameType::Repeated, 2000 }); + + FrameData nextPresent{}; + nextPresent.finalState = PresentResult::Presented; + nextPresent.displayed.PushBack({ FrameType::Application, 3000 }); + + auto results = ComputeMetricsForPresent(qpc, present, &nextPresent, state); + + Assert::IsFalse(results[0].metrics.msAnimationError.has_value(), + L"msAnimationError should be nullopt for Repeated frame type"); + Assert::IsFalse(results[0].metrics.msAnimationTime.has_value(), + L"msAnimationTime should be nullopt for Repeated frame type"); + } + + TEST_METHOD(AnimationError_MultipleDisplayInstances_OnlyLastAppIndex) + { + // Scenario: Present has 3 display instances, only middle one is Application + // Expected: Animation computed only for Application display instance + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + state.animationErrorSource = AnimationErrorSource::AppProvider; + + // Setup prior frame + FrameData frame1{}; + frame1.presentStartTime = 1000; + frame1.timeInPresent = 100; + frame1.appSimStartTime = 100; + frame1.finalState = PresentResult::Presented; + frame1.displayed.PushBack({ FrameType::Application, 1000 }); + + // Frame with multiple display instances + FrameData frame2{}; + frame2.presentStartTime = 2000; + frame2.timeInPresent = 100; + frame2.appSimStartTime = 150; + frame2.finalState = PresentResult::Presented; + frame2.displayed.PushBack({ FrameType::Repeated, 2000 }); // [0] + frame2.displayed.PushBack({ FrameType::Application, 2050 }); // [1] - appIndex + frame2.displayed.PushBack({ FrameType::Repeated, 2100 }); // [2] + + FrameData dummyNext{}; + dummyNext.finalState = PresentResult::Presented; + dummyNext.displayed.PushBack({ FrameType::Application, 3000 }); + ComputeMetricsForPresent(qpc, frame1, &dummyNext, state); + + // Process frame2 without next (should process [0] and [1]) + auto resultsPartial = ComputeMetricsForPresent(qpc, frame2, nullptr, state); + Assert::AreEqual(size_t(2), resultsPartial.size()); + + // First display instance (Repeated) - no animation metrics + Assert::IsFalse(resultsPartial[0].metrics.msAnimationError.has_value(), + L"Display [0] (Repeated) should not have animation error"); + + // Second display instance (Application) - has animation metrics + Assert::IsTrue(resultsPartial[1].metrics.msAnimationError.has_value(), + L"Display [1] (Application) should have animation error"); + + double simElapsed = qpc.DeltaUnsignedMilliSeconds(100, 150); + double displayElapsed = qpc.DeltaUnsignedMilliSeconds(1000, 2050); + double expected = simElapsed - displayElapsed; + Assert::AreEqual(expected, resultsPartial[1].metrics.msAnimationError.value(), 0.0001); + } + TEST_METHOD(Animation_AppProvider_PendingSequence_P1P2P3) + { + // This test mimics the real ReportMetrics pipeline for a single swapchain: + // + // P1 arrives: + // - ComputeMetricsForPresent(P1, nullptr, state) // Case 2, pending = P1 + // + // P2 arrives: + // - ComputeMetricsForPresent(P1, &P2, state) // Case 3, finalize P1 + // - ComputeMetricsForPresent(P2, nullptr, state) // Case 2, pending = P2 + // + // P3 arrives: + // - ComputeMetricsForPresent(P2, &P3, state) // Case 3, finalize P2 + // - ComputeMetricsForPresent(P3, nullptr, state) // Case 2, pending = P3 + // + // We verify: + // - P1's final metrics: no animation error/time (it seeds animation state). + // - P2's final metrics: non-null msAnimationTime & msAnimationError == 0.0. + // - SwapChainCoreState's animation fields evolve correctly: + // firstAppSimStartTime = 100 + // lastDisplayedSimStartTime = 200 after P2 + // lastDisplayedAppScreenTime = P2's screenTime + // + // QPC frequency = 10 MHz. + // App sim times: P1=100, P2=200, P3=300 + // Screen times: P1=1'000'000, P2=1'100'000, P3=1'200'000 + // -> sim Δ P1→P2: 100 ticks = 0.01 ms + // -> display Δ P1→P2: 100'000 ticks = 0.01 ms + // => animation error for P2 = 0.0 ms + // => animation time for P2 = (200 - 100) / 10e6 = 0.01 ms + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + // -------------------------------------------------------------------- + // P1: first displayed app frame with AppProvider data + // -------------------------------------------------------------------- + FrameData p1{}; + p1.presentStartTime = 500'000; + p1.timeInPresent = 10'000; + p1.readyTime = 510'000; + p1.appSimStartTime = 475'000; + p1.pclSimStartTime = 0; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + // Arrival of P1 -> Case 2 (no nextDisplayed yet), becomes pending + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); + Assert::AreEqual(size_t(0), p1_phase1.size(), + L"First call for P1 with next=nullptr should produce no metrics (pending only)."); + + // Animation state should still be in CpuStart mode; no provider seeded yet. + Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::CpuStart); + Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime); + Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(0), state.lastDisplayedAppScreenTime); + + // -------------------------------------------------------------------- + // P2: second displayed app frame + // -------------------------------------------------------------------- + FrameData p2{}; + p2.presentStartTime = 600'000; + p2.timeInPresent = 10'000; + p2.readyTime = 610'000; + p2.appSimStartTime = 575'000; + p2.pclSimStartTime = 0; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + // Arrival of P2: + // 1) Flush pending P1 using P2 as nextDisplayed -> Case 3, finalize P1. + auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); + Assert::AreEqual(size_t(1), p1_final.size()); + const auto& p1_metrics = p1_final[0].metrics; + + // P1 is the FIRST provider-driven frame, so it should only seed the state: + // no animation error or time yet. + Assert::IsFalse(p1_metrics.msAnimationError.has_value(), + L"P1 should not report animation error; it seeds the animation state."); + Assert::IsFalse(p1_metrics.msAnimationTime.has_value(), + L"P1 should not report animation time; it seeds the animation state."); + + // UpdateAfterPresent should have run for P1 and switched to AppProvider: + Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, + L"Animation source should transition to AppProvider after P1."); + Assert::AreEqual(uint64_t(475'000), state.firstAppSimStartTime, + L"firstAppSimStartTime should latch P1's appSimStartTime."); + Assert::AreEqual(uint64_t(475'000), state.lastDisplayedSimStartTime, + L"lastDisplayedSimStartTime should match P1's appSimStartTime."); + Assert::AreEqual(uint64_t(1'000'000), state.lastDisplayedAppScreenTime, + L"lastDisplayedAppScreenTime should match P1's screenTime."); + + // 2) Now process P2's arrival as pending: Case 2 with next=nullptr + auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, state); + Assert::AreEqual(size_t(0), p2_phase1.size(), + L"First call for P2 with next=nullptr should produce no metrics (pending only)."); + + // -------------------------------------------------------------------- + // P3: third displayed app frame + // -------------------------------------------------------------------- + FrameData p3{}; + p3.presentStartTime = 700'000; + p3.timeInPresent = 10'000; + p3.readyTime = 710'000; + p3.appSimStartTime = 675'000; + p3.pclSimStartTime = 0; + p3.finalState = PresentResult::Presented; + p3.displayed.PushBack({ FrameType::Application, 1'200'000 }); + + // Arrival of P3: + // 1) Flush pending P2 using P3 as nextDisplayed -> Case 3, finalize P2. + auto p2_final = ComputeMetricsForPresent(qpc, p2, &p3, state); + Assert::AreEqual(size_t(1), p2_final.size()); + const auto& p2_metrics = p2_final[0].metrics; + + // For P2: + // - previous displayed sim start = P1.appSimStartTime = 100 + // - current sim start = P2.appSimStartTime = 200 + // - previous screen time = P1.screenTime = 1'000'000 + // - current screen time = P2.screenTime = 1'100'000 + // => simElapsed = 100 ticks → 0.01 ms + // => displayElapsed = 100'000 ticks → 0.01 ms + // => animationError = 0.0 ms + // => animationTime = (200 - 100) ticks from firstAppSimStartTime -> 0.01 ms + + Assert::IsTrue(p2_metrics.msAnimationError.has_value(), + L"P2 should report animation error."); + Assert::IsTrue(p2_metrics.msAnimationTime.has_value(), + L"P2 should report animation time."); + + double expectedError = 0.0; + Assert::AreEqual(expectedError, p2_metrics.msAnimationError.value(), 0.0001, + L"P2's msAnimationError should be 0.0 when sim and display deltas match."); + + double expectedAnim = qpc.DeltaUnsignedMilliSeconds(475'000, 575'000); + Assert::AreEqual(expectedAnim, p2_metrics.msAnimationTime.value(), 0.0001, + L"P2's msAnimationTime should be based on firstAppSimStartTime (100) to current sim (200)."); + + // After finalizing P2, chain state should now reflect P2 as "last displayed" + Assert::AreEqual(uint64_t(475'000), state.firstAppSimStartTime, + L"firstAppSimStartTime should remain anchored to P1."); + Assert::AreEqual(uint64_t(575'000), state.lastDisplayedSimStartTime, + L"lastDisplayedSimStartTime should advance to P2's appSimStartTime."); + Assert::AreEqual(uint64_t(1'100'000), state.lastDisplayedAppScreenTime, + L"lastDisplayedAppScreenTime should advance to P2's screenTime."); + + // 2) Finally, process P3 as the new pending (Case 2 with next=nullptr). + auto p3_phase1 = ComputeMetricsForPresent(qpc, p3, nullptr, state); + Assert::AreEqual(size_t(0), p3_phase1.size(), + L"First call for P3 with next=nullptr should produce no metrics (pending only)."); + } + // ======================================================================== + // A7: Animation_AppProvider_PendingSequence_P2Discarded_SkipsAnimation + // ======================================================================== + TEST_METHOD(Animation_AppProvider_PendingSequence_P2Discarded_SkipsAnimation) + { + // This test mimics the real ReportMetrics pipeline for a single swapchain + // when a middle frame (P2) is discarded and never displayed. + // + // P1 arrives (displayed, has AppProvider sim start): + // - ComputeMetricsForPresent(P1, nullptr, state) // Case 2, pending = P1 + // + // P2 arrives (DISCARDED, not displayed, but with appSimStartTime): + // - ComputeMetricsForPresent(P2, nullptr, state) // Case 1, not displayed + // * Should produce a single metrics entry with NO animation time/error + // * Must NOT change firstAppSimStartTime / lastDisplayedSimStartTime + // + // P3 arrives (displayed, has AppProvider sim start): + // - ComputeMetricsForPresent(P1, &P3, state) // Case 3, finalize P1 + // * P1 is the FIRST provider-driven displayed frame, so it seeds state. + // * P1 should not report animation time/error. + // * State switches to AppProvider and latches P1's sim + screen times. + // - ComputeMetricsForPresent(P3, nullptr, state) // Case 2, pending = P3 + // + // App sim times are in QPC domain: + // P1.appSimStartTime = 475'000 + // P2.appSimStartTime = 575'000 + // P3.appSimStartTime = 675'000 + // + // Screen times: + // P1.screenTime = 1'000'000 + // P2 has no screenTime (discarded, not displayed) + // P3.screenTime = 1'100'000 + // + // We verify: + // - P2's metrics have no msAnimationTime / msAnimationError. + // - P2 does not change firstAppSimStartTime / lastDisplayedSimStartTime. + // - After finalizing P1 with P3 as nextDisplayed: + // * animationErrorSource == AppProvider + // * firstAppSimStartTime == 475'000 + // * lastDisplayedSimStartTime == 475'000 + // * lastDisplayedAppScreenTime == 1'000'000 + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + // -------------------------------------------------------------------- + // P1: first displayed app frame with AppProvider data + // -------------------------------------------------------------------- + FrameData p1{}; + p1.presentStartTime = 500'000; + p1.timeInPresent = 10'000; + p1.readyTime = 510'000; + p1.appSimStartTime = 475'000; // APC-provided sim start (QPC ticks) + p1.pclSimStartTime = 0; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 1'000'000 }); // screen time + + // P1 arrives -> Case 2 (no nextDisplayed yet), becomes pending + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); + Assert::AreEqual(size_t(0), p1_phase1.size(), + L"First call for P1 with next=nullptr should produce no metrics (pending only)."); + + // Still in CpuStart mode; no provider-seeded animation state yet. + Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::CpuStart); + Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime); + Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(0), state.lastDisplayedAppScreenTime); + + // -------------------------------------------------------------------- + // P2: discarded, not displayed, but with AppProvider sim start + // -------------------------------------------------------------------- + FrameData p2{}; + p2.presentStartTime = 600'000; + p2.timeInPresent = 10'000; + p2.readyTime = 610'000; + p2.appSimStartTime = 575'000; // APC timestamp, but this frame is not displayed + p2.pclSimStartTime = 0; + p2.finalState = PresentResult::Discarded; + // No displayed entries -> not displayed + + auto p2_results = ComputeMetricsForPresent(qpc, p2, nullptr, state); + Assert::AreEqual(size_t(1), p2_results.size(), + L"Discarded frame should produce a single not-displayed metrics entry."); + + const auto& p2_metrics = p2_results[0].metrics; + + // Discarded / not-displayed frame must NOT produce animation metrics. + Assert::IsFalse(p2_metrics.msAnimationTime.has_value(), + L"P2 (discarded) should not have msAnimationTime."); + Assert::IsFalse(p2_metrics.msAnimationError.has_value(), + L"P2 (discarded) should not have msAnimationError."); + + // And it must NOT disturb animation anchors, since it's not displayed. + Assert::AreEqual(uint64_t(0), state.firstAppSimStartTime, + L"P2 must not set firstAppSimStartTime; only displayed App/PCL frames do that."); + Assert::AreEqual(uint64_t(0), state.lastDisplayedSimStartTime, + L"P2 must not change lastDisplayedSimStartTime when not displayed."); + Assert::AreEqual(uint64_t(0), state.lastDisplayedAppScreenTime, + L"P2 must not change lastDisplayedAppScreenTime when not displayed."); + + // (Optional sanity: lastSimStartTime may track P2's appSimStartTime, which is fine for + // simulation plumbing but not for animation anchors.) + + // -------------------------------------------------------------------- + // P3: next displayed app frame + // -------------------------------------------------------------------- + FrameData p3{}; + p3.presentStartTime = 700'000; + p3.timeInPresent = 10'000; + p3.readyTime = 710'000; + p3.appSimStartTime = 675'000; // another +100'000 ticks in sim space + p3.pclSimStartTime = 0; + p3.finalState = PresentResult::Presented; + p3.displayed.PushBack({ FrameType::Application, 1'100'000 }); // next screen time + + // P3 arrives: + // 1) Flush pending P1 using P3 as nextDisplayed -> Case 3, finalize P1. + auto p1_final = ComputeMetricsForPresent(qpc, p1, &p3, state); + Assert::AreEqual(size_t(1), p1_final.size()); + const auto& p1_metrics = p1_final[0].metrics; + + // P1 is the FIRST displayed frame with AppProvider sim start. + // It should only seed animation state; no error/time yet. + Assert::IsFalse(p1_metrics.msAnimationError.has_value(), + L"P1 should not report animation error; it seeds the animation state."); + Assert::IsFalse(p1_metrics.msAnimationTime.has_value(), + L"P1 should not report animation time; it seeds the animation state."); + + // After finalizing P1, we must now be in AppProvider mode with anchors from P1. + Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, + L"Animation source should transition to AppProvider after first displayed AppSimStart frame (P1)."); + Assert::AreEqual(uint64_t(475'000), state.firstAppSimStartTime, + L"firstAppSimStartTime should latch P1's appSimStartTime."); + Assert::AreEqual(uint64_t(475'000), state.lastDisplayedSimStartTime, + L"lastDisplayedSimStartTime should match P1's appSimStartTime after P1 is finalized."); + Assert::AreEqual(uint64_t(1'000'000), state.lastDisplayedAppScreenTime, + L"lastDisplayedAppScreenTime should match P1's screenTime."); + + // 2) Process P3 as the new pending frame (Case 2 with next=nullptr). + auto p3_phase1 = ComputeMetricsForPresent(qpc, p3, nullptr, state); + Assert::AreEqual(size_t(0), p3_phase1.size(), + L"First call for P3 with next=nullptr should produce no metrics (pending only)."); + + // State remains anchored to P1 until a later frame finalizes P3 with a true nextDisplayed. + Assert::AreEqual(uint64_t(475'000), state.firstAppSimStartTime, + L"firstAppSimStartTime should remain anchored to P1 after P3's pending pass."); + Assert::AreEqual(uint64_t(475'000), state.lastDisplayedSimStartTime, + L"lastDisplayedSimStartTime should still reflect P1 until P3 is finalized."); + } + }; + // ============================================================================ + // SECTION: Input Latency Tests + // ============================================================================ + + TEST_CLASS(InputLatencyTests) + { + public: + // ======================================================================== + // Test 1: ClickToPhoton - displayed frame uses its own click + // ======================================================================== + TEST_METHOD(InputLatency_ClickToPhoton_DisplayedFrame_UsesOwnClickTime) + { + // Scenario: + // - P1 (displayed frame) has its own mouseClickTime = 400'000 + // - P1 computes msClickToPhotonLatency from click to its own display time + // - No pending click should remain in the chain + // + // Expected: + // - P1's msClickToPhotonLatency uses P1's own click (400'000 -> 1'000'000) + // - state.lastReceivedNotDisplayedMouseClickTime == 0 + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + // P1: displayed app frame with its own click + FrameData p1{}; + p1.presentStartTime = 500'000; + p1.timeInPresent = 100'000; + p1.mouseClickTime = 400'000; + p1.inputTime = 0; + p1.appSimStartTime = 450'000; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + // P2: next displayed frame + FrameData p2{}; + p2.presentStartTime = 1'050'000; + p2.timeInPresent = 50'000; + p2.mouseClickTime = 0; + p2.inputTime = 0; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + // P1 arrives (pending) + auto p1_pending = ComputeMetricsForPresent(qpc, p1, nullptr, state); + Assert::AreEqual(size_t(0), p1_pending.size(), L"P1 pending should be empty"); + + // P2 arrives, finalizes P1 + auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); + auto p2_pending = ComputeMetricsForPresent(qpc, p2, nullptr, state); + + // Assertions for P1 + Assert::AreEqual(size_t(1), p1_final.size()); + Assert::IsTrue(p1_final[0].metrics.msClickToPhotonLatency.has_value(), + L"P1 should have msClickToPhotonLatency"); + + double expected = qpc.DeltaUnsignedMilliSeconds(400'000, 1'000'000); + Assert::AreEqual(expected, p1_final[0].metrics.msClickToPhotonLatency.value(), 0.0001, + L"P1's click-to-photon should use its own click time"); + + // Verify no pending click remains + Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedMouseClickTime, + L"No pending click should remain after P1 used its own click"); + } + + // ======================================================================== + // Test 2: ClickToPhoton - dropped frame carries click to next displayed + // ======================================================================== + TEST_METHOD(InputLatency_ClickToPhoton_DroppedFrame_CarriesClickToNextDisplayed) + { + // Scenario: + // - P1 (dropped, not displayed) has mouseClickTime = 400'000 + // - P1 does not produce msClickToPhotonLatency + // - P1 stores click in lastReceivedNotDisplayedMouseClickTime + // - P2 (displayed, no own click) uses the stored click from P1 + // + // Expected: + // - P1: msClickToPhotonLatency == nullopt + // - After P1: state.lastReceivedNotDisplayedMouseClickTime == 400'000 + // - P2: msClickToPhotonLatency uses stored click (400'000 -> 1'000'000) + // - After P2: state.lastReceivedNotDisplayedMouseClickTime == 0 (consumed) + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + // P1: dropped frame with click + FrameData p1{}; + p1.presentStartTime = 300'000; + p1.timeInPresent = 50'000; + p1.mouseClickTime = 400'000; + p1.inputTime = 0; + p1.finalState = PresentResult::Discarded; + // displayed is empty (not displayed) + + // P2: first displayed frame (no own click) + FrameData p2{}; + p2.presentStartTime = 900'000; + p2.timeInPresent = 100'000; + p2.mouseClickTime = 0; + p2.inputTime = 0; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + // P3: later frame to finalize P2 + FrameData p3{}; + p3.presentStartTime = 1'050'000; + p3.timeInPresent = 50'000; + p3.finalState = PresentResult::Presented; + p3.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + // P1 arrives (dropped) + auto p1_results = ComputeMetricsForPresent(qpc, p1, nullptr, state); + + // Assertions for P1 + Assert::AreEqual(size_t(1), p1_results.size()); + Assert::IsFalse(p1_results[0].metrics.msClickToPhotonLatency.has_value(), + L"P1 (dropped) should not have msClickToPhotonLatency"); + Assert::AreEqual(uint64_t(400'000), state.lastReceivedNotDisplayedMouseClickTime, + L"P1's click should be stored as pending"); + + // P2 arrives (pending) + auto p2_pending = ComputeMetricsForPresent(qpc, p2, nullptr, state); + + // P3 arrives, finalizes P2 + auto p2_final = ComputeMetricsForPresent(qpc, p2, &p3, state); + auto p3_pending = ComputeMetricsForPresent(qpc, p3, nullptr, state); + + // Assertions for P2 + Assert::AreEqual(size_t(1), p2_final.size()); + Assert::IsTrue(p2_final[0].metrics.msClickToPhotonLatency.has_value(), + L"P2 should have msClickToPhotonLatency using P1's stored click"); + + double expected = qpc.DeltaUnsignedMilliSeconds(400'000, 1'000'000); + Assert::AreEqual(expected, p2_final[0].metrics.msClickToPhotonLatency.value(), 0.0001, + L"P2's click-to-photon should use P1's stored click"); + + // Optional: verify pending click is consumed + Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedMouseClickTime, + L"Pending click should be consumed after P2 uses it"); + } + + // ======================================================================== + // Test 3: AllInputPhoton - multiple dropped frames, last input wins + // ======================================================================== + TEST_METHOD(InputLatency_AllInputPhoton_MultipleDroppedFrames_LastInputWins) + { + // Scenario: + // - P1 (dropped) has inputTime = 300'000 + // - P2 (dropped) has inputTime = 450'000 (should override P1) + // - P3 (displayed, no own input) uses the last stored input (450'000) + // + // Expected: + // - P1: msAllInputPhotonLatency == nullopt, state stores 300'000 + // - P2: msAllInputPhotonLatency == nullopt, state updates to 450'000 + // - P3: msAllInputPhotonLatency uses 450'000 (last wins) + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + // P1: dropped with first input + FrameData p1{}; + p1.presentStartTime = 200'000; + p1.timeInPresent = 50'000; + p1.inputTime = 300'000; + p1.mouseClickTime = 0; + p1.finalState = PresentResult::Discarded; + // displayed empty + + // P2: dropped with later input (overrides P1) + FrameData p2{}; + p2.presentStartTime = 400'000; + p2.timeInPresent = 50'000; + p2.inputTime = 450'000; + p2.mouseClickTime = 0; + p2.finalState = PresentResult::Discarded; + // displayed empty + + // P3: first displayed frame (no own input) + FrameData p3{}; + p3.presentStartTime = 900'000; + p3.timeInPresent = 100'000; + p3.inputTime = 0; + p3.mouseClickTime = 0; + p3.finalState = PresentResult::Presented; + p3.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + // P4: later frame to finalize P3 + FrameData p4{}; + p4.presentStartTime = 1'050'000; + p4.timeInPresent = 50'000; + p4.finalState = PresentResult::Presented; + p4.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + // P1 arrives (dropped) + auto p1_results = ComputeMetricsForPresent(qpc, p1, nullptr, state); + Assert::AreEqual(size_t(1), p1_results.size()); + Assert::IsFalse(p1_results[0].metrics.msAllInputPhotonLatency.has_value(), + L"P1 (dropped) should not have msAllInputPhotonLatency"); + Assert::AreEqual(uint64_t(300'000), state.lastReceivedNotDisplayedAllInputTime, + L"P1's input should be stored"); + + // P2 arrives (dropped, overrides P1) + auto p2_results = ComputeMetricsForPresent(qpc, p2, nullptr, state); + Assert::AreEqual(size_t(1), p2_results.size()); + Assert::IsFalse(p2_results[0].metrics.msAllInputPhotonLatency.has_value(), + L"P2 (dropped) should not have msAllInputPhotonLatency"); + Assert::AreEqual(uint64_t(450'000), state.lastReceivedNotDisplayedAllInputTime, + L"P2's input should override P1's stored input (last wins)"); + + // P3 arrives (pending) + auto p3_pending = ComputeMetricsForPresent(qpc, p3, nullptr, state); + + // P4 arrives, finalizes P3 + auto p3_final = ComputeMetricsForPresent(qpc, p3, &p4, state); + auto p4_pending = ComputeMetricsForPresent(qpc, p4, nullptr, state); + + // Assertions for P3 + Assert::AreEqual(size_t(1), p3_final.size()); + Assert::IsTrue(p3_final[0].metrics.msAllInputPhotonLatency.has_value(), + L"P3 should have msAllInputPhotonLatency using last stored input"); + + double expected = qpc.DeltaUnsignedMilliSeconds(450'000, 1'000'000); + Assert::AreEqual(expected, p3_final[0].metrics.msAllInputPhotonLatency.value(), 0.0001, + L"P3's all-input-to-photon should use P2's input (last wins)"); + } + + // ======================================================================== + // Test 4: AllInputPhoton - displayed frame with own input overrides pending + // ======================================================================== + TEST_METHOD(InputLatency_AllInputPhoton_DisplayedFrame_WithOwnInput_OverridesPending) + { + // Scenario: + // - P0 (dropped) seeds pending input = 300'000 + // - P1 (displayed) has its own inputTime = 500'000 + // - P1's own input should override the pending 300'000 + // + // Expected: + // - P0: state.lastReceivedNotDisplayedAllInputTime == 300'000 + // - P1: msAllInputPhotonLatency uses P1's own input (500'000 -> 1'000'000) + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + // P0: dropped, seeds pending input + FrameData p0{}; + p0.presentStartTime = 200'000; + p0.timeInPresent = 50'000; + p0.inputTime = 300'000; + p0.mouseClickTime = 0; + p0.finalState = PresentResult::Discarded; + // displayed empty + + // P1: displayed with its own input + FrameData p1{}; + p1.presentStartTime = 900'000; + p1.timeInPresent = 100'000; + p1.inputTime = 500'000; + p1.mouseClickTime = 0; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + // P2: later frame to finalize P1 + FrameData p2{}; + p2.presentStartTime = 1'050'000; + p2.timeInPresent = 50'000; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + // P0 arrives (dropped) + auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); + Assert::AreEqual(uint64_t(300'000), state.lastReceivedNotDisplayedAllInputTime, + L"P0's input should be stored as pending"); + + // P1 arrives (pending) + auto p1_pending = ComputeMetricsForPresent(qpc, p1, nullptr, state); + + // P2 arrives, finalizes P1 + auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); + auto p2_pending = ComputeMetricsForPresent(qpc, p2, nullptr, state); + + // Assertions for P1 + Assert::AreEqual(size_t(1), p1_final.size()); + Assert::IsTrue(p1_final[0].metrics.msAllInputPhotonLatency.has_value(), + L"P1 should have msAllInputPhotonLatency using its own input"); + + double expected = qpc.DeltaUnsignedMilliSeconds(500'000, 1'000'000); + Assert::AreEqual(expected, p1_final[0].metrics.msAllInputPhotonLatency.value(), 0.0001, + L"P1's all-input-to-photon should use its own input (500'000), not pending (300'000)"); + } + + // ======================================================================== + // Test 5: InstrumentedInputTime - uses same sim start as animation source (AppProvider) + // ======================================================================== + TEST_METHOD(InputLatency_InstrumentedInputTime_UsesAppInputSample) + { + // Scenario: + // - Frame 1: First AppProvider frame (appSimStartTime = 475'000) - seeds state + // - Frame 2: Has appInputSample = 500'000, appSimStartTime = 575'000 + // - msInstrumentedInputTime should use Frame 2's appInputSample time = 500'000 + // - Frame 2: Display time is 1'100'000 + // + // Expected: + // - After Frame 1: animationErrorSource == AppProvider + // - Frame 2: msInstrumentedInputTime = (1'100'000 - 500'000) in ms + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + // state.animationErrorSource defaults to CpuStart + + // P1: First AppProvider frame (no input) + FrameData p1{}; + p1.presentStartTime = 500'000; + p1.timeInPresent = 100'000; + p1.appSimStartTime = 475'000; + p1.pclSimStartTime = 0; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 1'000'000 }); + + // P2: Frame with input (we assert on this) + FrameData p2{}; + p2.presentStartTime = 1'000'000; + p2.timeInPresent = 100'000; + p2.appSimStartTime = 575'000; + p2.pclSimStartTime = 0; + p2.appInputSample.first = 500'000; // Using same value for simplicity + p2.appInputSample.second = InputDeviceType::Mouse; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 1'100'000 }); + + // P3: Later frame to finalize P2 + FrameData p3{}; + p3.presentStartTime = 1'500'000; + p3.timeInPresent = 100'000; + p3.appSimStartTime = 675'000; + p3.finalState = PresentResult::Presented; + p3.displayed.PushBack({ FrameType::Application, 1'200'000 }); + + // P1 arrives (pending) + auto p1_pending = ComputeMetricsForPresent(qpc, p1, nullptr, state); + + // P2 arrives, finalizes P1 (switches to AppProvider) + auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); + auto p2_pending = ComputeMetricsForPresent(qpc, p2, nullptr, state); + + // Verify state after P1 + Assert::IsTrue(state.animationErrorSource == AnimationErrorSource::AppProvider, + L"Animation source should switch to AppProvider after P1"); + Assert::AreEqual(uint64_t(475'000), state.firstAppSimStartTime, + L"firstAppSimStartTime should be set to P1's appSimStartTime"); + + // P3 arrives, finalizes P2 + auto p2_final = ComputeMetricsForPresent(qpc, p2, &p3, state); + auto p3_pending = ComputeMetricsForPresent(qpc, p3, nullptr, state); + + // Assertions for P2 + Assert::AreEqual(size_t(1), p2_final.size()); + + // Verify P2 has animation time (sanity check we're in AppProvider mode) + Assert::IsTrue(p2_final[0].metrics.msAnimationTime.has_value(), + L"P2 should have msAnimationTime (AppProvider mode)"); + + // Verify msInstrumentedInputTime is present + Assert::IsTrue(p2_final[0].metrics.msInstrumentedInputTime.has_value(), + L"P2 should have msInstrumentedInputTime"); + + // Calculate expected: app input -> p2 screen + double expectedInstr = qpc.DeltaUnsignedMilliSeconds(500'000, 1'100'000); + Assert::AreEqual(expectedInstr, p2_final[0].metrics.msInstrumentedInputTime.value(), 0.0001, + L"msInstrumentedInputTime should be P2 app input time to P2 screen time"); + } + }; + TEST_CLASS(PcLatencyTests) + { + public: + + TEST_METHOD(PcLatency_PendingSequence_DroppedDroppedDisplayed_P0P1P2P3) + { + // This test mimics the real ReportMetrics pipeline for a single swapchain, + // focusing on the PC Latency accumulation across *dropped* frames, and its + // completion when the corresponding frame finally reaches the screen. + // + // We use four presents: + // + // P0: DROPPED + // - PclInputPingTime = 10'000 + // - PclSimStartTime = 20'000 + // -> initializes accumulatedInput2FrameStartTime with Δ(PING0, SIM0). + // + // P1: DROPPED + // - PclInputPingTime = 0 + // - PclSimStartTime = 30'000 + // -> extends accumulatedInput2FrameStartTime with Δ(SIM0, SIM1). + // + // P2: DISPLAYED + // - PclInputPingTime = 0 + // - PclSimStartTime = 40'000 + // - ScreenTime = 50'000 + // + // P3: DISPLAYED + // - no PCL data; used only as "nextDisplayed" for P2 to mimic ReportMetrics. + // + // QPC frequency = 10 MHz. + // + // Timing (ticks): + // PING0 = 10'000 + // SIM0 = 20'000 + // SIM1 = 30'000 + // SIM2 = 40'000 + // SCR2 = 50'000 + // + // Δ(PING0, SIM0) = 10'000 ticks = 1.0 ms + // Δ(SIM0, SIM1) = 10'000 ticks = 1.0 ms + // Δ(SIM1, SIM2) = 10'000 ticks = 1.0 ms + // => full input→frame-start for this chain = 3.0 ms + // + // Δ(SIM2, SCR2) = 10'000 ticks = 1.0 ms + // + // Legacy behavior: + // - AccumulatedInput2FrameStartTime builds to 3.0 ms over P0/P1/P2. + // - EMA seeds from that full 3.0 ms sample when P2 finally completes. + // - PC Latency for P2 ≈ 3.0 ms (input→frame-start) + 1.0 ms (sim→screen). + // + // The pipeline calls we mimic: + // + // P0 arrives (dropped): + // - ComputeMetricsForPresent(P0, nullptr, state) // Case 1, not displayed + // + // P1 arrives (dropped): + // - ComputeMetricsForPresent(P1, nullptr, state) // Case 1, not displayed + // + // P2 arrives (displayed): + // - ComputeMetricsForPresent(P2, nullptr, state) // Case 2, pending only (no metrics yet) + // + // P3 arrives (displayed): + // - ComputeMetricsForPresent(P2, &P3, state) // Case 3, finalize P2 + // - ComputeMetricsForPresent(P3, nullptr, state) // Case 2, pending = P3 + // + // We verify: + // - P0 & P1 produce no msPcLatency (dropped frames). + // - accumulatedInput2FrameStartTime grows after P0 and P1. + // - P2's first call (next=nullptr) produces no metrics and does NOT + // disturb the accumulatedInput2FrameStartTime. + // - The final P2 call (with next=P3) produces a non-null msPcLatency, + // and resets accumulatedInput2FrameStartTime and lastReceivedNotDisplayedPclSimStart + // to 0, matching the legacy PCL behavior. + // + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + const uint32_t PROCESS_ID = 1234; + const uint64_t SWAPCHAIN = 0xABC0; + + // -------------------------------------------------------------------- + // P0: DROPPED, first PCL frame with Ping+Sim + // -------------------------------------------------------------------- + FrameData p0{}; + p0.processId = PROCESS_ID; + p0.swapChainAddress = SWAPCHAIN; + p0.presentStartTime = 0; + p0.timeInPresent = 0; + p0.readyTime = 0; + p0.appSimStartTime = 0; + + p0.pclInputPingTime = 10'000; // PING0 + p0.pclSimStartTime = 20'000; // SIM0 + + p0.finalState = PresentResult::Discarded; + p0.displayed.Clear(); // not displayed + + // P0 arrival -> Case 1 (not displayed), process immediately. + auto p0_metrics_list = ComputeMetricsForPresent(qpc, p0, nullptr, state); + Assert::AreEqual(size_t(1), p0_metrics_list.size(), + L"P0: not-displayed present should produce a single metrics record."); + + const auto& p0_metrics = p0_metrics_list[0].metrics; + + // Dropped frames never report PC latency directly. + Assert::IsFalse(p0_metrics.msPcLatency.has_value(), + L"P0: dropped frame should not report msPcLatency."); + + // Accumulator should have been initialized from Ping0 -> Sim0. + Assert::IsTrue(state.accumulatedInput2FrameStartTime > 0.0, + L"P0: accumulatedInput2FrameStartTime should be initialized and > 0."); + Assert::AreEqual(uint64_t(20'000), state.lastReceivedNotDisplayedPclSimStart, + L"P0: lastReceivedNotDisplayedPclSimStart should match P0's pclSimStartTime (20'000)."); + + const double accumAfterP0 = state.accumulatedInput2FrameStartTime; + + // -------------------------------------------------------------------- + // P1: DROPPED, continuation of same PCL chain (Sim only) + // -------------------------------------------------------------------- + FrameData p1{}; + p1.processId = PROCESS_ID; + p1.swapChainAddress = SWAPCHAIN; + p1.presentStartTime = 0; + p1.timeInPresent = 0; + p1.readyTime = 0; + p1.appSimStartTime = 0; + + p1.pclInputPingTime = 0; // no new ping + p1.pclSimStartTime = 30'000; // SIM1 + + p1.finalState = PresentResult::Discarded; + p1.displayed.Clear(); // not displayed + + auto p1_metrics_list = ComputeMetricsForPresent(qpc, p1, nullptr, state); + Assert::AreEqual(size_t(1), p1_metrics_list.size(), + L"P1: not-displayed present should produce a single metrics record."); + + const auto& p1_metrics = p1_metrics_list[0].metrics; + + Assert::IsFalse(p1_metrics.msPcLatency.has_value(), + L"P1: dropped frame should not report msPcLatency."); + + // Accumulator should have grown: now includes SIM0->SIM1 as well. + Assert::IsTrue(state.accumulatedInput2FrameStartTime > accumAfterP0, + L"P1: accumulatedInput2FrameStartTime should be greater than after P0."); + Assert::AreEqual(uint64_t(30'000), state.lastReceivedNotDisplayedPclSimStart, + L"P1: lastReceivedNotDisplayedPclSimStart should match P1's pclSimStartTime (30'000)."); + + const double accumAfterP1 = state.accumulatedInput2FrameStartTime; + + // -------------------------------------------------------------------- + // P2: DISPLAYED, Sim only – this is the frame where the pending input + // will finally be visible on-screen. However, the metrics for P2 are + // only finalized when we know P3 (nextDisplayed), just like in the + // ReportMetrics pipeline. + // -------------------------------------------------------------------- + FrameData p2{}; + p2.processId = PROCESS_ID; + p2.swapChainAddress = SWAPCHAIN; + p2.presentStartTime = 0; + p2.timeInPresent = 0; + p2.readyTime = 0; + p2.appSimStartTime = 0; + + p2.pclInputPingTime = 0; // no new ping + p2.pclSimStartTime = 40'000; // SIM2 + + p2.finalState = PresentResult::Presented; + p2.displayed.Clear(); + p2.displayed.PushBack({ FrameType::Application, 50'000 }); // SCR2 + + // P2 arrival: Case 2 (displayed, no nextDisplayed), becomes pending. + auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, state); + Assert::AreEqual(size_t(0), p2_phase1.size(), + L"P2 (phase 1): first call with nextDisplayed=nullptr should produce no metrics (pending only)."); + + // The pending call MUST NOT disturb the accumulated PCL chain. + Assert::AreEqual(accumAfterP1, state.accumulatedInput2FrameStartTime, 1e-9, + L"P2 (phase 1): accumulatedInput2FrameStartTime should remain unchanged while pending."); + Assert::AreEqual(uint64_t(30'000), state.lastReceivedNotDisplayedPclSimStart, + L"P2 (phase 1): lastReceivedNotDisplayedPclSimStart should remain at P1's sim start (30'000)."); + + // -------------------------------------------------------------------- + // P3: DISPLAYED, used only as nextDisplayed when finalizing P2. + // -------------------------------------------------------------------- + FrameData p3{}; + p3.processId = PROCESS_ID; + p3.swapChainAddress = SWAPCHAIN; + p3.presentStartTime = 0; + p3.timeInPresent = 0; + p3.readyTime = 0; + p3.appSimStartTime = 0; + + p3.pclInputPingTime = 0; + p3.pclSimStartTime = 0; // no PCL for P3 itself + + p3.finalState = PresentResult::Presented; + p3.displayed.Clear(); + p3.displayed.PushBack({ FrameType::Application, 60'000 }); // some later screen time + + // P3 arrival: + // 1) Flush pending P2 using P3 as nextDisplayed -> Case 3, finalize P2. + auto p2_final = ComputeMetricsForPresent(qpc, p2, &p3, state); + Assert::AreEqual(size_t(1), p2_final.size(), + L"P2 (final): expected exactly one metrics record when flushing with nextDisplayed=P3."); + const auto& p2_metrics = p2_final[0].metrics; + + // 2) Now process P3's arrival as pending -> Case 2 with next=nullptr. + auto p3_phase1 = ComputeMetricsForPresent(qpc, p3, nullptr, state); + Assert::AreEqual(size_t(0), p3_phase1.size(), + L"P3 (phase 1): first call with nextDisplayed=nullptr should produce no metrics (pending only)."); + + // -------------------------------------------------------------------- + // Assertions for the P2 finalization (this is where PC Latency must appear). + // -------------------------------------------------------------------- + + // Precondition: we had a non-zero accumulated input→frame-start before finalizing P2. + Assert::IsTrue(accumAfterP1 > 0.0, + L"Precondition: expected non-zero accumulatedInput2FrameStartTime before P2 finalization."); + + // 1) PC Latency should be populated and positive for P2 when it finally + // reaches the screen after the dropped chain. + Assert::IsTrue(p2_metrics.msPcLatency.has_value(), + L"P2 (final): msPcLatency should be populated for the displayed frame completing the dropped PCL chain."); + Assert::IsTrue(p2_metrics.msPcLatency.value() > 0.0, + L"P2 (final): msPcLatency should be positive."); + + // 2) After completion, the accumulated input→frame-start time and the + // last-not-displayed PCL sim start should be reset to zero, matching + // the legacy PCL behavior. + Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 1e-9, + L"P2 (final): accumulatedInput2FrameStartTime should be reset to 0 after completion."); + Assert::AreEqual(uint64_t{ 0 }, state.lastReceivedNotDisplayedPclSimStart, + L"P2 (final): lastReceivedNotDisplayedPclSimStart should be reset to 0 after completion."); + } + + TEST_METHOD(PcLatency_NoPclData_AllFrames_NoLatency) + { + // Scenario: + // - P0 is dropped with no PC Latency timestamps. + // - P1 and P2 are displayed app frames but likewise carry no pclSimStartTime/pclInputPingTime. + // - We run the ReportMetrics-style scheduling: dropped frames are processed immediately, + // displayed frames are first queued (Case 2) and then finalized by the arrival of the + // next displayed present (Case 3). + // QPC plan (ticks at 10 MHz): + // - Screen times: SCR1 = 100'000, SCR2 = 120'000, SCR3 = 140'000. + // Expectations: + // - Every metrics record reports msPcLatency.has_value() == false. + // - accumulatedInput2FrameStartTime remains 0.0 throughout the sequence. + // - lastReceivedNotDisplayedPclSimStart never departs from 0 since no PCL timestamps exist. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + const uint32_t PROCESS_ID = 77; + const uint64_t SWAPCHAIN = 0x11AAu; + + // -------------------------------------------------------------------- + // P0: dropped frame without any PCL data + // -------------------------------------------------------------------- + FrameData p0{}; + p0.processId = PROCESS_ID; + p0.swapChainAddress = SWAPCHAIN; + p0.pclInputPingTime = 0; + p0.pclSimStartTime = 0; + p0.finalState = PresentResult::Discarded; + + auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); + Assert::AreEqual(size_t(1), p0_results.size(), + L"P0 (dropped) should emit one metrics record."); + Assert::IsFalse(p0_results[0].metrics.msPcLatency.has_value(), + L"P0 should not report msPcLatency without PCL data."); + Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, + L"P0 should not modify accumulatedInput2FrameStartTime when there is no PCL data."); + Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedPclSimStart, + L"P0 should leave lastReceivedNotDisplayedPclSimStart at 0."); + + // -------------------------------------------------------------------- + // P1: displayed frame without PCL data (pending, then finalized by P2) + // -------------------------------------------------------------------- + FrameData p1{}; + p1.processId = PROCESS_ID; + p1.swapChainAddress = SWAPCHAIN; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 100'000 }); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); + Assert::AreEqual(size_t(0), p1_phase1.size(), + L"P1 pending pass should not emit metrics."); + Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, + L"State.accumulatedInput2FrameStartTime must remain 0 after P1 pending pass."); + Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedPclSimStart, + L"lastReceivedNotDisplayedPclSimStart should remain 0 after P1 pending pass."); + + // -------------------------------------------------------------------- + // P2: displayed frame without PCL data (finalizes P1, then becomes pending) + // -------------------------------------------------------------------- + FrameData p2{}; + p2.processId = PROCESS_ID; + p2.swapChainAddress = SWAPCHAIN; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 120'000 }); + + auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); + Assert::AreEqual(size_t(1), p1_final.size(), + L"Finalizing P1 should emit exactly one metrics record."); + Assert::IsFalse(p1_final[0].metrics.msPcLatency.has_value(), + L"P1 final metrics should not report msPcLatency without PCL data."); + Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, + L"Accumulated input-to-frame-start time must remain 0 after finalizing P1."); + Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedPclSimStart, + L"lastReceivedNotDisplayedPclSimStart should remain 0 after finalizing P1."); + + auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, state); + Assert::AreEqual(size_t(0), p2_phase1.size(), + L"P2 pending pass should not emit metrics."); + + // -------------------------------------------------------------------- + // P3: helper displayed frame to flush P2 + // -------------------------------------------------------------------- + FrameData p3{}; + p3.processId = PROCESS_ID; + p3.swapChainAddress = SWAPCHAIN; + p3.finalState = PresentResult::Presented; + p3.displayed.PushBack({ FrameType::Application, 140'000 }); + + auto p2_final = ComputeMetricsForPresent(qpc, p2, &p3, state); + Assert::AreEqual(size_t(1), p2_final.size(), + L"Finalizing P2 should emit exactly one metrics record."); + Assert::IsFalse(p2_final[0].metrics.msPcLatency.has_value(), + L"P2 final metrics should not report msPcLatency without PCL data."); + Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, + L"Accumulated input-to-frame-start time must still be 0 after P2."); + Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedPclSimStart, + L"lastReceivedNotDisplayedPclSimStart should remain 0 through the entire sequence."); + + auto p3_phase1 = ComputeMetricsForPresent(qpc, p3, nullptr, state); + Assert::AreEqual(size_t(0), p3_phase1.size(), + L"P3 pending pass is only for completeness and should not emit metrics."); + } + + TEST_METHOD(PcLatency_SingleDisplayed_DirectSample_FirstEma) + { + // Scenario: + // - Single displayed frame P0 provides both pclInputPingTime and pclSimStartTime. + // - There is no subsequent present, so we exercise Case 2 (nextDisplayed == nullptr) + // with two display samples on P0 to mirror the ReportMetrics behavior where the + // last display instance is deferred. + // - The first metrics record must report msPcLatency immediately and seed the EMA. + // Timing (ticks at 10 MHz): + // - Ping0 = 10'000, Sim0 = 20'000 (Δ = 1.0 ms) + // - Display samples: SCR0 = 50'000, SCR0b = 60'000 (provides nextScreenTime). + // Expectations: + // - msPcLatency.has_value() == true and > 0. + // - accumulatedInput2FrameStartTime remains 0 (no dropped chain). + // - Input2FrameStartTimeEma equals CalculateEma(0.0, Δ(PING0,SIM0), 0.1). + // - msPcLatency equals EMA + Δ(SIM0, SCR0), proving pclSimStartTime was used. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 50'000 }); + p0.displayed.PushBack({ FrameType::Application, 60'000 }); + + auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); + Assert::AreEqual(size_t(1), p0_results.size(), + L"P0 should emit metrics immediately when nextDisplayed == nullptr and two display samples exist."); + + const auto& p0_metrics = p0_results[0].metrics; + Assert::IsTrue(p0_metrics.msPcLatency.has_value(), + L"P0 should report msPcLatency for a direct PCL sample."); + Assert::IsTrue(p0_metrics.msPcLatency.value() > 0.0, + L"P0 msPcLatency should be positive."); + Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, + L"Direct PCL sample should not touch accumulatedInput2FrameStartTime."); + Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedPclSimStart, + L"No dropped frames occurred, so there should be no pending pclSimStart."); + + double deltaPingSim = qpc.DeltaUnsignedMilliSeconds(10'000, 20'000); + double expectedEma = pmon::util::CalculateEma(0.0, deltaPingSim, 0.1); + Assert::AreEqual(expectedEma, state.Input2FrameStartTimeEma, 0.0001, + L"Input2FrameStartTimeEma should be seeded from the first Δ(PING,SIM)."); + + double expectedLatency = expectedEma + qpc.DeltaSignedMilliSeconds(20'000, 50'000); + Assert::AreEqual(expectedLatency, p0_metrics.msPcLatency.value(), 0.0001, + L"msPcLatency should use pclSimStartTime (not lastSimStartTime) plus the seeded EMA."); + } + + TEST_METHOD(PcLatency_TwoDisplayed_DirectSamples_UpdateEma) + { + // Scenario: + // - Two displayed frames (P0, P1) each provide direct PCL samples (Ping + Sim). + // - We mimic the ReportMetrics scheduling: + // P0 arrives -> pending only (Case 2). + // P1 arrives -> finalize P0 with nextDisplayed = P1 (Case 3), then queue P1 (Case 2). + // P2 helper -> finalize P1 (Case 3) to observe the EMA update. + // Expectations: + // - P0 final metrics report msPcLatency and seed the EMA. + // - P1 pending call emits no metrics. + // - After P1 is finalized, Input2FrameStartTimeEma changes (≠ value after P0) and stays > 0. + // - accumulatedInput2FrameStartTime stays 0 because no dropped chain exists. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + // -------------------------------------------------------------------- + // P0: first displayed frame with direct PCL sample + // -------------------------------------------------------------------- + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 50'000 }); + + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, state); + Assert::AreEqual(size_t(0), p0_phase1.size(), + L"P0 pending pass should not emit metrics."); + + // -------------------------------------------------------------------- + // P1: second displayed frame with direct PCL sample + // -------------------------------------------------------------------- + FrameData p1{}; + p1.pclInputPingTime = 30'000; + p1.pclSimStartTime = 40'000; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 70'000 }); + + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, state); + Assert::AreEqual(size_t(1), p0_final.size(), + L"Finalizing P0 with nextDisplayed=P1 should emit exactly one metrics record."); + Assert::IsTrue(p0_final[0].metrics.msPcLatency.has_value(), + L"P0 should report msPcLatency when finalized."); + double emaAfterP0 = state.Input2FrameStartTimeEma; + Assert::IsTrue(emaAfterP0 > 0.0, + L"EMA after P0 should be positive."); + Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, + L"Accumulated input-to-frame-start time should remain zero after P0."); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); + Assert::AreEqual(size_t(0), p1_phase1.size(), + L"P1 pending pass should not emit metrics."); + + // -------------------------------------------------------------------- + // P2: helper displayed frame to flush P1 + // -------------------------------------------------------------------- + FrameData p2{}; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 90'000 }); + + auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); + Assert::AreEqual(size_t(1), p1_final.size(), + L"Finalizing P1 should emit exactly one metrics record."); + Assert::IsTrue(p1_final[0].metrics.msPcLatency.has_value(), + L"P1 should report msPcLatency when finalized."); + double emaAfterP1 = state.Input2FrameStartTimeEma; + Assert::IsTrue(emaAfterP1 > 0.0, + L"EMA after P1 should stay positive."); + Assert::IsTrue(emaAfterP1 != emaAfterP0, + L"EMA after P1 must differ from the first-sample EMA after P0."); + Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, + L"No dropped chain should mean accumulatedInput2FrameStartTime stays at 0."); + + auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, state); + Assert::AreEqual(size_t(0), p2_phase1.size(), + L"P2 pending pass is only to mirror the pipeline; it should emit no metrics."); + } + + TEST_METHOD(PcLatency_Dropped_DirectPcl_InitializesAccum) + { + // Scenario: + // - A dropped frame P0 carries both pclInputPingTime and pclSimStartTime. + // - Without any displayed frame to consume it, the PC Latency accumulator should + // be initialized to Δ(PING0, SIM0) while msPcLatency remains absent. + // Expectations: + // - P0's metrics do not expose msPcLatency (it was dropped). + // - accumulatedInput2FrameStartTime equals Δ(PING0, SIM0) and > 0. + // - lastReceivedNotDisplayedPclSimStart latches SIM0. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; + p0.finalState = PresentResult::Discarded; + + auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); + Assert::AreEqual(size_t(1), p0_results.size(), + L"Dropped frames should emit one metrics record immediately."); + Assert::IsFalse(p0_results[0].metrics.msPcLatency.has_value(), + L"Dropped frames must not report msPcLatency."); + + double expectedAccum = qpc.DeltaUnsignedMilliSeconds(10'000, 20'000); + Assert::IsTrue(state.accumulatedInput2FrameStartTime > 0.0, + L"Accumulated input-to-frame-start time should be initialized."); + Assert::AreEqual(expectedAccum, state.accumulatedInput2FrameStartTime, 0.0001, + L"Accumulator should equal Δ(PING0, SIM0)."); + Assert::AreEqual(uint64_t(20'000), state.lastReceivedNotDisplayedPclSimStart, + L"lastReceivedNotDisplayedPclSimStart should track P0's pclSimStartTime."); + } + + TEST_METHOD(PcLatency_DroppedChain_SimOnly_ExtendsAccum) + { + // Scenario: + // - P0 (dropped) has both Ping and Sim, seeding the accumulator. + // - P1 (dropped) has only pclSimStartTime and should extend the accumulator by the + // delta between SIM0 and SIM1. + // Expectations: + // - P1 still reports no msPcLatency. + // - accumulatedInput2FrameStartTime after P1 > accumulated time after P0. + // - lastReceivedNotDisplayedPclSimStart equals SIM1. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; + p0.finalState = PresentResult::Discarded; + + auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); + Assert::AreEqual(size_t(1), p0_results.size()); + Assert::IsFalse(p0_results[0].metrics.msPcLatency.has_value()); + double accumAfterP0 = state.accumulatedInput2FrameStartTime; + + FrameData p1{}; + p1.pclInputPingTime = 0; + p1.pclSimStartTime = 30'000; + p1.finalState = PresentResult::Discarded; + + auto p1_results = ComputeMetricsForPresent(qpc, p1, nullptr, state); + Assert::AreEqual(size_t(1), p1_results.size(), + L"Second dropped frame should emit one metrics record."); + Assert::IsFalse(p1_results[0].metrics.msPcLatency.has_value(), + L"Dropped frames never report msPcLatency."); + Assert::IsTrue(state.accumulatedInput2FrameStartTime > accumAfterP0, + L"Accumulator should grow when a sim-only dropped frame follows an existing chain."); + Assert::AreEqual(uint64_t(30'000), state.lastReceivedNotDisplayedPclSimStart, + L"Sim-only dropped frames still update lastReceivedNotDisplayedPclSimStart."); + } + + TEST_METHOD(PcLatency_Dropped_SimOnly_NoAccum_NoEffect) + { + // Scenario: + // - A single dropped frame P0 only provides pclSimStartTime (no ping) and there is + // no existing accumulator. + // Expectations: + // - msPcLatency remains absent. + // - accumulatedInput2FrameStartTime stays at 0 (chain not started). + // - lastReceivedNotDisplayedPclSimStart updates to SIM0 for possible future chaining. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + FrameData p0{}; + p0.pclInputPingTime = 0; + p0.pclSimStartTime = 25'000; + p0.finalState = PresentResult::Discarded; + + auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, state); + Assert::AreEqual(size_t(1), p0_results.size()); + Assert::IsFalse(p0_results[0].metrics.msPcLatency.has_value()); + Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, + L"Accumulator should remain 0 when a sim-only drop has no pending chain."); + Assert::AreEqual(uint64_t(25'000), state.lastReceivedNotDisplayedPclSimStart, + L"Sim-only drop should remember its pclSimStartTime even if no accumulator exists yet."); + } + + TEST_METHOD(PcLatency_Displayed_SimOnly_NoAccum_UsesExistingEma) + { + // Scenario: + // - P0 is displayed with a direct PCL sample, seeding the EMA. + // - P1 is displayed with only pclSimStartTime (no ping) and there is no accumulated chain. + // - We follow the full pipeline: + // P0 pending, then finalized by P1 (Case 3). + // P1 pending, then finalized by helper P2. + // Expectations: + // - P1 final metrics report msPcLatency despite having no new ping. + // - accumulatedInput2FrameStartTime stays at 0 (no dropped chain was active). + // - Input2FrameStartTimeEma remains positive (not reset to 0). + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 50'000 }); + + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, state); + Assert::AreEqual(size_t(0), p0_phase1.size()); + + FrameData p1{}; + p1.pclInputPingTime = 0; + p1.pclSimStartTime = 35'000; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 70'000 }); + + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, state); + Assert::AreEqual(size_t(1), p0_final.size()); + Assert::IsTrue(p0_final[0].metrics.msPcLatency.has_value()); + double emaAfterP0 = state.Input2FrameStartTimeEma; + Assert::IsTrue(emaAfterP0 > 0.0); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); + Assert::AreEqual(size_t(0), p1_phase1.size()); + + FrameData p2{}; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 90'000 }); + + auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); + Assert::AreEqual(size_t(1), p1_final.size()); + const auto& p1_metrics = p1_final[0].metrics; + Assert::IsTrue(p1_metrics.msPcLatency.has_value(), + L"P1 should report msPcLatency despite missing pclInputPingTime."); + Assert::IsTrue(p1_metrics.msPcLatency.value() > 0.0, + L"P1 msPcLatency should stay positive."); + Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, + L"No dropped chain means the accumulator must stay zero."); + Assert::IsTrue(state.Input2FrameStartTimeEma > 0.0, + L"EMA should not be reset when a sim-only displayed frame uses existing history."); + + auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, state); + Assert::AreEqual(size_t(0), p2_phase1.size()); + } + + TEST_METHOD(PcLatency_Displayed_NoPclSim_UsesLastSimStart) + { + // Scenario: + // - P0 is displayed with a full PCL sample to seed both the EMA and lastSimStartTime. + // - P1 is displayed without any PCL timestamps (pclSimStartTime == 0, pclInputPingTime == 0). + // - P1 should still produce msPcLatency by combining the existing EMA with the fallback + // state.lastSimStartTime recorded after P0. + // Call schedule mirrors ReportMetrics: P0 pending, finalized by P1; P1 pending, finalized by P2. + // Expectations: + // - P1 final metrics report msPcLatency.has_value() == true. + // - Input2FrameStartTimeEma is unchanged from the P0 sample (no new data). + // - msPcLatency equals EMA_after_P0 + Δ(lastSimStartTime_after_P0, SCR1), proving the fallback path. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 30'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 70'000 }); + + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, state); + Assert::AreEqual(size_t(0), p0_phase1.size()); + + FrameData p1{}; + p1.pclInputPingTime = 0; + p1.pclSimStartTime = 0; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 90'000 }); + + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, state); + Assert::AreEqual(size_t(1), p0_final.size()); + Assert::IsTrue(p0_final[0].metrics.msPcLatency.has_value()); + double emaAfterP0 = state.Input2FrameStartTimeEma; + uint64_t fallbackSimStart = state.lastSimStartTime; + Assert::IsTrue(emaAfterP0 > 0.0, + L"EMA must be initialized after the first direct sample."); + Assert::AreEqual(uint64_t(30'000), fallbackSimStart, + L"lastSimStartTime should latch P0's pclSimStartTime when it is displayed."); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); + Assert::AreEqual(size_t(0), p1_phase1.size()); + + FrameData p2{}; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 110'000 }); + + auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, state); + Assert::AreEqual(size_t(1), p1_final.size()); + const auto& p1_metrics = p1_final[0].metrics; + Assert::IsTrue(p1_metrics.msPcLatency.has_value(), + L"P1 should still report msPcLatency using the fallback lastSimStartTime."); + Assert::AreEqual(emaAfterP0, state.Input2FrameStartTimeEma, 0.0001, + L"EMA should remain unchanged when no new PCL sample exists."); + double expectedLatency = emaAfterP0 + qpc.DeltaSignedMilliSeconds(fallbackSimStart, 90'000); + Assert::AreEqual(expectedLatency, p1_metrics.msPcLatency.value(), 0.0001, + L"msPcLatency should use the stored EMA plus the delta from lastSimStartTime to screen time."); + Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, + L"Accumulator should remain zero in this scenario."); + + auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, state); + Assert::AreEqual(size_t(0), p2_phase1.size()); + } + + TEST_METHOD(PcLatency_Dropped_DirectPcl_OverwritesOldAccum) + { + // Scenario: + // - Dropped frames P0 (Ping+Sim) and P1 (Sim only) create an accumulated chain A_old. + // - A new dropped frame P2 arrives with its own Ping+Sim and should overwrite (not extend) + // the accumulator, effectively starting a brand new chain. + // Expectations: + // - Accumulator after P2 equals Δ(PING2, SIM2) exactly (no residue from A_old). + // - lastReceivedNotDisplayedPclSimStart equals SIM2. + // - P2 still reports no msPcLatency. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + FrameData p0{}; + p0.pclInputPingTime = 10'000; + p0.pclSimStartTime = 20'000; + p0.finalState = PresentResult::Discarded; + ComputeMetricsForPresent(qpc, p0, nullptr, state); + + FrameData p1{}; + p1.pclInputPingTime = 0; + p1.pclSimStartTime = 30'000; + p1.finalState = PresentResult::Discarded; + ComputeMetricsForPresent(qpc, p1, nullptr, state); + + double accumBeforeP2 = state.accumulatedInput2FrameStartTime; + Assert::IsTrue(accumBeforeP2 > 0.0, + L"Precondition: accumulator should already be non-zero before introducing P2."); + + FrameData p2{}; + p2.pclInputPingTime = 100'000; + p2.pclSimStartTime = 120'000; + p2.finalState = PresentResult::Discarded; + + auto p2_results = ComputeMetricsForPresent(qpc, p2, nullptr, state); + Assert::AreEqual(size_t(1), p2_results.size()); + Assert::IsFalse(p2_results[0].metrics.msPcLatency.has_value()); + + double expectedAccum = qpc.DeltaUnsignedMilliSeconds(100'000, 120'000); + Assert::AreEqual(expectedAccum, state.accumulatedInput2FrameStartTime, 0.0001, + L"New dropped frame with Ping+Sim should overwrite the accumulator with its own delta."); + Assert::AreEqual(uint64_t(120'000), state.lastReceivedNotDisplayedPclSimStart, + L"lastReceivedNotDisplayedPclSimStart should latch the newest sim start."); + } + + TEST_METHOD(PcLatency_IncompleteDroppedChain_DoesNotAffectDirectSample) + { + // Scenario: + // - D0 (dropped, Ping+Sim) followed by D1 (dropped, Sim-only) builds an incomplete chain. + // - No displayed frame consumes it before a new direct-sample present P0 arrives. + // - P0 should be treated as a fresh direct measurement: EMA behaves like a first sample + // from P0 alone and the stale accumulator is cleared. + // Expectations: + // - D0/D1 never report msPcLatency. + // - P0 final metrics report msPcLatency.has_value() == true. + // - Input2FrameStartTimeEma after P0 equals CalculateEma(0.0, Δ(P0.Ping, P0.Sim), 0.1). + // - accumulatedInput2FrameStartTime resets to 0 after the displayed frame completes. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState state{}; + + // Dropped chain D0 -> D1 (incomplete) + FrameData d0{}; + d0.pclInputPingTime = 10'000; + d0.pclSimStartTime = 20'000; + d0.finalState = PresentResult::Discarded; + auto d0_results = ComputeMetricsForPresent(qpc, d0, nullptr, state); + Assert::AreEqual(size_t(1), d0_results.size()); + Assert::IsFalse(d0_results[0].metrics.msPcLatency.has_value()); + + FrameData d1{}; + d1.pclInputPingTime = 0; + d1.pclSimStartTime = 30'000; + d1.finalState = PresentResult::Discarded; + auto d1_results = ComputeMetricsForPresent(qpc, d1, nullptr, state); + Assert::AreEqual(size_t(1), d1_results.size()); + Assert::IsFalse(d1_results[0].metrics.msPcLatency.has_value()); + double accumBeforeDisplayed = state.accumulatedInput2FrameStartTime; + Assert::IsTrue(accumBeforeDisplayed > 0.0, + L"Incomplete chain should leave a non-zero accumulator."); + + // Displayed P0 with a brand-new direct sample + FrameData p0{}; + p0.pclInputPingTime = 100'000; + p0.pclSimStartTime = 120'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 150'000 }); + + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, state); + Assert::AreEqual(size_t(0), p0_phase1.size()); + + FrameData p1{}; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 180'000 }); + + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, state); + Assert::AreEqual(size_t(1), p0_final.size()); + const auto& p0_metrics = p0_final[0].metrics; + Assert::IsTrue(p0_metrics.msPcLatency.has_value(), + L"Displayed frame with direct PCL data must report msPcLatency."); + Assert::IsTrue(p0_metrics.msPcLatency.value() > 0.0, + L"msPcLatency should be positive for P0."); + + double expectedFirstEma = pmon::util::CalculateEma(0.0, + qpc.DeltaUnsignedMilliSeconds(100'000, 120'000), + 0.1); + Assert::AreEqual(expectedFirstEma, state.Input2FrameStartTimeEma, 0.0001, + L"EMA after P0 should match a first-sample EMA that ignores stale accumulation."); + Assert::AreEqual(0.0, state.accumulatedInput2FrameStartTime, 0.0001, + L"Accumulator must be cleared once the displayed frame consumes the chain."); + Assert::AreEqual(uint64_t(0), state.lastReceivedNotDisplayedPclSimStart, + L"Pending pclSimStart markers should be cleared once the chain completes."); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, state); + Assert::AreEqual(size_t(0), p1_phase1.size()); + } + }; + TEST_CLASS(InstrumentedMetricsTests) + { + public: + + TEST_METHOD(InstrumentedCpuGpu_AppFrame_FullData_UsesPclSimStart) + { + // This test verifies the "instrumented CPU/GPU" metrics on an application frame: + // + // - msInstrumentedSleep + // - msInstrumentedGpuLatency + // - msBetweenSimStarts (PCL sim preferred over App sim) + // + // We construct: + // + // QPC frequency = 10 MHz + // + // Pre-state in swapChain: + // lastSimStartTime = 10'000 (this represents the previous frame's sim start) + // + // P0 (the frame under test) – APP FRAME: + // appSleepStartTime = 1'000 + // appSleepEndTime = 11'000 // Δsleep = 10'000 ticks + // appSimStartTime =100'000 // should NOT be used for between-sim-starts + // pclSimStartTime = 20'000 // PCL sim should win for between-sim-starts + // gpuStartTime = 21'000 // GPU start time used for GPU latency + // displayed = one Application entry at screenTime = 50'000 + // + // Derived deltas: + // sleep Δ: 11'000 - 1'000 = 10'000 ticks + // PCL sim Δ: 20'000 - 10'000 = 10'000 ticks + // GPU latency Δ: 21'000 - 11'000 = 10'000 ticks + // + // With QPC = 10 MHz, 10'000 ticks = 0.001 ms. + // + // Call pattern (ReportMetrics-style for a single displayed app frame): + // + // P0 arrives: + // ComputeMetricsForPresent(P0, nullptr, chain) // Case 2, pending only + // + // P1 arrives later: + // ComputeMetricsForPresent(P0, &P1, chain) // Case 3, finalize P0 + // ComputeMetricsForPresent(P1, nullptr, chain) // pending P1 (ignored in this test) + // + // We verify on P0's final metrics: + // - msInstrumentedSleep has a value and matches Δ(appSleepStart, appSleepEnd). + // - msInstrumentedGpuLatency has a value and matches Δ(appSleepEnd, gpuStartTime). + // - msBetweenSimStarts has a value and matches Δ(lastSimStartTime, P0.pclSimStartTime), + // proving PCL sim is preferred over App sim for between-sim-starts. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + // Seed lastSimStartTime to simulate a previous frame. + chain.lastSimStartTime = 10'000; + chain.animationErrorSource = AnimationErrorSource::PCLatency; + + const uint32_t PROCESS_ID = 1234; + const uint64_t SWAPCHAIN = 0xABC0; + + // -------------------------------------------------------------------- + // P0: Application frame with full instrumented CPU/GPU data + // -------------------------------------------------------------------- + FrameData p0{}; + p0.processId = PROCESS_ID; + p0.swapChainAddress = SWAPCHAIN; + + p0.presentStartTime = 0; + p0.timeInPresent = 0; + p0.readyTime = 0; + + // Instrumented CPU / sim: + p0.appSleepStartTime = 1'000; + p0.appSleepEndTime = 11'000; + p0.appSimStartTime = 100'000; // should NOT be used for between-sim-starts + p0.pclSimStartTime = 20'000; // should be used instead + + // GPU start time for GPU latency: + p0.gpuStartTime = 21'000; + + // Mark as displayed Application frame + p0.finalState = PresentResult::Presented; + p0.displayed.Clear(); + p0.displayed.PushBack({ FrameType::Application, 50'000 }); // screenTime = 50'000 + + // First call: P0 arrives, becomes pending (no metrics yet). + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(0), p0_phase1.size(), + L"P0 (phase 1): pending-only call with nextDisplayed=nullptr should produce no metrics."); + + // -------------------------------------------------------------------- + // P1: simple next displayed app frame (used only as nextDisplayed for P0) + // -------------------------------------------------------------------- + FrameData p1{}; + p1.processId = PROCESS_ID; + p1.swapChainAddress = SWAPCHAIN; + + p1.presentStartTime = 0; + p1.timeInPresent = 0; + p1.readyTime = 0; + + p1.finalState = PresentResult::Presented; + p1.displayed.Clear(); + p1.displayed.PushBack({ FrameType::Application, 60'000 }); // later display, not important + + // Second call: P1 arrives, finalize P0 using P1 as nextDisplayed (Case 3). + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); + Assert::AreEqual(size_t(1), p0_final.size(), + L"P0 (final): expected exactly one metrics record when flushed with nextDisplayed=P1."); + + const auto& m0 = p0_final[0].metrics; + + // -------------------------------------------------------------------- + // Assertions for P0's instrumented CPU/GPU metrics + // -------------------------------------------------------------------- + // Expected values based on our chosen QPC times: + double expectedSleepMs = qpc.DeltaUnsignedMilliSeconds(1'000, 11'000); // 10'000 ticks + double expectedGpuMs = qpc.DeltaUnsignedMilliSeconds(11'000, 21'000); // 10'000 ticks + double expectedBetween = qpc.DeltaUnsignedMilliSeconds(10'000, 20'000); // 10'000 ticks + + // 1) Instrumented sleep + Assert::IsTrue(m0.msInstrumentedSleep.has_value(), + L"P0: msInstrumentedSleep should have a value for valid AppSleepStart/End."); + Assert::AreEqual(expectedSleepMs, m0.msInstrumentedSleep.value(), 1e-6, + L"P0: msInstrumentedSleep did not match expected Δ(AppSleepStart, AppSleepEnd)."); + + // 2) Instrumented GPU latency (start = AppSleepEndTime since it is non-zero) + Assert::IsTrue(m0.msInstrumentedGpuLatency.has_value(), + L"P0: msInstrumentedGpuLatency should have a value when InstrumentedStartTime and gpuStartTime are valid."); + Assert::AreEqual(expectedGpuMs, m0.msInstrumentedGpuLatency.value(), 1e-6, + L"P0: msInstrumentedGpuLatency did not match expected Δ(AppSleepEndTime, gpuStartTime)."); + + // 3) Between sim starts: PCL sim (20'000) must win over App sim (100'000) + Assert::IsTrue(m0.msBetweenSimStarts.has_value(), + L"P0: msBetweenSimStarts should have a value when lastSimStartTime and PclSimStartTime are non-zero."); + Assert::AreEqual(expectedBetween, m0.msBetweenSimStarts.value(), 1e-6, + L"P0: msBetweenSimStarts should be based on PCL sim start, not App sim start."); + } + TEST_METHOD(InstrumentedDisplay_AppFrame_FullData_ComputesAll) + { + // This test verifies the "instrumented display" metrics on a displayed + // application frame: + // + // - msInstrumentedRenderLatency + // - msReadyTimeToDisplayLatency + // - msInstrumentedLatency (total app-instrumented latency) + // + // New invariant (unified metrics): + // These metrics are only computed when: + // - the frame is an Application frame (isAppFrame == true), AND + // - the frame is displayed (isDisplayed == true). + // + // We construct: + // + // QPC frequency = 10 MHz + // + // P0 (the frame under test) – DISPLAYED APP FRAME: + // appRenderSubmitStartTime = 10'000 + // readyTime = 20'000 + // appSleepEndTime = 5'000 // used as InstrumentedStartTime + // screenTime = 30'000 (Application entry in displayed) + // + // Derived deltas: + // render latency: 30'000 - 10'000 = 20'000 ticks + // ready→display: 30'000 - 20'000 = 10'000 ticks + // total inst. latency: 30'000 - 5'000 = 25'000 ticks + // + // With QPC = 10 MHz: + // 10'000 ticks = 0.001 ms + // 20'000 ticks = 0.002 ms + // 25'000 ticks = 0.0025 ms + // + // Call pattern (mirroring ReportMetrics for a displayed app frame): + // + // P0 arrives: + // ComputeMetricsForPresent(P0, nullptr, chain) // Case 2, pending only + // + // P1 arrives: + // ComputeMetricsForPresent(P0, &P1, chain) // Case 3, finalize P0 + // ComputeMetricsForPresent(P1, nullptr, chain) // pending P1 (ignored) + // + // We verify on P0's final metrics: + // - msInstrumentedRenderLatency has a value and matches Δ(appRenderSubmitStartTime, screenTime). + // - msReadyTimeToDisplayLatency has a value and matches Δ(readyTime, screenTime). + // - msInstrumentedLatency has a value and matches Δ(appSleepEndTime, screenTime). + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + const uint32_t PROCESS_ID = 1234; + const uint64_t SWAPCHAIN = 0xABC0; + + // -------------------------------------------------------------------- + // P0: Displayed Application frame with full instrumented display data + // -------------------------------------------------------------------- + FrameData p0{}; + p0.processId = PROCESS_ID; + p0.swapChainAddress = SWAPCHAIN; + + p0.presentStartTime = 0; + p0.timeInPresent = 0; + p0.readyTime = 20'000; // ReadyTime + + // Instrumented markers + p0.appRenderSubmitStartTime = 10'000; + p0.appSleepEndTime = 5'000; + p0.appSimStartTime = 0; // not needed in this test + + // Mark as displayed Application frame with a single screen time. + p0.finalState = PresentResult::Presented; + p0.displayed.Clear(); + p0.displayed.PushBack({ FrameType::Application, 30'000 }); // screenTime = 30'000 + + // First call: P0 arrives, becomes pending. + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(0), p0_phase1.size(), + L"P0 (phase 1): pending-only call with nextDisplayed=nullptr should produce no metrics."); + + // -------------------------------------------------------------------- + // P1: Next displayed app frame (used only as nextDisplayed for P0) + // -------------------------------------------------------------------- + FrameData p1{}; + p1.processId = PROCESS_ID; + p1.swapChainAddress = SWAPCHAIN; + + p1.presentStartTime = 0; + p1.timeInPresent = 0; + p1.readyTime = 0; + + p1.finalState = PresentResult::Presented; + p1.displayed.Clear(); + p1.displayed.PushBack({ FrameType::Application, 40'000 }); // later display + + // Second call: finalize P0 with nextDisplayed=P1 + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); + Assert::AreEqual(size_t(1), p0_final.size(), + L"P0 (final): expected exactly one metrics record when flushed with nextDisplayed=P1."); + + const auto& m0 = p0_final[0].metrics; + + // For completeness, process P1 as pending (not used in this test). + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); + Assert::AreEqual(size_t(0), p1_phase1.size(), + L"P1 (phase 1): first call with nextDisplayed=nullptr should produce no metrics (pending only)."); + + // -------------------------------------------------------------------- + // Assertions for P0's instrumented display metrics + // -------------------------------------------------------------------- + double expectedRenderMs = qpc.DeltaUnsignedMilliSeconds(10'000, 30'000); // 20'000 ticks + double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(20'000, 30'000); // 10'000 ticks + double expectedTotalMs = qpc.DeltaUnsignedMilliSeconds(5'000, 30'000); // 25'000 ticks + + // Render latency + Assert::IsTrue(m0.msInstrumentedRenderLatency.has_value(), + L"P0: msInstrumentedRenderLatency should have a value for a displayed app frame with AppRenderSubmitStartTime."); + Assert::AreEqual(expectedRenderMs, m0.msInstrumentedRenderLatency.value(), 1e-6, + L"P0: msInstrumentedRenderLatency did not match expected Δ(AppRenderSubmitStartTime, screenTime)."); + + // Ready-to-display latency + Assert::IsTrue(m0.msReadyTimeToDisplayLatency.has_value(), + L"P0: msReadyTimeToDisplayLatency should have a value when ReadyTime and screenTime are valid."); + Assert::AreEqual(expectedReadyMs, m0.msReadyTimeToDisplayLatency.value(), 1e-6, + L"P0: msReadyTimeToDisplayLatency did not match expected Δ(ReadyTime, screenTime)."); + + // Total instrumented latency: from appSleepEndTime to screenTime + Assert::IsTrue(m0.msInstrumentedLatency.has_value(), + L"P0: msInstrumentedLatency should have a value when there is a valid instrumented start time."); + Assert::AreEqual(expectedTotalMs, m0.msInstrumentedLatency.value(), 1e-6, + L"P0: msInstrumentedLatency did not match expected Δ(AppSleepEndTime, screenTime)."); + } + + TEST_METHOD(InstrumentedCpuGpu_AppFrame_NoSleep_UsesAppSimStart) + { + // Scenario: + // - Validate the instrumented CPU/GPU metrics when the application never enters an + // instrumented sleep, forcing GPU latency to fall back to appSimStart. + // - Also ensure msBetweenSimStarts uses the stored lastSimStartTime → appSimStart delta + // when no PCL sim timestamp is present. + // + // QPC frequency: 10 MHz. + // + // Pre-state: + // chain.lastSimStartTime = 40'000 (represents the previous frame's sim start). + // + // P0 (displayed Application frame): + // appSleepStartTime = 0 + // appSleepEndTime = 0 + // appSimStartTime = 70'000 (used for GPU latency + between-sim-starts) + // pclSimStartTime = 0 (forces AppSim fallback) + // gpuStartTime = 90'000 + // screenTime = 120'000 (Application entry) + // Δ(sim start vs last) = 30'000 ticks, Δ(sim start → gpu) = 20'000 ticks. + // + // Call pattern (Case 2/3): + // P0 pending → Compute(..., nullptr) + // P1 arrives → Compute(P0, &P1) to finalize P0, then Compute(P1, nullptr) to seed next pending. + // + // Expectations on P0 final metrics: + // - msInstrumentedSleep has no value (no sleep interval). + // - msInstrumentedGpuLatency uses appSimStartTime (70'000 → 90'000). + // - msBetweenSimStarts computes 40'000 → 70'000. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + chain.lastSimStartTime = 40'000; + chain.animationErrorSource = AnimationErrorSource::AppProvider; + + const uint32_t PROCESS_ID = 4321; + const uint64_t SWAPCHAIN = 0x2222; + + // P0: displayed Application frame with no sleep range but valid appSimStart + FrameData p0{}; + p0.processId = PROCESS_ID; + p0.swapChainAddress = SWAPCHAIN; + p0.appSimStartTime = 70'000; + p0.gpuStartTime = 90'000; + p0.finalState = PresentResult::Presented; + p0.displayed.Clear(); + p0.displayed.PushBack({ FrameType::Application, 120'000 }); + + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(0), p0_phase1.size(), + L"P0 (phase 1) should stay pending when nextDisplayed is unavailable."); + + // P1: minimal next displayed application frame + FrameData p1{}; + p1.processId = PROCESS_ID; + p1.swapChainAddress = SWAPCHAIN; + p1.finalState = PresentResult::Presented; + p1.displayed.Clear(); + p1.displayed.PushBack({ FrameType::Application, 150'000 }); + + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); + Assert::AreEqual(size_t(1), p0_final.size(), + L"P0 (final) should emit exactly one metrics record once nextDisplayed is provided."); + + const auto& m0 = p0_final[0].metrics; + Assert::IsFalse(m0.msInstrumentedSleep.has_value(), + L"P0: Instrumented sleep must be absent when the app never emitted sleep markers."); + Assert::IsTrue(m0.msInstrumentedGpuLatency.has_value(), + L"P0: GPU latency should fall back to AppSimStart when no sleep end exists."); + Assert::IsTrue(m0.msBetweenSimStarts.has_value(), + L"P0: Between-sim-starts should use the stored lastSimStartTime when AppSimStart is valid."); + + double expectedGpuMs = qpc.DeltaUnsignedMilliSeconds(70'000, 90'000); + double expectedBetweenMs = qpc.DeltaUnsignedMilliSeconds(40'000, 70'000); + + Assert::AreEqual(expectedGpuMs, m0.msInstrumentedGpuLatency.value(), 1e-6, + L"P0: msInstrumentedGpuLatency should measure Δ(AppSimStartTime, gpuStartTime)."); + Assert::AreEqual(expectedBetweenMs, m0.msBetweenSimStarts.value(), 1e-6, + L"P0: msBetweenSimStarts should use AppSimStart when no PCL sim exists."); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); + Assert::AreEqual(size_t(0), p1_phase1.size(), + L"P1 (phase 1) remains pending for completeness."); + } + + TEST_METHOD(InstrumentedCpuGpu_AppFrame_NoSleepNoSim_NoInstrumentedCpuGpu) + { + // Scenario: + // - Displayed Application frame with no instrumented sleep markers and neither appSimStartTime + // nor pclSimStartTime populated. GPU start exists, but there is no instrumented start anchor. + // + // QPC frequency: 10 MHz. + // Pre-state: chain.lastSimStartTime = 55'000. + // P0 fields: appSleepStart=0, appSleepEnd=0, appSimStart=0, pclSimStart=0, gpuStart=80'000, + // screenTime=100'000 (Application display). + // Derived deltas: none are valid because the start markers are zero. + // + // Call pattern (Case 2/3): + // P0 pending → Compute(..., nullptr) + // P1 arrives → Compute(P0, &P1) to flush, then Compute(P1, nullptr) for completeness. + // + // Expectations: all three instrumented CPU metrics stay std::nullopt for P0. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + chain.lastSimStartTime = 55'000; + + const uint32_t PROCESS_ID = 9876; + const uint64_t SWAPCHAIN = 0xEF00; + + FrameData p0{}; + p0.processId = PROCESS_ID; + p0.swapChainAddress = SWAPCHAIN; + p0.gpuStartTime = 80'000; + p0.finalState = PresentResult::Presented; + p0.displayed.Clear(); + p0.displayed.PushBack({ FrameType::Application, 100'000 }); + + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(0), p0_phase1.size()); + + FrameData p1{}; + p1.processId = PROCESS_ID; + p1.swapChainAddress = SWAPCHAIN; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 120'000 }); + + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); + Assert::AreEqual(size_t(1), p0_final.size()); + + const auto& m0 = p0_final[0].metrics; + Assert::IsFalse(m0.msInstrumentedSleep.has_value(), + L"P0: sleep metrics require both start and end markers."); + Assert::IsFalse(m0.msInstrumentedGpuLatency.has_value(), + L"P0: GPU latency must remain off without an instrumented start time."); + Assert::IsFalse(m0.msBetweenSimStarts.has_value(), + L"P0: between-sim-starts cannot be computed without a new sim start."); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); + Assert::AreEqual(size_t(0), p1_phase1.size()); + } + + TEST_METHOD(InstrumentedCpuGpu_AppFrame_NotDisplayed_StillComputed) + { + // Scenario: + // - Dropped (not displayed) Application frame with full instrumented CPU/GPU markers, including PCL sim. + // - Validates that CPU/GPU metrics still compute even when the frame never displays, while + // display-only metrics remain unset. + // + // QPC frequency: 10 MHz. + // Pre-state: chain.lastSimStartTime = 5'000. + // P0 fields: appSleepStart=10'000, appSleepEnd=25'000, appSimStart=30'000, + // gpuStart=45'000, no displayed entries (finalState = Discarded). + // Derived deltas: sleep Δ = 15'000 ticks, GPU latency Δ = 20'000 ticks, + // between-sim-starts Δ = 30'000 ticks. + // + // Call pattern: Case 1 (pure dropped) → single ComputeMetricsForPresent call with nextDisplayed == nullptr. + // + // Expectations: instrumented sleep/GPU/betweenSimStarts all have values with the deltas above, + // and display instrumented metrics remain std::nullopt. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + chain.lastSimStartTime = 5'000; + chain.animationErrorSource = AnimationErrorSource::AppProvider; + + FrameData p0{}; + p0.appSleepStartTime = 10'000; + p0.appSleepEndTime = 25'000; + p0.appSimStartTime = 30'000; + p0.gpuStartTime = 45'000; + p0.finalState = PresentResult::Discarded; + + auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(1), p0_results.size(), + L"Dropped frames should emit their metrics immediately (Case 1)."); + + const auto& m0 = p0_results[0].metrics; + double expectedSleepMs = qpc.DeltaUnsignedMilliSeconds(10'000, 25'000); + double expectedGpuMs = qpc.DeltaUnsignedMilliSeconds(25'000, 45'000); + double expectedBetweenMs = qpc.DeltaUnsignedMilliSeconds(5'000, 30'000); + + Assert::IsTrue(m0.msInstrumentedSleep.has_value()); + Assert::AreEqual(expectedSleepMs, m0.msInstrumentedSleep.value(), 1e-6); + + Assert::IsTrue(m0.msInstrumentedGpuLatency.has_value()); + Assert::AreEqual(expectedGpuMs, m0.msInstrumentedGpuLatency.value(), 1e-6); + + Assert::IsTrue(m0.msBetweenSimStarts.has_value()); + Assert::AreEqual(expectedBetweenMs, m0.msBetweenSimStarts.value(), 1e-6); + + Assert::IsFalse(m0.msInstrumentedRenderLatency.has_value(), + L"Display-dependent metrics must stay off for non-displayed frames."); + Assert::IsFalse(m0.msReadyTimeToDisplayLatency.has_value()); + Assert::IsFalse(m0.msInstrumentedLatency.has_value()); + } + + TEST_METHOD(InstrumentedCpuGpu_NonAppFrame_Ignored) + { + // Scenario: + // - Displayed frame whose sole display entry is FrameType::Repeated, so DisplayIndexing never + // marks an Application display. + // - Even with instrumented CPU/GPU markers present, msInstrumentedSleep/GpuLatency/BetweenSimStarts + // must remain unset because the display instance is not an app frame. + // + // QPC frequency: 10 MHz. + // Pre-state: chain.lastSimStartTime = 60'000. + // P0 fields: appSleepStart=11'000, appSleepEnd=21'000, appSimStart=70'000, pclSimStart=72'000, + // gpuStart=90'000, displayed[0] = (Repeated, 120'000). + // Derived deltas (that should be ignored): sleep Δ = 10'000 ticks, GPU Δ = 69'000 ticks, + // between-sim-starts Δ = 10'000 ticks. + // + // Call pattern: Case 2/3 (P0 pending, finalized by a synthetic P1, then P1 pending). + // + // Expectations: all instrumented CPU metrics remain std::nullopt for P0. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + chain.lastSimStartTime = 60'000; + + const uint32_t PROCESS_ID = 5555; + const uint64_t SWAPCHAIN = 0xDEADBEEF; + + FrameData p0{}; + p0.processId = PROCESS_ID; + p0.swapChainAddress = SWAPCHAIN; + p0.appSleepStartTime = 11'000; + p0.appSleepEndTime = 21'000; + p0.appSimStartTime = 70'000; + p0.pclSimStartTime = 72'000; + p0.gpuStartTime = 90'000; + p0.finalState = PresentResult::Presented; + p0.displayed.Clear(); + p0.displayed.PushBack({ FrameType::Repeated, 120'000 }); + + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(0), p0_phase1.size()); + + FrameData p1{}; + p1.processId = PROCESS_ID; + p1.swapChainAddress = SWAPCHAIN; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 150'000 }); + + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); + Assert::AreEqual(size_t(1), p0_final.size()); + const auto& m0 = p0_final[0].metrics; + + Assert::IsFalse(m0.msInstrumentedSleep.has_value(), + L"Non-app displays must not emit instrumented CPU metrics."); + Assert::IsFalse(m0.msInstrumentedGpuLatency.has_value()); + Assert::IsFalse(m0.msBetweenSimStarts.has_value()); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); + Assert::AreEqual(size_t(0), p1_phase1.size()); + } + + TEST_METHOD(InstrumentedDisplay_AppFrame_NoRenderSubmit_RenderLatencyOff) + { + // Scenario: + // - Displayed Application frame missing appRenderSubmitStartTime but with readyTime + sleep end. + // + // QPC frequency: 10 MHz. + // P0 fields: readyTime = 80'000, appSleepEndTime = 50'000, no render submit, screenTime = 100'000. + // Derived deltas: ready→display Δ = 20'000 ticks, total latency Δ = 50'000 ticks. + // + // Call pattern: Case 2/3 (P0 pending, finalized by P1, then P1 pending). + // + // Expectations: msInstrumentedRenderLatency = nullopt, while msReadyTimeToDisplayLatency and + // msInstrumentedLatency match the deltas above. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData p0{}; + p0.readyTime = 80'000; + p0.appSleepEndTime = 50'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 100'000 }); + + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(0), p0_phase1.size()); + + FrameData p1{}; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 130'000 }); + + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); + Assert::AreEqual(size_t(1), p0_final.size()); + const auto& m0 = p0_final[0].metrics; + + double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(80'000, 100'000); + double expectedTotalMs = qpc.DeltaUnsignedMilliSeconds(50'000, 100'000); + + Assert::IsFalse(m0.msInstrumentedRenderLatency.has_value(), + L"Render latency must remain off without appRenderSubmitStartTime."); + Assert::IsTrue(m0.msReadyTimeToDisplayLatency.has_value()); + Assert::AreEqual(expectedReadyMs, m0.msReadyTimeToDisplayLatency.value(), 1e-6); + + Assert::IsTrue(m0.msInstrumentedLatency.has_value()); + Assert::AreEqual(expectedTotalMs, m0.msInstrumentedLatency.value(), 1e-6); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); + Assert::AreEqual(size_t(0), p1_phase1.size()); + } + + TEST_METHOD(InstrumentedDisplay_AppFrame_NoSleep_UsesAppSimStart) + { + // Scenario: + // - Displayed Application frame lacks appSleepEndTime but provides appSimStartTime. + // + // QPC timeline (10 MHz): + // P0.appRenderSubmitStartTime = 10'000 + // P0.appSimStartTime = 5'000 + // P0.readyTime = 30'000 + // P0.screenTime = 60'000 + // Derived deltas: + // - Render latency: 50'000 ticks + // - Ready→display: 30'000 ticks + // - Total instrumented latency (AppSim→screen): 55'000 ticks + // + // Call pattern: Case 2/3. + // + // Expectations: render + ready latencies computed normally; msInstrumentedLatency should fall back + // to AppSimStartTime since sleep end is missing. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData p0{}; + p0.appRenderSubmitStartTime = 10'000; + p0.appSimStartTime = 5'000; + p0.readyTime = 30'000; + p0.appSleepEndTime = 0; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 60'000 }); + + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(0), p0_phase1.size()); + + FrameData p1{}; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 90'000 }); + + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); + Assert::AreEqual(size_t(1), p0_final.size()); + const auto& m0 = p0_final[0].metrics; + + double expectedRenderMs = qpc.DeltaUnsignedMilliSeconds(10'000, 60'000); + double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(30'000, 60'000); + double expectedTotalMs = qpc.DeltaUnsignedMilliSeconds(5'000, 60'000); + + Assert::IsTrue(m0.msInstrumentedRenderLatency.has_value()); + Assert::AreEqual(expectedRenderMs, m0.msInstrumentedRenderLatency.value(), 1e-6); + Assert::IsTrue(m0.msReadyTimeToDisplayLatency.has_value()); + Assert::AreEqual(expectedReadyMs, m0.msReadyTimeToDisplayLatency.value(), 1e-6); + Assert::IsTrue(m0.msInstrumentedLatency.has_value()); + Assert::AreEqual(expectedTotalMs, m0.msInstrumentedLatency.value(), 1e-6, + L"Total latency should fall back to AppSimStartTime when sleep end is missing."); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); + Assert::AreEqual(size_t(0), p1_phase1.size()); + } + + TEST_METHOD(InstrumentedDisplay_AppFrame_NoSleepNoSim_NoTotalLatency) + { + // Scenario: + // - Displayed Application frame has render submit + ready markers but neither appSleepEndTime + // nor appSimStartTime, so the total instrumented latency should be disabled. + // + // QPC values (10 MHz): appRenderSubmitStartTime = 12'000, readyTime = 32'000, screenTime = 70'000. + // Derived deltas: + // - Render latency Δ = 58'000 ticks + // - Ready→display Δ = 38'000 ticks + // + // Call pattern: Case 2/3. + // + // Expectations: render + ready metrics populated with the deltas above; msInstrumentedLatency is nullopt. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData p0{}; + p0.appRenderSubmitStartTime = 12'000; + p0.readyTime = 32'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Application, 70'000 }); + + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(0), p0_phase1.size()); + + FrameData p1{}; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 90'000 }); + + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); + Assert::AreEqual(size_t(1), p0_final.size()); + const auto& m0 = p0_final[0].metrics; + + double expectedRenderMs = qpc.DeltaUnsignedMilliSeconds(12'000, 70'000); + double expectedReadyMs = qpc.DeltaUnsignedMilliSeconds(32'000, 70'000); + + Assert::IsTrue(m0.msInstrumentedRenderLatency.has_value()); + Assert::AreEqual(expectedRenderMs, m0.msInstrumentedRenderLatency.value(), 1e-6); + Assert::IsTrue(m0.msReadyTimeToDisplayLatency.has_value()); + Assert::AreEqual(expectedReadyMs, m0.msReadyTimeToDisplayLatency.value(), 1e-6); + Assert::IsFalse(m0.msInstrumentedLatency.has_value(), + L"Total instrumented latency must stay off without an instrumented start."); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); + Assert::AreEqual(size_t(0), p1_phase1.size()); + } + + TEST_METHOD(InstrumentedDisplay_NonAppFrame_Ignored) + { + // Scenario: + // - Displayed frame whose first (and only) display entry is FrameType::Repeated, so the + // DisplayIndexing logic never flags an application frame for this present. + // + // QPC values (10 MHz): appRenderSubmitStartTime = 10'000, readyTime = 30'000, appSleepEndTime = 5'000, + // screenTime = 60'000. These deltas should all be ignored. + // + // Call pattern: Case 2/3. + // + // Expectations: all instrumented display metrics remain std::nullopt for P0. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData p0{}; + p0.appRenderSubmitStartTime = 10'000; + p0.readyTime = 30'000; + p0.appSleepEndTime = 5'000; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Repeated, 60'000 }); + + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(0), p0_phase1.size()); + + FrameData p1{}; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 90'000 }); + + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); + Assert::AreEqual(size_t(1), p0_final.size()); + const auto& m0 = p0_final[0].metrics; + + Assert::IsFalse(m0.msInstrumentedRenderLatency.has_value()); + Assert::IsFalse(m0.msInstrumentedLatency.has_value()); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); + Assert::AreEqual(size_t(0), p1_phase1.size()); + } + + TEST_METHOD(InstrumentedDisplay_AppFrame_NotDisplayed_NoDisplayMetrics) + { + // Scenario: + // - Application frame with appRenderSubmit/ready/sleep/appSim markers that gets discarded (not displayed). + // - Ensures display-only instrumented metrics stay unset when there is no screen time. + // + // QPC values: appRenderSubmitStart = 9'000, readyTime = 19'000, appSleepEnd = 4'000, + // appSimStart = 2'000. No displayed entries, so screenTime is undefined. + // + // Call pattern: Case 1 (single call, nextDisplayed == nullptr). + // + // Expectations: msInstrumentedRenderLatency / msReadyTimeToDisplayLatency / msInstrumentedLatency are nullopt. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + FrameData p0{}; + p0.appRenderSubmitStartTime = 9'000; + p0.readyTime = 19'000; + p0.appSleepEndTime = 4'000; + p0.appSimStartTime = 2'000; + p0.finalState = PresentResult::Discarded; + + auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(1), p0_results.size()); + const auto& m0 = p0_results[0].metrics; + + Assert::IsFalse(m0.msInstrumentedRenderLatency.has_value()); + Assert::IsFalse(m0.msReadyTimeToDisplayLatency.has_value()); + Assert::IsFalse(m0.msInstrumentedLatency.has_value()); + } + + TEST_METHOD(InstrumentedInput_DroppedAppFrame_PendingProviderInput_ConsumedOnDisplay) + { + // Scenario: + // - P0 is a dropped Application frame that carries an App provider input sample at 20'000 ticks. + // - P1 is the next displayed Application frame (screenTime = 70'000) without its own sample. + // - P2 is a synthetic follower to flush P1, mirroring the ReportMetrics Case 2/3 pattern. + // + // QPC-derived delta: 70'000 - 20'000 = 50'000 ticks = 5 ms (with 10 MHz QPC). + // + // Call pattern: + // - P0 (Case 1) → Compute(..., nullptr) populates lastReceivedNotDisplayedAppProviderInputTime. + // - P1 pending → Compute(P1, nullptr). + // - P1 final → Compute(P1, &P2) produces metrics. + // + // Expectations: + // - Cached provider input equals 20'000 after P0. + // - P1 reports msInstrumentedInputTime = 5 ms and clears all pending caches afterward. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + const uint64_t pendingInputTime = 20'000; + + // P0: Dropped Application frame with provider input. + FrameData p0{}; + p0.appInputSample = { pendingInputTime, InputDeviceType::Mouse }; + p0.finalState = PresentResult::Discarded; + + auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(1), p0_results.size()); + Assert::AreEqual(pendingInputTime, chain.lastReceivedNotDisplayedAppProviderInputTime, + L"Dropped provider input should be cached until a displayed frame consumes it."); + + // P1: Displayed Application frame without its own AppInputSample. + FrameData p1{}; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 70'000 }); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); + Assert::AreEqual(size_t(0), p1_phase1.size()); + + // P2: Simple next displayed frame to flush P1. + FrameData p2{}; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 90'000 }); + + auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, chain); + Assert::AreEqual(size_t(1), p1_final.size()); + const auto& m1 = p1_final[0].metrics; + + Assert::IsTrue(m1.msInstrumentedInputTime.has_value(), + L"P1 should consume the cached provider input time once it is displayed."); + double expectedInputMs = qpc.DeltaUnsignedMilliSeconds(pendingInputTime, 70'000); + Assert::AreEqual(expectedInputMs, m1.msInstrumentedInputTime.value(), 1e-6); + + Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedAppProviderInputTime, + L"Pending provider input cache must be cleared after consumption."); + Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedAllInputTime); + Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedMouseClickTime); + + auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, chain); + Assert::AreEqual(size_t(0), p2_phase1.size()); + } + + TEST_METHOD(InstrumentedInput_DisplayedAppFrame_WithOwnSample_IgnoresPending) + { + // Scenario: + // - P0 (dropped) seeds pending provider input at 10'000 ticks. + // - P1 (displayed Application) carries its own sample at 15'000 ticks and displays at 60'000. + // - P2 finalizes P1. + // + // QPC-derived deltas: + // - Pending path would have produced 50'000 ticks, but we expect 45'000 ticks from P1's own sample. + // + // Call pattern: identical to Test 10 (Case 1 for P0, Case 2/3 for P1). + // + // Expectations: + // - Cached pending input updated after P0. + // - P1 final metrics use Δ(15'000, 60'000) only and clear the pending cache. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + const uint64_t pendingInputTime = 10'000; + const uint64_t directInputTime = 15'000; + + FrameData p0{}; + p0.appInputSample = { pendingInputTime, InputDeviceType::Keyboard }; + p0.finalState = PresentResult::Discarded; + + auto p0_results = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(1), p0_results.size()); + Assert::AreEqual(pendingInputTime, chain.lastReceivedNotDisplayedAppProviderInputTime); + + FrameData p1{}; + p1.appInputSample = { directInputTime, InputDeviceType::Mouse }; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 60'000 }); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); + Assert::AreEqual(size_t(0), p1_phase1.size()); + + FrameData p2{}; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 80'000 }); + + auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, chain); + Assert::AreEqual(size_t(1), p1_final.size()); + const auto& m1 = p1_final[0].metrics; + + double expectedInputMs = qpc.DeltaUnsignedMilliSeconds(directInputTime, 60'000); + Assert::IsTrue(m1.msInstrumentedInputTime.has_value()); + Assert::AreEqual(expectedInputMs, m1.msInstrumentedInputTime.value(), 1e-6, + L"P1 must prefer its own input marker over pending values."); + + Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedAppProviderInputTime); + + auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, chain); + Assert::AreEqual(size_t(0), p2_phase1.size()); + } + + TEST_METHOD(InstrumentedInput_NonAppFrame_DoesNotAffectInstrumentedInputTime) + { + // Scenario: + // - P0 is a displayed frame with FrameType::Repeated at screenTime = 50'000 and a provider + // input sample at 25'000 ticks. Because it is not an Application frame, it must NOT seed the + // pending provider cache. + // - P1 is the next displayed Application frame (screenTime = 80'000) without its own sample. + // - P2 finalizes P1. + // + // QPC expectation: since no pending sample exists, msInstrumentedInputTime for P1 must be nullopt. + // + // Call pattern: Case 2/3 for both P0 and P1 (since both are displayed frames). + // + // Expectations: + // - After P0 finalization, chain.lastReceivedNotDisplayedAppProviderInputTime remains 0. + // - P1 final metrics leave msInstrumentedInputTime unset. + + QpcConverter qpc(10'000'000, 0); + SwapChainCoreState chain{}; + + const uint64_t ignoredInputTime = 25'000; + + FrameData p0{}; + p0.appInputSample = { ignoredInputTime, InputDeviceType::Mouse }; + p0.finalState = PresentResult::Presented; + p0.displayed.PushBack({ FrameType::Repeated, 50'000 }); + + auto p0_phase1 = ComputeMetricsForPresent(qpc, p0, nullptr, chain); + Assert::AreEqual(size_t(0), p0_phase1.size()); + + FrameData p1{}; + p1.finalState = PresentResult::Presented; + p1.displayed.PushBack({ FrameType::Application, 80'000 }); + + auto p0_final = ComputeMetricsForPresent(qpc, p0, &p1, chain); + Assert::AreEqual(size_t(1), p0_final.size()); + Assert::AreEqual(uint64_t(0), chain.lastReceivedNotDisplayedAppProviderInputTime, + L"Non-app frames should not seed the pending provider input cache."); + + auto p1_phase1 = ComputeMetricsForPresent(qpc, p1, nullptr, chain); + Assert::AreEqual(size_t(0), p1_phase1.size()); + + FrameData p2{}; + p2.finalState = PresentResult::Presented; + p2.displayed.PushBack({ FrameType::Application, 100'000 }); + + auto p1_final = ComputeMetricsForPresent(qpc, p1, &p2, chain); + Assert::AreEqual(size_t(1), p1_final.size()); + const auto& m1 = p1_final[0].metrics; + Assert::IsFalse(m1.msInstrumentedInputTime.has_value(), + L"P1 should not report instrumented input latency because no app-frame pending sample existed."); + + auto p2_phase1 = ComputeMetricsForPresent(qpc, p2, nullptr, chain); + Assert::AreEqual(size_t(0), p2_phase1.size()); + } + }; +} diff --git a/IntelPresentMon/UnitTests/SwapChainTests.cpp b/IntelPresentMon/UnitTests/SwapChainTests.cpp new file mode 100644 index 000000000..088a4b568 --- /dev/null +++ b/IntelPresentMon/UnitTests/SwapChainTests.cpp @@ -0,0 +1,250 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#include "CppUnitTest.h" +#include "../CommonUtilities/mc/SwapChainState.h" +#include "../CommonUtilities/mc/MetricsTypes.h" +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace MetricsCoreTests +{ + TEST_CLASS(SwapChainStateTests) + { + public: + + TEST_METHOD(DefaultConstruction_AllFieldsInitialized) + { + // Test with a simple type to verify default initialization + pmon::util::metrics::SwapChainCoreState swapChain; + + // Verify timing state defaults to 0 + Assert::AreEqual(uint64_t(0), swapChain.lastSimStartTime); + Assert::AreEqual(uint64_t(0), swapChain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(0), swapChain.lastDisplayedScreenTime); + Assert::AreEqual(uint64_t(0), swapChain.lastDisplayedAppScreenTime); + Assert::AreEqual(uint64_t(0), swapChain.firstAppSimStartTime); + + // Verify dropped frame tracking defaults to 0 + Assert::AreEqual(uint64_t(0), swapChain.lastReceivedNotDisplayedAllInputTime); + Assert::AreEqual(uint64_t(0), swapChain.lastReceivedNotDisplayedMouseClickTime); + Assert::AreEqual(uint64_t(0), swapChain.lastReceivedNotDisplayedAppProviderInputTime); + Assert::AreEqual(uint64_t(0), swapChain.lastReceivedNotDisplayedPclSimStart); + Assert::AreEqual(uint64_t(0), swapChain.lastReceivedNotDisplayedPclInputTime); + + // Verify PC Latency accumulation defaults to 0.0 + Assert::AreEqual(0.0, swapChain.accumulatedInput2FrameStartTime); + + // Verify NVIDIA-specific defaults to 0 + Assert::AreEqual(uint64_t(0), swapChain.lastDisplayedFlipDelay); + + // Verify animation error source defaults to CpuStart + Assert::IsTrue(swapChain.animationErrorSource == pmon::util::metrics::AnimationErrorSource::CpuStart); + + // Verify optional presents are empty + Assert::IsFalse(swapChain.lastPresent.has_value()); + Assert::IsFalse(swapChain.lastAppPresent.has_value()); + } + + TEST_METHOD(OptionalPresents_HasValue) + { + pmon::util::metrics::SwapChainCoreState swapChain{}; + + // Initially empty + Assert::IsFalse(swapChain.lastPresent.has_value()); + Assert::IsFalse(swapChain.lastAppPresent.has_value()); + + pmon::util::metrics::FrameData present[2]{}; + present[0].appFrameId = 1; + present[1].appFrameId = 2; + + // Set lastPresent + swapChain.lastPresent = present[0]; + Assert::IsTrue(swapChain.lastPresent.has_value()); + Assert::AreEqual((uint32_t)1, swapChain.lastPresent.value().appFrameId); + Assert::IsFalse(swapChain.lastAppPresent.has_value()); + + // Set lastAppPresent + swapChain.lastAppPresent = present[1]; + Assert::IsTrue(swapChain.lastPresent.has_value()); + Assert::IsTrue(swapChain.lastAppPresent.has_value()); + Assert::AreEqual((uint32_t)1, swapChain.lastPresent.value().appFrameId); + Assert::AreEqual((uint32_t)2, swapChain.lastAppPresent.value().appFrameId); + + // Reset lastPresent + swapChain.lastPresent.reset(); + Assert::IsFalse(swapChain.lastPresent.has_value()); + Assert::IsTrue(swapChain.lastAppPresent.has_value()); + } + + TEST_METHOD(TimingState_AssignmentAndRetrieval) + { + pmon::util::metrics::SwapChainCoreState swapChain{}; + + // Set timing values + swapChain.lastSimStartTime = 1000; + swapChain.lastDisplayedSimStartTime = 2000; + swapChain.lastDisplayedScreenTime = 3000; + swapChain.lastDisplayedAppScreenTime = 4000; + swapChain.firstAppSimStartTime = 5000; + + // Verify retrieval + Assert::AreEqual(uint64_t(1000), swapChain.lastSimStartTime); + Assert::AreEqual(uint64_t(2000), swapChain.lastDisplayedSimStartTime); + Assert::AreEqual(uint64_t(3000), swapChain.lastDisplayedScreenTime); + Assert::AreEqual(uint64_t(4000), swapChain.lastDisplayedAppScreenTime); + Assert::AreEqual(uint64_t(5000), swapChain.firstAppSimStartTime); + } + + TEST_METHOD(DroppedFrameTracking_AssignmentAndRetrieval) + { + pmon::util::metrics::SwapChainCoreState swapChain{}; + + // Set dropped frame tracking values + swapChain.lastReceivedNotDisplayedAllInputTime = 1111; + swapChain.lastReceivedNotDisplayedMouseClickTime = 2222; + swapChain.lastReceivedNotDisplayedAppProviderInputTime = 3333; + swapChain.lastReceivedNotDisplayedPclSimStart = 4444; + swapChain.lastReceivedNotDisplayedPclInputTime = 5555; + + // Verify retrieval + Assert::AreEqual(uint64_t(1111), swapChain.lastReceivedNotDisplayedAllInputTime); + Assert::AreEqual(uint64_t(2222), swapChain.lastReceivedNotDisplayedMouseClickTime); + Assert::AreEqual(uint64_t(3333), swapChain.lastReceivedNotDisplayedAppProviderInputTime); + Assert::AreEqual(uint64_t(4444), swapChain.lastReceivedNotDisplayedPclSimStart); + Assert::AreEqual(uint64_t(5555), swapChain.lastReceivedNotDisplayedPclInputTime); + } + + TEST_METHOD(PCLatencyAccumulation_DoubleType) + { + pmon::util::metrics::SwapChainCoreState swapChain{}; + + // Initially 0.0 + Assert::AreEqual(0.0, swapChain.accumulatedInput2FrameStartTime); + + // Set value + swapChain.accumulatedInput2FrameStartTime = 16.7; + Assert::AreEqual(16.7, swapChain.accumulatedInput2FrameStartTime, 0.001); + + // Accumulate more + swapChain.accumulatedInput2FrameStartTime += 8.3; + Assert::AreEqual(25.0, swapChain.accumulatedInput2FrameStartTime, 0.001); + + // Reset + swapChain.accumulatedInput2FrameStartTime = 0.0; + Assert::AreEqual(0.0, swapChain.accumulatedInput2FrameStartTime); + } + + TEST_METHOD(AnimationErrorSource_EnumAssignment) + { + pmon::util::metrics::SwapChainCoreState swapChain{}; + + // Default is CpuStart + Assert::IsTrue(swapChain.animationErrorSource == pmon::util::metrics::AnimationErrorSource::CpuStart); + + // Change to AppProvider + swapChain.animationErrorSource = pmon::util::metrics::AnimationErrorSource::AppProvider; + Assert::IsTrue(swapChain.animationErrorSource == pmon::util::metrics::AnimationErrorSource::AppProvider); + Assert::IsFalse(swapChain.animationErrorSource == pmon::util::metrics::AnimationErrorSource::CpuStart); + + // Change to PCLatency + swapChain.animationErrorSource = pmon::util::metrics::AnimationErrorSource::PCLatency; + Assert::IsTrue(swapChain.animationErrorSource == pmon::util::metrics::AnimationErrorSource::PCLatency); + Assert::IsFalse(swapChain.animationErrorSource == pmon::util::metrics::AnimationErrorSource::AppProvider); + } + + TEST_METHOD(NvidiaFlipDelay_AssignmentAndRetrieval) + { + pmon::util::metrics::SwapChainCoreState swapChain{}; + + // Default is 0 + Assert::AreEqual(uint64_t(0), swapChain.lastDisplayedFlipDelay); + + // Set value + swapChain.lastDisplayedFlipDelay = 8888; + Assert::AreEqual(uint64_t(8888), swapChain.lastDisplayedFlipDelay); + } + + TEST_METHOD(MultipleFrameDataFields) + { + pmon::util::metrics::SwapChainCoreState swapChain{}; + pmon::util::metrics::FrameData present; + + present.appFrameId = 7777; + present.presentStartTime = 5555; + present.timeInPresent = 2000; + + // Store in core state + swapChain.lastPresent = present; + + // Verify access + Assert::IsTrue(swapChain.lastPresent.has_value()); + Assert::AreEqual(uint32_t(7777), swapChain.lastPresent.value().appFrameId); + Assert::AreEqual(uint64_t(5555), swapChain.lastPresent.value().presentStartTime); + Assert::AreEqual(uint64_t(2000), swapChain.lastPresent.value().timeInPresent); + } + + TEST_METHOD(StateTransitions_SimulateFrameProcessing) + { + pmon::util::metrics::SwapChainCoreState swapChain{}; + + // Frame 1: First frame received + pmon::util::metrics::FrameData presentOne; + + presentOne.presentStartTime = 1000; + presentOne.appFrameId = 1; + swapChain.lastPresent = presentOne; + swapChain.lastSimStartTime = 1000; + Assert::AreEqual(true, swapChain.lastPresent.has_value()); + + // Frame 2: Next frame received + pmon::util::metrics::FrameData presentTwo; + + int frame2 = 2000; + swapChain.lastPresent = presentTwo; + swapChain.lastSimStartTime = 2000; + + // Frame 2 displayed: Update display state + swapChain.lastDisplayedSimStartTime = 2000; + swapChain.lastDisplayedScreenTime = 2016; // +16ms latency + swapChain.lastDisplayedAppScreenTime = 2016; + Assert::AreEqual(uint64_t(2016), swapChain.lastDisplayedScreenTime); + + // Frame 3 + pmon::util::metrics::FrameData presentThree; + swapChain.lastPresent = presentThree; + swapChain.lastReceivedNotDisplayedAllInputTime = 2990; + Assert::AreEqual(uint64_t(2990), swapChain.lastReceivedNotDisplayedAllInputTime); + } + + TEST_METHOD(CopySemantics_Independent) + { + // Verify that copies are independent (important for value types) + pmon::util::metrics::SwapChainCoreState swapChainOne{}; + pmon::util::metrics::FrameData presents[3]; + + // Set state in core1 + swapChainOne.lastSimStartTime = 1234; + swapChainOne.lastPresent = presents[1]; + swapChainOne.accumulatedInput2FrameStartTime = 16.7; + + pmon::util::metrics::SwapChainCoreState swapChainTwo{}; + + // SwapChainOne to SwapChainTwo + swapChainTwo = swapChainOne; + + // Verify core2 has same values + Assert::AreEqual(uint64_t(1234), swapChainTwo.lastSimStartTime); + //Assert::AreEqual(presents[1], *swapChainTwo.lastPresent); + Assert::AreEqual(16.7, swapChainTwo.accumulatedInput2FrameStartTime, 0.001); + + // Modify SwapChainTwo + swapChainTwo.lastSimStartTime = 5678; + swapChainTwo.lastPresent = presents[2]; + + // Verify core1 is unchanged + Assert::AreEqual(uint64_t(1234), swapChainOne.lastSimStartTime); + //Assert::AreEqual(presents[1], *swapChainOne.lastPresent); + } + }; +} diff --git a/IntelPresentMon/UnitTests/UnitTests.vcxproj b/IntelPresentMon/UnitTests/UnitTests.vcxproj index e7578f2e1..d98943080 100644 --- a/IntelPresentMon/UnitTests/UnitTests.vcxproj +++ b/IntelPresentMon/UnitTests/UnitTests.vcxproj @@ -1,4 +1,4 @@ - + @@ -101,6 +101,9 @@ + + + @@ -115,4 +118,4 @@ - \ No newline at end of file + diff --git a/IntelPresentMon/UnitTests/UnitTests.vcxproj.filters b/IntelPresentMon/UnitTests/UnitTests.vcxproj.filters index e9ae29686..2988db508 100644 --- a/IntelPresentMon/UnitTests/UnitTests.vcxproj.filters +++ b/IntelPresentMon/UnitTests/UnitTests.vcxproj.filters @@ -3,6 +3,9 @@ + + + - \ No newline at end of file + diff --git a/IntelPresentMon/metrics.csv b/IntelPresentMon/metrics.csv index 7b1cc0b37..4c109e472 100644 --- a/IntelPresentMon/metrics.csv +++ b/IntelPresentMon/metrics.csv @@ -104,3 +104,6 @@ PM_METRIC_DISPLAYED_FRAME_TIME,1,FrameTime-Display,"The time between when the pr PM_METRIC_PRESENTED_FRAME_TIME,1,FrameTime-Presents,"The time between this Present call and the previous one, in milliseconds." PM_METRIC_BETWEEN_APP_START,,Ms Between App Start,"How long it took from the start of this frame until the CPU started working on the next frame, in milliseconds." PM_METRIC_FLIP_DELAY,,Ms Flip Delay,"Delay added to when the Present() was displayed." +PM_METRIC_SESSION_START_QPC,1,Session Start QPC,"QPC of the start of the ETW trace session, derived from the first received event timestamp." +,,, +PM_METRIC_COUNT_,1,Metric Count,"Sentinel value for enumeration/reflection only. Not an actual metric for use in queries." diff --git a/PresentData/ConsoleAdapter.h b/PresentData/ConsoleAdapter.h new file mode 100644 index 000000000..a833d7e6a --- /dev/null +++ b/PresentData/ConsoleAdapter.h @@ -0,0 +1,69 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: MIT +#pragma once +#include +#include +#include +#include "PresentMonTraceConsumer.hpp" + +namespace pmon::util::metrics { + // Zero-cost wrapper around Console's shared_ptr + // Provides same interface as PresentSnapshot via inline getters + class ConsoleAdapter { + const PresentEvent* ptr_; + + public: + // Constructors + explicit ConsoleAdapter(const std::shared_ptr& p) : ptr_(p.get()) {} + explicit ConsoleAdapter(const PresentEvent* p) : ptr_(p) {} + + // Inline getters - compile to direct field access (zero overhead) + uint64_t getPresentStartTime() const { return ptr_->PresentStartTime; } + uint64_t getReadyTime() const { return ptr_->ReadyTime; } + uint64_t getTimeInPresent() const { return ptr_->TimeInPresent; } + uint64_t getGPUStartTime() const { return ptr_->GPUStartTime; } + uint64_t getGPUDuration() const { return ptr_->GPUDuration; } + uint64_t getGPUVideoDuration() const { return ptr_->GPUVideoDuration; } + + // Propagated data + uint64_t getAppPropagatedPresentStartTime() const { return ptr_->AppPropagatedPresentStartTime; } + uint64_t getAppPropagatedTimeInPresent() const { return ptr_->AppPropagatedTimeInPresent; } + uint64_t getAppPropagatedGPUStartTime() const { return ptr_->AppPropagatedGPUStartTime; } + uint64_t getAppPropagatedReadyTime() const { return ptr_->AppPropagatedReadyTime; } + uint64_t getAppPropagatedGPUDuration() const { return ptr_->AppPropagatedGPUDuration; } + uint64_t getAppPropagatedGPUVideoDuration() const { return ptr_->AppPropagatedGPUVideoDuration; } + + // Instrumented data + uint64_t getAppSimStartTime() const { return ptr_->AppSimStartTime; } + uint64_t getAppSleepStartTime() const { return ptr_->AppSleepStartTime; } + uint64_t getAppSleepEndTime() const { return ptr_->AppSleepEndTime; } + uint64_t getAppRenderSubmitStartTime() const { return ptr_->AppRenderSubmitStartTime; } + std::pair getAppInputSample() const { return ptr_->AppInputSample; } + + // PC Latency + uint64_t getPclSimStartTime() const { return ptr_->PclSimStartTime; } + uint64_t getPclInputPingTime() const { return ptr_->PclInputPingTime; } + + // Input tracking + uint64_t getInputTime() const { return ptr_->InputTime; } + uint64_t getMouseClickTime() const { return ptr_->MouseClickTime; } + + // Display data - normalized access + size_t getDisplayedCount() const { return ptr_->Displayed.size(); } + FrameType getDisplayedFrameType(size_t idx) const { return ptr_->Displayed[idx].first; } + uint64_t getDisplayedScreenTime(size_t idx) const { return ptr_->Displayed[idx].second; } + + // Vendor-specific + uint64_t getFlipDelay() const { return ptr_->FlipDelay; } + + // Metadata + PresentResult getFinalState() const { return ptr_->FinalState; } + uint32_t getProcessId() const { return ptr_->ProcessId; } + uint64_t getSwapChainAddress() const { return ptr_->SwapChainAddress; } + + // Predicates + bool hasAppPropagatedData() const { return ptr_->AppPropagatedPresentStartTime != 0; } + bool hasPclSimStartTime() const { return ptr_->PclSimStartTime != 0; } + bool hasPclInputPingTime() const { return ptr_->PclInputPingTime != 0; } + }; +} \ No newline at end of file diff --git a/PresentData/PresentData.vcxproj b/PresentData/PresentData.vcxproj index 4c444ac52..ef1430651 100644 --- a/PresentData/PresentData.vcxproj +++ b/PresentData/PresentData.vcxproj @@ -342,6 +342,7 @@ + @@ -359,6 +360,7 @@ + diff --git a/PresentData/PresentData.vcxproj.filters b/PresentData/PresentData.vcxproj.filters index 262423f17..df266457d 100644 --- a/PresentData/PresentData.vcxproj.filters +++ b/PresentData/PresentData.vcxproj.filters @@ -48,6 +48,8 @@ + + diff --git a/PresentData/PresentEventEnums.hpp b/PresentData/PresentEventEnums.hpp new file mode 100644 index 000000000..ff808f590 --- /dev/null +++ b/PresentData/PresentEventEnums.hpp @@ -0,0 +1,43 @@ +// Copyright (C) 2017-2024 Intel Corporation +// Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved +// SPDX-License-Identifier: MIT +#pragma once + +enum class PresentMode { + Unknown = 0, + Hardware_Legacy_Flip = 1, + Hardware_Legacy_Copy_To_Front_Buffer = 2, + Hardware_Independent_Flip = 3, + Composed_Flip = 4, + Composed_Copy_GPU_GDI = 5, + Composed_Copy_CPU_GDI = 6, + Hardware_Composed_Independent_Flip = 8, +}; + +enum class PresentResult { + Unknown = 0, + Presented = 1, + Discarded = 2, +}; + +enum class Runtime { + Other = 0, + DXGI = 1, + D3D9 = 2, +}; + +enum class InputDeviceType { + None = 0, + Unknown = 1, + Mouse = 2, + Keyboard = 3, +}; + +enum class FrameType { + NotSet = 0, + Unspecified = 1, + Application = 2, + Repeated = 3, + Intel_XEFG = 50, + AMD_AFMF = 100, +}; \ No newline at end of file diff --git a/PresentData/PresentMonTraceConsumer.hpp b/PresentData/PresentMonTraceConsumer.hpp index 812486e0d..6d8aec977 100644 --- a/PresentData/PresentMonTraceConsumer.hpp +++ b/PresentData/PresentMonTraceConsumer.hpp @@ -27,6 +27,7 @@ #include "GpuTrace.hpp" #include "TraceConsumer.hpp" #include "NvidiaTraceConsumer.hpp" +#include "PresentEventEnums.hpp" #include "../IntelPresentMon/CommonUtilities/Hash.h" // PresentMode represents the different paths a present can take on windows. @@ -92,45 +93,6 @@ // -> DxgKrnl_PresentHistory_Info (by token ptr) // -> Assume DWM will compose this buffer on next present (missing InFrame event), follow windowed blit paths to screen time -enum class PresentMode { - Unknown = 0, - Hardware_Legacy_Flip = 1, - Hardware_Legacy_Copy_To_Front_Buffer = 2, - Hardware_Independent_Flip = 3, - Composed_Flip = 4, - Composed_Copy_GPU_GDI = 5, - Composed_Copy_CPU_GDI = 6, - Hardware_Composed_Independent_Flip = 8, -}; - -enum class PresentResult { - Unknown = 0, - Presented = 1, - Discarded = 2, -}; - -enum class Runtime { - Other = 0, - DXGI = 1, - D3D9 = 2, -}; - -enum class InputDeviceType { - None = 0, - Unknown = 1, - Mouse = 2, - Keyboard = 3, -}; - -enum class FrameType { - NotSet = 0, - Unspecified = 1, - Application = 2, - Repeated = 3, - Intel_XEFG = 50, - AMD_AFMF = 100, -}; - struct InputData { uint64_t Time; uint64_t MouseClickTime; diff --git a/PresentMon.sln b/PresentMon.sln index 6278dcce2..8ae90fc68 100644 --- a/PresentMon.sln +++ b/PresentMon.sln @@ -7,16 +7,12 @@ Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "PMInstaller", "IntelPresent {0F60DFD9-208E-443E-8D01-43C902B458A6} = {0F60DFD9-208E-443E-8D01-43C902B458A6} {3C39C9BC-0E85-42C0-894C-3561BB93E87F} = {3C39C9BC-0E85-42C0-894C-3561BB93E87F} {4EB9794B-1F12-48CE-ADC1-917E9810F29E} = {4EB9794B-1F12-48CE-ADC1-917E9810F29E} - {57ABAF22-837A-4DA3-9C92-1BA37CCFA550} = {57ABAF22-837A-4DA3-9C92-1BA37CCFA550} - {66E9F6C5-28DB-4218-81B9-31E0E146ECC0} = {66E9F6C5-28DB-4218-81B9-31E0E146ECC0} {79FE382A-C29C-4C32-A028-B8D1ABDF51B0} = {79FE382A-C29C-4C32-A028-B8D1ABDF51B0} {7A1C7F0B-ECB3-4C98-B74E-E5BBA63BA4A7} = {7A1C7F0B-ECB3-4C98-B74E-E5BBA63BA4A7} {807370FB-ADE0-403A-A278-DF50893E5F94} = {807370FB-ADE0-403A-A278-DF50893E5F94} {808F5EA9-EA09-4D72-87B4-5397D43CBA54} = {808F5EA9-EA09-4D72-87B4-5397D43CBA54} {892028E5-32F6-45FC-8AB2-90FCBCAC4BF6} = {892028E5-32F6-45FC-8AB2-90FCBCAC4BF6} {9E3C74BB-12BB-40AC-86BF-818D20140CBA} = {9E3C74BB-12BB-40AC-86BF-818D20140CBA} - {BF43064B-01F0-4C69-91FB-C2122BAF621D} = {BF43064B-01F0-4C69-91FB-C2122BAF621D} - {F7D9E1CD-298D-465A-82D2-8778C860BC46} = {F7D9E1CD-298D-465A-82D2-8778C860BC46} EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PMInstallerExtension", "IntelPresentMon\PMInstallerExtension\PMInstallerExtension.csproj", "{9E3C74BB-12BB-40AC-86BF-818D20140CBA}" @@ -27,12 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SDK", "SDK", "{6DCB803B-9FC EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PresentMonService", "IntelPresentMon\PresentMonService\PresentMonService.vcxproj", "{3A848E5B-A376-4A22-BBAC-E9B3C01BD385}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PresentMonUtils", "IntelPresentMon\PresentMonUtils\PresentMonUtils.vcxproj", "{66E9F6C5-28DB-4218-81B9-31E0E146ECC0}" -EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ControlLib", "IntelPresentMon\ControlLib\ControlLib.vcxproj", "{3C39C9BC-0E85-42C0-894C-3561BB93E87F}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Streamer", "IntelPresentMon\Streamer\Streamer.vcxproj", "{BF43064B-01F0-4C69-91FB-C2122BAF621D}" -EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PresentData", "PresentData\PresentData.vcxproj", "{892028E5-32F6-45FC-8AB2-90FCBCAC4BF6}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Core", "IntelPresentMon\Core\Core.vcxproj", "{808F5EA9-EA09-4D72-87B4-5397D43CBA54}" @@ -43,8 +35,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{B60F EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SampleClient", "IntelPresentMon\SampleClient\SampleClient.vcxproj", "{79FE382A-C29C-4C32-A028-B8D1ABDF51B0}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SampleStreamerClient", "IntelPresentMon\SampleStreamerClient\SampleStreamerClient.vcxproj", "{57ABAF22-837A-4DA3-9C92-1BA37CCFA550}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PresentMon", "PresentMon", "{9FFA4649-52C4-4492-83DF-2F417C8E3819}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "p2c", "p2c", "{0015EC44-0BF0-4F05-80CF-72000771F6EB}" @@ -62,8 +52,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Shaders", "IntelPresentMon\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Service", "Service", "{86FF3CD6-7065-40A5-B9D3-FAC961D2AAE0}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PresentMonULT", "IntelPresentMon\ULT\ULT.vcxproj", "{F7D9E1CD-298D-465A-82D2-8778C860BC46}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{B4CC5828-9638-42EC-A692-E81E8227DD84}" ProjectSection(SolutionItems) = preProject .gitignore = .gitignore @@ -165,17 +153,6 @@ Global {3A848E5B-A376-4A22-BBAC-E9B3C01BD385}.Release-EDSS|x86.ActiveCfg = Release|x64 {3A848E5B-A376-4A22-BBAC-E9B3C01BD385}.Release-EDSS-MSI|x64.ActiveCfg = Release|x64 {3A848E5B-A376-4A22-BBAC-E9B3C01BD385}.Release-EDSS-MSI|x86.ActiveCfg = Release|x64 - {66E9F6C5-28DB-4218-81B9-31E0E146ECC0}.Debug|x64.ActiveCfg = Debug|x64 - {66E9F6C5-28DB-4218-81B9-31E0E146ECC0}.Debug|x64.Build.0 = Debug|x64 - {66E9F6C5-28DB-4218-81B9-31E0E146ECC0}.Debug|x86.ActiveCfg = Debug|x64 - {66E9F6C5-28DB-4218-81B9-31E0E146ECC0}.Release|x64.ActiveCfg = Release|x64 - {66E9F6C5-28DB-4218-81B9-31E0E146ECC0}.Release|x64.Build.0 = Release|x64 - {66E9F6C5-28DB-4218-81B9-31E0E146ECC0}.Release|x86.ActiveCfg = Release|x64 - {66E9F6C5-28DB-4218-81B9-31E0E146ECC0}.Release-EDSS|x64.ActiveCfg = Release|x64 - {66E9F6C5-28DB-4218-81B9-31E0E146ECC0}.Release-EDSS|x64.Build.0 = Release|x64 - {66E9F6C5-28DB-4218-81B9-31E0E146ECC0}.Release-EDSS|x86.ActiveCfg = Release|x64 - {66E9F6C5-28DB-4218-81B9-31E0E146ECC0}.Release-EDSS-MSI|x64.ActiveCfg = Release|x64 - {66E9F6C5-28DB-4218-81B9-31E0E146ECC0}.Release-EDSS-MSI|x86.ActiveCfg = Release|x64 {3C39C9BC-0E85-42C0-894C-3561BB93E87F}.Debug|x64.ActiveCfg = Debug|x64 {3C39C9BC-0E85-42C0-894C-3561BB93E87F}.Debug|x64.Build.0 = Debug|x64 {3C39C9BC-0E85-42C0-894C-3561BB93E87F}.Debug|x86.ActiveCfg = Debug|x64 @@ -187,17 +164,6 @@ Global {3C39C9BC-0E85-42C0-894C-3561BB93E87F}.Release-EDSS|x86.ActiveCfg = Release|x64 {3C39C9BC-0E85-42C0-894C-3561BB93E87F}.Release-EDSS-MSI|x64.ActiveCfg = Release|x64 {3C39C9BC-0E85-42C0-894C-3561BB93E87F}.Release-EDSS-MSI|x86.ActiveCfg = Release|x64 - {BF43064B-01F0-4C69-91FB-C2122BAF621D}.Debug|x64.ActiveCfg = Debug|x64 - {BF43064B-01F0-4C69-91FB-C2122BAF621D}.Debug|x64.Build.0 = Debug|x64 - {BF43064B-01F0-4C69-91FB-C2122BAF621D}.Debug|x86.ActiveCfg = Debug|x64 - {BF43064B-01F0-4C69-91FB-C2122BAF621D}.Release|x64.ActiveCfg = Release|x64 - {BF43064B-01F0-4C69-91FB-C2122BAF621D}.Release|x64.Build.0 = Release|x64 - {BF43064B-01F0-4C69-91FB-C2122BAF621D}.Release|x86.ActiveCfg = Release|x64 - {BF43064B-01F0-4C69-91FB-C2122BAF621D}.Release-EDSS|x64.ActiveCfg = Release|x64 - {BF43064B-01F0-4C69-91FB-C2122BAF621D}.Release-EDSS|x64.Build.0 = Release|x64 - {BF43064B-01F0-4C69-91FB-C2122BAF621D}.Release-EDSS|x86.ActiveCfg = Release|x64 - {BF43064B-01F0-4C69-91FB-C2122BAF621D}.Release-EDSS-MSI|x64.ActiveCfg = Release|x64 - {BF43064B-01F0-4C69-91FB-C2122BAF621D}.Release-EDSS-MSI|x86.ActiveCfg = Release|x64 {892028E5-32F6-45FC-8AB2-90FCBCAC4BF6}.Debug|x64.ActiveCfg = Debug|x64 {892028E5-32F6-45FC-8AB2-90FCBCAC4BF6}.Debug|x64.Build.0 = Debug|x64 {892028E5-32F6-45FC-8AB2-90FCBCAC4BF6}.Debug|x86.ActiveCfg = Debug|Win32 @@ -233,16 +199,6 @@ Global {79FE382A-C29C-4C32-A028-B8D1ABDF51B0}.Release-EDSS|x86.ActiveCfg = Release|x64 {79FE382A-C29C-4C32-A028-B8D1ABDF51B0}.Release-EDSS-MSI|x64.ActiveCfg = Release|x64 {79FE382A-C29C-4C32-A028-B8D1ABDF51B0}.Release-EDSS-MSI|x86.ActiveCfg = Release|x64 - {57ABAF22-837A-4DA3-9C92-1BA37CCFA550}.Debug|x64.ActiveCfg = Debug|x64 - {57ABAF22-837A-4DA3-9C92-1BA37CCFA550}.Debug|x64.Build.0 = Debug|x64 - {57ABAF22-837A-4DA3-9C92-1BA37CCFA550}.Debug|x86.ActiveCfg = Debug|x64 - {57ABAF22-837A-4DA3-9C92-1BA37CCFA550}.Release|x64.ActiveCfg = Release|x64 - {57ABAF22-837A-4DA3-9C92-1BA37CCFA550}.Release|x64.Build.0 = Release|x64 - {57ABAF22-837A-4DA3-9C92-1BA37CCFA550}.Release|x86.ActiveCfg = Release|x64 - {57ABAF22-837A-4DA3-9C92-1BA37CCFA550}.Release-EDSS|x64.ActiveCfg = Release|x64 - {57ABAF22-837A-4DA3-9C92-1BA37CCFA550}.Release-EDSS|x86.ActiveCfg = Release|x64 - {57ABAF22-837A-4DA3-9C92-1BA37CCFA550}.Release-EDSS-MSI|x64.ActiveCfg = Release|x64 - {57ABAF22-837A-4DA3-9C92-1BA37CCFA550}.Release-EDSS-MSI|x86.ActiveCfg = Release|x64 {4EB9794B-1F12-48CE-ADC1-917E9810F29E}.Debug|x64.ActiveCfg = Debug|x64 {4EB9794B-1F12-48CE-ADC1-917E9810F29E}.Debug|x64.Build.0 = Debug|x64 {4EB9794B-1F12-48CE-ADC1-917E9810F29E}.Debug|x86.ActiveCfg = Debug|Win32 @@ -279,14 +235,6 @@ Global {0F60DFD9-208E-443E-8D01-43C902B458A6}.Release-EDSS|x86.ActiveCfg = Release|Win32 {0F60DFD9-208E-443E-8D01-43C902B458A6}.Release-EDSS-MSI|x64.ActiveCfg = Release|x64 {0F60DFD9-208E-443E-8D01-43C902B458A6}.Release-EDSS-MSI|x86.ActiveCfg = Release|Win32 - {F7D9E1CD-298D-465A-82D2-8778C860BC46}.Debug|x64.ActiveCfg = Debug|x64 - {F7D9E1CD-298D-465A-82D2-8778C860BC46}.Debug|x86.ActiveCfg = Debug|x64 - {F7D9E1CD-298D-465A-82D2-8778C860BC46}.Release|x64.ActiveCfg = Release|x64 - {F7D9E1CD-298D-465A-82D2-8778C860BC46}.Release|x86.ActiveCfg = Release|x64 - {F7D9E1CD-298D-465A-82D2-8778C860BC46}.Release-EDSS|x64.ActiveCfg = Release|x64 - {F7D9E1CD-298D-465A-82D2-8778C860BC46}.Release-EDSS|x86.ActiveCfg = Release|x64 - {F7D9E1CD-298D-465A-82D2-8778C860BC46}.Release-EDSS-MSI|x64.ActiveCfg = Release|x64 - {F7D9E1CD-298D-465A-82D2-8778C860BC46}.Release-EDSS-MSI|x86.ActiveCfg = Release|x64 {5DDBA061-53A0-4835-8AAF-943F403F924F}.Debug|x64.ActiveCfg = Debug|x64 {5DDBA061-53A0-4835-8AAF-943F403F924F}.Debug|x64.Build.0 = Debug|x64 {5DDBA061-53A0-4835-8AAF-943F403F924F}.Debug|x86.ActiveCfg = Debug|x64 @@ -490,19 +438,15 @@ Global {9E3C74BB-12BB-40AC-86BF-818D20140CBA} = {61877103-313D-4140-8449-834D5D73C72E} {3C80B001-E165-4E0E-A92D-5767CF4FB1F8} = {0015EC44-0BF0-4F05-80CF-72000771F6EB} {3A848E5B-A376-4A22-BBAC-E9B3C01BD385} = {86FF3CD6-7065-40A5-B9D3-FAC961D2AAE0} - {66E9F6C5-28DB-4218-81B9-31E0E146ECC0} = {B4CC5828-9638-42EC-A692-E81E8227DD84} {3C39C9BC-0E85-42C0-894C-3561BB93E87F} = {86FF3CD6-7065-40A5-B9D3-FAC961D2AAE0} - {BF43064B-01F0-4C69-91FB-C2122BAF621D} = {B4CC5828-9638-42EC-A692-E81E8227DD84} {892028E5-32F6-45FC-8AB2-90FCBCAC4BF6} = {9FFA4649-52C4-4492-83DF-2F417C8E3819} {808F5EA9-EA09-4D72-87B4-5397D43CBA54} = {0015EC44-0BF0-4F05-80CF-72000771F6EB} {61877103-313D-4140-8449-834D5D73C72E} = {B4CC5828-9638-42EC-A692-E81E8227DD84} {79FE382A-C29C-4C32-A028-B8D1ABDF51B0} = {B60F7D25-F7B1-4D91-AC59-34AE02E7E1C5} - {57ABAF22-837A-4DA3-9C92-1BA37CCFA550} = {B60F7D25-F7B1-4D91-AC59-34AE02E7E1C5} {4EB9794B-1F12-48CE-ADC1-917E9810F29E} = {9FFA4649-52C4-4492-83DF-2F417C8E3819} {7A1C7F0B-ECB3-4C98-B74E-E5BBA63BA4A7} = {0015EC44-0BF0-4F05-80CF-72000771F6EB} {0F60DFD9-208E-443E-8D01-43C902B458A6} = {9FFA4649-52C4-4492-83DF-2F417C8E3819} {51979337-0180-48BD-BAD9-8AEF57FEF96D} = {0015EC44-0BF0-4F05-80CF-72000771F6EB} - {F7D9E1CD-298D-465A-82D2-8778C860BC46} = {86FF3CD6-7065-40A5-B9D3-FAC961D2AAE0} {5DDBA061-53A0-4835-8AAF-943F403F924F} = {6DCB803B-9FCE-456C-87B9-1365F59BD190} {BE924A9E-AE24-42A6-9C7C-EABC506A33C9} = {6DCB803B-9FCE-456C-87B9-1365F59BD190} {34B60AAC-4646-4AA8-A267-9A5DD7C097D5} = {6DCB803B-9FCE-456C-87B9-1365F59BD190} diff --git a/PresentMon/Console.cpp b/PresentMon/Console.cpp index 7ff8f2af4..13bc0a09e 100644 --- a/PresentMon/Console.cpp +++ b/PresentMon/Console.cpp @@ -273,28 +273,28 @@ void UpdateConsole(uint32_t processId, ProcessInfo const& processInfo) ConsolePrint(L" %016llX", address); - if (chain.mLastPresent != nullptr) { + if (chain.mUnifiedSwapChain.swapChain.lastPresent.has_value()) { ConsolePrint(L" (%hs): SyncInterval=%d Flags=%d CPU=%.3fms (%.1f fps)", - RuntimeToString(chain.mLastPresent->Runtime), - chain.mLastPresent->SyncInterval, - chain.mLastPresent->PresentFlags, - chain.mAvgCPUDuration, - CalculateFPSForPrintf(chain.mAvgCPUDuration)); + RuntimeToString(chain.mUnifiedSwapChain.swapChain.lastPresent.value().runtime), + chain.mUnifiedSwapChain.swapChain.lastPresent.value().syncInterval, + chain.mUnifiedSwapChain.swapChain.lastPresent.value().presentFlags, + chain.mUnifiedSwapChain.avgCPUDuration, + CalculateFPSForPrintf(chain.mUnifiedSwapChain.avgCPUDuration)); if (args.mTrackDisplay) { ConsolePrint(L" Display=%.3fms (%.1f fps)", - chain.mAvgDisplayedTime, - CalculateFPSForPrintf(chain.mAvgDisplayedTime)); + chain.mUnifiedSwapChain.avgDisplayedTime, + CalculateFPSForPrintf(chain.mUnifiedSwapChain.avgDisplayedTime)); } if (args.mTrackGPU) { - ConsolePrint(L" GPU=%.3fms", chain.mAvgGPUDuration); + ConsolePrint(L" GPU=%.3fms", chain.mUnifiedSwapChain.avgGPUDuration); } if (args.mTrackDisplay) { ConsolePrint(L" Latency=%.3fms %hs", - chain.mAvgDisplayLatency, - PresentModeToString(chain.mLastPresent->PresentMode)); + chain.mUnifiedSwapChain.avgDisplayLatency, + PresentModeToString(chain.mUnifiedSwapChain.swapChain.lastPresent.value().presentMode)); } } diff --git a/PresentMon/CsvOutput.cpp b/PresentMon/CsvOutput.cpp index a3ca7c184..329898da2 100644 --- a/PresentMon/CsvOutput.cpp +++ b/PresentMon/CsvOutput.cpp @@ -137,6 +137,9 @@ void WriteCsvHeader(FILE* fp); template void WriteCsvRow(FILE* fp, PMTraceSession const& pmSession, ProcessInfo const& processInfo, PresentEvent const& p, FrameMetricsT const& metrics); +template +void WriteCsvRow(FILE* fp, PMTraceSession const& pmSession, ProcessInfo const& processInfo, pmon::util::metrics::FrameData const& p, FrameMetricsT const& metrics); + template<> void WriteCsvHeader(FILE* fp) { @@ -281,6 +284,86 @@ void WriteCsvRow( } } +template<> +void WriteCsvRow( + FILE* fp, + PMTraceSession const& pmSession, + ProcessInfo const& processInfo, + pmon::util::metrics::FrameData const& p, + FrameMetrics1 const& metrics) +{ + auto const& args = GetCommandLineArgs(); + + fwprintf(fp, L"%s,%d,0x%016llX,%hs,%d,%d,%hs", processInfo.mModuleName.c_str(), + p.processId, + p.swapChainAddress, + RuntimeToString(p.runtime), + p.syncInterval, + p.presentFlags, + FinalStateToDroppedString(p.finalState)); + switch (args.mTimeUnit) { + case TimeUnit::DateTime: { + SYSTEMTIME st = {}; + uint64_t ns = 0; + pmSession.TimestampToLocalSystemTime(p.presentStartTime, &st, &ns); + fwprintf(fp, L",%u-%u-%u %u:%02u:%02u.%09llu", st.wYear, + st.wMonth, + st.wDay, + st.wHour, + st.wMinute, + st.wSecond, + ns); + } break; + default: + fwprintf(fp, L",%.*lf", DBL_DIG - 1, 0.001 * pmSession.TimestampToMilliSeconds(p.presentStartTime)); + break; + } + fwprintf(fp, L",%.*lf,%.*lf", DBL_DIG - 1, metrics.msInPresentApi, + DBL_DIG - 1, metrics.msBetweenPresents); + if (args.mTrackDisplay) { + fwprintf(fp, L",%d,%hs,%.*lf,%.*lf,%.*lf,%.*lf", p.supportsTearing, + PresentModeToString(p.presentMode), + DBL_DIG - 1, metrics.msUntilRenderComplete, + DBL_DIG - 1, metrics.msUntilDisplayed, + DBL_DIG - 1, metrics.msBetweenDisplayChange, + DBL_DIG - 1, metrics.msFlipDelay); + } + if (args.mTrackGPU) { + fwprintf(fp, L",%.*lf,%.*lf", DBL_DIG - 1, metrics.msUntilRenderStart, + DBL_DIG - 1, metrics.msGPUDuration); + } + if (args.mTrackGPUVideo) { + fwprintf(fp, L",%.*lf", DBL_DIG - 1, metrics.msVideoDuration); + } + if (args.mTrackInput) { + fwprintf(fp, L",%.*lf", DBL_DIG - 1, metrics.msSinceInput); + } + switch (args.mTimeUnit) { + case TimeUnit::QPC: + fwprintf(fp, L",%llu", p.presentStartTime); + break; + case TimeUnit::QPCMilliSeconds: + fwprintf(fp, L",%.*lf", DBL_DIG - 1, 0.001 * pmSession.TimestampDeltaToMilliSeconds(p.presentStartTime)); + break; + } + if (args.mWriteDisplayTime) { + if (metrics.qpcScreenTime == 0) { + fwprintf(fp, L",NA"); + } + else { + fwprintf(fp, L",%.*lf", DBL_DIG - 1, 0.001 * pmSession.TimestampToMilliSeconds(metrics.qpcScreenTime)); + } + } + if (args.mWriteFrameId) { + fwprintf(fp, L",%u", p.frameId); + } + fwprintf(fp, L"\n"); + + if (args.mCSVOutput == CSVOutput::Stdout) { + fflush(fp); + } +} + template<> void WriteCsvHeader(FILE* fp) { @@ -422,6 +505,141 @@ void WriteCsvHeader(FILE* fp) } } +template<> +void WriteCsvHeader(FILE* fp) +{ + auto const& args = GetCommandLineArgs(); + + fwprintf(fp, L"Application" + L",ProcessID" + L",SwapChainAddress" + L",PresentRuntime" + L",SyncInterval" + L",PresentFlags"); + if (args.mTrackDisplay) { + fwprintf(fp, L",AllowsTearing" + L",PresentMode"); + } + if (args.mTrackFrameType) { + fwprintf(fp, L",FrameType"); + } + if (args.mTrackHybridPresent) { + fwprintf(fp, L",HybridPresent"); + } + if (args.mUseV2Metrics == false) { + switch (args.mTimeUnit) { + case TimeUnit::MilliSeconds: fwprintf(fp, L",TimeInMs"); break; + case TimeUnit::QPC: fwprintf(fp, L",TimeInQPC"); break; + case TimeUnit::DateTime: fwprintf(fp, L",TimeInDateTime"); break; + default: fwprintf(fp, L",TimeInSeconds"); break; + } + + fwprintf(fp, L",MsBetweenSimulationStart" + L",MsBetweenPresents"); + + if (args.mTrackDisplay) { + fwprintf(fp, L",MsBetweenDisplayChange"); + } + + fwprintf(fp, L",MsInPresentAPI" + L",MsRenderPresentLatency"); + + if (args.mTrackDisplay) { + fwprintf(fp, L",MsUntilDisplayed"); + if (args.mTrackPcLatency) { + fwprintf(fp, L",MsPCLatency"); + } + } + } + + if (args.mUseV2Metrics) { + switch (args.mTimeUnit) { + case TimeUnit::MilliSeconds: fwprintf(fp, L",CPUStartTime"); break; + case TimeUnit::QPC: fwprintf(fp, L",CPUStartQPC"); break; + case TimeUnit::QPCMilliSeconds: fwprintf(fp, L",CPUStartQPCTime"); break; + case TimeUnit::DateTime: fwprintf(fp, L",CPUStartDateTime"); break; + default: fwprintf(fp, L",CPUStartTime"); break; + } + fwprintf(fp, L",FrameTime" + L",CPUBusy" + L",CPUWait"); + if (args.mTrackGPU) { + fwprintf(fp, L",GPULatency" + L",GPUTime" + L",GPUBusy" + L",GPUWait"); + } + if (args.mTrackGPUVideo) { + fwprintf(fp, L",VideoBusy"); + } + if (args.mTrackDisplay) { + fwprintf(fp, + L",DisplayLatency" + L",DisplayedTime" + L",AnimationError" + L",AnimationTime" + L",MsFlipDelay"); + } + if (args.mTrackInput) { + fwprintf(fp, L",AllInputToPhotonLatency"); + fwprintf(fp, L",ClickToPhotonLatency"); + } + if (args.mTrackAppTiming) { + fwprintf(fp, L",InstrumentedLatency"); + } + } + else { + switch (args.mTimeUnit) { + case TimeUnit::MilliSeconds: fwprintf(fp, L",CPUStartTimeInMs"); break; + case TimeUnit::QPC: fwprintf(fp, L",CPUStartQPC"); break; + case TimeUnit::QPCMilliSeconds: fwprintf(fp, L",CPUStartQPCTimeInMs"); break; + case TimeUnit::DateTime: fwprintf(fp, L",CPUStartDateTime"); break; + default: fwprintf(fp, L",CPUStartTimeInSeconds"); break; + } + fwprintf(fp, L",MsBetweenAppStart" + L",MsCPUBusy" + L",MsCPUWait"); + if (args.mTrackGPU) { + fwprintf(fp, L",MsGPULatency" + L",MsGPUTime" + L",MsGPUBusy" + L",MsGPUWait"); + } + if (args.mTrackGPUVideo) { + fwprintf(fp, L",MsVideoBusy"); + } + if (args.mTrackDisplay) { + fwprintf(fp, L",MsAnimationError" + L",AnimationTime" + L",MsFlipDelay"); + } + if (args.mTrackInput) { + fwprintf(fp, L",MsAllInputToPhotonLatency"); + fwprintf(fp, L",MsClickToPhotonLatency"); + } + if (args.mTrackAppTiming) { + fwprintf(fp, L",MsInstrumentedLatency"); + } + } + if (args.mWriteDisplayTime) { + fwprintf(fp, L",DisplayTimeAbs"); + } + if (args.mWriteFrameId) { + fwprintf(fp, L",FrameId"); + if (args.mTrackAppTiming) { + fwprintf(fp, L",AppFrameId"); + } + if (args.mTrackPcLatency) { + fwprintf(fp, L",PCLFrameId"); + } + } + fwprintf(fp, L"\n"); + + if (args.mCSVOutput == CSVOutput::Stdout) { + fflush(fp); + } +} + template<> void WriteCsvRow( FILE* fp, @@ -643,55 +861,553 @@ void WriteCsvRow( } } -template -void UpdateCsvT( +template<> +void WriteCsvRow( + FILE* fp, PMTraceSession const& pmSession, - ProcessInfo* processInfo, + ProcessInfo const& processInfo, PresentEvent const& p, - FrameMetricsT const& metrics) + pmon::util::metrics::FrameMetrics const& metrics) { auto const& args = GetCommandLineArgs(); - // Early return if not outputing to CSV. - if (args.mCSVOutput == CSVOutput::None) { - return; + fwprintf(fp, L"%s,%d,0x%llX,%hs,%d,%d", processInfo.mModuleName.c_str(), + p.ProcessId, + p.SwapChainAddress, + RuntimeToString(p.Runtime), + p.SyncInterval, + p.PresentFlags); + if (args.mTrackDisplay) { + fwprintf(fp, L",%d,%hs", p.SupportsTearing, + PresentModeToString(p.PresentMode)); } - - // Don't output dropped frames (if requested). - auto presented = p.FinalState == PresentResult::Presented; - if (args.mExcludeDropped && !presented) { - return; + if (args.mTrackFrameType) { + fwprintf(fp, L",%hs", FrameTypeToString(metrics.frameType)); + } + if (args.mTrackHybridPresent) { + fwprintf(fp, L",%d", p.IsHybridPresent); } - // Get/create file - FILE** fp = args.mMultiCsv - ? &processInfo->mOutputCsv - : &gGlobalOutputCsv; + if (args.mUseV2Metrics == false) { + // Time in Seconds + switch (args.mTimeUnit) { + case TimeUnit::DateTime: { + SYSTEMTIME st = {}; + uint64_t ns = 0; + pmSession.TimestampToLocalSystemTime(metrics.timeInSeconds, &st, &ns); + fwprintf(fp, L",%u-%u-%u %u:%02u:%02u.%09llu", st.wYear, + st.wMonth, + st.wDay, + st.wHour, + st.wMinute, + st.wSecond, + ns); + } break; + case TimeUnit::MilliSeconds: + case TimeUnit::QPCMilliSeconds: + fwprintf(fp, L",%.4lf", pmSession.TimestampToMilliSeconds(metrics.timeInSeconds)); + break; + case TimeUnit::QPC: + fwprintf(fp, L",%llu", metrics.timeInSeconds); + break; + default: + fwprintf(fp, L",%.*lf", DBL_DIG - 1, 0.001 * pmSession.TimestampToMilliSeconds(metrics.timeInSeconds)); + break; + } - if (*fp == nullptr) { - if (args.mCSVOutput == CSVOutput::File) { - wchar_t path[MAX_PATH]; - GenerateFilename(path, processInfo->mModuleName, p.ProcessId); - if (_wfopen_s(fp, path, L"w,ccs=UTF-8")) { - return; - } - } else { - *fp = stdout; + // MsBetweenSimulationStart + if (metrics.msBetweenSimStarts.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msBetweenSimStarts.value()); + } + else { + fwprintf(fp, L",NA"); } - WriteCsvHeader(*fp); - } + // MsBetweenPresents + fwprintf(fp, L",%.*lf", DBL_DIG - 1, metrics.msBetweenPresents); - // Output in CSV format - WriteCsvRow(*fp, pmSession, *processInfo, p, metrics); -} + // MsBetweenDisplayChange -> Transition from DisplayedTime + if (args.mTrackDisplay) { + if (metrics.msBetweenDisplayChange == 0.0) { + fwprintf(fp, L",NA"); + } + else { + fwprintf(fp, L",%.*lf", DBL_DIG - 1, metrics.msBetweenDisplayChange); + } + } -void UpdateCsv(PMTraceSession const& pmSession, ProcessInfo* processInfo, PresentEvent const& p, FrameMetrics1 const& metrics) -{ - UpdateCsvT(pmSession, processInfo, p, metrics); -} + // MsInPresentAPI + fwprintf(fp, L",%.*lf", DBL_DIG - 1, metrics.msInPresentApi); -void UpdateCsv(PMTraceSession const& pmSession, ProcessInfo* processInfo, PresentEvent const& p, FrameMetrics const& metrics) + // MsRenderPresentLatency + fwprintf(fp, L",%.*lf", DBL_DIG - 1, metrics.msUntilRenderComplete); + + // MsUntilDisplayed + if (args.mTrackDisplay) { + if (metrics.msUntilDisplayed == 0.0) { + fwprintf(fp, L",NA"); + } + else { + fwprintf(fp, L",%.4lf", metrics.msUntilDisplayed); + } + } + if (args.mTrackPcLatency) { + if (metrics.msPcLatency.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msPcLatency.value()); + } + else { + fwprintf(fp, L",NA"); + } + } + } + + // CPUStartTime + switch (args.mTimeUnit) { + case TimeUnit::MilliSeconds: + fwprintf(fp, L",%.4lf", pmSession.TimestampToMilliSeconds(metrics.cpuStartQpc)); + break; + case TimeUnit::QPC: + fwprintf(fp, L",%llu", metrics.cpuStartQpc); + break; + case TimeUnit::QPCMilliSeconds: + fwprintf(fp, L",%.4lf", pmSession.TimestampDeltaToMilliSeconds(metrics.cpuStartQpc)); + break; + case TimeUnit::DateTime: { + SYSTEMTIME st = {}; + uint64_t ns = 0; + pmSession.TimestampToLocalSystemTime(metrics.cpuStartQpc, &st, &ns); + fwprintf(fp, L",%u-%u-%u %u:%02u:%02u.%09llu", st.wYear, + st.wMonth, + st.wDay, + st.wHour, + st.wMinute, + st.wSecond, + ns); + } + break; + default: + fwprintf(fp, L",%.4lf", 0.001 * pmSession.TimestampToMilliSeconds(metrics.cpuStartQpc)); + } + + // MsBetweenAppStart, MsCPUBusy, MsCPUWait + fwprintf(fp, L",%.4lf,%.4lf,%.4lf", metrics.msCPUBusy + metrics.msCPUWait, + metrics.msCPUBusy, + metrics.msCPUWait); + + if (args.mTrackGPU) { + fwprintf(fp, L",%.4lf,%.4lf,%.4lf,%.4lf", metrics.msGPULatency, + metrics.msGPUBusy + metrics.msGPUWait, + metrics.msGPUBusy, + metrics.msGPUWait); + } + if (args.mTrackGPUVideo) { + fwprintf(fp, L",%.4lf", metrics.msVideoBusy); + } + if (args.mTrackDisplay) { + if (args.mUseV2Metrics) { + if (metrics.msDisplayedTime == 0.0) { + fwprintf(fp, L",NA,NA"); + } + else { + fwprintf(fp, L",%.4lf,%.4lf", metrics.msDisplayLatency, + metrics.msDisplayedTime); + } + } + if (metrics.msAnimationError.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msAnimationError.value()); + } + else { + fwprintf(fp, L",NA"); + } + if (metrics.msAnimationTime.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msAnimationTime.value()); + } + else { + fwprintf(fp, L",NA"); + } + if (metrics.msFlipDelay.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msFlipDelay.value()); + } + else { + fwprintf(fp, L",NA"); + } + } + if (args.mTrackInput) { + if (metrics.msAllInputPhotonLatency.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msAllInputPhotonLatency.value()); + } + else { + fwprintf(fp, L",NA"); + } + if (metrics.msClickToPhotonLatency.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msClickToPhotonLatency.value()); + } + else { + fwprintf(fp, L",NA"); + } + } + if (args.mTrackAppTiming) { + if (metrics.msInstrumentedLatency.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msInstrumentedLatency.value()); + } + else { + fwprintf(fp, L",NA"); + } + } + if (args.mWriteDisplayTime) { + if (metrics.screenTimeQpc == 0) { + fwprintf(fp, L",NA"); + } + else { + fwprintf(fp, L",%.4lf", pmSession.TimestampToMilliSeconds(metrics.screenTimeQpc)); + } + } + if (args.mWriteFrameId) { + fwprintf(fp, L",%u", p.FrameId); + if (args.mTrackAppTiming) { + fwprintf(fp, L",%u", p.AppFrameId); + } + if (args.mTrackPcLatency) { + fwprintf(fp, L",%u", p.PclFrameId); + } + } + fwprintf(fp, L"\n"); + + if (args.mCSVOutput == CSVOutput::Stdout) { + fflush(fp); + } +} + +template<> +void WriteCsvRow( + FILE* fp, + PMTraceSession const& pmSession, + ProcessInfo const& processInfo, + pmon::util::metrics::FrameData const& p, + pmon::util::metrics::FrameMetrics const& metrics) +{ + auto const& args = GetCommandLineArgs(); + + fwprintf(fp, L"%s,%d,0x%llX,%hs,%d,%d", processInfo.mModuleName.c_str(), + p.processId, + p.swapChainAddress, + RuntimeToString(p.runtime), + p.syncInterval, + p.presentFlags); + if (args.mTrackDisplay) { + fwprintf(fp, L",%d,%hs", p.supportsTearing, + PresentModeToString(p.presentMode)); + } + if (args.mTrackFrameType) { + fwprintf(fp, L",%hs", FrameTypeToString(metrics.frameType)); + } + if (args.mTrackHybridPresent) { + fwprintf(fp, L",%d", p.isHybridPresent); + } + + if (args.mUseV2Metrics == false) { + // Time in Seconds + switch (args.mTimeUnit) { + case TimeUnit::DateTime: { + SYSTEMTIME st = {}; + uint64_t ns = 0; + pmSession.TimestampToLocalSystemTime(metrics.timeInSeconds, &st, &ns); + fwprintf(fp, L",%u-%u-%u %u:%02u:%02u.%09llu", st.wYear, + st.wMonth, + st.wDay, + st.wHour, + st.wMinute, + st.wSecond, + ns); + } break; + case TimeUnit::MilliSeconds: + case TimeUnit::QPCMilliSeconds: + fwprintf(fp, L",%.4lf", pmSession.TimestampToMilliSeconds(metrics.timeInSeconds)); + break; + case TimeUnit::QPC: + fwprintf(fp, L",%llu", metrics.timeInSeconds); + break; + default: + fwprintf(fp, L",%.*lf", DBL_DIG - 1, 0.001 * pmSession.TimestampToMilliSeconds(metrics.timeInSeconds)); + break; + } + + // MsBetweenSimulationStart + if (metrics.msBetweenSimStarts.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msBetweenSimStarts.value()); + } + else { + fwprintf(fp, L",NA"); + } + + // MsBetweenPresents + fwprintf(fp, L",%.*lf", DBL_DIG - 1, metrics.msBetweenPresents); + + // MsBetweenDisplayChange -> Transition from DisplayedTime + if (args.mTrackDisplay) { + if (metrics.msBetweenDisplayChange == 0.0) { + fwprintf(fp, L",NA"); + } + else { + fwprintf(fp, L",%.*lf", DBL_DIG - 1, metrics.msBetweenDisplayChange); + } + } + + // MsInPresentAPI + fwprintf(fp, L",%.*lf", DBL_DIG - 1, metrics.msInPresentApi); + + // MsRenderPresentLatency + fwprintf(fp, L",%.*lf", DBL_DIG - 1, metrics.msUntilRenderComplete); + + // MsUntilDisplayed + if (args.mTrackDisplay) { + if (metrics.msUntilDisplayed == 0.0) { + fwprintf(fp, L",NA"); + } + else { + fwprintf(fp, L",%.4lf", metrics.msUntilDisplayed); + } + } + if (args.mTrackPcLatency) { + if (metrics.msPcLatency.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msPcLatency.value()); + } + else { + fwprintf(fp, L",NA"); + } + } + } + + // CPUStartTime + switch (args.mTimeUnit) { + case TimeUnit::MilliSeconds: + fwprintf(fp, L",%.4lf", pmSession.TimestampToMilliSeconds(metrics.cpuStartQpc)); + break; + case TimeUnit::QPC: + fwprintf(fp, L",%llu", metrics.cpuStartQpc); + break; + case TimeUnit::QPCMilliSeconds: + fwprintf(fp, L",%.4lf", pmSession.TimestampDeltaToMilliSeconds(metrics.cpuStartQpc)); + break; + case TimeUnit::DateTime: { + SYSTEMTIME st = {}; + uint64_t ns = 0; + pmSession.TimestampToLocalSystemTime(metrics.cpuStartQpc, &st, &ns); + fwprintf(fp, L",%u-%u-%u %u:%02u:%02u.%09llu", st.wYear, + st.wMonth, + st.wDay, + st.wHour, + st.wMinute, + st.wSecond, + ns); + } + break; + default: + fwprintf(fp, L",%.4lf", 0.001 * pmSession.TimestampToMilliSeconds(metrics.cpuStartQpc)); + } + + // MsBetweenAppStart, MsCPUBusy, MsCPUWait + fwprintf(fp, L",%.4lf,%.4lf,%.4lf", metrics.msCPUBusy + metrics.msCPUWait, + metrics.msCPUBusy, + metrics.msCPUWait); + + if (args.mTrackGPU) { + fwprintf(fp, L",%.4lf,%.4lf,%.4lf,%.4lf", metrics.msGPULatency, + metrics.msGPUBusy + metrics.msGPUWait, + metrics.msGPUBusy, + metrics.msGPUWait); + } + if (args.mTrackGPUVideo) { + fwprintf(fp, L",%.4lf", metrics.msVideoBusy); + } + if (args.mTrackDisplay) { + if (args.mUseV2Metrics) { + if (metrics.msDisplayedTime == 0.0) { + fwprintf(fp, L",NA,NA"); + } + else { + fwprintf(fp, L",%.4lf,%.4lf", metrics.msDisplayLatency, + metrics.msDisplayedTime); + } + } + if (metrics.msAnimationError.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msAnimationError.value()); + } + else { + fwprintf(fp, L",NA"); + } + if (metrics.msAnimationTime.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msAnimationTime.value()); + } + else { + if (metrics.msDisplayedTime == 0.0 || (metrics.frameType != FrameType::Application && metrics.frameType != FrameType::NotSet)) { + fwprintf(fp, L",NA"); + } + else { + fwprintf(fp, L",%.4lf", 0.0); + } + } + if (metrics.msFlipDelay.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msFlipDelay.value()); + } + else { + fwprintf(fp, L",NA"); + } + } + if (args.mTrackInput) { + if (metrics.msAllInputPhotonLatency.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msAllInputPhotonLatency.value()); + } + else { + fwprintf(fp, L",NA"); + } + if (metrics.msClickToPhotonLatency.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msClickToPhotonLatency.value()); + } + else { + fwprintf(fp, L",NA"); + } + } + if (args.mTrackAppTiming) { + if (metrics.msInstrumentedLatency.has_value()) { + fwprintf(fp, L",%.4lf", metrics.msInstrumentedLatency.value()); + } + else { + fwprintf(fp, L",NA"); + } + } + if (args.mWriteDisplayTime) { + if (metrics.screenTimeQpc == 0) { + fwprintf(fp, L",NA"); + } + else { + fwprintf(fp, L",%.4lf", pmSession.TimestampToMilliSeconds(metrics.screenTimeQpc)); + } + } + if (args.mWriteFrameId) { + fwprintf(fp, L",%u", p.frameId); + if (args.mTrackAppTiming) { + fwprintf(fp, L",%u", p.appFrameId); + } + if (args.mTrackPcLatency) { + fwprintf(fp, L",%u", p.pclFrameId); + } + } + fwprintf(fp, L"\n"); + + if (args.mCSVOutput == CSVOutput::Stdout) { + fflush(fp); + } +} + +template +void UpdateCsvT( + PMTraceSession const& pmSession, + ProcessInfo* processInfo, + PresentEvent const& p, + FrameMetricsT const& metrics) +{ + auto const& args = GetCommandLineArgs(); + + // Early return if not outputing to CSV. + if (args.mCSVOutput == CSVOutput::None) { + return; + } + + // Don't output dropped frames (if requested). + auto presented = p.FinalState == PresentResult::Presented; + if (args.mExcludeDropped && !presented) { + return; + } + + // Get/create file + FILE** fp = args.mMultiCsv + ? &processInfo->mOutputCsv + : &gGlobalOutputCsv; + + if (*fp == nullptr) { + if (args.mCSVOutput == CSVOutput::File) { + wchar_t path[MAX_PATH]; + GenerateFilename(path, processInfo->mModuleName, p.ProcessId); + if (_wfopen_s(fp, path, L"w,ccs=UTF-8")) { + return; + } + } else { + *fp = stdout; + } + + WriteCsvHeader(*fp); + } + + // Output in CSV format + WriteCsvRow(*fp, pmSession, *processInfo, p, metrics); +} + +template +void UpdateCsvT( + PMTraceSession const& pmSession, + ProcessInfo* processInfo, + pmon::util::metrics::FrameData const& p, + FrameMetricsT const& metrics) +{ + auto const& args = GetCommandLineArgs(); + + // Early return if not outputing to CSV. + if (args.mCSVOutput == CSVOutput::None) { + return; + } + + // Don't output dropped frames (if requested). + auto presented = p.finalState == PresentResult::Presented; + if (args.mExcludeDropped && !presented) { + return; + } + + // Get/create file + FILE** fp = args.mMultiCsv + ? &processInfo->mOutputCsv + : &gGlobalOutputCsv; + + if (*fp == nullptr) { + if (args.mCSVOutput == CSVOutput::File) { + wchar_t path[MAX_PATH]; + GenerateFilename(path, processInfo->mModuleName, p.processId); + if (_wfopen_s(fp, path, L"w,ccs=UTF-8")) { + return; + } + } + else { + *fp = stdout; + } + + WriteCsvHeader(*fp); + } + + // Output in CSV format + WriteCsvRow(*fp, pmSession, *processInfo, p, metrics); +} + +void UpdateCsv(PMTraceSession const& pmSession, ProcessInfo* processInfo, PresentEvent const& p, FrameMetrics1 const& metrics) +{ + UpdateCsvT(pmSession, processInfo, p, metrics); +} + +void UpdateCsv(PMTraceSession const& pmSession, ProcessInfo* processInfo, PresentEvent const& p, FrameMetrics const& metrics) +{ + UpdateCsvT(pmSession, processInfo, p, metrics); +} + +void UpdateCsv(PMTraceSession const& pmSession, ProcessInfo* processInfo, PresentEvent const& p, pmon::util::metrics::FrameMetrics const& metrics) +{ + UpdateCsvT(pmSession, processInfo, p, metrics); +} + +void UpdateCsv(PMTraceSession const& pmSession, ProcessInfo* processInfo, pmon::util::metrics::FrameData const& p, pmon::util::metrics::FrameMetrics const& metrics) +{ + UpdateCsvT(pmSession, processInfo, p, metrics); +} + +void UpdateCsv(PMTraceSession const& pmSession, ProcessInfo* processInfo, pmon::util::metrics::FrameData const& p, FrameMetrics1 const& metrics) { UpdateCsvT(pmSession, processInfo, p, metrics); } diff --git a/PresentMon/OutputThread.cpp b/PresentMon/OutputThread.cpp index d16572386..a82997b27 100644 --- a/PresentMon/OutputThread.cpp +++ b/PresentMon/OutputThread.cpp @@ -5,6 +5,8 @@ #include "PresentMon.hpp" #include "../IntelPresentMon/CommonUtilities/Math.h" #include "../IntelPresentMon/CommonUtilities/str/String.h" +#include "../IntelPresentMon/CommonUtilities/Mc/MetricsTypes.h" +#include "../IntelPresentMon/CommonUtilities/Mc/MetricsCalculator.h" #include #include @@ -242,590 +244,6 @@ static void UpdateAverage(float* avg, double value) } } -static void UpdateChain( - SwapChainData* chain, - std::shared_ptr const& p) -{ - if (p->FinalState == PresentResult::Presented) { - if (p->Displayed.empty() == false) { - if (p->Displayed.back().first == FrameType::NotSet || - p->Displayed.back().first == FrameType::Application) { - - // If the chain animation error source has been set to either - // app provider or PCL latency then set the last displayed simulation start time and the - // first app simulation start time based on the animation error source type. - if (chain->mAnimationErrorSource == AnimationErrorSource::AppProvider) { - chain->mLastDisplayedSimStartTime = p->AppSimStartTime; - if (chain->mFirstAppSimStartTime == 0) { - // Received the first app sim start time. - chain->mFirstAppSimStartTime = p->AppSimStartTime; - } - chain->mLastDisplayedAppScreenTime = p->Displayed.back().second; - } else if (chain->mAnimationErrorSource == AnimationErrorSource::PCLatency) { - // In the case of PCLatency only set values if pcl sim start time is not zero. - if (p->PclSimStartTime != 0) { - chain->mLastDisplayedSimStartTime = p->PclSimStartTime; - if (chain->mFirstAppSimStartTime == 0) { - // Received the first app sim start time. - chain->mFirstAppSimStartTime = p->PclSimStartTime; - } - chain->mLastDisplayedAppScreenTime = p->Displayed.back().second; - } - } else { - // Currently sourcing animation error from CPU start time, however check - // to see if we have a valid app provider or PCL sim start time and set the - // new animation source and set the first app sim start time - if (p->AppSimStartTime != 0) { - chain->mAnimationErrorSource = AnimationErrorSource::AppProvider; - chain->mLastDisplayedSimStartTime = p->AppSimStartTime; - if (chain->mFirstAppSimStartTime == 0) { - // Received the first app sim start time. - chain->mFirstAppSimStartTime = p->AppSimStartTime; - } - chain->mLastDisplayedAppScreenTime = p->Displayed.back().second; - } else if (p->PclSimStartTime != 0) { - chain->mAnimationErrorSource = AnimationErrorSource::PCLatency; - chain->mLastDisplayedSimStartTime = p->PclSimStartTime; - if (chain->mFirstAppSimStartTime == 0) { - // Received the first app sim start time. - chain->mFirstAppSimStartTime = p->PclSimStartTime; - } - chain->mLastDisplayedAppScreenTime = p->Displayed.back().second; - } else { - if (chain->mLastAppPresent != nullptr) { - chain->mLastDisplayedSimStartTime = chain->mLastAppPresent->PresentStartTime + - chain->mLastAppPresent->TimeInPresent; - } - chain->mLastDisplayedAppScreenTime = p->Displayed.back().second; - } - } - } - } - // Want this to always be updated with the last displayed screen time regardless if the - // frame was generated or not - chain->mLastDisplayedScreenTime = p->Displayed.empty() ? 0 : p->Displayed.back().second; - // Update last flipDelay. For NV GPU, size of p->Displayed can only be empty or 1 - chain->mLastDisplayedFlipDelay = p->Displayed.empty() ? 0 : p->FlipDelay; - } - - if (p->Displayed.empty() == false) { - if (p->Displayed.back().first == FrameType::NotSet || - p->Displayed.back().first == FrameType::Application) { - chain->mLastAppPresent = p; - } - } else { - // If the last present was not displayed, we need to set the last app present to the last - // present. - chain->mLastAppPresent = p; - } - - // Set chain->mLastSimStartTime to either p->PclSimStartTime or p->AppSimStartTime depending on - // if either are not zero. If both are zero, do not set. - if (p->PclSimStartTime != 0) { - chain->mLastSimStartTime = p->PclSimStartTime; - } else if (p->AppSimStartTime != 0) { - chain->mLastSimStartTime = p->AppSimStartTime; - } - - // Want this to always be updated with the last present regardless if the - // frame was generated or not - chain->mLastPresent = p; -} - -static void AdjustScreenTimeForCollapsedPresentNV1( - SwapChainData* chain, - std::shared_ptr const& p, - uint64_t& screenTime) -{ - if (chain->mLastDisplayedFlipDelay > 0 && (chain->mLastDisplayedScreenTime > screenTime)) { - // If chain->mLastDisplayedScreenTime that is adjusted by flipDelay is larger than screenTime, - // it implies the last displayed present is a collapsed present, or a runt frame. - // So we adjust the screenTime and flipDelay of screenTime, - // effectively making screenTime equals to chain->mLastDisplayedScreenTime. - - // Cast away constness of p to adjust the screenTime and flipDelay. - PresentEvent* pp = const_cast(p.get()); - if (!pp->Displayed.empty()) { - pp->FlipDelay += chain->mLastDisplayedScreenTime - screenTime; - pp->Displayed[0].second = chain->mLastDisplayedScreenTime; - screenTime = pp->Displayed[0].second; - } - } -} - -static void ReportMetrics1( - PMTraceSession const& pmSession, - ProcessInfo* processInfo, - SwapChainData* chain, - std::shared_ptr const& p, - bool isRecording, - bool computeAvg) -{ - bool displayed = p->FinalState == PresentResult::Presented; - - uint64_t screenTime = p->Displayed.empty() ? 0 : p->Displayed[0].second; - - // Special handling for NV flipDelay - AdjustScreenTimeForCollapsedPresentNV1(chain, p, screenTime); - - FrameMetrics1 metrics; - metrics.msBetweenPresents = chain->mLastPresent == nullptr ? 0 : pmSession.TimestampDeltaToUnsignedMilliSeconds(chain->mLastPresent->PresentStartTime, p->PresentStartTime); - metrics.msInPresentApi = pmSession.TimestampDeltaToMilliSeconds(p->TimeInPresent); - metrics.msUntilRenderComplete = pmSession.TimestampDeltaToMilliSeconds(p->PresentStartTime, p->ReadyTime); - metrics.msUntilDisplayed = !displayed ? 0 : pmSession.TimestampDeltaToUnsignedMilliSeconds(p->PresentStartTime, screenTime); - metrics.msBetweenDisplayChange = !displayed || chain->mLastDisplayedScreenTime == 0 ? 0 : pmSession.TimestampDeltaToUnsignedMilliSeconds(chain->mLastDisplayedScreenTime, screenTime); - metrics.msUntilRenderStart = pmSession.TimestampDeltaToMilliSeconds(p->PresentStartTime, p->GPUStartTime); - metrics.msGPUDuration = pmSession.TimestampDeltaToMilliSeconds(p->GPUDuration); - metrics.msVideoDuration = pmSession.TimestampDeltaToMilliSeconds(p->GPUVideoDuration); - metrics.msSinceInput = p->InputTime == 0 ? 0 : pmSession.TimestampDeltaToMilliSeconds(p->PresentStartTime - p->InputTime); - metrics.qpcScreenTime = screenTime; - metrics.msFlipDelay = p->FlipDelay ? pmSession.TimestampDeltaToMilliSeconds(p->FlipDelay) : 0; - - if (isRecording) { - UpdateCsv(pmSession, processInfo, *p, metrics); - } - - if (computeAvg) { - UpdateAverage(&chain->mAvgCPUDuration, metrics.msBetweenPresents); - UpdateAverage(&chain->mAvgGPUDuration, metrics.msGPUDuration); - if (metrics.msUntilDisplayed > 0) { - UpdateAverage(&chain->mAvgDisplayLatency, metrics.msUntilDisplayed); - if (metrics.msBetweenDisplayChange > 0) { - UpdateAverage(&chain->mAvgDisplayedTime, metrics.msBetweenDisplayChange); - } - } - } - - UpdateChain(chain, p); -} - -static void CalculateAnimationTime( - PMTraceSession const& pmSession, - const uint64_t& firstAppSimStartTime, - const uint64_t& currentSimTime, - double& animationTime) { - - auto firstSimStartTime = firstAppSimStartTime != 0 ? firstAppSimStartTime : pmSession.mStartTimestamp.QuadPart; - if (currentSimTime > firstSimStartTime) { - animationTime = pmSession.TimestampDeltaToMilliSeconds(firstSimStartTime, currentSimTime); - } else { - animationTime = 0.; - } -} - - -static void AdjustScreenTimeForCollapsedPresentNV( - std::shared_ptr const& p, - PresentEvent const* nextDisplayedPresent, - uint64_t& screenTime, - uint64_t& nextScreenTime) -{ - // nextDisplayedPresent should always be non-null for NV GPU. - if (p->FlipDelay && screenTime > nextScreenTime && nextDisplayedPresent) { - // If screenTime that is adjusted by flipDelay is larger than nextScreenTime, - // it implies this present is a collapsed present, or a runt frame. - // So we adjust the screenTime and flipDelay of nextDisplayedPresent, - // effectively making nextScreenTime equals to screenTime. - - // Cast away constness of nextDisplayedPresent to adjust the screenTime and flipDelay. - PresentEvent* nextDispPresent = const_cast(nextDisplayedPresent); - nextDispPresent->FlipDelay += (screenTime - nextScreenTime); - nextScreenTime = screenTime; - nextDispPresent->Displayed[0].second = nextScreenTime; - } -} - -static void ReportMetricsHelper( - PMTraceSession const& pmSession, - ProcessInfo* processInfo, - SwapChainData* chain, - std::shared_ptr const& p, - PresentEvent const* nextDisplayedPresent, - bool isRecording, - bool computeAvg) -{ - // Figure out what display index to start processing. - // - // The following cases are expected: - // p.Displayed empty and nextDisplayedPresent == nullptr: process p as not displayed - // p.Displayed with size N and nextDisplayedPresent == nullptr: process p.Displayed[0..N-2] as displayed, postponing N-1 - // p.Displayed with size N and nextDisplayedPresent != nullptr: process p.Displayed[N-1] as displayed - auto displayCount = p->Displayed.size(); - bool displayed = p->FinalState == PresentResult::Presented && displayCount > 0; - size_t displayIndex = displayed && nextDisplayedPresent != nullptr ? displayCount - 1 : 0; - - // Figure out what display index to attribute cpu work, gpu work, animation error, and input - // latency to. Start looking from the current display index. - size_t appIndex = std::numeric_limits::max(); - if (displayCount > 0) { - for (size_t i = displayIndex; i < displayCount; ++i) { - if (p->Displayed[i].first == FrameType::NotSet || - p->Displayed[i].first == FrameType::Application) { - appIndex = i; - break; - } - } - } else { - // If there are no displayed frames - appIndex = 0; - } - - do { - // PB = PresentStartTime - // PE = PresentEndTime - // D = ScreenTime - // - // chain->mLastPresent: PB--PE----D - // p: | PB--PE----D - // ... | | | | PB--PE - // nextDisplayedPresent: | | | | PB--PE----D - // | | | | | - // mCPUStart/mCPUBusy: |------->| | | | - // mCPUWait: |-->| | | - // mDisplayLatency: |----------------->| | - // mDisplayedTime: |---------------------->| - - // Lookup the ScreenTime and next ScreenTime - uint64_t screenTime = 0; - uint64_t nextScreenTime = 0; - if (displayed) { - screenTime = p->Displayed[displayIndex].second; - - if (displayIndex + 1 < displayCount) { - nextScreenTime = p->Displayed[displayIndex + 1].second; - } else if (nextDisplayedPresent != nullptr) { - nextScreenTime = nextDisplayedPresent->Displayed[0].second; - } else { - return; - } - } - - double msGPUDuration = 0.0; - - FrameMetrics metrics{}; - metrics.mCPUStart = 0; - - // Calculate these metrics for every present - metrics.mTimeInSeconds = p->PresentStartTime; - metrics.mMsBetweenPresents = chain->mLastPresent == nullptr ? 0 : - pmSession.TimestampDeltaToUnsignedMilliSeconds(chain->mLastPresent->PresentStartTime, p->PresentStartTime); - metrics.mMsInPresentApi = pmSession.TimestampDeltaToMilliSeconds(p->TimeInPresent); - metrics.mMsUntilRenderComplete = pmSession.TimestampDeltaToMilliSeconds(p->PresentStartTime, p->ReadyTime); - - if (chain->mLastAppPresent) { - if (chain->mLastAppPresent->AppPropagatedPresentStartTime != 0) { - metrics.mCPUStart = chain->mLastAppPresent->AppPropagatedPresentStartTime + chain->mLastAppPresent->AppPropagatedTimeInPresent; - } - else { - metrics.mCPUStart = chain->mLastAppPresent->PresentStartTime + chain->mLastAppPresent->TimeInPresent; - } - } else { - metrics.mCPUStart = chain->mLastPresent == nullptr ? 0 : - chain->mLastPresent->PresentStartTime + chain->mLastPresent->TimeInPresent; - } - - if (displayIndex == appIndex) { - uint64_t gpuStartTime = 0; - if (p->AppPropagatedPresentStartTime != 0) { - msGPUDuration = pmSession.TimestampDeltaToUnsignedMilliSeconds(p->AppPropagatedGPUStartTime, p->AppPropagatedReadyTime); - gpuStartTime = p->AppPropagatedGPUStartTime; - metrics.mMsCPUBusy = pmSession.TimestampDeltaToUnsignedMilliSeconds(metrics.mCPUStart, p->AppPropagatedPresentStartTime); - metrics.mMsCPUWait = pmSession.TimestampDeltaToMilliSeconds(p->AppPropagatedTimeInPresent); - metrics.mMsGPULatency = pmSession.TimestampDeltaToUnsignedMilliSeconds(metrics.mCPUStart, gpuStartTime); - metrics.mMsGPUBusy = pmSession.TimestampDeltaToMilliSeconds(p->AppPropagatedGPUDuration); - metrics.mMsVideoBusy = pmSession.TimestampDeltaToMilliSeconds(p->AppPropagatedGPUVideoDuration); - metrics.mMsGPUWait = std::max(0.0, msGPUDuration - metrics.mMsGPUBusy); - } else { - msGPUDuration = pmSession.TimestampDeltaToUnsignedMilliSeconds(p->GPUStartTime, p->ReadyTime); - gpuStartTime = p->GPUStartTime; - metrics.mMsCPUBusy = pmSession.TimestampDeltaToUnsignedMilliSeconds(metrics.mCPUStart, p->PresentStartTime); - metrics.mMsCPUWait = pmSession.TimestampDeltaToMilliSeconds(p->TimeInPresent); - metrics.mMsGPULatency = pmSession.TimestampDeltaToUnsignedMilliSeconds(metrics.mCPUStart, gpuStartTime); - metrics.mMsGPUBusy = pmSession.TimestampDeltaToMilliSeconds(p->GPUDuration); - metrics.mMsVideoBusy = pmSession.TimestampDeltaToMilliSeconds(p->GPUVideoDuration); - metrics.mMsGPUWait = std::max(0.0, msGPUDuration - metrics.mMsGPUBusy); - } - - // Need both AppSleepStart and AppSleepEnd to calculate XellSleep - metrics.mMsInstrumentedSleep = (p->AppSleepEndTime == 0 || p->AppSleepStartTime == 0) ? 0 : - pmSession.TimestampDeltaToUnsignedMilliSeconds(p->AppSleepStartTime, p->AppSleepEndTime); - // If there isn't a valid sleep end time use the sim start time - auto InstrumentedStartTime = p->AppSleepEndTime != 0 ? p->AppSleepEndTime : p->AppSimStartTime; - // If neither the sleep end time or sim start time is valid, there is no - // way to calculate the Xell Gpu latency - metrics.mMsInstrumentedGpuLatency = InstrumentedStartTime == 0 ? 0 : - pmSession.TimestampDeltaToUnsignedMilliSeconds(InstrumentedStartTime, gpuStartTime); - - // If we have both a valid pcl sim start time and a valid app sim start time, we use the pcl sim start time. - if (p->PclSimStartTime != 0) { - metrics.mMsBetweenSimStarts = pmSession.TimestampDeltaToUnsignedMilliSeconds(chain->mLastSimStartTime, p->PclSimStartTime); - } else if (p->AppSimStartTime != 0) { - metrics.mMsBetweenSimStarts = pmSession.TimestampDeltaToUnsignedMilliSeconds(chain->mLastSimStartTime, p->AppSimStartTime); - } - } else { - metrics.mMsCPUBusy = 0; - metrics.mMsCPUWait = 0; - metrics.mMsGPULatency = 0; - metrics.mMsGPUBusy = 0; - metrics.mMsVideoBusy = 0; - metrics.mMsGPUWait = 0; - metrics.mMsInstrumentedSleep = 0; - metrics.mMsInstrumentedGpuLatency = 0; - metrics.mMsBetweenSimStarts = 0; - } - - // If the frame was displayed regardless of how it was produced, calculate the following - // metrics - if (displayed) { - // Special handling for NV flipDelay - AdjustScreenTimeForCollapsedPresentNV(p, nextDisplayedPresent, screenTime, nextScreenTime); - - // Calculate the various display metrics - metrics.mMsFlipDelay = p->FlipDelay ? pmSession.TimestampDeltaToMilliSeconds(p->FlipDelay) : 0; - metrics.mMsDisplayLatency = pmSession.TimestampDeltaToUnsignedMilliSeconds(metrics.mCPUStart, screenTime); - metrics.mMsDisplayedTime = pmSession.TimestampDeltaToUnsignedMilliSeconds(screenTime, nextScreenTime); - metrics.mMsUntilDisplayed = pmSession.TimestampDeltaToUnsignedMilliSeconds(p->PresentStartTime, screenTime); - metrics.mMsBetweenDisplayChange = chain->mLastDisplayedScreenTime == 0 ? 0 : - pmSession.TimestampDeltaToUnsignedMilliSeconds(chain->mLastDisplayedScreenTime, screenTime); - metrics.mScreenTime = screenTime; - - // If we have AppRenderSubmitStart calculate the render latency - metrics.mMsInstrumentedRenderLatency = p->AppRenderSubmitStartTime == 0 ? 0 : - pmSession.TimestampDeltaToUnsignedMilliSeconds(p->AppRenderSubmitStartTime, screenTime); - metrics.mMsReadyTimeToDisplayLatency = pmSession.TimestampDeltaToUnsignedMilliSeconds(p->ReadyTime, screenTime); - // If there isn't a valid sleep end time use the sim start time - auto InstrumentedStartTime = p->AppSleepEndTime != 0 ? p->AppSleepEndTime : p->AppSimStartTime; - // If neither the sleep end time or sim start time is valid, there is no - // way to calculate the Xell Gpu latency - metrics.mMsInstrumentedLatency = InstrumentedStartTime == 0 ? 0 : - pmSession.TimestampDeltaToUnsignedMilliSeconds(InstrumentedStartTime, screenTime); - - metrics.mMsPcLatency = 0.f; - // Check to see if we have a valid pc latency sim start time. - if (p->PclSimStartTime != 0) { - if (p->PclInputPingTime == 0) { - if (chain->mAccumulatedInput2FrameStartTime != 0) { - // This frame was displayed but we don't have a pc latency input time. However, there is accumulated time - // so there is a pending input that will now hit the screen. Add in the time from the last not - // displayed pc simulation start to this frame's pc simulation start. - chain->mAccumulatedInput2FrameStartTime += - pmSession.TimestampDeltaToUnsignedMilliSeconds(chain->mLastReceivedNotDisplayedPclSimStart, p->PclSimStartTime); - // Add all of the accumlated time to the average input to frame start time. - chain->mEmaInput2FrameStartTime = pmon::util::CalculateEma( - chain->mEmaInput2FrameStartTime, - chain->mAccumulatedInput2FrameStartTime, - 0.1); - // Reset the tracking variables for when we have a dropped frame with a pc latency input - chain->mAccumulatedInput2FrameStartTime = 0.f; - chain->mLastReceivedNotDisplayedPclSimStart = 0; - } - } else { - chain->mEmaInput2FrameStartTime = pmon::util::CalculateEma( - chain->mEmaInput2FrameStartTime, - pmSession.TimestampDeltaToUnsignedMilliSeconds(p->PclInputPingTime, p->PclSimStartTime), - 0.1); - } - } - // If we have a non-zero average input to frame start time and a PC Latency simulation - // start time calculate the PC Latency - auto simStartTime = p->PclSimStartTime != 0 ? p->PclSimStartTime : chain->mLastSimStartTime; - if (chain->mEmaInput2FrameStartTime != 0.f && simStartTime != 0) { - metrics.mMsPcLatency = chain->mEmaInput2FrameStartTime + - pmSession.TimestampDeltaToMilliSeconds(simStartTime, screenTime); - } - } else { - metrics.mMsDisplayLatency = 0; - metrics.mMsDisplayedTime = 0; - metrics.mMsUntilDisplayed = 0; - metrics.mMsBetweenDisplayChange = 0; - metrics.mScreenTime = 0; - metrics.mMsInstrumentedLatency = 0; - metrics.mMsInstrumentedRenderLatency = 0; - metrics.mMsReadyTimeToDisplayLatency = 0; - metrics.mMsPcLatency = 0; - if (p->PclSimStartTime != 0) { - if (p->PclInputPingTime != 0) { - // This frame was dropped but we have valid pc latency input and simulation start - // times. Calculate the initial input to sim start time. - chain->mAccumulatedInput2FrameStartTime = - pmSession.TimestampDeltaToUnsignedMilliSeconds(p->PclInputPingTime, p->PclSimStartTime); - } else if (chain->mAccumulatedInput2FrameStartTime != 0.f) { - // This frame was also dropped and there is no pc latency input time. However, since we have - // accumulated time this means we have a pending input that has had multiple dropped frames - // and has not yet hit the screen. Calculate the time between the last not displayed sim start and - // this sim start and add it to our accumulated total - chain->mAccumulatedInput2FrameStartTime += - pmSession.TimestampDeltaToUnsignedMilliSeconds(chain->mLastReceivedNotDisplayedPclSimStart, p->PclSimStartTime); - } - chain->mLastReceivedNotDisplayedPclSimStart = p->PclSimStartTime; - } - } - - // The following metrics use both the frame's displayed and origin information. - metrics.mMsClickToPhotonLatency = 0; - metrics.mMsAllInputPhotonLatency = 0; - metrics.mMsInstrumentedInputTime = 0; - metrics.mMsAnimationError = std::nullopt; - metrics.mAnimationTime = std::nullopt; - - if (displayIndex == appIndex) { - if (displayed) { - // For all input device metrics check to see if there were any previous device input times - // that were attached to a dropped frame and if so use the last received times for the - // metric calculations - auto updatedInputTime = chain->mLastReceivedNotDisplayedAllInputTime == 0 ? 0 : - pmSession.TimestampDeltaToUnsignedMilliSeconds(chain->mLastReceivedNotDisplayedAllInputTime, screenTime); - metrics.mMsAllInputPhotonLatency = p->InputTime == 0 ? updatedInputTime : - pmSession.TimestampDeltaToUnsignedMilliSeconds(p->InputTime, screenTime); - - updatedInputTime = chain->mLastReceivedNotDisplayedMouseClickTime == 0 ? 0 : - pmSession.TimestampDeltaToUnsignedMilliSeconds(chain->mLastReceivedNotDisplayedMouseClickTime, screenTime); - metrics.mMsClickToPhotonLatency = p->MouseClickTime == 0 ? updatedInputTime : - pmSession.TimestampDeltaToUnsignedMilliSeconds(p->MouseClickTime, screenTime); - - updatedInputTime = chain->mLastReceivedNotDisplayedAppProviderInputTime == 0 ? 0 : - pmSession.TimestampDeltaToUnsignedMilliSeconds(chain->mLastReceivedNotDisplayedAppProviderInputTime, screenTime); - metrics.mMsInstrumentedInputTime = p->AppInputSample.first == 0 ? updatedInputTime : - pmSession.TimestampDeltaToUnsignedMilliSeconds(p->AppInputSample.first, screenTime); - - // Reset all last received device times - chain->mLastReceivedNotDisplayedAllInputTime = 0; - chain->mLastReceivedNotDisplayedMouseClickTime = 0; - chain->mLastReceivedNotDisplayedAppProviderInputTime = 0; - - // Next calculate the animation error and animation time. First calculate the simulation - // start time. Simulation start can be either an app provided sim start time via the provider or - // PCL stats or, if not present,the cpu start. - uint64_t simStartTime = 0; - if (chain->mAnimationErrorSource == AnimationErrorSource::PCLatency) { - // If the pcl latency is the source of the animation error then use the pcl sim start time. - simStartTime = p->PclSimStartTime; - } else if (chain->mAnimationErrorSource == AnimationErrorSource::AppProvider) { - // If the app provider is the source of the animation error then use the app sim start time. - simStartTime = p->AppSimStartTime; - } else if (chain->mAnimationErrorSource == AnimationErrorSource::CpuStart) { - // If the cpu start time is the source of the animation error then use the cpu start time. - simStartTime = metrics.mCPUStart; - } - - if (chain->mLastDisplayedSimStartTime != 0) { - // If the simulation start time is less than the last displayed simulation start time it means - // we are transitioning to app provider events. - if (simStartTime > chain->mLastDisplayedSimStartTime) { - metrics.mMsAnimationError = pmSession.TimestampDeltaToMilliSeconds(screenTime - chain->mLastDisplayedAppScreenTime, - simStartTime - chain->mLastDisplayedSimStartTime); - } - } - // If we have a value in app sim start or pcl sim start and we haven't set the first - // sim start time then we are transitioning from using cpu start to - // an application provided timestamp. Set the animation time to zero - // for the first frame. - if ((p->AppSimStartTime != 0 || p->PclSimStartTime != 0) && chain->mFirstAppSimStartTime == 0) { - metrics.mAnimationTime = 0.; - } - else { - double animationTime = 0.; - CalculateAnimationTime(pmSession, chain->mFirstAppSimStartTime, simStartTime, animationTime); - metrics.mAnimationTime = animationTime; - } - } else { - if (p->InputTime != 0) { - chain->mLastReceivedNotDisplayedAllInputTime = p->InputTime; - } - if (p->MouseClickTime != 0) { - chain->mLastReceivedNotDisplayedMouseClickTime = p->MouseClickTime; - } - if (p->AppInputSample.first != 0) { - chain->mLastReceivedNotDisplayedAppProviderInputTime = p->AppInputSample.first; - } - } - } - - - if (p->Displayed.empty()) { - metrics.mFrameType = FrameType::NotSet; - } else { - metrics.mFrameType = p->Displayed[displayIndex].first; - } - - if (isRecording) { - UpdateCsv(pmSession, processInfo, *p, metrics); - } - - if (computeAvg) { - if (displayIndex == appIndex) { - UpdateAverage(&chain->mAvgCPUDuration, metrics.mMsCPUBusy + metrics.mMsCPUWait); - UpdateAverage(&chain->mAvgGPUDuration, msGPUDuration); - } - if (displayed) { - UpdateAverage(&chain->mAvgDisplayLatency, metrics.mMsDisplayLatency); - UpdateAverage(&chain->mAvgDisplayedTime, metrics.mMsDisplayedTime); - UpdateAverage(&chain->mAvgMsUntilDisplayed, metrics.mMsUntilDisplayed); - UpdateAverage(&chain->mAvgMsBetweenDisplayChange, metrics.mMsBetweenDisplayChange); - } - } - - displayIndex += 1; - } while (displayIndex < displayCount); - - UpdateChain(chain, p); -} - -static void ReportMetrics( - PMTraceSession const& pmSession, - ProcessInfo* processInfo, - SwapChainData* chain, - std::shared_ptr const& p, - bool isRecording, - bool computeAvg) -{ - // Remove Repeated flips if they are in Application->Repeated or Repeated->Application sequences. - for (size_t i = 0, n = p->Displayed.size(); i + 1 < n; ) { - if (p->Displayed[i].first == FrameType::Application && p->Displayed[i + 1].first == FrameType::Repeated) { - p->Displayed.erase(p->Displayed.begin() + i + 1); - n -= 1; - } else if (p->Displayed[i].first == FrameType::Repeated && p->Displayed[i + 1].first == FrameType::Application) { - p->Displayed.erase(p->Displayed.begin() + i); - n -= 1; - } else { - i += 1; - } - } - - // For the chain's first present, we just initialize mLastPresent to give a baseline for the - // first frame. - if (chain->mLastPresent == nullptr) { - UpdateChain(chain, p); - return; - } - - // If chain->mPendingPresents is non-empty, then it contains a displayed present followed by - // some number of discarded presents. If the displayed present has multiple Displayed entries, - // all but the last have already been handled. - // - // If p is displayed, then we can complete all pending presents, and complete any flips in p - // except for the last one, but then we have to add p to the pending list to wait for the next - // displayed frame. - // - // If p is not displayed, we can process it now unless it is blocked behind an earlier present - // waiting for the next displayed one, in which case we need to add it to the pending list as - // well. - if (p->FinalState == PresentResult::Presented) { - for (auto const& p2 : chain->mPendingPresents) { - ReportMetricsHelper(pmSession, processInfo, chain, p2, p.get(), isRecording, computeAvg); - } - ReportMetricsHelper(pmSession, processInfo, chain, p, nullptr, isRecording, computeAvg); - chain->mPendingPresents.clear(); - chain->mPendingPresents.push_back(p); - } else { - if (chain->mPendingPresents.empty()) { - ReportMetricsHelper(pmSession, processInfo, chain, p, nullptr, isRecording, computeAvg); - } else { - chain->mPendingPresents.push_back(p); - } - } -} - static void PruneOldSwapChainData( PMTraceSession const& pmSession, uint64_t latestTimestamp) @@ -843,8 +261,8 @@ static void PruneOldSwapChainData( auto processInfo = &pair.second; // Check if this is DWM process - const bool isDwmProcess = - util::str::ToLower(processInfo->mModuleName).contains(L"dwm.exe"); + const auto processNameLower = util::str::ToLower(processInfo->mModuleName); + const bool isDwmProcess = (processNameLower.find(L"dwm.exe") != std::wstring::npos); for (auto ii = processInfo->mSwapChain.begin(), ie = processInfo->mSwapChain.end(); ii != ie; ) { auto swapChainAddress = ii->first; @@ -852,8 +270,12 @@ static void PruneOldSwapChainData( // Don't prune DWM swap chains with address 0x0 bool shouldSkipPruning = isDwmProcess && swapChainAddress == 0x0; + bool shouldPrune = false; + if (!shouldSkipPruning) { + shouldPrune = chain->mUnifiedSwapChain.IsPrunableBefore(minTimestamp); + } - if (!shouldSkipPruning && chain->mLastPresent && chain->mLastPresent->PresentStartTime < minTimestamp) { + if (shouldPrune) { ii = processInfo->mSwapChain.erase(ii); } else { @@ -925,14 +347,15 @@ static bool GetPresentProcessInfo( } auto chain = &processInfo->mSwapChain[presentEvent->SwapChainAddress]; - if (chain->mLastPresent == nullptr) { - UpdateChain(chain, presentEvent); + if (!chain->mUnifiedSwapChain.swapChain.lastPresent.has_value()) { + using namespace pmon::util::metrics; + chain->mUnifiedSwapChain.SeedFromFirstPresent(FrameData::CopyFrameData(presentEvent)); return true; } *outProcessInfo = processInfo; *outChain = chain; - *outPresentTime = chain->mLastPresent->PresentStartTime; + *outPresentTime = chain->mUnifiedSwapChain.GetLastPresentQpc(); return false; } @@ -958,6 +381,23 @@ static void ProcessRecordingToggle( } } +static FrameMetrics1 ToFrameMetrics1(pmon::util::metrics::FrameMetrics const& m) +{ + FrameMetrics1 out{}; + out.msBetweenPresents = m.msBetweenPresents; + out.msInPresentApi = m.msInPresentApi; + out.msUntilRenderComplete = m.msUntilRenderComplete; + out.msUntilDisplayed = m.msUntilDisplayed; + out.msBetweenDisplayChange = m.msBetweenDisplayChange; + out.msUntilRenderStart = m.msUntilRenderStart; + out.msGPUDuration = m.msGpuDuration; + out.msVideoDuration = m.msVideoDuration; + out.msSinceInput = m.msSinceInput; + out.qpcScreenTime = m.screenTimeQpc; + out.msFlipDelay = m.msFlipDelay.has_value() ? m.msFlipDelay.value() : 0.0; + return out; +} + static void ProcessEvents( PMTraceSession const& pmSession, std::vector> const& presentEvents, @@ -979,6 +419,10 @@ static void ProcessEvents( size_t processEventCount = processEvents->size(); bool checkProcessTime = processEventCount > 0; + const uint64_t qpcFrequency = static_cast(pmSession.mTimestampFrequency.QuadPart); + const uint64_t qpcStart = static_cast(pmSession.mStartTimestamp.QuadPart); + pmon::util::QpcConverter qpc(qpcFrequency, qpcStart); + // Iterate through the processEvents, handling process events and recording toggles along the // way. uint64_t presentTime = 0; @@ -1035,19 +479,69 @@ static void ProcessEvents( continue; } - // If we are recording or presenting metrics to console then update the metrics and pending - // presents. Otherwise, just update the latest present details in the chain. - // - // If there are more than one pending PresentEvents, then the first one is displayed and the - // rest aren't. Otherwise, there will only be one (or zero) pending presents. - if (isRecording || computeAvg) { + auto ready = chain->mUnifiedSwapChain.Enqueue(pmon::util::metrics::FrameData::CopyFrameData(presentEvent), + args.mUseV1Metrics ? pmon::util::metrics::MetricsVersion::V1 : pmon::util::metrics::MetricsVersion::V2); + + // Do we need to emit metrics for this present? + const bool emit = (isRecording || computeAvg); + + for (auto& it : ready) { + // Build FrameData copies for the unified calculator state-advance (and V2 metrics). + using namespace pmon::util::metrics; + + FrameData& frame = (it.presentPtr != nullptr) ? *it.presentPtr : it.present; + FrameData* nextPtr = it.nextDisplayedPtr; + if (args.mUseV1Metrics) { - ReportMetrics1(pmSession, processInfo, chain, presentEvent, isRecording, computeAvg); - } else { - ReportMetrics(pmSession, processInfo, chain, presentEvent, isRecording, computeAvg); + // V1: compute immediately (no look-ahead) and emit legacy V1 CSV. + auto computed = ComputeMetricsForPresent(qpc, frame, nullptr, chain->mUnifiedSwapChain.swapChain, MetricsVersion::V1); + + if (emit) { + for (auto const& cm : computed) { + auto const m1 = ToFrameMetrics1(cm.metrics); + + if (isRecording) { + UpdateCsv(pmSession, processInfo, frame, m1); + } + + if (computeAvg) { + UpdateAverage(&chain->mUnifiedSwapChain.avgCPUDuration, m1.msBetweenPresents); + UpdateAverage(&chain->mUnifiedSwapChain.avgGPUDuration, m1.msGPUDuration); + + if (m1.msUntilDisplayed > 0) { + UpdateAverage(&chain->mUnifiedSwapChain.avgDisplayLatency, m1.msUntilDisplayed); + if (m1.msBetweenDisplayChange > 0) { + UpdateAverage(&chain->mUnifiedSwapChain.avgDisplayedTime, m1.msBetweenDisplayChange); + } + } + } + } + } + } + else { + // V2 unified metrics: compute + advance together + auto computed = ComputeMetricsForPresent(qpc, frame, nextPtr, chain->mUnifiedSwapChain.swapChain, MetricsVersion::V2); + + if (emit) { + for (auto const& cm : computed) { + auto const& m = cm.metrics; + + if (isRecording) { + UpdateCsv(pmSession, processInfo, frame, m); + } + + if (computeAvg) { + UpdateAverage(&chain->mUnifiedSwapChain.avgCPUDuration, m.msCPUBusy + m.msCPUWait); + if (m.msUntilDisplayed > 0) { + UpdateAverage(&chain->mUnifiedSwapChain.avgDisplayLatency, m.msDisplayLatency); + UpdateAverage(&chain->mUnifiedSwapChain.avgDisplayedTime, m.msDisplayedTime); + UpdateAverage(&chain->mUnifiedSwapChain.avgMsUntilDisplayed, m.msUntilDisplayed); + UpdateAverage(&chain->mUnifiedSwapChain.avgMsBetweenDisplayChange, m.msBetweenDisplayChange); + } + } + } + } } - } else { - UpdateChain(chain, presentEvent); } } @@ -1151,6 +645,7 @@ void Output(PMTraceSession const* pmSession) gRecordingToggleHistory.shrink_to_fit(); } + void StartOutputThread(PMTraceSession const& pmSession) { InitializeCriticalSection(&gRecordingToggleCS); @@ -1166,5 +661,4 @@ void StopOutputThread() DeleteCriticalSection(&gRecordingToggleCS); } -} - +} \ No newline at end of file diff --git a/PresentMon/PresentMon.args.json b/PresentMon/PresentMon.args.json index 0ca03c6e5..3b3942b97 100644 --- a/PresentMon/PresentMon.args.json +++ b/PresentMon/PresentMon.args.json @@ -68,11 +68,15 @@ }, { "Id": "8f014852-6512-466b-99cb-0a8724c36862", - "Command": "--output_file gold_5.csv" + "Command": "--output_file \"E:\\EtlTesting\\ETLDebugging\\test_case_8_unified-fid.csv\"" }, { "Id": "1606ee3e-ac1c-4fea-9a87-736864dd1f00", - "Command": "--etl_file \"C:\\Users\\Chili\\Desktop\\cpp\\ipm-relay\\Tests\\Gold\\test_case_5.etl\"" + "Command": "--etl_file \"E:\\EtlTesting\\GoldETLTests-External\\test_case_8.etl\"" + }, + { + "Id": "abf6cf88-1afe-4b6d-ac24-71ba39c9615a", + "Command": "--etl_file \"D:\\source\\repos\\drivers.gpu.tools.presentmon\\Tests\\Gold\\test_case_0.etl\"" }, { "Id": "8dc2827c-9d81-4c64-8596-ea11dbd81c8f", @@ -105,6 +109,18 @@ { "Id": "892a1932-a81e-4f5c-b30f-899e6884457d", "Command": "--track_frame_type" + }, + { + "Id": "3f809034-342e-40c3-a24a-c836e5c0ae29", + "Command": "--track_app_timing" + }, + { + "Id": "5e1344fb-cd04-46d7-a2c7-7b69faf0101a", + "Command": "--track_pc_latency" + }, + { + "Id": "279d1ed9-07a1-4217-a370-3d058d565a49", + "Command": "--v1_metrics" } ] } \ No newline at end of file diff --git a/PresentMon/PresentMon.hpp b/PresentMon/PresentMon.hpp index 611ccf32a..e581c2746 100644 --- a/PresentMon/PresentMon.hpp +++ b/PresentMon/PresentMon.hpp @@ -29,10 +29,18 @@ which is controlled from MainThread based on user input or timer. #include "../PresentData/PresentMonTraceConsumer.hpp" #include "../PresentData/PresentMonTraceSession.hpp" +#include "../IntelPresentMon/CommonUtilities/mc/UnifiedSwapChain.h" +#include "../IntelPresentMon/CommonUtilities/mc/SwapChainState.h" +#include "../IntelPresentMon/CommonUtilities/Qpc.h" #include #include #include +#include + +#ifndef PM_KEEP_LEGACY_SWAPCHAIN_DATA +#define PM_KEEP_LEGACY_SWAPCHAIN_DATA 0 +#endif // Verbosity of console output for normal operation: enum class ConsoleOutput { @@ -184,6 +192,7 @@ struct FrameMetrics2 { // - pending presents whose metrics cannot be computed until future presents are received, // - exponential averages of key metrics displayed in console output. struct SwapChainData { +#if PM_KEEP_LEGACY_SWAPCHAIN_DATA // Pending presents waiting for the next displayed present. std::vector> mPendingPresents; @@ -228,6 +237,10 @@ struct SwapChainData { // Internal NVIDIA Metrics uint64_t mLastDisplayedFlipDelay = 0; +#endif + + // Unified swap chain for unfied metrics calculations + pmon::util::metrics::UnifiedSwapChain mUnifiedSwapChain; }; struct ProcessInfo { @@ -267,6 +280,9 @@ const char* PresentModeToString(PresentMode mode); const char* RuntimeToString(Runtime rt); void UpdateCsv(PMTraceSession const& pmSession, ProcessInfo* processInfo, PresentEvent const& p, FrameMetrics const& metrics); void UpdateCsv(PMTraceSession const& pmSession, ProcessInfo* processInfo, PresentEvent const& p, FrameMetrics1 const& metrics); +void UpdateCsv(PMTraceSession const& pmSession, ProcessInfo* processInfo, pmon::util::metrics::FrameData const& p, FrameMetrics1 const& metrics); +void UpdateCsv(PMTraceSession const& pmSession, ProcessInfo* processInfo, PresentEvent const& p, pmon::util::metrics::FrameMetrics const& metrics); +void UpdateCsv(PMTraceSession const& pmSession, ProcessInfo* processInfo, pmon::util::metrics::FrameData const& p, pmon::util::metrics::FrameMetrics const& metrics); // MainThread.cpp: void ExitMainThread(); diff --git a/Tests/Gold/test_case_4.csv b/Tests/Gold/test_case_4.csv index 6068a9974..382da3963 100644 --- a/Tests/Gold/test_case_4.csv +++ b/Tests/Gold/test_case_4.csv @@ -91,7 +91,7 @@ dwm.exe,10376,0x1BF098B1ED0,DXGI,1,0,0,Hardware: Legacy Flip,Application,5545385 dwm.exe,10376,0x1BF098B1ED0,DXGI,1,0,0,Hardware: Legacy Flip,Application,55454017095,NA,16.06800000000000,16.66520000000000,0.10630000000000,1.91440000000000,16.0088,NA,55453857940,16.0218,15.9155,0.1063,0.0000,17.8749,2.6085,15.2664,0.0000,0.4262,2237.2199,NA,NA,NA,NA dwm.exe,10376,0x1BF098B1ED0,DXGI,1,0,0,Hardware: Legacy Flip,Application,55454220315,NA,20.32200000000000,16.67040000000000,0.07480000000000,0.45110000000000,12.3572,NA,55454018158,20.2905,20.2157,0.0748,0.0000,20.6730,1.8226,18.8504,0.0000,-0.6486,2253.2417,NA,NA,NA,NA Presenter.exe,5236,0x285B4C0D1B0,DXGI,0,0,0,Composed: Flip,Application,55454299820,NA,15.65140000000000,18.03750000000000,2.45370000000000,-11.33040000000000,22.4442,NA,55454168764,15.5593,13.1056,2.4537,0.8807,0.8945,0.8945,0.0000,0.0000,NA,2268.3023,NA,NA,NA,NA -Presenter.exe,5236,0x285B4C0D1B0,DXGI,0,0,1,Hardware: Independent Flip,Application,55454457377,NA,15.75570000000000,NA,0.24110000000000,5.50070000000000,5.5007,NA,55454324357,13.5431,13.3020,0.2411,0.1296,18.6731,0.9864,17.6867,0.0000,-1844674407370938.5000,2283.8616,NA,NA,NA,NA +Presenter.exe,5236,0x285B4C0D1B0,DXGI,0,0,1,Hardware: Independent Flip,Application,55454457377,NA,15.75570000000000,NA,0.24110000000000,5.50070000000000,5.5007,NA,55454324357,13.5431,13.3020,0.2411,0.1296,18.6731,0.9864,17.6867,0.0000,NA,2283.8616,NA,NA,NA,NA Presenter.exe,5236,0x285B4C0D1B0,DXGI,0,0,1,Hardware: Independent Flip,Application,55454612236,NA,15.48590000000000,10.29460000000000,0.30560000000000,-7.97160000000000,0.3094,NA,55454459788,15.5504,15.2448,0.3056,6.5527,0.7205,0.7205,0.0000,0.0000,3.2485,2297.4047,NA,NA,NA,NA Presenter.exe,5236,0x285B4C0D1B0,DXGI,0,0,1,Hardware: Independent Flip,Application,55454769193,NA,15.69570000000000,15.62230000000000,0.20100000000000,-13.73470000000000,0.2360,NA,55454615292,15.5911,15.3901,0.2010,0.8622,0.7932,0.7932,0.0000,0.0000,-0.0719,2312.9551,NA,NA,NA,NA Presenter.exe,5236,0x285B4C0D1B0,DXGI,0,0,1,Hardware: Independent Flip,Application,55454924961,NA,15.57680000000000,15.82180000000000,0.44410000000000,-14.07780000000000,0.4810,NA,55454771203,15.8199,15.3758,0.4441,0.4673,0.8307,0.8307,0.0000,0.0000,-0.2307,2328.5462,NA,NA,NA,NA diff --git a/Tests/Gold/test_case_4_v2.csv b/Tests/Gold/test_case_4_v2.csv index 3719e4550..679972a74 100644 --- a/Tests/Gold/test_case_4_v2.csv +++ b/Tests/Gold/test_case_4_v2.csv @@ -91,7 +91,7 @@ dwm.exe,10376,0x1BF098B1ED0,DXGI,1,0,0,Hardware: Legacy Flip,Application,5545368 dwm.exe,10376,0x1BF098B1ED0,DXGI,1,0,0,Hardware: Legacy Flip,Application,55453857940,16.0218,15.9155,0.1063,0.0000,17.8749,2.6085,15.2664,0.0000,31.9243,16.6704,0.4262,2237.2199,NA,NA,NA,NA dwm.exe,10376,0x1BF098B1ED0,DXGI,1,0,0,Hardware: Legacy Flip,Application,55454018158,20.2905,20.2157,0.0748,0.0000,20.6730,1.8226,18.8504,0.0000,32.5729,18.0375,-0.6486,2253.2417,NA,NA,NA,NA Presenter.exe,5236,0x285B4C0D1B0,DXGI,0,0,0,Composed: Flip,Application,55454168764,15.5593,13.1056,2.4537,0.8807,0.8945,0.8945,0.0000,0.0000,NA,NA,NA,2268.3023,NA,NA,NA,NA -Presenter.exe,5236,0x285B4C0D1B0,DXGI,0,0,1,Hardware: Independent Flip,Application,55454324357,13.5431,13.3020,0.2411,0.1296,18.6731,0.9864,17.6867,0.0000,18.8027,10.2946,-1844674407370938.5000,2283.8616,NA,NA,NA,NA +Presenter.exe,5236,0x285B4C0D1B0,DXGI,0,0,1,Hardware: Independent Flip,Application,55454324357,13.5431,13.3020,0.2411,0.1296,18.6731,0.9864,17.6867,0.0000,18.8027,10.2946,NA,2283.8616,NA,NA,NA,NA Presenter.exe,5236,0x285B4C0D1B0,DXGI,0,0,1,Hardware: Independent Flip,Application,55454459788,15.5504,15.2448,0.3056,6.5527,0.7205,0.7205,0.0000,0.0000,15.5542,15.6223,3.2485,2297.4047,NA,NA,NA,NA Presenter.exe,5236,0x285B4C0D1B0,DXGI,0,0,1,Hardware: Independent Flip,Application,55454615292,15.5911,15.3901,0.2010,0.8622,0.7932,0.7932,0.0000,0.0000,15.6261,15.8218,-0.0719,2312.9551,NA,NA,NA,NA Presenter.exe,5236,0x285B4C0D1B0,DXGI,0,0,1,Hardware: Independent Flip,Application,55454771203,15.8199,15.3758,0.4441,0.4673,0.8307,0.8307,0.0000,0.0000,15.8568,15.4013,-0.2307,2328.5462,NA,NA,NA,NA diff --git a/Tests/GoldEtlCsvTests.cpp b/Tests/GoldEtlCsvTests.cpp index 494ad4d9f..adcc2300a 100644 --- a/Tests/GoldEtlCsvTests.cpp +++ b/Tests/GoldEtlCsvTests.cpp @@ -92,7 +92,11 @@ class Tests : public ::testing::Test, TestArgs { const char* goldDecimalAddr = strchr(b, '.'); size_t testDecimalNumbersCount = testDecimalAddr == nullptr ? 0 : ((a + strlen(a)) - testDecimalAddr - 1); size_t goldDecimalNumbersCount = goldDecimalAddr == nullptr ? 0 : ((b + strlen(b)) - goldDecimalAddr - 1); - double threshold = pow(0.1, std::min(testDecimalNumbersCount, goldDecimalNumbersCount)); + size_t decimals = std::min(testDecimalNumbersCount, goldDecimalNumbersCount); + // Cap precision used for tolerance to avoid ultra-long double-to-string artifacts + // (e.g., 100.06490000000001 vs 100.06489999999999) + decimals = std::min(decimals, size_t(9)); + double threshold = pow(0.1, decimals); double difference = testNumber - goldNumber; if (difference > -threshold && difference < threshold) { diff --git a/Tests/aux-data.lock.json b/Tests/aux-data.lock.json index 5c2e87210..43ad67966 100644 --- a/Tests/aux-data.lock.json +++ b/Tests/aux-data.lock.json @@ -1,3 +1,3 @@ { - "pinnedCommitHash": "340d15ae64a23c54dded7793805f01dc2fcb4166" + "pinnedCommitHash": "1ef35d437a4bece0d8a1beca13da4fdcf98cd5ca" } diff --git a/Tools/generate/EnumMetric/metrics.awk b/Tools/generate/EnumMetric/metrics.awk index 77d77e896..b3750f561 100644 --- a/Tools/generate/EnumMetric/metrics.awk +++ b/Tools/generate/EnumMetric/metrics.awk @@ -29,7 +29,6 @@ BEGIN{ gsub(/ /, "", shortname) gsub(/-/, "", shortname) - if (shortname == "ProcessID") next if (shortname == "VideoBusy") next printf " \\\n\t\tX_(METRIC, %s, \"%s\", \"%s\", \"%s\")", substr(a[1], 11), name, shortname, desc diff --git a/vcpkg.json b/vcpkg.json index 1d3f311b7..88fd9a893 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -7,6 +7,7 @@ "cli11", "boost-interprocess", "boost-process", + "boost-circular-buffer", "cereal", "concurrentqueue", "boost-asio",