Performance & Large Images
A picture's cost follows its boundaries and its colours, not its cell count. This page covers what to reduce first, how to stay inside Pine's time limits, and how the million-cell demos are put together.
What costs time
| Driver | Why it matters |
|---|---|
| Boundary length | Every corner of every region becomes a polyline point. Fragmented, noisy pictures have long boundaries. |
| Number of regions | Each separate region is traced and linked. Thousands of single-cell speckles cost more than a few large shapes. |
| Colour levels | Each visible level needs its own polylines, and more levels split regions into smaller ones. |
| Opaque area | Transparent cells are skipped. Opaque cells have to be scanned, grouped, and joined. |
| Your own loops | Filling the source is the script's work. It is often the larger share. |
Resolution on its own is cheap. A smooth image of a million cells can be lighter than a noisy one of fifty thousand.
Tuning order
- Lower the colour levels: try 32 or fewer before changing anything else.
- Add a transparent floor: write
naor-1for cells that carry no information. - Smooth the data: averaging neighbouring cells turns speckle into regions.
- Redraw less often: render on closed bars instead of every tick.
- Reduce the grid: fewer rows or columns, last, because it costs the most detail.
Unchanged frames are nearly free
PineCanvas caches its contour work. When the source, the style, and the placement are unchanged, a draw call skips replacing the drawings. When only part of a picture changed, cached geometry for the rest is reused. A script that redraws on every tick therefore pays for a full render only when the picture actually differs.
The loop time limit
Pine stops a script when a single loop runs for more than 500 milliseconds on one bar. The limit applies to each outermost loop separately, not to the bar as a whole. That shapes how heavy work should be arranged:
- Several shorter loops in sequence are safer than one long loop.
- A loop that wraps other heavy calls puts all of their work under one limit. Call them one after another instead.
- Keep each loop well below the limit. A loop that usually takes 300 milliseconds will sometimes overrun on a busy server.
PineCanvas already splits its own heavy passes into shorter loops. The same applies to the loops that fill your source.
Spread work across bars
A script runs once on every historical bar while the chart loads. A large picture can be produced a slice at a time on those bars, then drawn once on the last one.
// ROWS, COLUMNS, colours and paletteIndexAt() are the script's own
var pc.PaletteField picture = pc.paletteField(ROWS, COLUMNS)
var pc.Canvas canvas = pc.newCanvas(colours)
var bool rendered = false
// The first bar that writes a row, so the last row lands just before the end
bool enoughBars = last_bar_index > math.max(ROWS, COLUMNS) + 1
int firstRowBar = last_bar_index - ROWS - 1
int step = bar_index - firstRowBar
// One row of the picture on each bar
if enoughBars and step >= 0 and step < ROWS
array<int> target = picture.row(step)
for column = 0 to COLUMNS - 1
target.set(column, paletteIndexAt(step, column))
// One render, when the chart has finished loading
if barstate.islastconfirmedhistory and enoughBars and step >= ROWS and not rendered
canvas.drawLookback(picture, 0, 0.0, float(ROWS))
rendered := true- The picture is written through
row(), which hands back the field's own storage for that row. - The render waits for
barstate.islastconfirmedhistory, so it runs once when loading finishes. - The chart must have more bars than the picture has rows to write and columns to place.
Toward a million cells
What happens automatically
Above 100,000 cells a source moves to the large renderer. It processes the picture in tiles, stores its data in bands, and caches each tile's geometry. Nothing in the calling script changes.
Strips on separate canvases
The published 1344 × 804 Julia fractal is split into vertical strips. Each strip is its own palette field, drawn by its own Canvas beside the previous one with draw(), and each Canvas is given a share of the 100 polylines through drawingLimit().
- Each strip's render is a separate piece of work under the loop limit.
- The strips are rendered by separate calls, not from inside a loop, for the same reason.
- Each strip is offered an even share of the polylines still free, so the last strip is not starved.
Few colours
The same fractal uses 13 colour levels. A level count that low is what lets a million cells fit in 100 polylines.
Smooth backgrounds
In the large renderer, neighbouring tiles are joined wherever cells of the same colour meet across a tile border. A smooth, fully opaque background makes that happen at nearly every border cell, and the cost then grows with the picture's area.
-1and draw the backdrop on a second, much coarser Canvas underneath. The library's cover scene draws its nebula on a 320 × 180 field and only the black hole at full resolution.Memory
- A field of a million cells is a million array elements. Scripts have a memory limit as well as a time limit.
- For a picture that is rendered once, set the field variable to
naafter drawing so its storage can be released. The polylines stay on the chart. - Render large layers one after another, not in the same moment, so their working storage is never held together.
Measure it
plot(float(canvas.componentCount()), "Regions", display = display.data_window)
plot(float(canvas.drawingCount()), "Polylines", display = display.data_window)A high region count for the picture's size points at fragmentation. A polyline count near the Canvas's limit points at too many colours or too much boundary. The Pine Profiler shows whether the time goes into your own loops or into the render. Common Pitfalls lists the errors a render can raise.