fsmonitor: optimize processing of directory events

Teach Git to perform binary search over the cache-entries for a directory notification and then linearly scan forward to find the immediate children. Previously, when the FSMonitor reported a modified directory Git would perform a linear search on the entire cache-entry array for all entries matching that directory prefix and invalidate them. Since the cache-entry array is already sorted, we can use a binary search to find the first matching entry and then only linearly walk forward and invalidate entries until the prefix changes. Also, the original code would invalidate anything having the same directory prefix. Since a directory event should only be received for items that are immediately within the directory (and not within sub-directories of it), only invalidate those entries and not the whole subtree. Signed-off-by: Jeff Hostetler <jeffhost@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jeff Hostetler committed May 26, 2022 at 21:47 UTC 95a4e78a746be2b87972c03c82e8b1fe8fe0bea1
1 file changed +54 -17
fsmonitor.c
+54 -17
@@ -184,30 +184,68 @@ static int query_fsmonitor_hook(struct repository *r,
184 static void fsmonitor_refresh_callback(struct index_state *istate, char *name)
185 {
186 int i, len = strlen(name);
187 - if (name[len - 1] == '/') {
187 + int pos = index_name_pos(istate, name, len);
188 +
189 + trace_printf_key(&trace_fsmonitor,
190 + "fsmonitor_refresh_callback '%s' (pos %d)",
191 + name, pos);
192
193 + if (name[len - 1] == '/') {
194 /*
190 - * TODO We should binary search to find the first path with
191 - * TODO this directory prefix. Then linearly update entries
192 - * TODO while the prefix matches. Taking care to search without
193 - * TODO the trailing slash -- because '/' sorts after a few
194 - * TODO interesting special chars, like '.' and ' '.
195 + * The daemon can decorate directory events, such as
196 + * moves or renames, with a trailing slash if the OS
197 + * FS Event contains sufficient information, such as
198 + * MacOS.
199 + *
200 + * Use this to invalidate the entire cone under that
201 + * directory.
202 + *
203 + * We do not expect an exact match because the index
204 + * does not normally contain directory entries, so we
205 + * start at the insertion point and scan.
206 */
207 + if (pos < 0)
208 + pos = -pos - 1;
209
210 /* Mark all entries for the folder invalid */
198 - for (i = 0; i < istate->cache_nr; i++) {
199 - if (istate->cache[i]->ce_flags & CE_FSMONITOR_VALID &&
200 - starts_with(istate->cache[i]->name, name))
201 - istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID;
211 + for (i = pos; i < istate->cache_nr; i++) {
212 + if (!starts_with(istate->cache[i]->name, name))
213 + break;
214 + istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID;
215 }
203 - /* Need to remove the / from the path for the untracked cache */
216 +
217 + /*
218 + * We need to remove the traling "/" from the path
219 + * for the untracked cache.
220 + */
221 name[len - 1] = '\0';
222 + } else if (pos >= 0) {
223 + /*
224 + * We have an exact match for this path and can just
225 + * invalidate it.
226 + */
227 + istate->cache[pos]->ce_flags &= ~CE_FSMONITOR_VALID;
228 } else {
206 - int pos = index_name_pos(istate, name, strlen(name));
207 -
208 - if (pos >= 0) {
209 - struct cache_entry *ce = istate->cache[pos];
210 - ce->ce_flags &= ~CE_FSMONITOR_VALID;
229 + /*
230 + * The path is not a tracked file -or- it is a
231 + * directory event on a platform that cannot
232 + * distinguish between file and directory events in
233 + * the event handler, such as Windows.
234 + *
235 + * Scan as if it is a directory and invalidate the
236 + * cone under it. (But remember to ignore items
237 + * between "name" and "name/", such as "name-" and
238 + * "name.".
239 + */
240 + pos = -pos - 1;
241 +
242 + for (i = pos; i < istate->cache_nr; i++) {
243 + if (!starts_with(istate->cache[i]->name, name))
244 + break;
245 + if ((unsigned char)istate->cache[i]->name[len] > '/')
246 + break;
247 + if (istate->cache[i]->name[len] == '/')
248 + istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID;
249 }
250 }
251
@@ -215,7 +253,6 @@ static void fsmonitor_refresh_callback(struct index_state *istate, char *name)
253 * Mark the untracked cache dirty even if it wasn't found in the index
254 * as it could be a new untracked file.
255 */
218 - trace_printf_key(&trace_fsmonitor, "fsmonitor_refresh_callback '%s'", name);
256 untracked_cache_invalidate_path(istate, name, 0);
257 }
258