packages feed

hs-opentelemetry-api-1.0.0.0: src/OpenTelemetry/Internal/AtomicBucketArray.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}

{- |
Module      :  OpenTelemetry.Internal.AtomicBucketArray
Copyright   :  (c) Ian Duncan, 2024-2026
License     :  BSD-3
Description :  Contiguous array of atomic counters for histogram bucket counts.
Stability   :  experimental

One 'MutableByteArray#' holds all bucket counts in a single allocation,
indexed by bucket number. Each slot supports atomic fetch-and-add via
@fetchAddIntArray#@, a single @ldadd@ (AArch64) or @lock xadd@ (x86)
instruction with no CAS retry and no allocation.

Used to eliminate the O(n) bucket vector copy that 'Data.Vector.Unboxed.modify'
performs on every histogram recording inside an @atomicModifyIORef'@ CAS loop.
-}
module OpenTelemetry.Internal.AtomicBucketArray (
  AtomicBucketArray,
  newAtomicBucketArray,
  atomicAddBucket,
  readBucketArray,
  readAndResetBucketArray,
) where


#include "MachDeps.h"

import Data.Vector.Unboxed (Vector)
import qualified Data.Vector.Unboxed as U
import Data.Word (Word64)
import GHC.Exts (
  Int (..),
  Int#,
  MutableByteArray#,
  RealWorld,
  State#,
  fetchAddIntArray#,
  isTrue#,
  negateInt#,
  newByteArray#,
  readIntArray#,
  writeIntArray#,
  (*#),
  (+#),
  (>=#),
 )
import GHC.IO (IO (..))


{- | Contiguous mutable array of atomic counters. Each element is one
machine-word 'Int', accessible via hardware fetch-and-add.

@since 0.0.1.0
-}
data AtomicBucketArray = AtomicBucketArray (MutableByteArray# RealWorld) Int#


{- | Allocate a new bucket array with all counters initialized to zero.

@since 0.0.1.0
-}
newAtomicBucketArray :: Int -> IO AtomicBucketArray
newAtomicBucketArray (I# n) = IO $ \s ->
  let !nbytes = n *# SIZEOF_HSINT#
  in case newByteArray# nbytes s of
       (# s1, arr #) -> case zeroFill arr n s1 of
         s2 -> (# s2, AtomicBucketArray arr n #)


zeroFill :: MutableByteArray# RealWorld -> Int# -> State# RealWorld -> State# RealWorld
zeroFill arr nwords s0 = go 0# s0
  where
    go i s
      | isTrue# (i >=# nwords) = s
      | otherwise = case writeIntArray# arr i 0# s of
          s' -> go (i +# 1#) s'


{- | Atomically increment the bucket at the given index by 1.
No bounds checking.

@since 0.0.1.0
-}
atomicAddBucket :: AtomicBucketArray -> Int -> IO ()
atomicAddBucket (AtomicBucketArray arr _) (I# i) = IO $ \s ->
  case fetchAddIntArray# arr i 1# s of
    (# s', _ #) -> (# s', () #)
{-# INLINE atomicAddBucket #-}


{- | Read all bucket values into an immutable 'U.Vector Word64'.
Each element is read individually; not globally atomic across buckets
(acceptable for metric snapshots).

@since 0.0.1.0
-}
readBucketArray :: AtomicBucketArray -> IO (Vector Word64)
readBucketArray (AtomicBucketArray arr len) = do
  let !n = I# len
  U.generateM n $ \(I# i) -> IO $ \s ->
    case readIntArray# arr i s of
      (# s', v #) -> (# s', fromIntegral (I# v) #)
{-# INLINE readBucketArray #-}


{- | Read all bucket values and atomically reset each to zero.
Uses fetch-and-add with the negated current value.
Between the read and the subtract another thread may increment,
but the net effect is correct across consecutive delta collections:
no sample is lost, at worst one sample shifts to the next cycle.

@since 0.0.1.0
-}
readAndResetBucketArray :: AtomicBucketArray -> IO (Vector Word64)
readAndResetBucketArray (AtomicBucketArray arr len) = do
  let !n = I# len
  U.generateM n $ \(I# i) -> IO $ \s ->
    case readIntArray# arr i s of
      (# s1, old #) -> case fetchAddIntArray# arr i (negateInt# old) s1 of
        (# s2, _ #) -> (# s2, fromIntegral (I# old) #)
{-# INLINE readAndResetBucketArray #-}