Skip to content

Commit 6fc5f2e

Browse files
Replace CodeWriter.GenerateCode() with custom TextReader
This is a much more significant change that adds a custom TextReader to read the contents of CodeWriter's data structures. When passed to SourceText.From, it is used to either produce a StringText (by calling TextReader.ReadToEnd()) or, if the length is too large, a LargeText that wraps multiple arrays of chars rather than a giant string. The TextReader provides overrides of the methods that will ultimately be called by SourceText.From.
1 parent bc1b91d commit 6fc5f2e

File tree

1 file changed

+223
-16
lines changed
  • src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/CodeGeneration

1 file changed

+223
-16
lines changed

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/CodeGeneration/CodeWriter.cs

Lines changed: 223 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Diagnostics;
88
using System.Diagnostics.CodeAnalysis;
9+
using System.IO;
910
using System.Runtime.CompilerServices;
1011
using System.Text;
1112
using Microsoft.AspNetCore.Razor.PooledObjects;
@@ -322,34 +323,240 @@ public CodeWriter WriteLine([InterpolatedStringHandlerArgument("")] ref WriteInt
322323

323324
public SourceText GetText()
324325
{
325-
// TODO: Introduce a TextReader to create the SourceText with rather than a giant string.
326-
return SourceText.From(GenerateCode(), Encoding.UTF8);
326+
using var reader = new Reader(this);
327+
return SourceText.From(reader, Length, Encoding.UTF8);
327328
}
328329

329-
public string GenerateCode()
330+
private sealed class Reader(CodeWriter codeWriter) : TextReader
330331
{
331-
// Eventually, we need to remove this and not return a giant string, which can
332-
// easily be allocated on the LOH. The work to remove this is tracked by
333-
// https://github.com/dotnet/razor/issues/8076.
334-
return CreateString(Length, _pages, static (span, pages) =>
332+
private LinkedListNode<ReadOnlyMemory<char>[]>? _page = codeWriter._pages.First;
333+
private int _remainingLength = codeWriter.Length;
334+
private int _chunkIndex;
335+
private int _charIndex;
336+
337+
public override int Read()
335338
{
336-
foreach (var page in pages)
339+
if (!TryGetNextCharReadLocation(out var page, out var chunkIndex, out var charIndex))
337340
{
338-
foreach (var chars in page)
341+
return -1;
342+
}
343+
344+
_page = page;
345+
_chunkIndex = chunkIndex;
346+
_charIndex = charIndex + 1; // Increment the char index for the next read.
347+
_remainingLength--;
348+
349+
return page.Value[chunkIndex].Span[charIndex];
350+
}
351+
352+
public override int Peek()
353+
{
354+
if (!TryGetNextCharReadLocation(out var page, out var chunkIndex, out var charIndex))
355+
{
356+
return -1;
357+
}
358+
359+
return page.Value[chunkIndex].Span[charIndex];
360+
}
361+
362+
private bool TryGetNextCharReadLocation([NotNullWhen(true)] out LinkedListNode<ReadOnlyMemory<char>[]>? page, out int chunkIndex, out int charIndex)
363+
{
364+
page = _page;
365+
chunkIndex = _chunkIndex;
366+
charIndex = _charIndex;
367+
368+
if (page is null)
369+
{
370+
return false;
371+
}
372+
373+
do
374+
{
375+
var chunks = page.Value.AsSpan(chunkIndex);
376+
377+
foreach (var chunk in chunks)
378+
{
379+
if (charIndex < chunk.Length)
380+
{
381+
return true;
382+
}
383+
384+
chunkIndex++;
385+
charIndex = 0;
386+
}
387+
388+
page = page.Next;
389+
chunkIndex = 0;
390+
charIndex = 0;
391+
}
392+
while (page is not null);
393+
394+
chunkIndex = -1;
395+
charIndex = -1;
396+
397+
return false;
398+
}
399+
400+
public override int Read(char[] buffer, int index, int count)
401+
{
402+
ArgHelper.ThrowIfNull(buffer);
403+
ArgHelper.ThrowIfNegative(index);
404+
ArgHelper.ThrowIfNegative(count);
405+
406+
if (buffer.Length - index < count)
407+
{
408+
throw new ArgumentException($"{count} is greater than the number of elements from {index} to the end of {buffer}.");
409+
}
410+
411+
if (_page is null)
412+
{
413+
return -1;
414+
}
415+
416+
var destination = buffer.AsSpan(index);
417+
var charsWritten = 0;
418+
419+
var page = _page;
420+
var chunkIndex = _chunkIndex;
421+
var charIndex = _charIndex;
422+
423+
Debug.Assert(chunkIndex >= 0);
424+
Debug.Assert(charIndex >= 0);
425+
426+
do
427+
{
428+
var chunks = page.Value.AsSpan(chunkIndex);
429+
var isFirst = true;
430+
431+
foreach (var chunk in chunks)
339432
{
340-
if (chars.IsEmpty)
433+
var source = chunk.Span;
434+
435+
// Slice if the first chunk is partial. Note that this only occurs for the first chunk.
436+
if (isFirst)
437+
{
438+
isFirst = false;
439+
440+
if (charIndex > 0)
441+
{
442+
source = source[charIndex..];
443+
}
444+
}
445+
446+
var endOfChunkWritten = true;
447+
448+
// Are we about to write past the end of the buffer? If so, adjust source.
449+
// This will be the last chunk we write, so be sure to update charIndex.
450+
if (source.Length > destination.Length)
451+
{
452+
source = source[..destination.Length];
453+
charIndex += source.Length;
454+
455+
// There's more to this chunk to write! Note this so that we don't update
456+
// chunkIndex later.
457+
endOfChunkWritten = false;
458+
}
459+
460+
source.CopyTo(destination);
461+
destination = destination[source.Length..];
462+
463+
charsWritten += source.Length;
464+
465+
// Be careful not to increment chunkIndex unless we actually wrote to the end of the chunk.
466+
if (endOfChunkWritten)
341467
{
342-
return;
468+
chunkIndex++;
343469
}
344470

345-
chars.Span.CopyTo(span);
346-
span = span[chars.Length..];
471+
// Break if we are done writing. chunkIndex and charIndex should have their correct values at this point.
472+
if (destination.IsEmpty)
473+
{
474+
break;
475+
}
347476

348-
Debug.Assert(span.Length >= 0);
477+
charIndex = 0;
349478
}
479+
480+
if (destination.IsEmpty)
481+
{
482+
break;
483+
}
484+
485+
page = page.Next;
486+
chunkIndex = 0;
487+
charIndex = 0;
350488
}
489+
while (page is not null);
351490

352-
Debug.Assert(span.Length == 0, "We didn't fill the whole span!");
353-
});
491+
if (page is not null)
492+
{
493+
_page = page;
494+
_chunkIndex = chunkIndex;
495+
_charIndex = charIndex;
496+
}
497+
else
498+
{
499+
_page = null;
500+
_chunkIndex = -1;
501+
_charIndex = -1;
502+
}
503+
504+
return charsWritten;
505+
}
506+
507+
public override string ReadToEnd()
508+
{
509+
if (_page is null)
510+
{
511+
return string.Empty;
512+
}
513+
514+
var result = CreateString(_remainingLength, (_page, _chunkIndex, _charIndex), static (destination, state) =>
515+
{
516+
var (page, chunkIndex, charIndex) = state;
517+
518+
Debug.Assert(page is not null);
519+
Debug.Assert(chunkIndex >= 0);
520+
Debug.Assert(charIndex >= 0);
521+
522+
// Use the current chunk index to slice the first set of chunks.
523+
var chunks = page.Value.AsSpan(chunkIndex);
524+
525+
do
526+
{
527+
foreach (var chunk in chunks)
528+
{
529+
var source = chunk.Span;
530+
531+
// Slice the first chunk if it's partial.
532+
if (charIndex > 0)
533+
{
534+
source = source[charIndex..];
535+
charIndex = 0;
536+
}
537+
538+
if (source.IsEmpty)
539+
{
540+
continue;
541+
}
542+
543+
source.CopyTo(destination);
544+
destination = destination[source.Length..];
545+
}
546+
547+
page = page.Next;
548+
chunks = (page?.Value ?? []).AsSpan();
549+
}
550+
while (page is not null);
551+
552+
Debug.Assert(destination.Length == 0, "We didn't fill the whole span!");
553+
});
554+
555+
_page = null;
556+
_chunkIndex = -1;
557+
_charIndex = 1;
558+
559+
return result;
560+
}
354561
}
355562
}

0 commit comments

Comments
 (0)