Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Windows Experience Score For Windows 10

5.00/5 (14 votes)
5 Nov 2019CPOL6 min read 14.5K   502  
A replacement Windows Experience Score tool for Windows 10

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.

Image 1

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:

C#
/// <summary>
/// XATTRIBUTE CLASS
/// Stores an xml attribute
/// </summary>

[Serializable]
public class xattribute
{
    public string aname;
    public string avalue;
}
/// <summary>
/// XITEM CLASS
/// Stores xml node
/// </summary>

[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.

C#
// GETXMLFROM FILE()
// Fills in itemlist "XI" from specified file
       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();   // current header
                           HeaderStack.Push(dataitem.name); // header next subnode
                                                            // is this item's name
                       }
                       else
                       {
                           dataitem.header = "";
                           HeaderStack.Push(dataitem.name); // header next subnode
                                                            // is this item's name
                       }

                       dataitem.data = reader.Value;        // prior code data="";
                       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;

                       //sb.Append("<?xml version='1.0'?>");
                       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 xitems, it can be searched for specific information to be displayed using:

C#
// GET DATA VALUE FROM XML NODE IN LIST
        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:

C#
// GET ATTRIBUTE FROM XML NODE IN LIST
        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:

C#
private static bool is64BitProcess = (IntPtr.Size == 8); // true if running as a 64bit process.
private static bool is64BitOperatingSystem = 
     is64BitProcess || InternalCheckIsWow64();           // true if current Windows OS is 64bit, 
                                                         // used for redirection handling

// Detect 32 or 64 bit OS
// Credits:
// MICROSOFT: Raymond Chen
// http://blogs.msdn.com/oldnewthing/archive/2005/02/01/364563.aspx

        [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;
            }
        }

        // Handle Folder Redirection in Windows 64
        // Credits:
        // http://tcamilli.blogspot.com/2005/07/disabling-wow64-file-system.html
        // https://stackoverflow.com/questions/29149512/
        // process-start-for-system32-applications-the-system-cannot-find-the-file-specif
        /// <summary>
        /// Use to enable and disable file redirection in Win 64
        /// </summary>
        public class Wow64Interop
        {
            [DllImport("Kernel32.Dll", EntryPoint = "Wow64EnableWow64FsRedirection")]
            public static extern bool EnableWow64FSRedirection(bool enable);
        }

and use this path to the WinSAT.exe file:

C#
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:

C#
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.

C#
    /// Call from a Background Worker DoWork() instance with Path and File extension filter.
    /// Watches for File Create
    /// </summary>
    /// <param name="Path">Used for Folder to watch.</param>
    /// <param name="Filter">Used for file extension to watch for as in *.xml.</param>
    /// <returns>Nothing, DoWork uses() Completed property to terminate</returns>
    public class FSWatcher
    {
        // Constructor
        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;

        }
        // Destructor
        ~FSWatcher()
            {
                watcher.Dispose();
            }
        //Public status accessor
        public bool Completed
        {
            get
            {
                return completed;
            }
        }
        // Public start method
        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;
    }

// BackgroundWorker DoWork()
        private void bwDoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker worker = sender as BackgroundWorker;
            FSWatcher FSW = new FSWatcher(DataStorePath, "*.*");        
            FSW.Start();
            while (!FSW.Completed)
            {
                // Wait for completion
            }            
        }

// BackgroundWorker completed
        private void bwCompleted(object sender, RunWorkerCompletedEventArgs e)
        {            
            Thread.Sleep(3000);            // Ensure file creation is completed 
                                           // and file is unlocked
            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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)