SearchAllSubDirectories Inefficient Under Load - vb.net

To detail the problem a bit here, I need to obtain the file "DKIM.txt" hundreds, potentially thousands of times from within different directories.
The file will always appear in a folders such as:
C:\CONX\Users\Jason\222\DKIM.txt
C:\CONX\Users\Donald\12\DKIM.txt
C:\CONX\Users\Yuri\1251\DKIM.txt
The folder depth will never change, the username and the identifier (eg. Jason, and 222) will always change.
My currently working code is as follows:
For Each UserDirectory As String In My.Computer.FileSystem.GetFiles("C:\CONX\Users", FileIO.SearchOption.SearchAllSubDirectories, "DKIM.txt")
Console.WriteLine(UserDirectory)
Next
The problem with the above is it's slow on our extremely populated machines. Their load at times can be 80-90% CPU and simply cycling through all the sub directories to search for one file we know will always exist is inefficient and slow.
So my question is, how would I just wildcard the directory username and identifier.
Example: Return all directories that match: C:\CONX\Users\*\*\DKIM.txt where * is our wildcard.
Thank you.

Depending on the number of sub directory, it could be faster to do your own custom search. You could do that search yourself with GetDirectory and GetFiles.
Loop All directory under C:\CONX\Users
Loop All sub-directory
Check if that sub-directory contains the file.
This also depends if each username have multiple directory under them. I would also suggest to cache the result. If you only need to run it once, it might not matter that it's slow.
Also, Console.WriteLine is very slow. You could write in a file instead you might see some improvement.

As Hans Passant pointed out the flaws so eloquently I'm going to go ahead and propose what I think can work. Here it goes.
Use a program to monitor your \Users folder for any file changes (created/deleted) and add a trigger to run a batch script to update a text file where you maintain all your users and identifier. You can write your own window process (daemon) if you like for this or use an off the shelf software like Watch 4 Folder. Here is a guide for that describes it's usability http://www.guidingtech.com/9861/automate-folder-actions-windows-watch-4-folder/
Now you will just be searching the new text file (master list) as opposed to traversing the entire directory and sub directory structure. It will be better if you can store the master list of user/identifier in the database and use a batch script or powershell to update the database.
Good luck.

Solution: Search with a predefined depth limit while also matching with a filename. This will eliminate wasted CPU cycles recursively searching for the file we knew existed in each directory.
Public Sub GetFiles(ByVal strFileFilter As String, ByVal strDirectory As String, ByVal intDepthLimit As Integer, ByVal intCurrentDepth As Integer)
Dim folderInfo As New DirectoryInfo(strDirectory)
' Is the current depth on this recursion less than our limit?
' If so, find any directories and get into them by calling GetFiles recursively (incrementing depth count)
If intCurrentDepth < intDepthLimit Then
Dim directories() As DirectoryInfo
directories = folderInfo.GetDirectories()
For Each fDirectory In directories
' Recursively call ourselves incrementing the depth using the given folder path.
GetFiles(strFileFilter, fDirectory.FullName, intDepthLimit, intCurrentDepth + 1)
Next
End If
' After we can't go further down, add any files which match our filter to listbox (in this case lstFiles)
Dim files() As FileInfo
files = folderInfo.GetFiles(strFileFilter)
For Each fFile In files
lstFiles.Items.Add(fFile.FullName)
Next
End Sub
Source: http://www.coderslexicon.com/playing-with-recursive-directory-diving-in-vb-net/
eg. GetFiles("DKIM.txt", "C:\CONX\Users", 3, 0)
Dropped the execution time for hundreds of large directories from multiple seconds using FileIO.SearchOption.SearchAllSubDirectories to less than half a second.

Related

How to limit GetFiles to get all paths created after a certain date?

I'm a beginner in VB and I'm throwing together a quick tool to pull data out of excel sheets into an SQL server.
The actual opening/manipulation of the excel files I can do, but I would like to limit the files I'm dealing with based on the created date, but I'm struggling with googling a solution
So in order to get all of the paths, I'm simply using:
Dim fname As String
For Each file As String In Directory.GetFiles(pathtoscan)
fname = Path.GetFileName(file)
Which works fine to get everything (writing to the SQL server table as I go), but of course the above means getting every single path, where as I'd like to "optimise" it by only getting paths created after a certain defined date.
Is this doable, or would it simply be a matter of filtering after grabbing all paths anyway, and thus mean no better "performance"?
Many thanks in advance.
You can use LINQ and File.GetCreationTime:
Dim relevantFiles = From f In Directory.EnumerateFiles(pathtoscan)
Where File.GetCreationTime(f) > yourDate
For Each file As String In relevantFiles
' ... '
Next
Also use EnumerateFiles instead of GetFiles. The latter returns an array of all files before you start filtering them. The former returns one after the other.

VBA - Excel Unable to dettect file size increase

This one is driving me crazy. Any help is welcome.
I have a numeric simulaiton that constantly writes to a text file. The file size increases constantly. I need to report file size and check if it's still increasing or if it has ceased increasing.
VBA can't seem to detect the file's increased size, it keeps registering the same size. But, if I have windows explorer oppened on the file folder and press F5, the size increases in VBA.
I need to know if it is supposed to work this way because how windows indexes files or if I'm doing something wrong.
I have used filelen(), filesystemobject.filezise(), datelastmodified(),datelastacessed() and nothing...
I've already found a workarround: If I copy the file in question to a temp file and then read the temp file size, I can detect the change in size. But this is an ugly solution. I would very much like to just check the file size and get the correct result.
Sorry if it isn't very clear. I will be happy to clarify further, should it be necessary.
Here is the code I've been using, but to test it you would have to mimic the file writing situation, like in a video encoding for example, because if you do it with a text file edited in notepad, the act of saving it also updates the information to vba.
Public fileSizeLastStep as long
Public Sub sizeWatch()
dim fso as new fileSystemObject
dim fileToMeasure as File
dim fileSizeNow as long
Set fileToMeasure = fso.GetFile("C:\filename.txt")
fileSizeNow = fileToMeasure.Size
If fileSizeNow <> fileSizeLastStep Then
fileSizeLastStep = fileSizeNow
Set fso = Nothing
Call Application.OnTime(Now + TimeValue("00:02:00"), "sizeWatch")
Else
MsgBox "Simulation Finished!"
'calls whathever function that starts next simulation
End If
End Sub
Thank you for your time.
Perhaps it's an issue with this:
https://blogs.msdn.microsoft.com/oldnewthing/20111226-00/?p=8813
Quote from the article:
If you really need the actual file size right now, you can do what
the first customer did and call GetĀ­FileĀ­Size. That function operates
on the actual file and not on the directory entry, so it gets the real
information and not the shadow copy. Mind you, if the file is being
continuously written-to, then the value you get is already wrong the
moment you receive it.
So try using GetFileSize instead.
Declare Function GetFileSize Lib "kernel32.dll" (ByVal hFile As Long, lpFileSizeHigh As Long) As Long

Limiting the amount of files grabbed from system.io.directory.getfiles

I've got a folder browser dialogue populating the directory location (path) of a system.io.directory.getfiles. The issue is if you accidentally select a folder with hundereds or thousands of files (which there's no reason you would ever need this for this app) it will lock up the app while it grabs all the files. All I'm grabbing are the directory locations as strings and want to put a limit on the amount of files that can be grabbed. Here's my current code that isn't working.
If JigFolderBrowse.ShowDialog = DialogResult.OK Then
Dim dirs(50) As String
dirs = System.IO.Directory.GetFiles(JigFolderBrowse.SelectedPath.ToString, "*", System.IO.SearchOption.AllDirectories)
If dirs.Length> 50 Then
MsgBox("Too Many Files Selected" + vbNewLine + "Select A Smaller Folder To Be Organized")
Exit Sub
End If
'Seperate Each File By Type
For i = 0 To dirs.Length - 1
If Not dirs(i).Contains("~$") Then
If dirs(i).Contains(".SLDPRT") Or dirs(i).Contains(".sldprt") Then
PartsListBx.Items.Add(dirs(i))
ElseIf dirs(i).Contains(".SLDASM") Or dirs(i).Contains(".sldasm") Then
AssemListBx.Items.Add(dirs(i))
ElseIf dirs(i).Contains(".SLDDRW") Or dirs(i).Contains(".slddrw") Then
DrawingListBx.Items.Add(dirs(i))
ElseIf dirs(i).Contains(".pdf") Or dirs(i).Contains(".PDF") Then
PDFsListBx.Items.Add(dirs(i))
ElseIf dirs(i).Contains(".DXF") Or dirs(i).Contains(".dxf") Then
DXFsListBx.Items.Add(dirs(i))
ElseIf Not dirs(i).Contains(".db") Then
OtherFilesListBx.Items.Add(dirs(i))
End If
End If
The Directory.GetFiles method always retrieves the full list of matching files before returning. There is no way to limit it (outside of specifying a more narrow search pattern, that is). There is, however, the Directory.EnumerateFiles method which does what you need. From the MSDN article:
The EnumerateFiles and GetFiles methods differ as follows: When you use EnumerateFiles, you can start enumerating the collection of names before the whole collection is returned; when you use GetFiles, you must wait for the whole array of names to be returned before you can access the array. Therefore, when you are working with many files and directories, EnumerateFiles can be more efficient.
So, for instance, you could do something like this:
dirs = Directory.
EnumerateFiles(
JigFolderBrowse.SelectedPath.ToString(),
"*",
SearchOption.AllDirectories).
Take(50).
ToArray()
Take is a LINQ extension method which returns only the first x-number of items from any IEnumerable(Of T) list. So, in order for that line to work, you'll need to import the System.Linq namespace. If you can't, or don't want to, use LINQ, you can just implement your own method that does the same sort of thing (iterates an IEnumerable list in a for loop and returns after reading only the first 50 items).
Side Note 1: Unused Array
Also, it's worth mentioning, in your code, you initialize your dirs variable to point to a 50-element string array. You then, in the very next line, set it to point to a whole new array (the one returned by the Directory.GetFiles method). While it's not breaking functionality, it is unnecessarily inefficient. You're creating that extra array, just giving the garbage collector extra work to do, for no reason. You never use that first array. It just gets dereferenced and discarded in the very next line. It would be better to create the array variable as null:
Dim dirs() As String
Or
Dim dirs() As String = Nothing
Or, better yet:
Dim dirs() As String = Directory.
EnumerateFiles(
JigFolderBrowse.SelectedPath.ToString(),
"*",
SearchOption.AllDirectories).
Take(50).
ToArray()
Side Note 2: File Extension Comparisons
Also, it looks like you are trying to compare the file extensions in a case-insensitive way. There are two problems with the way you are doing it. First, you only comparing it against two values: all lowercase (e.g. ".pdf") and all uppercase (e.g. ".PDF). That won't work with mixed-case (e.g. ".Pdf").
It is admittedly annoying that the String.Contains method does not have a case-sensitivity option. So, while it's a little hokey, the best option would be to make use of the String.IndexOf method, which does have a case-insensitive option:
If dirs(i).IndexOf(".pdf", StringComparison.CurrentCultureIgnoreCase) <> -1 Then
However, the second problem, which invalidates my last point of advice, is that you are checking to see if the string contains the particular file extension rather than checking to see if it ends with it. So, for instance, a file name like "My.pdf.zip" will still match, even though it's extension is ".zip" rather than ".pdf". Perhaps this was your intent, but, if not, I would recommend using the Path.GetExtension method to get the actual extension of the file name and then compare that. For instance:
Dim ext As String = Path.GetExtension(dirs(i))
If ext.Equals("pdf", StringComparison.CurrentCultureIgnoreCase) Then
' ...

VB.NET (2013) - Check string against huge file

I have a text file that is 125Mb in size, it contains 2.2 million records. I have another text file which doesn't match the original but I need to find out where it differs. Normally, with a smaller file I would read each line and process it in some way, or read the whole file into a string and do likewise, however the two files are too big for that and so I would like to create something to achieve my goal. Here's what I currently have.. excuse the mess of it.
Private Sub refUpdateBtn_Click(sender As Object, e As EventArgs) Handles refUpdateBtn.Click
Dim refOrig As String = refOriginalText.Text 'Original Reference File
Dim refLatest As String = refLatestText.Text 'Latest Reference
Dim srOriginal As StreamReader = New StreamReader(refOrig) 'start stream of original file
Dim srLatest As StreamReader = New StreamReader(refLatest) 'start stream of latest file
Dim recOrig, recLatest, baseDIR, parentDIR, recOutFile As String
baseDIR = vb.Left(refOrig, InStrRev(refOrig, ".ref") - 1) 'find parent folder
parentDIR = Path.GetDirectoryName(baseDIR) & "\"
recOutFile = parentDIR & "Updated.ref"
Me.Text = "Processing Reference File..." 'update the application
Update()
If Not File.Exists(recOutFile) Then
FileOpen(55, recOutFile, OpenMode.Append)
FileClose(55)
End If
Dim x As Integer = 0
Do While srLatest.Peek() > -1
Application.DoEvents()
recLatest = srLatest.ReadLine
recOrig = srOriginal.ReadLine ' check the original reference file
Do
If Not recLatest.Equals(recOrig) Then
recOrig = srOriginal.ReadLine
Else
FileOpen(55, recOutFile, OpenMode.Append)
Print(55, recLatest & Environment.NewLine)
FileClose(55)
x += 1
count.Text = "Record No: " & x
count.Refresh()
srOriginal.BaseStream.Seek(0, SeekOrigin.Begin)
GoTo 1
End If
Loop
1:
Loop
srLatest.Close()
srOriginal.Close()
FileClose(55)
End Sub
It's got poor programming and scary loops, but that's because I'm not a professional coder, just a guy trying to make his life easier.
Currently, this uses a form to insert the original file and the latest file and outputs each line that matches into a new file. This is less than perfect, but I don't know how to cope with the large file sizes as streamreader.readtoend crashes the program. I also don't need the output to be a copy of the latest input, but I don't know how to only output the records it doesn't find. Here's a sample of the records each file has:
doc:ARCHIVE.346CCBD3B06711E0B40E00163505A2EF
doc:ARCHIVE.346CE683B29811E0A06200163505A2EF
doc:ARCHIVE.346CEB15A91711E09E8900163505A2EF
doc:ARCHIVE.346CEC6AAA6411E0BEBB00163505A2EF
The program I have currently works... to a fashion, however I know there are better ways of doing it and I'm sure much better ways of using the CPU and memory, but I don't know this level of programming. All I would like is for you to take a look and offer your best answers to all or some of the code. Tell me what you think will make it better, what will help with one line, or all of it. I have no time limit on this because the code works, albeit slowly, I would just like someone to tell me where my code could be better and what I could do to get round the huge file sizes.
Your code is slow because it is doing a lot of file IO. You're on the right track by reading one line at a time, but this can be improved.
Firstly, I've created some test files based off the data that you provided. Those files contain three million lines and are about 130 MB in size (2.2 million records was less than 100 MB so I've increased the number of lines to get to the file size that you state).
Reading the entire file into a single string uses up about 600 MB of memory. Do this with two files (which I assume you were doing) and you have over 1GB of memory used, which may have been causing the crash (you don't say what error was shown, if any, when the crash occurred, so I can only assume that it was an OutOfMemoryException).
Here's a few tips before I go through your code:
Use Using Blocks
This won't help with performance, but it does make your code cleaner and easier to read.
Whenever you're dealing with a file (or anything that implements the IDisposable interface), it's always a good idea to use a Using statement. This will automatically dispose of the file (which closes the file), even if an error happens.
Don't use FileOpen
The FileOpen method is outdated (and even stated as being slow in its documentation). There are better alternatives that you are already (almost) using: StreamWriter (the cousin of StreamReader).
Opening and closing a file two million times (like you are doing inside your loop) won't be fast. This can be improved by opening the file once outside the loop.
DoEvents() is evil!
DoEvents is a legacy method from back in the VB6 days, and it's something that you really want to avoid, especially when you're calling it two million times in a loop!
The alternative is to perform all of your file processing on a separate thread so that your UI is still responsive.
Using a separate thread here is probably overkill, and there are a number of intricacies that you need to be aware of, so I have not used a separate thread in the code below.
So let's look at each part of your code and see what we can improve.
Creating the output file
You're almost right here, but you're doing some things that you don't need to do. GetDirectoryName works with file names, so there's no need to remove the extension from the original file name first. You can also use the Path.Combine method to combine a directory and file name.
recOutFile = Path.Combine(Path.GetDirectoryName(refOrig), "Updated.ref")
Reading the files
Since you're looping through each line in the "latest" file and finding a match in the "original" file, you can continue to read one line at a time from the "latest" file.
But instead of reading a line at a time from the "original" file, then seeking back to the start when you find a match, you will be better off reading all of those lines into memory.
Now, instead of reading the entire file into memory (which took up 600 MB as I mentioned earlier), you can read each line of the file into an array. This will use up less memory, and is quite easy to do thanks to the File class.
originalLines = File.ReadAllLines(refOrig)
This reads all of the lines from the file and returns a String array. Searching through this array for matches will be slow, so instead of reading into an array, we can read into a HashSet(Of String). This will use up a bit more memory, but it will be much faster to seach through.
originalLines = New HashSet(Of String)(File.ReadAllLines(refOrig))
Searching for matches
Since we now have all of the lines from the "original" line in an array or HashSet, searching for a line is very easy.
originalLines.Contains(recLatest)
Putting it all together
So let's put all of this together:
Private Sub refUpdateBtn_Click(sender As Object, e As EventArgs)
Dim refOrig As String
Dim refLatest As String
Dim recOutFile As String
Dim originalLines As HashSet(Of String)
refOrig = refOriginalText.Text 'Original Reference File
refLatest = refLatestText.Text 'Latest Reference
recOutFile = Path.Combine(Path.GetDirectoryName(refOrig), "Updated.ref")
Me.Text = "Processing Reference File..." 'update the application
Update()
originalLines = New HashSet(Of String)(File.ReadAllLines(refOrig))
Using latest As New StreamReader(refLatest),
updated As New StreamWriter(recOutFile, True)
Do
Dim line As String
line = latest.ReadLine()
' ReadLine returns Nothing when it reaches the end of the file.
If line Is Nothing Then
Exit Do
End If
If originalLines.Contains(line) Then
updated.WriteLine(line)
End If
Loop
End Using
End Sub
This uses around 400 MB of memory and takes about 4 seconds to run.

A faster way to read lines in text files quickly

My application is looking at huge text files (upwards to half a million lines) from a proxy server log. The problem is that a normal StreamRead iteration of the logs can take an excessive amount of time to process, so I'm looking for something faster.
On the form, the user picks the file they need to parse and enters up to three site filters to check for. The application then opens the file and begins to parse the date stamp and website URL from each line in the file. The average speed is about two lines per second, so for a file with 200,000 lines in it, this process will take about 28 hours to process a file.
I've been reading on the Task class, and I'm thinking this would probably be the route to take, but Microsoft doesn't give a very good example, so how can I can accomplish it?
I think you could use File.ReadLines() when reading large files.
According to MSDN :
The ReadLines and ReadAllLines methods differ as follows: When you use ReadLines, you can start enumerating the collection of strings before the whole collection is returned; when you use ReadAllLines, you must wait for the whole array of strings be returned before you can access the array. Therefore, when you are working with very large files, ReadLines can be more efficient.
For more detail, see MSDN File.ReadLines()
Instead of guessing about why it is slow, is it reading the file, processing the lines, etc. start by measuring how long it takes to read the file line-by-line.
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim stpw As New Stopwatch
Dim path As String = "path to your file here"
Dim sr As New IO.StreamReader(path)
Dim linect As Integer = 0
stpw.Restart()
Do While Not sr.EndOfStream
Dim s As String = sr.ReadLine
linect += 1
Loop
stpw.Stop()
sr.Close()
Debug.WriteLine(stpw.Elapsed.ToString)
Debug.WriteLine(linect)
End Sub
I ran this against a test file I have that is 20MB. It is close to 3,000,000 lines long(the lines are very short). It took about .3 of a second to run.
After you run this you will know whether the problem is the read or the processing, or both.
Thanks, dbasnett... the results were:
00:00:00.6991336
172900
Believe it or not, I found the problem. I had the textbox inside a GroupBox and was using the GroupBox.Text property to update statistics back to the user, using GroupBox.Refresh() to update the line x of y and matches found, etc. so the user had some idea of what was being found.
By leaving that information out and putting in a progress bar, the speed of the scans went up exponentially. Using 3 filters, I was able to parse 172900 lines in a matter of 3:19 minutes:
Scan complete!
Process complete!
Scanned 172900 lines out of 172900 lines.
Percentage (icc): 0.0052% (900 matches)
Percentage (facebook): 0.0057% (988 matches)
Percentage (illinois): 0.0005% (95 matches)
Total Matches: 1983
Elapsed Time: 00:03:19.1088851