Evening Project: A Crystal based super fast visitor counter

I have taken this week's holidays with the plan that the Overkill Workbench materials would be delivered today, but in the end, it will be delivered on Sunday, so I have a lot of free time on my hands.

Yesterday, while talking with some friends, I remembered old good <2008 websites, portals, and how we created them; one of the most prominent features I loved about that period was 88x31, and 120x60 sized Ad's/Counters and other goodies. It was always a fight between website authors fighting for a higher number of page visits and similar metrics. Nowadays, everything is hidden and typical, only seen by webmasters on Google Analytics and similar tools.

So today's my evening project is Crystal language-based 88x31 website visitor counter image rendered entirely in Crystal.

Requirements:

  • A single endpoint would return 88x31 sized png with numbers
  • Provide two numbers, one unique visitor count, and other total visits.
  • Do not depend on external libraries for image generation.
  • Use the least amount of resources like memory and disk space. (Maybe one day my blog will be viral, who knows)
  • Most important, be as fast as possible!

Architecture:

The plan is to run the crystal internal HTTP server without any overhangs and host it on Heroku free plan.

Store user identifiers in Redis for uniqueness measurement and fast lookup (Remember we need speed)

I found a shard (Crystal libraries are called shards) for image rendering, which can generate a PNG image using raw X, Y pixel information no external libraries used.

stumpycr/stumpy_png
Read/Write PNG images in pure Crystal. Contribute to stumpycr/stumpy_png development by creating an account on GitHub.

For Redis client, I am going to use this shard:

stefanwille/crystal-redis
Full featured Redis client for Crystal. Contribute to stefanwille/crystal-redis development by creating an account on GitHub.

Step 1: Rendering image

As I have chosen image size to be 88x31, I need to try to fit two numbers. Total visits - Every load counts and Unique Visitors - Number of unique visitors.

I have drawn some sample representation I imagine in Photoshop:

It looks tiny on my 4K monitor, but back in 2005, it looked huge on my 1024x768 monitor.

One of the problems now that I am not using external libraries is that I have no simple way to render text on the image. That's not a big deal, remembering practices I used for Graphical LCD/OLED on embedded electronic projects. I will create an array of Tuples of 3 uint8 integers of each pixel information in a 7x10 array for each number.

To make each number in array format, I need to generate 7x10 images of each number. Then using the https://javl.github.io/image2cpp/ tool, I generated arrays for each character.

Now that I have pixel data of each character, I can finally create a whole image.

Knowing the array's exact size, in our case, it's 7x10; we can loop through the array and fill in all pixels referenced from a given position.

After trying out VisitorCounter::Characters.render_character function I was able to see it working correctly.

Now it's time to wrap it all and make the main function, which would generate and return generated image as IO::Memory buffer.

To make more usable, I added this image generator to a simple HTTP server and returned random numbers generated in response.

After running this code and going to http://127.0.0.1:8080 I received generated image with random numbers.

Now we can move on to a more exciting part, which is counting visitors.

Step 2: Counting Visitors

To count visitors first, we need some kind of unique value. In this project, I am going to use the IP address of the client. As I plan to host this on Heroku, I know that IP will only be IPv4, so I can safely convert the IP address from 127.0.0.1 to its bytes equivalent by merging all 4 x Int8 parts of IP this way it will take less space in Redis memory 4 bytes instead of 15 bytes.

This is a function which extracts IP address from request. As I mentioned before, this will be hosted on Heroku, so the client IP address will be available in the HTTP header X-Forwarded-For as a load balancer will replace the client IP address with its own.

If the IP address is not available for some reason, I will skip this visit from a unique visit count and just increase the total visit count.

Now wrapping everything into WebHandler, which will nicely integrate into HTTP Server, we should have a working counter.

The main file code should look like this right now:

Running the main code now we should see the counter working as expected:

Notice the response times of the web request! It's around 1ms per request! That's crazy fast... But wait, it's not built correctly.

Now that's what I call FAST!

Just one issue... While running Apache benchmarks, I noticed that the total visit counter is increasing at every request, which is right, but it can be easily abused. We need to rate-limit the total visit counter so that a single IP address can have only one visit per X amount of time.

Easy, he said! Remember, we are using Redis for our storage, and Redis has a Keys with Expiration feature. Which is precisely what we need.

This way now only increases total visits only when the rate limit timeout will be reached; in this code, it's 5 seconds, but I am going to set something like 1 minute in production.

Deploying to Heroku

Now that we have our application working as we expect. We should deploy our application. I am going to follow the official Crystal guide for Heroku deployment.

After easy set up and install now we have an working counter running on heroku.

https://immense-beyond-23382.herokuapp.com/

Conclusion

The fully working source code is available on my Patreon account for all pledgers. I will try to add this small counter to this Ghost template as I really loved the idea of this small counter 15 years ago.

Don't forget to subscribe to the newsletters down bellow. Every new article will be delivered in a friendly email, readable format straight into your mailbox!