-
-
Notifications
You must be signed in to change notification settings - Fork 5
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.
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.
namespace DScript.Profiler;
public interface IProfiler
{
void Enter(string functionName, string url, int lineNumber, int columnNumber);
void Exit();
}Enter is called synchronously before each function body executes; Exit is called after it returns (including on exception). Every Enter is paired with exactly one Exit.
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 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.
CpuProfiler is an instrumented profiler, not a sampling profiler. It hooks actual function enter/exit events rather than taking periodic thread snapshots. It:
- Builds a call tree where each unique
(parent, functionName, url, line, col)path is one node. - Measures wall-clock self-time per node (total time minus time spent in callees).
- Synthesises a stream of virtual samples spaced
SampleIntervalMicrosapart, 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.
| 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. |
| 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. |
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.
- Save the
.cpuprofilefile anywhere on disk. - Ctrl+Shift+P → Developer: Open CPU Profile (or drag the file into the editor).
- The Performance panel opens with flame chart, bottom-up table, and call tree.
- Open DevTools → Performance tab.
- Click the ⋮ menu → Load profile… → select the
.cpuprofilefile.
| 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 |
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 Exit()
{
if (_spans.TryPop(out var span))
span.End();
}
}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.