Skip to content

Commit dc15b53

Browse files
committed
ft-analyzer replicator write immediately to file instead of collecting dataframes and move timestamp correction to replicator
1 parent 84a10a5 commit dc15b53

3 files changed

Lines changed: 66 additions & 44 deletions

File tree

tools/ft-analyzer/ftanalyzer/models/statistical_model.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,7 @@ def __init__(
177177
self._flows: pd.DataFrame = flows.astype(self.CSV_COLUMN_TYPES)
178178

179179
if isinstance(reference, str):
180-
logging.getLogger().debug("reading file with references=%s", reference)
181-
self._ref = pd.read_csv(
182-
reference, engine="pyarrow", dtype=self.CSV_COLUMN_TYPES
183-
)
180+
self._ref = None
184181
self._ref_path = reference
185182
else:
186183
self._ref = reference
@@ -190,9 +187,6 @@ def __init__(
190187
self._zero_icmp_ports(self._flows)
191188

192189
if stats.start_time > 0:
193-
self._ref["START_TIME"] = self._ref["START_TIME"] + stats.start_time
194-
self._ref["END_TIME"] = self._ref["END_TIME"] + stats.start_time
195-
196190
# filter out flows that start before the start time with 500 ms tolerance
197191
self._flows = self._flows[
198192
self._flows["START_TIME"] >= stats.start_time - 500

tools/ft-analyzer/ftanalyzer/replicator/flow_replicator.py

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@
1414
import ipaddress
1515
import logging
1616
import operator
17+
import os
1718
from pathlib import Path
1819
import re
1920
from dataclasses import dataclass
2021
import tempfile
2122
import time
2223
from typing import Any, Iterable, List, Optional, Union
24+
from lbr_testsuite.executable import Tool
2325

2426
import numpy as np
2527
import pandas as pd
28+
import psutil
2629
from ftanalyzer.common.pandas_multiprocessing import PandasMultiprocessingHelper
2730
from 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

tools/ft-orchestration/src/collector/jq.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ def save_csv(self, csv_file: str):
297297
kwargs = conn.connect_kwargs
298298

299299
if "key_filename" in kwargs:
300-
key_filename = kwargs["key_filename"]
300+
key_filename = kwargs["key_filename"][0]
301301
ssh_cmd = (
302302
f"ssh -i {key_filename} {user}@{host} '{self._cmd_csv}' >> {csv_file}"
303303
)

0 commit comments

Comments
 (0)