From bc2a0434c7764854b0047ad7bc4d78f6b8f01d89 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Tue, 22 Jul 2025 21:29:49 +0200 Subject: [PATCH] Parser callbacks, measure and log request time --- packages/myid/src/http.zig | 19 +++- packages/myid/src/http/Parser.zig | 149 +++++++++++++++++++----------- 2 files changed, 114 insertions(+), 54 deletions(-) diff --git a/packages/myid/src/http.zig b/packages/myid/src/http.zig index 76b7539..6623a23 100644 --- a/packages/myid/src/http.zig +++ b/packages/myid/src/http.zig @@ -119,7 +119,14 @@ pub fn process(conn: std.net.Server.Connection) !void { var leftover_bytes: usize = 0; while (true) { - var parser = Parser.init(); + const start = try std.time.Instant.now(); + + var route: Parser.Route = undefined; + + var parser = Parser.init(.{ + .self = &route, + .route = routeCallback, + }); var total_bytes_read: usize = 0; while (true) { @@ -185,5 +192,15 @@ pub fn process(conn: std.net.Server.Connection) !void { if (leftover_bytes > 0) { @memmove(&read_buffer, read_buffer[total_bytes_read - leftover_bytes .. total_bytes_read]); } + + const end = try std.time.Instant.now(); + const time_ns = end.since(start); + const time_us = @divFloor(time_ns, std.time.ns_per_us); + + log.info("{s} {s} ({} μs)", .{ @tagName(route.method), route.pathname, time_us }); } } + +fn routeCallback(self: ?*anyopaque, route: Parser.Route) void { + @as(*Parser.Route, @alignCast(@ptrCast(self))).* = route; +} diff --git a/packages/myid/src/http/Parser.zig b/packages/myid/src/http/Parser.zig index 06f3bb1..cdb36c6 100644 --- a/packages/myid/src/http/Parser.zig +++ b/packages/myid/src/http/Parser.zig @@ -2,7 +2,15 @@ const std = @import("std"); const Parser = @This(); -const log = std.log.scoped(.http_parser); +const Callbacks = struct { + self: ?*anyopaque = null, + + route: ?*const fn (self: ?*anyopaque, route: Route) void = null, + header: ?*const fn (self: ?*anyopaque, name: []const u8, value: []const u8) void = null, + body: ?*const fn (self: ?*anyopaque, body: []const u8) void = null, + + pub const init: Callbacks = .{}; +}; const Error = error{ MethodNotSupported, @@ -12,6 +20,32 @@ const Error = error{ }; const State = union(enum) { + pub fn methodComplete(method: Method) State { + return .{ + .method_complete = .{ + .method = method, + }, + }; + } + + pub fn pathname(method: Method, p: []const u8) State { + return .{ + .pathname_state = .{ + .method = method, + .pathname = p, + }, + }; + } + + pub fn headerValue(name: []const u8, value: []const u8) State { + return .{ + .header_value = .{ + .name = name, + .value = value, + }, + }; + } + init: void, method_d: void, method_g: void, @@ -30,8 +64,8 @@ const State = union(enum) { method_dele: void, method_patc: void, method_delet: void, - method_complete: void, - pathname: []const u8, + method_complete: struct { method: Method }, + pathname_state: struct { method: Method, pathname: []const u8 }, pathname_complete: void, version_h: void, version_ht: void, @@ -44,23 +78,23 @@ const State = union(enum) { start_line_end: void, header_name_start: void, header_name: []const u8, - header_value: []const u8, + header_value: struct { name: []const u8, value: []const u8 }, header_line_end: void, headers_end: void, body: []const u8, }; -pub const ConsumeResult = struct { +const ConsumeResult = struct { consumed: usize, done: bool, }; -pub const ConsumeCharResult = enum { +const ConsumeCharResult = enum { not_done, done, }; -const Method = enum { +pub const Method = enum { DELETE, GET, HEAD, @@ -69,12 +103,23 @@ const Method = enum { PUT, }; -state: State = .init, -current_header_is_content_length: bool = false, -content_length: usize = 0, +pub const Route = struct { + method: Method, + pathname: []const u8, +}; -pub fn init() Parser { - return .{}; +callbacks: Callbacks, +state: State, +current_header_is_content_length: bool, +content_length: usize, + +pub fn init(callbacks: Callbacks) Parser { + return .{ + .callbacks = callbacks, + .state = .init, + .current_header_is_content_length = false, + .content_length = 0, + }; } pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult { @@ -87,9 +132,17 @@ pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult { self.state = .{ .body = new_body }; i += to_consume; + const done = new_body.len >= self.content_length; + + if (done) { + if (self.callbacks.body) |bodyCallback| { + bodyCallback(self.callbacks.self, new_body); + } + } + return .{ .consumed = i, - .done = new_body.len >= self.content_length, + .done = done, }; }, else => { @@ -143,10 +196,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { else => return error.MethodNotSupported, }, .method_ge => switch (c) { - 'T' => { - self.state = .method_complete; - log.debug("Method: GET", .{}); - }, + 'T' => self.state = .methodComplete(.GET), else => return error.MethodNotSupported, }, .method_he => switch (c) { @@ -162,10 +212,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { else => return error.MethodNotSupported, }, .method_pu => switch (c) { - 'T' => { - self.state = .method_complete; - log.debug("Method: PUT", .{}); - }, + 'T' => self.state = .methodComplete(.PUT), else => return error.MethodNotSupported, }, .method_del => switch (c) { @@ -173,10 +220,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { else => return error.MethodNotSupported, }, .method_hea => switch (c) { - 'D' => { - self.state = .method_complete; - log.debug("Method: HEAD", .{}); - }, + 'D' => self.state = .methodComplete(.HEAD), else => return error.MethodNotSupported, }, .method_pat => switch (c) { @@ -184,10 +228,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { else => return error.MethodNotSupported, }, .method_pos => switch (c) { - 'T' => { - self.state = .method_complete; - log.debug("Method: POST", .{}); - }, + 'T' => self.state = .methodComplete(.POST), else => return error.MethodNotSupported, }, .method_dele => switch (c) { @@ -195,29 +236,28 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { else => return error.MethodNotSupported, }, .method_patc => switch (c) { - 'H' => { - self.state = .method_complete; - log.debug("Method: PATCH", .{}); - }, + 'H' => self.state = .methodComplete(.PATCH), else => return error.MethodNotSupported, }, .method_delet => switch (c) { - 'E' => { - self.state = .method_complete; - log.debug("Method: DELETE", .{}); - }, + 'E' => self.state = .methodComplete(.DELETE), else => return error.MethodNotSupported, }, - .method_complete => switch (c) { - ' ' => self.state = .{ .pathname = @as([*]const u8, @ptrCast(c_ptr))[1..1] }, + .method_complete => |s| switch (c) { + ' ' => self.state = .pathname(s.method, @as([*]const u8, @ptrCast(c_ptr))[1..1]), else => return error.MethodNotSupported, }, - .pathname => |pathname| switch (c) { + .pathname_state => |s| switch (c) { ' ' => { self.state = .pathname_complete; - log.debug("Pathname [{}]: {s}", .{ pathname.len, pathname }); + if (self.callbacks.route) |routeCallback| { + routeCallback(self.callbacks.self, .{ + .method = s.method, + .pathname = s.pathname, + }); + } }, - else => self.state = .{ .pathname = pathname.ptr[0 .. pathname.len + 1] }, + else => self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + 1]), }, .pathname_complete => switch (c) { 'H' => self.state = .version_h, @@ -248,10 +288,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { else => return error.HttpVersionNotSupported, }, .@"version_http/1.@" => switch (c) { - '1' => { - self.state = .version_complete; - log.debug("Version: HTTP/1.1", .{}); - }, + '1' => self.state = .version_complete, else => return error.HttpVersionNotSupported, }, .version_complete => switch (c) { @@ -268,23 +305,24 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { }, .header_name => |name| switch (c) { ':' => { - self.state = .{ .header_value = @as([*]const u8, @ptrCast(c_ptr))[1..1] }; + self.state = .headerValue(name, @as([*]const u8, @ptrCast(c_ptr))[1..1]); self.current_header_is_content_length = std.ascii.eqlIgnoreCase(name, "Content-Length"); - log.debug("Header name [{}]: {s}", .{ name.len, name }); }, else => self.state = .{ .header_name = name.ptr[0 .. name.len + 1] }, }, - .header_value => |value| switch (c) { + .header_value => |s| switch (c) { '\r' => { self.state = .header_line_end; - const value_trimmed = std.mem.trim(u8, value, " \t"); - log.debug("Header value [{}]: {s}", .{ value_trimmed.len, value_trimmed }); + const value_trimmed = std.mem.trim(u8, s.value, " \t"); if (self.current_header_is_content_length) { self.content_length = std.fmt.parseInt(usize, value_trimmed, 10) catch return error.InvalidContentLength; self.current_header_is_content_length = false; } + if (self.callbacks.header) |headerCallback| { + headerCallback(self.callbacks.self, s.name, value_trimmed); + } }, - else => self.state = .{ .header_value = value.ptr[0 .. value.len + 1] }, + else => self.state = .headerValue(s.name, s.value.ptr[0 .. s.value.len + 1]), }, .header_line_end => switch (c) { '\n' => self.state = .header_name_start, @@ -293,7 +331,9 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { .headers_end => switch (c) { '\n' => { if (self.content_length == 0) { - log.debug("End of request (no body)", .{}); + if (self.callbacks.body) |bodyCallback| { + bodyCallback(self.callbacks.self, &.{}); + } return .done; } self.state = .{ .body = @as([*]const u8, @ptrCast(c_ptr))[1..1] }; @@ -304,6 +344,9 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { const new_body = body.ptr[0 .. body.len + 1]; self.state = .{ .body = new_body }; if (new_body.len >= self.content_length) { + if (self.callbacks.body) |bodyCallback| { + bodyCallback(self.callbacks.self, new_body); + } return .done; } },