Skip to content

Commit f0243fe

Browse files
committed
Streamline mount/unmount for dehydrating folders
In microsoft#1890 I demonstrated that it's possible to dehydrate folders without unmounting at all. Unfortunately that requires deleting all the placeholders and hydrated files which is too slow for dehydrating anything substantial. The current implementation avoids this by moving instead of deleting (which has the additional benefit of providing a backup) but ProjFS doesn't support moving or renaming folders, so we can't do that while mounted. This pull request takes a different approach to reducing the overhead of unmounting for dehydration. Instead of unmounting, moving, and remounting from the dehydrate verb, those steps are moved into the mount process under its dehydrate message handler. The mount process only disposes and recreates the components required for virtualization, avoiding several costly steps (eg authentication with Azure DevOps, verification of the cache server, verification of ProjFS installation). For the repo I work in, dehydrating a top-level directory is reduced from 33 seconds to 11 seconds with this change. Specific changes: * Backup of non-src folders (.git, .gvfs) is added to dehydrate folders. Previously it was only done for full dehydrate. * Unmount, move/backup of folders, and mount are moved from DehydrateVerb to InProcessMount. To support this, the DehydrateFolders message has the backup folder added to its fields. * The core methods of Mount and Unmount have a parameter added to skip disposing (on unmount) and initialization (on mount) of certain components which are ok to leave alive during the temporary unmount. * Ownership of GVFSContext disposal fixed - FileSystemCallbacks was disposing it despite not owning it. * Missing disposal of a file stream in BackgroundFileSystemTaskRunner is fixed. * WindowsFileSystemVirtualizer.DehydrateFolder will now delete a tombstone file for the directory if present. This allows us to support fixing a directory that the user manually deleted while mounted (perhaps in a misguided attempt to dehydrate it), though that would typically require running 'gvfs dehydrate --no-status' to skip verifying that the working directory matches the index. * '--no-status' is now supported with '--folders'
1 parent 0ddb3fa commit f0243fe

File tree

6 files changed

+306
-144
lines changed

6 files changed

+306
-144
lines changed

GVFS/GVFS.Common/FileSystem/PhysicalFileSystem.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public virtual void DeleteDirectory(string path, bool recursive = true, bool ign
4949
}
5050
}
5151

52+
public virtual void MoveDirectory(string sourceDirName, string destDirName)
53+
{
54+
Directory.Move(sourceDirName, destDirName);
55+
}
56+
5257
public virtual void CopyDirectoryRecursive(
5358
string srcDirectoryPath,
5459
string dstDirectoryPath,

GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,21 +203,24 @@ public static class DehydrateFolders
203203

204204
public class Request
205205
{
206-
public Request(string folders)
206+
public Request(string backupFolderPath, string folders)
207207
{
208208
this.Folders = folders;
209+
this.BackupFolderPath = backupFolderPath;
209210
}
210211

211-
public Request(Message message)
212+
public static Request FromMessage(Message message)
212213
{
213-
this.Folders = message.Body;
214+
return JsonConvert.DeserializeObject<Request>(message.Body);
214215
}
215216

216217
public string Folders { get; }
217218

219+
public string BackupFolderPath { get; }
220+
218221
public Message CreateMessage()
219222
{
220-
return new Message(Dehydrate, this.Folders);
223+
return new Message(Dehydrate, JsonConvert.SerializeObject(this));
221224
}
222225
}
223226

GVFS/GVFS.Mount/InProcessMount.cs

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ private void HandleRequest(ITracer tracer, string request, NamedPipeServer.Conne
295295

296296
private void HandleDehydrateFolders(NamedPipeMessages.Message message, NamedPipeServer.Connection connection)
297297
{
298-
NamedPipeMessages.DehydrateFolders.Request request = new NamedPipeMessages.DehydrateFolders.Request(message);
298+
NamedPipeMessages.DehydrateFolders.Request request = NamedPipeMessages.DehydrateFolders.Request.FromMessage(message);
299299

300300
EventMetadata metadata = new EventMetadata();
301301
metadata.Add(nameof(request.Folders), request.Folders);
@@ -308,7 +308,9 @@ private void HandleDehydrateFolders(NamedPipeMessages.Message message, NamedPipe
308308
response = new NamedPipeMessages.DehydrateFolders.Response(NamedPipeMessages.DehydrateFolders.DehydratedResult);
309309
string[] folders = request.Folders.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
310310
StringBuilder resetFolderPaths = new StringBuilder();
311-
foreach (string folder in folders)
311+
List<string> movedFolders = BackupFoldersWhileUnmounted(request, response, folders);
312+
313+
foreach (string folder in movedFolders)
312314
{
313315
if (this.fileSystemCallbacks.TryDehydrateFolder(folder, out string errorMessage))
314316
{
@@ -357,6 +359,50 @@ private void HandleDehydrateFolders(NamedPipeMessages.Message message, NamedPipe
357359
connection.TrySendResponse(response.CreateMessage());
358360
}
359361

362+
private List<string> BackupFoldersWhileUnmounted(NamedPipeMessages.DehydrateFolders.Request request, NamedPipeMessages.DehydrateFolders.Response response, string[] folders)
363+
{
364+
/* We can't move folders while the virtual file system is mounted, so unmount it first.
365+
* After moving the folders, remount the virtual file system.
366+
*/
367+
368+
var movedFolders = new List<string>();
369+
try
370+
{
371+
/* Set to "Mounting" instead of "Unmounting" so that incoming requests
372+
* that are rejected will know they can try again soon.
373+
*/
374+
this.currentState = MountState.Mounting;
375+
this.UnmountAndStopWorkingDirectoryCallbacks(willRemountInSameProcess: true);
376+
foreach (string folder in folders)
377+
{
378+
try
379+
{
380+
var source = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, folder);
381+
var destination = Path.Combine(request.BackupFolderPath, folder);
382+
var destinationParent = Path.GetDirectoryName(destination);
383+
this.context.FileSystem.CreateDirectory(destinationParent);
384+
if (this.context.FileSystem.DirectoryExists(source))
385+
{
386+
this.context.FileSystem.MoveDirectory(source, destination);
387+
}
388+
movedFolders.Add(folder);
389+
}
390+
catch (Exception ex)
391+
{
392+
response.FailedFolders.Add($"{folder}\0{ex.Message}");
393+
continue;
394+
}
395+
}
396+
}
397+
finally
398+
{
399+
this.MountAndStartWorkingDirectoryCallbacks(this.cacheServer, alreadyInitialized: true);
400+
this.currentState = MountState.Ready;
401+
}
402+
403+
return movedFolders;
404+
}
405+
360406
private void HandleLockRequest(string messageBody, NamedPipeServer.Connection connection)
361407
{
362408
NamedPipeMessages.AcquireLock.Response response;
@@ -551,9 +597,9 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name
551597
/* If a commit is downloaded, it wasn't prefetched.
552598
* If any prefetch has been done, there is probably a commit in the prefetch packs that is close enough that
553599
* loose object download of missing trees will be faster than downloading a pack of all the trees for the commit.
554-
* Otherwise, the trees for the commit may be needed soon depending on the context.
600+
* Otherwise, the trees for the commit may be needed soon depending on the context.
555601
* e.g. git log (without a pathspec) doesn't need trees, but git checkout does.
556-
*
602+
*
557603
* Save the tree/commit so if more trees are requested we can download all the trees for the commit in a batch.
558604
*/
559605
this.treesWithDownloadedCommits[treeSha] = objectSha;
@@ -596,7 +642,7 @@ private bool ShouldDownloadCommitPack(string objectSha, out string commitSha)
596642
private void UpdateTreesForDownloadedCommits(string objectSha)
597643
{
598644
/* If we are downloading missing trees, we probably are missing more trees for the commit.
599-
* Update our list of trees associated with the commit so we can use the # of missing trees
645+
* Update our list of trees associated with the commit so we can use the # of missing trees
600646
* as a heuristic to decide whether to batch download all the trees for the commit the
601647
* next time a missing one is requested.
602648
*/
@@ -723,12 +769,15 @@ private void HandleUnmountRequest(NamedPipeServer.Connection connection)
723769
}
724770
}
725771

726-
private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache)
772+
private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache, bool alreadyInitialized = false)
727773
{
728774
string error;
729-
if (!this.context.Enlistment.Authentication.TryInitialize(this.context.Tracer, this.context.Enlistment, out error))
775+
if (!alreadyInitialized)
730776
{
731-
this.FailMountAndExit("Failed to obtain git credentials: " + error);
777+
if (!this.context.Enlistment.Authentication.TryInitialize(this.context.Tracer, this.context.Enlistment, out error))
778+
{
779+
this.FailMountAndExit("Failed to obtain git credentials: " + error);
780+
}
732781
}
733782

734783
GitObjectsHttpRequestor objectRequestor = new GitObjectsHttpRequestor(this.context.Tracer, this.context.Enlistment, cache, this.retryConfig);
@@ -763,19 +812,22 @@ private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache)
763812
}, "Failed to create src folder callback listener");
764813
this.maintenanceScheduler = this.CreateOrReportAndExit(() => new GitMaintenanceScheduler(this.context, this.gitObjects), "Failed to start maintenance scheduler");
765814

766-
int majorVersion;
767-
int minorVersion;
768-
if (!RepoMetadata.Instance.TryGetOnDiskLayoutVersion(out majorVersion, out minorVersion, out error))
815+
if (!alreadyInitialized)
769816
{
770-
this.FailMountAndExit("Error: {0}", error);
771-
}
817+
int majorVersion;
818+
int minorVersion;
819+
if (!RepoMetadata.Instance.TryGetOnDiskLayoutVersion(out majorVersion, out minorVersion, out error))
820+
{
821+
this.FailMountAndExit("Error: {0}", error);
822+
}
772823

773-
if (majorVersion != GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion)
774-
{
775-
this.FailMountAndExit(
776-
"Error: On disk version ({0}) does not match current version ({1})",
777-
majorVersion,
778-
GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion);
824+
if (majorVersion != GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion)
825+
{
826+
this.FailMountAndExit(
827+
"Error: On disk version ({0}) does not match current version ({1})",
828+
majorVersion,
829+
GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion);
830+
}
779831
}
780832

781833
try
@@ -794,7 +846,7 @@ private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache)
794846
this.heartbeat.Start();
795847
}
796848

797-
private void UnmountAndStopWorkingDirectoryCallbacks()
849+
private void UnmountAndStopWorkingDirectoryCallbacks(bool willRemountInSameProcess = false)
798850
{
799851
if (this.maintenanceScheduler != null)
800852
{
@@ -817,6 +869,12 @@ private void UnmountAndStopWorkingDirectoryCallbacks()
817869

818870
this.gvfsDatabase?.Dispose();
819871
this.gvfsDatabase = null;
872+
873+
if (!willRemountInSameProcess)
874+
{
875+
this.context?.Dispose();
876+
this.context = null;
877+
}
820878
}
821879
}
822880
}

GVFS/GVFS.Virtualization/Background/BackgroundFileSystemTaskRunner.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ protected void Dispose(bool disposing)
131131
this.backgroundThread.Dispose();
132132
this.backgroundThread = null;
133133
}
134+
if (this.backgroundTasks != null)
135+
{
136+
this.backgroundTasks.Dispose();
137+
this.backgroundTasks = null;
138+
}
134139
}
135140
}
136141

GVFS/GVFS.Virtualization/FileSystemCallbacks.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,6 @@ public void Dispose()
227227
this.backgroundFileSystemTaskRunner.Dispose();
228228
this.backgroundFileSystemTaskRunner = null;
229229
}
230-
231-
if (this.context != null)
232-
{
233-
this.context.Dispose();
234-
this.context = null;
235-
}
236230
}
237231

238232
public bool IsReadyForExternalAcquireLockRequests(NamedPipeMessages.LockData requester, out string denyMessage)

0 commit comments

Comments
 (0)