ls but with io_uring

perf: improve sorting perf, handle large dirs better

- User smaller struct for sorting
- Break out of stating when we hit the queue size so we can manage our
batch size a little better

rockorager.dev cc2a7861 38395fc4

verified
+74 -32
+74 -32
src/main.zig
··· 22 22 \\ -l, --long Display extended file metadata 23 23 ; 24 24 25 + const queue_size = 256; 26 + 25 27 const Options = struct { 26 28 all: bool = false, 27 29 @"almost-all": bool = false, ··· 213 215 cmd.opts.colors = .default; 214 216 } 215 217 216 - var ring: ourio.Ring = try .init(allocator, 256); 218 + var ring: ourio.Ring = try .init(allocator, queue_size); 217 219 defer ring.deinit(); 218 220 219 221 _ = try ring.open(cmd.opts.directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{ ··· 242 244 243 245 try ring.run(.until_done); 244 246 245 - std.sort.insertion(Entry, cmd.entries, cmd.opts, Entry.lessThan); 246 - 247 247 if (cmd.entries.len == 0) return; 248 248 249 - if (cmd.opts.long) 250 - try printLong(cmd, bw.writer()) 251 - else switch (cmd.opts.shortview) { 249 + if (cmd.opts.long) { 250 + try printLong(cmd, bw.writer()); 251 + } else switch (cmd.opts.shortview) { 252 252 .columns => try printShortColumns(cmd, bw.writer()), 253 253 .oneline => try printShortOnePerLine(cmd, bw.writer()), 254 254 } ··· 492 492 arena: std.mem.Allocator, 493 493 opts: Options = .{}, 494 494 entries: []Entry = &.{}, 495 + entry_idx: usize = 0, 495 496 496 497 tz: ?zeit.TimeZone = null, 497 498 groups: std.ArrayListUnmanaged(Group) = .empty, ··· 542 543 } 543 544 }; 544 545 545 - const Entry = struct { 546 + const MinimalEntry = struct { 546 547 name: [:0]const u8, 547 548 kind: std.fs.File.Kind, 548 - statx: ourio.Statx, 549 - link_name: [:0]const u8 = "", 550 - symlink_missing: bool = false, 551 549 552 - fn lessThan(opts: Options, lhs: Entry, rhs: Entry) bool { 550 + fn lessThan(opts: Options, lhs: MinimalEntry, rhs: MinimalEntry) bool { 553 551 if (opts.@"group-directories-first" and 554 552 lhs.kind != rhs.kind and 555 553 (lhs.kind == .directory or rhs.kind == .directory)) ··· 557 555 return lhs.kind == .directory; 558 556 } 559 557 560 - return std.ascii.orderIgnoreCase(lhs.name, rhs.name).compare(.lt); 558 + return std.ascii.lessThanIgnoreCase(lhs.name, rhs.name); 561 559 } 560 + }; 561 + 562 + const Entry = struct { 563 + name: [:0]const u8, 564 + kind: std.fs.File.Kind, 565 + statx: ourio.Statx, 566 + link_name: [:0]const u8 = "", 567 + symlink_missing: bool = false, 562 568 563 569 fn modeStr(self: Entry) [10]u8 { 564 570 var mode = [_]u8{'-'} ** 10; ··· 640 646 _ = try io.close(fd, .{}); 641 647 const dir: std.fs.Dir = .{ .fd = fd }; 642 648 643 - var results: std.ArrayListUnmanaged(Entry) = .empty; 649 + var temp_results: std.ArrayListUnmanaged(MinimalEntry) = .empty; 644 650 645 651 // Preallocate some memory 646 - try results.ensureUnusedCapacity(cmd.arena, 64); 652 + try temp_results.ensureUnusedCapacity(cmd.arena, queue_size); 647 653 648 654 // zig skips "." and "..", so we manually add them if needed 649 655 if (cmd.opts.all) { 650 - results.appendAssumeCapacity(.{ 656 + temp_results.appendAssumeCapacity(.{ 651 657 .name = ".", 652 658 .kind = .directory, 653 - .statx = undefined, 654 659 }); 655 - results.appendAssumeCapacity(.{ 660 + temp_results.appendAssumeCapacity(.{ 656 661 .name = "..", 657 662 .kind = .directory, 658 - .statx = undefined, 659 663 }); 660 664 } 661 665 ··· 663 667 while (try iter.next()) |dirent| { 664 668 if (!cmd.opts.@"almost-all" and std.mem.startsWith(u8, dirent.name, ".")) continue; 665 669 const nameZ = try cmd.arena.dupeZ(u8, dirent.name); 666 - try results.append(cmd.arena, .{ 670 + try temp_results.append(cmd.arena, .{ 667 671 .name = nameZ, 668 672 .kind = dirent.kind, 673 + }); 674 + } 675 + 676 + // sort the entries on the minimal struct. This has better memory locality since it is 677 + // much smaller than bringing in the ourio.Statx struct 678 + std.sort.pdq(MinimalEntry, temp_results.items, cmd.opts, MinimalEntry.lessThan); 679 + 680 + var results: std.ArrayListUnmanaged(Entry) = .empty; 681 + try results.ensureUnusedCapacity(cmd.arena, temp_results.items.len); 682 + for (temp_results.items) |tmp| { 683 + results.appendAssumeCapacity(.{ 684 + .name = tmp.name, 685 + .kind = tmp.kind, 669 686 .statx = undefined, 670 687 }); 671 688 } 672 689 cmd.entries = results.items; 673 690 674 - for (cmd.entries) |*entry| { 691 + for (cmd.entries, 0..) |*entry, i| { 692 + if (i >= queue_size) { 693 + cmd.entry_idx = i; 694 + break; 695 + } 675 696 const path = try std.fs.path.joinZ( 676 697 cmd.arena, 677 698 &.{ cmd.opts.directory, entry.name }, ··· 755 776 756 777 cmd.users.appendAssumeCapacity(user); 757 778 } 758 - std.mem.sort(User, cmd.users.items, {}, User.lessThan); 779 + std.sort.pdq(User, cmd.users.items, {}, User.lessThan); 759 780 }, 760 781 761 782 .group => { ··· 797 818 798 819 cmd.groups.appendAssumeCapacity(group); 799 820 } 800 - std.mem.sort(Group, cmd.groups.items, {}, Group.lessThan); 821 + std.sort.pdq(Group, cmd.groups.items, {}, Group.lessThan); 801 822 }, 802 823 803 824 .stat => { 804 - if (result.statx) |_| { 825 + _ = result.statx catch { 826 + const entry: *Entry = @fieldParentPtr("statx", task.req.statx.result); 827 + if (entry.symlink_missing) { 828 + // we already got here. Just zero out the statx; 829 + entry.statx = std.mem.zeroInit(ourio.Statx, entry.statx); 830 + return; 831 + } 832 + 833 + entry.symlink_missing = true; 834 + _ = try io.lstat(task.req.statx.path, task.req.statx.result, .{ 835 + .cb = onCompletion, 836 + .ptr = cmd, 837 + .msg = @intFromEnum(Msg.stat), 838 + }); 805 839 return; 806 - } else |_| {} 840 + }; 807 841 808 - const entry: *Entry = @fieldParentPtr("statx", task.req.statx.result); 809 - if (entry.symlink_missing) { 810 - // we already got here. Just zero out the statx; 811 - entry.statx = std.mem.zeroInit(ourio.Statx, entry.statx); 812 - return; 813 - } 842 + if (cmd.entry_idx >= cmd.entries.len) return; 814 843 815 - entry.symlink_missing = true; 816 - _ = try io.lstat(task.req.statx.path, task.req.statx.result, .{ 844 + const entry = &cmd.entries[cmd.entry_idx]; 845 + cmd.entry_idx += 1; 846 + const path = try std.fs.path.joinZ( 847 + cmd.arena, 848 + &.{ cmd.opts.directory, entry.name }, 849 + ); 850 + 851 + if (entry.kind == .sym_link) { 852 + var buf: [std.fs.max_path_bytes]u8 = undefined; 853 + 854 + // NOTE: Sadly, we can't do readlink via io_uring 855 + const link = try posix.readlink(path, &buf); 856 + entry.link_name = try cmd.arena.dupeZ(u8, link); 857 + } 858 + _ = try io.stat(path, &entry.statx, .{ 817 859 .cb = onCompletion, 818 860 .ptr = cmd, 819 861 .msg = @intFromEnum(Msg.stat),