Support loading assemblies and symbols into debugger via callback (#42255)
authorSafia Abdalla <safia@microsoft.com>
Fri, 18 Sep 2020 19:27:30 +0000 (19:27 +0000)
committerGitHub <noreply@github.com>
Fri, 18 Sep 2020 19:27:30 +0000 (12:27 -0700)
* Add test setup for debugging lazy-loaded assemblies
* Add mono_wasm_add_lazy_load_files callback
* Rely on assembly_load callback to register PDBs
* Address feedback on formatting
* Fix spacing on method invocations
* Address second round of feedback
* Transport assembly data as base64 string
* Revert automated whitespace changes
* [wasm][debugger] Fix some issues with lazy loading assemblies
- One of the problems I found was that we were sending lot of duplicate
assemblies.
    - this is a problem because we are converting them to base64, then
    sending them over the wire, unnecessarily.
    - Also, the proxy wasn't correctly dealing with them, and ended up
    processing them, and then sending out duplicate `scriptParsed`
    events!
- So, we try to solve this at the `mono_wasm_asm_loaded` point:
    - issue: the `MONO.mono_wasm_runtime_is_ready` check wasn't enough
    - What we want to do is to send the events only for assemblies that
      were not in the bundle, list of which we can get in
      `MONO.loaded_files`.
    - but that has the filename
    - But we don't seem to be getting the filename from the MonoImage,
      so we use the assembly name as a workaround.
    - So, as a heuristic (and trying not too add too much new stuff this
    close to rc2!), we check if the assembly name begins with `System.`,
    or `Microsoft.`, and if it does then we look for `$assembly_name.dll`
    in the the loaded filenames.
        - If found, then we can skip these!
- The other point in the Proxy:
    - where we avoid adding duplicate assemblies if we already have one
    keyed by the assembly name.
* Fix spacing in function definition
* [wasm][debugger] Handle errors in downloading from symbol servers
* [wasm][debugger] MonoProxy: Don't skip assembly if no pdb is available
- this caused such assemblies not getting added `DebugStore.assemblies`
- which broke download symbols on demand, because it couldn't find the
corresponding assembly in the list
* [wasm][debugger] Use the correct assembly name for the search
* Fixing and changes things related to Embedded Pdb.
* Update check for loaded assemblies and initialize resolver once
* Changing the order of the checks.
* Fix indentation.
* [wasm][debugger][tests] Improve tests a bit
.. which includes setting breakpoint using a url, before loading the
dynamic assembly. This essentially tests that the bp request gets resolved
automatically when the assembly gets loaded.
- And add the embedded pdb test for `DuplicateAssemblyLoadedEvent*`
tests
* Add back types to DebugStore
* Comply with new code style requirements

Co-authored-by: Ankit Jain <radical@gmail.com>
Co-authored-by: Thays <thaystg@gmail.com>
31 files changed:
src/mono/mono/metadata/debug-mono-ppdb.c
src/mono/mono/metadata/debug-mono-ppdb.h
src/mono/mono/mini/mini-wasm-debugger.c
src/mono/wasm/Makefile
src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs
src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
src/mono/wasm/debugger/DebuggerTestSuite/MonoJsTests.cs
src/mono/wasm/debugger/DebuggerTestSuite/Support.cs
src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs
src/mono/wasm/debugger/tests/debugger-test/debugger-array-test.cs [moved from src/mono/wasm/debugger/tests/debugger-array-test.cs with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/debugger-cfo-test.cs [moved from src/mono/wasm/debugger/tests/debugger-cfo-test.cs with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/debugger-datetime-test.cs [moved from src/mono/wasm/debugger/tests/debugger-datetime-test.cs with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/debugger-driver.html [moved from src/mono/wasm/debugger/tests/debugger-driver.html with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/debugger-evaluate-test.cs [moved from src/mono/wasm/debugger/tests/debugger-evaluate-test.cs with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/debugger-exception-test.cs [moved from src/mono/wasm/debugger/tests/debugger-exception-test.cs with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/debugger-get-properties-test.cs [moved from src/mono/wasm/debugger/tests/debugger-get-properties-test.cs with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/debugger-nullable-test.cs [moved from src/mono/wasm/debugger/tests/debugger-nullable-test.cs with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/debugger-pointers-test.cs [moved from src/mono/wasm/debugger/tests/debugger-pointers-test.cs with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/debugger-test.cs [moved from src/mono/wasm/debugger/tests/debugger-test.cs with 96% similarity]
src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj [moved from src/mono/wasm/debugger/tests/debugger-test.csproj with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/debugger-test2.cs [moved from src/mono/wasm/debugger/tests/debugger-test2.cs with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/debugger-valuetypes-test.cs [moved from src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/dependency.cs [moved from src/mono/wasm/debugger/tests/dependency.cs with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/other.js [moved from src/mono/wasm/debugger/tests/other.js with 100% similarity]
src/mono/wasm/debugger/tests/debugger-test/runtime-debugger.js [moved from src/mono/wasm/debugger/tests/runtime-debugger.js with 100% similarity]
src/mono/wasm/debugger/tests/lazy-debugger-test-embedded/lazy-debugger-test-embedded.cs [new file with mode: 0644]
src/mono/wasm/debugger/tests/lazy-debugger-test-embedded/lazy-debugger-test-embedded.csproj [new file with mode: 0644]
src/mono/wasm/debugger/tests/lazy-debugger-test/lazy-debugger-test.cs [new file with mode: 0644]
src/mono/wasm/debugger/tests/lazy-debugger-test/lazy-debugger-test.csproj [new file with mode: 0644]
src/mono/wasm/runtime/driver.c
src/mono/wasm/runtime/library_mono.js

index 38c619a..b7dc9ce 100644 (file)
@@ -41,6 +41,7 @@ struct _MonoPPDBFile {
        MonoImage *image;
        GHashTable *doc_hash;
        GHashTable *method_hash;
+       gboolean is_embedded;
 };
 
 typedef struct {
@@ -130,7 +131,7 @@ doc_free (gpointer key)
 }
 
 static MonoPPDBFile*
-create_ppdb_file (MonoImage *ppdb_image)
+create_ppdb_file (MonoImage *ppdb_image, gboolean is_embedded_ppdb)
 {
        MonoPPDBFile *ppdb;
 
@@ -138,6 +139,7 @@ create_ppdb_file (MonoImage *ppdb_image)
        ppdb->image = ppdb_image;
        ppdb->doc_hash = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) doc_free);
        ppdb->method_hash = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) g_free);
+       ppdb->is_embedded = is_embedded_ppdb;
        return ppdb;
 }
 
@@ -154,11 +156,12 @@ mono_ppdb_load_file (MonoImage *image, const guint8 *raw_contents, int size)
        guint8 *ppdb_data = NULL;
        guint8 *to_free = NULL;
        int ppdb_size = 0, ppdb_compressed_size = 0;
+       gboolean is_embedded_ppdb = FALSE;
 
        if (image->tables [MONO_TABLE_DOCUMENT].rows) {
                /* Embedded ppdb */
                mono_image_addref (image);
-               return create_ppdb_file (image);
+               return create_ppdb_file (image, TRUE);
        }
 
        if (!get_pe_debug_info (image, pe_guid, &pe_age, &pe_timestamp, &ppdb_data, &ppdb_size, &ppdb_compressed_size)) {
@@ -188,6 +191,7 @@ mono_ppdb_load_file (MonoImage *image, const guint8 *raw_contents, int size)
                raw_contents = data;
                size = ppdb_size;
                to_free = data;
+               is_embedded_ppdb = TRUE;
        }
 #endif
 
@@ -232,7 +236,7 @@ mono_ppdb_load_file (MonoImage *image, const guint8 *raw_contents, int size)
                return NULL;
        }
 
-       return create_ppdb_file (ppdb_image);
+       return create_ppdb_file (ppdb_image, is_embedded_ppdb);
 }
 
 void
@@ -442,7 +446,14 @@ mono_ppdb_lookup_location (MonoDebugMethodInfo *minfo, uint32_t offset)
 MonoImage *
 mono_ppdb_get_image (MonoPPDBFile *ppdb)
 {
-    return  ppdb->image;
+       return ppdb->image;
+}
+
+
+gboolean
+mono_ppdb_is_embedded (MonoPPDBFile *ppdb)
+{
+       return ppdb->is_embedded;
 }
 
 void
index 503a640..f7148be 100644 (file)
@@ -44,4 +44,7 @@ mono_ppdb_get_image (MonoPPDBFile *ppdb);
 char *
 mono_ppdb_get_sourcelink (MonoDebugHandle *handle);
 
+gboolean 
+mono_ppdb_is_embedded (MonoPPDBFile *ppdb);
+
 #endif
index a0eee7d..76eeede 100644 (file)
@@ -19,6 +19,7 @@
 #include <emscripten.h>
 
 #include "mono/metadata/assembly-internals.h"
+#include "mono/metadata/debug-mono-ppdb.h"
 
 static int log_level = 1;
 
@@ -64,11 +65,13 @@ extern void mono_wasm_add_properties_var (const char*, gint32);
 extern void mono_wasm_add_array_item (int);
 extern void mono_wasm_set_is_async_method (guint64);
 extern void mono_wasm_add_typed_value (const char *type, const char *str_value, double value);
+extern void mono_wasm_asm_loaded (const char *asm_name, const char *assembly_data, guint32 assembly_len, const char *pdb_data, guint32 pdb_len);
 
 G_END_DECLS
 
 static void describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAsyncLocalThis, int gpflags);
 static void handle_exception (MonoException *exc, MonoContext *throw_ctx, MonoContext *catch_ctx, StackFrameInfo *catch_frame);
+static void assembly_loaded (MonoProfiler *prof, MonoAssembly *assembly);
 
 //FIXME move all of those fields to the profiler object
 static gboolean debugger_enabled;
@@ -409,6 +412,7 @@ mono_wasm_debugger_init (void)
        mono_profiler_set_jit_done_callback (prof, jit_done);
        //FIXME support multiple appdomains
        mono_profiler_set_domain_loaded_callback (prof, appdomain_load);
+       mono_profiler_set_assembly_loaded_callback (prof, assembly_loaded);
 
        obj_to_objref = g_hash_table_new (NULL, NULL);
        objrefs = g_hash_table_new_full (NULL, NULL, NULL, mono_debugger_free_objref);
@@ -488,6 +492,26 @@ mono_wasm_setup_single_step (int kind)
 }
 
 static void
+assembly_loaded (MonoProfiler *prof, MonoAssembly *assembly)
+{
+       DEBUG_PRINTF (2, "assembly_loaded callback called for %s\n", assembly->aname.name);
+       MonoImage *assembly_image = assembly->image;
+       MonoImage *pdb_image = NULL;
+       if (mono_has_pdb_checksum ((char *) assembly_image->raw_data, assembly_image->raw_data_len)) { //if it's a release assembly we don't need to send to DebuggerProxy
+               MonoDebugHandle *handle = mono_debug_get_handle (assembly_image);
+               if (handle) {
+                       MonoPPDBFile *ppdb = handle->ppdb;
+                       if (!mono_ppdb_is_embedded (ppdb)) { //if it's an embedded pdb we don't need to send pdb extrated to DebuggerProxy. 
+                               pdb_image = mono_ppdb_get_image (ppdb);
+                               mono_wasm_asm_loaded (assembly_image->assembly_name, assembly_image->raw_data, assembly_image->raw_data_len, pdb_image->raw_data, pdb_image->raw_data_len);
+                               return;
+                       }
+               }
+               mono_wasm_asm_loaded (assembly_image->assembly_name, assembly_image->raw_data, assembly_image->raw_data_len, NULL, 0);
+       }
+}
+
+static void
 handle_exception (MonoException *exc, MonoContext *throw_ctx, MonoContext *catch_ctx, StackFrameInfo *catch_frame)
 {
        ERROR_DECL (error);
index 4b8c6ad..bcf54e1 100644 (file)
@@ -144,20 +144,24 @@ run-tests-jsc-%:
 run-tests-%:
        PATH="$(JSVU):$(PATH)" $(DOTNET) build $(TOP)/src/libraries/$*/tests/ /t:Test /p:TargetOS=Browser /p:TargetArchitecture=wasm /p:Configuration=$(CONFIG) $(MSBUILD_ARGS)
 
-build-debugger-test-app: 
-       $(DOTNET) build --configuration debug --nologo /p:TargetArchitecture=wasm /p:TargetOS=Browser /p:Configuration=Debug /p:RuntimeConfiguration=$(CONFIG) $(TOP)/src/mono/wasm/debugger/tests
-       cp $(TOP)/src/mono/wasm/debugger/tests/debugger-driver.html $(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish
-       cp $(TOP)/src/mono/wasm/debugger/tests/other.js $(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish
-       cp $(TOP)/src/mono/wasm/debugger/tests/runtime-debugger.js $(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish
+build-debugger-test-app:
+       $(DOTNET) build --configuration debug --nologo /p:TargetArchitecture=wasm /p:TargetOS=Browser /p:Configuration=Debug /p:RuntimeConfiguration=$(CONFIG) $(TOP)/src/mono/wasm/debugger/tests/debugger-test
+       $(DOTNET) build --configuration debug --nologo /p:TargetArchitecture=wasm /p:TargetOS=Browser /p:Configuration=Debug /p:RuntimeConfiguration=$(CONFIG) $(TOP)/src/mono/wasm/debugger/tests/lazy-debugger-test
+       $(DOTNET) build --configuration debug --nologo /p:TargetArchitecture=wasm /p:TargetOS=Browser /p:Configuration=Debug /p:RuntimeConfiguration=$(CONFIG) $(TOP)/src/mono/wasm/debugger/tests/lazy-debugger-test-embedded
+       cp $(TOP)/src/mono/wasm/debugger/tests/debugger-test/debugger-driver.html $(TOP)/src/mono/wasm/debugger/tests/debugger-test/bin/Debug/publish
+       cp $(TOP)/src/mono/wasm/debugger/tests/debugger-test/other.js $(TOP)/src/mono/wasm/debugger/tests/debugger-test/bin/Debug/publish
+       cp $(TOP)/src/mono/wasm/debugger/tests/debugger-test/runtime-debugger.js $(TOP)/src/mono/wasm/debugger/tests/debugger-test/bin/Debug/publish
+       cp $(TOP)/src/mono/wasm/debugger/tests/lazy-debugger-test/bin/Debug/publish/managed/* $(TOP)/src/mono/wasm/debugger/tests/debugger-test/bin/Debug/publish
+       cp $(TOP)/src/mono/wasm/debugger/tests/lazy-debugger-test-embedded/bin/Debug/publish/managed/* $(TOP)/src/mono/wasm/debugger/tests/debugger-test/bin/Debug/publish
 
 run-debugger-tests: build-debugger-test-app build-dbg-testsuite
        if [ ! -z "$(TEST_FILTER)" ]; then \
-               export TEST_SUITE_PATH=$(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish; \
+               export TEST_SUITE_PATH=$(TOP)/src/mono/wasm/debugger/tests/debugger-test/bin/Debug/publish; \
                export LC_ALL=en_US.UTF-8; \
                $(DOTNET) test  $(TOP)/src/mono/wasm/debugger/DebuggerTestSuite --filter FullyQualifiedName~$(TEST_FILTER); \
                unset TEST_SUITE_PATH LC_ALL; \
        else \
-               export TEST_SUITE_PATH=$(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish; \
+               export TEST_SUITE_PATH=$(TOP)/src/mono/wasm/debugger/tests/debugger-test/bin/Debug/publish; \
                export LC_ALL=en_US.UTF-8; \
                $(DOTNET) test  $(TOP)/src/mono/wasm/debugger/DebuggerTestSuite $(TEST_ARGS); \
                unset TEST_SUITE_PATH LC_ALL; \
index f1240dc..99b0804 100644 (file)
@@ -545,6 +545,9 @@ namespace Microsoft.WebAssembly.Diagnostics
         public int Id => id;
         public string Name => image.Name;
 
+        // "System.Threading", instead of "System.Threading, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
+        public string AssemblyNameUnqualified => image.Assembly.Name.Name;
+
         public SourceFile GetDocById(int document)
         {
             return sources.FirstOrDefault(s => s.SourceId.Document == document);
@@ -723,11 +726,13 @@ namespace Microsoft.WebAssembly.Diagnostics
         private List<AssemblyInfo> assemblies = new List<AssemblyInfo>();
         private readonly HttpClient client;
         private readonly ILogger logger;
+        private readonly IAssemblyResolver resolver;
 
         public DebugStore(ILogger logger, HttpClient client)
         {
             this.client = client;
             this.logger = logger;
+            this.resolver = new DefaultAssemblyResolver();
         }
 
         public DebugStore(ILogger logger) : this(logger, new HttpClient())
@@ -739,6 +744,35 @@ namespace Microsoft.WebAssembly.Diagnostics
             public Task<byte[][]> Data { get; set; }
         }
 
+        public IEnumerable<SourceFile> Add(SessionId sessionId, byte[] assembly_data, byte[] pdb_data)
+        {
+            AssemblyInfo assembly = null;
+            try
+            {
+                assembly = new AssemblyInfo(this.resolver, sessionId.ToString(), assembly_data, pdb_data);
+            }
+            catch (Exception e)
+            {
+                logger.LogDebug($"Failed to load assembly: ({e.Message})");
+                yield break;
+            }
+
+            if (assembly == null)
+                yield break;
+
+            if (GetAssemblyByUnqualifiedName(assembly.AssemblyNameUnqualified) != null)
+            {
+                logger.LogDebug($"Skipping adding {assembly.Name} into the debug store, as it already exists");
+                yield break;
+            }
+
+            assemblies.Add(assembly);
+            foreach (var source in assembly.Sources)
+            {
+                yield return source;
+            }
+        }
+
         public async IAsyncEnumerable<SourceFile> Load(SessionId sessionId, string[] loaded_files, [EnumeratorCancellation] CancellationToken token)
         {
             var asm_files = new List<string>();
@@ -758,8 +792,6 @@ namespace Microsoft.WebAssembly.Diagnostics
                 {
                     string candidate_pdb = Path.ChangeExtension(url, "pdb");
                     string pdb = pdb_files.FirstOrDefault(n => n == candidate_pdb);
-                    if (pdb == null)
-                        continue;
 
                     steps.Add(
                         new DebugItem
@@ -774,14 +806,13 @@ namespace Microsoft.WebAssembly.Diagnostics
                 }
             }
 
-            var resolver = new DefaultAssemblyResolver();
             foreach (DebugItem step in steps)
             {
                 AssemblyInfo assembly = null;
                 try
                 {
                     byte[][] bytes = await step.Data.ConfigureAwait(false);
-                    assembly = new AssemblyInfo(resolver, step.Url, bytes[0], bytes[1]);
+                    assembly = new AssemblyInfo(this.resolver, step.Url, bytes[0], bytes[1]);
                 }
                 catch (Exception e)
                 {
@@ -790,6 +821,12 @@ namespace Microsoft.WebAssembly.Diagnostics
                 if (assembly == null)
                     continue;
 
+                if (GetAssemblyByUnqualifiedName(assembly.AssemblyNameUnqualified) != null)
+                {
+                    logger.LogDebug($"Skipping loading {assembly.Name} into the debug store, as it already exists");
+                    continue;
+                }
+
                 assemblies.Add(assembly);
                 foreach (SourceFile source in assembly.Sources)
                     yield return source;
@@ -802,6 +839,8 @@ namespace Microsoft.WebAssembly.Diagnostics
 
         public AssemblyInfo GetAssemblyByName(string name) => assemblies.FirstOrDefault(a => a.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
 
+        public AssemblyInfo GetAssemblyByUnqualifiedName(string name) => assemblies.FirstOrDefault(a => a.AssemblyNameUnqualified.Equals(name, StringComparison.InvariantCultureIgnoreCase));
+
         /*
         V8 uses zero based indexing for both line and column.
         PPDBs uses one based indexing for both line and column.
index 4ef569d..c261b15 100644 (file)
@@ -718,6 +718,12 @@ namespace Microsoft.WebAssembly.Diagnostics
                     try
                     {
                         using HttpResponseMessage response = await client.GetAsync(downloadURL, token);
+                        if (!response.IsSuccessStatusCode)
+                        {
+                            Log("info", $"Unable to download symbols on demand url:{downloadURL} assembly: {asm.Name}");
+                            continue;
+                        }
+
                         using Stream streamToReadFrom = await response.Content.ReadAsStreamAsync(token);
                         var portablePdbReaderProvider = new PdbReaderProvider();
                         ISymbolReader symbolReader = portablePdbReaderProvider.GetSymbolReader(asm.Image, streamToReadFrom);
@@ -739,7 +745,7 @@ namespace Microsoft.WebAssembly.Diagnostics
                 break;
             }
 
-            Log("info", "Unable to load symbols on demand assembly: {asm.Name}");
+            Log("info", $"Unable to load symbols on demand assembly: {asm.Name}");
             return null;
         }
 
@@ -812,6 +818,8 @@ namespace Microsoft.WebAssembly.Diagnostics
 
             switch (eventName)
             {
+                case "AssemblyLoaded":
+                    return await OnAssemblyLoadedJSEvent(sessionId, eventArgs, token);
                 default:
                 {
                     logger.LogDebug($"Unknown js event name: {eventName} with args {eventArgs}");
@@ -820,6 +828,46 @@ namespace Microsoft.WebAssembly.Diagnostics
             }
         }
 
+        private async Task<bool> OnAssemblyLoadedJSEvent(SessionId sessionId, JObject eventArgs, CancellationToken token)
+        {
+            try
+            {
+                var store = await LoadStore(sessionId, token);
+                var assembly_name = eventArgs?["assembly_name"]?.Value<string>();
+
+                if (store.GetAssemblyByUnqualifiedName(assembly_name) != null)
+                {
+                    Log("debug", $"Got AssemblyLoaded event for {assembly_name}, but skipping it as it has already been loaded.");
+                    return true;
+                }
+
+                var assembly_b64 = eventArgs?["assembly_b64"]?.ToObject<string>();
+                var pdb_b64 = eventArgs?["pdb_b64"]?.ToObject<string>();
+
+                if (string.IsNullOrEmpty(assembly_b64))
+                {
+                    logger.LogDebug("No assembly data provided to load.");
+                    return false;
+                }
+
+                var assembly_data = Convert.FromBase64String(assembly_b64);
+                var pdb_data = string.IsNullOrEmpty(pdb_b64) ? null : Convert.FromBase64String(pdb_b64);
+
+                var context = GetContext(sessionId);
+                foreach (var source in store.Add(sessionId, assembly_data, pdb_data))
+                {
+                    await OnSourceFileAdded(sessionId, source, context, token);
+                }
+
+                return true;
+            }
+            catch (Exception e)
+            {
+                logger.LogDebug($"Failed to load assemblies and PDBs: {e}");
+                return false;
+            }
+        }
+
         private async Task<bool> OnEvaluateOnCallFrame(MessageId msg_id, int scope_id, string expression, CancellationToken token)
         {
             try
@@ -917,6 +965,22 @@ namespace Microsoft.WebAssembly.Diagnostics
             return bp;
         }
 
+        private async Task OnSourceFileAdded(SessionId sessionId, SourceFile source, ExecutionContext context, CancellationToken token)
+        {
+            JObject scriptSource = JObject.FromObject(source.ToScriptSource(context.Id, context.AuxData));
+            Log("debug", $"sending {source.Url} {context.Id} {sessionId.sessionId}");
+
+            SendEvent(sessionId, "Debugger.scriptParsed", scriptSource, token);
+
+            foreach (var req in context.BreakpointRequests.Values)
+            {
+                if (req.TryResolve(source))
+                {
+                    await SetBreakpoint(sessionId, context.store, req, true, token);
+                }
+            }
+        }
+
         private async Task<DebugStore> LoadStore(SessionId sessionId, CancellationToken token)
         {
             ExecutionContext context = GetContext(sessionId);
@@ -937,18 +1001,7 @@ namespace Microsoft.WebAssembly.Diagnostics
                 await
                 foreach (SourceFile source in context.store.Load(sessionId, loaded_files, token).WithCancellation(token))
                 {
-                    var scriptSource = JObject.FromObject(source.ToScriptSource(context.Id, context.AuxData));
-                    Log("verbose", $"\tsending {source.Url} {context.Id} {sessionId.sessionId}");
-
-                    SendEvent(sessionId, "Debugger.scriptParsed", scriptSource, token);
-
-                    foreach (BreakpointRequest req in context.BreakpointRequests.Values)
-                    {
-                        if (req.TryResolve(source))
-                        {
-                            await SetBreakpoint(sessionId, context.store, req, true, token);
-                        }
-                    }
+                    await OnSourceFileAdded(sessionId, source, context, token);
                 }
             }
             catch (Exception e)
index 1cec819..892fe61 100644 (file)
@@ -1,4 +1,5 @@
 using System;
+using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
 using Newtonsoft.Json;
@@ -212,5 +213,102 @@ namespace DebuggerTests
                     Assert.False(tcs.Task == t, "Event should not have been logged");
             });
         }
+
+        [Theory]
+        [InlineData(true, 1)]
+        [InlineData(false, 0)]
+        public async Task DuplicateAssemblyLoadedEventNotLoadedFromBundle(bool load_pdb, int expected_count)
+            => await AssemblyLoadedEventTest(
+                "lazy-debugger-test",
+                Path.Combine(DebuggerTestAppPath, "lazy-debugger-test.dll"),
+                load_pdb ? Path.Combine(DebuggerTestAppPath, "lazy-debugger-test.pdb") : null,
+                "/lazy-debugger-test.cs",
+                expected_count
+            );
+
+        [Theory]
+        [InlineData(true, 1)]
+        [InlineData(false, 1)] // Since it's being loaded from the bundle, it will have the pdb even if we don't provide one
+        public async Task DuplicateAssemblyLoadedEventForAssemblyFromBundle(bool load_pdb, int expected_count)
+            => await AssemblyLoadedEventTest(
+                "debugger-test",
+                Path.Combine(DebuggerTestAppPath, "managed/debugger-test.dll"),
+                load_pdb ? Path.Combine(DebuggerTestAppPath, "managed/debugger-test.pdb") : null,
+                "/debugger-test.cs",
+                expected_count
+            );
+
+        [Fact]
+        public async Task DuplicateAssemblyLoadedEventWithEmbeddedPdbNotLoadedFromBundle()
+            => await AssemblyLoadedEventTest(
+                "lazy-debugger-test-embedded",
+                Path.Combine(DebuggerTestAppPath, "lazy-debugger-test-embedded.dll"),
+                null,
+                "/lazy-debugger-test-embedded.cs",
+                expected_count: 1
+            );
+
+        async Task AssemblyLoadedEventTest(string asm_name, string asm_path, string pdb_path, string source_file, int expected_count)
+        {
+            var insp = new Inspector();
+            var scripts = SubscribeToScripts(insp);
+
+            int event_count = 0;
+            var tcs = new TaskCompletionSource<bool>();
+            insp.On("Debugger.scriptParsed", async (args, c) =>
+            {
+                try
+                {
+                    var url = args["url"]?.Value<string>();
+                    if (url?.EndsWith(source_file) == true)
+                    {
+                        event_count ++;
+                        if (event_count > expected_count)
+                            tcs.SetResult(false);
+                    }
+                }
+                catch (Exception ex)
+                {
+                    tcs.SetException(ex);
+                }
+
+                await Task.CompletedTask;
+            });
+
+            await Ready();
+            await insp.Ready(async (cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                byte[] bytes = File.ReadAllBytes(asm_path);
+                string asm_base64 = Convert.ToBase64String(bytes);
+
+                string pdb_base64 = String.Empty;
+                if (pdb_path != null)
+                {
+                    bytes = File.ReadAllBytes(pdb_path);
+                    pdb_base64 = Convert.ToBase64String(bytes);
+                }
+
+                var expression = $@"MONO.mono_wasm_raise_debug_event({{
+                    eventName: 'AssemblyLoaded',
+                    assembly_name: '{asm_name}',
+                    assembly_b64: '{asm_base64}',
+                    pdb_b64: '{pdb_base64}'
+                }});";
+
+                var res = await ctx.cli.SendCommand($"Runtime.evaluate", JObject.FromObject(new { expression }), ctx.token);
+                Assert.True(res.IsOk, $"Expected to pass for {expression}");
+
+                res = await ctx.cli.SendCommand($"Runtime.evaluate", JObject.FromObject(new { expression }), ctx.token);
+                Assert.True(res.IsOk, $"Expected to pass for {expression}");
+
+                var t = await Task.WhenAny(tcs.Task, Task.Delay(2000));
+                if (t.IsFaulted)
+                    throw t.Exception;
+
+                Assert.True(event_count <= expected_count, $"number of scriptParsed events received. Expected: {expected_count}, Actual: {event_count}");
+            });
+        }
     }
 }
index c4ec6da..4c1ff5d 100644 (file)
@@ -116,7 +116,19 @@ namespace DebuggerTests
     {
         protected Task startTask;
 
-        static string FindTestPath()
+        static string s_debuggerTestAppPath;
+        protected static string DebuggerTestAppPath
+        {
+            get
+            {
+                if (s_debuggerTestAppPath == null)
+                    s_debuggerTestAppPath = FindTestPath();
+
+                return s_debuggerTestAppPath;
+            }
+        }
+
+        static protected string FindTestPath()
         {
             //FIXME how would I locate it otherwise?
             var test_path = Environment.GetEnvironmentVariable("TEST_SUITE_PATH");
@@ -162,7 +174,7 @@ namespace DebuggerTests
 
         public DebuggerTestBase(string driver = "debugger-driver.html")
         {
-            startTask = TestHarnessProxy.Start(FindChromePath(), FindTestPath(), driver);
+            startTask = TestHarnessProxy.Start(FindChromePath(), DebuggerTestAppPath, driver);
         }
 
         public Task Ready() => startTask;
index f5263fb..d33fe76 100644 (file)
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
 using Microsoft.WebAssembly.Diagnostics;
@@ -1878,6 +1879,110 @@ namespace DebuggerTests
                     ?.Where(f => f["functionName"]?.Value<string>() == function_name)
                     ?.FirstOrDefault();
 
+        [Fact]
+        public async Task DebugLazyLoadedAssemblyWithPdb()
+        {
+            var insp = new Inspector();
+            var scripts = SubscribeToScripts(insp);
+            await Ready();
+            await insp.Ready(async (cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                int line = 9;
+                await SetBreakpoint(".*/lazy-debugger-test.cs$", line, 0, use_regex: true);
+                await LoadAssemblyDynamically(
+                        Path.Combine(DebuggerTestAppPath, "lazy-debugger-test.dll"),
+                        Path.Combine(DebuggerTestAppPath, "lazy-debugger-test.pdb"));
+
+                var source_location = "dotnet://lazy-debugger-test.dll/lazy-debugger-test.cs";
+                Assert.Contains(source_location, scripts.Values);
+
+                var pause_location = await EvaluateAndCheck(
+                   "window.setTimeout(function () { invoke_static_method('[lazy-debugger-test] LazyMath:IntAdd', 5, 10); }, 1);",
+                   source_location, line, 8,
+                   "IntAdd");
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                CheckNumber(locals, "a", 5);
+                CheckNumber(locals, "b", 10);
+            });
+        }
+
+        [Fact]
+        public async Task DebugLazyLoadedAssemblyWithEmbeddedPdb()
+        {
+            var insp = new Inspector();
+            var scripts = SubscribeToScripts(insp);
+            await Ready();
+
+            await insp.Ready(async (cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                int line = 9;
+                await SetBreakpoint(".*/lazy-debugger-test-embedded.cs$", line, 0, use_regex: true);
+                await LoadAssemblyDynamically(
+                        Path.Combine(DebuggerTestAppPath, "lazy-debugger-test-embedded.dll"),
+                        null);
+
+                var source_location = "dotnet://lazy-debugger-test-embedded.dll/lazy-debugger-test-embedded.cs";
+                Assert.Contains(source_location, scripts.Values);
+
+                var pause_location = await EvaluateAndCheck(
+                   "window.setTimeout(function () { invoke_static_method('[lazy-debugger-test-embedded] LazyMath:IntAdd', 5, 10); }, 1);",
+                   source_location, line, 8,
+                   "IntAdd");
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                CheckNumber(locals, "a", 5);
+                CheckNumber(locals, "b", 10);
+            });
+        }
+
+        [Fact]
+        public async Task CannotDebugLazyLoadedAssemblyWithoutPdb()
+        {
+            var insp = new Inspector();
+            var scripts = SubscribeToScripts(insp);
+            await Ready();
+            await insp.Ready(async (cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                int line = 9;
+                await SetBreakpoint(".*/lazy-debugger-test.cs$", line, 0, use_regex: true);
+                await LoadAssemblyDynamically(
+                        Path.Combine(DebuggerTestAppPath, "lazy-debugger-test.dll"),
+                        null);
+
+                // wait to bit to catch if the event might be raised a bit late
+                await Task.Delay(1000);
+
+                var source_location = "dotnet://lazy-debugger-test.dll/lazy-debugger-test.cs";
+                Assert.DoesNotContain(source_location, scripts.Values);
+            });
+        }
+
+        async Task LoadAssemblyDynamically(string asm_file, string pdb_file)
+        {
+            // Simulate loading an assembly into the framework
+            byte[] bytes = File.ReadAllBytes(asm_file);
+            string asm_base64 = Convert.ToBase64String(bytes);
+
+            string pdb_base64 = null;
+            if (pdb_file != null) {
+                bytes = File.ReadAllBytes(pdb_file);
+                pdb_base64 = Convert.ToBase64String(bytes);
+            }
+
+            var load_assemblies = JObject.FromObject(new
+            {
+                expression = $"{{ let asm_b64 = '{asm_base64}'; let pdb_b64 = '{pdb_base64}'; invoke_static_method('[debugger-test] LoadDebuggerTest:LoadLazyAssembly', asm_b64, pdb_b64); }}"
+            });
+
+            Result load_assemblies_res = await ctx.cli.SendCommand("Runtime.evaluate", load_assemblies, ctx.token);
+            Assert.True(load_assemblies_res.IsOk);
+        }
+
         //TODO add tests covering basic stepping behavior as step in/out/over
     }
 }
@@ -518,3 +518,16 @@ public struct EmptyStruct
         StaticMethodWithLocalEmptyStructAsync().Wait();
     }
 }
+
+public class LoadDebuggerTest {
+    public static void LoadLazyAssembly(string asm_base64, string pdb_base64)
+    {
+        byte[] asm_bytes = Convert.FromBase64String(asm_base64);
+        byte[] pdb_bytes = null;
+        if (pdb_base64 != null)
+            pdb_bytes = Convert.FromBase64String(pdb_base64);
+
+        var loadedAssembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromStream(new System.IO.MemoryStream(asm_bytes), new System.IO.MemoryStream(pdb_bytes));
+        Console.WriteLine($"Loaded - {loadedAssembly}");
+    }
+}
diff --git a/src/mono/wasm/debugger/tests/lazy-debugger-test-embedded/lazy-debugger-test-embedded.cs b/src/mono/wasm/debugger/tests/lazy-debugger-test-embedded/lazy-debugger-test-embedded.cs
new file mode 100644 (file)
index 0000000..203f535
--- /dev/null
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+public partial class LazyMath
+{
+    public static int IntAdd(int a, int b)
+    {
+        int c = a + b;
+        return c;
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/tests/lazy-debugger-test-embedded/lazy-debugger-test-embedded.csproj b/src/mono/wasm/debugger/tests/lazy-debugger-test-embedded/lazy-debugger-test-embedded.csproj
new file mode 100644 (file)
index 0000000..8269501
--- /dev/null
@@ -0,0 +1,50 @@
+<Project Sdk="Microsoft.NET.Sdk" DefaultTargets="BuildApp">
+  <PropertyGroup>
+    <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
+    <TargetArchitecture>wasm</TargetArchitecture>
+    <TargetOS>Browser</TargetOS>
+    <MicrosoftNetCoreAppRuntimePackDir>$(ArtifactsBinDir)microsoft.netcore.app.runtime.browser-wasm\Release\runtimes\browser-wasm\</MicrosoftNetCoreAppRuntimePackDir>
+    <BuildDir>$(MSBuildThisFileDirectory)obj\$(Configuration)\wasm</BuildDir>
+    <AppDir>$(MSBuildThisFileDirectory)bin\$(Configuration)\publish</AppDir>
+    <RuntimeBuildConfig Condition="'$(RuntimeBuildConfig)' == ''">$(Configuration)</RuntimeBuildConfig>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <OutputType>Library</OutputType>
+    <NoWarn>219</NoWarn>
+    <RunAnalyzers>false</RunAnalyzers>
+    <DebugType>embedded</DebugType>
+  </PropertyGroup>
+
+  <Target Name="RebuildWasmAppBuilder">
+    <ItemGroup>
+      <WasmAppBuildProject Include="$(RepoTasksDir)mobile.tasks\WasmAppBuilder\WasmAppBuilder.csproj" />
+    </ItemGroup>
+
+    <MSBuild Projects="@(WasmAppBuildProject)"
+             Properties="Configuration=$(Configuration);MSBuildRestoreSessionId=$([System.Guid]::NewGuid())"
+             Targets="Restore"/>
+
+    <MSBuild Projects="@(WasmAppBuildProject)"
+             Properties="Configuration=$(Configuration)"
+             Targets="Build;Publish"/>
+  </Target>
+
+  <UsingTask TaskName="WasmAppBuilder"
+             AssemblyFile="$(ArtifactsBinDir)WasmAppBuilder\$(Configuration)\$(NetCoreAppCurrent)\publish\WasmAppBuilder.dll"/>
+
+  <Target Name="BuildApp" DependsOnTargets="RebuildWasmAppBuilder;Build">
+    <ItemGroup>
+      <AssemblySearchPaths Include="$(MicrosoftNetCoreAppRuntimePackDir)native"/>
+      <AssemblySearchPaths Include="$(MicrosoftNetCoreAppRuntimePackDir)lib\$(NetCoreAppCurrent)"/>
+    </ItemGroup>
+    <WasmAppBuilder
+      AppDir="$(AppDir)"
+      MicrosoftNetCoreAppRuntimePackDir="$(MicrosoftNetCoreAppRuntimePackDir)"
+      MainAssembly="$(ArtifactsBinDir)lazy-debugger-test-embedded/wasm/Debug/lazy-debugger-test-embedded.dll"
+      MainJS="$(MonoProjectRoot)wasm\runtime-test.js"
+      DebugLevel="1"
+      AssemblySearchPaths="@(AssemblySearchPaths)"
+      ExtraAssemblies="$(ArtifactsBinDir)\System.Private.Runtime.InteropServices.JavaScript\$(NetCoreAppCurrent)-Browser-$(RuntimeConfiguration)\System.Private.Runtime.InteropServices.JavaScript.dll"/>
+    <Exec Command="chmod a+x $(AppDir)/run-v8.sh" />
+  </Target>
+
+</Project>
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/tests/lazy-debugger-test/lazy-debugger-test.cs b/src/mono/wasm/debugger/tests/lazy-debugger-test/lazy-debugger-test.cs
new file mode 100644 (file)
index 0000000..203f535
--- /dev/null
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+public partial class LazyMath
+{
+    public static int IntAdd(int a, int b)
+    {
+        int c = a + b;
+        return c;
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/tests/lazy-debugger-test/lazy-debugger-test.csproj b/src/mono/wasm/debugger/tests/lazy-debugger-test/lazy-debugger-test.csproj
new file mode 100644 (file)
index 0000000..74b2cdf
--- /dev/null
@@ -0,0 +1,50 @@
+<Project Sdk="Microsoft.NET.Sdk" DefaultTargets="BuildApp">
+  <PropertyGroup>
+    <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
+    <TargetArchitecture>wasm</TargetArchitecture>
+    <TargetOS>Browser</TargetOS>
+    <MicrosoftNetCoreAppRuntimePackDir>$(ArtifactsBinDir)microsoft.netcore.app.runtime.browser-wasm\Release\runtimes\browser-wasm\</MicrosoftNetCoreAppRuntimePackDir>
+    <BuildDir>$(MSBuildThisFileDirectory)obj\$(Configuration)\wasm</BuildDir>
+    <AppDir>$(MSBuildThisFileDirectory)bin\$(Configuration)\publish</AppDir>
+    <RuntimeBuildConfig Condition="'$(RuntimeBuildConfig)' == ''">$(Configuration)</RuntimeBuildConfig>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <OutputType>Library</OutputType>
+    <NoWarn>219</NoWarn>
+    <RunAnalyzers>false</RunAnalyzers>
+    <DebugType>portable</DebugType>
+  </PropertyGroup>
+
+  <Target Name="RebuildWasmAppBuilder">
+    <ItemGroup>
+      <WasmAppBuildProject Include="$(RepoTasksDir)mobile.tasks\WasmAppBuilder\WasmAppBuilder.csproj" />
+    </ItemGroup>
+
+    <MSBuild Projects="@(WasmAppBuildProject)"
+             Properties="Configuration=$(Configuration);MSBuildRestoreSessionId=$([System.Guid]::NewGuid())"
+             Targets="Restore"/>
+
+    <MSBuild Projects="@(WasmAppBuildProject)"
+             Properties="Configuration=$(Configuration)"
+             Targets="Build;Publish"/>
+  </Target>
+
+  <UsingTask TaskName="WasmAppBuilder"
+             AssemblyFile="$(ArtifactsBinDir)WasmAppBuilder\$(Configuration)\$(NetCoreAppCurrent)\publish\WasmAppBuilder.dll"/>
+
+  <Target Name="BuildApp" DependsOnTargets="RebuildWasmAppBuilder;Build">
+    <ItemGroup>
+      <AssemblySearchPaths Include="$(MicrosoftNetCoreAppRuntimePackDir)native"/>
+      <AssemblySearchPaths Include="$(MicrosoftNetCoreAppRuntimePackDir)lib\$(NetCoreAppCurrent)"/>
+    </ItemGroup>
+    <WasmAppBuilder
+      AppDir="$(AppDir)"
+      MicrosoftNetCoreAppRuntimePackDir="$(MicrosoftNetCoreAppRuntimePackDir)"
+      MainAssembly="$(ArtifactsBinDir)lazy-debugger-test/wasm/Debug/lazy-debugger-test.dll"
+      MainJS="$(MonoProjectRoot)wasm\runtime-test.js"
+      DebugLevel="1"
+      AssemblySearchPaths="@(AssemblySearchPaths)"
+      ExtraAssemblies="$(ArtifactsBinDir)\System.Private.Runtime.InteropServices.JavaScript\$(NetCoreAppCurrent)-Browser-$(RuntimeConfiguration)\System.Private.Runtime.InteropServices.JavaScript.dll"/>
+    <Exec Command="chmod a+x $(AppDir)/run-v8.sh" />
+  </Target>
+
+</Project>
\ No newline at end of file
index 8d396b1..f7a9a44 100644 (file)
@@ -39,6 +39,7 @@ void mono_wasm_enable_debugging (int);
 
 int mono_wasm_register_root (char *start, size_t size, const char *name);
 void mono_wasm_deregister_root (char *addr);
+int mono_wasm_assembly_already_added (const char *assembly_name);
 
 void mono_ee_interp_init (const char *opts);
 void mono_marshal_ilgen_init (void);
@@ -205,6 +206,22 @@ mono_wasm_add_assembly (const char *name, const unsigned char *data, unsigned in
        return mono_has_pdb_checksum (data, size);
 }
 
+EMSCRIPTEN_KEEPALIVE int
+mono_wasm_assembly_already_added (const char *assembly_name)
+{
+       if (assembly_count == 0)
+               return 0;
+
+       WasmAssembly *entry = assemblies;
+       while (entry != NULL) {
+               if (strcmp (entry->assembly.name, assembly_name) == 0)
+                       return 1;
+               entry = entry->next;
+       }
+
+       return 0;
+}
+
 typedef struct WasmSatelliteAssembly_ WasmSatelliteAssembly;
 
 struct WasmSatelliteAssembly_ {
index b908d6b..4ae04e1 100644 (file)
@@ -87,6 +87,113 @@ var MonoSupportLib = {
                        module ["mono_wasm_release_roots"] = MONO.mono_wasm_release_roots;
                },
 
+               _base64Converter: {
+                       // Code from JSIL:
+                       // https://github.com/sq/JSIL/blob/1d57d5427c87ab92ffa3ca4b82429cd7509796ba/JSIL.Libraries/Includes/Bootstrap/Core/Classes/System.Convert.js#L149
+                       // Thanks to Katelyn Gadd @kg
+
+                       _base64Table: [
+                               'A', 'B', 'C', 'D',
+                               'E', 'F', 'G', 'H',
+                               'I', 'J', 'K', 'L',
+                               'M', 'N', 'O', 'P',
+                               'Q', 'R', 'S', 'T',
+                               'U', 'V', 'W', 'X',
+                               'Y', 'Z',
+                               'a', 'b', 'c', 'd',
+                               'e', 'f', 'g', 'h',
+                               'i', 'j', 'k', 'l',
+                               'm', 'n', 'o', 'p',
+                               'q', 'r', 's', 't',
+                               'u', 'v', 'w', 'x',
+                               'y', 'z',
+                               '0', '1', '2', '3',
+                               '4', '5', '6', '7',
+                               '8', '9',
+                               '+', '/'
+                       ],
+
+                       _makeByteReader: function (bytes, index, count) {
+                               var position = (typeof (index) === "number") ? index : 0;
+                               var endpoint;
+
+                               if (typeof (count) === "number")
+                                       endpoint = (position + count);
+                               else
+                                       endpoint = (bytes.length - position);
+
+                               var result = {
+                                       read: function () {
+                                               if (position >= endpoint)
+                                                       return false;
+
+                                               var nextByte = bytes[position];
+                                               position += 1;
+                                               return nextByte;
+                                       }
+                               };
+
+                               Object.defineProperty(result, "eof", {
+                                       get: function () {
+                                               return (position >= endpoint);
+                                       },
+                                       configurable: true,
+                                       enumerable: true
+                               });
+
+                               return result;
+                       },
+
+                       toBase64StringImpl: function (inArray, offset, length) {
+                               var reader = this._makeByteReader(inArray, offset, length);
+                               var result = "";
+                               var ch1 = 0, ch2 = 0, ch3 = 0, bits = 0, equalsCount = 0, sum = 0;
+                               var mask1 = (1 << 24) - 1, mask2 = (1 << 18) - 1, mask3 = (1 << 12) - 1, mask4 = (1 << 6) - 1;
+                               var shift1 = 18, shift2 = 12, shift3 = 6, shift4 = 0;
+
+                               while (true) {
+                                       ch1 = reader.read();
+                                       ch2 = reader.read();
+                                       ch3 = reader.read();
+
+                                       if (ch1 === false)
+                                               break;
+                                       if (ch2 === false) {
+                                               ch2 = 0;
+                                               equalsCount += 1;
+                                       }
+                                       if (ch3 === false) {
+                                               ch3 = 0;
+                                               equalsCount += 1;
+                                       }
+
+                                       // Seems backwards, but is right!
+                                       sum = (ch1 << 16) | (ch2 << 8) | (ch3 << 0);
+
+                                       bits = (sum & mask1) >> shift1;
+                                       result += this._base64Table[bits];
+                                       bits = (sum & mask2) >> shift2;
+                                       result += this._base64Table[bits];
+
+                                       if (equalsCount < 2) {
+                                               bits = (sum & mask3) >> shift3;
+                                               result += this._base64Table[bits];
+                                       }
+
+                                       if (equalsCount === 2) {
+                                               result += "==";
+                                       } else if (equalsCount === 1) {
+                                               result += "=";
+                                       } else {
+                                               bits = (sum & mask4) >> shift4;
+                                               result += this._base64Table[bits];
+                                       }
+                               }
+
+                               return result;
+                       },
+               },
+
                _mono_wasm_root_buffer_prototype: {
                        _check_in_range: function (index) {
                                if ((index >= this.__count) || (index < 0))
@@ -2252,6 +2359,36 @@ var MonoSupportLib = {
                };
                debugger;
        },
+
+       mono_wasm_asm_loaded: function (assembly_name, assembly_ptr, assembly_len, pdb_ptr, pdb_len) {
+               // Only trigger this codepath for assemblies loaded after app is ready
+               if (MONO.mono_wasm_runtime_is_ready !== true)
+                       return;
+
+               if (!this.mono_wasm_assembly_already_added)
+                       this.mono_wasm_assembly_already_added = Module.cwrap ("mono_wasm_assembly_already_added", 'number', ['string']);
+
+               // And for assemblies that have not already been loaded
+               const assembly_name_str = assembly_name !== 0 ? Module.UTF8ToString(assembly_name).concat('.dll') : '';
+               if (this.mono_wasm_assembly_already_added(assembly_name_str))
+                       return;
+
+               const assembly_data = new Uint8Array(Module.HEAPU8.buffer, assembly_ptr, assembly_len);
+               const assembly_b64 = MONO._base64Converter.toBase64StringImpl(assembly_data);
+
+               let pdb_b64;
+               if (pdb_ptr) {
+                       const pdb_data = new Uint8Array(Module.HEAPU8.buffer, pdb_ptr, pdb_len);
+                       pdb_b64 = MONO._base64Converter.toBase64StringImpl(pdb_data);
+               }
+
+               MONO.mono_wasm_raise_debug_event({
+                       eventName: 'AssemblyLoaded',
+                       assembly_name: assembly_name_str,
+                       assembly_b64,
+                       pdb_b64
+               });
+       },
 };
 
 autoAddDeps(MonoSupportLib, '$MONO')