# Random Recursion in Perspective View

**URL:** <https://forum.inductiveautomation.com/t/random-recursion-in-perspective-view/91777>\
**Category:** Ignition\
**Tags:** scripting, perspective\
**Created:** [June 12, 2024, 7:49pm UTC](https://forum.inductiveautomation.com/t/random-recursion-in-perspective-view/91777 "2024-06-12T19:49:22Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![tmahaf](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/tmahaf/32/23492_2.png) [@tmahaf](https://forum.inductiveautomation.com/u/tmahaf)\
**Post date:** [June 12, 2024, 7:49pm UTC](https://forum.inductiveautomation.com/t/random-recursion-in-perspective-view/91777/1 "2024-06-12T19:49:22Z")

</div>

Hello.

I have a property change script that works perfectly fine almost all of the time.

However, once in a while, the script starts to spawn off other scripting threads, and, it also never stops.

Here is the thread of the original script that perspective calls, usually this script takes a second or two at most to execute:

```python
Thread [script-invoke-async] id=2461, (WAITING for java.util.concurrent.CompletableFuture$Signaller@50691877)
  java.base@17.0.9/jdk.internal.misc.Unsafe.park(Native Method)
  java.base@17.0.9/java.util.concurrent.locks.LockSupport.park(Unknown Source)
  java.base@17.0.9/java.util.concurrent.CompletableFuture$Signaller.block(Unknown Source)
  java.base@17.0.9/java.util.concurrent.ForkJoinPool.unmanagedBlock(Unknown Source)
  java.base@17.0.9/java.util.concurrent.ForkJoinPool.managedBlock(Unknown Source)
  java.base@17.0.9/java.util.concurrent.CompletableFuture.waitingGet(Unknown Source)
  java.base@17.0.9/java.util.concurrent.CompletableFuture.get(Unknown Source)
  com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper.getOnQueue(PropertyTreeScriptWrapper.java:153)
  com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper.blockingRead(PropertyTreeScriptWrapper.java:125)
  com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper.blockingRead(PropertyTreeScriptWrapper.java:121)
  com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper. __finditem__ (PropertyTreeScriptWrapper.java:306)
  com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper. __findattr_ex__ (PropertyTreeScriptWrapper.java:318)
  app//org.python.core.PyObject. __getattr__ (PyObject.java:957)
  org.python.pycode._pyx2831.getTableRepeaterInstancesV2$1(<custom-method getTableRepeaterInstancesV2>:92)
  org.python.pycode._pyx2831.call_function(<custom-method getTableRepeaterInstancesV2>)
  app//org.python.core.PyTableCode.call(PyTableCode.java:173)
  app//org.python.core.PyBaseCode.call(PyBaseCode.java:134)
  app//org.python.core.PyFunction. __call__ (PyFunction.java:416)
  app//org.python.core.PyMethod. __call__ (PyMethod.java:126)
  org.python.pycode._pyx2767.oneShot$3(<function:valueChanged>:43)
  org.python.pycode._pyx2767.call_function(<function:valueChanged>)
  app//org.python.core.PyTableCode.call(PyTableCode.java:173)
  app//org.python.core.PyBaseCode.call(PyBaseCode.java:306)
  app//org.python.core.PyFunction.function ___call__ (PyFunction.java:474)
  app//org.python.core.PyFunction. __call__ (PyFunction.java:469)
  app//org.python.core.PyFunction. __call__ (PyFunction.java:464)
  app//com.inductiveautomation.ignition.common.script.ScriptManager.runFunction(ScriptManager.java:847)
  app//com.inductiveautomation.ignition.gateway.script.GatewaySystemUtilities.lambda$_invokeAsyncImpl$0(GatewaySystemUtilities.java:152)
  app//com.inductiveautomation.ignition.gateway.script.GatewaySystemUtilities$$Lambda$5313/0x00007fa4c5738000.run(Unknown Source)
  java.base@17.0.9/java.lang.Thread.run(Unknown Source)

```

Here is a thread dump over another 'running script' that seems to spawn off whenever this situation occurs:

```python
Thread [perspective-worker-499] id=2573, (WAITING for java.util.concurrent.CompletableFuture$Signaller@61a61c32)
  java.base@17.0.9/jdk.internal.misc.Unsafe.park(Native Method)
  java.base@17.0.9/java.util.concurrent.locks.LockSupport.park(Unknown Source)
  java.base@17.0.9/java.util.concurrent.CompletableFuture$Signaller.block(Unknown Source)
  java.base@17.0.9/java.util.concurrent.ForkJoinPool.unmanagedBlock(Unknown Source)
  java.base@17.0.9/java.util.concurrent.ForkJoinPool.managedBlock(Unknown Source)
  java.base@17.0.9/java.util.concurrent.CompletableFuture.waitingGet(Unknown Source)
  java.base@17.0.9/java.util.concurrent.CompletableFuture.get(Unknown Source)
  com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper.getOnQueue(PropertyTreeScriptWrapper.java:153)
  com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper.blockingRead(PropertyTreeScriptWrapper.java:125)
  com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper.blockingRead(PropertyTreeScriptWrapper.java:121)
  com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper. __finditem__ (PropertyTreeScriptWrapper.java:306)
  com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper. __findattr_ex__ (PropertyTreeScriptWrapper.java:318)
  app//org.python.core.PyObject. __getattr__ (PyObject.java:957)
  org.python.pycode._pyx2716.transform$1(<transform>:8)
  org.python.pycode._pyx2716.call_function(<transform>)
  app//org.python.core.PyTableCode.call(PyTableCode.java:173)
  app//org.python.core.PyBaseCode.call(PyBaseCode.java:306)
  app//org.python.core.PyFunction.function ___call__ (PyFunction.java:474)
  app//org.python.core.PyFunction. __call__ (PyFunction.java:469)
  app//org.python.core.PyFunction. __call__ (PyFunction.java:464)
  app//com.inductiveautomation.ignition.common.script.ScriptManager.runFunction(ScriptManager.java:847)
  app//com.inductiveautomation.ignition.common.script.ScriptManager.runFunction(ScriptManager.java:829)
  app//com.inductiveautomation.ignition.gateway.project.ProjectScriptLifecycle$TrackingProjectScriptManager.runFunction(ProjectScriptLifecycle.java:868)
  com.inductiveautomation.perspective.gateway.binding.transforms.script.ScriptTransform.runFunction(ScriptTransform.java:118)
  com.inductiveautomation.perspective.gateway.binding.transforms.script.ScriptTransform.synchronousTransformInternal(ScriptTransform.java:162)
  com.inductiveautomation.perspective.gateway.binding.transforms.AbstractSynchronousTransform.transform(AbstractSynchronousTransform.java:30)
  com.inductiveautomation.perspective.gateway.model.AbstractBindingHarness$TransformSequencer.transform(AbstractBindingHarness.java:295)
  com.inductiveautomation.perspective.gateway.model.AbstractBindingHarness$TransformSequencer.run(AbstractBindingHarness.java:308)
  java.base@17.0.9/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
  java.base@17.0.9/java.util.concurrent.FutureTask.run(Unknown Source)
  java.base@17.0.9/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
  java.base@17.0.9/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
  com.inductiveautomation.perspective.gateway.threading.BlockingWork$BlockingWorkRunnable.run(BlockingWork.java:58)
  java.base@17.0.9/java.lang.Thread.run(Unknown Source)

```

When it is all said and done, I end up with an unresponsive gateway full of running scripts with no description:

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/9/3/93a72b5dbbe6d5dbec528435b8c93a1119061564.png)

The only running script that has a description is my one originating from my property change script. I only see this problem occur when I'm working in my view that contains said property change script.

What might be causing Ignition to spawn so many scripts from my original one? What is causing this script to not finish when normally it takes a second or two? It just seems like it is locking up and then making a bunch of other scripting threads randomly.

[ign01.dev.control.nglepws.com\_thread\_dump20240612-105718.json](https://forum.inductiveautomation.com/uploads/short-url/dK3IvtqaPa3Y4dNEdoR8o3xtpPr.json) (550.5 KB)  
[ign01.dev.control.nglepws.com\_thread\_dump20240612-103911.json](https://forum.inductiveautomation.com/uploads/short-url/2oAbP9QchfCclVPHven4hMQp9q4.json) (563.4 KB)  
[ign01.dev.control.nglepws.com\_thread\_dump20240612-133224.json](https://forum.inductiveautomation.com/uploads/short-url/gfGmS3lipZVETioHghUZBoO5qMa.json) (1.3 MB)  
[ign01.dev.control.nglepws.com\_thread\_dump20240612-110235.json](https://forum.inductiveautomation.com/uploads/short-url/u4pxtZXfsXBFNQwcxpk5VXthNg7.json) (1.6 MB)

I copied the entire thread dumps from when this is happening. I'm guessing it could be from every thread saying "WAITING for java.util.concurrent.CompletableFuture$Signaller@50691877"

---

<div class="post-metadata">

**Author:** ![Transistor](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/transistor/32/3380_2.png) [@Transistor](https://forum.inductiveautomation.com/u/Transistor)\
**Post date:** [June 12, 2024, 8:13pm UTC](https://forum.inductiveautomation.com/t/random-recursion-in-perspective-view/91777/2 "2024-06-12T20:13:47Z")

</div>

Can you show us your script? And what exactly is it bound to?

---

<div class="post-metadata">

**Author:** ![tmahaf](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/tmahaf/32/23492_2.png) [@tmahaf](https://forum.inductiveautomation.com/u/tmahaf)\
**Post date:** [June 12, 2024, 8:16pm UTC](https://forum.inductiveautomation.com/t/random-recursion-in-perspective-view/91777/3 "2024-06-12T20:16:59Z")

</div>

```python
	from com.inductiveautomation.ignition.common.script import ScriptManager
	from Helpers.LineNumber import getLineNumber
	
	logger = system.util.getLogger("TableRepeaterViewParamsInputParamsInstancesChange")
	
	def dumpRunningScripts():
		from com.inductiveautomation.ignition.common.script import ScriptManager
		import json
		
		runningScriptObjs = list(ScriptManager.executingScripts())
		
		# Start Process of converting to JSON and then to Python to Parse these Objs:
		jsonStringObjs = Perspective.Helpers.encoding.jsonEncodeFromObj(runningScriptObjs)
		pyObjs = system.util.jsonDecode(json.loads(jsonStringObjs))
		
		ls = []
		for runningScript in pyObjs:
# logger.info("runningScript = %s"%runningScript)
			description = runningScript.get('description') #Not all running scripts have a description, these are threads
			if description == None:
				continue
		
			if description == "Perspective - property change script on view.params.inputParams.instances":
				logger.info('description == "Perspective - property change script on view.params.inputParams.instances"')
				ls.append(runningScript)
				
		
			scriptName = description.split("script:")[-1]
			ls.append(scriptName)
		
		return pyObjs

	
	def oneShot():
		runningScripts = dumpRunningScripts()
		logger.info("runningScripts = %s"%runningScripts)
		if len(runningScripts) >20:
			logger.info("len(runningScripts) >20, returning")
			return
		self.getChild('root').getTableRepeaterInstancesV2()
	
	
	logString = "(%s) "%(getLineNumber())
	logger.info(logString)
	
	
	try:
		prevVal = previousValue.value
	except:
		prevVal = None
	
	try:
		curVal = currentValue.value
	except:
		curVal = None
	
	
	if origin != "Browser":
		logString = "(%s) origin != Browser, origin = %s"%(getLineNumber(),origin)
		logger.info(logString)
		return
	
	logString = "(%s) "%(getLineNumber())
	logger.info(logString)
	
	if previousValue == None:
		system.util.invokeAsynchronous(oneShot)
		return
	
	logString = "(%s) "%(getLineNumber())
	logger.info(logString)
	
	try:
		if list(prevVal) != list(curVal):
			system.util.invokeAsynchronous(oneShot)
	except:
		return

```

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/e/6/e66125eccd401a9561c06a5501524b6aa5bcbebe.png)

It's a pager for a flex repeater, if you have hundres of instances.

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/a/7/a78aeddc78ef70263063a0bb5aecb8e388b89fcc.png)

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [June 17, 2024, 4:43pm UTC](https://forum.inductiveautomation.com/t/random-recursion-in-perspective-view/91777/4 "2024-06-17T16:43:43Z")

</div>

> [@tmahaf](#):
>
> ```python
> com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper.getOnQueue(PropertyTreeScriptWrapper.java:153)
> com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper.blockingRead(PropertyTreeScriptWrapper.java:125)
> com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper.blockingRead(PropertyTreeScriptWrapper.java:121)
> com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper. __finditem__ (PropertyTreeScriptWrapper.java:306)
> com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper. __findattr_ex__ (PropertyTreeScriptWrapper.java:318)
> 
> ```

No matter how many threads you end up creating, you're still ultimately acting on a single-threaded queue that we manage automatically under the hood.  
Those threads are in contention, and the act of checking the state of that queue itself takes time. Spawn enough threads, with enough contention, and you get into this state. It's possible they're repeatedly polling, and the timing is such that it takes longer to deliver a value than that polling cycle, but I don't **think** that's the case - that is, if you got into this situation exactly once, it would eventually clear up. But I think your system is busy enough that it's repeatedly triggering this same flood of operations, which is just stacking the problem on itself.

Depending on what you're trying to do, consider doing a deep-copy of any property tree objects at the very beginning of your scripting operation(s), sharing only that deep copy with the rest of your scripting, and then writing that mutated object back as the very last operation in a single step.

---

<div class="post-metadata">

**Author:** ![tmahaf](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/tmahaf/32/23492_2.png) [@tmahaf](https://forum.inductiveautomation.com/u/tmahaf)\
**Post date:** [June 17, 2024, 5:54pm UTC](https://forum.inductiveautomation.com/t/random-recursion-in-perspective-view/91777/5 "2024-06-17T17:54:13Z")

</div>

Wow thanks for this response!

Can you give me an example of how to make a deep copy of a dictionary input param?

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [June 17, 2024, 6:13pm UTC](https://forum.inductiveautomation.com/t/random-recursion-in-perspective-view/91777/6 "2024-06-17T18:13:38Z")

</div>

You can use something like this to recursively unwrap our special 'wrapper' types into plain Python dictionaries/lists:

> [@Function to convert json string or python dict into ObjectWrapper](https://forum.inductiveautomation.com/t/function-to-convert-json-string-or-python-dict-into-objectwrapper/32861/16):
>
> Forcing each to the expected type while traversing makes a deep copy: def sanitize\_tree(element): if hasattr(element, '\_\_iter\_\_'): if hasattr(element, 'keys'): return dict((k, sanitize\_tree(element[k])) for k in element.keys()) else: return list(sanitize\_tree(x) for x in element) return element EDIT: Got rid of the unnecessary zip() and used only one keys() iterator instead of two.

---

<div class="post-metadata">

**Author:** ![tmahaf](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/tmahaf/32/23492_2.png) [@tmahaf](https://forum.inductiveautomation.com/u/tmahaf)\
**Post date:** [June 28, 2024, 2:44pm UTC](https://forum.inductiveautomation.com/t/random-recursion-in-perspective-view/91777/7 "2024-06-28T14:44:04Z")

</div>

I have found system.util.jsonDecode((system.util.jsonEncode(wrappedDictionary))

is a quick and reliable way to get this done.

---

<div class="post-metadata">

**Author:** ![lrose](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/lrose/32/62724_2.png) [@lrose](https://forum.inductiveautomation.com/u/lrose)\
**Post date:** [June 28, 2024, 2:54pm UTC](https://forum.inductiveautomation.com/t/random-recursion-in-perspective-view/91777/8 "2024-06-28T14:54:52Z")

</div>

> [@tmahaf](#):
>
> is a quick and reliable way to get this done

Sure, but be careful because JSON stringifys objects, which could result in data loss. It's quick and dirty and works but there are reasons not to.

I'm also not sure why @paul-griffith didn't mention it, but if you install the Ignition Extensions module, there is a `system.util.deepCopy()` function that will do this as well.

> [@Ignition Extensions - Convenience utilities for advanced users](https://forum.inductiveautomation.com/t/ignition-extensions-convenience-utilities-for-advanced-users/64615):
>
> Introducing a hopefully community-engaged effort to build a useful repository of scripting and other Ignition utilities for everyone to benefit from. Download the latest release from the [Releases](https://github.com/IgnitionModuleDevelopmentCommunity/ignition-extensions/releases) page and take advantage of the following functions: Scripting Functions system.util system.util.getContext(): Context A convenience function to retrieve the local "context" object, useful as an entrypoint to other advanced scripting. system.util.deepCopy(object): Object Converts a given Pyth…
