Skip to content

Commit 1e0ae37

Browse files
committed
Parsing code has been modularized, in preparation for next code contribution.
1 parent 7dc6d4e commit 1e0ae37

2 files changed

Lines changed: 189 additions & 134 deletions

File tree

execution_process_metrics_collector/aggregator.py

Lines changed: 6 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@
1919
# along with this program. If not, see <https://www.gnu.org/licenses/>.
2020

2121
import datetime
22-
import json
2322
import logging
24-
import os.path
2523
import pathlib
2624
import sys
2725

@@ -46,13 +44,8 @@
4644
from adjustText import adjust_text # type: ignore[import-untyped]
4745

4846

49-
from .collector import (
50-
REFERENCE_PID_FILENAME,
51-
SAMPLING_PERIOD_FILENAME,
52-
PIDS_FILENAME,
53-
CPU_DETAILS_FILENAME,
54-
COMMAND_JSON_FILENAME_TEMPLATE,
55-
METRICS_CSV_FILENAME_TEMPLATE,
47+
from .parser import (
48+
metrics_parser,
5649
)
5750

5851
logger = logging.getLogger(__name__)
@@ -61,33 +54,6 @@
6154
OTHER_COLOR = "#f5e050"
6255

6356

64-
def process_command(command: "Sequence[str]") -> "str":
65-
basename_command = os.path.basename(command[0])
66-
retlabel = basename_command
67-
if basename_command == "python":
68-
for i_token, token in enumerate(command[1:], 1):
69-
if not token.startswith("-"):
70-
retlabel += "\n" + os.path.basename(token)
71-
if retlabel == "cwltool":
72-
retlabel += " " + command[i_token + 1]
73-
break
74-
elif basename_command == "java":
75-
for token in command[1:]:
76-
if not token.startswith("-"):
77-
retlabel += "\n" + os.path.basename(token)
78-
break
79-
elif basename_command == "docker":
80-
for token in command[1:]:
81-
if token == "stats":
82-
retlabel = "docker stats"
83-
break
84-
elif not token.startswith("-") and token != "run":
85-
retlabel = "docker run\n" + token
86-
break
87-
88-
return retlabel
89-
90-
9157
def draw_tree(
9258
pids: "pd.DataFrame",
9359
pids_tree: "nx.DiGraph[str]",
@@ -501,106 +467,12 @@ def metrics_aggregator(
501467
tdp_in_w: "float",
502468
group_by_process_name: "Optional[str]" = None,
503469
) -> "None":
504-
if not series_dir.is_dir():
505-
logger.error(f"Path {series_dir.as_posix()} is not a directory")
506-
raise Exception()
507-
508-
reference_pid_filename = series_dir / REFERENCE_PID_FILENAME
509-
if not reference_pid_filename.is_file():
510-
logger.error(f"Path {reference_pid_filename.as_posix()} is not a filename")
511-
raise Exception()
512-
513-
with reference_pid_filename.open(mode="r", encoding="utf-8") as rF:
514-
reference_pid = float(rF.readline())
515-
516-
sampling_period_filename = series_dir / SAMPLING_PERIOD_FILENAME
517-
if not sampling_period_filename.is_file():
518-
logger.error(f"Path {sampling_period_filename.as_posix()} is not a filename")
519-
raise Exception()
520-
521-
with sampling_period_filename.open(mode="r", encoding="utf-8") as sF:
522-
sampling_period_seconds = float(sF.readline())
523-
sampling_period_milliseconds = int(round(sampling_period_seconds * 1000.0))
524-
sampling_period_td = pd.Timedelta(sampling_period_milliseconds, "ms")
525-
526-
cpu_details_filename = series_dir / CPU_DETAILS_FILENAME
527-
if not cpu_details_filename.is_file():
528-
logger.error(f"Path {cpu_details_filename.as_posix()} is not a filename")
529-
raise Exception()
530-
531-
with cpu_details_filename.open(mode="r", encoding="utf-8") as cF:
532-
cpu_details = json.load(cF)
533-
534-
# TODO, compute this per CPU
535-
num_cpu_cores = int(cpu_details[0]["cpu cores"])
536-
num_cpu_processors = len(cpu_details[0]["processors"])
537-
factor_cores_processors = float(num_cpu_cores) / float(num_cpu_processors) # noqa: F841
538-
539-
logger.info(
540-
f"Processing directory {series_dir.as_posix()} about pid {reference_pid}"
541-
)
542-
543-
pids_filename = series_dir / PIDS_FILENAME
544-
if not pids_filename.is_file():
545-
logger.error(f"Path {pids_filename.as_posix()} is not a filename")
546-
raise Exception()
547-
548-
# Reading all the pids
549-
pids = pd.read_table(
550-
pids_filename,
551-
na_values=["-"],
552-
dtype={"PID": "Int32", "PPID": "Int32"},
553-
parse_dates=["Time"],
554-
)
555-
# Generating the needed columns to generate a tree structure
556-
pids["node"] = pids.apply(
557-
lambda row: str(row.create_time) + "_" + str(row.PID), axis=1
558-
)
559-
pids["parent"] = pids.apply(
560-
lambda row: str(row.ppid_create_time) + "_" + str(row.PPID)
561-
if not pd.isna(row.PPID)
562-
else None,
563-
axis=1,
470+
pids, num_cpu_cores, sampling_period_seconds = metrics_parser(
471+
series_dir, outputs_dir, group_by_process_name=group_by_process_name
564472
)
565473

566-
# Now, let's read the command lines
567-
main_commands = []
568-
command_labels = []
569-
full_command = []
570-
full_stats = []
571-
subtree_root = []
572-
for index, row in pids.iterrows():
573-
command_json_filename = series_dir / COMMAND_JSON_FILENAME_TEMPLATE.format(
574-
row.PID, row.create_time
575-
)
576-
with command_json_filename.open(mode="r", encoding="utf-8") as cH:
577-
command_json = json.load(cH)
578-
assert isinstance(command_json, list)
579-
full_command.append(command_json)
580-
command = process_command(command_json)
581-
main_commands.append(command)
582-
command_labels.append(command + "\n" + str(index))
583-
subtree_root.append(
584-
command.startswith(group_by_process_name)
585-
if group_by_process_name is not None
586-
else row.PID == reference_pid
587-
)
588-
589-
metrics_csv_filename = series_dir / METRICS_CSV_FILENAME_TEMPLATE.format(
590-
row.PID, row.create_time
591-
)
592-
metrics = pd.read_csv(metrics_csv_filename, parse_dates=["Time"])
593-
594-
# Focus on groups based on the core where it was working
595-
# grouped = metrics.groupby(["core_num"])
596-
# print(metrics.head())
597-
full_stats.append(metrics)
598-
599-
pids["command"] = main_commands
600-
pids["command_label"] = command_labels
601-
pids["full_command"] = full_command
602-
pids["full_stats"] = full_stats
603-
pids["subtree_root"] = subtree_root
474+
sampling_period_milliseconds = int(round(sampling_period_seconds * 1000.0))
475+
sampling_period_td = pd.Timedelta(sampling_period_milliseconds, "ms")
604476

605477
# Compute the graph
606478
pids_tree = nx.from_pandas_edgelist(
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
# SPDX-License-Identifier: GPL-3.0-or-later
5+
# execution-process-metrics-collector, a process tree metrics gatherer.
6+
# Copyright (C) 2025 Barcelona Supercomputing Center, José M. Fernández
7+
#
8+
# This program is free software: you can redistribute it and/or modify
9+
# it under the terms of the GNU General Public License as published by
10+
# the Free Software Foundation, either version 3 of the License, or
11+
# (at your option) any later version.
12+
#
13+
# This program is distributed in the hope that it will be useful, but
14+
# WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
# General Public License for more details.
17+
#
18+
# You should have received a copy of the GNU General Public License
19+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
21+
import json
22+
import logging
23+
import os.path
24+
25+
from typing import (
26+
TYPE_CHECKING,
27+
)
28+
29+
if TYPE_CHECKING:
30+
from typing import (
31+
Optional,
32+
Sequence,
33+
Tuple,
34+
)
35+
36+
import pathlib
37+
38+
import pandas as pd
39+
40+
from .collector import (
41+
REFERENCE_PID_FILENAME,
42+
SAMPLING_PERIOD_FILENAME,
43+
PIDS_FILENAME,
44+
CPU_DETAILS_FILENAME,
45+
COMMAND_JSON_FILENAME_TEMPLATE,
46+
METRICS_CSV_FILENAME_TEMPLATE,
47+
)
48+
49+
logger = logging.getLogger(__name__)
50+
51+
52+
def process_command(command: "Sequence[str]") -> "str":
53+
basename_command = os.path.basename(command[0])
54+
retlabel = basename_command
55+
if basename_command == "python":
56+
for i_token, token in enumerate(command[1:], 1):
57+
if not token.startswith("-"):
58+
retlabel += "\n" + os.path.basename(token)
59+
if retlabel == "cwltool":
60+
retlabel += " " + command[i_token + 1]
61+
break
62+
elif basename_command == "java":
63+
for token in command[1:]:
64+
if not token.startswith("-"):
65+
retlabel += "\n" + os.path.basename(token)
66+
break
67+
elif basename_command == "docker":
68+
for token in command[1:]:
69+
if token == "stats":
70+
retlabel = "docker stats"
71+
break
72+
elif not token.startswith("-") and token != "run":
73+
retlabel = "docker run\n" + token
74+
break
75+
76+
return retlabel
77+
78+
79+
def metrics_parser(
80+
series_dir: "pathlib.Path",
81+
outputs_dir: "pathlib.Path",
82+
group_by_process_name: "Optional[str]" = None,
83+
) -> "Tuple[pd.DataFrame, int, float]":
84+
if not series_dir.is_dir():
85+
logger.error(f"Path {series_dir.as_posix()} is not a directory")
86+
raise Exception()
87+
88+
reference_pid_filename = series_dir / REFERENCE_PID_FILENAME
89+
if not reference_pid_filename.is_file():
90+
logger.error(f"Path {reference_pid_filename.as_posix()} is not a filename")
91+
raise Exception()
92+
93+
with reference_pid_filename.open(mode="r", encoding="utf-8") as rF:
94+
reference_pid = float(rF.readline())
95+
96+
sampling_period_filename = series_dir / SAMPLING_PERIOD_FILENAME
97+
if not sampling_period_filename.is_file():
98+
logger.error(f"Path {sampling_period_filename.as_posix()} is not a filename")
99+
raise Exception()
100+
101+
with sampling_period_filename.open(mode="r", encoding="utf-8") as sF:
102+
sampling_period_seconds = float(sF.readline())
103+
104+
cpu_details_filename = series_dir / CPU_DETAILS_FILENAME
105+
if not cpu_details_filename.is_file():
106+
logger.error(f"Path {cpu_details_filename.as_posix()} is not a filename")
107+
raise Exception()
108+
109+
with cpu_details_filename.open(mode="r", encoding="utf-8") as cF:
110+
cpu_details = json.load(cF)
111+
112+
# TODO, compute this per CPU
113+
num_cpu_cores = int(cpu_details[0]["cpu cores"])
114+
num_cpu_processors = len(cpu_details[0]["processors"])
115+
factor_cores_processors = float(num_cpu_cores) / float(num_cpu_processors) # noqa: F841
116+
117+
logger.info(
118+
f"Processing directory {series_dir.as_posix()} about pid {reference_pid}"
119+
)
120+
121+
pids_filename = series_dir / PIDS_FILENAME
122+
if not pids_filename.is_file():
123+
logger.error(f"Path {pids_filename.as_posix()} is not a filename")
124+
raise Exception()
125+
126+
# Reading all the pids
127+
pids = pd.read_table(
128+
pids_filename,
129+
na_values=["-"],
130+
dtype={"PID": "Int32", "PPID": "Int32"},
131+
parse_dates=["Time"],
132+
)
133+
# Generating the needed columns to generate a tree structure
134+
pids["node"] = pids.apply(
135+
lambda row: str(row.create_time) + "_" + str(row.PID), axis=1
136+
)
137+
pids["parent"] = pids.apply(
138+
lambda row: str(row.ppid_create_time) + "_" + str(row.PPID)
139+
if not pd.isna(row.PPID)
140+
else None,
141+
axis=1,
142+
)
143+
144+
# Now, let's read the command lines
145+
main_commands = []
146+
command_labels = []
147+
full_command = []
148+
full_stats = []
149+
subtree_root = []
150+
for index, row in pids.iterrows():
151+
command_json_filename = series_dir / COMMAND_JSON_FILENAME_TEMPLATE.format(
152+
row.PID, row.create_time
153+
)
154+
with command_json_filename.open(mode="r", encoding="utf-8") as cH:
155+
command_json = json.load(cH)
156+
assert isinstance(command_json, list)
157+
full_command.append(command_json)
158+
command = process_command(command_json)
159+
main_commands.append(command)
160+
command_labels.append(command + "\n" + str(index))
161+
subtree_root.append(
162+
command.startswith(group_by_process_name)
163+
if group_by_process_name is not None
164+
else row.PID == reference_pid
165+
)
166+
167+
metrics_csv_filename = series_dir / METRICS_CSV_FILENAME_TEMPLATE.format(
168+
row.PID, row.create_time
169+
)
170+
metrics = pd.read_csv(metrics_csv_filename, parse_dates=["Time"])
171+
172+
# Focus on groups based on the core where it was working
173+
# grouped = metrics.groupby(["core_num"])
174+
# print(metrics.head())
175+
full_stats.append(metrics)
176+
177+
pids["command"] = main_commands
178+
pids["command_label"] = command_labels
179+
pids["full_command"] = full_command
180+
pids["full_stats"] = full_stats
181+
pids["subtree_root"] = subtree_root
182+
183+
return pids, num_cpu_cores, sampling_period_seconds

0 commit comments

Comments
 (0)