I was looking at an xperf (ETW) trace recently and needed to know who had started a particular process. The parent process ID was stored in the trace so I could find its parent, and its parent’s parent, and so on, but doing this for many generations in WPA is tedious.
Luckily the 8.1 version of the Windows Performance Toolkit has a tool for exporting arbitrary data, so I used that and a bit of Python to write a tool that would create a process tree for an ETW trace.
The process export process
The process of exporting process data starts with configuring WPA so that it displays the process data that I want to export. To do this I closed all of the open WPA graphs and tables and opened the Processes graph, found under System Activity in the graph explorer – see the screenshot at the right. I put the Processes graph into Table Only mode (Ctrl+Shift+T) because the graph isn’t relevant for exporting.
Then it was just a matter of selecting and rearranging the columns that I wanted into the order that I wanted them. It made sense to have Process ID and Parent Process ID first, so that my script could easily find them. The remaining columns I treat as data payload and they can be rearranged any way you want. I ended up with something like this:
You can export this profile from WPA using the Export command in the Profile menu. It’s kind of obvious when you say it like that. You can then apply the profile to a loaded trace using the Apply command in the Profile menu, but we’re going to use the profile for exporting data.
I saved my profile as XperfProcessPercentage.wpaProfile.
Export made easy
Exporting the data is as simple as running this command:
wpaexporter mytrace.etl XperfProcessParentage.wpaProfile
This creates a .csv file containing the same data that was visible in the table. The only peculiar thing is the name of the .csv file, which cannot be directly specified. Not a big deal – just run the export process and look for the .csv file. In this case it will be Processes_Summary_Table_ProcessParentage.csv.
The .csv file name is derived from the table name and the view preset name. You can change the view preset name by going to the View Editor (Ctrl+E), then clicking Manage…
The rest is straightforward. I wrote the simplest possible .csv parsing code. It builds up two Python dictionaries – one that records the parent process ID for each process ID, and another which records additional data for each process ID. Then I loop through the processes. For each process the script finds the oldest ancestor, and then prints a tree starting from there. It avoids duplicate work by marking those processes which it has printed.
The result is something like this:
408 (missing process),
512, svchost.exe,C:\Windows\System32\svchost.exe -k Local…
7272, audiodg.exe,C:\Windows\system32\AUDIODG.EXE 0xbd8
1540, p4s.exe,”C:\Program Files\Perforce\Server\p4s.exe”
1968, svchost.exe,C:\Windows\system32\svchost.exe -k Local…
5044, OSPPSVC.EXE,”C:\Program Files\Common Files\Microsoft…
1108, svchost.exe,C:\Windows\system32\svchost.exe -k netsvcs
3160, svchost.exe,C:\Windows\System32\svchost.exe -k Local…
632, svchost.exe,C:\Windows\System32\svchost.exe -k Local…
Unlike a normal process tree this covers a range of time – the length of the trace. This means that not all of these processes necessarily lived at the same time. It would be trivial to add their lifetime data to the payload. It is possible that a process ID could be reused during the time period covered – in which case one of the processes would be ignored by my script. This is rare enough that I just check for it and print a message.
The process tree can be viewed quite effectively using a text editor, but if you want a proper tree view then just load the results into TabView.
Luke, I am your father (and also your son)
The process tree in Windows is a perfect tree, with one root that is the ancestor of all other processes. But this perfect tree is not present in the data – and things can get weird.
It is quite possible for a process to create another process, and then die. Now the child process has no living parent. Later on another process can be created that reuses the parent process ID, so now the child has an incorrect parent. And, there is no reason that this fake parent can’t be a descendant of the child, thus giving a loop.
This happened in the very first trace that I used as a test case. I thought my code was buggy, but I was just hitting unexpected data. Now my script checks for loops and when it detects one it stops and prints from there. I could try to improve this by selecting the oldest process in the loop as the root, but since most processes will have a start time of 0.0 (meaning the beginning of the trace) this is unlikely to help.
As Robin Giese pointed out in the comments you can also use an xperf processing action to dump the process tree:
xperf -i foo.etl -a process –tree
There are many available xperf processing actions, some of which do quite sophisticated processing, and if one of them meets your needs you should use it. To get a list of all of the available processing actions and help on the process action just run these two commands:
xperf -help processing
xperf -help processing process
The advantage of wpaexporter, however, is lets you export any data that you want, rather than being limited to the sets of data that the WPT developers support.
That is all
The Python script and .wpaprofile file both ship with UIforETW. You can download the latest release or find XperfProcessParentage.py and XperfProcessParentage.wpaProfile in the UIforETW bin directory.
This is one of my shortest posts. There is nothing more to say.