Athena 0.16.0

Athena Framework 0.16.0

This release focused on integrating the new Routing component into the framework as well as the introduction of the Athena monorepo.

Routing Component

Amber Router has served the framework well for the past few years. However, as alluded to in the last release, it was time for an upgrade. I’m happy to announce the Athena::Routing component is now finished and integrated into Athena Framework!

Just like all the other components, it can also be used outside of the ecosystem, such as other frameworks, to handle your routing needs. It has quite a few features, and none of the limitations of other Crystal routers:

  • Is entirely regex based, allows using regex to define validations in order to determine if a given route should match
    • Also supports route priorities and sub-domain matching
  • Allows multiple routes with placeholders in the same location thanks to the previous bullet
  • Built in support for default values
  • Provides annotations that can be used for custom implementations

A regex router you say? That can’t be fast! Thanks to PCRE2’s JIT compilation using the fast-path API, it sure is. The main bottleneck is URI.decode. Without that, the results are even better, otherwise is still faster than the other routers in every case except two.

Given routing is a core part of a web framework, this change does come with a few breaking changes. See the changelog for the full list.

Annotations

  • @[ATHA::Prefix(prefix: "/foo")] => @[ARTA::Route(path: "/foo")]
    • Prefixes no longer check parent types. Define a single ARTA::Route with the prefix on each controller
  • ATHA::* routing annotations have been replaced with ARTA::* routing annotations

Trailing slashes are now taken into consideration

/foo is a different route when compared to /foo/. Be sure to be consistent and follow a common pattern throughout your application.

Route Parameter Syntax

Route parameters are now defined by wrapping the name within {}, such as /user/{id}. The primary benefit of this is allowing parameters within other segments, such as /foo{slug}bar which would match everything between foo and bar. It also allows the format of the request path to be captured/restricted. E.g. /user/{id}.{_format?json}, which would default to json if not provided (/user/10) or allow a custom format (/user/10.xml). The routing component has a few special parameters, such as _format, which will set the format of the request to the matched format if used. This can be especially useful for Athena’s content negotiation feature.

Athena Monorepo

As mentioned in How I migrated Athena to a Monorepo...and you can too, I have migrated Athena to a monorepo such that all of the components can be more easily maintained within a single repository. Given this is the first release after this change you will need to update the repository within shard.yml in order to install the update. Athena Framework should now be installed as:

dependencies:
  athena:
    github: athena-framework/framework
    version: ~> 0.16.0

The only difference being athena-framework/athena => athena-framework/framework.


Checkout the release notes for a complete list of changes. As usual feel free to join me in the Athena Gitter channel if you have any suggestions, questions, or ideas. I’m also available on Discord (Blacksmoke16#0016) or via Email.

P.S. Athena now has a Discord server!

10 Likes

Wow, URI.decode sure seems to be slow! I sent a PR to optimize it: Optimize URI.decode by asterite · Pull Request #11741 · crystal-lang/crystal · GitHub

What results do you get with that PR on your side?

6 Likes

@asterite Whoa thanks! It deff made quite a difference! Not only did speed improve, memory per operation also was halved, and in some cases ~6x less! URI.decode must have also been a bottleneck in retour as it jumped up quite a bit as well. Given amber and radix didn’t change much I’m going to assume they’re not using it, so technically they may have an issue with matching an encoded URL, which makes the results even that much more impressive I’d say.

Benchmark Results w/ Optimizations
/get
         retour   2.52M (397.20ns) (± 1.42%)   672B/op   4.16× slower
          amber   3.20M (312.10ns) (± 1.15%)   387B/op   3.27× slower
Athena::Routing  10.48M ( 95.46ns) (± 1.35%)  64.0B/op        fastest
          radix   6.63M (150.87ns) (± 1.15%)   144B/op   1.58× slower

/get/books/23/chapters
         retour   2.01M (498.37ns) (± 2.49%)  736B/op   1.25× slower
          amber   1.21M (827.03ns) (± 1.55%)  966B/op   2.08× slower
Athena::Routing   2.52M (397.42ns) (± 1.37%)  322B/op        fastest
          radix   1.58M (632.64ns) (± 1.80%)  481B/op   1.59× slower

/get/books/23/pages
         retour   2.67M (375.00ns) (± 2.04%)   672B/op   2.82× slower
          amber   1.86M (537.34ns) (± 5.27%)   595B/op   4.04× slower
Athena::Routing   7.52M (133.04ns) (± 3.99%)  64.0B/op        fastest
          radix   1.72M (582.15ns) (± 1.93%)   416B/op   4.38× slower

/get/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z
         retour   2.30M (434.52ns) (± 2.14%)    672B/op   4.26× slower
          amber 332.26k (  3.01µs) (± 1.38%)  4.04kB/op  29.50× slower
Athena::Routing   9.80M (102.03ns) (± 4.70%)   64.0B/op        fastest
          radix   1.90M (525.10ns) (± 1.60%)    353B/op   5.15× slower

/get/var/2/3/4/5/6/7/8/9/0/1/2/3/4/5/6/7/8/9/0/1/2/3/4/5/6
         retour 735.58k (  1.36µs) (± 5.60%)  1.17kB/op        fastest
          amber 180.40k (  5.54µs) (± 2.41%)  6.19kB/op   4.08× slower
Athena::Routing 421.41k (  2.37µs) (± 5.22%)  2.29kB/op   1.75× slower
          radix 308.44k (  3.24µs) (± 2.67%)  2.86kB/op   2.38× slower

/get/foobarbizfoobarbizfoobarbizfoobarbizfoobarbizbat/3
         retour   1.93M (517.31ns) (± 1.61%)  768B/op   1.23× slower
          amber   1.29M (775.56ns) (± 4.63%)  864B/op   1.85× slower
Athena::Routing   2.38M (419.42ns) (± 1.53%)  368B/op        fastest
          radix   1.13M (886.71ns) (± 2.32%)  593B/op   2.11× slower

/post/products/23/reviews
         retour   1.88M (531.94ns) (± 1.12%)    768B/op   1.31× slower
          amber   1.18M (849.14ns) (± 1.78%)  1.04kB/op   2.09× slower
Athena::Routing   2.43M (411.63ns) (± 1.37%)    369B/op   1.01× slower
          radix   2.46M (406.87ns) (± 3.62%)    450B/op        fastest

/put/products/Winter-Windproof-Trapper-Hat/dp/B01J7DAMCQ
         retour   1.53M (655.02ns) (± 2.76%)   832B/op   1.25× slower
          amber 841.82k (  1.19µs) (± 6.68%)  1.3kB/op   2.26× slower
Athena::Routing   1.90M (524.94ns) (± 4.57%)   432B/op        fastest

/get/test/foo_99
         retour   1.93M (518.98ns) (± 1.93%)  736B/op   1.24× slower
          amber   1.32M (757.77ns) (± 2.23%)  822B/op   1.80× slower
Athena::Routing   2.38M (420.18ns) (± 2.78%)  338B/op        fastest

/get/test/foo_bar
        retour   2.83M (353.24ns) (± 1.90%)   672B/op   2.88× slower
          amber   2.02M (495.22ns) (± 5.12%)   501B/op   4.04× slower
Athena::Routing   8.15M (122.67ns) (± 1.10%)  65.0B/op        fastest
3 Likes

Why don’t you add a sample for an encoded URL?

Good idea. Given a route like /get/encoded/{slug} and trying to match /get/encoded/foo%20bar results in:

Retour: nil
Amber: "foo bar"
Athena: "foo bar"
Radix: "foo%20bar"

Adding in a sample for this, excluding retour, results in:

/get/encoded/foo%20bar
          amber   1.31M (765.24ns) (± 1.68%)  1.02kB/op   1.61× slower
Athena::Routing   1.72M (583.03ns) (± 0.98%)    577B/op   1.23× slower
          radix   2.10M (475.10ns) (± 1.04%)    449B/op        fastest

So about what I’d expect, radix is fasted since it doesn’t have the overhead of decoding it anyway.

I am seriously considering Athena::Routing as the official routing library to our 100% Crystal framework (yet to be published)

3 Likes

Nice! Glad to hear :slight_smile:. Any questions/issues feel free to jump into the Gitter Channel/Discord Server.

2 Likes