The problem
I have been trying to profile some code using the Eff monad from effectful but have been having trouble with the costs not being attributed properly to the effectful action. Any SCC annotation I add to an effectful action like so:
do
foo <- {-# SCC "foo" #-} someEffectfulAction x
doesn’t seem to capture the cost of the effectful action. If I use ghc-debug-brick to look at a thunk, in most cases, instead of seeing “foo” somewhere in the CCS, I will only see runEff and unsafeEff followed by high-level functions near the top of my program. Similarly, looking at the profile via speedscope shows a distinct lack of foo. Looking at the prof file from -- +RTS -p shows that the cost-centre “foo” is there, but it has a neglibible cost. However, based on traceEventIO and ghc-events-analyzer, I am certain that someEffectfulAction x is what’s contributing to a significant portion of the runtime, and that blocking IO calls are not the problem.
My current hypothesis for why this is the case is that because a value of type Eff es a is nothing but a function Env es -> IO a, the cost that the cost-centre foo is measuring is not the price of the IO action, but instead the cost of evaluating the function Env es -> IO a to weak head normal form, which is basically nothing in most cases.
This doesn’t seem to be a problem with code in the IO action directly. For example, attached to the runEff action, I have an SCC annotation defined in basically an identical manner:
do
bar <- {-# SCC "bar" #-} runEff theEffectfulAction
and the total cost of the effectful action does seem to be counted under the cost-centre “bar”.
I tried looking at the generated core and even stg of these two examples to see what differentiates the foo and bar case. What I saw in the bar case looks roughly like this:
case (scctick<bar> \s -> blah) s0 of ...
where s0 is the state token of type State# RealWorld from earlier code. Meanwhile, for foo, I typically saw something like this:
let {foo :: Eff [State Int] Bool
foo = scctick<foo> someEffValue) } in ...
Some things I’ve tried
- Using
>>=instead of do-notation - Using
withRunInIOto “run” the effectful action in IO, and attaching the SCC annotation to the IO - Using
unsafeEffandunEffto run the action in IO, adding an SCC annotation to the IO action- In a similar vein, importing the constructor of IO, attaching the SCC annotation to the (\s → …) function
- Even further, attaching the SCC annotation to the evaluation (in a
caseexpression) of the unwrapped IO action against aState# RealWorldtoken. E.g.,case {-# SCC "foo" #-} f s of
None of these approaches have worked, whether it be because of optimizations or otherwise.
What I’d like to know is how I should profile effectful code in a way that I can take advantage of tools like ghc-debug-brick and speedscope. Given that Eff es a is just a newtype for Env es -> IO a, I suspect that if a method to do this is found, it should also work for code using the monad ReaderT r IO, and vice versa.