Getting threads from the Gateway

Like the diagnostic dashboard, I made a project with a dashboard to show the threads on the gateway. I'm using system.util.threadDump() then parsing that json to get how many of each type of thread and the status.

However, I feel like there may be a better method that's not so resource intensive.

Thoughts?

You can go straight to the Java API, but then you don't get something in JSON that you can easily pull data out of.

Generating a thread dump is an inherently expensive operation.

1 Like

:100:

You really shouldn't even leave the gateway's thread monitoring page open.

Thanks for the info. As having never worked with APIs before, sounds like it's time to learn.

In case anyone was curious, this is what I was running on the gateway every 10 seconds, I had it running every second, but that was... problematic. (putting it lightly)

import json
threadDumpStr = system.util.threadDump()
threadDump = json.loads(threadDumpStr)
webserverTag = '[default]Dashboard/Threads/WebserverThreadCount'
timed_waitingTag = '[default]Dashboard/Threads/TIMED_WAITING'

# Count webserver threads
webserverCount = sum(1 for thread in threadDump['threads'] if thread['name'].startswith('webserver'))

# Count threads with TIMED_WAITING status
timedWaitingCount = sum(1 for thread in threadDump['threads'] if thread['state'] == 'TIMED_WAITING' and thread['name'].startswith('webserver'))

# Write the dataset to a tag
system.tag.write(webserverTag, webserverCount)
system.tag.write(timed_waitingTag, timedWaitingCount)

We cache the results from that route for 5 seconds, but still yes, especially on large systems.

If you're not paying any attention to the stack trace, you can avoid fetching it (ThreadMXBean (Java SE 17 & JDK 17)) which will make the calls significantly cheaper.

Is that something I can do through a project? How would that call be structured?

foo.bar.getAllThreadIDs()

then

foo.bar.getThreadInfo(threadArray, 0)

But I don't know what those somethings are.

Use the static method here to get an instance of the class, then get your array of thread IDs, then pass that to the call to get thread info.

I tried that and it doesn't seem like I'm getting the full thread dump from the gateway.

from java.lang.management import ManagementFactory
from java.lang import Thread

# Get the ThreadMXBean instance
thread_mx_bean = ManagementFactory.getThreadMXBean()

# Get a dump of all threads
thread_info_array = thread_mx_bean.dumpAllThreads(True, True)

# Initialize counters
webserver_count = 0
timed_waiting_count = 0

# Process the thread_info_array
for thread_info in thread_info_array:
    # Check if the thread name starts with "Webserver"
    if thread_info.getThreadName().startswith("Webserver"):
        webserver_count += 1
        # Check if the thread state is TIMED_WAITING
    	if thread_info.getThreadState() == Thread.State.TIMED_WAITING:
            timed_waiting_count += 1

# Write the counts to the specified tags
system.tag.write('[default]Dashboard/Threads/WebserverThreadCount', webserver_count)
system.tag.write('[default]Dashboard/Threads/TIMED_WAITING', timed_waiting_count)

If I change the .startswith() to something like "Swing" or "Fork" I get some results. With "Webserver" I would expect a "webserver" thread count of around 80 because that's what I can see on the VM when I do a thread dump there.

That implies you're calling this from a client or designer scope, not the actual gateway. You need to call the script on the gateway to get gateway scoped results.

I've got it running as a gateway event under scripting, shouldn't that be the correct scope? Or is there something I'm missing with how I'm calling the function?

I understand that if I run it in the script console that it would be the designer scope, but the saved script in the gateway events should run from the gateway?

If it's running as a gateway event, then yes, it should be executing on the gateway, but you also definitely shouldn't see any Swing related threads on the gateway.

Do you have Perspective? Can you run it from a button in Perspective to be sure?
Maybe also print out thread_info_array?

Correct, I only got Swing threads when I ran it from the script console in designer.

Okay, I got it working from the button. I found a few issues. I was calling dumpAllThreads when I should have been calling getThreadInfo

also .startswith() is case sensitive.

    from java.lang.management import ManagementFactory
	from java.lang import Thread
	
	# Get the ThreadMXBean instance
	thread_mx_bean = ManagementFactory.getThreadMXBean()
	
	# Get all live thread IDs
	thread_ids = thread_mx_bean.getAllThreadIds()
	
	# Get thread information for the specified threads
	thread_info_array = thread_mx_bean.getThreadInfo(thread_ids)
	
	# thread_info_array to text block
	self.parent.parent.getChild("TextArea").props.text = thread_info_array
	
	# Initialize counters
	webserver_count = 0
	timed_waiting_count = 0
	
	# Process the thread_info_array
	for thread_info in thread_info_array:
		if thread_info is not None:
			# Check if the thread name starts with "Webserver"
			if thread_info.getThreadName().startswith("webserver"):
				webserver_count += 1
			# Check if the thread state is TIMED_WAITING
				if thread_info.getThreadState() == Thread.State.TIMED_WAITING:
					timed_waiting_count += 1
		
		# Write the counts to the specified tags
	system.tag.write('[default]Dashboard/Threads/WebserverThreadCount', webserver_count)
	system.tag.write('[default]Dashboard/Threads/TIMED_WAITING', timed_waiting_count)

So it's working fine from the button but not from the gateway event script with a 10,000 ms timer.

I made a simple library script:

from java.lang.management import ManagementFactory
from java.lang import Thread
from com.inductiveautomation.ignition.common.util import SimplePatternSearcher

RUNNABLE = Thread.State.RUNNABLE
BLOCKED = Thread.State.BLOCKED
WAITING = Thread.State.WAITING
TIMED_WAITING = Thread.State.TIMED_WAITING
TERMINATED = Thread.State.TERMINATED

def get_threads(state=None, name_filter=None):
	thread_mx_bean = ManagementFactory.getThreadMXBean()
	thread_ids = thread_mx_bean.allThreadIds
	threads = thread_mx_bean.getThreadInfo(thread_ids)
	if state is not None:
		if isinstance(state, basestring):
			state = Thread.State.valueOf(state.upper())
		threads = filter(lambda info: info.threadState == state, threads)

	if name_filter is not None:
		pattern = SimplePatternSearcher(name_filter)
		threads = filter(lambda info: pattern.matches(info.threadName), threads)
	return threads

The 'SimplePatternSearcher' supports * and ? wildcards for multiple or single character replacements, and is inherently case insensitive.

print len(threads.get_threads(threads.TIMED_WAITING))
print len(threads.get_threads(state="TIMED_WAITING"))

print threads.get_threads(name_filter="swing*")
1 Like

Thanks, I figured out what happened.

On the gateway script I had it wrapped in a function but imported the java stuff from outside the function.

Thanks for all your help!

1 Like

That is legacy behavior in gateway events. One of the reasons I strongly recommend never writing an actual script in the event. Just put a one-liner there that calls a library script function. Library scripts don't have the funky problems with nested scopes.

1 Like