Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project sidebar loading optimisation #686

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 25 additions & 73 deletions CodeEditModules/Modules/WorkspaceClient/src/Live.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,17 @@ public extension WorkspaceClient {
var subItems: [FileItem]?

if isDir.boolValue {
// TODO: Possibly optimize to loading avoid cache dirs and/or large folders
// Recursively fetch subdirectories and files if the path points to a directory
subItems = try loadFiles(fromURL: itemURL)
}

let newFileItem = FileItem(url: itemURL, children: subItems?.sortItems(foldersOnTop: true))
subItems?.forEach {
$0.parent = newFileItem
}
subItems?.forEach { $0.parent = newFileItem }
items.append(newFileItem)
flattenedFileItems[newFileItem.id] = newFileItem
}
}

return items
}

Expand All @@ -74,7 +72,6 @@ public extension WorkspaceClient {
/// entirely new `FileItem`, to prevent the `OutlineView` from going crazy with folding.
/// - Parameter fileItem: The `FileItem` to correct the children of
func rebuildFiles(fromItem fileItem: FileItem) throws -> Bool {

var didChangeSomething = false

// get the actual directory children
Expand Down Expand Up @@ -117,6 +114,7 @@ public extension WorkspaceClient {
}
}

fileItem.children = fileItem.children?.sortItems(foldersOnTop: true)
fileItem.children?.forEach({
if $0.isFolder {
let childChanged = try? rebuildFiles(fromItem: $0)
Expand All @@ -128,87 +126,41 @@ public extension WorkspaceClient {
return didChangeSomething
}

var sources: [String: DispatchSourceFileSystemObject] = [:]

/// Function to apply listeners that rebuild the file index when the file system is changed.
/// Optimised so that it only deletes/creates the needed listeners instead of replacing every one.
func startListeningToDirectory() {
// iterate over every item, checking if its a directory first
for item in flattenedFileItems.values {
// check if it actually exists, doesn't have a listener, and is a folder
guard item.isFolder &&
!sources.keys.contains(item.id) &&
FileItem.fileManger.fileExists(atPath: item.url.path) else { continue }

let descriptor = open(item.url.path, O_EVTONLY) // open the folder to listen for changes
let source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: descriptor,
eventMask: .write,
queue: DispatchQueue.global()
)

source.setEventHandler {
// Something has changed inside the directory
// We should reload the files.
guard !isRunning else { // this runs when a file change is detected but is already running
anotherInstanceRan += 1
return
}
isRunning = true
flattenedFileItems = [workspaceItem.id: workspaceItem]
_ = try? rebuildFiles(fromItem: workspaceItem)
while anotherInstanceRan > 0 { // TODO: optimise
let somethingChanged = try? rebuildFiles(fromItem: workspaceItem)
anotherInstanceRan = !(somethingChanged ?? false) ? 0 : anotherInstanceRan - 1
}
subject.send(workspaceItem.children ?? [])
startListeningToDirectory()
isRunning = false
anotherInstanceRan = 0
// reload data in outline view controller through the main thread
DispatchQueue.main.async { onRefresh() }
}

source.setCancelHandler {
close(descriptor)
}

source.resume()

sources[item.id] = source
FileItem.watcherCode = {
// Something has changed inside the directory
// We should reload the files.
guard !isRunning else { // this runs when a file change is detected but is already running
anotherInstanceRan += 1
return
}

// test for deleted directories and remove their listeners
for (id, _) in sources {
var childExists = false
for item in flattenedFileItems.values {
childExists = item.id == id ? true : childExists
}

if !childExists {
sources.removeValue(forKey: id)?.cancel()
}
isRunning = true
flattenedFileItems = [workspaceItem.id: workspaceItem]
_ = try? rebuildFiles(fromItem: workspaceItem)
while anotherInstanceRan > 0 { // TODO: optimise
let somethingChanged = try? rebuildFiles(fromItem: workspaceItem)
anotherInstanceRan = !(somethingChanged ?? false) ? 0 : anotherInstanceRan - 1
}
subject.send(workspaceItem.children ?? [])
isRunning = false
anotherInstanceRan = 0
// reload data in outline view controller through the main thread
DispatchQueue.main.async { onRefresh() }
}

func stopListeningToDirectory(directory: URL? = nil) {
if let directory = directory {
sources[directory.path]?.cancel()
sources.removeValue(forKey: directory.path)
if directory != nil {
flattenedFileItems[directory!.relativePath]?.watcher?.cancel()
} else {
for source in sources.values {
source.cancel()
for item in flattenedFileItems.values {
item.watcher?.cancel()
}
sources = [:]
}
}

return Self(
folderURL: { folderURL },
getFiles: subject
.handleEvents(receiveSubscription: { _ in
startListeningToDirectory()
}, receiveCancel: {
.handleEvents(receiveCancel: {
stopListeningToDirectory()
})
.receive(on: RunLoop.main)
Expand Down
21 changes: 21 additions & 0 deletions CodeEditModules/Modules/WorkspaceClient/src/Model/FileItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ public extension WorkspaceClient {

public typealias ID = String

public var watcher: DispatchSourceFileSystemObject?
static var watcherCode: () -> Void = {}

public func activateWatcher() -> Bool {
let descriptor = open(self.url.path, O_EVTONLY)
guard descriptor > 0 else { return false }
let source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: descriptor,
eventMask: .write,
queue: DispatchQueue.global()
)
source.setEventHandler { FileItem.watcherCode() }
source.setCancelHandler { close(descriptor) }
source.resume()
self.watcher = source
return true
}

public init(
url: URL,
children: [FileItem]? = nil
Expand Down Expand Up @@ -96,6 +114,9 @@ public extension WorkspaceClient {
case nil:
return FileIcon.fileIcon(fileType: fileType)
case let .some(children):
if self.watcher == nil && !self.activateWatcher() {
return "questionmark.folder"
}
return folderIcon(children)
}
}
Expand Down