agent: reduce allocations and reuse buffers

This commit is contained in:
Adrià Arrufat
2026-05-05 11:22:12 +02:00
parent 431e3d6a84
commit 32b3fb08fb
3 changed files with 24 additions and 20 deletions

View File

@@ -443,18 +443,17 @@ fn runScript(self: *Self, path: []const u8) bool {
};
defer file.close();
const content = file.readToEndAlloc(self.allocator, 10 * 1024 * 1024) catch |err| {
self.terminal.printErrorFmt("Failed to read script: {s}", .{@errorName(err)});
return false;
};
defer self.allocator.free(content);
self.terminal.printInfoFmt("Running script: {s}", .{path});
var script_arena: std.heap.ArenaAllocator = .init(self.allocator);
defer script_arena.deinit();
const sa = script_arena.allocator();
const content = file.readToEndAlloc(sa, 10 * 1024 * 1024) catch |err| {
self.terminal.printErrorFmt("Failed to read script: {s}", .{@errorName(err)});
return false;
};
var iter: Command.ScriptIterator = .init(sa, content);
var last_comment: ?[]const u8 = null;
var replacements: std.ArrayList(Replacement) = .empty;
@@ -1021,8 +1020,8 @@ fn buildUserMessageParts(
}
var parts: std.ArrayList(zenai.provider.ContentPart) = .empty;
const combined = try std.fmt.allocPrint(ma, "{s}{s}", .{ text_prefix.items, user_input });
try parts.append(ma, .{ .text = combined });
try text_prefix.appendSlice(ma, user_input);
try parts.append(ma, .{ .text = try text_prefix.toOwnedSlice(ma) });
for (inline_parts.items) |p| try parts.append(ma, p);
return parts.toOwnedSlice(ma);
}

View File

@@ -8,6 +8,9 @@ const Self = @This();
allocator: std.mem.Allocator,
file: ?std.fs.File,
needs_separator: bool,
/// Reused between `record()` calls so each command line doesn't alloc/free.
/// Cleared with `clearRetainingCapacity` instead.
buf: std.Io.Writer.Allocating,
/// Append-open `path`, inserting a leading newline if the file is non-empty.
/// A null path disables recording.
@@ -27,10 +30,11 @@ pub fn init(allocator: std.mem.Allocator, path: ?[]const u8) Self {
break :blk f;
} else null;
return .{ .allocator = allocator, .file = file, .needs_separator = false };
return .{ .allocator = allocator, .file = file, .needs_separator = false, .buf = .init(allocator) };
}
pub fn deinit(self: *Self) void {
self.buf.deinit();
if (self.file) |f| f.close();
}
@@ -38,11 +42,10 @@ pub fn record(self: *Self, cmd: Command.Command) void {
const f = self.file orelse return;
if (!cmd.isRecorded()) return;
var aw: std.Io.Writer.Allocating = .init(self.allocator);
defer aw.deinit();
cmd.format(&aw.writer) catch return;
aw.writer.writeByte('\n') catch return;
_ = f.write(aw.written()) catch return;
self.buf.clearRetainingCapacity();
cmd.format(&self.buf.writer) catch return;
self.buf.writer.writeByte('\n') catch return;
_ = f.write(self.buf.written()) catch return;
self.needs_separator = true;
}
@@ -61,7 +64,7 @@ test "record writes state-mutating commands" {
const file = tmp.dir.createFile("test.lp", .{ .read = true }) catch unreachable;
var recorder: Self = .{ .allocator = std.testing.allocator, .file = file, .needs_separator = false };
var recorder: Self = .{ .allocator = std.testing.allocator, .file = file, .needs_separator = false, .buf = .init(std.testing.allocator) };
defer recorder.deinit();
recorder.record(Command.parse("GOTO https://example.com"));
@@ -103,7 +106,7 @@ test "record skips empty and comment lines" {
const file = tmp.dir.createFile("test2.lp", .{ .read = true }) catch unreachable;
var recorder: Self = .{ .allocator = std.testing.allocator, .file = file, .needs_separator = false };
var recorder: Self = .{ .allocator = std.testing.allocator, .file = file, .needs_separator = false, .buf = .init(std.testing.allocator) };
defer recorder.deinit();
recorder.record(Command.parse(""));
@@ -120,7 +123,7 @@ test "record skips empty and comment lines" {
}
test "recorder with null file is no-op" {
var recorder: Self = .{ .allocator = std.testing.allocator, .file = null, .needs_separator = false };
var recorder: Self = .{ .allocator = std.testing.allocator, .file = null, .needs_separator = false, .buf = .init(std.testing.allocator) };
recorder.record(Command.parse("GOTO https://example.com"));
recorder.recordComment("# test");
recorder.deinit();

View File

@@ -1011,9 +1011,11 @@ pub fn substituteEnvVars(arena: std.mem.Allocator, input: []const u8) []const u8
// Same gate as `execGetEnv`: only `LP_*` is resolvable. A
// prompt-injected `fill('$ANTHROPIC_API_KEY')` would otherwise
// leak the resolved value into the page DOM.
const env_val: ?[:0]const u8 = if (std.ascii.startsWithIgnoreCase(name, "LP_")) blk: {
const name_z = arena.dupeZ(u8, name) catch return input;
break :blk std.posix.getenv(name_z);
var name_buf: [256]u8 = undefined;
const env_val: ?[:0]const u8 = if (std.ascii.startsWithIgnoreCase(name, "LP_") and name.len < name_buf.len) blk: {
@memcpy(name_buf[0..name.len], name);
name_buf[name.len] = 0;
break :blk std.posix.getenv(name_buf[0..name.len :0]);
} else null;
if (env_val) |val| {
result.appendSlice(arena, val) catch return input;