nupp.profile
Two profiling channels: one for where the time went, one for whether it went there compiled.
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 for reading a report, and LuaJIT trace checking for finding the same aborts without running the program.
Submodules
| Module | Description |
|---|---|
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. |
Module contents
Types
| Type | Kind | Description |
|---|---|---|
AbortSite | record | One place the JIT gave up, and how often it did. |
Sample | record | One distinct stack, and what landed on it. |
SampleOptions | type | What profile.sample collects, and how much of it. |
SampleReport | record | What a sample session saw, as SampleSession:stop returns it. |
SampleSession | record | A running sampler, as profile.sample returns it. |
Session | interface | A running profiling session whose declaration chooses the report stop returns. |
Severity | type | How much an abort is worth reading. |
TraceOptions | type | What profile.trace counts. |
TraceProfile | record | The exact recorder configuration used to normalize a trace report. |
TraceReport | record | What a trace session saw, as TraceSession:stop returns it. |
TraceSession | record | A running trace-abort collector, as profile.trace returns it. |
Functions
| Function | Kind | Description |
|---|---|---|
sample | function | Starts sampling. |
trace | function | Starts collecting trace aborts. |
Types#
AbortSiterecord#
record profile.AbortSite
severity: profile.Severity
count: integer
reason: string
reasonId: string
reasonClass: 'blocker' | 'risk' | 'stop'
rawReason: string
location: string
zonePath: string
endOne place the JIT gave up, and how often it did.
A row of a TraceReport, built at stop.
Fields
reason#
reason: stringThe reason, from jit.vmdef.traceerr. An unrecordable bytecode is rendered with the opcode's name.
reasonClass#
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.
Samplerecord#
record profile.Sample
zonePath: string
stack: string
count: integer
compiled: integer
interpreted: integer
cCode: integer
collecting: integer
compiling: integer
endOne distinct stack, and what landed on it.
A row of a SampleReport, built at stop. The five state counts sum to count.
Fields
SampleOptionstype#
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 "<module>:<name>", 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.
SampleReportrecord#
record profile.SampleReport
intervalMs: integer
samples: integer
stacks: integer
text: string
__tostring: function(self): string
endWhat 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#
__tostring: function(self): stringArguments
| Name | Type | Description |
|---|---|---|
? | self |
Returns
| Type | Description |
|---|---|
string |
Fields
intervalMs#
intervalMs: integerThe interval the session ran at, so the sample counts can be read as time.
text#
text: stringOne 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.
SampleSessionrecord#
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
endA 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#
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#
resume: function(self)Resumes recording. Idempotent.
Arguments
| Name | Type | Description |
|---|---|---|
? | self |
Raises
once the session has stopped
stop#
stop: function(self, filename: string?): self.ReportEnds 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#
Report: associatedDeclSessioninterface#
interface profile.Session
associated type Report
pause: function(self)
resume: function(self)
stop: function(self, filename: string?): self.Report
endA 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#
pause: function(self)Stops recording without ending the session. Idempotent.
Arguments
| Name | Type | Description |
|---|---|---|
? | self |
stop#
stop: function(self, filename: string?): self.ReportEnds 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#
Report: associatedDeclWhat stop answers, chosen by the session declaration rather than here.
Severitytype#
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.
TraceOptionstype#
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.
TraceProfilerecord#
record profile.TraceProfile
id: string
luajitRevision: string
luajitVersion: integer
architecture: string
operatingSystem: string
enabledRecorderFeatures: {string}
bytecodeSchema: string
supported: boolean
endThe 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
luajitRevision#
luajitRevision: stringThe pinned LuaJIT commit, or "external:
enabledRecorderFeatures#
enabledRecorderFeatures: {string}The recorder flags jit.status reported, sorted.
bytecodeSchema#
bytecodeSchema: stringA digest of the VM's bytecode name table, so a renumbered opcode set is visible rather than silently remapped.
TraceReportrecord#
record profile.TraceReport
durationSec: integer
totalAborts: integer
blacklisted: integer
sites: {profile.AbortSite}
traceProfile: profile.TraceProfile
reasonCatalogId: string
reasonCatalogVersion: integer
__tostring: function(self): string
endWhat 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#
__tostring: function(self): stringArguments
| Name | Type | Description |
|---|---|---|
? | self |
Returns
| Type | Description |
|---|---|
string |
Fields
durationSec#
durationSec: integerWallclock 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#
totalAborts: integerAbort events recorded. Excludes the benign ones unless includeBenign was set.
sites#
One row per distinct severity, reason, location and zone. Ordered by severity, then by count descending.
traceProfile#
traceProfile: profile.TraceProfileThe recorder and registry identities under which the events were interpreted.
reasonCatalogVersion#
reasonCatalogVersion: integerIts version, so a stored report can be reread under a later one.
TraceSessionrecord#
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
endA 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#
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#
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#
resume: function(self)Resumes counting. Idempotent.
Arguments
| Name | Type | Description |
|---|---|---|
? | self |
Raises
once the session has stopped
stop#
stop: function(self, filename: string?): self.ReportEnds 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#
Report: associatedDeclsites#
Aborts so far, by severity, reason, location and zone joined.
traceProfile#
traceProfile: profile.TraceProfileFunctions#
profile.samplefunction#
function profile.sample(options: profile.SampleOptions?): profile.SampleSessionStarts sampling.
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 |
Raises
when a sample session is already running
profile.tracefunction#
function profile.trace(options: profile.TraceOptions?): profile.TraceSessionStarts collecting trace aborts.
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 |
Raises
when a trace session is already running