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..91f9db34a83a7f
--- /dev/null
+++ b/src/mono/browser/runtime/diagnostics/client-commands.ts
@@ -0,0 +1,447 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import type { DiagnosticCommandOptions } from "../types";
+
+import { SessionId } from "./common";
+import { runtimeHelpers } from "./globals";
+
+// ADVR_V1\0
+export const advert1 = [65, 68, 86, 82, 95, 86, 49, 0,];
+// DOTNET_IPC_V1\0
+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 advertise () {
+    // xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx
+    const uuid = new Uint8Array(16);
+    globalThis.crypto.getRandomValues(uuid);
+    uuid[7] = (uuid[7] & 0xf) | 0x40;// version 4
+
+    const pid = runtimeHelpers.mono_wasm_process_current_pid();
+
+    return Uint8Array.from([
+        ...advert1,
+        ...uuid,
+        ...serializeUint64([0, pid]),
+        0, 0// future
+    ]);
+}
+
+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 (options:DiagnosticCommandOptions) {
+    return commandCollectTracing2({
+        circularBufferMB:options.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
+            },
+            ...options.extraProviders || [],
+        ]
+    });
+}
+
+function uuidv4 () {
+    return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
+        (+c ^ globalThis.crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
+    );
+}
+
+export function commandCounters (options:DiagnosticCommandOptions) {
+    return commandCollectTracing2({
+        circularBufferMB:options.circularBufferMB || 256,
+        format: 1,
+        requestRundown: false,
+        providers: [
+            {
+                keywords: [0, Keywords.GCHandle],
+                logLevel: 4,
+                provider_name: "System.Diagnostics.Metrics",
+                arguments: `SessionId=SHARED;Metrics=System.Runtime;RefreshInterval=${options.intervalSeconds || 1};MaxTimeSeries=1000;MaxHistograms=10;ClientId=${uuidv4()};`,
+            },
+            ...options.extraProviders || [],
+        ]
+    });
+}
+
+export function commandSampleProfiler (options:DiagnosticCommandOptions) {
+    return commandCollectTracing2({
+        circularBufferMB:options.circularBufferMB || 256,
+        format: 1,
+        requestRundown: true,
+        providers: [
+            {
+                keywords: [
+                    0x0000_0000,
+                    0x0000_0000,
+                ],
+                logLevel: 4,
+                provider_name: "Microsoft-DotNETCore-SampleProfiler",
+                arguments: null
+            },
+            ...options.extraProviders || [],
+        ]
+    });
+}
+
+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,
+    CollectTracing3 = 4,
+    CollectTracing4 = 5,
+}
+
+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: [ number, 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..dab4823b6b78ac
--- /dev/null
+++ b/src/mono/browser/runtime/diagnostics/common.ts
@@ -0,0 +1,103 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import type { VoidPtr } from "../types/emscripten";
+import type { PromiseController } from "../types/internal";
+
+import { runtimeHelpers } from "./globals";
+import { loaderHelpers, Module } from "./globals";
+import { mono_log_info } from "./logging";
+
+let lastScheduledTimeoutId: any = undefined;
+
+// run another cycle of the event loop, which is EP threads on MT runtime
+export function diagnosticServerEventLoop () {
+    lastScheduledTimeoutId = undefined;
+    if (loaderHelpers.is_runtime_running()) {
+        try {
+            runtimeHelpers.mono_background_exec();// give GC chance to run
+            runtimeHelpers.mono_wasm_ds_exec();
+            scheduleDiagnosticServerEventLoop(100);
+        } catch (ex) {
+            loaderHelpers.mono_exit(1, ex);
+        }
+    }
+}
+
+export function scheduleDiagnosticServerEventLoop (delay = 0):void {
+    if (!lastScheduledTimeoutId || delay === 0) {
+        lastScheduledTimeoutId = Module.safeSetTimeout(diagnosticServerEventLoop, delay);
+    }
+}
+
+export class DiagnosticConnectionBase {
+    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 IDiagnosticConnection {
+    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 IDiagnosticSession {
+    session_id:SessionId;
+    store(message: Uint8Array): number;
+    sendCommand(message:Uint8Array):void;
+}
+
+export interface IDiagnosticClient {
+    skipDownload?:boolean;
+    onClosePromise:PromiseController<Uint8Array[]>;
+    commandOnAdvertise():Uint8Array;
+    onSessionStart?(session:IDiagnosticSession):void;
+    onData?(session:IDiagnosticSession, message:Uint8Array):void;
+    onClose?(messages:Uint8Array[]):void;
+    onError?(session:IDiagnosticSession, message:Uint8Array):void;
+}
+
+export type fnClientProvider = (scenarioName:string) => IDiagnosticClient;
+
+export function downloadBlob (messages:Uint8Array[]) {
+    const blob = new Blob(messages, { 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
+    }));
+}
diff --git a/src/mono/browser/runtime/diagnostics/diagnostics-js.ts b/src/mono/browser/runtime/diagnostics/diagnostics-js.ts
new file mode 100644
index 00000000000000..8fd885ba674b8c
--- /dev/null
+++ b/src/mono/browser/runtime/diagnostics/diagnostics-js.ts
@@ -0,0 +1,161 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import { advert1, CommandSetId, dotnet_IPC_V1, ServerCommandId } from "./client-commands";
+import { DiagnosticConnectionBase, downloadBlob, fnClientProvider, IDiagnosticClient, IDiagnosticConnection, IDiagnosticSession, scheduleDiagnosticServerEventLoop, SessionId } from "./common";
+import { PromiseAndController } from "../types/internal";
+import { loaderHelpers } from "./globals";
+import { mono_log_warn } from "./logging";
+import { collectCpuSamples } from "./dotnet-cpu-profiler";
+import { collectPerfCounters } from "./dotnet-counters";
+import { collectGcDump } from "./dotnet-gcdump";
+
+//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
+
+let nextJsClient:PromiseAndController<IDiagnosticClient>;
+let fromScenarioNameOnce = false;
+
+// Only the last which sent advert is receiving commands for all sessions
+export let serverSession:DiagnosticSession|undefined = undefined;
+
+// singleton wrapping the protocol with the diagnostic server in the Mono VM
+// there could be multiple connection at the same time.
+// 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 DiagnosticSession extends DiagnosticConnectionBase implements IDiagnosticConnection, IDiagnosticSession {
+    public session_id: SessionId = undefined as any;
+    public diagClient?: IDiagnosticClient;
+    public stopDelayedAfterLastMessage:number|undefined = undefined;
+    public resumedRuntime = false;
+
+    constructor (public client_socket:number) {
+        super(client_socket);
+    }
+
+    sendCommand (message: Uint8Array): void {
+        if (!serverSession) {
+            mono_log_warn("no server yet");
+            return;
+        }
+        serverSession.respond(message);
+    }
+
+    async connectNewClient () {
+        this.diagClient = await nextJsClient.promise;
+        cleanupClient();
+        const firstCommand = this.diagClient.commandOnAdvertise();
+        this.respond(firstCommand);
+    }
+
+    is_advert_message (message:Uint8Array):boolean {
+        return advert1.every((v, i) => v === message[i]);
+    }
+
+    is_response_message (message:Uint8Array):boolean {
+        return dotnet_IPC_V1.every((v, i) => v === message[i]) && message[16] == CommandSetId.Server;
+    }
+
+    is_response_ok_with_session (message:Uint8Array):boolean {
+        return message.byteLength === 28 && message[17] == ServerCommandId.OK;
+    }
+
+    parse_session_id (message:Uint8Array):SessionId {
+        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);
+        return [sessionIDHi, sessionIDLo] as SessionId;
+    }
+
+    // this is message from the diagnostic server, which is Mono VM in this browser
+    send (message:Uint8Array):number {
+        scheduleDiagnosticServerEventLoop();
+        if (this.is_advert_message(message)) {
+            // eslint-disable-next-line @typescript-eslint/no-this-alias
+            serverSession = this;
+            this.connectNewClient();
+        } else if (this.is_response_message(message)) {
+            if (this.is_response_ok_with_session(message)) {
+                this.session_id = this.parse_session_id(message);
+                if (this.diagClient?.onSessionStart) {
+                    this.diagClient.onSessionStart(this);
+                }
+            } else {
+                if (this.diagClient?.onError) {
+                    this.diagClient.onError(this, message);
+                } else {
+                    mono_log_warn("Diagnostic session " + this.session_id + " error : " + message.toString());
+                }
+            }
+        } else {
+            if (this.diagClient?.onData)
+                this.diagClient.onData(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);
+        scheduleDiagnosticServerEventLoop();
+    }
+
+    close (): number {
+        if (this.diagClient?.onClose) {
+            this.diagClient.onClose(this.messagesToSend);
+        }
+        if (this.messagesToSend.length === 0) {
+            return 0;
+        }
+        if (this.diagClient && !this.diagClient.skipDownload) {
+            downloadBlob(this.messagesToSend);
+        }
+        this.messagesToSend = [];
+        return 0;
+    }
+}
+
+export function cleanupClient () {
+    nextJsClient = loaderHelpers.createPromiseController<IDiagnosticClient>();
+}
+
+export function setupJsClient (client:IDiagnosticClient) {
+    nextJsClient.promise_control.resolve(client);
+}
+
+export function createDiagConnectionJs (socket_handle:number, scenarioName:string):DiagnosticSession {
+    if (!fromScenarioNameOnce) {
+        fromScenarioNameOnce = true;
+        if (scenarioName.startsWith("js://gcdump")) {
+            collectGcDump({});
+        }
+        if (scenarioName.startsWith("js://counters")) {
+            collectPerfCounters({});
+        }
+        if (scenarioName.startsWith("js://cpu-samples")) {
+            collectCpuSamples({});
+        }
+        const dotnetDiagnosticClient:fnClientProvider = (globalThis as any).dotnetDiagnosticClient;
+        if (typeof dotnetDiagnosticClient === "function" ) {
+            nextJsClient.promise_control.resolve(dotnetDiagnosticClient(scenarioName));
+        }
+    }
+    return new DiagnosticSession(socket_handle);
+}
diff --git a/src/mono/browser/runtime/diagnostics/diagnostics-ws.ts b/src/mono/browser/runtime/diagnostics/diagnostics-ws.ts
new file mode 100644
index 00000000000000..a5a18143c18c7b
--- /dev/null
+++ b/src/mono/browser/runtime/diagnostics/diagnostics-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 { IDiagnosticConnection, DiagnosticConnectionBase, diagnosticServerEventLoop, scheduleDiagnosticServerEventLoop } from "./common";
+import { mono_log_warn } from "./logging";
+
+export function createDiagConnectionWs (socket_handle:number, url:string):IDiagnosticConnection {
+    return new DiagnosticConnectionWS(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 <process ID>` or `dotnet-trace collect -p <process ID>`
+class DiagnosticConnectionWS extends DiagnosticConnectionBase implements IDiagnosticConnection {
+    private ws: WebSocket;
+
+    constructor (client_socket:number, url:string) {
+        super(client_socket);
+        const ws = this.ws = new WebSocket(url);
+        const onMessage = async (evt:MessageEvent<Blob>) => {
+            const buffer = await evt.data.arrayBuffer();
+            const message = new Uint8Array(buffer);
+            this.messagesReceived.push(message);
+            diagnosticServerEventLoop();
+        };
+        ws.addEventListener("open", () => {
+            for (const data of this.messagesToSend) {
+                ws.send(data);
+            }
+            this.messagesToSend = [];
+            diagnosticServerEventLoop();
+        }, { once: true });
+        ws.addEventListener("message", onMessage);
+        ws.addEventListener("error", () => {
+            mono_log_warn("Diagnostic server WebSocket connection was closed unexpectedly.");
+            ws.removeEventListener("message", onMessage);
+        }, { once: true });
+    }
+
+    send (message:Uint8Array):number {
+        scheduleDiagnosticServerEventLoop();
+        // copy the message
+        if (this.ws!.readyState == WebSocket.CLOSED) {
+            return -1;
+        }
+        if (this.ws!.readyState == WebSocket.CONNECTING) {
+            return super.store(message);
+        }
+
+        this.ws!.send(message);
+
+        return message.length;
+    }
+
+    close ():number {
+        scheduleDiagnosticServerEventLoop();
+        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..35ad332394c32b
--- /dev/null
+++ b/src/mono/browser/runtime/diagnostics/dotnet-counters.ts
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import type { DiagnosticCommandOptions } from "../types";
+
+import { commandStopTracing, commandCounters } from "./client-commands";
+import { IDiagnosticSession } from "./common";
+import { Module } from "./globals";
+import { serverSession, setupJsClient } from "./diagnostics-js";
+import { loaderHelpers } from "./globals";
+
+export function collectPerfCounters (options?:DiagnosticCommandOptions):Promise<Uint8Array[]> {
+    if (!options) options = {};
+    if (!serverSession) {
+        throw new Error("No active JS diagnostic session");
+    }
+
+    const onClosePromise = loaderHelpers.createPromiseController<Uint8Array[]>();
+    function onSessionStart (session: IDiagnosticSession): void {
+        // stop tracing after period of monitoring
+        Module.safeSetTimeout(() => {
+            session.sendCommand(commandStopTracing(session.session_id));
+        }, 1000 * (options?.durationSeconds ?? 60));
+    }
+    setupJsClient({
+        onClosePromise:onClosePromise.promise_control,
+        skipDownload:options.skipDownload,
+        commandOnAdvertise:() => commandCounters(options),
+        onSessionStart,
+    });
+    return onClosePromise.promise;
+}
diff --git a/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts b/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts
new file mode 100644
index 00000000000000..4dc34cb8d21484
--- /dev/null
+++ b/src/mono/browser/runtime/diagnostics/dotnet-cpu-profiler.ts
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import type { DiagnosticCommandOptions } from "../types";
+
+import { commandStopTracing, commandSampleProfiler } from "./client-commands";
+import { loaderHelpers, Module, runtimeHelpers } from "./globals";
+import { serverSession, setupJsClient } from "./diagnostics-js";
+import { IDiagnosticSession } from "./common";
+
+export function collectCpuSamples (options?:DiagnosticCommandOptions):Promise<Uint8Array[]> {
+    if (!options) options = {};
+    if (!serverSession) {
+        throw new Error("No active JS diagnostic session");
+    }
+    if (!runtimeHelpers.config.environmentVariables!["DOTNET_WasmPerfInstrumentation"]) {
+        throw new Error("method instrumentation is not enabled, please enable it with WasmPerfInstrumentation MSBuild property");
+    }
+
+    const onClosePromise = loaderHelpers.createPromiseController<Uint8Array[]>();
+    function onSessionStart (session: IDiagnosticSession): void {
+        // stop tracing after period of monitoring
+        Module.safeSetTimeout(() => {
+            session.sendCommand(commandStopTracing(session.session_id));
+        }, 1000 * (options?.durationSeconds ?? 60));
+    }
+
+    setupJsClient({
+        onClosePromise:onClosePromise.promise_control,
+        skipDownload:options.skipDownload,
+        commandOnAdvertise: () => commandSampleProfiler(options),
+        onSessionStart,
+    });
+    return onClosePromise.promise;
+}
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..d0d503db4d5ceb
--- /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 type { DiagnosticCommandOptions } from "../types";
+
+import { commandStopTracing, commandGcHeapDump, } from "./client-commands";
+import { IDiagnosticSession } from "./common";
+import { loaderHelpers, Module } from "./globals";
+import { serverSession, setupJsClient } from "./diagnostics-js";
+
+export function collectGcDump (options?:DiagnosticCommandOptions):Promise<Uint8Array[]> {
+    if (!options) options = {};
+    if (!serverSession) {
+        throw new Error("No active JS diagnostic session");
+    }
+
+    const onClosePromise = loaderHelpers.createPromiseController<Uint8Array[]>();
+    let stopDelayedAfterLastMessage = 0;
+    let stopSent = false;
+    function onData (session: IDiagnosticSession, message: Uint8Array): void {
+        session.store(message);
+        if (!stopSent) {
+            // stop 500ms after last GC message on this session, there will be more messages after that
+            if (stopDelayedAfterLastMessage) {
+                clearTimeout(stopDelayedAfterLastMessage);
+            }
+            stopDelayedAfterLastMessage = Module.safeSetTimeout(() => {
+                stopSent = true;
+                session.sendCommand(commandStopTracing(session.session_id));
+            }, 1000 * (options?.durationSeconds ?? 1));
+        }
+    }
+
+    setupJsClient({
+        onClosePromise: onClosePromise.promise_control,
+        skipDownload: options.skipDownload,
+        commandOnAdvertise: () => commandGcHeapDump(options),
+        onData,
+    });
+    return onClosePromise.promise;
+}
diff --git a/src/mono/browser/runtime/diagnostics/index.ts b/src/mono/browser/runtime/diagnostics/index.ts
index fc554e31287b5d..f7ddb450681387 100644
--- a/src/mono/browser/runtime/diagnostics/index.ts
+++ b/src/mono/browser/runtime/diagnostics/index.ts
@@ -4,34 +4,90 @@
 import type { GlobalObjects } from "../types/internal";
 import type { CharPtr, VoidPtr } from "../types/emscripten";
 
+import { Module, runtimeHelpers } from "./globals";
+import { cleanupClient as cleanup_js_client, createDiagConnectionJs, serverSession } from "./diagnostics-js";
+import { IDiagnosticConnection } from "./common";
+import { createDiagConnectionWs } from "./diagnostics-ws";
 import { diagnosticHelpers, setRuntimeGlobalsImpl } from "./globals";
+import { collectCpuSamples } from "./dotnet-cpu-profiler";
+import { collectPerfCounters } from "./dotnet-counters";
+import { collectGcDump } from "./dotnet-gcdump";
+import { advertise } from "./client-commands";
+
+let socket_handles:Map<number, IDiagnosticConnection> = undefined as any;
+let next_socket_handle = 1;
+let url_override:string | undefined = undefined;
 
-/* eslint-disable @typescript-eslint/no-unused-vars */
 export function setRuntimeGlobals (globalObjects: GlobalObjects): void {
     setRuntimeGlobalsImpl(globalObjects);
 
     diagnosticHelpers.ds_rt_websocket_create = (urlPtr :CharPtr):number => {
-        // Not implemented yet
-        return 1;
+        if (!socket_handles) {
+            socket_handles = new Map<number, IDiagnosticConnection>();
+        }
+        const url = url_override ?? runtimeHelpers.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;
     };
 
     diagnosticHelpers.ds_rt_websocket_send = (client_socket :number, buffer:VoidPtr, bytes_to_write:number):number => {
-        // Not implemented yet
-        return bytes_to_write;
+        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);
     };
 
     diagnosticHelpers.ds_rt_websocket_poll = (client_socket :number):number => {
-        // Not implemented yet
-        return 0;
+        const wrapper = socket_handles.get(client_socket);
+        if (!wrapper) {
+            return 0;
+        }
+        return wrapper.poll();
     };
 
     diagnosticHelpers.ds_rt_websocket_recv = (client_socket :number, buffer:VoidPtr, bytes_to_read:number):number => {
-        // Not implemented yet
-        return 0;
+        const wrapper = socket_handles.get(client_socket);
+        if (!wrapper) {
+            return -1;
+        }
+        return wrapper.recv(buffer, bytes_to_read);
     };
 
     diagnosticHelpers.ds_rt_websocket_close = (client_socket :number):number => {
-        // Not implemented yet
-        return 0;
+        const wrapper = socket_handles.get(client_socket);
+        if (!wrapper) {
+            return -1;
+        }
+        socket_handles.delete(client_socket);
+        return wrapper.close();
     };
+
+    globalObjects.api.collectCpuSamples = collectCpuSamples;
+    globalObjects.api.collectPerfCounters = collectPerfCounters;
+    globalObjects.api.collectGcDump = collectGcDump;
+    globalObjects.api.connectDSRouter = connectDSRouter;
+
+    cleanup_js_client();
+}
+
+// this will take over the existing connection to JS and send new advert message to WS client
+// use dotnet-dsrouter server-websocket -v trace
+function connectDSRouter (url: string): void {
+    if (!serverSession) {
+        throw new Error("No active session to reconnect");
+    }
+
+    // make sure new sessions hit the new URL
+    url_override = url;
+
+    const wrapper = createDiagConnectionWs(serverSession.client_socket, url);
+    socket_handles.set(serverSession.client_socket, wrapper);
+    wrapper.send(advertise());
 }
diff --git a/src/mono/browser/runtime/dotnet.d.ts b/src/mono/browser/runtime/dotnet.d.ts
index 0cd2872d719113..85cc8a6b7106e7 100644
--- a/src/mono/browser/runtime/dotnet.d.ts
+++ b/src/mono/browser/runtime/dotnet.d.ts
@@ -439,7 +439,7 @@ type DotnetModuleConfig = {
     imports?: any;
     exports?: string[];
 } & Partial<EmscriptenModule>;
-type APIType = {
+type RunAPIType = {
     /**
      * Runs the Main() method of the application.
      * Note: this will keep the .NET runtime alive and the APIs will be available for further calls.
@@ -489,6 +489,8 @@ type APIType = {
      * You can register the scripts using MonoConfig.resources.modulesAfterConfigLoaded and MonoConfig.resources.modulesAfterRuntimeReady.
      */
     invokeLibraryInitializers: (functionName: string, args: any[]) => Promise<void>;
+};
+type MemoryAPIType = {
     /**
      * Writes to the WASM linear memory
      */
@@ -630,6 +632,43 @@ type APIType = {
      */
     localHeapViewF64: () => Float64Array;
 };
+type DiagnosticsAPIType = {
+    /**
+     * creates diagnostic trace file. Default is 60 seconds.
+     * It could be opened in PerfView or Visual Studio as is.
+     */
+    collectCpuSamples: (options?: DiagnosticCommandOptions) => Promise<Uint8Array[]>;
+    /**
+     * creates diagnostic trace file. Default is 60 seconds.
+     * It could be opened in PerfView or Visual Studio as is.
+     * It could be summarized by `dotnet-trace report xxx.nettrace topN -n 10`
+     */
+    collectPerfCounters: (options?: DiagnosticCommandOptions) => Promise<Uint8Array[]>;
+    /**
+     * creates diagnostic trace file.
+     * It could be opened in PerfView as is.
+     * It could be converted for Visual Studio using `dotnet-gcdump convert`.
+     */
+    collectGcDump: (options?: DiagnosticCommandOptions) => Promise<Uint8Array[]>;
+    /**
+     * changes DOTNET_DiagnosticPorts and makes a new connection to WebSocket on that URL.
+     */
+    connectDSRouter(url: string): void;
+};
+type DiagnosticCommandProviderV2 = {
+    keywords: [number, number];
+    logLevel: number;
+    provider_name: string;
+    arguments: string | null;
+};
+type DiagnosticCommandOptions = {
+    durationSeconds?: number;
+    intervalSeconds?: number;
+    skipDownload?: boolean;
+    circularBufferMB?: number;
+    extraProviders?: DiagnosticCommandProviderV2[];
+};
+type APIType = RunAPIType & MemoryAPIType & DiagnosticsAPIType;
 type RuntimeAPI = {
     INTERNAL: any;
     Module: EmscriptenModule;
diff --git a/src/mono/browser/runtime/driver.c b/src/mono/browser/runtime/driver.c
index 6d62fd2a66d396..5a49b7753aa583 100644
--- a/src/mono/browser/runtime/driver.c
+++ b/src/mono/browser/runtime/driver.c
@@ -42,6 +42,7 @@ void bindings_initialize_internals ();
 char *monoeg_g_getenv(const char *variable);
 int monoeg_g_setenv(const char *variable, const char *value, int overwrite);
 char *mono_method_get_full_name (MonoMethod *method);
+char *mono_method_full_name (MonoMethod *method, int32_t signature);
 
 #ifndef INVARIANT_TIMEZONE
 extern void mono_register_timezones_bundle (void);
diff --git a/src/mono/browser/runtime/export-api.ts b/src/mono/browser/runtime/export-api.ts
index 3c8e6312c59062..83ddc05bca8d0c 100644
--- a/src/mono/browser/runtime/export-api.ts
+++ b/src/mono/browser/runtime/export-api.ts
@@ -57,6 +57,10 @@ export function export_api (): any {
         localHeapViewI64Big: localHeapViewI64Big,
         localHeapViewF32: localHeapViewF32,
         localHeapViewF64: localHeapViewF64,
+        collectCpuSamples:null as any,
+        collectPerfCounters:null as any,
+        collectGcDump:null as any,
+        connectDSRouter:null as any,
     };
     return api;
 }
diff --git a/src/mono/browser/runtime/exports-binding.ts b/src/mono/browser/runtime/exports-binding.ts
index e0e7faa06d57ea..fbed0f8a2d3fc2 100644
--- a/src/mono/browser/runtime/exports-binding.ts
+++ b/src/mono/browser/runtime/exports-binding.ts
@@ -11,7 +11,7 @@ import { mono_interp_jit_wasm_entry_trampoline, mono_interp_record_interp_entry
 import { mono_interp_jit_wasm_jit_call_trampoline, mono_interp_invoke_wasm_jit_call_trampoline, mono_interp_flush_jitcall_queue } from "./jiterpreter-jit-call";
 import { mono_wasm_resolve_or_reject_promise } from "./marshal-to-js";
 import { mono_wasm_schedule_timer, schedule_background_exec } from "./scheduling";
-import { mono_wasm_asm_loaded } from "./startup";
+import { mono_wasm_asm_loaded, mono_wasm_process_current_pid } from "./startup";
 import { mono_log_warn, mono_wasm_console_clear, mono_wasm_trace_logger } from "./logging";
 import { mono_wasm_browser_entropy } from "./crypto";
 import { mono_wasm_cancel_promise } from "./cancelable-promise";
@@ -83,6 +83,9 @@ export const mono_wasm_imports = [
     // src/native/minipal/random.c
     mono_wasm_browser_entropy,
 
+    // mono-proclib.c
+    mono_wasm_process_current_pid,
+
     // corebindings.c
     mono_wasm_console_clear,
     mono_wasm_release_cs_owned_object,
diff --git a/src/mono/browser/runtime/exports.ts b/src/mono/browser/runtime/exports.ts
index 3dc0117767161b..af519bc4b908aa 100644
--- a/src/mono/browser/runtime/exports.ts
+++ b/src/mono/browser/runtime/exports.ts
@@ -11,7 +11,7 @@ import { type RuntimeAPI } from "./types";
 
 import { Module, exportedRuntimeAPI, loaderHelpers, passEmscriptenInternals, runtimeHelpers, setRuntimeGlobals, } from "./globals";
 import { GlobalObjects, RuntimeHelpers } from "./types/internal";
-import { configureEmscriptenStartup, configureRuntimeStartup, configureWorkerStartup } from "./startup";
+import { configureEmscriptenStartup, configureRuntimeStartup, configureWorkerStartup, mono_wasm_process_current_pid } from "./startup";
 
 import { create_weak_ref } from "./weak-ref";
 import { export_internal } from "./exports-internal";
@@ -42,6 +42,7 @@ function initializeExports (globalObjects: GlobalObjects): RuntimeAPI {
         jiterpreter_dump_stats,
         forceDisposeProxies,
         utf8ToString,
+        mono_wasm_process_current_pid,
         mono_background_exec: () => tcwraps.mono_background_exec(),
         mono_wasm_ds_exec: () => tcwraps.mono_wasm_ds_exec(),
     };
diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts
index 3af2484b8893d0..60ab49bc0f6611 100644
--- a/src/mono/browser/runtime/startup.ts
+++ b/src/mono/browser/runtime/startup.ts
@@ -34,6 +34,12 @@ import { runtimeList } from "./exports";
 import { nativeAbort, nativeExit } from "./run";
 import { replaceEmscriptenPThreadInit } from "./pthreads/worker-thread";
 
+const pid = (globalThis.performance?.timeOrigin ?? Date.now()) | 0;
+
+export function mono_wasm_process_current_pid ():number {
+    return pid;
+}
+
 export async function configureRuntimeStartup (module: DotnetModuleInternal): Promise<void> {
     if (!module.out) {
         // eslint-disable-next-line no-console
@@ -540,6 +546,7 @@ export async function start_runtime () {
 
         if (runtimeHelpers.emscriptenBuildOptions.enablePerfTracing) {
             const diagnosticPorts = "DOTNET_DiagnosticPorts";
+            // connect JS client by default
             const jsReady = "js://ready";
             if (!environmentVariables[diagnosticPorts]) {
                 environmentVariables[diagnosticPorts] = jsReady;
diff --git a/src/mono/browser/runtime/types/index.ts b/src/mono/browser/runtime/types/index.ts
index e2653cc0d54580..b651b6828e210f 100644
--- a/src/mono/browser/runtime/types/index.ts
+++ b/src/mono/browser/runtime/types/index.ts
@@ -403,7 +403,7 @@ export type DotnetModuleConfig = {
     exports?: string[];
 } & Partial<EmscriptenModule>
 
-export type APIType = {
+export type RunAPIType = {
     /**
      * Runs the Main() method of the application.
      * Note: this will keep the .NET runtime alive and the APIs will be available for further calls.
@@ -453,6 +453,9 @@ export type APIType = {
      * You can register the scripts using MonoConfig.resources.modulesAfterConfigLoaded and MonoConfig.resources.modulesAfterRuntimeReady.
      */
     invokeLibraryInitializers: (functionName: string, args: any[]) => Promise<void>;
+}
+
+export type MemoryAPIType = {
     /**
      * Writes to the WASM linear memory
      */
@@ -595,6 +598,47 @@ export type APIType = {
     localHeapViewF64: () => Float64Array;
 }
 
+export type DiagnosticsAPIType = {
+    /**
+     * creates diagnostic trace file. Default is 60 seconds.
+     * It could be opened in PerfView or Visual Studio as is.
+     */
+    collectCpuSamples: (options?:DiagnosticCommandOptions) => Promise<Uint8Array[]>;
+    /**
+     * creates diagnostic trace file. Default is 60 seconds.
+     * It could be opened in PerfView or Visual Studio as is.
+     * It could be summarized by `dotnet-trace report xxx.nettrace topN -n 10`
+     */
+    collectPerfCounters: (options?:DiagnosticCommandOptions) => Promise<Uint8Array[]>;
+    /**
+     * creates diagnostic trace file.
+     * It could be opened in PerfView as is.
+     * It could be converted for Visual Studio using `dotnet-gcdump convert`.
+     */
+    collectGcDump: (options?:DiagnosticCommandOptions) => Promise<Uint8Array[]>;
+    /**
+     * changes DOTNET_DiagnosticPorts and makes a new connection to WebSocket on that URL.
+     */
+    connectDSRouter (url: string): void;
+}
+
+export type DiagnosticCommandProviderV2 = {
+    keywords: [ number, number ],
+    logLevel: number,
+    provider_name: string,
+    arguments: string|null
+}
+
+export type DiagnosticCommandOptions = {
+    durationSeconds?:number,
+    intervalSeconds?:number,
+    skipDownload?:boolean,
+    circularBufferMB?:number,
+    extraProviders?:DiagnosticCommandProviderV2[],
+}
+
+export type APIType = RunAPIType & MemoryAPIType & DiagnosticsAPIType;
+
 export type RuntimeAPI = {
     INTERNAL: any,
     Module: EmscriptenModule,
diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts
index 9b7512a11625e0..5e132209563ee8 100644
--- a/src/mono/browser/runtime/types/internal.ts
+++ b/src/mono/browser/runtime/types/internal.ts
@@ -244,6 +244,7 @@ export type RuntimeHelpers = {
     utf8ToString: (ptr: CharPtr) => string,
     mono_background_exec: () => void,
     mono_wasm_ds_exec: () => void,
+    mono_wasm_process_current_pid: () => number,
 }
 
 export type DiagnosticHelpers = {
diff --git a/src/mono/mono/utils/mono-proclib.c b/src/mono/mono/utils/mono-proclib.c
index 2627c5aa059ff1..0daec1915691c0 100644
--- a/src/mono/mono/utils/mono-proclib.c
+++ b/src/mono/mono/utils/mono-proclib.c
@@ -29,11 +29,17 @@
 #endif
 #endif
 
+#if defined(HOST_BROWSER)
+int mono_wasm_process_current_pid ();
+#endif
+
 int
 mono_process_current_pid (void)
 {
 #ifdef HOST_WIN32
 	return (int) GetCurrentProcessId ();
+#elif defined(HOST_BROWSER)
+	return mono_wasm_process_current_pid ();
 #elif defined(HAVE_GETPID)
 	return (int) getpid ();
 #elif defined(HOST_WASI)
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..9e6fed1bb7a1f1
--- /dev/null
+++ b/src/mono/sample/wasm/browser-eventpipe/Program.cs
@@ -0,0 +1,161 @@
+// 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.Reflection;
+using System.Threading.Tasks;
+using System.Runtime.InteropServices.JavaScript;
+using System.Runtime.InteropServices;
+using System.Diagnostics.Tracing;
+using System.Runtime.CompilerServices;
+
+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]
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        internal static void SayHi()
+        {
+            Console.WriteLine("Hi from C#! " + EventSource.CurrentThreadActivityId);
+            WasmHelloEventSource.Instance.HelloStart(counter);
+            Console.WriteLine("Wave from C#! " + EventSource.CurrentThreadActivityId);
+            for(int i = 0; i < 100000; i++)
+            {
+                WasmHelloEventSource.Instance.CountCall();
+            }
+            counter++;
+            SayHiCatch();
+            WasmHelloEventSource.Instance.HelloStop(counter, "counter"+counter);
+            Console.WriteLine("Bye from C#! " + EventSource.CurrentThreadActivityId);
+        }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        internal static void SayHiThrow()
+        {
+            throw new Exception("Hello from C#!");
+        }
+
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        internal static void SayHiCatch()
+        {
+            try
+            {
+                SayHiThrow();
+            }
+            catch (Exception e)
+            {
+                Console.WriteLine("Caught exception: " + e.Message);
+            }
+        }
+
+        [JSExport]
+        internal static Task SayHiAsync()
+        {
+            WasmHelloEventSource.Instance.HelloStart(counter);
+            Console.WriteLine("Hi from C#!");
+            for(int i = 0; i < 100000; i++)
+            {
+                WasmHelloEventSource.Instance.CountCall();
+            }
+            counter++;
+            Console.WriteLine("Hello from C#!");
+            WasmHelloEventSource.Instance.HelloStop(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 HelloStart(int n)
+        {
+            if (!IsEnabled())
+                return;
+
+            WriteEvent(1, n);
+        }
+
+        [Event(2, Message="Stopped Hello({0}) = {1}", Level = EventLevel.Informational)]
+        public void HelloStop(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..96e3a8058f6b8b
--- /dev/null
+++ b/src/mono/sample/wasm/browser-eventpipe/Wasm.Browser.EventPipe.Sample.csproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <Import Project="..\DefaultBrowserSample.targets" />
+  <PropertyGroup>
+    <WasmPerfTracing>true</WasmPerfTracing>
+    <WasmPerfInstrumentation>N:Sample</WasmPerfInstrumentation>
+    <MetricsSupport>true</MetricsSupport>
+    <EventSourceSupport>true</EventSourceSupport>
+    <!--
+    <RunAOTCompilation>true</RunAOTCompilation>
+    <WasmNativeDebugSymbols>true</WasmNativeDebugSymbols>
+    <WasmNativeStrip>false</WasmNativeStrip>
+    -->
+
+  </PropertyGroup>
+  <ItemGroup>
+    <WasmExtraFilesToDeploy Include="main.js" />
+  </ItemGroup>
+</Project>
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 @@
+<!DOCTYPE html>
+<!--  Licensed to the .NET Foundation under one or more agreements. -->
+<!-- The .NET Foundation licenses this file to you under the MIT license. -->
+<html>
+
+<head>
+  <title>Sample EventPipe profile session</title>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <script type='module' src="./main.js"></script>
+</head>
+
+<body>
+  <h3 id="header">Wasm Browser EventPipe profiling Sample</h3>
+  <div>
+    <br/>
+    <br/>
+    <button id="hello-button">Say Hi</button>
+    <br/>
+    <br/>
+  </div>
+  Answer to the Ultimate Question is : <span id="out"></span>
+</body>
+
+</html>
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..e6737c285f1c70
--- /dev/null
+++ b/src/mono/sample/wasm/browser-eventpipe/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 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("DOTNET_DiagnosticPorts", "js://cpu-samples")
+        //.withEnvironmentVariable("MONO_LOG_LEVEL", "debug")
+        //.withEnvironmentVariable("MONO_LOG_MASK", "all")
+        /*.withEnvironmentVariable("MONO_VERBOSE_METHOD", "System.Threading.Monitor:Exit")
+        .withRuntimeOptions([
+            "--no-jiterpreter-traces-enabled",
+            "--no-jiterpreter-interp-entry-enabled",
+            "--no-jiterpreter-jit-call-enabled",
+        ])*/
+        .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);
+        }
+    });
+
+    await runMain();
+
+    sayHi();
+    //sayHiAsync();
+
+    /*setInterval(async () => {
+        sayHi();
+        await sayHiAsync();
+    }, 1000);*/
+}
+catch (err) {
+    exit(2, err);
+}
diff --git a/src/mono/wasm/features.md b/src/mono/wasm/features.md
index a628d5f4b2b981..42de422d79fe26 100644
--- a/src/mono/wasm/features.md
+++ b/src/mono/wasm/features.md
@@ -403,6 +403,26 @@ See also log mask [categories](https://github.com/dotnet/runtime/blob/88633ae045
 
 `Timing-Allow-Origin` HTTP header allows for more precise time measurements.
 
+Then you can trigger collection of a trace from browser dev tools
+
+```js
+globalThis.getDotnetRuntime(0).collectGcDump()
+```
+
+The .nettrace file could be converted for VS via `dotnet-gcdump convert` or opened in `PerfView.exe` as is.
+```js
+globalThis.getDotnetRuntime(0).collectPerfCounters({durationSeconds: 60})
+```
+
+The counters could be opened in VS, `PerfView.exe` tools or via `dotnet-trace report xxx.nettrace topN -n 10`
+
+```js
+globalThis.getDotnetRuntime(0).collectCpuSamples({durationSeconds: 60})
+```
+
+The counters could be opened in VS or in `PerfView.exe`
+
+
 ### Profiling in the browser dev tools
 
 You can enable integration with the profiler in browser dev tools via following elements in your .csproj
