Random Recursion in Perspective View

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:

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:

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:

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 (550.5 KB)
ign01.dev.control.nglepws.com_thread_dump20240612-103911.json (563.4 KB)
ign01.dev.control.nglepws.com_thread_dump20240612-133224.json (1.3 MB)
ign01.dev.control.nglepws.com_thread_dump20240612-110235.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"

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

	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


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

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.

Wow thanks for this response!

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

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

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

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 @PGriffith 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.