Skip to content

Commit 1f885ee

Browse files
authored
DeferredStackTracingImpl refactor (#698)
* Make helper methods private in DeferredStackTracingImpl * Extract GetAddrInfo method from loop in DeferredStackTracingImpl * Make GetAddrInfo responsible for resizing the hashtable ...and update it's name to make it's new behavior clearer * Inline variables * Add show stack traces debug action * Introduce skipping frames in stack tracing This gets rid of the first line in every stack trace: DeferredStackTracing.Postfix * Extract DeferredStackTracingImpl hashtable into a dedicated class * Make AddrTable.size a property * AddrTable: change fields to properties * AddrTable: change it to a struct Now it no longer introduces extra pointer chasing compared to the previous inline implementation in DeferredStackTracingImpl * AddrInfo: remove unused field Should improve cache locality and reduce memory usage * AddrTable: StartingN = 10 The table easily grows to this size. It can just start out at this size * Move MaxDepth to StackTraceLogItemRaw * Clarify tracing behavior
1 parent c950171 commit 1f885ee

File tree

7 files changed

+124
-114
lines changed

7 files changed

+124
-114
lines changed

Source/Client/Debug/DebugActions.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ namespace Multiplayer.Client
2222
{
2323
static class MpDebugActions
2424
{
25-
const string MultiplayerCategory = "Multiplayer";
25+
public const string MultiplayerCategory = "Multiplayer";
26+
// Actions in this category are not synced and only happen on the client.
27+
public const string MultiplayerLocalCategory = "Multiplayer local";
2628

2729
[DebugAction(MultiplayerCategory, actionType = DebugActionType.ToolWorld, allowedGameStates = AllowedGameStates.PlayingOnWorld)]
2830
public static void SpawnCaravans()
@@ -135,9 +137,7 @@ public static void SaveGame()
135137
File.WriteAllBytes($"game_{Multiplayer.username}.xml", data);
136138
}
137139

138-
public const string TriggerDesyncActionName = "Trigger desync";
139-
140-
[DebugAction(MultiplayerCategory, name = TriggerDesyncActionName, allowedGameStates = AllowedGameStates.Playing)]
140+
[DebugAction(MultiplayerLocalCategory, name = "Trigger desync", allowedGameStates = AllowedGameStates.Playing)]
141141
public static void TriggerDesync()
142142
{
143143
var logItem = StackTraceLogItemRaw.GetFromPool();
@@ -149,6 +149,20 @@ public static void TriggerDesync()
149149
Multiplayer.game.sync.TryAddStackTraceForDesyncLogRaw(logItem, depth, hash);
150150
}
151151

152+
[DebugAction(MultiplayerLocalCategory, name = "Show stack traces", allowedGameStates = AllowedGameStates.Playing)]
153+
public static void ShowStackTraces()
154+
{
155+
var syncCoordinator = Multiplayer.game.sync;
156+
var opinions = syncCoordinator.knownClientOpinions;
157+
if (opinions.Count == 0)
158+
{
159+
Log.Message("No opinions available");
160+
return;
161+
}
162+
var stackTrace = opinions[0].GetFormattedStackTracesForRange(-1);
163+
Log.Message($"Current opinion stack trace: \n{stackTrace}");
164+
}
165+
152166
[DebugAction(MultiplayerCategory, name = "Show pending player", allowedGameStates = AllowedGameStates.Playing)]
153167
public static void ShowPendingPlayer()
154168
{

Source/Client/Debug/DebugSync.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ static class DebugActionNodeEnter
272272
static void Prefix(DebugActionNode __instance)
273273
{
274274
if (Multiplayer.Client != null && __instance.action is { Target: not MpDebugAction } &&
275-
__instance.label != MpDebugActions.TriggerDesyncActionName)
275+
__instance.category != MpDebugActions.MultiplayerLocalCategory)
276276
__instance.action = new MpDebugAction { node = __instance, original = __instance.action }.Action;
277277
}
278278

Source/Client/Desyncs/DeferredStackTracing.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public static void Postfix()
3333
var logItem = StackTraceLogItemRaw.GetFromPool();
3434
var trace = logItem.raw;
3535
int hash = 0;
36-
int depth = DeferredStackTracingImpl.TraceImpl(trace, ref hash);
36+
// Skip this (DeferredStackTracing.Postfix) frame. Keeping it in the trace doesn't provide any value.
37+
int depth = DeferredStackTracingImpl.TraceImpl(trace, ref hash, skipFrames: 1);
3738

3839
Multiplayer.game.sync.TryAddStackTraceForDesyncLogRaw(logItem, depth, hash);
3940

Source/Client/Desyncs/StackTraceLogItem.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public class StackTraceLogItemObj : StackTraceLogItem
3737

3838
public class StackTraceLogItemRaw : StackTraceLogItem
3939
{
40-
public long[] raw = new long[DeferredStackTracingImpl.MaxDepth];
40+
public const int MaxDepth = 32;
41+
public long[] raw = new long[MaxDepth];
4142
public int depth;
4243

4344
public int ticksGame;

Source/Client/UI/DebugPanel/SyncDebugPanel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,8 +530,8 @@ private static float DrawRngDebugSection(float x, float y, float width)
530530
DebugLine[] rngLines = [
531531
new("Rand Calls:", $"{DeferredStackTracing.acc}", Color.white),
532532
new("Max Trace Depth:", $"{DeferredStackTracing.maxTraceDepth}", Color.white),
533-
new("Hash Entries:", $"{DeferredStackTracingImpl.hashtableEntries}/{DeferredStackTracingImpl.hashtableSize}", Color.white),
534-
new("Hash Collisions:", $"{DeferredStackTracingImpl.collisions}", Color.white)
533+
new("Hash Entries:", $"{DeferredStackTracingImpl.hashTable.Entries}/{DeferredStackTracingImpl.hashTable.Size}", Color.white),
534+
new("Hash Collisions:", $"{DeferredStackTracingImpl.hashTable.Collisions}", Color.white)
535535
];
536536

537537
return DrawSection(x, y, width, new("RNG & DEBUG", rngLines));

Source/Client/UI/IngameDebug.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ internal static void DoDebugPrintout()
6060
text.AppendLine($"\nBuffered Changes: {SyncFieldUtil.bufferedChanges.Sum(kv => kv.Value.Count)}");
6161
text.AppendLine($"Map Cmds: {async.cmds.Count}{separator} World Cmds: {Multiplayer.AsyncWorldTime.cmds.Count}");
6262
text.AppendLine($"Stack Trace: {DeferredStackTracing.acc}{separator} Max Depth: {DeferredStackTracing.maxTraceDepth}");
63-
text.AppendLine($"Hash: {DeferredStackTracingImpl.hashtableEntries}/{DeferredStackTracingImpl.hashtableSize} ({DeferredStackTracingImpl.collisions} collisions)");
63+
text.AppendLine($"Hash: {DeferredStackTracingImpl.hashTable.Entries}/{DeferredStackTracingImpl.hashTable.Size} ({DeferredStackTracingImpl.hashTable.Collisions} collisions)");
6464

6565
text.AppendLine($"\nIdeology: {Find.IdeoManager.classicMode}{separator} Ideo ID: {Find.IdeoManager.IdeosInViewOrder.FirstOrDefault()?.id ?? 0}");
6666
text.AppendLine($"Haul Dest: {Find.CurrentMap.haulDestinationManager.AllHaulDestinationsListForReading.Count}{separator} Designations: {Find.CurrentMap.designationManager.designationsByDef.Count}");

Source/Common/DeferredStackTracingImpl.cs

Lines changed: 98 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -3,82 +3,115 @@
33

44
namespace Multiplayer.Client.Desyncs;
55

6-
public static class DeferredStackTracingImpl
6+
public struct AddrTable()
77
{
8-
struct AddrInfo
8+
private const int StartingN = 10; // 1024
9+
private const int StartingShift = 64 - StartingN;
10+
private const int StartingSize = 1 << StartingN;
11+
private const float LoadFactor = 0.5f;
12+
13+
private AddrInfo[] hashtable = new AddrInfo[StartingSize];
14+
15+
public int Size => hashtable.Length;
16+
public int Entries { get; private set; } = 0;
17+
public int Collisions { get; private set; } = 0;
18+
19+
private int shift = StartingShift;
20+
21+
public ref AddrInfo GetOrCreateAddrInfo(long ret)
922
{
10-
public long addr;
11-
public long stackUsage;
12-
public long nameHash;
13-
public long unused;
23+
int indexmask = Size - 1;
24+
int index = (int)(HashAddr((ulong)ret) >> shift);
25+
ref var info = ref hashtable[index];
26+
int colls = 0;
27+
28+
// Open addressing
29+
while (info.addr != 0 && info.addr != ret)
30+
{
31+
index = (index + 1) & indexmask;
32+
info = ref hashtable[index];
33+
colls++;
34+
}
35+
if (colls > Collisions) Collisions = colls;
36+
37+
// When returning an unpopulated AddrInfo, assume it's going to get populated shortly and consider it used
38+
// immediately.
39+
if (info.addr == 0 && Entries++ > Size * LoadFactor) ResizeHashtable();
40+
return ref info;
1441
}
1542

16-
const int StartingN = 7;
17-
const int StartingShift = 64 - StartingN;
18-
const int StartingSize = 1 << StartingN;
19-
const float LoadFactor = 0.5f;
43+
private static ulong HashAddr(ulong addr) => ((addr >> 4) | addr << 60) * 11400714819323198485;
2044

21-
static AddrInfo[] hashtable = new AddrInfo[StartingSize];
22-
public static int hashtableSize = StartingSize;
23-
public static int hashtableEntries;
24-
public static int hashtableShift = StartingShift;
25-
public static int collisions;
45+
private void ResizeHashtable()
46+
{
47+
var oldTable = hashtable;
2648

49+
hashtable = new AddrInfo[Size * 2];
50+
shift--;
51+
Collisions = 0;
52+
53+
int indexmask = Size - 1;
54+
55+
for (int i = 0; i < oldTable.Length; i++)
56+
{
57+
ref var oldInfo = ref oldTable[i];
58+
if (oldInfo.addr != 0)
59+
{
60+
int index = (int)(HashAddr((ulong)oldInfo.addr) >> shift);
61+
62+
while (hashtable[index].addr != 0)
63+
index = (index + 1) & indexmask;
64+
65+
ref var newInfo = ref hashtable[index];
66+
newInfo.addr = oldInfo.addr;
67+
newInfo.stackUsage = oldInfo.stackUsage;
68+
newInfo.nameHash = oldInfo.nameHash;
69+
}
70+
}
71+
}
72+
}
73+
74+
public struct AddrInfo
75+
{
76+
public long addr;
77+
public long stackUsage;
78+
public long nameHash;
79+
}
80+
81+
public static class DeferredStackTracingImpl
82+
{
2783
const long NotJit = long.MaxValue;
2884
const long RbpBased = long.MaxValue - 1;
2985

3086
const long UsesRbpAsGpr = 1L << 50;
3187
const long UsesRbx = 1L << 51;
3288
const long RbpInfoClearMask = ~(UsesRbpAsGpr | UsesRbx);
3389

34-
public const int MaxDepth = 32;
3590
public const int HashInfluence = 6;
3691

37-
public static unsafe int TraceImpl(long[] traceIn, ref int hash)
92+
public static AddrTable hashTable = new();
93+
94+
public static unsafe int TraceImpl(long[] traceIn, ref int hash, int skipFrames = 0)
3895
{
3996
if (Native.LmfPtr == 0)
4097
return 0;
4198

42-
long[] trace = traceIn;
4399
long rbp = GetRbp();
44100

45101
long stck = rbp;
46102
rbp = *(long*)rbp;
47103

48-
int indexmask = hashtableSize - 1;
49-
int shift = hashtableShift;
50-
51-
long ret;
52104
long lmfPtr = *(long*)Native.LmfPtr;
53105

54-
int depth = 0;
55-
106+
int depth = 0; // frames seen
107+
int index = 0; // frames returned through long[] traceIn
56108
while (true)
57109
{
58-
ret = *(long*)(stck + 8);
59-
60-
int index = (int)(HashAddr((ulong)ret) >> shift);
61-
ref var info = ref hashtable[index];
62-
int colls = 0;
63-
64-
// Open addressing
65-
while (info.addr != 0 && info.addr != ret)
66-
{
67-
index = (index + 1) & indexmask;
68-
info = ref hashtable[index];
69-
colls++;
70-
}
71-
72-
if (colls > collisions)
73-
collisions = colls;
74-
75-
long stackUsage = 0;
76-
77-
if (info.addr != 0)
78-
stackUsage = info.stackUsage;
79-
else
80-
stackUsage = UpdateNewElement(ref info, ret);
110+
var ret = *(long*)(stck + 8);
111+
ref var info = ref hashTable.GetOrCreateAddrInfo(ret);
112+
if (info.addr == 0) UpdateNewElement(ref info, ret);
81113

114+
long stackUsage = info.stackUsage;
82115
if (stackUsage == NotJit)
83116
{
84117
// LMF (Last Managed Frame) layout on x64:
@@ -98,13 +131,18 @@ public static unsafe int TraceImpl(long[] traceIn, ref int hash)
98131
continue;
99132
}
100133

101-
trace[depth] = ret;
134+
if (depth >= skipFrames)
135+
{
136+
traceIn[index] = ret;
137+
138+
// info.nameHash == 0 marks methods to skip
139+
if (index < HashInfluence && info.nameHash != 0) hash = HashCombineInt(hash, (int)info.nameHash);
102140

103-
// info.nameHash == 0 marks methods to skip
104-
if (depth < HashInfluence && info.nameHash != 0)
105-
hash = HashCombineInt(hash, (int)info.nameHash);
141+
index++;
142+
}
106143

107-
if (info.nameHash != 0 && ++depth == MaxDepth)
144+
// traceIn length limits above all how many frames are visited, not how many are populated.
145+
if (info.nameHash != 0 && ++depth == traceIn.Length)
108146
break;
109147

110148
if (stackUsage == RbpBased)
@@ -129,63 +167,21 @@ public static unsafe int TraceImpl(long[] traceIn, ref int hash)
129167
stck += stackUsage;
130168
}
131169

132-
return depth;
170+
return index;
133171
}
134172

135-
static long UpdateNewElement(ref AddrInfo info, long ret)
136-
{
137-
long stackUsage = GetStackUsage(ret);
138173

174+
private static void UpdateNewElement(ref AddrInfo info, long ret)
175+
{
139176
info.addr = ret;
140-
info.stackUsage = stackUsage;
177+
info.stackUsage = GetStackUsage(ret);
141178

142179
var normalizedMethodNameBetweenOS = Native.MethodNameNormalizedFromAddr(ret, true);
143180

144181
info.nameHash =
145182
normalizedMethodNameBetweenOS == null ? 1 :
146183
Native.GetMethodAggressiveInlining(ret) ? 0 :
147184
StableStringHash(normalizedMethodNameBetweenOS);
148-
149-
hashtableEntries++;
150-
if (hashtableEntries > hashtableSize * LoadFactor)
151-
ResizeHashtable();
152-
153-
return stackUsage;
154-
}
155-
156-
static ulong HashAddr(ulong addr) => ((addr >> 4) | addr << 60) * 11400714819323198485;
157-
158-
static int ResizeHashtable()
159-
{
160-
var oldTable = hashtable;
161-
162-
hashtableSize *= 2;
163-
hashtableShift--;
164-
165-
hashtable = new AddrInfo[hashtableSize];
166-
collisions = 0;
167-
168-
int indexmask = hashtableSize - 1;
169-
int shift = hashtableShift;
170-
171-
for (int i = 0; i < oldTable.Length; i++)
172-
{
173-
ref var oldInfo = ref oldTable[i];
174-
if (oldInfo.addr != 0)
175-
{
176-
int index = (int)(HashAddr((ulong)oldInfo.addr) >> shift);
177-
178-
while (hashtable[index].addr != 0)
179-
index = (index + 1) & indexmask;
180-
181-
ref var newInfo = ref hashtable[index];
182-
newInfo.addr = oldInfo.addr;
183-
newInfo.stackUsage = oldInfo.stackUsage;
184-
newInfo.nameHash = oldInfo.nameHash;
185-
}
186-
}
187-
188-
return indexmask;
189185
}
190186

191187
static unsafe long GetStackUsage(long addr)
@@ -243,19 +239,17 @@ private static unsafe void CheckRbpUsage(uint* at, ref long stackUsage)
243239
}
244240

245241
[MethodImpl(MethodImplOptions.NoInlining)]
246-
static unsafe long GetRbp()
242+
private static unsafe long GetRbp()
247243
{
248244
long rbp = 0;
249245

250246
return *(&rbp + 1);
251247
}
252248

253-
public static int HashCombineInt(int seed, int value)
254-
{
255-
return (int)(seed ^ (value + 2654435769u + (seed << 6) + (seed >> 2)));
256-
}
249+
private static int HashCombineInt(int seed, int value) =>
250+
(int)(seed ^ (value + 2654435769u + (seed << 6) + (seed >> 2)));
257251

258-
public static int StableStringHash(string? str)
252+
private static int StableStringHash(string? str)
259253
{
260254
if (str == null)
261255
{

0 commit comments

Comments
 (0)