-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Cache JS->.NET string decoding results within a single renderbatch #23773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
acb01f8
a7ebaef
f97325b
cf32d51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,17 +2,18 @@ import { DotNet } from '@microsoft/dotnet-js-interop'; | |
import { attachDebuggerHotkey, hasDebuggingEnabled } from './MonoDebugger'; | ||
import { showErrorNotification } from '../../BootErrors'; | ||
import { WebAssemblyResourceLoader, LoadingResource } from '../WebAssemblyResourceLoader'; | ||
import { Platform, System_Array, Pointer, System_Object, System_String } from '../Platform'; | ||
import { Platform, System_Array, Pointer, System_Object, System_String, HeapLock } from '../Platform'; | ||
import { loadTimezoneData } from './TimezoneDataFile'; | ||
import { WebAssemblyBootResourceType } from '../WebAssemblyStartOptions'; | ||
import { initializeProfiling } from '../Profiling'; | ||
|
||
let mono_string_get_utf8: (managedString: System_String) => Pointer; | ||
let mono_wasm_add_assembly: (name: string, heapAddress: number, length: number) => void; | ||
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 | ||
|
||
let currentHeapLock: MonoHeapLock | null = null; | ||
|
||
// Memory access helpers | ||
// The implementations are exactly equivalent to what the global getValue(addr, type) function does, | ||
// except without having to parse the 'type' parameter, and with less risk of mistakes at the call site | ||
|
@@ -124,12 +125,38 @@ export const monoPlatform: Platform = { | |
return unboxedValue; | ||
} | ||
|
||
return BINDING.conv_string(fieldValue as any as System_String); | ||
let decodedString: string | null | undefined; | ||
if (currentHeapLock) { | ||
decodedString = currentHeapLock.stringCache.get(fieldValue); | ||
if (decodedString === undefined) { | ||
decodedString = BINDING.conv_string(fieldValue as any as System_String); | ||
currentHeapLock.stringCache.set(fieldValue, decodedString); | ||
} | ||
} else { | ||
decodedString = BINDING.conv_string(fieldValue as any as System_String); | ||
} | ||
|
||
return decodedString; | ||
}, | ||
|
||
readStructField: function readStructField<T extends Pointer>(baseAddress: Pointer, fieldOffset?: number): T { | ||
return ((baseAddress as any as number) + (fieldOffset || 0)) as any as T; | ||
}, | ||
|
||
beginHeapLock: function() { | ||
assertHeapIsNotLocked(); | ||
currentHeapLock = new MonoHeapLock(); | ||
return currentHeapLock; | ||
}, | ||
|
||
invokeWhenHeapUnlocked: function(callback) { | ||
// This is somewhat like a sync context. If we're not locked, just pass through the call directly. | ||
if (!currentHeapLock) { | ||
callback(); | ||
} else { | ||
currentHeapLock.enqueuePostReleaseAction(callback); | ||
} | ||
} | ||
}; | ||
|
||
function addScriptTagsToDocument(resourceLoader: WebAssemblyResourceLoader) { | ||
|
@@ -246,7 +273,6 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade | |
module.preRun.push(() => { | ||
// By now, emscripten should be initialised enough that we can capture these methods for later use | ||
mono_wasm_add_assembly = cwrap('mono_wasm_add_assembly', null, ['string', 'number', 'number']); | ||
mono_string_get_utf8 = cwrap('mono_wasm_string_get_utf8', 'number', ['number']); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was not used |
||
MONO.loaded_files = []; | ||
|
||
if (timeZoneResource) { | ||
|
@@ -387,6 +413,7 @@ function attachInteropInvoker(): void { | |
|
||
DotNet.attachDispatcher({ | ||
beginInvokeDotNetFromJS: (callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: any | null, argsJson: string): void => { | ||
assertHeapIsNotLocked(); | ||
if (!dotNetObjectId && !assemblyName) { | ||
throw new Error('Either assemblyName or dotNetObjectId must have a non null value.'); | ||
} | ||
|
@@ -409,6 +436,7 @@ function attachInteropInvoker(): void { | |
); | ||
}, | ||
invokeDotNetFromJS: (assemblyName, methodIdentifier, dotNetObjectId, argsJson) => { | ||
assertHeapIsNotLocked(); | ||
return dotNetDispatcherInvokeMethodHandle( | ||
assemblyName ? assemblyName : null, | ||
methodIdentifier, | ||
|
@@ -460,3 +488,42 @@ function changeExtension(filename: string, newExtensionWithLeadingDot: string) { | |
|
||
return filename.substr(0, lastDotIndex) + newExtensionWithLeadingDot; | ||
} | ||
|
||
function assertHeapIsNotLocked() { | ||
if (currentHeapLock) { | ||
throw new Error('Assertion failed - heap is currently locked'); | ||
} | ||
} | ||
|
||
class MonoHeapLock implements HeapLock { | ||
// Within a given heap lock, it's safe to cache decoded strings since the memory can't change | ||
stringCache = new Map<number, string | null>(); | ||
|
||
private postReleaseActions?: Function[]; | ||
|
||
enqueuePostReleaseAction(callback: Function) { | ||
if (!this.postReleaseActions) { | ||
this.postReleaseActions = []; | ||
} | ||
|
||
this.postReleaseActions.push(callback); | ||
} | ||
|
||
release() { | ||
if (currentHeapLock !== this) { | ||
throw new Error('Trying to release a lock which isn\'t current'); | ||
} | ||
|
||
currentHeapLock = null; | ||
|
||
while (this.postReleaseActions?.length) { | ||
const nextQueuedAction = this.postReleaseActions.shift()!; | ||
|
||
// It's possible that the action we invoke here might itself take a succession of heap locks, | ||
// but since heap locks must be released synchronously, by the time we get back to this stack | ||
// frame, we know the heap should no longer be locked. | ||
nextQueuedAction(); | ||
assertHeapIsNotLocked(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,9 +10,9 @@ export function dispatchEvent(eventDescriptor: EventDescriptor, eventArgs: UIEve | |
throw new Error('eventDispatcher not initialized. Call \'setEventDispatcher\' to configure it.'); | ||
} | ||
|
||
return eventDispatcherInstance(eventDescriptor, eventArgs); | ||
eventDispatcherInstance(eventDescriptor, eventArgs); | ||
} | ||
|
||
export function setEventDispatcher(newDispatcher: (eventDescriptor: EventDescriptor, eventArgs: UIEventArgs) => Promise<void>): void { | ||
export function setEventDispatcher(newDispatcher: (eventDescriptor: EventDescriptor, eventArgs: UIEventArgs) => void): void { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've changed So having it return |
||
eventDispatcherInstance = newDispatcher; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
<h1>Event during batch rendering</h1> | ||
|
||
<p> | ||
While Blazor WebAssembly is rendering a batch, the JavaScript code reads data from the .NET heap directly. | ||
So, it's essential that .NET code doesn't run during this time (either to modify the state of the | ||
render tree or to perform garbage collection which may relocate objects in the heap). | ||
</p> | ||
<p> | ||
To ensure this is safe, batch rendering is a fully synchronous process during which the JS code doesn't | ||
yield control to user code. However, there are possible cases where user code may be triggered unavoidably | ||
including (1) JavaScript DOM mutation observers, (2) Web Component lifecycle events, and (3) edge cases | ||
where Blazor performing a DOM mutation can itself trigger a .NET-bound event such as "change". | ||
</p> | ||
<p> | ||
Cases (1) and (2) result in developer-supplied JS code executing, which may try to perform .NET interop. | ||
The intended behavior is that .NET interop calls should be blocked while a batch is being rendered. | ||
Developers need to wrap such calls in <code>requestAnimationFrame</code> or <code>setTimeout(..., 0)</code> | ||
or similar, so that it runs after the current batch has finished rendering. | ||
</p> | ||
<p> | ||
Case (3) more directly results in developer-supplied .NET code executing. The intended behavior in this case | ||
is that Blazor takes care of deferring the event dispatch until the current batch finishes rendering. This | ||
shouldn't be regarded as problematic, because Blazor has never guaranteed synchronous dispatch of DOM event | ||
handlers (in the Blazor Server case, all DOM event handlers run asynchronously). | ||
</p> | ||
|
||
<h2>WebComponent attempting JS interop during batch rendering (cases 1 & 2 above)</h2> | ||
|
||
@for (var i = 0; i < numWebComponents; i++) | ||
{ | ||
<custom-web-component-performing-js-interop>Instance @i</custom-web-component-performing-js-interop> | ||
} | ||
|
||
<button id="add-web-component" @onclick="@(() => numWebComponents++)">Add a web component</button> | ||
|
||
<pre id="web-component-error-log"></pre> | ||
|
||
<h2>DOM mutation triggering a .NET event handler (case 3 above)</h2> | ||
|
||
<p> | ||
Type into either text box. Each keystroke will swap the list order, causing a change event during batch rendering. | ||
</p> | ||
|
||
<div id="reversible-list"> | ||
@foreach (var item in itemsList) | ||
{ | ||
<div @key="item"> | ||
<input @oninput="@(() => itemsList.Reverse())" | ||
@onchange="@(evt => eventLog += $"Change event on item {item.Name} with value {evt.Value}\n")" /> | ||
</div> | ||
} | ||
</div> | ||
|
||
<pre id="event-log">@eventLog</pre> | ||
|
||
@code { | ||
string eventLog = ""; | ||
int numWebComponents = 0; | ||
|
||
class ListItem | ||
{ | ||
public string Name { get; set; } | ||
} | ||
|
||
List<ListItem> itemsList = new List<ListItem> | ||
{ | ||
new ListItem { Name = "First" }, | ||
new ListItem { Name = "Second" }, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// This web component is used from the EventDuringBatchRendering test case | ||
|
||
window.customElements.define('custom-web-component-performing-js-interop', class extends HTMLElement { | ||
connectedCallback() { | ||
this.attachShadow({ mode: 'open' }); | ||
this.shadowRoot.innerHTML = ` | ||
<div style='border: 2px dashed red; margin: 10px 0; padding: 5px; background: #dddddd;'> | ||
<slot></slot> | ||
</div> | ||
`; | ||
|
||
// Since this happens during batch rendering, it will be blocked. | ||
// In the future we could allow async calls, but this is enough of an edge case | ||
// that it doesn't need to be implemented currently. Developers who need to do this | ||
// can wrap their interop call in requestAnimationFrame or setTimeout(..., 0). | ||
(async function () { | ||
try { | ||
await DotNet.invokeMethodAsync('SomeAssembly', 'SomeMethodThatDoesntNeedToExistForThisTest'); | ||
} catch (ex) { | ||
document.getElementById('web-component-error-log').innerText += ex.toString() + '\n'; | ||
} | ||
})(); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there code that hits this code path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, any of the framework's built-in unmarshalled interop calls that read strings would hit this.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you could argue that they too should take a heap lock, but I'm not trying to change everything here. We only need to reason about all unmarshalled interop if/when we get multithreading.