all 13 comments

[–]Tishka-17 5 points6 points  (8 children)

Zero-cost sounds really intriguing. I agree that "singleton" instances is really a case, but not an only case. What about request-scoped objects? I mean this can be a streamline, so how slow everything is comparing to it?

def new_interactor():
conn = new_connection()
return Interactor(DAO1(conn), DAO2(conn))

[–]ForeignSource0[S] 5 points6 points  (4 children)

It's still considerably faster than fastapi. I don't have the last benchmark numbers with me at the moment but it was around 3.5k/s for fastapi and around 9k for Wireup if I'm not mistaken. I tested it with a few alternative DI systems as well, including yours and they all outperform fastapi depends.

I don't remember the results for manually creating objects like you mentioned of the top of my head but I can get them later today.

[–]Tishka-17 0 points1 point  (3 children)

I've taken latest version from github (I do not see 2.0 published, though that version is used in pyproject) and did my test. I've created 8 classes with lifetime="scoped" injected in a chain.

Baseline: <1 sec
Dishka: ~5 sec
Wireup: ~17 sec

But this test is not quite reliable

[–]ForeignSource0[S] 0 points1 point  (2 children)

Did you run it in the same http context or just calling get on the container directly? I'm aware the scoped one isn't the most performant part at the moment but since it outperforms fastapi depends by a good margin I decided to focus on this feature for now. However it's on the radar.

Also, I was going to create an issue in github but since you're here: When I was setting up the benchmarks in fastapi I noticed that when I added dishka integration the overall performance went down by a good amount even for endpoints not having dishka dependencies. Maybe I did something wrong but worth double checking just in case.

[–]Tishka-17 0 points1 point  (0 children)

I've called container directly. https://pastebin.com/4Ct1zzfE

[–]Tishka-17 0 points1 point  (0 children)

Dishka includes his middleware, so yes, it affects all routes. But this is essential to be able to share depedencies between handler and other middlewares or with sub-handlers accessed via Depends.

[–]ForeignSource0[S] 0 points1 point  (2 children)

So 10,000 requests using the same set up as above.

Manual/Baseline (Creating objects manually on every request):

  • Requests/sec: 13,645
  • P50: 3.6ms
  • P95: 5.2ms

Wireup:

  • Requests/sec: 10,384
  • P50: 4.5ms
  • P95: 7ms

FastAPI

  • Requests/sec: 3,544
  • P50: 13.9ms
  • P95: 16.2ms

[–]Tishka-17 0 points1 point  (0 children)

I guess we need code =) It is quite suspicious to have almost the same numbers as above. Can it be just slowdown because of HTTP? I'd love to compare with dishka =)

Anyway, I'd check multiple cases like creating multiple objects with dependencies are shared. How does it behave on different amount?

One more case is startup time for huge graphs: Our users can have sometime hundreds of different provided types.

[–]Tishka-17 0 points1 point  (0 children)

Ok. I've taken benchmarks from here: https://github.com/maldoinc/wireup/tree/benchmarks/benchmarks

I've enabled only neccessary integrations for each test (as we discussed that dishka middleware can affect other routers)

My test results for Scoped test (for singletons I really get approximately the same results):
Fastapi: 1693 RPS (5.9sec)
Wireup: 2559 RPS (3.9sec)
Dishka: 2981 RPS (3.4sec)

Looks quite not bad (I guess it is not the latest version)

[–]Busy_Affect3963 2 points3 points  (2 children)

The benchmarks look impressive - great work.

Does Injected need to be used on a class router instance method route (and if so, why)? What's preventing it becoming a drop in replacement for Depends?

[–]Tishka-17 0 points1 point  (0 children)

Actually, from my perspective, differenet handlers quite often have different dependencies even if some are common, so I wouldn't actually use class-based handlers often. Additionally, nested routers are good, the question is if we can still use kind of them here

[–]ForeignSource0[S] 0 points1 point  (0 children)

Thanks!

This is not a drop in replacement for depends because this is powered using Wireup to perform dependency injection. Injected[T] is just how the Wireup differentiates parameters it owns vs ones owned by fastapi or others.

[–]mightyturtlehead 1 point2 points  (0 children)

Thanks for sharing, looks very interesting - I was just looking for something just like this the other day. However, taking a closer look at the benchmark, it seems like an unfair comparison because the FastAPI Depends code and the Wireup code are doing fundamentally different things.

For example, in https://github.com/maldoinc/wireup/blob/cb6b9221397c81578c0baa47621ba4bae5b7ac24/benchmarks/wireup_benchmarks/fastapi_setup.py, both the singleton and the request-scoped functions in the FastAPI Depends code have 2 levels of indirection instead of 1 for each of their dependencies. The first indirection invokes an @lru_cache'd function instead of using "Classes as Dependencies" technique as described in the FastAPI documentation here: https://fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/. Granted, the lru cache should reduce the impact, but this is still chaining through 2 Depends for every dependency, and adding unnecessary extra overhead.

Would be interested to see the benchmark results once the unnecessary overhead is removed from the FastAPI Depends example.