Skip to content

Commit a897651

Browse files
committed
Undraft (tests, cleanup, documentation)
1 parent 79f3ccf commit a897651

File tree

8 files changed

+876
-184
lines changed

8 files changed

+876
-184
lines changed

src/Build.UnitTests/BackEnd/BinaryTranslator_Tests.cs

Lines changed: 583 additions & 0 deletions
Large diffs are not rendered by default.

src/Framework/BinaryTranslator.cs

Lines changed: 88 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -50,40 +50,34 @@ internal static ITranslator GetWriteTranslator(Stream stream)
5050
return new BinaryWriteTranslator(stream);
5151
}
5252

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

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

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

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

@@ -802,71 +796,76 @@ public bool TranslateNullable<T>(T value)
802796

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

810-
IsInterning = true;
804+
_isInterning = true;
811805

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

815-
IsInterning = false;
812+
_isInterning = false;
816813
}
817814

818-
public void Intern(ref string str, bool nullable)
815+
public void Intern(ref string str, bool nullable = true)
819816
{
820-
if (!IsInterning)
817+
if (!_isInterning)
821818
{
822819
Translate(ref str);
820+
return;
823821
}
824-
else if (nullable)
825-
{
826-
str = _interner.ReadNullable();
827-
}
828-
else
822+
823+
if (nullable && !TranslateNullable(string.Empty))
829824
{
830-
str = _interner.Read();
825+
str = null;
826+
return;
831827
}
828+
829+
str = _interner.Read();
832830
}
833831

834832
public void Intern(ref string[] array)
835833
{
836-
if (!IsInterning)
834+
if (!_isInterning)
837835
{
838836
Translate(ref array);
837+
return;
839838
}
840839

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

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

850848
for (int i = 0; i < count; i++)
851849
{
852-
array[i] = _interner.ReadNullable();
850+
array[i] = _interner.Read();
853851
}
854852
}
855853

856-
public void InternPath(ref string str, bool nullable)
854+
public void InternPath(ref string str, bool nullable = true)
857855
{
858-
if (!IsInterning)
856+
if (!_isInterning)
859857
{
860858
Translate(ref str);
859+
return;
861860
}
862-
else if (nullable)
863-
{
864-
str = _interner.ReadNullablePath();
865-
}
866-
else
861+
862+
if (nullable && !TranslateNullable(string.Empty))
867863
{
868-
str = _interner.ReadPath();
864+
str = null;
865+
return;
869866
}
867+
868+
str = _interner.ReadPath();
870869
}
871870
}
872871

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

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

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

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

909903
/// <summary>
@@ -1604,67 +1598,88 @@ public bool TranslateNullable<T>(T value)
16041598

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

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

1617-
public void Intern(ref string str, bool nullable)
1631+
public void Intern(ref string str, bool nullable = true)
16181632
{
1619-
if (!IsInterning)
1633+
if (!_isInterning)
16201634
{
16211635
Translate(ref str);
1636+
return;
16221637
}
1623-
else if (nullable)
1624-
{
1625-
_interner.InternNullable(str);
1626-
}
1627-
else
1638+
1639+
if (nullable && !TranslateNullable(str))
16281640
{
1629-
_interner.Intern(str);
1641+
return;
16301642
}
1643+
1644+
_interner.Intern(str);
16311645
}
16321646

16331647
public void Intern(ref string[] array)
16341648
{
1635-
if (!IsInterning)
1649+
if (!_isInterning)
16361650
{
16371651
Translate(ref array);
1652+
return;
16381653
}
16391654

1640-
if (!_interner.Translator.TranslateNullable(array))
1655+
if (!TranslateNullable(array))
16411656
{
16421657
return;
16431658
}
16441659

16451660
int count = array.Length;
1646-
_interner.Translator.Translate(ref count);
1661+
Translate(ref count);
16471662

16481663
for (int i = 0; i < count; i++)
16491664
{
1650-
_interner.InternNullable(array[i]);
1665+
_interner.Intern(array[i]);
16511666
}
16521667
}
16531668

1654-
public void InternPath(ref string str, bool nullable)
1669+
public void InternPath(ref string str, bool nullable = true)
16551670
{
1656-
if (!IsInterning)
1671+
if (!_isInterning)
16571672
{
16581673
Translate(ref str);
1674+
return;
16591675
}
1660-
else if (nullable)
1661-
{
1662-
_interner.InternNullablePath(str);
1663-
}
1664-
else
1676+
1677+
if (nullable && !TranslateNullable(str))
16651678
{
1666-
_interner.InternPath(str);
1679+
return;
16671680
}
1681+
1682+
_interner.InternPath(str);
16681683
}
16691684
}
16701685
}

src/Framework/ITranslator.cs

Lines changed: 50 additions & 9 deletions
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

Lines changed: 7 additions & 0 deletions
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)