Zig NEWS

Rene Schallner
Rene Schallner

Posted on • Updated on

Introducing ⚡zap⚡ - blazingly fast backends in zig

Zap is intended to become my zig replacement for the kind of REST APIs I used to write in python with Flask and mongodb, etc. It can be considered to be a microframework for web applications.

What I need for that is a blazingly fast, robust HTTP server that I can use with zig. While facil.io supports TLS, I don't care about HTTPS support. In production, I use nginx as a reverse proxy anyway.

Zap wraps and patches facil.io - the C web application framework.

At the time of writing, ZAP is only a few days old and aims to be:

  • robust
  • fast
  • minimal

⚡ZAP⚡ IS SUPER ALPHA

Under the hood, everything is super robust and fast. My zig wrappers are fresh, juicy, and alpha.

Here's what works:

  • Super easy build process: zap's build.zig fetches facilio's git sub-module, applies a patch to its logging for microsecond precision, and then builds and optionally runs everything.
    • tested on Linux and macOS (arm, M1)
  • hello: welcomes you with some static HTML
  • routes: a super easy example dispatching on the HTTP path
  • serve: the traditional static web server with optional dynamic request handling
  • hello_json: serves you json dependent on HTTP path
  • endpoint: a simple JSON REST API example featuring a /users endpoint for PUTting/DELETE-ing/GET-ting/POST-ing and listing users, together with a static HTML and JavaScript frontend to play with.

If you want to take it for a quick spin:

$ git clone https://github.com/renerocksai/zap.git 
$ cd zap 
$ zig build run-hello 
$ # open http://localhost:3000 in your browser 
Enter fullscreen mode Exit fullscreen mode

See the README for how easy it is to get started, how to run the examples, and how to use zap in your own projects.

I'll continue wrapping more of facil.io's functionality and adding stuff to zap to a point where I can use it as the JSON REST API backend for real research projects, serving thousands of concurrent clients. Now that the endpoint example works, ZAP has actually become pretty usable to me.

Side-note: It never ceases to amaze me how productive I can be in zig, eventhough I am still considering myself to be a newbie. Sometimes, it's almost like writing python but with all the nice speed and guarantees that zig gives you. Also, the C integration abilities of zig are just phenomenal! I am super excited about zig's future!

Now, on to the guiding principles of Zap.

robust

A common recommendation for doing web stuff in zig is to write the actual HTTP server in Go, and use zig for the real work. While there is a selection of notable and cool HTTP server implementations written in zig out there, at the time of writing, most of them seem to a) depend on zig's async facilities which are unsupported until ca. April 2023 when async will return to the self-hosted compiler, and b) have not matured to a point where I feel safe using them in production. These are just my opionions and they could be totally wrong though.

However, when I conduct my next online research experiment with thousands of concurrent clients, I cannot afford to run into potential maturity-problems of the HTTP server. These projects typically feature a you-get-one-shot process with little room for errors or re-tries.

With zap, if something should go wrong, at least I'd be close enough to the source-code to, hopefully, be able to fix it in production. With that out of the way, I am super confident that facil.io is very mature compared to many of the alternatives. My wrk tests also look promising.

I intend to add app-specific performance tests, e.g. stress-testing the endpoint example, to make sure the zap endpoint framework is able to sustain a high load without running into performance or memory problems. That will be interesting.

⚡blazingly fast⚡

Claiming to be blazingly fast is the new black. At least, zap doesn't slow you down and if your server performs poorly, it's probably not exactly zap's fault. Zap relies on the facil.io framework and so it can't really claim any performance fame for itself. In this initial implementation of zap, I didn't care about optimizations at all.

But, how fast is it? Being blazingly fast is relative. When compared with a simple GO HTTP server, a simple zig zap HTTP server performed really good on my machine:

  • zig zap was nearly 30% faster than GO
  • zig zap had over 50% more throughput than GO

I intentionally only tested static HTTP output, as that seemed to be the best common ground of all test subjects to me. The measurements were for just getting a ballpark baseline anyway.

Update: I was intrigued comparing to a basic rust HTTP server. Unfortunately, knowing nothing at all about rust, I couldn't find a simple, just-a-few-lines one like in Go and Python right away and hence tried to go for the one in the book The Rust Programming Language. Wanting it to be of a somewhat fair comparison, I opted for the multi-threaded example. It didn't work out-of-the-book, but I got it to work (essentially, by commenting out all "print" statements) and changed it to not read files but outputting static text just like the other examples. Maybe someone with rust experience can have a look at my wrk/rust/hello code and tell me why it is surprisingly 'slow', as I expected it to be faster than or at least on-par with the basic Go example. I'll enable the GitHub discussions for this matter. My suspicion is bad performance of the mutexes.

table

charts

So, being somewhere in the ballpark of basic GO performance, zig zap seems to be ... of reasonable performance 😎.

See more details in blazingly-fast.md.

minimal

Zap is minimal by necessity. I only (have time to) add what I need - for serving REST APIs and HTML. The primary use-case are frontends that I wrote that communicate with my APIs. Hence, the focus is more on getting stuff done rather than conforming to every standard there is. Even though facilio is able to support TLS, I don't care about that - at least for now. Also, if you present 404 - File not found as human-readable HTML to the user, nobody forces you to also set the status code to 404, so it can be OK to spare those nanoseconds. Gotta go fast!

Facilio comes with Mustache parsing, TLS via third-party libs, websockets, redis support, concurrency stuff, Base64 support, logging facilities, pub/sub / cluster messages API, hash algorithm implementations, its own memory allocator, and so forth. It is really an amazing project!

On the lower level, you can use all of the above by working with zap.C. I'll zig-wrap what I need for my projects first, before adding more fancy stuff.

Also, there are nice and well-typed zig implementations for some of the above extra functionalities, and zap-wrapping them needs careful consideration. E.g. it might not be worth the extra effort to wrap facil.io's mustache support when there is a good zig alternative already. Performance / out-of-the-box integration might be arguments pro wrapping them in zap.

wrapping up - zig is WYSIWYG code

I am super excited about both zig and zap's future. I am still impressed by how easy it is to integrate a C codebase into a zig project, then benefiting from and building on top of battle-tested high-performance C code. Additionally, with zig you get C-like performance with almost Python-like comfort. And you can be sure no exception is trying to get you when you least expect it. No hidden allocations, no hidden control-flows, how cool is that? WYSIWYG code!

Provided that the incorporated C code is well-written and -tested, WYSIWYG even holds mostly true for combined Zig and C projects.

You can truly build on the soulders of giants here. Mind you, it took me less than a week to arrive at the current state of zap where I am confident that I can already use it to write the one or other REST API with it and, after stress-testing, just move it into production - from merely researching Zig and C web frameworks a few days ago.

Oh, and have I mentioned Zig's built-in build system and testing framework? Those are both super amazing and super convenient. zig build is so much more useful than make (which I quite like to be honest). And zig test is just amazing, too. Zig's physical code layout: which file is located where and how can it be built, imported, tested - it all makes so much sense. Such a coherent, pleasant experience.

Looking forward, I am also tempted to try adding some log-and-replay facilities as a kind of backup for when things go wrong. I wouldn't be confident to attemt such things in C because I'd view them as being too much work; too much could go wrong. But with Zig, I am rather excited about the possibilities that open up and eager to try such things.

For great justice!

Latest comments (9)

Collapse
 
pismice profile image
Pismice

Hey great text, but what do you exactly mean by "A common recommendation for doing web stuff in zig is to write the actual HTTP server in Go, and use zig for the real work." ?
I dont really get how you can nicely have the 2 work together

Collapse
 
azizadx profile image
Azizadx • Edited

can I do simple api crud ?
it is also the first time seeing facli.io framework

Collapse
 
renerocksai profile image
Rene Schallner

Thanks to the amazing tips from GH user @felipetrz, I was able to provide additional timings of more realistic python and rust frameworks. Apparently, the images in the article have updated automatically - Markdown Magic 😄.

Here they are again just in case...

You can check out the ./wrk folder on GitHub, the measure.sh script is there as well as all servers I have implemented / copy-pasted. You'll see the --release flag on certain builds just in case you wonder. 😉

Collapse
 
jiacai2050 profile image
Jiacai Liu • Edited

I can't imagine rust has so bad performance, how you tried some framework with async runtime like aync_std or tokio?

stackoverflow.com/a/65029740/2163429

There is an example.

Collapse
 
azizadx profile image
Azizadx

I know it real interesting

Collapse
 
renerocksai profile image
Rene Schallner • Edited

Thanks for your suggestion. I couldn't believe it myself but it is true. "Basic" rust with standard sockets etc. seems to be that slow.

I have not tried tokio or something like that because I wanted it to be an equally unfair contrived comparison. Unfair because I am comparing basically the performance of an optimized C framework with super basic example servers. Contrived because I tested the timing of canned responses.

Have a look at them on GitHub, it's all there. I wanted to compare against a basic python SimpleHttpServer to get an idea for basic python performance. I compared against a standard basic Go implementation to see how zap compares to that. Just to get an idea if zap is 10x slower or if it can keep up.

Why I did it that way?

a) because it was simple

b) because I wanted to compare what you can do with the most simple zap server vs. the most simple python server vs the most simple Go server. If you don't know what to pick, this is maybe how you'd start evaluating. Then with rust, as I said, I literally went by the book. Made sure number of threads etc. is equal to the zap example. It just didn't want to perform that well.

You can find all the source code here.

So far, the question isn't answered why the rust example straight out of the book seems comparably slow - but I guess nobody uses such simple servers in the real world anyway when there's stuff like tokio out there.

I fully understand that there are faster python servers, probably faster Go servers, and certainly extremely fast rust servers out there. So for a fair "which is fastest" comparison, you'd have to check out some fastest server list, build them, build respective meaningful "apps" (as opposed to: contrived) and test against them. Then also build them in the ever evolving zap and see how it performs. I am happy with seeing responses in the us range on my dev PC when testing my frontends against a zap backend. That's the kind of performance I was looking for. I am sure there's faster stuff out there.

Collapse
 
nullfame profile image
David Pacsuta

I am guessing the Rust project was built without the --release flag

Collapse
 
renerocksai profile image
Rene Schallner

No, it was built with the --release flag, indeed. Check the script that compiles and runs it here.

Thread Thread
 
nullfame profile image
David Pacsuta

Thanks, I didn't see that.