Profiler mode
Before optimising, measure. Profiler mode shows, beside every significant line of your script, how much of the script's total runtime that line is responsible for and how many times it executed — measured on the real execution running on your chart.
Turn it on: publish the script (Add to Chart) so it is running, then open the Script menu (the script name in the editor toolbar) → Profiler mode. Turn it off the same way, or by closing the editor.
What you see:
- a bar in the gutter and
12.3% · 4,512×after the code: the line's share of the script's total compute time (history plus every live tick so far) and its execution count; - 🔥 on the three heaviest lines;
- on hover: the time spent, the executions, the average per execution and — for an
if/for/while/switch— the block time (the whole structure) next to the line time (block minus the statements inside it); - a status strip above the Problems list explaining why nothing shows yet (not published, not on a chart, still starting) or that the buffer no longer matches the published version.
How to read it:
- Counts reflect every context a line runs in. A statement inside a
forexecutes once per iteration per bar; a user function's body lines add up across all its call sites, like the reference platform's profiler. - Code inside a
request.securityexpression runs in the requested context and is not instrumented; its cost lands on the line holding therequest.securitycall. - Conditions of an
else if/switchchain are part of the enclosing structure's line time (they cannot be split per branch). - Declarations, imports,
input.*lines and literal assignments are not profiled. - Percentages are of the script body's own time; profiling adds overhead, so numbers are estimates — compare lines against each other, not against a stopwatch.
Under the hood a profiled run is a second, instrumented execution of your script (the chart keeps using the normal one), available only to the script's author; the editor's line numbers must match the published version, so re-publish after edits before reading the numbers.
The patterns below are the usual fixes once the profiler has pointed at a line.
Compute once, display selectively
A series-mode function (ta.sma, ta.rsi, …) should sit at the top level. If you only want to display it on some bars, build the value every bar and gate the plot:
// 😬 recomputes `ta.sma` state when the condition is true, loses it when false
plot(cond ? ta.sma(close, 20) : na)
// ✓ compute always, display selectively
ma = ta.sma(close, 20)
plot(cond ? ma : na)
Avoid request.security for the same thing twice
Every call pays the fetch cost. Fetch once, use many times:
// 😬 fetches twice
dClose = request.security(syminfo.tickerid, "D", close)
dOpen = request.security(syminfo.tickerid, "D", open)
// ✓ fetches once, destructures
[dOpen, dClose] = request.security(syminfo.tickerid, "D", [open, close])
Keep drawings bounded
// 😬 one line per bar — hits the 500 cap, oldest lines silently disappear
line.new(bar_index, high, bar_index + 10, high)
// ✓ delete the previous reference before creating a new one
var line last = na
if ta.change(ta.highest(high, 20)) != 0
line.delete(last)
last := line.new(bar_index, high, bar_index + 10, high)
Prefer plots over labels
Labels and lines are rendered individually and interact with the picker. A single plot() renders a continuous series in one pass. For high-density per-bar annotations use plotchar/plotshape instead of label.new:
plotchar(ta.crossover(close, ta.sma(close, 20)), "cross", "▲", location.belowbar, color=color.lime)
Index arrays, don't walk them
// 😬 O(N) per lookup
for v in sortedPivots
if v > level
found := v
break
// ✓ O(log N) with a sorted array
idx = sortedPivots.binary_search_leftmost(level)
Pre-aggregate inside loops
// 😬 recomputes `ta.sma(close, 20)` on each iteration
for i = 0 to length - 1
diff = close[i] - ta.sma(close, 20)[i]
// ✓ compute once
sma20 = ta.sma(close, 20)
for i = 0 to length - 1
diff = close[i] - sma20[i]
Minimise str.format on every bar
Formatted strings are expensive. For a value that updates once per bar, build the string only on the last bar:
var table t = table.new(position.top_right, 1, 1, bgcolor=color.new(color.black, 20))
if barstate.islast
table.cell(t, 0, 0, str.format("close={0,.2f}", close))
See also
- Limitations
- Debugging — log-before/log-after breakpoints
- Compute budget exceeded