[wasm] Add support for symbolicating native traces from JS, using a symbols file...
authorAnkit Jain <radical@gmail.com>
Sat, 5 Mar 2022 02:38:26 +0000 (21:38 -0500)
committerGitHub <noreply@github.com>
Sat, 5 Mar 2022 02:38:26 +0000 (21:38 -0500)
* [wasm] Fix run command lines for samples

* [wasm] Export FS_readFile

* [wasm] Add support for symbolicating traces in console errors, or

.. warnings. The regex patterns are hardcoded in `debug.ts`. And the
symbols are loaded from `dotnet.js.symbols`.

We try to symbolicate wherever possible.

* [wasm] Enable symbolicating for non-aot test builds by default

* [wasm] Enable symbol map for debugger tests

* Address feedback from @pavelsavara

* [wasm] Add a `mono_wasm_symbolicate_string` function

.. and use that to symbolicate traces marshaled as C# exceptions (thanks
to @pavelsavara for the suggestion).

13 files changed:
eng/testing/tests.wasm.targets
src/mono/sample/wasm/wasm.mk
src/mono/wasm/build/WasmApp.targets
src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj
src/mono/wasm/runtime/debug.ts
src/mono/wasm/runtime/dotnet.d.ts
src/mono/wasm/runtime/exports.ts
src/mono/wasm/runtime/imports.ts
src/mono/wasm/runtime/method-calls.ts
src/mono/wasm/runtime/run.ts
src/mono/wasm/runtime/types/emscripten.ts
src/mono/wasm/test-main.js
src/mono/wasm/wasm.proj

index 7c20c14..bbdd9c6 100644 (file)
@@ -5,11 +5,16 @@
     <WasmGenerateAppBundle Condition="'$(WasmGenerateAppBundle)' == ''">true</WasmGenerateAppBundle>
     <BundleTestAppTargets>$(BundleTestAppTargets);BundleTestWasmApp</BundleTestAppTargets>
     <DebuggerSupport Condition="'$(DebuggerSupport)' == '' and '$(Configuration)' == 'Debug'">true</DebuggerSupport>
-    <WasmNativeStrip Condition="'$(WasmNativeStrip)' == ''">false</WasmNativeStrip>
     <!-- Some tests expect to load satellite assemblies by path, eg. System.Runtime.Loader.Tests,
          so, just setting it true by default -->
     <IncludeSatelliteAssembliesInVFS Condition="'$(IncludeSatelliteAssembliesInVFS)' == ''">true</IncludeSatelliteAssembliesInVFS>
-    <WasmEmitSymbolMap Condition="'$(WasmEmitSymbolMap)' == ''">true</WasmEmitSymbolMap>
+
+    <!--
+      - For regular library tests, it will use the symbols file from the runtime pack.
+      - for AOT library tests, we use WasmNativeStrip=false, so we already have symbols
+    -->
+    <WasmNativeStrip Condition="'$(WasmNativeStrip)' == ''">false</WasmNativeStrip>
+    <WasmEmitSymbolMap Condition="'$(WasmEmitSymbolMap)' == '' and '$(RunAOTCompilation)' != 'true'">true</WasmEmitSymbolMap>
 
     <!-- Run only if previous command succeeded -->
     <_ShellCommandSeparator Condition="'$(OS)' == 'Windows_NT'">&amp;&amp;</_ShellCommandSeparator>
index 62c62fe..1d9d407 100644 (file)
@@ -39,7 +39,7 @@ run-browser:
        fi
 
 run-console:
-       cd bin/$(CONFIG)/AppBundle && $(V8_PATH) --stack-trace-limit=1000 --single-threaded --expose_wasm $(MAIN_JS) -- $(DOTNET_MONO_LOG_LEVEL) --run $(CONSOLE_DLL) $(ARGS)
+       cd bin/$(CONFIG)/AppBundle && $(V8_PATH) --stack-trace-limit=1000 --single-threaded --expose_wasm $(MAIN_JS) -- $(ARGS)
 
 run-console-node:
-       cd bin/$(CONFIG)/AppBundle && node --stack-trace-limit=1000 --single-threaded --expose_wasm $(MAIN_JS) -- $(DOTNET_MONO_LOG_LEVEL) --run $(CONSOLE_DLL) $(ARGS)
+       cd bin/$(CONFIG)/AppBundle && node --stack-trace-limit=1000 --single-threaded --expose_wasm $(MAIN_JS) $(ARGS)
index 5304109..b062cc3 100644 (file)
@@ -98,9 +98,6 @@
 
     <EnableDefaultWasmAssembliesToBundle Condition="'$(EnableDefaultWasmAssembliesToBundle)' == ''">true</EnableDefaultWasmAssembliesToBundle>
     <WasmBuildOnlyAfterPublish Condition="'$(WasmBuildOnlyAfterPublish)' == '' and '$(DeployOnBuild)' == 'true'">true</WasmBuildOnlyAfterPublish>
-
-    <!-- Temporarily `false`, till sdk gets a fix for supporting the new file -->
-    <WasmEmitSymbolMap Condition="'$(WasmEmitSymbolMap)' == ''">false</WasmEmitSymbolMap>
   </PropertyGroup>
 
   <!-- PUBLISH -->
index c19d678..a84d9c3 100644 (file)
@@ -7,6 +7,7 @@
     <WasmBuildAppDependsOn>PrepareForWasmBuildApp;$(WasmBuildAppDependsOn)</WasmBuildAppDependsOn>
     <WasmGenerateAppBundle>true</WasmGenerateAppBundle>
     <OutputType>library</OutputType>
+    <WasmEmitSymbolMap>true</WasmEmitSymbolMap>
   </PropertyGroup>
 
   <ItemGroup>
index ac7b035..1684ff3 100644 (file)
@@ -7,12 +7,30 @@ import cwraps from "./cwraps";
 import { VoidPtr, CharPtr } from "./types/emscripten";
 
 const commands_received : any = new Map<number, CommandResponse>();
+const wasm_func_map = new Map<number, string>();
 commands_received.remove = function (key: number) : CommandResponse { const value = this.get(key); this.delete(key); return value;};
 let _call_function_res_cache: any = {};
 let _next_call_function_res_id = 0;
 let _debugger_buffer_len = -1;
 let _debugger_buffer: VoidPtr;
 
+const regexes:any[] = [];
+
+// V8
+//   at <anonymous>:wasm-function[1900]:0x83f63
+//   at dlfree (<anonymous>:wasm-function[18739]:0x2328ef)
+regexes.push(/at (?<replaceSection>[^:()]+:wasm-function\[(?<funcNum>\d+)\]:0x[a-fA-F\d]+)((?![^)a-fA-F\d])|$)/);
+
+//# 5: WASM [009712b2], function #111 (''), pc=0x7c16595c973 (+0x53), pos=38740 (+11)
+regexes.push(/(?:WASM \[[\da-zA-Z]+\], (?<replaceSection>function #(?<funcNum>[\d]+) \(''\)))/);
+
+//# chrome
+//# at http://127.0.0.1:63817/dotnet.wasm:wasm-function[8963]:0x1e23f4
+regexes.push(/(?<replaceSection>[a-z]+:\/\/[^ )]*:wasm-function\[(?<funcNum>\d+)\]:0x[a-fA-F\d]+)/);
+
+//# <?>.wasm-function[8962]
+regexes.push(/(?<replaceSection><[^ >]+>[.:]wasm-function\[(?<funcNum>[0-9]+)\])/);
+
 export function mono_wasm_runtime_ready(): void {
     runtimeHelpers.mono_wasm_runtime_is_ready = true;
 
@@ -27,6 +45,8 @@ export function mono_wasm_runtime_ready(): void {
         debugger;
     else
         console.debug("mono_wasm_runtime_ready", "fe00e07a-5519-4dfe-b35a-f867dbaf2e28");
+
+    _readSymbolMapFile("dotnet.js.symbols");
 }
 
 export function mono_wasm_fire_debugger_agent_message(): void {
@@ -314,25 +334,93 @@ export function mono_wasm_debugger_log(level: number, message_ptr: CharPtr): voi
     console.debug(`Debugger.Debug: ${message}`);
 }
 
+function _readSymbolMapFile(filename: string): void {
+    try {
+        const res = Module.FS_readFile(filename, {flags: "r", encoding: "utf8"});
+        res.split(/[\r\n]/).forEach((line: string) => {
+            const parts:string[] = line.split(/:/);
+            if (parts.length < 2)
+                return;
+
+            parts[1] = parts.splice(1).join(":");
+            wasm_func_map.set(Number(parts[0]), parts[1]);
+        });
+
+        console.debug(`Loaded ${wasm_func_map.size} symbols`);
+    } catch (error:any) {
+        if (error.errno == 44) // NOENT
+            console.debug(`Could not find symbols file ${filename}. Ignoring.`);
+        else
+            console.log(`Error loading symbol file ${filename}: ${JSON.stringify(error)}`);
+        return;
+    }
+}
+
+export function mono_wasm_symbolicate_string(message: string): string {
+    try {
+        if (wasm_func_map.size == 0)
+            return message;
+
+        const origMessage = message;
+
+        for (let i = 0; i < regexes.length; i ++)
+        {
+            const newRaw = message.replace(new RegExp(regexes[i], "g"), (substring, ...args) => {
+                const groups = args.find(arg => {
+                    return typeof(arg) == "object" && arg.replaceSection !== undefined;
+                });
+
+                if (groups === undefined)
+                    return substring;
+
+                const funcNum = groups.funcNum;
+                const replaceSection = groups.replaceSection;
+                const name = wasm_func_map.get(Number(funcNum));
+
+                if (name === undefined)
+                    return substring;
+
+                return substring.replace(replaceSection, `${name} (${replaceSection})`);
+            });
+
+            if (newRaw !== origMessage)
+                return newRaw;
+        }
+
+        return origMessage;
+    } catch (error) {
+        console.debug(`failed to symbolicate: ${error}`);
+        return message;
+    }
+}
+
+export function mono_wasm_stringify_as_error_with_stack(err: Error | string): string {
+    let errObj: any = err;
+    if (!(err instanceof Error))
+        errObj = new Error(err);
+
+    // Error
+    return mono_wasm_symbolicate_string(errObj.stack);
+}
+
 export function mono_wasm_trace_logger(log_domain_ptr: CharPtr, log_level_ptr: CharPtr, message_ptr: CharPtr, fatal: number, user_data: VoidPtr): void {
-    const message = Module.UTF8ToString(message_ptr);
+    const origMessage = Module.UTF8ToString(message_ptr);
     const isFatal = !!fatal;
-    const domain = Module.UTF8ToString(log_domain_ptr); // is this always Mono?
+    const domain = Module.UTF8ToString(log_domain_ptr);
     const dataPtr = user_data;
     const log_level = Module.UTF8ToString(log_level_ptr);
 
+    const message = `[MONO] ${origMessage}`;
+
     if (INTERNAL["logging"] && typeof INTERNAL.logging["trace"] === "function") {
         INTERNAL.logging.trace(domain, log_level, message, isFatal, dataPtr);
         return;
     }
 
-    if (isFatal)
-        console.trace(message);
-
     switch (log_level) {
         case "critical":
         case "error":
-            console.error(message);
+            console.error(mono_wasm_stringify_as_error_with_stack(message));
             break;
         case "warning":
             console.warn(message);
@@ -389,4 +477,4 @@ type CFOResponse = {
     className?: string,
     description?: string,
     objectId?: string
-}
\ No newline at end of file
+}
index 6d210e5..d6a86af 100644 (file)
@@ -43,6 +43,7 @@ declare interface EmscriptenModule {
     UTF8ArrayToString(u8Array: Uint8Array, idx?: number, maxBytesToRead?: number): string;
     FS_createPath(parent: string, path: string, canRead?: boolean, canWrite?: boolean): string;
     FS_createDataFile(parent: string, name: string, data: TypedArray, canRead: boolean, canWrite: boolean, canOwn?: boolean): string;
+    FS_readFile(filename: string, opts: any): any;
     removeRunDependency(id: string): void;
     addRunDependency(id: string): void;
     ready: Promise<unknown>;
index 5dc4d84..8ec6dc1 100644 (file)
@@ -25,6 +25,8 @@ import {
     mono_wasm_trace_logger,
     mono_wasm_add_dbg_command_received,
     mono_wasm_change_debugger_log_level,
+    mono_wasm_symbolicate_string,
+    mono_wasm_stringify_as_error_with_stack,
 } from "./debug";
 import { ENVIRONMENT_IS_WEB, ExitStatusError, runtimeHelpers, setImportsAndExports } from "./imports";
 import { DotnetModuleConfigImports, DotnetModule } from "./types";
@@ -324,6 +326,10 @@ const INTERNAL: any = {
     // with mono_wasm_debugger_log and mono_wasm_trace_logger
     logging: undefined,
 
+    //
+    mono_wasm_symbolicate_string,
+    mono_wasm_stringify_as_error_with_stack,
+
     // used in debugger DevToolsHelper.cs
     mono_wasm_get_loaded_files,
     mono_wasm_send_dbg_command_with_parms,
index 808b579..a5c16b3 100644 (file)
@@ -22,6 +22,7 @@ export let locateFile: Function;
 export let quit: Function;
 export let ExitStatus: ExitStatusError;
 export let requirePromise: Promise<Function>;
+export let readFile: Function;
 
 export interface ExitStatusError {
     new(status: number): any;
index 3bd1057..ae2bcb2 100644 (file)
@@ -464,6 +464,8 @@ export function wrap_error(is_exception: Int32Ptr | null, ex: any): MonoString {
             else
                 res += "\n" + stack;
         }
+
+        res = INTERNAL.mono_wasm_symbolicate_string(res);
     }
     if (is_exception) {
         Module.setValue(is_exception, 1, "i32");
index f1e0000..4921883 100644 (file)
@@ -1,8 +1,7 @@
-import { ExitStatus, Module, quit } from "./imports";
+import { ExitStatus, INTERNAL, Module, quit } from "./imports";
 import { mono_call_assembly_entry_point } from "./method-calls";
 import { mono_wasm_set_main_args, runtime_is_initialized_reject } from "./startup";
 
-
 export async function mono_run_main_and_exit(main_assembly_name: string, args: string[]): Promise<void> {
     try {
         const result = await mono_run_main(main_assembly_name, args);
@@ -28,10 +27,12 @@ export function mono_on_abort(error: any): void {
 
 function set_exit_code(exit_code: number, reason?: any) {
     if (reason && !(reason instanceof ExitStatus)) {
-        Module.printErr(reason.toString());
-        if (reason.stack) {
-            Module.printErr(reason.stack);
-        }
+        if (reason instanceof Error)
+            Module.printErr(INTERNAL.mono_wasm_stringify_as_error_with_stack(reason));
+        else if (typeof reason == "string")
+            Module.printErr(reason);
+        else
+            Module.printErr(JSON.stringify(reason));
     }
     else {
         reason = new ExitStatus(exit_code);
index 0866d05..dde4798 100644 (file)
@@ -49,6 +49,7 @@ export declare interface EmscriptenModule {
     UTF8ArrayToString(u8Array: Uint8Array, idx?: number, maxBytesToRead?: number): string;
     FS_createPath(parent: string, path: string, canRead?: boolean, canWrite?: boolean): string;
     FS_createDataFile(parent: string, name: string, data: TypedArray, canRead: boolean, canWrite: boolean, canOwn?: boolean): string;
+    FS_readFile(filename: string, opts: any): any;
     removeRunDependency(id: string): void;
     addRunDependency(id: string): void;
 
index 0304dae..c64e487 100644 (file)
@@ -98,6 +98,22 @@ if (is_browser) {
         console[m] = proxyConsoleMethod(`console.${m}`, send, true);
 }
 
+function stringify_as_error_with_stack(err) {
+    if (!err)
+        return "";
+
+    if (App && App.INTERNAL)
+        return App.INTERNAL.mono_wasm_stringify_as_error_with_stack(err);
+
+    if (err.stack)
+        return err.stack;
+
+    if (typeof err == "string")
+        return err;
+
+    return JSON.stringify(err);
+}
+
 if (typeof globalThis.crypto === 'undefined') {
     // **NOTE** this is a simple insecure polyfill for testing purposes only
     // /dev/random doesn't work on js shells, so define our own
@@ -157,16 +173,11 @@ loadDotnet("./dotnet.js").then((createDotnetRuntime) => {
             App.init({ MONO, INTERNAL, BINDING, Module });
         },
         onAbort: (error) => {
-            console.log("ABORT: " + error);
-            const err = new Error();
-            console.log("Stacktrace: \n");
-            console.error(err.stack);
-            set_exit_code(1, error);
+            set_exit_code(1, stringify_as_error_with_stack(new Error()));
         },
     }))
 }).catch(function (err) {
-    console.error(err);
-    set_exit_code(1, "failed to load the dotnet.js file.\n" + err);
+    set_exit_code(1, "failed to load the dotnet.js file.\n" + stringify_as_error_with_stack(err));
 });
 
 const App = {
@@ -248,10 +259,12 @@ globalThis.App = App; // Necessary as System.Runtime.InteropServices.JavaScript.
 
 function set_exit_code(exit_code, reason) {
     if (reason) {
-        console.error(`${JSON.stringify(reason)}`);
-        if (reason.stack) {
-            console.error(reason.stack);
-        }
+        if (reason instanceof Error)
+            console.error(stringify_as_error_with_stack(reason));
+        else if (typeof reason == "string")
+            console.error(reason);
+        else
+            console.error(JSON.stringify(reason));
     }
 
     if (is_browser) {
index 5314c3b..d26d62c 100644 (file)
@@ -70,7 +70,7 @@
       <_EmccLinkFlags Include="-s ALLOW_MEMORY_GROWTH=1" />
       <_EmccLinkFlags Include="-s NO_EXIT_RUNTIME=1" />
       <_EmccLinkFlags Include="-s FORCE_FILESYSTEM=1" />
-      <_EmccLinkFlags Include="-s EXPORTED_RUNTIME_METHODS=&quot;['FS','print','ccall','cwrap','setValue','getValue','UTF8ToString','UTF8ArrayToString','FS_createPath','FS_createDataFile','removeRunDependency','addRunDependency']&quot;" />
+      <_EmccLinkFlags Include="-s EXPORTED_RUNTIME_METHODS=&quot;['FS','print','ccall','cwrap','setValue','getValue','UTF8ToString','UTF8ArrayToString','FS_createPath','FS_createDataFile','removeRunDependency','addRunDependency', 'FS_readFile']&quot;"  />
       <!-- _htons,_ntohs,__get_daylight,__get_timezone,__get_tzname are exported temporarily, until the issue is fixed in emscripten, https://github.com/dotnet/runtime/issues/64724  -->
       <_EmccLinkFlags Include="-s EXPORTED_FUNCTIONS=_free,_malloc,_htons,_ntohs,__get_daylight,__get_timezone,__get_tzname,_memalign" />
       <_EmccLinkFlags Include="--source-map-base http://example.com" />