refactor: App control flow refactoring#4969
Conversation
9350465 to
42da614
Compare
I'm confused: This is a self-reference and there is only one commit in here. |
Yes, sorry - during reworking of this PR the description got outdated. |
|
Should this be ready for review? |
b6d2c8b to
e238fc2
Compare
00f2217 to
9732728
Compare
|
The coverage job says: |
I'm aware of this but hesitating what to do. The situation is that
The first one is simpler and leaves all functions related to The second one splits The second option is nice because it clearly allows encapsulating different responsibilities and thanks to it - GHC can easily inline respective functions easily, because the functions stay internal to the modules they are used in. |
Nice! I can confirm that by reading the loadtest results. Also now we do have a reduction in LOCs! 🎉 @wolfgangwalther Any opposition towards changing the loadtest columns from
@mkleczek I'd go with the second option if the inlining does bring us more performance. |
Not opposed if you can make it work. I never liked it that way, but couldn't figure out how to affect the order of the output. |
9732728 to
c060e88
Compare
|
Q: Why this shows on the loadtest:
Did we really lose perf for that case? |
This is for the 90th percentile. That data is inherently more noisy. There is no reason to assume that there is a performance loss specific for that case, which is only visible on that percentile. |
c060e88 to
e3b825c
Compare
|
Has this refactor become a performance improvement now? Can these two be separated in any way or is that not easily possible? |
I wouldn't consider it as performance improvement PR. Any performance gains are a byproduct of polishing the PR - original one introduced performance regressions and after updating the code we have (modest) perf improvements. But the goal of this PR stays the same: make the request handling control flow explicit and easier to follow. I've split the changes into three separate commits to make the change history cleaner. |
Ah, I missed that. Right, agree then. |
4e04f96 to
012b811
Compare
| class MonadTiming m where | ||
| withTiming' :: forall name a. KnownSymbol name => m a -> m a | ||
| renderTimings :: m [HTTP.Header] | ||
|
|
||
| withTiming :: forall s m a. (KnownSymbol s, MonadTiming m) => m a -> m a | ||
| withTiming = withTiming' @m @s | ||
|
|
||
| instance Monad m => MonadTiming (IdentityT m) where | ||
| withTiming' = identity | ||
| renderTimings = pure mempty | ||
|
|
||
| instance MonadIO m => MonadTiming (StateT [Timing] m) where | ||
| withTiming' @name f = do | ||
| (t, result) <- timeItT f | ||
| modify ((BS.pack $ symbolVal (Proxy @name), t) :) | ||
| pure result | ||
| {-# INLINE withTiming' #-} | ||
| renderTimings = (foldMap (pure . serverTimingHeader) . nonEmpty) . reverse <$> get |
There was a problem hiding this comment.
Can we add a comment here to explain why these are needed? You have explained it the commit message, but it would be good to add that here too, I think.
There was a problem hiding this comment.
Guess we should remove this INLINE here too?
There was a problem hiding this comment.
Guess we should remove this
INLINEhere too?
I'm afraid not - without explicit INLINE GHC did not inline withTiming' for some reason, which in turn stopped it from performing more important optimizations: constant folding of symbolVal handling.
Will check again since I verified it before #5008.
f1ea79d to
38e7b57
Compare
38e7b57 to
ab8310a
Compare
|
Just a note: I looked at this multiple times, but this exceeds my mental capacity - there are too many new concepts in here, still unknown to me, that I have to learn each time, and looking at this at once is hard. I do understand the overall idea to be roughly that:
Ok, cool. And then come the language extensions... :) |
[...] It's not only about specialization/performance.
They are really to enable passing timing names as type level literals. Thanks to it in the "no-timing" case we can have no-op |
ab8310a to
44f1ac6
Compare
Make MainTx.mainTx and Query.mainQuery receive DbPlan instead of ActionPlan so that they do not have to handle unrelated NoDb cases. Removed "NoDbResult" constructor from "DbResult" datatype and factored out noDbActionResponse from actionResponse to clearly separate "DbResult" and "InfoPlan" handling. Server timing is now handled in a way that does not pollute the control flow. New MonadTiming typeclass with two implementations was introduced - one for `StateT Timings m` that times operations and remembers timing as state, and another for `IdentityT m` that does not do any measurements. The selection of monad is guarded by `configServerTimingsEnabled`. GHC with `-O2` is capable to specialize code so that `withTiming` calls are completely discarded for disabled case. Moved authentication back to App.postgrestResponse so that all steps that require timing are encapsulated. In 5359769 it was moved level up the call chain which, while achieving the goal of removing auth middleware, made the control flow complicated and difficult to follow. Thanks to introduction of WriterT based handling of auth role in d663421, it is now possible to return to have all request handling pipeline steps in App.postgrestResponse.
This change splits JWT handling into two separate groups: token parsing/signature verification and claims validation. The first group was moved to Auth.JwtCache as these functions are used in JWT cache. The second group was moved to Auth module (as it validates claims after looking them up in the JWT cache). Auth.JWT module was removed as no longer needed. Thanks to this change JWT handling became more cohesive and made it possible for GHC to aggressively inline and specialize JWT related functions.
getAuthResult suggests authentication result is already available and it just needs to be read. But in reality this function performs full JWT validation (ie. parsing, signature verification, claims validation). Renaming it to authenticate so that the name is no longer misleading.
44f1ac6 to
82bdbb0
Compare
| class MonadTiming m where | ||
| withTiming' :: forall name a. KnownSymbol name => m a -> m a | ||
| renderTimings :: m [HTTP.Header] | ||
|
|
||
| withTiming :: forall s m a. (KnownSymbol s, MonadTiming m) => m a -> m a | ||
| withTiming = withTiming' @m @s | ||
|
|
||
| -- Dummy MonadTiming instance. | ||
| -- Used when configServerTimingEnabled is False | ||
| -- GHC specialization will optimize away all calls to withTiming and to renderTimings, | ||
| -- hence hot path does not execute any unnecessary code and does not cause unnecessary allocations. | ||
| instance Monad m => MonadTiming (IdentityT m) where | ||
| withTiming' = identity | ||
| renderTimings = pure mempty | ||
|
|
||
| -- MonadTiming instance saving timings in StateT [Timing]. | ||
| -- Used when configServerTimingEnabled is True | ||
| instance MonadIO m => MonadTiming (StateT [Timing] m) where | ||
| withTiming' @name f = do |
There was a problem hiding this comment.
How about we move this to TimeIt.hs module? It seems like it is polluting this App.hs. WDYT?
Make MainTx.mainTx and Query.mainQuery receive DbPlan instead of ActionPlan so that they do not have to handle unrelated NoDb cases.
Removed "NoDbResult" constructor from "DbResult" datatype and factored out noDbActionResponse from actionResponse to clearly separate "DbResult" and "InfoPlan" handling.
Server timing is now handled in a way that does not pollute the control flow. New
MonadTimingtypeclass with two implementations was introduced - one forStateT Timings mthat times operations and remembers timing as state, and another forIdentityT mthat does not do any measurements. The selection of monad is guarded byconfigServerTimingsEnabled. GHC with-O2is capable to specialize code so thatwithTimingcalls are completely discarded for disabled case.Moved authentication back to App.postgrestResponse so that all steps that require timing are encapsulated. In #4884 it was moved level up the call chain which, while achieving the goal of removing auth middleware, made the control flow complicated and difficult to follow. Thanks to introduction of WriterT based handling of auth role in #4968, it is now possible to return to have all request handling pipeline steps in
App.postgrestResponse.This PR is a follow up on #4968.