Skip to content

Commit fe35a24

Browse files
committed
Basic prototype string interning
From my extremely rough and unscientific tests, this saves like 15 MB of client memory on the main menu. Probably also just improves load speed on startup too. It's per file to keep the implementation simple.
1 parent e875d89 commit fe35a24

File tree

2 files changed

+61
-15
lines changed

2 files changed

+61
-15
lines changed

Robust.Shared/Prototypes/PrototypeManager.YamlLoad.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public void LoadDirectory(ResPath path, bool overwrite = false,
5353

5454
var extractedList = new List<ExtractedMappingData>();
5555
var i = 0;
56-
foreach (var document in DataNodeParser.ParseYamlStream(reader))
56+
foreach (var document in DataNodeParser.ParseYamlStream(reader, internStrings: true))
5757
{
5858
i += 1;
5959
LoadedData?.Invoke(document);
@@ -152,7 +152,7 @@ public void LoadFile(ResPath file, bool overwrite = false, Dictionary<Type, Hash
152152
return;
153153

154154
var i = 0;
155-
foreach (var document in DataNodeParser.ParseYamlStream(reader))
155+
foreach (var document in DataNodeParser.ParseYamlStream(reader, internStrings: true))
156156
{
157157
LoadedData?.Invoke(document);
158158

@@ -254,7 +254,7 @@ public void LoadFromStream(TextReader stream, bool overwrite = false,
254254
_hasEverBeenReloaded = true;
255255

256256
var i = 0;
257-
foreach (var document in DataNodeParser.ParseYamlStream(stream))
257+
foreach (var document in DataNodeParser.ParseYamlStream(stream, internStrings: true))
258258
{
259259
LoadedData?.Invoke(document);
260260

Robust.Shared/Serialization/Markdown/DataNodeParser.cs

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.IO;
45
using Robust.Shared.Collections;
56
using Robust.Shared.Serialization.Markdown.Mapping;
@@ -17,22 +18,31 @@ public static class DataNodeParser
1718
{
1819
public static IEnumerable<DataNodeDocument> ParseYamlStream(TextReader reader)
1920
{
20-
return ParseYamlStream(new Parser(reader));
21+
return ParseYamlStream(reader, internStrings: false);
2122
}
2223

23-
internal static IEnumerable<DataNodeDocument> ParseYamlStream(Parser parser)
24+
internal static IEnumerable<DataNodeDocument> ParseYamlStream(TextReader reader, bool internStrings)
2425
{
26+
return ParseYamlStream(new Parser(reader), internStrings);
27+
}
28+
29+
internal static IEnumerable<DataNodeDocument> ParseYamlStream(Parser parser, bool internStrings = false)
30+
{
31+
var state = new ParserState(internStrings);
32+
2533
parser.Consume<StreamStart>();
2634

2735
while (!parser.TryConsume<StreamEnd>(out _))
2836
{
29-
yield return ParseDocument(parser);
37+
yield return ParseDocument(parser, state);
3038
}
39+
40+
// System.Console.WriteLine(state.TotalStringsSaved);
3141
}
3242

33-
private static DataNodeDocument ParseDocument(Parser parser)
43+
private static DataNodeDocument ParseDocument(Parser parser, ParserState parserState)
3444
{
35-
var state = new DocumentState();
45+
var state = new DocumentState(parserState);
3646

3747
parser.Consume<DocumentStart>();
3848

@@ -78,7 +88,11 @@ private static DataNode ParseAlias(Parser parser, DocumentState state)
7888
private static ValueDataNode ParseValue(Parser parser, DocumentState state)
7989
{
8090
var ev = parser.Consume<Scalar>();
81-
var node = new ValueDataNode(ev){Tag = ConvertTag(ev.Tag)};
91+
var node = new ValueDataNode(ev)
92+
{
93+
Tag = ConvertTag(ev.Tag, state.ParserState),
94+
Value = state.ParserState.InternString(ev.Value)
95+
};
8296

8397
NodeParsed(node, ev, false, state);
8498

@@ -100,7 +114,7 @@ private static SequenceDataNode ParseSequence(Parser parser, DocumentState state
100114
var ev = parser.Consume<SequenceStart>();
101115

102116
var node = new SequenceDataNode();
103-
node.Tag = ConvertTag(ev.Tag);
117+
node.Tag = ConvertTag(ev.Tag, state.ParserState);
104118
node.Start = ev.Start;
105119

106120
var unresolvedAlias = false;
@@ -127,14 +141,14 @@ private static MappingDataNode ParseMapping(Parser parser, DocumentState state)
127141
var ev = parser.Consume<MappingStart>();
128142

129143
var node = new MappingDataNode();
130-
node.Tag = ConvertTag(ev.Tag);
144+
node.Tag = ConvertTag(ev.Tag, state.ParserState);
131145

132146
var unresolvedAlias = false;
133147

134148
MappingEnd mapEnd;
135149
while (!parser.TryConsume(out mapEnd))
136150
{
137-
var key = ParseKey(parser);
151+
var key = state.ParserState.InternString(ParseKey(parser));
138152
var value = Parse(parser, state);
139153

140154
node.Add(key, value);
@@ -218,13 +232,14 @@ private static DataNode ResolveAlias(DataNodeAlias alias, DocumentState state)
218232
return node;
219233
}
220234

221-
private static string ConvertTag(TagName tag)
235+
private static string ConvertTag(TagName tag, ParserState state)
222236
{
223-
return (tag.IsNonSpecific || tag.IsEmpty) ? null : tag.Value;
237+
return (tag.IsNonSpecific || tag.IsEmpty) ? null : state.InternString(tag.Value);
224238
}
225239

226-
private sealed class DocumentState
240+
private sealed class DocumentState(ParserState parserState)
227241
{
242+
public readonly ParserState ParserState = parserState;
228243
public readonly Dictionary<AnchorName, DataNode> Anchors = new();
229244
public ValueList<DataNode> UnresolvedAliasOwners;
230245
}
@@ -256,6 +271,37 @@ public override DataNode PushInheritance(DataNode parent)
256271
throw new NotSupportedException();
257272
}
258273
}
274+
275+
#nullable enable
276+
277+
private sealed class ParserState(bool internStrings)
278+
{
279+
public readonly HashSet<string>? StringInternIndex = internStrings ? [] : null;
280+
//public int TotalStringsSaved = 0;
281+
282+
[return: NotNullIfNotNull(nameof(str))]
283+
public string? InternString(string? str)
284+
{
285+
if (StringInternIndex == null)
286+
return str;
287+
288+
if (str == null)
289+
return null;
290+
291+
// Use a basic string interning system to avoid releasing a bunch of equivalent strings.
292+
// This avoids having thousands of identical strings for stuff like "type" in prototypes stored in memory.
293+
if (StringInternIndex.TryGetValue(str, out var indexedString))
294+
{
295+
// if (!ReferenceEquals(str, indexedString))
296+
// TotalStringsSaved += 1;
297+
298+
return indexedString;
299+
}
300+
301+
StringInternIndex.Add(str);
302+
return str;
303+
}
304+
}
259305
}
260306

261307
public sealed class DataParseException : Exception

0 commit comments

Comments
 (0)