Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net 6. Running processes as separate tasks increases the execution time drastically #67506

Open
DanPristupov opened this issue Apr 3, 2022 · 29 comments
Milestone

Comments

@DanPristupov
Copy link

Description

Starting and running processes as separate tasks increases the execution time up to 10 times.

Configuration

.Net 6.0
So far, I've reproduced the problem on both Intel and M1 macs.

The following code runs ls, but it can be any other process.

using System.Diagnostics;
using System.Text;

Console.WriteLine("Start processes sync");
Run(16, "/bin/ls", async: false);

Console.WriteLine("Start processes async");
Run(16, "/bin/ls", async: true);

Console.WriteLine("Done!");


void Run(int count, string fileName, bool async)
{
    if (async)
    {
        var tasks = new Task[count];
        for (var i = 0; i < count; i += 1)
        {
            var task = new Task(() => {
                RunProcess(fileName);
            });
            tasks[i] = task;
            task.Start();
        }
        Task.WaitAll(tasks);
    }
    else
    {
        for (var i = 0; i < count; i += 1)
        {
            RunProcess(fileName);
        }
    }
}

void RunProcess(string fileName)
{
    var stopWatch = Stopwatch.StartNew();

    using (var process = new Process())
    {
        process.StartInfo = new ProcessStartInfo
        {
            FileName = fileName,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            ErrorDialog = false,
            CreateNoWindow = true,
            RedirectStandardInput = true,
            StandardOutputEncoding = Encoding.UTF8,
        };

        process.Start();

        var output = process.StandardOutput.ReadToEnd();

        process.WaitForExit();
    }
    Console.WriteLine($"{fileName}: {stopWatch.Elapsed.TotalMilliseconds}");
}

Data

dotnet run --release
Start processes sync
/bin/ls: 103.4258
/bin/ls: 32.4446
/bin/ls: 31.3135
/bin/ls: 26.2724
/bin/ls: 32.4081
/bin/ls: 28.5405
/bin/ls: 51.8288
/bin/ls: 27.8595
/bin/ls: 26.0885
/bin/ls: 31.0516
/bin/ls: 29.5644
/bin/ls: 28.7161
/bin/ls: 28.6573
/bin/ls: 25.2447
/bin/ls: 27.5642
/bin/ls: 28.2451
Start processes async
/bin/ls: 179.849
/bin/ls: 179.9062
/bin/ls: 179.9018
/bin/ls: 179.9322
/bin/ls: 179.7889
/bin/ls: 179.818
/bin/ls: 179.9126
/bin/ls: 255.7262
/bin/ls: 255.3477
/bin/ls: 256.478
/bin/ls: 152.9311
/bin/ls: 152.9212
/bin/ls: 152.8825
/bin/ls: 152.9134
/bin/ls: 152.9793
/bin/ls: 153.5875
Done!
@DanPristupov DanPristupov added the tenet-performance Performance related issue label Apr 3, 2022
@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.Diagnostics.Process untriaged New issue has not been triaged by the area owner labels Apr 3, 2022
@ghost
Copy link

ghost commented Apr 3, 2022

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

Starting and running processes as separate tasks increases the execution time up to 10 times.

Configuration

.Net 6.0
So far, I've reproduced the problem on both Intel and M1 macs.

The following code runs ls, but it can be any other process.

using System.Diagnostics;
using System.Text;

Console.WriteLine("Start processes sync");
Run(16, "/bin/ls", async: false);

Console.WriteLine("Start processes async");
Run(16, "/bin/ls", async: true);

Console.WriteLine("Done!");


void Run(int count, string fileName, bool async)
{
    if (async)
    {
        var tasks = new Task[count];
        for (var i = 0; i < count; i += 1)
        {
            var task = new Task(() => {
                RunProcess(fileName);
            });
            tasks[i] = task;
            task.Start();
        }
        Task.WaitAll(tasks);
    }
    else
    {
        for (var i = 0; i < count; i += 1)
        {
            RunProcess(fileName);
        }
    }
}

void RunProcess(string fileName)
{
    var stopWatch = Stopwatch.StartNew();

    using (var process = new Process())
    {
        process.StartInfo = new ProcessStartInfo
        {
            FileName = fileName,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            ErrorDialog = false,
            CreateNoWindow = true,
            RedirectStandardInput = true,
            StandardOutputEncoding = Encoding.UTF8,
        };

        process.Start();

        var output = process.StandardOutput.ReadToEnd();

        process.WaitForExit();
    }
    Console.WriteLine($"{fileName}: {stopWatch.Elapsed.TotalMilliseconds}");
}

Data

dotnet run --release
Start processes sync
/bin/ls: 103.4258
/bin/ls: 32.4446
/bin/ls: 31.3135
/bin/ls: 26.2724
/bin/ls: 32.4081
/bin/ls: 28.5405
/bin/ls: 51.8288
/bin/ls: 27.8595
/bin/ls: 26.0885
/bin/ls: 31.0516
/bin/ls: 29.5644
/bin/ls: 28.7161
/bin/ls: 28.6573
/bin/ls: 25.2447
/bin/ls: 27.5642
/bin/ls: 28.2451
Start processes async
/bin/ls: 179.849
/bin/ls: 179.9062
/bin/ls: 179.9018
/bin/ls: 179.9322
/bin/ls: 179.7889
/bin/ls: 179.818
/bin/ls: 179.9126
/bin/ls: 255.7262
/bin/ls: 255.3477
/bin/ls: 256.478
/bin/ls: 152.9311
/bin/ls: 152.9212
/bin/ls: 152.8825
/bin/ls: 152.9134
/bin/ls: 152.9793
/bin/ls: 153.5875
Done!
Author: DanPristupov
Assignees: -
Labels:

area-System.Diagnostics.Process, tenet-performance, untriaged

Milestone: -

@ChrML
Copy link

ChrML commented Apr 4, 2022

What happens if you use threads instead?

I see there's a difference in the task vs non-task variant in that the async example starts all processes simultaneously (or as many as the threadpool will allow). While the sync variant starts them one by one sequentially. Meaning the problem could be anywhere, perhaps not related to tasks at all.

What happens if you modify the code to wait for each task one by one?

@danmoseley
Copy link
Member

I ran this and consistently the child processes are slower -- even if I ensure the ls takes extra time by passing in -R /home/

How many cores do you have? I am guessing part of the issue is the the threadpool assumes you are giving it CPU bound work and will not allow more active threads than roughly the number of cores (or whatever its starting heuristic is). If launching and running ls is in fact mostly IO bound then this is a bad strategy.

Stepping back -- what are you trying to achieve?

@ghost
Copy link

ghost commented Apr 4, 2022

Tagging subscribers to this area: @dotnet/area-system-threading-tasks
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

Starting and running processes as separate tasks increases the execution time up to 10 times.

Configuration

.Net 6.0
So far, I've reproduced the problem on both Intel and M1 macs.

The following code runs ls, but it can be any other process.

using System.Diagnostics;
using System.Text;

Console.WriteLine("Start processes sync");
Run(16, "/bin/ls", async: false);

Console.WriteLine("Start processes async");
Run(16, "/bin/ls", async: true);

Console.WriteLine("Done!");


void Run(int count, string fileName, bool async)
{
    if (async)
    {
        var tasks = new Task[count];
        for (var i = 0; i < count; i += 1)
        {
            var task = new Task(() => {
                RunProcess(fileName);
            });
            tasks[i] = task;
            task.Start();
        }
        Task.WaitAll(tasks);
    }
    else
    {
        for (var i = 0; i < count; i += 1)
        {
            RunProcess(fileName);
        }
    }
}

void RunProcess(string fileName)
{
    var stopWatch = Stopwatch.StartNew();

    using (var process = new Process())
    {
        process.StartInfo = new ProcessStartInfo
        {
            FileName = fileName,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            ErrorDialog = false,
            CreateNoWindow = true,
            RedirectStandardInput = true,
            StandardOutputEncoding = Encoding.UTF8,
        };

        process.Start();

        var output = process.StandardOutput.ReadToEnd();

        process.WaitForExit();
    }
    Console.WriteLine($"{fileName}: {stopWatch.Elapsed.TotalMilliseconds}");
}

Data

dotnet run --release
Start processes sync
/bin/ls: 103.4258
/bin/ls: 32.4446
/bin/ls: 31.3135
/bin/ls: 26.2724
/bin/ls: 32.4081
/bin/ls: 28.5405
/bin/ls: 51.8288
/bin/ls: 27.8595
/bin/ls: 26.0885
/bin/ls: 31.0516
/bin/ls: 29.5644
/bin/ls: 28.7161
/bin/ls: 28.6573
/bin/ls: 25.2447
/bin/ls: 27.5642
/bin/ls: 28.2451
Start processes async
/bin/ls: 179.849
/bin/ls: 179.9062
/bin/ls: 179.9018
/bin/ls: 179.9322
/bin/ls: 179.7889
/bin/ls: 179.818
/bin/ls: 179.9126
/bin/ls: 255.7262
/bin/ls: 255.3477
/bin/ls: 256.478
/bin/ls: 152.9311
/bin/ls: 152.9212
/bin/ls: 152.8825
/bin/ls: 152.9134
/bin/ls: 152.9793
/bin/ls: 153.5875
Done!
Author: DanPristupov
Assignees: -
Labels:

area-System.Threading.Tasks, tenet-performance, untriaged

Milestone: -

@DanPristupov
Copy link
Author

Thank you for your response.

@danmoseley

Stepping back -- what are you trying to achieve?

My use case is pretty simple. GUI application runs about 10-15 relatively short (~30-50ms) git commands on start which makes a noticeable freeze. The operations are (seemingly) absolutely independent. Surprisingly, after running them as tasks, the execution time of a single process increases multiple times and the total time remains basically the same.

While I was investigating the problem, I noticed, that the problem can be reproduced with any process, even with /bin/ls or /bin/date.

A few notes:

  • My Intel MBP has 8 cores, the M1 Max machine has 10.

  • I tried to increase the threadpool size, but that doesn't help. (btw, threadpool overflow usually looks different: some tasks execute normally and some tasks don't start. Instead we see that all processes are waiting for something).

  • I also tried to run same operations first as warm up, but it makes no difference and I removed that code to make the example more simple.

  • in debug mode, the slow down is even more noticeable.

@ChrML

I see there's a difference in the task vs non-task variant in that the async example starts all processes simultaneously (or as many as the threadpool will allow)
Meaning the problem could be anywhere, perhaps not related to tasks at all.

It seems to me the problem is in the Process class, but I don't have enough expertise to confirm that (or not).

What happens if you use threads instead?

I don't see any difference. Expand the following section for the details:

Threads instead of Tasks. Increased Thread Pool
using System.Diagnostics;
using System.Text;

Console.WriteLine("Start processes sync");
Run(16, "/bin/ls", async: false);

Console.WriteLine("Start processes async");
Run(16, "/bin/ls", async: true);

Console.WriteLine("Done!");


void Run(int count, string fileName, bool async)
{
    if (async)
    {
        ThreadPool.SetMinThreads(count + 10, count + 10);

        var threads = new Thread[count];
        for (var i = 0; i < count; i += 1)
        {
            threads[i] = new Thread(ThreadProc);
        }
        for (var i = 0; i < count; i += 1)
        {
            threads[i].Start(fileName);
        }
        for (var i = 0; i < count; i += 1)
        {
            threads[i].Join();
        }
    }
    else
    {
        for (var i = 0; i < count; i += 1)
        {
            RunProcess(fileName);
        }
    }
}

void ThreadProc(object? obj)
{
    RunProcess((obj as string)!);
}

void RunProcess(string fileName)
{
    var stopWatch = Stopwatch.StartNew();

    using (var process = new Process())
    {
        process.StartInfo = new ProcessStartInfo
        {
            FileName = fileName,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            ErrorDialog = false,
            CreateNoWindow = true,
            RedirectStandardInput = true,
            StandardOutputEncoding = Encoding.UTF8,
        };

        process.Start();

        var output = process.StandardOutput.ReadToEnd();

        process.WaitForExit();
    }
    Console.WriteLine($"{fileName}: {stopWatch.Elapsed.TotalMilliseconds}");
}

Results:

dotnet run --release
Start processes sync
/bin/ls: 101.7049
/bin/ls: 30.1693
/bin/ls: 27.4484
/bin/ls: 25.5091
/bin/ls: 28.6075
/bin/ls: 28.6546
/bin/ls: 28.2078
/bin/ls: 51.508
/bin/ls: 24.8652
/bin/ls: 28.4048
/bin/ls: 25.6099
/bin/ls: 24.9029
/bin/ls: 25.7239
/bin/ls: 26.31
/bin/ls: 25.509
/bin/ls: 24.9722
Start processes async
/bin/ls: 49.5739
/bin/ls: 138.7723
/bin/ls: 115.4152
/bin/ls: 140.6831
/bin/ls: 164.2454
/bin/ls: 165.0776
/bin/ls: 228.894
/bin/ls: 185.5192
/bin/ls: 300.4582
/bin/ls: 185.252
/bin/ls: 184.9457
/bin/ls: 185.1772
/bin/ls: 323.0178
/bin/ls: 208.1066
/bin/ls: 208.5017
/bin/ls: 209.9374
Done!

What happens if you modify the code to wait for each task one by one?

This way the processes are executed as fast as they should. Please expand the section below for the details:

Wait for each thread
using System.Diagnostics;
using System.Text;

Console.WriteLine("Start processes sync");
Run(16, "/bin/ls", async: false);

Console.WriteLine("Start processes async");
Run(16, "/bin/ls", async: true);

Console.WriteLine("Done!");


void Run(int count, string fileName, bool async)
{
    if (async)
    {
        ThreadPool.SetMinThreads(count + 10, count + 10);

        var threads = new Thread[count];
        for (var i = 0; i < count; i += 1)
        {
            threads[i] = new Thread(ThreadProc);
        }
        for (var i = 0; i < count; i += 1)
        {
            threads[i].Start(fileName);
            threads[i].Join();
        }
    }
    else
    {
        for (var i = 0; i < count; i += 1)
        {
            RunProcess(fileName);
        }
    }
}

void ThreadProc(object? obj)
{
    RunProcess((obj as string)!);
}

void RunProcess(string fileName)
{
    var stopWatch = Stopwatch.StartNew();

    using (var process = new Process())
    {
        process.StartInfo = new ProcessStartInfo
        {
            FileName = fileName,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            ErrorDialog = false,
            CreateNoWindow = true,
            RedirectStandardInput = true,
            StandardOutputEncoding = Encoding.UTF8,
        };

        process.Start();

        var output = process.StandardOutput.ReadToEnd();

        process.WaitForExit();
    }
    Console.WriteLine($"{fileName}: {stopWatch.Elapsed.TotalMilliseconds}");
}

Results:

dotnet run --release
Start processes sync
/bin/ls: 92.785
/bin/ls: 21.1047
/bin/ls: 18.0883
/bin/ls: 19.9052
/bin/ls: 18.3877
/bin/ls: 20.165
/bin/ls: 18.944
/bin/ls: 19.4233
/bin/ls: 18.045
/bin/ls: 19.8717
/bin/ls: 37.9207
/bin/ls: 19.257
/bin/ls: 18.8873
/bin/ls: 21.1175
/bin/ls: 21.6771
/bin/ls: 19.7997
Start processes async
/bin/ls: 18.4013
/bin/ls: 19.3454
/bin/ls: 17.8117
/bin/ls: 19.2499
/bin/ls: 17.5375
/bin/ls: 18.1795
/bin/ls: 17.9971
/bin/ls: 19.0148
/bin/ls: 17.4245
/bin/ls: 19.1555
/bin/ls: 17.822
/bin/ls: 19.2319
/bin/ls: 18.1246
/bin/ls: 19.3281
/bin/ls: 19.161
/bin/ls: 19.4298
Done!


I think I've answered all your questions. Please let me know if I missed something.

@ChrML
Copy link

ChrML commented Apr 5, 2022

Nice details, and research!

Based on this, the slowdown should be related to the degree of parallelism and processes started simultaneously, and not tasks/threads by themselves. Since waiting for each task/thread seems to yield the fast result. Maybe the application has to wait for a common lock or a blocking OS resource during process Start.

I don't have too much insight into the Process implementation under Linux. Maybe someone else have and can explain why this occurs. Could also be valuable to see if the same occurs on Windows.

@devsko
Copy link
Contributor

devsko commented Apr 5, 2022

@DanPristupov Can you please measure the total elapsed time for sync / async.

@devsko
Copy link
Contributor

devsko commented Apr 5, 2022

Windows:

Start processes sync
C:\Windows\System32\cmd.exe: 89,7152
C:\Windows\System32\cmd.exe: 49,1774
C:\Windows\System32\cmd.exe: 49,8015
C:\Windows\System32\cmd.exe: 50,6141
C:\Windows\System32\cmd.exe: 52,5395
C:\Windows\System32\cmd.exe: 50,8179
C:\Windows\System32\cmd.exe: 48,7166
C:\Windows\System32\cmd.exe: 50,4042
C:\Windows\System32\cmd.exe: 53,0987
C:\Windows\System32\cmd.exe: 57,6313
C:\Windows\System32\cmd.exe: 64,7421
C:\Windows\System32\cmd.exe: 52,3299
C:\Windows\System32\cmd.exe: 53,0856
C:\Windows\System32\cmd.exe: 52,6622
C:\Windows\System32\cmd.exe: 56,1951
C:\Windows\System32\cmd.exe: 52,1156
TOTAL sync: 913,2566
Start processes async
C:\Windows\System32\cmd.exe: 109,2501
C:\Windows\System32\cmd.exe: 115,1507
C:\Windows\System32\cmd.exe: 129,4584
C:\Windows\System32\cmd.exe: 133,1002
C:\Windows\System32\cmd.exe: 145,9811
C:\Windows\System32\cmd.exe: 155,326
C:\Windows\System32\cmd.exe: 163,7159
C:\Windows\System32\cmd.exe: 179,9286
C:\Windows\System32\cmd.exe: 149,8854
C:\Windows\System32\cmd.exe: 140,2502
C:\Windows\System32\cmd.exe: 169,6546
C:\Windows\System32\cmd.exe: 161,2332
C:\Windows\System32\cmd.exe: 129,9286
C:\Windows\System32\cmd.exe: 140,7953
C:\Windows\System32\cmd.exe: 152,6978
C:\Windows\System32\cmd.exe: 130,9094
TOTAL async: 319,4197
Done!

@DanPristupov
Copy link
Author

DanPristupov commented Apr 5, 2022

@ChrML

I don't have too much insight into the Process implementation under Linux.

It's MacOS. Sorry if it wasn't clear. Still *nix though.

Could also be valuable to see if the same occurs on Windows.

@devsko posted that just above. Looks like Windows can also be affected.

the slowdown should be related to the degree of parallelism and processes started simultaneously

I tried to distribute the process start and added Thread.Sleep(10) to the task initialization loop, but it made no difference.

@DanPristupov
Copy link
Author

As a side note, starting a process on M1(ARM) CPU is 10 times slower than on Intel CPU:

Source code
using System.Diagnostics;
using System.Text;

Console.WriteLine("Start processes sync");
Run(16, "/bin/ls", async: false);

Console.WriteLine("Start processes async");
Run(16, "/bin/ls", async: true);

Console.WriteLine("Done!");

void Run(int count, string fileName, bool async)
{
    var stopWatch = Stopwatch.StartNew();
    if (async)
    {
        var tasks = new Task[count];
        for (var i = 0; i < count; i += 1)
        {
            var task = new Task(() => {
                RunProcess(fileName);
            });
            tasks[i] = task;
            task.Start();
        }
        Task.WaitAll(tasks);
    }
    else
    {
        for (var i = 0; i < count; i += 1)
        {
            RunProcess(fileName);
        }
    }
    var typeString = async ? "async" : "sync";
    Console.WriteLine($"Total type {typeString} {stopWatch.Elapsed.TotalMilliseconds}");
}

void RunProcess(string fileName)
{
    var stopWatch = Stopwatch.StartNew();

    using (var process = new Process())
    {
        process.StartInfo = new ProcessStartInfo
        {
            FileName = fileName,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            ErrorDialog = false,
            CreateNoWindow = true,
            RedirectStandardInput = true,
            StandardOutputEncoding = Encoding.UTF8,
        };

        process.Start();

        var output = process.StandardOutput.ReadToEnd();

        process.WaitForExit();
    }
    Console.WriteLine($"{fileName}: {stopWatch.Elapsed.TotalMilliseconds}");
}
Intel (2.3ms avg)
dotnet run --release
Start processes sync
/bin/ls: 25.0932
/bin/ls: 3.6286
/bin/ls: 3.565
/bin/ls: 3.6467
/bin/ls: 2.3761
/bin/ls: 2.3586
/bin/ls: 2.5641
/bin/ls: 2.4136
/bin/ls: 2.3298
/bin/ls: 2.2895
/bin/ls: 2.2861
/bin/ls: 2.274
/bin/ls: 2.2964
/bin/ls: 2.346
/bin/ls: 2.2755
/bin/ls: 2.3004
Total type sync 65.3108
Start processes async
/bin/ls: 17.7733
/bin/ls: 12.6006
/bin/ls: 12.7846
/bin/ls: 12.7583
/bin/ls: 14.2065
/bin/ls: 17.7802
/bin/ls: 13.1571
/bin/ls: 15.8159
/bin/ls: 15.4926
/bin/ls: 17.8502
/bin/ls: 16.8834
/bin/ls: 17.7844
/bin/ls: 17.8358
/bin/ls: 14.8688
/bin/ls: 15.8335
/bin/ls: 14.6468
Total type async 20.3364
Done!
M1 (25ms avg)
dotnet run --release
Start processes sync
/bin/ls: 100.2816
/bin/ls: 26.076
/bin/ls: 21.451
/bin/ls: 23.9278
/bin/ls: 21.1879
/bin/ls: 24.8415
/bin/ls: 25.9347
/bin/ls: 22.8774
/bin/ls: 42.4205
/bin/ls: 21.7012
/bin/ls: 23.1033
/bin/ls: 23.0439
/bin/ls: 20.8563
/bin/ls: 21.0834
/bin/ls: 20.9084
/bin/ls: 25.0697
Total type sync 468.7015
Start processes async
/bin/ls: 210.5708
/bin/ls: 210.7278
/bin/ls: 210.601
/bin/ls: 210.5951
/bin/ls: 210.6887
/bin/ls: 210.6435
/bin/ls: 210.7237
/bin/ls: 210.6702
/bin/ls: 210.6927
/bin/ls: 210.6554
/bin/ls: 120.661
/bin/ls: 120.6573
/bin/ls: 120.6377
/bin/ls: 120.5842
/bin/ls: 120.631
/bin/ls: 121.3627
Total type async 344.2012
Done!

Do you think I should create a separate issue for that?

@danmoseley
Copy link
Member

Do you think I should create a separate issue for that?

Yes please, it was flagged here #67339 but we didn't create an issue.

@danmoseley
Copy link
Member

I ran this and consistently the child processes are slower -- even if I ensure the ls takes extra time by passing in -R /home/

I just realized I used Linux. My M1 hasn't arrived yet. Does this repro on Linux for someone else? Or is somehow specific to Windows and Mac (?!)

What if you log the time (eg ticks) between when the loop starts, and when process.Start is called? presumably that will show a delay, which ideally in your case would be zero for each call.

@DanPristupov
Copy link
Author

@danmoseley

What if you log the time (eg ticks) between when the loop starts, and when process.Start is called? presumably that will show a delay, which ideally in your case would be zero for each call.

Source code
using System.Diagnostics;
using System.Text;

Console.WriteLine("Start processes async");
Run(16, "/bin/ls", async: true);

Console.WriteLine("Done!");

void Run(int count, string fileName, bool async)
{
    var stopWatch = Stopwatch.StartNew();
    if (async)
    {
        var tasks = new Task[count];
        for (var i = 0; i < count; i += 1)
        {
            var task = new Task(() => {
                Console.WriteLine($"delay: {stopWatch.ElapsedMilliseconds} ms");
                RunProcess(fileName);
            });
            tasks[i] = task;
            task.Start();
        }
        Task.WaitAll(tasks);
    }
    else
    {
        for (var i = 0; i < count; i += 1)
        {
            RunProcess(fileName);
        }
    }
    var typeString = async ? "async" : "sync";
    Console.WriteLine($"Total type {typeString} {stopWatch.Elapsed.TotalMilliseconds}");
}

void RunProcess(string fileName)
{
    var stopWatch = Stopwatch.StartNew();

    using (var process = new Process())
    {
        process.StartInfo = new ProcessStartInfo
        {
            FileName = fileName,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            ErrorDialog = false,
            CreateNoWindow = true,
            RedirectStandardInput = true,
            StandardOutputEncoding = Encoding.UTF8,
        };

        process.Start();

        var output = process.StandardOutput.ReadToEnd();

        process.WaitForExit();
    }
    Console.WriteLine($"{fileName}: {stopWatch.Elapsed.TotalMilliseconds}");
}

MBP16 M1 Max:

dotnet run --release
Start processes async
delay: 11 ms
delay: 11 ms
delay: 11 ms
delay: 11 ms
delay: 11 ms
delay: 11 ms
delay: 11 ms
delay: 11 ms
delay: 11 ms
delay: 11 ms
/bin/ls: 412.8496
/bin/ls: 412.8208
/bin/ls: 412.8002
/bin/ls: 412.8127
/bin/ls: 412.8233
/bin/ls: 412.7961
/bin/ls: 412.819
/bin/ls: 412.8425
/bin/ls: 412.837
/bin/ls: 412.8167
delay: 431 ms
delay: 431 ms
delay: 431 ms
delay: 431 ms
delay: 432 ms
delay: 432 ms
/bin/ls: 200.0511
/bin/ls: 200.1575
/bin/ls: 200.0651
/bin/ls: 200.1079
/bin/ls: 200.1359
/bin/ls: 200.1537
Total type async 632.3171
Done!

My current machine has 10 cores, so the ThreadPool size is also 10. There are 16 tasks, and because of the ThreadPool overflow 2 virtual groups appeared: 10 and 6 simultaneous tasks.

We can see that each single process in a group somehow waits for the whole group of the parallel processes.

@fernandozago
Copy link

fernandozago commented Apr 7, 2022

Hello everyone.
I ran it on windows 10 (Ryzen 3700x - 8cores, 16threads)

Command: CMD /c dir

Start processes sync
cmd: 40,7933
cmd: 26,7772
cmd: 25,3197
cmd: 23,502
cmd: 24,3611
cmd: 29,0904
cmd: 28,5455
cmd: 28,3005
cmd: 24,7782
cmd: 26,6514
cmd: 26,4133
cmd: 31,5373
cmd: 26,9445
cmd: 25,1255
cmd: 28,9803
cmd: 31,7677
Total type sync 458,4898
Start processes async
cmd: 37,7033
cmd: 40,5682
cmd: 37,5978
cmd: 49,1791
cmd: 51,8796
cmd: 53,8477
cmd: 58,7341
cmd: 60,0082
cmd: 63,2641
cmd: 49,1104
cmd: 56,1296
cmd: 55,8794
cmd: 68,7703
cmd: 58,1821
cmd: 57,7187
cmd: 59,6214
Total type async 102,5157
Done!

But i think it maybe something with multiple Tasks running at the same time.
Due common OS locking for starting processes like @ChrML said.
Or even an antivirus heuristic trying to monitor fast processes starts to avoid some virus replication.
If you change your code to run single instance at a time (like on the sync code), it returns nearly the same result for me.

Start processes sync
cmd: 44,3693
cmd: 26,5006
cmd: 23,815
cmd: 25,1439
cmd: 29,0849
cmd: 28,9684
cmd: 25,3439
cmd: 24,5733
cmd: 28,3871
cmd: 34,0787
cmd: 30,0783
cmd: 29,2867
cmd: 29,1504
cmd: 27,2343
cmd: 29,0069
cmd: 24,982
Total type sync 470,7642
Start processes async
cmd: 24,8412
cmd: 26,7439
cmd: 33,3553
cmd: 31,74
cmd: 28,5698
cmd: 27,5991
cmd: 31,2083
cmd: 28,8033
cmd: 26,6959
cmd: 23,7428
cmd: 22,9894
cmd: 23,6098
cmd: 23,5023
cmd: 23,6591
cmd: 22,7147
cmd: 22,8315
Total type async 425,0549
Done!

@DanPristupov
Copy link
Author

DanPristupov commented Apr 8, 2022

I decided to run the profiler.

Source code
using System.Diagnostics;
using System.Text;

System.Threading.Thread.Sleep(5*1000); // sleep to allow to attach the profiler

Console.WriteLine("Start processes async");
Run(16, "/bin/ls", async: true);

Console.WriteLine("Done!");

void Run(int count, string fileName, bool async)
{
    var stopWatch = Stopwatch.StartNew();
    if (async)
    {
        var tasks = new Task[count];
        for (var i = 0; i < count; i += 1)
        {
            var task = new Task(() => {
                RunProcess(fileName);
            });
            tasks[i] = task;
            task.Start();
        }
        Task.WaitAll(tasks);
    }
    else
    {
        for (var i = 0; i < count; i += 1)
        {
            RunProcess(fileName);
        }
    }
    var typeString = async ? "async" : "sync";
    Console.WriteLine($"Total type {typeString} {stopWatch.Elapsed.TotalMilliseconds}");
}

void RunProcess(string fileName)
{
    var stopWatch = Stopwatch.StartNew();

    using (var process = new Process())
    {
        process.StartInfo = new ProcessStartInfo
        {
            FileName = fileName,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            ErrorDialog = false,
            CreateNoWindow = true,
            RedirectStandardInput = true,
            StandardOutputEncoding = Encoding.UTF8,
        };

        process.Start();

        var output = process.StandardOutput.ReadToEnd();

        process.WaitForExit();
    }
    Console.WriteLine($"{fileName}: {stopWatch.Elapsed.TotalMilliseconds}");
}

Run the profiler on M1 Max:

% ~/.dotnet/tools/dotnet-trace collect -n StartProcessTes --format SpeedScope
No profile or providers specified, defaulting to trace profile 'cpu-sampling'

Provider Name                           Keywords            Level               Enabled By
Microsoft-DotNETCore-SampleProfiler     0x0000F00000000000  Informational(4)    --profile 
Microsoft-Windows-DotNETRuntime         0x00000014C14FCCBD  Informational(4)    --profile 

Process        : /Users/dan/src/StartProcessTest/bin/Debug/net6.0/StartProcessTest
Output File    : /Users/dan/src/StartProcessTest/StartProcessTest_20220408_102243.nettrace

[00:00:00:04]	Recording trace 460.164  (KB)
Press <Enter> or <Ctrl+C> to exit...

Trace completed.
Writing:	/Users/dan/src/StartProcessTest/StartProcessTest_20220408_102243.speedscope.json
Conversion complete

The hot point is the Interop.Sys.ForkAndExecProcess call in runtime/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs.

Screenshot 2022-04-08 at 10 39 09

Direct link to the source code:

                // Invoke the shim fork/execve routine.  It will create pipes for all requested
                // redirects, fork a child process, map the pipe ends onto the appropriate stdin/stdout/stderr
                // descriptors, and execve to execute the requested process.  The shim implementation
                // is used to fork/execve as executing managed code in a forked process is not safe (only
                // the calling thread will transfer, thread IDs aren't stable across the fork, etc.)
                int errno = Interop.Sys.ForkAndExecProcess(
                    resolvedFilename, argv, envp, cwd,
                    startInfo.RedirectStandardInput, startInfo.RedirectStandardOutput, startInfo.RedirectStandardError,
                    setCredentials, userId, groupId, groups,
                    out childPid, out stdinFd, out stdoutFd, out stderrFd);

I've attached the full trace file here StartProcessTest_20220408_102243.speedscope.json.zip

You can unzip and open it yourself on https://speedscope.net

Also: StartProcessTest_20220408_102243.nettrace.zip

How can we go deeper into Interop.Sys.ForkAndExecProcess?
Update: The Interop.Sys.ForkAndExecProcess source code is available here: https://github.com/dotnet/runtime/blob/6a889d234267a4c96ed21d0e1660dce787d78a38/src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.cs

Then it calls SystemNative_ForkAndExecProcess: https://github.com/dotnet/corefx/blob/5c83394112febe1b481ab1c0b61a45c850677165/src/Native/Unix/System.Native/pal_process.c#L206

@DanPristupov
Copy link
Author

DanPristupov commented Apr 8, 2022

There is a comment in ForkAndExecProcess which implies that allowing multiple processes to start concurrently was at least considered during initial the implementation.

            // Lock to avoid races with OnSigChild
            // By using a ReaderWriterLock we allow multiple processes to start concurrently.
            s_processStartLock.EnterReadLock();

Maybe if has never been working properly or it could be a regression.

@GSPP
Copy link

GSPP commented Apr 9, 2022

Even if there were a global lock I'd expect the total time to be roughly the same. A global lock would at most reduce concurrency to 1, as before. Lock overhead can't be relevant here. Why is it 10x slower?

@tmds
Copy link
Member

tmds commented Apr 10, 2022

The reader lock gets taken while starting processes, the writer lock is taken when a process exits.

In the async case, different processes are starting simultaneous, so the WaitForExit is blocked by acquiring the writer lock.

For the sync case, you need to sum the elapsed times.
For the async case, they are measuring concurrent events.

If you move the StopWatch to the Run method you get this result:

$ dotnet run -c Release
Start processes sync
/bin/ls: 60.3433
Start processes async
/bin/ls: 18.8684
Done!

@tmds
Copy link
Member

tmds commented Apr 10, 2022

For some additional info, see #25879.

@DanPristupov
Copy link
Author

DanPristupov commented Apr 10, 2022

@tmds

In the async case, different processes are starting simultaneous, so the WaitForExit is blocked by acquiring the writer lock.

I'm might be missing something, but it looks like WaitForExit is not related. At least on the profiler graph in my previous post (#67506 (comment)) it doesn't take a significant time (~20ms comparing to ~200ms of ForkAndExecProcess).

If you move the StopWatch to the Run method you get this result:

$ dotnet run -c Release
Start processes sync
/bin/ls: 60.3433
Start processes async
/bin/ls: 18.8684
Done!

Do you mean like this?

Source code
using System.Diagnostics;
using System.Text;

Console.WriteLine("Start processes sync");
Run(16, "/bin/ls", async: false);

Console.WriteLine("Start processes async");
Run(16, "/bin/ls", async: true);

Console.WriteLine("Done!");

void Run(int count, string fileName, bool async)
{
    var stopWatch = Stopwatch.StartNew();
    if (async)
    {
        var tasks = new Task[count];
        for (var i = 0; i < count; i += 1)
        {
            var task = new Task(() => {
                RunProcess(fileName);
            });
            tasks[i] = task;
            task.Start();
        }
        Task.WaitAll(tasks);
    }
    else
    {
        for (var i = 0; i < count; i += 1)
        {
            RunProcess(fileName);
        }
    }
    var typeString = async ? "async" : "sync";
    Console.WriteLine($"Total time {typeString} {stopWatch.Elapsed.TotalMilliseconds}");
}

void RunProcess(string fileName)
{
    var stopWatch = Stopwatch.StartNew();

    using (var process = new Process())
    {
        process.StartInfo = new ProcessStartInfo
        {
            FileName = fileName,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            ErrorDialog = false,
            CreateNoWindow = true,
            RedirectStandardInput = true,
            StandardOutputEncoding = Encoding.UTF8,
        };

        process.Start();

        var output = process.StandardOutput.ReadToEnd();

        process.WaitForExit();
    }
}

In my case, the total time doesn't change that much.

M1:

dotnet run -c Release
Start processes sync
Total time sync 444.3092
Start processes async
Total time async 383.883
Done!

For the sync case, you need to sum the elapsed times.

For the async case, they are measuring concurrent events.

Well, yes, but I don't understand why each of the concurrent events take 5-10 times longer. It doesn't excuse the fact that user must wait for output of a single process 200ms instead of 30ms.

@tmds
Copy link
Member

tmds commented Apr 10, 2022

Sorry, I had overlooked this issue was for macOS specifically.

On macOS, process starts are serialized due to missing pipe2:

#if !HAVE_PIPE2
// We do not have pipe2(); take the lock to emulate it race free.
// If another process were to be launched between the pipe creation and the fcntl call to set CLOEXEC on it, that
// file descriptor will be inherited into the other child process, eventually causing a deadlock either in the loop
// below that waits for that pipe to be closed or in StreamReader.ReadToEnd() in the calling code.
if (pthread_mutex_lock(&ProcessCreateLock) != 0)
{
// This check is pretty much just checking for trashed memory.
success = false;
goto done;
}
haveProcessCreateLock = true;

@tmds
Copy link
Member

tmds commented Apr 11, 2022

On macOS, process starts are serialized due to missing pipe2:

And for similar reasons, Windows has a lock also:

// Take a global lock to synchronize all redirect pipe handle creations and CreateProcess
// calls. We do not want one process to inherit the handles created concurrently for another
// process, as that will impact the ownership and lifetimes of those handles now inherited
// into multiple child processes.
lock (s_createProcessLock)

@DanPristupov
Copy link
Author

@tmds is there any possible workaround for that particular use case (i.e. running about 10 processes about 60ms each). I'm just trying to decrease the delay a user must wait for data to appear.

@tmds
Copy link
Member

tmds commented Apr 12, 2022

If you can remove the WaitForExit call, those Tasks won't end up waiting to acquire the writer lock which is being held by the processes that are starting sequentially. So the output of the first processes will be available sooner.

Also, if you are blocking Tasks you might starve the ThreadPool causing things to go even slower. For processes that run long, you should use LongRunning (which will use a dedicated thread for the Task). Or use async APIs in your Task.

@DanPristupov
Copy link
Author

If you can remove the WaitForExit call, those Tasks won't end up waiting to acquire the writer lock which is being held by the processes that are starting sequentially. So the output of the first processes will be available sooner.

No. I removed it and the result is the same :(

With WaitForExit (Total time async 335ms):

dotnet run -c Release
Start processes sync
/bin/ls: 98.0125
/bin/ls: 26.7105
/bin/ls: 20.9646
/bin/ls: 23.403
/bin/ls: 20.2838
/bin/ls: 20.8735
/bin/ls: 21.4462
/bin/ls: 24.2901
/bin/ls: 40.2885
/bin/ls: 20.1572
/bin/ls: 20.286
/bin/ls: 24.0907
/bin/ls: 20.8128
/bin/ls: 21.826
/bin/ls: 20.0096
/bin/ls: 24.9194
Total time sync 452.1408
Start processes async
/bin/ls: 158.3848
/bin/ls: 158.4169
/bin/ls: 158.3936
/bin/ls: 158.4039
/bin/ls: 158.3727
/bin/ls: 158.4184
/bin/ls: 158.4634
/bin/ls: 196.3038
/bin/ls: 196.3243
/bin/ls: 196.9643
/bin/ls: 126.9954
/bin/ls: 126.9804
/bin/ls: 127.0865
/bin/ls: 127.0429
/bin/ls: 127.0582
/bin/ls: 127.8567
Total time async 335.4064
Done!

Without WaitForExit (Total time async 400ms):

dotnet run -c Release
Start processes sync
/bin/ls: 99.3795
/bin/ls: 28.0014
/bin/ls: 26.4949
/bin/ls: 23.8075
/bin/ls: 22.1625
/bin/ls: 28.9287
/bin/ls: 25.5122
/bin/ls: 42.618
/bin/ls: 25.3854
/bin/ls: 23.391
/bin/ls: 23.8038
/bin/ls: 23.0368
/bin/ls: 24.1766
/bin/ls: 23.3039
/bin/ls: 25.1493
/bin/ls: 23.2374
Total time sync 492.1862
Start processes async
/bin/ls: 139.389
/bin/ls: 139.3716
/bin/ls: 139.3997
/bin/ls: 139.409
/bin/ls: 139.4599
/bin/ls: 139.9483
/bin/ls: 213.2685
/bin/ls: 237.7443
/bin/ls: 237.7407
/bin/ls: 238.8761
/bin/ls: 46.5697
/bin/ls: 92.3975
/bin/ls: 121.6485
/bin/ls: 121.612
/bin/ls: 122.8951
/bin/ls: 151.5456
Total time async 400.4798
Done!
Source code
using System.Diagnostics;
using System.Text;

Console.WriteLine("Start processes sync");
Run(16, "/bin/ls", async: false);

Console.WriteLine("Start processes async");
Run(16, "/bin/ls", async: true);

Console.WriteLine("Done!");

void Run(int count, string fileName, bool async)
{
    var stopWatch = Stopwatch.StartNew();
    if (async)
    {
        var tasks = new Task[count];
        for (var i = 0; i < count; i += 1)
        {
            var task = new Task(() => {
                RunProcess(fileName);
            });
            tasks[i] = task;
            task.Start();
        }
        Task.WaitAll(tasks);
    }
    else
    {
        for (var i = 0; i < count; i += 1)
        {
            RunProcess(fileName);
        }
    }
    var typeString = async ? "async" : "sync";
    Console.WriteLine($"Total time {typeString} {stopWatch.Elapsed.TotalMilliseconds}");
}

void RunProcess(string fileName)
{
    var stopWatch = Stopwatch.StartNew();

    using (var process = new Process())
    {
        process.StartInfo = new ProcessStartInfo
        {
            FileName = fileName,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            ErrorDialog = false,
            CreateNoWindow = true,
            RedirectStandardInput = true,
            StandardOutputEncoding = Encoding.UTF8,
        };

        process.Start();

        var output = process.StandardOutput.ReadToEnd();

//        process.WaitForExit();
    }
    Console.WriteLine($"{fileName}: {stopWatch.Elapsed.TotalMilliseconds}");
}

@tmds
Copy link
Member

tmds commented Apr 12, 2022

/bin/ls: 46.5697
/bin/ls: 92.3975

You get these lower values due to not waiting for exit.

@DanPristupov
Copy link
Author

I was curious how that case is handled by Swift. It turned out on Swift async is always faster.

M1 (sync: 17ms, async: 3ms). Async is 5.65 times faster!

dan@MBP16-M1 StartProcessTest % swiftc program.swift 
dan@MBP16-M1 StartProcessTest % ./program
Start processes sync
/bin/ls 2ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 0ms
/bin/ls 1ms
/bin/ls 0ms
/bin/ls 0ms
/bin/ls 0ms
/bin/ls 0ms
/bin/ls 0ms
/bin/ls 0ms
/bin/ls 0ms
/bin/ls 0ms
/bin/ls 0ms
Total time sync 17ms
Start processes async
/bin/ls 2ms
/bin/ls 2ms
/bin/ls 2ms
/bin/ls 2ms
/bin/ls 2ms
/bin/ls 2ms
/bin/ls 2ms
/bin/ls 2ms
/bin/ls 2ms
/bin/ls 3ms
/bin/ls 1ms
/bin/ls 2ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 2ms
/bin/ls 3ms
Total time async 3ms

Intel (sync: 31ms, async: 4ms). Async is 7.75 times faster!

dan@MBP16-Intel StartProcessTest % swiftc program.swift
dan@MBP16-Intel StartProcessTest % ./program
Start processes sync
/bin/ls 2ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
/bin/ls 1ms
Total time sync 31ms
Start processes async
/bin/ls 2ms
/bin/ls 2ms
/bin/ls 3ms
/bin/ls 2ms
/bin/ls 3ms
/bin/ls 3ms
/bin/ls 3ms
/bin/ls 3ms
/bin/ls 3ms
/bin/ls 3ms
/bin/ls 3ms
/bin/ls 3ms
/bin/ls 3ms
/bin/ls 3ms
/bin/ls 3ms
/bin/ls 3ms
Total time async 4ms
Source code
import Foundation


run(count: 16, path: "/bin/ls", async: false)
run(count: 16, path: "/bin/ls", async: true)

func run(count: Int, path: String, async: Bool) {
    print("Start processes \(async ? "async" : "sync")")
    let start = DispatchTime.now()
    let operationQueue = OperationQueue()

    if !async {
        operationQueue.addOperation {
            for _ in 0..<count {
                runProcess(path: path)
            }
        }
    } else {
        for _ in 0..<count {
            operationQueue.addOperation {
                runProcess(path: path)
            }
        }
    }
    
    operationQueue.waitUntilAllOperationsAreFinished()

    let nanoTime = DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds
    let elapsed = Int(Double(nanoTime) / 1_000_000)

    print("Total time \(async ? "async" : "sync") \(elapsed)ms")
}

func runProcess(path: String) {
    let start = DispatchTime.now()
    let process = Process()
    process.launchPath = path
    let outputPipe = Pipe()
    process.standardOutput = outputPipe

    process.launch()

    let _ = outputPipe.fileHandleForReading.readDataToEndOfFile()

    process.waitUntilExit()

    let nanoTime = DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds
    let elapsed = Int(Double(nanoTime) / 1_000_000)
    print("\(path) \(elapsed)ms")
}

@tmds
Copy link
Member

tmds commented Apr 13, 2022

I was curious how that case is handled by Swift. It turned out on Swift async is always faster.

This uses the macOS specific POSIX_SPAWN_CLOEXEC_DEFAULT which means child processes only inherit specified handles.

With .NET, any inheritable handle is available to any child.

There is an open issue for an API that would allow to specify the inheritance: #13943.

However, because there may be a child Process that enable inheritance, the lock that causes the .NET process starts to be serialized on macOS must still be used (even for processes that disable inheritance).

To use POSIX_SPAWN_CLOEXEC_DEFAULT on macOS like Swift does, inheritance must be disabled process-wide for all children.

@buyaa-n buyaa-n added this to the Future milestone Jul 11, 2022
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Jul 11, 2022
@ghost
Copy link

ghost commented Jan 20, 2023

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

Starting and running processes as separate tasks increases the execution time up to 10 times.

Configuration

.Net 6.0
So far, I've reproduced the problem on both Intel and M1 macs.

The following code runs ls, but it can be any other process.

using System.Diagnostics;
using System.Text;

Console.WriteLine("Start processes sync");
Run(16, "/bin/ls", async: false);

Console.WriteLine("Start processes async");
Run(16, "/bin/ls", async: true);

Console.WriteLine("Done!");


void Run(int count, string fileName, bool async)
{
    if (async)
    {
        var tasks = new Task[count];
        for (var i = 0; i < count; i += 1)
        {
            var task = new Task(() => {
                RunProcess(fileName);
            });
            tasks[i] = task;
            task.Start();
        }
        Task.WaitAll(tasks);
    }
    else
    {
        for (var i = 0; i < count; i += 1)
        {
            RunProcess(fileName);
        }
    }
}

void RunProcess(string fileName)
{
    var stopWatch = Stopwatch.StartNew();

    using (var process = new Process())
    {
        process.StartInfo = new ProcessStartInfo
        {
            FileName = fileName,
            UseShellExecute = false,
            RedirectStandardOutput = true,
            ErrorDialog = false,
            CreateNoWindow = true,
            RedirectStandardInput = true,
            StandardOutputEncoding = Encoding.UTF8,
        };

        process.Start();

        var output = process.StandardOutput.ReadToEnd();

        process.WaitForExit();
    }
    Console.WriteLine($"{fileName}: {stopWatch.Elapsed.TotalMilliseconds}");
}

Data

dotnet run --release
Start processes sync
/bin/ls: 103.4258
/bin/ls: 32.4446
/bin/ls: 31.3135
/bin/ls: 26.2724
/bin/ls: 32.4081
/bin/ls: 28.5405
/bin/ls: 51.8288
/bin/ls: 27.8595
/bin/ls: 26.0885
/bin/ls: 31.0516
/bin/ls: 29.5644
/bin/ls: 28.7161
/bin/ls: 28.6573
/bin/ls: 25.2447
/bin/ls: 27.5642
/bin/ls: 28.2451
Start processes async
/bin/ls: 179.849
/bin/ls: 179.9062
/bin/ls: 179.9018
/bin/ls: 179.9322
/bin/ls: 179.7889
/bin/ls: 179.818
/bin/ls: 179.9126
/bin/ls: 255.7262
/bin/ls: 255.3477
/bin/ls: 256.478
/bin/ls: 152.9311
/bin/ls: 152.9212
/bin/ls: 152.8825
/bin/ls: 152.9134
/bin/ls: 152.9793
/bin/ls: 153.5875
Done!
Author: DanPristupov
Assignees: -
Labels:

area-System.Diagnostics.Process, tenet-performance

Milestone: Future

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants