From 6311f804ed0f63409467c268d4d18086dc7c32eb Mon Sep 17 00:00:00 2001 From: Nischay Ram Mamidi Date: Tue, 13 Jun 2023 07:45:23 -0400 Subject: [PATCH 1/4] initial pyinstrument integration Signed-off-by: Nischay Ram Mamidi --- .gitignore | 2 ++ documentation/configuration.rst | 9 +++++++++ flexmeasures/app.py | 26 ++++++++++++++++++++++++++ flexmeasures/utils/config_defaults.py | 1 + requirements/app.txt | 4 ++-- requirements/dev.in | 3 ++- requirements/dev.txt | 6 ++++-- requirements/docs.txt | 4 ++-- requirements/test.txt | 4 ++-- 9 files changed, 50 insertions(+), 9 deletions(-) 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 # From bcaeec0b939589b5c9ff99fcd9fb3a29e7b49109 Mon Sep 17 00:00:00 2001 From: Nischay Ram Mamidi Date: Wed, 14 Jun 2023 14:55:05 -0400 Subject: [PATCH 2/4] merge with profile requests env Signed-off-by: Nischay Ram Mamidi --- documentation/configuration.rst | 11 ++---- flexmeasures/app.py | 53 ++++++++++++++++----------- flexmeasures/utils/config_defaults.py | 1 - 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/documentation/configuration.rst b/documentation/configuration.rst index 4d90fee16..861f6a67b 100644 --- a/documentation/configuration.rst +++ b/documentation/configuration.rst @@ -98,16 +98,13 @@ Default: ``"migrations/dumps"`` FLEXMEASURES_PROFILE_REQUESTS ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Whether to turn on a feature which times requests made through FlexMeasures. Interesting for developers. +Whether to turn on a feature which times requests made through FlexMeasures. If `pyinstrument` is installed, the Flask API endpoints are profiled. -Default: ``False`` - -FLEXMEASURES_PROFILE_PYINSTRUMENT -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The profiling results are stored in the ``profile_reports`` folder in the instance directory. -Whether to turn on a feature which profiles the Flask API endpoints using `pyinstrument`. Interesting for developers. +Note: Profile reports for API endpoints are overwritten on repetition of the same request. -The profiling results are stored in the ``profile_reports`` folder in the instance directory. +Interesting for developers. Default: ``False`` diff --git a/flexmeasures/app.py b/flexmeasures/app.py index 2d0cd2233..db164af31 100644 --- a/flexmeasures/app.py +++ b/flexmeasures/app.py @@ -99,8 +99,14 @@ 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): + if app.config.get("FLEXMEASURES_PROFILE_REQUESTS", False): Path("profile_reports").mkdir(parents=True, exist_ok=True) + try: + import pyinstrument # noqa F401 + except ImportError: + app.logger.warning( + "[PROFILE] pyinstrument not installed, cannot profile requests." + ) # Register database and models, including user auth security handlers @@ -155,27 +161,13 @@ 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 + try: + import pyinstrument # noqa F401 + + g.profiler = pyinstrument.Profiler() + g.profiler.start() + except ImportError: + pass @app.teardown_request def teardown_request(exception=None): @@ -185,5 +177,22 @@ def teardown_request(exception=None): app.logger.info( f"[PROFILE] {str(round(diff, 2)).rjust(6)} seconds to serve {request.url}." ) + if not hasattr(g, "profiler"): + return app + 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 app diff --git a/flexmeasures/utils/config_defaults.py b/flexmeasures/utils/config_defaults.py index 46b33c363..a8f10180a 100644 --- a/flexmeasures/utils/config_defaults.py +++ b/flexmeasures/utils/config_defaults.py @@ -131,7 +131,6 @@ 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 From a282faf91a769ac956127d1d30bdce34c00515c0 Mon Sep 17 00:00:00 2001 From: Nischay Ram Mamidi Date: Tue, 20 Jun 2023 09:32:06 -0400 Subject: [PATCH 3/4] improve documentation and formatting Signed-off-by: Nischay Ram Mamidi --- documentation/configuration.rst | 4 +++- flexmeasures/app.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/documentation/configuration.rst b/documentation/configuration.rst index 861f6a67b..899a06d98 100644 --- a/documentation/configuration.rst +++ b/documentation/configuration.rst @@ -98,7 +98,9 @@ Default: ``"migrations/dumps"`` FLEXMEASURES_PROFILE_REQUESTS ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Whether to turn on a feature which times requests made through FlexMeasures. If `pyinstrument` is installed, the Flask API endpoints are profiled. +If True, the processing time of requests are profiled. + +The overall time used by requests are logged to the console. In addiition, if `pyinstrument` is installed, then a profiling report is made (of time being spent in different function calls) for all Flask API endpoints. The profiling results are stored in the ``profile_reports`` folder in the instance directory. diff --git a/flexmeasures/app.py b/flexmeasures/app.py index db164af31..051600508 100644 --- a/flexmeasures/app.py +++ b/flexmeasures/app.py @@ -99,13 +99,16 @@ def create( # noqa C901 app.config["SECURITY_PASSWORD_SALT"] = app.config["SECRET_KEY"] if app.env not in ("documentation", "development"): SSLify(app) + + # Prepare profiling, if needed + if app.config.get("FLEXMEASURES_PROFILE_REQUESTS", False): Path("profile_reports").mkdir(parents=True, exist_ok=True) try: import pyinstrument # noqa F401 except ImportError: app.logger.warning( - "[PROFILE] pyinstrument not installed, cannot profile requests." + "FLEXMEASURES_PROFILE_REQUESTS is True, but pyinstrument not installed ― I cannot produce profiling reports for requests." ) # Register database and models, including user auth security handlers From 6754ddbf71a4dc7162dad3802c7e55f239ab40a9 Mon Sep 17 00:00:00 2001 From: Nischay Ram Mamidi Date: Wed, 21 Jun 2023 10:36:40 -0400 Subject: [PATCH 4/4] update changelog Signed-off-by: Nischay Ram Mamidi --- documentation/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index c2c485c92..1aa34023a 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -17,6 +17,8 @@ Bugfixes Infrastructure / Support ---------------------- +* Add support for profiling Flask API calls using ``pyinstrument`` (if installed). Can be enabled by setting the environment variable ``FLEXMEASURES_PROFILE_REQUESTS`` to ``True`` [see `PR #722 `_] + v0.14.1 | June XX, 2023 ============================