Perspective Table Freeze After refreshBinding: Fix and Debug

Patricia Callen9 min read
HMI / SCADAOther ManufacturerTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

A Perspective table that stops responding after repeated .refreshBinding("props.data") calls is usually a stalled session, and the table script is rarely the cause. The standard fix removes the manual refresh. Bind the tag read and the heavy formatting to a view custom property once. Then bind props.data to that property plus the checkbox values, so a filter change only filters rows that already exist. Before you change anything, open the browser's developer tools and find out whether the browser or the gateway is the side that stops.

How do you read the freeze symptoms?

Look at the pattern first. This installation shows the following:

  • The formatting script (system.tag.readBlocking() plus style and description processing for about 100 rows) completes in roughly 1–2 seconds.
  • The first few filter changes and Refresh clicks work.
  • After several cycles, the Refresh button, cell clicks and cell edits all stop responding.
  • Only a new browser tab with a fresh login recovers the session.
  • A lockout that blocked Refresh until the script finished made no difference.

That last point matters. If the gateway script were simply being called too often, the lockout would have helped. Because every interaction in the session dies, not just the Refresh button, the fault is in something they all share: the session's event and property-sync path, or the browser's main thread.

Walk the signal chain from the tag to the pixel. The table below shows what each stage should carry and what a wrong value looks like.

Signal Source Wrong-value symptom
Raw tag values system.tag.readBlocking() inside the binding transform Each refresh takes longer. Bad-quality values reach the formatting code as None and produce odd statuses.
Filter selections Checkbox props.selected values passed into the script The filter works once, then lags one click behind, or rows reappear after refresh.
Busy/lock flag A custom property (for example an isActive flag) set by the Refresh button script The flag is set after refreshBinding() is called, so a second click lands inside the open window.
Formatted row set Transform output written to props.data The browser console shows rendering errors. Cells show {} or blank values where a number was expected.
Property sync Session connection between gateway and browser The Network tab shows the session connection closing or reconnecting repeatedly. The UI stays visible but is dead.
Render/event dispatch Browser main thread The Performance profile shows long tasks during each refresh, and clicks queue without executing.

Where in the chain does refreshBinding stall the session?

A Perspective binding and its script transform execute on the gateway. The result is written into the session's property tree and then pushed to the browser, which diffs it and re-renders the component.

Calling refreshBinding("props.data") forces the whole chain to run again:

  1. A fresh blocking tag read.
  2. The full formatting pass: the per-country irradiance averaging, the connector evaluation, and the status and colour mapping.
  3. A complete replacement of props.data.

With per-cell style objects, 100 rows is not 100 values. It is 100 rows, times each column, times a style dictionary of a dozen keys (background colour, four radii, border styles, widths and colours, margin). Every refresh replaces all of it, even when the filter only removed rows.

Two failure paths follow from this:

  • Browser side. Each full replacement makes the table rebuild its rows and styles. If a row contains a value the table cannot render, or the rebuild is slow, the main thread stays busy or throws. Clicks are captured but never dispatched, so every control looks dead at once.
  • Gateway side. Component event scripts and binding executions for a session are handled on the gateway. A transform that blocks on I/O (a blocking tag read) or raises partway through can leave later events for that session waiting. The browser then keeps sending clicks that produce no response.

Both paths explain why a new tab recovers. A new tab starts a new session with an empty property tree and an empty event backlog.

Why did the busy-flag lockout not help?

The lockout only protects the gateway from re-entering the script. It does nothing for a browser that is still processing the previous payload, and nothing for a payload that breaks the table.

Order also matters. Set the flag to true before calling refreshBinding(), and clear it from the transform's completion rather than from the button script. If the button sets the flag after the call, or clears it immediately, the lock is open during exactly the window it was meant to cover.

Correct the ordering, but do not expect it to fix the freeze on its own. A lockout does not fix a render path. Removing the need for refreshBinding() does.

How do you restructure the view so filters never call refreshBinding?

Split the work so the expensive part runs on data change and the cheap part runs on filter change.

  1. Add a custom property on the view, for example view.custom.rawRows.
  2. Bind the tag read and full formatting to rawRows. Use tag bindings, or a polled binding with a script transform that calls the existing library process() with no filter. It should return every row with its styles and a plain status field (the status name such as "Status fault" or "Exclusion"). This is the only place system.tag.readBlocking() runs. Set its refresh rate to how often the plant data actually changes, not to how often operators click.
  3. Bind props.data with an expression structure binding to two keys: rows pointing at {view.custom.rawRows}, and filters holding each checkbox's selected value under the names the library already uses (powerOffCritical, noCommunication, powerOffConnector, excludedByOperator, noCondition).
  4. Add a script transform that only filters. Keep the filterToStatus mapping in the same project library as process() so both paths use one definition.
  5. Delete the Refresh button's refreshBinding() call. Delete the lock flag too, since nothing re-enters the heavy script on a click any more.
# Script transform on props.data (expression structure binding)
def transform(self, value, quality, timestamp):
    rows = value['rows'] or []
    filters = value['filters'] or {}
    active = [k for k in filters if filters[k]]
    if not active:
        return rows
    allowed = set()
    for k in active:
        allowed.update(AJ3.filterToStatus.get(k, []))
    return [r for r in rows if r.get('status') in allowed]

This version treats a checked box as "show this category". If your checkboxes mean "hide this category", invert the membership test. A filter pass over 100 already-built rows is trivial compared with a blocking tag read plus full formatting.

How do you prove whether the browser or the gateway is blocking?

A frozen UI is a front-end symptom, so start in the browser. Measure before you touch the script.

  1. Open developer tools on the session tab before reproducing. Keep the Console, Network and Performance tabs ready.
  2. Record a Performance profile while you change filters and click Refresh until the freeze appears. Long tasks that start at each refresh and grow with every cycle point at render cost or a render loop.
  3. Read the Console. A JavaScript error logged at the moment the UI stops means the table choked on a value in props.data. Note which row was being rendered.
  4. Watch the session connection in the Network tab. A connection that closes, reconnects, or stops carrying messages after a refresh points at payload size or a broken sync, not at the table.
  5. Check the gateway at the same moment. Look at the gateway logs for script exceptions from the transform, and at the gateway's Perspective session status pages for the stuck session. An exception on the Nth refresh, but not the first, usually means a specific tag value or quality broke the formatting code.
  6. Time the transform. Log a timestamp at entry and exit through a system.util.getLogger() logger, not with print. If exit times stretch or stop appearing while the browser is healthy, the gateway side is blocking.

Decide from the result. A healthy gateway with a busy or erroring browser means fixing the payload shape. A gateway that stops logging transform exits means removing the blocking read from the click path, which the restructure above already does.

How do you verify the fix holds?

  1. Toggle every checkbox combination repeatedly, at least as many cycles as it previously took to freeze, then several times more. Cell clicks and edits must keep responding throughout.
  2. Record a second Performance profile. Filter changes should produce short tasks that do not grow cycle over cycle.
  3. Confirm in the gateway log that the heavy transform on rawRows fires only at its polling rate or on tag change, never on a checkbox click.
  4. Force the ugly cases: a site with lack of communication, a connector with no State, a bad-quality tag. Confirm no row carries an object where the table expects a number.
  5. Leave the session open through several data refreshes of rawRows while clicking filters. The session must stay live without a new tab or a re-login.

Which script habits keep reintroducing the freeze?

  • Dict defaults on scalar fields. Calls like LV.get('State', {}) return an empty dict when the key is missing. Then {} != 2 evaluates true, and the code writes {"value": {}} into a cell that expects a number. Missing data becomes an object in props.data, which is exactly the kind of value that breaks rendering on one refresh but not another. Default scalars to None and handle None explicitly.
  • Truthiness tests on missing keys. not MV.get('Communication', {}) treats "key missing" and "communication lost" the same. Decide which one you mean, because they map to different statuses.
  • print inside per-row logic. The print for lost farm communication fires once per affected row, on every refresh. Use a logger with a level you can switch off.
  • Dead branches. Leftover if False: / if True: blocks hide which path actually runs. Remove them before profiling so the timing you measure reflects the real code.
  • Duplicated style dictionaries. Building the same styleInverter or styleEmpty dict into every cell inflates the payload. Where the table allows it, apply common styling at column or table level and carry only what varies (the status colour) per cell.
  • Reading tags on user action. Any design that calls system.tag.readBlocking() because an operator clicked something puts I/O latency on the UI path. Operator actions should filter, sort or select data that bindings already hold.

FAQ

Why does my Perspective table stop responding after several refreshBinding calls?

Each refreshBinding("props.data") re-runs the tag read and full formatting, then replaces the whole styled row set. Either the browser main thread stalls or errors on the new payload, or the session's gateway-side event handling blocks behind the transform. Check the browser Console and Performance tabs first, then the gateway logs.

Why does opening a new tab fix a frozen Perspective session?

A new tab starts a new session with a clean property tree and no queued events or broken render state. The problem returns once you repeat the same refresh cycles, which is why the fix is removing the manual refresh path, not reloading.

Why does a busy flag not stop the freeze when using refreshBinding?

The flag only prevents re-entry into the gateway script, and only if it is set before refreshBinding() is called. It cannot help a browser that is still rendering, or failing on, the previous payload. Filtering a cached custom property removes the need for the flag entirely.

When should I escalate a frozen Perspective session to Inductive Automation support?

Escalate when the restructured view (cached custom property plus filter-only transform) still freezes and the browser Console and gateway logs are clean. Also escalate if the session connection drops with no script error on either side. Open a case through the official support channel with the Performance profile, Console output, gateway logs from the same timestamp, and your Ignition version.

Back to blog