Skip to content

Commit cb676eb

Browse files
committed
Fixed #14306 and tested with 15K, 30K and 60K records. Chunking + async/await is a reliable pattern for large data processing.
1 parent 2a154dc commit cb676eb

File tree

1 file changed

+83
-62
lines changed

1 file changed

+83
-62
lines changed

src/Files.App/Utils/Storage/Operations/FileSizeCalculator.cs

Lines changed: 83 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -25,82 +25,103 @@ public FileSizeCalculator(params string[] paths)
2525

2626
public async Task ComputeSizeAsync(CancellationToken cancellationToken = default)
2727
{
28-
await Parallel.ForEachAsync(
29-
_paths,
30-
cancellationToken,
31-
async (path, token) => await Task.Factory.StartNew(() =>
32-
{
33-
ComputeSizeRecursively(path, token);
34-
},
35-
token,
36-
TaskCreationOptions.LongRunning,
37-
TaskScheduler.Default));
28+
const int ChunkSize = 1000;
29+
var queue = new Queue<string>(_paths);
30+
var batch = new List<string>(ChunkSize);
3831

39-
unsafe void ComputeSizeRecursively(string path, CancellationToken token)
32+
while (!cancellationToken.IsCancellationRequested && queue.TryDequeue(out var currentPath))
4033
{
41-
var queue = new Queue<string>();
42-
if (!Win32Helper.HasFileAttribute(path, FileAttributes.Directory))
34+
if (!Win32Helper.HasFileAttribute(currentPath, FileAttributes.Directory))
4335
{
44-
ComputeFileSize(path);
36+
batch.Add(currentPath);
4537
}
4638
else
4739
{
48-
queue.Enqueue(path);
49-
50-
while (queue.TryDequeue(out var directory))
40+
try
5141
{
52-
WIN32_FIND_DATAW findData = default;
53-
54-
fixed (char* pszFilePath = directory + "\\*.*")
42+
foreach (var file in Directory.EnumerateFiles(currentPath))
5543
{
56-
var hFile = PInvoke.FindFirstFileEx(
57-
pszFilePath,
58-
FINDEX_INFO_LEVELS.FindExInfoBasic,
59-
&findData,
60-
FINDEX_SEARCH_OPS.FindExSearchNameMatch,
61-
null,
62-
FIND_FIRST_EX_FLAGS.FIND_FIRST_EX_LARGE_FETCH);
63-
64-
if (!hFile.IsNull)
44+
if (cancellationToken.IsCancellationRequested)
45+
break;
46+
batch.Add(file);
47+
if (batch.Count >= ChunkSize)
6548
{
66-
do
67-
{
68-
FILE_FLAGS_AND_ATTRIBUTES attributes = (FILE_FLAGS_AND_ATTRIBUTES)findData.dwFileAttributes;
69-
70-
if (attributes.HasFlag(FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_REPARSE_POINT))
71-
// Skip symbolic links and junctions
72-
continue;
73-
74-
var itemPath = Path.Combine(directory, findData.cFileName.ToString());
75-
76-
// Skip current and parent directory entries
77-
var fileName = findData.cFileName.ToString();
78-
if (fileName.Equals(".", StringComparison.OrdinalIgnoreCase) ||
79-
fileName.Equals("..", StringComparison.OrdinalIgnoreCase))
80-
{
81-
continue;
82-
}
83-
84-
if (attributes.HasFlag(FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY))
85-
{
86-
queue.Enqueue(itemPath);
87-
}
88-
else
89-
{
90-
ComputeFileSize(itemPath);
91-
}
92-
93-
if (token.IsCancellationRequested)
94-
break;
95-
}
96-
while (PInvoke.FindNextFile(hFile, &findData));
97-
98-
PInvoke.FindClose(hFile);
49+
ComputeFileSizeBatch(batch);
50+
batch.Clear();
51+
await Task.Yield();
9952
}
10053
}
10154
}
55+
catch (UnauthorizedAccessException) { }
56+
catch (IOException) { }
57+
#if DEBUG
58+
catch (Exception ex)
59+
{
60+
System.Diagnostics.Debug.WriteLine(ex);
61+
}
62+
#endif
63+
64+
try
65+
{
66+
foreach (var dir in Directory.EnumerateDirectories(currentPath))
67+
{
68+
if (cancellationToken.IsCancellationRequested)
69+
break;
70+
queue.Enqueue(dir);
71+
}
72+
}
73+
catch (UnauthorizedAccessException) { }
74+
catch (IOException) { }
75+
#if DEBUG
76+
catch (Exception ex)
77+
{
78+
System.Diagnostics.Debug.WriteLine(ex);
79+
}
80+
#endif
81+
82+
}
83+
84+
if (batch.Count >= ChunkSize)
85+
{
86+
ComputeFileSizeBatch(batch);
87+
batch.Clear();
88+
await Task.Yield();
10289
}
10390
}
91+
92+
if (batch.Count > 0)
93+
{
94+
ComputeFileSizeBatch(batch);
95+
batch.Clear();
96+
}
97+
}
98+
99+
private void ComputeFileSizeBatch(IEnumerable<string> files)
100+
{
101+
long batchTotal = 0;
102+
foreach (var path in files)
103+
{
104+
if (_computedFiles.ContainsKey(path))
105+
continue;
106+
107+
using var hFile = PInvoke.CreateFile(
108+
path,
109+
(uint)FILE_ACCESS_RIGHTS.FILE_READ_ATTRIBUTES,
110+
FILE_SHARE_MODE.FILE_SHARE_READ,
111+
null,
112+
FILE_CREATION_DISPOSITION.OPEN_EXISTING,
113+
0,
114+
null);
115+
116+
if (!hFile.IsInvalid && PInvoke.GetFileSizeEx(hFile, out long size))
117+
{
118+
if (_computedFiles.TryAdd(path, size))
119+
batchTotal += size;
120+
}
121+
}
122+
123+
if (batchTotal > 0)
124+
Interlocked.Add(ref _size, batchTotal);
104125
}
105126

106127
private long ComputeFileSize(string path)

0 commit comments

Comments
 (0)