diff --git a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props index f2379fc9ff34d..6fd91975b78a8 100644 --- a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props +++ b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props @@ -220,6 +220,7 @@ + diff --git a/src/mono/browser/browser.proj b/src/mono/browser/browser.proj index 759fa68ae5474..669270cf332d0 100644 --- a/src/mono/browser/browser.proj +++ b/src/mono/browser/browser.proj @@ -385,8 +385,8 @@ $(ArtifactsObjDir)wasm/pinvoke-table.h $(ArtifactsObjDir)wasm/wasm_m2n_invoke.g.h - -g -Os -DDEBUG=1 -DENABLE_AOT_PROFILER=1 -DENABLE_BROWSER_PROFILER=1 - -Oz -DENABLE_BROWSER_PROFILER=1 + -g -Os -DDEBUG=1 -DENABLE_AOT_PROFILER=1 -DENABLE_BROWSER_PROFILER=1 -DENABLE_LOG_PROFILER=1 + -Oz $(CMakeConfigurationEmccFlags) -s ASSERTIONS=1 -O2 diff --git a/src/mono/browser/build/BrowserWasmApp.targets b/src/mono/browser/build/BrowserWasmApp.targets index 3b5683433fe86..79ab3cfd83313 100644 --- a/src/mono/browser/build/BrowserWasmApp.targets +++ b/src/mono/browser/build/BrowserWasmApp.targets @@ -314,6 +314,7 @@ <_EmccCFlags Include="-DLINK_ICALLS=1" Condition="'$(WasmLinkIcalls)' == 'true'" /> <_EmccCFlags Include="-DENABLE_AOT_PROFILER=1" Condition="$(WasmProfilers.Contains('aot'))" /> <_EmccCFlags Include="-DENABLE_BROWSER_PROFILER=1" Condition="$(WasmProfilers.Contains('browser'))" /> + <_EmccCFlags Include="-DENABLE_LOG_PROFILER=1" Condition="$(WasmProfilers.Contains('log'))" /> <_EmccCFlags Include="-DENABLE_JS_INTEROP_BY_VALUE=1" Condition="'$(WasmEnableJsInteropByValue)' == 'true'" /> <_EmccCFlags Include="-DGEN_PINVOKE=1" /> @@ -368,6 +369,7 @@ + diff --git a/src/mono/browser/runtime/cwraps.ts b/src/mono/browser/runtime/cwraps.ts index 2819e0c44b7f1..c65651dd95de2 100644 --- a/src/mono/browser/runtime/cwraps.ts +++ b/src/mono/browser/runtime/cwraps.ts @@ -65,9 +65,10 @@ const fn_signatures: SigLine[] = [ [false, "mono_wasm_exit", "void", ["number"]], [true, "mono_wasm_getenv", "number", ["string"]], [true, "mono_wasm_set_main_args", "void", ["number", "number"]], - // These two need to be lazy because they may be missing + // These three need to be lazy because they may be missing [() => !runtimeHelpers.emscriptenBuildOptions.enableAotProfiler, "mono_wasm_profiler_init_aot", "void", ["string"]], [() => !runtimeHelpers.emscriptenBuildOptions.enableBrowserProfiler, "mono_wasm_profiler_init_browser", "void", ["string"]], + [() => !runtimeHelpers.emscriptenBuildOptions.enableLogProfiler, "mono_wasm_profiler_init_log", "void", ["string"]], [true, "mono_wasm_profiler_init_browser", "void", ["number"]], [false, "mono_wasm_exec_regression", "number", ["number", "string"]], [false, "mono_wasm_invoke_jsexport", "void", ["number", "number"]], @@ -165,6 +166,7 @@ export interface t_ThreadingCwraps { export interface t_ProfilerCwraps { mono_wasm_profiler_init_aot(desc: string): void; mono_wasm_profiler_init_browser(desc: string): void; + mono_wasm_profiler_init_log(desc: string): void; } export interface t_Cwraps { diff --git a/src/mono/browser/runtime/driver.c b/src/mono/browser/runtime/driver.c index 1b3663e4f3f3c..2a1d5b1dbbf16 100644 --- a/src/mono/browser/runtime/driver.c +++ b/src/mono/browser/runtime/driver.c @@ -430,6 +430,18 @@ mono_wasm_profiler_init_browser (const char *desc) #endif +#ifdef ENABLE_LOG_PROFILER + +void mono_profiler_init_log (const char *desc); + +EMSCRIPTEN_KEEPALIVE void +mono_wasm_profiler_init_log (const char *desc) +{ + mono_profiler_init_log (desc); +} + +#endif + EMSCRIPTEN_KEEPALIVE void mono_wasm_init_finalizer_thread (void) { diff --git a/src/mono/browser/runtime/es6/dotnet.es6.lib.js b/src/mono/browser/runtime/es6/dotnet.es6.lib.js index 69b6c31376f9e..687025478570e 100644 --- a/src/mono/browser/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/browser/runtime/es6/dotnet.es6.lib.js @@ -11,6 +11,7 @@ const WASM_ENABLE_SIMD = process.env.WASM_ENABLE_SIMD === "1"; const WASM_ENABLE_EH = process.env.WASM_ENABLE_EH === "1"; const ENABLE_BROWSER_PROFILER = process.env.ENABLE_BROWSER_PROFILER === "1"; const ENABLE_AOT_PROFILER = process.env.ENABLE_AOT_PROFILER === "1"; +const ENABLE_LOG_PROFILER = process.env.ENABLE_LOG_PROFILER === "1"; const RUN_AOT_COMPILATION = process.env.RUN_AOT_COMPILATION === "1"; var methodIndexByName = undefined; var gitHash = undefined; @@ -88,6 +89,7 @@ function injectDependencies() { `wasmEnableEH: ${WASM_ENABLE_EH ? "true" : "false"},` + `enableAotProfiler: ${ENABLE_AOT_PROFILER ? "true" : "false"}, ` + `enableBrowserProfiler: ${ENABLE_BROWSER_PROFILER ? "true" : "false"}, ` + + `enableLogProfiler: ${ENABLE_LOG_PROFILER ? "true" : "false"}, ` + `runAOTCompilation: ${RUN_AOT_COMPILATION ? "true" : "false"}, ` + `wasmEnableThreads: ${USE_PTHREADS ? "true" : "false"}, ` + `gitHash: "${gitHash}", ` + diff --git a/src/mono/browser/runtime/profiler.ts b/src/mono/browser/runtime/profiler.ts index a223a7fdbf4a7..0829b3ec71a66 100644 --- a/src/mono/browser/runtime/profiler.ts +++ b/src/mono/browser/runtime/profiler.ts @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import { ENVIRONMENT_IS_WEB, mono_assert, runtimeHelpers } from "./globals"; -import { MonoMethod, AOTProfilerOptions, BrowserProfilerOptions } from "./types/internal"; +import { MonoMethod, AOTProfilerOptions, BrowserProfilerOptions, LogProfilerOptions } from "./types/internal"; import { profiler_c_functions as cwraps } from "./cwraps"; import { utf8ToString } from "./strings"; @@ -34,6 +34,12 @@ export function mono_wasm_init_browser_profiler (options: BrowserProfilerOptions cwraps.mono_wasm_profiler_init_browser(arg); } +export function mono_wasm_init_log_profiler (options: LogProfilerOptions): void { + mono_assert(runtimeHelpers.emscriptenBuildOptions.enableLogProfiler, "Log profiler is not enabled, please use log; in your project file."); + mono_assert(options.takeHeapshot, "Log profiler is not enabled, the takeHeapshot method must be defined in LogProfilerOptions.takeHeapshot"); + cwraps.mono_wasm_profiler_init_log( (options.configuration || "log:alloc,output=output.mlpd") + `,take-heapshot-method=${options.takeHeapshot}`); +} + export const enum MeasuredBlock { emscriptenStartup = "mono.emscriptenStartup", instantiateWasm = "mono.instantiateWasm", diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts index 834ceb81f2f7b..6bc0b69c11882 100644 --- a/src/mono/browser/runtime/startup.ts +++ b/src/mono/browser/runtime/startup.ts @@ -9,7 +9,7 @@ import { exportedRuntimeAPI, INTERNAL, loaderHelpers, Module, runtimeHelpers, cr import cwraps, { init_c_exports, threads_c_functions as tcwraps } from "./cwraps"; import { mono_wasm_raise_debug_event, mono_wasm_runtime_ready } from "./debug"; import { toBase64StringImpl } from "./base64"; -import { mono_wasm_init_aot_profiler, mono_wasm_init_browser_profiler } from "./profiler"; +import { mono_wasm_init_aot_profiler, mono_wasm_init_browser_profiler, mono_wasm_init_log_profiler } from "./profiler"; import { initialize_marshalers_to_cs } from "./marshal-to-cs"; import { initialize_marshalers_to_js } from "./marshal-to-js"; import { init_polyfills_async } from "./polyfills"; @@ -544,6 +544,9 @@ export async function start_runtime () { if (runtimeHelpers.config.browserProfilerOptions) mono_wasm_init_browser_profiler(runtimeHelpers.config.browserProfilerOptions); + if (runtimeHelpers.config.logProfilerOptions) + mono_wasm_init_log_profiler(runtimeHelpers.config.logProfilerOptions); + if (WasmEnableThreads) { // this is not mono-attached thread, so we can start it earlier await mono_wasm_init_diagnostics(); diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts index 0627543d1fb12..2b2792056a8d1 100644 --- a/src/mono/browser/runtime/types/internal.ts +++ b/src/mono/browser/runtime/types/internal.ts @@ -77,6 +77,7 @@ export type MonoConfigInternal = MonoConfig & { assets?: AssetEntryInternal[], runtimeOptions?: string[], // array of runtime options as strings aotProfilerOptions?: AOTProfilerOptions, // dictionary-style Object. If omitted, aot profiler will not be initialized. + logProfilerOptions?: LogProfilerOptions, // dictionary-style Object. If omitted, log profiler will not be initialized. browserProfilerOptions?: BrowserProfilerOptions, // dictionary-style Object. If omitted, browser profiler will not be initialized. waitForDebugger?: number, appendElementOnExit?: boolean @@ -274,6 +275,11 @@ export type AOTProfilerOptions = { export type BrowserProfilerOptions = { } +export type LogProfilerOptions = { + takeHeapshot?: string, + configuration?: string // log profiler options string" +} + // how we extended emscripten Module export type DotnetModule = EmscriptenModule & DotnetModuleConfig; export type DotnetModuleInternal = EmscriptenModule & DotnetModuleConfig & EmscriptenModuleInternal; @@ -290,6 +296,7 @@ export type EmscriptenBuildOptions = { wasmEnableEH: boolean, enableAotProfiler: boolean, enableBrowserProfiler: boolean, + enableLogProfiler: boolean, runAOTCompilation: boolean, wasmEnableThreads: boolean, gitHash: string, diff --git a/src/mono/mono.proj b/src/mono/mono.proj index 5be3d221fd5b7..8fe15830cabb5 100644 --- a/src/mono/mono.proj +++ b/src/mono/mono.proj @@ -1185,6 +1185,9 @@ JS_ENGINES = [NODE_JS] <_MonoRuntimeArtifacts Condition="'$(TargetsBrowser)' == 'true' and '$(BuildMonoAOTCrossCompilerOnly)' != 'true'" Include="$(MonoObjDir)out\lib\libmono-profiler-aot.a"> $(RuntimeBinDir)libmono-profiler-aot.a + <_MonoRuntimeArtifacts Condition="'$(TargetsBrowser)' == 'true' and '$(BuildMonoAOTCrossCompilerOnly)' != 'true'" Include="$(MonoObjDir)out\lib\libmono-profiler-log.a"> + $(RuntimeBinDir)libmono-profiler-log.a + <_MonoRuntimeArtifacts Condition="'$(TargetsBrowser)' == 'true' and '$(BuildMonoAOTCrossCompilerOnly)' != 'true'" Include="$(MonoObjDir)out\lib\libmono-profiler-browser.a"> $(RuntimeBinDir)libmono-profiler-browser.a diff --git a/src/mono/mono/mini/mini-wasm.c b/src/mono/mono/mini/mini-wasm.c index 4f094b9246810..2a210e4af6e01 100644 --- a/src/mono/mono/mini/mini-wasm.c +++ b/src/mono/mono/mini/mini-wasm.c @@ -577,7 +577,6 @@ mono_init_native_crash_info (void) void mono_runtime_setup_stat_profiler (void) { - g_error ("mono_runtime_setup_stat_profiler"); } gboolean diff --git a/src/mono/mono/profiler/CMakeLists.txt b/src/mono/mono/profiler/CMakeLists.txt index e172774a6ebc5..f4271dc8bd4f8 100644 --- a/src/mono/mono/profiler/CMakeLists.txt +++ b/src/mono/mono/profiler/CMakeLists.txt @@ -6,6 +6,7 @@ include_directories( ${PROJECT_BINARY_DIR}/../../mono/eglib ${CMAKE_CURRENT_SOURCE_DIR}/../.. ${PROJECT_SOURCE_DIR}/../ + ${PROJECT_SOURCE_DIR}/../../../native/public ${PROJECT_SOURCE_DIR}/../eglib ${PROJECT_SOURCE_DIR}/../sgen) @@ -47,4 +48,18 @@ if(NOT DISABLE_LIBS) set_target_properties(mono-profiler-browser-static PROPERTIES OUTPUT_NAME mono-profiler-browser) install(TARGETS mono-profiler-browser-static LIBRARY) endif() + + if(HOST_BROWSER) + add_library(mono-profiler-log-static STATIC helper.c log.c log-args.c) + set_target_properties(mono-profiler-log-static PROPERTIES OUTPUT_NAME mono-profiler-log) + install(TARGETS mono-profiler-log-static LIBRARY) + + if(NOT DISABLE_LOG_PROFILER_GZ) + if (CLR_CMAKE_USE_SYSTEM_ZLIB) + target_link_libraries(mono-profiler-log-static PRIVATE ${Z_LIBS}) + else() + target_link_libraries(mono-profiler-log-static PRIVATE zlib) + endif() + endif() + endif() endif() diff --git a/src/mono/mono/profiler/helper.c b/src/mono/mono/profiler/helper.c index bbff8e7bf957b..05fc31b670840 100644 --- a/src/mono/mono/profiler/helper.c +++ b/src/mono/mono/profiler/helper.c @@ -8,7 +8,9 @@ #include +#if !defined (HOST_WASM) #include +#endif #ifdef HAVE_UNISTD_H #include @@ -42,6 +44,7 @@ mono_profhelper_close_socket_fd (SOCKET fd) void mono_profhelper_setup_command_server (SOCKET *server_socket, int *command_port, const char* profiler_name) { +#if !defined (HOST_WASM) *server_socket = socket (PF_INET, SOCK_STREAM, 0); if (*server_socket == INVALID_SOCKET) { @@ -77,11 +80,13 @@ mono_profhelper_setup_command_server (SOCKET *server_socket, int *command_port, } *command_port = ntohs (server_address.sin_port); +#endif } void mono_profhelper_add_to_fd_set (fd_set *set, SOCKET fd, int *max_fd) { +#if !defined (HOST_WASM) /* * This should only trigger for the basic FDs (server socket, pipes) at * startup if for some mysterious reason they're too large. In this case, @@ -99,4 +104,5 @@ mono_profhelper_add_to_fd_set (fd_set *set, SOCKET fd, int *max_fd) if (*max_fd < GUINT64_TO_INT(fd)) *max_fd = (int)fd; +#endif } diff --git a/src/mono/mono/profiler/log-args.c b/src/mono/mono/profiler/log-args.c index c8609177d18b0..124a629a14ca3 100644 --- a/src/mono/mono/profiler/log-args.c +++ b/src/mono/mono/profiler/log-args.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -98,6 +99,9 @@ parse_arg (const char *arg, ProfilerConfig *config) } else if (match_option (arg, "heapshot-on-shutdown", NULL)) { config->hs_on_shutdown = TRUE; config->enable_mask |= PROFLOG_HEAPSHOT_ALIAS; + } else if (match_option (arg, "take-heapshot-method", &val)) { + printf ("take-heapshot-method: %s\n", val); + set_log_profiler_take_heapshot_method(val); } else if (match_option (arg, "sample", &val)) { set_sample_freq (config, val); config->sampling_mode = MONO_PROFILER_SAMPLE_MODE_PROCESS; diff --git a/src/mono/mono/profiler/log.c b/src/mono/mono/profiler/log.c index abe5f3f479f57..24d3834296604 100644 --- a/src/mono/mono/profiler/log.c +++ b/src/mono/mono/profiler/log.c @@ -189,6 +189,8 @@ typedef struct { int small_id; } MonoProfilerThread; +static MonoMethodDesc *log_profiler_take_heapshot_method; + // Default value in `profiler_tls` for new threads. #define MONO_PROFILER_THREAD_ZERO ((MonoProfilerThread *) NULL) @@ -617,6 +619,7 @@ buffer_lock_helper (void); static void buffer_lock (void) { +#if !defined (HOST_WASM) /* * If the thread holding the exclusive lock tries to modify the * reader count, just make it a no-op. This way, we also avoid @@ -657,6 +660,8 @@ buffer_lock (void) } mono_memory_barrier (); + +#endif // HOST_WASM } static void @@ -685,6 +690,7 @@ buffer_lock_helper (void) static void buffer_unlock (void) { +#if !defined (HOST_WASM) mono_memory_barrier (); gint32 state = mono_atomic_load_i32 (&log_profiler.buffer_lock_state); @@ -697,6 +703,7 @@ buffer_unlock (void) g_assert (!(state >> 16) && "Why is the exclusive lock held?"); mono_atomic_dec_i32 (&log_profiler.buffer_lock_state); +#endif // HOST_WASM } static void @@ -3499,6 +3506,7 @@ runtime_initialized (MonoProfiler *profiler) mono_os_sem_init (&log_profiler.attach_threads_sem, 0); +#if !defined (HOST_WASM) /* * We must start the helper thread before the writer thread. This is * because start_helper_thread () sets up the command port which is written @@ -3507,6 +3515,9 @@ runtime_initialized (MonoProfiler *profiler) start_helper_thread (); start_writer_thread (); start_dumper_thread (); +#else + dump_header (); +#endif /* * Wait for all the internal threads to be started. If we don't do this, we @@ -3588,7 +3599,7 @@ create_profiler (const char *args, const char *filename, GPtrArray *filters) } } if (*nf == '|') { -#if HAVE_API_SUPPORT_WIN32_PIPE_OPEN_CLOSE && !defined (HOST_WIN32) +#if HAVE_API_SUPPORT_WIN32_PIPE_OPEN_CLOSE && !defined (HOST_WIN32) && !defined (HOST_WASM) log_profiler.file = popen (nf + 1, "w"); log_profiler.pipe_output = 1; #else @@ -3636,6 +3647,44 @@ create_profiler (const char *args, const char *filename, GPtrArray *filters) log_profiler.startup_time = current_time (); } +void +set_log_profiler_take_heapshot_method (const char *val) +{ + log_profiler_take_heapshot_method = mono_method_desc_new (val, TRUE); + + if (!log_profiler_take_heapshot_method) { + mono_profiler_printf_err ("Could not parse method description: %s", val); + exit (1); + } +} + +static void +proflog_trigger_heapshot (void); + +static void +prof_jit_done (MonoProfiler *prof, MonoMethod *method, MonoJitInfo *jinfo) +{ + MonoImage *image = mono_class_get_image (mono_method_get_class (method)); + + if (!image->assembly || method->wrapper_type || !log_profiler_take_heapshot_method) + return; + + if (log_profiler_take_heapshot_method && mono_method_desc_match (log_profiler_take_heapshot_method, method)) { + printf ("log-profiler | taking heapshot\n"); + proflog_trigger_heapshot (); + return; + } + else { + printf ("log-profiler not called (%p)\n", log_profiler_take_heapshot_method); + } +} + +static void +prof_inline_method (MonoProfiler *prof, MonoMethod *method, MonoMethod *inlined_method) +{ + prof_jit_done (prof, inlined_method, NULL); +} + MONO_API void mono_profiler_init_log (const char *desc); @@ -3758,6 +3807,9 @@ mono_profiler_init_log (const char *desc) mono_profiler_enable_allocations (); mono_profiler_enable_clauses (); mono_profiler_enable_sampling (handle); + mono_profiler_set_jit_done_callback (handle, prof_jit_done); + mono_profiler_set_inline_method_callback (handle, prof_inline_method); + /* * If no sample option was given by the user, this just leaves the sampling @@ -3770,3 +3822,12 @@ mono_profiler_init_log (const char *desc) done: ; } + +static void +proflog_trigger_heapshot (void) +{ + trigger_heapshot (); + + while (handle_writer_queue_entry ()); + while (handle_dumper_queue_entry ()); +} \ No newline at end of file diff --git a/src/mono/mono/profiler/log.h b/src/mono/mono/profiler/log.h index 9e3b320a7cfb2..a246afd1c1788 100644 --- a/src/mono/mono/profiler/log.h +++ b/src/mono/mono/profiler/log.h @@ -526,5 +526,6 @@ typedef struct { } ProfilerConfig; void proflog_parse_args (ProfilerConfig *config, const char *desc); +void set_log_profiler_take_heapshot_method (const char *val); #endif /* __MONO_PROFLOG_H__ */ diff --git a/src/mono/sample/wasm/browser-logprofile/Makefile b/src/mono/sample/wasm/browser-logprofile/Makefile new file mode 100644 index 0000000000000..52389be6050cb --- /dev/null +++ b/src/mono/sample/wasm/browser-logprofile/Makefile @@ -0,0 +1,7 @@ +TOP=../../../../.. + +include ../wasm.mk + +PROJECT_NAME=Wasm.BrowserLogProfile.Sample.csproj + +run: run-browser \ No newline at end of file diff --git a/src/mono/sample/wasm/browser-logprofile/Program.cs b/src/mono/sample/wasm/browser-logprofile/Program.cs new file mode 100644 index 0000000000000..6bebd913c0592 --- /dev/null +++ b/src/mono/sample/wasm/browser-logprofile/Program.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.JavaScript; + +namespace Sample +{ + public partial class Test + { + public static void Main(string[] args) + { + Console.WriteLine ("Hello, World!"); + } + + [JSExport] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void TakeHeapshot() { } + + [JSExport] + public static int TestMeaning() + { + for(int i=0; i<100; i++){ + var r = new int[1000]; + } + + return 42; + } + } +} diff --git a/src/mono/sample/wasm/browser-logprofile/README.md b/src/mono/sample/wasm/browser-logprofile/README.md new file mode 100644 index 0000000000000..e652d1c04ddf1 --- /dev/null +++ b/src/mono/sample/wasm/browser-logprofile/README.md @@ -0,0 +1,7 @@ +## How to run a sample app with log profiling enabled + +### Build the sample +1. Build the runtime +1. Build the sample with `make` +1. Run the sample with `make run` +1. Navigate to https://localhost:8000 diff --git a/src/mono/sample/wasm/browser-logprofile/Wasm.BrowserLogProfile.Sample.csproj b/src/mono/sample/wasm/browser-logprofile/Wasm.BrowserLogProfile.Sample.csproj new file mode 100644 index 0000000000000..fcc6164c1331a --- /dev/null +++ b/src/mono/sample/wasm/browser-logprofile/Wasm.BrowserLogProfile.Sample.csproj @@ -0,0 +1,15 @@ + + + true + log; + true + false + + + + + + + + + diff --git a/src/mono/sample/wasm/browser-logprofile/index.html b/src/mono/sample/wasm/browser-logprofile/index.html new file mode 100644 index 0000000000000..9e6cc3984a755 --- /dev/null +++ b/src/mono/sample/wasm/browser-logprofile/index.html @@ -0,0 +1,18 @@ + + + + + + + Log Profiler Sample + + + + + + + + Result from Sample.Test.TestMeaning: + + + \ No newline at end of file diff --git a/src/mono/sample/wasm/browser-logprofile/main.js b/src/mono/sample/wasm/browser-logprofile/main.js new file mode 100644 index 0000000000000..0ad2db48d38c8 --- /dev/null +++ b/src/mono/sample/wasm/browser-logprofile/main.js @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnet, exit } from './_framework/dotnet.js' + +function saveProfile(Module) { + let profileData = readProfileFile(Module); + + const a = document.createElement('a'); + const blob = new Blob([profileData]); + a.href = URL.createObjectURL(blob); + a.download = "output.mlpd"; + // Append anchor to body. + document.body.appendChild(a); + a.click(); + + // Remove anchor from body + document.body.removeChild(a); +} + +function readProfileFile(Module) { + let profileFilePath="output.mlpd"; + + var stat = Module.FS.stat(profileFilePath); + + if (stat && stat.size > 0) { + return Module.FS.readFile(profileFilePath); + } + else { + console.debug(`Unable to fetch the profile file ${profileFilePath} as it is empty`); + return null; + } +} + +try { + const { INTERNAL, Module, getAssemblyExports: getAssemblyExports } = await dotnet + .withElementOnExit() + .withExitCodeLogging() + .withConfig({ + logProfilerOptions: { + takeHeapshot: "Sample.Test::TakeHeapshot", + configuration: "log:alloc,output=output.mlpd" + } + }) + .create(); + + console.log("not ready yet") + const exports = await getAssemblyExports("Wasm.BrowserLogProfile.Sample"); + const testMeaning = exports.Sample.Test.TestMeaning; + const takeHeapshot = exports.Sample.Test.TakeHeapshot; + console.log("ready"); + + dotnet.run(); + + const ret = testMeaning(); + document.getElementById("out").innerHTML = ret; + console.debug(`ret: ${ret}`); + + takeHeapshot(); + saveProfile(Module); + + let exit_code = ret == 42 ? 0 : 1; + exit(exit_code); +} catch (err) { + exit(-1, err); +} diff --git a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/MemoryTests.cs b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/MemoryTests.cs index e95aa351ce837..239950d1fd727 100644 --- a/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/MemoryTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/MemoryTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Text.RegularExpressions; using Xunit.Abstractions; using Xunit; @@ -37,4 +38,26 @@ public async Task AllocateLargeHeapThenRepeatedlyInterop() await RunSdkStyleAppForBuild(new (Configuration: config, TestScenario: "AllocateLargeHeapThenInterop")); } } + + [Fact] + public async Task RunSimpleAppWithProfiler() + { + string config = "Release"; + CopyTestAsset("WasmBasicTestApp", "ProfilerTest", "App"); + // are are linking all 3 profilers, but below we only initialize log profiler and test it + string extraArgs = $"-p:WasmProfilers=\"aot+browser+log\" -p:WasmBuildNative=true"; + BuildProject(config, assertAppBundle: false, extraArgs: extraArgs); + + var result = await RunSdkStyleAppForBuild(new (Configuration: config, TestScenario: "ProfilerTest")); + Regex regex = new Regex(@"Profile data of size (\d+) bytes"); + var match = result.TestOutput + .Select(line => regex.Match(line)) + .FirstOrDefault(m => m.Success); + Assert.True(match != null, $"TestOuptup did not contain log matching {regex}"); + if (!int.TryParse(match.Groups[1].Value, out int fileSize)) + { + Assert.Fail($"Failed to parse profile size from {match.Groups[1].Value} to int"); + } + Assert.True(fileSize >= 10 * 1024, $"Profile file size is less than 10KB. Actual size: {fileSize} bytes."); + } } diff --git a/src/mono/wasm/features.md b/src/mono/wasm/features.md index 0f84f0bd00e17..2511f9c5c7b1b 100644 --- a/src/mono/wasm/features.md +++ b/src/mono/wasm/features.md @@ -426,6 +426,45 @@ import { dotnet } from './dotnet.js' await dotnet.withConfig({browserProfilerOptions: {}}).run(); ``` +### Log Profiling for Memory Troubleshooting + +You can enable integration with log profiler via following elements in your .csproj: + +```xml + + log; + true + +``` + +In simple browser template, you can add following to your `main.js` + +```javascript +import { dotnet } from './dotnet.js' +await dotnet.withConfig({ + logProfilerOptions: { + takeHeapshot: "MyApp.Profiling::TakeHeapshot", + configuration: "log:alloc,output=output.mlpd" + }}).run(); +``` + +In order to trigger a heap shot, add the following: + +```csharp +namespace MyApp; + +class Profiling +{ + [JSExport] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void TakeHeapshot() { } +} +``` + +Invoke `MyApp.Profiling.TakeHeapshot()` from your code in order to create a memory heap shot and flush the contents of the profile to the VFS. Make sure to align the namespace and class of the `logProfilerOptions.takeHeapshot` with your class. + +You can download the mpld file to analyze it. + ### Diagnostic tools We have initial implementation of diagnostic server and [event pipe](https://learn.microsoft.com/dotnet/core/diagnostics/eventpipe) diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/ProfilerTest.cs b/src/mono/wasm/testassets/WasmBasicTestApp/App/ProfilerTest.cs new file mode 100644 index 0000000000000..ef3fa75dc92b1 --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/ProfilerTest.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.JavaScript; + +public partial class ProfilerTest +{ + [JSExport] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void TakeHeapshot() { } + + [JSExport] + public static int TestMeaning() + { + for(int i=0; i<100; i++){ + var r = new int[1000]; + } + + return 42; + } +} diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/WasmBasicTestApp.csproj b/src/mono/wasm/testassets/WasmBasicTestApp/App/WasmBasicTestApp.csproj index e16a8c4948ca5..548b310286bed 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/WasmBasicTestApp.csproj +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/WasmBasicTestApp.csproj @@ -21,5 +21,6 @@ + diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/index.html b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/index.html index 997c0d3047c08..5179693a495f6 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/index.html +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/index.html @@ -8,6 +8,7 @@ + diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js index f0515c8dfe5cb..75071104f676b 100644 --- a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/main.js @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import { dotnet, exit } from './_framework/dotnet.js' +import { saveProfile } from './profiler.js' // Read test case from query string const params = new URLSearchParams(location.search); @@ -121,9 +122,17 @@ switch (testCase) { }; dotnet.withConfig({ maxParallelDownloads: maxParallelDownloads }); break; + case "ProfilerTest": + dotnet.withConfig({ + logProfilerOptions: { + takeHeapshot: "ProfilerTest::TakeHeapshot", + configuration: "log:alloc,output=output.mlpd" + } + }) + break; } -const { setModuleImports, getAssemblyExports, getConfig, INTERNAL } = await dotnet.create(); +const { setModuleImports, Module, getAssemblyExports, getConfig, INTERNAL } = await dotnet.create(); const config = getConfig(); const exports = await getAssemblyExports(config.mainAssemblyName); const assemblyExtension = Object.keys(config.resources.coreAssembly)[0].endsWith('.wasm') ? ".wasm" : ".dll"; @@ -201,6 +210,25 @@ try { exports.MemoryTest.Run(); exit(0); break; + case "ProfilerTest": + console.log("not ready yet") + const myExports = await getAssemblyExports(config.mainAssemblyName); + const testMeaning = myExports.ProfilerTest.TestMeaning; + const takeHeapshot = myExports.ProfilerTest.TakeHeapshot; + console.log("ready"); + + dotnet.run(); + + const ret = testMeaning(); + document.getElementById("out").innerHTML = ret; + console.debug(`ret: ${ret}`); + + takeHeapshot(); + saveProfile(Module); + + let exit_code = ret == 42 ? 0 : 1; + exit(exit_code); + break; default: console.error(`Unknown test case: ${testCase}`); exit(3); diff --git a/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/profiler.js b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/profiler.js new file mode 100644 index 0000000000000..53d3841e316cf --- /dev/null +++ b/src/mono/wasm/testassets/WasmBasicTestApp/App/wwwroot/profiler.js @@ -0,0 +1,35 @@ +export function saveProfile(Module) { + const fileName = "output.mlpd"; + let profileData = readProfileFile(Module, fileName); + + if (!profileData) { + console.error("Profile data is empty or could not be read."); + return; + } + + const a = document.createElement('a'); + const blob = new Blob([profileData]); + a.href = URL.createObjectURL(blob); + a.download = fileName; + // Append anchor to body. + document.body.appendChild(a); + a.click(); + + console.log(`TestOutput -> Profile data of size ${profileData.length} bytes started downloading.`); + + // Remove anchor from body + document.body.removeChild(a); +} + +function readProfileFile(Module, fileName) { + + var stat = Module.FS.stat(fileName); + + if (stat && stat.size > 0) { + return Module.FS.readFile(fileName); + } + else { + console.debug(`Unable to fetch the profile file ${fileName} as it is empty`); + return null; + } +} \ No newline at end of file