# `nupp.profile`
Two profiling channels: one for where the time went, one for whether it went
there compiled.
```nupp
local profile = nupp.profile
local session = profile.sample({intervalMs = 5})
render()
local report = session:stop("profile.out")
print(report.samples, report.stacks)
```
`profile.sample` is the statistical sampler. A timer interrupts the program and
writes down where it was; what comes back is collapsed-stack text, the format
speedscope.app, FlameGraph.pl and inferno all read.
`profile.trace` watches the JIT give up. It aggregates trace aborts, so
blacklisted hot code, a bytecode the compiler will not record, and traces that
grew past a limit all become rows rather than silence. That is the question a
sampler cannot answer and the one that usually matters here: code the JIT
refused is an order of magnitude slower than code it took, and nothing says so
out loud.
Both channels attribute work through `nupp.profile.zone`, so a sample or an
abort carries the zone path that was open when it happened. Both are
process-wide rather than per-coroutine, and at most one session of each kind
runs at a time.
Neither is free. A sample session pays a timer interrupt, a stack walk and a
table write at every interval; a trace session pays a callback at every abort,
inside the compiler. Stop a session once the question it was opened for has an
answer.
See [Profiling](../../../learn/performance/profiling/index.html) for reading a report, and
[LuaJIT trace checking](../../../learn/performance/jit-trace-checking/index.html) for finding the same aborts without running
the program.
## Submodules
| Module | Description |
| --- | --- |
| [`nupp.profile.trace`](#nupp.profile.trace) | Stable identities shared by static trace checking and the opt-in runtime collector. |
| `nupp.profile.zone` | Gated LuaJIT profiler zones: the stack work is skipped until a profiler asks for it. |
## Types
### `AbortSite` _record_
```nupp
record profile.AbortSite
severity: profile.Severity
count: integer
reason: string
reasonId: string
reasonClass: 'blocker' | 'risk' | 'stop'
rawReason: string
location: string
zonePath: string
end
```
One place the JIT gave up, and how often it did.
A row of a `TraceReport`, built at `stop`.
#### Fields
##### `severity`
```nupp
severity: profile.Severity
```
How much it is worth reading.
##### `count`
```nupp
count: integer
```
Times this exact severity, reason, location and zone fired.
##### `reason`
```nupp
reason: string
```
The reason, from `jit.vmdef.traceerr`. An unrecordable bytecode is rendered with
the opcode's name.
##### `reasonId`
```nupp
reasonId: string
```
Stable compiler identity and classification for the raw VM reason above.
##### `reasonClass`
```nupp
reasonClass: 'blocker' | 'risk' | 'stop'
```
What the identity is worth acting on: a `blocker` cannot record at all, a
`risk` may or may not sit on a hot path, and a `stop` is trace formation
working as designed.
##### `rawReason`
```nupp
rawReason: string
```
The VM's own text, before the registry mapped it.
##### `location`
```nupp
location: string
```
":" of the function being recorded when it aborted.
##### `zonePath`
```nupp
zonePath: string
```
The zone path that was open, "" when none was.
### `Sample` _record_
```nupp
record profile.Sample
zonePath: string
stack: string
count: integer
compiled: integer
interpreted: integer
cCode: integer
collecting: integer
compiling: integer
end
```
One distinct stack, and what landed on it.
A row of a `SampleReport`, built at `stop`. The five state counts sum to `count`.
#### Fields
##### `zonePath`
```nupp
zonePath: string
```
The zone path the samples were taken under, "" when none was open.
##### `stack`
```nupp
stack: string
```
The stack as `dumpstack` rendered it, ";" between frames, outermost first.
##### `count`
```nupp
count: integer
```
Samples on this stack, in every VM state.
##### `compiled`
```nupp
compiled: integer
```
Samples running compiled machine code.
##### `interpreted`
```nupp
interpreted: integer
```
Samples in the interpreter.
##### `cCode`
```nupp
cCode: integer
```
Samples inside a C function.
##### `collecting`
```nupp
collecting: integer
```
Samples in the garbage collector.
##### `compiling`
```nupp
compiling: integer
```
Samples inside the JIT compiler itself.
### `SampleOptions` _type_
```nupp
type profile.SampleOptions = {
--- Milliseconds between samples; 10 by default, which is 100 a second. Below about
--- 10 the timer starts taking real time away from the thread it is measuring, so
--- lower it for a short window and read the result knowing that it was paid for.
intervalMs: integer?,
--- How many frames to walk per sample; 16 by default. The walk is linear in this
--- and it happens on the interrupted thread, so raise it only when a specific
--- question needs the depth.
stackDepth: integer?,
--- Keep only the samples taken under a zone path starting with this, so
--- "frame/render" reads as that subtree alone.
---
--- Applied at `stop` rather than while sampling: narrowing it costs nothing at
--- runtime, and widening it afterwards is not possible, because the prefix is fixed
--- when the session starts.
zone: string?,
--- The module the program starts at, as a stack frame names it. Everything below
--- the outermost frame from it is dropped.
---
--- A profiler samples the whole stack it is embedded in, and what is under the
--- program, the loader that read it and the pcall that guards it, is not the
--- program. Naming its module cuts the report back to it.
---
--- Frames read ":", so the module is the part to give. A stack with
--- no frame from it is kept whole rather than emptied, and two stacks that differ
--- only below the root become one, their counts summed.
root: string?
}
```
What `profile.sample` collects, and how much of it. Every field is optional.
### `SampleReport` _record_
```nupp
record profile.SampleReport
intervalMs: integer
samples: integer
stacks: integer
text: string
metamethod __tostring: function(self): string
end
```
What a sample session saw, as `SampleSession:stop` returns it.
`tostring` on it is the collapsed-stack text, so it prints and pipes directly.
#### Methods
##### `__tostring`
```nupp
__tostring: function(self): string
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `self` | |
###### Returns
| Type | Description |
| --- | --- |
| `string` | |
#### Fields
##### `intervalMs`
```nupp
intervalMs: integer
```
The interval the session ran at, so the sample counts can be read as time.
##### `samples`
```nupp
samples: integer
```
Samples recorded, after the zone filter.
##### `stacks`
```nupp
stacks: integer
```
Distinct stacks they fell on.
##### `text`
```nupp
text: string
```
One line per stack: semicolon-separated frames, a space, then the sample count.
Ordered by count descending. Empty when nothing was sampled, which a short
session and a zone prefix that matched nothing both produce.
### `SampleSession` _record_
```nupp
record profile.SampleSession is profile.Session
associated type Report = profile.SampleReport
intervalMs: integer
zoneFilter: string?
root: string?
paused: boolean
stopped: boolean
aggregate: {[string]: {[string]: profile.Sample}}
pause: function(self)
resume: function(self)
stop: function(self, filename: string?): self.Report
end
```
A running sampler, as `profile.sample` returns it.
Live until `stop`, and there is at most one at a time. Dropping the handle without
stopping leaves the timer running for the rest of the process.
#### Methods
##### `pause`
```nupp
pause: function(self)
```
Stops recording without ending the session. The timer keeps firing, at the cost of
one test per sample, and what was recorded on either side of the pause is kept,
which is how a benchmark leaves its setup out.
Idempotent.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `self` | |
###### Raises
- once the session has stopped
##### `resume`
```nupp
resume: function(self)
```
Resumes recording. Idempotent.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `self` | |
###### Raises
- once the session has stopped
##### `stop`
```nupp
stop: function(self, filename: string?): self.Report
```
Ends the session and reports what it saw.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `self` | |
| `filename` | `string?` | |
###### Returns
| Type | Description |
| --- | --- |
| `self.Report` | |
###### Raises
- once the session has stopped, so it cannot be called twice
#### Fields
##### `Report`
```nupp
Report: associatedDecl
```
##### `intervalMs`
```nupp
intervalMs: integer
```
The interval it was started at.
##### `zoneFilter`
```nupp
zoneFilter: string?
```
The zone prefix `stop` will filter by, or nil for all of them.
##### `root`
```nupp
root: string?
```
The module `stop` will cut the stacks back to, or nil to keep them whole.
##### `paused`
```nupp
paused: boolean
```
Whether recording is suspended. The timer keeps firing.
##### `stopped`
```nupp
stopped: boolean
```
Whether `stop` has run.
##### `aggregate`
```nupp
aggregate: {[string]: {[string]: profile.Sample}}
```
Samples so far, by zone path and then by stack. Two levels rather than one
joined key, so a sample taken in a zone that has not changed since the last one
costs a single table lookup.
### `Session` _interface_
```nupp
interface profile.Session
associated type Report
pause: function(self)
resume: function(self)
stop: function(self, filename: string?): self.Report
end
```
A running profiling session whose declaration chooses the report `stop` returns.
Sampling and trace-abort sessions share this lifecycle. Generic helpers can use
`S.Report` to preserve the concrete report chosen by a session declaration.
#### Methods
##### `pause`
```nupp
pause: function(self)
```
Stops recording without ending the session. Idempotent.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `self` | |
##### `resume`
```nupp
resume: function(self)
```
Resumes recording. Idempotent.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `self` | |
##### `stop`
```nupp
stop: function(self, filename: string?): self.Report
```
Ends the session and answers what it saw, optionally writing it to a file.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `self` | |
| `filename` | `string?` | |
###### Returns
| Type | Description |
| --- | --- |
| `self.Report` | |
#### Fields
##### `Report`
```nupp
Report: associatedDecl
```
What `stop` answers, chosen by the session declaration rather than here.
### `Severity` _type_
```nupp
type profile.Severity = "blacklist" | "warn" | "info"
```
How much an abort is worth reading.
`blacklist` is always actionable: the code is demoted to the interpreter for the
rest of the process. `warn` is a refusal that may or may not sit on a hot path.
`info` is trace formation working as designed.
### `TraceOptions` _type_
```nupp
type profile.TraceOptions = {
--- Include the aborts that are ordinary trace formation rather than a refusal:
--- leaving a loop, recursion, an inner loop. False by default; turn it on when the
--- question is why a particular trace never formed.
includeBenign: boolean?
}
```
What `profile.trace` counts. Every field is optional.
### `TraceProfile` _record_
```nupp
record profile.TraceProfile
id: string
luajitRevision: string
luajitVersion: integer
architecture: string
operatingSystem: string
enabledRecorderFeatures: {string}
bytecodeSchema: string
supported: boolean
end
```
The exact recorder configuration used to normalize a trace report.
Two reports are comparable when their `id` matches. A report taken under an
unsupported recorder still reads, but its reasons were mapped from a VM the
registry was not written against.
#### Fields
##### `id`
```nupp
id: string
```
Every field below joined, which is what two reports are compared on.
##### `luajitRevision`
```nupp
luajitRevision: string
```
The pinned LuaJIT commit, or "external:" for any other build.
##### `luajitVersion`
```nupp
luajitVersion: integer
```
The snapshot timestamp from the full version string.
##### `architecture`
```nupp
architecture: string
```
`jit.arch`.
##### `operatingSystem`
```nupp
operatingSystem: string
```
`jit.os`.
##### `enabledRecorderFeatures`
```nupp
enabledRecorderFeatures: {string}
```
The recorder flags `jit.status` reported, sorted.
##### `bytecodeSchema`
```nupp
bytecodeSchema: string
```
A digest of the VM's bytecode name table, so a renumbered opcode set is
visible rather than silently remapped.
##### `supported`
```nupp
supported: boolean
```
Whether this is the recorder the reason registry was written against.
### `TraceReport` _record_
```nupp
record profile.TraceReport
durationSec: integer
totalAborts: integer
blacklisted: integer
sites: {profile.AbortSite}
traceProfile: profile.TraceProfile
reasonCatalogId: string
reasonCatalogVersion: integer
metamethod __tostring: function(self): string
end
```
What a trace session saw, as `TraceSession:stop` returns it.
`tostring` on it renders `sites` as RFC 4180 CSV, so it prints, sorts and diffs
directly.
#### Methods
##### `__tostring`
```nupp
__tostring: function(self): string
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `self` | |
###### Returns
| Type | Description |
| --- | --- |
| `string` | |
#### Fields
##### `durationSec`
```nupp
durationSec: integer
```
Wallclock seconds the session was active, in whole seconds: it comes from
`os.time`, so a session shorter than one reads as zero and dividing by it is the
caller's problem.
##### `totalAborts`
```nupp
totalAborts: integer
```
Abort events recorded. Excludes the benign ones unless `includeBenign` was set.
##### `blacklisted`
```nupp
blacklisted: integer
```
Blacklist events among them. Always actionable.
##### `sites`
```nupp
sites: {profile.AbortSite}
```
One row per distinct severity, reason, location and zone. Ordered by severity,
then by count descending.
##### `traceProfile`
```nupp
traceProfile: profile.TraceProfile
```
The recorder and registry identities under which the events were interpreted.
##### `reasonCatalogId`
```nupp
reasonCatalogId: string
```
The reason registry the identities came from.
##### `reasonCatalogVersion`
```nupp
reasonCatalogVersion: integer
```
Its version, so a stored report can be reread under a later one.
### `TraceSession` _record_
```nupp
record profile.TraceSession is profile.Session
associated type Report = profile.TraceReport
includeBenign: boolean
startedAt: integer
paused: boolean
stopped: boolean
sites: {[string]: profile.AbortSite}
totalAborts: integer
blacklisted: integer
traceProfile: profile.TraceProfile
callback: function(...: any)
pause: function(self)
resume: function(self)
stop: function(self, filename: string?): self.Report
end
```
A running trace-abort collector, as `profile.trace` returns it.
Live until `stop`, and there is at most one at a time. Dropping the handle without
stopping leaves the event hook attached for the rest of the process.
#### Methods
##### `callback`
```nupp
callback: function(...: any)
```
The handler to hand back to `jit.attach` to detach it. Dropping the last
reference to a handler does not remove it.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `...` | `any` | |
##### `pause`
```nupp
pause: function(self)
```
Stops counting without ending the session. The hook stays attached, at the cost of
one test per abort, and what was counted on either side of the pause is kept.
Idempotent.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `self` | |
###### Raises
- once the session has stopped
##### `resume`
```nupp
resume: function(self)
```
Resumes counting. Idempotent.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `self` | |
###### Raises
- once the session has stopped
##### `stop`
```nupp
stop: function(self, filename: string?): self.Report
```
Ends the session and reports what it saw.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `?` | `self` | |
| `filename` | `string?` | |
###### Returns
| Type | Description |
| --- | --- |
| `self.Report` | |
###### Raises
- once the session has stopped, so it cannot be called twice
#### Fields
##### `Report`
```nupp
Report: associatedDecl
```
##### `includeBenign`
```nupp
includeBenign: boolean
```
Whether the benign trace-formation events are being counted.
##### `startedAt`
```nupp
startedAt: integer
```
`os.time` when the session started.
##### `paused`
```nupp
paused: boolean
```
Whether aggregation is suspended. The hook stays attached.
##### `stopped`
```nupp
stopped: boolean
```
Whether `stop` has run.
##### `sites`
```nupp
sites: {[string]: profile.AbortSite}
```
Aborts so far, by severity, reason, location and zone joined.
##### `totalAborts`
```nupp
totalAborts: integer
```
Aborts counted so far.
##### `blacklisted`
```nupp
blacklisted: integer
```
Blacklist events among them.
##### `traceProfile`
```nupp
traceProfile: profile.TraceProfile
```
## Functions
### `profile.sample` _function_
```nupp
function profile.sample(options: profile.SampleOptions?): profile.SampleSession
```
Starts sampling.
```nupp
local session = profile.sample({intervalMs = 5, zone = "frame/render"})
render()
local report = session:stop("render.collapsed")
print(report.samples, report.stacks)
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `options` | `profile.SampleOptions?` | omitted samples every zone at 10 ms to a depth of 16 |
#### Returns
| Type | Description |
| --- | --- |
| `profile.SampleSession` | the handle whose `stop` produces the report |
#### Raises
- when a sample session is already running
### `profile.trace` _function_
```nupp
function profile.trace(options: profile.TraceOptions?): profile.TraceSession
```
Starts collecting trace aborts.
```nupp
local session = profile.trace()
render()
local report = session:stop("aborts.csv")
print(report.blacklisted, report.totalAborts)
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `options` | `profile.TraceOptions?` | omitted leaves the benign trace-formation events out |
#### Returns
| Type | Description |
| --- | --- |
| `profile.TraceSession` | the handle whose `stop` produces the report |
#### Raises
- when a trace session is already running