1414import ipaddress
1515import logging
1616import operator
17+ import os
1718from pathlib import Path
1819import re
1920from dataclasses import dataclass
2021import tempfile
2122import time
2223from typing import Any , Iterable , List , Optional , Union
24+ from lbr_testsuite .executable import Tool
2325
2426import numpy as np
2527import pandas as pd
28+ import psutil
2629from ftanalyzer .common .pandas_multiprocessing import PandasMultiprocessingHelper
2730from src .generator .interface import GeneratorStats
2831
@@ -270,8 +273,6 @@ def replicate(
270273 ).name
271274 _TEMP_FILES .append (output_file )
272275
273- chunksize = int (max (100 , chunksize / loops ))
274-
275276 if output_file .startswith ("tmp_ref_" ):
276277 _TEMP_FILES .append (output_file )
277278
@@ -321,36 +322,23 @@ def replicate(
321322 )
322323 chunk ["ORIG_INDEX" ] = chunk .index
323324
324- replicated = self ._replicate (
325+ self ._replicate (
325326 chunk = chunk ,
326327 loops = loops ,
327328 loop_start = loop_start ,
328329 loop_length = loop_length ,
329330 time_multiplier = time_multiplier ,
331+ output_file = output_file ,
332+ first_write = first_write ,
333+ real_start = generator_stats .start_time ,
330334 )
335+ first_write = False
331336
332- with PandasMultiprocessingHelper () as binary_pool :
333- binary_pool .binary (
334- replicated ,
335- [
336- ("SRC_IP" , operator .add , "SRC_IP" , "_SRC_IP_OFFSET" , []),
337- ("DST_IP" , operator .add , "DST_IP" , "_DST_IP_OFFSET" , []),
338- ],
339- )
340-
341- if merge_across_loops :
342- self ._inactive_timeout = (
343- inactive_timeout * 1000 if inactive_timeout > - 1 else None
344- )
345- replicated = self ._merge_across_loop (replicated )
346-
347- replicated [self .CSV_COLUMN_TYPES .keys ()].to_csv (
348- output_file ,
349- index = False ,
350- mode = "w" if first_write else "a" ,
351- header = first_write ,
337+ if merge_across_loops :
338+ self ._inactive_timeout = (
339+ inactive_timeout * 1000 if inactive_timeout > - 1 else None
352340 )
353- first_write = False
341+ self . _merge_across_loop ( output_file )
354342
355343 end = time .time ()
356344 logging .getLogger ().info ("CSV replicated in %.2f seconds." , (end - start ))
@@ -451,8 +439,11 @@ def _replicate(
451439 loops : int ,
452440 loop_start : int ,
453441 loop_length : int ,
442+ real_start : int ,
454443 time_multiplier : float ,
455- ) -> pd .DataFrame :
444+ output_file : str ,
445+ first_write : bool ,
446+ ):
456447 """Replicate flows from source according to the configuration.
457448
458449 Parameters
@@ -473,27 +464,34 @@ def _replicate(
473464 ).astype (np .uint64 )
474465
475466 chunk ["_START_OFFSET" ] = (
476- (chunk ["START_TIME" ] - loop_start ) * time_multiplier
467+ (chunk ["START_TIME" ] - loop_start ) * time_multiplier + real_start
477468 ).astype (np .uint64 )
478469
479470 chunk ["_SRC_IP_OFFSET" ] = 0
480471 chunk ["_DST_IP_OFFSET" ] = 0
481472
482- tmp_dataframes = []
483473 for loop_n in range (loops ):
484474 logging .getLogger ().debug ("Processing loop %d..." , loop_n )
485475 if loop_n in self ._ignore_loops :
486476 continue
487477
488478 self ._flows = chunk # used internally by _process_single_loop
489479 replicated = self ._process_single_loop (loop_n , loop_start , loop_length )
490- tmp_dataframes .append (replicated )
491-
492- return (
493- pd .concat (tmp_dataframes , axis = 0 )
494- if tmp_dataframes
495- else pd .DataFrame (columns = chunk .columns )
496- )
480+ with PandasMultiprocessingHelper () as binary_pool :
481+ binary_pool .binary (
482+ replicated ,
483+ [
484+ ("SRC_IP" , operator .add , "SRC_IP" , "_SRC_IP_OFFSET" , []),
485+ ("DST_IP" , operator .add , "DST_IP" , "_DST_IP_OFFSET" , []),
486+ ],
487+ )
488+ replicated [self .CSV_COLUMN_TYPES .keys ()].to_csv (
489+ output_file ,
490+ index = False ,
491+ mode = "w" if first_write else "a" ,
492+ header = first_write ,
493+ )
494+ first_write = False
497495
498496 def _process_single_loop (
499497 self , loop_n : int , global_start : int , loop_length : int
@@ -619,7 +617,7 @@ def _merge_func(self, group: pd.DataFrame) -> pd.DataFrame:
619617 return res_group
620618 return group
621619
622- def _merge_across_loop (self , flows : pd . DataFrame ) -> pd .DataFrame :
620+ def _merge_across_loop (self , flows_file : os . PathLike ) -> pd .DataFrame :
623621 """Merge replicated flows across loops.
624622 Feature description is provided in FlowReplicator docstring.
625623
@@ -636,5 +634,35 @@ def _merge_across_loop(self, flows: pd.DataFrame) -> pd.DataFrame:
636634 pd.DataFrame
637635 Merged flows.
638636 """
637+ if (
638+ psutil .virtual_memory ().available - 1024 ** 3
639+ ) < self ._estimate_memory_from_file (flows_file ):
640+ logging .error ("Not merging accross loops because not enough free memory" )
641+ return
642+
643+ (
644+ pd .read_csv (
645+ flows_file ,
646+ usecols = self .CSV_COLUMN_TYPES .keys (),
647+ dtype = self .CSV_COLUMN_TYPES ,
648+ engine = "pyarrow" ,
649+ )
650+ .groupby (self .FLOW_KEY )
651+ .apply (self ._merge_func )
652+ .to_csv (flows_file , index = False )
653+ )
639654
640- return flows .groupby (self .FLOW_KEY ).apply (self ._merge_func )
655+ def _estimate_memory_from_file (csv_path : str , column_types : dict ):
656+ bytes_per_row = sum (
657+ np .dtype (t ).itemsize
658+ for t in column_types .values ()
659+ if hasattr (t , "itemsize" )
660+ )
661+ bytes_per_row += sum (
662+ 60 for s in column_types if isinstance (s , str )
663+ ) # assume average of 60 bytes per string
664+ rows , _ = Tool (f"wc -l { csv_path } " ).run ()
665+ rows = int (rows .split (" " , 1 )[0 ]) - 1
666+
667+ estimated_memory_bytes = rows * bytes_per_row
668+ return estimated_memory_bytes
0 commit comments