packages feed

mptcpanalyzer-0.0.2.0: src/Net/Tcp/Stats.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE StandaloneDeriving #-}
{-
Module:  Net.Tcp.Stats
Description : Compute basic TCP statistics
Maintainer  : matt
Portability : Linux
-}

module Net.Tcp.Stats (
  TcpUnidirectionalStats (..)
  , getTcpGoodput
  , getTcpThroughput
  , getTcpSeqRange
  , getTcpStatsFromAFrame
  , getSeqRange
  , showTcpUnidirectionalStats
)
where

import MptcpAnalyzer.ArtificialFields
import MptcpAnalyzer.Types
import MptcpAnalyzer.Utils.Text
import Net.Tcp.Connection

import qualified Control.Foldl as L
import Control.Lens hiding (argument)
import qualified Data.Map as Map
import Data.Maybe (fromJust)
import Data.Word (Word32, Word64)
-- import Data.ByteUnits

import qualified Data.Foldable as F
import Data.Ord (comparing)
import qualified Data.Text as T
import Frames
import qualified Frames as F
import qualified Frames.InCore as F

type Byte = Int

-- tus = tcp Unidrectional Stats
data TcpUnidirectionalStats = TcpUnidirectionalStats {
    -- sum of tcplen / should be the same for tcp/mptcp
    -- Include redundant packets contrary to '''
    -- tusThroughput :: Byte

    -- tusStartPacketId :: Word64
    -- , tusEndPacketId :: Word64
    tusNrPackets :: Int
    -- duration
    -- , tusDuration :: Double

    -- TODO convert to date ?
    , tusStartTime :: Double
    , tusEndTime :: Double

    -- For now = max(tcpseq) - minx(tcpseq). Should add the size of packets'''
    -- , tusMinSeq :: Word32
    -- , tusMaxSeq :: Word32

    -- transferred bytes
    -- , tusCumulativeBytes :: Map Word64 Word16
    , tusMinSeq :: Word32
    , tusSndUna :: Word32
    , tusSndNext :: Word32
    -- , tusReinjectedBytes :: Map Word64 Word16
    , tusReinjectedBytes :: Word32 -- ^Amount of reinjected bytes
    -- , tusUniqueBytes :: Word64

    -- , tusMinAck :: Word32
    -- , tusMaxAck :: Word32

    -- application data = goodput = useful bytes '''
    -- TODO move to its own ? / Maybe
    -- , mptcp_application_bytes :: Byte
    -- , tusThroughputContribution :: Double
    -- , tusGoodputContribution :: Double

    -- TODO this should be updated
    -- For now = max(tcpseq) - minx(tcpseq). Should add the size of packets'''
    -- , tusGoodput :: Byte
    }  deriving (Show, Eq)
    -- deriving Semigroup via WrappedMonoid TcpUnidirectionalStats

-- deriving instance Semigroup TcpUnidirectionalStats
-- deriving instance Monoid TcpUnidirectionalStats

instance Semigroup TcpUnidirectionalStats where
   -- (<>) :: a -> a -> a
   -- TODO this does nothing
   (<>) a b = TcpUnidirectionalStats {
      -- tusThroughput = 0
      -- tusStartPacketId = min (tusStartPacketId a) (tusStartPacketId b)
      -- , tusEndPacketId = max (tusEndPacketId a) (tusEndPacketId b)
      tusNrPackets = tusNrPackets a + tusNrPackets b
      , tusStartTime = min (tusStartTime a) (tusStartTime b)
      , tusEndTime = max (tusEndTime a) (tusEndTime b)
      -- let it to 0 for now
      , tusMinSeq = 0

      -- TODO should be max of seen acks
      , tusSndUna = max (tusSndUna a) (tusSndUna b)
      , tusSndNext = max (tusSndNext a) (tusSndNext b)
      , tusReinjectedBytes = 0
      -- , tusSnd = 0
      -- , tusNumberOfPackets = mempty
    }

instance Monoid TcpUnidirectionalStats where
  mempty = TcpUnidirectionalStats {
      -- tusThroughput = 0
      -- tusStartPacketId = 0 -- (frameRow frame 0) ^. packetId
      -- , tusEndPacketId = 0 -- (frameRow frame (frameLength frame - 1)) ^. packetId
      tusNrPackets = 0
      , tusStartTime = 0
      , tusEndTime = 0
      , tusMinSeq = 0        -- TODO fill it

      -- TODO should be max of seen acks
      , tusSndUna = 0
      , tusSndNext = 0
      , tusReinjectedBytes = 0
      -- , tusSnd = 0
      -- , tusNumberOfPackets = mempty
    }

-- | TODO check boundaries etc
getSeqRange :: Num a => a -> a
  -> a
  -- -> (a, a, a)
-- getSeqRange maxSeq minSeq = (maxSeq - minSeq + 1, maxSeq, minSeq)
getSeqRange maxSeq minSeq = maxSeq - minSeq + 1



-- TODO add a Functor to FilteredFrame
genTcpStats :: Frame Packet -> TcpUnidirectionalStats
genTcpStats aframe = TcpUnidirectionalStats {

    -- TODO we should run a minmax instead
    -- tusStartPacketId = minPktId
    -- , tusEndPacketId = maxPktId
    tusNrPackets = frameLength aframe
    --     maxTime = maximum $ F.toList $ view relTime <$> frame
    -- minTime = minimum $ F.toList $ view relTime <$> frame

    -- We could just take first and last
    , tusStartTime = minTime
    , tusEndTime = maxTime

    , tusMinSeq = 0 -- not used
    , tusSndUna = maxUna
    , tusSndNext = 0
    , tusReinjectedBytes = 0
  }
  where
    -- we could use the Statistics vector if we could use the
    (minPktId, maxPktId) = case L.fold ((,) <$> L.minimum <*> L.maximum) $ F.toList $ view packetId <$> aframe of
        (Just pmin, Just pmax) -> (pmin, pmax)
        _otherwise -> error "Could not find either min or max"

    (minTime, maxTime) = case L.fold ((,) <$> L.minimum <*> L.maximum) $ F.toList $ view relTime <$> aframe of
        (Just pmin, Just pmax) -> (pmin, pmax)
        _otherwise -> error "Could not find either min or max"

    (minSeq, maxSeq) = case L.fold ((,) <$> L.minimum <*> L.maximum) $ F.toList $ view tcpSeq <$> aframe of
        (Just pmin, Just pmax) -> (pmin, pmax)
        _otherwise -> error "Could not find either min or max"

    -- TODO
    (minUna, maxUna) = case L.fold ((,) <$> L.minimum <*> L.maximum) $ F.toList $ view tcpAck <$> aframe of
        (Just pmin, Just pmax) -> (pmin, pmax)
        _otherwise -> error "Could not find either min or max"



-- | Destination should have been filtered upstream
-- see @genTcpDestFrame@
-- TODO suffix fromFrame
getTcpStatsFromAFrame :: (
  TcpSeq F.∈ rs
  , F.RecVec rs
  , TcpLen F.∈ rs, RelTime F.∈ rs
  , PacketId F.∈ rs
  -- disabled for now, we assumed it's filtered upstream
  , TcpDest F.∈ rs
  )
  => FrameFiltered TcpConnection (F.Record rs)
  -> ConnectionRole
  -> TcpUnidirectionalStats
getTcpStatsFromAFrame aframe dest =
  if frameLength frame == 0 then
    mempty
  else
    TcpUnidirectionalStats {
      -- tusThroughput = 0
      -- tusStartPacketId = 0 -- (frameRow frame 0) ^. packetId
      -- , tusEndPacketId = 0 -- (frameRow frame (frameLength frame - 1)) ^. packetId
      tusNrPackets = frameLength frame
      , tusStartTime = minTime
      , tusEndTime = maxTime
      -- TODO fill it
      , tusMinSeq = minSeq

      -- TODO should be max of seen acks
      , tusSndUna = maxSeqRow ^. tcpSeq + fromIntegral ( maxSeqRow ^. tcpLen) :: Word32
      , tusSndNext = maxSeqRow ^. tcpSeq + fromIntegral ( maxSeqRow ^. tcpLen ) :: Word32
      , tusReinjectedBytes = 0
    }
  where
    frame = F.filterFrame (\x -> x ^. tcpDest == dest) (ffFrame aframe)

    -- these return Maybes
    -- I need to find its id and add tcpSize afterwards
    -- TODO use     minimumBy
    minSeq = case F.toList $ view tcpSeq <$> frame of
      [] -> 0
      l -> minimum l
    -- maxSeq = maximum $ F.toList $ view tcpSeq <$> frame

    -- $ F.toList $ view tcpSeq <$> frame
    maxSeqRow = F.maximumBy (comparing (^. tcpSeq)) frame

    -- compareRows x y = if (x ^. tcpSeq) (y ^. tcpSeq)

    maxTime = maximum $ F.toList $ view relTime <$> frame
    minTime = minimum $ F.toList $ view relTime <$> frame


-- byteValue
getTcpSeqRange :: TcpUnidirectionalStats -> Double
getTcpSeqRange s =
  fromIntegral (tusSndUna s - tusMinSeq s - 1)

-- | Computes throughput
-- TODO should return a quantity with number
getTcpThroughput :: TcpUnidirectionalStats -> Double
getTcpThroughput s =
  if tusNrPackets s == 0 then 0
  else fromIntegral (tusSndUna s - tusMinSeq s - 1) / (tusEndTime s - tusStartTime s)

-- | Computes goodput
getTcpGoodput :: TcpUnidirectionalStats -> Double
getTcpGoodput s =
  fromIntegral (tusSndUna s - tusMinSeq s + 1 - tusReinjectedBytes s) / (tusEndTime s - tusStartTime s)

showTcpUnidirectionalStats :: TcpUnidirectionalStats -> Text
showTcpUnidirectionalStats stats =
  T.unlines [
    "Timestamps:" <> tshow (tusStartTime stats) <> " -> " <> tshow (tusEndTime stats)
    , "Reinjected bytes: " <> tshow (tusReinjectedBytes stats)
    , "Current goodput: " <> tshow (getTcpGoodput stats)
  ]

    -- duration = maxTime - minTime



-- No instance for (Ord (Frames.Frame.Frame GHC.Word.Word32))
-- instance Ord a => Ord (Frame a)
-- def transmitted_seq_range(df, seq_name):
--     '''
--     test
--     '''
--     log.debug("Computing byte range for sequence field %s", seq_name)

--     sorted_seq = df.dropna(subset=[seq_name]).sort_values(by=seq_name)
--     log.log(mp.TRACE, "sorted_seq %s", sorted_seq)

--     seq_min = sorted_seq.loc[sorted_seq.first_valid_index(), seq_name]
--     last_valid_index = sorted_seq.last_valid_index()
--     seq_max = sorted_seq.loc[last_valid_index, seq_name] \
--         + sorted_seq.loc[last_valid_index, "tcplen"]

--     # -1 because of SYN
--     # seq_range = seq_max - seq_min - 1
--     seq_range = seq_max - seq_min - 1

--     msg = "seq_range ({}) = {} (seq_max) - {} (seq_min) - 1"
--     log.log(mp.TRACE, msg.format(seq_range, seq_max, seq_min))

--     return seq_range, seq_max, seq_min


  -- where
  --   packetStreams = filterStreamPackets frame streamId (Just role)
    -- log.debug("Getting TCP stats for stream %d", tcpstreamid)
    -- assert destination in ConnectionRoles, "destination is %r" % type(destination)

    -- df = rawdf[rawdf.tcpstream == tcpstreamid]
    -- if df.empty:
    --     raise MpTcpException("No packet with tcp.stream == %d" % tcpstreamid)

    -- df2 = df

    -- log.debug("df2 size = %d" % len(df2))
    -- log.debug("Looking at role %s" % destination)
    -- # assume it's already filtered ?
    -- sdf = df2[df2.tcpdest == destination]
    -- bytes_transferred = Byte(sdf["tcplen"].sum())
    -- assert bytes_transferred >= 0

    -- # -1 to account for SYN
    -- tcp_byte_range, seq_max, seq_min = transmitted_seq_range(sdf, "tcpseq")

    -- # print(sdf["abstime"].head())
    -- # print(dir(sdf["abstime"].dt))
    -- # print(sdf["abstime"].dt.end_time)
    -- times = sdf["abstime"]
    -- tcp_duration = times.iloc[-1] - times.iloc[0]
    -- # duration = sdf["abstime"].dt.end_time - sdf["abstime"].dt.start_time

    -- assert tcp_byte_range is not None

    -- return TcpUnidirectionalStats(
    --     tcpstreamid,
    --     tcp_duration=tcp_duration,
    --     throughput_bytes=bytes_transferred,
    --     # FIX convert to int because Byte does not support np.int64
    --     tcp_byte_range=Byte(tcp_byte_range)
    -- )