Skip to content
bizzehdee edited this page Jun 24, 2026 · 3 revisions

Profiler

DScript includes two profilers:

Profiler Output format What it measures
CpuProfiler .cpuprofile Time spent in each function
MemoryProfiler .heapprofile Bytes allocated in each function

Both output files that open directly in Chrome DevTools or VS Code's built-in profiling views.

CPU quick start

using DScript;
using DScript.Profiler;

var profiler = new CpuProfiler();
var engine   = new ScriptEngine();
engine.AttachProfiler(profiler);

engine.Run(ScriptEngine.Compile(@"
    function fib(n) { return n <= 1 ? n : fib(n-1) + fib(n-2); }
    fib(30);
"));

File.WriteAllText("output.cpuprofile", profiler.GetProfile());

Open the file in VS Code: Ctrl+Shift+P → Developer: Open CPU Profile → select output.cpuprofile.

You get flame chart, bottom-up, and call-tree views with no additional tooling required.

Memory quick start

using DScript;
using DScript.Profiler;

var profiler = new MemoryProfiler();
var engine   = new ScriptEngine();
profiler.AttachTo(engine);      // wires both call-stack tracking and allocation hook

engine.Run(ScriptEngine.Compile(@"
    function buildArray(n) {
        var a = [];
        for (var i = 0; i < n; i++) a.push({ value: i });
        return a;
    }
    buildArray(1000);
"));

profiler.DetachFrom(engine);    // stops counting; call before GetProfile()

File.WriteAllText("output.heapprofile", profiler.GetProfile());

Open in VS Code: Ctrl+Shift+P → Developer: Open CPU Profile → select output.heapprofile (the same command accepts heap profiles). The Memory panel shows a flame chart where node width is proportional to bytes allocated directly in each function.

IProfiler interface

namespace DScript.Profiler;

public interface IProfiler
{
    void Enter(string functionName, string url, int lineNumber, int columnNumber);
    void Leave();
}

Enter is called synchronously before each function body executes; Leave is called after it returns (including on exception). Every Enter is paired with exactly one Leave.

Implement this interface directly if you need custom profiling output (e.g. writing to a database, emitting OpenTelemetry spans, or feeding a real-time dashboard).

CpuProfiler

CpuProfiler implements IProfiler and serialises the collected data as V8 CPU profile JSON, the format used by Chrome DevTools, VS Code, and most Node.js profiling tools.

How it works

CpuProfiler is an instrumented profiler, not a sampling profiler. It hooks actual function enter/exit events rather than taking periodic thread snapshots. It:

  1. Builds a call tree where each unique (parent, functionName, url, line, col) path is one node.
  2. Measures wall-clock self-time per node (total time minus time spent in callees).
  3. Synthesises a stream of virtual samples spaced SampleIntervalMicros apart, proportional to each node's self-time, so that Chrome's flame-chart widths are correct.

Recursive functions accumulate on the same tree node — you see total self-time across all recursive calls, not one node per invocation.

Properties

Property Type Default Description
SampleIntervalMicros int 10 Spacing between synthesised samples in microseconds. Lower values produce finer-grained output at the cost of a larger JSON file.

Methods

Method Description
GetProfile() Finalise timing and return the V8 .cpuprofile JSON string. Safe to call multiple times — subsequent calls return the same snapshot taken at the moment of the first call.

MemoryProfiler

MemoryProfiler implements IProfiler and serialises allocation data as V8 heap sampling profile JSON (.heapprofile), the format consumed by Chrome DevTools' Memory panel and VS Code's JavaScript profiler.

How it works

MemoryProfiler intercepts every ScriptVar created through the factory via ScriptVar.SetAllocationHook. For each allocation it:

  1. Determines the current JS call stack from the IProfiler Enter/Leave callbacks.
  2. Estimates the allocation size in bytes based on the ScriptVar type.
  3. Adds the size to the call-tree node for the currently executing function.
  4. Records an individual sample entry (up to MaxSamples).

Size estimates are proportionally correct but do not reflect actual CLR managed-heap sizes:

ScriptVar type Estimated size
undefined, null 24 bytes
number (integer) 32 bytes
number (double) 40 bytes
string 64 bytes (fixed; string body is a separate heap object)
object, array, function 80 bytes
symbol, bigint, regexp, proxy 48 bytes

Attach / detach methods

Method Description
AttachTo(ScriptEngine engine) Attaches call-stack instrumentation to engine and sets the allocation hook. Call before engine.Run().
DetachFrom(ScriptEngine engine) Clears the allocation hook and detaches from engine. Call after engine.Run().
Attach() Sets the allocation hook only (no call-stack tracking). Use for standalone counting without per-function attribution.
Detach() Clears the allocation hook set by Attach().

Note: ScriptVar.SetAllocationHook is a static global. Only one MemoryProfiler (or other consumer) may be active at a time; calling AttachTo overwrites any previously registered hook.

Properties

Property Type Default Description
MaxSamples int 100 000 Maximum number of individual allocation sample entries in the output. Per-node byte totals still accumulate beyond this cap; only the flat sample list is capped.

GetProfile() output

The JSON has three top-level keys:

Key Description
head Recursive call tree. Each node has a callFrame, a selfSize (bytes allocated directly in that function), an id, and a children array.
samples Flat list of individual allocation events, each with size, nodeId, and ordinal.
_summary DScript extension (ignored by V8 tooling). Contains totalAllocations, totalBytes, samplesRecorded, and a byType breakdown.

The file is idempotent — subsequent calls to GetProfile() return the same snapshot taken at the first call.

Static helpers

string label = MemoryProfiler.TypeNameOf(scriptVar);  // e.g. "object", "number(int)", …
long   bytes = MemoryProfiler.EstimateSize(scriptVar); // estimated bytes for this var

ScriptEngine API

engine.AttachProfiler(IProfiler profiler);
engine.DetachProfiler();

AttachProfiler takes effect on the next Run call. DetachProfiler removes the profiler; any subsequent Run executes at full speed and does not modify the already-collected profile.

Viewing profiles

VS Code

  1. Save the .cpuprofile file anywhere on disk.
  2. Ctrl+Shift+P → Developer: Open CPU Profile (or drag the file into the editor).
  3. The Performance panel opens with flame chart, bottom-up table, and call tree.

Chrome DevTools

  1. Open DevTools → Performance tab.
  2. Click the menu → Load profile… → select the .cpuprofile file.

What is and isn't profiled

Call type Profiled
Synchronous script functions
Native C# callbacks (with a name child)
Native C# callbacks (without a name child) ✅ — appear as (native)
Generator function bodies (per .next() resume) ❌ — the iterator-factory call is captured; body execution time is attributed to the microtask infrastructure
Async function bodies ❌ — same reason; await resumes are not individually tracked

Custom IProfiler example — OpenTelemetry spans

using DScript.Profiler;
using OpenTelemetry.Trace;

class OtelProfiler : IProfiler
{
    private readonly Tracer _tracer;
    private readonly Stack<TelemetrySpan> _spans = new();

    public OtelProfiler(Tracer tracer) => _tracer = tracer;

    public void Enter(string functionName, string url, int line, int col)
    {
        var span = _tracer.StartActiveSpan(functionName);
        span.SetAttribute("url",  url ?? "");
        span.SetAttribute("line", line);
        _spans.Push(span);
    }

    public void Leave()
    {
        if (_spans.TryPop(out var span))
            span.End();
    }
}

Combining the profiler with the debugger

The profiler and debugger can be attached simultaneously — they use independent hooks and do not interfere:

engine.AttachDebugger(myDebugger, DebugAction.Continue);
engine.AttachProfiler(myProfiler);
engine.Run(program);
engine.DetachDebugger();
engine.DetachProfiler();

Note that the debugger's single-step pauses inflate wall-clock times considerably. Attach the profiler without the debugger for representative performance measurements.

Clone this wiki locally