From 45545c2fda3dfafa31fb7341b31b786ad143e67d Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 13 May 2026 10:34:30 +0300 Subject: [PATCH] veb, fasthttp: efficicent memory management with -prealloc (memory is freed after the request is finished, no gc needed) --- vlib/builtin/prealloc.c.v | 316 ++++++++++++++++++++++-- vlib/fasthttp/README.md | 27 ++ vlib/fasthttp/fasthttp.v | 68 ++++- vlib/fasthttp/fasthttp_bsd.c.v | 75 ++++-- vlib/fasthttp/fasthttp_linux.v | 20 +- vlib/net/http/header.v | 31 ++- vlib/net/http/request.v | 44 +++- vlib/net/http/response.v | 31 ++- vlib/v/gen/c/cgen.v | 9 + vlib/v/gen/c/comptime.v | 107 +++++++- vlib/v/gen/c/consts_and_globals.v | 14 +- vlib/v/gen/c/coutput_test.v | 74 ++++++ vlib/v/gen/c/if.v | 3 + vlib/v/pref/default.v | 68 +++-- vlib/v/pref/default_tcc_compiler_test.v | 24 ++ vlib/v/pref/pref.v | 3 + vlib/v/pref/pref_test.v | 17 ++ vlib/veb/README.md | 26 ++ vlib/veb/context.v | 24 +- vlib/veb/static_handler.v | 27 ++ vlib/veb/veb.v | 261 ++++++++++++++++--- vlib/veb/veb_fasthttp.v | 81 ++++-- 22 files changed, 1203 insertions(+), 147 deletions(-) diff --git a/vlib/builtin/prealloc.c.v b/vlib/builtin/prealloc.c.v index 7b759ba18..22d78de81 100644 --- a/vlib/builtin/prealloc.c.v +++ b/vlib/builtin/prealloc.c.v @@ -6,27 +6,44 @@ module builtin // V code, that can fit inside the chunk, will use it instead, each bumping a // pointer, till the chunk is filled. Once a chunk is filled, a new chunk will // be allocated by calling libc's malloc, and the process continues. -// Each new chunk has a pointer to the old one, and at the end of the program, -// the entire linked list of chunks is freed. +// Each new chunk has a pointer to the old one. The base arena is thread-local; +// scoped arenas can be freed earlier with `prealloc_scope_end` or transferred +// and freed later with `prealloc_scope_free_after`. // The goal of all this is to amortize the cost of calling libc's malloc, // trading higher memory usage for a compiler (or any single threaded batch // mode program), for a ~8-10% speed increase. -// Note: `-prealloc` is NOT safe to be used for multithreaded programs! -// size of the preallocated chunk +// size of the process/thread preallocated chunk const prealloc_block_size = 16 * 1024 * 1024 +// size of the first chunk for a scoped prealloc arena. Request-scoped arenas +// should not force a 16MB libc allocation for every request. +const prealloc_scope_block_size = 256 * 1024 + +// `malloc` has to return memory suitably aligned for any V value. Keep the +// default at the common max alignment used by libc malloc on current targets. +const prealloc_default_align = sizeof(voidptr) * 2 + __global g_memory_block &VMemoryBlock @[heap] struct VMemoryBlock { mut: - current &u8 = 0 // 8 - stop &u8 = 0 // 8 - start &u8 = 0 // 8 - previous &VMemoryBlock = 0 // 8 - next &VMemoryBlock = 0 // 8 - id int // 4 - mallocs int // 4 + current &u8 = 0 // 8 + stop &u8 = 0 // 8 + start &u8 = 0 // 8 + previous &VMemoryBlock = 0 // 8 + next &VMemoryBlock = 0 // 8 + min_block_size isize + is_scope bool + id int // 4 + mallocs int // 4 +} + +@[heap] +struct VPreallocScope { +mut: + previous &VMemoryBlock = 0 + first &VMemoryBlock = 0 } fn vmemory_abort_on_nil(p voidptr, bytes isize) { @@ -36,8 +53,70 @@ fn vmemory_abort_on_nil(p voidptr, bytes isize) { } } +fn vmemory_effective_align(align isize) isize { + default_align := isize(prealloc_default_align) + if align > default_align { + return align + } + return default_align +} + +@[unsafe] +fn vmemory_align_up(ptr &u8, align isize) &u8 { + if align <= 1 { + return ptr + } + addr := u64(ptr) + alignment := u64(align) + offset := addr % alignment + if offset == 0 { + return ptr + } + return unsafe { &u8(i64(addr + alignment - offset)) } +} + +fn vmemory_block_used(mb &VMemoryBlock) i64 { + return unsafe { i64(mb.current) - i64(mb.start) } +} + +fn vmemory_block_size(mb &VMemoryBlock) i64 { + return unsafe { i64(mb.stop) - i64(mb.start) } +} + +@[unsafe] +fn prealloc_trace_scope(action &char, scope &VPreallocScope) { + $if trace_prealloc ? { + if scope == unsafe { nil } { + C.fprintf(C.stderr, c'[trace_prealloc] scope %s scope=%p\n', action, scope) + return + } + unsafe { + mut blocks := 0 + mut used := i64(0) + mut size := i64(0) + mut mallocs := 0 + mut mb := scope.first + for mb != 0 { + blocks++ + used += vmemory_block_used(mb) + size += vmemory_block_size(mb) + mallocs += mb.mallocs + mb = mb.next + } + C.fprintf(C.stderr, + c'[trace_prealloc] scope %s scope=%p previous=%p first=%p blocks=%d used=%lld size=%lld mallocs=%d\n', + action, scope, scope.previous, scope.first, blocks, used, size, mallocs) + } + } +} + @[unsafe] fn vmemory_block_new(prev &VMemoryBlock, at_least isize, align isize) &VMemoryBlock { + return unsafe { vmemory_block_new_sized(prev, at_least, align, isize(prealloc_block_size)) } +} + +@[unsafe] +fn vmemory_block_new_sized(prev &VMemoryBlock, at_least isize, align isize, min_block_size isize) &VMemoryBlock { vmem_block_size := sizeof(VMemoryBlock) mut v := unsafe { &VMemoryBlock(C.calloc(1, vmem_block_size)) } vmemory_abort_on_nil(v, vmem_block_size) @@ -48,9 +127,16 @@ fn vmemory_block_new(prev &VMemoryBlock, at_least isize, align isize) &VMemoryBl v.previous = prev if unsafe { prev != 0 } { prev.next = v + v.is_scope = prev.is_scope } - base_block_size := if at_least < isize(prealloc_block_size) { + effective_min_block_size := if min_block_size > 0 { + min_block_size + } else { isize(prealloc_block_size) + } + v.min_block_size = effective_min_block_size + base_block_size := if at_least < effective_min_block_size { + effective_min_block_size } else { at_least } @@ -85,6 +171,13 @@ fn vmemory_block_new(prev &VMemoryBlock, at_least isize, align isize) &VMemoryBl } v.stop = unsafe { &u8(i64(v.start) + block_size) } v.current = v.start + $if trace_prealloc ? { + if v.is_scope { + C.fprintf(C.stderr, + c'[trace_prealloc] block alloc block=%p previous=%p id=%d size=%lld at_least=%lld align=%lld start=%p stop=%p\n', + v, prev, v.id, block_size, at_least, align, v.start, v.stop) + } + } return v } @@ -102,19 +195,83 @@ fn vmemory_block_malloc(n isize, align isize) &u8 { g_memory_block.id, n, align) } unsafe { - remaining := i64(g_memory_block.stop) - i64(g_memory_block.current) + fixed_align := vmemory_effective_align(align) + mut current := vmemory_align_up(g_memory_block.current, fixed_align) + remaining := i64(g_memory_block.stop) - i64(current) if _unlikely_(remaining < n) { - g_memory_block = vmemory_block_new(g_memory_block, n, align) + min_block_size := if g_memory_block.min_block_size > 0 { + g_memory_block.min_block_size + } else { + isize(prealloc_block_size) + } + g_memory_block = vmemory_block_new_sized(g_memory_block, n, fixed_align, min_block_size) + current = vmemory_align_up(g_memory_block.current, fixed_align) } - res := &u8(g_memory_block.current) + res := &u8(current) + g_memory_block.current = current g_memory_block.current += n $if prealloc_stats ? { g_memory_block.mallocs++ + } $else { + $if trace_prealloc ? { + g_memory_block.mallocs++ + } + } + $if trace_prealloc ? { + if g_memory_block.is_scope { + used := vmemory_block_used(g_memory_block) + size := vmemory_block_size(g_memory_block) + C.fprintf(C.stderr, + c'[trace_prealloc] alloc block=%p ptr=%p size=%lld align=%lld used=%lld/%lld mallocs=%d\n', + g_memory_block, res, n, fixed_align, used, size, g_memory_block.mallocs) + } } return res } } +@[unsafe] +fn vmemory_block_free(mb &VMemoryBlock) { + $if trace_prealloc ? { + if mb.is_scope { + C.fprintf(C.stderr, + c'[trace_prealloc] block free block=%p id=%d start=%p used=%lld size=%lld mallocs=%d\n', + mb, mb.id, mb.start, vmemory_block_used(mb), vmemory_block_size(mb), mb.mallocs) + } + } + $if windows { + // Warning! On windows, we always use _aligned_free to free memory. + C._aligned_free(mb.start) + } $else { + C.free(mb.start) + } + C.free(mb) +} + +@[unsafe] +fn vmemory_block_free_after(marker &VMemoryBlock) { + if marker == unsafe { nil } { + return + } + unsafe { + mut mb := marker.next + marker.next = nil + vmemory_block_free_chain(mb) + } +} + +@[unsafe] +fn vmemory_block_free_chain(first &VMemoryBlock) { + unsafe { + mut mb := first + for mb != 0 { + next := mb.next + vmemory_block_free(mb) + mb = next + } + } +} + ///////////////////////////////////////////////// @[unsafe] @@ -210,6 +367,135 @@ fn prealloc_vcleanup() { } } +// prealloc_scope_begin starts a nested arena on the current thread. All V +// allocations after this call use the nested arena until `prealloc_scope_end`. +// The returned scope can be passed across threads and later freed with +// `prealloc_scope_free_after`, which is useful when a response buffer outlives +// the request handler thread. +@[unsafe] +pub fn prealloc_scope_begin() voidptr { + unsafe { + scope := &VPreallocScope(C.calloc(1, sizeof(VPreallocScope))) + vmemory_abort_on_nil(scope, sizeof(VPreallocScope)) + scope.previous = g_memory_block + scope.first = vmemory_block_new_sized(scope.previous, isize(prealloc_scope_block_size), 0, + isize(prealloc_scope_block_size)) + scope.first.is_scope = true + g_memory_block = scope.first + prealloc_trace_scope(c'begin', scope) + return scope + } +} + +@[unsafe] +pub fn prealloc_scope_checkpoint(label &char) { + $if trace_prealloc ? { + unsafe { + if g_memory_block == 0 || !g_memory_block.is_scope { + return + } + mut blocks := 0 + mut used := i64(0) + mut size := i64(0) + mut mallocs := 0 + mut first := g_memory_block + for first.previous != 0 && first.previous.is_scope { + first = first.previous + } + mut mb := first + for mb != 0 { + blocks++ + used += vmemory_block_used(mb) + size += vmemory_block_size(mb) + mallocs += mb.mallocs + mb = mb.next + } + C.fprintf(C.stderr, + c'[trace_prealloc] checkpoint label=%s first=%p current=%p blocks=%d used=%lld size=%lld mallocs=%d\n', + label, first, g_memory_block, blocks, used, size, mallocs) + } + } +} + +@[unsafe] +fn prealloc_scope_free_blocks(scope &VPreallocScope) { + if scope == unsafe { nil } { + return + } + unsafe { + if scope.previous != 0 { + scope.previous.next = nil + } + vmemory_block_free_chain(scope.first) + } +} + +// prealloc_scope_end frees a nested arena and restores the current thread arena +// to the state before `prealloc_scope_begin`. +@[unsafe] +pub fn prealloc_scope_end(scope_ptr voidptr) { + if scope_ptr == unsafe { nil } { + return + } + unsafe { + scope := &VPreallocScope(scope_ptr) + prealloc_trace_scope(c'end', scope) + prealloc_scope_free_blocks(scope) + g_memory_block = scope.previous + C.free(scope) + } +} + +// prealloc_scope_leave restores the current thread arena without freeing the +// scoped blocks. Call this before another thread takes ownership of the scope. +@[unsafe] +pub fn prealloc_scope_leave(scope_ptr voidptr) { + if scope_ptr == unsafe { nil } { + return + } + unsafe { + scope := &VPreallocScope(scope_ptr) + prealloc_trace_scope(c'leave', scope) + previous := scope.previous + if previous != 0 { + previous.next = nil + } + g_memory_block = previous + scope.previous = nil + } +} + +// prealloc_scope_abandon restores the current thread arena and intentionally +// leaks the scoped blocks. It is only for APIs that transfer request state to +// user code without providing a close hook yet. +@[unsafe] +pub fn prealloc_scope_abandon(scope_ptr voidptr) { + if scope_ptr == unsafe { nil } { + return + } + unsafe { + prealloc_trace_scope(c'abandon', &VPreallocScope(scope_ptr)) + prealloc_scope_leave(scope_ptr) + C.free(scope_ptr) + } +} + +// prealloc_scope_free_after frees a nested arena from a marker without touching +// the caller's thread-local arena pointer. Use this when another thread finishes +// sending data that was allocated in the request thread. +@[unsafe] +pub fn prealloc_scope_free_after(scope_ptr voidptr) { + if scope_ptr == unsafe { nil } { + return + } + unsafe { + scope := &VPreallocScope(scope_ptr) + prealloc_trace_scope(c'free-after', scope) + prealloc_scope_free_blocks(scope) + C.free(scope) + } +} + @[unsafe] fn prealloc_malloc(n isize) &u8 { return unsafe { vmemory_block_malloc(n, 0) } diff --git a/vlib/fasthttp/README.md b/vlib/fasthttp/README.md index 296503036..d7e52d0a8 100644 --- a/vlib/fasthttp/README.md +++ b/vlib/fasthttp/README.md @@ -168,6 +168,33 @@ detailed server implementation with multiple routes and controllers. - Use goroutines within handlers if you need to perform long-running operations without blocking the I/O loop +## Request-scoped allocation with `-prealloc` + +When an application is compiled with `-prealloc`, `fasthttp` starts a scoped +prealloc arena for each request before decoding the HTTP request and before +calling the request handler. All V allocations made by the request parser, the +handler, and code called by the handler use that request arena while the handler +is running. + +The arena is freed as a unit after the response no longer needs request-owned +data. On Linux the normal response path sends the response synchronously, then +ends the request arena. On macOS and BSD the response buffer can be kept by the +connection until `kqueue` finishes writing it; in that case `fasthttp` detaches +the scope from the request thread and frees it after the write completes. + +This means request-local V allocations are cheap bump-pointer allocations, and +freeing them does not require walking individual objects. Startup state, server +state, and allocations made directly by C libraries are not part of a request +arena. Manual takeover responses transfer ownership to user code and currently +abandon the request arena, so long-lived takeover handlers should manage their +own allocation lifetime explicitly. + +To inspect request arena usage while developing, build with: + +```sh +v -prealloc -d trace_prealloc run . +``` + ## Notes - HTTP headers are currently not parsed; the entire request is available in the buffer diff --git a/vlib/fasthttp/fasthttp.v b/vlib/fasthttp/fasthttp.v index adcd91e01..cfeaf2e94 100644 --- a/vlib/fasthttp/fasthttp.v +++ b/vlib/fasthttp/fasthttp.v @@ -70,11 +70,77 @@ pub enum ResponseTakeoverMode { } pub struct HttpResponse { -pub: +pub mut: content []u8 file_path string takeover_mode ResponseTakeoverMode should_close bool // if true, close the connection after sending (Connection: close) + // content_owned lets the backend free or move content after it has been sent. + content_owned bool + // request_arena is a prealloc scope handle that must be freed after sending. + request_arena voidptr +} + +fn (mut resp HttpResponse) free_owned_content() { + if resp.content_owned && resp.content.cap > 0 { + unsafe { resp.content.free() } + resp.content = []u8{} + } +} + +fn (mut resp HttpResponse) take_or_clone_content() []u8 { + if resp.content_owned { + content := resp.content + resp.content = []u8{} + return content + } + return resp.content.clone() +} + +fn end_request_arena_current_thread(request_arena voidptr) { + $if prealloc { + if request_arena != unsafe { nil } { + unsafe { prealloc_scope_end(request_arena) } + } + } +} + +fn leave_request_arena_current_thread(request_arena voidptr) { + $if prealloc { + if request_arena != unsafe { nil } { + unsafe { prealloc_scope_leave(request_arena) } + } + } +} + +fn abandon_request_arena_current_thread(request_arena voidptr) { + $if prealloc { + if request_arena != unsafe { nil } { + unsafe { prealloc_scope_abandon(request_arena) } + } + } +} + +fn (mut resp HttpResponse) attach_request_arena_if_empty(request_arena voidptr) { + if resp.request_arena == unsafe { nil } { + resp.request_arena = request_arena + } +} + +fn (mut resp HttpResponse) end_request_arena_current_thread() { + end_request_arena_current_thread(resp.request_arena) + resp.request_arena = unsafe { nil } +} + +fn (mut resp HttpResponse) abandon_request_arena_current_thread() { + abandon_request_arena_current_thread(resp.request_arena) + resp.request_arena = unsafe { nil } +} + +fn (mut resp HttpResponse) take_request_arena() voidptr { + request_arena := resp.request_arena + resp.request_arena = unsafe { nil } + return request_arena } // ServerConfig bundles the parameters needed to start a fasthttp server. diff --git a/vlib/fasthttp/fasthttp_bsd.c.v b/vlib/fasthttp/fasthttp_bsd.c.v index 96ae1765a..2c1fd9d8c 100644 --- a/vlib/fasthttp/fasthttp_bsd.c.v +++ b/vlib/fasthttp/fasthttp_bsd.c.v @@ -122,10 +122,27 @@ mut: read_start i64 // monotonic timestamp (in microseconds) when first data was received // Sendfile state - file_fd int = -1 - file_len i64 - file_pos i64 - should_close bool + file_fd int = -1 + file_len i64 + file_pos i64 + should_close bool + request_arena voidptr +} + +fn (mut c Conn) free_write_buf() { + if c.write_buf.cap > 0 { + unsafe { c.write_buf.free() } + c.write_buf = []u8{} + } +} + +fn (mut c Conn) free_request_arena() { + $if prealloc { + if c.request_arena != unsafe { nil } { + unsafe { prealloc_scope_free_after(c.request_arena) } + c.request_arena = unsafe { nil } + } + } } pub struct Server { @@ -181,7 +198,7 @@ fn delete_event(kq int, ident u64, filter i16, udata voidptr) { C.kevent(kq, &ev, 1, unsafe { nil }, 0, unsafe { nil }) } -fn close_conn(server Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) { +fn close_conn(server &Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) { mut c := unsafe { &Conn(c_ptr) } clients.delete(c.fd) delete_event(kq, u64(c.fd), i16(C.EVFILT_READ), c) @@ -191,9 +208,8 @@ fn close_conn(server Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) server.end_request() c.request_active = false } - if c.write_buf.cap > 0 { - unsafe { c.write_buf.free() } - } + c.free_write_buf() + c.free_request_arena() if c.read_extra.cap > 0 { unsafe { c.read_extra.free() } } @@ -261,14 +277,14 @@ fn send_request_timeout(fd int) { C.send(fd, status_408_response.data, status_408_response.len, send_flags) } -fn handle_write(server Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) { +fn handle_write(server &Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) { if send_pending(c_ptr) { return } complete_response(server, kq, c_ptr, mut clients, true) } -fn complete_response(server Server, kq int, c_ptr voidptr, mut clients map[int]voidptr, remove_write_event bool) { +fn complete_response(server &Server, kq int, c_ptr voidptr, mut clients map[int]voidptr, remove_write_event bool) { mut c := unsafe { &Conn(c_ptr) } if remove_write_event { delete_event(kq, u64(c.fd), i16(C.EVFILT_WRITE), c) @@ -281,7 +297,8 @@ fn complete_response(server Server, kq int, c_ptr voidptr, mut clients map[int]v server.end_request() c.request_active = false } - c.write_buf.clear() + c.free_write_buf() + c.free_request_arena() c.write_pos = 0 c.read_len = 0 if c.read_extra.cap > 0 { @@ -293,9 +310,13 @@ fn complete_response(server Server, kq int, c_ptr voidptr, mut clients map[int]v } // process_request handles a complete HTTP request: decodes, calls the handler, -// sends the response (or handles takeover/sendfile). Runs in a spawned thread. -fn process_request(server Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) { +// sends the response (or handles takeover/sendfile). +fn process_request(server &Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) { mut c := unsafe { &Conn(c_ptr) } + mut request_arena := voidptr(unsafe { nil }) + $if prealloc { + request_arena = unsafe { prealloc_scope_begin() } + } mut req_buf := c.get_full_request_data() if c.read_extra.cap > 0 { @@ -305,19 +326,28 @@ fn process_request(server Server, kq int, c_ptr voidptr, mut clients map[int]voi mut decoded := decode_http_request(req_buf) or { send_bad_request(c.fd) + end_request_arena_current_thread(request_arena) close_conn(server, kq, c_ptr, mut clients) return } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'fasthttp decoded request') } + } server.begin_request() c.request_active = true decoded.client_conn_fd = c.fd decoded.user_data = server.user_data - resp := server.request_handler(decoded) or { + mut resp := server.request_handler(decoded) or { send_bad_request(c.fd) + end_request_arena_current_thread(request_arena) close_conn(server, kq, c_ptr, mut clients) return } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'fasthttp handler returned') } + } + resp.attach_request_arena_if_empty(request_arena) match resp.takeover_mode { .manual { @@ -330,6 +360,8 @@ fn process_request(server Server, kq int, c_ptr voidptr, mut clients map[int]voi server.end_request() c.request_active = false } + resp.free_owned_content() + resp.abandon_request_arena_current_thread() unsafe { free(c_ptr) } return } @@ -342,6 +374,8 @@ fn process_request(server Server, kq int, c_ptr voidptr, mut clients map[int]voi server.end_request() c.request_active = false } + resp.free_owned_content() + resp.end_request_arena_current_thread() if server.is_shutting_down() || resp.should_close { close_conn(server, kq, c_ptr, mut clients) } @@ -351,7 +385,14 @@ fn process_request(server Server, kq int, c_ptr voidptr, mut clients map[int]voi } c.should_close = resp.should_close - c.write_buf = resp.content.clone() + c.free_write_buf() + c.free_request_arena() + c.request_arena = resp.take_request_arena() + c.write_buf = resp.take_or_clone_content() + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'fasthttp response retained') } + } + leave_request_arena_current_thread(c.request_arena) if resp.file_path != '' { fd := C.open(resp.file_path.str, C.O_RDONLY, 0) if fd != -1 { @@ -398,7 +439,7 @@ fn (c &Conn) get_full_request_data() []u8 { return req_buf } -fn handle_read(server Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) { +fn handle_read(server &Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) { mut c := unsafe { &Conn(c_ptr) } // Drain the socket for this kqueue notification. EV_CLEAR only rearms once @@ -527,7 +568,7 @@ fn accept_clients(kq int, listen_fd int, mut clients map[int]voidptr) { } } -fn close_all_conns(server Server, kq int, mut clients map[int]voidptr) { +fn close_all_conns(server &Server, kq int, mut clients map[int]voidptr) { for client_fd in clients.keys() { c_ptr := clients[client_fd] or { continue } close_conn(server, kq, c_ptr, mut clients) diff --git a/vlib/fasthttp/fasthttp_linux.v b/vlib/fasthttp/fasthttp_linux.v index ca75a4be5..9655204e4 100644 --- a/vlib/fasthttp/fasthttp_linux.v +++ b/vlib/fasthttp/fasthttp_linux.v @@ -258,6 +258,10 @@ fn send_terminal_response_and_drain(client_fd int, response []u8, mut client_buf } fn process_request(server &Server, epoll_fd int, client_fd int, request_buffer []u8, mut client_fds map[int]bool, mut client_buffers map[int][]u8, mut client_read_starts map[int]i64, mut closing_client_fds map[int]bool) { + mut request_arena := voidptr(unsafe { nil }) + $if prealloc { + request_arena = unsafe { prealloc_scope_begin() } + } client_read_starts.delete(client_fd) server.begin_request() defer { @@ -267,20 +271,33 @@ fn process_request(server &Server, epoll_fd int, client_fd int, request_buffer [ eprintln('Error decoding request ${err}') C.send(client_fd, tiny_bad_request_response.data, tiny_bad_request_response.len, C.MSG_NOSIGNAL) + end_request_arena_current_thread(request_arena) handle_client_closure(epoll_fd, client_fd, mut client_fds, mut client_buffers, mut client_read_starts, mut closing_client_fds) return } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'fasthttp decoded request') } + } decoded_http_request.client_conn_fd = client_fd decoded_http_request.user_data = server.user_data - response := server.request_handler(decoded_http_request) or { + mut response := server.request_handler(decoded_http_request) or { eprintln('Error handling request ${err}') C.send(client_fd, tiny_bad_request_response.data, tiny_bad_request_response.len, C.MSG_NOSIGNAL) + end_request_arena_current_thread(request_arena) handle_client_closure(epoll_fd, client_fd, mut client_fds, mut client_buffers, mut client_read_starts, mut closing_client_fds) return } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'fasthttp handler returned') } + } + response.attach_request_arena_if_empty(request_arena) + defer { + response.free_owned_content() + response.end_request_arena_current_thread() + } match response.takeover_mode { .manual { @@ -291,6 +308,7 @@ fn process_request(server &Server, epoll_fd int, client_fd int, request_buffer [ client_read_starts.delete(client_fd) closing_client_fds.delete(client_fd) remove_fd_from_epoll(epoll_fd, client_fd) + response.abandon_request_arena_current_thread() return } .reusable { diff --git a/vlib/net/http/header.v b/vlib/net/http/header.v index d2fc047aa..237acd11e 100644 --- a/vlib/net/http/header.v +++ b/vlib/net/http/header.v @@ -358,6 +358,26 @@ pub: value string } +@[inline] +fn header_lower_ascii_byte(b u8) u8 { + if b >= `A` && b <= `Z` { + return b + 32 + } + return b +} + +fn header_key_eq(a string, b string) bool { + if a.len != b.len { + return false + } + for i in 0 .. a.len { + if header_lower_ascii_byte(a[i]) != header_lower_ascii_byte(b[i]) { + return false + } + } + return true +} + // Create a new Header object pub fn new_header(kvs ...HeaderConfig) Header { mut h := Header{ @@ -497,7 +517,7 @@ pub fn (h Header) contains(key CommonHeader) bool { } key_str := key.str() for i := 0; i < h.cur_pos; i++ { - if h.data[i].key == key_str { + if header_key_eq(h.data[i].key, key_str) { return true } } @@ -522,10 +542,9 @@ pub fn (h Header) contains_custom(key string, flags HeaderQueryConfig) bool { } return false } else { - lower_key := key.to_lower() for i := 0; i < h.cur_pos; i++ { kv := h.data[i] - if kv.key.to_lower() == lower_key { + if header_key_eq(kv.key, key) { return true } } @@ -552,11 +571,10 @@ pub fn (h Header) get_custom(key string, flags HeaderQueryConfig) !string { } } } else { - lower_key := key.to_lower() // for kv in h.data { for i := 0; i < h.cur_pos; i++ { kv := h.data[i] - if kv.key.to_lower() == lower_key { + if header_key_eq(kv.key, key) { return kv.value } } @@ -595,10 +613,9 @@ pub fn (h Header) custom_values(key string, flags HeaderQueryConfig) []string { } return res } else { - lower_key := key.to_lower() for i := 0; i < h.cur_pos; i++ { kv := h.data[i] - if kv.key.to_lower() == lower_key && kv.value != '' { // empty value means a deleted header + if header_key_eq(kv.key, key) && kv.value != '' { // empty value means a deleted header res << kv.value } } diff --git a/vlib/net/http/request.v b/vlib/net/http/request.v index d713942fa..85a73cf88 100644 --- a/vlib/net/http/request.v +++ b/vlib/net/http/request.v @@ -822,27 +822,29 @@ pub fn parse_request_head_str(s string) !Request { return error('malformed request: no request line found') } line0 := s[..pos0].trim_space() - method, target, version := parse_request_line(line0)! + method, target, version := parse_request_line_fast(line0)! // headers mut header := new_header() - // split by newline and skip the first line (request line) - lines := s[pos0 + 1..].split('\n') - - for line_raw in lines { - line := line_raw.trim_right('\r') - + mut line_start := pos0 + 1 + for line_start < s.len { + mut line_end := s.index_after_('\n', line_start) + if line_end == -1 { + line_end = s.len + } + mut line := s[line_start..line_end] + if line.len > 0 && line[line.len - 1] == `\r` { + line = line[..line.len - 1] + } // IMPORTANT: HTTP headers end at the first empty line. // If we hit this, we are now at the body, so we stop parsing headers. if line == '' { break } - - if !line.contains(':') { + mut pos := parse_header_fast(line) or { + line_start = line_end + 1 continue } - - mut pos := parse_header_fast(line)! key := line[..pos] // Skip space or tab after the colon @@ -855,6 +857,7 @@ pub fn parse_request_head_str(s string) !Request { value := line[val_start..] header.add_custom(key, value)! } + line_start = line_end + 1 } mut request_cookies := map[string]string{} @@ -864,7 +867,7 @@ pub fn parse_request_head_str(s string) !Request { return Request{ method: method - url: target.str() + url: target header: header host: (header.get(.host) or { '' }).clone() version: version @@ -872,6 +875,23 @@ pub fn parse_request_head_str(s string) !Request { } } +fn parse_request_line_fast(line string) !(Method, string, Version) { + space1 := line.index_u8(` `) + if space1 <= 0 { + return error('bad request header') + } + space2_rel := line.index_after_(' ', space1 + 1) + if space2_rel == -1 || space2_rel == space1 + 1 || space2_rel >= line.len - 1 { + return error('bad request header') + } + method := method_from_str(line[..space1]) + version := version_from_str(line[space2_rel + 1..]) + if version == .unknown { + return error('unsupported version') + } + return method, line[space1 + 1..space2_rel], version +} + const headers_body_boundary = '\r\n\r\n' // parse_request_str parses a raw HTTP request string into a Request object. diff --git a/vlib/net/http/response.v b/vlib/net/http/response.v index f003d16e9..a583af69b 100644 --- a/vlib/net/http/response.v +++ b/vlib/net/http/response.v @@ -7,6 +7,7 @@ import compress.gzip import compress.zlib import net.http.chunked import strconv +import strings // Response represents the result of the request pub struct Response { @@ -24,15 +25,37 @@ fn (mut resp Response) free() { // Formats resp to bytes suitable for HTTP response transmission pub fn (resp Response) bytes() []u8 { - // TODO: build []u8 directly; this uses two allocations - return resp.bytestr().bytes() + mut sb := strings.new_builder(resp.response_buffer_cap()) + resp.write_into_builder(mut sb) + return unsafe { sb.reuse_as_plain_u8_array() } } // Formats resp to a string suitable for HTTP response transmission pub fn (resp Response) bytestr() string { - return 'HTTP/${resp.http_version} ${resp.status_code} ${resp.status_msg}\r\n' + '${resp.header.render( + mut sb := strings.new_builder(resp.response_buffer_cap()) + resp.write_into_builder(mut sb) + res := sb.str() + unsafe { sb.free() } + return res +} + +fn (resp Response) response_buffer_cap() int { + return resp.body.len + 64 + resp.header.cur_pos * 48 +} + +fn (resp Response) write_into_builder(mut sb strings.Builder) { + sb.write_string('HTTP/') + sb.write_string(resp.http_version) + sb.write_u8(` `) + sb.write_decimal(resp.status_code) + sb.write_u8(` `) + sb.write_string(resp.status_msg) + sb.write_string('\r\n') + resp.header.render_into_sb(mut sb, version: resp.version() - )}\r\n' + resp.body + ) + sb.write_string('\r\n') + sb.write_string(resp.body) } // Parse a raw HTTP response into a Response object diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 44a52292a..7b26ff997 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -7725,6 +7725,11 @@ fn (mut g Gen) scope_gc_pin_pregen(node_pos int) []ScopeGcPin { || g.pref.gc_mode !in [.boehm_full, .boehm_incr, .boehm_full_opt, .boehm_incr_opt] { return []ScopeGcPin{} } + if g.inside_veb_tmpl { + // Veb template statements are inlined into the route body; their AST scopes + // do not always match the generated C block scopes for template locals. + return []ScopeGcPin{} + } if g.inside_defer_generation { return []ScopeGcPin{} } @@ -7832,6 +7837,10 @@ fn (mut g Gen) write_scope_gc_pins(pos token.Pos) { if g.pref.gc_mode !in [.boehm_full, .boehm_incr, .boehm_full_opt, .boehm_incr_opt] { return } + if g.inside_veb_tmpl { + // See scope_gc_pin_pregen for why template code skips these pins. + return + } if g.fn_decl == unsafe { nil } || g.fn_decl.scope == unsafe { nil } { return } diff --git a/vlib/v/gen/c/comptime.v b/vlib/v/gen/c/comptime.v index e2d6bdfb5..85c1a409c 100644 --- a/vlib/v/gen/c/comptime.v +++ b/vlib/v/gen/c/comptime.v @@ -405,6 +405,8 @@ fn (mut g Gen) comptime_call(mut node ast.ComptimeCall) { for stmt in node.veb_tmpl.stmts { if stmt is ast.FnDecl { if stmt.name.starts_with('main.veb_tmpl') { + prev_inside_veb_tmpl := g.inside_veb_tmpl + prev_veb_filter_fn_name := g.veb_filter_fn_name if is_html { g.inside_veb_tmpl = true g.veb_filter_fn_name = 'veb__filter' @@ -412,8 +414,10 @@ fn (mut g Gen) comptime_call(mut node ast.ComptimeCall) { // insert stmts from veb_tmpl fn g.stmts(stmt.stmts.filter(it !is ast.Return)) // - g.inside_veb_tmpl = false - g.veb_filter_fn_name = '' + if is_html { + g.inside_veb_tmpl = prev_inside_veb_tmpl + g.veb_filter_fn_name = prev_veb_filter_fn_name + } break } } @@ -1128,6 +1132,101 @@ fn (mut g Gen) pop_comptime_info() { g.clear_type_resolution_caches() } +fn (mut g Gen) eval_comptime_for_if_cond(cond ast.Expr) ?bool { + match cond { + ast.ParExpr { + return g.eval_comptime_for_if_cond(cond.expr) + } + ast.PrefixExpr { + if cond.op == .not { + return !(g.eval_comptime_for_if_cond(cond.right)?) + } + } + ast.InfixExpr { + match cond.op { + .and, .logical_or { + left := g.eval_comptime_for_if_cond(cond.left)? + right := g.eval_comptime_for_if_cond(cond.right)? + return if cond.op == .and { left && right } else { left || right } + } + .key_is, .not_is { + if cond.left is ast.SelectorExpr && cond.right is ast.TypeNode { + if cond.left.field_name == 'return_type' && cond.left.expr is ast.Ident + && cond.left.expr.name == g.comptime.comptime_for_method_var { + left_type := + g.table.unaliased_type(g.unwrap_generic(g.comptime.comptime_for_method_ret_type)) + right_type := g.table.unaliased_type(g.unwrap_generic(cond.right.typ)) + is_true := left_type == right_type + return if cond.op == .key_is { is_true } else { !is_true } + } + } + } + .eq, .ne { + if cond.left is ast.SelectorExpr && cond.right is ast.StringLiteral { + if cond.left.field_name == 'name' && cond.left.expr is ast.Ident + && cond.left.expr.name == g.comptime.comptime_for_method_var { + is_true := g.comptime.comptime_for_method.name == cond.right.val + return if cond.op == .eq { is_true } else { !is_true } + } + } + } + else {} + } + } + else {} + } + + return none +} + +fn (mut g Gen) comptime_if_has_live_branch(node ast.IfExpr) bool { + if g.pref.output_cross_c || node.has_else { + return true + } + comptime_branch_context_str := g.gen_branch_context_string() + for branch in node.branches { + if evaluated := g.eval_comptime_for_if_cond(branch.cond) { + if evaluated { + return true + } + continue + } + mut idx_str := comptime_branch_context_str + '|id=${branch.id}|' + if g.comptime.inside_comptime_for && g.comptime.comptime_for_field_var != '' { + idx_str += '|field_type=${g.comptime.comptime_for_field_type}|' + } + if is_true := g.table.comptime_is_true[idx_str] { + if is_true.val { + return true + } + } else { + return true + } + } + return false +} + +fn (mut g Gen) comptime_for_iteration_has_live_stmts(stmts []ast.Stmt) bool { + for stmt in stmts { + match stmt { + ast.EmptyStmt, ast.SemicolonStmt {} + ast.ExprStmt { + if stmt.expr is ast.IfExpr && stmt.expr.is_comptime { + if g.comptime_if_has_live_branch(stmt.expr) { + return true + } + } else { + return true + } + } + else { + return true + } + } + } + return false +} + fn (mut g Gen) comptime_for(node ast.ComptimeFor) { resolved_typ := if node.expr !is ast.EmptyExpr { mut expr_typ := g.unwrap_generic(g.recheck_concrete_type(g.resolved_expr_type(node.expr, @@ -1183,6 +1282,10 @@ fn (mut g Gen) comptime_for(node ast.ComptimeFor) { g.comptime.comptime_for_method = unsafe { &method } g.comptime.comptime_for_method_var = node.val_var g.comptime.comptime_for_method_ret_type = method.return_type + if !g.comptime_for_iteration_has_live_stmts(node.stmts) { + g.pop_comptime_info() + continue + } g.writeln('/* method ${i} : ${method.name} */ {') g.writeln('\t${node.val_var}.name = _S("${method.name}");') mlocation := util.cescaped_path(util.path_styled_for_error_messages(method.file)) diff --git a/vlib/v/gen/c/consts_and_globals.v b/vlib/v/gen/c/consts_and_globals.v index de5b6b2eb..f68e0ce63 100644 --- a/vlib/v/gen/c/consts_and_globals.v +++ b/vlib/v/gen/c/consts_and_globals.v @@ -592,7 +592,12 @@ fn (mut g Gen) global_decl(node ast.GlobalDecl) { continue } if field.is_extern { - def_builder.writeln('${extern}${field_visibility_kw}${qualifiers}${styp} ${attributes}${final_c_name}; // global 2') + thread_local := if field.name == 'g_memory_block' && g.pref.prealloc { + '_Thread_local ' + } else { + '' + } + def_builder.writeln('${extern}${thread_local}${field_visibility_kw}${qualifiers}${styp} ${attributes}${final_c_name}; // global 2') g.global_const_defs[name] = GlobalConstDef{ mod: node.mod def: def_builder.str() @@ -602,7 +607,12 @@ fn (mut g Gen) global_decl(node ast.GlobalDecl) { } mut needs_ending_semicolon := false if field.language != .c || field.has_expr { - def_builder.write_string('${extern}${field_visibility_kw}${qualifiers}${styp} ${attributes}${final_c_name}') + thread_local := if field.name == 'g_memory_block' && g.pref.prealloc { + '_Thread_local ' + } else { + '' + } + def_builder.write_string('${extern}${thread_local}${field_visibility_kw}${qualifiers}${styp} ${attributes}${final_c_name}') needs_ending_semicolon = true } if field.has_expr || cinit { diff --git a/vlib/v/gen/c/coutput_test.v b/vlib/v/gen/c/coutput_test.v index 4d0f4b540..315208181 100644 --- a/vlib/v/gen/c/coutput_test.v +++ b/vlib/v/gen/c/coutput_test.v @@ -473,6 +473,80 @@ fn test_veb_implicit_ctx_alias_on_context_receiver_tmpl_not_found() { assert not_found_body.contains('return veb__Context_html(&c->Context, _tmpl_res_') } +fn test_veb_template_scope_gc_pin_does_not_escape_loop_var() { + os.chdir(vroot) or {} + test_dir := os.join_path(os.vtmp_dir(), 'coutput_veb_template_scope_gc_pin') + os.rmdir_all(test_dir) or {} + os.mkdir_all(os.join_path(test_dir, 'templates'))! + template_lines := [ + '
', + ' @if is_top_directory', + ' @for i, p in ctx.parts', + ' @p', + ' @end', + ' @end', + ' @if is_repo_watcher', + ' @watcher_count', + ' @end', + '
', + ] + os.write_file(os.join_path(test_dir, 'templates', 'tree.html'), + template_lines.join('\n') + '\n')! + test_source := os.join_path(test_dir, 'main.v') + source_lines := [ + 'module main', + '', + 'import veb', + '', + 'pub struct Context {', + '\tveb.Context', + 'pub mut:', + '\tparts []string', + '}', + '', + 'pub struct App {}', + '', + 'pub struct Repo {', + '\tuser_name string', + '}', + '', + 'pub fn (ctx &Context) make_path(branch_name string, i int) string {', + '\treturn branch_name + i.str()', + '}', + '', + 'pub fn (mut app App) index(mut ctx Context) veb.Result {', + "\trepo := Repo{ user_name: 'gitly' }", + "\tbranch_name := 'master'", + '\tis_top_directory := true', + '\tis_repo_watcher := false', + '\twatcher_count := 0', + "\treturn \$veb.html('templates/tree.html')", + '}', + '', + 'fn main() {', + '\tmut app := App{}', + "\tmut ctx := Context{ parts: ['src'] }", + '\t_ = app.index(mut ctx)', + '}', + ] + os.write_file(test_source, source_lines.join('\n') + '\n')! + defer { + os.rmdir_all(test_dir) or {} + } + test_exe := os.join_path(test_dir, 'app') + compile_cmd := '${os.quoted_path(vexe)} -gc boehm_full_opt -o ${os.quoted_path(test_exe)} ${os.quoted_path(test_source)}' + ensure_compilation_succeeded(os.execute(compile_cmd), compile_cmd) + c_cmd := '${os.quoted_path(vexe)} -gc boehm_full_opt -o - ${os.quoted_path(test_source)}' + compilation := os.execute(c_cmd) + ensure_compilation_succeeded(compilation, c_cmd) + index_start := 'veb__Result main__App_index(main__App* app, main__Context* ctx) {' + assert compilation.output.contains(index_start) + index_body := compilation.output.all_after(index_start).all_before('VV_LOC void main__main') + assert index_body.contains('string p =') + assert !index_body.contains('GC_reachable_here(&p);') + assert index_body.contains('veb__Context_html(&ctx->Context, _tmpl_res_') +} + fn does_line_match_one_of_generated_lines(line string, generated_c_lines []string) bool { for cline in generated_c_lines { if line == cline { diff --git a/vlib/v/gen/c/if.v b/vlib/v/gen/c/if.v index 8a35c5de0..26a1db31d 100644 --- a/vlib/v/gen/c/if.v +++ b/vlib/v/gen/c/if.v @@ -30,6 +30,9 @@ fn (g &Gen) if_guard_else_uses_err(node ast.IfExpr, branch_idx int) bool { } fn (mut g Gen) write_if_guard_gc_pin(scope &ast.Scope, name string, cvar_name string) { + if g.inside_veb_tmpl { + return + } if g.if_guard_var_needs_gc_pin(scope, name) { g.writeln('\tGC_reachable_here(&${cvar_name});') } diff --git a/vlib/v/pref/default.v b/vlib/v/pref/default.v index ea01a65e1..83d2b31da 100644 --- a/vlib/v/pref/default.v +++ b/vlib/v/pref/default.v @@ -319,35 +319,16 @@ pub fn (mut p Preferences) fill_with_defaults() { // defaults after the effective C compiler has been resolved. pub fn (mut p Preferences) normalize_gc_defaults_for_resolved_ccompiler() { p.disable_tcc_shared_backtraces() + if p.prealloc { + p.gc_mode = .no_gc + p.clear_gc_options() + return + } if p.os != .windows || p.ccompiler_type != .msvc || p.gc_set_by_flag { return } p.gc_mode = .no_gc - p.compile_defines = p.compile_defines.filter(it !in windows_default_gc_defines) - p.compile_defines_all = p.compile_defines_all.filter(it !in windows_default_gc_defines) - for define in windows_default_gc_defines { - p.compile_values.delete(define) - } - mut build_options := []string{cap: p.build_options.len + 2} - mut i := 0 - for i < p.build_options.len { - option := p.build_options[i] - if option == '-gc' { - i += 2 - continue - } - if option.starts_with('-d ') { - define := option[3..].all_before('=') - if define in windows_default_gc_defines { - i++ - continue - } - } - build_options << option - i++ - } - build_options << ['-gc', 'none'] - p.build_options = build_options + p.clear_gc_options() } fn (p &Preferences) default_output_name(rpath string) string { @@ -431,6 +412,11 @@ fn (mut p Preferences) try_to_use_tcc_by_default() { return } if p.ccompiler == '' { + // -prealloc uses thread-local allocator state. The bundled tcc does not + // support TLS declarations, so use the platform C compiler by default. + if p.prealloc { + return + } // use an optimizing compiler (i.e. gcc or clang) on -prod mode if p.is_prod { return @@ -476,6 +462,38 @@ pub fn default_tcc_compiler() string { return usable_system_tcc_compiler() } +fn (mut p Preferences) clear_gc_options() { + p.compile_defines = p.compile_defines.filter(it !in windows_default_gc_defines) + p.compile_defines_all = p.compile_defines_all.filter(it !in windows_default_gc_defines) + for define in windows_default_gc_defines { + p.compile_values.delete(define) + } + mut build_options := []string{cap: p.build_options.len + 2} + mut i := 0 + for i < p.build_options.len { + option := p.build_options[i] + if option == '-gc' { + i += 2 + continue + } + if option.starts_with('-gc ') { + i++ + continue + } + if option.starts_with('-d ') { + define := option[3..].all_before('=') + if define in windows_default_gc_defines { + i++ + continue + } + } + build_options << option + i++ + } + build_options << ['-gc', 'none'] + p.build_options = build_options +} + pub fn (mut p Preferences) default_c_compiler() { // TODO: fix $if after 'string' $if windows { diff --git a/vlib/v/pref/default_tcc_compiler_test.v b/vlib/v/pref/default_tcc_compiler_test.v index 0ae1181d3..bf107ca58 100644 --- a/vlib/v/pref/default_tcc_compiler_test.v +++ b/vlib/v/pref/default_tcc_compiler_test.v @@ -110,6 +110,30 @@ fn test_try_to_use_tcc_by_default_skips_broken_bundled_tcc_off_musl() { assert prefs.ccompiler == '' } +fn test_try_to_use_tcc_by_default_skips_tcc_for_prealloc() { + $if windows { + return + } + test_root := os.join_path(os.vtmp_dir(), 'v_pref_default_tcc_compiler_test') + prepare_test_tcc_binary(test_root, 'exit 0') + fake_vexe := os.join_path(test_root, 'v') + old_vexe := os.getenv('VEXE') + os.setenv('VEXE', fake_vexe, true) + defer { + if old_vexe == '' { + os.unsetenv('VEXE') + } else { + os.setenv('VEXE', old_vexe, true) + } + os.rmdir_all(test_root) or {} + } + mut prefs := Preferences{ + prealloc: true + } + prefs.try_to_use_tcc_by_default() + assert prefs.ccompiler == '' +} + fn test_usable_system_tcc_compiler_prefers_termux_tcc_from_path() { $if windows { return diff --git a/vlib/v/pref/pref.v b/vlib/v/pref/pref.v index a43f4335a..cab30fd17 100644 --- a/vlib/v/pref/pref.v +++ b/vlib/v/pref/pref.v @@ -943,6 +943,9 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin } '-prealloc' { res.prealloc = true + if !res.gc_set_by_flag { + res.gc_mode = .no_gc + } res.build_options << arg } '-no-parallel' { diff --git a/vlib/v/pref/pref_test.v b/vlib/v/pref/pref_test.v index f72dd0294..1fa2fc24f 100644 --- a/vlib/v/pref/pref_test.v +++ b/vlib/v/pref/pref_test.v @@ -295,6 +295,23 @@ fn test_musl_keeps_explicit_gc_selection() { assert prefs.gc_mode == .boehm_full_opt } +fn test_prealloc_defaults_to_no_gc() { + target := os.join_path(vroot, 'examples', 'hello_world.v') + prefs, _ := pref.parse_args_and_show_errors([], ['', '-prealloc', target], false) + assert prefs.prealloc + assert prefs.gc_mode == .no_gc +} + +fn test_prealloc_overrides_explicit_gc_selection() { + target := os.join_path(vroot, 'examples', 'hello_world.v') + prefs, _ := pref.parse_args_and_show_errors([], ['', '-gc', 'boehm', '-prealloc', target], + false) + assert prefs.prealloc + assert prefs.gc_mode == .no_gc + assert 'gcboehm' !in prefs.compile_defines + assert prefs.build_options.join(' ').contains('-gc none') +} + fn stale_windows_gc_prefs(gc_set_by_flag bool) pref.Preferences { mut prefs := pref.Preferences{ os: .windows diff --git a/vlib/veb/README.md b/vlib/veb/README.md index d61bd90a3..70f2f6559 100644 --- a/vlib/veb/README.md +++ b/vlib/veb/README.md @@ -126,6 +126,32 @@ It only affects the default non-SSL picoev backend and currently requires Linux or Termux. When running with `-d new_veb`, the fasthttp backend is already multi-threaded and ignores `nr_workers`. +## Request-scoped allocation with `-prealloc` + +When a `veb` app runs on the `fasthttp` backend and is compiled with +`-prealloc`, each request is handled inside a scoped prealloc arena created by +`fasthttp`. V allocations made while decoding the request, creating the +`veb.Context`, matching routes, running middleware, rendering templates, and +serializing the response all use that request arena. + +The arena is freed after the response has been sent, not merely when the route +handler returns. On macOS and BSD, the response buffer can be retained by the +connection while `kqueue` finishes writing it; the arena is detached from the +request thread and freed after the write completes. This keeps request-local +allocations cheap while preserving response-buffer lifetime. + +Do not store request-scoped strings, arrays, maps, `Context` values, or template +output in app fields, globals, caches, or other threads unless you deliberately +copy them into longer-lived storage. Process startup data, route tables, static +file maps, database pools, and allocations made directly by C libraries are not +part of the per-request arena. + +To trace request arena allocation and free points while developing, build with: + +```sh +v -prealloc -d trace_prealloc -d new_veb run . +``` + ## HTTPS To serve HTTPS directly from `veb`, pass an `mbedtls.SSLConnectConfig` in `RunParams`: diff --git a/vlib/veb/context.v b/vlib/veb/context.v index d098dcb70..16300e836 100644 --- a/vlib/veb/context.v +++ b/vlib/veb/context.v @@ -49,8 +49,9 @@ mut: static_compression_mime_types []string // controls whether veb should automatically send the response or whether the handler // takes over response writing. - takeover_mode ContextTakeoverMode - return_file string + takeover_mode ContextTakeoverMode + return_file string + custom_mime_types_ref &map[string]string = unsafe { nil } // raw client file descriptor, used by the fasthttp backend to create a TcpConn // on demand when takeover_conn() is called client_fd int = -1 @@ -112,6 +113,10 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, response strin // ctx.done is only set in this function, so in order to sent a response over the connection // this value has to be set to true. Assuming the user doesn't use `ctx.conn` directly. ctx.done = true + if ctx.res.body.len > 0 { + unsafe { ctx.res.body.free() } + ctx.res.body = '' + } $if veb_livereload ? { if mimetype == 'text/html' { ctx.res.body = response.replace('', @@ -147,6 +152,8 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, response strin } if ctx.takeover_mode != .none && ctx.conn != unsafe { nil } { fast_send_resp(mut ctx.conn, ctx.res) or {} + unsafe { ctx.res.body.free() } + ctx.res.body = '' } // result is send in `veb.v`, `handle_route` return Result{} @@ -183,6 +190,17 @@ pub fn (mut ctx Context) json_pretty[T](j T) Result { } // Response HTTP_OK with file as payload +fn (ctx &Context) custom_mime_type(ext string) ?string { + if ct := ctx.custom_mime_types[ext] { + return ct + } + if unsafe { ctx.custom_mime_types_ref != nil } { + custom_mime_types := unsafe { *ctx.custom_mime_types_ref } + return custom_mime_types[ext] + } + return none +} + pub fn (mut ctx Context) file(file_path string) Result { if !os.exists(file_path) { eprintln('[veb] file "${file_path}" does not exist') @@ -191,7 +209,7 @@ pub fn (mut ctx Context) file(file_path string) Result { ext := os.file_ext(file_path) mut content_type := ctx.content_type if content_type.len == 0 { - if ct := ctx.custom_mime_types[ext] { + if ct := ctx.custom_mime_type(ext) { content_type = ct } else { content_type = mime_types[ext] diff --git a/vlib/veb/static_handler.v b/vlib/veb/static_handler.v index 46cef0630..7f104a293 100644 --- a/vlib/veb/static_handler.v +++ b/vlib/veb/static_handler.v @@ -7,6 +7,7 @@ mut: static_files map[string]string static_mime_types map[string]string static_hosts map[string]string + static_prefixes []string enable_static_gzip bool enable_static_zstd bool enable_static_compression bool @@ -21,6 +22,7 @@ pub mut: static_files map[string]string static_mime_types map[string]string static_hosts map[string]string + static_prefixes []string // enable_static_gzip enables gzip compression for static files. // Use this for gzip-only compression. For automatic zstd/gzip selection, use enable_static_compression. // Default: false @@ -144,6 +146,30 @@ pub fn (mut sh StaticHandler) host_serve_static(host string, url string, file_pa } sh.static_files[url] = file_path sh.static_hosts[url] = host + sh.register_static_prefix(url) +} + +fn static_prefix_for_url(url string) string { + if url.len == 0 || url[0] != `/` { + return url + } + mut slash_count := 0 + for i in 0 .. url.len { + if url[i] == `/` { + slash_count++ + if slash_count == 2 { + return url[..i + 1] + } + } + } + return url +} + +fn (mut sh StaticHandler) register_static_prefix(url string) { + prefix := static_prefix_for_url(url) + if prefix !in sh.static_prefixes { + sh.static_prefixes << prefix + } } fn app_static_handler[A](app &A) StaticHandler { @@ -162,5 +188,6 @@ fn app_static_handler[A](app &A) StaticHandler { static_files: map[string]string{} static_mime_types: map[string]string{} static_hosts: map[string]string{} + static_prefixes: []string{} } } diff --git a/vlib/veb/veb.v b/vlib/veb/veb.v index 7148c7679..23ffdacdc 100644 --- a/vlib/veb/veb.v +++ b/vlib/veb/veb.v @@ -29,12 +29,20 @@ pub fn no_result() Result { struct Route { methods []http.Method path string + words []string host string mut: middlewares []RouteMiddleware after_middlewares []RouteMiddleware } +fn route_path_words(path string) []string { + if path.len == 0 || path == '/' { + return []string{} + } + return path.split('/').filter(it != '') +} + // Generate route structs for an app fn generate_routes[A, X](app &A) !map[string]Route { // Parsing methods attributes @@ -48,6 +56,7 @@ fn generate_routes[A, X](app &A) !map[string]Route { mut route := Route{ methods: http_methods path: route_path + words: route_path_words(route_path) host: host } @@ -278,7 +287,7 @@ fn handle_ssl_request[A, X](req http.Request, params &SslRequestParams) ?&Contex return none } host_with_port := req.header.get(.host) or { '' } - host, _ := urllib.split_host_port(host_with_port) + host := request_host_name(host_with_port) mut ctx := &Context{ req: req page_gen_start: page_gen_start @@ -287,15 +296,17 @@ fn handle_ssl_request[A, X](req http.Request, params &SslRequestParams) ?&Contex files: files } ctx.client_wants_to_close = request_has_connection_close(req) + mut user_context := X{ + Context: ctx + } $if A is StaticApp { - ctx.custom_mime_types = global_app.static_mime_types.clone() - mut user_context := X{} - user_context.Context = ctx + ctx.custom_mime_types_ref = unsafe { &global_app.static_mime_types } if serve_if_static[X](static_handler_config(global_app.static_files, - global_app.static_mime_types, global_app.static_hosts, global_app.enable_static_gzip, - global_app.enable_static_zstd, global_app.enable_static_compression, - global_app.static_compression_max_size, global_app.static_compression_mime_types, - global_app.enable_markdown_negotiation), mut user_context, url, host) + global_app.static_mime_types, global_app.static_hosts, global_app.static_prefixes, + global_app.enable_static_gzip, global_app.enable_static_zstd, + global_app.enable_static_compression, global_app.static_compression_max_size, + global_app.static_compression_mime_types, global_app.enable_markdown_negotiation), mut + user_context, url, host) { // Preserve the handled context on the heap before the stack-local user context goes away. unsafe { @@ -309,8 +320,6 @@ fn handle_ssl_request[A, X](req http.Request, params &SslRequestParams) ?&Contex return completed_context } } - mut user_context := X{} - user_context.Context = ctx handle_route[A, X](mut global_app, mut user_context, url, host, params.routes) // Preserve the handled context on the heap before the stack-local user context goes away. unsafe { @@ -358,26 +367,72 @@ fn write_ssl_response(mut ssl_conn mbedtls.SSLConn, resp http.Response) ! { } fn request_has_connection_close(req http.Request) bool { - return (req.header.get(.connection) or { '' }).to_lower() == 'close' + conn := req.header.get(.connection) or { return false } + return ascii_eq_ignore_case(conn, 'close') } -fn should_close_connection(req http.Request, resp http.Response, client_wants_to_close bool) bool { - if client_wants_to_close { - return true +fn request_host_name(host_with_port string) string { + if host_with_port.len == 0 { + return '' } - resp_conn := (resp.header.get(.connection) or { '' }).to_lower() - if resp_conn == 'close' { - return true + if host_with_port[0] == `[` { + end := host_with_port.index_u8(`]`) + if end > 0 { + return host_with_port[1..end] + } + return host_with_port + } + colon := host_with_port.last_index_u8(`:`) + if colon == -1 { + return host_with_port + } + for i in 0 .. colon { + if host_with_port[i] == `:` { + return host_with_port + } } - if resp_conn == 'keep-alive' { + return host_with_port[..colon] +} + +fn ascii_eq_ignore_case(a string, b string) bool { + if a.len != b.len { return false } - req_conn := (req.header.get(.connection) or { '' }).to_lower() - if req_conn == 'close' { + for i in 0 .. a.len { + mut ca := a[i] + if ca >= `A` && ca <= `Z` { + ca += 32 + } + mut cb := b[i] + if cb >= `A` && cb <= `Z` { + cb += 32 + } + if ca != cb { + return false + } + } + return true +} + +fn should_close_connection(req http.Request, resp http.Response, client_wants_to_close bool) bool { + if client_wants_to_close { return true } - if req_conn == 'keep-alive' { - return false + if resp_conn := resp.header.get(.connection) { + if ascii_eq_ignore_case(resp_conn, 'close') { + return true + } + if ascii_eq_ignore_case(resp_conn, 'keep-alive') { + return false + } + } + if req_conn := req.header.get(.connection) { + if ascii_eq_ignore_case(req_conn, 'close') { + return true + } + if ascii_eq_ignore_case(req_conn, 'keep-alive') { + return false + } } return req.version != .v1_1 } @@ -437,7 +492,7 @@ fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string // Context.takeover_mode is set, so the user must send a response. } - url_words := url.path.split('/').filter(it != '') + is_root_path := url.path.len == 0 || url.path == '/' $if veb_livereload ? { if url.path.starts_with('/veb_livereload/') { @@ -460,6 +515,9 @@ fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string if user_context.Context.done { return } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb before_request done') } + } // then execute global middleware functions $if A is MiddlewareApp { @@ -470,16 +528,83 @@ fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string } $if A is StaticApp { - if serve_if_static[X](static_handler_config(app.static_files, app.static_mime_types, - app.static_hosts, app.enable_static_gzip, app.enable_static_zstd, - app.enable_static_compression, app.static_compression_max_size, - app.static_compression_mime_types, app.enable_markdown_negotiation), mut user_context, - url, host) - { - // successfully served a static file - return + should_check_static := !is_root_path || app.enable_markdown_negotiation + || app.static_files['/'] != '' || app.static_files['/index.html'] != '' + || app.static_files['/index.htm'] != '' + if should_check_static { + if serve_if_static[X](static_handler_config(app.static_files, app.static_mime_types, + app.static_hosts, app.static_prefixes, app.enable_static_gzip, + app.enable_static_zstd, app.enable_static_compression, + app.static_compression_max_size, app.static_compression_mime_types, + app.enable_markdown_negotiation), mut user_context, url, host) + { + // successfully served a static file + return + } } } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb route static checked') } + } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb before route match') } + } + + if is_root_path { + $for method in A.methods { + $if method.name == 'index' && method.return_type is Result { + route = (*routes)[method.name] or { + eprintln('[veb] parsed attributes for the `${method.name}` are not found, skipping...') + Route{} + } + if user_context.Context.req.method in route.methods + && (route.host == '' || route.host == host) + && (route.path == '/' || route.path == '/index') { + $if A is MiddlewareApp { + if validate_middleware[X](mut user_context, get_handlers_for_method(route.middlewares, + user_context.Context.req.method)) == false { + middleware_has_sent_response = true + return + } + } + can_have_data_args := user_context.Context.req.method == .post + || user_context.Context.req.method == .get + if method.args.len > 1 && can_have_data_args { + mut args := []string{cap: method.args.len + 1} + data := if user_context.Context.req.method == .get { + user_context.Context.query + } else { + user_context.Context.form + } + for param in method.args[1..] { + args << data[param.name] + } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb before route handler') } + } + app.$method(mut user_context, ...args) + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb after route handler') } + } + } else { + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb before route handler') } + } + app.$method(mut user_context) + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb after route handler') } + } + } + return + } + } + } + } + + url_words := route_path_words(url.path) + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb route words parsed') } + } // Route matching and match route specific middleware as last step $for method in A.methods { @@ -492,7 +617,7 @@ fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string // Skip if the HTTP request method does not match the attributes if user_context.Context.req.method in route.methods { // Used for route matching - route_words := route.path.split('/').filter(it != '') + route_words := route.words // Skip if the host does not match or is empty if route.host == '' || route.host == host { @@ -521,14 +646,27 @@ fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string for param in method.args[1..] { args << data[param.name] } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb before route handler') } + } app.$method(mut user_context, ...args) + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb after route handler') } + } } else { + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb before route handler') } + } app.$method(mut user_context) + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb after route handler') } + } } return } - if url_words.len == 0 && route_words == ['index'] && method.name == 'index' { + if url_words.len == 0 && route_words.len == 1 && route_words[0] == 'index' + && method.name == 'index' { $if A is MiddlewareApp { if validate_middleware[X](mut user_context, get_handlers_for_method(route.middlewares, user_context.Context.req.method)) == false { @@ -548,9 +686,21 @@ fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string for param in method.args[1..] { args << data[param.name] } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb before route handler') } + } app.$method(mut user_context, ...args) + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb after route handler') } + } } else { + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb before route handler') } + } app.$method(mut user_context) + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb after route handler') } + } } return } @@ -575,7 +725,13 @@ fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string if method_args.len + 1 != method.args.len { eprintln('[veb] warning: uneven parameters count (${method.args.len}) in `${method.name}`, compared to the veb route `${method.attrs}` (${method_args.len})') } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb before route handler') } + } app.$method(mut user_context, ...method_args) + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb after route handler') } + } return } } @@ -599,7 +755,13 @@ fn handle_route[A, X](mut app A, mut user_context X, url urllib.URL, host string if method_args.len + 1 != method.args.len { eprintln('[veb] warning: uneven parameters count (${method.args.len}) in `${method.name}`, compared to the veb route `${method.attrs}` (${method_args.len})') } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb before route handler') } + } app.$method(mut user_context, ...method_args) + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb after route handler') } + } return } } @@ -680,8 +842,18 @@ fn route_matches(url_words []string, route_words []string) ?[]string { // returns true if we served a static file, false otherwise fn serve_if_static[X](app StaticHandler, mut user_context X, url urllib.URL, host string) bool { // TODO: handle url parameters properly - for now, ignore them - mut asked_path := url.path.clone() + mut asked_path := url.path static_handler := app + if !static_handler.enable_markdown_negotiation && asked_path == '/' + && static_handler.static_files['/'] == '' + && static_handler.static_files['/index.html'] == '' + && static_handler.static_files['/index.htm'] == '' { + return false + } + if !static_handler.enable_markdown_negotiation + && !static_handler.may_contain_static_path(asked_path) { + return false + } // Content negotiation for markdown files (if enabled) if static_handler.enable_markdown_negotiation { @@ -746,17 +918,34 @@ fn serve_if_static[X](app StaticHandler, mut user_context X, url urllib.URL, hos static_handler.static_compression_max_size } else { 1048576 // Default: 1MB - }, static_handler.static_compression_mime_types.clone()) + }, static_handler.static_compression_mime_types) user_context.send_file(mime_type, static_file) return true } -fn static_handler_config(static_files map[string]string, static_mime_types map[string]string, static_hosts map[string]string, enable_static_gzip bool, enable_static_zstd bool, enable_static_compression bool, static_compression_max_size int, static_compression_mime_types []string, enable_markdown_negotiation bool) StaticHandler { +fn (sh StaticHandler) may_contain_static_path(path string) bool { + if sh.static_prefixes.len == 0 || path == '/' { + return true + } + for prefix in sh.static_prefixes { + if prefix.ends_with('/') { + if path.starts_with(prefix) { + return true + } + } else if path == prefix { + return true + } + } + return false +} + +fn static_handler_config(static_files map[string]string, static_mime_types map[string]string, static_hosts map[string]string, static_prefixes []string, enable_static_gzip bool, enable_static_zstd bool, enable_static_compression bool, static_compression_max_size int, static_compression_mime_types []string, enable_markdown_negotiation bool) StaticHandler { return StaticHandler{ static_files: static_files static_mime_types: static_mime_types static_hosts: static_hosts + static_prefixes: static_prefixes enable_static_gzip: enable_static_gzip enable_static_zstd: enable_static_zstd enable_static_compression: enable_static_compression diff --git a/vlib/veb/veb_fasthttp.v b/vlib/veb/veb_fasthttp.v index c5d345db2..3a3cbcd4f 100644 --- a/vlib/veb/veb_fasthttp.v +++ b/vlib/veb/veb_fasthttp.v @@ -90,9 +90,13 @@ fn parallel_request_handler[A, X](req fasthttp.HttpRequest) !fasthttp.HttpRespon // Parse the request head into a standard `http.Request`, then copy just the body. mut req2 := http.parse_request_head_str(head) or { return fasthttp.HttpResponse{ - content: 'HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes() + content: 'HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes() + content_owned: true } } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb parsed http head') } + } if req.body.len > 0 { req2.data = req.buffer[req.body.start..req.body.start + req.body.len].bytestr() } @@ -100,7 +104,8 @@ fn parallel_request_handler[A, X](req fasthttp.HttpRequest) !fasthttp.HttpRespon if transfer_encoding_is_chunked(req2.header) { req2.data = decode_chunked_body(req2.data) or { return fasthttp.HttpResponse{ - content: 'HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes() + content: 'HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes() + content_owned: true } } } @@ -108,7 +113,10 @@ fn parallel_request_handler[A, X](req fasthttp.HttpRequest) !fasthttp.HttpRespon return invalid_resp } // Create and populate the `veb.Context`. - completed_context := handle_request_and_route[A, X](mut global_app, req2, client_fd, params) + mut completed_context := handle_request_and_route[A, X](mut global_app, req2, client_fd, params) + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb handled route') } + } match completed_context.takeover_mode { .manual { @@ -120,29 +128,42 @@ fn parallel_request_handler[A, X](req fasthttp.HttpRequest) !fasthttp.HttpRespon } } .reusable { + should_close := should_close_connection(completed_context.req, completed_context.res, + completed_context.client_wants_to_close) return fasthttp.HttpResponse{ takeover_mode: .reusable - should_close: should_close_connection(completed_context.req, - completed_context.res, completed_context.client_wants_to_close) + should_close: should_close } } .none {} } - if completed_context.return_type == .file { + should_close := should_close_connection(completed_context.req, completed_context.res, + completed_context.client_wants_to_close) + content := completed_context.res.bytes() + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb serialized response') } + } + unsafe { completed_context.res.body.free() } + completed_context.res.body = '' + return_type := completed_context.return_type + return_file := completed_context.return_file + unsafe { free(completed_context) } + + if return_type == .file { return fasthttp.HttpResponse{ - content: completed_context.res.bytes() - file_path: completed_context.return_file - should_close: should_close_connection(completed_context.req, completed_context.res, - completed_context.client_wants_to_close) + content: content + content_owned: true + file_path: return_file + should_close: should_close } } // The fasthttp server expects a complete response buffer to be returned. return fasthttp.HttpResponse{ - content: completed_context.res.bytes() - should_close: should_close_connection(completed_context.req, completed_context.res, - completed_context.client_wants_to_close) + content: content + content_owned: true + should_close: should_close } } // handle_request_and_route is a unified function that creates the context, @@ -158,11 +179,12 @@ fn content_length_validation_response(req fasthttp.HttpRequest, parsed http.Requ } if actual_length < expected_length { return fasthttp.HttpResponse{ - content: http_408.bytes() + content: http_408.bytes() + content_owned: true } } return fasthttp.HttpResponse{ - content: http.new_response( + content: http.new_response( status: .bad_request body: 'Mismatch of body length and Content-Length header' header: http.new_header( @@ -170,6 +192,7 @@ fn content_length_validation_response(req fasthttp.HttpRequest, parsed http.Requ value: 'text/plain' ).join(headers_close) ).bytes() + content_owned: true } } @@ -192,9 +215,15 @@ fn handle_request_and_route[A, X](mut app A, req http.Request, _client_fd int, p bad_ctx.request_error('Failed to parse form data: ${err.msg()}') return bad_ctx } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb parsed url/form') } + } host_with_port := req.header.get(.host) or { '' } - host, _ := urllib.split_host_port(host_with_port) + host := request_host_name(host_with_port) page_gen_start := if params.benchmark_page_generation { time.ticks() } else { 0 } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb parsed host') } + } mut ctx := &Context{ req: req page_gen_start: page_gen_start @@ -204,12 +233,16 @@ fn handle_request_and_route[A, X](mut app A, req http.Request, _client_fd int, p form: form files: files } + mut user_context := X{ + Context: ctx + } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb context initialized') } + } $if A is StaticApp { - ctx.custom_mime_types = app.static_mime_types.clone() - mut user_context := X{} - user_context.Context = ctx + ctx.custom_mime_types_ref = unsafe { &app.static_mime_types } if serve_if_static[X](static_handler_config(app.static_files, app.static_mime_types, - app.static_hosts, app.enable_static_gzip, app.enable_static_zstd, + app.static_hosts, app.static_prefixes, app.enable_static_gzip, app.enable_static_zstd, app.enable_static_compression, app.static_compression_max_size, app.static_compression_mime_types, app.enable_markdown_negotiation), mut user_context, url, host) @@ -221,6 +254,9 @@ fn handle_request_and_route[A, X](mut app A, req http.Request, _client_fd int, p return ctx } } + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb pre-route static checked') } + } // Match controller paths first $if A is ControllerInterface { if completed_context := handle_controllers[X](params.controllers_sorted, ctx, mut url, host) { @@ -228,9 +264,10 @@ fn handle_request_and_route[A, X](mut app A, req http.Request, _client_fd int, p } } // Create a new user context and pass veb's context - mut user_context := X{} - user_context.Context = ctx handle_route[A, X](mut app, mut user_context, url, host, params.routes) + $if trace_prealloc ? { + unsafe { prealloc_scope_checkpoint(c'veb route returned') } + } // Preserve the handled context on the heap before the stack-local user context goes away. unsafe { *ctx = user_context.Context -- 2.39.5