Skip to content

Commit acb01f8

Browse files
Cache JS->.NET string decoding results within a single renderbatch
1 parent c202344 commit acb01f8

File tree

4 files changed

+62
-6
lines changed

4 files changed

+62
-6
lines changed

src/Components/Web.JS/dist/Release/blazor.webassembly.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Boot.WebAssembly.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,19 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
2929
window['Blazor'].platform = platform;
3030
window['Blazor']._internal.renderBatch = (browserRendererId: number, batchAddress: Pointer) => {
3131
profileStart('renderBatch');
32-
renderBatch(browserRendererId, new SharedMemoryRenderBatch(batchAddress));
32+
33+
// We're going to read directly from the .NET memory heap, so indicate to the platform
34+
// that we don't want anything to modify the memory contents during this time. Currently this
35+
// is only guaranteed by the fact that .NET code doesn't run during this time, but in the
36+
// future (when multithreading is implemented) we might need the .NET runtime to understand
37+
// that GC compaction isn't allowed during this critical section.
38+
const heapLock = monoPlatform.beginHeapLock();
39+
try {
40+
renderBatch(browserRendererId, new SharedMemoryRenderBatch(batchAddress));
41+
} finally {
42+
heapLock.release();
43+
}
44+
3345
profileEnd('renderBatch');
3446
};
3547

src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@ import { DotNet } from '@microsoft/dotnet-js-interop';
22
import { attachDebuggerHotkey, hasDebuggingEnabled } from './MonoDebugger';
33
import { showErrorNotification } from '../../BootErrors';
44
import { WebAssemblyResourceLoader, LoadingResource } from '../WebAssemblyResourceLoader';
5-
import { Platform, System_Array, Pointer, System_Object, System_String } from '../Platform';
5+
import { Platform, System_Array, Pointer, System_Object, System_String, HeapLock } from '../Platform';
66
import { loadTimezoneData } from './TimezoneDataFile';
77
import { WebAssemblyBootResourceType } from '../WebAssemblyStartOptions';
88
import { initializeProfiling } from '../Profiling';
99

10-
let mono_string_get_utf8: (managedString: System_String) => Pointer;
1110
let mono_wasm_add_assembly: (name: string, heapAddress: number, length: number) => void;
1211
const appBinDirName = 'appBinDir';
1312
const uint64HighOrderShift = Math.pow(2, 32);
1413
const maxSafeNumberHighPart = Math.pow(2, 21) - 1; // The high-order int32 from Number.MAX_SAFE_INTEGER
1514

15+
let currentHeapLock: MonoHeapLock | null = null;
16+
1617
// Memory access helpers
1718
// The implementations are exactly equivalent to what the global getValue(addr, type) function does,
1819
// except without having to parse the 'type' parameter, and with less risk of mistakes at the call site
@@ -124,12 +125,29 @@ export const monoPlatform: Platform = {
124125
return unboxedValue;
125126
}
126127

127-
return BINDING.conv_string(fieldValue as any as System_String);
128+
let decodedString: string | null | undefined;
129+
if (currentHeapLock) {
130+
decodedString = currentHeapLock.stringCache.get(fieldValue);
131+
if (decodedString === undefined) {
132+
decodedString = BINDING.conv_string(fieldValue as any as System_String);
133+
currentHeapLock.stringCache.set(fieldValue, decodedString);
134+
}
135+
} else {
136+
decodedString = BINDING.conv_string(fieldValue as any as System_String);
137+
}
138+
139+
return decodedString;
128140
},
129141

130142
readStructField: function readStructField<T extends Pointer>(baseAddress: Pointer, fieldOffset?: number): T {
131143
return ((baseAddress as any as number) + (fieldOffset || 0)) as any as T;
132144
},
145+
146+
beginHeapLock: function() {
147+
assertHeapIsNotLocked();
148+
currentHeapLock = new MonoHeapLock();
149+
return currentHeapLock;
150+
}
133151
};
134152

135153
function addScriptTagsToDocument(resourceLoader: WebAssemblyResourceLoader) {
@@ -246,7 +264,6 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
246264
module.preRun.push(() => {
247265
// By now, emscripten should be initialised enough that we can capture these methods for later use
248266
mono_wasm_add_assembly = cwrap('mono_wasm_add_assembly', null, ['string', 'number', 'number']);
249-
mono_string_get_utf8 = cwrap('mono_wasm_string_get_utf8', 'number', ['number']);
250267
MONO.loaded_files = [];
251268

252269
if (timeZoneResource) {
@@ -387,6 +404,7 @@ function attachInteropInvoker(): void {
387404

388405
DotNet.attachDispatcher({
389406
beginInvokeDotNetFromJS: (callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: any | null, argsJson: string): void => {
407+
assertHeapIsNotLocked();
390408
if (!dotNetObjectId && !assemblyName) {
391409
throw new Error('Either assemblyName or dotNetObjectId must have a non null value.');
392410
}
@@ -409,6 +427,7 @@ function attachInteropInvoker(): void {
409427
);
410428
},
411429
invokeDotNetFromJS: (assemblyName, methodIdentifier, dotNetObjectId, argsJson) => {
430+
assertHeapIsNotLocked();
412431
return dotNetDispatcherInvokeMethodHandle(
413432
assemblyName ? assemblyName : null,
414433
methodIdentifier,
@@ -460,3 +479,22 @@ function changeExtension(filename: string, newExtensionWithLeadingDot: string) {
460479

461480
return filename.substr(0, lastDotIndex) + newExtensionWithLeadingDot;
462481
}
482+
483+
function assertHeapIsNotLocked() {
484+
if (currentHeapLock) {
485+
throw new Error('Assertion failed - heap is currently locked');
486+
}
487+
}
488+
489+
class MonoHeapLock implements HeapLock {
490+
// Within a given heap lock, it's safe to cache decoded strings since the memory can't change
491+
stringCache = new Map<number, string | null>();
492+
493+
release() {
494+
if (currentHeapLock !== this) {
495+
throw new Error('Trying to release a lock which isn\'t current');
496+
}
497+
498+
currentHeapLock = null;
499+
}
500+
}

src/Components/Web.JS/src/Platform/Platform.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export interface Platform {
1818
readObjectField<T extends System_Object>(baseAddress: Pointer, fieldOffset?: number): T;
1919
readStringField(baseAddress: Pointer, fieldOffset?: number, readBoolValueAsString?: boolean): string | null;
2020
readStructField<T extends Pointer>(baseAddress: Pointer, fieldOffset?: number): T;
21+
22+
beginHeapLock(): HeapLock;
23+
}
24+
25+
export interface HeapLock {
26+
release();
2127
}
2228

2329
// We don't actually instantiate any of these at runtime. For perf it's preferable to

0 commit comments

Comments
 (0)