Skip to content

Commit ee5fcba

Browse files
committed
ft-analyzer statistical_model.py make initial flows read in chunks
1 parent efde5f9 commit ee5fcba

1 file changed

Lines changed: 84 additions & 66 deletions

File tree

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

Lines changed: 84 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88

99
import atexit
1010
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
11-
import gc
1211
import ipaddress
1312
import logging
1413
import operator
1514
from os import PathLike
15+
import os
1616
from pathlib import Path
1717
import shutil
1818
import tempfile
@@ -126,6 +126,7 @@ class StatisticalModel:
126126
"EXPORT_TIME": "first",
127127
"SEQ_NUMBER": "first",
128128
"MSG_LENGTH": "first",
129+
"FLOW_COUNT": ("START_TIME", "count"),
129130
}
130131

131132
def __init__(
@@ -167,68 +168,32 @@ def __init__(
167168

168169
self._log_dir = log_dir
169170

170-
try:
171-
logging.getLogger().debug("reading file with flows=%s", flows)
172-
# ports could be empty in flows with protocol like ICMP
173-
self._flows_path = flows
174-
flows = pd.read_csv(flows, engine="pyarrow", dtype=self.CSV_COLUMN_TYPES)
175-
flows["SRC_PORT"] = flows["SRC_PORT"].fillna(0)
176-
flows["DST_PORT"] = flows["DST_PORT"].fillna(0)
177-
self._flows: pd.DataFrame = flows.astype(self.CSV_COLUMN_TYPES)
178-
179-
if isinstance(reference, str):
180-
self._ref = None
181-
self._ref_path = reference
182-
else:
183-
self._ref = reference
184-
except Exception as err:
185-
raise SMException("Unable to read file with flows.") from err
186-
187-
self._zero_icmp_ports(self._flows)
188-
189-
if stats.start_time > 0:
190-
# filter out flows that start before the start time with 500 ms tolerance
191-
self._flows = self._flows[
192-
self._flows["START_TIME"] >= stats.start_time - 500
193-
]
194-
195-
# if stats.end_time > 0:
196-
# # filter out flows that start before the end time
197-
# self._flows = self._flows[self._flows["START_TIME"] <= stats.end_time]
198-
199-
self._filter_multicast()
200-
201-
if merge:
202-
self._merge_flows(biflows_ts_correction)
203-
204-
# write dataframes back to file and read later when needed
205-
if not (hasattr(self, "_flows_path") and self._flows_path):
206-
self._flows_path = tempfile.NamedTemporaryFile(
207-
delete=False, suffix=".csv"
208-
).name
209-
self._flows.to_csv(self._flows_path, index=False)
210-
del self._flows
211-
gc.collect()
212-
213-
_TEMP_FILES.append(self._flows_path)
214-
215-
if not (hasattr(self, "_ref_path") and self._ref_path):
216-
self._ref_path = tempfile.NamedTemporaryFile(
217-
delete=False, suffix=".csv"
171+
if isinstance(reference, str):
172+
self._ref_path = reference
173+
else:
174+
self._ref_path = self._ref_path = tempfile.NamedTemporaryFile(
175+
delete=False, prefix="tmp_ref", suffix=".csv"
218176
).name
219-
if self._ref:
220-
self._ref.to_csv(self._ref_path, index=False)
221-
del self._ref
222-
gc.collect()
223-
224-
_TEMP_FILES.append(self._ref_path)
177+
reference.to_csv(
178+
self._ref_path,
179+
index=False,
180+
)
181+
reference = pd.DataFrame(columns=self.CSV_COLUMN_TYPES.keys())
225182

226183
self._generator_stats: GeneratorStats = stats
227184
self._flows_ip_addresses_converted = False
228185
self._ref_ip_addresses_converted = isinstance(reference, pd.DataFrame)
229186
self._stat_counter = use_statistical_counter
230187
self._inactive_timeout = inactive_timeout
231188

189+
try:
190+
self._flows_path = self._init_flows(flows)
191+
except Exception as err:
192+
raise SMException("Unable to read file with flows.") from err
193+
194+
if merge:
195+
self._merge_flows(biflows_ts_correction)
196+
232197
if use_statistical_counter:
233198
# statistic objects
234199
self._executor = (
@@ -256,6 +221,52 @@ def __init__(
256221
self._future_ref = None
257222
self._future_sim = None
258223

224+
def _init_flows(self, path: os.PathLike):
225+
"""initial read of flows.csv in chunks
226+
replaces faulty values and filters out some flows
227+
228+
Args:
229+
path (os.PathLike): _description_
230+
231+
Returns:
232+
_type_: _description_
233+
"""
234+
out_file = tempfile.NamedTemporaryFile(
235+
delete=False, prefix="tmp_flows", suffix=".csv"
236+
).name
237+
first_write = True
238+
logging.getLogger().debug("reading file with flows=%s", path)
239+
# ports could be empty in flows with protocol like ICMP
240+
for chunk in pd.read_csv(path, dtype=self.CSV_COLUMN_TYPES, chunksize=100_000):
241+
chunk["SRC_PORT"] = chunk["SRC_PORT"].fillna(0)
242+
chunk["DST_PORT"] = chunk["DST_PORT"].fillna(0)
243+
chunk = chunk.astype(self.CSV_COLUMN_TYPES)
244+
245+
self._zero_icmp_ports(chunk)
246+
247+
if self._generator_stats.start_time > 0:
248+
# filter out flows that start before the start time with 500 ms tolerance
249+
chunk = chunk[
250+
chunk["START_TIME"] >= self._generator_stats.start_time - 500
251+
]
252+
253+
# if stats.end_time > 0:
254+
# # filter out flows that start before the end time
255+
# chunk = chunk[chunk["START_TIME"] <= stats.end_time]
256+
257+
self._filter_multicast(chunk)
258+
259+
chunk.to_csv(
260+
out_file,
261+
index=False,
262+
mode="w" if first_write else "a",
263+
header=first_write,
264+
)
265+
first_write = False
266+
267+
os.remove(path)
268+
return out_file
269+
259270
@staticmethod
260271
def _run_sim(
261272
host_stats: GeneratorStats,
@@ -417,12 +428,12 @@ def validate(
417428

418429
return report
419430

420-
def _filter_multicast(self):
431+
def _filter_multicast(self, flows: pd.DataFrame):
421432
# ipv4
422-
self._flows = self._flows[self._flows["DST_IP"] != "255.255.255.255"]
433+
flows = flows[flows["DST_IP"] != "255.255.255.255"]
423434

424435
# ipv6
425-
self._flows = self._flows[~self._flows["DST_IP"].str.startswith("ff02::")]
436+
flows = flows[flows["DST_IP"].str.startswith("ff02::")]
426437

427438
def _merge_flows(self, biflows_ts_correction: bool) -> None:
428439
"""
@@ -437,18 +448,25 @@ def _merge_flows(self, biflows_ts_correction: bool) -> None:
437448
Timestamps in reverse direction flows is corrected.
438449
"""
439450

440-
assert len(self._ref.index) == self._ref.groupby(self.FLOW_KEY).ngroups, (
451+
ref_df = self._load_ref_df
452+
assert len(ref_df.index) == ref_df.groupby(self.FLOW_KEY).ngroups, (
441453
"Cannot merge flows, duplicated key."
442454
)
455+
del ref_df
456+
457+
# wait till self._flows_path is not used anymore
458+
while self._future_sim.running:
459+
time.sleep(1)
443460

444-
flows = self._flows.groupby(self.FLOW_KEY).aggregate(self.AGGREGATE_FLOWS)
445-
flows["FLOW_COUNT"] = self._flows.groupby(self.FLOW_KEY).size()
446-
self._flows = flows.reset_index()
461+
flows_df = self._load_flows_df()
462+
flows = flows_df.groupby(self.FLOW_KEY).aggregate(self.AGGREGATE_FLOWS)
463+
flows["FLOW_COUNT"] = flows_df.groupby(self.FLOW_KEY).size()
464+
flows_df = flows.reset_index()
447465

448466
if biflows_ts_correction:
449467
# correct timestamps in reverse direction of flows originating from biflows
450468
# using direction invariant flow key
451-
flows = self._flows
469+
flows = flows_df
452470

453471
swap_cond = flows["SRC_IP"] > flows["DST_IP"]
454472
flows["INV_SRC_IP"] = np.where(swap_cond, flows["DST_IP"], flows["SRC_IP"])
@@ -464,9 +482,9 @@ def _merge_flows(self, biflows_ts_correction: bool) -> None:
464482
flows["START_TIME"] = grouped["START_TIME"].transform("min")
465483
flows["END_TIME"] = grouped["END_TIME"].transform("max")
466484

467-
self._flows = flows.loc[
468-
:, list(self.CSV_COLUMN_TYPES.keys()) + ["FLOW_COUNT"]
469-
]
485+
flows_df = flows.loc[:, list(self.CSV_COLUMN_TYPES.keys()) + ["FLOW_COUNT"]]
486+
487+
flows_df.to_csv(self._ref_path, index=False)
470488

471489
def _convert_ip_addresses(self) -> None:
472490
"""Convert str ip addresses to objects (ipaddress library) in DataFrames."""

0 commit comments

Comments
 (0)