Introduction
I started this project after wanting to check the Windows Experience score in my current Windows 10 Pro install and realizing that the system tool for displaying the score was no longer included in the OS. However, the benchmark utility, WinSAT.exe is still present in the %windows%system32 directory. It can be run from the command line using the "formal" parameter to generate a new benchmark, which is stored as a *.xml file in the Windows\Performance\WinSAT\DataStore directory. The results can be seen reading the XML code, and there are multiple work-arounds on-line for viewing the results in Administration Tools, for example. While others have written viewers that emulate the old Windows score display, I wanted to write my own to learn how better to work with XML files generally, and as a programming exercise.
Background
The Windows Experience score is a built-in system benchmark that first appeared in Windows Vista. While there are many other more sophisticated benchmarking tools, it still provides a quick (1 minute or so) test of overall system components and a summary score of the PC as a whole, and is included for free with the OS. While previous version of Windows displayed this score on demand, the tool provided to view disappeared with Windows 10.
Using the Code
The program I came up uses a simple class to turn the contents of the *.xml datafile into a flat database that can be manipulated as a List<>
object. It uses the XmlReader
class to navigate along the node tree of the XML string storing each node as an "xitem
" object that contains a header string (the name of the node higher in the hierarchy, if any), a name string, and a data string (which is the "value
" property). Since XML nodes may also have attribute strings, these are stored in xitem
as an internal List
in each xitem
, where each attribute also has a name and a value (using an xattribute class
). The relevant data from WinSAT benchmarks is stored mainly in the Text Node type, but XmlNodeTypes
such as XmlDeclaration
, DocumentType
, EntityReference
, Comment
, ProcessingInstruction
, CDATA
, and Comment
are also stored in the database. A Last-in-First-Out stack is used to hold the names of each node which is then "popped" off when an EndElement XmlNode.Type
is encountered so the hierarchical order of the document is preserved in the database. Once the XML file is parsed, it can be searched for specific target data using two methods, string
GetdataValue(string header, string name)
and string
GetAttributeValue(string header, string name, int number)
. The method bool ItemContainsData(int itemnumber)
is helpful when searching the List<xitem>
object. In the case of the WinSAT XML files, for example, the overall System Score from the benchmark can be read as: string systemscore = GetDataValue("WinSPR","SystemScore")
and the date
and time
when the assessment was run as: string lastupdatetext = GetAttributeValue("SystemEnvironment","ExecDateTOD",1)
. This methodology should work for any XML file storing data, recognizing that the arrangement of names and values is not standardized so it would have to be customized for any given application after reading the XML source as text file to locate the data you want to work with. The program displays the benchmark results culled from the XML in a form similar to that in older versions of Windows. The "Rerun Benchmark" button spawns a new process running WinSAT.exe from the command line to generate a new benchmark *.xml file. When the process completes, a background FileSystemWatcher
monitoring the DataStore
directory alerts the main form, which updates. It also collects a list of all older assessments in the DataStore
directory so these can be reviewed and compared, if desired.
These are the two classes used to store XML Nodes in a List<>
object:
[Serializable]
public class xattribute
{
public string aname;
public string avalue;
}
[Serializable]
public class xitem
{
public string header;
public string name;
public List<xattribute> attributes = new List<xattribute>();
public string data;
}
This method uses the XmlReader
Class to navigate the nodes of the benchmark XML source file and convert them into a List
of xitem
objects.
private void GetXmlFromFile(string filename)
{
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Parse;
XmlReader reader = XmlReader.Create(filename, settings);
XI.Clear();
int count = 0;
int x = 0;
bool AddToXi = false;
while (reader.Read())
{
xitem dataitem = new xitem();
AddToXi = false;
switch (reader.NodeType)
{
case XmlNodeType.Element:
dataitem.name = reader.Name.ToString();
if (!HeaderStack.IsEmpty())
{
dataitem.header = HeaderStack.GetLastItem();
HeaderStack.Push(dataitem.name);
}
else
{
dataitem.header = "";
HeaderStack.Push(dataitem.name);
}
dataitem.data = reader.Value;
AddToXi = true;
break;
case XmlNodeType.Text:
XI[XI.Count - 1].data = reader.Value.ToString();
break;
case XmlNodeType.CDATA:
XI[XI.Count - 1].data = reader.Value.ToString();
break;
case XmlNodeType.ProcessingInstruction:
dataitem.name = reader.Name;
dataitem.data = reader.Value;
dataitem.header = "PROCESSING INSTRUCTION";
AddToXi = true;
break;
case XmlNodeType.Comment:
dataitem.name = reader.Name;
dataitem.data = reader.Value;
dataitem.header = "COMMENT";
AddToXi = true;
break;
case XmlNodeType.XmlDeclaration:
dataitem.header = "XML DECLARATION";
dataitem.name = reader.Name;
dataitem.data = reader.Value;
AddToXi = true;
?>");
break;
case XmlNodeType.Document:
break;
case XmlNodeType.DocumentType:
dataitem.name = "<!DOCTYPE " + reader.Name + " " + reader.Value;
dataitem.header = "";
dataitem.data = reader.Value.ToString();
AddToXi = true;
break;
case XmlNodeType.EntityReference:
dataitem.name = "ENTITY REFERENCE";
dataitem.data = reader.Name.ToString();
dataitem.header = "";
AddToXi = true;
break;
case XmlNodeType.EndElement:
HeaderStack.Pop();
break;
}
if (reader.HasAttributes)
{
for (x = 0; x < reader.AttributeCount; x++)
{
reader.MoveToAttribute(x);
xattribute xa = new xattribute();
xa.aname = reader.Name;
xa.avalue = reader.Value;
dataitem.attributes.Add(xa);
}
}
if (AddToXi)
{
XI.Add(dataitem);
count++;
}
}
}
Once in the List
of xitem
s, it can be searched for specific information to be displayed using:
private string GetDataValue(string header, string name)
{
string result = "";
int x = 0;
for (x = 0; x < XI.Count; x++)
{
if (XI[x].header == header && XI[x].name == name)
{
result = XI[x].data;
}
}
return result;
}
and:
private string GetAttributeValue(string header, string name, int number)
{
string result = "";
int x = 0;
for (x = 0; x < XI.Count; x++)
{
if (XI[x].header == header && XI[x].name == name)
{
if (XI[x].attributes.Count >= number)
{
result = XI[x].attributes[number - 1].avalue;
break;
}
}
}
return result;
}
Points of Interest
System File Redirection Handling
Writing this code was my first experience with the Windows 32/64 File Redirection issue. If you are not aware of this process, you will be confounded by the inability of reading or running a system file, such as WinSAT.exe in the system32 directory programmatically even though you can see the file when you browse the folder with Explorer. This occurs because, in general, the system files in %system32% in a 64 bit install of Windows 10 are in fact 64 bit versions of 32 bit files with the same names as those in a 32 bit Install. If you are running a 32 bit application in your 64 bit Windows 10 environment, the OS assumes when you try to access the %system32% directory that you really want the 32 bit version of whatever system EXE or DLL you are seeking, and "redirects" your program to the SysWow64 folder instead, where the 32 bit versions of the OS system files have been moved in 64 bit Windows. However, WinSAT.exe exists in only one location, the original %system32% directory. For some reason, it is not duplicated in SysWow64
. Hence, attempting to access it with File.Exists("C:\\Windows\\system32\\WinSAT.exe")
or Process.StartInfo
will fail. Windows 10 running in 64 bit mode does provide a "virtual" folder, "sysnative" which you can use as a substitute for "system32" to access the old 32 bit system file folder, but I found this confusing to work with.
While you could simply copy WinSAT.exe to another non-redirected folder, the simplest way to access it in whatever normal folder it occupies, depending on which version of Windows 10 is running, is to turn off folder redirection if the current environment is 64 bit windows, using this code fragment to determine which version is running:
private static bool is64BitProcess = (IntPtr.Size == 8);
private static bool is64BitOperatingSystem =
is64BitProcess || InternalCheckIsWow64();
[DllImport("kernel32.dll", SetLastError = true,
CallingConvention = CallingConvention.Winapi)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool IsWow64Process(
[In] IntPtr hProcess,
[Out] out bool wow64Process
);
private static bool InternalCheckIsWow64()
{
if ((Environment.OSVersion.Version.Major == 5 &&
Environment.OSVersion.Version.Minor >= 1) ||
Environment.OSVersion.Version.Major >= 6)
{
using (Process p = Process.GetCurrentProcess())
{
bool retVal;
if (!IsWow64Process(p.Handle, out retVal))
{
return false;
}
return retVal;
}
}
else
{
return false;
}
}
public class Wow64Interop
{
[DllImport("Kernel32.Dll", EntryPoint = "Wow64EnableWow64FsRedirection")]
public static extern bool EnableWow64FSRedirection(bool enable);
}
and use this path to the WinSAT.exe file:
public static string ExePathFor64BitApplication = Path.Combine(Environment.GetFolderPath
(Environment.SpecialFolder.Windows), @"system32\winsat.exe");
Then launch WinSAT as a separate process to create the benchmark xml:
Process n = new Process();
n.StartInfo.FileName = ExePathFor64BitApplication;
if (is64BitOperatingSystem)
{
Wow64Interop.EnableWow64FSRedirection(false);
n.Start();
ID = n.Id;
Wow64Interop.EnableWow64FSRedirection(true);
}
else
{
n.Start();
ID = n.Id;
ProcessHandle = n.Handle;
}
Watching the Datastore Folder for a New Winsat-Created XML File
A final programming point-of-interest I encountered was how to determine if WinSAT.exe had finished running and created a new benchmark file, so I could refresh the application window with the new data automatically. My solution was to create a wrapper class encapsulating a FileSystemWatcher
object, that could in turn be instantiated in a BackGroundWorker DoWork()
method which exits when the FileSystemWatcher.Changed
event fires. The BackGroundWorker.Completed
method then refreshes the form with the new benchmark.
public class FSWatcher
{
public FSWatcher(string Path,string Filter)
{
watcher = new FileSystemWatcher(Path,Filter);
watcher.Created += watcher_changed;
watcher.NotifyFilter = NotifyFilters.LastAccess
| NotifyFilters.LastWrite
| NotifyFilters.FileName
| NotifyFilters.DirectoryName;
}
~FSWatcher()
{
watcher.Dispose();
}
public bool Completed
{
get
{
return completed;
}
}
public void Start()
{
completed = false;
watcher_start();
}
private FileSystemWatcher watcher;
private void watcher_changed(object sendwer, FileSystemEventArgs e)
{
watcher.EnableRaisingEvents = false;
completed = true;
}
private void watcher_start()
{
watcher.EnableRaisingEvents = true;
}
private bool completed = false;
}
private void bwDoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
FSWatcher FSW = new FSWatcher(DataStorePath, "*.*");
FSW.Start();
while (!FSW.Completed)
{
}
}
private void bwCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Thread.Sleep(3000);
PopulateDataFileList();
PopulateAssessmentFileNames();
if (AssessmentFileNames.Count > 0)
{
ParseXml(AssessmentFileNames[AssessmentFileNames.Count - 1]);
}
btnGetBenchMark.Enabled = true;
}
Summary
In the process of re-creating the Windows Experience tool so I could more easily check the system benchmark for my home built PC, I learned more about the XML file structure which is commonly used to store data for program and hardware installations. Having a simple way to load and search these files can be useful, recognizing that the way data is stored within nodes varies with each creating application so one has to read the XML as a text file first to find out where the information you are seeking is located. Once found, items such as the test results from WinSAT can be easily displayed and manipulated.
History
- 1.0.1.0 11-04-2010: First version