DEV Community

Cover image for How Do You Find the Lua Function That Is Actually Costing You Frame Time?
GameOptim
GameOptim

Posted on

How Do You Find the Lua Function That Is Actually Costing You Frame Time?

A Lua entry in the top-20 cost list is the start of the investigation, not the answer. Three stack views plus a reversed call tree point you at the leaf function worth fixing.

Lua rarely shows up in a Unity performance report as one big number. It shows up as a handful of function names inside the logic-code cost ranking, and the expensive part is usually not the function listed there — it is a leaf several levels down its call stack.

The practical workflow is short: open the GOT Online Lua report's Code Efficiency → CPU Time page, choose the stack view that matches the question you are asking (whole-session, single-scene, or single-frame), then reverse the stack so the deepest and most expensive children surface first. This article walks through that sequence, explains the Lua function naming format the report uses, and covers the one test setting that silently breaks the whole analysis.

Summary

  • Lua cost appears in reports as individual functions in the top-20 logic cost list, not as a single aggregate figure.
  • The report offers three stack views: overall cost, specified scene, and any single frame. Each answers a different question; using the wrong one sends you to the wrong function.
  • Lua functions are identified as X@Y:Z — function name, defining file, defining line. When the name cannot be resolved, X becomes unknown.
  • When Lua runs as bytecode, the line number Z is always 0. Test with Lua source if you want line-level attribution.
  • Reverse stack analysis reorders the cost stack so deep sub-functions that actually consume the time appear on top, instead of forcing you to expand dozens of levels by hand.

What Is the Core Problem?

When Lua usage grows in a commercial project, CPU cost spreads across many small functions rather than concentrating in one obviously slow place. In a report, that looks like several Lua functions mixed into the top-20 logic cost list — enough to know Lua is involved, not enough to know where to change code.

The cost hides for two reasons:

  1. The entry function is not the expensive function. A Lua call often passes through wrappers, dispatch tables, and utility layers before reaching the function that does the work. The top-level name accumulates cost; the leaf generates it.
  2. Aggregate and peak are different measurements. A function can dominate total session cost while never appearing in the frame where the game stuttered, and vice versa. Locating one does not locate the other.

Why Does the Cost Hide in the Call Stack?

A stack trace orders calls from the outermost caller to the innermost callee. Read top-down, the first entries are the ones you recognize — event handlers, update loops, framework entry points — and they carry the largest accumulated totals simply because everything below them rolls up into their number.

The actual work sits at the bottom. To see it in the default view, you expand frame after frame, often dozens of levels deep, and the numbers you are comparing are already diluted by sibling branches. That is the structural reason a "Lua is slow" finding stalls: the report is correct, but the ordering hides the answer.

Reversing the stack fixes the ordering. Instead of reading caller → callee, you read callee → caller, so the deepest sub-function with the highest self-cost lands at the top of the view. The bottleneck becomes visible without manual expansion.

How Is a Lua Function Identified in the Report?

Every Lua entry carries a name in a fixed format that maps it back to source:

Part Meaning Notes
X Function name Falls back to unknown when it cannot be resolved
Y File where the function is defined The script location, not the call site
Z Line where the function is defined Always 0 under bytecode execution

X@Y:Z is enough to search your codebase directly: file plus line lands you on the definition, and the function name confirms it. This is why the format matters more than the visual stack — it converts a profiler row into a code location.

The bytecode caveat is the one that trips teams up. If the Lua scripts are shipped as bytecode, Z is 0 for everything, and you lose line-level attribution entirely. The recommendation is to run the test with Lua source wherever possible, so the stack data stays resolvable.

How Can You Tell Whether the Function Is Actually a Problem?

Match the stack view to the question. Each view isolates a different kind of cost, and the answers do not transfer between them.

View Question it answers Use it when
Overall CPU cost stack Which Lua functions accumulate the most cost across the whole session Building the hotspot shortlist at the start
Specified-scene stack Which Lua functions dominate in one particular scene Cost differs by scene, and you want to target one
Single-frame cost stack Which Lua functions consumed time in the exact frame where cost peaked Explaining a specific spike or stutter

Three signals confirm that a function is a genuine target rather than noise:

  • It appears high in the reversed overall stack, meaning its own cost is large rather than inherited from children.
  • It reappears in the per-scene view for the scene that carries the memory or frame-time pressure.
  • It positions itself in the peak frame's stack, so the aggregate number and the visible symptom agree.

If a function only satisfies the first signal, you have found volume, not a bottleneck. If it only satisfies the third, you have found a one-off spike, not a pattern.

What Should You Check First?

  1. Open the Code Efficiency → CPU Time page and confirm Lua entries exist. If Lua does not appear in the top-20 logic cost list, this workflow has nothing to target yet.
  2. Start with the overall stack to build a shortlist. Do not optimize from this list; use it to pick candidates.
  3. Reverse the stack before judging any candidate. The entry with the biggest number in the default order is frequently the caller, not the cause.
  4. Record the X@Y:Z identifier for each candidate. File and line are what make the finding actionable in code review.
  5. Confirm the test ran on source, not bytecode. If every Z is 0, fix the test setup before drawing conclusions — the data is coarse by construction.
  6. Re-check the candidate in the scene view and the peak-frame view. A function that survives all three is a real target.

What Should You Optimize?

Optimize the leaf, then the path that reaches it.

  • The leaf function itself is where the self-cost sits. The reversed stack identifies it; the X@Y:Z identifier locates it.
  • The call frequency of that path decides whether the total cost is a problem. The per-frame stack shows what one call costs; the aggregate stack shows how often it happens. A cheap function on a hot path costs more than an expensive function called twice.
  • The scene where it concentrates tells you whether the fix is local (a scene-specific feature) or systemic (a shared utility used everywhere).

Each change should be re-measured with the same view that produced the finding. A fix that lowers aggregate cost but leaves the peak-frame stack unchanged has moved volume without removing the spike.

What Should You Avoid?

  • Do not optimize the top entry of the default stack. It inherits cost from everything beneath it; changing it usually means changing design, not code.
  • Do not profile bytecode builds and expect line numbers. Z = 0 is a test configuration result, not a report bug.
  • Do not expand the stack by hand to find the bottom. Toggle the reversed view instead; dozens of manual expansions is how bottlenecks get missed.
  • Do not treat an aggregate hotspot as a frame-time problem without checking the peak frame. Total cost and peak cost point at different code.

Practical Recommendations

Problem What to inspect How to judge What to change What to re-measure
Lua appears in the cost list but you cannot name the culprit Reversed overall stack Highest self-cost at the deepest level That leaf function Overall stack
Cost is concentrated in one scene Specified-scene stack Same function tops the scene view Scene-scoped logic Scene view plus overall
Cost peaks only in one frame Single-frame stack Function present in the peak frame The call path taken in that frame Peak-frame view
Line numbers are all 0 Test script delivery Z equals 0 across entries Run the test on Lua source Naming resolved to file and line
A candidate looks large but changes nothing when tuned Peak-frame view Function absent from the spike frame Investigate the actual spike frame Stutter metric, not total cost

Key Takeaways

  • Lua cost in a report is a list of functions, and the listed function is often the caller rather than the cause.
  • Reverse stack analysis surfaces the deep sub-function that actually holds the cost.
  • X@Y:Z — name, file, line — is the bridge from a profiler row to a code location.
  • Bytecode execution forces Z to 0. Test with source when line-level attribution matters.
  • Overall, per-scene, and per-frame stacks answer different questions; a real bottleneck shows up in the ones that match your symptom.

FAQ

What does the X@Y:Z format in a Lua stack entry mean?
X is the function name, Y is the file where the function is defined, and Z is the line number of its definition. If the name cannot be resolved, X becomes unknown.

Why is the line number always 0 in my Lua data?
Because the scripts are running as bytecode. Under bytecode execution the line value is always 0, so run the test with Lua source to get usable line-level attribution.

Should I use the overall stack or the single-frame stack?
Use the overall stack to find which functions accumulate cost across the session, and the single-frame stack to explain a specific spike or stutter. A function can be large in total while never appearing in the frame you are investigating.

Why does reversing the stack make the bottleneck easier to find?
The default order lists callers above callees, so entry points carry inflated totals. Reversed, the deepest callee with the highest self-cost appears first, which removes the need to expand dozens of levels manually.

Is a function that tops the overall cost list necessarily the one to optimize?
Not necessarily. It may simply be a caller that aggregates everything beneath it. Confirm that the self-cost is high and that it also appears in the scene or peak-frame view before treating it as a target.

Top comments (0)