Make ArrayBuilder<T>.ToSegment() safe even when reusing underlying arrays (#11903)

This commit is contained in:
Steve Sanderson 2019-07-05 16:01:53 +01:00 committed by GitHub
parent 7d545d40aa
commit 6f6d099113
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 197 additions and 53 deletions

View File

@ -713,6 +713,18 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
namespace Microsoft.AspNetCore.Components.RenderTree
{
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public readonly partial struct ArrayBuilderSegment<T> : System.Collections.Generic.IEnumerable<T>, System.Collections.IEnumerable
{
private readonly object _dummy;
private readonly int _dummyPrimitive;
public T[] Array { get { throw null; } }
public int Count { get { throw null; } }
public T this[int index] { get { throw null; } }
public int Offset { get { throw null; } }
System.Collections.Generic.IEnumerator<T> System.Collections.Generic.IEnumerable<T>.GetEnumerator() { throw null; }
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public readonly partial struct ArrayRange<T>
{
@ -759,7 +771,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
public readonly partial struct RenderTreeDiff
{
public readonly int ComponentId;
public readonly System.ArraySegment<Microsoft.AspNetCore.Components.RenderTree.RenderTreeEdit> Edits;
public readonly Microsoft.AspNetCore.Components.RenderTree.ArrayBuilderSegment<Microsoft.AspNetCore.Components.RenderTree.RenderTreeEdit> Edits;
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Explicit)]
public readonly partial struct RenderTreeEdit

View File

@ -152,13 +152,13 @@ namespace Microsoft.AspNetCore.Components.RenderTree
=> new ArrayRange<T>(_items, _itemsInUse);
/// <summary>
/// Produces an <see cref="ArraySegment{T}"/> structure describing the selected contents.
/// Produces an <see cref="ArrayBuilderSegment{T}"/> structure describing the selected contents.
/// </summary>
/// <param name="fromIndexInclusive">The index of the first item in the segment.</param>
/// <param name="toIndexExclusive">One plus the index of the last item in the segment.</param>
/// <returns>The <see cref="ArraySegment{T}"/>.</returns>
public ArraySegment<T> ToSegment(int fromIndexInclusive, int toIndexExclusive)
=> new ArraySegment<T>(_items, fromIndexInclusive, toIndexExclusive - fromIndexInclusive);
public ArrayBuilderSegment<T> ToSegment(int fromIndexInclusive, int toIndexExclusive)
=> new ArrayBuilderSegment<T>(this, fromIndexInclusive, toIndexExclusive - fromIndexInclusive);
private void SetCapacity(int desiredCapacity, bool preserveContents)
{

View File

@ -0,0 +1,59 @@
// 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 System;
using System.Collections;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Components.RenderTree
{
/// <summary>
/// Represents a range of elements within an instance of <see cref="ArrayBuilder{T}"/>.
/// </summary>
/// <typeparam name="T">The type of the elements in the array</typeparam>
public readonly struct ArrayBuilderSegment<T> : IEnumerable<T>
{
private readonly ArrayBuilder<T> _builder;
private readonly int _offset;
private readonly int _count;
internal ArrayBuilderSegment(ArrayBuilder<T> builder, int offset, int count)
{
_builder = builder;
_offset = offset;
_count = count;
}
/// <summary>
/// Gets the current underlying array holding the segment's elements.
/// </summary>
public T[] Array => _builder?.Buffer;
/// <summary>
/// Gets the offset into the underlying array holding the segment's elements.
/// </summary>
public int Offset => _offset;
/// <summary>
/// Gets the number of items in the segment.
/// </summary>
public int Count => _count;
/// <summary>
/// Gets the specified item from the segment.
/// </summary>
/// <param name="index">The index into the segment.</param>
/// <returns>The array entry at the specified index within the segment.</returns>
public T this[int index]
=> _builder.Buffer[_offset + index];
IEnumerator<T> IEnumerable<T>.GetEnumerator()
=> ((IEnumerable<T>)new ArraySegment<T>(_builder.Buffer, _offset, _count)).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> ((IEnumerable)new ArraySegment<T>(_builder.Buffer, _offset, _count)).GetEnumerator();
// TODO: If this assembly later moves to netstandard2.1, consider adding a public
// GetEnumerator method that returns ArraySegment.Enumerator to avoid boxing.
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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 System;
@ -18,11 +18,11 @@ namespace Microsoft.AspNetCore.Components.RenderTree
/// <summary>
/// Gets the changes to the render tree since a previous state.
/// </summary>
public readonly ArraySegment<RenderTreeEdit> Edits;
public readonly ArrayBuilderSegment<RenderTreeEdit> Edits;
internal RenderTreeDiff(
int componentId,
ArraySegment<RenderTreeEdit> entries)
ArrayBuilderSegment<RenderTreeEdit> entries)
{
ComponentId = componentId;
Edits = entries;

View File

@ -0,0 +1,55 @@
// 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 System;
using Microsoft.AspNetCore.Components.RenderTree;
using Xunit;
namespace Microsoft.AspNetCore.Components.Rendering
{
public class ArrayBuilderSegmentTest
{
[Fact]
public void BasicPropertiesWork()
{
// Arrange: builder containing 1..5
var builder = new ArrayBuilder<int>();
builder.Append(new[] { 1, 2, 3, 4, 5 }, 0, 5);
// Act: take segment containing 2..3
var segment = builder.ToSegment(1, 3);
// Act
Assert.Same(builder.Buffer, segment.Array);
Assert.Equal(1, segment.Offset);
Assert.Equal(2, segment.Count);
Assert.Equal(2, segment[0]);
Assert.Equal(3, segment[1]);
Assert.Equal(new[] { 2, 3 }, segment);
}
[Fact]
public void StillWorksAfterUnderlyingCapacityChange()
{
// Arrange: builder containing 1..8
var builder = new ArrayBuilder<int>(capacity: 10);
builder.Append(new[] { 1, 2, 3, 4, 5, 6, 7, 8 }, 0, 8);
var originalBuffer = builder.Buffer;
// Act/Assert 1: take segment containing 1..5
var segment = builder.ToSegment(0, 5);
Assert.Equal(new[] { 1, 2, 3, 4, 5 }, segment);
Assert.Same(originalBuffer, segment.Array);
// Act 2: grow the builder enough to force a resize
builder.Append(new[] { 9, 10, 11 }, 0, 3);
Array.Clear(originalBuffer, 0, originalBuffer.Length); // Extra proof that we're not using the original storage
// Assert 2
Assert.Same(builder.Buffer, segment.Array);
Assert.NotSame(originalBuffer, segment.Array); // Since there was a resize
Assert.Equal(new[] { 1, 2, 3, 4, 5 }, segment);
Assert.Equal(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }, builder.ToSegment(0, builder.Count));
}
}
}

View File

@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
if (cancellationStatus.Canceled)
{
// Avoid creating a circuit host if other component earlier in the pipeline already triggered
// cancelation (e.g., by navigating or throwing). Instead render nothing.
// cancellation (e.g., by navigating or throwing). Instead render nothing.
return new ComponentPrerenderResult(Array.Empty<string>());
}
var circuitHost = GetOrCreateCircuitHost(context, cancellationStatus);

View File

@ -152,11 +152,13 @@ namespace Microsoft.AspNetCore.Components.Server
RenderTreeEdit.UpdateMarkup(108, 109),
RenderTreeEdit.RemoveAttribute(110, "Some removed attribute"), // To test deduplication
};
var editsBuilder = new ArrayBuilder<RenderTreeEdit>();
editsBuilder.Append(edits, 0, edits.Length);
var editsSegment = editsBuilder.ToSegment(1, edits.Length); // Skip first to show offset is respected
var bytes = Serialize(new RenderBatch(
new ArrayRange<RenderTreeDiff>(new[]
{
new RenderTreeDiff(123, new ArraySegment<RenderTreeEdit>(
edits, 1, edits.Length - 1)) // Skip first to show offset is respected
new RenderTreeDiff(123, editsSegment)
}, 1),
default,
default,

View File

@ -34,9 +34,11 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
}
// Clone the diff, because its underlying storage will get reused in subsequent batches
var cloneBuilder = new ArrayBuilder<RenderTreeEdit>();
cloneBuilder.Append(diff.Edits.ToArray(), 0, diff.Edits.Count);
var diffClone = new RenderTreeDiff(
diff.ComponentId,
new ArraySegment<RenderTreeEdit>(diff.Edits.ToArray()));
cloneBuilder.ToSegment(0, diff.Edits.Count));
DiffsByComponentId[componentId].Add(diffClone);
DiffsInOrder.Add(diffClone);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import { RenderBatch, ArraySegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch';
import { RenderBatch, ArrayBuilderSegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch';
import { EventDelegator } from './EventDelegator';
import { EventForDotNet, UIEventArgs } from './EventForDotNet';
import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd, permuteLogicalChildren, getClosestDomElement } from './LogicalElements';
@ -29,7 +29,7 @@ export class BrowserRenderer {
rootComponentsPendingFirstRender[componentId] = element;
}
public updateComponent(batch: RenderBatch, componentId: number, edits: ArraySegment<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>): void {
public updateComponent(batch: RenderBatch, componentId: number, edits: ArrayBuilderSegment<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>): void {
const element = this.childComponentLocations[componentId];
if (!element) {
throw new Error(`No element is currently associated with component ${componentId}`);
@ -71,17 +71,17 @@ export class BrowserRenderer {
this.childComponentLocations[componentId] = element;
}
private applyEdits(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, edits: ArraySegment<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>) {
private applyEdits(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, edits: ArrayBuilderSegment<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>) {
let currentDepth = 0;
let childIndexAtCurrentDepth = childIndex;
let permutationList: PermutationListEntry[] | undefined;
const arraySegmentReader = batch.arraySegmentReader;
const arrayBuilderSegmentReader = batch.arrayBuilderSegmentReader;
const editReader = batch.editReader;
const frameReader = batch.frameReader;
const editsValues = arraySegmentReader.values(edits);
const editsOffset = arraySegmentReader.offset(edits);
const editsLength = arraySegmentReader.count(edits);
const editsValues = arrayBuilderSegmentReader.values(edits);
const editsOffset = arrayBuilderSegmentReader.offset(edits);
const editsLength = arrayBuilderSegmentReader.count(edits);
const maxEditIndexExcl = editsOffset + editsLength;
for (let editIndex = editsOffset; editIndex < maxEditIndexExcl; editIndex++) {

View File

@ -1,4 +1,4 @@
import { RenderBatch, ArrayRange, RenderTreeDiff, ArrayValues, RenderTreeEdit, EditType, FrameType, RenderTreeFrame, RenderTreeDiffReader, RenderTreeFrameReader, RenderTreeEditReader, ArrayRangeReader, ArraySegmentReader, ArraySegment } from './RenderBatch';
import { RenderBatch, ArrayRange, RenderTreeDiff, ArrayValues, RenderTreeEdit, EditType, FrameType, RenderTreeFrame, RenderTreeDiffReader, RenderTreeFrameReader, RenderTreeEditReader, ArrayRangeReader, ArrayBuilderSegmentReader, ArrayBuilderSegment } from './RenderBatch';
import { decodeUtf8 } from './Utf8Decoder';
const updatedComponentsEntryLength = 4; // Each is a single int32 giving the location of the data
@ -13,7 +13,7 @@ export class OutOfProcessRenderBatch implements RenderBatch {
const stringReader = new OutOfProcessStringReader(batchData);
this.arrayRangeReader = new OutOfProcessArrayRangeReader(batchData);
this.arraySegmentReader = new OutOfProcessArraySegmentReader(batchData);
this.arrayBuilderSegmentReader = new OutOfProcessArrayBuilderSegmentReader(batchData);
this.diffReader = new OutOfProcessRenderTreeDiffReader(batchData);
this.editReader = new OutOfProcessRenderTreeEditReader(batchData, stringReader);
this.frameReader = new OutOfProcessRenderTreeFrameReader(batchData, stringReader);
@ -62,7 +62,7 @@ export class OutOfProcessRenderBatch implements RenderBatch {
arrayRangeReader: ArrayRangeReader;
arraySegmentReader: ArraySegmentReader;
arrayBuilderSegmentReader: ArrayBuilderSegmentReader;
}
class OutOfProcessRenderTreeDiffReader implements RenderTreeDiffReader {
@ -207,24 +207,24 @@ class OutOfProcessArrayRangeReader implements ArrayRangeReader {
}
}
class OutOfProcessArraySegmentReader implements ArraySegmentReader {
class OutOfProcessArrayBuilderSegmentReader implements ArrayBuilderSegmentReader {
constructor(private batchDataUint8: Uint8Array) {
}
offset<T>(arraySegment: ArraySegment<T>) {
offset<T>(arrayBuilderSegment: ArrayBuilderSegment<T>) {
// Not used by the out-of-process representation of RenderBatch data.
// This only exists on the ArraySegmentReader for the shared-memory representation.
// This only exists on the ArrayBuilderSegmentReader for the shared-memory representation.
return 0;
}
count<T>(arraySegment: ArraySegment<T>) {
count<T>(arrayBuilderSegment: ArrayBuilderSegment<T>) {
// First int is count
return readInt32LE(this.batchDataUint8, arraySegment as any);
return readInt32LE(this.batchDataUint8, arrayBuilderSegment as any);
}
values<T>(arraySegment: ArraySegment<T>): ArrayValues<T> {
values<T>(arrayBuilderSegment: ArrayBuilderSegment<T>): ArrayValues<T> {
// Entries data starts after the 'count' int (i.e., after 4 bytes)
return arraySegment as any + 4;
return arrayBuilderSegment as any + 4;
}
}

View File

@ -13,7 +13,7 @@ export interface RenderBatch {
editReader: RenderTreeEditReader;
frameReader: RenderTreeFrameReader;
arrayRangeReader: ArrayRangeReader;
arraySegmentReader: ArraySegmentReader;
arrayBuilderSegmentReader: ArrayBuilderSegmentReader;
}
export interface ArrayRangeReader {
@ -21,15 +21,15 @@ export interface ArrayRangeReader {
values<T>(arrayRange: ArrayRange<T>): ArrayValues<T>;
}
export interface ArraySegmentReader {
offset<T>(arraySegment: ArraySegment<T>): number;
count<T>(arraySegment: ArraySegment<T>): number;
values<T>(arraySegment: ArraySegment<T>): ArrayValues<T>;
export interface ArrayBuilderSegmentReader {
offset<T>(arrayBuilderSegment: ArrayBuilderSegment<T>): number;
count<T>(arrayBuilderSegment: ArrayBuilderSegment<T>): number;
values<T>(arrayBuilderSegment: ArrayBuilderSegment<T>): ArrayValues<T>;
}
export interface RenderTreeDiffReader {
componentId(diff: RenderTreeDiff): number;
edits(diff: RenderTreeDiff): ArraySegment<RenderTreeEdit>;
edits(diff: RenderTreeDiff): ArrayBuilderSegment<RenderTreeEdit>;
editsEntry(values: ArrayValues<RenderTreeEdit>, index: number): RenderTreeEdit;
}
@ -55,7 +55,7 @@ export interface RenderTreeFrameReader {
}
export interface ArrayRange<T> { ArrayRange__DO_NOT_IMPLEMENT: any }
export interface ArraySegment<T> { ArraySegment__DO_NOT_IMPLEMENT: any }
export interface ArrayBuilderSegment<T> { ArrayBuilderSegment__DO_NOT_IMPLEMENT: any }
export interface ArrayValues<T> { ArrayValues__DO_NOT_IMPLEMENT: any }
export interface RenderTreeDiff { RenderTreeDiff__DO_NOT_IMPLEMENT: any }

View File

@ -1,6 +1,6 @@
import { platform } from '../../Environment';
import { RenderBatch, ArrayRange, ArrayRangeReader, ArraySegment, RenderTreeDiff, RenderTreeEdit, RenderTreeFrame, ArrayValues, EditType, FrameType, RenderTreeFrameReader } from './RenderBatch';
import { Pointer, System_Array } from '../../Platform/Platform';
import { RenderBatch, ArrayRange, ArrayRangeReader, ArrayBuilderSegment, RenderTreeDiff, RenderTreeEdit, RenderTreeFrame, ArrayValues, EditType, FrameType, RenderTreeFrameReader } from './RenderBatch';
import { Pointer, System_Array, System_Object } from '../../Platform/Platform';
// Used when running on Mono WebAssembly for shared-memory interop. The code here encapsulates
// our knowledge of the memory layout of RenderBatch and all referenced types.
@ -49,7 +49,7 @@ export class SharedMemoryRenderBatch implements RenderBatch {
arrayRangeReader = arrayRangeReader;
arraySegmentReader = arraySegmentReader;
arrayBuilderSegmentReader = arrayBuilderSegmentReader;
diffReader = diffReader;
@ -65,19 +65,24 @@ const arrayRangeReader = {
count: <T>(arrayRange: ArrayRange<T>) => platform.readInt32Field(arrayRange as any, 4),
};
// Keep in sync with memory layout in ArraySegment
const arraySegmentReader = {
// Keep in sync with memory layout in ArrayBuilderSegment
const arrayBuilderSegmentReader = {
structLength: 12,
values: <T>(arraySegment: ArraySegment<T>) => platform.readObjectField<System_Array<T>>(arraySegment as any, 0) as any as ArrayValues<T>,
offset: <T>(arraySegment: ArraySegment<T>) => platform.readInt32Field(arraySegment as any, 4),
count: <T>(arraySegment: ArraySegment<T>) => platform.readInt32Field(arraySegment as any, 8),
values: <T>(arrayBuilderSegment: ArrayBuilderSegment<T>) => {
// Evaluate arrayBuilderSegment->_builder->_items, i.e., two dereferences needed
const builder = platform.readObjectField<System_Object>(arrayBuilderSegment as any, 0);
const builderFieldsAddress = platform.getObjectFieldsBaseAddress(builder);
return platform.readObjectField<System_Array<T>>(builderFieldsAddress, 0) as any as ArrayValues<T>;
},
offset: <T>(arrayBuilderSegment: ArrayBuilderSegment<T>) => platform.readInt32Field(arrayBuilderSegment as any, 4),
count: <T>(arrayBuilderSegment: ArrayBuilderSegment<T>) => platform.readInt32Field(arrayBuilderSegment as any, 8),
};
// Keep in sync with memory layout in RenderTreeDiff.cs
const diffReader = {
structLength: 4 + arraySegmentReader.structLength,
structLength: 4 + arrayBuilderSegmentReader.structLength,
componentId: (diff: RenderTreeDiff) => platform.readInt32Field(diff as any, 0),
edits: (diff: RenderTreeDiff) => platform.readStructField<Pointer>(diff as any, 4) as any as ArraySegment<RenderTreeEdit>,
edits: (diff: RenderTreeDiff) => platform.readStructField<Pointer>(diff as any, 4) as any as ArrayBuilderSegment<RenderTreeEdit>,
editsEntry: (values: ArrayValues<RenderTreeEdit>, index: number) => arrayValuesEntry(values, index, editReader.structLength),
};

View File

@ -101,11 +101,13 @@ namespace Ignitor
RenderTreeEdit.UpdateMarkup(108, 109),
RenderTreeEdit.RemoveAttribute(110, "Some removed attribute"), // To test deduplication
};
var editsBuilder = new ArrayBuilder<RenderTreeEdit>();
editsBuilder.Append(edits, 0, edits.Length);
var editsSegment = editsBuilder.ToSegment(1, edits.Length); // Skip first to show offset is respected
var bytes = RoundTripSerialize(new RenderBatch(
new ArrayRange<RenderTreeDiff>(new[]
{
new RenderTreeDiff(123, new ArraySegment<RenderTreeEdit>(
edits, 1, edits.Length - 1)) // Skip first to show offset is respected
new RenderTreeDiff(123, editsSegment)
}, 1),
default,
default,

View File

@ -77,7 +77,7 @@ namespace Ignitor
}
}
private void UpdateComponent(RenderBatch batch, int componentId, ArraySegment<RenderTreeEdit> edits)
private void UpdateComponent(RenderBatch batch, int componentId, ArrayBuilderSegment<RenderTreeEdit> edits)
{
if (!Components.TryGetValue(componentId, out var component))
{
@ -98,7 +98,7 @@ namespace Ignitor
}
private void ApplyEdits(RenderBatch batch, ContainerNode parent, int childIndex, ArraySegment<RenderTreeEdit> edits)
private void ApplyEdits(RenderBatch batch, ContainerNode parent, int childIndex, ArrayBuilderSegment<RenderTreeEdit> edits)
{
var currentDepth = 0;
var childIndexAtCurrentDepth = childIndex;

View File

@ -115,12 +115,19 @@ namespace Ignitor
}
}
result[i / 4] = new RenderTreeDiff(componentId, new ArraySegment<RenderTreeEdit>(edits));
result[i / 4] = new RenderTreeDiff(componentId, ToArrayBuilderSegment(edits));
}
return new ArrayRange<RenderTreeDiff>(result, result.Length);
}
private static ArrayBuilderSegment<T> ToArrayBuilderSegment<T>(T[] entries)
{
var builder = new ArrayBuilder<T>();
builder.Append(entries, 0, entries.Length);
return builder.ToSegment(0, entries.Length);
}
private static ArrayRange<RenderTreeFrame> ReadReferenceFrames(ReadOnlySpan<byte> data, string[] strings)
{
var result = new RenderTreeFrame[data.Length / 16];