Преглед изворни кода

Optimizing directory loading by checking existence of subfolders on the background

Source commit: 1d692a4c8fdea5e5394f6118e69801beda885c6f
Martin Prikryl пре 1 година
родитељ
комит
7343d82489
1 измењених фајлова са 393 додато и 26 уклоњено
  1. 393 26
      source/packages/filemng/DriveView.pas

+ 393 - 26
source/packages/filemng/DriveView.pas

@@ -45,7 +45,7 @@ uses
   Dialogs, ComCtrls, ShellApi, CommCtrl, ExtCtrls, ActiveX,  ShlObj,
   Dialogs, ComCtrls, ShellApi, CommCtrl, ExtCtrls, ActiveX,  ShlObj,
   DirView, ShellDialogs, DragDrop, DragDropFilesEx, FileChanges, FileOperator,
   DirView, ShellDialogs, DragDrop, DragDropFilesEx, FileChanges, FileOperator,
   DiscMon, IEDriveInfo, IEListView, BaseUtils, CustomDirView,
   DiscMon, IEDriveInfo, IEListView, BaseUtils, CustomDirView,
-  CustomDriveView, System.Generics.Collections;
+  CustomDriveView, System.Generics.Collections, CompThread;
 
 
 const
 const
   msThreadChangeDelay = 50;
   msThreadChangeDelay = 50;
@@ -57,6 +57,7 @@ const
   dvdsRereadAllways   = 16; {Refresh drivestatus in any case}
   dvdsRereadAllways   = 16; {Refresh drivestatus in any case}
 
 
   WM_USER_SHCHANGENOTIFY = WM_USER + $2000 + 13;
   WM_USER_SHCHANGENOTIFY = WM_USER + $2000 + 13;
+  WM_USER_SUBDIRREADER = WM_USER_SHCHANGENOTIFY + 1;
 
 
 type
 type
   EInvalidDirName  = class(Exception);
   EInvalidDirName  = class(Exception);
@@ -86,6 +87,13 @@ type
 
 
   TDriveView = class;
   TDriveView = class;
 
 
+  TSubDirReaderSchedule = class
+    Node: TTreeNode;
+    Path: string;
+    Deleted: Boolean;
+    Processed: Boolean;
+  end;
+
   TNodeData = class
   TNodeData = class
   private
   private
     FDirName: string;
     FDirName: string;
@@ -94,6 +102,7 @@ type
     FData: Pointer;
     FData: Pointer;
     FIsRecycleBin: Boolean;
     FIsRecycleBin: Boolean;
     FIconEmpty: Boolean;
     FIconEmpty: Boolean;
+    FSchedule: TSubDirReaderSchedule;
 
 
   public
   public
     constructor Create;
     constructor Create;
@@ -105,12 +114,44 @@ type
     property Data: Pointer read FData write FData;
     property Data: Pointer read FData write FData;
     property IsRecycleBin: Boolean read FIsRecycleBin;
     property IsRecycleBin: Boolean read FIsRecycleBin;
     property IconEmpty: Boolean read FIconEmpty write FIconEmpty;
     property IconEmpty: Boolean read FIconEmpty write FIconEmpty;
+    property Schedule: TSubDirReaderSchedule read FSchedule write FSchedule;
   end;
   end;
 
 
   TDriveTreeNode = class(TTreeNode)
   TDriveTreeNode = class(TTreeNode)
     procedure Assign(Source: TPersistent); override;
     procedure Assign(Source: TPersistent); override;
   end;
   end;
 
 
+  TSubDirReaderThread = class(TCompThread)
+  public
+    destructor Destroy; override;
+    procedure Terminate; override;
+
+  protected
+    constructor Create(DriveView: TDriveView);
+    procedure Add(Node: TTreeNode; Path: string);
+    procedure Delete(Node: TTreeNode);
+    function Detach: Integer;
+    procedure Reattach(Count: Integer);
+
+    procedure Execute; override;
+
+  private
+    FDriveView: TDriveView;
+    FEvent: THandle;
+    FQueue: TStack<TSubDirReaderSchedule>;
+    FResults: TQueue<TSubDirReaderSchedule>;
+    FSection: TRTLCriticalSection;
+    FTimer: TTimer;
+    FWindowHandle: HWND;
+
+    procedure TriggerEvent;
+    procedure ScheduleProcess;
+    procedure Process;
+    function ProcessResult: Boolean;
+    procedure Timer(Sender: TObject);
+    procedure WndProc(var Msg: TMessage);
+  end;
+
   TDriveViewRefreshDrives = procedure(Sender: TObject; Global: Boolean) of object;
   TDriveViewRefreshDrives = procedure(Sender: TObject; Global: Boolean) of object;
   TDriveViewContinueLoading = procedure(Sender: TObject; var Start: TDateTime; Path: string; Count: Integer; var Stop: Boolean) of object;
   TDriveViewContinueLoading = procedure(Sender: TObject; var Start: TDateTime; Path: string; Count: Integer; var Stop: Boolean) of object;
 
 
@@ -136,6 +177,7 @@ type
     FPrevSelected: TTreeNode;
     FPrevSelected: TTreeNode;
     FPrevSelectedIndex: Integer;
     FPrevSelectedIndex: Integer;
     FChangeTimerSuspended: Integer;
     FChangeTimerSuspended: Integer;
+    FSubDirReaderThread: TSubDirReaderThread;
 
 
     {Additional events:}
     {Additional events:}
     FOnDisplayContextMenu: TNotifyEvent;
     FOnDisplayContextMenu: TNotifyEvent;
@@ -179,7 +221,7 @@ type
     procedure SetDirView(Value: TDirView);
     procedure SetDirView(Value: TDirView);
     procedure SetDirectory(Value: string); override;
     procedure SetDirectory(Value: string); override;
     function  DoScanDir(FromNode: TTreeNode): Boolean;
     function  DoScanDir(FromNode: TTreeNode): Boolean;
-    function  AddChildNode(ParentNode: TTreeNode; ParentPath: string; SRec: TSearchRec): TTreeNode;
+    procedure AddChildNode(ParentNode: TTreeNode; ParentPath: string; SRec: TSearchRec);
     procedure CreateWatchThread(Drive: string);
     procedure CreateWatchThread(Drive: string);
     function NodeWatched(Node: TTreeNode): Boolean;
     function NodeWatched(Node: TTreeNode): Boolean;
     procedure TerminateWatchThread(Drive: string);
     procedure TerminateWatchThread(Drive: string);
@@ -207,6 +249,7 @@ type
     procedure Edit(const Item: TTVItem); override;
     procedure Edit(const Item: TTVItem); override;
 
 
     procedure WMUserRename(var Message: TMessage); message WM_USER_RENAME;
     procedure WMUserRename(var Message: TMessage); message WM_USER_RENAME;
+    procedure CMRecreateWnd(var Msg: TMessage); message CM_RECREATEWND;
 
 
     function GetCustomDirView: TCustomDirView; override;
     function GetCustomDirView: TCustomDirView; override;
     procedure SetCustomDirView(Value: TCustomDirView); override;
     procedure SetCustomDirView(Value: TCustomDirView); override;
@@ -402,7 +445,7 @@ procedure Register;
 implementation
 implementation
 
 
 uses
 uses
-  CompThread, PasTools, UITypes, Types, System.Generics.Defaults;
+  PasTools, UITypes, SyncObjs, IOUtils;
 
 
 type
 type
   PInt = ^Integer;
   PInt = ^Integer;
@@ -421,14 +464,334 @@ begin
   FDirName := '';
   FDirName := '';
   FIsRecycleBin := False;
   FIsRecycleBin := False;
   FIconEmpty := True;
   FIconEmpty := True;
+  FSchedule := nil;
 end; {TNodeData.Create}
 end; {TNodeData.Create}
 
 
 destructor TNodeData.Destroy;
 destructor TNodeData.Destroy;
 begin
 begin
+  Assert(not Assigned(FSchedule));
   SetLength(FDirName, 0);
   SetLength(FDirName, 0);
   inherited;
   inherited;
 end; {TNodeData.Destroy}
 end; {TNodeData.Destroy}
 
 
+  { TSubDirReaderThread }
+
+constructor TSubDirReaderThread.Create(DriveView: TDriveView);
+begin
+  inherited Create(True);
+  FDriveView := DriveView;
+  FSection.Initialize;
+  FEvent := CreateEvent(nil, False, False, nil);
+  FQueue := TStack<TSubDirReaderSchedule>.Create;
+  FResults := TQueue<TSubDirReaderSchedule>.Create;
+  FTimer := TTimer.Create(FDriveView);
+  FTimer.Enabled := False;
+  FTimer.Interval := 200;
+  FTimer.OnTimer := Timer;
+  FWindowHandle := AllocateHWnd(WndProc);
+end;
+
+destructor TSubDirReaderThread.Destroy;
+
+  procedure DestroyScheduleList(List: TEnumerable<TSubDirReaderSchedule>);
+  var
+    Schedule: TSubDirReaderSchedule;
+  begin
+    for Schedule in List do
+    begin
+      if not Schedule.Deleted then
+        TNodeData(Schedule.Node.Data).Schedule := nil;
+      Schedule.Free;
+    end;
+    List.Destroy;
+  end;
+
+begin
+  inherited;
+
+  DeallocateHWnd(FWindowHandle);
+
+  DestroyScheduleList(FQueue);
+  DestroyScheduleList(FResults);
+
+  CloseHandle(FEvent);
+  FTimer.Destroy;
+  FSection.Destroy;
+end;
+
+procedure TSubDirReaderThread.WndProc(var Msg: TMessage);
+begin
+  if Msg.Msg = WM_USER_SUBDIRREADER then
+    ScheduleProcess
+  else
+    Msg.Result := DefWindowProc(FWindowHandle, Msg.Msg, Msg.wParam, Msg.lParam);
+end;
+
+procedure TSubDirReaderThread.Process;
+var
+  Started: DWORD;
+  Elapsed: Integer;
+  Later: Boolean;
+begin
+  Started := GetTickCount;
+  Later := False;
+  while (not Later) and ProcessResult do
+  begin
+    Elapsed := GetTickCount - Started;
+    Later := (Elapsed < 0) or (Elapsed > 20);
+  end;
+
+  if not Later then
+    FTimer.Enabled := False;
+end;
+
+procedure TSubDirReaderThread.Timer(Sender: TObject);
+begin
+  Process;
+end;
+
+procedure TSubDirReaderThread.Add(Node: TTreeNode; Path: string);
+var
+  NodeData: TNodeData;
+  Schedule: TSubDirReaderSchedule;
+begin
+  if Suspended then
+    Resume;
+
+  FSection.Enter;
+  try
+    NodeData := TNodeData(Node.Data);
+    Assert(not Assigned(NodeData.Schedule));
+    Schedule := TSubDirReaderSchedule.Create;
+    Schedule.Node := Node;
+    Schedule.Path := Path;
+    Schedule.Deleted := False;
+    Schedule.Processed := False;
+    FQueue.Push(Schedule);
+    NodeData.Schedule := Schedule;
+  finally
+    FSection.Leave;
+  end;
+  TriggerEvent;
+end;
+
+procedure TSubDirReaderThread.Delete(Node: TTreeNode);
+var
+  NodeData: TNodeData;
+begin
+  FSection.Enter;
+  try
+    NodeData := TNodeData(Node.Data);
+    if Assigned(NodeData.Schedule) then
+    begin
+      NodeData.Schedule.Deleted := True;
+      NodeData.Schedule := nil;
+    end;
+  finally
+    FSection.Leave;
+  end;
+  TriggerEvent;
+end;
+
+function TSubDirReaderThread.Detach: Integer;
+
+  procedure DetachList(List: TEnumerable<TSubDirReaderSchedule>);
+  var
+    Schedule: TSubDirReaderSchedule;
+  begin
+    for Schedule in List do
+    begin
+      if Schedule.Deleted then Schedule.Free
+        else
+      begin
+        Assert(Schedule.Processed = (List = FResults));
+        Schedule.Node := nil;
+        Inc(Result);
+      end;
+    end;
+  end;
+
+begin
+  // block thread while handle is being recreated
+  FSection.Enter;
+  try
+    Result := 0;
+
+    DetachList(FQueue);
+    DetachList(FResults);
+
+    FQueue.Clear;
+    FResults.Clear;
+  except
+    FSection.Leave;
+    raise;
+  end;
+end;
+
+procedure TSubDirReaderThread.Reattach(Count: Integer);
+var
+  Node: TTreeNode;
+  Schedule: TSubDirReaderSchedule;
+begin
+  try
+    if Count > 0 then
+    begin
+      Node := FDriveView.Items.GetFirstNode;
+      while Assigned(Node) do
+      begin
+        Schedule := TNodeData(Node.Data).Schedule;
+        if Assigned(Schedule) then
+        begin
+          Assert(not Assigned(Schedule.Node));
+          Schedule.Node := Node;
+          if not Schedule.Processed then
+            FQueue.Push(Schedule)
+          else
+            FResults.Enqueue(Schedule);
+          Assert(Count > 0);
+          // Can be optimized to stop once Count = 0
+          Dec(Count);
+        end;
+        Node := Node.GetNext;
+      end;
+      Assert(Count = 0);
+    end;
+  finally
+    FSection.Leave;
+  end;
+
+  TriggerEvent;
+  ScheduleProcess;
+end;
+
+procedure TSubDirReaderThread.Terminate;
+begin
+  inherited;
+  TriggerEvent;
+end;
+
+procedure TSubDirReaderThread.TriggerEvent;
+begin
+  SetEvent(FEvent);
+end;
+
+function TSubDirReaderThread.ProcessResult: Boolean;
+var
+  Node: TTreeNode;
+  NodeData: TNodeData;
+  Schedule: TSubDirReaderSchedule;
+begin
+  FSection.Enter;
+  try
+    Result := (FResults.Count > 0);
+    if Result then
+    begin
+      Schedule := FResults.Dequeue;
+      if not Schedule.Deleted then
+      begin
+        Assert(Schedule.Processed);
+        Node := Schedule.Node;
+        Node.HasChildren := False;
+        NodeData := TNodeData(Node.Data);
+        NodeData.Scanned := not Node.HasChildren; // = True
+        Assert(NodeData.Schedule = Schedule);
+        NodeData.Schedule := nil;
+      end;
+      Schedule.Free;
+    end;
+  finally
+    FSection.Leave;
+  end;
+end;
+
+procedure TSubDirReaderThread.ScheduleProcess;
+begin
+  // process the first batch immediatelly, to make it more likely that the first seen subdirectories
+  // will immediatelly show correct status
+  Process;
+  FTimer.Enabled := True;
+end;
+
+procedure TSubDirReaderThread.Execute;
+var
+  SRec: TSearchRec;
+  HasSubDirs: Boolean;
+  NodeData: TNodeData;
+  Schedule: TSubDirReaderSchedule;
+  DelayStart, DelayStartStep: Integer;
+begin
+  DelayStart := 3000;
+  DelayStartStep := 100;
+  while (DelayStart > 0) and (not Terminated) do
+  begin
+    Sleep(DelayStartStep);
+    Dec(DelayStart, DelayStartStep)
+  end;
+
+  while not Terminated do
+  begin
+    WaitForSingleObject(FEvent, INFINITE);
+
+    while not Terminated do
+    begin
+      FSection.Enter;
+      try
+        if FQueue.Count = 0 then
+        begin
+          Schedule := nil; // shut up
+          Break;
+        end
+          else
+        begin
+          Schedule := FQueue.Pop;
+          if Schedule.Deleted then
+          begin
+            Schedule.Free;
+            // Can be optimized to loop within locked critical section until first non-deleted schedule is found
+            Continue;
+          end;
+          Assert(not Schedule.Processed);
+        end
+      finally
+        FSection.Leave;
+      end;
+
+      HasSubDirs := FDriveView.FindFirstSubDir(IncludeTrailingBackslash(Schedule.Path) + '*.*', SRec);
+      FindClose(SRec);
+
+      FSection.Enter;
+      try
+        if Schedule.Deleted then
+        begin
+          Schedule.Free;
+        end
+          else
+        begin
+          Schedule.Processed := True;
+          if not HasSubDirs then // optimization
+          begin
+            FResults.Enqueue(Schedule);
+            if FResults.Count = 1 then
+              PostMessage(FWindowHandle, WM_USER_SUBDIRREADER, 0, 0);
+          end
+            else
+          begin
+            // can happen only if the tree handle is just being recreated
+            if Assigned(Schedule.Node) then
+            begin
+              NodeData := TNodeData(Schedule.Node.Data);
+              NodeData.Schedule := nil;
+            end;
+            Schedule.Free;
+          end;
+        end;
+      finally
+        FSection.Leave;
+      end;
+    end;
+  end;
+end;
+
   { TDriveTreeNode }
   { TDriveTreeNode }
 
 
 // Not sure if this is ever used (possibly only then "assigning" tree view to another instance, what never do).
 // Not sure if this is ever used (possibly only then "assigning" tree view to another instance, what never do).
@@ -476,6 +839,7 @@ begin
   end;
   end;
 
 
   FFileOperator := TFileOperator.Create(Self);
   FFileOperator := TFileOperator.Create(Self);
+  FSubDirReaderThread := TSubDirReaderThread.Create(Self);
 
 
   FShowVolLabel := True;
   FShowVolLabel := True;
   FChangeFlag := False;
   FChangeFlag := False;
@@ -539,6 +903,7 @@ begin
 
 
   if Assigned(FFileOperator) then
   if Assigned(FFileOperator) then
     FFileOperator.Free;
     FFileOperator.Free;
+  FSubDirReaderThread.Free;
 
 
   inherited Destroy;
   inherited Destroy;
 end; {Destroy}
 end; {Destroy}
@@ -758,7 +1123,13 @@ procedure TDriveView.DestroyWnd;
 var
 var
   DriveStatus: TDriveStatus;
   DriveStatus: TDriveStatus;
 begin
 begin
-  if CreateWndRestores and (Items.Count > 0) and (csRecreating in ControlState) then
+  if not (csRecreating in ControlState) then
+  begin
+    FSubDirReaderThread.Terminate;
+    FSubDirReaderThread.WaitFor;
+  end
+    else
+  if CreateWndRestores and (Items.Count > 0) then
   begin
   begin
     FPrevSelectedIndex := -1;
     FPrevSelectedIndex := -1;
     if Assigned(FPrevSelected) then
     if Assigned(FPrevSelected) then
@@ -1030,6 +1401,7 @@ begin
 
 
   if Assigned(NodeData) and not (csRecreating in ControlState) then
   if Assigned(NodeData) and not (csRecreating in ControlState) then
   begin
   begin
+    FSubDirReaderThread.Delete(Node);
     NodeData.Destroy;
     NodeData.Destroy;
   end;
   end;
 end; {OnDelete}
 end; {OnDelete}
@@ -1418,11 +1790,10 @@ begin
   end;
   end;
 end; {RefreshRootNodes}
 end; {RefreshRootNodes}
 
 
-function TDriveView.AddChildNode(ParentNode: TTreeNode; ParentPath: string; SRec: TSearchRec): TTreeNode;
+procedure TDriveView.AddChildNode(ParentNode: TTreeNode; ParentPath: string; SRec: TSearchRec);
 var
 var
   NewNode: TTreeNode;
   NewNode: TTreeNode;
   NodeData: TNodeData;
   NodeData: TNodeData;
-  SubSRec: TSearchRec;
 begin
 begin
   NodeData := TNodeData.Create;
   NodeData := TNodeData.Create;
   NodeData.Attr := SRec.Attr;
   NodeData.Attr := SRec.Attr;
@@ -1433,30 +1804,14 @@ begin
     (SameText(SRec.Name, 'RECYCLED') or
     (SameText(SRec.Name, 'RECYCLED') or
      SameText(SRec.Name, 'RECYCLER') or
      SameText(SRec.Name, 'RECYCLER') or
      SameText(SRec.Name, '$RECYCLE.BIN'));
      SameText(SRec.Name, '$RECYCLE.BIN'));
+  NodeData.Scanned := False;
 
 
   NewNode := Self.Items.AddChildObject(ParentNode, '', NodeData);
   NewNode := Self.Items.AddChildObject(ParentNode, '', NodeData);
   NewNode.Text := GetDisplayName(NewNode);
   NewNode.Text := GetDisplayName(NewNode);
+  NewNode.HasChildren := True;
+  if GetDriveTypeToNode(ParentNode) <> DRIVE_REMOTE then
+    FSubDirReaderThread.Add(NewNode, IncludeTrailingBackslash(ParentPath) + SRec.Name);
 
 
-  // query content ("has subfolder") only if tree view is visible
-  // to avoid unnecessary scan of subfolders (which may take some time)
-  // if tree view is not visible anyway
-  if Visible and
-     Assigned(Parent) and
-     // Ad-hoc test for "other/right" panel, which is not hidden directly, but indirectly by hiding it container panel
-     ((Parent is TCustomForm) or Parent.Visible) and
-     (GetDriveTypeToNode(ParentNode) <> DRIVE_REMOTE) then
-  begin
-    NewNode.HasChildren := FindFirstSubDir(IncludeTrailingBackslash(ParentPath) + NodeData.DirName + '\*.*', SubSRec);
-    FindClose(SubSRec);
-  end
-    else
-  begin
-    NewNode.HasChildren := True;
-  end;
-
-  NodeData.Scanned := not NewNode.HasChildren;
-
-  Result := NewNode;
 end; {AddChildNode}
 end; {AddChildNode}
 
 
 function TDriveView.GetDriveStatus(Drive: string): TDriveStatus;
 function TDriveView.GetDriveStatus(Drive: string): TDriveStatus;
@@ -2739,4 +3094,16 @@ begin
   end;
   end;
 end; {PasteFromClipBoard}
 end; {PasteFromClipBoard}
 
 
+procedure TDriveView.CMRecreateWnd(var Msg: TMessage);
+var
+  ScheduledCount: Integer;
+begin
+  ScheduledCount := FSubDirReaderThread.Detach;
+  try
+    inherited;
+  finally
+    FSubDirReaderThread.Reattach(ScheduledCount);
+  end;
+end;
+
 end.
 end.