Is anyone else having crippling issues after adding Perspective to projects?

Support are probably already looking at your thread dumps already, but I wrote a couple of scripts to help try to diagnose the issues or at least get a better perspective on them.

The first script has the function saveThreadDump() which will use the system.util.threadDump() function to take a thread dump. This is far more useful than dumping from the gateway webpage as it actually has CPU usage against each thread. I attached this to the CPU utilisation system tag change event if it gets above 0.50 with a 30s cooldown - i send myself an email when it runs as well. This in particular has been extremely useful for support as it’s captured logs when the CPU has been high in different scenarios

The second will take a thread dump created by this and convert it into a csv that you can paste into Excel where you can then sort by CPU usage and other things. Note: it does also work with the webpage dump as well. Both dumps are slightly different, but the headers I’ve kept the same so there might be blank values for some headings depending which dump you’ve converted.

I run the second script in PyCharm using Python 3.9. Haven’t checked if it’ll run in Ignition. You’ll need to install the pywin32 library for Windows to get access to the clipboard, otherwise you could always just write the csv contents to a file.

You’ll end up with something like this:


def saveThreadDump():
	'''
	Description:
		Takes a thread dump and records the current CPU, memory, and disk utilisation at the top of the file. 
		This method is far more useful that using the webpage thread download button as it contains CPU usage per thread.
		File is stored local to the client that issues the function call.
	'''
	now_formatted = system.date.format(system.date.now(), 'yyyyMMdd_HHmmss')
	filename = 'IgnitionThreadDump_AU6_{}.txt'.format(now_formatted)
	
	fileFolderPath = 'C:/Ignition Reports/Thread Dumps/'
	filePath = '{}{}'.format(fileFolderPath, filename)
	
	dump = system.util.threadDump()
	# collect some basic overall performance stats and record them at the top of the file
	stats = system.tag.readBlocking(['[System]Gateway/Performance/CPU Usage', '[System]Gateway/Performance/Disk Utilization', '[System]Gateway/Performance/Memory Utilization'])
	cpu_util = stats[0].value*100
	disk_util = stats[1].value*100
	mem_util = stats[2].value*100
	
	dump = 'CPU Utilisation: {:.2f}%\r\nMemory Utilisation: {:.2f}%\r\nDisk Utilisation: {:.2f}%\r\n{}'.format(cpu_util, mem_util, disk_util, dump)
	
	system.file.writeFile(filePath, dump)
	
	return filePath

Tag change script on [System]Gateway/Performance/CPU Usage:

cpu_log_condition = 0.5
	if currentValue.value > cpu_log_condition:
		now = system.date.now()
		lastDump = system.tag.readBlocking(['[default]System/Diagnostic/Last Thread Dump Time'])[0].value
		if lastDump is None:
			lastDump = system.date.parse('1987-01-01 00:00:00')
		dumpLogRate = system.tag.readBlocking(['[default]System/Diagnostic/Thread Dump Log Rate'])[0].value
		if system.date.secondsBetween(lastDump, now) > dumpLogRate: 
			filepath = shared.dev.diag.saveThreadDump()
			shared.errors.sendEmail('CPU Usage High {:.1f}%'.format(currentValue.value*100), \
									'Thread dump saved to disk with filepath: {}'.format(filepath))

Thread dump converter function to convert to CSV

import sys
import re
import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox
import win32clipboard # part of library: pywin32

def setClipboard(text):
    # set clipboard data
    win32clipboard.OpenClipboard()
    win32clipboard.EmptyClipboard()
    win32clipboard.SetClipboardText(text)
    win32clipboard.CloseClipboard()

def getClipboard():
    # get clipboard data
    win32clipboard.OpenClipboard()
    data = win32clipboard.GetClipboardData()
    win32clipboard.CloseClipboard()

    return data


def choose_file():
    root = tk.Tk()
    root.withdraw()
    file_path = filedialog.askopenfilename()
    return file_path

def parseThreadDump():
    file_path = choose_file()

    f = open(file_path, "r")
    thread_dump = f.read()
    f.close()
    thread_dump = thread_dump.splitlines()

    thread_id = ''
    thread_type = ''
    thread_cat = ''
    thread_name = ''
    thread_cpu = ''
    thread_status = ''
    thread_jobs = []

    headers = ['ID', 'Type', 'Thread Category', 'Thread Name', 'CPU', 'Status', 'Job Count', 'Jobs']
    rows = []

    # find the ignition version row. Should be at the top, but user may have added additional stuff there such as cpu/mem/disk utilisation
    for line_num, line in enumerate(thread_dump):
        find = re.findall('(Ignition[a-zA-Z :]+)(\d+.\d+.\d+)( \(b[\d]+\))', line)
        if len(find) > 0:
            ignition_version_line_num = line_num
            ignition_version = '{} {}'.format(find[0][1], find[0][2])
            break

    # check what type of dump was created (from webpage or system.util.threadDump, as they are formatted slightly differently)
    # threads start on the 2nd line after the ignition version row
    if 'id=' in thread_dump[ignition_version_line_num+2]:
        dump_type = 'webpage'
        thread_jobs_prefix = '    '
    else:
        dump_type = 'script'
        thread_jobs_prefix = '      '

    for line_num in range(ignition_version_line_num+1, len(thread_dump)):
        line = thread_dump[line_num]

        # if the line contains thread jobs
        if line[0:len(thread_jobs_prefix)] == thread_jobs_prefix:
            thread_jobs.append(line.replace(thread_jobs_prefix, ''))
        # if the line contains CPU usage (only applicable for dump_type == 'script', webpage doesn't have this info)
        elif "CPU: " in line:
            thread_cpu = re.findall('[\d.]+', line)[0]
        # if the line contains thread status (only applicable for dump_type == 'script', webpage has this in thread line)
        elif "java.lang.Thread.State" in line:
            thread_status = re.findall('(: )([A-Z_]+)', line)[0][1]
        # ignore any blank lines
        elif line in ['','"']:
            pass
        # else assume it contains a thread definition
        else:
            # if a previous thread is currently in memory, push it into the rows list
            if thread_status != '':
                thread_details = [thread_id,
                                   thread_type,
                                   thread_cat,
                                   thread_name,
                                   thread_cpu,
                                   thread_status,
                                   str(len(thread_jobs)),
                                   thread_jobs]
                rows.append(thread_details)

                # clear the variables
                thread_id = ''
                thread_type = ''
                thread_cat = ''
                thread_name = ''
                thread_cpu = ''
                thread_status = ''
                thread_jobs = []

            # record the current thread's info
            try:
                if dump_type == 'webpage':
                    thread_id = re.findall('id=(\d+),', line)[0]
                    thread_type = re.findall('^([a-zA-Z ]+) \[', line)[0]
                    thread_name = re.findall('\[([\w -./:\d@]+)\]', line)[0]
                    thread_status = re.findall('\(([\w_]+)\)', line)[0]
                    thread_cat = re.findall('([\w -./:\d@]+)-[\d]+', thread_name)
                    thread_cat = thread_cat[0] if len(thread_cat) > 0 else thread_name
                elif dump_type == 'script':
                    thread_name = line.replace('"', '')
                    thread_id = re.findall('-([\d]+)"', line)
                    if len(thread_id) == 0:
                        thread_id = ''
                    else:
                        thread_id = thread_id[0]
                    thread_cat = thread_name.replace("-{}".format(thread_id), '')
            except Exception as e:
                print(e)
                print('thread_name=' + thread_name)
                sys.exit()
            if thread_id == '16':
                pass
    # if a previous thread is currently in memory, push it into the rows list
    if thread_status != '':
        thread_details = [thread_id,
                          thread_type,
                          thread_cat,
                          thread_name,
                          thread_cpu,
                          thread_status,
                          str(len(thread_jobs)),
                          thread_jobs]
        rows.append(thread_details)
    table = [headers]
    table.extend(rows)
    return table

def convertThreadArrayToExcelTable(rows):
    headers = rows[0]
    text = '"' + '","'.join(headers) + '"\r\n'

    data = rows[1:]
    for row in data:
        # replace the jobs list with a text version with new lines for each job
        row[-1] = '\r\n'.join(row[-1])

    for row in data:
        try:
            text += '"' + '","'.join(row) + '"\r\n'
        except Exception as e:
            print(e)

    return text


table = convertThreadArrayToExcelTable(parseThreadDump())
setClipboard(table)
messagebox.showinfo(title='Extracted Thread Dump', message='Extracted thread dump into a table. Copied to clipboard to paste into Excel.')
2 Likes