-
-
Notifications
You must be signed in to change notification settings - Fork 5
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.
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.
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.
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 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. |
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.
MemoryProfiler intercepts every ScriptVar created through the factory via ScriptVar.SetAllocationHook. For each allocation it:
- Determines the current JS call stack from the
IProfilerEnter/Leave callbacks. - Estimates the allocation size in bytes based on the
ScriptVartype. - Adds the size to the call-tree node for the currently executing function.
- 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 |
| 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.SetAllocationHookis a static global. Only oneMemoryProfiler(or other consumer) may be active at a time; callingAttachTooverwrites any previously registered hook.
| 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. |
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.
string label = MemoryProfiler.TypeNameOf(scriptVar); // e.g. "object", "number(int)", …
long bytes = MemoryProfiler.EstimateSize(scriptVar); // estimated bytes for this varengine.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 Leave()
{
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.