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

Add gui #246

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions Dockerfile.gui
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM debian:bookworm-slim

RUN apt update
RUN apt install -y r-base r-base-dev pandoc curl libcurl4-openssl-dev libssl-dev
RUN apt install -y python3 python3-flask
RUN Rscript -e "install.packages('rmarkdown')"
RUN Rscript -e "install.packages('sqldf')"
RUN Rscript -e "install.packages('mixtools')"
RUN Rscript -e "install.packages('zeallot')"

COPY ./src/analysis/common.R /reports/common.R
COPY ./src/analysis/measure_marginal_single.Rmd /reports/measure_marginal_single.Rmd
COPY ./src/analysis/final_estimation.Rmd /reports/final_estimation.Rmd
COPY ./src/analysis/measure_arguments_single.Rmd /reports/measure_arguments_single.Rmd

COPY ./src/program_generator/data/current_gas_cost.csv /reports/current_gas_cost.csv

COPY ./src/gui /gui/
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,16 @@ To render `final_estimate` report provide your params and an output file and exe
```shell
docker run -it -v /your/path/to/data:/data --rm imapp-pl/gas-cost-estimator/reports:4.0 Rscript -e "rmarkdown::render('/reports/final_estimation.Rmd', params = list(estimate_files='besu_marginal_estimated_cost.csv, erigon_marginal_estimated_cost.csv, ethereumjs_marginal_estimated_cost.csv, geth_marginal_estimated_cost.csv, nethermind_marginal_estimated_cost.csv, revm_marginal_estimated_cost.csv', current_gas_cost='current_gas_cost.csv'), output_file = '/data/final_estimation.html')"
```

## GUI - reports
If you have docker installed on your machine. you can use a web app to generate reports.

To build the app, run:
```shell
./build_gui.sh
```
The script build a docker image with the app. Run it:
```shell
./run_gui.sh path_to_directory_with_programs_and_results
```
This starts a Python Flask app at http://127.0.0.1:5000
1 change: 1 addition & 0 deletions build_gui.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docker build . -f Dockerfile.gui -t imapp-pl/gas-cost-estimator/gui:4.0
14 changes: 14 additions & 0 deletions run_gui.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash

# Check if an argument was provided
if [ $# -eq 0 ]; then
echo "No argument provided. Please provide an aboslute directory path to your data."
exit 1
fi

# if the argument is a directory, run the GUI
if [ -d "$1" ]; then
docker run -it -p 5000:5000 -v "$1:/data" imapp-pl/gas-cost-estimator/gui:4.0 python3 /gui/web_api.py "$1"
else
echo "The provided argument is not a directory: $1"
fi
32 changes: 32 additions & 0 deletions src/gui/templates/final_report_step_1.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Step 2</title>
</head>
<body>
<div id="loadingMessage" style="display:none;">GENERATING REPORT...</div>
<div id="generateForm">
<h3>Select files</h3>
<form method="POST" action="/FINAL">
<select id="paths" name="paths" multiple>
{% for path in paths %}
<option value="{{ path }}">
{{ path }}
</option>
{% endfor %}
</select>
<button type="submit" id="generateBtn">Generate report</button>
</form>
</div>
<script>
document.getElementById('generateBtn').addEventListener('click', function() {
// Show the loading message
const loadingMessage = document.getElementById('loadingMessage');
loadingMessage.style.display = 'block';
const generateForm = document.getElementById('generateForm');
generateForm.style.display = 'none';
});
</script>
</body>
</html>
13 changes: 13 additions & 0 deletions src/gui/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Select raport type</title>
</head>
<body>
<a href='/MARGINAL'><button>Generate measure marginal report</button></a></br>
<a href='/ARGUMENTS'><button>Generate measure arguments report</button></a></br>
<a href='/FINAL'><button>Generate final report</button></a></br>
</form>
</body>
</html>
23 changes: 23 additions & 0 deletions src/gui/templates/report_step1.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Select raport type and EVM implementation</title>
</head>
<body>
<form method="POST" action="/{{ report_type }}">
<input type="hidden" name="step" value="1">
<label for="env">Select EVM implementation:</label>
<select id="env" name="env">
<option value="geth">geth</option>
<option value="erigon">erigon</option>
<option value="ethereumjs">ethereumjs</option>
<option value="netheremind">netheremind</option>
<option value="besu">besu</option>
<option value="revm">revm</option>
</select>
<br><br>
<input type="submit" value="Next">
</form>
</body>
</html>
22 changes: 22 additions & 0 deletions src/gui/templates/report_step2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Step 2</title>
</head>
<body>
<h3>Select files with bytecodes</h3>
<form method="POST" action="/{{ report_type }}">
<input type="hidden" name="step" value="2">
<select id="selected_bytecodes_paths" name="selected_bytecodes_paths" multiple>
{% for path in bytecodes_paths %}
<option value="{{ path }}">
{{ path }}
</option>
{% endfor %}
</select>
<input type="hidden" name="env" value="{{ env }}">
<button type="submit">Next</button>
</form>
</body>
</html>
37 changes: 37 additions & 0 deletions src/gui/templates/report_step3.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Step 3</title>
</head>
<body>
<div id="loadingMessage" style="display:none;">GENERATING REPORT...</div>
<div id="generateForm">
<h3>Select files with benchmark results</h3>
<form method="POST" action="/{{ report_type }}">
<input type="hidden" name="step" value="3">
<select id="selected_results_paths" name="selected_results_paths" multiple>
{% for path in results_paths %}
<option value="{{ path }}">
{{ path }}
</option>
{% endfor %}
</select>
{% for path in selected_bytecodes_paths %}
<input type="hidden" name="selected_bytecodes_paths" value="{{ path }}">
{% endfor %}
<input type="hidden" name="env" value="{{ env }}">
<button type="submit" id="generateBtn">Generate raport</button>
</form>
</div>
<script>
document.getElementById('generateBtn').addEventListener('click', function() {
// Show the loading message
const loadingMessage = document.getElementById('loadingMessage');
loadingMessage.style.display = 'block';
const generateForm = document.getElementById('generateForm');
generateForm.style.display = 'none';
});
</script>
</body>
</html>
12 changes: 12 additions & 0 deletions src/gui/templates/results_error.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Summary</title>
</head>
<body>
<h4>Error when generating report</h4>
{{error_message}}
<a href="/">Start Over</a>
</body>
</html>
Empty file.
11 changes: 11 additions & 0 deletions src/gui/templates/results_success.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Summary</title>
</head>
<body>
<h4>Raport available <a href="file://localhost/{{ output_file_name }}">{{ output_file_name }}</a>.</h4>
<a href="/">Start Again</a>
</body>
</html>
118 changes: 118 additions & 0 deletions src/gui/web_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from flask import Flask, render_template, request
import os
import subprocess
import sys

SCRIPTS = {
'FINAL': r'/reports/final_estimation.Rmd',
'MARGINAL': r'/reports/measure_marginal_single.Rmd',
'ARGUMENTS': r'/reports/measure_arguments_single.Rmd',
}
INPUT_DIR = '/data/'
CURRENT_GAS_COST_PATH = '/reports/current_gas_cost.csv'
OUTPUT_DIR = INPUT_DIR
ALLOWED_ENVS = ['geth', 'erigon', 'ethereumjs', 'nethermind', 'revm', 'besu']


host_output_dir = ""
available_input_paths = []
app = Flask(__name__)


@app.route("/", methods=["GET"])
def index():
return render_template("index.html")


@app.route("/MARGINAL", methods=["GET", "POST"])
def marginal_report():
return handle_report('MARGINAL', request)


@app.route("/ARGUMENTS", methods=["GET", "POST"])
def arguments_report():
return handle_report('ARGUMENTS', request)


@app.route("/FINAL", methods=["GET", "POST"])
def final_report():
return handle_final_report(request)


def handle_report(report_type, request):
assert report_type in ['MARGINAL', 'ARGUMENTS']
# handles GET request to render the initial form
if request.method == "GET":
return render_template("report_step1.html", report_type=report_type)

# handles POST requests to render the next form
else:
step = request.form.get("step")
env = request.form.get("env")
assert env in ALLOWED_ENVS

if step == "1":
return render_template("report_step2.html", report_type=report_type, env=env, bytecodes_paths=available_input_paths)
elif step == "2":
selected_files = request.form.getlist("selected_bytecodes_paths")
assert len(selected_files) > 0

return render_template("report_step3.html", report_type=report_type, env=env, results_paths=available_input_paths, selected_bytecodes_paths=selected_files)
elif step == "3":
selected_files = request.form.getlist("selected_bytecodes_paths")
assert len(selected_files) > 0

selected_results_paths = request.form.getlist("selected_results_paths")
assert len(selected_results_paths) > 0

local_output_file_name = OUTPUT_DIR + selected_files[0] + '_raport_' + report_type + '.html'
host_output_file_name = host_output_dir + selected_files[0] + '_raport_' + report_type + '.html'

if report_type == 'MARGINAL':
r_command = f"rmarkdown::render('{SCRIPTS[report_type]}', params=list(env='{env}', programs='{INPUT_DIR + selected_files[0]}', results='{INPUT_DIR + selected_results_paths[0]}', output_estimated_cost='{env}_final_estimated_cost.csv'), output_file='{local_output_file_name}')"
else:
r_command = f"rmarkdown::render('{SCRIPTS[report_type]}', params=list(env='{env}', programs='{INPUT_DIR + selected_files[0]}', results='{INPUT_DIR + selected_results_paths[0]}', marginal_estimated_cost='{env}_marginal_estimated_cost.csv', output_estimated_cost='{env}_final_estimated_cost.csv'), output_file='{local_output_file_name}')"

return run_r_script(r_command, host_output_file_name)


def handle_final_report(request):
if request.method == "GET":
return render_template("final_report_step_1.html", paths=available_input_paths)
else:
paths = request.form.getlist("paths")
assert len(paths) > 0

local_output_file_name = OUTPUT_DIR + 'final_raport.html'
host_output_file_name = host_output_dir + 'final_raport.html'
r_command = f"rmarkdown::render('{SCRIPTS['FINAL']}', params=list(estimate_files='{','.join(paths)}', current_gas_cost='{CURRENT_GAS_COST_PATH}'), output_file='{local_output_file_name}')"

return run_r_script(r_command, host_output_file_name)


def run_r_script(r_command, host_output_file_name):
print(r_command)
pro = subprocess.Popen(['Rscript', '-e', r_command], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
_, stderr = pro.communicate()
if pro.returncode != 0:
return render_template("results_error.html", error_message=stderr)

return render_template("results_success.html", output_file_name=host_output_file_name)


def main():
global available_input_paths
global host_output_dir
host_output_dir = sys.argv[1]

# Check if the provided path is a directory
assert os.path.isdir(INPUT_DIR)
# Get the list of files in the directory
available_input_paths = [f for f in os.listdir(INPUT_DIR) if f.endswith(".csv")]
print(available_input_paths)

app.run(debug=True, host="0.0.0.0")


if __name__ == "__main__":
main()
22 changes: 9 additions & 13 deletions src/instrumentation_measurement/measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,16 +152,14 @@ def measure(self, sample_size=1, evm="evmone", input_file="", exec_path=""):
csv_chunk = '\n'.join(result_row)
print(csv_chunk)

def _parse_geth_benchmark_output(self, stdout, stderr):
text = stderr

def _parse_geth_benchmark_output(self, output):
execution_time_pattern = r"execution time:\s*([\d\.]+)(µs|ms)"
allocations_pattern = r"allocations:\s*([\d\.]+)"
allocated_bytes_pattern = r"allocated bytes:\s*([\d\.]+)"

execution_time_match = re.search(execution_time_pattern, text)
allocations = re.search(allocations_pattern, text)
allocated_bytes = re.search(allocated_bytes_pattern, text)
execution_time_match = re.search(execution_time_pattern, output)
allocations = re.search(allocations_pattern, output)
allocated_bytes = re.search(allocated_bytes_pattern, output)

if execution_time_match:
execution_time = float(execution_time_match.group(1))
Expand All @@ -185,17 +183,16 @@ def run_geth_benchmark(self, program, sample_size, exec_path):
else:
exec_path = os.path.abspath(exec_path)

args = ['--code', program.bytecode, '--bench', 'run']
args = ['run', '--bench', program.bytecode]
invocation = [exec_path] + args

results = []
for run_id in range(1, sample_size + 1):
pro = subprocess.Popen(
invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
stdout, stderr = pro.communicate()
_, stderr = pro.communicate()

instrumenter_result = self._parse_geth_benchmark_output(
stdout, stderr)
instrumenter_result = self._parse_geth_benchmark_output(stderr)

results.append(str(run_id) + "," + instrumenter_result)
return results
Expand Down Expand Up @@ -266,10 +263,9 @@ def run_erigon_benchmark(self, program, sample_size, exec_path):
for run_id in range(1, sample_size + 1):
pro = subprocess.Popen(
invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
stdout, stderr = pro.communicate()
_, stderr = pro.communicate()

instrumenter_result = self._parse_geth_benchmark_output(
stdout, stderr)
instrumenter_result = self._parse_geth_benchmark_output(stderr)

results.append(str(run_id) + "," + instrumenter_result)
return results
Expand Down