From 84a584e56468ac777a83cafd6b09804f9aae75f3 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 28 Jan 2025 18:46:13 +0100 Subject: [PATCH] wip --- .../Diagnostics/Metrics/AggregationManager.cs | 1 + .../System.Private.CoreLib.Shared.projitems | 4 + .../Tracing/CounterGroup.Threads.cs | 54 +++ .../Diagnostics/Tracing/CounterGroup.Wasm.cs | 34 ++ .../Diagnostics/Tracing/CounterGroup.cs | 41 +- .../EventPipeEventDispatcher.Threads.cs | 56 +++ .../Tracing/EventPipeEventDispatcher.Wasm.cs | 43 ++ .../Tracing/EventPipeEventDispatcher.cs | 51 +- .../System/Diagnostics/Tracing/EventSource.cs | 4 +- .../runtime/diagnostics/client-commands.ts | 436 ++++++++++++++++++ .../browser/runtime/diagnostics/common.ts | 90 ++++ .../browser/runtime/diagnostics/diag-js.ts | 134 ++++++ .../browser/runtime/diagnostics/diag-ws.ts | 63 +++ .../runtime/diagnostics/dotnet-counters.ts | 33 ++ .../runtime/diagnostics/dotnet-gcdump.ts | 41 ++ .../runtime/diagnostics/dotnet-profiler.ts | 33 ++ src/mono/browser/runtime/diagnostics/index.ts | 78 ++++ src/mono/llvm/llvm-init.proj | 2 +- .../sample/wasm/browser-eventpipe/Program.cs | 137 ++++++ .../Wasm.Browser.EventPipe.Sample.csproj | 14 + .../sample/wasm/browser-eventpipe/index.html | 25 + .../sample/wasm/browser-eventpipe/main.js | 59 +++ 22 files changed, 1349 insertions(+), 84 deletions(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.Threads.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.Wasm.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventPipeEventDispatcher.Threads.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventPipeEventDispatcher.Wasm.cs create mode 100644 src/mono/browser/runtime/diagnostics/client-commands.ts create mode 100644 src/mono/browser/runtime/diagnostics/common.ts create mode 100644 src/mono/browser/runtime/diagnostics/diag-js.ts create mode 100644 src/mono/browser/runtime/diagnostics/diag-ws.ts create mode 100644 src/mono/browser/runtime/diagnostics/dotnet-counters.ts create mode 100644 src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts create mode 100644 src/mono/browser/runtime/diagnostics/dotnet-profiler.ts create mode 100644 src/mono/browser/runtime/diagnostics/index.ts create mode 100644 src/mono/sample/wasm/browser-eventpipe/Program.cs create mode 100644 src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj create mode 100644 src/mono/sample/wasm/browser-eventpipe/index.html create mode 100644 src/mono/sample/wasm/browser-eventpipe/main.js diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/AggregationManager.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/AggregationManager.cs index 2dd9450c6eae50..5856dc2d73487c 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/AggregationManager.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/AggregationManager.cs @@ -154,6 +154,7 @@ private void PublishedInstrument(Instrument instrument, MeterListener _) } } + // TODO Pavel public void Start() { // if already started or already stopped we can't be started again diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 064e2579bdca3c..d4fad477d31a86 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1545,6 +1545,8 @@ + + @@ -1552,6 +1554,8 @@ + + diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.Threads.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.Threads.cs new file mode 100644 index 00000000000000..a719bd04af2c44 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.Threads.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Runtime.Versioning; +using System.Threading; + +namespace System.Diagnostics.Tracing +{ + internal sealed partial class CounterGroup + { + private static Thread? s_pollingThread; + // Used for sleeping for a certain amount of time while allowing the thread to be woken up + private static AutoResetEvent? s_pollingThreadSleepEvent; + + private static void CreatePollingTimer() + { + // Create the polling thread and init all the shared state if needed + if (s_pollingThread == null) + { + s_pollingThreadSleepEvent = new AutoResetEvent(false); + s_counterGroupEnabledList = new List(); + // TODO + s_pollingThread = new Thread(PollForValues) + { + IsBackground = true, + Name = ".NET Counter Poller" + }; + s_pollingThread.Start(); + } + else + { + // notify the polling thread that the polling interval may have changed and the sleep should be recomputed + s_pollingThreadSleepEvent!.Set(); + } + } + + private static void PollForValues() + { + AutoResetEvent? sleepEvent = null; + lock (s_counterGroupLock) + { + sleepEvent = s_pollingThreadSleepEvent; + } + + while (true) + { + var sleepDurationInMilliseconds = PollOnce(); + + sleepEvent?.WaitOne(sleepDurationInMilliseconds); + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.Wasm.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.Wasm.cs new file mode 100644 index 00000000000000..2f9a0acb6788e8 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.Wasm.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Runtime.Versioning; +using System.Threading; + +namespace System.Diagnostics.Tracing +{ + internal sealed partial class CounterGroup + { + private static Timer? s_pollingTimer; + + private static void CreatePollingTimer() + { + if (s_pollingTimer == null) + { + s_pollingTimer = new Timer(PollForValues, null, 0, 0); + s_counterGroupEnabledList = new List(); + } + else + { + // notify the polling callback that the polling interval may have changed and the sleep should be recomputed + s_pollingTimer.Change(0, 0); + } + } + + private static void PollForValues(object? state) + { + var sleepDurationInMilliseconds = PollOnce(); + s_pollingTimer!.Change(sleepDurationInMilliseconds, 0); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.cs index d08c6949616c10..d5a73b3ee40e67 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterGroup.cs @@ -7,10 +7,7 @@ namespace System.Diagnostics.Tracing { -#if !ES_BUILD_STANDALONE - [UnsupportedOSPlatform("browser")] -#endif - internal sealed class CounterGroup + internal sealed partial class CounterGroup { private readonly EventSource _eventSource; private readonly List _counters; @@ -159,27 +156,12 @@ private void EnableTimer(float pollingIntervalInSeconds) _timeStampSinceCollectionStarted = DateTime.UtcNow; _nextPollingTimeStamp = DateTime.UtcNow + new TimeSpan(0, 0, (int)pollingIntervalInSeconds); - // Create the polling thread and init all the shared state if needed - if (s_pollingThread == null) - { - s_pollingThreadSleepEvent = new AutoResetEvent(false); - s_counterGroupEnabledList = new List(); - s_pollingThread = new Thread(PollForValues) - { - IsBackground = true, - Name = ".NET Counter Poller" - }; - s_pollingThread.Start(); - } + CreatePollingTimer(); if (!s_counterGroupEnabledList!.Contains(this)) { s_counterGroupEnabledList.Add(this); } - - // notify the polling thread that the polling interval may have changed and the sleep should - // be recomputed - s_pollingThreadSleepEvent!.Set(); } } @@ -267,29 +249,21 @@ private void OnTimer() } } - private static Thread? s_pollingThread; - // Used for sleeping for a certain amount of time while allowing the thread to be woken up - private static AutoResetEvent? s_pollingThreadSleepEvent; - private static List? s_counterGroupEnabledList; private static List s_needsResetIncrementingPollingCounters = []; - private static void PollForValues() + private static int PollOnce() { - AutoResetEvent? sleepEvent = null; - // Cache of onTimer callbacks for each CounterGroup. // We cache these outside of the scope of s_counterGroupLock because // calling into the callbacks can cause a re-entrancy into CounterGroup.Enable() // and result in a deadlock. (See https://github.com/dotnet/runtime/issues/40190 for details) var onTimers = new List(); List? countersToReset = null; - while (true) - { + int sleepDurationInMilliseconds = int.MaxValue; lock (s_counterGroupLock) { - sleepEvent = s_pollingThreadSleepEvent; foreach (CounterGroup counterGroup in s_counterGroupEnabledList!) { DateTime now = DateTime.UtcNow; @@ -316,8 +290,6 @@ private static void PollForValues() { counter.UpdateMetric(); } - - countersToReset = null; } foreach (CounterGroup onTimer in onTimers) @@ -329,10 +301,9 @@ private static void PollForValues() { sleepDurationInMilliseconds = -1; // WaitOne uses -1 to mean infinite } - sleepEvent?.WaitOne(sleepDurationInMilliseconds); - } - } + return sleepDurationInMilliseconds; + } #endregion // Timer Processing } diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventPipeEventDispatcher.Threads.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventPipeEventDispatcher.Threads.cs new file mode 100644 index 00000000000000..93c7f3473c8f5f --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventPipeEventDispatcher.Threads.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Diagnostics.Tracing +{ + internal sealed partial class EventPipeEventDispatcher + { + private void StartDispatchTask(ulong sessionID, DateTime syncTimeUtc, long syncTimeQPC, long timeQPCFrequency) + { + Debug.Assert(Monitor.IsEntered(m_dispatchControlLock)); + Debug.Assert(sessionID != 0); + + m_dispatchTaskCancellationSource = new CancellationTokenSource(); + Task? previousDispatchTask = m_dispatchTask; + m_dispatchTask = Task.Factory.StartNew(() => DispatchEventsToEventListeners(sessionID, syncTimeUtc, syncTimeQPC, timeQPCFrequency, previousDispatchTask, m_dispatchTaskCancellationSource.Token), CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + + private void DispatchEventsToEventListeners(ulong sessionID, DateTime syncTimeUtc, long syncTimeQPC, long timeQPCFrequency, Task? previousDispatchTask, CancellationToken token) + { + Debug.Assert(sessionID != 0); + previousDispatchTask?.Wait(CancellationToken.None); + + // Struct to fill with the call to GetNextEvent. + while (!token.IsCancellationRequested) + { + bool eventsReceived = DispatchEventsToEventListenersOnce(sessionID, syncTimeUtc, syncTimeQPC, timeQPCFrequency, token); + + // Wait for more events. + if (!token.IsCancellationRequested) + { + if (!eventsReceived) + { + EventPipeInternal.WaitForSessionSignal(sessionID, Timeout.Infinite); + } + + Thread.Sleep(10); + } + } + + // Wait for SignalSession() to be called before we call disable, otherwise + // the SignalSession() call could be on a disabled session. + SpinWait sw = default; + while (Volatile.Read(ref m_sessionID) == sessionID) + { + sw.SpinOnce(); + } + + // Disable the old session. This can happen asynchronously since we aren't using the old session + // anymore. + EventPipeInternal.Disable(sessionID); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventPipeEventDispatcher.Wasm.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventPipeEventDispatcher.Wasm.cs new file mode 100644 index 00000000000000..78f626044fed7d --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventPipeEventDispatcher.Wasm.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Diagnostics.Tracing +{ + // this is single-threaded version of EventPipeEventDispatcher + internal sealed partial class EventPipeEventDispatcher + { + private void StartDispatchTask(ulong sessionID, DateTime syncTimeUtc, long syncTimeQPC, long timeQPCFrequency) + { + Debug.Assert(Monitor.IsEntered(m_dispatchControlLock)); + Debug.Assert(sessionID != 0); + + m_dispatchTaskCancellationSource = new CancellationTokenSource(); + Task? previousDispatchTask = m_dispatchTask; + if (previousDispatchTask != null) + { + m_dispatchTask = previousDispatchTask.ContinueWith(_ => DispatchEventsToEventListeners(sessionID, syncTimeUtc, syncTimeQPC, timeQPCFrequency, m_dispatchTaskCancellationSource.Token), + m_dispatchTaskCancellationSource.Token, TaskContinuationOptions.None, TaskScheduler.Default); + } + else + { + m_dispatchTask = DispatchEventsToEventListeners(sessionID, syncTimeUtc, syncTimeQPC, timeQPCFrequency, m_dispatchTaskCancellationSource.Token); + } + } + + private async Task DispatchEventsToEventListeners(ulong sessionID, DateTime syncTimeUtc, long syncTimeQPC, long timeQPCFrequency, CancellationToken token) + { + Debug.Assert(sessionID != 0); + + while (!token.IsCancellationRequested) + { + DispatchEventsToEventListenersOnce(sessionID, syncTimeUtc, syncTimeQPC, timeQPCFrequency, token); + await Task.Delay(100, token).ConfigureAwait(false); + } + + EventPipeInternal.Disable(sessionID); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventPipeEventDispatcher.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventPipeEventDispatcher.cs index ffa63f1c98db1c..83911a0a13a492 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventPipeEventDispatcher.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventPipeEventDispatcher.cs @@ -6,7 +6,7 @@ namespace System.Diagnostics.Tracing { - internal sealed class EventPipeEventDispatcher + internal sealed partial class EventPipeEventDispatcher { internal sealed class EventListenerSubscription { @@ -129,21 +129,6 @@ private void CommitDispatchConfiguration() StartDispatchTask(sessionID, syncTimeUtc, syncTimeQPC, timeQPCFrequency); } - private void StartDispatchTask(ulong sessionID, DateTime syncTimeUtc, long syncTimeQPC, long timeQPCFrequency) - { - if (OperatingSystem.IsBrowser() || OperatingSystem.IsWasi()) - { - throw new PlatformNotSupportedException(); - } - - Debug.Assert(Monitor.IsEntered(m_dispatchControlLock)); - Debug.Assert(sessionID != 0); - - m_dispatchTaskCancellationSource = new CancellationTokenSource(); - Task? previousDispatchTask = m_dispatchTask; - m_dispatchTask = Task.Factory.StartNew(() => DispatchEventsToEventListeners(sessionID, syncTimeUtc, syncTimeQPC, timeQPCFrequency, previousDispatchTask, m_dispatchTaskCancellationSource.Token), CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); - } - private void SetStopDispatchTask() { Debug.Assert(Monitor.IsEntered(m_dispatchControlLock)); @@ -160,16 +145,13 @@ private void SetStopDispatchTask() Volatile.Write(ref m_sessionID, 0); } - private unsafe void DispatchEventsToEventListeners(ulong sessionID, DateTime syncTimeUtc, long syncTimeQPC, long timeQPCFrequency, Task? previousDispatchTask, CancellationToken token) + private unsafe bool DispatchEventsToEventListenersOnce(ulong sessionID, DateTime syncTimeUtc, long syncTimeQPC, long timeQPCFrequency, CancellationToken token) { - Debug.Assert(sessionID != 0); - previousDispatchTask?.Wait(CancellationToken.None); + bool eventsReceived = false; // Struct to fill with the call to GetNextEvent. EventPipeEventInstanceData instanceData; - while (!token.IsCancellationRequested) - { - bool eventsReceived = false; + // Get the next event. while (!token.IsCancellationRequested && EventPipeInternal.GetNextEvent(sessionID, &instanceData)) { @@ -184,30 +166,7 @@ private unsafe void DispatchEventsToEventListeners(ulong sessionID, DateTime syn NativeRuntimeEventSource.Log.ProcessEvent(instanceData.EventID, instanceData.ThreadID, dateTimeStamp, instanceData.ActivityId, instanceData.ChildActivityId, payload); } } - - // Wait for more events. - if (!token.IsCancellationRequested) - { - if (!eventsReceived) - { - EventPipeInternal.WaitForSessionSignal(sessionID, Timeout.Infinite); - } - - Thread.Sleep(10); - } - } - - // Wait for SignalSession() to be called before we call disable, otherwise - // the SignalSession() call could be on a disabled session. - SpinWait sw = default; - while (Volatile.Read(ref m_sessionID) == sessionID) - { - sw.SpinOnce(); - } - - // Disable the old session. This can happen asynchronously since we aren't using the old session - // anymore. - EventPipeInternal.Disable(sessionID); + return eventsReceived; } /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventSource.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventSource.cs index b234e6d96ea3ed..92366952a87540 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventSource.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventSource.cs @@ -1426,12 +1426,12 @@ protected unsafe void WriteEventWithRelatedActivityIdCore(int eventId, Guid* rel if (m_Dispatchers != null && metadata.EnabledForAnyListener) { -#if MONO && !TARGET_BROWSER && !TARGET_WASI +#if MONO && !TARGET_WASI // On Mono, managed events from NativeRuntimeEventSource are written using WriteEventCore which can be // written doubly because EventPipe tries to pump it back up to EventListener via NativeRuntimeEventSource.ProcessEvents. // So we need to prevent this from getting written directly to the Listeners. if (this.GetType() != typeof(NativeRuntimeEventSource)) -#endif // MONO && !TARGET_BROWSER && !TARGET_WASI +#endif // MONO && !TARGET_WASI { var eventCallbackArgs = new EventWrittenEventArgs(this, eventId, pActivityId, relatedActivityId); WriteToAllListeners(eventCallbackArgs, eventDataCount, data); diff --git a/src/mono/browser/runtime/diagnostics/client-commands.ts b/src/mono/browser/runtime/diagnostics/client-commands.ts new file mode 100644 index 00000000000000..abe14a8cc2711e --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/client-commands.ts @@ -0,0 +1,436 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { SessionId } from "./common"; + +export const advert1 = [65, 68, 86, 82, 95]; +export const dotnet_IPC_V1 = [68, 79, 84, 78, 69, 84, 95, 73, 80, 67, 95, 86, 49, 0]; + + +// this file contains the IPC commands that are sent by client (like dotnet-trace) to the diagnostic server (like Mono VM in the browser) +// just formatting bytes, no sessions management here + +export function commandStopTracing (sessionID:SessionId) { + return Uint8Array.from([ + ...serializeHeader(CommandSetId.EventPipe, EventPipeCommandId.StopTracing, computeMessageByteLength(8)), + ...serializeUint64(sessionID), + ]); +} + +export function commandResumeRuntime () { + return Uint8Array.from([ + ...serializeHeader(CommandSetId.Process, ProcessCommandId.ResumeRuntime, computeMessageByteLength(0)), + ]); +} + +export function commandProcessInfo3 () { + return Uint8Array.from([ + ...serializeHeader(CommandSetId.Process, ProcessCommandId.ProcessInfo3, computeMessageByteLength(0)), + ]); +} + +export function commandGcHeapDump () { + return commandCollectTracing2({ + circularBufferMB: 256, + format: 1, + requestRundown: true, + providers: [ + { + keywords: [ + 0x0000_0000, + Keywords.GCHeapSnapshot, // 0x1980001 + // GC_HEAP_DUMP_VTABLE_CLASS_REF_KEYWORD 0x8000000 + // GC_FINALIZATION_KEYWORD 0x1000000 + // GC_HEAP_COLLECT_KEYWORD 0x0800000 + // GC_KEYWORD 0x0000001 + ], + logLevel: 5, + provider_name: "Microsoft-Windows-DotNETRuntime", + arguments: null + } + ] + }); +} + +export function commandCounters () { + return commandCollectTracing2({ + circularBufferMB: 256, + format: 1, + requestRundown: false, + providers: [ + /*{ + keywords: [ + 0x0000_0000, + 0x0000_0000, + ], + logLevel: 4, + provider_name: "WasmHello", + arguments: "EventCounterIntervalSec=1" + },*/ + { + keywords: [ + 0x0000_0000, + 0x0000_0000, + ], + logLevel: 4, + provider_name: "System.Runtime", + arguments: "EventCounterIntervalSec=1" + }, + { + keywords: [ + 0x0000_0000, + 0x0000_0002, + ], + logLevel: 4, + provider_name: "System.Diagnostics.Metrics", + arguments: "SessionId=SHARED;Metrics=System.Runtime;RefreshInterval=1;MaxTimeSeries=1000;MaxHistograms=10;ClientId=c98f989b-369c-41af-bc8e-7ab261fba16c" + } + ] + }); +} + +export function commandSampleProfiler () { + return commandCollectTracing2({ + circularBufferMB: 256, + format: 1, + requestRundown: true, + providers: [ + { + keywords: [ + 0x0000_0000, + 0x0000_0000, + ], + logLevel: 4, + provider_name: "Microsoft-DotNETCore-SampleProfiler", + arguments: null + } + ] + }); +} + +function commandCollectTracing2 (payload2:PayloadV2) { + const payloadLength = computeCollectTracing2PayloadByteLength(payload2); + const messageLength = computeMessageByteLength(payloadLength); + const message = [ + ...serializeHeader(CommandSetId.EventPipe, EventPipeCommandId.CollectTracing2, messageLength), + ...serializeUint32(payload2.circularBufferMB), + ...serializeUint32(payload2.format), + ...serializeUint8(payload2.requestRundown ? 1 : 0), + ...serializeUint32(payload2.providers.length), + ]; + for (const provider of payload2.providers) { + message.push(...serializeUint64(provider.keywords)); + message.push(...serializeUint32(provider.logLevel)); + message.push(...serializeString(provider.provider_name)); + message.push(...serializeString(provider.arguments)); + } + return Uint8Array.from(message); +} + +const enum Keywords { + None = 0, + All = 0xFFFF_FFFF, + // + // Summary: + // Logging when garbage collections and finalization happen. + GC = 1, + // + // Summary: + // Events when GC handles are set or destroyed. + GCHandle = 2, + Binder = 4, + // + // Summary: + // Logging when modules actually get loaded and unloaded. + Loader = 8, + // + // Summary: + // Logging when Just in time (JIT) compilation occurs. + Jit = 0x10, + // + // Summary: + // Logging when precompiled native (NGEN) images are loaded. + NGen = 0x20, + // + // Summary: + // Indicates that on attach or module load , a rundown of all existing methods should + // be done + StartEnumeration = 0x40, + // + // Summary: + // Indicates that on detach or process shutdown, a rundown of all existing methods + // should be done + StopEnumeration = 0x80, + // + // Summary: + // Events associated with validating security restrictions. + Security = 0x400, + // + // Summary: + // Events for logging resource consumption on an app-domain level granularity + AppDomainResourceManagement = 0x800, + // + // Summary: + // Logging of the internal workings of the Just In Time compiler. This is fairly + // verbose. It details decisions about interesting optimization (like inlining and + // tail call) + JitTracing = 0x1000, + // + // Summary: + // Log information about code thunks that transition between managed and unmanaged + // code. + Interop = 0x2000, + // + // Summary: + // Log when lock contention occurs. (Monitor.Enters actually blocks) + Contention = 0x4000, + // + // Summary: + // Log exception processing. + Exception = 0x8000, + // + // Summary: + // Log events associated with the threadpoo, and other threading events. + Threading = 0x10000, + // + // Summary: + // Dump the native to IL mapping of any method that is JIT compiled. (V4.5 runtimes + // and above). + JittedMethodILToNativeMap = 0x20000, + // + // Summary: + // If enabled will suppress the rundown of NGEN events on V4.0 runtime (has no effect + // on Pre-V4.0 runtimes). + OverrideAndSuppressNGenEvents = 0x40000, + // + // Summary: + // Enables the 'BulkType' event + Type = 0x80000, + // + // Summary: + // Enables the events associated with dumping the GC heap + GCHeapDump = 0x100000, + // + // Summary: + // Enables allocation sampling with the 'fast'. Sample to limit to 100 allocations + // per second per type. This is good for most detailed performance investigations. + // Note that this DOES update the allocation path to be slower and only works if + // the process start with this on. + GCSampledObjectAllocationHigh = 0x200000, + // + // Summary: + // Enables events associate with object movement or survival with each GC. + GCHeapSurvivalAndMovement = 0x400000, + // + // Summary: + // Triggers a GC. Can pass a 64 bit value that will be logged with the GC Start + // event so you know which GC you actually triggered. + GCHeapCollect = 0x800000, + // + // Summary: + // Indicates that you want type names looked up and put into the events (not just + // meta-data tokens). + GCHeapAndTypeNames = 0x1000000, + // + // Summary: + // Enables allocation sampling with the 'slow' rate, Sample to limit to 5 allocations + // per second per type. This is reasonable for monitoring. Note that this DOES update + // the allocation path to be slower and only works if the process start with this + // on. + GCSampledObjectAllocationLow = 0x2000000, + // + // Summary: + // Turns on capturing the stack and type of object allocation made by the .NET Runtime. + // This is only supported after V4.5.3 (Late 2014) This can be very verbose and + // you should seriously using GCSampledObjectAllocationHigh instead (and GCSampledObjectAllocationLow + // for production scenarios). + GCAllObjectAllocation = 0x2200000, + // + // Summary: + // This suppresses NGEN events on V4.0 (where you have NGEN PDBs), but not on V2.0 + // (which does not know about this bit and also does not have NGEN PDBS). + SupressNGen = 0x40000, + // + // Summary: + // TODO document + PerfTrack = 0x20000000, + // + // Summary: + // Also log the stack trace of events for which this is valuable. + Stack = 0x40000000, + // + // Summary: + // This allows tracing work item transfer events (thread pool enqueue/dequeue/ioenqueue/iodequeue/a.o.) + ThreadTransfer = 0x80000000, + // + // Summary: + // .NET Debugger events + Debugger = 0x100000000, + // + // Summary: + // Events intended for monitoring on an ongoing basis. + Monitoring = 0x200000000, + // + // Summary: + // Events that will dump PDBs of dynamically generated assemblies to the ETW stream. + Codesymbols = 0x400000000, + // + // Summary: + // Events that provide information about compilation. + Compilation = 0x1000000000, + // + // Summary: + // Diagnostic events for diagnosing compilation and pre-compilation features. + CompilationDiagnostic = 0x2000000000, + // + // Summary: + // Diagnostic events for capturing token information for events that express MethodID + MethodDiagnostic = 0x4000000000, + // + // Summary: + // Diagnostic events for diagnosing issues involving the type loader. + TypeDiagnostic = 0x8000000000, + // + // Summary: + // Events for wait handle waits. + WaitHandle = 0x40000000000, + // + // Summary: + // Recommend default flags (good compromise on verbosity). + Default = 0x14C14FCCBD, + // + // Summary: + // What is needed to get symbols for JIT compiled code. + JITSymbols = 0x60098, + // + // Summary: + // This provides the flags commonly needed to take a heap .NET Heap snapshot with + // ETW. + GCHeapSnapshot = 0x1980001 +} + +export const enum CommandSetId { + Reserved = 0, + Dump = 1, + EventPipe = 2, + Profiler = 3, + Process = 4, + + // replies + Server = 0xFF, +} + +const enum EventPipeCommandId { + StopTracing = 1, + CollectTracing = 2, + CollectTracing2 = 3, +} + +const enum ProcessCommandId { + ProcessInfo = 0, + ResumeRuntime = 1, + ProcessEnvironment = 2, + SetEnvVar = 3, + ProcessInfo2 = 4, + EnablePerfmap = 5, + DisablePerfmap = 6, + ApplyStartupHook = 7, + ProcessInfo3 = 8, +} + +export const enum ServerCommandId { + OK = 0, + Error = 0xFF, +} + +function serializeMagic () { + return Uint8Array.from(dotnet_IPC_V1); +} + +function serializeUint8 (value:number) { + return Uint8Array.from([value]); +} + +function serializeUint16 (value:number) { + return new Uint8Array(Uint16Array.from([value]).buffer); +} + +function serializeUint32 (value:number) { + return new Uint8Array(Uint32Array.from([value]).buffer); +} + +function serializeUint64 (value:[number, number]) { + // value == [hi, lo] + return new Uint8Array(Uint32Array.from([value[1], value[0]]).buffer); +} + +function serializeString (value:string|null) { + const message = []; + if (value === null || value === undefined || value === "") { + message.push(...serializeUint32(1)); + message.push(...serializeUint16(0)); + } else { + const len = value.length; + const hasNul = value[len - 1] === "\0"; + message.push(...serializeUint32(len + (hasNul ? 0 : 1))); + for (let i = 0; i < len; i++) { + message.push(...serializeUint16(value.charCodeAt(i))); + } + if (!hasNul) { + message.push(...serializeUint16(0)); + } + } + return message; +} + +function computeStringByteLength (s:string|null) { + if (s === undefined || s === null || s === "") + return 4 + 2; // just length of empty zero terminated string + return 4 + 2 * s.length + 2; // length + UTF16 + null +} + +function computeMessageByteLength (payloadLength:number) { + const fullHeaderSize = 14 + 2 // magic, len + + 1 + 1 // commandSet, command + + 2; // reserved ; + return fullHeaderSize + payloadLength; +} + +function serializeHeader (commandSet:CommandSetId, command:ServerCommandId|EventPipeCommandId|ProcessCommandId, len:number) { + return Uint8Array.from([ + ...serializeMagic(), + ...serializeUint16(len), + ...serializeUint8(commandSet), + ...serializeUint8(command), + ...serializeUint16(0), // reserved*/ + ]); +} + +function computeCollectTracing2PayloadByteLength (payload2:PayloadV2) { + let len = 0; + len += 4; // circularBufferMB + len += 4; // format + len += 1; // requestRundown + len += 4; // providers length + for (const provider of payload2.providers) { + len += 8; // keywords + len += 4; // level + len += computeStringByteLength(provider.provider_name); + len += computeStringByteLength(provider.arguments); + } + return len; +} + +type ProviderV2 ={ + keywords: [ 0, Keywords, ], + logLevel: number, + provider_name: string, + arguments: string|null +} + +type PayloadV2 = { + circularBufferMB: number, + format: number, + requestRundown: boolean, + providers: ProviderV2[] +} diff --git a/src/mono/browser/runtime/diagnostics/common.ts b/src/mono/browser/runtime/diagnostics/common.ts new file mode 100644 index 00000000000000..f57a6998eb0403 --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/common.ts @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import cwraps from "../cwraps"; +import { loaderHelpers, Module } from "../globals"; +import { VoidPtr } from "../types/emscripten"; + +let lastScheduledTimeoutId: any = undefined; + +// run another cycle of the event loop, which is EP threads on MT runtime +export function diagnostic_server_loop () { + lastScheduledTimeoutId = undefined; + if (loaderHelpers.is_runtime_running()) { + try { + cwraps.mono_background_exec();// give GC chance to run + cwraps.mono_wasm_ds_exec(); + schedule_diagnostic_server_loop(100); + } catch (ex) { + loaderHelpers.mono_exit(1, ex); + } + } +} + +export function schedule_diagnostic_server_loop (delay = 0):void { + if (!lastScheduledTimeoutId) { + lastScheduledTimeoutId = Module.safeSetTimeout(diagnostic_server_loop, delay); + } +} + +export class DiagConnectionBase { + protected messagesToSend: Uint8Array[] = []; + protected messagesReceived: Uint8Array[] = []; + constructor (public client_socket:number) { + } + + store (message:Uint8Array):number { + this.messagesToSend.push(message); + return message.byteLength; + } + + poll ():number { + return this.messagesReceived.length; + } + + recv (buffer:VoidPtr, bytes_to_read:number):number { + if (this.messagesReceived.length === 0) { + return 0; + } + const message = this.messagesReceived[0]!; + const bytes_read = Math.min(message.length, bytes_to_read); + Module.HEAPU8.set(message.subarray(0, bytes_read), buffer as any); + if (bytes_read === message.length) { + this.messagesReceived.shift(); + } else { + this.messagesReceived[0] = message.subarray(bytes_read); + } + return bytes_read; + } +} + +export interface IDiagConnection { + send (message: Uint8Array):number ; + poll ():number ; + recv (buffer:VoidPtr, bytes_to_read:number):number ; + close ():number ; +} + +// [hi,lo] +export type SessionId=[number, number]; + +export interface IDiagClient { + onAdvertise(server:IDiagServer):void; + onSessionStart(server:IDiagServer, session:IDiagSession):void; + onData(server:IDiagServer, session:IDiagSession, message:Uint8Array):void; + onError(server:IDiagServer, session:IDiagSession, message:Uint8Array):void; +} + +export interface IDiagServer { + createSession(message:Uint8Array):void; + sendCommand(message:Uint8Array):void; +} + +export interface IDiagSession { + session_id:SessionId; + store(message: Uint8Array): number; + respond(message: Uint8Array): void; + close():void; +} + +export type fnClientProvider = (scenarioName:string) => IDiagClient; diff --git a/src/mono/browser/runtime/diagnostics/diag-js.ts b/src/mono/browser/runtime/diagnostics/diag-js.ts new file mode 100644 index 00000000000000..71a73406ae85b3 --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/diag-js.ts @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import WasmEnablePerfTracing from "consts:wasmEnablePerfTracing"; + +import { advert1, dotnet_IPC_V1, ServerCommandId, CommandSetId } from "./client-commands"; +import { DiagConnectionBase, fnClientProvider, IDiagClient, IDiagConnection, IDiagServer, IDiagSession, schedule_diagnostic_server_loop, SessionId } from "./common"; +import { mono_log_info } from "../logging"; +import { GcDumpDiagClient } from "./dotnet-gcdump"; +import { CountersClient } from "./dotnet-counters"; +import { SampleProfilerClient } from "./dotnet-profiler"; + +let diagClient:IDiagClient|undefined = undefined as any; +let server:DiagServer = undefined as any; + +// configure your application +// .withEnvironmentVariable("DOTNET_DiagnosticPorts", "download:gcdump") +// or implement function globalThis.dotnetDiagnosticClient with IDiagClient interface +export function createDiagConnectionJs (socket_handle:number, scenarioName:string):DiagConnectionJs { + if (!WasmEnablePerfTracing) { + return undefined as any; + } + if (diagClient === undefined) { + diagClient = init_diag_client(scenarioName); + } + return new DiagConnectionJs(socket_handle); +} + +function init_diag_client (scenarioName:string):IDiagClient { + server = new DiagServer(); + if (scenarioName.startsWith("download:gcdump")) { + return new GcDumpDiagClient(); + } + if (scenarioName.startsWith("download:counters")) { + return new CountersClient(); + } + if (scenarioName.startsWith("download:samples")) { + return new SampleProfilerClient(); + } + const dotnetDiagnosticClient:fnClientProvider = (globalThis as any).dotnetDiagnosticClient; + if (typeof dotnetDiagnosticClient === "function" ) { + return dotnetDiagnosticClient(scenarioName); + } + throw new Error(`Unknown scenario: ${scenarioName}`); +} + + +// singleton wrapping the protocol with the diagnostic server in the Mono VM +// there could be multiple connection at the same time. Only the last which sent advert is receiving commands for all sessions +// DS:advert ->1 +// 1<- DC1: command to start tracing session +// DS:OK, session ID ->1 +// DS:advert ->2 +// DS:events ->1 +// DS:events ->1 +// DS:events ->1 +// DS:events ->1 +// 2<- DC1: command to stop tracing session +// DS:close ->1 +class DiagServer implements IDiagServer { + public commandConnection:DiagConnectionJs = undefined as any; + sendCommand (message: Uint8Array): void { + this.commandConnection.respond(message); + } + createSession (message: Uint8Array): void { + this.commandConnection.diagClient = diagClient; + this.commandConnection.respond(message); + } +} + +class DiagConnectionJs extends DiagConnectionBase implements IDiagConnection, IDiagSession { + public session_id: SessionId = undefined as any; + public diagClient?: IDiagClient; + public stopDelayedAfterLastMessage:number|undefined = undefined; + public resumedRuntime = false; + + constructor (public client_socket:number) { + super(client_socket); + } + + // this is message from the diagnostic server, which is Mono VM in this browser + send (message:Uint8Array):number { + schedule_diagnostic_server_loop(); + if (advert1.every((v, i) => v === message[i])) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + server.commandConnection = this; + diagClient?.onAdvertise(server); + } else if (dotnet_IPC_V1.every((v, i) => v === message[i]) && message[16] == CommandSetId.Server) { + if (message[17] == ServerCommandId.OK) { + if (message.byteLength === 28) { + const view = message.subarray(20, 28); + const sessionIDLo = view[0] | (view[1] << 8) | (view[2] << 16) | (view[3] << 24); + const sessionIDHi = view[4] | (view[5] << 8) | (view[6] << 16) | (view[7] << 24); + const sessionId = [sessionIDHi, sessionIDLo] as SessionId; + this.session_id = sessionId; + diagClient?.onSessionStart(server, this); + } + } else { + diagClient?.onError(server, this, message); + } + } else { + if (this.diagClient) + this.diagClient.onData(server, this, message); + else { + this.store(message); + } + } + + return message.length; + } + + // this is message to the diagnostic server, which is Mono VM in this browser + respond (message:Uint8Array) : void { + this.messagesReceived.push(message); + schedule_diagnostic_server_loop(); + } + + close (): number { + if (this.messagesToSend.length === 0) { + return 0; + } + const blob = new Blob(this.messagesToSend, { type: "application/octet-stream" }); + const blobUrl = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.download = "trace." + (new Date()).valueOf() + ".nettrace"; + mono_log_info(`Downloading trace ${link.download} - ${blob.size} bytes`); + link.href = blobUrl; + document.body.appendChild(link); + link.dispatchEvent(new MouseEvent("click", { + bubbles: true, cancelable: true, view: window + })); + return 0; + } +} diff --git a/src/mono/browser/runtime/diagnostics/diag-ws.ts b/src/mono/browser/runtime/diagnostics/diag-ws.ts new file mode 100644 index 00000000000000..5a03a6ce29ba1a --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/diag-ws.ts @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import WasmEnablePerfTracing from "consts:wasmEnablePerfTracing"; + +import { IDiagConnection, DiagConnectionBase, diagnostic_server_loop, schedule_diagnostic_server_loop } from "./common"; + +export function createDiagConnectionWs (socket_handle:number, url:string):IDiagConnection { + if (!WasmEnablePerfTracing) { + return undefined as any; + } + return new DiagConnectionWS(socket_handle, url); +} + +// this is used together with `dotnet-dsrouter` which will create IPC pipe on your local machine +// 1. run `dotnet-dsrouter server-websocket` this will print process ID and websocket URL +// 2. configure your wasm dotnet application `.withEnvironmentVariable("DOTNET_DiagnosticPorts", "ws://127.0.0.1:8088/diagnostics")` +// 3. run your wasm application +// 4. run `dotnet-gcdump -p ` or `dotnet-trace collect -p ` +class DiagConnectionWS extends DiagConnectionBase implements IDiagConnection { + private ws: WebSocket; + + constructor (client_socket:number, url:string) { + super(client_socket); + const ws = this.ws = new WebSocket(url); + const onMessage = async (evt:MessageEvent) => { + const buffer = await evt.data.arrayBuffer(); + const data = new Uint8Array(buffer); + this.messagesReceived.push(data); + diagnostic_server_loop(); + }; + ws.addEventListener("open", () => { + for (const data of this.messagesToSend) { + ws.send(data); + } + this.messagesToSend = []; + diagnostic_server_loop(); + }, { once: true }); + ws.addEventListener("message", onMessage); + ws.addEventListener("error", () => { + ws.removeEventListener("message", onMessage); + }, { once: true }); + } + + send (message:Uint8Array):number { + schedule_diagnostic_server_loop(); + // copy the message + if (this.ws!.readyState == WebSocket.CONNECTING) { + return super.store(message); + } + + this.ws!.send(message); + + return message.length; + } + + close ():number { + schedule_diagnostic_server_loop(); + this.ws.close(); + return 0; + } +} + diff --git a/src/mono/browser/runtime/diagnostics/dotnet-counters.ts b/src/mono/browser/runtime/diagnostics/dotnet-counters.ts new file mode 100644 index 00000000000000..78d6fc631712b4 --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/dotnet-counters.ts @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { commandStopTracing, commandCounters } from "./client-commands"; +import { IDiagClient, IDiagServer, IDiagSession } from "./common"; +import { mono_log_warn } from "../logging"; +import { Module } from "../globals"; + +export class CountersClient implements IDiagClient { + private firstAdvert = false; + private firstSession = false; + onAdvertise (server: IDiagServer): void { + if (!this.firstAdvert) { + this.firstAdvert = true; + server.createSession(commandCounters()); + } + } + onSessionStart (server: IDiagServer, session: IDiagSession): void { + if (!this.firstSession) { + this.firstSession = true; + // stop tracing after 20 seconds of monitoring + Module.safeSetTimeout(() => { + server.sendCommand(commandStopTracing(session.session_id)); + }, 20000); + } + } + onData (server: IDiagServer, session: IDiagSession, message: Uint8Array): void { + session.store(message); + } + onError (server: IDiagServer, session: IDiagSession, message: Uint8Array): void { + mono_log_warn("Diagnostic session " + session.session_id + " error : " + message.toString()); + } +} diff --git a/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts b/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts new file mode 100644 index 00000000000000..50e8f5eb0cb9c6 --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/dotnet-gcdump.ts @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { commandStopTracing, commandGcHeapDump, } from "./client-commands"; +import { IDiagClient, IDiagServer, IDiagSession } from "./common"; +import { mono_log_warn } from "../logging"; +import { Module } from "../globals"; + +export class GcDumpDiagClient implements IDiagClient { + private firstAdvert = false; + private firstSession = false; + private stopSent = false; + private stopDelayedAfterLastMessage:number = undefined as any; + onAdvertise (server: IDiagServer): void { + if (!this.firstAdvert) { + this.firstAdvert = true; + server.createSession(commandGcHeapDump()); + } + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onSessionStart (server: IDiagServer, session: IDiagSession): void { + if (!this.firstSession) { + this.firstSession = true; + } + } + onData (server: IDiagServer, session: IDiagSession, message: Uint8Array): void { + session.store(message); + if (this.firstAdvert && this.firstSession && !this.stopSent) { + if (this.stopDelayedAfterLastMessage) { + clearTimeout(this.stopDelayedAfterLastMessage); + } + this.stopDelayedAfterLastMessage = Module.safeSetTimeout(() => { + this.stopSent = true; + server.sendCommand(commandStopTracing(session.session_id)); + }, 500); + } + } + onError (server: IDiagServer, session: IDiagSession, message: Uint8Array): void { + mono_log_warn("Diagnostic session " + session.session_id + " error : " + message.toString()); + } +} diff --git a/src/mono/browser/runtime/diagnostics/dotnet-profiler.ts b/src/mono/browser/runtime/diagnostics/dotnet-profiler.ts new file mode 100644 index 00000000000000..17d3dc4e5bdcad --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/dotnet-profiler.ts @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { commandStopTracing, commandSampleProfiler } from "./client-commands"; +import { IDiagClient, IDiagServer, IDiagSession } from "./common"; +import { mono_log_warn } from "../logging"; +import { Module } from "../globals"; + +export class SampleProfilerClient implements IDiagClient { + private firstAdvert = false; + private firstSession = false; + onAdvertise (server: IDiagServer): void { + if (!this.firstAdvert) { + this.firstAdvert = true; + server.createSession(commandSampleProfiler()); + } + } + onSessionStart (server: IDiagServer, session: IDiagSession): void { + if (!this.firstSession) { + this.firstSession = true; + // stop tracing after 20 seconds of monitoring + Module.safeSetTimeout(() => { + server.sendCommand(commandStopTracing(session.session_id)); + }, 20000); + } + } + onData (server: IDiagServer, session: IDiagSession, message: Uint8Array): void { + session.store(message); + } + onError (server: IDiagServer, session: IDiagSession, message: Uint8Array): void { + mono_log_warn("Diagnostic session " + session.session_id + " error : " + message.toString()); + } +} diff --git a/src/mono/browser/runtime/diagnostics/index.ts b/src/mono/browser/runtime/diagnostics/index.ts new file mode 100644 index 00000000000000..bf771142ea753e --- /dev/null +++ b/src/mono/browser/runtime/diagnostics/index.ts @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import WasmEnablePerfTracing from "consts:wasmEnablePerfTracing"; + +import { Module } from "../globals"; +import { utf8ToString } from "../strings"; +import { CharPtr, VoidPtr } from "../types/emscripten"; +import { createDiagConnectionJs } from "./diag-js"; +import { IDiagConnection } from "./common"; +import { createDiagConnectionWs } from "./diag-ws"; + +let socket_handles:Map = undefined as any; +let next_socket_handle = 1; + +export function ds_rt_websocket_create (urlPtr :CharPtr):number { + if (!WasmEnablePerfTracing) { + return -1; + } + if (!socket_handles) { + socket_handles = new Map(); + } + const url = utf8ToString(urlPtr); + const socket_handle = next_socket_handle++; + const isWebSocket = url.startsWith("ws://") || url.startsWith("wss://"); + const wrapper = isWebSocket + ? createDiagConnectionWs(socket_handle, url) + : createDiagConnectionJs(socket_handle, url); + socket_handles.set(socket_handle, wrapper); + return socket_handle; +} + +export function ds_rt_websocket_send (client_socket :number, buffer:VoidPtr, bytes_to_write:number):number { + if (!WasmEnablePerfTracing) { + return -1; + } + const wrapper = socket_handles.get(client_socket); + if (!wrapper) { + return -1; + } + const message = (new Uint8Array(Module.HEAPU8.buffer, buffer as any, bytes_to_write)).slice(); + return wrapper.send(message); +} + +export function ds_rt_websocket_poll (client_socket :number):number { + if (!WasmEnablePerfTracing) { + return -1; + } + const wrapper = socket_handles.get(client_socket); + if (!wrapper) { + return 0; + } + return wrapper.poll(); +} + +export function ds_rt_websocket_recv (client_socket :number, buffer:VoidPtr, bytes_to_read:number):number { + if (!WasmEnablePerfTracing) { + return -1; + } + const wrapper = socket_handles.get(client_socket); + if (!wrapper) { + return -1; + } + return wrapper.recv(buffer, bytes_to_read); +} + +export function ds_rt_websocket_close (client_socket :number):number { + if (!WasmEnablePerfTracing) { + return -1; + } + const wrapper = socket_handles.get(client_socket); + if (!wrapper) { + return -1; + } + socket_handles.delete(client_socket); + return wrapper.close(); +} + diff --git a/src/mono/llvm/llvm-init.proj b/src/mono/llvm/llvm-init.proj index 6e1445dd817f23..db09086337c48a 100644 --- a/src/mono/llvm/llvm-init.proj +++ b/src/mono/llvm/llvm-init.proj @@ -13,7 +13,7 @@ $(runtimelinuxmuslx64MicrosoftNETCoreRuntimeMonoLLVMToolsVersion) $(runtimewinx64MicrosoftNETCoreRuntimeMonoLLVMToolsVersion) $(runtimeosxx64MicrosoftNETCoreRuntimeMonoLLVMToolsVersion) - .Debug + .Debug diff --git a/src/mono/sample/wasm/browser-eventpipe/Program.cs b/src/mono/sample/wasm/browser-eventpipe/Program.cs new file mode 100644 index 00000000000000..42cc8d00e41ba9 --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/Program.cs @@ -0,0 +1,137 @@ +// 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.Threading.Tasks; +using System.Runtime.InteropServices.JavaScript; +using System.Runtime.InteropServices; +using System.Diagnostics.Tracing; + +namespace Sample +{ + class ConsoleWriterEventListener : EventListener + { + public static ConsoleWriterEventListener Instance; + + protected override void OnEventSourceCreated(EventSource eventSource) + { + if(eventSource.Name == "WasmHello") + { + EnableEvents(eventSource, EventLevel.Informational); + } + if(eventSource.Name == "System.Runtime") + { + EnableEvents(eventSource, EventLevel.Informational); + } + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + Console.WriteLine(eventData.TimeStamp + " " + eventData.EventName); + } + } + + public partial class Test + { + public static int Main(string[] args) + { + DisplayMeaning(42); + + WasmHelloEventSource.Instance.NewCallsCounter(); + ConsoleWriterEventListener.Instance = new ConsoleWriterEventListener(); + + // SayHi(); + return 0; + } + + [JSImport("Sample.Test.displayMeaning", "main.js")] + internal static partial void DisplayMeaning(int meaning); + + public static int counter; + + [JSExport] + internal static void SayHi() + { + WasmHelloEventSource.Instance.StartHello(counter); + Console.WriteLine("Hi from C#!"); + for(int i = 0; i < 100000; i++) + { + WasmHelloEventSource.Instance.CountCall(); + } + counter++; + Console.WriteLine("Hello from C#!"); + WasmHelloEventSource.Instance.StopHello(counter, "counter"+counter); + } + + [JSExport] + internal static Task SayHiAsync() + { + WasmHelloEventSource.Instance.StartHello(counter); + Console.WriteLine("Hi from C#!"); + for(int i = 0; i < 100000; i++) + { + WasmHelloEventSource.Instance.CountCall(); + } + counter++; + Console.WriteLine("Hello from C#!"); + WasmHelloEventSource.Instance.StopHello(counter, "counter"+counter); + + return Task.CompletedTask; + } + } + + + + [EventSource(Name = "WasmHello")] + public class WasmHelloEventSource : EventSource + { + public static readonly WasmHelloEventSource Instance = new (); + + private IncrementingEventCounter _calls; + + private WasmHelloEventSource () + { + } + + [NonEvent] + public void NewCallsCounter() + { + _calls?.Dispose(); + _calls = new ("hello-calls", this) + { + DisplayName = "Hello calls", + }; + } + + [NonEvent] + public void CountCall() { + _calls?.Increment(1.0); + } + + protected override void Dispose (bool disposing) + { + _calls?.Dispose(); + _calls = null; + + base.Dispose(disposing); + } + + [Event(1, Message="Started Hello({0})", Level = EventLevel.Informational)] + public void StartHello(int n) + { + if (!IsEnabled()) + return; + + WriteEvent(1, n); + } + + [Event(2, Message="Stopped Hello({0}) = {1}", Level = EventLevel.Informational)] + public void StopHello(int n, string result) + { + if (!IsEnabled()) + return; + + WriteEvent(2, n, result); + } + } +} diff --git a/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj new file mode 100644 index 00000000000000..ee2c79b5f101dc --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj @@ -0,0 +1,14 @@ + + + + true + true + false + false + true + true + + + + + diff --git a/src/mono/sample/wasm/browser-eventpipe/index.html b/src/mono/sample/wasm/browser-eventpipe/index.html new file mode 100644 index 00000000000000..0c99eac49653aa --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/index.html @@ -0,0 +1,25 @@ + + + + + + + Sample EventPipe profile session + + + + + + + +
+
+
+ +
+
+
+ Answer to the Ultimate Question is : + + + diff --git a/src/mono/sample/wasm/browser-eventpipe/main.js b/src/mono/sample/wasm/browser-eventpipe/main.js new file mode 100644 index 00000000000000..f14f56bcd3a76a --- /dev/null +++ b/src/mono/sample/wasm/browser-eventpipe/main.js @@ -0,0 +1,59 @@ +// 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 displayMeaning(meaning) { + document.getElementById("out").innerHTML = `${meaning}`; +} + +try { + const { setModuleImports, runMain, getAssemblyExports, getConfig } = await dotnet + //.withEnvironmentVariable("MONO_DIAGNOSTICS", "--diagnostic-mono-profiler=enable")// --diagnostic-ports=mock:../mock.js,suspend + //.withEnvironmentVariable("DOTNET_DiagnosticPorts", "ws://127.0.0.1:8088/diagnostics,suspend") + //.withEnvironmentVariable("DOTNET_DiagnosticPorts", "ws://127.0.0.1:8088/diagnostics") + // dotnet-trace collect --providers Microsoft-Windows-DotNETRuntime:0x1980001:5 -p 41732 + // dotnet-gcdump collect -p 41732 + // dotnet-counters + //.withEnvironmentVariable("DOTNET_DiagnosticPorts", "download:gcdump") + //.withEnvironmentVariable("DOTNET_DiagnosticPorts", "download:counters") + .withEnvironmentVariable("DOTNET_DiagnosticPorts", "download:samples") + //.withEnvironmentVariable("MONO_LOG_LEVEL", "debug") + //.withEnvironmentVariable("MONO_LOG_MASK", "all") + .withElementOnExit() + .withExitOnUnhandledError() + .create(); + + setModuleImports("main.js", { + Sample: { + Test: { + displayMeaning + } + } + }); + const config = getConfig(); + const exports = await getAssemblyExports(config.mainAssemblyName); + const sayHi = exports.Sample.Test.SayHi; + const sayHiAsync = exports.Sample.Test.SayHiAsync; + + document.querySelector("#hello-button").addEventListener("click", () => { + try { + sayHi(); + } catch (exc) { + alert(exc); + } + }); + + sayHi(); + sayHiAsync(); + + await runMain(); + + setInterval(async () => { + sayHi(); + await sayHiAsync(); + }, 0); +} +catch (err) { + exit(2, err); +}