1

I have an application that reads a file into memory. It then does a number of checks of this data (checks run concurrently in the background on multiple tasks) before presenting the results to the user.

Although the application doesn't crash due to OutOfMemory exception, I notice that even though the processing is happening in the background, the UI will hitch up and the window will suddenly be "Not Responding". The application will eventually complete the task and display the results but in the meantime the application looks broken and would prompt users to close the window.

After much Googling I'm unsure how to handle this. Is there something I should be doing in order to make sure the the app doesn't become unresponsive? Should I attempt to calculate the estimated memory availability vs usage on startup and alert the user if the machine doesn't have enough RAM? Should I monitor memory usage and do something if available memory drops below a certain amount?

Just to be clear, all of the processing is happening in the background. I'm using Task.Run. I think I can tell that the UI thread isn't being blocked by processing as when I run on a machine with enough memory the UI doesn't hitch.

Snippet

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    TaskList();
}

private async Task TaskList()
{
var taskList = new List<Task>();
txbStatus.Text += "Processing...\n";

taskList.Add(Task.Run(async () =>
{
        await GetFileLists();

    Application.Current.Dispatcher.Invoke(() => { txbStatus.Text += "Get file list completed\n"; });
}));

await Task.WhenAll(taskList.ToArray());
}

private async Task GetFileLists()
{
    var taskList = new List<Task>();

    if (!string.IsNullOrEmpty(TextDirPath))
        taskList.Add(Task.Run(() =>
        {
            _textFilesInFolderPathList =
                FileList.GetFileList(TextDirPath);
        }));
    if (!string.IsNullOrEmpty(ImagesDirPath))
        taskList.Add(Task.Run(() =>
        {
            _imageFilesInFolderPathList =
                FileList.GetFileList(ImagesDirPath));
        }));

    if (!string.IsNullOrEmpty(NativesDirPath))
        taskList.Add(Task.Run(() =>
        {
            _nativesFilesInFolderPathList =
                FileList.GetFileList(NativesDirPath);
        }));

    await Task.WhenAll(taskList.ToArray());
}

Screen Shots

enter image description here enter image description here

windowsgm
  • 1,566
  • 4
  • 23
  • 55
  • Don't do your work on the UI thread. Spin up another thread to handle processing heavy tasks. The UI thread is responsible for keeping the application responsive, make another thread to do your other tasks. You say the processing is happening in the background, does that mean you're already on a different thread? – EMUEVIL Jan 18 '18 at 17:09
  • @EMUEVIL Like I said, everything is happening in the background. I'm using Task.Run. On a machine with more memory the UI doesn't hitch. – windowsgm Jan 18 '18 at 17:13
  • "Just to be clear, all of the processing is happening in the background." Clearly it's not. Your UI thread is frozen because you're doing long running work in that UI thread. You need to not do that. – Servy Jan 18 '18 at 17:19
  • It depends on how you are managing the resources you use in those Tasks. Are all objects correctly disposed when a task ends? The tasks run continuously and use some sort of synchronization to update their results? Maybe post some code, to see what's happening. – Jimi Jan 18 '18 at 17:21
  • code snippet would definitely clear things up ;) – pkuderov Jan 18 '18 at 17:43
  • @Servy This wouldn't explain why it doesn't happen on a machine with sufficient memory. – windowsgm Jan 18 '18 at 17:51
  • @pkuderov Snippet added. – windowsgm Jan 18 '18 at 17:52
  • @windowskm Having enough memory affects how long it takes things to run. If those things are running in the UI thread, it affects how long the UI is unresponsive for. – Servy Jan 18 '18 at 18:14
  • 2
    So, it's WPF. The first `await Task.WhenAll(taskList.ToArray())` is not clear in what context is awaited. Do you need to add that Task to a list? Could you `await Task.Run(async () => (...))` ? And update the UI after it returns? The actual worker, the method that consumes, seems to be `FileList.GetFileList()`, if this is a matter of memory consumption, but you're not showing it. As it is, it looks like a async/await context not properly set. What is calling the code in the first snippet? – Jimi Jan 18 '18 at 18:58
  • @Jimi Call included but not much to it! – windowsgm Jan 22 '18 at 10:12
  • Try this. In the `TaskList()` method, after `txbStatus.Text += "Processing...\n";` remove everything except `await GetFileLists();` and keep just `txbStatus.Text += "..."` after that. Set a breakpoint on this last line before starting the program. Let's see what happens. – Jimi Jan 22 '18 at 11:38
  • @Jimi Still getting a lot of unresponsiveness during this part of the processing with your suggested edits. See screenshots. – windowsgm Jan 23 '18 at 10:38
  • 1
    Memory usage is high (since you only have 2 Gb of RAM) but I don't think it's your problem. The System can reclaim some memory if a running task needs more of it. I'm switching to answer-mode (I need more space to write something down), let's see if we can work this out. – Jimi Jan 23 '18 at 14:39

1 Answers1

0

1) You have some IO-intensive processes that access the disk(s).
2) Your UI becomes non-responsive during this phase.
3) Your UI has another requirement, to show an animated image while those processes are running.
Update:
4) Tests states that the primary problem is the lack of physical memory, which forces the System to swap to page file, while a new sequential IO-bound quasi-concurrent request is made.
This causes stuttering.

I'm proposing the following method to detach the UI from the IO processes that renders it non-responsive.
Test it first as it is, then running your actual Tasks.
Update:
In the current context, Async methods alone can't solve the problem.
The UI stutters because the System does.

Thus, new proposed solution:
Access the large files using a segment (or segments) of virtual memory.
This is achieved using a MemoryMappedFile, which correlates the file and the memory space, letting the mapped portion(s) be treated as primary memory. File Read and Write operations are performed in the same way, but through a MemoryMappedViewAccessor (random access) or a MemoryMappedViewStream (sequential access). A Write operation is performed when the related Accessor is Flushed()
An Accessor can be shared. Processes can access the same Accessor without concurrency.


Class MyTaskResults is used to store results and to pass parameters to the async method responsible for the IO-bound processes.

Edit 1:
The returned value must be a List<string> (?)
(Why using .FirstOrDefault() on the returned List)?

Edit 3:
Added two shared MemoryMappedFile objects to the main class.

public class MyTaskResults
{
    public int TaskID { get; set; }
    public string TextDirPath { get; set; }
    public string ImagesDirPath { get; set; }
    public string NativesDirPath { get; set; }

    public List<string> TextDirPathResult { get; set; }
    public List<string> ImagesDirPathResult { get; set; }
    public List<string> NativesDirPathResult { get; set; }
}

//List of MyTaskResults Class, used to store all Tasks run and their results
List<MyTaskResults> _ListOfTasks = new List<MyTaskResults>();

private static MemoryMappedFile _mmfDatData; 
private static MemoryMappedFile _mmfOptData;

bool CriticalJobRunning = false;
int _TasksCounter = 0;

You can run TaskRunProxy() from any other method, which doesn't need to be an async one.

Edit2:
Moved the call to TaskRunProxy() in MainWindow.Loaded() event handler

private void wMain_Loaded(object sender, RoutedEventArgs e) { TaskRunProxy(); }

Added search directories for TextDirPath, ImagesDirPath, NativesDirPath.
Results:

TextFiles: 765 (*.txt) - ImageFiles: 697 (*.jpg) - NativeFiles: 28422 (*.dll)

Elapsed Time: 88428ms
Initial: Page File Size: 4096 Memory (Working Set): 60.907.520
Final: Page File Size: 4096 Memory (Working Set): 91.385.856

Total: 29884 files found that matched the supplied patterns.
Total Memory: 30.478.336 (~29Mb)

The UI didn't even notice it.
=> With a maxed out memory, the System may be swapping heavily. A test ought to be performed after zeroing-rebooting-rebuilding the system page file (and general clean-up/defrag).

Edit3:
Create 2 Memory Mapped Files correlating the large disk files to the Virtual Memory space:

public MainWindow()
{
    InitializeComponent();

    string _datFilePath = @"PATHTOLARGEFILE";//~200MB
    string _optFilePath = @"PATHTOLARGEFILE2";//~200MB

    Int64 _sizeDatData = new FileInfo(_datFilePath).Length;
    Int64 _sizeOptData = new FileInfo(_optFilePath).Length;

    //Capacity = 0 means a capacity equal to the full size of the file on disk. 
    //Or _sizeDatData and _sizeOptData can be used.
    _mmfDatData = MemoryMappedFile.CreateFromFile(_datFilePath, 
                                    FileMode.Open, 
                                    "DatData", 0, 
                                    MemoryMappedFileAccess.ReadWrite);
    _mmfOptData = MemoryMappedFile.CreateFromFile(_optFilePath, 
                                    FileMode.Open, 
                                    "OptData", 0, 
                                    MemoryMappedFileAccess.ReadWrite);
}


Read and write operations: (Random access)
[TYPE] can be a Reference Type or a Value Type (structs are of course included)
The example uses the first 128 MB for Read/Write operations.

    MemoryMappedViewAccessor _viewOptData = _mmfOptData.CreateViewAccessor(
                             0, 
                             0x8000000L, 
                             MemoryMappedFileAccess.ReadWrite);

    _viewOptData.Read<[TYPE]>([Position], out [TYPE]);
    _viewOptData.Write<[TYPE]>([Position], ref [TYPE]);


Run the IO-bound Tasks after the UI is presented.

private void wMain_Loaded(object sender, RoutedEventArgs e)
{
    TaskRunProxy();
}


private async void TaskRunProxy()
{
    _TasksCounter += 1;

    MyTaskResults _Task = new MyTaskResults
    {
        TaskID = _TasksCounter,
        TextDirPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
        ImagesDirPath = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
        NativesDirPath = Environment.GetFolderPath(Environment.SpecialFolder.Windows)
    };

        Console.WriteLine("Page File Size: " + Environment.SystemPageSize.ToString());
        Console.WriteLine("Memory (Working Set): " + Environment.WorkingSet.ToString());

        Stopwatch _SW = new Stopwatch();
        _SW.Start();
        CriticalJobRunning = true;
        _ListOfTasks.Add(await GetFileListsAsync(_Task));
        CriticalJobRunning = false;
        _SW.Stop();

        Console.WriteLine("Time: " + _SW.ElapsedMilliseconds + Environment.NewLine);

        Console.WriteLine("TextFiles: " +  _Task.TextDirPathResult.Count + 
                        "  ImageFiles: " + _Task.ImagesDirPathResult.Count + 
                        "  NativeFiles: " + _Task.NativesDirPathResult.Count);

        Console.WriteLine("Page File Size: " + Environment.SystemPageSize.ToString());
        Console.WriteLine("Memory (Working Set): " + Environment.WorkingSet.ToString());
}

private void wMain_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    if (this.CriticalJobRunning)
        e.Cancel = true;
    //Let the use know
}

The async method used to run the (probably synchronous) Tasks:

Edit1:
Return value changed to List<string>
Edit2:
Changed dummy Thread.Sleep(x) with "real-life" file enumeration + memory loading:

Directory.GetFiles(_Task.[PATH], "[PATTERN]", SearchOption.AllDirectories).ToList<string>();

private async Task<MyTaskResults> GetFileListsAsync(MyTaskResults _Task)
{
    if (!string.IsNullOrEmpty(_Task.TextDirPath))
        _Task.TextDirPathResult = await Task.Run(() =>
        {
            return Directory.GetFiles(_Task.TextDirPath, 
                                     "*.txt", 
                                      SearchOption.AllDirectories).ToList<string>();
            //Thread.Sleep(4000);
            //return new List<string> {TextDirPathResult Completed"};
            //return FileList.GetFileList(_Task.TextDirPath);
        });

    if (!string.IsNullOrEmpty(_Task.ImagesDirPath))
        _Task.ImagesDirPathResult = await Task.Run(() =>
        {
            return Directory.GetFiles(_Task.ImagesDirPath, 
                                     "*.jpg", 
                                      SearchOption.AllDirectories).ToList<string>();
            //Thread.Sleep(3000);
            //return new List<string> {"TextDirPathResult Completed"};
            //return FileList.GetFileList(_Task.ImagesDirPath);
        });

    if (!string.IsNullOrEmpty(_Task.NativesDirPath))
        _Task.NativesDirPathResult = await Task.Run(() =>
        {
            return Directory.GetFiles(_Task.NativesDirPath, 
                                     "*.dll", 
                                      SearchOption.AllDirectories).ToList<string>();
            //Thread.Sleep(3000);
            //return new List<string> {"TextDirPathResult Completed"};
            //return FileList.GetFileList(_Task.NativesDirPath);
        });

    return _Task;
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • I converted this to a WPF app and run and noticed no unresponsiveness. Then to further simulate my app, I read the two very large files that my app has in memory at this point which maxes out the 2GB memory. This does cause the above code to hitch and become unresponsive for a time before completing. – windowsgm Jan 23 '18 at 17:57
  • For added info, I also have updated the paths so it is actually running against populated folders. At the time of starting TaskRunProxy while have the files in memory, Task Mgr lists Physical Memory; Total: 2047; Cached: 166; Available: 165; Free 0. – windowsgm Jan 23 '18 at 17:59
  • I can make the update code available to you via PasteBin if that'd help? – windowsgm Jan 23 '18 at 18:02
  • @windowskm Why not. Paste away. -- If you max out your ram in a short term, the memory swapping can cause this, so tweaking the system swap file settings may help. But this one after. – Jimi Jan 23 '18 at 18:19
  • https://fnpaste.com/jnR3 Its a bit thrown together but you'll get the idea. Comment `TaskRunProxy` and uncomment `TaskList` to run the original structure I had. Timer is used to simulate UI updating. – windowsgm Jan 23 '18 at 18:26
  • @windowskm What are your Target Framework and VS version (I have 2013 and 2017 available)? – Jimi Jan 23 '18 at 18:51
  • @windowskm I need info on some of your object/methods. What `OpenFileClass()` actually does (you didn't include it). In your project, does it actually execute its `OpenFile()` method in the MainWindow Init process? `FileList.GetFileList()`, is it a wrapper for `Directory.EnumerateFiles()`, so it returns a (IEnumerable) List ? What's `Console`?. Is it a custom Console-like panel to show progress? Why do you need to `Dispatcher.Invoke()` to "talk" to it. Isn't it in the same thread as the main window? Or not? The `timerMemoryWatcher_Elapsed` event is. – Jimi Jan 24 '18 at 01:55
  • Apologies, more detail was needed. Console was just the lazy name I gave to a Listbox, GetFileList code can actually be found here in another question (https://stackoverflow.com/questions/47471744/very-fast-filelist-limitations). Dispatcher used otherwise the timer throws "..thread cannot access this object because a different thread owns it". OpenFile is running in the Initialiser, I can provide this code also if needed, but I think its significance is the memory occupation. – windowsgm Jan 24 '18 at 10:21
  • Well, I suppose `OpenFile()` ...opens a file. Then loads it in memory (does it?). Try the code with the edits (leave out the timer and don't load those files in memory), so we are on the same page. From what I see, and also knowing what `GetFileList()` does, I really suspect that the System can't swap the required memory to the page file, being too busy to fulfill your disk access request, hence the stuttering. Possibly, this time, speed is counterproductive. Maybe, it could be an idea to delay the enumeration proc. to give the System time to allocate the memory that `OpenFile()` requires. – Jimi Jan 24 '18 at 12:01
  • So I've ran your edited code (slightly modified to run as WPF) with and without the files loaded into memory, with restarting the machine in between. Stuttering occurs only with the files loaded into memory. Here's the results (page file (4096) and file counts are obviously the same in both cases); W/ filesloaded; Memory before (working set): 1264074752 Time: 139010 Memory after (working set): 679620608 W/o filesloaded; Memory before (working set): 31981568 Time: 76256 Memory after (working set): 699363328 – windowsgm Jan 24 '18 at 16:25
  • 1
    Those memory results are quite off: 1.2G before, 680Mb after and 32Mb before, 700Mb after? However, it's pretty clear, memory becomes scarce :). Is it possible for you to access those large files as [Memory Mapped Files](https://msdn.microsoft.com/en-us/library/system.io.memorymappedfiles.memorymappedfile(v=vs.110).aspx))? Performace should improve, especially in this scenario. Also, have you tested the result with a delay (10~15 sec.) between the file loading proc. and the directories enumeration proc.? (BTW, I tested that code as it is in a WPF project) – Jimi Jan 24 '18 at 17:38
  • Yeah, they seemed like odd results to me too. :/ I'll try with the delay and report results! Also, I hadn't come across Memory Mapped Files before. Interesting. – windowsgm Jan 24 '18 at 17:45
  • I tried to load in memory 3 files, 3+ GB each, in a 8 GB (16Gb Fixed Size Swap File), I5 4690K machine to test what the UI had to say. Well, not a peep. Ran it outside VS. Compiled as Release. – Jimi Jan 24 '18 at 17:51
  • Huh interesting, what do you mean by "(16Gb Fixed Size Swap File)"? Also, I believe my numbers above do make sense after all. I realised the before figure is after I have the files loaded into memory! – windowsgm Jan 24 '18 at 18:05
  • I mean that the Swap file size has been set to manual, with a Min = Max size of 16GB (so, fixed). The off part to me is that the "after" is smaller that the "before" in the "with" part; in the w/o part, memory increase is extreme (but I don't know how many files/dirs paths you have loaded. It's however very high. Unless it's a typo:). – Jimi Jan 24 '18 at 18:15
  • So I built+released it and have run it on two machines, 8GB Win10 (16GB Page file) & 2GB Win7. I added in a StoryBoard animation in XAML so I can easily see when the hitching is occurring. For both machines, the window didn't say 'not responding' (I assume because the hitch didn't last long enough?) but for brief moments I could not move the window and the animation had stopped. At the same time I had task manager open and could see it coincided with when the memory usage would be >95%. I'm at a loss as to what to try next. :/ – windowsgm Jan 25 '18 at 14:21
  • Have you seen the updates? About the MemoryMappedFile? Give it a try. I don't know what you are doing with those file, but it shouldn't matter much. If you still want to open/load them the "traditional" way (even if I can't say why you have to keep them loaded), do that in another thread too, not in main window initiaiization proc. Maybe, give more details on what's happening, because I can't replicate this on a 8Gb machine. – Jimi Jan 25 '18 at 14:34
  • I'll try out the Memory Mapped File. Initially it seems to load into memory weirdly quickly, is that expected? Also, if you are running this in WPF how are you getting Console.WriteLine working? – windowsgm Jan 25 '18 at 16:22
  • @windowskm Yes, it's expected. It doesn't load anything in memory. Only the needed page(s) when you modify the file with is Accessor. A Virtual Memory space is created on disk. Think of something like Isolated Storage. More efficient, but somewhat slower when it come to write to the actual file. Remember to `Dispose()` of those objects. The MemoryMappedFiles on exit. About the Console. I'm no expert in WPF, I just wrote `Console.WriteLine()` and it writes. Shouldn't it? – Jimi Jan 25 '18 at 16:27
  • So once I have the files loaded in (as DataTables) and the filelists are complete I have to do a lot of comparisons of the filelists and the DataTables. For this reason I don't think I'll be able to escape having the files in memory. RE Console.WriteLine I don't believe it would work ootb, you can change the properties of the WPF project to Console and that would work! – windowsgm Jan 25 '18 at 17:55
  • Unless there is someway to interact with the Memory Mapped File as a DataTable. Otherwise its going to be very complicated trying to reference columns of data from the Memory Mapped File, correct? – windowsgm Jan 25 '18 at 17:57
  • So, we're down to this line: `_loadedDatData = ofc.OpenFile(_datFilePath);` DataTable in a in-Memory Object, if you *have* to use this tool, well, that's it. But the source file, the data, what is it? Can you be more specific on what you're dealing with? Also, you should mention the FW you're targeting. And VS/C# version. Here (since I saw the Win7 TaskManager, I've build a solution in that system) I'm using VS 2013, .NET FW 4.7.1, C# 5.0. Again about the Console: if I create a new WPF project, the Output window is there and Console.WriteLine() is available and working as usual. – Jimi Jan 26 '18 at 09:38
  • This answer and comment section is turning into a conversation. This is not how Stack Overflow works. – Emond Feb 05 '18 at 18:29
  • @Erno de Weerd Yes, you're right. As you can see from the dates, this weird in-progress answer has probably come to an end. Also, as it is, it is not that useful for anyone either. I will probably (read surely) delete it. – Jimi Feb 05 '18 at 18:38