packages feed

fastcdc-0.0.0: src/Data/Conduit/Algorithms/FastCDC.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveDataTypeable #-}

module Data.Conduit.Algorithms.FastCDC
  ( fastCDC
  , FastCDCParameters(fastCDCMinBlockSize, fastCDCMaxBlockSize, fastCDCNormalBlockSize, fastCDCMaskSmallBits, fastCDCMaskLargeBits, fastCDCGearHashTable)
  , fastCDCParameters, recommendFastCDCParameters
  ) where

import Prelude
import Data.Conduit
import Data.Digest.GearHash

import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString

import qualified Data.BitVector.LittleEndian as BitVector
import Data.Bits

import Numeric.Natural (Natural)

import GHC.Generics (Generic)
import Data.Typeable (Typeable)

import Control.Monad (guard)

import Data.Ratio ((%))


data FastCDCParameters = FastCDCParameters
  { fastCDCMinBlockSize
  , fastCDCMaxBlockSize
  , fastCDCNormalBlockSize :: !Int
  , fastCDCMaskSmallBits
  , fastCDCMaskLargeBits :: !Int
  , fastCDCWindowSize :: !Int
  , fastCDCGearHashTable :: !GearHashTable
  } deriving (Eq, Ord, Show, Generic, Typeable)

fastCDCParameters :: Int -- ^ Minimum block size
                  -> Int -- ^ Normal block size
                  -> Int -- ^ Maximum block size
                  -> Int -- ^ Mask popcount to generate normal sized chunks
                  -> Int -- ^ Mask popcount to generate small chunks
                  -> GearHashTable
                  -> Maybe FastCDCParameters
fastCDCParameters fastCDCMinBlockSize fastCDCNormalBlockSize fastCDCMaxBlockSize fastCDCMaskSmallBits fastCDCMaskLargeBits fastCDCGearHashTable = do
  fastCDCWindowSize <- gearHashTableHashLength fastCDCGearHashTable
  guard $ fastCDCMinBlockSize >= 0
  guard $ fastCDCMinBlockSize <= fastCDCNormalBlockSize
  guard $ fastCDCMaxBlockSize >= fastCDCNormalBlockSize
  guard $ fastCDCMaxBlockSize > 0
  guard $ fastCDCMaskSmallBits <= fastCDCMaskLargeBits
  guard $ fastCDCMaskLargeBits <= fastCDCWindowSize
  return FastCDCParameters{..}

recommendFastCDCParameters :: Int -- ^ @log2@ of target average block size
                           -> Int -- ^ Rolling hash window size
                           -> Maybe FastCDCParameters
recommendFastCDCParameters targetMaskSize windowSize = fastCDCParameters minBlockSize normalBlockSize maxBlockSize maskSmallBits maskLargeBits =<< defaultGearHashTableFor windowSize
  where
    minBlockSize = floor $ normalBlockSize % 4
    maxBlockSize = normalBlockSize * 8
    normalBlockSize = 2 ^ targetMaskSize
    maskSmallBits = max 0 $ targetMaskSize - 2
    maskLargeBits = targetMaskSize + 2

  
fastCDC :: forall m.
           Monad m
        => FastCDCParameters
        -> ConduitT ByteString ByteString m ()
fastCDC FastCDCParameters{..} = conduitGo ByteString.empty $ hashInitWith fastCDCGearHashTable
  where
    conduitGo :: ByteString -- ^ Chunk accumulator
              -> GearHashState
              -> ConduitT ByteString ByteString m ()
    conduitGo acc hState = do
      nextChunk <- await
      case nextChunk of
        Nothing -> yield acc
        Just nextChunk' -> go acc hState nextChunk' >>= uncurry conduitGo

    go :: forall i.
          ByteString -- ^ Chunk accumulator
       -> GearHashState
       -> ByteString -- ^ Input
       -> ConduitT i ByteString m (ByteString, GearHashState) -- ^ Return new chunk accumulator
    go acc hState inpBS
      | ByteString.null inpBS = return (acc, hState)
      | accLength < fastCDCMinBlockSize
      , (inpBSIgnore, inpBS') <- ByteString.splitAt (fastCDCMinBlockSize - accLength) inpBS
      = go (acc <> inpBSIgnore) hState inpBS'
      where accLength = ByteString.length acc
    go acc hState inpBS
      | breakPos >= inpLength = return (acc <> inpBS, hState3)
      | otherwise = do
          yield $ acc <> inpPrefix
          go ByteString.empty hState3 inpSuffix
      where
        (breakPos, hState3) = go' hState 0
        (inpPrefix, inpSuffix) = ByteString.splitAt breakPos inpBS
        
        go' :: GearHashState -> Int -> (Int, GearHashState)
        go' hState' ix
          | ix >= inpLength
          = (inpLength, hState')
          | ix >= fastCDCMaxBlockSize'
          = (fastCDCMaxBlockSize', hState')
          | otherwise = case compare ix fastCDCNormalBlockSize' of
              LT | not $ testMask hVal maskLarge
                   -> go' hState'' $ succ ix
              _  | not $ testMask hVal maskSmall
                   -> go' hState'' $ succ ix
              _ -> (ix, hState'')
          where
            hState'' = hashUpdate (ByteString.index inpBS ix) hState'
            hVal = hashFinalize hState''

        inpLength = ByteString.length inpBS
        accLength = ByteString.length acc
        fastCDCMaxBlockSize' = fastCDCMaxBlockSize - accLength
        fastCDCNormalBlockSize' = fastCDCNormalBlockSize - accLength
    
    maskSmall, maskLarge :: BitVector
    !maskSmall = mkMask fastCDCMaskSmallBits
    !maskLarge = mkMask fastCDCMaskLargeBits

    mkMask :: Int -> BitVector
    mkMask maskSize = BitVector.fromNumber (fromIntegral fastCDCWindowSize) ((pred $ 1 `shift` maskSize) `shiftL` (fastCDCWindowSize - maskSize) :: Natural)

    testMask :: BitVector -> BitVector -> Bool
    testMask v mask = BitVector.isZeroVector $ v .&. mask