S. M.
S. M.

Reputation: 227

How to make Win32_Process query faster?

I have the following function to fetch process information into a DataTable:

public DataTable getProcesses()
        {
            DataTable dt = new DataTable();

            dt.Columns.Add("ID");
            dt.Columns.Add("Name");
            dt.Columns.Add("Path");
            dt.Columns.Add("User");
            dt.Columns.Add("Priority");

            string pid = "-";
            string name = "-";
            string path = "-";
            string priort = "-";
            string user = "-";

            string query = "Select * From Win32_Process";
            ManagementObjectSearcher searcher = new ManagementObjectSearcher(query);
            ManagementObjectCollection processList = searcher.Get();

            foreach (ManagementObject proc in processList)
            {
                pid = proc["ProcessID"].ToString();
                name = proc["Name"].ToString();

                if (proc["ExecutablePath"] != null)
                {
                    path = proc["ExecutablePath"].ToString();
                    priort = proc["Priority"].ToString();
                }
                string[] argList = new string[2];
                int returnVal = Convert.ToInt32(proc.InvokeMethod("GetOwner", argList));
                if (returnVal == 0)
                {
                    // return DOMAIN\user
                    user = argList[1] + "\\" + argList[0];
                }

                dt.Rows.Add(pid, name, path, user, priort);
            }

            return dt;
        }

This ultimately works and gives me the return I want, but it takes 20-30 seconds to execute.

I was wondering if there's any way to optimize the function, or the query specifically, which is most likely the source of the "delay".

edit

After doing some of the things suggested in the comments, it has come down to an average of 15-20 seconds, but that's still way too long. At most, 4-5 seconds would be tolerable. I still haven't changed the query, is there anything I can do to make it faster?

edit 2

After applying some of the changes suggested by @NicoRiff, and still getting the same runtime, I did some debugging to see what was effectively taking so long. Turns out it's one line in specific: int returnVal = Convert.ToInt32(proc.InvokeMethod("GetOwner", argList));, which will get me the user that "owns" each process.

This line takes about 60ms while all the others take 1-2ms. Over 200 iterations (which is the number of processes I have, I can only imagine the time it would take with a bigger list), it takes up about 12-13 seconds total (just for that one line), giving the 15-20 total overall.

Now that I've "singled-out" the problem, how can I optimize that function?

Upvotes: 1

Views: 2877

Answers (2)

wnutt
wnutt

Reputation: 549

Technically, this does not answer you question, because it does not utilize a Win32_Process query. However, it does produce the same result in a fraction of the time (~1.5 vs ~25 seconds), using Powershell.

You need to run this in X64 mode to interrogate 64 bit processes and requires elevated rights for Powershell to return the UserName.

Please note that this is my first time invoking Powershell scripts from C# and there might be better ways of doing it. It also needs some error trapping to make it more robust (not production worthy as-is).

using System.Management.Automation;
using System.Management.Automation.Runspaces;

private DataTable getProcesses() 
{
    // Create the datatable and columns
    DataTable dt = new DataTable();
    dt.Columns.Add("ID");
    dt.Columns.Add("Name");
    dt.Columns.Add("Path");
    dt.Columns.Add("User");
    dt.Columns.Add("Priority");
    dt.Columns.Add("BasePriority"); 

    string script = $"get-process -IncludeUserName | select id, processname, path, username, priorityclass";

    List<string[]> psOutput = new List<string[]>();

    // Invoke Powershell cmdlet and get output
    using (PowerShell ps = PowerShell.Create())
    {
        ps.AddScript(script);
        var output = ps.Invoke();
        if(ps.Streams.Error.Count > 0)
        {
            throw new Exception($"Error running script:{Environment.NewLine}{string.Join(Environment.NewLine, ps.Streams.Error.Select(e => e.ToString()))}");
        }

        // clean and split the output
        psOutput.AddRange(output.Select(i => i.ToString().Replace("; ", ";").TrimStart("@{".ToCharArray()).TrimEnd('}').Split(';')));
    }

    // populate the DataTable
    psOutput
        .AsParallel()           // this does not really help when not calling Process.GetProcessById
        .Select(p => p.Select(f => f.Split("=")).ToDictionary(k => k.FirstOrDefault(), v => v.LastOrDefault()))
        .Select(kv => new object[] {    // "flatten" the dictionaries into object[]'s that will become the datarows
                        kv["Id"]
                        , kv["ProcessName"]
                        , kv["Path"]
                        , kv["UserName"]
                        , kv["PriorityClass"]
                        , Process.GetProcessById(int.Parse(kv["Id"])).BasePriority  // if you need the numerical base priority - takes quite a bit longer though (Not sure how to get this via Powershell)
                    }
            )
        .ToList()
        .ForEach(r => dt.Rows.Add(r));  // add each object[] to the datatable

    // return the datatable 
    return dt;
}

Upvotes: 1

NicoRiff
NicoRiff

Reputation: 4883

I´ve personally always wanted to get rid of using DataTable. You can work with List<> or collections that will solve many potential problems you´ll have.

Having said that, you might want to check out ORMi library to get WMI information on a List<>. You can achieve what you are trying the following way:

        WMIHelper helper = new WMIHelper("root\\CimV2");

        string pid = "-";
        string name = "-";
        string path = "-";
        string priort = "-";
        string user = "-";

        var processes = helper.Query("Select * From Win32_Process").ToList();

        foreach (var p in processes)
        {
            pid = p.ProcessID;
            name = p.Name;
            path = p.ExecutablePath ?? String.Empty;
            priort = p.Priority ?? String.Empty;
        }

The above code works with dynamic objects and does not require you to write any model. Just that little code. If you need to use methods, then you can declare your model and work with strong typed objects:

1) Define your model:

[WMIClass(Name = "Win32_Process", Namespace = "root\\CimV2")]
public class Process
{
    public int Handle { get; set; }
    public string Name { get; set; }
    public int ProcessID { get; set; }
    public string ExecutablePath { get; set; }
    public int Priority { get; set; }

    /// <summary>
    /// Date the process begins executing.
    /// </summary>
    public DateTime CreationDate { get; set; }

    public dynamic GetOwnerSid()
    {
        return WMIMethod.ExecuteMethod(this);
    }

    public ProcessOwner GetOwner()
    {
        return WMIMethod.ExecuteMethod<ProcessOwner>(this);
    }

    public int AttachDebugger()
    {
        return WMIMethod.ExecuteMethod<int>(this);
    }
}

public class ProcessOwner
{
    public string Domain { get; set; }
    public int ReturnValue { get; set; }
    public string User { get; set; }
}

2) Query WMI

        List<Process> processes = helper.Query<Process>().ToList();

        foreach (Process p in processes)
        {
            pid = p.ProcessID;
            name = p.Name;
            path = p.ExecutablePath ?? String.Empty;
            priort = p.Priority ?? String.Empty;

            dynamic d = p.GetOwnerSid();
            ProcessOwner po = p.GetOwner();
        }

Even if for this task the second way may seem a little too much work, you will get much more cleaner and understandable code.

NOTE: I´ve tried your code with ORMi and I´m getting the results in 1-2 seconds. As others said that might depend on your environment.

NOTE 2: Always use only the properties you need on the SELECT statement. It is very expensive to WMI a SELECT *. ALWAYS specify the properties. In your case it would be:

Select ProcessID, Name, ExecutablePath, Priority From Win32_Process

(ORMi will solve that for you also as it always query the properties that are set on the model.)

Upvotes: 1

Related Questions