Search code examples
c#.netiolimit

How to limit I/O operations in .NET application?


I'm developing an application (.NET 4.0, C#) that:
1. Scans file system.
2. Opens and reads some files.

The app will work in background and should have low impact on the disk usage. It shouldn't bother users if they are doing their usual tasks and the disk usage is high. And vice versa, the app can go faster if nobody is using the disk.
The main issue is I don't know real amount and size of I/O operations because of using API (mapi32.dll) to read files. If I ask API to do something I don't know how many bytes it reads to handle my response.

So the question is how to monitor and manage the disk usage? Including file system scanning and files reading...

Check performance counters that are used by standard Performance Monitor tool? Or any other ways?


Solution

  • Using the System.Diagnostics.PerformanceCounter class, attach to the PhysicalDisk counter related to the drive that you are indexing.

    Below is some code to illustrate, although its currently hard coded to the "C:" drive. You will want to change "C:" to whichever drive your process is scanning. (This is rough sample code only to illustrate the existence of performance counters - don't take it as providing accurate information - should always be used as a guide only. Change for your own purpose)

    Observe the % Idle Time counter which indicates how often the drive is doing anything. 0% idle means the disk is busy, but does not necessarily mean that it is flat-out and cannot transfer more data.

    Combine the % Idle Time with Current Disk Queue Length and this will tell you if the drive is getting so busy that it cannot service all the requests for data. As a general guideline, anything over 0 means the drive is probably flat-out busy and anything over 2 means the drive is completely saturated. These rules apply to both SSD and HDD fairly well.

    Also, any value that you read is an instantaneous value at a point in time. You should do a running average over a few results, e.g. take a reading every 100ms and average 5 readings before using the information from the result to make a decision (i.e., waiting until the counters settle before making your next IO request).

    internal DiskUsageMonitor(string driveName)
    {
    
        // Get a list of the counters and look for "C:"
    
        var perfCategory = new PerformanceCounterCategory("PhysicalDisk");
        string[] instanceNames = perfCategory.GetInstanceNames();
    
        foreach (string name in instanceNames)
        {
            if (name.IndexOf("C:") > 0)
            {
                if (string.IsNullOrEmpty(driveName))
                   driveName = name;
            }
        }
    
    
        _readBytesCounter = new PerformanceCounter("PhysicalDisk", 
                                                   "Disk Read Bytes/sec", 
                                                   driveName);
    
        _writeBytesCounter = new PerformanceCounter("PhysicalDisk", 
                                                    "Disk Write Bytes/sec", 
                                                    driveName);
    
        _diskQueueCounter = new PerformanceCounter("PhysicalDisk", 
                                                   "Current Disk Queue Length", 
                                                   driveName);
    
        _idleCounter = new PerformanceCounter("PhysicalDisk",
                                              "% Idle Time", 
                                              driveName);
        InitTimer();
    }
    
    internal event DiskUsageResultHander DiskUsageResult;
    
    private void InitTimer()
    {
        StopTimer();
        _perfTimer = new Timer(_updateResolutionMillisecs);
        _perfTimer.Elapsed += PerfTimerElapsed;
        _perfTimer.Start();
    }
    
    private void PerfTimerElapsed(object sender, ElapsedEventArgs e)
    {
        float diskReads = _readBytesCounter.NextValue();
        float diskWrites = _writeBytesCounter.NextValue();
        float diskQueue = _diskQueueCounter.NextValue();
        float idlePercent = _idleCounter.NextValue();
    
        if (idlePercent > 100)
        {
            idlePercent = 100;
        }
    
        if (DiskUsageResult != null)
        {
            var stats = new DiskUsageStats
                            {
                                    DriveName = _readBytesCounter.InstanceName,
                                    DiskQueueLength = (int)diskQueue,
                                    ReadBytesPerSec = (int)diskReads,
                                    WriteBytesPerSec = (int)diskWrites,
                                    DiskUsagePercent = 100 - (int)idlePercent
                            };
            DiskUsageResult(stats);
        }
    }