Skip to content

Commit e2697ff

Browse files
committed
runtime: Implement explicit object allocation
This commit implements the full allocation token API as described in issue #13. The new API works by making all allocations explicit through a new method on Heap called getAllocation. This method allows a user to ask for a specific amount of bytes, which the heap will make available on eden (garbage collecting if necessary) and then return a token object which gives that piece of code a claim for the requested amount of bytes. The AllocationToken object returned by getAllocation allows a user to allocate through it in either the object or the byte array segment. If the requested amount of bytes are exceeded, the VM will immediately crash. This is intended to significantly reduce the amount of problems caused by implicit garbage collection being allowed anywhere an allocation happens. As a side effect, this commit removes a lot of try keywords from the overall code due to the reduction of fallible API usage (through heap.allocateInObjectSegment). Closes #13.
1 parent 123e029 commit e2697ff

23 files changed

+444
-351
lines changed

src/debug.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ pub const LOOKUP_DEBUG = false;
66
pub const SLOTS_LOOKUP_DEBUG = false;
77
/// Debugging of garbage collections.
88
pub const GC_DEBUG = false;
9+
/// Debugging of allocation token requests.
10+
pub const GC_TOKEN_DEBUG = false;
911
/// Debugging of which addresses called track and then didn't untrack the object.
1012
pub const GC_TRACK_SOURCE_DEBUG = false;
1113
/// Debugging of functions related to older generation objects pointing to newer

src/runtime/Activation.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub fn initInPlace(
6363
stack_snapshot: Actor.StackSnapshot,
6464
creator_message: Value,
6565
created_from: SourceRange,
66-
) !void {
66+
) void {
6767
self.* = .{
6868
.activation_id = newActivationID(),
6969
.activation_object = activation_object,

src/runtime/Actor.zig

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
const std = @import("std");
66
const Allocator = std.mem.Allocator;
77

8+
const Heap = @import("./Heap.zig");
89
const Slot = @import("./slot.zig").Slot;
910
const debug = @import("../debug.zig");
1011
const Value = value_import.Value;
@@ -145,34 +146,15 @@ pub const Message = struct {
145146
}
146147
};
147148

148-
/// Spawn a new actor, activating the given method on it.
149-
pub fn spawn(
150-
vm: *VirtualMachine,
151-
actor_context: Value,
152-
method: *Object.Method,
153-
source_range: SourceRange,
154-
target_location: RegisterLocation,
155-
) !*Self {
156-
std.debug.assert(method.getArgumentSlotCount() == 0);
157-
158-
const self = try create(vm, actor_context);
159-
errdefer self.destroy(vm.allocator);
160-
161-
const new_activation = try self.activation_stack.getNewActivationSlot(vm.allocator);
162-
try method.activateMethod(vm, self.id, actor_context, &.{}, target_location, source_range, new_activation);
163-
164-
return self;
165-
}
166-
167-
pub fn create(vm: *VirtualMachine, actor_context: Value) !*Self {
149+
pub fn create(vm: *VirtualMachine, token: *Heap.AllocationToken, actor_context: Value) !*Self {
168150
const self = try vm.allocator.create(Self);
169151
errdefer vm.allocator.destroy(self);
170152

171153
// NOTE: If we're not in actor mode, then we belong to the global actor (which is this actor for the
172154
// first call to create); otherwise, we are always owned by the genesis actor.
173155
const owning_actor_id = if (vm.isInActorMode()) vm.genesis_actor.?.id else 0;
174156

175-
const actor_object = try Object.Actor.create(vm.heap, owning_actor_id, self, actor_context);
157+
const actor_object = try Object.Actor.create(token, owning_actor_id, self, actor_context);
176158

177159
self.init(actor_object);
178160
return self;
@@ -205,6 +187,18 @@ fn deinit(self: *Self, allocator: Allocator) void {
205187
self.activation_stack.deinit(allocator);
206188
}
207189

190+
pub fn activateMethod(
191+
self: *Self,
192+
vm: *VirtualMachine,
193+
token: *Heap.AllocationToken,
194+
method: *Object.Method,
195+
target_location: RegisterLocation,
196+
source_range: SourceRange,
197+
) !void {
198+
const activation_slot = try self.activation_stack.getNewActivationSlot(vm.allocator);
199+
method.activateMethod(vm, token, self.id, self.actor_object.get().context, &.{}, target_location, source_range, activation_slot);
200+
}
201+
208202
pub fn execute(self: *Self, vm: *VirtualMachine) !ActorResult {
209203
const current_activation_ref = self.activation_stack.getCurrent().takeRef(self.activation_stack);
210204

@@ -213,13 +207,14 @@ pub fn execute(self: *Self, vm: *VirtualMachine) !ActorResult {
213207
{
214208
var it = self.mailbox.first;
215209
while (it) |node| : (it = node.next) {
216-
const method = node.data.method.get();
210+
var method = node.data.method.get();
217211

218-
try vm.heap.ensureSpaceInEden(method.requiredSizeForActivation());
212+
var token = try vm.heap.getAllocation(method.requiredSizeForActivation());
213+
method = node.data.method.get();
219214

220215
const actor_context = self.actor_object.get().context;
221216
const new_activation = try self.activation_stack.getNewActivationSlot(vm.allocator);
222-
try method.activateMethod(vm, self.id, actor_context, node.data.arguments, .zero, node.data.source_range, new_activation);
217+
method.activateMethod(vm, &token, self.id, actor_context, node.data.arguments, .zero, node.data.source_range, new_activation);
223218

224219
self.message_sender = node.data.sender;
225220

src/runtime/ByteArray.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ const Self = @This();
1313

1414
header: *align(@alignOf(u64)) Header,
1515

16-
pub fn createFromString(heap: *Heap, string: []const u8) !Self {
17-
var self = try createUninitialized(heap, string.len);
16+
pub fn createFromString(token: *Heap.AllocationToken, string: []const u8) Self {
17+
var self = createUninitialized(token, string.len);
1818
std.mem.copy(u8, self.getValues(), string);
1919
return self;
2020
}
2121

22-
pub fn createUninitialized(heap: *Heap, size: usize) !Self {
23-
var memory_area = try heap.allocateInByteVectorSegment(requiredSizeForAllocation(size));
22+
pub fn createUninitialized(token: *Heap.AllocationToken, size: usize) Self {
23+
var memory_area = token.allocate(.ByteArray, requiredSizeForAllocation(size));
2424
var header = @ptrCast(*Header, memory_area);
2525

2626
header.init(size);

src/runtime/Heap.zig

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ const hash = @import("../utility/hash.zig");
1111
const debug = @import("../debug.zig");
1212
const Value = @import("./value.zig").Value;
1313
const Object = @import("./Object.zig");
14-
const ByteVector = @import("./ByteArray.zig");
14+
const ByteArray = @import("./ByteArray.zig");
1515
const Activation = @import("./Activation.zig");
1616
const HandleArea = @import("./HandleArea.zig");
1717
const VirtualMachine = @import("./VirtualMachine.zig");
1818
const ActivationStack = Activation.ActivationStack;
1919

2020
const GC_DEBUG = debug.GC_DEBUG;
21+
const GC_TOKEN_DEBUG = debug.GC_TOKEN_DEBUG;
2122
const GC_TRACK_SOURCE_DEBUG = debug.GC_TRACK_SOURCE_DEBUG;
2223
const REMEMBERED_SET_DEBUG = debug.REMEMBERED_SET_DEBUG;
2324

@@ -58,6 +59,27 @@ const EdenSize = 1 * 1024 * 1024;
5859
const NewSpaceSize = 4 * 1024 * 1024;
5960
const InitialOldSpaceSize = 16 * 1024 * 1024;
6061

62+
const Segment = enum { Object, ByteArray };
63+
pub const AllocationToken = struct {
64+
heap: *Self,
65+
total_bytes: usize,
66+
bytes_left: usize,
67+
68+
pub fn allocate(self: *@This(), segment: Segment, bytes: usize) [*]u64 {
69+
if (self.bytes_left < bytes) {
70+
std.debug.panic(
71+
"!!! Attempted to allocate {} bytes from {} byte-sized allocation token with {} bytes remaining!",
72+
.{ bytes, self.total_bytes, self.bytes_left },
73+
);
74+
}
75+
76+
self.bytes_left -= bytes;
77+
// NOTE: The only error this can raise is allocation failure during lazy allocation
78+
// which eden does not do.
79+
return self.heap.eden.allocateInSegment(self.heap.allocator, segment, bytes) catch unreachable;
80+
}
81+
};
82+
6183
pub fn create(allocator: Allocator, vm: *VirtualMachine) !*Self {
6284
const self = try allocator.create(Self);
6385
errdefer allocator.destroy(self);
@@ -115,15 +137,14 @@ fn deinit(self: *Self) void {
115137
}
116138
}
117139

118-
// Attempts to allocate `size` bytes in the object segment of the eden. If
119-
// necessary, garbage collection is performed in the process.
120-
// The given size must be a multiple of `@sizeOf(u64)`.
121-
pub fn allocateInObjectSegment(self: *Self, size: usize) ![*]u64 {
122-
return try self.eden.allocateInObjectSegment(self.allocator, size);
123-
}
140+
pub fn getAllocation(self: *Self, bytes: usize) !AllocationToken {
141+
if (GC_TOKEN_DEBUG) std.debug.print("Heap.getAllocation: Attempting to get a token of size {}\n", .{bytes});
142+
try self.eden.collectGarbage(self.allocator, bytes);
143+
144+
if (bytes % @sizeOf(u64) != 0)
145+
std.debug.panic("!!! Attempted to allocate {} bytes which is not a multiple of @sizeOf(u64)!", .{bytes});
124146

125-
pub fn allocateInByteVectorSegment(self: *Self, size: usize) ![*]u64 {
126-
return try self.eden.allocateInByteVectorSegment(self.allocator, size);
147+
return AllocationToken{ .heap = self, .total_bytes = bytes, .bytes_left = bytes };
127148
}
128149

129150
/// Mark the given address within the heap as an object which needs to know when
@@ -174,13 +195,6 @@ pub fn untrack(self: *Self, tracked: Tracked) void {
174195
}
175196
}
176197

177-
/// Ensures that the given amount of bytes are immediately available in eden, so
178-
/// garbage collection won't happen. Performs a pre-emptive garbage collection
179-
/// if there isn't enough space.
180-
pub fn ensureSpaceInEden(self: *Self, required_memory: usize) !void {
181-
try self.eden.collectGarbage(self.allocator, required_memory);
182-
}
183-
184198
/// Go through the whole heap, updating references to the given value with the
185199
/// new value.
186200
pub fn updateAllReferencesTo(self: *Self, old_value: Value, new_value: Value) !void {
@@ -434,15 +448,15 @@ const Space = struct {
434448
return new_address;
435449
}
436450

437-
/// Same as copyObjectTo, but for byte vectors.
438-
fn copyByteVectorTo(allocator: Allocator, address: [*]u64, target_space: *Space) [*]u64 {
439-
const byte_array = ByteVector.fromAddress(address);
451+
/// Same as copyObjectTo, but for byte arrays.
452+
fn copyByteArrayTo(allocator: Allocator, address: [*]u64, target_space: *Space) [*]u64 {
453+
const byte_array = ByteArray.fromAddress(address);
440454
const byte_array_size = byte_array.getSizeInMemory();
441455
std.debug.assert(byte_array_size % @sizeOf(u64) == 0);
442456

443457
const byte_array_size_in_words = byte_array_size / @sizeOf(u64);
444458
// We must have enough space at this point.
445-
const new_address = target_space.allocateInByteVectorSegment(allocator, byte_array_size) catch unreachable;
459+
const new_address = target_space.allocateInByteArraySegment(allocator, byte_array_size) catch unreachable;
446460
std.mem.copy(u64, new_address[0..byte_array_size_in_words], address[0..byte_array_size_in_words]);
447461

448462
return new_address;
@@ -454,7 +468,7 @@ const Space = struct {
454468
if (self.objectSegmentContains(address)) {
455469
return self.copyObjectTo(allocator, address, target_space);
456470
} else if (self.byteArraySegmentContains(address)) {
457-
return copyByteVectorTo(allocator, address, target_space);
471+
return copyByteArrayTo(allocator, address, target_space);
458472
} else if (require_copy) {
459473
std.debug.panic("!!! copyAddress called with an address that's not allocated in this space!", .{});
460474
}
@@ -568,8 +582,8 @@ const Space = struct {
568582
.{ target_object_segment_index, target_space.object_segment.len },
569583
);
570584

571-
// Try to catch up to the target space's object and byte vector cursors,
572-
// copying any other objects/byte vectors that still exist in this
585+
// Try to catch up to the target space's object and byte array cursors,
586+
// copying any other objects/byte arrays that still exist in this
573587
// space.
574588
while (target_object_segment_index < target_space.object_segment.len) : (target_object_segment_index += 1) {
575589
const word_ptr = &target_space.object_segment[target_object_segment_index];
@@ -581,7 +595,7 @@ const Space = struct {
581595
if (self.objectSegmentContains(address)) {
582596
word_ptr.* = Value.fromObjectAddress(try self.copyObjectTo(allocator, address, target_space)).data;
583597
} else if (self.byteArraySegmentContains(address)) {
584-
word_ptr.* = Value.fromObjectAddress(copyByteVectorTo(allocator, address, target_space)).data;
598+
word_ptr.* = Value.fromObjectAddress(copyByteArrayTo(allocator, address, target_space)).data;
585599
}
586600
}
587601
}
@@ -753,13 +767,11 @@ const Space = struct {
753767
}
754768

755769
/// Allocates the requested amount in bytes in the object segment of this
756-
/// space, garbage collecting if there is not enough space.
757-
pub fn allocateInObjectSegment(self: *Space, allocator: Allocator, size: usize) ![*]u64 {
770+
/// space. Panics if there isn't enough memory.
771+
fn allocateInObjectSegment(self: *Space, allocator: Allocator, size: usize) ![*]u64 {
758772
if (self.lazy_allocate)
759773
try self.allocateMemory(allocator);
760774

761-
if (self.freeMemory() < size) try self.collectGarbage(allocator, size);
762-
763775
const size_in_words = @divExact(size, @sizeOf(u64));
764776
const current_object_segment_offset = self.object_segment.len;
765777
self.object_segment.len += size_in_words;
@@ -771,14 +783,12 @@ const Space = struct {
771783
return start_of_object;
772784
}
773785

774-
/// Allocates the requested amount in bytes in the byte vector segment of
775-
/// this space, garbage collecting if there is not enough space.
776-
pub fn allocateInByteVectorSegment(self: *Space, allocator: Allocator, size: usize) ![*]u64 {
786+
/// Allocates the requested amount in bytes in the byte array segment of
787+
/// this space. Panics if there isn't enough memory.
788+
fn allocateInByteArraySegment(self: *Space, allocator: Allocator, size: usize) ![*]u64 {
777789
if (self.lazy_allocate)
778790
try self.allocateMemory(allocator);
779791

780-
if (self.freeMemory() < size) try self.collectGarbage(allocator, size);
781-
782792
const size_in_words = @divExact(size, @sizeOf(u64));
783793
self.byte_array_segment.ptr -= size_in_words;
784794
self.byte_array_segment.len += size_in_words;
@@ -789,6 +799,15 @@ const Space = struct {
789799
return self.byte_array_segment.ptr;
790800
}
791801

802+
pub fn allocateInSegment(self: *Space, allocator: Allocator, segment: Segment, size: usize) Allocator.Error![*]u64 {
803+
return switch (segment) {
804+
.Object => self.allocateInObjectSegment(allocator, size),
805+
.ByteArray => self.allocateInByteArraySegment(allocator, size),
806+
};
807+
}
808+
809+
/// Allocates the requested amount of bytes in the appropriate segment of
810+
/// this space.
792811
/// Adds the given address to the finalization set of this space.
793812
pub fn addToFinalizationSet(self: *Space, allocator: Allocator, address: [*]u64) !void {
794813
try self.finalization_set.put(allocator, address, {});
@@ -1019,14 +1038,14 @@ test "link an object to another and perform scavenge" {
10191038

10201039
// The object being referenced
10211040
var referenced_object_map = try Object.Map.Slots.create(heap, 1);
1022-
var actual_name = try ByteVector.createFromString(heap, "actual");
1041+
var actual_name = try ByteArray.createFromString(heap, "actual");
10231042
referenced_object_map.getSlots()[0].initConstant(actual_name, .NotParent, Value.fromUnsignedInteger(0xDEADBEEF));
10241043
var referenced_object = try Object.Slots.create(heap, referenced_object_map, &[_]Value{});
10251044

10261045
// The "activation object", which is how we get a reference to the object in
10271046
// the from space after the tenure is done
10281047
var activation_object_map = try Object.Map.Slots.create(heap, 1);
1029-
var reference_name = try ByteVector.createFromString(heap, "reference");
1048+
var reference_name = try ByteArray.createFromString(heap, "reference");
10301049
activation_object_map.getSlots()[0].initMutable(Object.Map.Slots, activation_object_map, reference_name, .NotParent);
10311050
var activation_object = try Object.Slots.create(heap, activation_object_map, &[_]Value{referenced_object.asValue()});
10321051

@@ -1042,15 +1061,15 @@ test "link an object to another and perform scavenge" {
10421061
var new_activation_object_map = new_activation_object.getMap();
10431062
try std.testing.expect(activation_object_map != new_activation_object_map);
10441063
try std.testing.expect(activation_object_map.getSlots()[0].name.asObjectAddress() != new_activation_object_map.getSlots()[0].name.asObjectAddress());
1045-
try std.testing.expectEqualStrings("reference", new_activation_object_map.getSlots()[0].name.asByteVector().getValues());
1064+
try std.testing.expectEqualStrings("reference", new_activation_object_map.getSlots()[0].name.asByteArray().getValues());
10461065

10471066
// Find the new referenced object
10481067
var new_referenced_object = new_activation_object.getAssignableSlotValueByName("reference").?.asObject().asSlotsObject();
10491068
try std.testing.expect(referenced_object != new_referenced_object);
10501069
var new_referenced_object_map = new_referenced_object.getMap();
10511070
try std.testing.expect(referenced_object_map != new_referenced_object_map);
10521071
try std.testing.expect(referenced_object_map.getSlots()[0].name.asObjectAddress() != new_referenced_object_map.getSlots()[0].name.asObjectAddress());
1053-
try std.testing.expectEqualStrings("actual", new_referenced_object_map.getSlots()[0].name.asByteVector().getValues());
1072+
try std.testing.expectEqualStrings("actual", new_referenced_object_map.getSlots()[0].name.asByteArray().getValues());
10541073

10551074
// Verify that the map map is shared (aka forwarding addresses work)
10561075
try std.testing.expectEqual(

src/runtime/Object.zig

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,17 @@ pub fn asValue(self: Self) Value {
104104
return Value.fromObjectAddress(self.getAddress());
105105
}
106106

107-
pub fn clone(self: Self, heap: *Heap, actor_id: u31) !Self {
107+
pub fn clone(self: Self, token: *Heap.AllocationToken, actor_id: u31) Self {
108108
return switch (self.header.getObjectType()) {
109109
.ForwardingReference, .Activation, .Method, .Map => unreachable,
110-
.Slots => fromAddress((try self.asSlotsObject().clone(heap, actor_id)).asObjectAddress()),
111-
.Block => fromAddress((try self.asBlockObject().clone(heap, actor_id)).asObjectAddress()),
112-
.ByteArray => fromAddress((try self.asByteArrayObject().clone(heap, actor_id)).asObjectAddress()),
113-
.Array => fromAddress((try self.asArrayObject().clone(heap, actor_id)).asObjectAddress()),
110+
.Slots => fromAddress(self.asSlotsObject().clone(token, actor_id).asObjectAddress()),
111+
.Block => fromAddress(self.asBlockObject().clone(token, actor_id).asObjectAddress()),
112+
.ByteArray => fromAddress(self.asByteArrayObject().clone(token, actor_id).asObjectAddress()),
113+
.Array => fromAddress(self.asArrayObject().clone(token, actor_id).asObjectAddress()),
114114
// Managed values are not clonable. Instead we return the same (immutable) reference.
115115
.Managed => self,
116116
.Actor => @panic("TODO Handle cloning of actor object"),
117-
.ActorProxy => fromAddress((try self.asActorProxyObject().clone(heap, actor_id)).asObjectAddress()),
117+
.ActorProxy => fromAddress(self.asActorProxyObject().clone(token, actor_id).asObjectAddress()),
118118
};
119119
}
120120

0 commit comments

Comments
 (0)