Nea

the webserver that never allocates

Folkert de Vries, SYCL 2024

Nea is...

written in Rust

Proof of Concept

Nea is...

roc platform

web server

systems programming 

Our Problem

webserver woes

A webserver

  1. accept a new request
  2. run a request handler
  3. send response
  4. GOTO 1

🔁

A webserver

in production

you pay for resources

locally

resource are infinite

🌄

💸

A webserver

running out of memory is bad for your users

"have you tried turning it off and on again" is not great ux

A webserver

you are at the mercy of your runtime

haskell/java/js/ruby/... runtimes are mediocre at everything.

Partial Solutions

heuristics

not guarantees

hard to find optimal settings

⚖️

Goal 1

get a firm upper bound on the amount of memory used by our webserver

🧠

Goal 2

a memory-hungry request should not impact any other request

Catching the Culprit

tracking down memory usage

catching the culprit

pub fn reserve(
    &mut self, additional: usize
);

pub fn try_reserve(
    &mut self, additional: usize,
) -> Result<(), TryReserveError>;

catching the culprit

in its lane, flourishing

catching the culprit

the malloc that broke the cammel's back

catching the culprit

impossible to track down problematic requests

Solutions

Nea's design

our starting point

refcounting, so no GC pauses

roc is (relatively) fast

🚀

♻️

solution 1: one big allocation

using mmap

(or similar)

don't want to recompile

get a firm upper bound on the amount of memory used by our webserver

constraint: worst case == best case

our memory consumption is always maximal

⛰️

solution 2: memory separation

a memory-hungry request should not impact other requests

idea: each request gets its own bump arena

the arena is cleared when a request is done

constraint: # of concurrent requests

we now run into a hard limit: there needs to be a bucket available in order to serve a request.

🪣

but we can do MATH!

constraint: similar work

bucket size is fixed: memory is used most effectively if most requests do a roughly equal amount of allocating

⚖️

error recovery

↩️

setjmp/longjmp

back up the stack

send error 500

log request

error recovery

#include < setjmp.h >

void main() {
  jmp_buf env;
  int i;

  i = setjmp(env);
  printf("i = %d\n", i);

  if (i != 0) { exit(0) };

  longjmp(env, 2);
  printf("Never gets here\n");
}

error recovery

setjmp:
    mov [rdi],    rbx     ; // Store caller saved registers
    mov [rdi+8],  rbp     ; // ^
    mov [rdi+16], r12     ; // ^
    mov [rdi+24], r13     ; // ^
    mov [rdi+32], r14     ; // ^
    mov [rdi+40], r15     ; // ^
    lea rdx,      [rsp+8] ; // go one value up (as if setjmp wasn't called)
    mov [rdi+48], rdx     ; // Store the new rsp pointer in env[7]
    mov rdx,      [rsp]   ; // go one value up (as if setjmp wasn't called)
    mov [rdi+56], rdx     ; // Store the address we will resume at in env[8]
    xor eax,      eax     ; // Always return 0
    ret

error recovery

longjmp:
    xor eax, eax         ; // set eax to 0
    cmp esi, 1           ; // CF = val ? 0 : 1
    adc eax, esi         ; // eax = val + !val
    mov rbx, [rdi]       ; // Load in caller saved registers
    mov rbp, [rdi+8]     ; // ^
    mov r12, [rdi+16]    ; // ^
    mov r13, [rdi+24]    ; // ^
    mov r14, [rdi+32]    ; // ^
    mov r15, [rdi+40]    ; // ^
    mov rsp, [rdi+48]    ; // Value of rsp before setjmp call
    jmp [rdi+56]         ; // goto saved address without altering rsp

error recovery

loop {
    work = queue.pop().await?;
    match setjmp(&mut jmp_buf) { 
        0 => { 
            // snapshot has been made 
            call_roc(work)
        }
        _ => {
            // allocation failed
            todo!()
        }
    }
}
fn roc_alloc() -> *mut c_void { 
    let ptr = ...;

    if ptr.is_null() {
        longjmp(&jmp_buf, 1)
    } else {
        ptr
    }
}

error recovery

setjmp/longjmp are so unsafe, even an unsafe block cannot contain them

#include < setjmp.h >

void main() {
  jmp_buf env;
  int i;

  i = setjmp(env);
  printf("i = %d\n", i);

  if (i != 0) { exit(0) };

  longjmp(env, 2);
  printf("Never gets here\n");
}

async IO

async IO makes sense when for a webserver

🧵

async IO

async fn foo() {
    // ...
    tcp_stream.flush().await;
}

rust has no idea how to actually run this

async IO

Future Work

  • make it more robust
  • run actual roc applications
  • benchmarks

Summary

roc platforms combine the convenient and performant

custom allocators are a superpower

hard bounds mean we can do MATH!

NEver Allocate!

Thanks

github: folkertdev  

@folkertdev@hachyderm.io

constraints ⬌ guarantees

no free lunch

constraints ⬌ guarantees

> Array(8).join("wat" - 1) + " Batman!";

constraints ⬌ guarantees

> Array(8).join("wat" - 1) + " Batman!";
< 'NaNNaNNaNNaNNaNNaNNaN Batman!'

constraints ⬌ guarantees

Inductive Vector (A : Type) : nat -> Type :=
| VNil : Vector A 0
| VCons : forall (n : nat), A -> Vector A n -> Vector A (S n).

Nea Sycl

By folkert de vries

Nea Sycl

RustNL 2024

  • 43