// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.IO; using System.Collections.Generic; using AutomationTool; using UnrealBuildTool; using System.Diagnostics; using System.Diagnostics.Eventing.Reader; using Gauntlet.Utils; namespace Gauntlet { public class PGOConfig : UnrealTestConfiguration, IAutoParamNotifiable { /// /// Output directory to write the resulting profile data to. /// [AutoParam("")] public string ProfileOutputDirectory; /// /// Directory to save periodic screenshots to whilst the PGO run is in progress. /// [AutoParam("")] public string ScreenshotDirectory; [AutoParam("")] public string PGOAccountSandbox; [AutoParam("")] public string PgcFilenamePrefix; public virtual void ParametersWereApplied(string[] Params) { if (String.IsNullOrEmpty(ProfileOutputDirectory)) { throw new AutomationException("ProfileOutputDirectory option must be specified for profiling data"); } } } public class PGONode : UnrealTestNode where TConfigClass : PGOConfig, new() { protected string LocalOutputDirectory; internal IPGOPlatform PGOPlatform; bool ProcessPGODataFailed = false; public PGONode(UnrealTestContext InContext) : base(InContext) { } public override TConfigClass GetConfiguration() { if (CachedConfig != null) { return CachedConfig as TConfigClass; } var Config = CachedConfig = base.GetConfiguration(); // Set max duration to 1 hour Config.MaxDuration = 60 * 60; // Get output filenames LocalOutputDirectory = Path.GetFullPath(Config.ProfileOutputDirectory); // Create the local profiling data directory if needed if (!Directory.Exists(LocalOutputDirectory)) { Directory.CreateDirectory(LocalOutputDirectory); } ScreenshotDirectory = Config.ScreenshotDirectory; if (!String.IsNullOrEmpty(ScreenshotDirectory)) { if (Directory.Exists(ScreenshotDirectory)) { Directory.Delete(ScreenshotDirectory, true); } Directory.CreateDirectory(ScreenshotDirectory); } PGOPlatform = PGOPlatformManager.GetPGOPlatform(Context.GetRoleContext(UnrealTargetRole.Client).Platform); PGOPlatform.ApplyConfiguration(Config); return Config as TConfigClass; } private DateTime ScreenshotStartTime = DateTime.UtcNow; private DateTime ScreenshotTime = DateTime.MinValue; private TimeSpan ScreenshotInterval = TimeSpan.FromSeconds(30); private const float ScreenshotScale = 1.0f / 3.0f; private const int ScreenshotQuality = 30; protected string ScreenshotDirectory; protected bool ResultsGathered = false; protected override TestResult GetUnrealTestResult() { if (ProcessPGODataFailed) { return TestResult.Failed; } return base.GetUnrealTestResult(); } public override void TickTest() { base.TickTest(); if (GetTestStatus() == TestStatus.Complete || ResultsGathered) { if (!ResultsGathered) { ResultsGathered = true; try { // Gather results and merge PGO data Log.Info("Gathering profiling results to {0}", TestInstance.ClientApps[0].ArtifactPath); PGOPlatform.GatherResults(TestInstance.ClientApps[0].ArtifactPath); } catch (Exception Ex) { ProcessPGODataFailed = true; Log.Error("Error getting PGO results: {0}", Ex); } } return; } // Handle device screenshot update TimeSpan Delta = DateTime.Now - ScreenshotTime; ITargetDevice Device = TestInstance.ClientApps[0].Device; string ImageFilename; if (!String.IsNullOrEmpty(ScreenshotDirectory) && Delta >= ScreenshotInterval && Device != null && PGOPlatform.TakeScreenshot(Device, ScreenshotDirectory, out ImageFilename)) { ScreenshotTime = DateTime.Now; if (!File.Exists(Path.Combine(ScreenshotDirectory, ImageFilename))) { Log.Info("PGOPlatform.TakeScreenshot returned true, but output image {0} does not exist! skipping", ImageFilename); } else if(new FileInfo(Path.Combine(ScreenshotDirectory, ImageFilename)).Length <= 0) { Log.Info("PGOPlatform.TakeScreenshot returned true, but output image {0} is size 0! skipping", ImageFilename); } else { try { TimeSpan ImageTimestamp = DateTime.UtcNow - ScreenshotStartTime; string ImageOutputPath = Path.Combine(ScreenshotDirectory, ImageTimestamp.ToString().Replace(':', '-') + ".jpg"); ImageUtils.ResaveImageAsJpgWithScaleAndQuality(Path.Combine(ScreenshotDirectory, ImageFilename), ImageOutputPath, ScreenshotScale, ScreenshotQuality); // Delete the temporary image file try { File.Delete(Path.Combine(ScreenshotDirectory, ImageFilename)); } catch (Exception e) { Log.Warning("Got Exception Deleting temp iamge: {0}", e.ToString()); } } catch (Exception e) { Log.Info("Got Exception Renaming PGO image {0}: {1}", ImageFilename, e.ToString()); TimeSpan ImageTimestamp = DateTime.UtcNow - ScreenshotStartTime; string CopyFileName = Path.Combine(ScreenshotDirectory, ImageTimestamp.ToString().Replace(':', '-') + ".bmp"); Log.Info("Copying unconverted image {0} to {1}", ImageFilename, CopyFileName); try { File.Copy(Path.Combine(ScreenshotDirectory, ImageFilename), CopyFileName); } catch (Exception e2) { Log.Warning("Got Exception copying un-converted screenshot image: {0}", e2.ToString()); } } } } } } public interface IPGOCustomReplayPlatform { void ConfigureClientForReplay( PGOConfig Config, UnrealTestRole ClientRole, string LocalReplayFileName ); } public class BasicReplayPGOConfig : PGOConfig { [AutoParam("")] public string LocalReplay; [AutoParam("")] public string AdditionalCommandLine; public override void ParametersWereApplied(string[] Params) { base.ParametersWereApplied(Params); // Check that replay file exists if specified if (string.IsNullOrEmpty(LocalReplay) || !File.Exists(LocalReplay)) { throw new AutomationException("LocalReplay option must be specified with absolute path to existing replay file"); } } } /// /// PGO Profiling Node that runs a replay & gathers the results. Requires an existing build compiled with -PGOProfile -Configuration=Shipping /// e.g. RunUnreal -Test=BasicReplayPGONode -project=MyProject -platform=Win64 -configuration=Shipping -build=MyProject\Saved\StagedBuilds\Windows -LocalReplay=MyProject\Replays\PGO.replay -ProfileOutputDirectory=MyProject\Platforms\Windows\Build\PGO -MaxDuration=7200 /// public class BasicReplayPGONode : PGONode { public BasicReplayPGONode(UnrealTestContext InContext) : base(InContext) { } public override BasicReplayPGOConfig GetConfiguration() { if (CachedConfig != null) { return CachedConfig; } var Config = base.GetConfiguration(); // create a client that runs the given replay & then exits when it's finished UnrealTestRole ClientRole = Config.RequireRole(UnrealTargetRole.Client); ClientRole.CommandLine += $" -Deterministic -ExitAfterReplay -NoCheckpointHangDetector"; ClientRole.CommandLine += $" {Config.AdditionalCommandLine}"; if (PGOPlatform is IPGOCustomReplayPlatform PGOCustomReplayPlatform) { // the platform needs bespoke replay handling PGOCustomReplayPlatform.ConfigureClientForReplay(Config, ClientRole, Config.LocalReplay); } else { // normal replay handling - copy to the demos folder and then launch from command line string ReplayName = Path.GetFileNameWithoutExtension(Config.LocalReplay); ClientRole.FilesToCopy.Add(new UnrealFileToCopy(Config.LocalReplay, EIntendedBaseCopyDirectory.Demos, Path.GetFileName(Config.LocalReplay))); ClientRole.CommandLine += $" -Replay=\"{ReplayName}\" "; } return Config; } } internal interface IPGOPlatform { void ApplyConfiguration(PGOConfig Config); void GatherResults(string ArtifactPath); bool TakeScreenshot(ITargetDevice Device, string ScreenshotDirectory, out string ImageFilename); UnrealTargetPlatform GetPlatform(); } /// /// PGO platform manager /// internal abstract class PGOPlatformManager { public static IPGOPlatform GetPGOPlatform(UnrealTargetPlatform Platform) { Type PGOPlatformType; if (!PGOPlatforms.TryGetValue(Platform, out PGOPlatformType)) { throw new AutomationException("Invalid PGO Platform: {0}", Platform); } return Activator.CreateInstance(PGOPlatformType) as IPGOPlatform; } protected static void RegisterPGOPlatform(UnrealTargetPlatform Platform, Type PGOPlatformType) { PGOPlatforms[Platform] = PGOPlatformType; } static Dictionary PGOPlatforms = new Dictionary(); static PGOPlatformManager() { IEnumerable DiscoveredPGOPlatforms = Utils.InterfaceHelpers.FindImplementations(); foreach (IPGOPlatform PGOPlatform in DiscoveredPGOPlatforms) { PGOPlatforms[PGOPlatform.GetPlatform()] = PGOPlatform.GetType(); } } } }