diff --git a/.gitignore b/.gitignore index 169f946a7..dd173e77f 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ db_schema.png .coverage htmlcov +test/* +profile_reports/* diff --git a/documentation/configuration.rst b/documentation/configuration.rst index cdb1769cc..4d90fee16 100644 --- a/documentation/configuration.rst +++ b/documentation/configuration.rst @@ -102,6 +102,15 @@ Whether to turn on a feature which times requests made through FlexMeasures. Int Default: ``False`` +FLEXMEASURES_PROFILE_PYINSTRUMENT +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Whether to turn on a feature which profiles the Flask API endpoints using `pyinstrument`. Interesting for developers. + +The profiling results are stored in the ``profile_reports`` folder in the instance directory. + +Default: ``False`` + UI -- diff --git a/flexmeasures/app.py b/flexmeasures/app.py index e9c2a5554..2d0cd2233 100644 --- a/flexmeasures/app.py +++ b/flexmeasures/app.py @@ -5,6 +5,9 @@ from __future__ import annotations import time +import os +from pathlib import Path +from datetime import date from flask import Flask, g, request from flask.cli import load_dotenv @@ -96,6 +99,8 @@ def create( # noqa C901 app.config["SECURITY_PASSWORD_SALT"] = app.config["SECRET_KEY"] if app.env not in ("documentation", "development"): SSLify(app) + if app.config.get("FLEXMEASURES_PROFILE_PYINSTRUMENT", False): + Path("profile_reports").mkdir(parents=True, exist_ok=True) # Register database and models, including user auth security handlers @@ -150,6 +155,27 @@ def create( # noqa C901 def before_request(): if app.config.get("FLEXMEASURES_PROFILE_REQUESTS", False): g.start = time.time() + if app.config.get("FLEXMEASURES_PROFILE_PYINSTRUMENT", False): + import pyinstrument + + g.profiler = pyinstrument.Profiler() + g.profiler.start() + + @app.after_request + def after_request(response): + if app.config.get("FLEXMEASURES_PROFILE_PYINSTRUMENT", False): + g.profiler.stop() + output_html = g.profiler.output_html(timeline=True) + endpoint = request.endpoint + if endpoint is None: + endpoint = "unknown" + today = date.today() + profile_filename = f"pyinstrument_{endpoint}.html" + profile_output_path = Path("profile_reports", today.strftime("%Y-%m-%d")) + profile_output_path.mkdir(parents=True, exist_ok=True) + with open(os.path.join(profile_output_path, profile_filename), "w+") as f: + f.write(output_html) + return response @app.teardown_request def teardown_request(exception=None): diff --git a/flexmeasures/utils/config_defaults.py b/flexmeasures/utils/config_defaults.py index a8f10180a..46b33c363 100644 --- a/flexmeasures/utils/config_defaults.py +++ b/flexmeasures/utils/config_defaults.py @@ -131,6 +131,7 @@ class Config(object): FLEXMEASURES_API_SUNSET_ACTIVE: bool = False # if True, sunset endpoints return 410 (Gone) responses; if False, they return 404 (Not Found) responses or will work as before, depending on whether the current FlexMeasures version still contains the endpoint logic FLEXMEASURES_API_SUNSET_DATE: str | None = None # e.g. 2023-05-01 FLEXMEASURES_API_SUNSET_LINK: str | None = None # e.g. https://flexmeasures.readthedocs.io/en/latest/api/introduction.html#deprecation-and-sunset + FLEXMEASURES_PROFILE_PYINSTRUMENT: bool = False # names of settings which cannot be None diff --git a/requirements/app.txt b/requirements/app.txt index 215052b2e..e0afa16f1 100644 --- a/requirements/app.txt +++ b/requirements/app.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: # # pip-compile --output-file=requirements/app.txt requirements/app.in # diff --git a/requirements/dev.in b/requirements/dev.in index e1f392c32..63c71088b 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -10,4 +10,5 @@ flake8-blind-except mypy pytest-runner setuptools_scm -watchdog \ No newline at end of file +watchdog +pyinstrument \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index 6e019a511..625d3f8a8 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: # # pip-compile --output-file=requirements/dev.txt requirements/dev.in # @@ -52,6 +52,8 @@ pycodestyle==2.8.0 # via flake8 pyflakes==2.4.0 # via flake8 +pyinstrument==4.5.0 + # via -r requirements/dev.in pytest-runner==6.0.0 # via -r requirements/dev.in pyyaml==6.0 diff --git a/requirements/docs.txt b/requirements/docs.txt index f185451c4..baa53083b 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: # # pip-compile --output-file=requirements/docs.txt requirements/docs.in # diff --git a/requirements/test.txt b/requirements/test.txt index 827b7d973..edd5791ca 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: # # pip-compile --output-file=requirements/test.txt requirements/test.in #