{"id":28469785,"url":"https://github.com/answerdotai/fh-pyinstrument","last_synced_at":"2026-04-11T05:09:18.260Z","repository":{"id":295673409,"uuid":"952849337","full_name":"AnswerDotAI/fh-pyinstrument","owner":"AnswerDotAI","description":"Using pyinstrument to profile FastHTML apps","archived":false,"fork":false,"pushed_at":"2025-03-27T01:36:00.000Z","size":405,"stargazers_count":1,"open_issues_count":3,"forks_count":0,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-07-18T21:21:20.557Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://answerdotai.github.io/fh-pyinstrument/","language":"Jupyter Notebook","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AnswerDotAI.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-03-22T02:14:40.000Z","updated_at":"2025-05-26T16:18:49.000Z","dependencies_parsed_at":"2025-05-26T20:03:23.865Z","dependency_job_id":"6c186988-f9a3-4ecd-8be8-d2fc3d38d39c","html_url":"https://github.com/AnswerDotAI/fh-pyinstrument","commit_stats":null,"previous_names":["answerdotai/fh-pyinstrument"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/AnswerDotAI/fh-pyinstrument","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Ffh-pyinstrument","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Ffh-pyinstrument/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Ffh-pyinstrument/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Ffh-pyinstrument/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AnswerDotAI","download_url":"https://codeload.github.com/AnswerDotAI/fh-pyinstrument/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Ffh-pyinstrument/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265899816,"owners_count":23845875,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-07T09:08:39.936Z","updated_at":"2026-04-11T05:09:18.222Z","avatar_url":"https://github.com/AnswerDotAI.png","language":"Jupyter Notebook","funding_links":[],"categories":[],"sub_categories":[],"readme":"# fh-pyinstrument\n\n\n\u003c!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! --\u003e\n\nSometimes when building FastHTML apps we run into performance\nbottlenecks. Figuring out what is slow can be challenging, especially\nwhen building apps with async components. That’s where profiling tools\nlike [pyinstrument](https://pyinstrument.readthedocs.io/en/latest/) can\nhelp. Profilers are tools that show exactly how long each component of a\nproject takes to run. Identifying slow parts of an app is the first step\nin figuring out how to make things run faster.\n\n![](https://raw.githubusercontent.com/joerick/pyinstrument/main/docs/img/screenshot.jpg)\n\n## How to install\n\nInstall from PyPI:\n\n``` sh\npip install fh-pyinstrument\n```\n\n## How to configure\n\nThe easiest way is to import the ProfileMiddleware into your project and\nadd it to your app’s list of Middleware via the `app.add_middleware()`\nmethod:\n\n``` python\nfrom fasthtml.common import *\nfrom fh_pyinstrument import ProfileMiddleware\n\napp, rt = fast_app()\napp.add_middleware(ProfileMiddleware, save_dir='/tmp/profiles')\n\n@rt\ndef index(): return Titled('Hello, profiler!')\n\nserve()\n```\n\nIf you want to add it to the project when `fast_app()` is declared,\nyou’ll need to run it through Starlette’s middleware pre-processor:\n\n``` python\nfrom starlette.middleware import Middleware\n\napp, rt = fast_app(middleware=(Middleware(ProfileMiddleware)))\n\n@rt\ndef index(): return Titled('Hello, profiler!')\n\nserve()\n```\n\nIf you want to change the querypath trigger key from `profile` to\nsomething else, set the desired value to `PYINSTRUMENT_TRIGGER`.\n\n``` sh\nexport PYINSTRUMENT_TRIGGER=instrument\n```\n\nNow this will trigger the report:\n\nhttps://localhost:5001/?instrument=1\n\n## How to use the middleware\n\nSimply add `?profile=1` to any url, that will cause the app to display\nan amazing chart set in the browser. In the example above, run it and\nclick this link:\n\n\u003chttp://127.0.0.1:5000/?profile=1\u003e\n\nIf instead you want to have the results show up in the terminal, also\nadd `term=1` to the query string. The normal web page will display in\nyour browser, and the pyinstrument view will show up in limited form\nwithin the terminal.\n\n## How to use the stand-alone `@instrument` decorator\n\nIf you want to temporarily use fh-pyinstrument on an isolated route\nhandler, the `@instrument` decorator can be used. This triggers on any\ncall to the affected route, isn’t triggered by the \\`?profile=1” query\nvalue.\n\n``` python\nfrom fh_pyinstrument import instrument\n\n@rt\n@instrument\ndef index(): return Titled('Hello, profiler!')\n```\n\n## Saving and analysing sessions\n\nPass `save_dir` to the middleware to pickle each profiled request’s\nsession to disk:\n\n``` python\napp.add_middleware(ProfileMiddleware, save_dir='/tmp/profiles')\n```\n\nEach request with `?profile=1` now saves a `.pkl` file alongside the\nHTML view. Load it later for analysis:\n\n``` python\nfrom pathlib import Path\nfrom fh_pyinstrument import load_session, render_session\n\nsess = load_session(sorted(Path('/tmp/profiles').glob('*.pkl'))[-1])\nprint(render_session(sess))\n```\n\n`render_session` returns console text by default (with\n`show_all=False, short_mode=True`). Pass `text=False` for HTML output.\n\n## Programmatic analysis\n\nSessions have four analysis methods patched in. All accept an optional\n`paths` list to filter by file path substrings\n(e.g. `['myapp/', 'fasthtml/']`) and `n` to limit results.\n\n**`sess.flat(paths, n)`** — Which functions have the most self-time?\n\n``` python\nsess.flat(paths=['myapp/'], n=10)\n# [ProfileEntry(time=0.287, func='_to_xml', file='fastcore/xml.py', line=177),\n#  ProfileEntry(time=0.115, func='_get_deps', file='myapp/db.py', line=525), ...]\n```\n\n**`sess.callers(func_name, paths, n)`** — Who’s calling this hot\nfunction?\n\n``` python\nsess.callers('ft_html')\n# Shows which functions call ft_html and how much time they contribute\n```\n\n**`sess.callees(func_name, paths, n)`** — What does a function spend its\ntime on?\n\n``` python\nsess.callees('MsgButtons', paths=['myapp/', 'fasthtml/'], n=5)\n# Shows the most expensive functions called inside MsgButtons\n```\n\n**`sess.hot_paths(paths, n, depth)`** — Most expensive call stacks,\ncollapsed to matching frames:\n\n``` python\nfor t, s in sess.hot_paths(paths=['myapp/'], n=5):\n    print(f'{t*1000:.1f}ms  {s}')\n# 245.0ms  dialog_ core.py:2236 → chat core.py:2206 → MessageCard cards.py:249 → _to_xml xml.py:177\n```\n\n## Quick reference: pyinstrument’s `Session` / `Frame`\n\nA `Session` contains a tree of `Frame` objects. The key attributes on\n`Frame`:\n\n| Attribute             | Description                                     |\n|-----------------------|-------------------------------------------------|\n| `function`            | Function name                                   |\n| `file_path`           | Absolute path to source file                    |\n| `line_no`             | Line number                                     |\n| `children`            | List of child frames                            |\n| `time`                | Total time including children                   |\n| `total_self_time`     | Time in this function only (excluding children) |\n| `absorbed_time`       | Time hidden by pyinstrument’s grouping          |\n| `is_application_code` | Whether pyinstrument considers this app code    |\n\nWalking the tree manually:\n\n``` python\nfrom pyinstrument.session import Session\n\nsess = load_session('profile.pkl')\nroot = sess.root_frame()\nprint(f'Duration: {sess.duration:.2f}s')\n\ndef walk(frame, depth=0):\n    if frame.total_self_time \u003e 0.01:\n        print(f'{\"  \"*depth}{frame.total_self_time*1000:.0f}ms  {frame.function}  {frame.file_path}:{frame.line_no}')\n    for c in frame.children: walk(c, depth+1)\n\nwalk(root)\n```\n\nYou can also render a session directly without going through `Profiler`:\n\n``` python\nfrom pyinstrument.renderers import ConsoleRenderer, HTMLRenderer\n\nprint(ConsoleRenderer(show_all=False, short_mode=True).render(sess))\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanswerdotai%2Ffh-pyinstrument","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fanswerdotai%2Ffh-pyinstrument","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanswerdotai%2Ffh-pyinstrument/lists"}