Skip to content

Commit 254a976

Browse files
🔨 Profile api server (#4754)
1 parent 6d223c8 commit 254a976

File tree

5 files changed

+68
-0
lines changed

5 files changed

+68
-0
lines changed

services/api-server/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ will start the api-server in development-mode together with a postgres db initia
3636
- http://127.0.0.1:8000/docs: redoc documentation
3737
- http://127.0.0.1:8000/dev/docs: swagger type of documentation
3838

39+
### Profiling requests to the api server
40+
When in development mode (the environment variable `API_SERVER_DEV_FEATURES_ENABLED` is =1 in the running container) one can profile calls to the API server directly from the client side. On the server, the profiling is done using [Pyinstrument](https://github.com/joerick/pyinstrument). If we have our request in the form of a curl command, one simply adds the custom header `x-profile-api-server:true` to the command, in which case the profile is received under the `profile` key of the response body. This makes it easy to visualise the profiling report directly in bash:
41+
```bash
42+
<curl_command> -H 'x-profile-api-server: true' | jq -r .profile
43+
```
3944

4045
## Clients
4146

services/api-server/requirements/_test.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ docker
1919
faker
2020
jsonref
2121
moto[server] # mock out tests based on AWS-S3
22+
pyinstrument
2223
pytest
2324
pytest-asyncio
2425
pytest-cov

services/api-server/requirements/_test.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ pycparser==2.21
234234
# via
235235
# -c requirements/_base.txt
236236
# cffi
237+
pyinstrument==4.5.0
238+
# via
239+
# -c requirements/_base.txt
240+
# -r requirements/_test.in
237241
pyparsing==3.1.1
238242
# via moto
239243
pyrsistent==0.19.3
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import json
2+
3+
from fastapi import FastAPI
4+
from pyinstrument import Profiler
5+
from starlette.requests import Request
6+
7+
8+
def _generate_response_headers(content: bytes) -> list[tuple[bytes, bytes]]:
9+
headers: dict = dict()
10+
headers[b"content-length"] = str(len(content)).encode("utf8")
11+
headers[b"content-type"] = b"application/json"
12+
return list(headers.items())
13+
14+
15+
class ApiServerProfilerMiddleware:
16+
"""Following
17+
https://www.starlette.io/middleware/#cleanup-and-error-handling
18+
https://www.starlette.io/middleware/#reusing-starlette-components
19+
https://fastapi.tiangolo.com/advanced/middleware/#advanced-middleware
20+
"""
21+
22+
def __init__(self, app: FastAPI):
23+
self._app: FastAPI = app
24+
self._profile_header_trigger: str = "x-profile-api-server"
25+
26+
async def __call__(self, scope, receive, send):
27+
if scope["type"] != "http":
28+
await self._app(scope, receive, send)
29+
return
30+
31+
profiler = Profiler(async_mode="enabled")
32+
request: Request = Request(scope)
33+
headers = dict(request.headers)
34+
if self._profile_header_trigger in headers:
35+
headers.pop(self._profile_header_trigger)
36+
scope["headers"] = [
37+
(k.encode("utf8"), v.encode("utf8")) for k, v in headers.items()
38+
]
39+
profiler.start()
40+
41+
async def send_wrapper(message):
42+
if profiler.is_running:
43+
profiler.stop()
44+
if profiler.last_session:
45+
body: bytes = json.dumps(
46+
{"profile": profiler.output_text(unicode=True, color=True)}
47+
).encode("utf8")
48+
if message["type"] == "http.response.start":
49+
message["headers"] = _generate_response_headers(body)
50+
elif message["type"] == "http.response.body":
51+
message["body"] = body
52+
await send(message)
53+
54+
await self._app(scope, receive, send_wrapper)

services/api-server/src/simcore_service_api_server/core/application.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ def init_app(settings: ApplicationSettings | None = None) -> FastAPI:
113113
else None,
114114
),
115115
)
116+
if settings.API_SERVER_DEV_FEATURES_ENABLED:
117+
from ._profiler_middleware import ApiServerProfilerMiddleware
118+
119+
app.add_middleware(ApiServerProfilerMiddleware)
116120

117121
# routing
118122

0 commit comments

Comments
 (0)