// Copyright Epic Games, Inc. All Rights Reserved. #include "ThreadTimingTrack.h" #include "CborReader.h" #include "Fonts/FontMeasure.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "HAL/PlatformApplicationMisc.h" #include "Serialization/MemoryReader.h" #include "Styling/SlateBrush.h" // Insights #include "Insights/Common/PaintUtils.h" #include "Insights/Common/TimeUtils.h" #include "Insights/InsightsManager.h" #include "Insights/ITimingViewSession.h" #include "Insights/TimingProfilerManager.h" #include "Insights/ViewModels/TimerNode.h" #include "Insights/ViewModels/ThreadTrackEvent.h" #include "Insights/ViewModels/TimingEventSearch.h" #include "Insights/ViewModels/TimingTrackViewport.h" #include "Insights/ViewModels/TimingViewDrawHelper.h" #include "Insights/ViewModels/TooltipDrawState.h" #include "Insights/Widgets/STimingView.h" #define LOCTEXT_NAMESPACE "ThreadTimingTrack" //////////////////////////////////////////////////////////////////////////////////////////////////// static void AppendMetadataToTooltip(FTooltipDrawState& Tooltip, TArrayView& Metadata) { FMemoryReaderView MemoryReader(Metadata); FCborReader CborReader(&MemoryReader, ECborEndianness::StandardCompliant); FCborContext Context; if (!CborReader.ReadNext(Context) || Context.MajorType() != ECborCode::Map) { return; } Tooltip.AddTitle(TEXT("Metadata:")); while (true) { // Read key if (!CborReader.ReadNext(Context) || !Context.IsString()) { break; } FString Key(Context.AsLength(), Context.AsCString()); Key += TEXT(":"); if (!CborReader.ReadNext(Context)) { break; } switch (Context.MajorType()) { case ECborCode::Int: case ECborCode::Uint: { uint64 Value = Context.AsUInt(); FString ValueStr = FString::Printf(TEXT("%llu"), Value); Tooltip.AddNameValueTextLine(Key, ValueStr); continue; } case ECborCode::TextString: { FString Value = Context.AsString(); Tooltip.AddNameValueTextLine(Key, Value); continue; } case ECborCode::ByteString: { FAnsiStringView Value(Context.AsCString(), Context.AsLength()); FString ValueStr(Value); Tooltip.AddNameValueTextLine(Key, ValueStr); continue; } } if (Context.RawCode() == (ECborCode::Prim | ECborCode::Value_4Bytes)) { float Value = Context.AsFloat(); FString ValueStr = FString::Printf(TEXT("%f"), Value); Tooltip.AddNameValueTextLine(Key, ValueStr); continue; } if (Context.RawCode() == (ECborCode::Prim | ECborCode::Value_8Bytes)) { double Value = Context.AsDouble(); FString ValueStr = FString::Printf(TEXT("%g"), Value); Tooltip.AddNameValueTextLine(Key, ValueStr); continue; } if (Context.IsFiniteContainer()) { CborReader.SkipContainer(ECborCode::Array); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// static void AppendMetadataToString(FString& Str, TArrayView& Metadata) { FMemoryReaderView MemoryReader(Metadata); FCborReader CborReader(&MemoryReader, ECborEndianness::StandardCompliant); FCborContext Context; if (!CborReader.ReadNext(Context) || Context.MajorType() != ECborCode::Map) { return; } bool bFirst = true; while (true) { // Read key if (!CborReader.ReadNext(Context) || !Context.IsString()) { break; } if (bFirst) { bFirst = false; Str += TEXT(" - "); } else { Str += TEXT(", "); } // FString Key(Context.AsLength(), Context.AsCString()); // Str += Key; // Str += TEXT(":"); if (!CborReader.ReadNext(Context)) { break; } switch (Context.MajorType()) { case ECborCode::Int: case ECborCode::Uint: { uint64 Value = Context.AsUInt(); Str += FString::Printf(TEXT("%llu"), Value); continue; } case ECborCode::TextString: { Str += Context.AsString(); continue; } case ECborCode::ByteString: { Str.AppendChars(Context.AsCString(), Context.AsLength()); continue; } } if (Context.RawCode() == (ECborCode::Prim | ECborCode::Value_4Bytes)) { float Value = Context.AsFloat(); Str += FString::Printf(TEXT("%f"), Value); continue; } if (Context.RawCode() == (ECborCode::Prim | ECborCode::Value_8Bytes)) { double Value = Context.AsDouble(); Str += FString::Printf(TEXT("%g"), Value); continue; } if (Context.IsFiniteContainer()) { CborReader.SkipContainer(ECborCode::Array); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// static void AddTimingEventToBuilder(ITimingEventsTrackDrawStateBuilder& Builder, double EventStartTime, double EventEndTime, uint32 EventDepth, uint32 TimerIndex, const Trace::FTimingProfilerTimer* Timer) { // const uint32 EventColor = FTimingEvent::ComputeEventColor(Timer->Id); const uint32 EventColor = FTimingEvent::ComputeEventColor(Timer->Name); Builder.AddEvent(EventStartTime, EventEndTime, EventDepth, EventColor, [TimerIndex, Timer, EventStartTime, EventEndTime](float Width) { FString EventName = Timer->Name; if (Width > EventName.Len() * 4.0f + 32.0f) { // EventName = TEXT("*") + EventName; // for debugging const double Duration = EventEndTime - EventStartTime; FTimingEventsTrackDrawStateBuilder::AppendDurationToEventName(EventName, Duration); if (int32(TimerIndex) < 0) // has metadata? { // EventName = TEXT("!") + EventName; // for debugging TSharedPtr Session = FInsightsManager::Get()->GetSession(); check(Session.IsValid()); // Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::ITimingProfilerProvider& TimingProfilerProvider = *Trace::ReadTimingProfilerProvider(*Session.Get()); const Trace::ITimingProfilerTimerReader* TimerReader; TimingProfilerProvider.ReadTimers([&TimerReader](const Trace::ITimingProfilerTimerReader& Out) { TimerReader = &Out; }); TArrayView Metadata = TimerReader->GetMetadata(TimerIndex); if (Metadata.Num() > 0) { AppendMetadataToString(EventName, Metadata); } } } return EventName; }); } //////////////////////////////////////////////////////////////////////////////////////////////////// // FThreadTimingSharedState //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedPtr FThreadTimingSharedState::GetCpuTrack(uint32 InThreadId) { TSharedPtr* const TrackPtrPtr = CpuTracks.Find(InThreadId); return TrackPtrPtr ? *TrackPtrPtr : nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool FThreadTimingSharedState::IsGpuTrackVisible() const { return (GpuTrack != nullptr && GpuTrack->IsVisible()) || (Gpu2Track != nullptr && Gpu2Track->IsVisible()); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool FThreadTimingSharedState::IsCpuTrackVisible(uint32 InThreadId) const { const TSharedPtr* const TrackPtrPtr = CpuTracks.Find(InThreadId); return TrackPtrPtr && (*TrackPtrPtr)->IsVisible(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingSharedState::GetVisibleCpuThreads(TSet& OutSet) const { OutSet.Reset(); for (const auto& KV: CpuTracks) { const FCpuTimingTrack& Track = *KV.Value; if (Track.IsVisible()) { OutSet.Add(KV.Key); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingSharedState::OnBeginSession(Insights::ITimingViewSession& InSession) { if (&InSession != TimingView) { return; } if (TimingView && TimingView->IsAssetLoadingModeEnabled()) { bShowHideAllGpuTracks = false; bShowHideAllCpuTracks = false; } else { bShowHideAllGpuTracks = true; bShowHideAllCpuTracks = true; } GpuTrack = nullptr; Gpu2Track = nullptr; CpuTracks.Reset(); ThreadGroups.Reset(); TimingProfilerTimelineCount = 0; LoadTimeProfilerTimelineCount = 0; } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingSharedState::OnEndSession(Insights::ITimingViewSession& InSession) { if (&InSession != TimingView) { return; } bShowHideAllGpuTracks = false; bShowHideAllCpuTracks = false; GpuTrack = nullptr; Gpu2Track = nullptr; CpuTracks.Reset(); ThreadGroups.Reset(); TimingProfilerTimelineCount = 0; LoadTimeProfilerTimelineCount = 0; } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingSharedState::Tick(Insights::ITimingViewSession& InSession, const Trace::IAnalysisSession& InAnalysisSession) { if (&InSession != TimingView) { return; } const Trace::ITimingProfilerProvider* TimingProfilerProvider = Trace::ReadTimingProfilerProvider(InAnalysisSession); const Trace::ILoadTimeProfilerProvider* LoadTimeProfilerProvider = Trace::ReadLoadTimeProfilerProvider(InAnalysisSession); if (TimingProfilerProvider) { Trace::FAnalysisSessionReadScope SessionReadScope(InAnalysisSession); const uint64 CurrentTimingProfilerTimelineCount = TimingProfilerProvider->GetTimelineCount(); const uint64 CurrentLoadTimeProfilerTimelineCount = (LoadTimeProfilerProvider) ? LoadTimeProfilerProvider->GetTimelineCount() : 0; if (CurrentTimingProfilerTimelineCount != TimingProfilerTimelineCount || CurrentLoadTimeProfilerTimelineCount != LoadTimeProfilerTimelineCount) { TimingProfilerTimelineCount = CurrentTimingProfilerTimelineCount; LoadTimeProfilerTimelineCount = CurrentLoadTimeProfilerTimelineCount; // Check if we have a GPU track. if (!GpuTrack.IsValid()) { uint32 GpuTimelineIndex; if (TimingProfilerProvider->GetGpuTimelineIndex(GpuTimelineIndex)) { GpuTrack = MakeShared(*this, TEXT("GPU"), nullptr, GpuTimelineIndex, 0); GpuTrack->SetOrder(FTimingTrackOrder::Gpu); GpuTrack->SetVisibilityFlag(bShowHideAllGpuTracks); InSession.AddScrollableTrack(GpuTrack); } } if (!Gpu2Track.IsValid()) { uint32 GpuTimelineIndex; if (TimingProfilerProvider->GetGpu2TimelineIndex(GpuTimelineIndex)) { Gpu2Track = MakeShared(*this, TEXT("GPU2"), nullptr, GpuTimelineIndex, 0); Gpu2Track->SetOrder(FTimingTrackOrder::Gpu); Gpu2Track->SetVisibilityFlag(bShowHideAllGpuTracks); InSession.AddScrollableTrack(Gpu2Track); } } bool bTracksOrderChanged = false; int32 Order = FTimingTrackOrder::Cpu; // Iterate through threads. const Trace::IThreadProvider& ThreadProvider = Trace::ReadThreadProvider(InAnalysisSession); ThreadProvider.EnumerateThreads([this, &InSession, &bTracksOrderChanged, &Order, TimingProfilerProvider, LoadTimeProfilerProvider](const Trace::FThreadInfo& ThreadInfo) { // Check if this thread is part of a group? bool bIsGroupVisible = bShowHideAllCpuTracks; const TCHAR* const GroupName = ThreadInfo.GroupName ? ThreadInfo.GroupName : ThreadInfo.Name; if (GroupName != nullptr) { if (!ThreadGroups.Contains(GroupName)) { // UE_LOG(TimingProfiler, Log, TEXT("New CPU Thread Group (%d) : \"%s\""), ThreadGroups.Num() + 1, GroupName); ThreadGroups.Add(GroupName, {GroupName, bIsGroupVisible, 0, Order}); } else { FThreadGroup& ThreadGroup = ThreadGroups[GroupName]; bIsGroupVisible = ThreadGroup.bIsVisible; ThreadGroup.Order = Order; } } // Check if there is an available Asset Loading track for this thread. bool bIsLoadingThread = false; uint32 LoadingTimelineIndex; if (LoadTimeProfilerProvider && LoadTimeProfilerProvider->GetCpuThreadTimelineIndex(ThreadInfo.Id, LoadingTimelineIndex)) { bIsLoadingThread = true; } // Check if there is an available CPU track for this thread. uint32 CpuTimelineIndex; if (TimingProfilerProvider->GetCpuThreadTimelineIndex(ThreadInfo.Id, CpuTimelineIndex)) { TSharedPtr* TrackPtrPtr = CpuTracks.Find(ThreadInfo.Id); if (TrackPtrPtr == nullptr) { FString TrackName(ThreadInfo.Name && *ThreadInfo.Name ? ThreadInfo.Name : FString::Printf(TEXT("Thread %u"), ThreadInfo.Id)); // Create new Timing Events track for the CPU thread. TSharedPtr Track = MakeShared(*this, TrackName, GroupName, CpuTimelineIndex, ThreadInfo.Id); Track->SetOrder(Order); CpuTracks.Add(ThreadInfo.Id, Track); FThreadGroup& ThreadGroup = ThreadGroups[GroupName]; ThreadGroup.NumTimelines++; if (TimingView && TimingView->IsAssetLoadingModeEnabled() && bIsLoadingThread) { Track->SetVisibilityFlag(true); ThreadGroup.bIsVisible = true; } else { Track->SetVisibilityFlag(bIsGroupVisible); } InSession.AddScrollableTrack(Track); } else { TSharedPtr Track = *TrackPtrPtr; if (Track->GetOrder() != Order) { Track->SetOrder(Order); bTracksOrderChanged = true; } } } constexpr int32 OrderIncrement = FTimingTrackOrder::GroupRange / 1000; // distribute max 1000 tracks in the order group range static_assert(OrderIncrement >= 1, "Order group range too small"); Order += OrderIncrement; }); if (bTracksOrderChanged) { InSession.InvalidateScrollableTracksOrder(); } } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingSharedState::ExtendFilterMenu(Insights::ITimingViewSession& InSession, FMenuBuilder& InOutMenuBuilder) { if (&InSession != TimingView) { return; } InOutMenuBuilder.BeginSection("ThreadProfiler", LOCTEXT("ThreadProfilerHeading", "Threads")); { // TODO: MenuBuilder.AddMenuEntry(Commands.ShowAllGpuTracks); InOutMenuBuilder.AddMenuEntry( LOCTEXT("ShowAllGpuTracks", "GPU Track - Y"), LOCTEXT("ShowAllGpuTracks_Tooltip", "Show/hide the GPU track"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &FThreadTimingSharedState::ShowHideAllGpuTracks), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &FThreadTimingSharedState::IsAllGpuTracksToggleOn)), NAME_None, //"QuickFilterSeparator", EUserInterfaceActionType::ToggleButton); // TODO: MenuBuilder.AddMenuEntry(Commands.ShowAllCpuTracks); InOutMenuBuilder.AddMenuEntry( LOCTEXT("ShowAllCpuTracks", "CPU Thread Tracks - U"), LOCTEXT("ShowAllCpuTracks_Tooltip", "Show/hide all CPU tracks (and all CPU thread groups)"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &FThreadTimingSharedState::ShowHideAllCpuTracks), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &FThreadTimingSharedState::IsAllCpuTracksToggleOn)), NAME_None, //"QuickFilterSeparator", EUserInterfaceActionType::ToggleButton); } InOutMenuBuilder.EndSection(); InOutMenuBuilder.BeginSection("ThreadGroups", LOCTEXT("ThreadGroupsHeading", "CPU Thread Groups")); CreateThreadGroupsMenu(InOutMenuBuilder); InOutMenuBuilder.EndSection(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingSharedState::CreateThreadGroupsMenu(FMenuBuilder& InOutMenuBuilder) { // Sort the list of thread groups. TArray SortedThreadGroups; SortedThreadGroups.Reserve(ThreadGroups.Num()); for (const auto& KV: ThreadGroups) { SortedThreadGroups.Add(&KV.Value); } Algo::SortBy(SortedThreadGroups, &FThreadGroup::GetOrder); for (const FThreadGroup* ThreadGroupPtr: SortedThreadGroups) { const FThreadGroup& ThreadGroup = *ThreadGroupPtr; if (ThreadGroup.NumTimelines > 0) { InOutMenuBuilder.AddMenuEntry( // FText::FromString(ThreadGroup.Name), FText::Format(LOCTEXT("ThreadGroupFmt", "{0} ({1})"), FText::FromString(ThreadGroup.Name), ThreadGroup.NumTimelines), TAttribute(), // no tooltip FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &FThreadTimingSharedState::ToggleTrackVisibilityByGroup_Execute, ThreadGroup.Name), FCanExecuteAction::CreateLambda([] { return true; }), FIsActionChecked::CreateSP(this, &FThreadTimingSharedState::ToggleTrackVisibilityByGroup_IsChecked, ThreadGroup.Name)), NAME_None, EUserInterfaceActionType::ToggleButton); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingSharedState::SetAllCpuTracksToggle(bool bOnOff) { bShowHideAllCpuTracks = bOnOff; for (const auto& KV: CpuTracks) { FCpuTimingTrack& Track = *KV.Value; Track.SetVisibilityFlag(bShowHideAllCpuTracks); } for (auto& KV: ThreadGroups) { KV.Value.bIsVisible = bShowHideAllCpuTracks; } if (TimingView) { TimingView->OnTrackVisibilityChanged(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingSharedState::SetAllGpuTracksToggle(bool bOnOff) { bShowHideAllGpuTracks = bOnOff; if (GpuTrack.IsValid()) { GpuTrack->SetVisibilityFlag(bShowHideAllGpuTracks); } if (Gpu2Track.IsValid()) { Gpu2Track->SetVisibilityFlag(bShowHideAllGpuTracks); } if (GpuTrack.IsValid() || Gpu2Track.IsValid()) { if (TimingView) { TimingView->OnTrackVisibilityChanged(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool FThreadTimingSharedState::ToggleTrackVisibilityByGroup_IsChecked(const TCHAR* InGroupName) const { if (ThreadGroups.Contains(InGroupName)) { const FThreadGroup& ThreadGroup = ThreadGroups[InGroupName]; return ThreadGroup.bIsVisible; } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingSharedState::ToggleTrackVisibilityByGroup_Execute(const TCHAR* InGroupName) { if (ThreadGroups.Contains(InGroupName)) { FThreadGroup& ThreadGroup = ThreadGroups[InGroupName]; ThreadGroup.bIsVisible = !ThreadGroup.bIsVisible; for (const auto& KV: CpuTracks) { FCpuTimingTrack& Track = *KV.Value; if (Track.GetGroupName() == InGroupName) { Track.SetVisibilityFlag(ThreadGroup.bIsVisible); } } if (TimingView) { TimingView->OnTrackVisibilityChanged(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// // FThreadTimingTrack //////////////////////////////////////////////////////////////////////////////////////////////////// INSIGHTS_IMPLEMENT_RTTI(FThreadTimingTrack) //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingTrack::BuildDrawState(ITimingEventsTrackDrawStateBuilder& Builder, const ITimingTrackUpdateContext& Context) { TSharedPtr Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid() && Trace::ReadTimingProfilerProvider(*Session.Get())) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::ITimingProfilerProvider& TimingProfilerProvider = *Trace::ReadTimingProfilerProvider(*Session.Get()); const Trace::ITimingProfilerTimerReader* TimerReader; TimingProfilerProvider.ReadTimers([&TimerReader](const Trace::ITimingProfilerTimerReader& Out) { TimerReader = &Out; }); const FTimingTrackViewport& Viewport = Context.GetViewport(); TimingProfilerProvider.ReadTimeline(TimelineIndex, [&Viewport, this, &Builder, TimerReader](const Trace::ITimingProfilerProvider::Timeline& Timeline) { if (FTimingEventsTrack::bUseDownSampling) { const double SecondsPerPixel = 1.0 / Viewport.GetScaleX(); Timeline.EnumerateEventsDownSampled(Viewport.GetStartTime(), Viewport.GetEndTime(), SecondsPerPixel, [this, &Builder, TimerReader](double StartTime, double EndTime, uint32 Depth, const Trace::FTimingProfilerEvent& Event) { const Trace::FTimingProfilerTimer* Timer = TimerReader->GetTimer(Event.TimerIndex); if (ensure(Timer != nullptr)) { AddTimingEventToBuilder(Builder, StartTime, EndTime, Depth, Event.TimerIndex, Timer); } else { Builder.AddEvent(StartTime, EndTime, Depth, 0xFF000000, [&Event](float) { return FString::Printf(TEXT("[%u]"), Event.TimerIndex); }); } return Trace::EEventEnumerate::Continue; }); } else { Timeline.EnumerateEvents(Viewport.GetStartTime(), Viewport.GetEndTime(), [this, &Builder, TimerReader](double StartTime, double EndTime, uint32 Depth, const Trace::FTimingProfilerEvent& Event) { const Trace::FTimingProfilerTimer* Timer = TimerReader->GetTimer(Event.TimerIndex); if (ensure(Timer != nullptr)) { AddTimingEventToBuilder(Builder, StartTime, EndTime, Depth, Event.TimerIndex, Timer); } else { Builder.AddEvent(StartTime, EndTime, Depth, 0xFF000000, [&Event](float) { return FString::Printf(TEXT("[%u]"), Event.TimerIndex); }); } return Trace::EEventEnumerate::Continue; }); } }); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingTrack::BuildFilteredDrawState(ITimingEventsTrackDrawStateBuilder& Builder, const ITimingTrackUpdateContext& Context) { const TSharedPtr EventFilterPtr = Context.GetEventFilter(); if (EventFilterPtr.IsValid() && EventFilterPtr->FilterTrack(*this)) { bool bFilterOnlyByEventType = false; // this is the most often use case, so the below code tries to optimize it uint64 FilterEventType = 0; if (EventFilterPtr->Is()) { bFilterOnlyByEventType = true; const FTimingEventFilterByEventType& EventFilter = EventFilterPtr->As(); FilterEventType = EventFilter.GetEventType(); } TSharedPtr Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid() && Trace::ReadTimingProfilerProvider(*Session.Get())) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::ITimingProfilerProvider& TimingProfilerProvider = *Trace::ReadTimingProfilerProvider(*Session.Get()); const Trace::ITimingProfilerTimerReader* TimerReader; TimingProfilerProvider.ReadTimers([&TimerReader](const Trace::ITimingProfilerTimerReader& Out) { TimerReader = &Out; }); const FTimingTrackViewport& Viewport = Context.GetViewport(); if (bFilterOnlyByEventType) { // TODO: Add a setting to switch this on/off if (true) { TimingProfilerProvider.ReadTimeline(TimelineIndex, [&Viewport, this, &Builder, TimerReader, FilterEventType](const Trace::ITimingProfilerProvider::Timeline& Timeline) { TArray> FilteredEvents; Trace::ITimeline::EnumerateAsyncParams Params; Params.IntervalStart = Viewport.GetStartTime(); Params.IntervalEnd = Viewport.GetEndTime(); Params.Resolution = 0.0; Params.SetupCallback = [&FilteredEvents](uint32 NumTasks) { FilteredEvents.AddDefaulted(NumTasks); }; Params.Callback = [this, &Builder, TimerReader, FilterEventType, &FilteredEvents](double StartTime, double EndTime, uint32 Depth, const Trace::FTimingProfilerEvent& Event, uint32 TaskIndex) { const Trace::FTimingProfilerTimer* Timer = TimerReader->GetTimer(Event.TimerIndex); if (ensure(Timer != nullptr)) { if (Timer->Id == FilterEventType) { FPendingEventInfo TimelineEvent; TimelineEvent.StartTime = StartTime; TimelineEvent.EndTime = EndTime; TimelineEvent.Depth = Depth; TimelineEvent.TimerIndex = Event.TimerIndex; FilteredEvents[TaskIndex].Add(TimelineEvent); } } return Trace::EEventEnumerate::Continue; }; // Note: Enumerating events for filtering should not use downsampling. Timeline.EnumerateEventsDownSampledAsync(Params); for (TArray& Array: FilteredEvents) { for (FPendingEventInfo& TimelineEvent: Array) { const Trace::FTimingProfilerTimer* Timer = TimerReader->GetTimer(TimelineEvent.TimerIndex); AddTimingEventToBuilder(Builder, TimelineEvent.StartTime, TimelineEvent.EndTime, TimelineEvent.Depth, TimelineEvent.TimerIndex, Timer); } } }); } else { TimingProfilerProvider.ReadTimeline(TimelineIndex, [&Viewport, this, &Builder, TimerReader, FilterEventType](const Trace::ITimingProfilerProvider::Timeline& Timeline) { // Note: Enumerating events for filtering should not use downsampling. Timeline.EnumerateEventsDownSampled(Viewport.GetStartTime(), Viewport.GetEndTime(), 0, [this, &Builder, TimerReader, FilterEventType](double StartTime, double EndTime, uint32 Depth, const Trace::FTimingProfilerEvent& Event) { const Trace::FTimingProfilerTimer* Timer = TimerReader->GetTimer(Event.TimerIndex); if (ensure(Timer != nullptr)) { if (Timer->Id == FilterEventType) { AddTimingEventToBuilder(Builder, StartTime, EndTime, Depth, Event.TimerIndex, Timer); } } return Trace::EEventEnumerate::Continue; }); }); } } else // generic filter { TimingProfilerProvider.ReadTimeline(TimelineIndex, [&Viewport, this, &Builder, TimerReader, &EventFilterPtr](const Trace::ITimingProfilerProvider::Timeline& Timeline) { // Note: Enumerating events for filtering should not use downsampling. // const double SecondsPerPixel = 1.0 / Viewport.GetScaleX(); // Timeline.EnumerateEventsDownSampled(Viewport.GetStartTime(), Viewport.GetEndTime(), SecondsPerPixel, Timeline.EnumerateEvents(Viewport.GetStartTime(), Viewport.GetEndTime(), [this, &Builder, TimerReader, &EventFilterPtr](double StartTime, double EndTime, uint32 Depth, const Trace::FTimingProfilerEvent& Event) { const Trace::FTimingProfilerTimer* Timer = TimerReader->GetTimer(Event.TimerIndex); if (ensure(Timer != nullptr)) { FThreadTrackEvent TimingEvent(SharedThis(this), StartTime, EndTime, Depth); TimingEvent.SetTimerId(Timer->Id); TimingEvent.SetTimerIndex(Event.TimerIndex); if (EventFilterPtr->FilterEvent(TimingEvent)) { AddTimingEventToBuilder(Builder, StartTime, EndTime, Depth, Event.TimerIndex, Timer); } } return Trace::EEventEnumerate::Continue; }); }); } } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingTrack::PostDraw(const ITimingTrackDrawContext& Context) const { const TSharedPtr SelectedEventPtr = Context.GetSelectedEvent(); if (SelectedEventPtr.IsValid() && SelectedEventPtr->CheckTrack(this) && SelectedEventPtr->Is()) { const FThreadTrackEvent& SelectedEvent = SelectedEventPtr->As(); const ITimingViewDrawHelper& Helper = Context.GetHelper(); DrawSelectedEventInfo(SelectedEvent, Context.GetViewport(), Context.GetDrawContext(), Helper.GetWhiteBrush(), Helper.GetEventFont()); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingTrack::DrawSelectedEventInfo(const FThreadTrackEvent& SelectedEvent, const FTimingTrackViewport& Viewport, const FDrawContext& DrawContext, const FSlateBrush* WhiteBrush, const FSlateFontInfo& Font) const { TSharedPtr Session = FInsightsManager::Get()->GetSession(); check(Session.IsValid()); Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::ITimingProfilerProvider& TimingProfilerProvider = *Trace::ReadTimingProfilerProvider(*Session.Get()); const Trace::ITimingProfilerTimerReader* TimerReader; TimingProfilerProvider.ReadTimers([&TimerReader](const Trace::ITimingProfilerTimerReader& Out) { TimerReader = &Out; }); const Trace::FTimingProfilerTimer* Timer = TimerReader->GetTimer(SelectedEvent.GetTimerIndex()); if (Timer != nullptr) { FString Str = FString::Printf(TEXT("%s (Incl.: %s, Excl.: %s)"), Timer->Name, *TimeUtils::FormatTimeAuto(SelectedEvent.GetDuration()), *TimeUtils::FormatTimeAuto(SelectedEvent.GetExclusiveTime())); const TSharedRef FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); const FVector2D Size = FontMeasureService->Measure(Str, Font); const float X = Viewport.GetWidth() - Size.X - 23.0f; const float Y = Viewport.GetHeight() - Size.Y - 18.0f; const FLinearColor BackgroundColor(0.05f, 0.05f, 0.05f, 1.0f); const FLinearColor TextColor(0.7f, 0.7f, 0.7f, 1.0f); DrawContext.DrawBox(X - 8.0f, Y - 2.0f, Size.X + 16.0f, Size.Y + 4.0f, WhiteBrush, BackgroundColor); DrawContext.LayerId++; DrawContext.DrawText(X, Y, Str, Font, TextColor); DrawContext.LayerId++; } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingTrack::InitTooltip(FTooltipDrawState& InOutTooltip, const ITimingEvent& InTooltipEvent) const { InOutTooltip.ResetContent(); if (InTooltipEvent.CheckTrack(this) && InTooltipEvent.Is()) { const FThreadTrackEvent& TooltipEvent = InTooltipEvent.As(); TSharedPtr ParentTimingEvent; TSharedPtr RootTimingEvent; GetParentAndRoot(TooltipEvent, ParentTimingEvent, RootTimingEvent); TSharedPtr Session = FInsightsManager::Get()->GetSession(); check(Session.IsValid()); Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::ITimingProfilerProvider& TimingProfilerProvider = *Trace::ReadTimingProfilerProvider(*Session.Get()); const Trace::ITimingProfilerTimerReader* TimerReader; TimingProfilerProvider.ReadTimers([&TimerReader](const Trace::ITimingProfilerTimerReader& Out) { TimerReader = &Out; }); const Trace::FTimingProfilerTimer* Timer = TimerReader->GetTimer(TooltipEvent.GetTimerIndex()); FString TimerName = (Timer != nullptr) ? Timer->Name : TEXT("N/A"); InOutTooltip.AddTitle(TimerName); if (ParentTimingEvent.IsValid() && TooltipEvent.GetDepth() > 0) { Timer = TimerReader->GetTimer(ParentTimingEvent->GetTimerIndex()); const TCHAR* ParentTimerName = (Timer != nullptr) ? Timer->Name : TEXT("N/A"); FNumberFormattingOptions FormattingOptions; FormattingOptions.MaximumFractionalDigits = 2; const FString ValueStr = FString::Printf(TEXT("%s %s"), *FText::AsPercent(TooltipEvent.GetDuration() / ParentTimingEvent->GetDuration(), &FormattingOptions).ToString(), ParentTimerName); InOutTooltip.AddNameValueTextLine(TEXT("% of Parent:"), ValueStr); } if (RootTimingEvent.IsValid() && TooltipEvent.GetDepth() > 1) { Timer = TimerReader->GetTimer(RootTimingEvent->GetTimerIndex()); const TCHAR* RootTimerName = (Timer != nullptr) ? Timer->Name : TEXT("N/A"); FNumberFormattingOptions FormattingOptions; FormattingOptions.MaximumFractionalDigits = 2; const FString ValueStr = FString::Printf(TEXT("%s %s"), *FText::AsPercent(TooltipEvent.GetDuration() / RootTimingEvent->GetDuration(), &FormattingOptions).ToString(), RootTimerName); InOutTooltip.AddNameValueTextLine(TEXT("% of Root:"), ValueStr); } InOutTooltip.AddNameValueTextLine(TEXT("Inclusive Time:"), TimeUtils::FormatTimeAuto(TooltipEvent.GetDuration())); { FNumberFormattingOptions FormattingOptions; FormattingOptions.MaximumFractionalDigits = 2; const FString ExclStr = FString::Printf(TEXT("%s (%s)"), *TimeUtils::FormatTimeAuto(TooltipEvent.GetExclusiveTime()), *FText::AsPercent(TooltipEvent.GetExclusiveTime() / TooltipEvent.GetDuration(), &FormattingOptions).ToString()); InOutTooltip.AddNameValueTextLine(TEXT("Exclusive Time:"), ExclStr); } InOutTooltip.AddNameValueTextLine(TEXT("Depth:"), FString::Printf(TEXT("%d"), TooltipEvent.GetDepth())); TArrayView Metadata = TimerReader->GetMetadata(TooltipEvent.GetTimerIndex()); if (Metadata.Num() > 0) { AppendMetadataToTooltip(InOutTooltip, Metadata); } } InOutTooltip.UpdateLayout(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingTrack::GetParentAndRoot(const FThreadTrackEvent& TimingEvent, TSharedPtr& OutParentTimingEvent, TSharedPtr& OutRootTimingEvent) const { if (TimingEvent.GetDepth() > 0) { TSharedPtr Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); if (Trace::ReadTimingProfilerProvider(*Session.Get())) { const Trace::ITimingProfilerProvider& TimingProfilerProvider = *Trace::ReadTimingProfilerProvider(*Session.Get()); TimingProfilerProvider.ReadTimeline(GetTimelineIndex(), [&TimingEvent, &OutParentTimingEvent, &OutRootTimingEvent](const Trace::ITimingProfilerProvider::Timeline& Timeline) { const double Time = (TimingEvent.GetStartTime() + TimingEvent.GetEndTime()) / 2; TimelineEventInfo EventInfo; bool IsFound = Timeline.GetEventInfo(Time, 0, TimingEvent.GetDepth() - 1, EventInfo); if (IsFound) { CreateFThreadTrackEventFromInfo(EventInfo, TimingEvent.GetTrack(), TimingEvent.GetDepth() - 1, OutParentTimingEvent); } IsFound = Timeline.GetEventInfo(Time, 0, 0, EventInfo); if (IsFound) { CreateFThreadTrackEventFromInfo(EventInfo, TimingEvent.GetTrack(), 0, OutRootTimingEvent); } }); } } } } //////////////////////////////////////////////////////////////////////////////////////////////////// const TSharedPtr FThreadTimingTrack::GetEvent(float InPosX, float InPosY, const FTimingTrackViewport& Viewport) const { TSharedPtr TimingEvent; const FTimingViewLayout& Layout = Viewport.GetLayout(); const float TopLaneY = GetPosY() + 1.0f + Layout.TimelineDY; // +1.0f is for horizontal line between timelines const float DY = InPosY - TopLaneY; // If mouse is not above first sub-track or below last sub-track... if (DY >= 0 && DY < GetHeight() - 1.0f - 2 * Layout.TimelineDY) { const int32 Depth = DY / (Layout.EventH + Layout.EventDY); const double SecondsPerPixel = 1.0 / Viewport.GetScaleX(); const double EventTime = Viewport.SlateUnitsToTime(InPosX); TSharedPtr Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); if (Trace::ReadTimingProfilerProvider(*Session.Get())) { const Trace::ITimingProfilerProvider& TimingProfilerProvider = *Trace::ReadTimingProfilerProvider(*Session.Get()); TimingProfilerProvider.ReadTimeline(GetTimelineIndex(), [this, &EventTime, &Depth, &TimingEvent, &SecondsPerPixel](const Trace::ITimingProfilerProvider::Timeline& Timeline) { TimelineEventInfo EventInfo; bool IsFound = Timeline.GetEventInfo(EventTime, 2 * SecondsPerPixel, Depth, EventInfo); if (IsFound) { CreateFThreadTrackEventFromInfo(EventInfo, SharedThis(this), Depth, TimingEvent); } }); } } } return TimingEvent; } //////////////////////////////////////////////////////////////////////////////////////////////////// const TSharedPtr FThreadTimingTrack::SearchEvent(const FTimingEventSearchParameters& InSearchParameters) const { TSharedPtr FoundEvent; FindTimingProfilerEvent(InSearchParameters, [this, &FoundEvent](double InFoundStartTime, double InFoundEndTime, uint32 InFoundDepth, const Trace::FTimingProfilerEvent& InFoundEvent) { FoundEvent = MakeShared(SharedThis(this), InFoundStartTime, InFoundEndTime, InFoundDepth); FoundEvent->SetTimerIndex(InFoundEvent.TimerIndex); uint32 TimerId = 0; bool ret = FThreadTimingTrack::TimerIndexToTimerId(InFoundEvent.TimerIndex, TimerId); if (ret) { FoundEvent->SetTimerId(TimerId); } }); return FoundEvent; } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingTrack::UpdateEventStats(ITimingEvent& InOutEvent) const { if (InOutEvent.CheckTrack(this) && InOutEvent.Is()) { FThreadTrackEvent& TrackEvent = InOutEvent.As(); if (TrackEvent.IsExclusiveTimeComputed()) { return; } TSharedPtr Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); if (Trace::ReadTimingProfilerProvider(*Session.Get())) { const Trace::ITimingProfilerProvider& TimingProfilerProvider = *Trace::ReadTimingProfilerProvider(*Session.Get()); // Get Exclusive Time. TimingProfilerProvider.ReadTimeline(GetTimelineIndex(), [&TrackEvent](const Trace::ITimingProfilerProvider::Timeline& Timeline) { TimelineEventInfo EventInfo; bool bIsFound = Timeline.GetEventInfo(TrackEvent.GetStartTime(), 0.0, TrackEvent.GetDepth(), EventInfo); if (bIsFound) { TrackEvent.SetExclusiveTime(EventInfo.ExclTime); TrackEvent.SetIsExclusiveTimeComputed(true); } }); } } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingTrack::OnEventSelected(const ITimingEvent& InSelectedEvent) const { if (InSelectedEvent.CheckTrack(this) && InSelectedEvent.Is()) { const FThreadTrackEvent& TrackEvent = InSelectedEvent.As(); // Select the timer node corresponding to timing event type of selected timing event. FTimingProfilerManager::Get()->SetSelectedTimer(TrackEvent.GetTimerId()); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingTrack::OnClipboardCopyEvent(const ITimingEvent& InSelectedEvent) const { if (InSelectedEvent.CheckTrack(this) && InSelectedEvent.Is()) { const FThreadTrackEvent& TrackEvent = InSelectedEvent.As(); FTimerNodePtr TimerNodePtr = FTimingProfilerManager::Get()->GetTimerNode(TrackEvent.GetTimerId()); if (TimerNodePtr) { // Copy name of selected timing event to clipboard. FPlatformApplicationMisc::ClipboardCopy(*TimerNodePtr->GetName().ToString()); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingTrack::BuildContextMenu(FMenuBuilder& MenuBuilder) { if (GetGroupName() != nullptr) { MenuBuilder.BeginSection(TEXT("Misc")); { MenuBuilder.AddMenuEntry( FText::Format(LOCTEXT("CpuThreadGroupFmt", "CPU Thread Group: {0}"), FText::FromString(GetGroupName())), FText(), FSlateIcon(), FUIAction(FExecuteAction(), FCanExecuteAction::CreateLambda([]() { return false; })), NAME_None, EUserInterfaceActionType::Button); const FString ThreadIdStr = FString::Printf(TEXT("%s%u (0x%X)"), ThreadId & 0x70000000 ? "*" : "", ThreadId & ~0x70000000, ThreadId); MenuBuilder.AddMenuEntry( FText::Format(LOCTEXT("CpuThreadIdFmt", "Thread Id: {0}"), FText::FromString(ThreadIdStr)), FText(), FSlateIcon(), FUIAction(FExecuteAction(), FCanExecuteAction::CreateLambda([]() { return false; })), NAME_None, EUserInterfaceActionType::Button); } MenuBuilder.EndSection(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool FThreadTimingTrack::FindTimingProfilerEvent(const FThreadTrackEvent& InTimingEvent, TFunctionRef InFoundPredicate) const { auto MatchEvent = [&InTimingEvent](double InStartTime, double InEndTime, uint32 InDepth) { return InDepth == InTimingEvent.GetDepth() && InStartTime == InTimingEvent.GetStartTime() && InEndTime == InTimingEvent.GetEndTime(); }; const double Time = (InTimingEvent.GetStartTime() + InTimingEvent.GetEndTime()) / 2; FTimingEventSearchParameters SearchParameters(Time, Time, ETimingEventSearchFlags::StopAtFirstMatch, MatchEvent); SearchParameters.SearchHandle = &InTimingEvent.GetSearchHandle(); return FindTimingProfilerEvent(SearchParameters, InFoundPredicate); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool FThreadTimingTrack::FindTimingProfilerEvent(const FTimingEventSearchParameters& InParameters, TFunctionRef InFoundPredicate) const { return TTimingEventSearch::Search( InParameters, [this](TTimingEventSearch::FContext& InContext) { TSharedPtr Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); if (Trace::ReadTimingProfilerProvider(*Session.Get())) { const Trace::ITimingProfilerProvider& TimingProfilerProvider = *Trace::ReadTimingProfilerProvider(*Session.Get()); TimingProfilerProvider.ReadTimeline(GetTimelineIndex(), [&InContext](const Trace::ITimingProfilerProvider::Timeline& Timeline) { Timeline.EnumerateEvents(InContext.GetParameters().StartTime, InContext.GetParameters().EndTime, [&InContext](double EventStartTime, double EventEndTime, uint32 EventDepth, const Trace::FTimingProfilerEvent& Event) { InContext.Check(EventStartTime, EventEndTime, EventDepth, Event); return InContext.ShouldContinueSearching() ? Trace::EEventEnumerate::Continue : Trace::EEventEnumerate::Stop; }); }); } } }, [&InFoundPredicate](double InFoundStartTime, double InFoundEndTime, uint32 InFoundDepth, const Trace::FTimingProfilerEvent& InEvent) { InFoundPredicate(InFoundStartTime, InFoundEndTime, InFoundDepth, InEvent); }, SearchCache); } //////////////////////////////////////////////////////////////////////////////////////////////////// void FThreadTimingTrack::CreateFThreadTrackEventFromInfo(const TimelineEventInfo& InEventInfo, const TSharedRef InTrack, int32 InDepth, TSharedPtr& OutTimingEvent) { OutTimingEvent = MakeShared(InTrack, InEventInfo.StartTime, InEventInfo.EndTime, InDepth); FThreadTrackEvent& Event = OutTimingEvent->As(); Event.SetExclusiveTime(InEventInfo.ExclTime); Event.SetIsExclusiveTimeComputed(true); Event.SetTimerIndex(InEventInfo.Event.TimerIndex); uint32 TimerId = 0; bool ret = FThreadTimingTrack::TimerIndexToTimerId(InEventInfo.Event.TimerIndex, TimerId); if (ret) { Event.SetTimerId(TimerId); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool FThreadTimingTrack::TimerIndexToTimerId(uint32 InTimerIndex, uint32& OutTimerId) { TSharedPtr Session = FInsightsManager::Get()->GetSession(); check(Session.IsValid()) Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::ITimingProfilerProvider& TimingProfilerProvider = *Trace::ReadTimingProfilerProvider(*Session.Get()); const Trace::ITimingProfilerTimerReader* TimerReader; TimingProfilerProvider.ReadTimers([&TimerReader](const Trace::ITimingProfilerTimerReader& Out) { TimerReader = &Out; }); const Trace::FTimingProfilerTimer* Timer = TimerReader->GetTimer(InTimerIndex); if (Timer == nullptr) { return false; } OutTimerId = Timer->Id; return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// #undef LOCTEXT_NAMESPACE