Skip to content

Commit

Permalink
chore(all): reduce allocations & general perf
Browse files Browse the repository at this point in the history
  • Loading branch information
mookums committed Dec 23, 2024
1 parent 35df0ff commit 12d05b9
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 91 deletions.
7 changes: 7 additions & 0 deletions examples/middleware/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ fn pre_middleware(next: *Next, _: void) !void {
return try next.run();
}

fn pre_fail_middleware(next: *Next, _: void) !void {
log.info("pre fail request middleware: {s}", .{next.ctx.request.uri.?});
return error.ExpectedFailure;
}

fn post_middleware(next: *Next, _: void) !void {
log.info("post request middleware: {s}", .{next.ctx.request.uri.?});
return try next.run();
Expand All @@ -67,6 +72,8 @@ pub fn main() !void {
var router = try Router.init(allocator, &.{
Middleware.init().before({}, pre_middleware).after({}, post_middleware).layer(),
Route.init("/").get(num, root_handler).layer(),
Middleware.init().before({}, pre_fail_middleware).layer(),
Route.init("/fail").get(num, root_handler).layer(),
}, .{});
defer router.deinit(allocator);
router.print_route_tree();
Expand Down
2 changes: 1 addition & 1 deletion src/http/context.zig
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub const Context = struct {
captures: []Capture,
queries: *QueryMap,
provision: *Provision,
next: *Next,
next: Next,
triggered: bool = false,

pub fn to_sse(self: *Self, then: TaskFn(bool, *SSE)) !void {
Expand Down
25 changes: 25 additions & 0 deletions src/http/provision.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ const Capture = @import("router/routing_trie.zig").Capture;
const QueryMap = @import("router/routing_trie.zig").QueryMap;
const Request = @import("request.zig").Request;
const Response = @import("response.zig").Response;
const Context = @import("context.zig").Context;
const ServerConfig = @import("server.zig").ServerConfig;

const Runtime = @import("tardy").Runtime;

pub const Stage = union(enum) {
header,
body: usize,
Expand All @@ -25,9 +28,11 @@ pub const Provision = struct {
request: Request,
response: Response,
stage: Stage,
context: Context,

pub const InitContext = struct {
allocator: std.mem.Allocator,
runtime: *Runtime,
config: ServerConfig,
};

Expand Down Expand Up @@ -58,6 +63,26 @@ pub const Provision = struct {
provision.response = Response.init(ctx.allocator, config.header_count_max) catch {
@panic("attempting to statically allocate more memory than available. (Response)");
};

// Anything undefined within here is sourced at Handler time.
provision.context = .{
.provision = provision,
.allocator = provision.arena.allocator(),
.request = &provision.request,
.response = &provision.response,
.runtime = ctx.runtime,
.next = .{
.ctx = &provision.context,
.stage = .pre,
// Handler Time.
.pre_chain = undefined,
.post_chain = undefined,
},

// Handler Time.
.captures = undefined,
.queries = undefined,
};
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/http/router.zig
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ pub const Router = struct {
captures: []Capture,
queries: *QueryMap,
) !FoundBundle {
return try self.routes.get_route(path, captures, queries) orelse {
queries.clear();
queries.clear();

return try self.routes.get_bundle(path, captures, queries) orelse {
const not_found_bundle: Bundle = .{
.pre = &.{},
.route = Route.init("").all({}, self.configuration.not_found),
Expand Down
24 changes: 22 additions & 2 deletions src/http/router/middleware.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const std = @import("std");
const log = std.log.scoped(.@"zzz/router/middleware");
const assert = std.debug.assert;

const Runtime = @import("tardy").Runtime;
Expand All @@ -9,6 +10,7 @@ const Task = @import("tardy").TaskFn;
const Pseudoslice = @import("../../core/pseudoslice.zig").Pseudoslice;
const Server = @import("../server.zig").Server;

const Mime = @import("../mime.zig").Mime;
const Route = @import("route.zig").Route;
const HandlerWithData = @import("route.zig").HandlerWithData;
const Layer = @import("layer.zig").Layer;
Expand All @@ -34,13 +36,31 @@ pub const Next = struct {
assert(n.pre_chain.chain.len > 0);
const next_middleware = n.pre_chain.chain[0];
n.pre_chain.chain = n.pre_chain.chain[1..];
try @call(.auto, next_middleware.middleware, .{ n, next_middleware.data });
@call(.auto, next_middleware.middleware, .{ n, next_middleware.data }) catch |e| {
log.err("\"{s}\" [pre] middleware failed with error: {}", .{ n.ctx.provision.request.uri.?, e });
n.ctx.provision.response.set(.{
.status = .@"Internal Server Error",
.mime = Mime.HTML,
.body = "",
});

return try n.ctx.respond_without_middleware();
};
},
.post => {
assert(n.post_chain.len > 0);
const next_middleware = n.post_chain[0];
n.post_chain = n.post_chain[1..];
try @call(.auto, next_middleware.middleware, .{ n, next_middleware.data });
@call(.auto, next_middleware.middleware, .{ n, next_middleware.data }) catch |e| {
log.err("\"{s}\" [post] middleware failed with error: {}", .{ n.ctx.provision.request.uri.?, e });
n.ctx.provision.response.set(.{
.status = .@"Internal Server Error",
.mime = Mime.HTML,
.body = "",
});

return try n.ctx.respond_without_middleware();
};
},
}
}
Expand Down
112 changes: 52 additions & 60 deletions src/http/router/routing_trie.zig
Original file line number Diff line number Diff line change
Expand Up @@ -233,70 +233,62 @@ pub const RoutingTrie = struct {
print_node(&self.root, 0);
}

pub fn get_route(
pub fn get_bundle(
self: Self,
path: []const u8,
captures: []Capture,
queries: *QueryMap,
) !?FoundBundle {
var capture_idx: usize = 0;

queries.clear();
const query_pos = std.mem.indexOfScalar(u8, path, '?');
var iter = std.mem.tokenizeScalar(u8, path[0..(query_pos orelse path.len)], '/');

var current = self.root;

slash_loop: while (iter.next()) |chunk| {
const fragment = Token{ .fragment = chunk };

// If it is the fragment, match it here.
if (current.children.get(fragment)) |child| {
current = child;
continue;
}

var matched = false;
for (std.meta.tags(TokenMatch)) |token_type| {
const token = Token{ .match = token_type };
if (current.children.get(token)) |child| {
matched = true;
switch (token_type) {
.signed => if (std.fmt.parseInt(i64, chunk, 10)) |value| {
captures[capture_idx] = Capture{ .signed = value };
} else |_| continue,
.unsigned => if (std.fmt.parseInt(u64, chunk, 10)) |value| {
captures[capture_idx] = Capture{ .unsigned = value };
} else |_| continue,
.float => if (std.fmt.parseFloat(f64, chunk)) |value| {
captures[capture_idx] = Capture{ .float = value };
} else |_| continue,
.string => captures[capture_idx] = Capture{ .string = chunk },
// This ends the matching sequence and claims everything.
// Does not claim the query values.
.remaining => {
const rest = iter.buffer[(iter.index - chunk.len)..];
captures[capture_idx] = Capture{ .remaining = rest };

current = child;
capture_idx += 1;

break :slash_loop;
},
}

current = child;
capture_idx += 1;
var child_iter = current.children.iterator();
child_loop: while (child_iter.next()) |entry| {
const token = entry.key_ptr.*;
const child = entry.value_ptr.*;

switch (token) {
.fragment => |inner| if (std.mem.eql(u8, inner, chunk)) {
current = child;
continue :slash_loop;
},
.match => |kind| {
switch (kind) {
.signed => if (std.fmt.parseInt(i64, chunk, 10)) |value| {
captures[capture_idx] = Capture{ .signed = value };
} else |_| continue :child_loop,
.unsigned => if (std.fmt.parseInt(u64, chunk, 10)) |value| {
captures[capture_idx] = Capture{ .unsigned = value };
} else |_| continue :child_loop,
.float => if (std.fmt.parseFloat(f64, chunk)) |value| {
captures[capture_idx] = Capture{ .float = value };
} else |_| continue :child_loop,
.string => captures[capture_idx] = Capture{ .string = chunk },
.remaining => {
const rest = iter.buffer[(iter.index - chunk.len)..];
captures[capture_idx] = Capture{ .remaining = rest };

current = child;
capture_idx += 1;

break :slash_loop;
},
}

if (capture_idx > captures.len) return error.TooManyCaptures;
break;
current = child;
capture_idx += 1;
if (capture_idx > captures.len) return error.TooManyCaptures;
continue :slash_loop;
},
}
}

// If we failed to match, this is an invalid route.
if (!matched) {
return null;
}
return null;
}

if (query_pos) |pos| {
Expand Down Expand Up @@ -416,17 +408,17 @@ test "Routing with Paths" {

var captures: [8]Capture = [_]Capture{undefined} ** 8;

try testing.expectEqual(null, try s.get_route("/item/name", captures[0..], &q));
try testing.expectEqual(null, try s.get_bundle("/item/name", captures[0..], &q));

{
const captured = (try s.get_route("/item/name/HELLO", captures[0..], &q)).?;
const captured = (try s.get_bundle("/item/name/HELLO", captures[0..], &q)).?;

try testing.expectEqual(Route.init("/item/name/%s"), captured.bundle.route);
try testing.expectEqualStrings("HELLO", captured.captures[0].string);
}

{
const captured = (try s.get_route("/item/2112.22121/price_float", captures[0..], &q)).?;
const captured = (try s.get_bundle("/item/2112.22121/price_float", captures[0..], &q)).?;

try testing.expectEqual(Route.init("/item/%f/price_float"), captured.bundle.route);
try testing.expectEqual(2112.22121, captured.captures[0].float);
Expand All @@ -447,27 +439,27 @@ test "Routing with Remaining" {

var captures: [8]Capture = [_]Capture{undefined} ** 8;

try testing.expectEqual(null, try s.get_route("/item/name", captures[0..], &q));
try testing.expectEqual(null, try s.get_bundle("/item/name", captures[0..], &q));

{
const captured = (try s.get_route("/item/name/HELLO", captures[0..], &q)).?;
const captured = (try s.get_bundle("/item/name/HELLO", captures[0..], &q)).?;
try testing.expectEqual(Route.init("/item/name/%r"), captured.bundle.route);
try testing.expectEqualStrings("HELLO", captured.captures[0].remaining);
}
{
const captured = (try s.get_route("/item/name/THIS/IS/A/FILE/SYSTEM/PATH.html", captures[0..], &q)).?;
const captured = (try s.get_bundle("/item/name/THIS/IS/A/FILE/SYSTEM/PATH.html", captures[0..], &q)).?;
try testing.expectEqual(Route.init("/item/name/%r"), captured.bundle.route);
try testing.expectEqualStrings("THIS/IS/A/FILE/SYSTEM/PATH.html", captured.captures[0].remaining);
}

{
const captured = (try s.get_route("/item/2112.22121/price_float", captures[0..], &q)).?;
const captured = (try s.get_bundle("/item/2112.22121/price_float", captures[0..], &q)).?;
try testing.expectEqual(Route.init("/item/%f/price_float"), captured.bundle.route);
try testing.expectEqual(2112.22121, captured.captures[0].float);
}

{
const captured = (try s.get_route("/item/100/price/283.21", captures[0..], &q)).?;
const captured = (try s.get_bundle("/item/100/price/283.21", captures[0..], &q)).?;
try testing.expectEqual(Route.init("/item/%i/price/%f"), captured.bundle.route);
try testing.expectEqual(100, captured.captures[0].signed);
try testing.expectEqual(283.21, captured.captures[1].float);
Expand All @@ -488,10 +480,10 @@ test "Routing with Queries" {

var captures: [8]Capture = [_]Capture{undefined} ** 8;

try testing.expectEqual(null, try s.get_route("/item/name", captures[0..], &q));
try testing.expectEqual(null, try s.get_bundle("/item/name", captures[0..], &q));

{
const captured = (try s.get_route("/item/name/HELLO?name=muki&food=waffle", captures[0..], &q)).?;
const captured = (try s.get_bundle("/item/name/HELLO?name=muki&food=waffle", captures[0..], &q)).?;
try testing.expectEqual(Route.init("/item/name/%r"), captured.bundle.route);
try testing.expectEqualStrings("HELLO", captured.captures[0].remaining);
try testing.expectEqual(2, q.dirty());
Expand All @@ -501,15 +493,15 @@ test "Routing with Queries" {

{
// Purposefully bad format with no keys or values.
const captured = (try s.get_route("/item/2112.22121/price_float?", captures[0..], &q)).?;
const captured = (try s.get_bundle("/item/2112.22121/price_float?", captures[0..], &q)).?;
try testing.expectEqual(Route.init("/item/%f/price_float"), captured.bundle.route);
try testing.expectEqual(2112.22121, captured.captures[0].float);
try testing.expectEqual(0, q.dirty());
}

{
// Purposefully bad format with incomplete key/value pair.
const captured = (try s.get_route("/item/100/price/283.21?help", captures[0..], &q)).?;
const captured = (try s.get_bundle("/item/100/price/283.21?help", captures[0..], &q)).?;
try testing.expectEqual(Route.init("/item/%i/price/%f"), captured.bundle.route);
try testing.expectEqual(100, captured.captures[0].signed);
try testing.expectEqual(283.21, captured.captures[1].float);
Expand All @@ -518,7 +510,7 @@ test "Routing with Queries" {

{
// Purposefully have too many queries.
const captured = try s.get_route(
const captured = try s.get_bundle(
"/item/100/price/283.21?a=1&b=2&c=3&d=4&e=5&f=6&g=7&h=8&i=9&j=10&k=11",
captures[0..],
&q,
Expand Down
Loading

0 comments on commit 12d05b9

Please sign in to comment.