From 961a4b6b31e20d4aaa561a367168e85ed729ee5e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 4 Jul 2024 14:50:47 -0700 Subject: [PATCH] terminal: support page oom with hyperlinks --- src/terminal/PageList.zig | 57 ++++++++++++++++++++++++++++++++++++ src/terminal/Screen.zig | 61 +++++++++++++++++++++++++++++++++++---- src/terminal/page.zig | 4 ++- 3 files changed, 116 insertions(+), 6 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 447c8b622..e339f66ba 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1849,6 +1849,12 @@ pub const AdjustCapacity = struct { /// Adjust the number of available grapheme bytes in the page. grapheme_bytes: ?usize = null, + + /// Adjust the number of available hyperlink bytes in the page. + hyperlink_bytes: ?usize = null, + + /// Adjust the number of available string bytes in the page. + string_bytes: ?usize = null, }; /// Adjust the capcaity of the given page in the list. This should @@ -1884,6 +1890,14 @@ pub fn adjustCapacity( const aligned = try std.math.ceilPowerOfTwo(usize, v); cap.grapheme_bytes = @max(cap.grapheme_bytes, aligned); } + if (adjustment.hyperlink_bytes) |v| { + const aligned = try std.math.ceilPowerOfTwo(usize, v); + cap.hyperlink_bytes = @max(cap.hyperlink_bytes, aligned); + } + if (adjustment.string_bytes) |v| { + const aligned = try std.math.ceilPowerOfTwo(usize, v); + cap.string_bytes = @max(cap.string_bytes, aligned); + } log.info("adjusting page capacity={}", .{cap}); @@ -4040,6 +4054,49 @@ test "PageList adjustCapacity to increase graphemes" { } } +test "PageList adjustCapacity to increase hyperlinks" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 2, 0); + defer s.deinit(); + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + // Write all our data so we can assert its the same after + for (0..s.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(x) }, + }; + } + } + } + + // Increase our graphemes + _ = try s.adjustCapacity( + s.pages.first.?, + .{ .hyperlink_bytes = @max(std_capacity.hyperlink_bytes * 2, 2048) }, + ); + + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + for (0..s.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + try testing.expectEqual( + @as(u21, @intCast(x)), + rac.cell.content.codepoint, + ); + } + } + } +} + test "PageList pageIterator single page" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 2c4797841..885412768 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1380,19 +1380,65 @@ pub fn startHyperlink( self: *Screen, uri: []const u8, id_: ?[]const u8, +) !void { + // Loop until we have enough page memory to add the hyperlink + while (true) { + if (self.startHyperlinkOnce(uri, id_)) { + return; + } else |err| switch (err) { + // An actual self.alloc OOM is a fatal error. + error.RealOutOfMemory => return error.OutOfMemory, + + // strings table is out of memory, adjust it up + error.StringsOutOfMemory => _ = try self.pages.adjustCapacity( + self.cursor.page_pin.page, + .{ .string_bytes = self.cursor.page_pin.page.data.capacity.string_bytes * 2 }, + ), + + // hyperlink set is out of memory, adjust it up + error.SetOutOfMemory => _ = try self.pages.adjustCapacity( + self.cursor.page_pin.page, + .{ .hyperlink_bytes = self.cursor.page_pin.page.data.capacity.hyperlink_bytes * 2 }, + ), + + // hyperlink set is too full, rehash it + error.SetNeedsRehash => _ = try self.pages.adjustCapacity( + self.cursor.page_pin.page, + .{}, + ), + } + + // If we get here, we adjusted capacity so our page has changed + // so we need to reload the cursor pins. + self.cursorReload(); + self.assertIntegrity(); + } +} + +/// This is like startHyperlink but if we have to adjust page capacities +/// this returns error.PageAdjusted. This is useful so that we unwind +/// all the previous state and try again. +fn startHyperlinkOnce( + self: *Screen, + uri: []const u8, + id_: ?[]const u8, ) !void { // End any prior hyperlink self.endHyperlink(); // Create our hyperlink state. - const link = try Hyperlink.create(self.alloc, uri, id_); + const link = Hyperlink.create(self.alloc, uri, id_) catch |err| switch (err) { + error.OutOfMemory => return error.RealOutOfMemory, + }; errdefer link.destroy(self.alloc); // Copy our URI into the page memory. var page = &self.cursor.page_pin.page.data; const string_alloc = &page.string_alloc; const page_uri: Offset(u8).Slice = uri: { - const buf = try string_alloc.alloc(u8, page.memory, uri.len); + const buf = string_alloc.alloc(u8, page.memory, uri.len) catch |err| switch (err) { + error.OutOfMemory => return error.StringsOutOfMemory, + }; errdefer string_alloc.free(page.memory, buf); @memcpy(buf, uri); @@ -1408,7 +1454,9 @@ pub fn startHyperlink( // Copy our ID into page memory or create an implicit ID via the counter const page_id: hyperlink.Hyperlink.Id = if (id_) |id| explicit: { - const buf = try string_alloc.alloc(u8, page.memory, id.len); + const buf = string_alloc.alloc(u8, page.memory, id.len) catch |err| switch (err) { + error.OutOfMemory => return error.StringsOutOfMemory, + }; errdefer string_alloc.free(page.memory, buf); @memcpy(buf, id); @@ -1431,11 +1479,14 @@ pub fn startHyperlink( }; // Put our hyperlink into the hyperlink set to get an ID - const id = try page.hyperlink_set.addContext( + const id = page.hyperlink_set.addContext( page.memory, .{ .id = page_id, .uri = page_uri }, .{ .page = page }, - ); + ) catch |err| switch (err) { + error.OutOfMemory => return error.SetOutOfMemory, + error.NeedsRehash => return error.SetNeedsRehash, + }; errdefer page.hyperlink_set.release(page.memory, id); // Save it all diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 37b7c6458..0ee041fa0 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -58,7 +58,7 @@ const string_bytes_default = string_count_default * string_chunk; /// The cell multiplier is the number of cells per hyperlink entry that /// we support. A hyperlink can be longer than this multiplier; the multiplier /// just sets the total capacity to simplify adjustable size metrics. -const hyperlink_count_default = 32; +const hyperlink_count_default = 4; const hyperlink_bytes_default = hyperlink_count_default * @sizeOf(hyperlink.Set.Item); const hyperlink_cell_multiplier = 16; @@ -681,6 +681,8 @@ pub const Page = struct { .{ .page = self }, ); try self.setHyperlink(dst_row, dst_cell, dst_id); + + // TODO: copy the strings } if (src_cell.style_id != style.default_id) { dst_row.styled = true;