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

Profiler

DScript includes an instrumented CPU profiler. Attach a CpuProfiler to a ScriptEngine, run your script, then call GetProfile() to get a .cpuprofile JSON file that loads directly into Chrome DevTools or VS Code's built-in performance viewer.

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.

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.

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