// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.IO; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; using System.Linq; namespace MemoryProfiler2 { /// Holds the size and callstack associated with a live allocation. public struct FLiveAllocationInfo { /// Size of allocation. public readonly long Size; /// Index of callstack that performed allocation. public readonly int CallStackIndex; /// Tags index associated with this allocation. public readonly int TagsIndex; /// Constructor, initializing all member variables to passed in values. public FLiveAllocationInfo(long InSize, int InCallStackIndex, int InTagsIndex) { Size = InSize; CallStackIndex = InCallStackIndex; TagsIndex = InTagsIndex; } } /// Holds the allocation size and count associated with a given group of tags. public struct FCallStackTagsAllocationInfo { /// Size of allocation. public long Size; /// Number of allocations. public int Count; /// Constructor, initializing all member variables to passed in values. public FCallStackTagsAllocationInfo(long InSize, int InCount) { Size = InSize; Count = InCount; } public static FCallStackTagsAllocationInfo operator+(FCallStackTagsAllocationInfo LHS, FCallStackTagsAllocationInfo RHS) { return new FCallStackTagsAllocationInfo(LHS.Size + RHS.Size, LHS.Count + RHS.Count); } public static FCallStackTagsAllocationInfo operator-(FCallStackTagsAllocationInfo LHS, FCallStackTagsAllocationInfo RHS) { return new FCallStackTagsAllocationInfo(LHS.Size - RHS.Size, LHS.Count - RHS.Count); } } /// Helper class encapsulating information for a callstack and associated allocation size. Shared with stream parser. public class FCallStackAllocationInfo { /// Index of callstack that performed allocation. public readonly int CallStackIndex; /// Allocation info not associated with any tags. FCallStackTagsAllocationInfo UntaggedAllocationInfo; /// Mapping between a tags index and the associated allocation info. Dictionary TaggedAllocationInfo; /// Total size of allocation. public long TotalSize { get { long TotalSize = UntaggedAllocationInfo.Size; if (TaggedAllocationInfo != null) { foreach (var TaggedAllocationInfoPair in TaggedAllocationInfo) { TotalSize += TaggedAllocationInfoPair.Value.Size; } } return TotalSize; } } /// Total number of allocations. public int TotalCount { get { int TotalCount = UntaggedAllocationInfo.Count; if (TaggedAllocationInfo != null) { foreach (var TaggedAllocationInfoPair in TaggedAllocationInfo) { TotalCount += TaggedAllocationInfoPair.Value.Count; } } return TotalCount; } } /// Constructor, initializing all member variables to passed in values. public FCallStackAllocationInfo( long InSize, int InCallStackIndex, int InCount, int InTagsIndex ) { CallStackIndex = InCallStackIndex; UntaggedAllocationInfo = new FCallStackTagsAllocationInfo(0, 0); TaggedAllocationInfo = null; if (InTagsIndex == -1) { UntaggedAllocationInfo += new FCallStackTagsAllocationInfo(InSize, InCount); } else { EnsureTaggedAllocationsAvailable(); TaggedAllocationInfo.Add(InTagsIndex, new FCallStackTagsAllocationInfo(InSize, InCount)); } } /// Private constructor used by DeepCopy. private FCallStackAllocationInfo( int InCallStackIndex, FCallStackTagsAllocationInfo InUntaggedAllocationInfo, Dictionary InTaggedAllocationInfo ) { CallStackIndex = InCallStackIndex; UntaggedAllocationInfo = InUntaggedAllocationInfo; TaggedAllocationInfo = InTaggedAllocationInfo; } /// Performs a deep copy of the relevant data structures. public FCallStackAllocationInfo DeepCopy() { return new FCallStackAllocationInfo( CallStackIndex, UntaggedAllocationInfo, TaggedAllocationInfo != null ? new Dictionary(TaggedAllocationInfo) : null ); } /// Call prior to adding data to TaggedAllocationInfo. private void EnsureTaggedAllocationsAvailable() { if (TaggedAllocationInfo == null) { TaggedAllocationInfo = new Dictionary(16); } } /// /// Adds the passed in size and count to this callstack info. /// /// Size to add /// Count to add /// Index of the tags associated with the allocation public void Add( long SizeToAdd, int CountToAdd, int InTagsIndex ) { if (InTagsIndex == -1) { UntaggedAllocationInfo += new FCallStackTagsAllocationInfo(SizeToAdd, CountToAdd); } else { EnsureTaggedAllocationsAvailable(); FCallStackTagsAllocationInfo CurrentTagsAllocationInfo; if (!TaggedAllocationInfo.TryGetValue(InTagsIndex, out CurrentTagsAllocationInfo)) { CurrentTagsAllocationInfo = new FCallStackTagsAllocationInfo(0, 0); } TaggedAllocationInfo[InTagsIndex] = CurrentTagsAllocationInfo + new FCallStackTagsAllocationInfo(SizeToAdd, CountToAdd); } } /// /// Adds the passed callstack info to this callstack info. /// /// Callstack info to add public void Add(FCallStackAllocationInfo InOther) { if (CallStackIndex != InOther.CallStackIndex) { throw new InvalidDataException(); } UntaggedAllocationInfo += InOther.UntaggedAllocationInfo; if (InOther.TaggedAllocationInfo != null) { EnsureTaggedAllocationsAvailable(); foreach (var TaggedAllocationInfoPair in InOther.TaggedAllocationInfo) { FCallStackTagsAllocationInfo CurrentTagsAllocationInfo; if (!TaggedAllocationInfo.TryGetValue(TaggedAllocationInfoPair.Key, out CurrentTagsAllocationInfo)) { CurrentTagsAllocationInfo = new FCallStackTagsAllocationInfo(0, 0); } TaggedAllocationInfo[TaggedAllocationInfoPair.Key] = CurrentTagsAllocationInfo + TaggedAllocationInfoPair.Value; } } } /// /// Diffs the two passed in callstack infos and returns the difference. /// /// Newer callstack info to subtract older from /// Older callstack info to subtract from older public static FCallStackAllocationInfo Diff( FCallStackAllocationInfo New, FCallStackAllocationInfo Old ) { if( New.CallStackIndex != Old.CallStackIndex ) { throw new InvalidDataException(); } FCallStackAllocationInfo DiffData = New.DeepCopy(); DiffData.UntaggedAllocationInfo -= Old.UntaggedAllocationInfo; if (Old.TaggedAllocationInfo != null) { DiffData.EnsureTaggedAllocationsAvailable(); foreach (var TaggedAllocationInfoPair in Old.TaggedAllocationInfo) { FCallStackTagsAllocationInfo CurrentTagsAllocationInfo; if (!DiffData.TaggedAllocationInfo.TryGetValue(TaggedAllocationInfoPair.Key, out CurrentTagsAllocationInfo)) { CurrentTagsAllocationInfo = new FCallStackTagsAllocationInfo(0, 0); } DiffData.TaggedAllocationInfo[TaggedAllocationInfoPair.Key] = CurrentTagsAllocationInfo - TaggedAllocationInfoPair.Value; } } return DiffData; } /// /// Get a new callstack info that only contains information about allocations that match the given tags (note: this removes any tag information from the resultant object). /// /// Tags to filter on /// true to include alloctions that match the tags, false to include allocations that don't match the tags public FCallStackAllocationInfo GetAllocationInfoForTags(ISet InActiveTags, bool bInclusiveFilter) { long TagsSize; int TagsCount; GetSizeAndCountForTags(InActiveTags, bInclusiveFilter, out TagsSize, out TagsCount); return new FCallStackAllocationInfo(TagsSize, CallStackIndex, TagsCount, -1); } public void GetSizeAndCountForTags(ISet InActiveTags, bool bInclusiveFilter, out long OutSize, out int OutCount) { OutSize = 0; OutCount = 0; if (AllocationMatchesTagFilter(FStreamInfo.GlobalInstance.TagHierarchy.GetUntaggedNode().GetTag().Value, InActiveTags, bInclusiveFilter)) { OutSize += UntaggedAllocationInfo.Size; OutCount += UntaggedAllocationInfo.Count; } if (TaggedAllocationInfo != null) { foreach (var TaggedAllocationInfoPair in TaggedAllocationInfo) { if (AllocationMatchesTagFilter(TaggedAllocationInfoPair.Key, InActiveTags, bInclusiveFilter)) { OutSize += TaggedAllocationInfoPair.Value.Size; OutCount += TaggedAllocationInfoPair.Value.Count; } } } } private bool AllocationMatchesTagFilter(FAllocationTag InTag, ISet InActiveTags, bool bInclusiveFilter) { if (InActiveTags == null || InActiveTags.Count == 0) { // No filter, so match anything return true; } return InActiveTags.Contains(InTag) ? bInclusiveFilter : !bInclusiveFilter; } private bool AllocationMatchesTagFilter(int InTagsIndex, ISet InActiveTags, bool bInclusiveFilter) { if (InActiveTags == null || InActiveTags.Count == 0) { // No filter, so match anything return true; } // Inclusive filter will pass if we match ANY tag // Exclusive filter will pass if we match NO tags foreach (var AllocTag in FStreamInfo.GlobalInstance.TagsArray[InTagsIndex].Tags) { if (InActiveTags.Contains(AllocTag)) { return bInclusiveFilter; } } return !bInclusiveFilter; } } public enum ESliceTypesV4 { PlatformUsedPhysical, BinnedWasteCurrent, BinnedUsedCurrent, BinnedSlackCurrent, MemoryProfilingOverhead, OverallAllocatedMemory, Count }; public class FMemorySlice { public long[] MemoryInfo = null; public FMemorySlice( FStreamSnapshot Snapshot ) { MemoryInfo = new long[] { Snapshot.MemoryAllocationStats4[FMemoryAllocationStatsV4.PlatformUsedPhysical], Snapshot.MemoryAllocationStats4[FMemoryAllocationStatsV4.BinnedWasteCurrent], Snapshot.MemoryAllocationStats4[FMemoryAllocationStatsV4.BinnedUsedCurrent], Snapshot.MemoryAllocationStats4[FMemoryAllocationStatsV4.BinnedSlackCurrent], Snapshot.MemoryAllocationStats4[FMemoryAllocationStatsV4.MemoryProfilingOverhead], Snapshot.AllocationSize, }; } public long GetSliceInfoV4( ESliceTypesV4 SliceType ) { return ( MemoryInfo[(int)SliceType] ); } } /// Snapshot of allocation state at a specific time in token stream. public class FStreamSnapshot { /// User defined description of time of snapshot. public string Description; /// List of callstack allocations. public List ActiveCallStackList; /// List of lifetime callstack allocations for memory churn. public List LifetimeCallStackList; /// Position in the stream. public ulong StreamIndex; /// Frame number. public int FrameNumber; /// Current time. public float CurrentTime; /// Current time starting from the previous snapshot marker. public float ElapsedTime; /// Snapshot type. public EProfilingPayloadSubType SubType = EProfilingPayloadSubType.SUBTYPE_SnapshotMarker; /// Index of snapshot. public int SnapshotIndex; /// Platform dependent array of memory metrics. public List MetricArray; /// A list of indices into the name table, one for each loaded level including persistent level. public List LoadedLevels = new List(); /// Array of names of all currently loaded levels formated for usage in details view tab. public List LoadedLevelNames = new List(); /// Generic memory allocation stats. public FMemoryAllocationStatsV4 MemoryAllocationStats4 = new FMemoryAllocationStatsV4(); /// Running count of number of allocations. public long AllocationCount = 0; /// Running total of size of allocations. public long AllocationSize = 0; /// Running total of size of allocations. public long AllocationMaxSize = 0; /// True if this snapshot was created as a diff of two snapshots. public bool bIsDiffResult; /// Running total of allocated memory. public List OverallMemorySlice; /// Constructor, naming the snapshot and initializing map. public FStreamSnapshot( string InDescription ) { Description = InDescription; ActiveCallStackList = new List(); // Presize lifetime callstack array and populate. LifetimeCallStackList = new List( FStreamInfo.GlobalInstance.CallStackArray.Count ); for( int CallStackIndex=0; CallStackIndex(); } /// Performs a deep copy of the relevant data structures. public FStreamSnapshot DeepCopy( Dictionary PointerToPointerInfoMap ) { // Create new snapshot object. FStreamSnapshot Snapshot = new FStreamSnapshot( "Copy" ); // Manually perform a deep copy of LifetimeCallstackList foreach( FCallStackAllocationInfo AllocationInfo in LifetimeCallStackList ) { Snapshot.LifetimeCallStackList[ AllocationInfo.CallStackIndex ] = AllocationInfo.DeepCopy(); } Snapshot.AllocationCount = AllocationCount; Snapshot.AllocationSize = AllocationSize; Snapshot.AllocationMaxSize = AllocationMaxSize; Snapshot.FinalizeSnapshot( PointerToPointerInfoMap ); // Return deep copy of this snapshot. return Snapshot; } public void FillActiveCallStackList( Dictionary PointerToPointerInfoMap ) { ActiveCallStackList.Clear(); ActiveCallStackList.Capacity = LifetimeCallStackList.Count; foreach( var PointerData in PointerToPointerInfoMap ) { // make sure allocationInfoList is big enough while(PointerData.Value.CallStackIndex >= ActiveCallStackList.Count ) { ActiveCallStackList.Add( new FCallStackAllocationInfo( 0, ActiveCallStackList.Count, 0, -1 ) ); } ActiveCallStackList[PointerData.Value.CallStackIndex].Add(PointerData.Value.Size, 1, PointerData.Value.TagsIndex); } // strip out any callstacks with no allocations ActiveCallStackList.RemoveAll(Item => Item.TotalCount == 0); ActiveCallStackList.TrimExcess(); } /// Convert "callstack to allocation" mapping (either passed in or generated from pointer map) to callstack info list. public void FinalizeSnapshot( Dictionary PointerToPointerInfoMap ) { FillActiveCallStackList( PointerToPointerInfoMap ); } /// Diffs two snapshots and creates a result one. public static FStreamSnapshot DiffSnapshots( FStreamSnapshot Old, FStreamSnapshot New ) { // Create result snapshot object. FStreamSnapshot ResultSnapshot = new FStreamSnapshot( "Diff " + Old.Description + " <-> " + New.Description ); using( FScopedLogTimer LoadingTime = new FScopedLogTimer( "FStreamSnapshot.DiffSnapshots" ) ) { // Copy over allocation count so we can track where the graph starts ResultSnapshot.AllocationCount = Old.AllocationCount; Debug.Assert( Old.MetricArray.Count == New.MetricArray.Count ); ResultSnapshot.MetricArray = new List( Old.MetricArray.Count ); for( int CallstackIndex = 0; CallstackIndex < Old.MetricArray.Count; CallstackIndex++ ) { ResultSnapshot.MetricArray.Add( New.MetricArray[ CallstackIndex ] - Old.MetricArray[ CallstackIndex ] ); } ResultSnapshot.MemoryAllocationStats4 = FMemoryAllocationStatsV4.Diff( Old.MemoryAllocationStats4, New.MemoryAllocationStats4 ); ResultSnapshot.StreamIndex = New.StreamIndex; ResultSnapshot.bIsDiffResult = true; ResultSnapshot.AllocationMaxSize = New.AllocationMaxSize - Old.AllocationMaxSize; ResultSnapshot.AllocationSize = New.AllocationSize - Old.AllocationSize; ResultSnapshot.CurrentTime = 0; ResultSnapshot.ElapsedTime = New.CurrentTime - Old.CurrentTime; ResultSnapshot.FrameNumber = New.FrameNumber - Old.FrameNumber; ResultSnapshot.LoadedLevels = New.LoadedLevels; // These lists are guaranteed to be sorted by callstack index. List OldActiveCallStackList = Old.ActiveCallStackList; List NewActiveCallStackList = New.ActiveCallStackList; List ResultActiveCallStackList = new List( FStreamInfo.GlobalInstance.CallStackArray.Count ); int OldIndex = 0; int NewIndex = 0; while( true ) { FCallStackAllocationInfo OldAllocInfo = OldActiveCallStackList[ OldIndex ]; FCallStackAllocationInfo NewAllocInfo = NewActiveCallStackList[ NewIndex ]; if( OldAllocInfo.CallStackIndex == NewAllocInfo.CallStackIndex ) { long ResultSize = NewAllocInfo.TotalSize - OldAllocInfo.TotalSize; int ResultCount = NewAllocInfo.TotalCount - OldAllocInfo.TotalCount; if( ResultSize != 0 || ResultCount != 0 ) { ResultActiveCallStackList.Add( new FCallStackAllocationInfo( ResultSize, NewAllocInfo.CallStackIndex, ResultCount, -1 ) ); } OldIndex++; NewIndex++; } else if( OldAllocInfo.CallStackIndex > NewAllocInfo.CallStackIndex ) { ResultActiveCallStackList.Add( NewAllocInfo ); NewIndex++; } else // OldAllocInfo.CallStackIndex < NewAllocInfo.CallStackIndex { ResultActiveCallStackList.Add( new FCallStackAllocationInfo( -OldAllocInfo.TotalSize, OldAllocInfo.CallStackIndex, -OldAllocInfo.TotalCount, -1 ) ); OldIndex++; } if( OldIndex >= OldActiveCallStackList.Count ) { for( ; NewIndex < NewActiveCallStackList.Count; NewIndex++ ) { ResultActiveCallStackList.Add( NewActiveCallStackList[ NewIndex ] ); } break; } if( NewIndex >= NewActiveCallStackList.Count ) { for( ; OldIndex < OldActiveCallStackList.Count; OldIndex++ ) { ResultActiveCallStackList.Add( OldActiveCallStackList[ OldIndex ] ); } break; } } // Check that list was correctly constructed. for( int CallstackIndex = 0; CallstackIndex < ResultActiveCallStackList.Count - 1; CallstackIndex++ ) { Debug.Assert( ResultActiveCallStackList[ CallstackIndex ].CallStackIndex < ResultActiveCallStackList[ CallstackIndex + 1 ].CallStackIndex ); } ResultActiveCallStackList.TrimExcess(); ResultSnapshot.ActiveCallStackList = ResultActiveCallStackList; // Iterate over new lifetime callstack info and subtract previous one. for( int CallStackIndex = 0; CallStackIndex < New.LifetimeCallStackList.Count; CallStackIndex++ ) { ResultSnapshot.LifetimeCallStackList[ CallStackIndex ] = FCallStackAllocationInfo.Diff( New.LifetimeCallStackList[ CallStackIndex ], Old.LifetimeCallStackList[ CallStackIndex ] ); } // Handle overall memory timeline if( New.OverallMemorySlice.Count > Old.OverallMemorySlice.Count ) { ResultSnapshot.OverallMemorySlice = new List( New.OverallMemorySlice ); ResultSnapshot.OverallMemorySlice.RemoveRange( 0, Old.OverallMemorySlice.Count ); } else { ResultSnapshot.OverallMemorySlice = new List( Old.OverallMemorySlice ); ResultSnapshot.OverallMemorySlice.RemoveRange( 0, New.OverallMemorySlice.Count ); ResultSnapshot.OverallMemorySlice.Reverse(); } } return ResultSnapshot; } /// Exports this snapshot to a CSV file of the passed in name. /// File name to export to /// Whether to export active allocations or lifetime allocations public void ExportToCSV( string FileName, bool bShouldExportActiveAllocations ) { // Create stream writer used to output call graphs to CSV. StreamWriter CSVWriter = new StreamWriter(FileName); // Figure out which list to export. List CallStackList = null; if( bShouldExportActiveAllocations ) { CallStackList = ActiveCallStackList; } else { CallStackList = LifetimeCallStackList; } // Iterate over each unique call graph and spit it out. The sorting is per call stack and not // allocation size. Excel can be used to sort by allocation if needed. This sorting is more robust // and also what the call graph parsing code needs. foreach( FCallStackAllocationInfo AllocationInfo in CallStackList ) { // Skip callstacks with no contribution in this snapshot. if( AllocationInfo.TotalCount > 0 ) { // Dump size first, followed by count. CSVWriter.Write(AllocationInfo.TotalSize + "," + AllocationInfo.TotalCount + ","); // Iterate over ach address in callstack and dump to CSV. FCallStack CallStack = FStreamInfo.GlobalInstance.CallStackArray[AllocationInfo.CallStackIndex]; foreach( int AddressIndex in CallStack.AddressIndices ) { FCallStackAddress Address = FStreamInfo.GlobalInstance.CallStackAddressArray[AddressIndex]; string SymbolFunctionName = FStreamInfo.GlobalInstance.NameArray[Address.FunctionIndex]; string SymbolFileName = FStreamInfo.GlobalInstance.NameArray[Address.FilenameIndex]; // Write out function followed by filename and then line number if valid if( SymbolFunctionName != "" || SymbolFileName != "" ) { CSVWriter.Write( SymbolFunctionName + " @ " + SymbolFileName + ":" + Address.LineNumber + "," ); } else { CSVWriter.Write("Unknown,"); } } CSVWriter.WriteLine(""); } } // Close the file handle now that we're done writing. CSVWriter.Close(); } /// Exports this snapshots internal properties into a human readable format. public override string ToString() { StringBuilder StrBuilder = new StringBuilder( 1024 ); ///// User defined description of time of snapshot. //public string Description; StrBuilder.AppendLine( "Description: " + Description ); ///// List of callstack allocations. //public List ActiveCallStackList; ///// List of lifetime callstack allocations for memory churn. //public List LifetimeCallStackList; ///// Position in the stream. //public ulong StreamIndex; StrBuilder.AppendLine( "Stream Index: " + StreamIndex ); ///// Frame number. //public int FrameNumber; StrBuilder.AppendLine( "Frame Number: " + FrameNumber ); ///// Current time. //public float CurrentTime; StrBuilder.AppendLine( "Current Time: " + CurrentTime + " seconds" ); ///// Current time starting from the previous snapshot marker. //public float ElapsedTime; StrBuilder.AppendLine( "Elapsed Time: " + ElapsedTime + " seconds" ); ///// Snapshot type. //public EProfilingPayloadSubType SubType = EProfilingPayloadSubType.SUBTYPE_SnapshotMarker; StrBuilder.AppendLine( "Snapshot Type: " + SubType.ToString() ); ///// Index of snapshot. //public int SnapshotIndex; StrBuilder.AppendLine( "Snapshot Index: " + SnapshotIndex ); ///// Array of names of all currently loaded levels formated for usage in details view tab. //public List LoadedLevelNames = new List(); StrBuilder.AppendLine( "Loaded Levels: " + LoadedLevels.Count ); foreach( string LevelName in LoadedLevelNames ) { StrBuilder.AppendLine( " " + LevelName ); } ///// Generic memory allocation stats. //public FMemoryAllocationStats MemoryAllocationStats = new FMemoryAllocationStats(); StrBuilder.AppendLine( "Memory Allocation Stats: " ); StrBuilder.Append( MemoryAllocationStats4.ToString() ); ///// Running count of number of allocations. //public long AllocationCount = 0; StrBuilder.AppendLine( "Allocation Count: " + AllocationCount.ToString("N0") ); ///// Running total of size of allocations. //public long AllocationSize = 0; StrBuilder.AppendLine( "Allocation Size: " + MainWindow.FormatSizeString2( AllocationSize ) ); ///// Running total of size of allocations. //public long AllocationMaxSize = 0; StrBuilder.AppendLine( "Allocation Max Size: " + MainWindow.FormatSizeString2( AllocationMaxSize ) ); ///// True if this snapshot was created as a diff of two snapshots. //public bool bIsDiffResult; StrBuilder.AppendLine( "Is Diff Result: " + bIsDiffResult ); ///// Running total of allocated memory. //public List OverallMemorySlice; //StrBuilder.AppendLine( "Overall Memory Slice: @TODO" ); return StrBuilder.ToString(); } /// Encapsulates indices to levels in the diff snapshots in relation to start and end snapshot. struct FLevelIndex { public FLevelIndex( int InLeftIndex, int InRightIndex ) { LeftIndex = InLeftIndex; RightIndex = InRightIndex; } public override string ToString() { return LeftIndex + " <-> " + RightIndex; } public int LeftIndex; public int RightIndex; } /// /// Prepares three array with level names. Arrays will be placed into LoadedLevelNames properties of each of snapshots. /// "-" in level name means that level was unloaded. /// "+" in level name means that level was loaded. /// public static void PrepareLevelNamesForDetailsTab( FStreamSnapshot LeftSnapshot, FStreamSnapshot DiffSnapshot, FStreamSnapshot RightSnapshot ) { if( DiffSnapshot != null && LeftSnapshot != null && RightSnapshot != null ) { LeftSnapshot.LoadedLevelNames.Clear(); DiffSnapshot.LoadedLevelNames.Clear(); RightSnapshot.LoadedLevelNames.Clear(); List LevelIndexArray = new List( LeftSnapshot.LoadedLevels.Count + RightSnapshot.LoadedLevels.Count ); List AllLevelIndexArray = new List( LeftSnapshot.LoadedLevels ); AllLevelIndexArray.AddRange( RightSnapshot.LoadedLevels ); IEnumerable AllLevelIndicesDistinct = AllLevelIndexArray.Distinct(); foreach( int LevelIndex in AllLevelIndicesDistinct ) { int StartLevelIndex = LeftSnapshot.LoadedLevels.IndexOf( LevelIndex ); int EndLevelIndex = RightSnapshot.LoadedLevels.IndexOf( LevelIndex ); LevelIndexArray.Add( new FLevelIndex( StartLevelIndex, EndLevelIndex ) ); } foreach( FLevelIndex LevelIndex in LevelIndexArray ) { string LeftLevelName = ""; string DiffLevelName = ""; string RightLevelName = ""; if( LevelIndex.LeftIndex != -1 ) { LeftLevelName = FStreamInfo.GlobalInstance.NameArray[ LeftSnapshot.LoadedLevels[ LevelIndex.LeftIndex ] ]; } if( LevelIndex.RightIndex != -1 ) { RightLevelName = FStreamInfo.GlobalInstance.NameArray[ RightSnapshot.LoadedLevels[ LevelIndex.RightIndex ] ]; } if( LevelIndex.LeftIndex != -1 && LevelIndex.RightIndex == -1 ) { DiffLevelName = "-" + LeftLevelName; } else if( LevelIndex.LeftIndex == -1 && LevelIndex.RightIndex != -1 ) { DiffLevelName = "+" + RightLevelName; } else if( LevelIndex.LeftIndex != -1 && LevelIndex.RightIndex != -1 ) { DiffLevelName = " " + RightLevelName; } LeftSnapshot.LoadedLevelNames.Add( LeftLevelName ); DiffSnapshot.LoadedLevelNames.Add( DiffLevelName ); RightSnapshot.LoadedLevelNames.Add( RightLevelName ); } } else if( LeftSnapshot != null ) { LeftSnapshot.LoadedLevelNames.Clear(); foreach( int LevelIndex in LeftSnapshot.LoadedLevels ) { LeftSnapshot.LoadedLevelNames.Add( FStreamInfo.GlobalInstance.NameArray[ LevelIndex ] ); } } else if( RightSnapshot != null ) { RightSnapshot.LoadedLevelNames.Clear(); foreach( int LevelIndex in RightSnapshot.LoadedLevels ) { RightSnapshot.LoadedLevelNames.Add( FStreamInfo.GlobalInstance.NameArray[ LevelIndex ] ); } } } }; }