// Copyright Epic Games, Inc. All Rights Reserved. #include "MarkersTimingTrack.h" #include "Fonts/FontMeasure.h" #include "Framework/Application/SlateApplication.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Styling/CoreStyle.h" #include "TraceServices/AnalysisService.h" // Insights #include "Insights/Common/PaintUtils.h" #include "Insights/InsightsManager.h" #include "Insights/InsightsStyle.h" #include "Insights/TimingProfilerManager.h" #include "Insights/ViewModels/TimingTrackViewport.h" //////////////////////////////////////////////////////////////////////////////////////////////////// #define LOCTEXT_NAMESPACE "MarkersTimingTrack" //////////////////////////////////////////////////////////////////////////////////////////////////// INSIGHTS_IMPLEMENT_RTTI(FMarkersTimingTrack) //////////////////////////////////////////////////////////////////////////////////////////////////// FMarkersTimingTrack::FMarkersTimingTrack() : FBaseTimingTrack(TEXT("Markers (Bookmarks / Logs)")) //, TimeMarkerBoxes() //, TimeMarkerTexts() , bUseOnlyBookmarks(true), Header(*this), NumLogMessages(0), NumDrawBoxes(0), NumDrawTexts(0), WhiteBrush(FInsightsStyle::Get().GetBrush("WhiteBrush")), Font(FCoreStyle::GetDefaultFontStyle("Regular", 8)) { SetValidLocations(ETimingTrackLocation::TopDocked | ETimingTrackLocation::BottomDocked); SetOrder(FTimingTrackOrder::Markers); } //////////////////////////////////////////////////////////////////////////////////////////////////// FMarkersTimingTrack::~FMarkersTimingTrack() { } //////////////////////////////////////////////////////////////////////////////////////////////////// void FMarkersTimingTrack::Reset() { FBaseTimingTrack::Reset(); TimeMarkerBoxes.Reset(); TimeMarkerTexts.Reset(); bUseOnlyBookmarks = true; Header.Reset(); Header.SetIsInBackground(true); Header.SetCanBeCollapsed(true); NumLogMessages = 0; NumDrawBoxes = 0; NumDrawTexts = 0; UpdateTrackNameAndHeight(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void FMarkersTimingTrack::UpdateTrackNameAndHeight() { if (bUseOnlyBookmarks) { const FString NameString = TEXT("Bookmarks"); SetName(NameString); SetHeight(14.0f); } else { const FString NameString = TEXT("Logs"); SetName(NameString); SetHeight(28.0f); } Header.UpdateSize(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void FMarkersTimingTrack::Update(const ITimingTrackUpdateContext& Context) { Header.Update(Context); const FTimingTrackViewport& Viewport = Context.GetViewport(); if (IsDirty() || Viewport.IsHorizontalViewportDirty()) { ClearDirtyFlag(); UpdateDrawState(Viewport); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FMarkersTimingTrack::PostUpdate(const ITimingTrackUpdateContext& Context) { const float MouseY = Context.GetMousePosition().Y; SetHoveredState(MouseY >= GetPosY() && MouseY < GetPosY() + GetHeight()); Header.PostUpdate(Context); } //////////////////////////////////////////////////////////////////////////////////////////////////// void FMarkersTimingTrack::UpdateDrawState(const FTimingTrackViewport& InViewport) { FTimeMarkerTrackBuilder Builder(*this, InViewport); TSharedPtr Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::ILogProvider& LogProvider = Trace::ReadLogProvider(*Session.Get()); Builder.BeginLog(LogProvider); LogProvider.EnumerateMessages( Builder.GetViewport().GetStartTime(), Builder.GetViewport().GetEndTime(), [&Builder](const Trace::FLogMessage& Message) { Builder.AddLogMessage(Message); }); Builder.EndLog(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FMarkersTimingTrack::Draw(const ITimingTrackDrawContext& Context) const { FDrawContext& DrawContext = Context.GetDrawContext(); const FTimingTrackViewport& Viewport = Context.GetViewport(); // Draw background. const FLinearColor BackgroundColor(0.04f, 0.04f, 0.04f, 1.0f); DrawContext.DrawBox(0.0f, GetPosY(), Viewport.GetWidth(), GetHeight(), WhiteBrush, BackgroundColor); DrawContext.LayerId++; Header.Draw(Context); } //////////////////////////////////////////////////////////////////////////////////////////////////// void FMarkersTimingTrack::PostDraw(const ITimingTrackDrawContext& Context) const { FDrawContext& DrawContext = Context.GetDrawContext(); const FTimingTrackViewport& Viewport = Context.GetViewport(); ////////////////////////////////////////////////// // Draw vertical lines. // Multiple adjacent vertical lines with same color are merged into a single box. float BoxY, BoxH; if (IsCollapsed()) { BoxY = GetPosY(); BoxH = GetHeight(); } else { BoxY = 0.0f; BoxH = Viewport.GetHeight(); } const int32 NumBoxes = TimeMarkerBoxes.Num(); for (int32 BoxIndex = 0; BoxIndex < NumBoxes; BoxIndex++) { const FTimeMarkerBoxInfo& Box = TimeMarkerBoxes[BoxIndex]; DrawContext.DrawBox(Box.X, BoxY, Box.W, BoxH, WhiteBrush, Box.Color); } DrawContext.LayerId++; NumDrawBoxes = NumBoxes; ////////////////////////////////////////////////// // Draw texts (strings are already truncated). const float CategoryY = GetPosY() + 2.0f; const float MessageY = GetPosY() + (IsBookmarksTrack() ? 1.0f : 14.0f); const int32 NumTexts = TimeMarkerTexts.Num(); for (int32 TextIndex = 0; TextIndex < NumTexts; TextIndex++) { const FTimeMarkerTextInfo& TextInfo = TimeMarkerTexts[TextIndex]; if (!IsBookmarksTrack() && TextInfo.Category.Len() > 0) { DrawContext.DrawText(TextInfo.X, CategoryY, TextInfo.Category, Font, TextInfo.Color); NumDrawTexts++; } if (TextInfo.Message.Len() > 0) { DrawContext.DrawText(TextInfo.X, MessageY, TextInfo.Message, Font, TextInfo.Color); NumDrawTexts++; } } DrawContext.LayerId++; ////////////////////////////////////////////////// Header.PostDraw(Context); } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply FMarkersTimingTrack::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { FReply Reply = FReply::Unhandled(); if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) { if (IsVisible() && IsHeaderHovered()) { ToggleCollapsed(); Reply = FReply::Handled(); } } return Reply; } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply FMarkersTimingTrack::OnMouseButtonDoubleClick(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { return OnMouseButtonDown(MyGeometry, MouseEvent); } //////////////////////////////////////////////////////////////////////////////////////////////////// void FMarkersTimingTrack::BuildContextMenu(FMenuBuilder& MenuBuilder) { MenuBuilder.BeginSection(TEXT("Misc")); { MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_ToggleCollapsed", "Collapsed"), LOCTEXT("ContextMenu_ToggleCollapsed_Desc", "Whether the vertical marker lines are collapsed or expanded."), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &FMarkersTimingTrack::ToggleCollapsed), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &FMarkersTimingTrack::IsCollapsed)), NAME_None, EUserInterfaceActionType::ToggleButton); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_Bookmarks", "Bookmarks"), LOCTEXT("ContextMenu_Bookmarks_Desc", "Change this track to show only the bookmarks."), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &FMarkersTimingTrack::SetBookmarksTrack), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &FMarkersTimingTrack::IsBookmarksTrack)), NAME_None, EUserInterfaceActionType::RadioButton); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_Logs", "Logs"), LOCTEXT("ContextMenu_Logs_Desc", "Change this track to show all logs."), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &FMarkersTimingTrack::SetLogsTrack), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &FMarkersTimingTrack::IsLogsTrack)), NAME_None, EUserInterfaceActionType::RadioButton); } MenuBuilder.EndSection(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // FTimeMarkerTrackBuilder //////////////////////////////////////////////////////////////////////////////////////////////////// FTimeMarkerTrackBuilder::FTimeMarkerTrackBuilder(FMarkersTimingTrack& InTrack, const FTimingTrackViewport& InViewport) : Track(InTrack), Viewport(InViewport), FontMeasureService(FSlateApplication::Get().GetRenderer()->GetFontMeasureService()), Font(FCoreStyle::GetDefaultFontStyle("Regular", 8)) { Track.ResetCache(); Track.NumLogMessages = 0; } //////////////////////////////////////////////////////////////////////////////////////////////////// void FTimeMarkerTrackBuilder::BeginLog(const Trace::ILogProvider& LogProvider) { LogProviderPtr = &LogProvider; LastX1 = -1000.0f; LastX2 = -1000.0f; LastLogIndex = 0; LastVerbosity = ELogVerbosity::NoLogging; LastCategory = nullptr; LastMessage = nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// void FTimeMarkerTrackBuilder::AddLogMessage(const Trace::FLogMessage& Message) { Track.NumLogMessages++; // Add also the log message imediately on the left of the screen (if any). if (Track.NumLogMessages == 1 && Message.Index > 0) { // Note: Reading message at Index-1 will not work as expected when using filter! // TODO: Search API like: LogProviderPtr->SearchMessage(StartIndex, ESearchDirection::Backward, LambdaPredicate, bResolveFormatString); LogProviderPtr->ReadMessage( Message.Index - 1, [this](const Trace::FLogMessage& Message) { AddLogMessage(Message); }); } check(Message.Category != nullptr); // check(Message.Category->Name != nullptr); const TCHAR* CategoryName = Message.Category->Name != nullptr ? Message.Category->Name : TEXT(""); if (!Track.bUseOnlyBookmarks || FCString::Strcmp(CategoryName, TEXT("LogBookmark")) == 0) { float X = Viewport.TimeToSlateUnitsRounded(Message.Time); if (X < 0.0f) { X = -1.0f; } AddTimeMarker(X, Message.Index, Message.Verbosity, CategoryName, Message.Message); } } //////////////////////////////////////////////////////////////////////////////////////////////////// FLinearColor FTimeMarkerTrackBuilder::GetColorByCategory(const TCHAR* const Category) { // Strip the "Log" prefix. FString CategoryStr(Category); if (CategoryStr.StartsWith(TEXT("Log"))) { CategoryStr.RightChopInline(3, false); } uint32 Hash = 0; for (const TCHAR* c = *CategoryStr; *c; ++c) { Hash = (Hash + *c) * 0x2c2c57ed; } // Divided by 128.0 in order to force bright colors. return FLinearColor(((Hash >> 16) & 0xFF) / 128.0f, ((Hash >> 8) & 0xFF) / 128.0f, (Hash & 0xFF) / 128.0f, 1.0f); } //////////////////////////////////////////////////////////////////////////////////////////////////// FLinearColor FTimeMarkerTrackBuilder::GetColorByVerbosity(const ELogVerbosity::Type Verbosity) { static FLinearColor Colors[] = { FLinearColor(0.0f, 0.0f, 0.0f, 1.0f), // NoLogging FLinearColor(1.0f, 0.0f, 0.0f, 1.0f), // Fatal FLinearColor(1.0f, 0.3f, 0.0f, 1.0f), // Error FLinearColor(0.7f, 0.5f, 0.0f, 1.0f), // Warning FLinearColor(0.0f, 0.7f, 0.0f, 1.0f), // Display FLinearColor(0.0f, 0.7f, 1.0f, 1.0f), // Log FLinearColor(0.7f, 0.7f, 0.7f, 1.0f), // Verbose FLinearColor(1.0f, 1.0f, 1.0f, 1.0f), // VeryVerbose }; static_assert(sizeof(Colors) / sizeof(FLinearColor) == (int)ELogVerbosity::Type::All + 1, "ELogVerbosity::Type has changed!?"); // return Colors[Verbosity & ELogVerbosity::VerbosityMask]; return Colors[Verbosity & 7]; // using 7 instead of ELogVerbosity::VerbosityMask (15) to make static analyzer happy } //////////////////////////////////////////////////////////////////////////////////////////////////// void FTimeMarkerTrackBuilder::Flush(float AvailableTextW) { // Is last marker valid? if (LastCategory != nullptr) { const FLinearColor Color = GetColorByCategory(LastCategory); bool bAddNewBox = true; if (Track.TimeMarkerBoxes.Num() > 0) { FTimeMarkerBoxInfo& PrevBox = Track.TimeMarkerBoxes[Track.TimeMarkerBoxes.Num() - 1]; if (PrevBox.X + PrevBox.W == LastX1 && PrevBox.Color.R == Color.R && PrevBox.Color.G == Color.G && PrevBox.Color.B == Color.B) { // Extend previous box instead. PrevBox.W += LastX2 - LastX1; bAddNewBox = false; } } if (bAddNewBox) { // Add new Box info to cache. FTimeMarkerBoxInfo& Box = Track.TimeMarkerBoxes[Track.TimeMarkerBoxes.AddDefaulted()]; Box.X = LastX1; Box.W = LastX2 - LastX1; Box.Color = Color; Box.Color.A = 0.25f; } if (AvailableTextW > 6.0f) { // Strip the "Log" prefix. FString CategoryStr(LastCategory); if (CategoryStr.StartsWith(TEXT("Log"))) { CategoryStr.RightChopInline(3, false); } const int32 LastWholeCharacterIndexCategory = FontMeasureService->FindLastWholeCharacterIndexBeforeOffset(CategoryStr, Font, FMath::RoundToInt(AvailableTextW - 2.0f)); const int32 LastWholeCharacterIndexMessage = FontMeasureService->FindLastWholeCharacterIndexBeforeOffset(LastMessage, Font, FMath::RoundToInt(AvailableTextW - 2.0f)); if (LastWholeCharacterIndexCategory >= 0 || LastWholeCharacterIndexMessage >= 0) { // Add new Text info to cache. FTimeMarkerTextInfo& TextInfo = Track.TimeMarkerTexts[Track.TimeMarkerTexts.AddDefaulted()]; TextInfo.X = LastX2 + 2.0f; TextInfo.Color = Color; if (LastWholeCharacterIndexCategory >= 0) { TextInfo.Category = CategoryStr.Left(LastWholeCharacterIndexCategory + 1); } if (LastWholeCharacterIndexMessage >= 0) { TextInfo.Message.AppendChars(LastMessage, LastWholeCharacterIndexMessage + 1); } } } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void FTimeMarkerTrackBuilder::AddTimeMarker(const float X, const uint64 LogIndex, const ELogVerbosity::Type Verbosity, const TCHAR* const Category, const TCHAR* Message) { const float W = X - LastX2; if (W > 0.0f) // There is at least 1px from previous box? { // Flush previous marker (if any). Flush(W); // Begin new marker info. LastX1 = X; LastX2 = X + 1.0f; } else if (W == 0.0f) // Adjacent to previous box? { // Same color as previous marker? if (Category == LastCategory) { // Extend previous box. LastX2++; } else { // Flush previous marker (if any). Flush(0.0f); // Begin new box. LastX1 = X; LastX2 = X + 1.0f; } } else // Overlaps previous box? { // Same color as previous marker? if (Category == LastCategory) { // Keep previous box. } else { // Shrink previous box. LastX2--; if (LastX2 > LastX1) { // Flush previous marker (if any). Flush(0.0f); } // Begin new box. LastX1 = X; LastX2 = X + 1.0f; } } // Save marker. LastCategory = Category; LastVerbosity = Verbosity; LastLogIndex = LogIndex; LastMessage = Message; } //////////////////////////////////////////////////////////////////////////////////////////////////// void FTimeMarkerTrackBuilder::EndLog() { Flush(Viewport.GetWidth() - LastX2); } //////////////////////////////////////////////////////////////////////////////////////////////////// #undef LOCTEXT_NAMESPACE