packages feed

fb-util-0.1.0.1: Util/AllocLimit.hs

{-
  Copyright (c) Meta Platforms, Inc. and affiliates.
  All rights reserved.

  This source code is licensed under the BSD-style license found in the
  LICENSE file in the root directory of this source tree.
-}

{-# OPTIONS_GHC -fno-warn-unused-imports #-}

module Util.AllocLimit
  ( limitAllocs
  , limitAllocsThrow
  , withAllocLimit
  , withSavedAllocLimit
  , withAllocCounter
  , handleAllExceptions
  ) where

import GHC.Conc
import Data.Int
import Control.Exception

-- | Impose an allocation limit on the given 'IO' action.  If the
-- action exceeds the limit, then 'Nothing' will be returned.
limitAllocs :: Int64 -> IO a -> IO (Maybe a)
limitAllocs limit action =
  handle (\AllocationLimitExceeded -> return Nothing) $
    Just <$> limitAllocsThrow limit action

-- | Impose an allocation limit on the given 'IO' action.  If the
-- action exceeds the limit, then 'AllocationLimitExceeded' will be
-- thrown.
limitAllocsThrow :: Int64 -> IO a -> IO a
limitAllocsThrow limit action = do
  e <- handleAllExceptions (return . Left) $
    Right <$> withSavedAllocLimit limit action
  either throwIO return e

-- | Impose an allocation limit on the given 'IO' action.  If the
-- action exceeds the limit, then 'AllocationLimitExceeded' will be
-- thrown. (Note: most uses should use 'limitAllocs' instead).
--
-- Note that if this function throws an exception
-- ('AllocationLimitExceeded' or otherwise), even though there will be
-- no /more/ 'AllocationLimitExceeded' exceptions, there might be
-- multiple 'AllocationLimitExceeded' exceptions already in flight.
-- This happens as follows: the first exception gets thrown, which
-- causes an exception handler to run, which triggers another
-- 'AllocationLimitExceeded' exception, which doesn't get thrown
-- immediately because the exception handler is implicitly masked, and
-- so on.
--
-- To deal with this we provide 'handleAllExceptions', which you
-- should wrap around 'withAllocLimit'.
--
-- Note: this function clobbers the allocation counter, so
-- e.g. `Util.Timing.timeIt` will give bogus results if the
-- computation being measured calls `withAllocLimit`. To avoid this,
-- use `withSavedAllocLimit` instead.
withAllocLimit :: Int64 -> IO a -> IO a
withAllocLimit limit =
  bracket_
    (do setAllocationCounter limit; enableAllocationLimit)
    disableAllocationLimit

-- | Like `withAllocLimit`, but does not clobber the allocation
-- counter.
withSavedAllocLimit :: Int64 -> IO a -> IO a
withSavedAllocLimit limit io = bracket set unset $ \_ -> io
  where
  set = do
    prev <- getAllocationCounter
    setAllocationCounter limit
    enableAllocationLimit
    return prev
  unset prev = do
    cur <- getAllocationCounter
    disableAllocationLimit
    setAllocationCounter (prev - (limit - cur))

-- | Sets the allocation counter for the given 'IO' action, then restores the
-- original counter value after the 'IO' action is done.
--
-- Note that as opposed to 'withAllocLimit' this function provides no guarantee
-- that if the set alloc counter is consumed, an 'AllocationLimitExceeded' will
-- be thrown, that depends on whether 'enableAllocationLimit' has been called
-- or not. If that guarantee is something you need, use 'withAllocLimit', but be
-- mindful of the fact that 'withAllocLimit' calls should not be nested inside
-- each other while 'withAllocCounter' calls can.
--
-- This function can be useful when there's a need to "isolate" certain
-- calls from the current allocation limit by setting a separate allocation
-- limit for them, then restoring the initial limit when done.
--
-- Once this is done: T30781590. We will be able to use the new primitive in
-- 'withAllocLimit' and deprecate 'withAllocCounter'
withAllocCounter :: Int64 -> IO a -> IO a
withAllocCounter counter action = do
  initialAllocCounter <- getAllocationCounter
  bracket_
    (setAllocationCounter counter)
    (setAllocationCounter initialAllocCounter)
    action

-- | Like 'Control.Exception.handle', but if there are pending async
-- exceptions, all are swallowed except for the last one, which is
-- passed to the handler.  This is mainly for wrapping around
-- 'withAllocLimit', to ensure that no 'AllocationLimitExceeded'
-- exceptions can leak out.
--
handleAllExceptions :: (SomeException -> IO a) -> IO a -> IO a
handleAllExceptions handler = handle errorHandler
  where
    errorHandler exn =
      handle errorHandler $ do
        allowInterrupt -- let any pending async exceptions throw
        handler exn