// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Blazor.Rendering; using Microsoft.AspNetCore.Blazor.RenderTree; using System; using System.Collections.Generic; using System.IO; using System.Text; namespace Microsoft.AspNetCore.Blazor.Server.Circuits { // TODO: We should consider *not* having this type of infrastructure in the .Server // project, but instead in some new project called .Remote or similar, since it // would also be used in Electron and possibly WebWorker cases. /// /// Provides a custom binary serializer for instances. /// This is designed with both server-side and client-side perf in mind: /// /// * Array-like regions always have a fixed size per entry (even if some entry types /// don't require as much space as others) so the recipient can index directly. /// * The indices describing where field data starts, where each string value starts, /// etc., are written *after* that data, so when writing the data we don't have to /// compute the locations up front or seek back to an earlier point in the stream. /// The recipient can only process the data after reading it all into a buffer, /// so it's no disadvantage for the location info to be at the end. /// * We only serialize the data that the JS side will need. For example, we don't /// emit frame sequence numbers, or any representation of nonstring attribute /// values, or component instances, etc. /// /// We don't have or need a .NET reader for this format. We only read it from JS code. /// internal class RenderBatchWriter : IDisposable { private readonly List _strings; private readonly Dictionary _deduplicatedStringIndices; private readonly BinaryWriter _binaryWriter; public RenderBatchWriter(Stream output, bool leaveOpen) { _strings = new List(); _deduplicatedStringIndices = new Dictionary(); _binaryWriter = new BinaryWriter(output, Encoding.UTF8, leaveOpen); } public void Write(in RenderBatch renderBatch) { var updatedComponentsOffset = Write(renderBatch.UpdatedComponents); var referenceFramesOffset = Write(renderBatch.ReferenceFrames); var disposedComponentIdsOffset = Write(renderBatch.DisposedComponentIDs); var disposedEventHandlerIdsOffset = Write(renderBatch.DisposedEventHandlerIDs); var stringTableOffset = WriteStringTable(); _binaryWriter.Write(updatedComponentsOffset); _binaryWriter.Write(referenceFramesOffset); _binaryWriter.Write(disposedComponentIdsOffset); _binaryWriter.Write(disposedEventHandlerIdsOffset); _binaryWriter.Write(stringTableOffset); } int Write(in ArrayRange diffs) { var count = diffs.Count; var diffsIndexes = new int[count]; var array = diffs.Array; var baseStream = _binaryWriter.BaseStream; for (var i = 0; i < count; i++) { diffsIndexes[i] = (int)baseStream.Position; Write(array[i]); } // Now write out the table of locations var tableStartPos = (int)baseStream.Position; _binaryWriter.Write(count); for (var i = 0; i < count; i++) { _binaryWriter.Write(diffsIndexes[i]); } return tableStartPos; } void Write(in RenderTreeDiff diff) { _binaryWriter.Write(diff.ComponentId); var edits = diff.Edits; _binaryWriter.Write(edits.Count); var editsArray = edits.Array; var editsEndIndexExcl = edits.Offset + edits.Count; for (var i = edits.Offset; i < editsEndIndexExcl; i++) { Write(editsArray[i]); } } void Write(in RenderTreeEdit edit) { // We want all RenderTreeEdit outputs to be of the same length, so that // the recipient can index into the array directly without walking it. // So we output some value for all properties, even when not applicable // for this specific RenderTreeEditType. _binaryWriter.Write((int)edit.Type); _binaryWriter.Write(edit.SiblingIndex); _binaryWriter.Write(edit.ReferenceFrameIndex); WriteString(edit.RemovedAttributeName, allowDeduplication: true); } int Write(in ArrayRange frames) { var startPos = (int)_binaryWriter.BaseStream.Position; var array = frames.Array; var count = frames.Count; _binaryWriter.Write(count); for (var i = 0; i < count; i++) { Write(array[i]); } return startPos; } void Write(in RenderTreeFrame frame) { _binaryWriter.Write((int)frame.FrameType); // We want each frame to take up the same number of bytes, so that the // recipient can index into the array directly instead of having to // walk through it. // Since we can fit every frame type into 3 ints, use that as the // common size. For smaller frames, we add padding to expand it to // 12 bytes (i.e., 3 x 4-byte ints). // The total size then for each frame is 16 bytes (frame type, then // 3 other ints). switch (frame.FrameType) { case RenderTreeFrameType.Attribute: WriteString(frame.AttributeName, allowDeduplication: true); if (frame.AttributeValue is bool boolValue) { // Encoding the bool as either "" or null is pretty odd, but avoids // having to pack any "what type of thing is this" info into the same // 4 bytes as the string table index. If, later, we need a way of // distinguishing whether an attribute value is really a bool or a string // or something else, we'll need a different encoding mechanism. Since there // would never be more than (say) 2^28 (268 million) distinct string table // entries, we could use the first 4 bits to encode the value type. WriteString(boolValue ? string.Empty : null, allowDeduplication: true); } else { var attributeValueString = frame.AttributeValue as string; WriteString(attributeValueString, allowDeduplication: string.IsNullOrEmpty(attributeValueString)); } _binaryWriter.Write(frame.AttributeEventHandlerId); break; case RenderTreeFrameType.Component: _binaryWriter.Write(frame.ComponentSubtreeLength); _binaryWriter.Write(frame.ComponentId); WritePadding(_binaryWriter, 4); break; case RenderTreeFrameType.ComponentReferenceCapture: // The client doesn't need to know about these. But we still have // to include them in the array otherwise the ReferenceFrameIndex // values in the edits data would be wrong. WritePadding(_binaryWriter, 12); break; case RenderTreeFrameType.Element: _binaryWriter.Write(frame.ElementSubtreeLength); WriteString(frame.ElementName, allowDeduplication: true); WritePadding(_binaryWriter, 4); break; case RenderTreeFrameType.ElementReferenceCapture: WriteString(frame.ElementReferenceCaptureId, allowDeduplication: false); WritePadding(_binaryWriter, 8); break; case RenderTreeFrameType.Region: _binaryWriter.Write(frame.RegionSubtreeLength); WritePadding(_binaryWriter, 8); break; case RenderTreeFrameType.Text: WriteString( frame.TextContent, allowDeduplication: string.IsNullOrWhiteSpace(frame.TextContent)); WritePadding(_binaryWriter, 8); break; case RenderTreeFrameType.Markup: WriteString(frame.MarkupContent, allowDeduplication: false); WritePadding(_binaryWriter, 8); break; default: throw new ArgumentException($"Unsupported frame type: {frame.FrameType}"); } } int Write(in ArrayRange numbers) { var startPos = (int)_binaryWriter.BaseStream.Position; _binaryWriter.Write(numbers.Count); var array = numbers.Array; var count = numbers.Count; for (var index = 0; index < count; index++) { _binaryWriter.Write(array[index]); } return startPos; } void WriteString(string value, bool allowDeduplication) { if (value == null) { _binaryWriter.Write(-1); } else { int stringIndex; if (!allowDeduplication || !_deduplicatedStringIndices.TryGetValue(value, out stringIndex)) { stringIndex = _strings.Count; _strings.Add(value); if (allowDeduplication) { _deduplicatedStringIndices.Add(value, stringIndex); } } _binaryWriter.Write(stringIndex); } } int WriteStringTable() { // Capture the locations of each string var stringsCount = _strings.Count; var locations = new int[stringsCount]; for (var i = 0; i < stringsCount; i++) { var stringValue = _strings[i]; locations[i] = (int)_binaryWriter.BaseStream.Position; _binaryWriter.Write(stringValue); } // Now write the locations var locationsStartPos = (int)_binaryWriter.BaseStream.Position; for (var i = 0; i < stringsCount; i++) { _binaryWriter.Write(locations[i]); } return locationsStartPos; } static void WritePadding(BinaryWriter writer, int numBytes) { while (numBytes >= 4) { writer.Write(0); numBytes -= 4; } while (numBytes > 0) { writer.Write((byte)0); numBytes--; } } public void Dispose() { _binaryWriter.Dispose(); } } }