-
Notifications
You must be signed in to change notification settings - Fork 7.6k
improvements to file system events processing for search #12885
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -73,12 +73,24 @@ define(function (require, exports, module) { | |
var searchModel = new SearchModel(); | ||
|
||
/* Forward declarations */ | ||
var _documentChangeHandler, _fileSystemChangeHandler, _fileNameChangeHandler, clearSearch; | ||
var _documentChangeHandler, | ||
_fileSystemChangeHandler, | ||
_processCachedFileSystemEvents, | ||
_debouncedFileSystemChangeHandler, | ||
_fileNameChangeHandler, | ||
clearSearch; | ||
|
||
/** | ||
* Waits for FS changes to stack up until processing them | ||
* (scripts like npm install can do a lot of movements on the disk) | ||
* @const | ||
*/ | ||
var FILE_SYSTEM_EVENT_DEBOUNCE_TIME = 100; | ||
|
||
/** Remove the listeners that were tracking potential search result changes */ | ||
function _removeListeners() { | ||
DocumentModule.off("documentChange", _documentChangeHandler); | ||
FileSystem.off("change", _fileSystemChangeHandler); | ||
FileSystem.off("change", _debouncedFileSystemChangeHandler); | ||
DocumentManager.off("fileNameChange", _fileNameChangeHandler); | ||
} | ||
|
||
|
@@ -88,7 +100,7 @@ define(function (require, exports, module) { | |
_removeListeners(); | ||
|
||
DocumentModule.on("documentChange", _documentChangeHandler); | ||
FileSystem.on("change", _fileSystemChangeHandler); | ||
FileSystem.on("change", _debouncedFileSystemChangeHandler); | ||
DocumentManager.on("fileNameChange", _fileNameChangeHandler); | ||
} | ||
|
||
|
@@ -658,7 +670,7 @@ define(function (require, exports, module) { | |
* @param {array} fileList The list of files that changed. | ||
*/ | ||
function filesChanged(fileList) { | ||
if (FindUtils.isNodeSearchDisabled() || fileList.length === 0) { | ||
if (FindUtils.isNodeSearchDisabled() || !fileList || fileList.length === 0) { | ||
return; | ||
} | ||
var updateObject = { | ||
|
@@ -676,7 +688,7 @@ define(function (require, exports, module) { | |
* @param {array} fileList The list of files that was removed. | ||
*/ | ||
function filesRemoved(fileList) { | ||
if (FindUtils.isNodeSearchDisabled()) { | ||
if (FindUtils.isNodeSearchDisabled() || !fileList || fileList.length === 0) { | ||
return; | ||
} | ||
var updateObject = { | ||
|
@@ -732,69 +744,83 @@ define(function (require, exports, module) { | |
|
||
/* | ||
* Remove existing search results that match the given entry's path | ||
* @param {(File|Directory)} entry | ||
* @param {Array.<(File|Directory)>} entries | ||
*/ | ||
function _removeSearchResultsForEntry(entry) { | ||
Object.keys(searchModel.results).forEach(function (fullPath) { | ||
if (fullPath === entry.fullPath || | ||
(entry.isDirectory && fullPath.indexOf(entry.fullPath) === 0)) { | ||
// node search : inform node that the file is removed | ||
filesRemoved([fullPath]); | ||
if (findOrReplaceInProgress) { | ||
searchModel.removeResults(fullPath); | ||
resultsChanged = true; | ||
function _removeSearchResultsForEntries(entries) { | ||
var fullPaths = []; | ||
entries.forEach(function (entry) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth checking out if couple of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Valid point 👍 |
||
Object.keys(searchModel.results).forEach(function (fullPath) { | ||
if (fullPath === entry.fullPath || | ||
(entry.isDirectory && fullPath.indexOf(entry.fullPath) === 0)) { | ||
// node search : inform node that the file is removed | ||
fullPaths.push(fullPath); | ||
if (findOrReplaceInProgress) { | ||
searchModel.removeResults(fullPath); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be called once with a large array too? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. zaggino@ Bro how are you today? Please Bro help us we want CoffeeScript in Brakets and also |
||
resultsChanged = true; | ||
} | ||
} | ||
} | ||
}); | ||
}); | ||
// this should be called once with a large array instead of numerous calls with single items | ||
filesRemoved(fullPaths); | ||
} | ||
|
||
/* | ||
* Add new search results for this entry and all of its children | ||
* @param {(File|Directory)} entry | ||
* Add new search results for these entries and all of its children | ||
* @param {Array.<(File|Directory)>} entries | ||
* @return {jQuery.Promise} Resolves when the results have been added | ||
*/ | ||
function _addSearchResultsForEntry(entry) { | ||
var addedFiles = [], | ||
addedFilePaths = [], | ||
deferred = new $.Deferred(); | ||
|
||
// gather up added files | ||
var visitor = function (child) { | ||
// Replicate filtering that getAllFiles() does | ||
if (ProjectManager.shouldShow(child)) { | ||
if (child.isFile && _isReadableText(child.name)) { | ||
// Re-check the filtering that the initial search applied | ||
if (_inSearchScope(child)) { | ||
addedFiles.push(child); | ||
addedFilePaths.push(child.fullPath); | ||
function _addSearchResultsForEntries(entries) { | ||
var fullPaths = []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you have a rough idea of the memory usage here? If we store a lot of path could cause some trouble? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, this array is not being persisted besides the function execution lifetime and I wouldn't expect there to be a problem even if you have thousands of entries in it. On the other hand, without it, trying to send thousands of messages to a node process will definitely crash it (or socket server). In other words, I don't think memory usage is something to worry about. |
||
return Async.doInParallel(entries, function (entry) { | ||
var addedFiles = [], | ||
addedFilePaths = [], | ||
deferred = new $.Deferred(); | ||
|
||
// gather up added files | ||
var visitor = function (child) { | ||
// Replicate filtering that getAllFiles() does | ||
if (ProjectManager.shouldShow(child)) { | ||
if (child.isFile && _isReadableText(child.name)) { | ||
// Re-check the filtering that the initial search applied | ||
if (_inSearchScope(child)) { | ||
addedFiles.push(child); | ||
addedFilePaths.push(child.fullPath); | ||
} | ||
} | ||
return true; | ||
} | ||
return true; | ||
} | ||
return false; | ||
}; | ||
return false; | ||
}; | ||
|
||
entry.visit(visitor, function (err) { | ||
if (err) { | ||
deferred.reject(err); | ||
return; | ||
} | ||
entry.visit(visitor, function (err) { | ||
if (err) { | ||
deferred.reject(err); | ||
return; | ||
} | ||
|
||
//node Search : inform node about the file changes | ||
filesChanged(addedFilePaths); | ||
//node Search : inform node about the file changes | ||
//filesChanged(addedFilePaths); | ||
fullPaths = fullPaths.concat(addedFilePaths); | ||
|
||
if (findOrReplaceInProgress) { | ||
// find additional matches in all added files | ||
Async.doInParallel(addedFiles, function (file) { | ||
return _doSearchInOneFile(file) | ||
.done(function (foundMatches) { | ||
resultsChanged = resultsChanged || foundMatches; | ||
}); | ||
}).always(deferred.resolve); | ||
} | ||
}); | ||
if (findOrReplaceInProgress) { | ||
// find additional matches in all added files | ||
Async.doInParallel(addedFiles, function (file) { | ||
return _doSearchInOneFile(file) | ||
.done(function (foundMatches) { | ||
resultsChanged = resultsChanged || foundMatches; | ||
}); | ||
}).always(deferred.resolve); | ||
} else { | ||
deferred.resolve(); | ||
} | ||
}); | ||
|
||
return deferred.promise(); | ||
return deferred.promise(); | ||
}).always(function () { | ||
// this should be called once with a large array instead of numerous calls with single items | ||
filesChanged(fullPaths); | ||
}); | ||
} | ||
|
||
if (!entry) { | ||
|
@@ -804,23 +830,23 @@ define(function (require, exports, module) { | |
|
||
var addPromise; | ||
if (entry.isDirectory) { | ||
if (!added || !removed || (added.length === 0 && removed.length === 0)) { | ||
if (added.length === 0 && removed.length === 0) { | ||
// If the added or removed sets are null, must redo the search for the entire subtree - we | ||
// don't know which child files/folders may have been added or removed. | ||
_removeSearchResultsForEntry(entry); | ||
_removeSearchResultsForEntries([ entry ]); | ||
|
||
var deferred = $.Deferred(); | ||
addPromise = deferred.promise(); | ||
entry.getContents(function (err, entries) { | ||
Async.doInParallel(entries, _addSearchResultsForEntry).always(deferred.resolve); | ||
_addSearchResultsForEntries(entries).always(deferred.resolve); | ||
}); | ||
} else { | ||
removed.forEach(_removeSearchResultsForEntry); | ||
addPromise = Async.doInParallel(added, _addSearchResultsForEntry); | ||
_removeSearchResultsForEntries(removed); | ||
addPromise = _addSearchResultsForEntries(added); | ||
} | ||
} else { // entry.isFile | ||
_removeSearchResultsForEntry(entry); | ||
addPromise = _addSearchResultsForEntry(entry); | ||
_removeSearchResultsForEntries([ entry ]); | ||
addPromise = _addSearchResultsForEntries([ entry ]); | ||
} | ||
|
||
addPromise.always(function () { | ||
|
@@ -830,6 +856,56 @@ define(function (require, exports, module) { | |
} | ||
}); | ||
}; | ||
|
||
/** | ||
* This stores file system events emitted by watchers that were not yet processed | ||
*/ | ||
var _cachedFileSystemEvents = []; | ||
|
||
/** | ||
* Debounced function to process emitted file system events | ||
* for cases when there's a lot of fs events emitted in a very short period of time | ||
*/ | ||
_processCachedFileSystemEvents = _.debounce(function () { | ||
// we need to reduce _cachedFileSystemEvents not to contain duplicates! | ||
_cachedFileSystemEvents = _cachedFileSystemEvents.reduce(function (result, obj) { | ||
var fullPath = obj.entry ? obj.entry.fullPath : null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deeper in the code there's a handle for this case (doing a full refresh I think) so it didn't want to stop it here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the assignment below is safe: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it actuall assigns |
||
// merge added & removed | ||
if (result[fullPath] && obj.isDirectory) { | ||
obj.added = obj.added.concat(result[fullPath].added); | ||
obj.removed = obj.removed.concat(result[fullPath].removed); | ||
} | ||
// use the latest event as base | ||
result[fullPath] = obj; | ||
return result; | ||
}, {}); | ||
_.forEach(_cachedFileSystemEvents, function (obj) { | ||
_fileSystemChangeHandler(obj.event, obj.entry, obj.added, obj.removed); | ||
}); | ||
_cachedFileSystemEvents = []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we are accessing the same global, and this function is called with a delay, don't we risk here to delete the events that have been added in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How exactly? There's a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably I was overthinking, but I was wondering if there could be some races between There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, races certainly can't happen in this case. They are both completely sync functions - no promises or async-await there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The debounce and relative timout are threwing me off. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
}, FILE_SYSTEM_EVENT_DEBOUNCE_TIME); | ||
|
||
/** | ||
* Wrapper function for _fileSystemChangeHandler which handles all incoming fs events | ||
* putting them to cache and executing a debounced function | ||
*/ | ||
_debouncedFileSystemChangeHandler = function (event, entry, added, removed) { | ||
// normalize this here so we don't need to handle null later | ||
var isDirectory = false; | ||
if (entry && entry.isDirectory) { | ||
isDirectory = true; | ||
added = added || []; | ||
removed = removed || []; | ||
} | ||
_cachedFileSystemEvents.push({ | ||
event: event, | ||
entry: entry, | ||
isDirectory: isDirectory, | ||
added: added, | ||
removed: removed | ||
}); | ||
_processCachedFileSystemEvents(); | ||
}; | ||
|
||
/** | ||
* On project change, inform node about the new list of files that needs to be crawled. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
!fileList
coversfileList.length === 0
too.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no it doesn't
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, sorry about that @zaggino, I automatically assumed that
fileList
would never be an empty array just undefined. Again, sorry.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving it as is, it's a very cheap check and better be safe than sorry :)