Files
PJB3005 fe35a24e88 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.
2025-12-09 12:31:50 +01:00

324 lines
8.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Robust.Shared.Collections;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Value;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
namespace Robust.Shared.Serialization.Markdown;
// YDN has broken nullable annotations. Yeppers.
#nullable disable
public static class DataNodeParser
{
public static IEnumerable<DataNodeDocument> ParseYamlStream(TextReader reader)
{
return ParseYamlStream(reader, internStrings: false);
}
internal static IEnumerable<DataNodeDocument> ParseYamlStream(TextReader reader, bool internStrings)
{
return ParseYamlStream(new Parser(reader), internStrings);
}
internal static IEnumerable<DataNodeDocument> ParseYamlStream(Parser parser, bool internStrings = false)
{
var state = new ParserState(internStrings);
parser.Consume<StreamStart>();
while (!parser.TryConsume<StreamEnd>(out _))
{
yield return ParseDocument(parser, state);
}
// System.Console.WriteLine(state.TotalStringsSaved);
}
private static DataNodeDocument ParseDocument(Parser parser, ParserState parserState)
{
var state = new DocumentState(parserState);
parser.Consume<DocumentStart>();
var root = Parse(parser, state);
parser.Consume<DocumentEnd>();
ResolveAliases(state);
return new DataNodeDocument(root);
}
private static DataNode Parse(Parser parser, DocumentState state)
{
if (parser.Current is Scalar)
return ParseValue(parser, state);
if (parser.Current is SequenceStart)
return ParseSequence(parser, state);
if (parser.Current is MappingStart)
return ParseMapping(parser, state);
if (parser.Current is AnchorAlias)
return ParseAlias(parser, state);
throw new NotSupportedException();
}
private static DataNode ParseAlias(Parser parser, DocumentState state)
{
var alias = parser.Consume<AnchorAlias>();
if (!state.Anchors.TryGetValue(alias.Value, out var node))
{
// Don't have this anchor yet. It may be defined later in the document.
return new DataNodeAlias(alias.Value);
}
return node;
}
private static ValueDataNode ParseValue(Parser parser, DocumentState state)
{
var ev = parser.Consume<Scalar>();
var node = new ValueDataNode(ev)
{
Tag = ConvertTag(ev.Tag, state.ParserState),
Value = state.ParserState.InternString(ev.Value)
};
NodeParsed(node, ev, false, state);
return node;
}
private static string ParseKey(Parser parser)
{
var ev = parser.Consume<Scalar>();
if (!ev.Anchor.IsEmpty)
throw new NotSupportedException();
return ev.Value;
}
private static SequenceDataNode ParseSequence(Parser parser, DocumentState state)
{
var ev = parser.Consume<SequenceStart>();
var node = new SequenceDataNode();
node.Tag = ConvertTag(ev.Tag, state.ParserState);
node.Start = ev.Start;
var unresolvedAlias = false;
SequenceEnd seqEnd;
while (!parser.TryConsume(out seqEnd))
{
var value = Parse(parser, state);
node.Add(value);
unresolvedAlias |= value is DataNodeAlias;
}
node.End = seqEnd.End;
NodeParsed(node, ev, unresolvedAlias, state);
return node;
}
private static MappingDataNode ParseMapping(Parser parser, DocumentState state)
{
var ev = parser.Consume<MappingStart>();
var node = new MappingDataNode();
node.Tag = ConvertTag(ev.Tag, state.ParserState);
var unresolvedAlias = false;
MappingEnd mapEnd;
while (!parser.TryConsume(out mapEnd))
{
var key = state.ParserState.InternString(ParseKey(parser));
var value = Parse(parser, state);
node.Add(key, value);
unresolvedAlias |= value is DataNodeAlias;
}
node.End = mapEnd.End;
NodeParsed(node, ev, unresolvedAlias, state);
return node;
}
private static void NodeParsed(DataNode node, NodeEvent ev, bool unresolvedAlias, DocumentState state)
{
if (unresolvedAlias)
state.UnresolvedAliasOwners.Add(node);
if (ev.Anchor.IsEmpty)
return;
if (state.Anchors.ContainsKey(ev.Anchor))
throw new DataParseException($"Duplicate anchor defined in document: {ev.Anchor}");
state.Anchors[ev.Anchor] = node;
}
private static void ResolveAliases(DocumentState state)
{
foreach (var node in state.UnresolvedAliasOwners)
{
switch (node)
{
case MappingDataNode mapping:
ResolveMappingAliases(mapping, state);
break;
case SequenceDataNode sequence:
ResolveSequenceAliases(sequence, state);
break;
}
}
}
private static void ResolveMappingAliases(MappingDataNode mapping, DocumentState state)
{
var swaps = new ValueList<(string key, DataNode value)>();
foreach (var (key, value) in mapping)
{
if (value is not DataNodeAlias valueAlias)
return;
var newValue = ResolveAlias(valueAlias, state);
swaps.Add((key, newValue));
mapping.Remove(key);
}
foreach (var (key, value) in swaps)
{
mapping[key] = value;
}
}
private static void ResolveSequenceAliases(SequenceDataNode sequence, DocumentState state)
{
for (var i = 0; i < sequence.Count; i++)
{
if (sequence[i] is DataNodeAlias alias)
sequence[i] = ResolveAlias(alias, state);
}
}
private static DataNode ResolveAlias(DataNodeAlias alias, DocumentState state)
{
if (!state.Anchors.TryGetValue(alias.Anchor, out var node))
throw new DataParseException($"Unable to resolve alias '{alias.Anchor}'");
return node;
}
private static string ConvertTag(TagName tag, ParserState state)
{
return (tag.IsNonSpecific || tag.IsEmpty) ? null : state.InternString(tag.Value);
}
private sealed class DocumentState(ParserState parserState)
{
public readonly ParserState ParserState = parserState;
public readonly Dictionary<AnchorName, DataNode> Anchors = new();
public ValueList<DataNode> UnresolvedAliasOwners;
}
private sealed class DataNodeAlias : DataNode
{
public readonly AnchorName Anchor;
public DataNodeAlias(AnchorName anchor) : base(default, default)
{
Anchor = anchor;
}
public override bool IsEmpty => true;
public override DataNode Copy()
{
throw new NotSupportedException();
}
public override DataNode Except(DataNode node)
{
throw new NotSupportedException();
}
[Obsolete("Use SerializationManager.PushComposition()")]
public override DataNode PushInheritance(DataNode parent)
{
throw new NotSupportedException();
}
}
#nullable enable
private sealed class ParserState(bool internStrings)
{
public readonly HashSet<string>? StringInternIndex = internStrings ? [] : null;
//public int TotalStringsSaved = 0;
[return: NotNullIfNotNull(nameof(str))]
public string? InternString(string? str)
{
if (StringInternIndex == null)
return str;
if (str == null)
return null;
// Use a basic string interning system to avoid releasing a bunch of equivalent strings.
// This avoids having thousands of identical strings for stuff like "type" in prototypes stored in memory.
if (StringInternIndex.TryGetValue(str, out var indexedString))
{
// if (!ReferenceEquals(str, indexedString))
// TotalStringsSaved += 1;
return indexedString;
}
StringInternIndex.Add(str);
return str;
}
}
}
public sealed class DataParseException : Exception
{
public DataParseException()
{
}
public DataParseException(string message) : base(message)
{
}
public DataParseException(string message, Exception inner) : base(message, inner)
{
}
}
public sealed record DataNodeDocument(DataNode Root);