From 7a33711f8ceb56760939a06b37acfe1b49ba6041 Mon Sep 17 00:00:00 2001 From: William Belcher Date: Tue, 12 Aug 2025 17:57:57 +1000 Subject: [PATCH 01/35] Large Refactor. Reduce public facing code. Add editor based options. Fix incorrect memory use calculation --- Plugins/Buccaneer/Buccaneer.uplugin | 4 +- Plugins/Buccaneer/README.md | 93 --- .../BuccaneerCommon/BuccaneerCommon.build.cs | 5 +- .../Private/BuccaneerCommon.cpp | 189 ------ .../Private/BuccaneerCommonModule.cpp | 122 ++++ .../Private/BuccaneerCommonModule.h | 28 + .../Private/BuccaneerSettings.cpp | 555 ++++++++++++++++++ .../BuccaneerCommon/Private/Logging.cpp | 5 + .../Source/BuccaneerCommon/Private/Logging.h | 8 + .../BuccaneerCommon/Public/BuccaneerCommon.h | 48 -- .../Public/BuccaneerSettings.h | 91 +++ .../Public/IBuccaneerCommonModule.h | 61 ++ .../BuccaneerEvents.build.cs} | 4 +- ...uccaneerEventsBlueprintFunctionLibrary.cpp | 8 + .../BuccaneerEventsBlueprintFunctionLibrary.h | 18 + .../Private/BuccaneerEventsModule.cpp | 35 ++ .../Private/BuccaneerEventsModule.h | 14 + .../BuccaneerEvents/Private/Logging.cpp | 5 + .../Source/BuccaneerEvents/Private/Logging.h | 10 + .../Public/IBuccaneerEventsModule.h | 36 ++ .../BuccaneerStats.build.cs} | 8 +- .../Private/BuccaneerStatsModule.cpp} | 81 ++- .../Private/BuccaneerStatsModule.h} | 13 +- .../Source/BuccaneerStats/Private/Logging.cpp | 5 + .../Source/BuccaneerStats/Private/Logging.h | 10 + .../Public/IBuccaneerStatsModule.h | 33 ++ .../Private/SemanticEventEmitter.cpp | 57 -- .../SemanticEventBlueprintFunctionLibrary.cpp | 13 - .../SemanticEventBlueprintFunctionLibrary.h | 17 - .../Public/SemanticEventEmitter.h | 22 - 30 files changed, 1099 insertions(+), 499 deletions(-) delete mode 100644 Plugins/Buccaneer/README.md delete mode 100644 Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommon.cpp create mode 100644 Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp create mode 100644 Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h create mode 100644 Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp create mode 100644 Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.cpp create mode 100644 Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.h delete mode 100644 Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerCommon.h create mode 100644 Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h create mode 100644 Plugins/Buccaneer/Source/BuccaneerCommon/Public/IBuccaneerCommonModule.h rename Plugins/Buccaneer/Source/{SemanticEventEmitter/SemanticEventEmitter.build.cs => BuccaneerEvents/BuccaneerEvents.build.cs} (74%) create mode 100644 Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.cpp create mode 100644 Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.h create mode 100644 Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.cpp create mode 100644 Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.h create mode 100644 Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.cpp create mode 100644 Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.h create mode 100644 Plugins/Buccaneer/Source/BuccaneerEvents/Public/IBuccaneerEventsModule.h rename Plugins/Buccaneer/Source/{TimeSeriesDataEmitter/TimeSeriesDataEmitter.build.cs => BuccaneerStats/BuccaneerStats.build.cs} (66%) rename Plugins/Buccaneer/Source/{TimeSeriesDataEmitter/Private/TimeSeriesDataEmitter.cpp => BuccaneerStats/Private/BuccaneerStatsModule.cpp} (66%) rename Plugins/Buccaneer/Source/{TimeSeriesDataEmitter/Public/TimeSeriesDataEmitter.h => BuccaneerStats/Private/BuccaneerStatsModule.h} (83%) create mode 100644 Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.cpp create mode 100644 Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.h create mode 100644 Plugins/Buccaneer/Source/BuccaneerStats/Public/IBuccaneerStatsModule.h delete mode 100644 Plugins/Buccaneer/Source/SemanticEventEmitter/Private/SemanticEventEmitter.cpp delete mode 100644 Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.cpp delete mode 100644 Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.h delete mode 100644 Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventEmitter.h diff --git a/Plugins/Buccaneer/Buccaneer.uplugin b/Plugins/Buccaneer/Buccaneer.uplugin index 3766b88..4b7fbd5 100644 --- a/Plugins/Buccaneer/Buccaneer.uplugin +++ b/Plugins/Buccaneer/Buccaneer.uplugin @@ -21,12 +21,12 @@ "LoadingPhase": "Default" }, { - "Name": "TimeSeriesDataEmitter", + "Name": "BuccaneerStats", "Type": "Runtime", "LoadingPhase": "Default" }, { - "Name": "SemanticEventEmitter", + "Name": "BuccaneerEvents", "Type": "Runtime", "LoadingPhase": "Default" } diff --git a/Plugins/Buccaneer/README.md b/Plugins/Buccaneer/README.md deleted file mode 100644 index 08c3d3d..0000000 --- a/Plugins/Buccaneer/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# Buccaneer - - - -## Getting started - -To make it easy for you to get started with GitLab, here's a list of recommended next steps. - -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - -## Add your files - -- [ ] [Create](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: - -``` -cd existing_repo -git remote add origin https://gitlab.com/TensorWorks/internal/buccaneer.git -git branch -M main -git push -uf origin main -``` - -## Integrate with your tools - -- [ ] [Set up project integrations](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://gitlab.com/TensorWorks/internal/buccaneer/-/settings/integrations) - -## Collaborate with your team - -- [ ] [Invite team members and collaborators](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Automatically merge when pipeline succeeds](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) - -## Test and Deploy - -Use the built-in continuous integration in GitLab. - -- [ ] [Get started with GitLab CI/CD](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://docs.gitlab.com/ee/ci/environments/protected_environments.html) - -*** - -# Editing this README - -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://gitlab.com/-/experiment/new_project_readme_content:8e73215e80fadca4226373d8725698de?https://www.makeareadme.com/) for this template. - -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - -## Name -Choose a self-explaining name for your project. - -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. - -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. - -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. - -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. - -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. - -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. - -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. - -## Contributing -State if you are open to contributions and what your requirements are for accepting them. - -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. - -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. - -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. - -## License -For open source projects, say how it is licensed. - -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. - diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/BuccaneerCommon.build.cs b/Plugins/Buccaneer/Source/BuccaneerCommon/BuccaneerCommon.build.cs index 9dfc4f1..be796ee 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/BuccaneerCommon.build.cs +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/BuccaneerCommon.build.cs @@ -1,4 +1,4 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. using UnrealBuildTool; @@ -17,6 +17,9 @@ public BuccaneerCommon(ReadOnlyTargetRules Target) : base(Target) PublicDependencyModuleNames.AddRange(new string[]{ "HTTP", + "CoreUObject", + "DeveloperSettings", + "EngineSettings" }); } } diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommon.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommon.cpp deleted file mode 100644 index 9c613c6..0000000 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommon.cpp +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#include "BuccaneerCommon.h" -#include "Logging/LogMacros.h" - -DEFINE_LOG_CATEGORY(BuccaneerCommon); - -FBuccaneerCommonModule *FBuccaneerCommonModule::BuccaneerCommonModule = nullptr; - -void FBuccaneerCommonModule::StartupModule() -{ - CVarBuccaneerEnableStats = IConsoleManager::Get().RegisterConsoleVariable( - TEXT("Buccaneer.EnableStats"), - true, - TEXT("Disables the collection of and logging of performance metrics"), - ECVF_Default); - - CVarBuccaneerEnableEvents = IConsoleManager::Get().RegisterConsoleVariable( - TEXT("Buccaneer.EnableEvents"), - true, - TEXT("Disables the collection and logging of semantic events"), - ECVF_Default); - - Setup(); -} - -void FBuccaneerCommonModule::ShutdownModule() -{ -} - -void FBuccaneerCommonModule::Setup() -{ - ParseCommandLineOption(TEXT("BuccaneerEnableStats"), CVarBuccaneerEnableStats); - ParseCommandLineOption(TEXT("BuccaneerEnableEvents"), CVarBuccaneerEnableEvents); - - if (!FParse::Value(FCommandLine::Get(), TEXT("BuccaneerURL="), BuccaneerURL)) - { - FString BuccaneerIP; - uint16 BuccaneerPort; - if (FParse::Value(FCommandLine::Get(), TEXT("BuccaneerIP="), BuccaneerIP) && FParse::Value(FCommandLine::Get(), TEXT("BuccaneerPort="), BuccaneerPort)) - { - // build the proper url. - BuccaneerURL = FString::Printf(TEXT("http://%s:%d"), *BuccaneerIP, BuccaneerPort); - } - } - - if (BuccaneerURL.IsEmpty()) - { - UE_LOG(BuccaneerCommon, Warning, TEXT("Buccanner events and stats disabled, provide `BuccaneerURL` cmd-args to enable it")); - CVarBuccaneerEnableEvents->Set(false, ECVF_SetByCommandline); - CVarBuccaneerEnableStats->Set(false, ECVF_SetByCommandline); - return; - } - - // Try and parse an instance ID - if (!FParse::Value(FCommandLine::Get(), TEXT("BuccaneerID="), InstanceID)) - { - // Try and parse a pixel streaming ID for users who don't want to pollute their command line by specifying two IDs - if (!FParse::Value(FCommandLine::Get(), TEXT("PixelStreamingID="), InstanceID)) - { - // Generate an instance ID if one isn't provided - InstanceID = FGuid::NewGuid().ToString(); - } - } - - // Additional Metadata - MetadataJson = MakeShareable(new FJsonObject()); - FString CmdLineMetadata; - if (FParse::Value(FCommandLine::Get(), TEXT("BuccaneerMetadata="), CmdLineMetadata)) - { - UE_LOG(BuccaneerCommon, Warning, TEXT("%s"), *CmdLineMetadata); - TArray ParsedMetadata; - CmdLineMetadata.ParseIntoArray(ParsedMetadata, TEXT(";"), false); - for (FString Element : ParsedMetadata) - { - if(Element.IsEmpty()) - { - continue; - } - - FString Key, Value; - Element.Split(TEXT(":"), &Key, &Value); - if(Key.IsEmpty() || Value.IsEmpty()) - { - continue; - } - - MetadataJson->SetField(*Key, MakeShared((TEXT("%s"), *Value))); - } - } - - SetupComplete.Broadcast(); -} - -FBuccaneerCommonModule *FBuccaneerCommonModule::GetModule() -{ - if (BuccaneerCommonModule) - { - return BuccaneerCommonModule; - } - FBuccaneerCommonModule *Module = FModuleManager::Get().LoadModulePtr("BuccaneerCommon"); - if (Module) - { - BuccaneerCommonModule = Module; - } - return BuccaneerCommonModule; -} - -void FBuccaneerCommonModule::SendStats(TSharedPtr JsonObject) -{ - JsonObject->SetField("id", MakeShared((TEXT("%s"), *InstanceID))); - JsonObject->SetField("metadata", MakeShared(MetadataJson)); - SendHTTP(BuccaneerURL + FString("/stats"), JsonObject); -} - -void FBuccaneerCommonModule::SendEvent(TSharedPtr JsonObject) -{ - JsonObject->SetField("id", MakeShared((TEXT("%s"), *InstanceID))); - SendHTTP(BuccaneerURL + FString("/event"), JsonObject); -} - -void FBuccaneerCommonModule::SendHTTP(FString URL, TSharedPtr JsonObject) -{ - FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); - - FString body; - TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&body); - if (!ensure(FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter))) - { - UE_LOG(BuccaneerCommon, Warning, TEXT("Cannot serialize json object")); - } - - HttpRequest->SetURL(URL); - HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - HttpRequest->SetVerb(TEXT("POST")); - HttpRequest->SetContentAsString(body); - bool bInFlight = true; - HttpRequest->OnProcessRequestComplete().BindLambda( - [&bInFlight](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) - { - FString ResponseStr, ErrorStr; - - if (bSucceeded && HttpResponse.IsValid()) - { - ResponseStr = HttpResponse->GetContentAsString(); - if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) - { - ErrorStr = FString::Printf(TEXT("Invalid response. code=%d error=%s"), - HttpResponse->GetResponseCode(), *ResponseStr); - } - } - else - { - ErrorStr = TEXT("No response"); - } - - if (!ErrorStr.IsEmpty()) - { - UE_LOG(BuccaneerCommon, Warning, TEXT("Push event response: %s"), *ErrorStr); - } - - bInFlight = false; - }); - HttpRequest->ProcessRequest(); -} - -void FBuccaneerCommonModule::ParseCommandLineOption(const TCHAR *Match, IConsoleVariable *CVar) -{ - FString ValueMatch(Match); - ValueMatch.Append(TEXT("=")); - FString Value; - if (FParse::Value(FCommandLine::Get(), *ValueMatch, Value)) - { - if (Value.Equals(FString(TEXT("true")), ESearchCase::IgnoreCase)) - { - CVar->Set(true, ECVF_SetByCommandline); - } - else if (Value.Equals(FString(TEXT("false")), ESearchCase::IgnoreCase)) - { - CVar->Set(false, ECVF_SetByCommandline); - } - } - else if (FParse::Param(FCommandLine::Get(), Match)) - { - CVar->Set(true, ECVF_SetByCommandline); - } -} - -IMPLEMENT_MODULE(FBuccaneerCommonModule, BuccaneerCommon) \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp new file mode 100644 index 0000000..d75d5fc --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -0,0 +1,122 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "BuccaneerCommonModule.h" + +#include "BuccaneerSettings.h" +#include "HttpModule.h" +#include "Interfaces/IHttpRequest.h" +#include "Interfaces/IHttpResponse.h" +#include "Logging.h" + +void FBuccaneerCommonModule::StartupModule() +{ + if (UBuccaneerSettings::CVarURL.GetValueOnAnyThread().IsEmpty()) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Buccanner events and stats disabled, provide `BuccaneerURL` cmd-args to enable it"); + UBuccaneerSettings::CVarEnableStats->Set(false, ECVF_SetByCommandline); + UBuccaneerSettings::CVarEnableEvents->Set(false, ECVF_SetByCommandline); + return; + } + + FString InstanceIDOverride; + // Try and parse a pixel streaming ID for users who don't want to pollute their command line by specifying two IDs + if (FParse::Value(FCommandLine::Get(), TEXT("PixelStreamingID="), InstanceIDOverride)) + { + UBuccaneerSettings::CVarID->Set(*InstanceIDOverride, ECVF_SetByCommandline); + } + + if (UBuccaneerSettings::FDelegates* Delegates = UBuccaneerSettings::Delegates()) + { + Delegates->OnMetadataChanged.AddRaw(this, &FBuccaneerCommonModule::FormatMetadata); + } + + FormatMetadata(nullptr); + + bModuleReady = true; + ReadyEvent.Broadcast(*this); +} + +void FBuccaneerCommonModule::ShutdownModule() +{ +} + +FBuccaneerCommonModule::FReadyEvent& FBuccaneerCommonModule::OnReady() +{ + return ReadyEvent; +} + +bool FBuccaneerCommonModule::IsReady() +{ + return bModuleReady; +} + +void FBuccaneerCommonModule::SendStats(TSharedPtr JsonObject) +{ + JsonObject->SetField("id", MakeShared((TEXT("%s"), *UBuccaneerSettings::CVarID.GetValueOnAnyThread()))); + JsonObject->SetField("metadata", MakeShared(MetadataJson)); + SendHTTP(UBuccaneerSettings::CVarURL.GetValueOnAnyThread() + FString("/stats"), JsonObject); +} + +void FBuccaneerCommonModule::SendEvent(TSharedPtr JsonObject) +{ + JsonObject->SetField("id", MakeShared((TEXT("%s"), *UBuccaneerSettings::CVarID.GetValueOnAnyThread()))); + SendHTTP(UBuccaneerSettings::CVarURL.GetValueOnAnyThread() + FString("/event"), JsonObject); +} + +void FBuccaneerCommonModule::SendHTTP(FString URL, TSharedPtr JsonObject) +{ + FHttpRequestRef HttpRequest = FHttpModule::Get().CreateRequest(); + + FString Body; + TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&Body); + if (!ensure(FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter))) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Cannot serialize json object"); + } + + HttpRequest->SetURL(URL); + HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + HttpRequest->SetVerb(TEXT("POST")); + HttpRequest->SetContentAsString(Body); + HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) + { + FString ResponseStr, ErrorStr; + + if (bSucceeded && HttpResponse.IsValid()) + { + ResponseStr = HttpResponse->GetContentAsString(); + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorStr = FString::Printf(TEXT("Invalid response. code=%d error=%s"), HttpResponse->GetResponseCode(), *ResponseStr); + } + } + else + { + ErrorStr = TEXT("No response"); + } + + if (!ErrorStr.IsEmpty()) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Push event response: {0}", *ErrorStr); + } + }); + + HttpRequest->ProcessRequest(); +} + +void FBuccaneerCommonModule::FormatMetadata(IConsoleVariable* Var) +{ + // Additional Metadata + TMap MetadataMap = UBuccaneerSettings::GetMetadata(); + for (const TPair& Pair : MetadataMap) + { + if(Pair.Key.IsEmpty() || Pair.Value.IsEmpty()) + { + continue; + } + + MetadataJson->SetField(*Pair.Key, MakeShared((TEXT("%s"), *Pair.Value))); + } +} + +IMPLEMENT_MODULE(FBuccaneerCommonModule, BuccaneerCommon) \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h new file mode 100644 index 0000000..018bb5a --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h @@ -0,0 +1,28 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "IBuccaneerCommonModule.h" + +class FBuccaneerCommonModule : public IBuccaneerCommonModule +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + virtual FReadyEvent& OnReady() override; + virtual bool IsReady() override; + virtual void SendStats(TSharedPtr JsonObject) override; + virtual void SendEvent(TSharedPtr JsonObject) override; + +private: + bool bModuleReady = false; + FReadyEvent ReadyEvent; + +private: + void SendHTTP(FString URL, TSharedPtr JsonObject); + void FormatMetadata(IConsoleVariable* Var); + + TSharedPtr MetadataJson = MakeShareable(new FJsonObject()); +}; diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp new file mode 100644 index 0000000..31d9c83 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp @@ -0,0 +1,555 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "BuccaneerSettings.h" + +#include "Logging.h" +#include "Misc/CommandLine.h" +#include "UObject/ReflectedTypeAccessors.h" + +namespace Util +{ + FString ConsoleVariableToCommandArgValue(const FString InCVarName) + { + // CVars are . deliminated by section. To get their equivilent commandline arg for parsing + // we need to remove the . and add a "=" + return InCVarName.Replace(TEXT("."), TEXT("")).Append(TEXT("=")); + } + + FString ConsoleVariableToCommandArgParam(const FString InCVarName) + { + // CVars are . deliminated by section. To get their equivilent commandline arg parameter, we need to to remove the . + return InCVarName.Replace(TEXT("."), TEXT("")); + } + + FString FindCVarFromProperty(const TSet> Set, const FString& Value) + { + for (const TPair& Pair : Set) + { + if (Pair.Value == Value) + { + return Pair.Key; + } + } + + return ""; + } + + void SetMetadataCVarFromProperty(UObject* This, FProperty* Property) + { + if (FMapProperty* MapProperty = CastField(Property)) + { + FString CVarString = ""; + + TMap& Map = *MapProperty->ContainerPtrToValuePtr>(This); + for (const TPair& Pair : Map) + { + if (Pair.Key.IsEmpty() || Pair.Value.IsEmpty()) + { + continue; + } + + CVarString += FString::Printf(TEXT("%s:%s;"), *Pair.Key, *Pair.Value); + } + + UBuccaneerSettings::CVarMetadata->Set(*CVarString, ECVF_SetByProjectSetting); + + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [Buccaneer.Metadata] to [\"{1}\"] from Property [Metadata]", CVarString); + } + } + + void SetMetadataCVarAndPropertyFromValue(UObject* This, FProperty* Property, const FString& CmdValue) + { + UBuccaneerSettings::CVarMetadata->Set(*CmdValue, ECVF_SetByCommandline); + + if (FMapProperty* MapProperty = CastField(Property)) + { + TMap& Map = *MapProperty->ContainerPtrToValuePtr>(This); + + TArray Pairs; + CmdValue.ParseIntoArray(Pairs, TEXT(";"), true); + for (FString Pair : Pairs) + { + if (Pair.IsEmpty()) + { + continue; + } + + FString Key, Value; + Pair.Split(TEXT(":"), &Key, &Value); + if(!Key.IsEmpty() && Value.IsEmpty()) + { + Map.Add(Key, Value); + } + } + } + + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [Buccaneer.Metadata] and Property [Metadata] to [{0}] from command line", CmdValue); + } +} + +static const TSet> GetCmdArg = { + { "Buccaneer.URL", "URL" }, + { "Buccaneer.ID", "ID" }, + { "Buccaneer.EnableStats", "EnableStats" }, + { "Buccaneer.EnableEvents", "EnableEvents" }, + { "Buccaneer.Metadata", "Metadata" } +}; + +// Map a legacy cvar to its new property +static const TSet> GetLegacyCmdArg = { + { "Buccaneer.IP", "URL" }, // Moved to URL + { "Buccaneer.Port", "URL" } // Moved to URL +}; + +TAutoConsoleVariable UBuccaneerSettings::CVarURL( + TEXT("Buccaneer.URL"), + TEXT(""), + TEXT("URL to send stats and events to. This should be a URL to a Buccaneer Server"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneerSettings::CVarID( + TEXT("Buccaneer.ID"), + TEXT(""), + TEXT("ID to identify this instance. Defaults to a new GUID"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneerSettings::CVarEnableStats( + TEXT("Buccaneer.EnableStats"), + true, + TEXT("Enables the collection of performance metrics (default: true)"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneerSettings::CVarEnableEvents( + TEXT("Buccaneer.EnableEvents"), + true, + TEXT("Enables the collection of semantic events (default: true)"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneerSettings::CVarMetadata( + TEXT("Buccaneer.Metadata"), + TEXT(""), + TEXT(""), + FConsoleVariableDelegate::CreateLambda([](IConsoleVariable* Var) { Delegates()->OnMetadataChanged.Broadcast(Var); }), + ECVF_Default); + +UBuccaneerSettings::FDelegates* UBuccaneerSettings::DelegateSingleton = nullptr; + +UBuccaneerSettings::FDelegates* UBuccaneerSettings::Delegates() +{ + if (DelegateSingleton == nullptr && !IsEngineExitRequested()) + { + DelegateSingleton = new UBuccaneerSettings::FDelegates(); + return DelegateSingleton; + } + return DelegateSingleton; +} + +UBuccaneerSettings::~UBuccaneerSettings() +{ + DelegateSingleton = nullptr; +} + +TMap UBuccaneerSettings::GetMetadata() +{ + TMap MetadataMap; + + FString MetadataString = CVarMetadata.GetValueOnAnyThread(); + if (!MetadataString.IsEmpty()) + { + TArray Pairs; + MetadataString.ParseIntoArray(Pairs, TEXT(";"), true); + for (FString Pair : Pairs) + { + if (Pair.IsEmpty()) + { + continue; + } + + FString Key, Value; + Pair.Split(TEXT(":"), &Key, &Value); + if(!Key.IsEmpty() && Value.IsEmpty()) + { + MetadataMap.Add(Key, Value); + } + } + } + + return MetadataMap; +} + +FName UBuccaneerSettings::GetCategoryName() const +{ + return TEXT("Plugins"); +} + +#if WITH_EDITOR +FText UBuccaneerSettings::GetSectionText() const +{ + return NSLOCTEXT("BuccaneerPlugin", "BuccaneerSettingsSection", "Buccaneer"); +} + +void UBuccaneerSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + FString PropertyName = PropertyChangedEvent.Property->GetNameCPP(); + + FString CVarName; + if (CVarName = Util::FindCVarFromProperty(GetCmdArg, PropertyName); !CVarName.IsEmpty()) + { + if (PropertyName == "Metadata") + { + Util::SetMetadataCVarFromProperty(this, PropertyChangedEvent.Property); + } + else + { + SetCVarFromProperty(CVarName, PropertyChangedEvent.Property); + } + } +} +#endif + +void UBuccaneerSettings::SetCVarAndPropertyFromValue(const FString& CVarName, FProperty* Property, const FString& Value) +{ + IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (!CVar) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Failed to find CVar: {0}", CVarName); + return; + } + + if (FByteProperty* ByteProperty = CastField(Property); ByteProperty != NULL && ByteProperty->Enum != NULL) + { + CVar->Set(FCString::Atoi(*Value), ECVF_SetByCommandline); + ByteProperty->SetPropertyValue_InContainer(this, FCString::Atoi(*Value)); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atoi(*Value)); + } + else if (FEnumProperty* EnumProperty = CastField(Property)) + { + int64 EnumIndex = EnumProperty->GetEnum()->GetIndexByNameString(Value.Replace(TEXT("_"), TEXT(""))); + if (EnumIndex != INDEX_NONE) + { + CVar->Set(*EnumProperty->GetEnum()->GetNameStringByIndex(EnumIndex), ECVF_SetByCommandline); + + FNumericProperty* UnderlyingProp = EnumProperty->GetUnderlyingProperty(); + int64* PropertyAddress = EnumProperty->ContainerPtrToValuePtr(this); + *PropertyAddress = EnumProperty->GetEnum()->GetValueByIndex(EnumIndex); + + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), EnumProperty->GetEnum()->GetNameStringByIndex(EnumIndex)); + } + else + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "{0} is not a valid enum value for {1}", Value, EnumProperty->GetEnum()->CppType); + } + } + else if (FBoolProperty* BoolProperty = CastField(Property)) + { + bool bValue = false; + if (Value.Equals(FString(TEXT("true")), ESearchCase::IgnoreCase)) + { + bValue = true; + } + else if (Value.Equals(FString(TEXT("false")), ESearchCase::IgnoreCase)) + { + bValue = false; + } + CVar->Set(bValue, ECVF_SetByCommandline); + BoolProperty->SetPropertyValue_InContainer(this, bValue); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), bValue); + } + else if (FIntProperty* IntProperty = CastField(Property)) + { + CVar->Set(FCString::Atoi(*Value), ECVF_SetByCommandline); + IntProperty->SetPropertyValue_InContainer(this, FCString::Atoi(*Value)); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atoi(*Value)); + } + else if (FFloatProperty* FloatProperty = CastField(Property)) + { + CVar->Set(FCString::Atof(*Value), ECVF_SetByCommandline); + FloatProperty->SetPropertyValue_InContainer(this, FCString::Atof(*Value)); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atof(*Value)); + } + else if (FStrProperty* StringProperty = CastField(Property)) + { + CVar->Set(*Value, ECVF_SetByCommandline); + StringProperty->SetPropertyValue_InContainer(this, *Value); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } + else if (FNameProperty* NameProperty = CastField(Property)) + { + CVar->Set(*Value, ECVF_SetByCommandline); + NameProperty->SetPropertyValue_InContainer(this, FName(*Value)); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } + else if (FArrayProperty* ArrayProperty = CastField(Property)) + { + // TODO (william.belcher): Only FString array properties are currently supported + CVar->Set(*Value, ECVF_SetByCommandline); + + TArray StringArray; + Value.ParseIntoArray(StringArray, TEXT(","), true); + + TArray& Array = *ArrayProperty->ContainerPtrToValuePtr>(this); + Array = StringArray; + + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } +} + +void UBuccaneerSettings::SetCVarFromProperty(const FString& CVarName, FProperty* Property) +{ + IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (!CVar) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Failed to find CVar: {0}", CVarName); + return; + } + + if (FByteProperty* ByteProperty = CastField(Property); ByteProperty != NULL && ByteProperty->Enum != NULL) + { + CVar->Set(ByteProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, ByteProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FEnumProperty* EnumProperty = CastField(Property)) + { + void* PropertyAddress = EnumProperty->ContainerPtrToValuePtr(this); + int64 CurrentValue = EnumProperty->GetUnderlyingProperty()->GetSignedIntPropertyValue(PropertyAddress); + CVar->Set(*EnumProperty->GetEnum()->GetNameStringByValue(CurrentValue), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, EnumProperty->GetEnum()->GetNameStringByValue(CurrentValue), Property->GetNameCPP()); + } + else if (FBoolProperty* BoolProperty = CastField(Property)) + { + CVar->Set(BoolProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, BoolProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FIntProperty* IntProperty = CastField(Property)) + { + CVar->Set(IntProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, IntProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FFloatProperty* FloatProperty = CastField(Property)) + { + CVar->Set(FloatProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, FloatProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FStrProperty* StringProperty = CastField(Property)) + { + CVar->Set(*StringProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, StringProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FNameProperty* NameProperty = CastField(Property)) + { + CVar->Set(*NameProperty->GetPropertyValue_InContainer(this).ToString(), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, NameProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FArrayProperty* ArrayProperty = CastField(Property)) + { + // TODO (william.belcher): Only FString array properties are currently supported + TArray Array = *ArrayProperty->ContainerPtrToValuePtr>(this); + CVar->Set(*FString::Join(Array, TEXT(",")), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneerCommon, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, FString::Join(Array, TEXT(",")), Property->GetNameCPP()); + } +} + +void UBuccaneerSettings::InitializeCVarsFromProperties() +{ + UE_LOGFMT(LogBuccaneerCommon, Log, "Initializing CVars from ini"); + for (FProperty* Property = GetClass()->PropertyLink; Property; Property = Property->PropertyLinkNext) + { + if (!Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + // Handle the majority of commandline argument + if (Property->GetNameCPP() == "Metadata") + { + Util::SetMetadataCVarFromProperty(this, Property); + continue; + } + + + FString CVarName; + if (CVarName = Util::FindCVarFromProperty(GetCmdArg, Property->GetNameCPP()); !CVarName.IsEmpty()) + { + SetCVarFromProperty(CVarName, Property); + continue; + } + } +} + +void UBuccaneerSettings::ValidateCommandLineArgs() +{ + FString CommandLine = FCommandLine::Get(); + + TArray CommandArray; + CommandLine.ParseIntoArray(CommandArray, TEXT(" "), true); + + for (FString Command : CommandArray) + { + Command.RemoveFromStart(TEXT("-")); + if (!Command.StartsWith("Buccaneer")) + { + continue; + } + + // Get the pure command line arg from an arg that contains an '=', eg BuccaneerURL= + FString CurrentCommandLineArg = Command; + if (Command.Contains("=")) + { + Command.Split(TEXT("="), &CurrentCommandLineArg, nullptr); + } + + bool bValidArg = false; + for (const TPair& Pair : GetCmdArg) + { + FString ValidCommandLineArg = Util::ConsoleVariableToCommandArgParam(Pair.Key); + if (CurrentCommandLineArg == ValidCommandLineArg) + { + bValidArg = true; + break; + } + } + + if (!bValidArg) + { + for (const TPair& Pair : GetLegacyCmdArg) + { + FString ValidCommandLineArg = Util::ConsoleVariableToCommandArgParam(Pair.Key); + if (CurrentCommandLineArg == ValidCommandLineArg) + { + bValidArg = true; + break; + } + } + } + + if (!bValidArg) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Unknown Buccaneer command line arg: {0}", CurrentCommandLineArg); + } + } +} + +void UBuccaneerSettings::ParseCommandlineArgs() +{ + UE_LOGFMT(LogBuccaneerCommon, Verbose, "Updating CVars and properties with command line args"); + for (const TPair& Pair : GetCmdArg) + { + FString CVarString = Pair.Key; + FString PropertyName = Pair.Value; + + FProperty* Property = GetClass()->FindPropertyByName(FName(*PropertyName)); + if (!Property || !Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + if (PropertyName == "Metadata") + { + FString ConsoleString; + if (FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(CVarString), ConsoleString)) + { + Util::SetMetadataCVarAndPropertyFromValue(this, Property, ConsoleString); + } + continue; + } + + // Handle a directly parsable commandline + FString ConsoleString; + if (FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(CVarString), ConsoleString)) + { + SetCVarAndPropertyFromValue(CVarString, Property, ConsoleString); + } + else if (FParse::Param(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgParam(CVarString))) + { + SetCVarAndPropertyFromValue(CVarString, Property, TEXT("true")); + } + } +} + +void UBuccaneerSettings::ParseLegacyCommandlineArgs() +{ + FString BuccaneerIP; + FString BuccaneerPort; + + for (const TPair& Pair : GetLegacyCmdArg) + { + FString LegacyCVarString = Pair.Key; + FString PropertyName = Pair.Value; + + FProperty* Property = GetClass()->FindPropertyByName(FName(*PropertyName)); + if (!Property || !Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + FString NewCVarString; + if (FString CmdArgCVar = Util::FindCVarFromProperty(GetCmdArg, PropertyName); !CmdArgCVar.IsEmpty()) + { + NewCVarString = CmdArgCVar; + } + else + { + continue; + } + + if (LegacyCVarString == "Buccaneer.IP" || LegacyCVarString == "Buccaneer.Port") + { + if (LegacyCVarString == "Buccaneer.IP") + { + FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(LegacyCVarString), BuccaneerIP); + } + else if (LegacyCVarString == "Buccaneer.Port") + { + FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(LegacyCVarString), BuccaneerPort); + } + + if (!BuccaneerIP.IsEmpty() && !BuccaneerPort.IsEmpty()) + { + FString LegacyUrl = TEXT("http://") + BuccaneerIP + TEXT(":") + BuccaneerPort; + SetCVarAndPropertyFromValue(NewCVarString, Property, LegacyUrl); + UE_LOGFMT(LogBuccaneerCommon, Warning, "BuccaneerIP and BuccaneerPort are legacy settings converted to -BuccaneerURL={0}", CVarURL.GetValueOnAnyThread()); + } + + continue; + } + + FString ConsoleString; + if (FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(LegacyCVarString), ConsoleString)) + { + SetCVarAndPropertyFromValue(NewCVarString, Property, ConsoleString); + } + else if (FParse::Param(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgParam(LegacyCVarString))) + { + SetCVarAndPropertyFromValue(NewCVarString, Property, TEXT("true")); + } + else + { + continue; + } + + UE_LOGFMT(LogBuccaneerCommon, Warning, "{0} is a legacy setting and has been converted to {1}", Util::ConsoleVariableToCommandArgParam(LegacyCVarString), Util::ConsoleVariableToCommandArgParam(NewCVarString)); + } + + // End legacy buccaneer command line args +} + +void UBuccaneerSettings::PostInitProperties() +{ + Super::PostInitProperties(); + + UE_LOGFMT(LogBuccaneerCommon, Log, "Initialising Buccaneer settings."); + + // Set all the CVars to reflect the state of the ini + InitializeCVarsFromProperties(); + + // Validate command line args to log if they're invalid + ValidateCommandLineArgs(); + + // Update CVars and properties based on command line args + ParseCommandlineArgs(); + + // Handle parsing of legacy command line args (such as -PixelStreamingIP) after .ini and new commandline args. + ParseLegacyCommandlineArgs(); +} \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.cpp new file mode 100644 index 0000000..fac5a79 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.cpp @@ -0,0 +1,5 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Logging.h" + +DEFINE_LOG_CATEGORY(LogBuccaneerCommon); \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.h new file mode 100644 index 0000000..ed6022d --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/Logging.h @@ -0,0 +1,8 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" +#include "Logging/StructuredLog.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogBuccaneerCommon, Log, All); \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerCommon.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerCommon.h deleted file mode 100644 index 2c01570..0000000 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerCommon.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "Modules/ModuleManager.h" -#include "Interfaces/IHttpRequest.h" -#include "Interfaces/IHttpResponse.h" -#include "HttpModule.h" -#include "Dom/JsonObject.h" -#include "Serialization/JsonWriter.h" -#include "HAL/IConsoleManager.h" -#include "Misc/CommandLine.h" - -#include -#include - -DECLARE_MULTICAST_DELEGATE(FOnSetupComplete); - -DECLARE_LOG_CATEGORY_EXTERN(BuccaneerCommon, Log, All); - -class BUCCANEERCOMMON_API FBuccaneerCommonModule : public IModuleInterface -{ -public: - /** IModuleInterface implementation */ - virtual void StartupModule() override; - virtual void ShutdownModule() override; - - static FBuccaneerCommonModule *GetModule(); - static void ParseCommandLineOption(const TCHAR *Match, IConsoleVariable *CVar); - - void SendStats(TSharedPtr JsonObject); - void SendEvent(TSharedPtr JsonObject); - - FOnSetupComplete SetupComplete; - - IConsoleVariable *CVarBuccaneerEnableStats; - IConsoleVariable *CVarBuccaneerEnableEvents; - -private: - void Setup(); - - void SendHTTP(FString URL, TSharedPtr JsonObject); - - FString BuccaneerURL; - FString InstanceID; - TSharedPtr MetadataJson; - - static FBuccaneerCommonModule *BuccaneerCommonModule; -}; diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h new file mode 100644 index 0000000..834a486 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h @@ -0,0 +1,91 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Containers/UnrealString.h" +#include "CoreMinimal.h" +#include "Engine/DeveloperSettings.h" + +#include "BuccaneerSettings.generated.h" + +// Config loaded/saved to an .ini file. +// It is also exposed through the plugin settings page in editor. +UCLASS(config = Game, defaultconfig, meta = (DisplayName = "Buccaneer")) +class BUCCANEERCOMMON_API UBuccaneerSettings : public UDeveloperSettings +{ + GENERATED_BODY() + + virtual ~UBuccaneerSettings(); + +public: + static TAutoConsoleVariable CVarURL; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "URL", + ToolTip = "URL to send stats and events to. This should be a URL to a Buccaneer Server" + )) + FString URL; + + static TAutoConsoleVariable CVarID; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "ID", + ToolTip = "ID to identify this instance. Defaults to a new GUID" + )) + FString ID = FGuid::NewGuid().ToString(); + + static TAutoConsoleVariable CVarEnableStats; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "Enable Stats", + ToolTip = "Enables the collection of performance metrics" + )) + bool EnableStats = true; + + static TAutoConsoleVariable CVarEnableEvents; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "Enable Events", + ToolTip = "Enables the collection of semantic events" + )) + bool EnableEvents = true; + + static TAutoConsoleVariable CVarMetadata; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "Metadata", + ToolTip = "Key:Value pairs of metadata to send with this instance", + ForceInlineRow + )) + TMap Metadata; + + static TMap GetMetadata(); + + // Begin UDeveloperSettings Interface + virtual FName GetCategoryName() const override; + +#if WITH_EDITOR + virtual FText GetSectionText() const override; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + // End UDeveloperSettings Interface + + // Begin UObject Interface + virtual void PostInitProperties() override; + // End UObject Interface + + struct FDelegates + { + DECLARE_TS_MULTICAST_DELEGATE_OneParam(FOnMetadataChanged, IConsoleVariable*); + FOnMetadataChanged OnMetadataChanged; + }; + + static FDelegates* Delegates(); + +private: + void SetCVarAndPropertyFromValue(const FString& CVarName, FProperty* Property, const FString& Value); + void SetCVarFromProperty(const FString& CVarName, FProperty* Property); + + void InitializeCVarsFromProperties(); + void ValidateCommandLineArgs(); + void ParseCommandlineArgs(); + void ParseLegacyCommandlineArgs(); + + static FDelegates* DelegateSingleton; + +}; \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/IBuccaneerCommonModule.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/IBuccaneerCommonModule.h new file mode 100644 index 0000000..99dcf7d --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/IBuccaneerCommonModule.h @@ -0,0 +1,61 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "CoreTypes.h" +#include "Dom/JsonObject.h" +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" + +class BUCCANEERCOMMON_API IBuccaneerCommonModule : public IModuleInterface +{ +public: + /** + * Singleton-like access to this module's interface. + * Beware calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline IBuccaneerCommonModule& Get() + { + return FModuleManager::LoadModuleChecked("BuccaneerCommon"); + } + + /** + * Checks to see if this module is loaded. + * + * @return True if the module is loaded. + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("BuccaneerCommon"); + } + + /** + * Event fired when internal streamer is initialized and the methods on this module are ready for use. + */ + DECLARE_EVENT_OneParam(IBuccaneerCommonModule, FReadyEvent, IBuccaneerCommonModule&); + + /** + * A getter for the OnReady event. Intent is for users to call IBuccaneerCommonModule::Get().OnReady().AddXXX. + * @return The bindable OnReady event. + */ + virtual FReadyEvent& OnReady() = 0; + + /** + * Is the BuccaneerCommon module actually ready to use? Is the streamer created. + * @return True if BuccaneerCommon module methods are ready for use. + */ + virtual bool IsReady() = 0; + + /** + * + */ + virtual void SendStats(TSharedPtr JsonObject) = 0; + + /** + * + */ + virtual void SendEvent(TSharedPtr JsonObject) = 0; + +}; \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/SemanticEventEmitter/SemanticEventEmitter.build.cs b/Plugins/Buccaneer/Source/BuccaneerEvents/BuccaneerEvents.build.cs similarity index 74% rename from Plugins/Buccaneer/Source/SemanticEventEmitter/SemanticEventEmitter.build.cs rename to Plugins/Buccaneer/Source/BuccaneerEvents/BuccaneerEvents.build.cs index 93fd52a..382b29d 100644 --- a/Plugins/Buccaneer/Source/SemanticEventEmitter/SemanticEventEmitter.build.cs +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/BuccaneerEvents.build.cs @@ -2,9 +2,9 @@ using UnrealBuildTool; -public class SemanticEventEmitter : ModuleRules +public class BuccaneerEvents : ModuleRules { - public SemanticEventEmitter(ReadOnlyTargetRules Target) : base(Target) + public BuccaneerEvents(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.cpp b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.cpp new file mode 100644 index 0000000..85d87e6 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.cpp @@ -0,0 +1,8 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "BuccaneerEventsBlueprintFunctionLibrary.h" + +void UBuccaneerEventsBlueprintFunctionLibrary::EmitEvent(FString Level, FString Event) +{ + IBuccaneerEventsModule::Get().EmitEvent(Level, Event); +} \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.h b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.h new file mode 100644 index 0000000..7eca779 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsBlueprintFunctionLibrary.h @@ -0,0 +1,18 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "IBuccaneerEventsModule.h" + +#include "BuccaneerEventsBlueprintFunctionLibrary.generated.h" + +UCLASS() +class BUCCANEEREVENTS_API UBuccaneerEventsBlueprintFunctionLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() +public: + UFUNCTION(BlueprintCallable, Category="Buccaneer") + static void EmitEvent(FString Level, FString Event); +}; diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.cpp new file mode 100644 index 0000000..ae469b3 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.cpp @@ -0,0 +1,35 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "BuccaneerEventsModule.h" + +#include "CoreMinimal.h" +#include "Logging.h" +#include "Dom/JsonObject.h" +#include "IBuccaneerCommonModule.h" +#include "BuccaneerSettings.h" + +void FBuccaneerEventsModule::StartupModule() +{ +} + +void FBuccaneerEventsModule::ShutdownModule() +{ +} + +void FBuccaneerEventsModule::EmitEvent(FString Level, FString Event) +{ + if (!UBuccaneerSettings::CVarEnableEvents.GetValueOnAnyThread()) + { + return; + } + + UE_LOGFMT(LogBuccaneerEvents, Verbose, "{0}: {1}", Level, Event); + + TSharedPtr JsonObject = MakeShareable(new FJsonObject()); + JsonObject->SetField("level", MakeShared((TEXT("%s"), *Level))); + JsonObject->SetField("message", MakeShared((TEXT("%s"), *Event))); + + IBuccaneerCommonModule::Get().SendEvent(JsonObject); +} + +IMPLEMENT_MODULE(FBuccaneerEventsModule, BuccaneerEvents) \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.h b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.h new file mode 100644 index 0000000..0c93c41 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.h @@ -0,0 +1,14 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "IBuccaneerEventsModule.h" + +class FBuccaneerEventsModule : public IBuccaneerEventsModule +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + virtual void EmitEvent(FString Level, FString Event) override; +}; diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.cpp b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.cpp new file mode 100644 index 0000000..fc06cc9 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.cpp @@ -0,0 +1,5 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Logging.h" + +DEFINE_LOG_CATEGORY(LogBuccaneerEvents); \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.h b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.h new file mode 100644 index 0000000..4212d77 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/Logging.h @@ -0,0 +1,10 @@ + + +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" +#include "Logging/StructuredLog.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogBuccaneerEvents, Log, All); \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Public/IBuccaneerEventsModule.h b/Plugins/Buccaneer/Source/BuccaneerEvents/Public/IBuccaneerEventsModule.h new file mode 100644 index 0000000..bc34f0f --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Public/IBuccaneerEventsModule.h @@ -0,0 +1,36 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class BUCCANEEREVENTS_API IBuccaneerEventsModule : public IModuleInterface +{ +public: + /** + * Singleton-like access to this module's interface. + * Beware calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline IBuccaneerEventsModule& Get() + { + return FModuleManager::LoadModuleChecked("BuccaneerEvents"); + } + + /** + * Checks to see if this module is loaded. + * + * @return True if the module is loaded. + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("BuccaneerEvents"); + } + + /** + * + */ + virtual void EmitEvent(FString Level, FString Event) = 0; +}; diff --git a/Plugins/Buccaneer/Source/TimeSeriesDataEmitter/TimeSeriesDataEmitter.build.cs b/Plugins/Buccaneer/Source/BuccaneerStats/BuccaneerStats.build.cs similarity index 66% rename from Plugins/Buccaneer/Source/TimeSeriesDataEmitter/TimeSeriesDataEmitter.build.cs rename to Plugins/Buccaneer/Source/BuccaneerStats/BuccaneerStats.build.cs index 1a30dc7..3752d28 100644 --- a/Plugins/Buccaneer/Source/TimeSeriesDataEmitter/TimeSeriesDataEmitter.build.cs +++ b/Plugins/Buccaneer/Source/BuccaneerStats/BuccaneerStats.build.cs @@ -1,10 +1,10 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. using UnrealBuildTool; -public class TimeSeriesDataEmitter : ModuleRules +public class BuccaneerStats : ModuleRules { - public TimeSeriesDataEmitter(ReadOnlyTargetRules Target) : base(Target) + public BuccaneerStats(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; @@ -17,7 +17,7 @@ public TimeSeriesDataEmitter(ReadOnlyTargetRules Target) : base(Target) "Slate", "SlateCore", "RenderCore", - "SemanticEventEmitter", + "BuccaneerEvents", "Json" }); diff --git a/Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Private/TimeSeriesDataEmitter.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp similarity index 66% rename from Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Private/TimeSeriesDataEmitter.cpp rename to Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp index f2a0bb9..7eb79a3 100644 --- a/Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Private/TimeSeriesDataEmitter.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp @@ -1,35 +1,34 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. -#include "TimeSeriesDataEmitter.h" +#include "BuccaneerStatsModule.h" + +#include "BuccaneerSettings.h" #include "CoreMinimal.h" #include "Engine/Engine.h" +#include "IBuccaneerEventsModule.h" +#include "Logging.h" #include "RHI.h" -#include "SemanticEventEmitter.h" -#include "BuccaneerCommon.h" #include "Stats/Stats.h" #include "Stats/StatsData.h" -#define LOCTEXT_NAMESPACE "FTimeSeriesDataEmitterModule" #define COMPUTE_MEAN(CurrentMean, NewTime, FrameCount) \ ((FrameCount - 1) * CurrentMean + NewTime) / FrameCount; -DEFINE_LOG_CATEGORY(TimeSeriesDataEmitter); - -void FTimeSeriesDataEmitterModule::StartupModule() +TMap StatDescriptionMap = { + { "mean_fps", "The average fps" }, + { "mean_frametime", "The average frametime" }, + { "mean_gamethreadtime", "The average game thread time" }, + { "mean_gputime", "The average gpu time" }, + { "mean_rendertime", "The average render thread time" }, + { "mean_rhithreadtime", "The average rhi thread time" }, + { "memory_virtual", "The virtual memory usage" }, + { "memory_physical", "The physical memory usage" }, + { "memory_gpu", "The gpu memory usage" }, + { "num_hangs", "The number of frames hung in the recording interval" } +}; + +void FBuccaneerStatsModule::StartupModule() { - StatDescriptionMap = { - { "mean_fps", "The average fps" }, - { "mean_frametime", "The average frametime" }, - { "mean_gamethreadtime", "The average game thread time" }, - { "mean_gputime", "The average gpu time" }, - { "mean_rendertime", "The average render thread time" }, - { "mean_rhithreadtime", "The average rhi thread time" }, - { "memory_virtual", "The virtual memory usage" }, - { "memory_physical", "The physical memory usage" }, - { "memory_gpu", "The gpu memory usage" }, - { "num_hangs", "The number of frames hung in the recording interval" } - }; - MetricJson = MakeShareable(new FJsonObject()); JsonObject = MakeShareable(new FJsonObject()); JsonObject->SetField(TEXT("metrics"), MakeShared(MetricJson)); @@ -37,11 +36,11 @@ void FTimeSeriesDataEmitterModule::StartupModule() LastTickTime = InterimStart = FPlatformTime::Seconds(); } -void FTimeSeriesDataEmitterModule::UpdateMetric(FString Name, double Value) +void FBuccaneerStatsModule::UpdateMetric(FString Name, double Value) { if(!StatDescriptionMap.Contains(Name)) { - UE_LOG(TimeSeriesDataEmitter, Log, TEXT("No description for metric (%s)"), *Name); + UE_LOGFMT(LogBuccaneerStats, Log, "No description for metric {0}", Name); return; } @@ -51,25 +50,25 @@ void FTimeSeriesDataEmitterModule::UpdateMetric(FString Name, double Value) MetricJson->SetField(*Name, MakeShared(MetricInfoJson)); } -void FTimeSeriesDataEmitterModule::ShutdownModule() +void FBuccaneerStatsModule::ShutdownModule() { // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, // we call this function before unloading the module. } -bool FTimeSeriesDataEmitterModule::IsTickableWhenPaused() const +bool FBuccaneerStatsModule::IsTickableWhenPaused() const { return true; } -bool FTimeSeriesDataEmitterModule::IsTickableInEditor() const +bool FBuccaneerStatsModule::IsTickableInEditor() const { return true; } -void FTimeSeriesDataEmitterModule::Tick(float DeltaTime) +void FBuccaneerStatsModule::Tick(float DeltaTime) { - if (!FBuccaneerCommonModule::GetModule()->CVarBuccaneerEnableStats->GetBool()) + if (!UBuccaneerSettings::CVarEnableStats->GetBool()) { // Performance profiling hasn't been inititialized. Don't continue return; @@ -83,11 +82,7 @@ void FTimeSeriesDataEmitterModule::Tick(float DeltaTime) if (FrameTime > 0.25) { InterimHangCount++; - FSemanticEventEmitterModule *Module = FSemanticEventEmitterModule::GetModule(); - if (Module) - { - Module->EmitSemanticEvent(FString(TEXT("warning")), FString(TEXT("Frame hung"))); - } + IBuccaneerEventsModule::Get().EmitEvent(TEXT("warning"), TEXT("Frame hung")); } else { @@ -114,13 +109,13 @@ void FTimeSeriesDataEmitterModule::Tick(float DeltaTime) LastTickTime = NowTime; } -void FTimeSeriesDataEmitterModule::ComputeUsedMemory() +void FBuccaneerStatsModule::ComputeUsedMemory() { FPlatformMemoryStats MemoryStats = FPlatformMemory::GetStats(); - const unsigned int BitsPerMB = (8u * 1024u * 1024u); - UsedVirtualMemory = static_cast(MemoryStats.UsedVirtual) / BitsPerMB; - UsedPhysicalMemory = static_cast(MemoryStats.UsedPhysical) / BitsPerMB; + const unsigned int BytesPerMB = (8u * 1024u * 1024u); + UsedVirtualMemory = static_cast(MemoryStats.UsedVirtual) / BytesPerMB; + UsedPhysicalMemory = static_cast(MemoryStats.UsedPhysical) / BytesPerMB; #if !UE_BUILD_SHIPPING TArray Stats; @@ -141,7 +136,7 @@ void FTimeSeriesDataEmitterModule::ComputeUsedMemory() #endif } -void FTimeSeriesDataEmitterModule::PushStatsHTTP() +void FBuccaneerStatsModule::PushStatsHTTP() { // Collected Metrics // name value @@ -156,14 +151,14 @@ void FTimeSeriesDataEmitterModule::PushStatsHTTP() UpdateMetric("memory_gpu", UsedGPUMemory); UpdateMetric("num_hangs", InterimHangCount); - FBuccaneerCommonModule::GetModule()->SendStats(JsonObject); + IBuccaneerCommonModule::Get().SendStats(JsonObject); } -TStatId FTimeSeriesDataEmitterModule::GetStatId() const +TStatId FBuccaneerStatsModule::GetStatId() const { - RETURN_QUICK_DECLARE_CYCLE_STAT(FTimeSeriesDataEmitterModule, STATGROUP_Tickables); + RETURN_QUICK_DECLARE_CYCLE_STAT(FBuccaneerStatsModule, STATGROUP_Tickables); } -#undef LOCTEXT_NAMESPACE +#undef COMPUTE_MEAN -IMPLEMENT_MODULE(FTimeSeriesDataEmitterModule, TimeSeriesDataEmitter) \ No newline at end of file +IMPLEMENT_MODULE(FBuccaneerStatsModule, BuccaneerStats) \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Public/TimeSeriesDataEmitter.h b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h similarity index 83% rename from Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Public/TimeSeriesDataEmitter.h rename to Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h index f2b9bc4..66107bf 100644 --- a/Plugins/Buccaneer/Source/TimeSeriesDataEmitter/Public/TimeSeriesDataEmitter.h +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h @@ -1,16 +1,15 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. #pragma once #include "CoreMinimal.h" +#include "Dom/JsonObject.h" +#include "IBuccaneerCommonModule.h" +#include "IBuccaneerStatsModule.h" #include "Modules/ModuleManager.h" -#include "BuccaneerCommon.h" #include "Tickable.h" -#include "Dom/JsonObject.h" -DECLARE_LOG_CATEGORY_EXTERN(TimeSeriesDataEmitter, Log, All); - -class TIMESERIESDATAEMITTER_API FTimeSeriesDataEmitterModule : public IModuleInterface, public FTickableGameObject +class FBuccaneerStatsModule : public IBuccaneerStatsModule, public FTickableGameObject { public: /** IModuleInterface implementation */ @@ -50,6 +49,4 @@ class TIMESERIESDATAEMITTER_API FTimeSeriesDataEmitterModule : public IModuleInt // Variable for storing logging URL and logging object TSharedPtr JsonObject; TSharedPtr MetricJson; - - TMap StatDescriptionMap; }; diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.cpp new file mode 100644 index 0000000..b2374da --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.cpp @@ -0,0 +1,5 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Logging.h" + +DEFINE_LOG_CATEGORY(LogBuccaneerStats); \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.h b/Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.h new file mode 100644 index 0000000..eef7f4e --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/Logging.h @@ -0,0 +1,10 @@ + + +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" +#include "Logging/StructuredLog.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogBuccaneerStats, Log, All); \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Public/IBuccaneerStatsModule.h b/Plugins/Buccaneer/Source/BuccaneerStats/Public/IBuccaneerStatsModule.h new file mode 100644 index 0000000..5d522e5 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Public/IBuccaneerStatsModule.h @@ -0,0 +1,33 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "CoreTypes.h" +#include "Dom/JsonObject.h" +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" + +class BUCCANEERSTATS_API IBuccaneerStatsModule : public IModuleInterface +{ +public: + /** + * Singleton-like access to this module's interface. + * Beware calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline IBuccaneerStatsModule& Get() + { + return FModuleManager::LoadModuleChecked("BuccaneerStats"); + } + + /** + * Checks to see if this module is loaded. + * + * @return True if the module is loaded. + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("BuccaneerStats"); + } +}; \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/SemanticEventEmitter/Private/SemanticEventEmitter.cpp b/Plugins/Buccaneer/Source/SemanticEventEmitter/Private/SemanticEventEmitter.cpp deleted file mode 100644 index 005f0bd..0000000 --- a/Plugins/Buccaneer/Source/SemanticEventEmitter/Private/SemanticEventEmitter.cpp +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#include "SemanticEventEmitter.h" -#include "CoreMinimal.h" -#include "HAL/IConsoleManager.h" -#include "Logging/LogMacros.h" -#include "Dom/JsonObject.h" -#include "BuccaneerCommon.h" - -#define LOCTEXT_NAMESPACE "SemanticEventEmitterModule" - -DEFINE_LOG_CATEGORY(SemanticEventEmitter); - -FSemanticEventEmitterModule *FSemanticEventEmitterModule::SemanticEmitterModule = nullptr; - -void FSemanticEventEmitterModule::StartupModule() -{ - -} - -void FSemanticEventEmitterModule::ShutdownModule() -{ -} - -void FSemanticEventEmitterModule::EmitSemanticEvent(FString Level, FString Event) -{ - if (!FBuccaneerCommonModule::GetModule()->CVarBuccaneerEnableEvents->GetBool()) - { - return; - } - - UE_LOG(SemanticEventEmitter, Verbose, TEXT("%s: %s"), *Level, *Event); - - TSharedPtr JsonObject = MakeShareable(new FJsonObject()); - JsonObject->SetField("level", MakeShared((TEXT("%s"), *Level))); - JsonObject->SetField("message", MakeShared((TEXT("%s"), *Event))); - - FBuccaneerCommonModule::GetModule()->SendEvent(JsonObject); -} - -FSemanticEventEmitterModule *FSemanticEventEmitterModule::GetModule() -{ - if (SemanticEmitterModule) - { - return SemanticEmitterModule; - } - FSemanticEventEmitterModule *Module = FModuleManager::Get().GetModulePtr("SemanticEventEmitter"); - if (Module) - { - SemanticEmitterModule = Module; - } - return SemanticEmitterModule; -} - -#undef LOCTEXT_NAMESPACE - -IMPLEMENT_MODULE(FSemanticEventEmitterModule, SemanticEventEmitter) \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.cpp b/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.cpp deleted file mode 100644 index 7e8e81d..0000000 --- a/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#include "SemanticEventBlueprintFunctionLibrary.h" - - -void USemanticEventEmitterBlueprintLibrary::EmitSemanticEvent(FString Level, FString Event) -{ - FSemanticEventEmitterModule* Module = FSemanticEventEmitterModule::GetModule(); - if(Module) - { - Module->EmitSemanticEvent(Level, Event); - } -} \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.h b/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.h deleted file mode 100644 index e2f78c4..0000000 --- a/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventBlueprintFunctionLibrary.h +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "SemanticEventEmitter.h" -#include "Kismet/BlueprintFunctionLibrary.h" -#include "SemanticEventBlueprintFunctionLibrary.generated.h" - -UCLASS() -class SEMANTICEVENTEMITTER_API USemanticEventEmitterBlueprintLibrary : public UBlueprintFunctionLibrary -{ - GENERATED_BODY() -public: - UFUNCTION(BlueprintCallable, Category="Buccaneer") - static void EmitSemanticEvent(FString Level, FString Event); -}; diff --git a/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventEmitter.h b/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventEmitter.h deleted file mode 100644 index 7a6ec38..0000000 --- a/Plugins/Buccaneer/Source/SemanticEventEmitter/Public/SemanticEventEmitter.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "Modules/ModuleManager.h" - - -DECLARE_LOG_CATEGORY_EXTERN(SemanticEventEmitter, Log, All); - -class SEMANTICEVENTEMITTER_API FSemanticEventEmitterModule : public IModuleInterface -{ -public: - /** IModuleInterface implementation */ - virtual void StartupModule() override; - virtual void ShutdownModule() override; - void EmitSemanticEvent(FString Level, FString Event); - static FSemanticEventEmitterModule *GetModule(); - -private: - static FSemanticEventEmitterModule *SemanticEmitterModule; -}; From 8067ae0baeda9bdcf7d14f1401605a931c8fb121 Mon Sep 17 00:00:00 2001 From: William Belcher Date: Tue, 12 Aug 2025 18:33:03 +1000 Subject: [PATCH 02/35] Refactor Bucc4PS --- .../Buccaneer4PixelStreaming.Build.cs | 7 +- .../Private/Buccaneer4PixelStreaming.cpp | 137 ++++---- .../Buccaneer4PixelStreaming.h | 21 +- .../Buccaneer4PixelStreamingSettings.cpp | 315 ++++++++++++++++++ .../Private/Logging.cpp | 5 + .../Private/Logging.h | 8 + .../Public/Buccaneer4PixelStreamingSettings.h | 48 +++ .../Public/IBuccaneer4PixelStreamingModule.h | 33 ++ 8 files changed, 476 insertions(+), 98 deletions(-) rename Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/{Public => Private}/Buccaneer4PixelStreaming.h (66%) create mode 100644 Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp create mode 100644 Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.cpp create mode 100644 Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.h create mode 100644 Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h create mode 100644 Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreamingModule.h diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs index b302020..fb5eb26 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs @@ -13,8 +13,10 @@ public Buccaneer4PixelStreaming(ReadOnlyTargetRules Target) : base(Target) { "Core", "PixelStreaming", - "BuccaneerCommon", - "Json" + "Json", + "CoreUObject", + "DeveloperSettings", + "EngineSettings" }); @@ -25,7 +27,6 @@ public Buccaneer4PixelStreaming(ReadOnlyTargetRules Target) : base(Target) "Engine", "Slate", "SlateCore", - "PixelStreaming", "BuccaneerCommon" }); } diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp index 678494e..19dd728 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp @@ -1,77 +1,56 @@ // Copyright Epic Games, Inc. All Rights Reserved. #include "Buccaneer4PixelStreaming.h" -#include "Logging/LogMacros.h" -#include "PixelStreamingDelegates.h" - -#define LOCTEXT_NAMESPACE "FBuccaneer4PixelStreamingModule" - - -DEFINE_LOG_CATEGORY(BuccaneerPixelStreaming); -namespace Buccaneer4PixelStreaming -{ - -} +#include "Logging.h" +#include "PixelStreamingDelegates.h" +#include "Buccaneer4PixelStreamingSettings.h" + +TMap StatDescriptionMap = { + {"jitterBufferDelay", "jitterBufferDelay"}, + {"framesSent", "framesSent"}, + {"framesPerSecond", "framesPerSecond"}, + {"framesReceived", "framesReceived"}, + {"framesDropped", "framesDropped"}, + {"framesDecoded", "framesDecoded"}, + {"framesCorrupted", "framesCorrupted"}, + {"partialFramesLost", "partialFramesLost"}, + {"fullFramesLost", "fullFramesLost"}, + {"hugeFramesSent", "hugeFramesSent"}, + {"jitterBufferTargetDelay", "jitterBufferTargetDelay"}, + {"interruptionCount", "interruptionCount"}, + {"totalInterruptionDuration", "totalInterruptionDuration"}, + {"freezeCount", "freezeCount"}, + {"pauseCount", "pauseCount"}, + {"totalFreezesDuration", "totalFreezesDuration"}, + {"totalPausesDuration", "totalPausesDuration"}, + {"firCount", "firCount"}, + {"pliCount", "pliCount"}, + {"nackCount", "nackCount"}, + {"sliCount", "sliCount"}, + {"retransmittedBytesSent", "retransmittedBytesSent"}, + {"totalEncodedBytesTarget", "totalEncodedBytesTarget"}, + {"keyFramesEncoded", "keyFramesEncoded"}, + {"frameWidth", "frameWidth"}, + {"frameHeight", "frameHeight"}, + {"bytesSent", "bytesSent"}, + {"qpSum", "qpSum"}, + {"totalEncodeTime", "totalEncodeTime"}, + {"totalPacketSendDelay", "totalPacketSendDelay"}, + {"packetSendDelay", "packetSendDelay"}, + {"framesEncoded", "framesEncoded"}, + {"transmitFps", "transmit fps"}, + {"bitrate", "bitrate (kb/s)"}, + {"qp", "qp"}, + {"encodeTime", "encode time (ms)"}, + {"encodeFps", "encode fps"}, + {"captureToSend", "capture to send (ms)"}, + {"captureFps", "capture fps"} +}; void FBuccaneer4PixelStreamingModule::StartupModule() { - StatDescriptionMap = { - {"jitterBufferDelay", "jitterBufferDelay"}, - {"framesSent", "framesSent"}, - {"framesPerSecond", "framesPerSecond"}, - {"framesReceived", "framesReceived"}, - {"framesDropped", "framesDropped"}, - {"framesDecoded", "framesDecoded"}, - {"framesCorrupted", "framesCorrupted"}, - {"partialFramesLost", "partialFramesLost"}, - {"fullFramesLost", "fullFramesLost"}, - {"hugeFramesSent", "hugeFramesSent"}, - {"jitterBufferTargetDelay", "jitterBufferTargetDelay"}, - {"interruptionCount", "interruptionCount"}, - {"totalInterruptionDuration", "totalInterruptionDuration"}, - {"freezeCount", "freezeCount"}, - {"pauseCount", "pauseCount"}, - {"totalFreezesDuration", "totalFreezesDuration"}, - {"totalPausesDuration", "totalPausesDuration"}, - {"firCount", "firCount"}, - {"pliCount", "pliCount"}, - {"nackCount", "nackCount"}, - {"sliCount", "sliCount"}, - {"retransmittedBytesSent", "retransmittedBytesSent"}, - {"totalEncodedBytesTarget", "totalEncodedBytesTarget"}, - {"keyFramesEncoded", "keyFramesEncoded"}, - {"frameWidth", "frameWidth"}, - {"frameHeight", "frameHeight"}, - {"bytesSent", "bytesSent"}, - {"qpSum", "qpSum"}, - {"totalEncodeTime", "totalEncodeTime"}, - {"totalPacketSendDelay", "totalPacketSendDelay"}, - {"packetSendDelay", "packetSendDelay"}, - {"framesEncoded", "framesEncoded"}, - {"transmitFps", "transmit fps"}, - {"bitrate", "bitrate (kb/s)"}, - {"qp", "qp"}, - {"encodeTime", "encode time (ms)"}, - {"encodeFps", "encode fps"}, - {"captureToSend", "capture to send (ms)"}, - {"captureFps", "capture fps"} - }; - - CVarBuccaneer4PixelStreamingEnableStats = IConsoleManager::Get().RegisterConsoleVariable( - TEXT("Buccaneer4PixelStreaming.EnableStats"), - true, - TEXT("Disables the collection and logging of Pixel Streaming stats with Buccaneer"), - ECVF_Default); - - Setup(); -} - -void FBuccaneer4PixelStreamingModule::Setup() -{ - FBuccaneerCommonModule::ParseCommandLineOption(TEXT("Buccaneer4PixelStreamingEnableStats"), CVarBuccaneer4PixelStreamingEnableStats); - - LoggingStart = FPlatformTime::Seconds(); + LoggingStart = FPlatformTime::Seconds(); ReportingInterval = 1; JsonObject = MakeShareable(new FJsonObject()); @@ -90,7 +69,7 @@ void FBuccaneer4PixelStreamingModule::ShutdownModule() void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId PlayerId, FName StatName, float StatValue) { - if(!CVarBuccaneer4PixelStreamingEnableStats->GetBool() || PlayerId == TEXT("Application")) + if (!UBuccaneer4PixelStreamingSettings::CVarEnabled.GetValueOnAnyThread() || PlayerId == TEXT("Application")) { return; } @@ -102,11 +81,11 @@ void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId Player * ] * } */ - - const TSharedPtr* MetricJson = nullptr; - if(JsonObject->TryGetObjectField((TEXT("%s"), *StatName.ToString()), MetricJson)) + const TSharedPtr MetricJson; + const TSharedPtr* MetricJsonPtr = &MetricJson; + if(JsonObject->TryGetObjectField(*StatName.ToString(), MetricJsonPtr)) { - TArray> ValueArray = (*MetricJson)->GetArrayField(TEXT("value")); + TArray> ValueArray = MetricJson->GetArrayField(TEXT("value")); bool bRequiresCreation = true; for (int i = 0; i < ValueArray.Num(); i++) @@ -129,18 +108,18 @@ void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId Player ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); - (*MetricJson)->SetArrayField((TEXT("value")), ValueArray); + MetricJson->SetArrayField((TEXT("value")), ValueArray); } } else { if(!StatDescriptionMap.Contains(*StatName.ToString())) { - UE_LOG(BuccaneerPixelStreaming, Log, TEXT("%s"), *StatName.ToString()); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "{0}", StatName.ToString()); return; } TSharedPtr NewMetricJson = MakeShareable(new FJsonObject()); - NewMetricJson->SetField("description", MakeShared((TEXT("%s"), *StatDescriptionMap[*StatName.ToString()]))); + NewMetricJson->SetField("description", MakeShared(*StatDescriptionMap[*StatName.ToString()])); TSharedPtr ValueJson = MakeShareable(new FJsonObject()); @@ -151,7 +130,7 @@ void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId Player NewMetricJson->SetArrayField((TEXT("value")), ValueArray); - JsonObject->SetObjectField((TEXT("%s"), *StatName.ToString()), NewMetricJson); + JsonObject->SetObjectField(*StatName.ToString(), NewMetricJson); } double NowTime = FPlatformTime::Seconds(); @@ -160,12 +139,10 @@ void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId Player LoggingStart = NowTime; TSharedPtr PayloadJson = MakeShareable(new FJsonObject()); PayloadJson->SetObjectField(TEXT("metrics"), JsonObject); - FBuccaneerCommonModule::GetModule()->SendStats(PayloadJson); + IBuccaneerCommonModule::Get().SendStats(PayloadJson); - JsonObject = MakeShareable(new FJsonObject()); + JsonObject = MakeShareable(new FJsonObject()); } } - -#undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FBuccaneer4PixelStreamingModule, Buccaneer4PixelStreaming) \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h similarity index 66% rename from Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming.h rename to Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h index 30c2a0a..e3649d5 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming.h +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h @@ -3,35 +3,26 @@ #pragma once #include "CoreMinimal.h" -#include "BuccaneerCommon.h" +#include "Dom/JsonObject.h" +#include "IBuccaneerCommonModule.h" +#include "IBuccaneer4PixelStreamingModule.h" +#include "IPixelStreamingModule.h" #include "Modules/ModuleManager.h" #include "PixelStreamingDelegates.h" -#include "IPixelStreamingModule.h" - -#include "Dom/JsonObject.h" #include "PixelStreamingPlayerId.h" -DECLARE_LOG_CATEGORY_EXTERN(BuccaneerPixelStreaming, Log, All); - -class FBuccaneer4PixelStreamingModule : public IModuleInterface +class FBuccaneer4PixelStreamingModule : public IBuccaneer4PixelStreamingModule { public: - /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; - void Setup(); - UFUNCTION() - void ConsumeStat(FPixelStreamingPlayerId PlayerId, FName StatName, float StatValue); - IConsoleVariable* CVarBuccaneer4PixelStreamingEnableStats; + void ConsumeStat(FPixelStreamingPlayerId PlayerId, FName StatName, float StatValue); private: - double LoggingStart; double ReportingInterval; - TMap StatDescriptionMap; - TSharedPtr JsonObject; }; diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp new file mode 100644 index 0000000..5bbbcf8 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp @@ -0,0 +1,315 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Buccaneer4PixelStreamingSettings.h" + +#include "Logging.h" +#include "Misc/CommandLine.h" +#include "UObject/ReflectedTypeAccessors.h" + +namespace Util +{ + FString ConsoleVariableToCommandArgValue(const FString InCVarName) + { + // CVars are . deliminated by section. To get their equivilent commandline arg for parsing + // we need to remove the . and add a "=" + return InCVarName.Replace(TEXT("."), TEXT("")).Append(TEXT("=")); + } + + FString ConsoleVariableToCommandArgParam(const FString InCVarName) + { + // CVars are . deliminated by section. To get their equivilent commandline arg parameter, we need to to remove the . + return InCVarName.Replace(TEXT("."), TEXT("")); + } + + FString FindCVarFromProperty(const TSet> Set, const FString& Value) + { + for (const TPair& Pair : Set) + { + if (Pair.Value == Value) + { + return Pair.Key; + } + } + + return ""; + } +} + +static const TSet> GetCmdArg = { + { "Buccaneer4PixelStreaming.EnableStats", "Enabled" } +}; + +TAutoConsoleVariable UBuccaneer4PixelStreamingSettings::CVarEnabled( + TEXT("Buccaneer4PixelStreaming.EnableStats"), + true, + TEXT("Enables the collection and logging of Pixel Streaming stats with Buccaneer (default: true)"), + ECVF_Default); + +FName UBuccaneer4PixelStreamingSettings::GetCategoryName() const +{ + return TEXT("Plugins"); +} + +#if WITH_EDITOR +FText UBuccaneer4PixelStreamingSettings::GetSectionText() const +{ + return NSLOCTEXT("Buccaneer4PixelStreamingPlugin", "Buccaneer4PixelStreamingSettingsSection", "Buccaneer4PixelStreaming"); +} + +void UBuccaneer4PixelStreamingSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + FString PropertyName = PropertyChangedEvent.Property->GetNameCPP(); + + FString CVarName; + if (CVarName = Util::FindCVarFromProperty(GetCmdArg, PropertyName); !CVarName.IsEmpty()) + { + SetCVarFromProperty(CVarName, PropertyChangedEvent.Property); + } +} +#endif + +void UBuccaneer4PixelStreamingSettings::SetCVarAndPropertyFromValue(const FString& CVarName, FProperty* Property, const FString& Value) +{ + IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (!CVar) + { + UE_LOGFMT(LogBuccaneer4PixelStreaming, Warning, "Failed to find CVar: {0}", CVarName); + return; + } + + if (FByteProperty* ByteProperty = CastField(Property); ByteProperty != NULL && ByteProperty->Enum != NULL) + { + CVar->Set(FCString::Atoi(*Value), ECVF_SetByCommandline); + ByteProperty->SetPropertyValue_InContainer(this, FCString::Atoi(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atoi(*Value)); + } + else if (FEnumProperty* EnumProperty = CastField(Property)) + { + int64 EnumIndex = EnumProperty->GetEnum()->GetIndexByNameString(Value.Replace(TEXT("_"), TEXT(""))); + if (EnumIndex != INDEX_NONE) + { + CVar->Set(*EnumProperty->GetEnum()->GetNameStringByIndex(EnumIndex), ECVF_SetByCommandline); + + FNumericProperty* UnderlyingProp = EnumProperty->GetUnderlyingProperty(); + int64* PropertyAddress = EnumProperty->ContainerPtrToValuePtr(this); + *PropertyAddress = EnumProperty->GetEnum()->GetValueByIndex(EnumIndex); + + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), EnumProperty->GetEnum()->GetNameStringByIndex(EnumIndex)); + } + else + { + UE_LOGFMT(LogBuccaneer4PixelStreaming, Warning, "{0} is not a valid enum value for {1}", Value, EnumProperty->GetEnum()->CppType); + } + } + else if (FBoolProperty* BoolProperty = CastField(Property)) + { + bool bValue = false; + if (Value.Equals(FString(TEXT("true")), ESearchCase::IgnoreCase)) + { + bValue = true; + } + else if (Value.Equals(FString(TEXT("false")), ESearchCase::IgnoreCase)) + { + bValue = false; + } + CVar->Set(bValue, ECVF_SetByCommandline); + BoolProperty->SetPropertyValue_InContainer(this, bValue); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), bValue); + } + else if (FIntProperty* IntProperty = CastField(Property)) + { + CVar->Set(FCString::Atoi(*Value), ECVF_SetByCommandline); + IntProperty->SetPropertyValue_InContainer(this, FCString::Atoi(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atoi(*Value)); + } + else if (FFloatProperty* FloatProperty = CastField(Property)) + { + CVar->Set(FCString::Atof(*Value), ECVF_SetByCommandline); + FloatProperty->SetPropertyValue_InContainer(this, FCString::Atof(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atof(*Value)); + } + else if (FStrProperty* StringProperty = CastField(Property)) + { + CVar->Set(*Value, ECVF_SetByCommandline); + StringProperty->SetPropertyValue_InContainer(this, *Value); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } + else if (FNameProperty* NameProperty = CastField(Property)) + { + CVar->Set(*Value, ECVF_SetByCommandline); + NameProperty->SetPropertyValue_InContainer(this, FName(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } + else if (FArrayProperty* ArrayProperty = CastField(Property)) + { + // TODO (william.belcher): Only FString array properties are currently supported + CVar->Set(*Value, ECVF_SetByCommandline); + + TArray StringArray; + Value.ParseIntoArray(StringArray, TEXT(","), true); + + TArray& Array = *ArrayProperty->ContainerPtrToValuePtr>(this); + Array = StringArray; + + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } +} + +void UBuccaneer4PixelStreamingSettings::SetCVarFromProperty(const FString& CVarName, FProperty* Property) +{ + IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (!CVar) + { + UE_LOGFMT(LogBuccaneer4PixelStreaming, Warning, "Failed to find CVar: {0}", CVarName); + return; + } + + if (FByteProperty* ByteProperty = CastField(Property); ByteProperty != NULL && ByteProperty->Enum != NULL) + { + CVar->Set(ByteProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, ByteProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FEnumProperty* EnumProperty = CastField(Property)) + { + void* PropertyAddress = EnumProperty->ContainerPtrToValuePtr(this); + int64 CurrentValue = EnumProperty->GetUnderlyingProperty()->GetSignedIntPropertyValue(PropertyAddress); + CVar->Set(*EnumProperty->GetEnum()->GetNameStringByValue(CurrentValue), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, EnumProperty->GetEnum()->GetNameStringByValue(CurrentValue), Property->GetNameCPP()); + } + else if (FBoolProperty* BoolProperty = CastField(Property)) + { + CVar->Set(BoolProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, BoolProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FIntProperty* IntProperty = CastField(Property)) + { + CVar->Set(IntProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, IntProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FFloatProperty* FloatProperty = CastField(Property)) + { + CVar->Set(FloatProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, FloatProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FStrProperty* StringProperty = CastField(Property)) + { + CVar->Set(*StringProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, StringProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FNameProperty* NameProperty = CastField(Property)) + { + CVar->Set(*NameProperty->GetPropertyValue_InContainer(this).ToString(), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, NameProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FArrayProperty* ArrayProperty = CastField(Property)) + { + // TODO (william.belcher): Only FString array properties are currently supported + TArray Array = *ArrayProperty->ContainerPtrToValuePtr>(this); + CVar->Set(*FString::Join(Array, TEXT(",")), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, FString::Join(Array, TEXT(",")), Property->GetNameCPP()); + } +} + +void UBuccaneer4PixelStreamingSettings::InitializeCVarsFromProperties() +{ + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Initializing CVars from ini"); + for (FProperty* Property = GetClass()->PropertyLink; Property; Property = Property->PropertyLinkNext) + { + if (!Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + FString CVarName; + if (CVarName = Util::FindCVarFromProperty(GetCmdArg, Property->GetNameCPP()); !CVarName.IsEmpty()) + { + SetCVarFromProperty(CVarName, Property); + continue; + } + } +} + +void UBuccaneer4PixelStreamingSettings::ValidateCommandLineArgs() +{ + FString CommandLine = FCommandLine::Get(); + + TArray CommandArray; + CommandLine.ParseIntoArray(CommandArray, TEXT(" "), true); + + for (FString Command : CommandArray) + { + Command.RemoveFromStart(TEXT("-")); + if (!Command.StartsWith("Buccaneer4PixelStreaming")) + { + continue; + } + + // Get the pure command line arg from an arg that contains an '=', eg BuccaneerURL= + FString CurrentCommandLineArg = Command; + if (Command.Contains("=")) + { + Command.Split(TEXT("="), &CurrentCommandLineArg, nullptr); + } + + bool bValidArg = false; + for (const TPair& Pair : GetCmdArg) + { + FString ValidCommandLineArg = Util::ConsoleVariableToCommandArgParam(Pair.Key); + if (CurrentCommandLineArg == ValidCommandLineArg) + { + bValidArg = true; + break; + } + } + + if (!bValidArg) + { + UE_LOGFMT(LogBuccaneer4PixelStreaming, Warning, "Unknown Buccaneer4PixelStreaming command line arg: {0}", CurrentCommandLineArg); + } + } +} + +void UBuccaneer4PixelStreamingSettings::ParseCommandlineArgs() +{ + UE_LOGFMT(LogBuccaneer4PixelStreaming, Verbose, "Updating CVars and properties with command line args"); + for (const TPair& Pair : GetCmdArg) + { + FString CVarString = Pair.Key; + FString PropertyName = Pair.Value; + + FProperty* Property = GetClass()->FindPropertyByName(FName(*PropertyName)); + if (!Property || !Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + // Handle a directly parsable commandline + FString ConsoleString; + if (FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(CVarString), ConsoleString)) + { + SetCVarAndPropertyFromValue(CVarString, Property, ConsoleString); + } + else if (FParse::Param(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgParam(CVarString))) + { + SetCVarAndPropertyFromValue(CVarString, Property, TEXT("true")); + } + } +} + +void UBuccaneer4PixelStreamingSettings::PostInitProperties() +{ + Super::PostInitProperties(); + + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Initialising Buccaneer4PixelStreaming settings."); + + // Set all the CVars to reflect the state of the ini + InitializeCVarsFromProperties(); + + // Validate command line args to log if they're invalid + ValidateCommandLineArgs(); + + // Update CVars and properties based on command line args + ParseCommandlineArgs(); +} \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.cpp new file mode 100644 index 0000000..5db9e43 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.cpp @@ -0,0 +1,5 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Logging.h" + +DEFINE_LOG_CATEGORY(LogBuccaneer4PixelStreaming); \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.h new file mode 100644 index 0000000..89ea0f2 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Logging.h @@ -0,0 +1,8 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" +#include "Logging/StructuredLog.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogBuccaneer4PixelStreaming, Log, All); \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h new file mode 100644 index 0000000..3370f68 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h @@ -0,0 +1,48 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Containers/UnrealString.h" +#include "CoreMinimal.h" +#include "Engine/DeveloperSettings.h" + +#include "Buccaneer4PixelStreamingSettings.generated.h" + +// Config loaded/saved to an .ini file. +// It is also exposed through the plugin settings page in editor. +UCLASS(config = Game, defaultconfig, meta = (DisplayName = "Buccaneer4PixelStreaming")) +class BUCCANEER4PIXELSTREAMING_API UBuccaneer4PixelStreamingSettings : public UDeveloperSettings +{ + GENERATED_BODY() + + virtual ~UBuccaneer4PixelStreamingSettings() = default; + +public: + static TAutoConsoleVariable CVarEnabled; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer4PixelStreaming", meta = ( + DisplayName = "Enabled", + ToolTip = "Enables the collection of Pixel Streaming performance metrics" + )) + bool Enabled = true; + + // Begin UDeveloperSettings Interface + virtual FName GetCategoryName() const override; + +#if WITH_EDITOR + virtual FText GetSectionText() const override; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + // End UDeveloperSettings Interface + + // Begin UObject Interface + virtual void PostInitProperties() override; + // End UObject Interface + +private: + void SetCVarAndPropertyFromValue(const FString& CVarName, FProperty* Property, const FString& Value); + void SetCVarFromProperty(const FString& CVarName, FProperty* Property); + + void InitializeCVarsFromProperties(); + void ValidateCommandLineArgs(); + void ParseCommandlineArgs(); +}; \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreamingModule.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreamingModule.h new file mode 100644 index 0000000..b65d713 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreamingModule.h @@ -0,0 +1,33 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "CoreTypes.h" +#include "Dom/JsonObject.h" +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" + +class BUCCANEER4PIXELSTREAMING_API IBuccaneer4PixelStreamingModule : public IModuleInterface +{ +public: + /** + * Singleton-like access to this module's interface. + * Beware calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline IBuccaneer4PixelStreamingModule& Get() + { + return FModuleManager::LoadModuleChecked("Buccaneer4PixelStreaming"); + } + + /** + * Checks to see if this module is loaded. + * + * @return True if the module is loaded. + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("Buccaneer4PixelStreaming"); + } +}; \ No newline at end of file From fafb1c219d7f9d1b196b0fba05f2f749126bbbf0 Mon Sep 17 00:00:00 2001 From: William Belcher Date: Tue, 12 Aug 2025 18:42:08 +1000 Subject: [PATCH 03/35] Add Bucc4PS2 --- .../Buccaneer4PixelStreaming2.uplugin | 34 ++ .../Resources/Icon128.png | Bin 0 -> 12699 bytes .../Buccaneer4PixelStreaming2.Build.cs | 33 ++ .../Private/Buccaneer4PixelStreaming2.cpp | 147 ++++++++ .../Private/Buccaneer4PixelStreaming2.h | 26 ++ .../Buccaneer4PixelStreaming2Settings.cpp | 315 ++++++++++++++++++ .../Private/Logging.cpp | 5 + .../Private/Logging.h | 8 + .../Buccaneer4PixelStreaming2Settings.h | 48 +++ .../Public/IBuccaneer4PixelStreaming2Module.h | 33 ++ 10 files changed, 649 insertions(+) create mode 100644 Plugins/Buccaneer4PixelStreaming2/Buccaneer4PixelStreaming2.uplugin create mode 100644 Plugins/Buccaneer4PixelStreaming2/Resources/Icon128.png create mode 100644 Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs create mode 100644 Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp create mode 100644 Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h create mode 100644 Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp create mode 100644 Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.cpp create mode 100644 Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.h create mode 100644 Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming2Settings.h create mode 100644 Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreaming2Module.h diff --git a/Plugins/Buccaneer4PixelStreaming2/Buccaneer4PixelStreaming2.uplugin b/Plugins/Buccaneer4PixelStreaming2/Buccaneer4PixelStreaming2.uplugin new file mode 100644 index 0000000..f2f1486 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Buccaneer4PixelStreaming2.uplugin @@ -0,0 +1,34 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "Buccaneer4PixelStreaming2", + "Description": "", + "Category": "Other", + "CreatedBy": "", + "CreatedByURL": "", + "DocsURL": "", + "MarketplaceURL": "", + "SupportURL": "", + "CanContainContent": true, + "IsBetaVersion": false, + "IsExperimentalVersion": true, + "Installed": false, + "Modules": [ + { + "Name": "Buccaneer4PixelStreaming2", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "PixelStreaming2", + "Enabled": true + }, + { + "Name": "Buccaneer", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Resources/Icon128.png b/Plugins/Buccaneer4PixelStreaming2/Resources/Icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..1231d4aad4d0d462fb7b178eb5b30aa61a10df0b GIT binary patch literal 12699 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 a;7#~yJh_ha>A&A^fRdb=Y>l)<=>Gxy=2LS3 literal 0 HcmV?d00001 diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs new file mode 100644 index 0000000..371d4d2 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs @@ -0,0 +1,33 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class Buccaneer4PixelStreaming2 : ModuleRules +{ + public Buccaneer4PixelStreaming2(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "PixelStreaming2", + "Json", + "CoreUObject", + "DeveloperSettings", + "EngineSettings" + }); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "BuccaneerCommon" + }); + } +} diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp new file mode 100644 index 0000000..866bf90 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp @@ -0,0 +1,147 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "Buccaneer4PixelStreaming2.h" + +#include "Logging.h" +#include "Buccaneer4PixelStreaming2Settings.h" + +TMap StatDescriptionMap = { + {"jitterBufferDelay", "jitterBufferDelay"}, + {"framesSent", "framesSent"}, + {"framesPerSecond", "framesPerSecond"}, + {"framesReceived", "framesReceived"}, + {"framesDropped", "framesDropped"}, + {"framesDecoded", "framesDecoded"}, + {"framesCorrupted", "framesCorrupted"}, + {"partialFramesLost", "partialFramesLost"}, + {"fullFramesLost", "fullFramesLost"}, + {"hugeFramesSent", "hugeFramesSent"}, + {"jitterBufferTargetDelay", "jitterBufferTargetDelay"}, + {"interruptionCount", "interruptionCount"}, + {"totalInterruptionDuration", "totalInterruptionDuration"}, + {"freezeCount", "freezeCount"}, + {"pauseCount", "pauseCount"}, + {"totalFreezesDuration", "totalFreezesDuration"}, + {"totalPausesDuration", "totalPausesDuration"}, + {"firCount", "firCount"}, + {"pliCount", "pliCount"}, + {"nackCount", "nackCount"}, + {"sliCount", "sliCount"}, + {"retransmittedBytesSent", "retransmittedBytesSent"}, + {"totalEncodedBytesTarget", "totalEncodedBytesTarget"}, + {"keyFramesEncoded", "keyFramesEncoded"}, + {"frameWidth", "frameWidth"}, + {"frameHeight", "frameHeight"}, + {"bytesSent", "bytesSent"}, + {"qpSum", "qpSum"}, + {"totalEncodeTime", "totalEncodeTime"}, + {"totalPacketSendDelay", "totalPacketSendDelay"}, + {"packetSendDelay", "packetSendDelay"}, + {"framesEncoded", "framesEncoded"}, + {"transmitFps", "transmit fps"}, + {"bitrate", "bitrate (kb/s)"}, + {"qp", "qp"}, + {"encodeTime", "encode time (ms)"}, + {"encodeFps", "encode fps"}, + {"captureToSend", "capture to send (ms)"}, + {"captureFps", "capture fps"} +}; + +void FBuccaneer4PixelStreaming2Module::StartupModule() +{ + LoggingStart = FPlatformTime::Seconds(); + ReportingInterval = 1; + + JsonObject = MakeShareable(new FJsonObject()); + + if (UPixelStreaming2Delegates* Delegates = UPixelStreaming2Delegates::Get()) + { + Delegates->OnStatChangedNative.AddRaw(this, &FBuccaneer4PixelStreaming2Module::ConsumeStat); + } +} + +void FBuccaneer4PixelStreaming2Module::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +void FBuccaneer4PixelStreaming2Module::ConsumeStat(FString PlayerId, FName StatName, float StatValue) +{ + if (!UBuccaneer4PixelStreaming2Settings::CVarEnabled.GetValueOnAnyThread() || PlayerId == TEXT("Application")) + { + return; + } + /** + * "{StatName}": { + * "description": "{StatDescription}", + * "value": [ + * "{PlayerId}": {StatValue} + * ] + * } + */ + const TSharedPtr MetricJson; + const TSharedPtr* MetricJsonPtr = &MetricJson; + if(JsonObject->TryGetObjectField(*StatName.ToString(), MetricJsonPtr)) + { + TArray> ValueArray = MetricJson->GetArrayField(TEXT("value")); + + bool bRequiresCreation = true; + for (int i = 0; i < ValueArray.Num(); i++) + { + const TSharedPtr ValueJson = ValueArray[i]->AsObject(); + double val; + if(ValueJson->TryGetNumberField(*PlayerId, val)) + { + // This metric already has this player id, update the value accordingly + ValueJson->SetField(*PlayerId, MakeShared(StatValue)); + bRequiresCreation = false; + break; + } + } + + if(bRequiresCreation) + { + TSharedPtr ValueJson = MakeShareable(new FJsonObject()); + ValueJson->SetField(*PlayerId, MakeShared(StatValue)); + + ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); + + MetricJson->SetArrayField((TEXT("value")), ValueArray); + } + } + else + { + if(!StatDescriptionMap.Contains(*StatName.ToString())) + { + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "{0}", StatName.ToString()); + return; + } + TSharedPtr NewMetricJson = MakeShareable(new FJsonObject()); + NewMetricJson->SetField("description", MakeShared(*StatDescriptionMap[*StatName.ToString()])); + + + TSharedPtr ValueJson = MakeShareable(new FJsonObject()); + ValueJson->SetField(*PlayerId, MakeShared(StatValue)); + + TArray> ValueArray; + ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); + + NewMetricJson->SetArrayField((TEXT("value")), ValueArray); + + JsonObject->SetObjectField(*StatName.ToString(), NewMetricJson); + } + + double NowTime = FPlatformTime::Seconds(); + if ( (NowTime - LoggingStart) >= ReportingInterval ) + { + LoggingStart = NowTime; + TSharedPtr PayloadJson = MakeShareable(new FJsonObject()); + PayloadJson->SetObjectField(TEXT("metrics"), JsonObject); + IBuccaneerCommonModule::Get().SendStats(PayloadJson); + + JsonObject = MakeShareable(new FJsonObject()); + } +} + +IMPLEMENT_MODULE(FBuccaneer4PixelStreaming2Module, Buccaneer4PixelStreaming2) \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h new file mode 100644 index 0000000..892c8f0 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h @@ -0,0 +1,26 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Dom/JsonObject.h" +#include "IBuccaneerCommonModule.h" +#include "IBuccaneer4PixelStreaming2Module.h" +#include "Modules/ModuleManager.h" +#include "PixelStreaming2Delegates.h" + +class FBuccaneer4PixelStreaming2Module : public IBuccaneer4PixelStreaming2Module +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + void ConsumeStat(FString PlayerId, FName StatName, float StatValue); + +private: + double LoggingStart; + double ReportingInterval; + + TSharedPtr JsonObject; +}; diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp new file mode 100644 index 0000000..7564dc5 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp @@ -0,0 +1,315 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Buccaneer4PixelStreaming2Settings.h" + +#include "Logging.h" +#include "Misc/CommandLine.h" +#include "UObject/ReflectedTypeAccessors.h" + +namespace Util +{ + FString ConsoleVariableToCommandArgValue(const FString InCVarName) + { + // CVars are . deliminated by section. To get their equivilent commandline arg for parsing + // we need to remove the . and add a "=" + return InCVarName.Replace(TEXT("."), TEXT("")).Append(TEXT("=")); + } + + FString ConsoleVariableToCommandArgParam(const FString InCVarName) + { + // CVars are . deliminated by section. To get their equivilent commandline arg parameter, we need to to remove the . + return InCVarName.Replace(TEXT("."), TEXT("")); + } + + FString FindCVarFromProperty(const TSet> Set, const FString& Value) + { + for (const TPair& Pair : Set) + { + if (Pair.Value == Value) + { + return Pair.Key; + } + } + + return ""; + } +} + +static const TSet> GetCmdArg = { + { "Buccaneer4PixelStreaming2.EnableStats", "Enabled" } +}; + +TAutoConsoleVariable UBuccaneer4PixelStreaming2Settings::CVarEnabled( + TEXT("Buccaneer4PixelStreaming2.EnableStats"), + true, + TEXT("Enables the collection and logging of Pixel Streaming stats with Buccaneer (default: true)"), + ECVF_Default); + +FName UBuccaneer4PixelStreaming2Settings::GetCategoryName() const +{ + return TEXT("Plugins"); +} + +#if WITH_EDITOR +FText UBuccaneer4PixelStreaming2Settings::GetSectionText() const +{ + return NSLOCTEXT("Buccaneer4PixelStreaming2Plugin", "Buccaneer4PixelStreaming2SettingsSection", "Buccaneer4PixelStreaming2"); +} + +void UBuccaneer4PixelStreaming2Settings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + FString PropertyName = PropertyChangedEvent.Property->GetNameCPP(); + + FString CVarName; + if (CVarName = Util::FindCVarFromProperty(GetCmdArg, PropertyName); !CVarName.IsEmpty()) + { + SetCVarFromProperty(CVarName, PropertyChangedEvent.Property); + } +} +#endif + +void UBuccaneer4PixelStreaming2Settings::SetCVarAndPropertyFromValue(const FString& CVarName, FProperty* Property, const FString& Value) +{ + IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (!CVar) + { + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Warning, "Failed to find CVar: {0}", CVarName); + return; + } + + if (FByteProperty* ByteProperty = CastField(Property); ByteProperty != NULL && ByteProperty->Enum != NULL) + { + CVar->Set(FCString::Atoi(*Value), ECVF_SetByCommandline); + ByteProperty->SetPropertyValue_InContainer(this, FCString::Atoi(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atoi(*Value)); + } + else if (FEnumProperty* EnumProperty = CastField(Property)) + { + int64 EnumIndex = EnumProperty->GetEnum()->GetIndexByNameString(Value.Replace(TEXT("_"), TEXT(""))); + if (EnumIndex != INDEX_NONE) + { + CVar->Set(*EnumProperty->GetEnum()->GetNameStringByIndex(EnumIndex), ECVF_SetByCommandline); + + FNumericProperty* UnderlyingProp = EnumProperty->GetUnderlyingProperty(); + int64* PropertyAddress = EnumProperty->ContainerPtrToValuePtr(this); + *PropertyAddress = EnumProperty->GetEnum()->GetValueByIndex(EnumIndex); + + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), EnumProperty->GetEnum()->GetNameStringByIndex(EnumIndex)); + } + else + { + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Warning, "{0} is not a valid enum value for {1}", Value, EnumProperty->GetEnum()->CppType); + } + } + else if (FBoolProperty* BoolProperty = CastField(Property)) + { + bool bValue = false; + if (Value.Equals(FString(TEXT("true")), ESearchCase::IgnoreCase)) + { + bValue = true; + } + else if (Value.Equals(FString(TEXT("false")), ESearchCase::IgnoreCase)) + { + bValue = false; + } + CVar->Set(bValue, ECVF_SetByCommandline); + BoolProperty->SetPropertyValue_InContainer(this, bValue); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), bValue); + } + else if (FIntProperty* IntProperty = CastField(Property)) + { + CVar->Set(FCString::Atoi(*Value), ECVF_SetByCommandline); + IntProperty->SetPropertyValue_InContainer(this, FCString::Atoi(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atoi(*Value)); + } + else if (FFloatProperty* FloatProperty = CastField(Property)) + { + CVar->Set(FCString::Atof(*Value), ECVF_SetByCommandline); + FloatProperty->SetPropertyValue_InContainer(this, FCString::Atof(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [{2}] from command line", CVarName, Property->GetNameCPP(), FCString::Atof(*Value)); + } + else if (FStrProperty* StringProperty = CastField(Property)) + { + CVar->Set(*Value, ECVF_SetByCommandline); + StringProperty->SetPropertyValue_InContainer(this, *Value); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } + else if (FNameProperty* NameProperty = CastField(Property)) + { + CVar->Set(*Value, ECVF_SetByCommandline); + NameProperty->SetPropertyValue_InContainer(this, FName(*Value)); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } + else if (FArrayProperty* ArrayProperty = CastField(Property)) + { + // TODO (william.belcher): Only FString array properties are currently supported + CVar->Set(*Value, ECVF_SetByCommandline); + + TArray StringArray; + Value.ParseIntoArray(StringArray, TEXT(","), true); + + TArray& Array = *ArrayProperty->ContainerPtrToValuePtr>(this); + Array = StringArray; + + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] and Property [{1}] to [\"{2}\"] from command line", CVarName, Property->GetNameCPP(), Value); + } +} + +void UBuccaneer4PixelStreaming2Settings::SetCVarFromProperty(const FString& CVarName, FProperty* Property) +{ + IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(*CVarName); + if (!CVar) + { + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Warning, "Failed to find CVar: {0}", CVarName); + return; + } + + if (FByteProperty* ByteProperty = CastField(Property); ByteProperty != NULL && ByteProperty->Enum != NULL) + { + CVar->Set(ByteProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, ByteProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FEnumProperty* EnumProperty = CastField(Property)) + { + void* PropertyAddress = EnumProperty->ContainerPtrToValuePtr(this); + int64 CurrentValue = EnumProperty->GetUnderlyingProperty()->GetSignedIntPropertyValue(PropertyAddress); + CVar->Set(*EnumProperty->GetEnum()->GetNameStringByValue(CurrentValue), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, EnumProperty->GetEnum()->GetNameStringByValue(CurrentValue), Property->GetNameCPP()); + } + else if (FBoolProperty* BoolProperty = CastField(Property)) + { + CVar->Set(BoolProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, BoolProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FIntProperty* IntProperty = CastField(Property)) + { + CVar->Set(IntProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, IntProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FFloatProperty* FloatProperty = CastField(Property)) + { + CVar->Set(FloatProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [{1}] from Property [{2}]", CVarName, FloatProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FStrProperty* StringProperty = CastField(Property)) + { + CVar->Set(*StringProperty->GetPropertyValue_InContainer(this), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, StringProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FNameProperty* NameProperty = CastField(Property)) + { + CVar->Set(*NameProperty->GetPropertyValue_InContainer(this).ToString(), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, NameProperty->GetPropertyValue_InContainer(this), Property->GetNameCPP()); + } + else if (FArrayProperty* ArrayProperty = CastField(Property)) + { + // TODO (william.belcher): Only FString array properties are currently supported + TArray Array = *ArrayProperty->ContainerPtrToValuePtr>(this); + CVar->Set(*FString::Join(Array, TEXT(",")), ECVF_SetByProjectSetting); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Setting CVar [{0}] to [\"{1}\"] from Property [{2}]", CVarName, FString::Join(Array, TEXT(",")), Property->GetNameCPP()); + } +} + +void UBuccaneer4PixelStreaming2Settings::InitializeCVarsFromProperties() +{ + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Initializing CVars from ini"); + for (FProperty* Property = GetClass()->PropertyLink; Property; Property = Property->PropertyLinkNext) + { + if (!Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + FString CVarName; + if (CVarName = Util::FindCVarFromProperty(GetCmdArg, Property->GetNameCPP()); !CVarName.IsEmpty()) + { + SetCVarFromProperty(CVarName, Property); + continue; + } + } +} + +void UBuccaneer4PixelStreaming2Settings::ValidateCommandLineArgs() +{ + FString CommandLine = FCommandLine::Get(); + + TArray CommandArray; + CommandLine.ParseIntoArray(CommandArray, TEXT(" "), true); + + for (FString Command : CommandArray) + { + Command.RemoveFromStart(TEXT("-")); + if (!Command.StartsWith("Buccaneer4PixelStreaming2")) + { + continue; + } + + // Get the pure command line arg from an arg that contains an '=', eg BuccaneerURL= + FString CurrentCommandLineArg = Command; + if (Command.Contains("=")) + { + Command.Split(TEXT("="), &CurrentCommandLineArg, nullptr); + } + + bool bValidArg = false; + for (const TPair& Pair : GetCmdArg) + { + FString ValidCommandLineArg = Util::ConsoleVariableToCommandArgParam(Pair.Key); + if (CurrentCommandLineArg == ValidCommandLineArg) + { + bValidArg = true; + break; + } + } + + if (!bValidArg) + { + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Warning, "Unknown Buccaneer4PixelStreaming2 command line arg: {0}", CurrentCommandLineArg); + } + } +} + +void UBuccaneer4PixelStreaming2Settings::ParseCommandlineArgs() +{ + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Verbose, "Updating CVars and properties with command line args"); + for (const TPair& Pair : GetCmdArg) + { + FString CVarString = Pair.Key; + FString PropertyName = Pair.Value; + + FProperty* Property = GetClass()->FindPropertyByName(FName(*PropertyName)); + if (!Property || !Property->HasAnyPropertyFlags(CPF_Config)) + { + continue; + } + + // Handle a directly parsable commandline + FString ConsoleString; + if (FParse::Value(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgValue(CVarString), ConsoleString)) + { + SetCVarAndPropertyFromValue(CVarString, Property, ConsoleString); + } + else if (FParse::Param(FCommandLine::Get(), *Util::ConsoleVariableToCommandArgParam(CVarString))) + { + SetCVarAndPropertyFromValue(CVarString, Property, TEXT("true")); + } + } +} + +void UBuccaneer4PixelStreaming2Settings::PostInitProperties() +{ + Super::PostInitProperties(); + + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "Initialising Buccaneer4PixelStreaming2 settings."); + + // Set all the CVars to reflect the state of the ini + InitializeCVarsFromProperties(); + + // Validate command line args to log if they're invalid + ValidateCommandLineArgs(); + + // Update CVars and properties based on command line args + ParseCommandlineArgs(); +} \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.cpp new file mode 100644 index 0000000..c30147d --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.cpp @@ -0,0 +1,5 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#include "Logging.h" + +DEFINE_LOG_CATEGORY(LogBuccaneer4PixelStreaming2); \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.h new file mode 100644 index 0000000..a43062e --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Logging.h @@ -0,0 +1,8 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Logging/LogMacros.h" +#include "Logging/StructuredLog.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogBuccaneer4PixelStreaming2, Log, All); \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming2Settings.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming2Settings.h new file mode 100644 index 0000000..e2f3308 --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming2Settings.h @@ -0,0 +1,48 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "Containers/UnrealString.h" +#include "CoreMinimal.h" +#include "Engine/DeveloperSettings.h" + +#include "Buccaneer4PixelStreaming2Settings.generated.h" + +// Config loaded/saved to an .ini file. +// It is also exposed through the plugin settings page in editor. +UCLASS(config = Game, defaultconfig, meta = (DisplayName = "Buccaneer4PixelStreaming2")) +class BUCCANEER4PIXELSTREAMING2_API UBuccaneer4PixelStreaming2Settings : public UDeveloperSettings +{ + GENERATED_BODY() + + virtual ~UBuccaneer4PixelStreaming2Settings() = default; + +public: + static TAutoConsoleVariable CVarEnabled; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer4PixelStreaming2", meta = ( + DisplayName = "Enabled", + ToolTip = "Enables the collection of Pixel Streaming 2 performance metrics" + )) + bool Enabled = true; + + // Begin UDeveloperSettings Interface + virtual FName GetCategoryName() const override; + +#if WITH_EDITOR + virtual FText GetSectionText() const override; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + // End UDeveloperSettings Interface + + // Begin UObject Interface + virtual void PostInitProperties() override; + // End UObject Interface + +private: + void SetCVarAndPropertyFromValue(const FString& CVarName, FProperty* Property, const FString& Value); + void SetCVarFromProperty(const FString& CVarName, FProperty* Property); + + void InitializeCVarsFromProperties(); + void ValidateCommandLineArgs(); + void ParseCommandlineArgs(); +}; \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreaming2Module.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreaming2Module.h new file mode 100644 index 0000000..7e01d4e --- /dev/null +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/IBuccaneer4PixelStreaming2Module.h @@ -0,0 +1,33 @@ +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + +#pragma once + +#include "CoreTypes.h" +#include "Dom/JsonObject.h" +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" + +class BUCCANEER4PIXELSTREAMING2_API IBuccaneer4PixelStreaming2Module : public IModuleInterface +{ +public: + /** + * Singleton-like access to this module's interface. + * Beware calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline IBuccaneer4PixelStreaming2Module& Get() + { + return FModuleManager::LoadModuleChecked("Buccaneer4PixelStreaming2"); + } + + /** + * Checks to see if this module is loaded. + * + * @return True if the module is loaded. + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("Buccaneer4PixelStreaming2"); + } +}; \ No newline at end of file From c504859a066f80942da4d7a38df887694354ecad Mon Sep 17 00:00:00 2001 From: William Belcher Date: Tue, 12 Aug 2025 18:45:06 +1000 Subject: [PATCH 04/35] Fix copyright notices --- .../Buccaneer/Source/BuccaneerEvents/BuccaneerEvents.build.cs | 2 +- .../Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs | 3 ++- .../Private/Buccaneer4PixelStreaming.cpp | 3 ++- .../Private/Buccaneer4PixelStreaming.h | 3 ++- .../Buccaneer4PixelStreaming2.Build.cs | 3 ++- .../Private/Buccaneer4PixelStreaming2.cpp | 3 ++- .../Private/Buccaneer4PixelStreaming2.h | 3 ++- 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/BuccaneerEvents.build.cs b/Plugins/Buccaneer/Source/BuccaneerEvents/BuccaneerEvents.build.cs index 382b29d..ef9a82f 100644 --- a/Plugins/Buccaneer/Source/BuccaneerEvents/BuccaneerEvents.build.cs +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/BuccaneerEvents.build.cs @@ -1,4 +1,4 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. using UnrealBuildTool; diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs index fb5eb26..3994322 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs @@ -1,4 +1,5 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + using UnrealBuildTool; diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp index 19dd728..8e11005 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp @@ -1,4 +1,5 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + #include "Buccaneer4PixelStreaming.h" diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h index e3649d5..ab49ef5 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h @@ -1,4 +1,5 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + #pragma once diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs index 371d4d2..d540ae6 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs @@ -1,4 +1,5 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + using UnrealBuildTool; diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp index 866bf90..60561be 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp @@ -1,4 +1,5 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + #include "Buccaneer4PixelStreaming2.h" diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h index 892c8f0..b4ae159 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h @@ -1,4 +1,5 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright TensorWorks Pty Ltd. All Rights Reserved. + #pragma once From 7b87d8408788c813b5b5b8f33d377f692e91207e Mon Sep 17 00:00:00 2001 From: William Belcher Date: Wed, 13 Aug 2025 11:00:44 +1000 Subject: [PATCH 05/35] Update namespacing --- .../Public/BuccaneerSettings.h | 9 +++++ .../Buccaneer4PixelStreamingSettings.cpp | 30 +------------- .../Buccaneer4PixelStreaming2.Build.cs | 1 - .../Private/Buccaneer4PixelStreaming2.cpp | 7 ++-- .../Private/Buccaneer4PixelStreaming2.h | 1 - .../Buccaneer4PixelStreaming2Settings.cpp | 40 +++---------------- 6 files changed, 19 insertions(+), 69 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h index 834a486..9707ed8 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h @@ -8,6 +8,15 @@ #include "BuccaneerSettings.generated.h" +namespace Util +{ + BUCCANEERCOMMON_API FString ConsoleVariableToCommandArgValue(const FString InCVarName); + + BUCCANEERCOMMON_API FString ConsoleVariableToCommandArgParam(const FString InCVarName); + + BUCCANEERCOMMON_API FString FindCVarFromProperty(const TSet> Set, const FString& Value); +} + // Config loaded/saved to an .ini file. // It is also exposed through the plugin settings page in editor. UCLASS(config = Game, defaultconfig, meta = (DisplayName = "Buccaneer")) diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp index 5bbbcf8..feee1b1 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp @@ -2,39 +2,11 @@ #include "Buccaneer4PixelStreamingSettings.h" +#include "BuccaneerSettings.h" #include "Logging.h" #include "Misc/CommandLine.h" #include "UObject/ReflectedTypeAccessors.h" -namespace Util -{ - FString ConsoleVariableToCommandArgValue(const FString InCVarName) - { - // CVars are . deliminated by section. To get their equivilent commandline arg for parsing - // we need to remove the . and add a "=" - return InCVarName.Replace(TEXT("."), TEXT("")).Append(TEXT("=")); - } - - FString ConsoleVariableToCommandArgParam(const FString InCVarName) - { - // CVars are . deliminated by section. To get their equivilent commandline arg parameter, we need to to remove the . - return InCVarName.Replace(TEXT("."), TEXT("")); - } - - FString FindCVarFromProperty(const TSet> Set, const FString& Value) - { - for (const TPair& Pair : Set) - { - if (Pair.Value == Value) - { - return Pair.Key; - } - } - - return ""; - } -} - static const TSet> GetCmdArg = { { "Buccaneer4PixelStreaming.EnableStats", "Enabled" } }; diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs index d540ae6..c5f5046 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs @@ -1,6 +1,5 @@ // Copyright TensorWorks Pty Ltd. All Rights Reserved. - using UnrealBuildTool; public class Buccaneer4PixelStreaming2 : ModuleRules diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp index 60561be..e1ba971 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp @@ -1,12 +1,11 @@ // Copyright TensorWorks Pty Ltd. All Rights Reserved. - #include "Buccaneer4PixelStreaming2.h" #include "Logging.h" #include "Buccaneer4PixelStreaming2Settings.h" -TMap StatDescriptionMap = { +TMap PSStatDescriptionMap = { {"jitterBufferDelay", "jitterBufferDelay"}, {"framesSent", "framesSent"}, {"framesPerSecond", "framesPerSecond"}, @@ -113,13 +112,13 @@ void FBuccaneer4PixelStreaming2Module::ConsumeStat(FString PlayerId, FName StatN } else { - if(!StatDescriptionMap.Contains(*StatName.ToString())) + if(!PSStatDescriptionMap.Contains(*StatName.ToString())) { UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "{0}", StatName.ToString()); return; } TSharedPtr NewMetricJson = MakeShareable(new FJsonObject()); - NewMetricJson->SetField("description", MakeShared(*StatDescriptionMap[*StatName.ToString()])); + NewMetricJson->SetField("description", MakeShared(*PSStatDescriptionMap[*StatName.ToString()])); TSharedPtr ValueJson = MakeShareable(new FJsonObject()); diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h index b4ae159..a668878 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h @@ -1,6 +1,5 @@ // Copyright TensorWorks Pty Ltd. All Rights Reserved. - #pragma once #include "CoreMinimal.h" diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp index 7564dc5..ef1b5c3 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp @@ -2,40 +2,12 @@ #include "Buccaneer4PixelStreaming2Settings.h" +#include "BuccaneerSettings.h" #include "Logging.h" #include "Misc/CommandLine.h" #include "UObject/ReflectedTypeAccessors.h" -namespace Util -{ - FString ConsoleVariableToCommandArgValue(const FString InCVarName) - { - // CVars are . deliminated by section. To get their equivilent commandline arg for parsing - // we need to remove the . and add a "=" - return InCVarName.Replace(TEXT("."), TEXT("")).Append(TEXT("=")); - } - - FString ConsoleVariableToCommandArgParam(const FString InCVarName) - { - // CVars are . deliminated by section. To get their equivilent commandline arg parameter, we need to to remove the . - return InCVarName.Replace(TEXT("."), TEXT("")); - } - - FString FindCVarFromProperty(const TSet> Set, const FString& Value) - { - for (const TPair& Pair : Set) - { - if (Pair.Value == Value) - { - return Pair.Key; - } - } - - return ""; - } -} - -static const TSet> GetCmdArg = { +static const TSet> GetCmdLineArg = { { "Buccaneer4PixelStreaming2.EnableStats", "Enabled" } }; @@ -63,7 +35,7 @@ void UBuccaneer4PixelStreaming2Settings::PostEditChangeProperty(FPropertyChanged FString PropertyName = PropertyChangedEvent.Property->GetNameCPP(); FString CVarName; - if (CVarName = Util::FindCVarFromProperty(GetCmdArg, PropertyName); !CVarName.IsEmpty()) + if (CVarName = Util::FindCVarFromProperty(GetCmdLineArg, PropertyName); !CVarName.IsEmpty()) { SetCVarFromProperty(CVarName, PropertyChangedEvent.Property); } @@ -223,7 +195,7 @@ void UBuccaneer4PixelStreaming2Settings::InitializeCVarsFromProperties() } FString CVarName; - if (CVarName = Util::FindCVarFromProperty(GetCmdArg, Property->GetNameCPP()); !CVarName.IsEmpty()) + if (CVarName = Util::FindCVarFromProperty(GetCmdLineArg, Property->GetNameCPP()); !CVarName.IsEmpty()) { SetCVarFromProperty(CVarName, Property); continue; @@ -254,7 +226,7 @@ void UBuccaneer4PixelStreaming2Settings::ValidateCommandLineArgs() } bool bValidArg = false; - for (const TPair& Pair : GetCmdArg) + for (const TPair& Pair : GetCmdLineArg) { FString ValidCommandLineArg = Util::ConsoleVariableToCommandArgParam(Pair.Key); if (CurrentCommandLineArg == ValidCommandLineArg) @@ -274,7 +246,7 @@ void UBuccaneer4PixelStreaming2Settings::ValidateCommandLineArgs() void UBuccaneer4PixelStreaming2Settings::ParseCommandlineArgs() { UE_LOGFMT(LogBuccaneer4PixelStreaming2, Verbose, "Updating CVars and properties with command line args"); - for (const TPair& Pair : GetCmdArg) + for (const TPair& Pair : GetCmdLineArg) { FString CVarString = Pair.Key; FString PropertyName = Pair.Value; From 9fa3d6c58076e736b7bfa831631623576aa073b8 Mon Sep 17 00:00:00 2001 From: William Belcher Date: Thu, 14 Aug 2025 10:45:11 +1000 Subject: [PATCH 06/35] Fix Bucc4PS2 crash and add reporting interval project setting --- .../Private/BuccaneerSettings.cpp | 9 ++++++++- .../BuccaneerCommon/Public/BuccaneerSettings.h | 7 +++++++ .../Private/BuccaneerStatsModule.cpp | 4 ++-- .../Private/BuccaneerStatsModule.h | 1 - .../Private/Buccaneer4PixelStreaming2.cpp | 16 +++++++--------- .../Private/Buccaneer4PixelStreaming2.h | 1 - .../Buccaneer4PixelStreaming2Settings.cpp | 9 ++++++++- .../Public/Buccaneer4PixelStreaming2Settings.h | 7 +++++++ 8 files changed, 39 insertions(+), 15 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp index 31d9c83..c476293 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp @@ -92,7 +92,8 @@ static const TSet> GetCmdArg = { { "Buccaneer.ID", "ID" }, { "Buccaneer.EnableStats", "EnableStats" }, { "Buccaneer.EnableEvents", "EnableEvents" }, - { "Buccaneer.Metadata", "Metadata" } + { "Buccaneer.Metadata", "Metadata" }, + { "Buccaneer.ReportingInterval", "ReportingInterval" } }; // Map a legacy cvar to its new property @@ -132,6 +133,12 @@ TAutoConsoleVariable UBuccaneerSettings::CVarMetadata( FConsoleVariableDelegate::CreateLambda([](IConsoleVariable* Var) { Delegates()->OnMetadataChanged.Broadcast(Var); }), ECVF_Default); +TAutoConsoleVariable UBuccaneerSettings::CVarReportingInterval( + TEXT("Buccaneer.ReportingInterval"), + 1.0f, + TEXT("The interval at which to report performance metrics (default: 1.0 seconds)"), + ECVF_Default); + UBuccaneerSettings::FDelegates* UBuccaneerSettings::DelegateSingleton = nullptr; UBuccaneerSettings::FDelegates* UBuccaneerSettings::Delegates() diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h index 9707ed8..d54f435 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h @@ -65,6 +65,13 @@ class BUCCANEERCOMMON_API UBuccaneerSettings : public UDeveloperSettings static TMap GetMetadata(); + static TAutoConsoleVariable CVarReportingInterval; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "Reporting Interval (seconds)", + ToolTip = "The interval at which to report performance metrics. <= 0 disables reporting" + )) + float ReportingInterval = 1.0f; + // Begin UDeveloperSettings Interface virtual FName GetCategoryName() const override; diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp index 7eb79a3..36e7684 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp @@ -68,7 +68,7 @@ bool FBuccaneerStatsModule::IsTickableInEditor() const void FBuccaneerStatsModule::Tick(float DeltaTime) { - if (!UBuccaneerSettings::CVarEnableStats->GetBool()) + if (!UBuccaneerSettings::CVarEnableStats.GetValueOnAnyThread() || UBuccaneerSettings::CVarReportingInterval.GetValueOnAnyThread() <= 0) { // Performance profiling hasn't been inititialized. Don't continue return; @@ -99,7 +99,7 @@ void FBuccaneerStatsModule::Tick(float DeltaTime) ComputeUsedMemory(); } - if ((NowTime - InterimStart) >= InterimDuration) + if ((NowTime - InterimStart) >= UBuccaneerSettings::CVarReportingInterval.GetValueOnAnyThread()) { PushStatsHTTP(); InterimStart = NowTime; diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h index 66107bf..16cff2a 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h @@ -30,7 +30,6 @@ class FBuccaneerStatsModule : public IBuccaneerStatsModule, public FTickableGame // Time keeping variables double LastTickTime = 0.0; double InterimStart = 0.0; - double InterimDuration = 1.0; // Rolling average of times recorded during the defined period double InterimMeanFrameTime = 0.0; double InterimMeanGameThreadTime = 0.0; diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp index e1ba971..d739962 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp @@ -50,7 +50,6 @@ TMap PSStatDescriptionMap = { void FBuccaneer4PixelStreaming2Module::StartupModule() { LoggingStart = FPlatformTime::Seconds(); - ReportingInterval = 1; JsonObject = MakeShareable(new FJsonObject()); @@ -68,7 +67,7 @@ void FBuccaneer4PixelStreaming2Module::ShutdownModule() void FBuccaneer4PixelStreaming2Module::ConsumeStat(FString PlayerId, FName StatName, float StatValue) { - if (!UBuccaneer4PixelStreaming2Settings::CVarEnabled.GetValueOnAnyThread() || PlayerId == TEXT("Application")) + if (!UBuccaneer4PixelStreaming2Settings::CVarEnabled.GetValueOnAnyThread() || PlayerId == TEXT("Application") || UBuccaneer4PixelStreaming2Settings::CVarReportingInterval.GetValueOnAnyThread() <= 0) { return; } @@ -80,11 +79,10 @@ void FBuccaneer4PixelStreaming2Module::ConsumeStat(FString PlayerId, FName StatN * ] * } */ - const TSharedPtr MetricJson; - const TSharedPtr* MetricJsonPtr = &MetricJson; - if(JsonObject->TryGetObjectField(*StatName.ToString(), MetricJsonPtr)) + const TSharedPtr* MetricJson = nullptr; + if(JsonObject->TryGetObjectField(*StatName.ToString(), MetricJson)) { - TArray> ValueArray = MetricJson->GetArrayField(TEXT("value")); + TArray> ValueArray = (*MetricJson)->GetArrayField(TEXT("value")); bool bRequiresCreation = true; for (int i = 0; i < ValueArray.Num(); i++) @@ -107,14 +105,14 @@ void FBuccaneer4PixelStreaming2Module::ConsumeStat(FString PlayerId, FName StatN ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); - MetricJson->SetArrayField((TEXT("value")), ValueArray); + (*MetricJson)->SetArrayField((TEXT("value")), ValueArray); } } else { if(!PSStatDescriptionMap.Contains(*StatName.ToString())) { - UE_LOGFMT(LogBuccaneer4PixelStreaming2, Log, "{0}", StatName.ToString()); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Verbose, "{0}", StatName.ToString()); return; } TSharedPtr NewMetricJson = MakeShareable(new FJsonObject()); @@ -133,7 +131,7 @@ void FBuccaneer4PixelStreaming2Module::ConsumeStat(FString PlayerId, FName StatN } double NowTime = FPlatformTime::Seconds(); - if ( (NowTime - LoggingStart) >= ReportingInterval ) + if ((NowTime - LoggingStart) >= UBuccaneer4PixelStreaming2Settings::CVarReportingInterval.GetValueOnAnyThread()) { LoggingStart = NowTime; TSharedPtr PayloadJson = MakeShareable(new FJsonObject()); diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h index a668878..6a9ffd4 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h @@ -20,7 +20,6 @@ class FBuccaneer4PixelStreaming2Module : public IBuccaneer4PixelStreaming2Module private: double LoggingStart; - double ReportingInterval; TSharedPtr JsonObject; }; diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp index ef1b5c3..a2f2932 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2Settings.cpp @@ -8,7 +8,8 @@ #include "UObject/ReflectedTypeAccessors.h" static const TSet> GetCmdLineArg = { - { "Buccaneer4PixelStreaming2.EnableStats", "Enabled" } + { "Buccaneer4PixelStreaming2.EnableStats", "Enabled" }, + { "Buccaneer4PixelStreaming2.ReportingInterval", "ReportingInterval" } }; TAutoConsoleVariable UBuccaneer4PixelStreaming2Settings::CVarEnabled( @@ -17,6 +18,12 @@ TAutoConsoleVariable UBuccaneer4PixelStreaming2Settings::CVarEnabled( TEXT("Enables the collection and logging of Pixel Streaming stats with Buccaneer (default: true)"), ECVF_Default); +TAutoConsoleVariable UBuccaneer4PixelStreaming2Settings::CVarReportingInterval( + TEXT("Buccaneer4PixelStreaming2.ReportingInterval"), + 1.0f, + TEXT("The interval at which to report Pixel Streaming 2 performance metrics (default: 1.0 seconds)"), + ECVF_Default); + FName UBuccaneer4PixelStreaming2Settings::GetCategoryName() const { return TEXT("Plugins"); diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming2Settings.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming2Settings.h index e2f3308..047986d 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming2Settings.h +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreaming2Settings.h @@ -25,6 +25,13 @@ class BUCCANEER4PIXELSTREAMING2_API UBuccaneer4PixelStreaming2Settings : public )) bool Enabled = true; + static TAutoConsoleVariable CVarReportingInterval; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer4PixelStreaming2", meta = ( + DisplayName = "Reporting Interval (seconds)", + ToolTip = "The interval at which to report Pixel Streaming 2 performance metrics. <= 0 disables reporting" + )) + float ReportingInterval = 1.0f; + // Begin UDeveloperSettings Interface virtual FName GetCategoryName() const override; From 91ac785172fb60d1a0f3d6046d914ba2bd671b63 Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:25:20 +1000 Subject: [PATCH 07/35] Barnacle Gemini New barnacle based branch of buccaneer, with code changes from Gemini Code assist --- .../Private/BuccaneerCommonModule.cpp | 52 +++++++++++++++++-- .../Private/BuccaneerCommonModule.h | 1 + .../Private/BuccaneerSettings.cpp | 16 +++++- .../Public/BuccaneerSettings.h | 14 +++++ .../Private/BuccaneerStatsModule.cpp | 26 ++-------- config.template.bat | 0 6 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 config.template.bat diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index d75d5fc..3c37502 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -10,9 +10,9 @@ void FBuccaneerCommonModule::StartupModule() { - if (UBuccaneerSettings::CVarURL.GetValueOnAnyThread().IsEmpty()) + if (UBuccaneerSettings::CVarURL.GetValueOnAnyThread().IsEmpty() && !UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) { - UE_LOGFMT(LogBuccaneerCommon, Warning, "Buccanner events and stats disabled, provide `BuccaneerURL` cmd-args to enable it"); + UE_LOGFMT(LogBuccaneerCommon, Warning, "Buccanner events and stats disabled, provide `BuccaneerURL` or `BuccaneerEnableJSONOutput` cmd-args to enable it"); UBuccaneerSettings::CVarEnableStats->Set(false, ECVF_SetByCommandline); UBuccaneerSettings::CVarEnableEvents->Set(false, ECVF_SetByCommandline); return; @@ -54,13 +54,57 @@ void FBuccaneerCommonModule::SendStats(TSharedPtr JsonObject) { JsonObject->SetField("id", MakeShared((TEXT("%s"), *UBuccaneerSettings::CVarID.GetValueOnAnyThread()))); JsonObject->SetField("metadata", MakeShared(MetadataJson)); - SendHTTP(UBuccaneerSettings::CVarURL.GetValueOnAnyThread() + FString("/stats"), JsonObject); + + if (UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) + { + SendJSON(TEXT("stats.json"), JsonObject); + } + else + { + SendHTTP(UBuccaneerSettings::CVarURL.GetValueOnAnyThread() + FString("/stats"), JsonObject); + } } void FBuccaneerCommonModule::SendEvent(TSharedPtr JsonObject) { JsonObject->SetField("id", MakeShared((TEXT("%s"), *UBuccaneerSettings::CVarID.GetValueOnAnyThread()))); - SendHTTP(UBuccaneerSettings::CVarURL.GetValueOnAnyThread() + FString("/event"), JsonObject); + + if (UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) + { + SendJSON(TEXT("events.json"), JsonObject); + } + else + { + SendHTTP(UBuccaneerSettings::CVarURL.GetValueOnAnyThread() + FString("/event"), JsonObject); + } +} + +void FBuccaneerCommonModule::SendJSON(FString FileName, TSharedPtr JsonObject) +{ + FString FilePath = FPaths::Combine(UBuccaneerSettings::CVarJSONOutputDirectory.GetValueOnAnyThread(), FileName); + + FString JsonString; + TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&JsonString); + if (!ensure(FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter))) + { + UE_LOGFMT(LogBuccaneerCommon, Warning, "Cannot serialize json object"); + return; + } + + FString FileContent; + FFileHelper::LoadFileToString(FileContent, *FilePath); + + if (FileContent.IsEmpty()) + { + FileContent = TEXT("[") + JsonString + TEXT("]"); + } + else + { + FileContent.RemoveFromEnd(TEXT("]")); + FileContent += TEXT(",") + JsonString + TEXT("]"); + } + + FFileHelper::SaveStringToFile(FileContent, *FilePath); } void FBuccaneerCommonModule::SendHTTP(FString URL, TSharedPtr JsonObject) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h index 018bb5a..f2b7691 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h @@ -22,6 +22,7 @@ class FBuccaneerCommonModule : public IBuccaneerCommonModule private: void SendHTTP(FString URL, TSharedPtr JsonObject); + void SendJSON(FString FileName, TSharedPtr JsonObject); void FormatMetadata(IConsoleVariable* Var); TSharedPtr MetadataJson = MakeShareable(new FJsonObject()); diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp index c476293..5828952 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp @@ -93,7 +93,9 @@ static const TSet> GetCmdArg = { { "Buccaneer.EnableStats", "EnableStats" }, { "Buccaneer.EnableEvents", "EnableEvents" }, { "Buccaneer.Metadata", "Metadata" }, - { "Buccaneer.ReportingInterval", "ReportingInterval" } + { "Buccaneer.ReportingInterval", "ReportingInterval" }, + { "Buccaneer.EnableJSONOutput", "EnableJSONOutput" }, + { "Buccaneer.JSONOutputDirectory", "JSONOutputDirectory" } }; // Map a legacy cvar to its new property @@ -139,6 +141,18 @@ TAutoConsoleVariable UBuccaneerSettings::CVarReportingInterval( TEXT("The interval at which to report performance metrics (default: 1.0 seconds)"), ECVF_Default); +TAutoConsoleVariable UBuccaneerSettings::CVarEnableJSONOutput( + TEXT("Buccaneer.EnableJSONOutput"), + false, + TEXT("Enables writing stats and events to a JSON file (default: false)"), + ECVF_Default); + +TAutoConsoleVariable UBuccaneerSettings::CVarJSONOutputDirectory( + TEXT("Buccaneer.JSONOutputDirectory"), + TEXT(""), + TEXT("The directory to write JSON files to"), + ECVF_Default); + UBuccaneerSettings::FDelegates* UBuccaneerSettings::DelegateSingleton = nullptr; UBuccaneerSettings::FDelegates* UBuccaneerSettings::Delegates() diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h index d54f435..bc521cc 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h @@ -72,6 +72,20 @@ class BUCCANEERCOMMON_API UBuccaneerSettings : public UDeveloperSettings )) float ReportingInterval = 1.0f; + static TAutoConsoleVariable CVarEnableJSONOutput; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "Enable JSON Output", + ToolTip = "Enables writing stats and events to a JSON file" + )) + bool EnableJSONOutput = false; + + static TAutoConsoleVariable CVarJSONOutputDirectory; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "JSON Output Directory", + ToolTip = "The directory to write JSON files to" + )) + FString JSONOutputDirectory = FPaths::ProjectLogDir(); + // Begin UDeveloperSettings Interface virtual FName GetCategoryName() const override; diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp index 36e7684..dbbad17 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp @@ -14,19 +14,6 @@ #define COMPUTE_MEAN(CurrentMean, NewTime, FrameCount) \ ((FrameCount - 1) * CurrentMean + NewTime) / FrameCount; -TMap StatDescriptionMap = { - { "mean_fps", "The average fps" }, - { "mean_frametime", "The average frametime" }, - { "mean_gamethreadtime", "The average game thread time" }, - { "mean_gputime", "The average gpu time" }, - { "mean_rendertime", "The average render thread time" }, - { "mean_rhithreadtime", "The average rhi thread time" }, - { "memory_virtual", "The virtual memory usage" }, - { "memory_physical", "The physical memory usage" }, - { "memory_gpu", "The gpu memory usage" }, - { "num_hangs", "The number of frames hung in the recording interval" } -}; - void FBuccaneerStatsModule::StartupModule() { MetricJson = MakeShareable(new FJsonObject()); @@ -38,16 +25,7 @@ void FBuccaneerStatsModule::StartupModule() void FBuccaneerStatsModule::UpdateMetric(FString Name, double Value) { - if(!StatDescriptionMap.Contains(Name)) - { - UE_LOGFMT(LogBuccaneerStats, Log, "No description for metric {0}", Name); - return; - } - - TSharedPtr MetricInfoJson = MakeShareable(new FJsonObject()); - MetricInfoJson->SetField("description", MakeShared((TEXT("%s"), *StatDescriptionMap[Name]))); - MetricInfoJson->SetField("value", MakeShared(Value)); - MetricJson->SetField(*Name, MakeShared(MetricInfoJson)); + MetricJson->SetField(*Name, MakeShared(Value)); } void FBuccaneerStatsModule::ShutdownModule() @@ -150,6 +128,8 @@ void FBuccaneerStatsModule::PushStatsHTTP() UpdateMetric("memory_physical", UsedPhysicalMemory); UpdateMetric("memory_gpu", UsedGPUMemory); UpdateMetric("num_hangs", InterimHangCount); + + JsonObject->SetField(TEXT("timestamp"), MakeShared(FDateTime::UtcNow().ToIso8601())); IBuccaneerCommonModule::Get().SendStats(JsonObject); } diff --git a/config.template.bat b/config.template.bat new file mode 100644 index 0000000..e69de29 From 07135e74b708cf9f0f0ca93f38c98fd58858299c Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Wed, 20 Aug 2025 12:53:30 +1000 Subject: [PATCH 08/35] Metrics and time stamp updates --- .../Private/BuccaneerStatsModule.cpp | 14 ++++++++------ .../BuccaneerStats/Private/BuccaneerStatsModule.h | 3 ++- .../Private/Buccaneer4PixelStreaming.cpp | 2 -- .../Private/Buccaneer4PixelStreaming2.cpp | 2 -- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp index dbbad17..370a726 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp @@ -10,22 +10,21 @@ #include "RHI.h" #include "Stats/Stats.h" #include "Stats/StatsData.h" +#include "Math/UnrealMathUtility.h" #define COMPUTE_MEAN(CurrentMean, NewTime, FrameCount) \ ((FrameCount - 1) * CurrentMean + NewTime) / FrameCount; void FBuccaneerStatsModule::StartupModule() { - MetricJson = MakeShareable(new FJsonObject()); JsonObject = MakeShareable(new FJsonObject()); - JsonObject->SetField(TEXT("metrics"), MakeShared(MetricJson)); - LastTickTime = InterimStart = FPlatformTime::Seconds(); + AppStartTime = LastTickTime = InterimStart = FPlatformTime::Seconds(); } void FBuccaneerStatsModule::UpdateMetric(FString Name, double Value) { - MetricJson->SetField(*Name, MakeShared(Value)); + JsonObject->SetField(*Name, MakeShared(Value)); } void FBuccaneerStatsModule::ShutdownModule() @@ -128,8 +127,11 @@ void FBuccaneerStatsModule::PushStatsHTTP() UpdateMetric("memory_physical", UsedPhysicalMemory); UpdateMetric("memory_gpu", UsedGPUMemory); UpdateMetric("num_hangs", InterimHangCount); - - JsonObject->SetField(TEXT("timestamp"), MakeShared(FDateTime::UtcNow().ToIso8601())); + + const double ElapsedSeconds = FPlatformTime::Seconds() - AppStartTime; + const double ElapsedMilliseconds = ElapsedSeconds * 1000; + const int64 RoundedMilliseconds = FMath::RoundToInt64(ElapsedMilliseconds); + JsonObject->SetField(TEXT("timestamp"), MakeShared(RoundedMilliseconds)); IBuccaneerCommonModule::Get().SendStats(JsonObject); } diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h index 16cff2a..1e8b186 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h @@ -47,5 +47,6 @@ class FBuccaneerStatsModule : public IBuccaneerStatsModule, public FTickableGame // Variable for storing logging URL and logging object TSharedPtr JsonObject; - TSharedPtr MetricJson; + + double AppStartTime = 0.0; }; diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp index 8e11005..536ec52 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp @@ -120,9 +120,7 @@ void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId Player return; } TSharedPtr NewMetricJson = MakeShareable(new FJsonObject()); - NewMetricJson->SetField("description", MakeShared(*StatDescriptionMap[*StatName.ToString()])); - TSharedPtr ValueJson = MakeShareable(new FJsonObject()); ValueJson->SetField(*PlayerId, MakeShared(StatValue)); diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp index d739962..2dad406 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp @@ -116,9 +116,7 @@ void FBuccaneer4PixelStreaming2Module::ConsumeStat(FString PlayerId, FName StatN return; } TSharedPtr NewMetricJson = MakeShareable(new FJsonObject()); - NewMetricJson->SetField("description", MakeShared(*PSStatDescriptionMap[*StatName.ToString()])); - TSharedPtr ValueJson = MakeShareable(new FJsonObject()); ValueJson->SetField(*PlayerId, MakeShared(StatValue)); From f8a998397d8050abeb8d25a8aff4b78e80cdec18 Mon Sep 17 00:00:00 2001 From: Denis Phoenix <127062860+DenisTensorWorks@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:24:07 +1000 Subject: [PATCH 09/35] Update BuccaneerCommonModule.cpp - Added new logic to write to file without opening/closing it --- .../Private/BuccaneerCommonModule.cpp | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index 3c37502..d8874f2 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -8,6 +8,10 @@ #include "Interfaces/IHttpResponse.h" #include "Logging.h" +#include "HAL/PlatformFilemanager.h" +#include "Misc/FileHelper.h" +#include "HAL/FileManager.h" + void FBuccaneerCommonModule::StartupModule() { if (UBuccaneerSettings::CVarURL.GetValueOnAnyThread().IsEmpty() && !UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) @@ -82,7 +86,9 @@ void FBuccaneerCommonModule::SendEvent(TSharedPtr JsonObject) void FBuccaneerCommonModule::SendJSON(FString FileName, TSharedPtr JsonObject) { FString FilePath = FPaths::Combine(UBuccaneerSettings::CVarJSONOutputDirectory.GetValueOnAnyThread(), FileName); - + +// This is how we turn the JSON object into a string + FString JsonString; TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&JsonString); if (!ensure(FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter))) @@ -90,21 +96,38 @@ void FBuccaneerCommonModule::SendJSON(FString FileName, TSharedPtr UE_LOGFMT(LogBuccaneerCommon, Warning, "Cannot serialize json object"); return; } + + IFileManager& FileManager = IFileManager::Get(); + + // Check if file exists + bool bFileExists = FileManager.FileExists(*FilePath); + + // Open for read/write (no "truncate") + TUniquePtr FileAr(FileManager.CreateFileWriter(*FilePath, FILEWRITE_Append)); - FString FileContent; - FFileHelper::LoadFileToString(FileContent, *FilePath); + if (!FileAr) + { + UE_LOG(LogTemp, Error, TEXT("Failed to open file for append: %s"), *FilePath); + return; + } - if (FileContent.IsEmpty()) + if (!bFileExists) { - FileContent = TEXT("[") + JsonString + TEXT("]"); + // First time writing: start a fresh JSON array + FString Start = TEXT("[") + TEXT("\n") + JsonString + TEXT("\n") + TEXT("]"); + FTCHARToUTF8 Converter(*Start); + FileAr->Serialize((UTF8CHAR*)Converter.Get(), Converter.Length()); } else { - FileContent.RemoveFromEnd(TEXT("]")); - FileContent += TEXT(",") + JsonString + TEXT("]"); + // Not first time writing: append new array to the end of the file, just before the "]" + FileAr->Seek(FileAr->TotalSize() - 2); + FString Append = TEXT(",") + TEXT("\n") + JsonString; + FTCHARToUTF8 Converter(*Append); + FileAr->Serialize((UTF8CHAR*)Converter.Get(), Converter.Length()); } - FFileHelper::SaveStringToFile(FileContent, *FilePath); + FileAr->Close(); } void FBuccaneerCommonModule::SendHTTP(FString URL, TSharedPtr JsonObject) @@ -163,4 +186,4 @@ void FBuccaneerCommonModule::FormatMetadata(IConsoleVariable* Var) } } -IMPLEMENT_MODULE(FBuccaneerCommonModule, BuccaneerCommon) \ No newline at end of file +IMPLEMENT_MODULE(FBuccaneerCommonModule, BuccaneerCommon) From 71bd4e6e8619ae1974e3dfc197f6c533a25ceab8 Mon Sep 17 00:00:00 2001 From: Denis Phoenix <127062860+DenisTensorWorks@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:39:03 +1000 Subject: [PATCH 10/35] Update BuccaneerCommonModule.cpp - Fix TEXT arrays issue --- .../Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index d8874f2..60d8141 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -114,7 +114,7 @@ void FBuccaneerCommonModule::SendJSON(FString FileName, TSharedPtr if (!bFileExists) { // First time writing: start a fresh JSON array - FString Start = TEXT("[") + TEXT("\n") + JsonString + TEXT("\n") + TEXT("]"); + FString Start = TEXT("[\n") + JsonString + TEXT("\n]"); FTCHARToUTF8 Converter(*Start); FileAr->Serialize((UTF8CHAR*)Converter.Get(), Converter.Length()); } @@ -122,7 +122,7 @@ void FBuccaneerCommonModule::SendJSON(FString FileName, TSharedPtr { // Not first time writing: append new array to the end of the file, just before the "]" FileAr->Seek(FileAr->TotalSize() - 2); - FString Append = TEXT(",") + TEXT("\n") + JsonString; + FString Append = TEXT(",\n") + JsonString; FTCHARToUTF8 Converter(*Append); FileAr->Serialize((UTF8CHAR*)Converter.Get(), Converter.Length()); } From 4bad6e8b4516ca5b1461838b16d5479787e47bf5 Mon Sep 17 00:00:00 2001 From: Denis Phoenix <127062860+DenisTensorWorks@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:34:10 +1000 Subject: [PATCH 11/35] Update BuccaneerCommonModule.cpp - Final fix --- .../Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index 60d8141..4e94050 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -122,7 +122,7 @@ void FBuccaneerCommonModule::SendJSON(FString FileName, TSharedPtr { // Not first time writing: append new array to the end of the file, just before the "]" FileAr->Seek(FileAr->TotalSize() - 2); - FString Append = TEXT(",\n") + JsonString; + FString Append = TEXT(",\n") + JsonString + TEXT("\n]"); FTCHARToUTF8 Converter(*Append); FileAr->Serialize((UTF8CHAR*)Converter.Get(), Converter.Length()); } From 1aa7d3ba594d4e503a534720a7e55fc9d7b4daf7 Mon Sep 17 00:00:00 2001 From: Luke Bermingham <1215582+lukehb@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:44:29 +1000 Subject: [PATCH 12/35] Update Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index 4e94050..af2ff5d 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -8,8 +8,6 @@ #include "Interfaces/IHttpResponse.h" #include "Logging.h" -#include "HAL/PlatformFilemanager.h" -#include "Misc/FileHelper.h" #include "HAL/FileManager.h" void FBuccaneerCommonModule::StartupModule() From 5c5b3205fa83e4a34ae09803202730549d28fc08 Mon Sep 17 00:00:00 2001 From: Denis Phoenix <127062860+DenisTensorWorks@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:51:47 +1000 Subject: [PATCH 13/35] - Added logic to prevent race condition - Added protection from writing into a corrupted file that is not big enough --- .../Private/BuccaneerCommonModule.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index af2ff5d..50731be 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -109,7 +109,8 @@ void FBuccaneerCommonModule::SendJSON(FString FileName, TSharedPtr return; } - if (!bFileExists) + // If TotalSize is 0, the file was just created or was empty. + if (FileAr->TotalSize() == 0) { // First time writing: start a fresh JSON array FString Start = TEXT("[\n") + JsonString + TEXT("\n]"); @@ -118,7 +119,15 @@ void FBuccaneerCommonModule::SendJSON(FString FileName, TSharedPtr } else { - // Not first time writing: append new array to the end of the file, just before the "]" + // Not first time writing: append new array to the end of the file, just before the "]" + const int64 CurrentSize = FileAr->TotalSize(); + if (CurrentSize < 2) + { + UE_LOG(LogBuccaneerCommon, Error, TEXT("Cannot append to JSON file '%s': file is too small to be a valid array."), *FilePath); + return; + } + + // Seek before the last two characters, assuming they are '\n]'. FileAr->Seek(FileAr->TotalSize() - 2); FString Append = TEXT(",\n") + JsonString + TEXT("\n]"); FTCHARToUTF8 Converter(*Append); From 65a46fa227c3c3a5eb996cab0faca7ee20c577cc Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:28:01 +1000 Subject: [PATCH 14/35] Fix false Append removal --- .../Private/BuccaneerCommonModule.cpp | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index 50731be..9ffe9dc 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -101,7 +101,7 @@ void FBuccaneerCommonModule::SendJSON(FString FileName, TSharedPtr bool bFileExists = FileManager.FileExists(*FilePath); // Open for read/write (no "truncate") - TUniquePtr FileAr(FileManager.CreateFileWriter(*FilePath, FILEWRITE_Append)); + TUniquePtr FileAr(FileManager.CreateFileWriter(*FilePath, FILEWRITE_Append )); if (!FileAr) { @@ -109,28 +109,22 @@ void FBuccaneerCommonModule::SendJSON(FString FileName, TSharedPtr return; } - // If TotalSize is 0, the file was just created or was empty. - if (FileAr->TotalSize() == 0) + // If file is empty or just an empty array like "[]", start a new array. + if (FileAr->TotalSize() <= 2) { - // First time writing: start a fresh JSON array + // First time writing OR writing to a corrupted file. + FileAr->Seek(0); FString Start = TEXT("[\n") + JsonString + TEXT("\n]"); FTCHARToUTF8 Converter(*Start); FileAr->Serialize((UTF8CHAR*)Converter.Get(), Converter.Length()); } else { - // Not first time writing: append new array to the end of the file, just before the "]" - const int64 CurrentSize = FileAr->TotalSize(); - if (CurrentSize < 2) - { - UE_LOG(LogBuccaneerCommon, Error, TEXT("Cannot append to JSON file '%s': file is too small to be a valid array."), *FilePath); - return; - } - + // Not first time writing: Insert new JSON at correct position. // Seek before the last two characters, assuming they are '\n]'. FileAr->Seek(FileAr->TotalSize() - 2); - FString Append = TEXT(",\n") + JsonString + TEXT("\n]"); - FTCHARToUTF8 Converter(*Append); + FString Content = TEXT(",\n") + JsonString + TEXT("\n]"); + FTCHARToUTF8 Converter(*Content); FileAr->Serialize((UTF8CHAR*)Converter.Get(), Converter.Length()); } From de1a9ef2168913620c09095eb0ffd94d4c433e5b Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:41:31 +1000 Subject: [PATCH 15/35] Update PushStatsHTTP to PushStats Better describes the function --- .../Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp | 4 ++-- .../Source/BuccaneerStats/Private/BuccaneerStatsModule.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp index 370a726..2d259a0 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp @@ -78,7 +78,7 @@ void FBuccaneerStatsModule::Tick(float DeltaTime) } if ((NowTime - InterimStart) >= UBuccaneerSettings::CVarReportingInterval.GetValueOnAnyThread()) { - PushStatsHTTP(); + PushStats(); InterimStart = NowTime; InterimHangCount = 0; InterimFrameCount = 1; @@ -113,7 +113,7 @@ void FBuccaneerStatsModule::ComputeUsedMemory() #endif } -void FBuccaneerStatsModule::PushStatsHTTP() +void FBuccaneerStatsModule::PushStats() { // Collected Metrics // name value diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h index 1e8b186..50a792a 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h @@ -23,7 +23,7 @@ class FBuccaneerStatsModule : public IBuccaneerStatsModule, public FTickableGame TStatId GetStatId() const override; private: - void PushStatsHTTP(); + void PushStats(); void ComputeUsedMemory(); void UpdateMetric(FString Name, double Value); From f066ecbb5352667b8dd3c75914f9a463b1b72275 Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Thu, 28 Aug 2025 14:36:19 +1000 Subject: [PATCH 16/35] Reverting back to previous stat sending to restore original Buccaneer functionality. Tested and working. Co-authored-by: Luke Bermingham Denis was here too! --- .../Private/BuccaneerCommonModule.cpp | 1 - .../Private/BuccaneerStatsModule.cpp | 30 ++++++++++++++++--- .../Private/BuccaneerStatsModule.h | 4 +-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index 9ffe9dc..6ca0424 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -7,7 +7,6 @@ #include "Interfaces/IHttpRequest.h" #include "Interfaces/IHttpResponse.h" #include "Logging.h" - #include "HAL/FileManager.h" void FBuccaneerCommonModule::StartupModule() diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp index 2d259a0..0ee7738 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp @@ -15,16 +15,38 @@ #define COMPUTE_MEAN(CurrentMean, NewTime, FrameCount) \ ((FrameCount - 1) * CurrentMean + NewTime) / FrameCount; +TMap StatDescriptionMap = { + {"mean_fps", "The average fps"}, + {"mean_frametime", "The average frametime"}, + {"mean_gamethreadtime", "The average game thread time"}, + {"mean_gputime", "The average gpu time"}, + {"mean_rendertime", "The average render thread time"}, + {"mean_rhithreadtime", "The average rhi thread time"}, + {"memory_virtual", "The virtual memory usage"}, + {"memory_physical", "The physical memory usage"}, + {"memory_gpu", "The gpu memory usage"}, + {"num_hangs", "The number of frames hung in the recording interval"}}; + void FBuccaneerStatsModule::StartupModule() { + MetricJson = MakeShareable(new FJsonObject()); JsonObject = MakeShareable(new FJsonObject()); - + JsonObject->SetField(TEXT("metrics"), MakeShared(MetricJson)); AppStartTime = LastTickTime = InterimStart = FPlatformTime::Seconds(); } void FBuccaneerStatsModule::UpdateMetric(FString Name, double Value) { - JsonObject->SetField(*Name, MakeShared(Value)); + if (!StatDescriptionMap.Contains(Name)) + { + UE_LOGFMT(LogBuccaneerStats, Log, "No description for metric {0}", Name); + return; + } + + TSharedPtr MetricInfoJson = MakeShareable(new FJsonObject()); + MetricInfoJson->SetField("description", MakeShared((TEXT("%s"), *StatDescriptionMap[Name]))); + MetricInfoJson->SetField("value", MakeShared(Value)); + MetricJson->SetField(*Name, MakeShared(MetricInfoJson)); } void FBuccaneerStatsModule::ShutdownModule() @@ -110,7 +132,7 @@ void FBuccaneerStatsModule::ComputeUsedMemory() } } UsedGPUMemory = (double)(TotalMemory / 1024.f / 1024.f); -#endif +#endif } void FBuccaneerStatsModule::PushStats() @@ -128,7 +150,7 @@ void FBuccaneerStatsModule::PushStats() UpdateMetric("memory_gpu", UsedGPUMemory); UpdateMetric("num_hangs", InterimHangCount); - const double ElapsedSeconds = FPlatformTime::Seconds() - AppStartTime; + const double ElapsedSeconds = FPlatformTime::Seconds() - AppStartTime; const double ElapsedMilliseconds = ElapsedSeconds * 1000; const int64 RoundedMilliseconds = FMath::RoundToInt64(ElapsedMilliseconds); JsonObject->SetField(TEXT("timestamp"), MakeShared(RoundedMilliseconds)); diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h index 50a792a..24b8090 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h @@ -47,6 +47,6 @@ class FBuccaneerStatsModule : public IBuccaneerStatsModule, public FTickableGame // Variable for storing logging URL and logging object TSharedPtr JsonObject; - - double AppStartTime = 0.0; + TSharedPtr MetricJson; + double AppStartTime = 0.0; }; From 2c063f30b853fd6abee684650cd2e02aae321275 Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:56:09 +1000 Subject: [PATCH 17/35] Refactor metrics handling to use FMetricsCollection Introduces FMetricsCollection and FBuccaneerMetric structures for unified metrics representation. Replaces ad-hoc JSON construction with structured metrics collection and serialization in BuccaneerCommon, BuccaneerStats, and PixelStreaming modules. Updates interfaces and implementations to use SendMetrics instead of SendStats, improving code clarity and extensibility for metrics reporting. Co-Authored-By: Denis Phoenix <127062860+DenisTensorWorks@users.noreply.github.com> Co-Authored-By: Luke Bermingham <1215582+lukehb@users.noreply.github.com> --- .../Private/BuccaneerCommonModule.cpp | 84 +++++++------ .../Private/BuccaneerCommonModule.h | 10 +- .../Private/BuccaneerMetrics.cpp | 107 ++++++++++++++++ .../BuccaneerCommon/Public/BuccaneerMetrics.h | 42 +++++++ .../Public/IBuccaneerCommonModule.h | 24 ++-- .../Private/BuccaneerStatsModule.cpp | 70 +++-------- .../Private/BuccaneerStatsModule.h | 6 +- .../Private/Buccaneer4PixelStreaming.cpp | 114 +++++++----------- .../Private/Buccaneer4PixelStreaming.h | 4 +- .../Private/Buccaneer4PixelStreaming2.cpp | 110 +++++++---------- .../Private/Buccaneer4PixelStreaming2.h | 4 +- 11 files changed, 326 insertions(+), 249 deletions(-) create mode 100644 Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp create mode 100644 Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index 6ca0424..b3a2f22 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -1,7 +1,7 @@ // Copyright TensorWorks Pty Ltd. All Rights Reserved. #include "BuccaneerCommonModule.h" - +#include "BuccaneerMetrics.h" #include "BuccaneerSettings.h" #include "HttpModule.h" #include "Interfaces/IHttpRequest.h" @@ -26,10 +26,10 @@ void FBuccaneerCommonModule::StartupModule() UBuccaneerSettings::CVarID->Set(*InstanceIDOverride, ECVF_SetByCommandline); } - if (UBuccaneerSettings::FDelegates* Delegates = UBuccaneerSettings::Delegates()) - { - Delegates->OnMetadataChanged.AddRaw(this, &FBuccaneerCommonModule::FormatMetadata); - } + if (UBuccaneerSettings::FDelegates *Delegates = UBuccaneerSettings::Delegates()) + { + Delegates->OnMetadataChanged.AddRaw(this, &FBuccaneerCommonModule::FormatMetadata); + } FormatMetadata(nullptr); @@ -41,7 +41,7 @@ void FBuccaneerCommonModule::ShutdownModule() { } -FBuccaneerCommonModule::FReadyEvent& FBuccaneerCommonModule::OnReady() +FBuccaneerCommonModule::FReadyEvent &FBuccaneerCommonModule::OnReady() { return ReadyEvent; } @@ -51,41 +51,48 @@ bool FBuccaneerCommonModule::IsReady() return bModuleReady; } -void FBuccaneerCommonModule::SendStats(TSharedPtr JsonObject) +void FBuccaneerCommonModule::SendMetrics(const FMetricsCollection &StatsCollection) { - JsonObject->SetField("id", MakeShared((TEXT("%s"), *UBuccaneerSettings::CVarID.GetValueOnAnyThread()))); - JsonObject->SetField("metadata", MakeShared(MetadataJson)); + const FString BuccaneerID = UBuccaneerSettings::CVarID.GetValueOnAnyThread(); if (UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) { - SendJSON(TEXT("stats.json"), JsonObject); + // Case: Sending stats to disk (we want to use our un-nested JSON format) + TSharedPtr JsonObject = StatsCollection.ToJsonUnnested(); + TSharedPtr JsonBuccaneerID = MakeShared((TEXT("%s"), *BuccaneerID)); + JsonObject->SetField("id", JsonBuccaneerID); + WriteJSON(TEXT("stats.json"), JsonObject); } - else + else if (UBuccaneerSettings::CVarURL.GetValueOnAnyThread() != "") { + // Case: Sending stats to Buccaneer server (we want to use the nested JSON format) + TSharedPtr JsonObject = StatsCollection.ToJsonNested(); + TSharedPtr JsonBuccaneerID = MakeShared((TEXT("%s"), *BuccaneerID)); + JsonObject->SetField("id", JsonBuccaneerID); + JsonObject->SetField("metadata", MakeShared(MetadataJson)); SendHTTP(UBuccaneerSettings::CVarURL.GetValueOnAnyThread() + FString("/stats"), JsonObject); } } void FBuccaneerCommonModule::SendEvent(TSharedPtr JsonObject) { - JsonObject->SetField("id", MakeShared((TEXT("%s"), *UBuccaneerSettings::CVarID.GetValueOnAnyThread()))); + const FString BuccaneerID = UBuccaneerSettings::CVarID.GetValueOnAnyThread(); + TSharedPtr JsonBuccaneerID = MakeShared((TEXT("%s"), *BuccaneerID)); + JsonObject->SetField("id", JsonBuccaneerID); - if (UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) - { - SendJSON(TEXT("events.json"), JsonObject); - } - else + // Only send events to server if we are not in JSON writing mode + if (!UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) { SendHTTP(UBuccaneerSettings::CVarURL.GetValueOnAnyThread() + FString("/event"), JsonObject); } } -void FBuccaneerCommonModule::SendJSON(FString FileName, TSharedPtr JsonObject) +void FBuccaneerCommonModule::WriteJSON(FString FileName, TSharedPtr JsonObject) { FString FilePath = FPaths::Combine(UBuccaneerSettings::CVarJSONOutputDirectory.GetValueOnAnyThread(), FileName); - -// This is how we turn the JSON object into a string - + + // This is how we turn the JSON object into a string + FString JsonString; TSharedRef> JsonWriter = TJsonWriterFactory<>::Create(&JsonString); if (!ensure(FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter))) @@ -93,14 +100,14 @@ void FBuccaneerCommonModule::SendJSON(FString FileName, TSharedPtr UE_LOGFMT(LogBuccaneerCommon, Warning, "Cannot serialize json object"); return; } - - IFileManager& FileManager = IFileManager::Get(); + + IFileManager &FileManager = IFileManager::Get(); // Check if file exists bool bFileExists = FileManager.FileExists(*FilePath); // Open for read/write (no "truncate") - TUniquePtr FileAr(FileManager.CreateFileWriter(*FilePath, FILEWRITE_Append )); + TUniquePtr FileAr(FileManager.CreateFileWriter(*FilePath, FILEWRITE_Append)); if (!FileAr) { @@ -108,23 +115,23 @@ void FBuccaneerCommonModule::SendJSON(FString FileName, TSharedPtr return; } - // If file is empty or just an empty array like "[]", start a new array. - if (FileAr->TotalSize() <= 2) + // If file is empty or just an empty array like "[]", start a new array. + if (FileAr->TotalSize() <= 2) { // First time writing OR writing to a corrupted file. FileAr->Seek(0); FString Start = TEXT("[\n") + JsonString + TEXT("\n]"); FTCHARToUTF8 Converter(*Start); - FileAr->Serialize((UTF8CHAR*)Converter.Get(), Converter.Length()); + FileAr->Serialize((UTF8CHAR *)Converter.Get(), Converter.Length()); } else { - // Not first time writing: Insert new JSON at correct position. - // Seek before the last two characters, assuming they are '\n]'. - FileAr->Seek(FileAr->TotalSize() - 2); + // Not first time writing: Insert new JSON at correct position. + // Seek before the last two characters, assuming they are '\n]'. + FileAr->Seek(FileAr->TotalSize() - 2); FString Content = TEXT(",\n") + JsonString + TEXT("\n]"); - FTCHARToUTF8 Converter(*Content); - FileAr->Serialize((UTF8CHAR*)Converter.Get(), Converter.Length()); + FTCHARToUTF8 Converter(*Content); + FileAr->Serialize((UTF8CHAR *)Converter.Get(), Converter.Length()); } FileAr->Close(); @@ -146,7 +153,7 @@ void FBuccaneerCommonModule::SendHTTP(FString URL, TSharedPtr JsonO HttpRequest->SetVerb(TEXT("POST")); HttpRequest->SetContentAsString(Body); HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded) - { + { FString ResponseStr, ErrorStr; if (bSucceeded && HttpResponse.IsValid()) @@ -165,23 +172,22 @@ void FBuccaneerCommonModule::SendHTTP(FString URL, TSharedPtr JsonO if (!ErrorStr.IsEmpty()) { UE_LOGFMT(LogBuccaneerCommon, Warning, "Push event response: {0}", *ErrorStr); - } - }); + } }); HttpRequest->ProcessRequest(); } -void FBuccaneerCommonModule::FormatMetadata(IConsoleVariable* Var) +void FBuccaneerCommonModule::FormatMetadata(IConsoleVariable *Var) { // Additional Metadata TMap MetadataMap = UBuccaneerSettings::GetMetadata(); - for (const TPair& Pair : MetadataMap) + for (const TPair &Pair : MetadataMap) { - if(Pair.Key.IsEmpty() || Pair.Value.IsEmpty()) + if (Pair.Key.IsEmpty() || Pair.Value.IsEmpty()) { continue; } - + MetadataJson->SetField(*Pair.Key, MakeShared((TEXT("%s"), *Pair.Value))); } } diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h index f2b7691..7a13a28 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h @@ -4,6 +4,8 @@ #include "IBuccaneerCommonModule.h" +struct FMetricsCollection; + class FBuccaneerCommonModule : public IBuccaneerCommonModule { public: @@ -13,16 +15,16 @@ class FBuccaneerCommonModule : public IBuccaneerCommonModule virtual FReadyEvent& OnReady() override; virtual bool IsReady() override; - virtual void SendStats(TSharedPtr JsonObject) override; + virtual void SendMetrics(const FMetricsCollection& StatsCollection) override; virtual void SendEvent(TSharedPtr JsonObject) override; private: - bool bModuleReady = false; - FReadyEvent ReadyEvent; + bool bModuleReady = false; + FReadyEvent ReadyEvent; private: void SendHTTP(FString URL, TSharedPtr JsonObject); - void SendJSON(FString FileName, TSharedPtr JsonObject); + void WriteJSON(FString FileName, TSharedPtr JsonObject); void FormatMetadata(IConsoleVariable* Var); TSharedPtr MetadataJson = MakeShareable(new FJsonObject()); diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp new file mode 100644 index 0000000..9993843 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp @@ -0,0 +1,107 @@ +#include "BuccaneerMetrics.h" + +TSharedPtr FMetricsCollection::ToJsonNested() const +{ + // This function makes a JSON object that follows this structure: + /* + * + * { + * "metrics": + * { + * "fps": + * { + * "description": "The framerate of the game" + * "value": 60 + * } + * "player0": + * { + * "bitrate": + * { + * "description": "This peer's streaming bitrate" + * "value": 1000 + * } + * } + * } + * } + */ + + TSharedPtr JsonObject = MakeShareable(new FJsonObject()); + TSharedPtr MetricsJson = MakeShareable(new FJsonObject()); + + for (const FBuccaneerMetric &Stat : Metrics) + { + TSharedPtr StatJson = MakeShareable(new FJsonObject()); + StatJson->SetStringField(TEXT("description"), Stat.Description); + StatJson->SetNumberField(TEXT("value"), Stat.Value); + MetricsJson->SetObjectField(Stat.Name, StatJson); + } + + // This is what it wants for Pixel Streaming: + /** + * "{StatName}": { + * "description": "{StatDescription}", + * "value": [ + * "{PlayerId}": {StatValue} + * ] + * } + */ + + // Basically just need to do this logic from original Buc: + + // From: Private/Buccaneer4PixelStreaming.cpp + // TSharedPtr ValueJson = MakeShareable(new FJsonObject()); + // ValueJson->SetField(*PlayerId, MakeShared(StatValue)); + // TArray> ValueArray; + // ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); + // NewMetricJson->SetArrayField((TEXT("value")), ValueArray); + + // Add PlayerMetrics directly under MetricsJson + for (auto const& PlayerEntry : PlayerMetrics) + { + FString PlayerId = PlayerEntry.Key; + const TArray& PlayerStats = PlayerEntry.Value; + + TSharedPtr SinglePlayerMetricsJson = MakeShareable(new FJsonObject()); + for (const FBuccaneerMetric &Stat : PlayerStats) + { + TSharedPtr StatJson = MakeShareable(new FJsonObject()); + StatJson->SetStringField(TEXT("description"), Stat.Description); + // todo: This needs to be changed from being set to a "number" to instead be set using `SetArrayField` as above + StatJson->SetNumberField(TEXT("value"), Stat.Value); + SinglePlayerMetricsJson->SetObjectField(Stat.Name, StatJson); + } + MetricsJson->SetObjectField(PlayerId, SinglePlayerMetricsJson); + } + + JsonObject->SetObjectField(TEXT("metrics"), MetricsJson); + JsonObject->SetNumberField(TEXT("timestamp"), Timestamp); + + return JsonObject; +} + +TSharedPtr FMetricsCollection::ToJsonUnnested() const +{ + TSharedPtr JsonObject = MakeShareable(new FJsonObject()); + + for (const FBuccaneerMetric &Stat : Metrics) + { + JsonObject->SetField(Stat.Name, MakeShared(Stat.Value)); + } + + // Add PlayerMetrics (unnested, so composite name) + for (auto const& PlayerEntry : PlayerMetrics) + { + FString PlayerId = PlayerEntry.Key; + const TArray& PlayerStats = PlayerEntry.Value; + + for (const FBuccaneerMetric &Stat : PlayerStats) + { + FString CompositeName = FString::Printf(TEXT("%s_%s"), *PlayerId, *Stat.Name); + JsonObject->SetField(CompositeName, MakeShared(Stat.Value)); + } + } + + JsonObject->SetField(TEXT("timestamp"), MakeShared(Timestamp)); + + return JsonObject; +} diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h new file mode 100644 index 0000000..082cd17 --- /dev/null +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h @@ -0,0 +1,42 @@ +/** + * @file BuccaneerStats.h + * @brief Defines the data structures for collecting and storing statistics. + */ + +#pragma once + +#include "Dom/JsonObject.h" + +/** + * @struct FBuccaneerMetric + * @brief Represents a single metric, including its name, description, and value. + */ +struct FBuccaneerMetric +{ + FString Name; + FString Description; + double Value; +}; + +/** + * @struct FMetricsCollection + * @brief Represents a collection of metrics captured at a specific moment in time. + */ +struct FMetricsCollection +{ + double Timestamp; + TArray Metrics; // For global metrics + TMap> PlayerMetrics; // For per-player metrics + + /** + * @brief Converts the FMetricsCollection to a nested FJsonObject. + * @return A TSharedPtr to the created FJsonObject. + */ + TSharedPtr ToJsonNested() const; + + /** + * @brief Converts the FMetricsCollection to an unnested FJsonObject without descriptions. + * @return A TSharedPtr to the created FJsonObject. + */ + TSharedPtr ToJsonUnnested() const; +}; diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/IBuccaneerCommonModule.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/IBuccaneerCommonModule.h index 99dcf7d..ea46d0c 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/IBuccaneerCommonModule.h +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/IBuccaneerCommonModule.h @@ -6,6 +6,7 @@ #include "Dom/JsonObject.h" #include "Modules/ModuleInterface.h" #include "Modules/ModuleManager.h" +#include "BuccaneerMetrics.h" class BUCCANEERCOMMON_API IBuccaneerCommonModule : public IModuleInterface { @@ -21,7 +22,7 @@ class BUCCANEERCOMMON_API IBuccaneerCommonModule : public IModuleInterface return FModuleManager::LoadModuleChecked("BuccaneerCommon"); } - /** + /** * Checks to see if this module is loaded. * * @return True if the module is loaded. @@ -31,7 +32,7 @@ class BUCCANEERCOMMON_API IBuccaneerCommonModule : public IModuleInterface return FModuleManager::Get().IsModuleLoaded("BuccaneerCommon"); } - /** + /** * Event fired when internal streamer is initialized and the methods on this module are ready for use. */ DECLARE_EVENT_OneParam(IBuccaneerCommonModule, FReadyEvent, IBuccaneerCommonModule&); @@ -42,20 +43,19 @@ class BUCCANEERCOMMON_API IBuccaneerCommonModule : public IModuleInterface */ virtual FReadyEvent& OnReady() = 0; - /** + /** * Is the BuccaneerCommon module actually ready to use? Is the streamer created. * @return True if BuccaneerCommon module methods are ready for use. */ virtual bool IsReady() = 0; - /** - * - */ - virtual void SendStats(TSharedPtr JsonObject) = 0; - - /** - * - */ - virtual void SendEvent(TSharedPtr JsonObject) = 0; + /** + * + */ + virtual void SendMetrics(const FMetricsCollection& StatsCollection) = 0; + /** + * + */ + virtual void SendEvent(TSharedPtr JsonObject) = 0; }; \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp index 0ee7738..9d18828 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp @@ -11,44 +11,16 @@ #include "Stats/Stats.h" #include "Stats/StatsData.h" #include "Math/UnrealMathUtility.h" +#include "BuccaneerMetrics.h" #define COMPUTE_MEAN(CurrentMean, NewTime, FrameCount) \ ((FrameCount - 1) * CurrentMean + NewTime) / FrameCount; -TMap StatDescriptionMap = { - {"mean_fps", "The average fps"}, - {"mean_frametime", "The average frametime"}, - {"mean_gamethreadtime", "The average game thread time"}, - {"mean_gputime", "The average gpu time"}, - {"mean_rendertime", "The average render thread time"}, - {"mean_rhithreadtime", "The average rhi thread time"}, - {"memory_virtual", "The virtual memory usage"}, - {"memory_physical", "The physical memory usage"}, - {"memory_gpu", "The gpu memory usage"}, - {"num_hangs", "The number of frames hung in the recording interval"}}; - void FBuccaneerStatsModule::StartupModule() { - MetricJson = MakeShareable(new FJsonObject()); - JsonObject = MakeShareable(new FJsonObject()); - JsonObject->SetField(TEXT("metrics"), MakeShared(MetricJson)); AppStartTime = LastTickTime = InterimStart = FPlatformTime::Seconds(); } -void FBuccaneerStatsModule::UpdateMetric(FString Name, double Value) -{ - if (!StatDescriptionMap.Contains(Name)) - { - UE_LOGFMT(LogBuccaneerStats, Log, "No description for metric {0}", Name); - return; - } - - TSharedPtr MetricInfoJson = MakeShareable(new FJsonObject()); - MetricInfoJson->SetField("description", MakeShared((TEXT("%s"), *StatDescriptionMap[Name]))); - MetricInfoJson->SetField("value", MakeShared(Value)); - MetricJson->SetField(*Name, MakeShared(MetricInfoJson)); -} - void FBuccaneerStatsModule::ShutdownModule() { // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, @@ -117,14 +89,14 @@ void FBuccaneerStatsModule::ComputeUsedMemory() UsedPhysicalMemory = static_cast(MemoryStats.UsedPhysical) / BytesPerMB; #if !UE_BUILD_SHIPPING - TArray Stats; - GetPermanentStats(Stats); + TArray Metrics; + GetPermanentStats(Metrics); FName NAME_STATGROUP_RHI(FStatGroup_STATGROUP_RHI::GetGroupName()); int64 TotalMemory = 0; - for (int32 Index = 0; Index < Stats.Num(); Index++) + for (int32 Index = 0; Index < Metrics.Num(); Index++) { - FStatMessage const &Meta = Stats[Index]; + FStatMessage const &Meta = Metrics[Index]; FName LastGroup = Meta.NameAndInfo.GetGroupName(); if (LastGroup == NAME_STATGROUP_RHI && Meta.NameAndInfo.GetFlag(EStatMetaFlags::IsMemory)) { @@ -137,25 +109,19 @@ void FBuccaneerStatsModule::ComputeUsedMemory() void FBuccaneerStatsModule::PushStats() { - // Collected Metrics - // name value - UpdateMetric("mean_fps", InterimMeanFrameTime != 0.0 ? (float)(1000.0 / InterimMeanFrameTime) : 0.0f); - UpdateMetric("mean_frametime", InterimMeanFrameTime); - UpdateMetric("mean_gamethreadtime", InterimMeanGameThreadTime); - UpdateMetric("mean_gputime", InterimMeanGPUTime); - UpdateMetric("mean_rendertime", InterimMeanRenderThreadTime); - UpdateMetric("mean_rhithreadtime", InterimMeanRHIThreadTime); - UpdateMetric("memory_virtual", UsedVirtualMemory); - UpdateMetric("memory_physical", UsedPhysicalMemory); - UpdateMetric("memory_gpu", UsedGPUMemory); - UpdateMetric("num_hangs", InterimHangCount); - - const double ElapsedSeconds = FPlatformTime::Seconds() - AppStartTime; - const double ElapsedMilliseconds = ElapsedSeconds * 1000; - const int64 RoundedMilliseconds = FMath::RoundToInt64(ElapsedMilliseconds); - JsonObject->SetField(TEXT("timestamp"), MakeShared(RoundedMilliseconds)); - - IBuccaneerCommonModule::Get().SendStats(JsonObject); + FMetricsCollection MetricsCollection; + MetricsCollection.Timestamp = FMath::RoundToInt64((FPlatformTime::Seconds() - AppStartTime) * 1000); + MetricsCollection.Metrics.Add({"mean_fps", "The average fps", InterimMeanFrameTime != 0.0 ? (float)(1000.0 / InterimMeanFrameTime) : 0.0f}); + MetricsCollection.Metrics.Add({"mean_frametime", "The average frametime", InterimMeanFrameTime}); + MetricsCollection.Metrics.Add({"mean_gamethreadtime", "The average game thread time", InterimMeanGameThreadTime}); + MetricsCollection.Metrics.Add({"mean_gputime", "The average gpu time", InterimMeanGPUTime}); + MetricsCollection.Metrics.Add({"mean_rendertime", "The average render thread time", InterimMeanRenderThreadTime}); + MetricsCollection.Metrics.Add({"mean_rhithreadtime", "The average rhi thread time", InterimMeanRHIThreadTime}); + MetricsCollection.Metrics.Add({"memory_virtual", "The virtual memory usage", UsedVirtualMemory}); + MetricsCollection.Metrics.Add({"memory_physical", "The physical memory usage", UsedPhysicalMemory}); + MetricsCollection.Metrics.Add({"memory_gpu", "The gpu memory usage", UsedGPUMemory}); + MetricsCollection.Metrics.Add({"num_hangs", "The number of frames hung in the recording interval", (double)InterimHangCount}); + IBuccaneerCommonModule::Get().SendMetrics(MetricsCollection); } TStatId FBuccaneerStatsModule::GetStatId() const diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h index 24b8090..4bf8860 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h @@ -25,7 +25,6 @@ class FBuccaneerStatsModule : public IBuccaneerStatsModule, public FTickableGame private: void PushStats(); void ComputeUsedMemory(); - void UpdateMetric(FString Name, double Value); // Time keeping variables double LastTickTime = 0.0; @@ -45,8 +44,5 @@ class FBuccaneerStatsModule : public IBuccaneerStatsModule, public FTickableGame double InterimHangCount = 0.0; uint32 InterimFrameCount = 1; - // Variable for storing logging URL and logging object - TSharedPtr JsonObject; - TSharedPtr MetricJson; double AppStartTime = 0.0; -}; +}; \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp index 536ec52..d8353be 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp @@ -1,6 +1,5 @@ // Copyright TensorWorks Pty Ltd. All Rights Reserved. - #include "Buccaneer4PixelStreaming.h" #include "Logging.h" @@ -8,8 +7,8 @@ #include "Buccaneer4PixelStreamingSettings.h" TMap StatDescriptionMap = { - {"jitterBufferDelay", "jitterBufferDelay"}, - {"framesSent", "framesSent"}, + {"jitterBufferDelay", "jitterBufferDelay"}, + {"framesSent", "framesSent"}, {"framesPerSecond", "framesPerSecond"}, {"framesReceived", "framesReceived"}, {"framesDropped", "framesDropped"}, @@ -38,7 +37,7 @@ TMap StatDescriptionMap = { {"qpSum", "qpSum"}, {"totalEncodeTime", "totalEncodeTime"}, {"totalPacketSendDelay", "totalPacketSendDelay"}, - {"packetSendDelay", "packetSendDelay"}, + {"packetSendDelay", "packetSendDelay"}, {"framesEncoded", "framesEncoded"}, {"transmitFps", "transmit fps"}, {"bitrate", "bitrate (kb/s)"}, @@ -46,17 +45,14 @@ TMap StatDescriptionMap = { {"encodeTime", "encode time (ms)"}, {"encodeFps", "encode fps"}, {"captureToSend", "capture to send (ms)"}, - {"captureFps", "capture fps"} -}; + {"captureFps", "capture fps"}}; void FBuccaneer4PixelStreamingModule::StartupModule() { - LoggingStart = FPlatformTime::Seconds(); + LoggingStart = FPlatformTime::Seconds(); ReportingInterval = 1; - JsonObject = MakeShareable(new FJsonObject()); - - if (UPixelStreamingDelegates* Delegates = UPixelStreamingDelegates::GetPixelStreamingDelegates()) + if (UPixelStreamingDelegates *Delegates = UPixelStreamingDelegates::GetPixelStreamingDelegates()) { Delegates->OnStatChangedNative.AddRaw(this, &FBuccaneer4PixelStreamingModule::ConsumeStat); } @@ -70,78 +66,60 @@ void FBuccaneer4PixelStreamingModule::ShutdownModule() void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId PlayerId, FName StatName, float StatValue) { - if (!UBuccaneer4PixelStreamingSettings::CVarEnabled.GetValueOnAnyThread() || PlayerId == TEXT("Application")) + if (!UBuccaneer4PixelStreamingSettings::CVarEnabled.GetValueOnAnyThread()) { return; } - /** - * "{StatName}": { - * "description": "{StatDescription}", - * "value": [ - * "{PlayerId}": {StatValue} - * ] - * } - */ - const TSharedPtr MetricJson; - const TSharedPtr* MetricJsonPtr = &MetricJson; - if(JsonObject->TryGetObjectField(*StatName.ToString(), MetricJsonPtr)) - { - TArray> ValueArray = MetricJson->GetArrayField(TEXT("value")); - bool bRequiresCreation = true; - for (int i = 0; i < ValueArray.Num(); i++) - { - const TSharedPtr ValueJson = ValueArray[i]->AsObject(); - double val; - if(ValueJson->TryGetNumberField(*PlayerId, val)) - { - // This metric already has this player id, update the value accordingly - ValueJson->SetField(*PlayerId, MakeShared(StatValue)); - bRequiresCreation = false; - break; - } - } - - if(bRequiresCreation) - { - TSharedPtr ValueJson = MakeShareable(new FJsonObject()); - ValueJson->SetField(*PlayerId, MakeShared(StatValue)); - - ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); + // We don't care about application level stats + if(PlayerId == TEXT("Application")) + { + return; + } - MetricJson->SetArrayField((TEXT("value")), ValueArray); - } + FBuccaneerMetric NewMetric; + NewMetric.Name = StatName.ToString(); + if (const FString* Description = StatDescriptionMap.Find(StatName.ToString())) + { + NewMetric.Description = *Description; } else { - if(!StatDescriptionMap.Contains(*StatName.ToString())) - { - UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "{0}", StatName.ToString()); - return; - } - TSharedPtr NewMetricJson = MakeShareable(new FJsonObject()); - - TSharedPtr ValueJson = MakeShareable(new FJsonObject()); - ValueJson->SetField(*PlayerId, MakeShared(StatValue)); - - TArray> ValueArray; - ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); - - NewMetricJson->SetArrayField((TEXT("value")), ValueArray); + UE_LOGFMT(LogBuccaneer4PixelStreaming, Log, "Unknown stat {0}", StatName.ToString()); + NewMetric.Description = StatName.ToString(); // Default description + } + NewMetric.Value = StatValue; - JsonObject->SetObjectField(*StatName.ToString(), NewMetricJson); + // All metrics are now player-specific + TArray& PlayerStats = PlayerMetricsMap.FindOrAdd(PlayerId); + bool bFound = false; + for (FBuccaneerMetric& Metric : PlayerStats) + { + if (Metric.Name == NewMetric.Name) + { + Metric.Value = NewMetric.Value; + bFound = true; + break; + } + } + if (!bFound) + { + PlayerStats.Add(NewMetric); } - double NowTime = FPlatformTime::Seconds(); - if ( (NowTime - LoggingStart) >= ReportingInterval ) + double NowTime = FPlatformTime::Seconds(); + if ((NowTime - LoggingStart) >= ReportingInterval) { LoggingStart = NowTime; - TSharedPtr PayloadJson = MakeShareable(new FJsonObject()); - PayloadJson->SetObjectField(TEXT("metrics"), JsonObject); - IBuccaneerCommonModule::Get().SendStats(PayloadJson); + + FMetricsCollection Collection; + Collection.Timestamp = LoggingStart; + Collection.PlayerMetrics = PlayerMetricsMap; // Copy player metrics - JsonObject = MakeShareable(new FJsonObject()); + IBuccaneerCommonModule::Get().SendMetrics(Collection); + + PlayerMetricsMap.Empty(); } } - + IMPLEMENT_MODULE(FBuccaneer4PixelStreamingModule, Buccaneer4PixelStreaming) \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h index ab49ef5..9e269bc 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h @@ -4,7 +4,7 @@ #pragma once #include "CoreMinimal.h" -#include "Dom/JsonObject.h" +#include "BuccaneerMetrics.h" #include "IBuccaneerCommonModule.h" #include "IBuccaneer4PixelStreamingModule.h" #include "IPixelStreamingModule.h" @@ -25,5 +25,5 @@ class FBuccaneer4PixelStreamingModule : public IBuccaneer4PixelStreamingModule double LoggingStart; double ReportingInterval; - TSharedPtr JsonObject; + TMap> PlayerMetrics; }; diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp index 2dad406..d4b6bb3 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp @@ -6,8 +6,8 @@ #include "Buccaneer4PixelStreaming2Settings.h" TMap PSStatDescriptionMap = { - {"jitterBufferDelay", "jitterBufferDelay"}, - {"framesSent", "framesSent"}, + {"jitterBufferDelay", "jitterBufferDelay"}, + {"framesSent", "framesSent"}, {"framesPerSecond", "framesPerSecond"}, {"framesReceived", "framesReceived"}, {"framesDropped", "framesDropped"}, @@ -36,7 +36,7 @@ TMap PSStatDescriptionMap = { {"qpSum", "qpSum"}, {"totalEncodeTime", "totalEncodeTime"}, {"totalPacketSendDelay", "totalPacketSendDelay"}, - {"packetSendDelay", "packetSendDelay"}, + {"packetSendDelay", "packetSendDelay"}, {"framesEncoded", "framesEncoded"}, {"transmitFps", "transmit fps"}, {"bitrate", "bitrate (kb/s)"}, @@ -44,16 +44,13 @@ TMap PSStatDescriptionMap = { {"encodeTime", "encode time (ms)"}, {"encodeFps", "encode fps"}, {"captureToSend", "capture to send (ms)"}, - {"captureFps", "capture fps"} -}; + {"captureFps", "capture fps"}}; void FBuccaneer4PixelStreaming2Module::StartupModule() { - LoggingStart = FPlatformTime::Seconds(); + LoggingStart = FPlatformTime::Seconds(); - JsonObject = MakeShareable(new FJsonObject()); - - if (UPixelStreaming2Delegates* Delegates = UPixelStreaming2Delegates::Get()) + if (UPixelStreaming2Delegates *Delegates = UPixelStreaming2Delegates::Get()) { Delegates->OnStatChangedNative.AddRaw(this, &FBuccaneer4PixelStreaming2Module::ConsumeStat); } @@ -67,77 +64,60 @@ void FBuccaneer4PixelStreaming2Module::ShutdownModule() void FBuccaneer4PixelStreaming2Module::ConsumeStat(FString PlayerId, FName StatName, float StatValue) { - if (!UBuccaneer4PixelStreaming2Settings::CVarEnabled.GetValueOnAnyThread() || PlayerId == TEXT("Application") || UBuccaneer4PixelStreaming2Settings::CVarReportingInterval.GetValueOnAnyThread() <= 0) + if (!UBuccaneer4PixelStreaming2Settings::CVarEnabled.GetValueOnAnyThread() || UBuccaneer4PixelStreaming2Settings::CVarReportingInterval.GetValueOnAnyThread() <= 0) { return; } - /** - * "{StatName}": { - * "description": "{StatDescription}", - * "value": [ - * "{PlayerId}": {StatValue} - * ] - * } - */ - const TSharedPtr* MetricJson = nullptr; - if(JsonObject->TryGetObjectField(*StatName.ToString(), MetricJson)) - { - TArray> ValueArray = (*MetricJson)->GetArrayField(TEXT("value")); - - bool bRequiresCreation = true; - for (int i = 0; i < ValueArray.Num(); i++) - { - const TSharedPtr ValueJson = ValueArray[i]->AsObject(); - double val; - if(ValueJson->TryGetNumberField(*PlayerId, val)) - { - // This metric already has this player id, update the value accordingly - ValueJson->SetField(*PlayerId, MakeShared(StatValue)); - bRequiresCreation = false; - break; - } - } - - if(bRequiresCreation) - { - TSharedPtr ValueJson = MakeShareable(new FJsonObject()); - ValueJson->SetField(*PlayerId, MakeShared(StatValue)); - ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); + // We don't care about application level stats + if(PlayerId == TEXT("Application")) + { + return; + } - (*MetricJson)->SetArrayField((TEXT("value")), ValueArray); - } + FBuccaneerMetric NewMetric; + NewMetric.Name = StatName.ToString(); + if (const FString* Description = PSStatDescriptionMap.Find(StatName.ToString())) + { + NewMetric.Description = *Description; } else { - if(!PSStatDescriptionMap.Contains(*StatName.ToString())) - { - UE_LOGFMT(LogBuccaneer4PixelStreaming2, Verbose, "{0}", StatName.ToString()); - return; - } - TSharedPtr NewMetricJson = MakeShareable(new FJsonObject()); - - TSharedPtr ValueJson = MakeShareable(new FJsonObject()); - ValueJson->SetField(*PlayerId, MakeShared(StatValue)); - - TArray> ValueArray; - ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); - - NewMetricJson->SetArrayField((TEXT("value")), ValueArray); + UE_LOGFMT(LogBuccaneer4PixelStreaming2, Verbose, "Unknown stat {0}", StatName.ToString()); + NewMetric.Description = StatName.ToString(); // Default description + } + NewMetric.Value = StatValue; - JsonObject->SetObjectField(*StatName.ToString(), NewMetricJson); + // All metrics are now player-specific + TArray& PlayerStats = PlayerMetricsMap.FindOrAdd(PlayerId); + bool bFound = false; + for (FBuccaneerMetric& Metric : PlayerStats) + { + if (Metric.Name == NewMetric.Name) + { + Metric.Value = NewMetric.Value; + bFound = true; + break; + } + } + if (!bFound) + { + PlayerStats.Add(NewMetric); } - double NowTime = FPlatformTime::Seconds(); + double NowTime = FPlatformTime::Seconds(); if ((NowTime - LoggingStart) >= UBuccaneer4PixelStreaming2Settings::CVarReportingInterval.GetValueOnAnyThread()) { LoggingStart = NowTime; - TSharedPtr PayloadJson = MakeShareable(new FJsonObject()); - PayloadJson->SetObjectField(TEXT("metrics"), JsonObject); - IBuccaneerCommonModule::Get().SendStats(PayloadJson); + + FMetricsCollection Collection; + Collection.Timestamp = LoggingStart; + Collection.PlayerMetrics = PlayerMetricsMap; // Copy player metrics - JsonObject = MakeShareable(new FJsonObject()); + IBuccaneerCommonModule::Get().SendMetrics(Collection); + + PlayerMetricsMap.Empty(); } } - + IMPLEMENT_MODULE(FBuccaneer4PixelStreaming2Module, Buccaneer4PixelStreaming2) \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h index 6a9ffd4..44b5be1 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h @@ -3,7 +3,7 @@ #pragma once #include "CoreMinimal.h" -#include "Dom/JsonObject.h" +#include "BuccaneerMetrics.h" #include "IBuccaneerCommonModule.h" #include "IBuccaneer4PixelStreaming2Module.h" #include "Modules/ModuleManager.h" @@ -21,5 +21,5 @@ class FBuccaneer4PixelStreaming2Module : public IBuccaneer4PixelStreaming2Module private: double LoggingStart; - TSharedPtr JsonObject; + TMap> PlayerMetricsMap; }; From a05cb120211374b834e9efa6db5141d9c3b905e7 Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:24:54 +1000 Subject: [PATCH 18/35] Prelim scaffolding prior to refactor bucc metrics Good commit for Denis and Michael to practice/attempt reformat of JSON structure. --- .../BuccaneerCommon/Private/BuccaneerMetrics.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp index 9993843..3b10f1f 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp @@ -28,6 +28,7 @@ TSharedPtr FMetricsCollection::ToJsonNested() const TSharedPtr JsonObject = MakeShareable(new FJsonObject()); TSharedPtr MetricsJson = MakeShareable(new FJsonObject()); + // Build JSON object using the global metrics we have stored in the FMetricsCollection for (const FBuccaneerMetric &Stat : Metrics) { TSharedPtr StatJson = MakeShareable(new FJsonObject()); @@ -43,6 +44,18 @@ TSharedPtr FMetricsCollection::ToJsonNested() const * "value": [ * "{PlayerId}": {StatValue} * ] + * } + * Example: + * "fps": { + * "description": "The framerate of the game", + * "value": [ + * { + * "player0": 60 + * }, + * { + * "player1": 57 + * }, + * ] * } */ @@ -55,7 +68,7 @@ TSharedPtr FMetricsCollection::ToJsonNested() const // ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); // NewMetricJson->SetArrayField((TEXT("value")), ValueArray); - // Add PlayerMetrics directly under MetricsJson + // Build JSON using each of the player metrics that we have stored in the FMetricsCollection for (auto const& PlayerEntry : PlayerMetrics) { FString PlayerId = PlayerEntry.Key; From 98cec273f793c672d808d076eda5c2445cd33e92 Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:57:13 +1100 Subject: [PATCH 19/35] Updated barnacle to work with buccaneer. Pixel Streaming stats report correctly. Tested with PS1/2 --- .../Private/BuccaneerCommonModule.cpp | 2 +- .../Private/BuccaneerMetrics.cpp | 124 +++++++++--------- .../BuccaneerCommon/Public/BuccaneerMetrics.h | 6 - .../Private/BuccaneerStatsModule.cpp | 4 +- .../Private/BuccaneerStatsModule.h | 2 - .../Public/IBuccaneerStatsModule.h | 9 ++ .../Buccaneer4PixelStreaming.Build.cs | 6 +- .../Private/Buccaneer4PixelStreaming.cpp | 4 +- .../Private/Buccaneer4PixelStreaming.h | 2 +- .../Buccaneer4PixelStreaming2.Build.cs | 3 +- .../Private/Buccaneer4PixelStreaming2.cpp | 4 +- 11 files changed, 82 insertions(+), 84 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index b3a2f22..53b1d8b 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -58,7 +58,7 @@ void FBuccaneerCommonModule::SendMetrics(const FMetricsCollection &StatsCollecti if (UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) { // Case: Sending stats to disk (we want to use our un-nested JSON format) - TSharedPtr JsonObject = StatsCollection.ToJsonUnnested(); + TSharedPtr JsonObject = StatsCollection.ToJsonNested(); TSharedPtr JsonBuccaneerID = MakeShared((TEXT("%s"), *BuccaneerID)); JsonObject->SetField("id", JsonBuccaneerID); WriteJSON(TEXT("stats.json"), JsonObject); diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp index 3b10f1f..2d04f16 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp @@ -2,28 +2,6 @@ TSharedPtr FMetricsCollection::ToJsonNested() const { - // This function makes a JSON object that follows this structure: - /* - * - * { - * "metrics": - * { - * "fps": - * { - * "description": "The framerate of the game" - * "value": 60 - * } - * "player0": - * { - * "bitrate": - * { - * "description": "This peer's streaming bitrate" - * "value": 1000 - * } - * } - * } - * } - */ TSharedPtr JsonObject = MakeShareable(new FJsonObject()); TSharedPtr MetricsJson = MakeShareable(new FJsonObject()); @@ -37,7 +15,7 @@ TSharedPtr FMetricsCollection::ToJsonNested() const MetricsJson->SetObjectField(Stat.Name, StatJson); } - // This is what it wants for Pixel Streaming: + // This is the format that Buc wants from Pixel Streaming 1/2: /** * "{StatName}": { * "description": "{StatDescription}", @@ -59,62 +37,78 @@ TSharedPtr FMetricsCollection::ToJsonNested() const * } */ - // Basically just need to do this logic from original Buc: - - // From: Private/Buccaneer4PixelStreaming.cpp - // TSharedPtr ValueJson = MakeShareable(new FJsonObject()); - // ValueJson->SetField(*PlayerId, MakeShared(StatValue)); - // TArray> ValueArray; - // ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); - // NewMetricJson->SetArrayField((TEXT("value")), ValueArray); - // Build JSON using each of the player metrics that we have stored in the FMetricsCollection for (auto const& PlayerEntry : PlayerMetrics) { FString PlayerId = PlayerEntry.Key; const TArray& PlayerStats = PlayerEntry.Value; - TSharedPtr SinglePlayerMetricsJson = MakeShareable(new FJsonObject()); + TMap> PlayerStatsJsonMap = TMap>(); + + // Iterate each stat within the individual player's stats + // and extract the value for each and store it on MetricsJson for (const FBuccaneerMetric &Stat : PlayerStats) { - TSharedPtr StatJson = MakeShareable(new FJsonObject()); - StatJson->SetStringField(TEXT("description"), Stat.Description); - // todo: This needs to be changed from being set to a "number" to instead be set using `SetArrayField` as above - StatJson->SetNumberField(TEXT("value"), Stat.Value); - SinglePlayerMetricsJson->SetObjectField(Stat.Name, StatJson); + const FString& OriginalStatName = Stat.Name; + // Replace any hyphens from the stat name with underscores to ensure valid prometheus metric names + FString StatName = OriginalStatName; + StatName.ReplaceInline(TEXT("-"), TEXT("_")); + + if(!PlayerStatsJsonMap.Contains(StatName)) + { + // Make JSON for the `description` field + TSharedPtr StatJson = MakeShareable(new FJsonObject()); + StatJson->SetStringField(TEXT("description"), Stat.Description); + // Make JSON for the `value` field (which is a JSON array) + TArray> ValueArray; + + // Note each value is stored in the JSON array as { "playerId": value } + TSharedPtr PlayerStatJson = MakeShareable(new FJsonObject()); + PlayerStatJson->SetNumberField(PlayerId, Stat.Value); + ValueArray.Add(MakeShareable(new FJsonValueObject(PlayerStatJson))); + + // Set the `value` array on the StatJson + StatJson->SetArrayField(TEXT("value"), ValueArray); + + // Put the whole StatJson object we just made into the map + // so we can add to it as we iterate through the other players + PlayerStatsJsonMap.Add(StatName, StatJson); + } + else + { + // We have already created the StatJson for this stat name + // so just need to add to the `value` array + TSharedPtr StatJson = PlayerStatsJsonMap[StatName]; + const TArray>* ValueArrayPtr; + if(StatJson->TryGetArrayField(TEXT("value"), ValueArrayPtr)) + { + // Copy the existing array so we can modify it + TArray> ValueArray = *ValueArrayPtr; + + // Create a new JSON object for this player's value + TSharedPtr ValueJson = MakeShareable(new FJsonObject()); + ValueJson->SetNumberField(PlayerId, Stat.Value); + + // Add it to the array + ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); + + // Set the modified array back + StatJson->SetArrayField(TEXT("value"), ValueArray); + } + } } - MetricsJson->SetObjectField(PlayerId, SinglePlayerMetricsJson); - } - - JsonObject->SetObjectField(TEXT("metrics"), MetricsJson); - JsonObject->SetNumberField(TEXT("timestamp"), Timestamp); - - return JsonObject; -} - -TSharedPtr FMetricsCollection::ToJsonUnnested() const -{ - TSharedPtr JsonObject = MakeShareable(new FJsonObject()); - for (const FBuccaneerMetric &Stat : Metrics) - { - JsonObject->SetField(Stat.Name, MakeShared(Stat.Value)); - } - - // Add PlayerMetrics (unnested, so composite name) - for (auto const& PlayerEntry : PlayerMetrics) - { - FString PlayerId = PlayerEntry.Key; - const TArray& PlayerStats = PlayerEntry.Value; - - for (const FBuccaneerMetric &Stat : PlayerStats) + // Iterate the `PlayerStatsJsonMap` and add each stat to the main MetricsJson + for(auto const& StatEntry : PlayerStatsJsonMap) { - FString CompositeName = FString::Printf(TEXT("%s_%s"), *PlayerId, *Stat.Name); - JsonObject->SetField(CompositeName, MakeShared(Stat.Value)); + FString StatName = StatEntry.Key; + TSharedPtr StatJson = StatEntry.Value; + MetricsJson->SetObjectField(StatName, StatJson); } } - JsonObject->SetField(TEXT("timestamp"), MakeShared(Timestamp)); + JsonObject->SetObjectField(TEXT("metrics"), MetricsJson); + JsonObject->SetNumberField(TEXT("timestamp"), Timestamp); return JsonObject; } diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h index 082cd17..333eddf 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h @@ -33,10 +33,4 @@ struct FMetricsCollection * @return A TSharedPtr to the created FJsonObject. */ TSharedPtr ToJsonNested() const; - - /** - * @brief Converts the FMetricsCollection to an unnested FJsonObject without descriptions. - * @return A TSharedPtr to the created FJsonObject. - */ - TSharedPtr ToJsonUnnested() const; }; diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp index 9d18828..69d7b47 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp @@ -18,7 +18,7 @@ void FBuccaneerStatsModule::StartupModule() { - AppStartTime = LastTickTime = InterimStart = FPlatformTime::Seconds(); + LastTickTime = InterimStart = FPlatformTime::Seconds(); } void FBuccaneerStatsModule::ShutdownModule() @@ -110,7 +110,7 @@ void FBuccaneerStatsModule::ComputeUsedMemory() void FBuccaneerStatsModule::PushStats() { FMetricsCollection MetricsCollection; - MetricsCollection.Timestamp = FMath::RoundToInt64((FPlatformTime::Seconds() - AppStartTime) * 1000); + MetricsCollection.Timestamp = IBuccaneerStatsModule::GetStatsTimestamp(); MetricsCollection.Metrics.Add({"mean_fps", "The average fps", InterimMeanFrameTime != 0.0 ? (float)(1000.0 / InterimMeanFrameTime) : 0.0f}); MetricsCollection.Metrics.Add({"mean_frametime", "The average frametime", InterimMeanFrameTime}); MetricsCollection.Metrics.Add({"mean_gamethreadtime", "The average game thread time", InterimMeanGameThreadTime}); diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h index 4bf8860..3837e51 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h @@ -43,6 +43,4 @@ class FBuccaneerStatsModule : public IBuccaneerStatsModule, public FTickableGame // (using an unsigned int as there shouldn't be more than 4.2 million hangs during a time period, and if there is you have bigger problems) double InterimHangCount = 0.0; uint32 InterimFrameCount = 1; - - double AppStartTime = 0.0; }; \ No newline at end of file diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Public/IBuccaneerStatsModule.h b/Plugins/Buccaneer/Source/BuccaneerStats/Public/IBuccaneerStatsModule.h index 5d522e5..9fad7da 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Public/IBuccaneerStatsModule.h +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Public/IBuccaneerStatsModule.h @@ -30,4 +30,13 @@ class BUCCANEERSTATS_API IBuccaneerStatsModule : public IModuleInterface { return FModuleManager::Get().IsModuleLoaded("BuccaneerStats"); } + + /* + * Get the timestamp for the purposes of stats reporting (so we all use the same clock/timekeeping system) + */ + static int64 GetStatsTimestamp() + { + // Return platform timestamp in milliseconds + return FMath::RoundToInt64((FPlatformTime::Seconds()) * 1000); + } }; \ No newline at end of file diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs index 3994322..50869be 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming.Build.cs @@ -17,7 +17,8 @@ public Buccaneer4PixelStreaming(ReadOnlyTargetRules Target) : base(Target) "Json", "CoreUObject", "DeveloperSettings", - "EngineSettings" + "EngineSettings", + "BuccaneerCommon" }); @@ -28,7 +29,8 @@ public Buccaneer4PixelStreaming(ReadOnlyTargetRules Target) : base(Target) "Engine", "Slate", "SlateCore", - "BuccaneerCommon" + "BuccaneerCommon", + "BuccaneerStats" }); } } diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp index d8353be..f3523a0 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp @@ -1,7 +1,7 @@ // Copyright TensorWorks Pty Ltd. All Rights Reserved. #include "Buccaneer4PixelStreaming.h" - +#include "IBuccaneerStatsModule.h" #include "Logging.h" #include "PixelStreamingDelegates.h" #include "Buccaneer4PixelStreamingSettings.h" @@ -107,7 +107,7 @@ void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId Player PlayerStats.Add(NewMetric); } - double NowTime = FPlatformTime::Seconds(); + double NowTime = IBuccaneerStatsModule::GetStatsTimestamp(); if ((NowTime - LoggingStart) >= ReportingInterval) { LoggingStart = NowTime; diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h index 9e269bc..560c729 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h @@ -25,5 +25,5 @@ class FBuccaneer4PixelStreamingModule : public IBuccaneer4PixelStreamingModule double LoggingStart; double ReportingInterval; - TMap> PlayerMetrics; + TMap> PlayerMetricsMap; }; diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs index c5f5046..e818715 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Buccaneer4PixelStreaming2.Build.cs @@ -27,7 +27,8 @@ public Buccaneer4PixelStreaming2(ReadOnlyTargetRules Target) : base(Target) "Engine", "Slate", "SlateCore", - "BuccaneerCommon" + "BuccaneerCommon", + "BuccaneerStats" }); } } diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp index d4b6bb3..50b0e3b 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp @@ -1,7 +1,7 @@ // Copyright TensorWorks Pty Ltd. All Rights Reserved. #include "Buccaneer4PixelStreaming2.h" - +#include "IBuccaneerStatsModule.h" #include "Logging.h" #include "Buccaneer4PixelStreaming2Settings.h" @@ -105,7 +105,7 @@ void FBuccaneer4PixelStreaming2Module::ConsumeStat(FString PlayerId, FName StatN PlayerStats.Add(NewMetric); } - double NowTime = FPlatformTime::Seconds(); + double NowTime = IBuccaneerStatsModule::GetStatsTimestamp(); if ((NowTime - LoggingStart) >= UBuccaneer4PixelStreaming2Settings::CVarReportingInterval.GetValueOnAnyThread()) { LoggingStart = NowTime; From 0a46ee27d66d7ccce82292fc06f18f77fc52b8ac Mon Sep 17 00:00:00 2001 From: Luke Bermingham <1215582+lukehb@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:53:10 +1000 Subject: [PATCH 20/35] Making FMetricsCollection more general --- .../Private/BuccaneerCommonModule.cpp | 25 ++-- .../Private/BuccaneerMetrics.cpp | 127 +++++++++++------- .../BuccaneerCommon/Public/BuccaneerMetrics.h | 12 +- .../Private/BuccaneerStatsModule.cpp | 20 +-- .../Private/Buccaneer4PixelStreaming.cpp | 2 +- .../Private/Buccaneer4PixelStreaming2.cpp | 2 +- 6 files changed, 116 insertions(+), 72 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index 53b1d8b..43fd7af 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -51,25 +51,30 @@ bool FBuccaneerCommonModule::IsReady() return bModuleReady; } -void FBuccaneerCommonModule::SendMetrics(const FMetricsCollection &StatsCollection) +void FBuccaneerCommonModule::SendMetrics(const FMetricsCollection& StatsCollection) { const FString BuccaneerID = UBuccaneerSettings::CVarID.GetValueOnAnyThread(); + TSharedPtr JsonObject = StatsCollection.ToJson(); + TSharedPtr JsonBuccaneerID = MakeShared((TEXT("%s"), *BuccaneerID)); + JsonObject->SetField("id", JsonBuccaneerID); + + // Check if there is any metadata to send + if(UBuccaneerSettings::CVarMetadata.GetValueOnAnyThread() != "") + { + JsonObject->SetField("metadata", MakeShared(MetadataJson)); + } + + // Write metrics JSON to either HTTP Buccaneer server or to disk depending on the CVar settings + + // Case: Sending stats to disk if (UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) { - // Case: Sending stats to disk (we want to use our un-nested JSON format) - TSharedPtr JsonObject = StatsCollection.ToJsonNested(); - TSharedPtr JsonBuccaneerID = MakeShared((TEXT("%s"), *BuccaneerID)); - JsonObject->SetField("id", JsonBuccaneerID); WriteJSON(TEXT("stats.json"), JsonObject); } + // Case: Sending stats to Buccaneer server else if (UBuccaneerSettings::CVarURL.GetValueOnAnyThread() != "") { - // Case: Sending stats to Buccaneer server (we want to use the nested JSON format) - TSharedPtr JsonObject = StatsCollection.ToJsonNested(); - TSharedPtr JsonBuccaneerID = MakeShared((TEXT("%s"), *BuccaneerID)); - JsonObject->SetField("id", JsonBuccaneerID); - JsonObject->SetField("metadata", MakeShared(MetadataJson)); SendHTTP(UBuccaneerSettings::CVarURL.GetValueOnAnyThread() + FString("/stats"), JsonObject); } } diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp index 2d04f16..896518f 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp @@ -1,31 +1,58 @@ #include "BuccaneerMetrics.h" -TSharedPtr FMetricsCollection::ToJsonNested() const +TSharedPtr FMetricsCollection::ToJson() const { TSharedPtr JsonObject = MakeShareable(new FJsonObject()); TSharedPtr MetricsJson = MakeShareable(new FJsonObject()); - // Build JSON object using the global metrics we have stored in the FMetricsCollection - for (const FBuccaneerMetric &Stat : Metrics) + /** + * This is the format that the Buccaneer server wants from "single value" metrics. + * + * Single value metrics could be: + * - Global application stats (like framerate, memory usage, etc) + * - Gameplay stats that are not per-player (like play time, session unix timestamp, session id etc) + * - Any other type of stats where there is only one value for the stat + * + * "{MetricName}": { + * "description": "{StatDescription}", + * "value": {StatValue} + * } + * Example: + * "memory_used": { + * "description": "The memory used by the game", + * "value": 1024 + * } + */ + + // Build JSON object using the "single value metrics" we have stored in the FMetricsCollection + for (const FBuccaneerMetric &Stat : SingleValueMetrics) { - TSharedPtr StatJson = MakeShareable(new FJsonObject()); - StatJson->SetStringField(TEXT("description"), Stat.Description); - StatJson->SetNumberField(TEXT("value"), Stat.Value); - MetricsJson->SetObjectField(Stat.Name, StatJson); + TSharedPtr MetricJson = MakeShareable(new FJsonObject()); + MetricJson->SetStringField(TEXT("description"), Stat.Description); + MetricJson->SetNumberField(TEXT("value"), Stat.Value); + MetricsJson->SetObjectField(Stat.Name, MetricJson); } - // This is the format that Buc wants from Pixel Streaming 1/2: /** - * "{StatName}": { + * This is the format that the Buccaneer server wants for grouped metrics. + * + * Grouped metrics could be: + * - Pixel Streaming peer stats (if we are using one of the Buccaneer Pixel Streaming plugins, each player/peer will have their own stats) + * - Per-player gameplay stats (if the game is local multiplayer for example and you extended Buccaneer to support that) + * - Any other type of stats where there are multiple entities each with their own value for the same stat + * + * "{MetricName}": { * "description": "{StatDescription}", * "value": [ - * "{PlayerId}": {StatValue} + * { + * "{GroupId}": {StatValue} + * }, * ] * } * Example: - * "fps": { - * "description": "The framerate of the game", + * "bitrate": { + * "description": "The bitrate of the stream", * "value": [ * { * "player0": 60 @@ -37,74 +64,80 @@ TSharedPtr FMetricsCollection::ToJsonNested() const * } */ - // Build JSON using each of the player metrics that we have stored in the FMetricsCollection - for (auto const& PlayerEntry : PlayerMetrics) - { - FString PlayerId = PlayerEntry.Key; - const TArray& PlayerStats = PlayerEntry.Value; + // We need to perform a transformation on our grouped metrics because it does not match the JSON format that the server expects. + // Specifically, the server is grouped by metric name, whereas our data structure is grouped by group id (e.g. player id). - TMap> PlayerStatsJsonMap = TMap>(); + // We use this map to build up a JSON object for each stat name (key=stat name, value=JSON object) + TMap> GroupMetricsToJsonMap = TMap>(); - // Iterate each stat within the individual player's stats - // and extract the value for each and store it on MetricsJson - for (const FBuccaneerMetric &Stat : PlayerStats) + // Build JSON using each of the multi-entry metrics (e.g. per player metrics) that we have stored in the FMetricsCollection + for (auto const& MetricGroup : GroupedMetrics) + { + FString GroupId = MetricGroup.Key; + const TArray& GroupMetricsArr = MetricGroup.Value; + + // Iterate each stored value within the current multi value metric (e.g. for FPS: player0's fps, player1's fps, etc) + // and store it the JSON we are building up + for (const FBuccaneerMetric& Metric : GroupMetricsArr) { - const FString& OriginalStatName = Stat.Name; + const FString& OriginalMetricName = Metric.Name; + // Replace any hyphens from the stat name with underscores to ensure valid prometheus metric names - FString StatName = OriginalStatName; - StatName.ReplaceInline(TEXT("-"), TEXT("_")); + FString MetricName = OriginalMetricName; + MetricName.ReplaceInline(TEXT("-"), TEXT("_")); - if(!PlayerStatsJsonMap.Contains(StatName)) + if(!GroupMetricsToJsonMap.Contains(MetricName)) { // Make JSON for the `description` field - TSharedPtr StatJson = MakeShareable(new FJsonObject()); - StatJson->SetStringField(TEXT("description"), Stat.Description); + TSharedPtr MetricJson = MakeShareable(new FJsonObject()); + MetricJson->SetStringField(TEXT("description"), Metric.Description); // Make JSON for the `value` field (which is a JSON array) TArray> ValueArray; - // Note each value is stored in the JSON array as { "playerId": value } - TSharedPtr PlayerStatJson = MakeShareable(new FJsonObject()); - PlayerStatJson->SetNumberField(PlayerId, Stat.Value); - ValueArray.Add(MakeShareable(new FJsonValueObject(PlayerStatJson))); + // Note: For these grouped metrics each value is stored in the JSON object like so { "GroupId": value } + TSharedPtr MetricsValueJson = MakeShareable(new FJsonObject()); + MetricsValueJson->SetNumberField(GroupId, Metric.Value); + ValueArray.Add(MakeShareable(new FJsonValueObject(MetricsValueJson))); - // Set the `value` array on the StatJson - StatJson->SetArrayField(TEXT("value"), ValueArray); + // Set the `value` array on the MetricJson + MetricJson->SetArrayField(TEXT("value"), ValueArray); - // Put the whole StatJson object we just made into the map + // Put the whole MetricJson object we just made into the map // so we can add to it as we iterate through the other players - PlayerStatsJsonMap.Add(StatName, StatJson); + GroupMetricsToJsonMap.Add(MetricName, MetricJson); } else { - // We have already created the StatJson for this stat name + // We have already created the MetricJson for this MetricName // so just need to add to the `value` array - TSharedPtr StatJson = PlayerStatsJsonMap[StatName]; + TSharedPtr MetricJson = GroupMetricsToJsonMap[MetricName]; const TArray>* ValueArrayPtr; - if(StatJson->TryGetArrayField(TEXT("value"), ValueArrayPtr)) + if(MetricJson->TryGetArrayField(TEXT("value"), ValueArrayPtr)) { // Copy the existing array so we can modify it TArray> ValueArray = *ValueArrayPtr; // Create a new JSON object for this player's value TSharedPtr ValueJson = MakeShareable(new FJsonObject()); - ValueJson->SetNumberField(PlayerId, Stat.Value); + ValueJson->SetNumberField(GroupId, Metric.Value); // Add it to the array ValueArray.Add(MakeShareable(new FJsonValueObject(ValueJson))); // Set the modified array back - StatJson->SetArrayField(TEXT("value"), ValueArray); + MetricJson->SetArrayField(TEXT("value"), ValueArray); } } } - // Iterate the `PlayerStatsJsonMap` and add each stat to the main MetricsJson - for(auto const& StatEntry : PlayerStatsJsonMap) - { - FString StatName = StatEntry.Key; - TSharedPtr StatJson = StatEntry.Value; - MetricsJson->SetObjectField(StatName, StatJson); - } + } + + // Iterate the `GroupMetricsToJsonMap` and add each stat to the main MetricsJson + for(auto const& MetricEntry : GroupMetricsToJsonMap) + { + FString& MetricName = MetricEntry.Key; + TSharedPtr& MetricJson = MetricEntry.Value; + MetricsJson->SetObjectField(MetricName, MetricJson); } JsonObject->SetObjectField(TEXT("metrics"), MetricsJson); diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h index 333eddf..b041447 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerMetrics.h @@ -25,12 +25,18 @@ struct FBuccaneerMetric struct FMetricsCollection { double Timestamp; - TArray Metrics; // For global metrics - TMap> PlayerMetrics; // For per-player metrics + + // These metrics have a single value each. + // They can be used to record things such as global metrics, application-wide stats, session stats, etc. + TArray SingleValueMetrics; + + // These metrics are stored in logical groups by unique ids (key: player id, value: array of metrics). + // For example, they could be used for per-peer metrics for Pixel Streaming or per-player metrics in a local multiplayer game. + TMap> GroupedMetrics; /** * @brief Converts the FMetricsCollection to a nested FJsonObject. * @return A TSharedPtr to the created FJsonObject. */ - TSharedPtr ToJsonNested() const; + TSharedPtr ToJson() const; }; diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp index 69d7b47..f9ffda8 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp @@ -111,16 +111,16 @@ void FBuccaneerStatsModule::PushStats() { FMetricsCollection MetricsCollection; MetricsCollection.Timestamp = IBuccaneerStatsModule::GetStatsTimestamp(); - MetricsCollection.Metrics.Add({"mean_fps", "The average fps", InterimMeanFrameTime != 0.0 ? (float)(1000.0 / InterimMeanFrameTime) : 0.0f}); - MetricsCollection.Metrics.Add({"mean_frametime", "The average frametime", InterimMeanFrameTime}); - MetricsCollection.Metrics.Add({"mean_gamethreadtime", "The average game thread time", InterimMeanGameThreadTime}); - MetricsCollection.Metrics.Add({"mean_gputime", "The average gpu time", InterimMeanGPUTime}); - MetricsCollection.Metrics.Add({"mean_rendertime", "The average render thread time", InterimMeanRenderThreadTime}); - MetricsCollection.Metrics.Add({"mean_rhithreadtime", "The average rhi thread time", InterimMeanRHIThreadTime}); - MetricsCollection.Metrics.Add({"memory_virtual", "The virtual memory usage", UsedVirtualMemory}); - MetricsCollection.Metrics.Add({"memory_physical", "The physical memory usage", UsedPhysicalMemory}); - MetricsCollection.Metrics.Add({"memory_gpu", "The gpu memory usage", UsedGPUMemory}); - MetricsCollection.Metrics.Add({"num_hangs", "The number of frames hung in the recording interval", (double)InterimHangCount}); + MetricsCollection.SingleValueMetrics.Add({"mean_fps", "The average fps", InterimMeanFrameTime != 0.0 ? (float)(1000.0 / InterimMeanFrameTime) : 0.0f}); + MetricsCollection.SingleValueMetrics.Add({"mean_frametime", "The average frametime", InterimMeanFrameTime}); + MetricsCollection.SingleValueMetrics.Add({"mean_gamethreadtime", "The average game thread time", InterimMeanGameThreadTime}); + MetricsCollection.SingleValueMetrics.Add({"mean_gputime", "The average gpu time", InterimMeanGPUTime}); + MetricsCollection.SingleValueMetrics.Add({"mean_rendertime", "The average render thread time", InterimMeanRenderThreadTime}); + MetricsCollection.SingleValueMetrics.Add({"mean_rhithreadtime", "The average rhi thread time", InterimMeanRHIThreadTime}); + MetricsCollection.SingleValueMetrics.Add({"memory_virtual", "The virtual memory usage", UsedVirtualMemory}); + MetricsCollection.SingleValueMetrics.Add({"memory_physical", "The physical memory usage", UsedPhysicalMemory}); + MetricsCollection.SingleValueMetrics.Add({"memory_gpu", "The gpu memory usage", UsedGPUMemory}); + MetricsCollection.SingleValueMetrics.Add({"num_hangs", "The number of frames hung in the recording interval", (double)InterimHangCount}); IBuccaneerCommonModule::Get().SendMetrics(MetricsCollection); } diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp index f3523a0..76a3ae0 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp @@ -114,7 +114,7 @@ void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId Player FMetricsCollection Collection; Collection.Timestamp = LoggingStart; - Collection.PlayerMetrics = PlayerMetricsMap; // Copy player metrics + Collection.GroupedMetrics = PlayerMetricsMap; // Copy player metrics IBuccaneerCommonModule::Get().SendMetrics(Collection); diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp index 50b0e3b..1d41d6d 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp @@ -112,7 +112,7 @@ void FBuccaneer4PixelStreaming2Module::ConsumeStat(FString PlayerId, FName StatN FMetricsCollection Collection; Collection.Timestamp = LoggingStart; - Collection.PlayerMetrics = PlayerMetricsMap; // Copy player metrics + Collection.GroupedMetrics = PlayerMetricsMap; // Copy player metrics IBuccaneerCommonModule::Get().SendMetrics(Collection); From 668502b9be7b54880c6e1db3fa20221ba56cd2b1 Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:10:27 +1100 Subject: [PATCH 21/35] Update BuccaneerMetrics.cpp Fixing compile error with consts --- .../BuccaneerCommon/Private/BuccaneerMetrics.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp index 896518f..c5d319a 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp @@ -133,12 +133,12 @@ TSharedPtr FMetricsCollection::ToJson() const } // Iterate the `GroupMetricsToJsonMap` and add each stat to the main MetricsJson - for(auto const& MetricEntry : GroupMetricsToJsonMap) - { - FString& MetricName = MetricEntry.Key; - TSharedPtr& MetricJson = MetricEntry.Value; - MetricsJson->SetObjectField(MetricName, MetricJson); - } + for (const auto& MetricEntry : GroupMetricsToJsonMap) + { + const FString& MetricName = MetricEntry.Key; + const TSharedPtr& MetricJson = MetricEntry.Value; + MetricsJson->SetObjectField(MetricName, MetricJson); + } JsonObject->SetObjectField(TEXT("metrics"), MetricsJson); JsonObject->SetNumberField(TEXT("timestamp"), Timestamp); From 8061e2e6778c359856b20113d0707aebaec3f383 Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:50:45 +1100 Subject: [PATCH 22/35] Added ResX and ResY from the application --- .../Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp | 7 +++++++ .../Source/BuccaneerStats/Private/BuccaneerStatsModule.h | 3 +++ 2 files changed, 10 insertions(+) diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp index f9ffda8..6e23c01 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp @@ -57,6 +57,11 @@ void FBuccaneerStatsModule::Tick(float DeltaTime) } else { + // Get application resolution + const FVector2D ViewportSize = FVector2D(GEngine->GameViewport->Viewport->GetSizeXY()); + ResolutionX = ViewportSize.X; + ResolutionY = ViewportSize.Y; + double GameThreadTime = FPlatformTime::ToMilliseconds(GGameThreadTime); double GPUFrameTime = FPlatformTime::ToMilliseconds(RHIGetGPUFrameCycles(0)); double RenderThreadTime = FPlatformTime::ToMilliseconds(GRenderThreadTime); @@ -121,6 +126,8 @@ void FBuccaneerStatsModule::PushStats() MetricsCollection.SingleValueMetrics.Add({"memory_physical", "The physical memory usage", UsedPhysicalMemory}); MetricsCollection.SingleValueMetrics.Add({"memory_gpu", "The gpu memory usage", UsedGPUMemory}); MetricsCollection.SingleValueMetrics.Add({"num_hangs", "The number of frames hung in the recording interval", (double)InterimHangCount}); + MetricsCollection.SingleValueMetrics.Add({"res_x", "The width of the application", ResolutionX}); + MetricsCollection.SingleValueMetrics.Add({"res_y", "The height of the application", ResolutionY}); IBuccaneerCommonModule::Get().SendMetrics(MetricsCollection); } diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h index 3837e51..014486a 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.h @@ -43,4 +43,7 @@ class FBuccaneerStatsModule : public IBuccaneerStatsModule, public FTickableGame // (using an unsigned int as there shouldn't be more than 4.2 million hangs during a time period, and if there is you have bigger problems) double InterimHangCount = 0.0; uint32 InterimFrameCount = 1; + // Resolution of the application + double ResolutionX = 0.0; + double ResolutionY = 0.0; }; \ No newline at end of file From 2e8d1742caf91295f6fbea7f02040d1aeb4fe9bc Mon Sep 17 00:00:00 2001 From: Luke Bermingham <1215582+lukehb@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:22:11 +1000 Subject: [PATCH 23/35] Added sending of application stats from Pixel Streaming 1 and 2 --- .../Private/Buccaneer4PixelStreaming.cpp | 57 ++++++++++++------- .../Private/Buccaneer4PixelStreaming.h | 2 +- .../Private/Buccaneer4PixelStreaming2.cpp | 57 ++++++++++++------- .../Private/Buccaneer4PixelStreaming2.h | 2 +- 4 files changed, 72 insertions(+), 46 deletions(-) diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp index 76a3ae0..0624322 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp @@ -71,12 +71,6 @@ void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId Player return; } - // We don't care about application level stats - if(PlayerId == TEXT("Application")) - { - return; - } - FBuccaneerMetric NewMetric; NewMetric.Name = StatName.ToString(); if (const FString* Description = StatDescriptionMap.Find(StatName.ToString())) @@ -90,35 +84,54 @@ void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId Player } NewMetric.Value = StatValue; - // All metrics are now player-specific - TArray& PlayerStats = PlayerMetricsMap.FindOrAdd(PlayerId); - bool bFound = false; - for (FBuccaneerMetric& Metric : PlayerStats) + // Application level stats go into SingleValueMetrics + if(PlayerId == TEXT("Application")) { - if (Metric.Name == NewMetric.Name) + bool bFound = false; + for (FBuccaneerMetric& Metric : MetricsCollection.SingleValueMetrics) { - Metric.Value = NewMetric.Value; - bFound = true; - break; + if (Metric.Name == NewMetric.Name) + { + Metric.Value = NewMetric.Value; + bFound = true; + break; + } + } + if (!bFound) + { + MetricsCollection.SingleValueMetrics.Add(NewMetric); } } - if (!bFound) + else { - PlayerStats.Add(NewMetric); + // All other metrics are player-specific + TArray& PlayerStats = MetricsCollection.GroupedMetrics.FindOrAdd(PlayerId); + bool bFound = false; + for (FBuccaneerMetric& Metric : PlayerStats) + { + if (Metric.Name == NewMetric.Name) + { + Metric.Value = NewMetric.Value; + bFound = true; + break; + } + } + if (!bFound) + { + PlayerStats.Add(NewMetric); + } } double NowTime = IBuccaneerStatsModule::GetStatsTimestamp(); if ((NowTime - LoggingStart) >= ReportingInterval) { LoggingStart = NowTime; - - FMetricsCollection Collection; - Collection.Timestamp = LoggingStart; - Collection.GroupedMetrics = PlayerMetricsMap; // Copy player metrics + MetricsCollection.Timestamp = LoggingStart; - IBuccaneerCommonModule::Get().SendMetrics(Collection); + IBuccaneerCommonModule::Get().SendMetrics(MetricsCollection); - PlayerMetricsMap.Empty(); + MetricsCollection.SingleValueMetrics.Empty(); + MetricsCollection.GroupedMetrics.Empty(); } } diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h index 560c729..22f39ef 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h @@ -25,5 +25,5 @@ class FBuccaneer4PixelStreamingModule : public IBuccaneer4PixelStreamingModule double LoggingStart; double ReportingInterval; - TMap> PlayerMetricsMap; + FMetricsCollection MetricsCollection; }; diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp index 1d41d6d..efb3538 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp @@ -69,12 +69,6 @@ void FBuccaneer4PixelStreaming2Module::ConsumeStat(FString PlayerId, FName StatN return; } - // We don't care about application level stats - if(PlayerId == TEXT("Application")) - { - return; - } - FBuccaneerMetric NewMetric; NewMetric.Name = StatName.ToString(); if (const FString* Description = PSStatDescriptionMap.Find(StatName.ToString())) @@ -88,35 +82,54 @@ void FBuccaneer4PixelStreaming2Module::ConsumeStat(FString PlayerId, FName StatN } NewMetric.Value = StatValue; - // All metrics are now player-specific - TArray& PlayerStats = PlayerMetricsMap.FindOrAdd(PlayerId); - bool bFound = false; - for (FBuccaneerMetric& Metric : PlayerStats) + // Application level stats go into SingleValueMetrics + if(PlayerId == TEXT("Application")) { - if (Metric.Name == NewMetric.Name) + bool bFound = false; + for (FBuccaneerMetric& Metric : MetricsCollection.SingleValueMetrics) { - Metric.Value = NewMetric.Value; - bFound = true; - break; + if (Metric.Name == NewMetric.Name) + { + Metric.Value = NewMetric.Value; + bFound = true; + break; + } + } + if (!bFound) + { + MetricsCollection.SingleValueMetrics.Add(NewMetric); } } - if (!bFound) + else { - PlayerStats.Add(NewMetric); + // All other metrics are player-specific + TArray& PlayerStats = MetricsCollection.GroupedMetrics.FindOrAdd(PlayerId); + bool bFound = false; + for (FBuccaneerMetric& Metric : PlayerStats) + { + if (Metric.Name == NewMetric.Name) + { + Metric.Value = NewMetric.Value; + bFound = true; + break; + } + } + if (!bFound) + { + PlayerStats.Add(NewMetric); + } } double NowTime = IBuccaneerStatsModule::GetStatsTimestamp(); if ((NowTime - LoggingStart) >= UBuccaneer4PixelStreaming2Settings::CVarReportingInterval.GetValueOnAnyThread()) { LoggingStart = NowTime; - - FMetricsCollection Collection; - Collection.Timestamp = LoggingStart; - Collection.GroupedMetrics = PlayerMetricsMap; // Copy player metrics + MetricsCollection.Timestamp = LoggingStart; - IBuccaneerCommonModule::Get().SendMetrics(Collection); + IBuccaneerCommonModule::Get().SendMetrics(MetricsCollection); - PlayerMetricsMap.Empty(); + MetricsCollection.SingleValueMetrics.Empty(); + MetricsCollection.GroupedMetrics.Empty(); } } diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h index 44b5be1..8038066 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.h @@ -21,5 +21,5 @@ class FBuccaneer4PixelStreaming2Module : public IBuccaneer4PixelStreaming2Module private: double LoggingStart; - TMap> PlayerMetricsMap; + FMetricsCollection MetricsCollection; }; From b80b79d702dd9f4531933e0aa77f0f65a6c78527 Mon Sep 17 00:00:00 2001 From: Luke Bermingham <1215582+lukehb@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:12:18 +1000 Subject: [PATCH 24/35] Added better logging, exposed port for buc server as arg, make docker compose up work for local dev, improved stat name sanitizing --- Examples/Compose/docker-compose-all.yml | 58 ++++++++++++++ Examples/Compose/docker-compose.yml | 28 ++----- .../Private/BuccaneerMetrics.cpp | 30 ++++++-- Server/BuccaneerServer/Dockerfile | 3 +- Server/BuccaneerServer/main.go | 50 +++++++++++-- readme.md | 75 +++++++++++++++++-- 6 files changed, 202 insertions(+), 42 deletions(-) create mode 100644 Examples/Compose/docker-compose-all.yml diff --git a/Examples/Compose/docker-compose-all.yml b/Examples/Compose/docker-compose-all.yml new file mode 100644 index 0000000..89ffd29 --- /dev/null +++ b/Examples/Compose/docker-compose-all.yml @@ -0,0 +1,58 @@ +services: + unreal: + image: "tensorworks/buccaneerdemo-application" + command: [ "-PixelStreamingURL=ws://127.0.0.1:8888", "-BuccaneerURL=http://127.0.0.1:8000", "-RenderOffScreen", "-Res=1920x1080" ] + container_name: unreal + network_mode: "host" + deploy: + resources: + reservations: + devices: + - driver: nvidia + capabilities: [gpu] + count: 1 + + cirrus: + image: "tensorworks/buccaneerdemo-cirrus" + container_name: cirrus + network_mode: "host" + + buccaneerserver: + image: "tensorworks/buccaneerdemo-buccaneerserver" + container_name: buccaneerserver + network_mode: "host" + + prometheus: + image: "prom/prometheus" + container_name: prometheus + network_mode: "host" + volumes: + - "../../Configs/prometheus.yml:/etc/prometheus/prometheus.yml" + + grafana: + image: grafana/grafana + container_name: grafana + network_mode: "host" + volumes: + - "../../Configs/grafana-dashboard-config.yaml:/etc/grafana/provisioning/dashboards/grafana-dashboard-config.yaml" + - "../../Configs/grafana-datasource-config.yaml:/etc/grafana/provisioning/datasources/grafana-datasource-config.yaml" + - "../../Dashboards/:/etc/dashboards" + + loki: + image: grafana/loki + container_name: loki + network_mode: "host" + volumes: + - "../../Configs/loki-local-config.yaml:/etc/loki/loki-local-config.yaml" + + promtail: + image: grafana/promtail + container_name: promtail + command: -config.file=/etc/promtail/promtail-local-config.yaml + network_mode: "host" + volumes: + - "../../Configs/promtail-local-config.yaml:/etc/promtail/promtail-local-config.yaml" + - "eventslogs:/EventsServer" + +volumes: + eventslogs: diff --git a/Examples/Compose/docker-compose.yml b/Examples/Compose/docker-compose.yml index 89ffd29..3bf7d58 100644 --- a/Examples/Compose/docker-compose.yml +++ b/Examples/Compose/docker-compose.yml @@ -1,26 +1,11 @@ services: - unreal: - image: "tensorworks/buccaneerdemo-application" - command: [ "-PixelStreamingURL=ws://127.0.0.1:8888", "-BuccaneerURL=http://127.0.0.1:8000", "-RenderOffScreen", "-Res=1920x1080" ] - container_name: unreal - network_mode: "host" - deploy: - resources: - reservations: - devices: - - driver: nvidia - capabilities: [gpu] - count: 1 - - cirrus: - image: "tensorworks/buccaneerdemo-cirrus" - container_name: cirrus - network_mode: "host" - - buccaneerserver: - image: "tensorworks/buccaneerdemo-buccaneerserver" + buccaneer-server: + build: + context: ../../Server/BuccaneerServer + dockerfile: Dockerfile container_name: buccaneerserver - network_mode: "host" + ports: + - "8000:8000" prometheus: image: "prom/prometheus" @@ -44,6 +29,7 @@ services: network_mode: "host" volumes: - "../../Configs/loki-local-config.yaml:/etc/loki/loki-local-config.yaml" + command: -config.file=/etc/loki/loki-local-config.yaml promtail: image: grafana/promtail diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp index c5d319a..93e6ca3 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp @@ -1,5 +1,25 @@ #include "BuccaneerMetrics.h" +namespace +{ + // Helper function to format metric names to be Prometheus-compatible + // Valid Prometheus names must be alphanumeric or underscores + FString FormatMetricName(const FString& Name) + { + FString FormattedName = Name; + FormattedName.ReplaceInline(TEXT("-"), TEXT("_")); + FormattedName.ReplaceInline(TEXT("."), TEXT("_")); + FormattedName.ReplaceInline(TEXT(" "), TEXT("_")); + FormattedName.ReplaceInline(TEXT("/"), TEXT("_")); + FormattedName.ReplaceInline(TEXT("\\"), TEXT("_")); + FormattedName.ReplaceInline(TEXT("("), TEXT("")); + FormattedName.ReplaceInline(TEXT(")"), TEXT("")); + FormattedName.ReplaceInline(TEXT("\""), TEXT("")); + FormattedName.ReplaceInline(TEXT("'"), TEXT("")); + return FormattedName; + } +} + TSharedPtr FMetricsCollection::ToJson() const { @@ -28,10 +48,12 @@ TSharedPtr FMetricsCollection::ToJson() const // Build JSON object using the "single value metrics" we have stored in the FMetricsCollection for (const FBuccaneerMetric &Stat : SingleValueMetrics) { + FString MetricName = FormatMetricName(Stat.Name); + TSharedPtr MetricJson = MakeShareable(new FJsonObject()); MetricJson->SetStringField(TEXT("description"), Stat.Description); MetricJson->SetNumberField(TEXT("value"), Stat.Value); - MetricsJson->SetObjectField(Stat.Name, MetricJson); + MetricsJson->SetObjectField(MetricName, MetricJson); } /** @@ -80,11 +102,7 @@ TSharedPtr FMetricsCollection::ToJson() const // and store it the JSON we are building up for (const FBuccaneerMetric& Metric : GroupMetricsArr) { - const FString& OriginalMetricName = Metric.Name; - - // Replace any hyphens from the stat name with underscores to ensure valid prometheus metric names - FString MetricName = OriginalMetricName; - MetricName.ReplaceInline(TEXT("-"), TEXT("_")); + FString MetricName = FormatMetricName(Metric.Name); if(!GroupMetricsToJsonMap.Contains(MetricName)) { diff --git a/Server/BuccaneerServer/Dockerfile b/Server/BuccaneerServer/Dockerfile index 5197ba8..cecaef8 100644 --- a/Server/BuccaneerServer/Dockerfile +++ b/Server/BuccaneerServer/Dockerfile @@ -4,8 +4,7 @@ FROM golang:1.20.6-alpine WORKDIR /app -COPY go.mod ./ -COPY go.sum ./ +COPY go.mod go.sum ./ RUN go mod download COPY *.go ./ diff --git a/Server/BuccaneerServer/main.go b/Server/BuccaneerServer/main.go index 0320e1a..442f98d 100644 --- a/Server/BuccaneerServer/main.go +++ b/Server/BuccaneerServer/main.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "flag" "io" "log" "net/http" @@ -98,6 +99,15 @@ func removeStaleCollectors() { } func main() { + // Determine the port to use: command line arg > environment variable > default + port := flag.String("port", os.Getenv("PORT"), "Port to listen on (can also be set via PORT environment variable)") + flag.Parse() + + // Use default if neither flag nor env var is set + if *port == "" { + *port = "8000" + } + // start sub-routine go removeStaleCollectors() @@ -179,6 +189,12 @@ func main() { metricJson := value.(map[string]interface{}) + // Get description, defaulting to the metric name if not provided + description := key + if desc, ok := metricJson["description"].(string); ok { + description = desc + } + if valueArray, ok := metricJson["value"].([]interface{}); ok { // if the value object is an array, then loop through this array recordMap := make(map[string]record) @@ -193,7 +209,7 @@ func main() { } collector.metrics[key] = metric{ - description: prometheus.NewDesc(key, metricJson["description"].(string), []string{"player"}, collector.metadata), + description: prometheus.NewDesc(key, description, []string{"player"}, collector.metadata), records: recordMap, } } else { @@ -203,7 +219,7 @@ func main() { time: ts, } collector.metrics[key] = metric{ - description: prometheus.NewDesc(key, metricJson["description"].(string), nil, collector.metadata), + description: prometheus.NewDesc(key, description, nil, collector.metadata), records: recordMap, } } @@ -243,6 +259,12 @@ func main() { for key, value := range metricsJson { metricJson := value.(map[string]interface{}) + // Get description, defaulting to the metric name if not provided + description := key + if desc, ok := metricJson["description"].(string); ok { + description = desc + } + if valueArray, ok := metricJson["value"].([]interface{}); ok { // if the value object is an array, then loop through this array recordMap := make(map[string]record) @@ -256,7 +278,7 @@ func main() { } collector.metrics[key] = metric{ - description: prometheus.NewDesc(key, metricJson["description"].(string), []string{"player"}, collector.metadata), + description: prometheus.NewDesc(key, description, []string{"player"}, collector.metadata), records: recordMap, } } else { @@ -266,7 +288,7 @@ func main() { time: time.Now().Unix(), } collector.metrics[key] = metric{ - description: prometheus.NewDesc(key, metricJson["description"].(string), nil, collector.metadata), + description: prometheus.NewDesc(key, description, nil, collector.metadata), records: recordMap, } } @@ -281,8 +303,26 @@ func main() { res.WriteHeader(http.StatusOK) }) + // handler for root endpoint - health check + http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + res.Write([]byte("Buccaneer Server is Running")) + }) + // handler for when prometheus scrapes data http.Handle("/metrics", promhttp.Handler()) - log.Fatal(http.ListenAndServe(":8000", nil)) + // Log server startup information + addr := "127.0.0.1:" + *port + log.Println("===========================================") + log.Printf("Buccaneer Server starting on http://%s", addr) + log.Println("===========================================") + log.Println("Available endpoints:") + log.Printf(" GET http://%s/ - Server health check", addr) + log.Printf(" POST http://%s/event - Receive semantic events from Buccaneer clients such as UE Buccaneer plugins", addr) + log.Printf(" POST http://%s/stats - Receive performance metrics from Buccaneer clients such as UE Buccaneer plugins", addr) + log.Printf(" GET http://%s/metrics - Prometheus scrape endpoint", addr) + log.Println("===========================================") + + log.Fatal(http.ListenAndServe(":"+*port, nil)) } diff --git a/readme.md b/readme.md index 6db7e78..49a6013 100644 --- a/readme.md +++ b/readme.md @@ -120,23 +120,82 @@ MyBuccaneerApplication.exe -BuccaneerURL="http://127.0.0.1:8000" ``` -## Running the Docker Compose demo +## Running with Docker Compose (Development Setup) -To try Buccaneer with a demo project, you can use the Docker Compose demo located in the [Examples](./Examples) subdirectory. The demo has the following requirements: +For development and testing with your own Unreal Engine application, Buccaneer provides a lightweight Docker Compose configuration that starts only the Buccaneer server and monitoring components (Prometheus, Grafana, Loki, and Promtail). This allows you to quickly set up the monitoring stack while you develop and run your own Unreal Engine application locally. -- One of the Linux distributions that is [supported by the NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#supported-platforms) +### Requirements -- The proprietary NVIDIA GPU drivers +- [Docker](https://www.docker.com/) with Docker Compose support + +### Starting the Monitoring Stack + +To start the Buccaneer server and monitoring components, run the following command from the repository root: + +```bash +docker compose -f Examples/Compose/docker-compose.yml up +``` + +Or navigate to the [Examples/Compose](./Examples/Compose) subdirectory and run: + +```bash +docker compose up +``` + +Docker Compose will build the Buccaneer server from the local source code and start all required containers. Once everything is running, you can access: + +- - Grafana dashboard for viewing metrics. Log in using the username `admin` and the password `admin`, and select "Unreal Engine Metrics" from the list of available dashboards. + +### Running Your Unreal Engine Application + +After starting the monitoring stack, launch your Unreal Engine application with the Buccaneer plugin enabled. Make sure to specify the Buccaneer server URL in the launch arguments: + +```bash +MyUnrealApp.exe -BuccaneerURL="http://127.0.0.1:8000" +``` -- [Docker](https://www.docker.com/) +For Pixel Streaming applications, you may also want to include: +```bash +MyUnrealApp.exe -BuccaneerURL="http://127.0.0.1:8000" -PixelStreamingUrl=ws://127.0.0.1:8888 +``` + +The application will now send performance metrics and semantic events to the Buccaneer server, which will be scraped by Prometheus and displayed in the Grafana dashboard. + + +## Running the Full Demo with Docker Compose + +For a complete batteries-included demonstration, Buccaneer provides a full Docker Compose setup that includes: +- The Buccaneer server +- All monitoring components (Prometheus, Grafana, Loki, and Promtail) +- A demo Unreal Engine application with Pixel Streaming enabled +- A Pixel Streaming signaling server + +### Requirements + +- One of the Linux distributions that is [supported by the NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#supported-platforms) +- The proprietary NVIDIA GPU drivers +- [Docker](https://www.docker.com/) with Docker Compose support - [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/overview.html) -To start the demo, simply run the command `docker compose up` in the [Examples/Compose](./Examples/Compose) subdirectory. Docker Compose will automatically download all of the required container images and start the containers for each component of the stack. Once everything is running, open two web browser tabs: +### Running the Demo + +To start the full demo, run the following command from the repository root: + +```bash +docker compose -f Examples/Compose/docker-compose-all.yml up +``` + +Or navigate to the [Examples/Compose](./Examples/Compose) subdirectory and run: + +```bash +docker compose -f docker-compose-all.yml up +``` -- - This is a demo Unreal Engine application that uses Pixel Streaming to allow streaming via a browser. +Docker Compose will automatically download all required container images and start all components. Once everything is running, open two web browser tabs: -- - This is the Grafana dashboard that displays metrics collected from the Unreal Engine application. Log in using the username `admin` and the password `admin`, and select "Unreal Engine Metrics" from the list of available dashboards. +- - The demo Unreal Engine application streaming via Pixel Streaming +- - Grafana dashboard displaying metrics from the application. Log in using the username `admin` and the password `admin`, and select "Unreal Engine Metrics" from the list of available dashboards. ## Legal From 66b99b2704a9eb03bb50d37cdeb1f2baec1bcbac Mon Sep 17 00:00:00 2001 From: Luke Bermingham <1215582+lukehb@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:47:38 +1000 Subject: [PATCH 25/35] Added missing locks/unlocks to main.go --- Server/BuccaneerServer/main.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Server/BuccaneerServer/main.go b/Server/BuccaneerServer/main.go index 442f98d..6d789ed 100644 --- a/Server/BuccaneerServer/main.go +++ b/Server/BuccaneerServer/main.go @@ -48,12 +48,18 @@ type record struct { type arbitraryJson map[string]interface{} func (collector *collector) Describe(ch chan<- *prometheus.Desc) { + collectorsMutex.Lock() + defer collectorsMutex.Unlock() + for _, metric := range collector.metrics { ch <- metric.description } } func (collector *collector) Collect(ch chan<- prometheus.Metric) { + collectorsMutex.Lock() + defer collectorsMutex.Unlock() + for _, metric := range collector.metrics { if record, ok := metric.records[""]; ok { @@ -161,6 +167,8 @@ func main() { if _, exists := collectors.Load(id.(string)); !exists { // this is the first time we're seeing this ID, so configure accordingly + collectorsMutex.Lock() + ts := time.Now().Unix() collector := collector{ metadata: make(map[string]string), @@ -232,6 +240,8 @@ func main() { prometheus.Register(&collector) log.Printf("Registering collector for instance \"%s\"", id) + collectorsMutex.Unlock() + // return OK res.WriteHeader(http.StatusOK) return From 89a641bdcbfb7ba97f6dffbd319380b37d4f97c4 Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:00:09 +1100 Subject: [PATCH 26/35] Added better stat descriptions and < > format fixes --- .../Private/BuccaneerMetrics.cpp | 2 + .../Private/Buccaneer4PixelStreaming.cpp | 80 +++++++++---------- .../Private/Buccaneer4PixelStreaming2.cpp | 78 +++++++++--------- 3 files changed, 81 insertions(+), 79 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp index 93e6ca3..e6acff7 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerMetrics.cpp @@ -16,6 +16,8 @@ namespace FormattedName.ReplaceInline(TEXT(")"), TEXT("")); FormattedName.ReplaceInline(TEXT("\""), TEXT("")); FormattedName.ReplaceInline(TEXT("'"), TEXT("")); + FormattedName.ReplaceInline(TEXT(">"), TEXT("")); + FormattedName.ReplaceInline(TEXT("<"), TEXT("")); return FormattedName; } } diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp index 0624322..55dee23 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp @@ -7,46 +7,46 @@ #include "Buccaneer4PixelStreamingSettings.h" TMap StatDescriptionMap = { - {"jitterBufferDelay", "jitterBufferDelay"}, - {"framesSent", "framesSent"}, - {"framesPerSecond", "framesPerSecond"}, - {"framesReceived", "framesReceived"}, - {"framesDropped", "framesDropped"}, - {"framesDecoded", "framesDecoded"}, - {"framesCorrupted", "framesCorrupted"}, - {"partialFramesLost", "partialFramesLost"}, - {"fullFramesLost", "fullFramesLost"}, - {"hugeFramesSent", "hugeFramesSent"}, - {"jitterBufferTargetDelay", "jitterBufferTargetDelay"}, - {"interruptionCount", "interruptionCount"}, - {"totalInterruptionDuration", "totalInterruptionDuration"}, - {"freezeCount", "freezeCount"}, - {"pauseCount", "pauseCount"}, - {"totalFreezesDuration", "totalFreezesDuration"}, - {"totalPausesDuration", "totalPausesDuration"}, - {"firCount", "firCount"}, - {"pliCount", "pliCount"}, - {"nackCount", "nackCount"}, - {"sliCount", "sliCount"}, - {"retransmittedBytesSent", "retransmittedBytesSent"}, - {"totalEncodedBytesTarget", "totalEncodedBytesTarget"}, - {"keyFramesEncoded", "keyFramesEncoded"}, - {"frameWidth", "frameWidth"}, - {"frameHeight", "frameHeight"}, - {"bytesSent", "bytesSent"}, - {"qpSum", "qpSum"}, - {"totalEncodeTime", "totalEncodeTime"}, - {"totalPacketSendDelay", "totalPacketSendDelay"}, - {"packetSendDelay", "packetSendDelay"}, - {"framesEncoded", "framesEncoded"}, - {"transmitFps", "transmit fps"}, - {"bitrate", "bitrate (kb/s)"}, - {"qp", "qp"}, - {"encodeTime", "encode time (ms)"}, - {"encodeFps", "encode fps"}, - {"captureToSend", "capture to send (ms)"}, - {"captureFps", "capture fps"}}; - + {"jitterBufferDelay", "Current playout delay introduced by the jitter buffer"}, + {"framesSent", "Total number of video frames sent"}, + {"framesPerSecond", "Current streaming rate in frames (fps)"}, + {"framesReceived", "Total number of video frames received"}, + {"framesDropped", "Number of frames dropped to preserve bitrate"}, + {"framesDecoded", "Total number of video frames decoded"}, + {"framesCorrupted", "Total number of corrupted video frames"}, + {"partialFramesLost", "Number of partially lost frames"}, + {"fullFramesLost", "Number of fully lost frames"}, + {"hugeFramesSent", "Number of huge frames sent"}, + {"jitterBufferTargetDelay", "Target delay the jitter buffer aims to maintain"}, + {"interruptionCount", "Number of playback interruptions"}, + {"totalInterruptionDuration", "Total duration of playback interruptions"}, + {"freezeCount", "Number of playback freezes (lags of 300ms+)"}, + {"pauseCount", "Number of playback pauses"}, + {"totalFreezesDuration", "Total duration of playback freezes"}, + {"totalPausesDuration", "Total duration of playback pauses"}, + {"firCount", "Number of Full Intra Request (FIR) packets sent"}, + {"pliCount", "Number of Picture Loss Indication (PLI) packets sent"}, + {"nackCount", "Number of Negative Acknowledgement (NACK) packets sent"}, + {"sliCount", "Number of Slice Loss Indication (SLI) packets sent"}, + {"retransmittedBytesSent", "Number of retransmitted bytes sent"}, + {"totalEncodedBytesTarget", "Target total encoded bytes over time"}, + {"keyFramesEncoded", "Total number of key frames encoded"}, + {"frameWidth", "Width of the video frame"}, + {"frameHeight", "Height of the video frame"}, + {"bytesSent", "Total number of bytes sent"}, + {"qpSum", "Sum of quantization parameters for encoded frames, a good measure video quality"}, + {"totalEncodeTime", "Total encoding time (ms)"}, + {"totalPacketSendDelay", "Total packet send delay (ms)"}, + {"packetSendDelay", "Packet send delay (ms)"}, + {"framesEncoded", "Total number of frames encoded"}, + {"transmitFps", "Transmit frames per second (fps)"}, + {"bitrate", "Bitrate (kb/s)"}, + {"qp", "Quantization parameter used for video encoding, a good measure of video quality"}, + {"encodeTime", "Encode time (ms)"}, + {"encodeFps", "Encode frames per second (fps)"}, + {"captureToSend", "Capture to send time (ms)"}, + {"captureFps", "Capture frames per second (fps)"}}; + void FBuccaneer4PixelStreamingModule::StartupModule() { LoggingStart = FPlatformTime::Seconds(); diff --git a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp index efb3538..0457258 100644 --- a/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp +++ b/Plugins/Buccaneer4PixelStreaming2/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming2.cpp @@ -6,45 +6,45 @@ #include "Buccaneer4PixelStreaming2Settings.h" TMap PSStatDescriptionMap = { - {"jitterBufferDelay", "jitterBufferDelay"}, - {"framesSent", "framesSent"}, - {"framesPerSecond", "framesPerSecond"}, - {"framesReceived", "framesReceived"}, - {"framesDropped", "framesDropped"}, - {"framesDecoded", "framesDecoded"}, - {"framesCorrupted", "framesCorrupted"}, - {"partialFramesLost", "partialFramesLost"}, - {"fullFramesLost", "fullFramesLost"}, - {"hugeFramesSent", "hugeFramesSent"}, - {"jitterBufferTargetDelay", "jitterBufferTargetDelay"}, - {"interruptionCount", "interruptionCount"}, - {"totalInterruptionDuration", "totalInterruptionDuration"}, - {"freezeCount", "freezeCount"}, - {"pauseCount", "pauseCount"}, - {"totalFreezesDuration", "totalFreezesDuration"}, - {"totalPausesDuration", "totalPausesDuration"}, - {"firCount", "firCount"}, - {"pliCount", "pliCount"}, - {"nackCount", "nackCount"}, - {"sliCount", "sliCount"}, - {"retransmittedBytesSent", "retransmittedBytesSent"}, - {"totalEncodedBytesTarget", "totalEncodedBytesTarget"}, - {"keyFramesEncoded", "keyFramesEncoded"}, - {"frameWidth", "frameWidth"}, - {"frameHeight", "frameHeight"}, - {"bytesSent", "bytesSent"}, - {"qpSum", "qpSum"}, - {"totalEncodeTime", "totalEncodeTime"}, - {"totalPacketSendDelay", "totalPacketSendDelay"}, - {"packetSendDelay", "packetSendDelay"}, - {"framesEncoded", "framesEncoded"}, - {"transmitFps", "transmit fps"}, - {"bitrate", "bitrate (kb/s)"}, - {"qp", "qp"}, - {"encodeTime", "encode time (ms)"}, - {"encodeFps", "encode fps"}, - {"captureToSend", "capture to send (ms)"}, - {"captureFps", "capture fps"}}; + {"jitterBufferDelay", "Current playout delay introduced by the jitter buffer"}, + {"framesSent", "Total number of video frames sent"}, + {"framesPerSecond", "Current streaming rate in frames (fps)"}, + {"framesReceived", "Total number of video frames received"}, + {"framesDropped", "Number of frames dropped to preserve bitrate"}, + {"framesDecoded", "Total number of video frames decoded"}, + {"framesCorrupted", "Total number of corrupted video frames"}, + {"partialFramesLost", "Number of partially lost frames"}, + {"fullFramesLost", "Number of fully lost frames"}, + {"hugeFramesSent", "Number of huge frames sent"}, + {"jitterBufferTargetDelay", "Target delay the jitter buffer aims to maintain"}, + {"interruptionCount", "Number of playback interruptions"}, + {"totalInterruptionDuration", "Total duration of playback interruptions"}, + {"freezeCount", "Number of playback freezes (lags of 300ms+)"}, + {"pauseCount", "Number of playback pauses"}, + {"totalFreezesDuration", "Total duration of playback freezes"}, + {"totalPausesDuration", "Total duration of playback pauses"}, + {"firCount", "Number of Full Intra Request (FIR) packets sent"}, + {"pliCount", "Number of Picture Loss Indication (PLI) packets sent"}, + {"nackCount", "Number of Negative Acknowledgement (NACK) packets sent"}, + {"sliCount", "Number of Slice Loss Indication (SLI) packets sent"}, + {"retransmittedBytesSent", "Number of retransmitted bytes sent"}, + {"totalEncodedBytesTarget", "Target total encoded bytes over time"}, + {"keyFramesEncoded", "Total number of key frames encoded"}, + {"frameWidth", "Width of the video frame"}, + {"frameHeight", "Height of the video frame"}, + {"bytesSent", "Total number of bytes sent"}, + {"qpSum", "Sum of quantization parameters for encoded frames, a good measure video quality"}, + {"totalEncodeTime", "Total encoding time (ms)"}, + {"totalPacketSendDelay", "Total packet send delay (ms)"}, + {"packetSendDelay", "Packet send delay (ms)"}, + {"framesEncoded", "Total number of frames encoded"}, + {"transmitFps", "Transmit frames per second (fps)"}, + {"bitrate", "Bitrate (kb/s)"}, + {"qp", "Quantization parameter used for video encoding, a good measure of video quality"}, + {"encodeTime", "Encode time (ms)"}, + {"encodeFps", "Encode frames per second (fps)"}, + {"captureToSend", "Capture to send time (ms)"}, + {"captureFps", "Capture frames per second (fps)"}}; void FBuccaneer4PixelStreaming2Module::StartupModule() { From fa8050dba58fe46a5bf6e679816ee305f2d87909 Mon Sep 17 00:00:00 2001 From: Luke Bermingham <1215582+lukehb@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:05:29 +1000 Subject: [PATCH 27/35] Trying to fix deadlocking --- Server/BuccaneerServer/main.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Server/BuccaneerServer/main.go b/Server/BuccaneerServer/main.go index 6d789ed..fdbeeba 100644 --- a/Server/BuccaneerServer/main.go +++ b/Server/BuccaneerServer/main.go @@ -236,12 +236,14 @@ func main() { // store collector in our internal map collectors.Store(id, collector) - // register collector with Prometheus - prometheus.Register(&collector) - log.Printf("Registering collector for instance \"%s\"", id) collectorsMutex.Unlock() + // register collector with Prometheus (must be done outside the mutex lock + // because Prometheus will call Describe which also needs the mutex) + prometheus.Register(&collector) + log.Printf("Registering collector for instance \"%s\"", id) + // return OK res.WriteHeader(http.StatusOK) return From aa82e64d5f2db328d1f4d6feb3e745eaa48d8dcf Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:43:07 +1100 Subject: [PATCH 28/35] New icons, fixes for crash on startup with editor, added reporting interval cvar for Bucc4PS1 plugin --- Plugins/Buccaneer/Resources/Icon128.png | Bin 12699 -> 13090 bytes .../Private/BuccaneerStatsModule.cpp | 9 ++++++--- .../Resources/Icon128.png | Bin 12699 -> 14211 bytes .../Private/Buccaneer4PixelStreaming.cpp | 4 ++-- .../Private/Buccaneer4PixelStreaming.h | 2 -- .../Buccaneer4PixelStreamingSettings.cpp | 9 ++++++++- .../Public/Buccaneer4PixelStreamingSettings.h | 2 ++ .../Resources/Icon128.png | Bin 12699 -> 14240 bytes 8 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Plugins/Buccaneer/Resources/Icon128.png b/Plugins/Buccaneer/Resources/Icon128.png index 1231d4aad4d0d462fb7b178eb5b30aa61a10df0b..c175b12948690dc707b3a727edab24153b97ac28 100644 GIT binary patch literal 13090 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 a;7#~yJh_ha>A&A^fRdb=Y>l)<=>Gxy=2LS3 diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp index 6e23c01..ab86a4e 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp @@ -58,9 +58,12 @@ void FBuccaneerStatsModule::Tick(float DeltaTime) else { // Get application resolution - const FVector2D ViewportSize = FVector2D(GEngine->GameViewport->Viewport->GetSizeXY()); - ResolutionX = ViewportSize.X; - ResolutionY = ViewportSize.Y; + if(GEngine && GEngine->GameViewport && GEngine->GameViewport->Viewport) + { + const FVector2D ViewportSize = FVector2D(GEngine->GameViewport->Viewport->GetSizeXY()); + ResolutionX = ViewportSize.X; + ResolutionY = ViewportSize.Y; + } double GameThreadTime = FPlatformTime::ToMilliseconds(GGameThreadTime); double GPUFrameTime = FPlatformTime::ToMilliseconds(RHIGetGPUFrameCycles(0)); diff --git a/Plugins/Buccaneer4PixelStreaming/Resources/Icon128.png b/Plugins/Buccaneer4PixelStreaming/Resources/Icon128.png index 1231d4aad4d0d462fb7b178eb5b30aa61a10df0b..202edfc109001e420265d719d59dba881c925de8 100644 GIT binary patch literal 14211 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 a;7#~yJh_ha>A&A^fRdb=Y>l)<=>Gxy=2LS3 diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp index 55dee23..4cffcc8 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.cpp @@ -50,7 +50,6 @@ TMap StatDescriptionMap = { void FBuccaneer4PixelStreamingModule::StartupModule() { LoggingStart = FPlatformTime::Seconds(); - ReportingInterval = 1; if (UPixelStreamingDelegates *Delegates = UPixelStreamingDelegates::GetPixelStreamingDelegates()) { @@ -123,7 +122,8 @@ void FBuccaneer4PixelStreamingModule::ConsumeStat(FPixelStreamingPlayerId Player } double NowTime = IBuccaneerStatsModule::GetStatsTimestamp(); - if ((NowTime - LoggingStart) >= ReportingInterval) + const double ReportingIntervalSeconds = UBuccaneer4PixelStreamingSettings::CVarReportingInterval.GetValueOnAnyThread(); + if (ReportingIntervalSeconds > 0.0 && (NowTime - LoggingStart) >= ReportingIntervalSeconds) { LoggingStart = NowTime; MetricsCollection.Timestamp = LoggingStart; diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h index 22f39ef..73dde74 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreaming.h @@ -23,7 +23,5 @@ class FBuccaneer4PixelStreamingModule : public IBuccaneer4PixelStreamingModule private: double LoggingStart; - double ReportingInterval; - FMetricsCollection MetricsCollection; }; diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp index feee1b1..4aae0b4 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Private/Buccaneer4PixelStreamingSettings.cpp @@ -8,7 +8,8 @@ #include "UObject/ReflectedTypeAccessors.h" static const TSet> GetCmdArg = { - { "Buccaneer4PixelStreaming.EnableStats", "Enabled" } + { "Buccaneer4PixelStreaming.EnableStats", "Enabled" }, + { "Buccaneer4PixelStreaming.ReportingInterval", "ReportingInterval" } }; TAutoConsoleVariable UBuccaneer4PixelStreamingSettings::CVarEnabled( @@ -17,6 +18,12 @@ TAutoConsoleVariable UBuccaneer4PixelStreamingSettings::CVarEnabled( TEXT("Enables the collection and logging of Pixel Streaming stats with Buccaneer (default: true)"), ECVF_Default); +TAutoConsoleVariable UBuccaneer4PixelStreamingSettings::CVarReportingInterval( + TEXT("Buccaneer4PixelStreaming.ReportingInterval"), + 1.0f, + TEXT("The interval at which to report Pixel Streaming performance metrics (default: 1.0 seconds)"), + ECVF_Default); + FName UBuccaneer4PixelStreamingSettings::GetCategoryName() const { return TEXT("Plugins"); diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h index 3370f68..aaab581 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h @@ -25,6 +25,8 @@ class BUCCANEER4PIXELSTREAMING_API UBuccaneer4PixelStreamingSettings : public UD )) bool Enabled = true; + static TAutoConsoleVariable CVarReportingInterval; + // Begin UDeveloperSettings Interface virtual FName GetCategoryName() const override; diff --git a/Plugins/Buccaneer4PixelStreaming2/Resources/Icon128.png b/Plugins/Buccaneer4PixelStreaming2/Resources/Icon128.png index 1231d4aad4d0d462fb7b178eb5b30aa61a10df0b..63db5e4fd375ad7c2fbe9f2661e2cc175238876e 100644 GIT binary patch literal 14240 zcmV;RH($t!P)6M`nOmpbP5l1(=GiB?JNL|))6bcE=ibHFT-yOb!^my~aSbz_ z7=>xpNunJS8Nxb&O#lrDWBb@Tg_#iwOga*Rh>1>^T_A%!Ye8U%R3#FbM;3{SI2&3m z2WGg(03p&Di$DeeHaX`+smTCg6Hx0Ugu%cbn12a_lvpD*RS;|fV1`?SL~aZiEO4z9 zjwLb{hu%pTsS|ASCmb|+ z28ZeSSOp*eBZ)n1W5!OyvEta^kDZhsuE0dBG5pl>bYdqKMdTE$v`!bnyb)n*HiyQ> zODRU#K!k^vX>y){rB9sHJSGB$u!g1s6U6Yy(qIwGd$u?)hmko@S_v|(Bpg}vkINWi z*#+x?*gh1&u*9+$G9`kLq&$ZaDuhN4=4lX5AypV;SyM>_V-=)Khn%cAKbFIWoyKe{ zzxIQmvE#LexQ>>@Up+cIYsF&MY`6C@k!H(=8nHoJJjN&icy4Pqw)j%i){s;8Gm0 z&+=31(r?)|vt_%?f`Y<{6Q0ZL&|Za~aBMiyzzCPmN?g)bXpl#U36q3XV7w0-z0W>9 z_Vk%EA_N%LnF1BX2@3VOhw zGBeu)%%@*`*{WUU)VJS(L55bec7N7=Fm~aRrGP}&ADjijLFj|wV80W`KZi7Ff-mkr za1c5dE?x=%lU^9FQv%1QPoDuK3l=S5C`0!Tz7K}4|M5q)a5#RIr3eGYR*6jDG#6q> zGHt?&n6SF#Vl3t-XUG66(+@Un-ol+_tOSOGTtILj7yT5(*^Zg*LHfXX_MW}a*Za-^ z@K=w&F!1QnKam#YEF`*x>VYCTIT>aFfkMb$s7Vvf0yc29)-7AY*R|``!{0Ziy$!zg})8QZp#nYqT3t3Hc&!DKa+cti-LLtt(RNSHa;;3P~8 z1_gg-eE5Ekn{NV>96W5KMF9~|F$Bm-Qg0wZuylzn222}8(~tHQlzP~-0Jeb>z(lQos|eU=cgY&z{8zL?s$pwA^?LI~@)BY)QtmBQTxWzCEZAK9(N? z3y>sqs}jf9$J~`HtG?pEFeg}-(XFg-0V1;oygnHe6JN!L)@)Z2l6Y|i(SzuG3 z6@3(hU8s~GXz2JM`v1OnFPI*iNuR0Xmds^wmbay|Hf zU5BCBvQ&tEw2IR{kcyxOh~%Wt5@Cyl*qv-YYg4?r$k&3xHo2mb6EC5(A}t5d3S{to zkORg%^$hg9_{tQd1pzQ&$eGq4J-+KjiyqMX+%*797{H+C%9Se+h(V^Hq{I8NL4t6K zBZifQ*1%C{?w#-)TQz~<>$%q|gsd72nNCdj0P83Or;uQsl>x9srT*Qx94$Ajas?|- zVCHN1CIjnM!o)5;{$XOfqFiAKqzUARbJd&}B@E)(lrhL*0P_Qp8I7v5Z?{xqLIEQP%K{)Ongyd4%-NbuPJ|Y@Ong^3 zlSH2fV;;^zgc)nJ=Ma!F6tKJ@<|4aLLoxlt>QORgo)zp}ra-n-&PD~o_+;rc9pO7g z6j;XSFquFo%c*wk5eq?U1mQacix#L65b#Dvq_MVdGT3CNmn#TDNFW#t_yd6;{0~_t zT`srB10UL47z=Si8PEvfEyJ9fSfH%s$$_4wQtADy+JxwY?At)*IzWIHbjbD-38uPc zT?@m=yxYZE5aUS3fhHREd?-Z9%FC*%s=VH))RdIEb!w%gB&Q@Lx!pzxy;4w^mv{M6 zZeDI~UTJBmC&J^4@wwe@i%~grBjGD!1P0to zWHAFH=VXi&Rzo$1{r*5vadB*{uhVrMZ|c_dy3BS>8)c-XCL?#0Xac07sG#WYGv~7R z9o+WI&fULdU%qlV))yP;jRci)pmHpe%_)3f6gs6eBbvN}y@IRaYucJskl?Xwr_!G# z1{A48MKh6xtAY;>kr0%#DJd!a@4Dl@d-}I--3lSMcCreEg49je=!vy8vb~>g*5>LD8j%C_Es(BiY6ymCoq{k<|}U5wgV>-*&Zdu23d|XmOLY+Fb z>ea0i8Li(4@}oc%MeOmysNKKqety!+N00rLl$3~wSo8^u+DRCR#D^2~ze$R0V-yg^ z8+pZG#9ZYN&u>FGW3Ae02nCS8Lc8zhD=fG& zX6!RRZQ7cel1!x4ksKk!rg9gnpJN&EDiwP8DX|@bloZM45k63=wBi(kWB@At{DK?0 zbecQ!b6;#c@|Pf&8@Z#;ojd)-thsBx|KVaz4wOAn-Y6(?TrL;8LI9D1GC(C2l@$ea2=|E|7|J~X0E-TEk42Io*jOu+Ad?D4Uy*KJVhKqfz&O*%)FY8-_hP9tS%-Fl9M z2%5y))dEGcG{hzu9urpeK!f7FsHnJA%c~d8ofR7g(l133k;oM_Z^42wW1ri){TGlp z)Iw2SuU_d3>}v@aL#74w#Kc6GmsfzVe7AOeVnST&Hti6Uqy9=)L}ag?xBZr#ef-3! z=;$bcSBVcrX+{JH9?%a?-ix2nqTi}fv< z`$e6)4K(RT`KtoYPMG-Kho6Ae$H&LHTrPx=goc~e>W3jdUkrq)Z`XXEmzR6%%{PN` zARM_PV|sMIVa>V?g@r{C5fQS`utOe1&ONr298O_Y3g)pjk;((Y%%*fv=A@Hz2%;AP z8lqRKi{G94YS*saQJ@4xcq_`Q9(r`l%J0_Hu9XTAUcdyAA8Z^JTxI1Ib|JxfB`U*tCzD!i^M|shu&8&>+nygkp4OTW_p#@Rr^bPu)~;Pk zKm&pH-C;G8(tYy>xNsufIdy)~Ui0{TvrEcgjV899~&S%qt8TLhvYzR#~ht(Z{%YY=Q9f zDV>0Yv~IrX;{t&o2;zn3#%jxlAacikGGqGJt5(;kmC7!)fLedR59@+9z=4BMpbYB8 zBZm$?@$AI7SRWJgX#+h~`2+7voigs}r)$@)gF;nc%P8bGJ{Z-jm)^He@6_akA2)7> z!c+@KHo4##+js8nb?c3(b?U>yA|@_1=&#)H^Cn0YGB2#vhG7N^EFo2A?J7Hny;ORDO`e5;cFH@8kET#>K}W5<)J|GZQBMb>gHE9GC_*($vY51`i&h zm6cV}-3UU}fP(GXb&QH|{kU<9R_E9-kVr~PO3$6YFrfdP)Xy+!d3AG$a)8$3aSNQv zJj#q9#BDV;4$O0G4>ltR*UTc?#iAh07gk#=ucl-3jv;Rqm4?U-20=i-yIO|_8ll!+Y>t$J47Z+8Twflu|&*^+3wL!%QF%7 zK)lt=!8nDcKrjeG88z~LdKm|DF9}Tl{3{QwdBn&HMQYYP{b_$NfIQIq4GbwtN*Ll_7uW_donpD>b!l z-8yjUhaZ2^@nfvMW&%0?_w2ntY}lYXL2P{T*wLdP6UZfqaD7)P35m*mHuxgY+_X1zeFDNM~wQg7n{CFcHr++a!6x1?7 zsN&v1_ug_-w}Qe#HV{cQi_MW-XWE=Hic+)B=WfDnOA%@v5do1q73&NvOrfvmCS7un>For5BDJKX&x! z(bryiu}!O%-bnA0qaW$h>puXt`}e&irRwqqX-<&6XW!S$S7=TUz%JL+DKEvx$5m8R zDuzgclxsqDuWZM{v3)sOwMLLo*b8A~?=9|S-XUKp4%$eGC>w4Ut?zi=+tgPIz=?AyRU6_9* zDHYZ{o7!euaamzQ(44g?xi5pfgPfAIl6|g7v`*mv`9{n_A=?IUYvGh;`Oed0-a z*#{t8QLn!C#?PC#LOrVF6RODu4bs3cPoDbw;GrX3uD>oWE-5}C!R_`gT)b%B!etPe z>ea2?p+l$Wm}p?<`0-<-$BsLA=x}UYEQFfUvhv7?2pMfegeT`x?v?!f|Mcud*JZ?i zRr6N0Q@pV!uF(6F>HAM!C+hsWdbB_!tMT@Z|HU-jfcR7+jr#1q1UFpyKUPJ zh?X(Y(GbU6Tw+ixD>>Vd^0gR+_yc#XI@0B6X(`P;$VoUSvVsrDtGuG3xVRYp!l$AF zWbOwwgUC8}%zWyJ(XYSqeA8wvD4)%@t_&zy-+>Dc3O2LgdL>wj$9=9-K~SHV&o1uJXSZE(+kJ3Dr02YLk; zEG~jSr9h0yR|T*h87W~Sj4aq=DLUExx&WyMIGHh{0KzLn2v_<>k``AryRm!sC@;N7 zo|=-Jl#o!jPObFx1}U}DH1G-(&@Ln(_mZVcUYz^}RE=O_>f5t~K*;)W?Kds1ZcR5v znAI9AgFl?{<>AAJTU>QjVq%;JV!6i?3Wjno=N~_QynXw&kN@wM`ghbbpbHUJa}$%|G?`*>mU5U%rxmCBLAms;aD_ zYWHuyA2@WFmJEay5+i!E#urj52V)s7w=2RL8K#EkkQmztp{Qt3KW+7jMRD;d(wHiG zeTL{eL_8NRoSXK}2VbxFHr5vt5fK?CeSqB!A9CN5k3W9=*pV&Uca0o2xL#Vie(Ja1 zc7O8u%^>HP2aF)gp8T{yBek>GGBH7cIGPF()Z8L0P2pjs;#- zmXeeN$iV=i78*AAz6XX6PEM(#pFVu}z!x*;xZJMMkBn;G;u^j4QqDi~7cE=7borUH z=MobWVxpr8i;I2Qb)oR-3g*r)EQH0~&>>ln`P0)In-o>ii>x6lfr2H)j(`4nP%!&2 zx@hB!#4u94V#YqNE5Lf?3_G3z3)vXkhR?=0NQUuoaT~r{np~^4u_MY&E4h-Q;&cDx z960pHmTfzJ*}3atPEMRw9fm@rn)fJ}4KTZ1E(mI1q(w!=85!wAvIY(8cSnP?y2$Od zDGpTpb?VIWm8+MpTy^f;dGLwI$VgbPL9;IRTf%XgS$Bov;*!+V)UMZey7|U#*S5Z< zVf}jXP$FaPLM)=|@<3Iv&w#AIPM(U6^4bCyDneOeNR*_$6j1C75-UqS>sq)32}0>M zZk$tUG%C!UePcx3J0|$jiG7nHlWV7vqD7t&I3#8-~0W2nJv{Nb^!|E+p+Y zUwdW5(7})}wXg=FsSJi(?|ks_J0DC3A%YddEX5@ykcwh`;*Fi^=R=rY>L@LP0x34u z2g-nyn|05C|32~vUAuUEzwX*TXy_3O393d|b0);cyZP<0HIfCU(r?I`Rw`I#lhz3oX z8L|HFQzzyvT>ACORU||pCXE=H)iAx0e(KPHJutA-^*6xrh=)dP+OoY`8p6aSs6el7 z(R|H{B_21VG2*Hrv%{RvyGSQ5ji`&)C$~`5Cjr$2iF#Kw1)XqSlE-EZq zv}^_4w+!nnSH!~)4uz2FIE<*jvE8tgNN?B>qz?t*O*h@rq}kQbdHnd%=O?|?cfh@$ zeL1VDveNIb`sDL3`}V*0rB|n%`0E$|w7#}Or|Y}YCix#1I1?x_PcJ;=bx#+Ct1a$u798Lt*tRErzWIRr#OC zP7LbbJ0>m>;&QXAS{yuh=z1t}T&TRF?DaR_ zp7`<zL;~d^L6baJ+8{Cz{j7=Sn~DCs3@;X%J}SB z6HB#;@rVwlx2!vL%zX8wi7pfX^RKF`cy8jWrCKmlyk*Fek6eaYUkNt^Blc}61`xBl zz}y)DQ0Rfb5uOML`ZKBNwD$_G z}QMnAJNPFN`;ei-Ie zZ9c{&VOB0gov~JaW)#|m77p=XRa!6wJf%zUc)dBJQ1GQ__ z{@=nyd-ffGD#$4of>wa23pu&*@$qx#FFf?eQLk1h3OMyIJU1LAP&h!E1-qI4{GB?J<*qPEs#0$&Oq^}9B2^+1i%tH6bxY()*guw z%V}p`O-s`*PJja5o;o?}zAU=5#BSt@dib%?Yu9biFWd?-#;zpI5JcG1!t@Nq*zf@2 z45~`#vK6yEUa$Am-={BMx!m)%|IkY-!OF%>=|dB{cJGGOX>@cHcDVH@=KzbnNV<{M z<@H874P?oL3<((mmU~M4@-gA3h6N2&%`-lFuist$=q50lD5Gb5I%DSSdGuX4=7N>A zX`2YWO5~%37+K7SVz{^+g2L1}DgZgzSm&doqkr9K zh#o~imE(5N8@z&TYa-M(t>}-7(LdS&tj|K4gpGX~Y)sXQR}U0z9=f#)*6C^W>a}m% zx?Q_#LE_f^qEN{H=3DR0_+l2+yR?E(J>`n5XUEugGlnr){uGmhISAGlIQepKfN+4t zJbhR4^E-BEH~FQBnVB86%e>`uO;4X8wL(ycp9%8^HE_Lft;YUKeW8M0`1H`{TMr&Q z@apStZr`yhIVlNDK}g`ToGm{PUPP{@$aGPHtQ#^?f)F7kLhS@#Zn z^q~jR(;I1L%$o!#Ou&{lCxmx!mNJ_H`58u)vqNxqd9+La=vhu>BqKi3~&YV4e>Cz=Y zOdqh}BR<=R;)st*%4pyai_H50N{6ah2qKb0*tE$yFdHm$Dyymj{(xSAP$2-phH_>h+ML}Yvn%`*vfkOoh z^J)tb;*Ih;ZVZqL3c{KMmLRZdBtoJT_+;eadWBpgmRig>MmH>pY%W3;(GD2`7CxP? z>v(tne%jMT6uQbEpqBxvz&iqj+gwgSUT)f4u*{8#ilWv^?>R?&zkdDZt=p|fn!@C1 z{Zv_b*@*kITDENE!a=tuYUAe3Yu2v!F*nW}YYL|R#_TJPuqadZCppCFS$GYJhaW19 zF;1v_P1P**ag_j%z&fa`tZLdgoG#k6s!Ev>W|O03X!2!t<0!C4Ku0#--(D#-tu!d3m{hw?J@mIg@Z- z87s#6a~Vc>vK5GB;aMhbR0-I8K7ergBvucrWJa$T*%QbLs!1?5*n=EU<$TW;q*GFM z^vHqBmo6DkgsD*1ty>%Fq8iMpPfQ@!W;Cd8bp!)JHveN;dBowH>Db2x^wQdKPxdJ%i$&$sp+*!S-Hd-T40=B!zC@$3?X z1Fe((%LFysVcgB> zC8-7?X%2o@1QIg6Ga}VVE|;r9mVgzOzs04cAVfKUBqYRRWU~?o1``t!kfLD}Djz;< z*u4Y$}Z@>O(tvYFRN0-@P8SHX+iiWjrY<3Ce%+MhX#?mRaXfYkd z#g-{{frS`dVSX?ZZ!LdhGmTsYIZ=sSxT>-eY*17{sN#mMH+1iI{rL+QN=iyD=HxbQ z(&U~2{aDLofFX;9K79AB!Gi|o=3NFe=cl5Vq4F+Yfpy0$yZdy$jBLbN3tGnL4>adIk!Lf!@Fr3b-R9`t=Q;RVPD zt-2>s_~Uas7trC{T&GMzq%?Mrs*?KF5mu<<*or^pbK+ zu(I^GJ$v>aI#NV@9K9#>Rh^B9Eu6xa2?mvMyfCQ9N)VtHJ5LeJp#D z&=kcpBWH|-OA*E(=C9eKhjk(;kT5E--OvOO=n;YvETBqD%PwEeuivn-#1N)Wup+;` zQ8Gf;ht-1AAuNn^r!?io?e$&Xr8|5W$bh+NUeKRAzW|bV5&JkXXIg}@IhBwUlI1y& z`|OeCc#k-(vYfOpjF}t1Scx;_@W@4zXF1VoAzWEec_Am~+Aw7W3DH#;!ufd@j{kM? z{Dq6)H(sx|P0OnrHElsPM3F_FJ$>T9p~Db!pk!)Lzkc&(jeYT{3ajUuJ0~X>9EE*G ziUY+a<3ixoc_%!#Y=Xri!^*dW>xZi6F_ns@2TXN?#q&s7d<$w&_5_20vuDpMXwVyd zR4DrB(ZgTN`ro!+cIM^fX=N*AJ}y44ch6fVzw`ox@5xiAtX{iL|Je>uDyXM!o!U2d z?>6dzVNIG|g+gU0L{!-E>2v3xI+CAH=KKINGPSP3$;sAofAM`{a_zywTi1ew2(2tb zWlLgzfLC?#NAO=P6XSB7`1`bkS8MbZeAHL7=S+L|1E_`K;^Ja_F_zxJ4;C-`=8vO) zdLklz&)%Dun2?m1XgDV!xw(0>=Pg+I-I@u{J~eXqaD*$2R~qx}*zprYdZ7o)sA0L7 z|B86IW0q$#_3;8EiX)?W?4+u+UOE1;w$C~#TgwV{tw0NN%?`331Qr^m!Pr?@&{y|G zy)kXthtod;$)<>}S|t#;Yo(_Ab>bBC!vSHg$bb_7+SHWfs;a8zCcb?3+_{$~PDJiX zYxS!G0sJ3Fk1L;^v?v#UV?bg=F-DVy<8yXw1lKp`VkJ6dR$x1vmm~rXE`~AHQ6LZi zsX@p-_ScD=f6gHnBM2IKqCWcgqYtNlo|=;4^jr-c9_57qqI?xGbb@d}4Ij_=^0hbK zp!-c+%!141DJ{DE=kdSv&7*o`V=&vS@~M#d$0h(_epxu|#~5sY@z;WsQHU`5z&3U! zA#h`{MK^nMi}~4e{Z?*jYD$^*0C!$)-i}|h?Q8^*#}z&6t69_D`#3el*b*Z17{1ws z7RI$wlBa(*bK2W)(^qrQC)f09A@cXV`_G;K2fh^*71yp^8-k(j^G>GC_L$J>+L#R( zVTxyBUtr|lJs^*ih|meO_CXr@5Q3xC*lUUy6%-af`q0QvKmMRWgZc%9MT@^)MXMcJ z6~{p2nLcC2ORr5!FyEGoow+D3E&=g{za0td?Udx?52kF4psm$ zF)>qKd2z~%6X=}@Lb&N9yXGWg^23#a!U5uk*r%-W$odT#Hh0UFs1Wy&4Qbu^vJ6K^ zI0UobjOLAu_Q~M~#e*iu(A|FJpZ^{g*NXNSzx{WJ+=p_`&HjXI~iqY~#jFAOyxlM}P3n8-oV+ zuT!V~=qI0qkZ8R~m*JLIK9NqbUxis^w%G)rpCg$00SCl`%7az)kvN}<9vLzGzmlIn zZtUo1pBblNsM@sUm$mCQtl#icenCN^^yU=8j0ImRC@PAHj(TDIxCcgzfSx5wmrj}b zR$)=GcEK*3JAk((B_;Ol-RthV`gZPooxU;Y+izDq{>%$;u|915`VZkHV2WG87aOjH zDqC69$N~``)OHFx0(PDVdq18kV-p?{nMo3=l#U(Rdn3K4PMeX6_b4mf zxnB;8=dN8kPkDJ_o7UHA+s6rV`;Hwu`pT548@Fup#l+C}*6=oEr!ehDX^QFM%iE&) zRrS;A1%km}fBQWY40^Oz)DU5SD#_Vg34g;6L~0$V6d)vF=N#gwSCdLCcJO4=9pUAK zguGBTMn;C2CqzPJWn~o=mCc(qd-$Od!-w39T#@v_6^mUO*+Wa0ee>y;U;TOPIIP8d zG12gwWxOOGKnxTUcRkA~#(&mzNf< z{O)^5Fb5AFE-5Vqe*zEFU#zax=5)`rw)Yyd1d5S|r(FIWG)2UAh+5TFZ<+$a1TPU> z>XD@{ghhrPL2Yc{qC51XqUr15ii(24klW)1D~2Ul(qM))rc;L{soxf)9*>~c^Dabb!m6gFj5b*nAVURhd@d--ySc84G`Dh5pd(I=kK-Y6Go6Dv#HsVY&%7Mbzz z^!dEL=mx$F?d~_F`;!@;{CVs|N^+tuW$`)~vI}dDK?Mo?qj)t8AvzFam)cAFWC;Gh zc+B?HFnnPZ1{M#YkGTGpy{}V;Ht89S=&h?zu*N1Iw$Yc(1nKTam-mk&2gW`(36>5t zKVvCIhsm^Ry2Ef)lSVb~vgL>*iK{CJDfl4g11#ah9OalfMSq1S!t?CJSMzc&>aT$Q zcf5pe9&bf?$ukpPhT4~X+Djx-DOH|k1xMkVqo!z=xiJf=Ai|(#%}1=D7#O+y;*qcg zAAmsZc;@W6F=L+%`2E_WLN(Swcq!0JK%Q|EChgsSz<5?!c!QgsoaEWq@GJ6S2399C zMZB?cVD=a7F;&EiQfUH8KBc0~*0z}{3}nhufjO{t7{o+J|8ex#slWg3+p8A_!O>S_ z)X>fKa!LT#?k!{QE3ZzTw`f^vN{Y^^m_8Y@G3MEElz_>e-OtZ03&-xXgS_fNZ3`xR zd<>r<1PYMz9d({nfQ6M&_TK#`PM+*}+pX@%813aZHPL|()HGz&jXaZHdgaSG^HP$N zoNf)WbUMZZ9YTIt38xZ?A|Xgf7HoQe90$XDzmRgB)x(ijtz{9zsz3)2E`H12)BT37 zaS18v9YNJJ`f@j@KA(7M+`^^H!S?k_J>ghTMtP1-)hk|X3?}u0B-vv@>yEFhv1e6z zhcXKhege5XMvA=b>r#i!WzAGhyPzoZKYy-G*2em$fz#s8$gp@rb1! z1@EF2h6_QCt11WOD5-Q>Kq`;*LyHH@f$8xYN=Eo>|7GV7KmA-QHMx0F^WRtYiv{iX$T< zdfs}=h+%`fTz|c_z5ejwgYy?JTfOG{{QQE1gm{O&!5 z%z-Hbg8^%so5}-}hM;Ujnv9=`dr?3bHzML&C z<+rOhZr)l_TAC0a=kX}+=O~s?VYDF+kkggKsn{eV6Dwc~P#uD-w^Jw$pNPs9CX;22 ziS#l@$F%PdhlO8FC&YL&mew09{q!M+LG~%J?w3D&SO#gU|UKN8BhWJLw&4xD+7S zCc6>eJjyLk=8i+Y$HFKu$y&FZbHYbMrDHMT=CE1)PExaQ%8`T>A3L2KZTRV!NWj>d z!nz#{R+v;ecG=`AM;ddOCcrjr;z`esKe9i2@w~Yq&?| zCcL283B#wWFlUj*%f9Tz^XG~u*txm5CY+VxvSWxM)>)VJRRP2s`;ILeb;0SMp+mAP zfGbz7DA;2kJGait(lj-c*`d9G+|npQ>%LnFJ#b2afjH~W;$L=08H{g~7m-BX8VpLX zw6vobL4P_6@G(TBuq6A)#eZcRUw$E^KPK0*U1rO+nFR%f6URRX6TI=(+wgVFQ_ncE z7e-!$BX0hjSrmWUOqc==SOqI0Rdwdf@}ZaxNf)5zyl(YMq{-2? zYw&J97FofBiW{NRXU^zhcF6FN3l=Q_Y#HhFS2O0%o~2{fLj?6j(*<{I-Kd)zd@R(4 z8S0)on1bna>VNf5X8ZQky)09O5cGl;f;8s?;sNud=O^e+50E-$oiz(N_0G)pmdtwJ zIRLZ>R4iD$#DeD}%BviQ4M9%&P{U_u@7;gkAb1Hd`Rr3;p=aTerN9_i0rZ1i!`HUg zwt=%uZDdaX;)VkdH?U+y_^vnILREs$um?tUz3~_cmZvnz4O&AY& zroaClkOb2~C%^!z>^*x$nP3`DEwO}&f#$0U9AwI36aaeMd=vbgGjBdi_u%0pbtXYZ zAoK+bm-Ox33wqYBUr!zU&9~vW+YMc0jq13EJvd7Ls#{hViu8%Vv_VEN5z^8P9Dta> z;@~WV5j|iO6c(~SG(v$1`s3)ININwjv`#dbgaodTF$r9|ZasAVbKxQ!r`4+q$N}5Q zlcxY+=kDFmnb9y^9Hq%hH$h!ViULgkqJ8Cm%(Dhf4<@?2e&4fK5R)zrgLJcj6Ii3d zG%yz3mO&y&OTUf*l$jNj4VVw_%Z3BJM^EO5wU7OHw^-eabtgn5C4Q?VCnxL1-lR!m z0D$ld$LlxzD2k4t86Ul`n;=ams#L+UqVxCJiNnc*6^OFZZlK~ zL~0FE8X$*crh6*Qw>)xS8Ke(30omB{4EuJ7%JN*o_v$KV#|FfK&|&WEy?cQ~q{$r& zAIwCL68eWGVFA7NfpQkU=`g?pM9(o}X*u}r2Oq)Ti4&gNv3a9hQ0iuGRav?UVRn!^ z^gx=`b9jxK;3Y`$71#lUkmM|OxLVmXHG^M`su>@?Z@q7YaX!2JFgNGcoJv_E?848< z-2@mXRLY5jg{3@PUU|xar6z_HK3r%94xtrHO1FJI5YakHUU@hK92ru$f+&R*B%QCe zvnDWz7oY6%M0QwyIifsf$(Q|IUW6;b6jK`w3L)!QFEnX{l;M^3XY>noX!RZ-p;wx4 z7#j?4fJ&od6+onkvbER@FeM{Tu0NfG+MkR+6b6X%Det z2p2jW=pg(+78XmVY8bfL;c`#40tw?H)_B%{WTgtg%7RBGSVQaK0%#3rZ5vk&P)m$(DLTqr*0am}R=3m9%Fy1EysDG24452V= z%gJ1xR5mx}(I|VM>=^N1#VtqwBMf&%Lr(y}BK#jf zKz1$}0AQ*+$jE4D*t>bTdD^?VLzHA>AnqUCY#p3!0Kj)CPuosM`+!93ZuMGPISQJp z?50JG4$+d1g%Tw(uux;*zmK9WS|rx&A&`?prWh)WLW+-vekImq!;ZmRK-;GN79aLK zDrV$qBjCH!T*uw+_)F8g_+HgjUc)3B3>`aNkw=pcid`=KmS8<>uy0^vn?o`Llg=H$ zM{oE*?Fpv^0rx?oqO3G9v@QVT`xgrxfT`xdxZXq}@D8Q3OhC{tAedK@pfWm?2$1xT zm;M1r%7dVJnGD)MAu?bwYHhUzXs`nojKRBq0chTRRsaYvPNgOW6(#`?LYpXAz+MEX zn$(Mt0}QwTB3tD?Az*^LOfIcrRh_$YBOLV+R}XG z5igtl_3B*-O|*0}b3gqw;=|?|+Y^%b8Xr*SC=LopVlOkbM!HpI#5eGQZQcREIlI=mKs7Qw4`2&0$Ifv(8i;aW`*BV_b4L2ilu`LM-ge#C@1kLa%;utKy(!; zFU3BBg(6Ml+ml3wfOnzK5giKLsUh{6Vl&uHGHqo74Xr4$WR4Ad4B%OG#)cnOv;1Tc`kX!bJFq?9Q)GPDys^pRP;m~XgrKWNx7u@TiRc8ds6#5huVFwc7lItZ`CrU^ruG;6!tUr zk*J#RIFBD>0arM>Liq#X$RKG>+)!Cm1E4LSL#;eX&h-&Xxo*Gltot9 zmAUCi6bBi?qfrfitNd1%Db_6fX};Al0Ku|;-Qdec?SxYq;T^))$MAD}@$)B^Uzu>q zU$J5p%cZ6(mQGCl5dz0@%Fm`XFQf?`&Q&X_luDSq&(v~k;*I8~%) zq#IN!R%%u%9Ch;7oRsGM=#=|q_!NRGHTa&|JO$|qd zQwc@UFIk^%*V5C>{4O(SzKUDvs$b{cSVVwm+iZXXWGM@xD3?m~7E)xeT}rd}lyqpk`23Jybo- z)>3Wz!Tdu+MMPzAd~E#N_*@oWju`j+yS<#focWx!77HU^Bev$U=2jb}`fZ~hhNsOP zuHi;Ph9w5NMy3t&)p^zQbHA#8l@gS;simk@=Fi#vuDfU+ZZ21 zJEZ6ksSsoE)4l&^>h5?6;boiK`o$BeuZ3+=#8L^N)uB5*)ztPw$BEU{cYB!=NfQpZ z;Tl2vb5m%RyOy!PgRmLHBg6G0B;wtp49Nd*XYl#_S&{KvlYNv;mtD=V<5m}{Wq;4d zB3{AaD7qxj&f6|Az+r1RHfxY)pyaIlMu>x@hTqk>Ywh{uDsnS#6KgAgG?R14)ZMRW zqW3zyl%$;F6`OFnq)L>UVCuOPK1&(NSNcmrANqJqzh25-I~vYE{C}brWK3Azs$D9w zsQM=#Cw1`o(e?9`u+lRGRqDbYi^f?74D+3wJ8 z*Y?wBl}&j4OTTMu3+LN3v|*=)#3~d+cFbn!ANx8+O!F*g^>#M;w%y~=BSPtw`K;q7 zV+|wAi2}K21&EVZy{|Tsn@b{;_1P&6b~~#ah3Z8;{FX7dh*4N0^iZorTVtA8TxQiP zPxLctf;t)eRh>f2dPYKfnm|rRSh|=y;ekgh^Czb22Aqa#O_q-lc@*Nr(J?hd%cL2^ z!3#_)zB?3=ZX?}UE2)j;m3?g=CT*u}4|Z4C^Nn%SD>8O7a9wd0ml|=_^cqiYZsnFa zGsc;ge}y&6w0-XuZSAlr9iA8$k5q;Xj@J*JL?=@A~JIBB0}z_jq>MxZ@5k zKHRme3({4cwVkzjQhI8*lcFmpF z`5f)+Cu1w)cJ(pwKXZqx{?7`_RCu|(qK1C&uXKhTmJUMyrr2Fhe$7kE3k>3TSg~0C z)*P^BJ+bD9=XTbP@3k>4hlt%1=@6MPxoq{itY6+C)Nj?#t`#rTH562#nWzL40z&MSYnyZ*bIHIjcp9~t2jqrVn? z7*DG^)H}?tB~PRlW&TCZN*KSaES#+bJHmVlul}qk+@XetO}-@EB;d)QBxEIwM&Lvo z9&WR1y{D5NpA{df4_o!AuDIho3jvQ>9NSuTxSG$Vi!2&(=Kb z%m3+3h_#}YDggM?|EEL40N?@fA0GgKHx~dLS^$7>CIFDSC7bul0|3K-lB|@D@6vIg zUn1SS;ojNP>S$%fVW z#12W5G<6LP^A;bT0=v(A6_TS0O_j}`0llI>mpYs z_ua-5ci#0whKVQN93R15{6_uVehg4Euk`|D@RU&F{SH*#&b_LN&|;^jR96dZgv#CS zjYCRIa7~W#;;dUp88xc;#T&(d{&lIY9_ZlJxmt|7CR0e4B&^g^68QiSZd#nLHcs>g zS7F~b_R1Py-n&YkeK=^W0qjs;vv1&R%x^N~VhZK7c=%=jX0s9uVM^HrGpp7sx>pcCh@s?Z6#4M;F&Bb4;%rgn!{ zf8A<+pdy3t&4>~BPMQVT8(Bh?!P|%;7E&X5tp9B9S>+`~LOBWI1G-5TE-nD%z|%!fM@p4h zpy&YTiA5jH0fN--j+JLJl&y=>8M^-WBh06Hph_Bmq)hnJ9Jo$W1xY?3<(Td$9y&h@ zLyI>A7Uj)q!1d=o(O$7fGz3a0+e%2USHKaaL{jNM4IxH52p-CTpBMXn{hM`FxrUYq zfiMLrWWupqg8RT3`CNDDXsz!!0J6$t)iGv8(KC;Y9;IUoFD9)7%8!NnY>x{yAOj$1 zl*enoLs=*k$yF<~WO~?@Ex5eZYMd3e_+A1?#9QM&lZ z{nZrIA0_&Pp|6}qo~oG7bYColkn+j;a@zn~8eIv>StN0SNNisxsR^lt9(w$rEY)!& z&Z2=BiV=V?HAm1mUc_EHB;c13EL$Dz1{3s8RYMU_JV>^$-BUCXc}Y~P2(>>_T{=4| zr;;x=Jj&PFZK-Z@$U?TLtCh@0Wk%788QS`a9s^>)&l4_)!jBF!z?x>WdPh@dkfFwE z$D-dbEunIJQvc&JN@-8czeiE74>lv876np#%}Mq?GjP7h>OOr4Y+r)j%aT~v*f78% zs*@*io-x)#JiK~cbg#h@O3Wtj=;wDnJ(9L%q<#@qC;YBR4Uj3M@tAq6h=Nl zj}Kc^k;MMGCvNrIJ`feA2V!Qnu`=(v<({>QRQ)LXxjaqSTb_bM9jQ?}xP3P$4y zdJ&Hguo<4CMguj7`iXA`vv~Dx^NV6Qogq8Kia6rEf<76~-AggQzeYgdoxSM_yH&g) z1tN>@Dsma$cw%#P$cPTQeyniL_StUQkWxS1iqoCuWJx=2rD82ph;1o+f4Q=!6NzR4X;_uw4gVIY4sNl;4oxe8ivoKg;xvUI}qz9 zBn-}O1y^?Fw?vkh{z{7h@49C!w4!g)WjvYOHWe6mDI7aN-{}KP&?JePXlHSDcsuVmZ)WsJIzS%0ly19Px0i8coNv2edS{PU& zD#d8ZR81uNj+uWp{SnNnW@!2&aTmIwpI05o8OInrji(Tih8cjufvgxpM3|ZZsufM# zBXGbg7L~Nw25dZ_5L&aGwoM5IZXDGKUBo-8i7I@JpD{Nu_;+bP z1LeMlFIEBMPZnXbBsSEj_ddcv$5&_Ta)KB^6&mp|!ai=~%E{RiA zRzaI#eU{m?&q_93W_ihh)8d7qiMNtfpb;KW(il!6*g0J)YO%MfmUj1KEGWd_37@gF z0){+%i1gF@z%xkj-3CgSL&kKMNvxSCrX;Iu3`#~}r`c~7(OqZJ0T!>3BP8IqH_p>R z^aW?{c(hNmDy-+7q)H#AEO}PY$6$vt*biXBhDJ5go96o1?rJ*i4luEw z+1@@HhNI{O=?sP`vX&^zm9YAhT-Uw1g?OXC&lnad8Jcw?e*lN8tlO4d+sh(Ald-I#3V~!(cg{ct*V$oRngnx zYRZ4PKeT-UzT_DC6-9Y&YAMSWcXS1rk5M{^UL;2|zO~Y0Oyww{{A#J1Kt5gR44=^? zHUTF_`s;HhfeA$13maC<&?UvjN2M6jg7pmXhgg>N@wfqW3`vqc6_)xKow0U17W#ap z>BWDLE)v2E;UaY5ykrWj2q8brVmpV(9+YE-6}&vm)b0b!2Q( z*2G$j_@XI6^e^fzemCl0O84NV0|z}JTF<#wPFGt(BD@mmnUMIbP7uRMG+9a?VPsYH zi(9=efpI5B@q4JK>iWB%MmTkII@l0{lX7*#0{Axyy5`;2JT0I^@iHyLCkpIKBTq#ymvf- z`F8j3hi6SeV;Vi19lWpHk*91Szt**Tc)UTO4LJ=8s+fsqgdh3!98T_0J$5s{m zLzi>LZbcPD^WZ<)q4l%^>qp5zXbiO&0ouH910(}11ARu&x~!j=O-!?x z_4u*R#x1xB5 z)LGbvSyDfym8ejr&kP42=_huk4v>h%qU#@di>!t`0m_e|V$5X8ZGtMxO%qw+^ce}J zR7Q@X#oE$F%9@Zc38vsts~1x$I*1mjywg@p!T893n;E9M#Oh*0{8hv_kS~t$M~8*| zI5w`3Ic8m^WHP2Al9g<^G7e7x#X{BpK@+^eCH00g2LPxS&*S2pJM-X|gxovU8z5YF8BTe=8|`)T%oTK?=Ax?>g1)*>0XI zh!MNc?f6a1S&^zU^0OmcXatpx+aOD9q_NMBXH zcteYxjadqLLaA*;z=0F%ITwkjWYRvnKSp`_v`zC4|8s8xj);mhFU&%L5p$g z6Gb>2Ck7x^HmYf%_7*9)k55sJdxB*~+HJ#F{Lh7+P0WPqx#-`?N3&Fy zv(XLt+zFVG)fCsEGrbrgfv}J-$dQbX@>(*#-aSkPZB&j}yL)8IJ#W?%NLlrjw2>QR z41!7O)ZUSHkO&M~>ynR`* zC9ixLKm}f!l8y{gra>shS9fuALo`A7dt30lG2M=3CGFEEP-tLRnZjT{`%KEwx*ffw z$0^Z0KU&@)-B3-OB80ui+jl%7qhA){r8W9;KqAU7Q z?VZ3n$;9mHU4cCKsu!D)cv;c8$s!r)k!JsxYs> zjXq?W?icPuYfbp1)gMK0R2nHR&ME_>X0#i=9`X@cogiA`WdOs*GFhiRg-WCukahJZ`Gbvp(q+~_daG~-4x$Vh$qC1YrDguY}qe@6a_T#V=F8@ zaY>$D&|8LQ^vC;Gz8)24=-#MZ&~=YXzL4>m%^BwHM)Y6;jIX1JAWsrV)5wNd)JnD2 zh8ls-SoX-?^oPqd$dWS!f@J)>hn~zys&QRPHT?P6VNWm)dGl5MkK<_NFS?oanE#1%b;-?SB3mE!p#F zN}IYu&H@e6nqFdGirCy(XPhKORot46u<(Dj=kL;y>a?#k<7|pZ)BKetCs~(txpe9P zVTkf550T3!C*tii8ra7}Q1xcmCxM!aE30+VNk)sPpG`Xdh$~bcQIPvjDY`03l!@FA zyWUO=jFjxOBwZqyQ@Tjj2`6-@YD(6g_&wZLvL0xd5i(|iA4{jhLp>cfO+LOkPD?xW zFf~GCUm#eCk-Wga{%ww)xPCPTIvfxgZ`XpFJR6(dK1Tx~H9<{M^oOV5hdsHTk|-O3 z<=Qr{&f6zWf+S^C;lL&(TUTOI37l_cJ2ztM4}pO|5>Hyi!o3`rA&sMz17xm^rFhr? z1PJ|vWnG5|umY3?EFBao56^gD$)ox(G5Wu5iZ3`_G zk=etx_Ld{J%f#-kFSURUKR9(6cOtuLjYFYc#{d}*vB z+MHiwifwGWzj-n1nhk&Hr>s#<Gs|L5YMDC2lcs z=HAVZ*-Cb+T*KEN9M(@hv7?25#+~?6a~Me?m#OF1hO~~G`}I^l>aqqan1Q2ov-6P{Ax`Rtqy`vLw?J{f7zmykPi9Cn zezwzl812$SV`ZB+y% ziUb`Z$y|1Nw2n|mk|@tV-yHer()W_EZ*k7}?Ec})!quU>z$>XfvJ@3{`q_(lPO*WOXZdlKg=>hcgv&E? zIM7vxXb4ydmxVU4V|#bj4}6Z3$Q_orEP?Kycg~AHina%H6&DW|$5amT;|JUY^qhBJ zeorExDe0q+_GBPd!tunf!vsTz7I~}3CRHZr;laFhC#!b4XVrm|RLgBAalcOw^Nb%q z5&h-zf9|(FtC~69aX9414`aSk?OV+D!dDz_b8c+2lKyGXdfNT@z?2s6<(D~E0(>?s z<4eV~@!{IH@iFZ?mpBy(HqwrROVbSVZvhav5_eQU9${|gbW8AN^I8Y)!qrIl58xm6 ziy-T(V~Ks%z5UL__Gdz((Rtw^gu}d5vO|KdSIKn$ug0}yECTL>>r^G%-KxA`x!e#^ z=hnIZ47A}xS5v&*uBPAN`i>N@&v?xr!SR$Wjc~>h@cQ%{$38j)U>yvV5bJw~0?aj(DH01FS4>`1Ud@sWk zO27rtW!x=P`k|0pomO2fwxx2TxmUqS`I^&Ict+ysA|ymQnCwBE+mr84xPsa0%^72X zkS1aN>bFj=^DqtnM^x`}USRSLwm5d{Z1tX>RVZhh0U#`DS!Wj{tJd(p-T8^;)_J`z zpFX~zQAVToCVs+jY;63XTqyQEU(a=JKkMM5W-NRBglo^w5&Da=c0XsnO`sDKQs8jV zN>5P1{g2|yjS>tQNbxycMJ#+gI;(oFXu7KH(Lw|g@3;1ok=_7N;bj8`o%z{U z5;@|<5tPuGwWbT$pS_FY7mPYgE^}3GAqC$+XXGos9xoTb+E(Bzy&xl={&$LC-BQki zFTK}B7+?{U@Dr$;67tdhYDC(Oq)Kq7i+eBI-LsUXG0WyaZnY|RtaecM%`^2?Ww1&K z+-=O9T@7>lSXo41P(R|&GY*(j(V0lDNZw!{tr9TuLk~rlDxw-Q*q>q zeI1rh4W1lAzVC7aH`97^B=bzJ+0b?AX=OsiwITRgc{nXvKm#a@W>Fr&y%;*OO zbgdo-r83usKQ}$}XzkQa)*ZL+3p~A;l@I2Nc5tgX$TH{SO0Ut))OJ5C?a(S%U&@$U zt{lr}afDy`!({8?VehGbf=}M$j_N2eM|{Ff$H=EK_<)sK_LO)s;Xt<+oj% z1(S6*ghH)~3NbGS0`eb^)n5+!=Uz8zeINj?J-ff7%DFp{+;PsRbbXAF+B-n_P92#B z!)+Mdx=#ikd{%?B{p(le?+RYdVF}CI9}r_5Ff37bsgM-sc7S5|uW0BQ!4N^_QK5)| z0vA6c8bK5#FOS#n6%>Gp1WOD1AD>evr-hI}-b5d}%Gi{cRBIisXcT&qTem;z&i-E! zKmTqjiKm}&SIaFfIcv?{-$gHaQ}3qcQ*va}J|*dgE3+t8%O#V$XG{MK)x%~Ar5P?U zmrM=Gsn!W&dpp!%K##oj#w5GESNe{Dz-#KsTK~WML|?D6BY@f#)M(O+zOO(L;EsI# zJh*mu-NT_YTfP?R+IjI23$U`gXbR@)*H0KyCq(Hp!z;Ag=<6*enKP&>U6+;QXmGVg zc~4MgS>OrA0yjv0v~o8isq^DYtUrX@r1idBWL=0`cx(N#dHq``{i!A%z8}Uw)Du7s zmmus~y1r{)ToN!Q(dvxXsSVg|8c}pyxtRk`5p=i%!ux2ubqpcn z=0~h)t)CsG#ccwM5WVee^lT)tL6gU%W8v%Id(qqm+SfluKaxVxlMQhQq*(pzOD4{2 zsXR64_jb+Q6T}|K<8w3HdJS4YbkbEt&q4QpxKhnWLaM@;u(bb}p3YQzKkNxBUBcB! z;xj&XZ$EvP{*%MmwKrH3WI@%LhFLLXW9IvUOFb4{GLa^zK$4oW%YDr=M)ZFe@1SLEkh8^{&#A%dqkOqY-fex;iZXa z0nqWc65+XAhD-XvE8&E#kBPby(!`&@$~XP44Qt#y5fP{yXS+rcaASe4>h8e?slwl@ z-|kN5)zV*{=eurr81-UANu|kKnKVAHO-}xM^Cg@z7NC7Re4oD%C)T*Xt6Q1IPEWv^ zDi-kLv_YzEWv}xyM*!H;j3_yLRbnLIK*^>DLI8`uY#QN_o|$K;MN5)F3JjYM-cNY8 z>pCaI0G?lheHE@R&H_Z(KKG65RZW8y-Am$P15^a8&1b?dTWnA<{KQ7~c2y>v5m^&us34Y|V@ zlqhIsp`f`JEbox|0|`)Z{b+!&&Tz}`qKooBKBXjzG9XK_>T>k38vB+ms4`9`D2ys- z+`r*LRhvsz&pGi=ycyx?w1$#97qree=p(D?WhypXdK_^g_k{c1)e%p5wM><2@jW1) za#&TKUg}lEtEh$?Q%~OY&3T}W7T{>uZfCV;GsU-w)%~!BUMP5lfVjW#K0SV~%|prM zW163_u}&c#Q&B(Cua0~_ZspJ4e>6y>V$?r;fL|NuCYOso@(KO#A(ig1O5n8opA60j zE%(Y#=B6)4i^2qfILZ=r!ninMS9EE=AQ5`%{HG6)~7-;Y@W~m);U^4jBgV* zb&27D7vzTbLrA-?w-QXp93bRQ&wdoh=SZsNh<<4n-^UBPf8=3har!~-j<@$di23L1 zq=dM)7hLu5M^TEQd>J`E^2};oxh#rx75aKDH$BvvT9Is&K)-?znkYrHDH$LwL5@y24vK9_bRCZDHjQmHSo1COORCw6;Nc^>L$B&g=aKa z*P=OiqyAoAi`Sae;Gbbt-(uo?=(U+&uggSUY}(neK>a+PnZx?~inkAAKt2H)Wf9kZ zzd!(O?6__+7e3cxMQ+jxeaeOf=11XH^A0JO_srr!vcxXNs-+zM`c&=^dTsC2TDxEA zl99DxEvAq}V3eo?&TG9r+42yFs;kmQ$g3vq)OagA8NzI}T8RjEfdGgmO(4vpNy zT|dRvqUBD=T5iz50G=F@gX7HP_a>8}44iI)Yost5RB`3np-VL@Gt9;h@C z6GA5$FY4aAkmMz{{{pZ$+&)78X4Z;CvUKN>OT23*zwv-lti-RKXHcYyDJ_^o z6ZO~=1VRoay_R|qBLw_)7bvL2H0g~tLreO@^T!cBJt!fv*D|U>aAfEi@6*$4-7~+y zD(HU3<_>;PMT+yH=W@DGvvj=S-04X1T`z0GD&k%zJu5_gDhRZxRaS^+Hgg6PkFcs8 z*$+vnsQQVi6IQBI1)pj^@teE^;Ym}3=DScs9e;Jj@z48e5{I5T#awr1md>$K6$O!0I8 z{Rk%+=bKF4rYs5675%;e!XLt?(beOfFE>;=YwiX}BQQjKWCQV`2vuU0i{j_^+ zj?S^(#h_6Mygf)o6o3fY{pue!b%#m12af^}56VFfqenmZcXG?~e~wJA&(u^Waw`0A?6P-3` zmGW0Hkq}80#uvKUY8CBr@$X|qdtQ^VU@h{(PwT;WE^If~`g6|alt){+{baJ4&9oe- zK2B|Q^Ivpoe#^#S`H!@MaqCMF`pf5SC&~Qm=rac!B%?GT;%k>{*NeL#NP9K#2_hwO z-iESn_Pf$`!6>O{QBH$G;-CFRTw%_S`2qNJ1li1aS006dZ0K&lUlw-JHIBlzyE74h z!8l|^iJ%=K`F%wITBUr4^6Z4}MEUbtM@r7BHWIWQbT51_4lUg1Tst@YF3p=#C=_OY`xFQL zfnz*<-IavyUEj*^P6JD8W^!1yCScorz&X+8fkTRDOj9TmA79aAEH(f5WCM+dqz_!N(z2Yc$k256D`7 zokD-nLN;IloasUxE|xHTmudJK*|lVNJI{>hCrCl3u3*o1lYsE<%jghb^beRP;wlR7 zpAUOiD@Q)$Vj?dBR;1AV$qu*?!df~1wxi}5!qGU6ksnFloq5F%V@?-4$yNwQs0#{^ykl?EYK&=dPQZ8veX{Vob3^yttw8^cc{bu}|E*TaPekZu$QUxtSLP a;7#~yJh_ha>A&A^fRdb=Y>l)<=>Gxy=2LS3 From 0c76eae42f9083dd73a37d4178ee62b5d7b0fee4 Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:43:56 +1100 Subject: [PATCH 29/35] Removed useless bat file --- config.template.bat | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 config.template.bat diff --git a/config.template.bat b/config.template.bat deleted file mode 100644 index e69de29..0000000 From 2d1724346aea0a197e72f8874a3a0aafcf78d130 Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:08:21 +1100 Subject: [PATCH 30/35] New dashboards for Grafana PS stats --- Dashboards/PS1 Barnacle Dashboard.json | 1355 ++++++++++++++++++++++++ Dashboards/PS2 Barnacle Dashboard.json | 1355 ++++++++++++++++++++++++ 2 files changed, 2710 insertions(+) create mode 100644 Dashboards/PS1 Barnacle Dashboard.json create mode 100644 Dashboards/PS2 Barnacle Dashboard.json diff --git a/Dashboards/PS1 Barnacle Dashboard.json b/Dashboards/PS1 Barnacle Dashboard.json new file mode 100644 index 0000000..6564a64 --- /dev/null +++ b/Dashboards/PS1 Barnacle Dashboard.json @@ -0,0 +1,1355 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 3, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "res_y", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "frameHeight", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "res_x", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "frameWidth", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "targetBitrate", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "targetBitrate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "bitrate", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Bitrate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 11, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "encodeFps", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "encodeFPS", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 15 + }, + { + "color": "#EAB839", + "value": 50 + }, + { + "color": "#6ED0E0", + "value": 100 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "PixelStreaming2_WebRTC_Fps", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "WebRTC FPS", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "jitter", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Jitter", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "qp", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Quantization Parameter (QP)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "PixelStreaming2_Encoder_MinQuality", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "EncoderMinQuality", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 11, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "PixelStreaming2_Encoder_MaxQuality", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "EncoderMaxQuality", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "encodeTime", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "encodeTime", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "packetsLost", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Packets Lost", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 48 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "memory_physical", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Physical Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 48 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "memory_gpu", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "GPU Memory Usage", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "5s", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "PS2 Barnacle Dashboard", + "uid": "c9df3f76-9c17-4a1e-89a3-bd6851876b30", + "version": 34 +} \ No newline at end of file diff --git a/Dashboards/PS2 Barnacle Dashboard.json b/Dashboards/PS2 Barnacle Dashboard.json new file mode 100644 index 0000000..6564a64 --- /dev/null +++ b/Dashboards/PS2 Barnacle Dashboard.json @@ -0,0 +1,1355 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 3, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "res_y", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "frameHeight", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "res_x", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "frameWidth", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "targetBitrate", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "targetBitrate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "bitrate", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Bitrate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 11, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "encodeFps", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "encodeFPS", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 15 + }, + { + "color": "#EAB839", + "value": 50 + }, + { + "color": "#6ED0E0", + "value": 100 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "PixelStreaming2_WebRTC_Fps", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "WebRTC FPS", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "jitter", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Jitter", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "qp", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Quantization Parameter (QP)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "PixelStreaming2_Encoder_MinQuality", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "EncoderMinQuality", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 11, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "PixelStreaming2_Encoder_MaxQuality", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "EncoderMaxQuality", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "encodeTime", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "encodeTime", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "packetsLost", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Packets Lost", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 48 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "memory_physical", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Physical Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "beur6k8b9ni0we" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 12, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 48 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "memory_gpu", + "fullMetaSearch": false, + "includeNullMetadata": true, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "GPU Memory Usage", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "5s", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "PS2 Barnacle Dashboard", + "uid": "c9df3f76-9c17-4a1e-89a3-bd6851876b30", + "version": 34 +} \ No newline at end of file From 2cf1ffcd918894ef0883fd492c44be7d02b4567b Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:10:58 +1100 Subject: [PATCH 31/35] Resolving all Gemini review issues --- .../Private/BuccaneerCommonModule.cpp | 11 +++++++--- .../Private/BuccaneerSettings.cpp | 20 +++++++++---------- .../Private/BuccaneerEventsModule.cpp | 4 ++-- .../Private/BuccaneerStatsModule.cpp | 2 +- .../Public/Buccaneer4PixelStreamingSettings.h | 2 -- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index 43fd7af..fb6ce3e 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -56,7 +56,7 @@ void FBuccaneerCommonModule::SendMetrics(const FMetricsCollection& StatsCollecti const FString BuccaneerID = UBuccaneerSettings::CVarID.GetValueOnAnyThread(); TSharedPtr JsonObject = StatsCollection.ToJson(); - TSharedPtr JsonBuccaneerID = MakeShared((TEXT("%s"), *BuccaneerID)); + TSharedPtr JsonBuccaneerID = MakeShared(BuccaneerID); JsonObject->SetField("id", JsonBuccaneerID); // Check if there is any metadata to send @@ -82,7 +82,7 @@ void FBuccaneerCommonModule::SendMetrics(const FMetricsCollection& StatsCollecti void FBuccaneerCommonModule::SendEvent(TSharedPtr JsonObject) { const FString BuccaneerID = UBuccaneerSettings::CVarID.GetValueOnAnyThread(); - TSharedPtr JsonBuccaneerID = MakeShared((TEXT("%s"), *BuccaneerID)); + TSharedPtr JsonBuccaneerID = MakeShared(BuccaneerID); JsonObject->SetField("id", JsonBuccaneerID); // Only send events to server if we are not in JSON writing mode @@ -94,6 +94,9 @@ void FBuccaneerCommonModule::SendEvent(TSharedPtr JsonObject) void FBuccaneerCommonModule::WriteJSON(FString FileName, TSharedPtr JsonObject) { + static FCriticalSection JsonFileMutex; + FScopeLock Lock(&JsonFileMutex); + FString FilePath = FPaths::Combine(UBuccaneerSettings::CVarJSONOutputDirectory.GetValueOnAnyThread(), FileName); // This is how we turn the JSON object into a string @@ -185,6 +188,8 @@ void FBuccaneerCommonModule::SendHTTP(FString URL, TSharedPtr JsonO void FBuccaneerCommonModule::FormatMetadata(IConsoleVariable *Var) { // Additional Metadata + MetadataJson = MakeShared(); + TMap MetadataMap = UBuccaneerSettings::GetMetadata(); for (const TPair &Pair : MetadataMap) { @@ -193,7 +198,7 @@ void FBuccaneerCommonModule::FormatMetadata(IConsoleVariable *Var) continue; } - MetadataJson->SetField(*Pair.Key, MakeShared((TEXT("%s"), *Pair.Value))); + MetadataJson->SetField(*Pair.Key, MakeShared(Pair.Value)); } } diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp index 5828952..7c68c19 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp @@ -75,11 +75,11 @@ namespace Util } FString Key, Value; - Pair.Split(TEXT(":"), &Key, &Value); - if(!Key.IsEmpty() && Value.IsEmpty()) - { - Map.Add(Key, Value); - } + Pair.Split(TEXT(":"), &Key, &Value); + if(!Key.IsEmpty() && !Value.IsEmpty()) + { + Map.Add(Key, Value); + } } } @@ -187,11 +187,11 @@ TMap UBuccaneerSettings::GetMetadata() } FString Key, Value; - Pair.Split(TEXT(":"), &Key, &Value); - if(!Key.IsEmpty() && Value.IsEmpty()) - { - MetadataMap.Add(Key, Value); - } + Pair.Split(TEXT(":"), &Key, &Value); + if(!Key.IsEmpty() && !Value.IsEmpty()) + { + MetadataMap.Add(Key, Value); + } } } diff --git a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.cpp index ae469b3..d4fd99f 100644 --- a/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerEvents/Private/BuccaneerEventsModule.cpp @@ -26,8 +26,8 @@ void FBuccaneerEventsModule::EmitEvent(FString Level, FString Event) UE_LOGFMT(LogBuccaneerEvents, Verbose, "{0}: {1}", Level, Event); TSharedPtr JsonObject = MakeShareable(new FJsonObject()); - JsonObject->SetField("level", MakeShared((TEXT("%s"), *Level))); - JsonObject->SetField("message", MakeShared((TEXT("%s"), *Event))); + JsonObject->SetField("level", MakeShared(Level)); + JsonObject->SetField("message", MakeShared(Event)); IBuccaneerCommonModule::Get().SendEvent(JsonObject); } diff --git a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp index ab86a4e..9c8c522 100644 --- a/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerStats/Private/BuccaneerStatsModule.cpp @@ -92,7 +92,7 @@ void FBuccaneerStatsModule::ComputeUsedMemory() { FPlatformMemoryStats MemoryStats = FPlatformMemory::GetStats(); - const unsigned int BytesPerMB = (8u * 1024u * 1024u); + const unsigned int BytesPerMB = (1024u * 1024u); UsedVirtualMemory = static_cast(MemoryStats.UsedVirtual) / BytesPerMB; UsedPhysicalMemory = static_cast(MemoryStats.UsedPhysical) / BytesPerMB; diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h index aaab581..3370f68 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h @@ -25,8 +25,6 @@ class BUCCANEER4PIXELSTREAMING_API UBuccaneer4PixelStreamingSettings : public UD )) bool Enabled = true; - static TAutoConsoleVariable CVarReportingInterval; - // Begin UDeveloperSettings Interface virtual FName GetCategoryName() const override; From 8ce48c989ede97fd4b492b33b1e3ccc6b6bd4470 Mon Sep 17 00:00:00 2001 From: Michael Wallace <90592038+MWillWallT@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:21:53 +1100 Subject: [PATCH 32/35] Update Dashboards/PS1 Barnacle Dashboard.json Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- Dashboards/PS1 Barnacle Dashboard.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dashboards/PS1 Barnacle Dashboard.json b/Dashboards/PS1 Barnacle Dashboard.json index 6564a64..3095e2b 100644 --- a/Dashboards/PS1 Barnacle Dashboard.json +++ b/Dashboards/PS1 Barnacle Dashboard.json @@ -1349,7 +1349,7 @@ }, "timepicker": {}, "timezone": "browser", - "title": "PS2 Barnacle Dashboard", + "title": "PS1 Barnacle Dashboard", "uid": "c9df3f76-9c17-4a1e-89a3-bd6851876b30", "version": 34 } \ No newline at end of file From b3013f24b041fa3e7df3f3e32e5f9228e3056f47 Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:14:43 +1100 Subject: [PATCH 33/35] New filename arg + updating default filename New arg: -BuccaneerJSONOutputFile= Tested to ensure it works with or without .json file extension. Default output filename has been updated to BuccID_Unixtime. Unix time is based on application start time. Using -BuccaneerID= allows users to specify the BuccID in the default name output. Tested changes / args to ensure robustness. --- .../Private/BuccaneerCommonModule.cpp | 25 +++- .../Private/BuccaneerCommonModule.h | 1 + .../Private/BuccaneerSettings.cpp | 9 +- .../Public/BuccaneerSettings.h | 7 + json_to_csv.py | 129 ++++++++++++++++++ 5 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 json_to_csv.py diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index fb6ce3e..9072bc1 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -33,6 +33,29 @@ void FBuccaneerCommonModule::StartupModule() FormatMetadata(nullptr); + // Generate the JSON output filename once at startup if using JSON output + if (UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) + { + FString OutputFile = UBuccaneerSettings::CVarJSONOutputFile.GetValueOnAnyThread(); + if (OutputFile.IsEmpty()) + { + // Generate dynamic filename: __Stats.json + const FString BuccaneerID = UBuccaneerSettings::CVarID.GetValueOnAnyThread(); + int64 UnixTimestamp = FDateTime::UtcNow().ToUnixTimestamp(); + FString SanitizedID = BuccaneerID.Replace(TEXT(":"), TEXT("-")).Replace(TEXT("/"), TEXT("-")); + OutputFile = FString::Printf(TEXT("%s_%lld_Stats.json"), *SanitizedID, UnixTimestamp); + } + else + { + // Ensure .json extension is present (add if missing, don't duplicate if already there) + if (!OutputFile.EndsWith(TEXT(".json"), ESearchCase::IgnoreCase)) + { + OutputFile += TEXT(".json"); + } + } + CachedJSONOutputFileName = OutputFile; + } + bModuleReady = true; ReadyEvent.Broadcast(*this); } @@ -70,7 +93,7 @@ void FBuccaneerCommonModule::SendMetrics(const FMetricsCollection& StatsCollecti // Case: Sending stats to disk if (UBuccaneerSettings::CVarEnableJSONOutput.GetValueOnAnyThread()) { - WriteJSON(TEXT("stats.json"), JsonObject); + WriteJSON(CachedJSONOutputFileName, JsonObject); } // Case: Sending stats to Buccaneer server else if (UBuccaneerSettings::CVarURL.GetValueOnAnyThread() != "") diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h index 7a13a28..c046a4a 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.h @@ -28,4 +28,5 @@ class FBuccaneerCommonModule : public IBuccaneerCommonModule void FormatMetadata(IConsoleVariable* Var); TSharedPtr MetadataJson = MakeShareable(new FJsonObject()); + FString CachedJSONOutputFileName; }; diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp index 7c68c19..a137063 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerSettings.cpp @@ -95,7 +95,8 @@ static const TSet> GetCmdArg = { { "Buccaneer.Metadata", "Metadata" }, { "Buccaneer.ReportingInterval", "ReportingInterval" }, { "Buccaneer.EnableJSONOutput", "EnableJSONOutput" }, - { "Buccaneer.JSONOutputDirectory", "JSONOutputDirectory" } + { "Buccaneer.JSONOutputDirectory", "JSONOutputDirectory" }, + { "Buccaneer.JSONOutputFile", "JSONOutputFile" } }; // Map a legacy cvar to its new property @@ -153,6 +154,12 @@ TAutoConsoleVariable UBuccaneerSettings::CVarJSONOutputDirectory( TEXT("The directory to write JSON files to"), ECVF_Default); +TAutoConsoleVariable UBuccaneerSettings::CVarJSONOutputFile( + TEXT("Buccaneer.JSONOutputFile"), + TEXT(""), + TEXT("The filename for JSON output (default: __Stats.json)"), + ECVF_Default); + UBuccaneerSettings::FDelegates* UBuccaneerSettings::DelegateSingleton = nullptr; UBuccaneerSettings::FDelegates* UBuccaneerSettings::Delegates() diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h index bc521cc..e6588be 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h @@ -86,6 +86,13 @@ class BUCCANEERCOMMON_API UBuccaneerSettings : public UDeveloperSettings )) FString JSONOutputDirectory = FPaths::ProjectLogDir(); + static TAutoConsoleVariable CVarJSONOutputFile; + UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( + DisplayName = "JSON Output File", + ToolTip = "The filename for JSON output (default: __Stats.json)" + )) + FString JSONOutputFile; + // Begin UDeveloperSettings Interface virtual FName GetCategoryName() const override; diff --git a/json_to_csv.py b/json_to_csv.py new file mode 100644 index 0000000..545de47 --- /dev/null +++ b/json_to_csv.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +""" +JSON to CSV Converter for BuccLog Stats +Converts stats.json performance metrics to CSV format for time-series analysis. +""" + +import json +import csv +from datetime import datetime +from pathlib import Path + + +def convert_json_to_csv(json_file_path, csv_file_path=None): + """ + Convert stats.json to CSV format. + + Args: + json_file_path: Path to the input JSON file + csv_file_path: Path to the output CSV file (optional, defaults to same name with .csv extension) + """ + # Set default output path if not provided + if csv_file_path is None: + json_path = Path(json_file_path) + csv_file_path = json_path.parent / f"{json_path.stem}.csv" + + # Read the JSON file + print(f"Reading JSON file: {json_file_path}") + with open(json_file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + print(f"Found {len(data)} records") + + # Collect all unique metric names across all records + all_metrics = set() + for record in data: + if 'metrics' in record: + all_metrics.update(record['metrics'].keys()) + + # Sort metrics for consistent column order + metric_names = sorted(all_metrics) + + # Create CSV header + header = ['timestamp', 'timestamp_readable', 'id'] + metric_names + + # Prepare rows + rows = [] + for record in data: + # Base fields + timestamp = record.get('timestamp', '') + record_id = record.get('id', '') + + # Convert timestamp to readable format (assuming milliseconds since epoch) + timestamp_readable = '' + if timestamp: + try: + # Adjust divisor based on timestamp magnitude + # If timestamp is very large, it might be in microseconds or nanoseconds + if timestamp > 10**12: # Likely microseconds or nanoseconds + timestamp_seconds = timestamp / 10**6 # Try microseconds first + else: + timestamp_seconds = timestamp / 1000 # Milliseconds + timestamp_readable = datetime.fromtimestamp(timestamp_seconds).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + except (ValueError, OSError): + timestamp_readable = 'N/A' + + # Create row with base fields + row = { + 'timestamp': timestamp, + 'timestamp_readable': timestamp_readable, + 'id': record_id + } + + # Add metric values + metrics = record.get('metrics', {}) + for metric_name in metric_names: + if metric_name in metrics: + row[metric_name] = metrics[metric_name].get('value', '') + else: + row[metric_name] = '' # Empty value if metric not present in this record + + rows.append(row) + + # Write to CSV + print(f"Writing CSV file: {csv_file_path}") + with open(csv_file_path, 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=header) + writer.writeheader() + writer.writerows(rows) + + print(f"Successfully converted {len(rows)} records") + print(f"CSV file created: {csv_file_path}") + print(f"Columns: {len(header)} ({len(metric_names)} metrics)") + + return csv_file_path + + +def main(): + """Main entry point for the script.""" + import sys + + # Get input file from command line or use default + if len(sys.argv) > 1: + input_file = sys.argv[1] + else: + # Default to stats.json in the same directory as the script + script_dir = Path(__file__).parent + input_file = script_dir / "stats.json" + + # Get output file from command line (optional) + output_file = sys.argv[2] if len(sys.argv) > 2 else None + + # Check if input file exists + if not Path(input_file).exists(): + print(f"Error: Input file not found: {input_file}") + print("\nUsage: python json_to_csv.py [input_json_file] [output_csv_file]") + sys.exit(1) + + # Convert the file + try: + convert_json_to_csv(input_file, output_file) + except Exception as e: + print(f"Error during conversion: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() From 80c9ce397f330e05d2ca9b542edea96ceda32c71 Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:17:48 +1100 Subject: [PATCH 34/35] Changing default filename for json output Defaults to a shortened GUID. --- .../Private/BuccaneerCommonModule.cpp | 19 +++++++++++-------- .../Public/BuccaneerSettings.h | 6 +++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp index 9072bc1..b91cafd 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Private/BuccaneerCommonModule.cpp @@ -19,11 +19,16 @@ void FBuccaneerCommonModule::StartupModule() return; } - FString InstanceIDOverride; - // Try and parse a pixel streaming ID for users who don't want to pollute their command line by specifying two IDs - if (FParse::Value(FCommandLine::Get(), TEXT("PixelStreamingID="), InstanceIDOverride)) + // Auto-generate BuccaneerID if not provided + FString BuccaneerID = UBuccaneerSettings::CVarID.GetValueOnAnyThread(); + if (BuccaneerID.IsEmpty()) { - UBuccaneerSettings::CVarID->Set(*InstanceIDOverride, ECVF_SetByCommandline); + // Generate unique ID using short GUID + BuccaneerID = FGuid::NewGuid().ToString(EGuidFormats::Short); + + UBuccaneerSettings::CVarID->Set(*BuccaneerID, ECVF_SetByCommandline); + UE_LOGFMT(LogBuccaneerCommon, Warning, + "No BuccaneerID provided. Auto-generated: {0}", BuccaneerID); } if (UBuccaneerSettings::FDelegates *Delegates = UBuccaneerSettings::Delegates()) @@ -39,11 +44,9 @@ void FBuccaneerCommonModule::StartupModule() FString OutputFile = UBuccaneerSettings::CVarJSONOutputFile.GetValueOnAnyThread(); if (OutputFile.IsEmpty()) { - // Generate dynamic filename: __Stats.json - const FString BuccaneerID = UBuccaneerSettings::CVarID.GetValueOnAnyThread(); - int64 UnixTimestamp = FDateTime::UtcNow().ToUnixTimestamp(); + // Generate default filename: _Stats.json FString SanitizedID = BuccaneerID.Replace(TEXT(":"), TEXT("-")).Replace(TEXT("/"), TEXT("-")); - OutputFile = FString::Printf(TEXT("%s_%lld_Stats.json"), *SanitizedID, UnixTimestamp); + OutputFile = FString::Printf(TEXT("%s_Stats.json"), *SanitizedID); } else { diff --git a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h index e6588be..b077786 100644 --- a/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h +++ b/Plugins/Buccaneer/Source/BuccaneerCommon/Public/BuccaneerSettings.h @@ -37,9 +37,9 @@ class BUCCANEERCOMMON_API UBuccaneerSettings : public UDeveloperSettings static TAutoConsoleVariable CVarID; UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( DisplayName = "ID", - ToolTip = "ID to identify this instance. Defaults to a new GUID" + ToolTip = "ID to identify this instance. Auto-generates a short GUID if not provided" )) - FString ID = FGuid::NewGuid().ToString(); + FString ID; static TAutoConsoleVariable CVarEnableStats; UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( @@ -89,7 +89,7 @@ class BUCCANEERCOMMON_API UBuccaneerSettings : public UDeveloperSettings static TAutoConsoleVariable CVarJSONOutputFile; UPROPERTY(config, EditAnywhere, Category = "Buccaneer", meta = ( DisplayName = "JSON Output File", - ToolTip = "The filename for JSON output (default: __Stats.json)" + ToolTip = "The filename for JSON output (default: _Stats.json)" )) FString JSONOutputFile; From 18dcc7240e15e95abefa8a9ffb8d440caffc0d02 Mon Sep 17 00:00:00 2001 From: MWillWallT <90592038+MWillWallT@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:30:33 +1100 Subject: [PATCH 35/35] Fixing compilation error with Bucc PS1 --- .../Public/Buccaneer4PixelStreamingSettings.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h index 3370f68..aaab581 100644 --- a/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h +++ b/Plugins/Buccaneer4PixelStreaming/Source/Buccaneer4PixelStreaming/Public/Buccaneer4PixelStreamingSettings.h @@ -25,6 +25,8 @@ class BUCCANEER4PIXELSTREAMING_API UBuccaneer4PixelStreamingSettings : public UD )) bool Enabled = true; + static TAutoConsoleVariable CVarReportingInterval; + // Begin UDeveloperSettings Interface virtual FName GetCategoryName() const override;