Store event handler IDs as longs. Fixes #12058 (#12305)

* Change event handler IDs to be longs

* Update unit tests

* Enable detailed errors for test app

* CR: Explicitly check if we exceed Number.MAX_SAFE_INTEGER

* Update ref assemblies
This commit is contained in:
Steve Sanderson 2019-07-18 17:53:07 +01:00 committed by GitHub
parent 010ffe6121
commit 4e04b81415
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 181 additions and 120 deletions

View File

@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
public override Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get { throw null; } }
public System.Threading.Tasks.Task AddComponentAsync(System.Type componentType, string domElementSelector) { throw null; }
public System.Threading.Tasks.Task AddComponentAsync<TComponent>(string domElementSelector) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
public override System.Threading.Tasks.Task DispatchEventAsync(int eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo eventFieldInfo, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
public override System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo eventFieldInfo, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
protected override void Dispose(bool disposing) { }
protected override void HandleException(System.Exception exception) { }
protected override System.Threading.Tasks.Task UpdateDisplayAsync(in Microsoft.AspNetCore.Components.Rendering.RenderBatch batch) { throw null; }

View File

@ -120,7 +120,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
}
/// <inheritdoc />
public override Task DispatchEventAsync(int eventHandlerId, EventFieldInfo eventFieldInfo, UIEventArgs eventArgs)
public override Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo eventFieldInfo, UIEventArgs eventArgs)
{
// Be sure we only run one event handler at once. Although they couldn't run
// simultaneously anyway (there's only one thread), they could run nested on
@ -183,12 +183,12 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
readonly struct IncomingEventInfo
{
public readonly int EventHandlerId;
public readonly ulong EventHandlerId;
public readonly EventFieldInfo EventFieldInfo;
public readonly UIEventArgs EventArgs;
public readonly TaskCompletionSource<object> TaskCompletionSource;
public IncomingEventInfo(int eventHandlerId, EventFieldInfo eventFieldInfo, UIEventArgs eventArgs)
public IncomingEventInfo(ulong eventHandlerId, EventFieldInfo eventFieldInfo, UIEventArgs eventArgs)
{
EventHandlerId = eventHandlerId;
EventFieldInfo = eventFieldInfo;

View File

@ -724,7 +724,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
{
private readonly object _dummy;
public Microsoft.AspNetCore.Components.RenderTree.ArrayRange<int> DisposedComponentIDs { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public Microsoft.AspNetCore.Components.RenderTree.ArrayRange<int> DisposedEventHandlerIDs { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public Microsoft.AspNetCore.Components.RenderTree.ArrayRange<ulong> DisposedEventHandlerIDs { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame> ReferenceFrames { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiff> UpdatedComponents { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
}
@ -735,7 +735,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
public event System.UnhandledExceptionEventHandler UnhandledSynchronizationException { add { } remove { } }
protected internal virtual void AddToRenderQueue(int componentId, Microsoft.AspNetCore.Components.RenderFragment renderFragment) { }
protected internal int AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent component) { throw null; }
public virtual System.Threading.Tasks.Task DispatchEventAsync(int eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
public virtual System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, Microsoft.AspNetCore.Components.UIEventArgs eventArgs) { throw null; }
public void Dispose() { }
protected virtual void Dispose(bool disposing) { }
protected abstract void HandleException(System.Exception exception);

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
/// <summary>
/// Represents an entry in a tree of user interface (UI) items.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
[StructLayout(LayoutKind.Explicit, Pack = 4)]
public readonly struct RenderTreeFrame
{
// Note that the struct layout has to be valid in both 32-bit and 64-bit runtime platforms,
@ -90,7 +90,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Attribute"/>
/// gets the ID of the corresponding event handler, if any.
/// </summary>
[FieldOffset(8)] public readonly int AttributeEventHandlerId;
[FieldOffset(8)] public readonly ulong AttributeEventHandlerId;
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Attribute"/>,
@ -267,7 +267,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
}
// Attribute constructor
private RenderTreeFrame(int sequence, string attributeName, object attributeValue, int attributeEventHandlerId, string attributeEventUpdatesAttributeName)
private RenderTreeFrame(int sequence, string attributeName, object attributeValue, ulong attributeEventHandlerId, string attributeEventUpdatesAttributeName)
: this()
{
FrameType = RenderTreeFrameType.Attribute;
@ -337,7 +337,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
internal RenderTreeFrame WithComponent(ComponentState componentState)
=> new RenderTreeFrame(Sequence, componentSubtreeLength: ComponentSubtreeLength, ComponentType, componentState, ComponentKey);
internal RenderTreeFrame WithAttributeEventHandlerId(int eventHandlerId)
internal RenderTreeFrame WithAttributeEventHandlerId(ulong eventHandlerId)
=> new RenderTreeFrame(Sequence, attributeName: AttributeName, AttributeValue, eventHandlerId, AttributeEventUpdatesAttributeName);
internal RenderTreeFrame WithAttributeValue(object attributeValue)

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 Microsoft.AspNetCore.Components.RenderTree;
@ -30,13 +30,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <summary>
/// Gets the IDs of the event handlers that were disposed.
/// </summary>
public ArrayRange<int> DisposedEventHandlerIDs { get; }
public ArrayRange<ulong> DisposedEventHandlerIDs { get; }
internal RenderBatch(
ArrayRange<RenderTreeDiff> updatedComponents,
ArrayRange<RenderTreeFrame> referenceFrames,
ArrayRange<int> disposedComponentIDs,
ArrayRange<int> disposedEventHandlerIDs)
ArrayRange<ulong> disposedEventHandlerIDs)
{
UpdatedComponents = updatedComponents;
ReferenceFrames = referenceFrames;

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
// Primary result data
public ArrayBuilder<RenderTreeDiff> UpdatedComponentDiffs { get; } = new ArrayBuilder<RenderTreeDiff>();
public ArrayBuilder<int> DisposedComponentIds { get; } = new ArrayBuilder<int>();
public ArrayBuilder<int> DisposedEventHandlerIds { get; } = new ArrayBuilder<int>();
public ArrayBuilder<ulong> DisposedEventHandlerIds { get; } = new ArrayBuilder<ulong>();
// Buffers referenced by UpdatedComponentDiffs
public ArrayBuilder<RenderTreeEdit> EditsBuffer { get; } = new ArrayBuilder<RenderTreeEdit>(64);

View File

@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
{
internal class RenderTreeUpdater
{
public static void UpdateToMatchClientState(RenderTreeBuilder renderTreeBuilder, int eventHandlerId, object newFieldValue)
public static void UpdateToMatchClientState(RenderTreeBuilder renderTreeBuilder, ulong eventHandlerId, object newFieldValue)
{
// We only allow the client to supply string or bool currently, since those are the only kinds of
// values we output on attributes that go to the client

View File

@ -22,8 +22,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
private static readonly Action<ILogger, int, Type, Exception> _disposingComponent =
LoggerMessage.Define<int, Type>(LogLevel.Debug, new EventId(4, "DisposingComponent"), "Disposing component {ComponentId} of type {ComponentType}");
private static readonly Action<ILogger, int, string, Exception> _handlingEvent =
LoggerMessage.Define<int, string>(LogLevel.Debug, new EventId(5, "HandlingEvent"), "Handling event {EventId} of type '{EventType}'");
private static readonly Action<ILogger, ulong, string, Exception> _handlingEvent =
LoggerMessage.Define<ulong, string>(LogLevel.Debug, new EventId(5, "HandlingEvent"), "Handling event {EventId} of type '{EventType}'");
public static void InitializingComponent(ILogger logger, ComponentState componentState, ComponentState parentComponentState)
{
@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
}
internal static void HandlingEvent(ILogger<Renderer> logger, int eventHandlerId, UIEventArgs eventArgs)
internal static void HandlingEvent(ILogger<Renderer> logger, ulong eventHandlerId, UIEventArgs eventArgs)
{
_handlingEvent(logger, eventHandlerId, eventArgs?.Type ?? "null", null);
}

View File

@ -20,13 +20,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
private readonly IServiceProvider _serviceProvider;
private readonly Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
private readonly Dictionary<int, EventCallback> _eventBindings = new Dictionary<int, EventCallback>();
private readonly Dictionary<int, int> _eventHandlerIdReplacements = new Dictionary<int, int>();
private readonly Dictionary<ulong, EventCallback> _eventBindings = new Dictionary<ulong, EventCallback>();
private readonly Dictionary<ulong, ulong> _eventHandlerIdReplacements = new Dictionary<ulong, ulong>();
private readonly ILogger<Renderer> _logger;
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
private bool _isBatchInProgress;
private int _lastEventHandlerId = 0;
private ulong _lastEventHandlerId;
private List<Task> _pendingTasks;
/// <summary>
@ -206,7 +206,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// A <see cref="Task"/> which will complete once all asynchronous processing related to the event
/// has completed.
/// </returns>
public virtual Task DispatchEventAsync(int eventHandlerId, EventFieldInfo fieldInfo, UIEventArgs eventArgs)
public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo fieldInfo, UIEventArgs eventArgs)
{
EnsureSynchronizationContext();
@ -354,7 +354,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
}
internal void TrackReplacedEventHandlerId(int oldEventHandlerId, int newEventHandlerId)
internal void TrackReplacedEventHandlerId(ulong oldEventHandlerId, ulong newEventHandlerId)
{
// Tracking the chain of old->new replacements allows us to interpret incoming EventFieldInfo
// values even if they refer to an event handler ID that's since been superseded. This is essential
@ -362,7 +362,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
_eventHandlerIdReplacements.Add(oldEventHandlerId, newEventHandlerId);
}
private int FindLatestEventHandlerIdInChain(int eventHandlerId)
private ulong FindLatestEventHandlerIdInChain(ulong eventHandlerId)
{
while (_eventHandlerIdReplacements.TryGetValue(eventHandlerId, out var replacementEventHandlerId))
{
@ -573,7 +573,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
}
private void RemoveEventHandlerIds(ArrayRange<int> eventHandlerIds, Task afterTaskIgnoreErrors)
private void RemoveEventHandlerIds(ArrayRange<ulong> eventHandlerIds, Task afterTaskIgnoreErrors)
{
if (eventHandlerIds.Count == 0)
{
@ -598,7 +598,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
// Factor out the async part into a separate local method purely so, in the
// synchronous case, there's no state machine or task construction
async Task ContinueAfterTask(ArrayRange<int> eventHandlerIds, Task afterTaskIgnoreErrors)
async Task ContinueAfterTask(ArrayRange<ulong> eventHandlerIds, Task afterTaskIgnoreErrors)
{
// We need to delay the actual removal (e.g., until we've confirmed the client
// has processed the batch and hence can be sure not to reuse the handler IDs
@ -637,7 +637,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
}
private void UpdateRenderTreeToMatchClientState(int eventHandlerId, EventFieldInfo fieldInfo)
private void UpdateRenderTreeToMatchClientState(ulong eventHandlerId, EventFieldInfo fieldInfo)
{
var componentState = GetOptionalComponentState(fieldInfo.ComponentId);
if (componentState != null)

View File

@ -859,7 +859,7 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Equal(0, entry.ReferenceFrameIndex);
});
AssertFrame.Attribute(referenceFrames[0], "onbar", addedHandler);
Assert.NotEqual(0, removedEventHandlerFrame.AttributeEventHandlerId);
Assert.NotEqual(default, removedEventHandlerFrame.AttributeEventHandlerId);
Assert.Equal(
new[] { removedEventHandlerFrame.AttributeEventHandlerId },
batchBuilder.DisposedEventHandlerIDs.AsEnumerable());
@ -1592,7 +1592,7 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Empty(result.Edits);
AssertFrame.Attribute(oldAttributeFrame, "ontest", retainedHandler);
AssertFrame.Attribute(newAttributeFrame, "ontest", retainedHandler);
Assert.NotEqual(0, oldAttributeFrame.AttributeEventHandlerId);
Assert.NotEqual(default, oldAttributeFrame.AttributeEventHandlerId);
Assert.Equal(oldAttributeFrame.AttributeEventHandlerId, newAttributeFrame.AttributeEventHandlerId);
Assert.Empty(batchBuilder.DisposedEventHandlerIDs.AsEnumerable());
}
@ -1619,7 +1619,7 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Single(result.Edits);
AssertFrame.Attribute(oldAttributeFrame, "ontest", retainedHandler);
AssertFrame.Attribute(newAttributeFrame, "ontest", retainedHandler);
Assert.NotEqual(0, oldAttributeFrame.AttributeEventHandlerId);
Assert.NotEqual(default, oldAttributeFrame.AttributeEventHandlerId);
Assert.Equal(oldAttributeFrame.AttributeEventHandlerId, newAttributeFrame.AttributeEventHandlerId);
Assert.Empty(batchBuilder.DisposedEventHandlerIDs.AsEnumerable());
}

View File

@ -3313,7 +3313,7 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Equal(RenderTreeEditType.SetAttribute, edit.Type);
var attributeFrame = batch2.ReferenceFrames[edit.ReferenceFrameIndex];
AssertFrame.Attribute(attributeFrame, "ontestevent", typeof(Action<UIChangeEventArgs>));
Assert.NotEqual(0, attributeFrame.AttributeEventHandlerId);
Assert.NotEqual(default, attributeFrame.AttributeEventHandlerId);
Assert.NotEqual(eventHandlerId, attributeFrame.AttributeEventHandlerId);
});
}
@ -3365,7 +3365,7 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Equal(RenderTreeEditType.SetAttribute, edit.Type);
var attributeFrame = latestBatch.ReferenceFrames[edit.ReferenceFrameIndex];
AssertFrame.Attribute(attributeFrame, "ontestevent", typeof(Action<UIChangeEventArgs>));
Assert.NotEqual(0, attributeFrame.AttributeEventHandlerId);
Assert.NotEqual(default, attributeFrame.AttributeEventHandlerId);
Assert.NotEqual(eventHandlerId, attributeFrame.AttributeEventHandlerId);
});
}

View File

@ -130,16 +130,15 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
void Write(in RenderTreeFrame frame)
{
// TODO: Change this to write as a short, saving 2 bytes per 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
// Since we can fit every frame type into 16 bytes, 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).
// 16 bytes.
switch (frame.FrameType)
{
case RenderTreeFrameType.Attribute:
@ -160,41 +159,41 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var attributeValueString = frame.AttributeValue as string;
WriteString(attributeValueString, allowDeduplication: string.IsNullOrEmpty(attributeValueString));
}
_binaryWriter.Write(frame.AttributeEventHandlerId);
_binaryWriter.Write(frame.AttributeEventHandlerId); // 8 bytes
break;
case RenderTreeFrameType.Component:
_binaryWriter.Write(frame.ComponentSubtreeLength);
_binaryWriter.Write(frame.ComponentId);
WritePadding(_binaryWriter, 4);
WritePadding(_binaryWriter, 8);
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);
WritePadding(_binaryWriter, 16);
break;
case RenderTreeFrameType.Element:
_binaryWriter.Write(frame.ElementSubtreeLength);
WriteString(frame.ElementName, allowDeduplication: true);
WritePadding(_binaryWriter, 4);
WritePadding(_binaryWriter, 8);
break;
case RenderTreeFrameType.ElementReferenceCapture:
WriteString(frame.ElementReferenceCaptureId, allowDeduplication: false);
WritePadding(_binaryWriter, 8);
WritePadding(_binaryWriter, 12);
break;
case RenderTreeFrameType.Region:
_binaryWriter.Write(frame.RegionSubtreeLength);
WritePadding(_binaryWriter, 8);
WritePadding(_binaryWriter, 12);
break;
case RenderTreeFrameType.Text:
WriteString(
frame.TextContent,
allowDeduplication: string.IsNullOrWhiteSpace(frame.TextContent));
WritePadding(_binaryWriter, 8);
WritePadding(_binaryWriter, 12);
break;
case RenderTreeFrameType.Markup:
WriteString(frame.MarkupContent, allowDeduplication: false);
WritePadding(_binaryWriter, 8);
WritePadding(_binaryWriter, 12);
break;
default:
throw new ArgumentException($"Unsupported frame type: {frame.FrameType}");
@ -216,6 +215,21 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
return startPos;
}
int Write(in ArrayRange<ulong> 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)

View File

@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.Components.Server
new ArrayRange<RenderTreeDiff>(),
new ArrayRange<RenderTreeFrame>(),
new ArrayRange<int>(),
new ArrayRange<int>(new[] { 123, int.MaxValue, int.MinValue, 456 }, 3) // Only use first 3 to show that param is respected
new ArrayRange<ulong>(new ulong[] { 123, ulong.MaxValue, ulong.MinValue, 456 }, 3) // Only use first 3 to show that param is respected
));
// Assert
@ -84,15 +84,15 @@ namespace Microsoft.AspNetCore.Components.Server
0, // Length of UpdatedComponents
0, // Length of ReferenceFrames
0, // Length of DisposedComponentIds
3, 123, int.MaxValue, int.MinValue, // DisposedEventHandlerIds as length-prefixed array
3, (ulong)123, ulong.MaxValue, ulong.MinValue, // DisposedEventHandlerIds as length-prefixed array
0, // Index of UpdatedComponents
4, // Index of ReferenceFrames
8, // Index of DisposedComponentIds
12, // Index of DisposedEventHandlerIds
28 // Index of strings
40 // Index of strings
);
Assert.Equal(48, bytes.Length); // No other data
Assert.Equal(60, bytes.Length); // No other data
}
[Fact]
@ -200,7 +200,7 @@ namespace Microsoft.AspNetCore.Components.Server
RenderTreeFrame.Attribute(123, "Attribute with string value", "String value"),
RenderTreeFrame.Attribute(124, "Attribute with nonstring value", 1),
RenderTreeFrame.Attribute(125, "Attribute with delegate value", new Action(() => { }))
.WithAttributeEventHandlerId(789),
.WithAttributeEventHandlerId(((ulong)uint.MaxValue) + 1),
RenderTreeFrame.ChildComponent(126, typeof(object))
.WithComponentSubtreeLength(5678)
.WithComponent(new ComponentState(renderer, 2000, new FakeComponent(), null)),
@ -230,22 +230,22 @@ namespace Microsoft.AspNetCore.Components.Server
var referenceFramesStartIndex = ReadInt(bytes, bytes.Length - 16);
AssertBinaryContents(bytes, referenceFramesStartIndex,
16, // Number of frames
RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0,
RenderTreeFrameType.Attribute, "Attribute with nonstring value", NullStringMarker, 0,
RenderTreeFrameType.Attribute, "Attribute with delegate value", NullStringMarker, 789,
RenderTreeFrameType.Component, 5678, 2000, 0,
RenderTreeFrameType.ComponentReferenceCapture, 0, 0, 0,
RenderTreeFrameType.Element, 1234, "Some element", 0,
RenderTreeFrameType.ElementReferenceCapture, "my unique ID", 0, 0,
RenderTreeFrameType.Region, 1234, 0, 0,
RenderTreeFrameType.Text, "Some text", 0, 0,
RenderTreeFrameType.Markup, "Some markup", 0, 0,
RenderTreeFrameType.Text, "\n\t ", 0, 0,
RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0,
RenderTreeFrameType.Element, 999, "Some element", 0,
RenderTreeFrameType.Text, "Some text", 0, 0,
RenderTreeFrameType.Markup, "Some markup", 0, 0,
RenderTreeFrameType.Text, "\n\t ", 0, 0
RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0, 0,
RenderTreeFrameType.Attribute, "Attribute with nonstring value", NullStringMarker, 0, 0,
RenderTreeFrameType.Attribute, "Attribute with delegate value", NullStringMarker, ((ulong)uint.MaxValue) + 1,
RenderTreeFrameType.Component, 5678, 2000, 0, 0,
RenderTreeFrameType.ComponentReferenceCapture, 0, 0, 0, 0,
RenderTreeFrameType.Element, 1234, "Some element", 0, 0,
RenderTreeFrameType.ElementReferenceCapture, "my unique ID", 0, 0, 0,
RenderTreeFrameType.Region, 1234, 0, 0, 0,
RenderTreeFrameType.Text, "Some text", 0, 0, 0,
RenderTreeFrameType.Markup, "Some markup", 0, 0, 0,
RenderTreeFrameType.Text, "\n\t ", 0, 0, 0,
RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0, 0,
RenderTreeFrameType.Element, 999, "Some element", 0, 0,
RenderTreeFrameType.Text, "Some text", 0, 0, 0,
RenderTreeFrameType.Markup, "Some markup", 0, 0, 0,
RenderTreeFrameType.Text, "\n\t ", 0, 0, 0
);
Assert.Equal(new[]
@ -320,6 +320,10 @@ namespace Microsoft.AspNetCore.Components.Server
{
Assert.Equal(expectedInt, reader.ReadInt32());
}
else if (expectedEntry is ulong expectedUlong)
{
Assert.Equal(expectedUlong, reader.ReadUInt64());
}
else if (expectedEntry is string || expectedEntry == NullStringMarker)
{
// For strings, we have to look up the value in the table of strings

View File

@ -60,10 +60,10 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
public new Task RenderRootComponentAsync(int componentId, ParameterCollection parameters)
=> Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters));
public Task DispatchEventAsync(int eventHandlerId, UIEventArgs args)
public Task DispatchEventAsync(ulong eventHandlerId, UIEventArgs args)
=> Dispatcher.InvokeAsync(() => base.DispatchEventAsync(eventHandlerId, null, args));
public new Task DispatchEventAsync(int eventHandlerId, EventFieldInfo eventFieldInfo, UIEventArgs args)
public new Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo eventFieldInfo, UIEventArgs args)
=> Dispatcher.InvokeAsync(() => base.DispatchEventAsync(eventHandlerId, eventFieldInfo, args));
private static Task UnwrapTask(Task task)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -13,6 +13,8 @@ let invoke_method: (method: MethodHandle, target: System_Object, argsArrayPtr: n
let mono_string_get_utf8: (managedString: System_String) => Mono.Utf8Ptr;
let mono_string: (jsString: string) => System_String;
const appBinDirName = 'appBinDir';
const uint64HighOrderShift = Math.pow(2, 32);
const maxSafeNumberHighPart = Math.pow(2, 21) - 1; // The high-order int32 from Number.MAX_SAFE_INTEGER
export const monoPlatform: Platform = {
start: function start(loadAssemblyUrls: string[]) {
@ -122,6 +124,19 @@ export const monoPlatform: Platform = {
return Module.getValue((baseAddress as any as number) + (fieldOffset || 0), 'i32');
},
readUint64Field: function readHeapUint64(baseAddress: Pointer, fieldOffset?: number): number {
// Module.getValue(..., 'i64') doesn't work because the implementation treats 'i64' as
// being the same as 'i32'. Also we must take care to read both halves as unsigned.
const address = (baseAddress as any as number) + (fieldOffset || 0);
const heapU32Index = address >> 2;
const highPart = Module.HEAPU32[heapU32Index + 1];
if (highPart > maxSafeNumberHighPart) {
throw new Error(`Cannot read uint64 with high order part ${highPart}, because the result would exceed Number.MAX_SAFE_INTEGER.`);
}
return (highPart * uint64HighOrderShift) + Module.HEAPU32[heapU32Index];
},
readFloatField: function readHeapFloat(baseAddress: Pointer, fieldOffset?: number): number {
return Module.getValue((baseAddress as any as number) + (fieldOffset || 0), 'float');
},

View File

@ -16,6 +16,7 @@ export interface Platform {
getObjectFieldsBaseAddress(referenceTypedObject: System_Object): Pointer;
readInt16Field(baseAddress: Pointer, fieldOffset?: number): number;
readInt32Field(baseAddress: Pointer, fieldOffset?: number): number;
readUint64Field(baseAddress: Pointer, fieldOffset?: number): number;
readFloatField(baseAddress: Pointer, fieldOffset?: number): number;
readObjectField<T extends System_Object>(baseAddress: Pointer, fieldOffset?: number): T;
readStringField(baseAddress: Pointer, fieldOffset?: number): string | null;

View File

@ -2,11 +2,13 @@ import { RenderBatch, ArrayRange, RenderTreeDiff, ArrayValues, RenderTreeEdit, E
import { decodeUtf8 } from './Utf8Decoder';
const updatedComponentsEntryLength = 4; // Each is a single int32 giving the location of the data
const referenceFramesEntryLength = 16; // 1 byte for frame type, then 3 bytes for type-specific data
const referenceFramesEntryLength = 20; // 1 int for frame type, then 16 bytes for type-specific data
const disposedComponentIdsEntryLength = 4; // Each is an int32 giving the ID
const disposedEventHandlerIdsEntryLength = 4; // Each is an int32 giving the ID
const disposedEventHandlerIdsEntryLength = 8; // Each is an int64 giving the ID
const editsEntryLength = 16; // 4 ints
const stringTableEntryLength = 4; // Each is an int32 giving the string data location, or -1 for null
const uint64HighPartShift = Math.pow(2, 32);
const maxSafeNumberHighPart = Math.pow(2, 21) - 1; // The high-order int32 from Number.MAX_SAFE_INTEGER
export class OutOfProcessRenderBatch implements RenderBatch {
constructor(private batchData: Uint8Array) {
@ -51,7 +53,7 @@ export class OutOfProcessRenderBatch implements RenderBatch {
disposedEventHandlerIdsEntry(values: ArrayValues<number>, index: number): number {
const entryPos = (values as any) + index * disposedEventHandlerIdsEntryLength;
return readInt32LE(this.batchData, entryPos);
return readUint64LE(this.batchData, entryPos);
}
diffReader: RenderTreeDiffReader;
@ -160,7 +162,7 @@ class OutOfProcessRenderTreeFrameReader implements RenderTreeFrameReader {
}
attributeEventHandlerId(frame: RenderTreeFrame) {
return readInt32LE(this.batchDataUint8, frame as any + 12); // 4th int
return readUint64LE(this.batchDataUint8, frame as any + 12); // Bytes 12-19
}
}
@ -235,6 +237,24 @@ function readInt32LE(buffer: Uint8Array, position: number): any {
| (buffer[position + 3] << 24);
}
function readUint32LE(buffer: Uint8Array, position: number): any {
return (buffer[position])
+ (buffer[position + 1] << 8)
+ (buffer[position + 2] << 16)
+ ((buffer[position + 3] << 24) >>> 0); // The >>> 0 coerces the value to unsigned
}
function readUint64LE(buffer: Uint8Array, position: number): any {
// This cannot be done using bit-shift operators in JavaScript, because
// those all implicitly convert to int32
const highPart = readUint32LE(buffer, position + 4);
if (highPart > maxSafeNumberHighPart) {
throw new Error(`Cannot read uint64 with high order part ${highPart}, because the result would exceed Number.MAX_SAFE_INTEGER.`);
}
return (highPart * uint64HighPartShift) + readUint32LE(buffer, position);
}
function readLEB128(buffer: Uint8Array, position: number) {
let result = 0;
let shift = 0;

View File

@ -43,8 +43,8 @@ export class SharedMemoryRenderBatch implements RenderBatch {
}
disposedEventHandlerIdsEntry(values: ArrayValues<number>, index: number) {
const pointer = arrayValuesEntry(values, index, /* int length */ 4);
return platform.readInt32Field(pointer as any as Pointer);
const pointer = arrayValuesEntry(values, index, /* long length */ 8);
return platform.readUint64Field(pointer as any as Pointer);
}
arrayRangeReader = arrayRangeReader;
@ -108,7 +108,7 @@ const frameReader = {
markupContent: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16)!,
attributeName: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
attributeValue: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 24),
attributeEventHandlerId: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 8),
attributeEventHandlerId: (frame: RenderTreeFrame) => platform.readUint64Field(frame as any, 8),
};
function arrayValuesEntry<T>(arrayValues: ArrayValues<T>, index: number, itemSize: number): T {

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Components.Web
public int BrowserRendererId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string EventArgsType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Components.Rendering.EventFieldInfo EventFieldInfo { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public int EventHandlerId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public ulong EventHandlerId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
}
}

View File

@ -128,7 +128,7 @@ namespace Microsoft.AspNetCore.Components.Web
/// <summary>
/// For framework use only.
/// </summary>
public int EventHandlerId { get; set; }
public ulong EventHandlerId { get; set; }
/// <summary>
/// For framework use only.

View File

@ -150,7 +150,7 @@ namespace Ignitor
RenderTreeFrame.Attribute(123, "Attribute with string value", "String value"),
RenderTreeFrame.Attribute(124, "Attribute with nonstring value", 1),
RenderTreeFrame.Attribute(125, "Attribute with delegate value", new Action(() => { }))
.WithAttributeEventHandlerId(789),
.WithAttributeEventHandlerId((ulong)uint.MaxValue + 1),
RenderTreeFrame.ChildComponent(126, typeof(object))
.WithComponentSubtreeLength(5678)
.WithComponent(new ComponentState(renderer, 2000, new FakeComponent(), null)),
@ -180,22 +180,22 @@ namespace Ignitor
var referenceFramesStartIndex = ReadInt(bytes, bytes.Length - 16);
AssertBinaryContents(bytes, referenceFramesStartIndex,
16, // Number of frames
RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0,
RenderTreeFrameType.Attribute, "Attribute with nonstring value", NullStringMarker, 0,
RenderTreeFrameType.Attribute, "Attribute with delegate value", NullStringMarker, 789,
RenderTreeFrameType.Component, 5678, 2000, 0,
RenderTreeFrameType.ComponentReferenceCapture, 0, 0, 0,
RenderTreeFrameType.Element, 1234, "Some element", 0,
RenderTreeFrameType.ElementReferenceCapture, "my unique ID", 0, 0,
RenderTreeFrameType.Region, 1234, 0, 0,
RenderTreeFrameType.Text, "Some text", 0, 0,
RenderTreeFrameType.Markup, "Some markup", 0, 0,
RenderTreeFrameType.Text, "\n\t ", 0, 0,
RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0,
RenderTreeFrameType.Element, 999, "Some element", 0,
RenderTreeFrameType.Text, "Some text", 0, 0,
RenderTreeFrameType.Markup, "Some markup", 0, 0,
RenderTreeFrameType.Text, "\n\t ", 0, 0
RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0, 0,
RenderTreeFrameType.Attribute, "Attribute with nonstring value", NullStringMarker, 0, 0,
RenderTreeFrameType.Attribute, "Attribute with delegate value", NullStringMarker, (ulong)uint.MaxValue + 1,
RenderTreeFrameType.Component, 5678, 2000, 0, 0,
RenderTreeFrameType.ComponentReferenceCapture, 0, 0, 0, 0,
RenderTreeFrameType.Element, 1234, "Some element", 0, 0,
RenderTreeFrameType.ElementReferenceCapture, "my unique ID", 0, 0, 0,
RenderTreeFrameType.Region, 1234, 0, 0, 0,
RenderTreeFrameType.Text, "Some text", 0, 0, 0,
RenderTreeFrameType.Markup, "Some markup", 0, 0, 0,
RenderTreeFrameType.Text, "\n\t ", 0, 0, 0,
RenderTreeFrameType.Attribute, "Attribute with string value", "String value", 0, 0,
RenderTreeFrameType.Element, 999, "Some element", 0, 0,
RenderTreeFrameType.Text, "Some text", 0, 0, 0,
RenderTreeFrameType.Markup, "Some markup", 0, 0, 0,
RenderTreeFrameType.Text, "\n\t ", 0, 0, 0
);
Assert.Equal(new[]
@ -279,6 +279,10 @@ namespace Ignitor
{
Assert.Equal(expectedInt, reader.ReadInt32());
}
else if (expectedEntry is ulong expectedUlong)
{
Assert.Equal(expectedUlong, reader.ReadUInt64());
}
else if (expectedEntry is string || expectedEntry == NullStringMarker)
{
// For strings, we have to look up the value in the table of strings

View File

@ -17,7 +17,10 @@ namespace ComponentsApp.Server
{
services.AddMvc();
services.AddSingleton<CircuitHandler, LoggingCircuitHandler>();
services.AddServerSideBlazor();
services.AddServerSideBlazor(options =>
{
options.JSInteropDetailedErrors = true;
});
services.AddSingleton<WeatherForecastService, DefaultWeatherForecastService>();
}

View File

@ -93,7 +93,7 @@ namespace Ignitor
}
private void DisposeEventHandler(int eventHandlerId)
private void DisposeEventHandler(ulong eventHandlerId)
{
}

View File

@ -101,7 +101,7 @@ namespace Ignitor
public class ElementEventDescriptor
{
public ElementEventDescriptor(string eventName, int eventId)
public ElementEventDescriptor(string eventName, ulong eventId)
{
EventName = eventName ?? throw new ArgumentNullException(nameof(eventName));
EventId = eventId;
@ -109,7 +109,7 @@ namespace Ignitor
public string EventName { get; }
public int EventId { get; }
public ulong EventId { get; }
}
public async Task ClickAsync(HubConnection connection)

View File

@ -14,6 +14,8 @@ namespace Ignitor
{
public static class RenderBatchReader
{
private const int ReferenceFrameSize = 20;
private static readonly Renderer Renderer = new FakeRenderer();
public static RenderBatch Read(ReadOnlySpan<byte> data)
@ -131,69 +133,67 @@ namespace Ignitor
private static ArrayRange<RenderTreeFrame> ReadReferenceFrames(ReadOnlySpan<byte> data, string[] strings)
{
var result = new RenderTreeFrame[data.Length / 16];
var result = new RenderTreeFrame[data.Length / ReferenceFrameSize];
for (var i = 0; i < data.Length; i += 16)
for (var i = 0; i < data.Length; i += ReferenceFrameSize)
{
var frameData = data.Slice(i, 16);
var frameData = data.Slice(i, ReferenceFrameSize);
var type = (RenderTreeFrameType)BitConverter.ToInt32(frameData.Slice(0, 4));
// 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
// Since we can fit every frame type into 16 bytes, 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).
// 16 bytes.
switch (type)
{
case RenderTreeFrameType.Attribute:
var attributeName = ReadString(frameData.Slice(4, 4), strings);
var attributeValue = ReadString(frameData.Slice(8, 4), strings);
var attributeEventHandlerId = BitConverter.ToInt32(frameData.Slice(12, 4));
result[i / 16] = RenderTreeFrame.Attribute(0, attributeName, attributeValue).WithAttributeEventHandlerId(attributeEventHandlerId);
var attributeEventHandlerId = BitConverter.ToUInt64(frameData.Slice(12, 8));
result[i / ReferenceFrameSize] = RenderTreeFrame.Attribute(0, attributeName, attributeValue).WithAttributeEventHandlerId(attributeEventHandlerId);
break;
case RenderTreeFrameType.Component:
var componentSubtreeLength = BitConverter.ToInt32(frameData.Slice(4, 4));
var componentId = BitConverter.ToInt32(frameData.Slice(8, 4)); // Nowhere to put this without creating a ComponentState
result[i / 16] = RenderTreeFrame.ChildComponent(0, componentType: null)
result[i / ReferenceFrameSize] = RenderTreeFrame.ChildComponent(0, componentType: null)
.WithComponentSubtreeLength(componentSubtreeLength)
.WithComponent(new ComponentState(Renderer, componentId, new FakeComponent(), null));
break;
case RenderTreeFrameType.ComponentReferenceCapture:
// Client doesn't process these, skip.
result[i / 16] = RenderTreeFrame.ComponentReferenceCapture(0, null, 0);
result[i / ReferenceFrameSize] = RenderTreeFrame.ComponentReferenceCapture(0, null, 0);
break;
case RenderTreeFrameType.Element:
var elementSubtreeLength = BitConverter.ToInt32(frameData.Slice(4, 4));
var elementName = ReadString(frameData.Slice(8, 4), strings);
result[i / 16] = RenderTreeFrame.Element(0, elementName).WithElementSubtreeLength(elementSubtreeLength);
result[i / ReferenceFrameSize] = RenderTreeFrame.Element(0, elementName).WithElementSubtreeLength(elementSubtreeLength);
break;
case RenderTreeFrameType.ElementReferenceCapture:
var referenceCaptureId = ReadString(frameData.Slice(4, 4), strings);
result[i / 16] = RenderTreeFrame.ElementReferenceCapture(0, null)
result[i / ReferenceFrameSize] = RenderTreeFrame.ElementReferenceCapture(0, null)
.WithElementReferenceCaptureId(referenceCaptureId);
break;
case RenderTreeFrameType.Region:
var regionSubtreeLength = BitConverter.ToInt32(frameData.Slice(4, 4));
result[i / 16] = RenderTreeFrame.Region(0).WithRegionSubtreeLength(regionSubtreeLength);
result[i / ReferenceFrameSize] = RenderTreeFrame.Region(0).WithRegionSubtreeLength(regionSubtreeLength);
break;
case RenderTreeFrameType.Text:
var text = ReadString(frameData.Slice(4, 4), strings);
result[i / 16] = RenderTreeFrame.Text(0, text);
result[i / ReferenceFrameSize] = RenderTreeFrame.Text(0, text);
break;
case RenderTreeFrameType.Markup:
var markup = ReadString(frameData.Slice(4, 4), strings);
result[i / 16] = RenderTreeFrame.Markup(0, markup);
result[i / ReferenceFrameSize] = RenderTreeFrame.Markup(0, markup);
break;
default:
@ -209,9 +209,9 @@ namespace Ignitor
return new ArrayRange<int>(Array.Empty<int>(), 0);
}
private static ArrayRange<int> ReadDisposedEventHandlerIds(ReadOnlySpan<byte> data)
private static ArrayRange<ulong> ReadDisposedEventHandlerIds(ReadOnlySpan<byte> data)
{
return new ArrayRange<int>(Array.Empty<int>(), 0);
return new ArrayRange<ulong>(Array.Empty<ulong>(), 0);
}
private static string ReadString(ReadOnlySpan<byte> data, string[] strings)
@ -276,7 +276,7 @@ namespace Ignitor
{
// This is a count-prefixed contiguous array of RenderTreeFrame.
var count = BitConverter.ToInt32(data.Slice(_referenceFrames, 4));
return data.Slice(_referenceFrames + 4, count * 16);
return data.Slice(_referenceFrames + 4, count * ReferenceFrameSize);
}
public ReadOnlySpan<byte> GetStringTableIndexes(ReadOnlySpan<byte> data)