| ... | ... | @@ -659,45 +659,40 @@ test "reset while retaining a buffer" { |
| 659 | 659 | try std.testing.expectEqual(2, arena_allocator.queryCapacity()); |
| 660 | 660 | } |
| 661 | 661 | |
| 662 | | test "fuzz" { |
| 662 | test "fuzz multi threaded" { |
| 663 | 663 | @disableInstrumentation(); |
| 664 | 664 | if (@import("builtin").single_threaded) return error.SkipZigTest; |
| 665 | 665 | |
| 666 | 666 | const gpa = std.heap.smp_allocator; |
| 667 | 667 | |
| 668 | var io_instance: std.Io.Threaded = .init(gpa, .{}); |
| 669 | defer io_instance.deinit(); |
| 670 | |
| 668 | 671 | var arena_state: ArenaAllocator.State = .init; |
| 669 | 672 | // No need to deinit arena_state, all allocations are in `sample_buffer`! |
| 670 | 673 | |
| 671 | | const control_buffer = try gpa.alloc(u8, 64 << 10 << 10); |
| 674 | const buffer_size = FuzzContext.max_alloc_count * FuzzContext.max_alloc_size; |
| 675 | |
| 676 | const control_buffer = try gpa.alloc(u8, buffer_size); |
| 672 | 677 | defer gpa.free(control_buffer); |
| 673 | 678 | var control_instance: std.heap.FixedBufferAllocator = .init(control_buffer); |
| 674 | 679 | |
| 675 | | const sample_buffer = try gpa.alloc(u8, 64 << 10 << 10); |
| 680 | const sample_buffer = try gpa.alloc(u8, buffer_size); |
| 676 | 681 | defer gpa.free(sample_buffer); |
| 677 | 682 | var sample_instance: FuzzAllocator = .init(sample_buffer); |
| 678 | 683 | |
| 679 | | var allocs: FuzzContext.Allocs = try .initCapacity(gpa, FuzzContext.max_alloc_count); |
| 680 | | defer allocs.deinit(gpa); |
| 681 | | |
| 682 | 684 | try std.testing.fuzz(FuzzContext.Init{ |
| 683 | | .gpa = gpa, |
| 684 | | .allocs = &allocs, |
| 685 | .threaded_instance = &io_instance, |
| 685 | 686 | .arena_state = &arena_state, |
| 686 | 687 | .control_instance = &control_instance, |
| 687 | 688 | .sample_instance = &sample_instance, |
| 688 | | }, fuzzArenaAllocator, .{}); |
| 689 | }, fuzzMultiThreaded, .{}); |
| 689 | 690 | } |
| 690 | 691 | |
| 691 | | fn fuzzArenaAllocator(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) anyerror!void { |
| 692 | fn fuzzMultiThreaded(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) anyerror!void { |
| 692 | 693 | @disableInstrumentation(); |
| 693 | 694 | const testing = std.testing; |
| 694 | | |
| 695 | | // We use a 'fresh' `Threaded` instance every time to reset threadlocals to |
| 696 | | // their default values. |
| 697 | | |
| 698 | | var io_instance: std.Io.Threaded = .init(fuzz_init.gpa, .{}); |
| 699 | | defer io_instance.deinit(); |
| 700 | | const io = io_instance.io(); |
| 695 | const io = fuzz_init.threaded_instance.io(); |
| 701 | 696 | |
| 702 | 697 | fuzz_init.sample_instance.prepareFailures(smith); |
| 703 | 698 | |
| ... | ... | @@ -708,59 +703,56 @@ fn fuzzArenaAllocator(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) an |
| 708 | 703 | defer fuzz_init.arena_state.* = arena_instance.state; |
| 709 | 704 | |
| 710 | 705 | var ctx: FuzzContext = .init( |
| 711 | | io, |
| 712 | 706 | control_allocator, |
| 713 | 707 | arena_instance.allocator(), |
| 714 | | fuzz_init.allocs, |
| 715 | 708 | ); |
| 716 | 709 | defer ctx.deinit(); |
| 717 | 710 | |
| 718 | | ctx.rwl.lockUncancelable(io); |
| 719 | | |
| 720 | 711 | var group: std.Io.Group = .init; |
| 721 | 712 | defer group.cancel(io); |
| 722 | 713 | |
| 714 | var n_allocs: usize = 0; |
| 723 | 715 | var n_actions: usize = 0; |
| 724 | 716 | while (!smith.eosWeightedSimple(99, 1) and n_actions < FuzzContext.max_action_count) { |
| 725 | 717 | errdefer comptime unreachable; |
| 726 | 718 | |
| 727 | | const ActionTag = @typeInfo(FuzzContext.Action).@"union".tag_type.?; |
| 728 | | const weights: []const testing.Smith.Weight = weights: { |
| 729 | | if (ctx.allocs.len == ctx.allocs.capacity) |
| 730 | | break :weights &.{ |
| 731 | | .value(ActionTag, .resize, 1), |
| 732 | | .value(ActionTag, .remap, 1), |
| 733 | | .value(ActionTag, .free, 1), |
| 734 | | }; |
| 735 | | break :weights testing.Smith.baselineWeights(ActionTag) ++ |
| 736 | | .{testing.Smith.Weight.value(ActionTag, .alloc, 2)}; |
| 737 | | }; |
| 738 | | const action: FuzzContext.Action = switch (smith.valueWeighted(ActionTag, weights)) { |
| 739 | | .alloc => action: { |
| 740 | | const alloc_index = ctx.allocs.addOneBounded() catch continue; |
| 741 | | ctx.allocs.items(.len)[alloc_index] = .free; |
| 742 | | break :action .{ .alloc = .{ |
| 743 | | .len = nextLen(smith), |
| 744 | | .alignment = smith.valueRangeAtMost( |
| 719 | const weights: []const testing.Smith.Weight = if (n_allocs == FuzzContext.max_alloc_count) |
| 720 | &.{ |
| 721 | .value(FuzzContext.Action, .resize, 1), |
| 722 | .value(FuzzContext.Action, .remap, 1), |
| 723 | .value(FuzzContext.Action, .free, 1), |
| 724 | } |
| 725 | else |
| 726 | &.{ |
| 727 | .value(FuzzContext.Action, .resize, 1), |
| 728 | .value(FuzzContext.Action, .remap, 1), |
| 729 | .value(FuzzContext.Action, .free, 1), |
| 730 | .value(FuzzContext.Action, .alloc, 3), |
| 731 | }; |
| 732 | switch (smith.valueWeighted(FuzzContext.Action, weights)) { |
| 733 | .alloc => { |
| 734 | const alloc_index = n_allocs; |
| 735 | n_allocs += 1; |
| 736 | ctx.allocs[alloc_index].common.len = .free; |
| 737 | group.concurrent(io, FuzzContext.doOneAlloc, .{ |
| 738 | &ctx, nextLen(smith), |
| 739 | smith.valueRangeAtMost( |
| 745 | 740 | Alignment, |
| 746 | 741 | .@"1", |
| 747 | 742 | .fromByteUnits(2 * std.heap.page_size_max), |
| 748 | 743 | ), |
| 749 | | .index = alloc_index, |
| 750 | | } }; |
| 744 | @enumFromInt(alloc_index), |
| 745 | }) catch unreachable; |
| 751 | 746 | }, |
| 752 | | .resize => .{ .resize = .{ .new_len = nextLen(smith) } }, |
| 753 | | .remap => .{ .remap = .{ .new_len = nextLen(smith) } }, |
| 754 | | .free => .free, |
| 755 | | }; |
| 756 | | group.concurrent(io, FuzzContext.doOneAction, .{ &ctx, action }) catch break; |
| 747 | .resize => group.concurrent(io, FuzzContext.doOneResize, .{ &ctx, nextLen(smith) }) catch unreachable, |
| 748 | .remap => group.concurrent(io, FuzzContext.doOneRemap, .{ &ctx, nextLen(smith) }) catch unreachable, |
| 749 | .free => group.concurrent(io, FuzzContext.doOneFree, .{&ctx}) catch unreachable, |
| 750 | } |
| 757 | 751 | n_actions += 1; |
| 758 | 752 | } |
| 759 | 753 | |
| 760 | | ctx.rwl.unlock(io); |
| 761 | | |
| 762 | 754 | try group.await(io); |
| 763 | | try ctx.check(); |
| 755 | try ctx.check(n_allocs); |
| 764 | 756 | |
| 765 | 757 | // This also covers the `deinit` logic since `free_all` uses it internally. |
| 766 | 758 | |
| ... | ... | @@ -783,73 +775,71 @@ fn fuzzArenaAllocator(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) an |
| 783 | 775 | } |
| 784 | 776 | |
| 785 | 777 | fuzz_init.control_instance.reset(); |
| 786 | | fuzz_init.allocs.clearRetainingCapacity(); |
| 787 | | } |
| 788 | | fn nextLen(smith: *std.testing.Smith) usize { |
| 789 | | @disableInstrumentation(); |
| 790 | | return usizeRange(smith, 1, 16 << 10 << 10); |
| 791 | 778 | } |
| 792 | | fn usizeRange(smith: *std.testing.Smith, at_least: usize, at_most: usize) usize { |
| 779 | fn nextLen(smith: *std.testing.Smith) @typeInfo(FuzzContext.Alloc.Len).@"enum".tag_type { |
| 793 | 780 | @disableInstrumentation(); |
| 794 | | const Int = @Int(.unsigned, @min(64, @bitSizeOf(usize))); |
| 795 | | return smith.valueRangeAtMost(Int, @intCast(at_least), @intCast(at_most)); |
| 781 | const BackingInt = @typeInfo(FuzzContext.Alloc.Len).@"enum".tag_type; |
| 782 | return smith.valueRangeAtMost(BackingInt, 1, FuzzContext.max_alloc_size); |
| 796 | 783 | } |
| 797 | 784 | |
| 798 | 785 | const FuzzContext = struct { |
| 799 | | io: std.Io, |
| 800 | | rwl: std.Io.RwLock, |
| 801 | | |
| 802 | 786 | control_allocator: Allocator, |
| 803 | 787 | sample_allocator: Allocator, |
| 804 | 788 | |
| 805 | | allocs: *Allocs, |
| 789 | last_alloc_index: Alloc.Index, |
| 790 | allocs: [max_alloc_count]Alloc, |
| 806 | 791 | |
| 807 | | const max_alloc_count = 4096; |
| 792 | const max_alloc_count = 64; |
| 808 | 793 | const max_action_count = 2 * max_alloc_count; |
| 809 | 794 | |
| 810 | | const Allocs = std.MultiArrayList(struct { |
| 795 | const max_alloc_size = 16 << 10; |
| 796 | |
| 797 | const Alloc = struct { |
| 811 | 798 | control_ptr: [*]u8, |
| 812 | 799 | sample_ptr: [*]u8, |
| 813 | | len: Len, |
| 814 | | alignment: Alignment, |
| 815 | | }); |
| 800 | common: packed struct(usize) { |
| 801 | len: Len, |
| 802 | alignment: Alignment, |
| 803 | _: @Int(.unsigned, padding_bits) = 0, |
| 804 | }, |
| 805 | |
| 806 | const Len = enum(@Int(.unsigned, len_bits)) { |
| 807 | free = (1 << len_bits) - 1, |
| 808 | _, |
| 809 | }; |
| 810 | const len_bits = @min(64, @bitSizeOf(usize)) - @bitSizeOf(Alignment); |
| 811 | const padding_bits = @bitSizeOf(usize) - (len_bits + @bitSizeOf(Alignment)); |
| 816 | 812 | |
| 817 | | const Len = enum(usize) { |
| 818 | | free = std.math.maxInt(usize), |
| 819 | | _, |
| 813 | const Index = enum(usize) { |
| 814 | none = std.math.maxInt(usize), |
| 815 | _, |
| 816 | }; |
| 820 | 817 | }; |
| 821 | 818 | |
| 822 | | const Action = union(enum(u8)) { |
| 823 | | alloc: struct { len: usize, alignment: Alignment, index: usize }, |
| 824 | | resize: struct { new_len: usize }, |
| 825 | | remap: struct { new_len: usize }, |
| 819 | const Action = enum { |
| 820 | alloc, |
| 821 | resize, |
| 822 | remap, |
| 826 | 823 | free, |
| 827 | 824 | }; |
| 828 | 825 | |
| 829 | | threadlocal var tls_next: u8 = 0; |
| 830 | | threadlocal var tls_last_index: ?usize = null; |
| 831 | | |
| 832 | 826 | const Init = struct { |
| 833 | | gpa: Allocator, |
| 834 | | allocs: *FuzzContext.Allocs, |
| 827 | threaded_instance: *std.Io.Threaded, |
| 835 | 828 | arena_state: *ArenaAllocator.State, |
| 836 | 829 | control_instance: *std.heap.FixedBufferAllocator, |
| 837 | 830 | sample_instance: *FuzzAllocator, |
| 838 | 831 | }; |
| 839 | 832 | |
| 840 | 833 | fn init( |
| 841 | | io: std.Io, |
| 842 | 834 | control_allocator: Allocator, |
| 843 | 835 | sample_allocator: Allocator, |
| 844 | | allocs: *Allocs, |
| 845 | 836 | ) FuzzContext { |
| 846 | 837 | @disableInstrumentation(); |
| 847 | 838 | return .{ |
| 848 | | .io = io, |
| 849 | | .rwl = .init, |
| 850 | 839 | .control_allocator = control_allocator, |
| 851 | 840 | .sample_allocator = sample_allocator, |
| 852 | | .allocs = allocs, |
| 841 | .last_alloc_index = .none, |
| 842 | .allocs = undefined, |
| 853 | 843 | }; |
| 854 | 844 | } |
| 855 | 845 | |
| ... | ... | @@ -858,35 +848,22 @@ const FuzzContext = struct { |
| 858 | 848 | ctx.* = undefined; |
| 859 | 849 | } |
| 860 | 850 | |
| 861 | | fn check(ctx: *const FuzzContext) !void { |
| 851 | fn check(ctx: *const FuzzContext, n_allocs: usize) !void { |
| 862 | 852 | @disableInstrumentation(); |
| 863 | | for (0..ctx.allocs.len) |index| { |
| 864 | | const len: usize = switch (ctx.allocs.items(.len)[index]) { |
| 853 | for (ctx.allocs[0..n_allocs]) |allocation| { |
| 854 | const len: usize = switch (allocation.common.len) { |
| 865 | 855 | .free => continue, |
| 866 | 856 | _ => |len| @intFromEnum(len), |
| 867 | 857 | }; |
| 868 | | const control = ctx.allocs.items(.control_ptr)[index][0..len]; |
| 869 | | const sample = ctx.allocs.items(.sample_ptr)[index][0..len]; |
| 858 | const control = allocation.control_ptr[0..len]; |
| 859 | const sample = allocation.sample_ptr[0..len]; |
| 870 | 860 | try std.testing.expectEqualSlices(u8, control, sample); |
| 871 | 861 | } |
| 872 | 862 | } |
| 873 | 863 | |
| 874 | | fn doOneAction(ctx: *FuzzContext, action: Action) std.Io.Cancelable!void { |
| 875 | | @disableInstrumentation(); |
| 876 | | ctx.rwl.lockSharedUncancelable(ctx.io); |
| 877 | | defer ctx.rwl.unlockShared(ctx.io); |
| 878 | | |
| 879 | | switch (action) { |
| 880 | | .alloc => |act| ctx.doOneAlloc(act.len, act.alignment, act.index), |
| 881 | | .resize => |act| ctx.doOneResize(act.new_len), |
| 882 | | .remap => |act| ctx.doOneRemap(act.new_len), |
| 883 | | .free => ctx.doOneFree(), |
| 884 | | } |
| 885 | | } |
| 886 | | |
| 887 | | fn doOneAlloc(ctx: *FuzzContext, len: usize, alignment: Alignment, index: usize) void { |
| 864 | fn doOneAlloc(ctx: *FuzzContext, len: usize, alignment: Alignment, index: Alloc.Index) void { |
| 888 | 865 | @disableInstrumentation(); |
| 889 | | assert(ctx.allocs.items(.len)[index] == .free); |
| 866 | assert(ctx.allocs[@intFromEnum(index)].common.len == .free); |
| 890 | 867 | |
| 891 | 868 | const control_ptr = ctx.control_allocator.rawAlloc(len, alignment, @returnAddress()) orelse |
| 892 | 869 | return; |
| ... | ... | @@ -895,38 +872,42 @@ const FuzzContext = struct { |
| 895 | 872 | return; |
| 896 | 873 | }; |
| 897 | 874 | |
| 898 | | ctx.allocs.set(index, .{ |
| 875 | ctx.allocs[@intFromEnum(index)] = .{ |
| 899 | 876 | .control_ptr = control_ptr, |
| 900 | 877 | .sample_ptr = sample_ptr, |
| 901 | | .len = @enumFromInt(len), |
| 902 | | .alignment = alignment, |
| 903 | | }); |
| 904 | | |
| 905 | | for (control_ptr[0..len], sample_ptr[0..len]) |*control, *sample| { |
| 906 | | control.* = tls_next; |
| 907 | | sample.* = tls_next; |
| 908 | | tls_next +%= 1; |
| 878 | .common = .{ |
| 879 | .len = @enumFromInt(len), |
| 880 | .alignment = alignment, |
| 881 | }, |
| 882 | }; |
| 883 | |
| 884 | for (control_ptr[0..len], sample_ptr[0..len], 0..) |*control, *sample, i| { |
| 885 | control.* = @truncate(i); |
| 886 | sample.* = @truncate(i); |
| 909 | 887 | } |
| 910 | 888 | |
| 911 | | tls_last_index = index; |
| 889 | @atomicStore(Alloc.Index, &ctx.last_alloc_index, index, .release); |
| 912 | 890 | } |
| 913 | 891 | fn doOneResize(ctx: *FuzzContext, new_len: usize) void { |
| 914 | 892 | @disableInstrumentation(); |
| 915 | | const index = tls_last_index orelse return; |
| 916 | | const len = ctx.allocs.items(.len)[index]; |
| 917 | | assert(len != .free); |
| 918 | | const memory = ctx.allocs.items(.sample_ptr)[index][0..@intFromEnum(len)]; |
| 919 | | const alignment = ctx.allocs.items(.alignment)[index]; |
| 920 | 893 | |
| 921 | | assert(alignment.check(@intFromPtr(ctx.allocs.items(.control_ptr)[index]))); |
| 922 | | assert(alignment.check(@intFromPtr(ctx.allocs.items(.sample_ptr)[index]))); |
| 894 | const index = @atomicRmw(Alloc.Index, &ctx.last_alloc_index, .Xchg, .none, .acquire); |
| 895 | if (index == .none) return; |
| 896 | |
| 897 | const allocation = &ctx.allocs[@intFromEnum(index)]; |
| 898 | assert(allocation.common.len != .free); |
| 899 | const memory = allocation.sample_ptr[0..@intFromEnum(allocation.common.len)]; |
| 900 | const alignment = allocation.common.alignment; |
| 901 | |
| 902 | assert(alignment.check(@intFromPtr(allocation.control_ptr))); |
| 903 | assert(alignment.check(@intFromPtr(allocation.sample_ptr))); |
| 923 | 904 | |
| 924 | 905 | // Since `resize` is fallible, we have to ensure that `control_allocator` |
| 925 | 906 | // is always successful by reserving the memory we need beforehand. |
| 926 | 907 | const new_control_ptr = ctx.control_allocator.rawAlloc(new_len, alignment, @returnAddress()) orelse |
| 927 | 908 | return; |
| 928 | 909 | if (ctx.sample_allocator.rawResize(memory, alignment, new_len, @returnAddress())) { |
| 929 | | const old_control = ctx.allocs.items(.control_ptr)[index][0..memory.len]; |
| 910 | const old_control = allocation.control_ptr[0..memory.len]; |
| 930 | 911 | const overlap = @min(memory.len, new_len); |
| 931 | 912 | @memcpy(new_control_ptr[0..overlap], old_control[0..overlap]); |
| 932 | 913 | ctx.control_allocator.rawFree(old_control, alignment, @returnAddress()); |
| ... | ... | @@ -935,23 +916,27 @@ const FuzzContext = struct { |
| 935 | 916 | return; |
| 936 | 917 | } |
| 937 | 918 | |
| 938 | | ctx.allocs.set(index, .{ |
| 919 | ctx.allocs[@intFromEnum(index)] = .{ |
| 939 | 920 | .control_ptr = new_control_ptr, |
| 940 | 921 | .sample_ptr = memory.ptr, |
| 941 | | .len = @enumFromInt(new_len), |
| 942 | | .alignment = alignment, |
| 943 | | }); |
| 922 | .common = .{ |
| 923 | .len = @enumFromInt(new_len), |
| 924 | .alignment = alignment, |
| 925 | }, |
| 926 | }; |
| 944 | 927 | |
| 945 | 928 | if (new_len > memory.len) { |
| 946 | 929 | for ( |
| 947 | | ctx.allocs.items(.control_ptr)[index][memory.len..new_len], |
| 948 | | ctx.allocs.items(.sample_ptr)[index][memory.len..new_len], |
| 949 | | ) |*control, *sample| { |
| 950 | | control.* = tls_next; |
| 951 | | sample.* = tls_next; |
| 952 | | tls_next +%= 1; |
| 930 | allocation.control_ptr[memory.len..new_len], |
| 931 | allocation.sample_ptr[memory.len..new_len], |
| 932 | 0.., |
| 933 | ) |*control, *sample, i| { |
| 934 | control.* = @truncate(i); |
| 935 | sample.* = @truncate(i); |
| 953 | 936 | } |
| 954 | 937 | } |
| 938 | |
| 939 | @atomicStore(Alloc.Index, &ctx.last_alloc_index, index, .release); |
| 955 | 940 | } |
| 956 | 941 | fn doOneRemap(ctx: *FuzzContext, new_len: usize) void { |
| 957 | 942 | @disableInstrumentation(); |
| ... | ... | @@ -959,26 +944,29 @@ const FuzzContext = struct { |
| 959 | 944 | } |
| 960 | 945 | fn doOneFree(ctx: *FuzzContext) void { |
| 961 | 946 | @disableInstrumentation(); |
| 962 | | const index = tls_last_index orelse return; |
| 963 | | const len = ctx.allocs.items(.len)[index]; |
| 964 | | assert(len != .free); |
| 965 | | const memory = ctx.allocs.items(.sample_ptr)[index][0..@intFromEnum(len)]; |
| 966 | | const alignment = ctx.allocs.items(.alignment)[index]; |
| 967 | 947 | |
| 968 | | assert(alignment.check(@intFromPtr(ctx.allocs.items(.control_ptr)[index]))); |
| 969 | | assert(alignment.check(@intFromPtr(ctx.allocs.items(.sample_ptr)[index]))); |
| 948 | const index = @atomicRmw(Alloc.Index, &ctx.last_alloc_index, .Xchg, .none, .acquire); |
| 949 | if (index == .none) return; |
| 970 | 950 | |
| 971 | | ctx.control_allocator.rawFree(ctx.allocs.items(.control_ptr)[index][0..memory.len], alignment, @returnAddress()); |
| 972 | | ctx.sample_allocator.rawFree(ctx.allocs.items(.sample_ptr)[index][0..memory.len], alignment, @returnAddress()); |
| 951 | const allocation = &ctx.allocs[@intFromEnum(index)]; |
| 952 | assert(allocation.common.len != .free); |
| 953 | const len: usize = @intFromEnum(allocation.common.len); |
| 954 | const alignment = allocation.common.alignment; |
| 973 | 955 | |
| 974 | | ctx.allocs.set(index, .{ |
| 956 | assert(alignment.check(@intFromPtr(allocation.control_ptr))); |
| 957 | assert(alignment.check(@intFromPtr(allocation.sample_ptr))); |
| 958 | |
| 959 | ctx.control_allocator.rawFree(allocation.control_ptr[0..len], alignment, @returnAddress()); |
| 960 | ctx.sample_allocator.rawFree(allocation.sample_ptr[0..len], alignment, @returnAddress()); |
| 961 | |
| 962 | ctx.allocs[@intFromEnum(index)] = .{ |
| 975 | 963 | .control_ptr = undefined, |
| 976 | 964 | .sample_ptr = undefined, |
| 977 | | .len = .free, |
| 978 | | .alignment = undefined, |
| 979 | | }); |
| 980 | | |
| 981 | | tls_last_index = null; |
| 965 | .common = .{ |
| 966 | .len = .free, |
| 967 | .alignment = .@"1", |
| 968 | }, |
| 969 | }; |
| 982 | 970 | } |
| 983 | 971 | }; |
| 984 | 972 | |