authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2022-10-05 03:17:52-07:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2022-10-05 03:27:25-07:00
log274d19575ea1ebaea593cdca7c5afa8303153cb4
treecdfc0fdc70ae18f08a6e1e5faf2c4455c32f5391
parente9889cd25ff219f2f5ac7a336c4f99a577b19d44

fs: Optimize Dir.deleteTree for non-deeply-nested directories

`deleteTree` now uses a stack-allocated stack for the first 16 nested directories, and then falls back to the previous implementation (which only keeps 1 directory open at a time) when it runs out of room in its stack. This allows the function to perform as well as a recursive implementation for most use-cases without needing allocation or introducing the possibility of stack overflow.

1 files changed, 189 insertions(+), 25 deletions(-)

lib/std/fs.zig+189-25
......@@ -2076,7 +2076,6 @@ pub const Dir = struct {
20762076 /// this function recursively removes its entries and then tries again.
20772077 /// This operation is not atomic on most file systems.
20782078 pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void {
2079 // First, try deleting the item as a file. This way we don't follow sym links.
20802079 if (self.deleteFile(sub_path)) {
20812080 return;
20822081 } else |err| switch (err) {
......@@ -2096,31 +2095,196 @@ pub const Dir = struct {
20962095 => |e| return e,
20972096 }
20982097
2099 start_over: while (true) {
2100 var iterable_dir = self.openIterableDir(sub_path, .{ .no_follow = true }) catch |err| switch (err) {
2101 error.NotDir => {
2102 // Somehow the sub_path got changed into a file while we were trying to delete the tree.
2103 // This implies that the dir at the sub_path was deleted at some point so we consider this
2104 // as a successful delete and return.
2105 return;
2106 },
2107 error.FileNotFound => {
2108 // That's fine, we were trying to remove this directory anyway.
2109 return;
2098 const StackItem = struct {
2099 name: []const u8,
2100 parent_dir: Dir,
2101 iter: IterableDir.Iterator,
2102 };
2103
2104 var stack = std.BoundedArray(StackItem, 16){};
2105 defer {
2106 for (stack.slice()) |*item| {
2107 item.iter.dir.close();
2108 }
2109 }
2110
2111 var initial_iterable_dir = self.openIterableDir(sub_path, .{ .no_follow = true }) catch |err| switch (err) {
2112 error.NotDir => {
2113 // Somehow the sub_path got changed into a file while we were trying to delete the tree.
2114 // This implies that the dir at the sub_path was deleted at some point so we consider this
2115 // as a successful delete and return.
2116 return;
2117 },
2118 error.FileNotFound => {
2119 // That's fine, we were trying to remove this directory anyway.
2120 return;
2121 },
2122 error.InvalidHandle,
2123 error.AccessDenied,
2124 error.SymLinkLoop,
2125 error.ProcessFdQuotaExceeded,
2126 error.NameTooLong,
2127 error.SystemFdQuotaExceeded,
2128 error.NoDevice,
2129 error.SystemResources,
2130 error.Unexpected,
2131 error.InvalidUtf8,
2132 error.BadPathName,
2133 error.DeviceBusy,
2134 => |e| return e,
2135 };
2136
2137 stack.appendAssumeCapacity(StackItem{
2138 .name = sub_path,
2139 .parent_dir = self,
2140 .iter = initial_iterable_dir.iterateAssumeFirstIteration(),
2141 });
2142
2143 process_stack: while (stack.len != 0) {
2144 var top = &(stack.slice()[stack.len - 1]);
2145 while (try top.iter.next()) |entry| {
2146 var treat_as_dir = entry.kind == .Directory;
2147 handle_entry: while (true) {
2148 if (treat_as_dir) {
2149 if (stack.ensureUnusedCapacity(1)) {
2150 var iterable_dir = top.iter.dir.openIterableDir(entry.name, .{ .no_follow = true }) catch |err| switch (err) {
2151 error.NotDir => {
2152 treat_as_dir = false;
2153 continue :handle_entry;
2154 },
2155 error.FileNotFound => {
2156 // That's fine, we were trying to remove this directory anyway.
2157 break :handle_entry;
2158 },
2159
2160 error.InvalidHandle,
2161 error.AccessDenied,
2162 error.SymLinkLoop,
2163 error.ProcessFdQuotaExceeded,
2164 error.NameTooLong,
2165 error.SystemFdQuotaExceeded,
2166 error.NoDevice,
2167 error.SystemResources,
2168 error.Unexpected,
2169 error.InvalidUtf8,
2170 error.BadPathName,
2171 error.DeviceBusy,
2172 => |e| return e,
2173 };
2174 stack.appendAssumeCapacity(StackItem{
2175 .name = entry.name,
2176 .parent_dir = top.iter.dir,
2177 .iter = iterable_dir.iterateAssumeFirstIteration(),
2178 });
2179 continue :process_stack;
2180 } else |_| {
2181 try top.iter.dir.deleteTreeFallback(entry.name, entry.kind);
2182 break :handle_entry;
2183 }
2184 } else {
2185 if (top.iter.dir.deleteFile(entry.name)) {
2186 break :handle_entry;
2187 } else |err| switch (err) {
2188 error.FileNotFound => break :handle_entry,
2189
2190 error.NotDir => unreachable,
2191
2192 error.IsDir => {
2193 treat_as_dir = true;
2194 continue :handle_entry;
2195 },
2196
2197 error.AccessDenied,
2198 error.InvalidUtf8,
2199 error.SymLinkLoop,
2200 error.NameTooLong,
2201 error.SystemResources,
2202 error.ReadOnlyFileSystem,
2203 error.FileSystem,
2204 error.FileBusy,
2205 error.BadPathName,
2206 error.Unexpected,
2207 => |e| return e,
2208 }
2209 }
2210 }
2211 }
2212
2213 top.parent_dir.deleteDir(top.name) catch |err| switch (err) {
2214 error.FileNotFound => {},
2215 error.DirNotEmpty => {
2216 // reset the iterator and try again
2217 top.iter.reset();
2218 continue :process_stack;
21102219 },
2111 error.InvalidHandle,
2112 error.AccessDenied,
2113 error.SymLinkLoop,
2114 error.ProcessFdQuotaExceeded,
2115 error.NameTooLong,
2116 error.SystemFdQuotaExceeded,
2117 error.NoDevice,
2118 error.SystemResources,
2119 error.Unexpected,
2120 error.InvalidUtf8,
2121 error.BadPathName,
2122 error.DeviceBusy,
2123 => |e| return e,
2220 else => |e| return e,
2221 };
2222
2223 top.iter.dir.close();
2224 _ = stack.pop();
2225 }
2226 }
2227
2228 /// Fallback version of deleteTree that is less efficient but works on arbitrarily
2229 /// nested directories without needing recursion or allocation.
2230 fn deleteTreeFallback(self: Dir, sub_path: []const u8, kind_hint: File.Kind) DeleteTreeError!void {
2231 start_over: while (true) {
2232 var iterable_dir = iterable_dir: {
2233 var treat_as_dir = kind_hint == .Directory;
2234
2235 handle_entry: while (true) {
2236 if (treat_as_dir) {
2237 break :iterable_dir self.openIterableDir(sub_path, .{ .no_follow = true }) catch |err| switch (err) {
2238 error.NotDir => {
2239 treat_as_dir = false;
2240 continue :handle_entry;
2241 },
2242 error.FileNotFound => {
2243 // That's fine, we were trying to remove this directory anyway.
2244 return;
2245 },
2246
2247 error.InvalidHandle,
2248 error.AccessDenied,
2249 error.SymLinkLoop,
2250 error.ProcessFdQuotaExceeded,
2251 error.NameTooLong,
2252 error.SystemFdQuotaExceeded,
2253 error.NoDevice,
2254 error.SystemResources,
2255 error.Unexpected,
2256 error.InvalidUtf8,
2257 error.BadPathName,
2258 error.DeviceBusy,
2259 => |e| return e,
2260 };
2261 } else {
2262 if (self.deleteFile(sub_path)) {
2263 return;
2264 } else |err| switch (err) {
2265 error.FileNotFound => return,
2266
2267 error.NotDir => unreachable,
2268
2269 error.IsDir => {
2270 treat_as_dir = true;
2271 continue :handle_entry;
2272 },
2273
2274 error.AccessDenied,
2275 error.InvalidUtf8,
2276 error.SymLinkLoop,
2277 error.NameTooLong,
2278 error.SystemResources,
2279 error.ReadOnlyFileSystem,
2280 error.FileSystem,
2281 error.FileBusy,
2282 error.BadPathName,
2283 error.Unexpected,
2284 => |e| return e,
2285 }
2286 }
2287 }
21242288 };
21252289 var cleanup_dir_parent: ?IterableDir = null;
21262290 defer if (cleanup_dir_parent) |*d| d.close();