Skip to content

Commit 34017b7

Browse files
committed
Undraft (tests, cleanup, documentation)
1 parent 13ad7fc commit 34017b7

8 files changed

+876
-184
lines changed

src/Build.UnitTests/BackEnd/BinaryTranslator_Tests.cs

+583
Large diffs are not rendered by default.

src/Framework/BinaryTranslator.cs

+88-73
Original file line numberDiff line numberDiff line change
@@ -51,40 +51,34 @@ internal static ITranslator GetWriteTranslator(Stream stream)
5151
return new BinaryWriteTranslator(stream);
5252
}
5353

54-
// TODO: Avoid expsoing write translator?
55-
internal static ITranslator GetWriteTranslator(Stream stream, InterningWriteTranslator interner)
56-
{
57-
return new BinaryWriteTranslator(stream, interner, isInterning: true);
58-
}
59-
6054
/// <summary>
6155
/// Implementation of ITranslator for reading from a stream.
6256
/// </summary>
6357
private class BinaryReadTranslator : ITranslator
6458
{
6559
/// <summary>
66-
/// The stream used as a source or destination for data.
60+
/// The intern reader used in an intern scope.
6761
/// </summary>
68-
private Stream _packetStream;
62+
private readonly InterningReadTranslator _interner;
6963

7064
/// <summary>
7165
/// The binary reader used in read mode.
7266
/// </summary>
7367
private BinaryReader _reader;
7468

75-
private InterningReadTranslator _interner;
76-
77-
public bool IsInterning { get; private set; }
69+
/// <summary>
70+
/// Whether the caller has entered an intern scope.
71+
/// </summary>
72+
private bool _isInterning;
7873

7974
#nullable enable
8075
/// <summary>
8176
/// Constructs a serializer from the specified stream, operating in the designated mode.
8277
/// </summary>
8378
public BinaryReadTranslator(Stream packetStream, BinaryReaderFactory buffer)
8479
{
85-
_packetStream = packetStream;
8680
_reader = buffer.Create(packetStream);
87-
_interner = new(this);
81+
_interner = new InterningReadTranslator(this);
8882
}
8983
#nullable disable
9084

@@ -803,71 +797,76 @@ public bool TranslateNullable<T>(T value)
803797

804798
public void WithInterning(IEqualityComparer<string> comparer, int initialCapacity, Action<ITranslator> internBlock)
805799
{
806-
if (IsInterning)
800+
if (_isInterning)
807801
{
808802
throw new InvalidOperationException("Cannot enter recursive intern block.");
809803
}
810804

811-
IsInterning = true;
805+
_isInterning = true;
812806

807+
// Deserialize the intern header before entering the intern scope.
813808
_interner.Translate(this);
809+
810+
// No other setup is needed since we can parse the packet directly from the stream.
814811
internBlock(this);
815812

816-
IsInterning = false;
813+
_isInterning = false;
817814
}
818815

819-
public void Intern(ref string str, bool nullable)
816+
public void Intern(ref string str, bool nullable = true)
820817
{
821-
if (!IsInterning)
818+
if (!_isInterning)
822819
{
823820
Translate(ref str);
821+
return;
824822
}
825-
else if (nullable)
826-
{
827-
str = _interner.ReadNullable();
828-
}
829-
else
823+
824+
if (nullable && !TranslateNullable(string.Empty))
830825
{
831-
str = _interner.Read();
826+
str = null;
827+
return;
832828
}
829+
830+
str = _interner.Read();
833831
}
834832

835833
public void Intern(ref string[] array)
836834
{
837-
if (!IsInterning)
835+
if (!_isInterning)
838836
{
839837
Translate(ref array);
838+
return;
840839
}
841840

842841
if (!TranslateNullable(array))
843842
{
844843
return;
845844
}
846845

847-
848846
int count = _reader.ReadInt32();
849847
array = new string[count];
850848

851849
for (int i = 0; i < count; i++)
852850
{
853-
array[i] = _interner.ReadNullable();
851+
array[i] = _interner.Read();
854852
}
855853
}
856854

857-
public void InternPath(ref string str, bool nullable)
855+
public void InternPath(ref string str, bool nullable = true)
858856
{
859-
if (!IsInterning)
857+
if (!_isInterning)
860858
{
861859
Translate(ref str);
860+
return;
862861
}
863-
else if (nullable)
864-
{
865-
str = _interner.ReadNullablePath();
866-
}
867-
else
862+
863+
if (nullable && !TranslateNullable(string.Empty))
868864
{
869-
str = _interner.ReadPath();
865+
str = null;
866+
return;
870867
}
868+
869+
str = _interner.ReadPath();
871870
}
872871
}
873872

@@ -876,35 +875,30 @@ public void InternPath(ref string str, bool nullable)
876875
/// </summary>
877876
private class BinaryWriteTranslator : ITranslator
878877
{
879-
/// <summary>
880-
/// The stream used as a source or destination for data.
881-
/// </summary>
882-
private Stream _packetStream;
883-
884878
/// <summary>
885879
/// The binary writer used in write mode.
886880
/// </summary>
887881
private BinaryWriter _writer;
888882

889-
private readonly InterningWriteTranslator _interner = new();
883+
/// <summary>
884+
/// The intern writer used in an intern scope.
885+
/// This must be lazily instantiated since the interner has its own internal write translator, and
886+
/// would otherwise go into a recursive loop on initalization.
887+
/// </summary>
888+
private InterningWriteTranslator _interner;
890889

891-
public bool IsInterning { get; private set; }
890+
/// <summary>
891+
/// Whether the caller has entered an intern scope.
892+
/// </summary>
893+
private bool _isInterning;
892894

893895
/// <summary>
894896
/// Constructs a serializer from the specified stream, operating in the designated mode.
895897
/// </summary>
896898
/// <param name="packetStream">The stream serving as the source or destination of data.</param>
897899
public BinaryWriteTranslator(Stream packetStream)
898-
: this(packetStream, new InterningWriteTranslator())
899-
{
900-
}
901-
902-
internal BinaryWriteTranslator(Stream packetStream, InterningWriteTranslator interner, bool isInterning = false)
903900
{
904-
_packetStream = packetStream;
905901
_writer = new BinaryWriter(packetStream);
906-
_interner = interner;
907-
IsInterning = isInterning;
908902
}
909903

910904
/// <summary>
@@ -1605,67 +1599,88 @@ public bool TranslateNullable<T>(T value)
16051599

16061600
public void WithInterning(IEqualityComparer<string> comparer, int initialCapacity, Action<ITranslator> internBlock)
16071601
{
1608-
if (IsInterning)
1602+
if (_isInterning)
16091603
{
16101604
throw new InvalidOperationException("Cannot enter recursive intern block.");
16111605
}
16121606

1613-
_interner.InitCapacity(comparer, initialCapacity);
1614-
internBlock(_interner.Translator);
1607+
// Every new scope requires the interner's state to be reset.
1608+
_interner ??= new InterningWriteTranslator();
1609+
_interner.Setup(comparer, initialCapacity);
1610+
1611+
// Temporaily swap our writer with the interner.
1612+
// This forwards all writes to this translator into the interning buffer, so that any non-interned
1613+
// writes which are interleaved will be in the correct order.
1614+
BinaryWriter streamWriter = _writer;
1615+
_writer = _interner.Writer;
1616+
_isInterning = true;
1617+
1618+
try
1619+
{
1620+
internBlock(this);
1621+
}
1622+
finally
1623+
{
1624+
_writer = streamWriter;
1625+
_isInterning = false;
1626+
}
1627+
1628+
// Write the interned buffer into the real output stream.
16151629
_interner.Translate(this);
16161630
}
16171631

1618-
public void Intern(ref string str, bool nullable)
1632+
public void Intern(ref string str, bool nullable = true)
16191633
{
1620-
if (!IsInterning)
1634+
if (!_isInterning)
16211635
{
16221636
Translate(ref str);
1637+
return;
16231638
}
1624-
else if (nullable)
1625-
{
1626-
_interner.InternNullable(str);
1627-
}
1628-
else
1639+
1640+
if (nullable && !TranslateNullable(str))
16291641
{
1630-
_interner.Intern(str);
1642+
return;
16311643
}
1644+
1645+
_interner.Intern(str);
16321646
}
16331647

16341648
public void Intern(ref string[] array)
16351649
{
1636-
if (!IsInterning)
1650+
if (!_isInterning)
16371651
{
16381652
Translate(ref array);
1653+
return;
16391654
}
16401655

1641-
if (!_interner.Translator.TranslateNullable(array))
1656+
if (!TranslateNullable(array))
16421657
{
16431658
return;
16441659
}
16451660

16461661
int count = array.Length;
1647-
_interner.Translator.Translate(ref count);
1662+
Translate(ref count);
16481663

16491664
for (int i = 0; i < count; i++)
16501665
{
1651-
_interner.InternNullable(array[i]);
1666+
_interner.Intern(array[i]);
16521667
}
16531668
}
16541669

1655-
public void InternPath(ref string str, bool nullable)
1670+
public void InternPath(ref string str, bool nullable = true)
16561671
{
1657-
if (!IsInterning)
1672+
if (!_isInterning)
16581673
{
16591674
Translate(ref str);
1675+
return;
16601676
}
1661-
else if (nullable)
1662-
{
1663-
_interner.InternNullablePath(str);
1664-
}
1665-
else
1677+
1678+
if (nullable && !TranslateNullable(str))
16661679
{
1667-
_interner.InternPath(str);
1680+
return;
16681681
}
1682+
1683+
_interner.InternPath(str);
16691684
}
16701685
}
16711686
}

src/Framework/ITranslator.cs

+50-9
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
using System.IO;
88
using Microsoft.Build.Framework;
99

10-
#nullable disable
11-
1210
namespace Microsoft.Build.BackEnd
1311
{
1412
/// <summary>
@@ -98,8 +96,6 @@ BinaryWriter Writer
9896
get;
9997
}
10098

101-
bool IsInterning { get; }
102-
10399
/// <summary>
104100
/// Translates a boolean.
105101
/// </summary>
@@ -369,13 +365,58 @@ void TranslateDictionary<D, T>(ref D dictionary, ObjectTranslator<T> objectTrans
369365
/// <returns>True if the object should be written, false otherwise.</returns>
370366
bool TranslateNullable<T>(T value);
371367

372-
void Intern(ref string str, bool nullable = false);
373-
374-
void Intern(ref string[] array);
368+
/// <summary>
369+
/// Creates a scope which activates string interning / deduplication for any Intern_xx method.
370+
/// This should generally be called from the root level packet.
371+
/// </summary>
372+
/// <param name="comparer">The strseaparating comparer to use when populating the intern cache.</param>
373+
/// <param name="initialCapacity">The initial capacity of the intern cache.</param>
374+
/// <param name="internBlock">A delegate providing a translator, in which all Intern_xx calls will go through the intern cache.</param>
375+
/// <remarks>
376+
/// Packet interning is implemented via a header with an array of all interned strings, followed by the body in
377+
/// which any interned / duplicated strings are replaced by their ID.
378+
/// <see cref="TranslationDirection"/> modes have different ordering requirements, so it would not be
379+
/// possible to implement direction-agnostic serialization via the Intern_xx methods alone:
380+
/// - Write: Because we don't know the full list of strings ahead of time, we need to create a temporary buffer
381+
/// for the packet body, which we can later offset when flushing to the real stream.
382+
/// - Read: The intern header needs to be deserialized before the packet body, otherwise we won't know what
383+
/// string each ID maps to.
384+
/// This method abstracts these requirements to the caller, such that the underlying translator will
385+
/// automatically handle the appropriate IO ordering when entering / exiting the delegate scope.
386+
/// </remarks>
387+
void WithInterning(IEqualityComparer<string> comparer, int initialCapacity, Action<ITranslator> internBlock);
375388

389+
/// <summary>
390+
/// Interns the string if the translator is currently within an intern block.
391+
/// Otherwise, this forwards to the regular Translate method.
392+
/// </summary>
393+
/// <param name="str">The value to be translated.</param>
394+
/// <param name="nullable">
395+
/// Whether to null check and translate the nullable marker.
396+
/// Setting this to false can reduce packet sizes when interning large numbers of strings
397+
/// which are validated to always be non-null, such as dictionary keys.
398+
/// </param>
399+
void Intern(ref string str, bool nullable = true);
376400

377-
void InternPath(ref string str, bool nullable = false);
401+
/// <summary>
402+
/// Interns each string in the array if the translator is currently within an intern block.
403+
/// Otherwise, this forwards to the regular Translate method. To match behavior, all strings
404+
/// assumed to be non-null.
405+
/// </summary>
406+
/// <param name="array">The array to be translated.</param>
407+
void Intern(ref string[] array);
378408

379-
void WithInterning(IEqualityComparer<string> comparer, int initialCapacity, Action<ITranslator> internBlock);
409+
/// <summary>
410+
/// Interns the string if the translator is currently within an intern block.
411+
/// Otherwise, this forwards to the regular Translate method.
412+
/// If the string is determined to be path-like, the path components will be interned separately.
413+
/// </summary>
414+
/// <param name="str">The value to be translated.</param>
415+
/// <param name="nullable">
416+
/// Whether to null check and translate the nullable marker.
417+
/// Setting this to false can reduce packet sizes when interning large numbers of strings
418+
/// which are validated to always be non-null, such as dictionary keys.
419+
/// </param>
420+
void InternPath(ref string str, bool nullable = true);
380421
}
381422
}

src/Framework/InternPathIds.cs

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.Build.BackEnd
5+
{
6+
internal readonly record struct InternPathIds(int DirectoryId, int FileNameId);
7+
}

0 commit comments

Comments
 (0)