Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript Web module #202

Open
wants to merge 75 commits into
base: devel
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
7a9db29
web collector init
Mar 1, 2024
bda6bea
perun/collect/__init__.py
Mar 1, 2024
8df374f
build and register directory as package
Mar 1, 2024
7379ce0
comment module for tests
Mar 1, 2024
009a356
make function command
Mar 2, 2024
82e70a6
rewrite run script, before, collect, after methods
Mar 8, 2024
35f4feb
parser
Mar 10, 2024
f567fbc
port option
Mar 11, 2024
c0395ac
web collector init
Mar 1, 2024
d427fd6
perun/collect/__init__.py
Mar 1, 2024
b493b01
build and register directory as package
Mar 1, 2024
2b81182
comment module for tests
Mar 1, 2024
012c90c
make function command
Mar 2, 2024
fcba482
rewrite run script, before, collect, after methods
Mar 8, 2024
40771ed
parser
Mar 10, 2024
b03144d
port option
Mar 11, 2024
07a5100
Merge branch 'ts-module' of github.com:TomValent/perun into ts-module
Mar 12, 2024
c1b506f
uid
Mar 12, 2024
48eac2b
uid
Mar 12, 2024
152a087
web collector init
Mar 1, 2024
5475d13
build and register directory as package
Mar 1, 2024
6264f46
comment module for tests
Mar 1, 2024
18904bc
make function command
Mar 2, 2024
2ced48b
rewrite run script, before, collect, after methods
Mar 8, 2024
f456c8f
parser
Mar 10, 2024
582614e
port option
Mar 11, 2024
88b07e1
uid
Mar 12, 2024
723d31d
uid
Mar 12, 2024
1b5409c
rebase done
Mar 12, 2024
5206e86
Merge branch 'ts-module' of github.com:TomValent/perun into ts-module
Mar 12, 2024
847290f
edit parameters, run process
Mar 15, 2024
ad17998
metrics file move & backup
Mar 16, 2024
6b0ebcb
timestamp to filename
Mar 16, 2024
2710da5
call graph
Mar 16, 2024
1b458fd
cg to fungtion
Mar 16, 2024
27eabd0
call graph second option
Mar 16, 2024
c256e3f
detect ctrl c
Mar 16, 2024
6d57b95
collector done, init view command for web
Mar 22, 2024
e41fad9
line graphs
Mar 24, 2024
ccfbef2
pairplot
Mar 27, 2024
79b6337
heatmap
Mar 29, 2024
30a595d
annotations
Mar 29, 2024
3188fc4
Fix: code style
Mar 29, 2024
a76963e
default values
Mar 31, 2024
ff02c38
Log info for call graph (its ts only)
Mar 31, 2024
6c44db2
add logs, return types
Apr 2, 2024
9d7d385
Fix: saving pairplot
Apr 2, 2024
5daf36c
minor fixes + new metrics
Apr 13, 2024
b7fe86d
timestamp fix
Apr 14, 2024
b75029a
wip
Apr 14, 2024
dbfb3a7
dynamical labels for pairplot
Apr 14, 2024
c26df40
fix grouping, fill empty tiles in heatmap
Apr 14, 2024
22d0604
uncomment graphs
Apr 14, 2024
bfd5ef7
uncomment graphs
Apr 14, 2024
3b1b52a
remove useless before phase
Apr 15, 2024
746d31e
group by amount
Apr 15, 2024
d548368
pairplot 9x9
Apr 17, 2024
f69e30c
route heatmap
Apr 17, 2024
160a992
colorful pairplot
Apr 18, 2024
f5a6818
Done
Apr 18, 2024
3e629e5
Done
Apr 18, 2024
03badeb
Finally done
Apr 18, 2024
aa3a78b
Save plots as SVG, remove required parameter
Apr 21, 2024
54adf9e
annotations
Apr 26, 2024
7438985
Annotations
Apr 26, 2024
4390dc7
edit help for perun show command
Apr 26, 2024
1b62c2b
help, default value of option
Apr 26, 2024
3c6f35a
Fix: help messages
TomValent Apr 26, 2024
cbef2e3
pairplot for different routes
Apr 27, 2024
6e8ebb6
Fix: typo
Apr 27, 2024
8aee1f1
Fix: default aggreg value
Apr 27, 2024
5334768
Fix: label for latency
Apr 27, 2024
36ecf42
Fix: legend
Apr 27, 2024
47b5731
fix grouping
Apr 28, 2024
246a482
Merge branch 'devel' into ts-module
Jun 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions perun/collect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def lazy_get_cli_commands() -> list[Callable[..., Any]]:
import perun.collect.memory.run as memory_run
import perun.collect.time.run as time_run
import perun.collect.trace.run as trace_run
import perun.collect.web.run as web_run

return [
bounds_run.bounds,
Expand All @@ -60,4 +61,5 @@ def lazy_get_cli_commands() -> list[Callable[..., Any]]:
memory_run.memory,
time_run.time,
trace_run.trace,
web_run.web,
]
3 changes: 2 additions & 1 deletion perun/collect/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ subdir('complexity')
subdir('kperf')
subdir('memory')
subdir('time')
subdir('trace')
subdir('trace')
subdir('web')
22 changes: 22 additions & 0 deletions perun/collect/web/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Trace collector collects running times of C/C++ functions.
The collected data are suitable for further postprocessing using
the regression analysis and visualization by scatter plots.
"""

COLLECTOR_TYPE = "web"
COLLECTOR_DEFAULT_UNITS = {
"page_requests": "",
"error_count": "",
"error_code_count": "",
"error_message_count": "",
"memory_usage_counter": "MB",
"request_latency_summary": "ms",
"throughput": "requests per second",
"user_cpu_usage": "s",
"system_cpu_usage": "s",
"user_cpu_time": "s",
"system_cpu_time": "s",
"fs_read": "",
"fs_write": "",
"voluntary_context_switches": "",
}
12 changes: 12 additions & 0 deletions perun/collect/web/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
perun_collect_web_dir = perun_collect_dir / 'web'

perun_collect_web_files = files(
'__init__.py',
'run.py',
'parser.py'
)

py3.install_sources(
perun_collect_web_files,
subdir: perun_collect_web_dir,
)
29 changes: 29 additions & 0 deletions perun/collect/web/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import datetime
import json
import string


class Parser:
""" Parser for OpenTelemetry logs"""

def parse_metric(self, line: str) -> tuple[str, float, str, datetime.date]:
"""Parse metric from log for profile"""

data = json.loads(line)
current = data['aggregator']['_current']

timestamp_seconds = data['aggregator']['_lastUpdateTime'][0]
timestamp = datetime.datetime.fromtimestamp(timestamp_seconds)

if not isinstance(current, int) and not isinstance(current, float):
value = current.get("sum")
else:
value = current

return data["descriptor"]["name"], value, data["labels"].get("route", ""), timestamp.isoformat()

def parse_trace(self, line: str) -> None:
"""Parse trace from log for profile"""

data_dict = json.loads(line)
pass
219 changes: 219 additions & 0 deletions perun/collect/web/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
"""Wrapper for web collector, which collects profiling data from
TypeScript/JavaScript web applications

Specifies before, collect, after and teardown functions to perform the initialization,
collection and postprocessing of collection data.
"""

import os
import time
import click
import psutil
import requests
import subprocess
import perun.logic.runner as runner

from time import sleep
from typing import Any, List
from datetime import datetime
from perun.utils import log as perun_log
from perun.utils.external import processes
from perun.collect.web.parser import Parser
from perun.utils.structs import CollectStatus


def collect(**kwargs) -> tuple[CollectStatus, str, dict[str, Any]]:
"""Assembles the engine collect program according to input parameters and collection strategy.
Runs the created collection program and the profiled command.

:param kwargs: dictionary containing the configuration and probe settings for the collector
:returns: tuple (CollectStatus enum code,
string as a status message, mainly for error states,
dict of kwargs (possibly with some new values))
"""

project_path = kwargs["proj"]
express_file = kwargs["express"]
timeout = kwargs["timeout"]
kwargs["prof_port"] = 9000
project_port = kwargs["port"]
profiler_port = kwargs["prof_port"]
profiler_path = kwargs["otp"]
command = "start"

if project_path == "":
project_path = os.getcwd() + "/"
print(project_path)

with processes.nonblocking_subprocess(
"yarn " + command + " --silent " + "--path " + project_path + express_file,
{"cwd": profiler_path}
) as prof_process:
perun_log.minor_info("Warm up phase...")
server_url = "http://localhost:" + str(project_port)
if wait_until_server_starts(server_url):
perun_log.minor_info("Collect phase...")

sleep(timeout)
kill_processes([project_port, profiler_port])

perun_log.minor_info("Collecting finished...")
else:
perun_log.error("Could not start profiler or target project...")

return CollectStatus.OK, "", dict(kwargs)


def kill_processes(ports: List[int]) -> None:
"""Terminate processes listening on specified ports after timeout.

:param ports: List of integers representing ports on which processes are running.
:return: None
"""

for port in ports:
for conn in psutil.net_connections():
if conn.laddr.port == port and conn.status == 'LISTEN' and port != 0:
subprocess.run(["kill", "-9", str(conn.pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


def wait_until_server_starts(url: str, max_attempts: int = 20, wait_time: float = 0.5) -> bool:
"""Wait until a server at the specified URL starts responding.

:param url: The URL of the server to wait for.
:param max_attempts: The maximum number of attempts to make before giving up. Default is 10.
:param wait_time: The time to wait (in seconds) between attempts. Default is 1.
:return: True if the server started within the specified attempts, False otherwise.
"""

for _ in range(max_attempts):
try:
response = requests.get(url)
if response.status_code == 200:
return True
except requests.ConnectionError:
pass
sleep(wait_time)
return False


def after(**kwargs) -> tuple[CollectStatus, str, dict[str, dict[str, dict[str, list[dict[str, Any]] | float]]]]:
"""Parses the trace collector output and transforms it into profile resources

:param kwargs: the configuration settings for the collector
:returns: tuple (CollectStatus enum code,
string as a status message, mainly for error states,
dict of kwargs (possibly with some new values))
"""
perun_log.minor_info("Post-processing phase... ")

otp_dir = kwargs["otp"]
metrics = os.path.join(otp_dir, "data/metrics/metrics.log")
done_folder = os.path.join(otp_dir, "data/metrics/done")
timestamp = time.strftime("%Y%m%d%H%M%S")
new_filename = f"{timestamp}_metrics.log"
os.makedirs(done_folder, exist_ok=True)

metrics_data = []
parser = Parser()

try:
with open(metrics, 'r') as metrics_file:
for line in metrics_file:
parsed_line = parser.parse_metric(line)
metrics_data.append(parsed_line)
os.rename(metrics, os.path.join(done_folder, new_filename))
except FileNotFoundError as e:
perun_log.error(f"File {metrics} was not created or cannot be opened")
exit(1)

perun_log.minor_info("Data processing finished.")

return (
CollectStatus.OK,
"",
{
"profile": {
"global": {
"timestamp": datetime.now().timestamp(),
"resources": [
{
"amount": value,
"uid": route,
"type": key,
"timestamp": timestamp,
}
for (key, value, route, timestamp) in metrics_data
]
}
}
}
)


def teardown(**kwargs) -> tuple[CollectStatus, str, dict[str, Any]]:
"""Perform a cleanup of all the collection resources that need it, i.e. files, locks,
processes, kernel modules etc.

:param kwargs: the configuration settings for the collector
:returns: tuple (CollectStatus enum code,
string as a status message, mainly for error states,
dict of kwargs (possibly with some new values))
"""
perun_log.minor_info("Teardown phase...")

kill_processes([kwargs.get("port", 0), kwargs.get("prof_port", 0)])

return CollectStatus.OK, "", dict(kwargs)


@click.command()
@click.option(
"--otp",
"-o",
type=str,
required=True,
default="",
help="Path to the OpenTelemetry profiler script."
)
@click.option(
"--proj",
"-p",
type=str,
required=False,
default="",
help="Path to the project to be profiled.\n"
"If it's not given actual directory is used."
)
@click.option(
"--express",
"-e",
type=str,
required=True,
help="Path to file in your project containing express() app with export.\n"
"The export must be default or named 'app'\n"
"Examples: 'src/app', 'src/app.ts'"
)
@click.option(
"--port",
type=int,
required=True,
help="Port on which project run."
)
@click.option(
"--timeout",
"-t",
type=int,
required=False,
default=60,
help="Timeout for the runtime of profiling in seconds.\n"
"Default is seconds."
)
@click.pass_context
def web(ctx: click.Context, **kwargs: Any) -> None:
"""Generates a `web` performance profile, capturing various metrics including response latency (ms), request count,
error occurrences, file system activity, memory usage, throughput, user CPU usage, system CPU usage,
user CPU time, system CPU time, etc.
"""

runner.run_collector_from_cli_context(ctx, "web", kwargs)
2 changes: 2 additions & 0 deletions perun/view/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ def lazy_get_cli_commands() -> list[Callable[..., Any]]:
import perun.view.flow.run as flow_run
import perun.view.scatter.run as scatter_run
import perun.view.tableof.run as tableof_run
import perun.view.web.run as web_run

return [
bars_run.bars,
flamegraph_run.flamegraph,
flow_run.flow,
scatter_run.scatter,
tableof_run.tableof,
web_run.web,
]
3 changes: 2 additions & 1 deletion perun/view/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ subdir('bars')
subdir('flamegraph')
subdir('flow')
subdir('scatter')
subdir('tableof')
subdir('tableof')
subdir('web')
5 changes: 5 additions & 0 deletions perun/view/web/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""`Web` interprets the data in many different ways. Web collector collects multiple different metrics
with different units. `Web` view module visualize different metrics in different ways
"""

SUPPORTED_PROFILES = ["web"]
12 changes: 12 additions & 0 deletions perun/view/web/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
perun_view_web_dir = perun_view_dir / 'web'

perun_view_web_files = files(
'__init__.py',
'run.py',
'unsupported_metric_exception.py'
)

py3.install_sources(
perun_view_web_files,
subdir: perun_view_web_dir
)
Loading
Loading