EM_Task/TraceInsights/Private/Insights/MemoryProfiler/ViewModels/MemoryGraphTrack.cpp

819 lines
32 KiB
C++
Raw Normal View History

2026-02-13 16:18:33 +08:00
// Copyright Epic Games, Inc. All Rights Reserved.
#include "MemoryGraphTrack.h"
#include "Fonts/FontMeasure.h"
#include "Fonts/SlateFontInfo.h"
#include "Styling/SlateBrush.h"
#include "TraceServices/AnalysisService.h"
#include "TraceServices/Model/TimingProfiler.h"
#include "TraceServices/Model/Counters.h"
#include "TraceServices/Model/Memory.h"
// Insights
#include "Insights/Common/PaintUtils.h"
#include "Insights/Common/TimeUtils.h"
#include "Insights/InsightsManager.h"
#include "Insights/MemoryProfiler/ViewModels/MemorySharedState.h"
#include "Insights/ViewModels/AxisViewportDouble.h"
#include "Insights/ViewModels/GraphTrackBuilder.h"
#include "Insights/ViewModels/ITimingViewDrawHelper.h"
#include "Insights/ViewModels/TimingTrackViewport.h"
#include "Insights/ViewModels/TooltipDrawState.h"
#include <limits>
#define LOCTEXT_NAMESPACE "MemoryGraphTrack"
////////////////////////////////////////////////////////////////////////////////////////////////////
// FMemoryGraphSeries
////////////////////////////////////////////////////////////////////////////////////////////////////
FString FMemoryGraphSeries::FormatValue(double Value) const
{
const int64 MemValue = static_cast<int64>(Value);
if (MemValue == 0)
{
return TEXT("0");
}
else if (MemValue < 0)
{
return FString::Printf(TEXT("-%s (%s bytes)"), *FText::AsMemory(-MemValue).ToString(), *FText::AsNumber(MemValue).ToString());
}
else
{
return FString::Printf(TEXT("%s (%s bytes)"), *FText::AsMemory(MemValue).ToString(), *FText::AsNumber(MemValue).ToString());
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// FMemoryGraphTrack
////////////////////////////////////////////////////////////////////////////////////////////////////
INSIGHTS_IMPLEMENT_RTTI(FMemoryGraphTrack)
////////////////////////////////////////////////////////////////////////////////////////////////////
FMemoryGraphTrack::FMemoryGraphTrack(FMemorySharedState& InSharedState)
: FGraphTrack(), SharedState(InSharedState), LabelUnit(EGraphTrackLabelUnit::Auto), LabelDecimalDigitCount(-1), DefaultMinValue(+std::numeric_limits<double>::infinity()), DefaultMaxValue(-std::numeric_limits<double>::infinity()), AllSeriesMinValue(0.0), AllSeriesMaxValue(0.0), bAutoZoom(false), bIsStacked(false)
{
EnabledOptions = // EGraphOptions::ShowDebugInfo |
// EGraphOptions::ShowPoints |
EGraphOptions::ShowPointsWithBorder |
EGraphOptions::ShowLines |
EGraphOptions::ShowPolygon |
EGraphOptions::UseEventDuration |
// EGraphOptions::ShowBars |
// EGraphOptions::ShowBaseline |
EGraphOptions::ShowVerticalAxisGrid |
EGraphOptions::ShowHeader |
EGraphOptions::None;
for (uint32 Mode = 0; Mode < static_cast<uint32>(EMemoryTrackHeightMode::Count); ++Mode)
{
SetAvailableTrackHeight(static_cast<EMemoryTrackHeightMode>(Mode), 100.0f * (Mode + 1));
}
SetHeight(200.0f);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FMemoryGraphTrack::ResetDefaultValueRange()
{
DefaultMinValue = +std::numeric_limits<double>::infinity();
DefaultMaxValue = -std::numeric_limits<double>::infinity();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
FMemoryGraphTrack::~FMemoryGraphTrack()
{
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FMemoryGraphTrack::Update(const ITimingTrackUpdateContext& Context)
{
FGraphTrack::Update(Context);
const bool bIsEntireGraphTrackDirty = IsDirty() || Context.GetViewport().IsHorizontalViewportDirty();
bool bNeedsUpdate = bIsEntireGraphTrackDirty;
if (!bNeedsUpdate)
{
for (TSharedPtr<FGraphSeries>& Series: AllSeries)
{
if (Series->IsVisible() && Series->IsDirty())
{
// At least one series is dirty.
bNeedsUpdate = true;
break;
}
}
}
if (bNeedsUpdate)
{
ClearDirtyFlag();
NumAddedEvents = 0;
const FTimingTrackViewport& Viewport = Context.GetViewport();
AllSeriesMinValue = DefaultMinValue;
AllSeriesMaxValue = DefaultMaxValue;
for (TSharedPtr<FGraphSeries>& Series: AllSeries)
{
if (Series->IsVisible())
{
// TODO: if (Series->Is<FMemoryGraphSeries>())
TSharedPtr<FMemoryGraphSeries> MemorySeries = StaticCastSharedPtr<FMemoryGraphSeries>(Series);
if (bIsEntireGraphTrackDirty || Series->IsDirty())
{
PreUpdateMemTagSeries(*MemorySeries, Viewport);
}
const double CurrentSeriesMinValue = MemorySeries->GetMinValue();
const double CurrentSeriesMaxValue = MemorySeries->GetMaxValue();
if (CurrentSeriesMinValue < AllSeriesMinValue)
{
AllSeriesMinValue = CurrentSeriesMinValue;
}
if (CurrentSeriesMaxValue > AllSeriesMaxValue)
{
AllSeriesMaxValue = CurrentSeriesMaxValue;
}
}
}
if (bAutoZoom)
{
const float TopY = 4.0f;
const float BottomY = GetHeight() - 4.0f;
if (TopY < BottomY)
{
for (TSharedPtr<FGraphSeries>& Series: AllSeries)
{
if (Series->IsVisible())
{
if (Series->UpdateAutoZoomEx(TopY, BottomY, AllSeriesMinValue, AllSeriesMaxValue, true))
{
Series->SetDirtyFlag();
}
}
}
}
}
for (TSharedPtr<FGraphSeries>& Series: AllSeries)
{
if (Series->IsVisible() && (bIsEntireGraphTrackDirty || Series->IsDirty()))
{
// Clear the flag before updating, because the update itself may further need to set the series as dirty.
Series->ClearDirtyFlag();
// TODO: if (Series->Is<FMemoryGraphSeries>())
TSharedPtr<FMemoryGraphSeries> MemorySeries = StaticCastSharedPtr<FMemoryGraphSeries>(Series);
UpdateMemTagSeries(*MemorySeries, Viewport);
if (Series->IsAutoZoomDirty())
{
Series->SetDirtyFlag();
}
}
}
UpdateStats();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// LLM Tag Series
////////////////////////////////////////////////////////////////////////////////////////////////////
TSharedPtr<FMemoryGraphSeries> FMemoryGraphTrack::GetMemTagSeries(Insights::FMemoryTagId MemTagId)
{
TSharedPtr<FGraphSeries>* Ptr = AllSeries.FindByPredicate([MemTagId](const TSharedPtr<FGraphSeries>& Series)
{
// TODO: if (Series->Is<FMemoryGraphSeries>())
const TSharedPtr<FMemoryGraphSeries> MemorySeries = StaticCastSharedPtr<FMemoryGraphSeries>(Series);
return MemorySeries->GetTagId() == MemTagId;
});
return (Ptr != nullptr) ? StaticCastSharedPtr<FMemoryGraphSeries>(*Ptr) : nullptr;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
TSharedPtr<FMemoryGraphSeries> FMemoryGraphTrack::AddMemTagSeries(Insights::FMemoryTrackerId MemTrackerId, Insights::FMemoryTagId MemTagId)
{
TSharedPtr<FMemoryGraphSeries> Series = GetMemTagSeries(MemTagId);
if (!Series.IsValid())
{
Series = MakeShared<FMemoryGraphSeries>();
Series->SetName(TEXT("LLM Tag"));
Series->SetDescription(TEXT("Low Level Memory Tag"));
Series->SetTrackerId(MemTrackerId);
Series->SetTagId(MemTagId);
Series->SetValueRange(0.0f, 0.0f);
Series->SetBaselineY(GetHeight() - 1.0f);
Series->SetScaleY(1.0);
AllSeries.Add(Series);
SetDirtyFlag();
}
return Series;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
int32 FMemoryGraphTrack::RemoveMemTagSeries(Insights::FMemoryTagId MemTagId)
{
SetDirtyFlag();
return AllSeries.RemoveAll([MemTagId](const TSharedPtr<FGraphSeries>& GraphSeries)
{
// TODO: if (Series->Is<FMemoryGraphSeries>())
const TSharedPtr<FMemoryGraphSeries> Series = StaticCastSharedPtr<FMemoryGraphSeries>(GraphSeries);
return Series->GetTagId() == MemTagId;
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////
int32 FMemoryGraphTrack::RemoveAllMemTagSeries()
{
SetDirtyFlag();
int32 Count = AllSeries.Num();
AllSeries.Reset();
return Count;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FMemoryGraphTrack::PreUpdateMemTagSeries(FMemoryGraphSeries& Series, const FTimingTrackViewport& Viewport)
{
TSharedPtr<const Trace::IAnalysisSession> Session = FInsightsManager::Get()->GetSession();
if (Session.IsValid())
{
Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get());
// Compute Min/Max values.
const Trace::IMemoryProvider& MemoryProvider = Trace::ReadMemoryProvider(*Session.Get());
const uint64 TotalSampleCount = MemoryProvider.GetTagSampleCount(Series.GetTrackerId(), Series.GetTagId());
if (TotalSampleCount > 0)
{
double MinValue = +std::numeric_limits<double>::infinity();
double MaxValue = -std::numeric_limits<double>::infinity();
MemoryProvider.EnumerateTagSamples(Series.GetTrackerId(), Series.GetTagId(), Viewport.GetStartTime(), Viewport.GetEndTime(), true,
[&MinValue, &MaxValue](double Time, double Duration, const Trace::FMemoryTagSample& Sample)
{
const double Value = static_cast<double>(Sample.Value);
if (Value < MinValue)
{
MinValue = Value;
}
if (Value > MaxValue)
{
MaxValue = Value;
}
});
Series.SetValueRange(MinValue, MaxValue);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FMemoryGraphTrack::UpdateMemTagSeries(FMemoryGraphSeries& Series, const FTimingTrackViewport& Viewport)
{
FGraphTrackBuilder Builder(*this, Series, Viewport);
TSharedPtr<const Trace::IAnalysisSession> Session = FInsightsManager::Get()->GetSession();
if (Session.IsValid())
{
Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get());
const Trace::IMemoryProvider& MemoryProvider = Trace::ReadMemoryProvider(*Session.Get());
const uint64 TotalSampleCount = MemoryProvider.GetTagSampleCount(Series.GetTrackerId(), Series.GetTagId());
if (TotalSampleCount > 0)
{
const float TopY = 4.0f;
const float BottomY = GetHeight() - 4.0f;
if (Series.IsAutoZoomEnabled() && TopY < BottomY)
{
Series.UpdateAutoZoom(TopY, BottomY, Series.GetMinValue(), Series.GetMaxValue());
}
MemoryProvider.EnumerateTagSamples(Series.GetTrackerId(), Series.GetTagId(), Viewport.GetStartTime(), Viewport.GetEndTime(), true,
[this, &Builder](double Time, double Duration, const Trace::FMemoryTagSample& Sample)
{
Builder.AddEvent(Time, Duration, static_cast<double>(Sample.Value));
});
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FMemoryGraphTrack::Draw(const ITimingTrackDrawContext& Context) const
{
FGraphTrack::Draw(Context);
// Show warnings for series using llm tag id with incompatible tracker.
{
int WarningCount = 0;
float X = 12.0f;
float Y = GetPosY() + 12.0f;
const float MaxY = GetPosY() + GetHeight() - 8.0f;
for (const TSharedPtr<FGraphSeries>& Series: AllSeries)
{
if (Series->IsVisible())
{
if (Y > MaxY)
{
break;
}
// TODO: if (Series->Is<FMemoryGraphSeries>())
TSharedPtr<FMemoryGraphSeries> MemorySeries = StaticCastSharedPtr<FMemoryGraphSeries>(Series);
const Insights::FMemoryTag* Tag = SharedState.GetTagList().GetTagById(MemorySeries->GetTagId());
const uint64 TrackerFlag = 1ULL << MemorySeries->GetTrackerId();
if ((Tag->GetTrackers() & TrackerFlag) != TrackerFlag)
{
FDrawContext& DrawContext = Context.GetDrawContext();
const FSlateBrush* Brush = Context.GetHelper().GetWhiteBrush();
if (WarningCount == 0)
{
const FText WarningText1 = FText::Format(LOCTEXT("WarningFmt1", "Warning: One or more LLM tags in this graph are not used by the current tracker ({0})!"),
SharedState.GetCurrentTracker() ? FText::FromString(SharedState.GetCurrentTracker()->GetName()) : FText::GetEmpty());
const FLinearColor WarningTextColor1(1.0f, 0.2f, 0.2f, 1.0f);
DrawContext.DrawText(DrawContext.LayerId, X, Y, WarningText1.ToString(), Font, WarningTextColor1);
Y += 12.0f;
const FText WarningText2 = FText::Format(LOCTEXT("WarningLine2", "In order to see graph series for these LLM tags, change the current tracker to one that uses the LLM tag."),
SharedState.GetCurrentTracker() ? FText::FromString(SharedState.GetCurrentTracker()->GetName()) : FText::GetEmpty());
const FLinearColor WarningTextColor2(0.75f, 0.5f, 0.25f, 1.0f);
DrawContext.DrawText(DrawContext.LayerId, X, Y, WarningText2.ToString(), Font, WarningTextColor2);
Y += 12.0f;
}
if (Tag->GetTrackers() != 0)
{
const FText Conjunction = LOCTEXT("WarningTrackersConjunction", " and the ");
const FText Text = FText::Format(LOCTEXT("WarningFmt2", "The {0} LLM tag is only used by the {1} tracker(s)."),
FText::FromString(Tag->GetStatName()),
FText::FromString(SharedState.TrackersToString(Tag->GetTrackers(), *Conjunction.ToString())));
const FLinearColor TextColor(1.0f, 0.75f, 0.5f, 1.0f);
DrawContext.DrawText(DrawContext.LayerId, X, Y, Text.ToString(), Font, TextColor);
Y += 12.0f;
}
else
{
const FText Text = FText::Format(LOCTEXT("WarningFmt3", "The {0} LLM tag is not used by any tracker."),
FText::FromString(Tag->GetStatName()));
const FLinearColor TextColor(1.0f, 0.75f, 0.5f, 1.0f);
DrawContext.DrawText(DrawContext.LayerId, X, Y, Text.ToString(), Font, TextColor);
Y += 12.0f;
}
++WarningCount;
}
}
}
if (WarningCount > 0)
{
FDrawContext& DrawContext = Context.GetDrawContext();
DrawContext.LayerId++;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FMemoryGraphTrack::DrawVerticalAxisGrid(const ITimingTrackDrawContext& Context) const
{
TSharedPtr<FMemoryGraphSeries> Series = MainSeries;
if (!Series.IsValid() && AllSeries.Num() > 0)
{
// TODO: if (Series->Is<FMemoryGraphSeries>())
Series = StaticCastSharedPtr<FMemoryGraphSeries>(AllSeries[0]);
}
if (!Series.IsValid())
{
return;
}
FAxisViewportDouble ViewportY;
ViewportY.SetSize(GetHeight());
ViewportY.SetScaleLimits(std::numeric_limits<double>::min(), std::numeric_limits<double>::max());
ViewportY.SetScale(Series->GetScaleY());
ViewportY.ScrollAtPos(Series->GetBaselineY() - GetHeight());
const float ViewWidth = Context.GetViewport().GetWidth();
const float RoundedViewHeight = FMath::RoundToFloat(GetHeight());
const float X0 = ViewWidth - 12.0f; // let some space for the vertical scrollbar
const float Y0 = GetPosY();
constexpr float MinDY = 32.0f; // min vertical distance between horizontal grid lines
constexpr float TextH = 14.0f; // label height
const float MinLabelY = Y0 + 1.0f;
const float MaxLabelY = Y0 + RoundedViewHeight - TextH;
float MinValueY = Y0 - MinDY; // a value below the track
float MaxValueY = Y0 + RoundedViewHeight + MinDY; // a value above the track
float ActualMinValueY = MinValueY;
float ActualMaxValueY = MaxValueY;
const bool bHasMinMax = (AllSeriesMinValue <= AllSeriesMaxValue);
if (bHasMinMax)
{
const float MinValueOffset = ViewportY.GetOffsetForValue(AllSeriesMinValue);
const float MinValueRoundedOffset = FMath::RoundToFloat(MinValueOffset);
ActualMinValueY = Y0 + RoundedViewHeight - MinValueRoundedOffset;
MinValueY = FMath::Min(MaxLabelY, FMath::Max(MinLabelY, ActualMinValueY - TextH / 2));
const float MaxValueOffset = ViewportY.GetOffsetForValue(AllSeriesMaxValue);
const float MaxValueRoundedOffset = FMath::RoundToFloat(MaxValueOffset);
ActualMaxValueY = Y0 + RoundedViewHeight - MaxValueRoundedOffset;
MaxValueY = FMath::Min(MaxLabelY, FMath::Max(MinLabelY, ActualMaxValueY - TextH / 2));
}
FDrawContext& DrawContext = Context.GetDrawContext();
const FSlateBrush* Brush = Context.GetHelper().GetWhiteBrush();
// const FSlateFontInfo& Font = Context.GetHelper().GetEventFont();
const TSharedRef<FSlateFontMeasure> FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
const double TopValue = ViewportY.GetValueAtOffset(RoundedViewHeight);
const double GridValue = ViewportY.GetValueAtOffset(MinDY);
const double BottomValue = ViewportY.GetValueAtOffset(0.0f);
const double Delta = GridValue - BottomValue;
double Precision = FMath::Abs(TopValue - BottomValue);
for (int32 Digit = FMath::Abs(LabelDecimalDigitCount); Digit > 0; --Digit)
{
Precision *= 10.0;
}
if (Delta > 0.0)
{
int64 DeltaBytes = static_cast<int64>(Delta);
if (DeltaBytes <= 0)
{
DeltaBytes = 1;
}
// Compute rounding based on magnitude of visible range of values (Delta).
int64 Delta2 = DeltaBytes;
int64 Power2 = 1;
while (Delta2 > 0)
{
Delta2 >>= 1;
Power2 <<= 1;
}
if (Power2 >> 1 == DeltaBytes)
{
Power2 >>= 1;
}
const double Grid = static_cast<double>(Power2);
const double StartValue = FMath::GridSnap(BottomValue, Grid);
TDrawHorizontalAxisLabelParams Params(DrawContext, Brush, FontMeasureService);
Params.TextBgColor = FLinearColor(0.05f, 0.05f, 0.05f, 1.0f);
Params.TextColor = FLinearColor(1.0f, 1.0f, 1.0f, 1.0f);
Params.X = X0;
// Params.Y;
// Params.Value;
Params.Precision = Precision;
const FLinearColor GridColor(0.0f, 0.0f, 0.0f, 0.1f);
for (double Value = StartValue; Value < TopValue; Value += Grid)
{
const float Y = Y0 + RoundedViewHeight - FMath::RoundToFloat(ViewportY.GetOffsetForValue(Value));
const float LabelY = FMath::Min(MaxLabelY, FMath::Max(MinLabelY, Y - TextH / 2));
// Do not overlap with the min/max values.
if (bHasMinMax && (FMath::Abs(LabelY - MinValueY) < TextH || FMath::Abs(LabelY - MaxValueY) < TextH))
{
continue;
}
// Draw horizontal grid line.
DrawContext.DrawBox(0, Y, ViewWidth, 1, Brush, GridColor);
// Draw label.
Params.Y = LabelY;
Params.Value = Value;
DrawHorizontalAxisLabel(Params);
}
}
if (bHasMinMax && GetHeight() >= TextH)
{
TDrawHorizontalAxisLabelParams Params(DrawContext, Brush, FontMeasureService);
if (MainSeries.IsValid() || AllSeries.Num() == 1)
{
Params.TextBgColor = (Series->GetColor() * 0.05f).CopyWithNewOpacity(1.0f);
Params.TextColor = Series->GetBorderColor().CopyWithNewOpacity(1.0f);
}
else
{
Params.TextBgColor = FLinearColor(0.02f, 0.02f, 0.02f, 1.0f);
Params.TextColor = FLinearColor(1.0f, 1.0f, 1.0f, 1.0f);
}
Params.X = X0;
// Params.Y;
// Params.Value;
Params.Precision = -Precision; // format with detailed text
int32 MinMaxAxis = 0;
// Draw horizontal axis at the max value.
if (MaxValueY >= Y0 && MaxValueY <= Y0 + RoundedViewHeight)
{
Params.Value = AllSeriesMaxValue;
Params.Y = MaxValueY;
DrawHorizontalAxisLabel(Params);
++MinMaxAxis;
}
// Draw horizontal axis at the min value.
if (MinValueY >= Y0 && MinValueY <= Y0 + RoundedViewHeight && FMath::Abs(MaxValueY - MinValueY) > TextH)
{
Params.Y = MinValueY;
Params.Value = AllSeriesMinValue;
DrawHorizontalAxisLabel(Params);
++MinMaxAxis;
}
if (MinMaxAxis == 2)
{
const float MX = Context.GetMousePosition().X;
const float MY = Context.GetMousePosition().Y;
// constexpr float MX1 = 80.0f; // start fading out
constexpr float MX2 = 120.0f; // completly faded out
if (MX > ViewWidth - MX2 && MY >= MaxValueY && MY < MinValueY + TextH)
{
// const float Alpha = FMath::Min(1.0f, (MX - ViewWidth + MX2) / (MX2 - MX1));
// Params.TextBgColor.A = Alpha;
// Params.TextColor.A = Alpha;
const float LineX = MX - 16.0f;
DrawContext.DrawBox(DrawContext.LayerId + 1, LineX, ActualMaxValueY, X0 - LineX, 1.0f, Params.Brush, Params.TextBgColor);
DrawContext.DrawBox(DrawContext.LayerId + 1, LineX, ActualMaxValueY, 1.0f, ActualMinValueY - ActualMaxValueY, Params.Brush, Params.TextBgColor);
DrawContext.DrawBox(DrawContext.LayerId + 1, LineX, ActualMinValueY, X0 - LineX, 1.0f, Params.Brush, Params.TextBgColor);
DrawContext.LayerId += 3; // ensure to draw on top of other labels
Params.X = MX;
Params.Y = MY - TextH / 2;
Params.Value = AllSeriesMaxValue - AllSeriesMinValue;
Params.Precision = -Precision * 0.1; // format with detailed text and increased precision
Params.Prefix = TEXT("\u0394=");
DrawHorizontalAxisLabel(Params);
}
}
}
DrawContext.LayerId += 3;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FMemoryGraphTrack::DrawHorizontalAxisLabel(const TDrawHorizontalAxisLabelParams& Params) const
{
FString LabelText = Params.Prefix;
if (FMath::IsNearlyZero(Params.Value, 0.5))
{
LabelText += TEXT("0");
}
else
{
double UnitValue;
const TCHAR* UnitText;
GetUnit(LabelUnit, FMath::Abs(Params.Precision), UnitValue, UnitText);
LabelText += FormatValue(Params.Value, UnitValue, UnitText, LabelDecimalDigitCount);
if (Params.Precision < 0 && LabelUnit == EGraphTrackLabelUnit::Auto)
{
double ValueUnitValue;
const TCHAR* ValueUnitText;
GetUnit(LabelUnit, Params.Value, ValueUnitValue, ValueUnitText);
if (ValueUnitValue > UnitValue)
{
FString LabelTextDetail = FormatValue(Params.Value, ValueUnitValue, ValueUnitText, LabelDecimalDigitCount);
LabelText += TEXT(" (");
LabelText += LabelTextDetail;
LabelText += TEXT(")");
}
}
}
const FVector2D TextSize = Params.FontMeasureService->Measure(LabelText, Font);
constexpr float TextH = 14.0f;
// Draw background for value text.
Params.DrawContext.DrawBox(Params.DrawContext.LayerId + 1, Params.X - TextSize.X - 4.0f, Params.Y, TextSize.X + 4.0f, TextH, Params.Brush, Params.TextBgColor);
// Draw value text.
Params.DrawContext.DrawText(Params.DrawContext.LayerId + 2, Params.X - TextSize.X - 2.0f, Params.Y + 1.0f, LabelText, Font, Params.TextColor);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FMemoryGraphTrack::GetUnit(const EGraphTrackLabelUnit InLabelUnit, const double InPrecision, double& OutUnitValue, const TCHAR*& OutUnitText)
{
constexpr double KiB = (double)(1LL << 10); // 2^10 bytes
constexpr double MiB = (double)(1LL << 20); // 2^20 bytes
constexpr double GiB = (double)(1LL << 30); // 2^30 bytes
constexpr double TiB = (double)(1LL << 40); // 2^40 bytes
constexpr double PiB = (double)(1LL << 50); // 2^50 bytes
constexpr double EiB = (double)(1LL << 60); // 2^60 bytes
switch (InLabelUnit)
{
case EGraphTrackLabelUnit::Auto: {
if (InPrecision >= EiB)
{
OutUnitValue = EiB;
OutUnitText = TEXT("EiB");
}
else if (InPrecision >= PiB)
{
OutUnitValue = PiB;
OutUnitText = TEXT("PiB");
}
else if (InPrecision >= TiB)
{
OutUnitValue = TiB;
OutUnitText = TEXT("TiB");
}
else if (InPrecision >= GiB)
{
OutUnitValue = GiB;
OutUnitText = TEXT("GiB");
}
else if (InPrecision >= MiB)
{
OutUnitValue = MiB;
OutUnitText = TEXT("MiB");
}
else if (InPrecision >= KiB)
{
OutUnitValue = KiB;
OutUnitText = TEXT("KiB");
}
else
{
OutUnitValue = 1.0;
OutUnitText = TEXT("B");
}
}
break;
case EGraphTrackLabelUnit::KiB:
OutUnitValue = KiB;
OutUnitText = TEXT("KiB");
break;
case EGraphTrackLabelUnit::MiB:
OutUnitValue = MiB;
OutUnitText = TEXT("MiB");
break;
case EGraphTrackLabelUnit::GiB:
OutUnitValue = GiB;
OutUnitText = TEXT("GiB");
break;
case EGraphTrackLabelUnit::TiB:
OutUnitValue = TiB;
OutUnitText = TEXT("TiB");
break;
case EGraphTrackLabelUnit::PiB:
OutUnitValue = PiB;
OutUnitText = TEXT("PiB");
break;
case EGraphTrackLabelUnit::EiB:
OutUnitValue = EiB;
OutUnitText = TEXT("EiB");
break;
case EGraphTrackLabelUnit::Byte:
default:
OutUnitValue = 1.0;
OutUnitText = TEXT("B");
break;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
FString FMemoryGraphTrack::FormatValue(const double InValue, const double InUnitValue, const TCHAR* InUnitText, const int32 InDecimalDigitCount)
{
FString OutText;
TCHAR FormatString[32];
FCString::Snprintf(FormatString, sizeof(FormatString), TEXT("%%.%df"), FMath::Abs(InDecimalDigitCount));
OutText = FString::Printf(FormatString, InValue / InUnitValue);
if (InDecimalDigitCount < 0)
{
// Remove ending 0s.
while (OutText.Len() > 0 && OutText[OutText.Len() - 1] == TEXT('0'))
{
OutText.RemoveAt(OutText.Len() - 1, 1);
}
// Remove ending dot.
if (OutText.Len() > 0 && OutText[OutText.Len() - 1] == TEXT('.'))
{
OutText.RemoveAt(OutText.Len() - 1, 1);
}
}
OutText += TEXT(' ');
OutText += InUnitText;
return OutText;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FMemoryGraphTrack::SetAvailableTrackHeight(EMemoryTrackHeightMode InMode, float InTrackHeight)
{
check(static_cast<uint32>(InMode) < static_cast<uint32>(EMemoryTrackHeightMode::Count));
AvailableTrackHeights[static_cast<uint32>(InMode)] = InTrackHeight;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FMemoryGraphTrack::SetCurrentTrackHeight(EMemoryTrackHeightMode InMode)
{
check(static_cast<uint32>(InMode) < static_cast<uint32>(EMemoryTrackHeightMode::Count));
SetHeight(AvailableTrackHeights[static_cast<uint32>(InMode)]);
const float TrackHeight = GetHeight();
for (TSharedPtr<FGraphSeries> Series: AllSeries)
{
Series->SetBaselineY(TrackHeight - 1.0f);
Series->SetDirtyFlag();
}
SetDirtyFlag();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void FMemoryGraphTrack::InitTooltip(FTooltipDrawState& InOutTooltip, const ITimingEvent& InTooltipEvent) const
{
if (InTooltipEvent.CheckTrack(this) && InTooltipEvent.Is<FGraphTrackEvent>())
{
const FGraphTrackEvent& TooltipEvent = InTooltipEvent.As<FGraphTrackEvent>();
const TSharedRef<const FMemoryGraphSeries> Series = StaticCastSharedRef<const FMemoryGraphSeries, const FGraphSeries>(TooltipEvent.GetSeries());
InOutTooltip.ResetContent();
InOutTooltip.AddTitle(Series->GetName().ToString(), Series->GetColor());
FString SubTitle = FString::Printf(TEXT("(tag id %lli, tracker id %lli)"), (int64)Series->GetTagId(), (int64)Series->GetTrackerId());
InOutTooltip.AddTitle(SubTitle, Series->GetColor());
InOutTooltip.AddNameValueTextLine(TEXT("Time:"), TimeUtils::FormatTimeAuto(TooltipEvent.GetStartTime()));
if (Series->HasEventDuration())
{
InOutTooltip.AddNameValueTextLine(TEXT("Duration:"), TimeUtils::FormatTimeAuto(TooltipEvent.GetDuration()));
}
InOutTooltip.AddNameValueTextLine(TEXT("Value:"), Series->FormatValue(TooltipEvent.GetValue()));
InOutTooltip.UpdateLayout();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
#undef LOCTEXT_NAMESPACE