[{"data":1,"prerenderedAt":795},["ShallowReactive",2],{"page-\u002Fsystematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002Fprofiling-async-code-with-yappi\u002F":3},{"id":4,"title":5,"body":6,"description":761,"extension":762,"meta":763,"navigation":79,"path":791,"seo":792,"stem":793,"__hash__":794},"content\u002Fsystematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002Fprofiling-async-code-with-yappi\u002Findex.md","Profiling Async Code with yappi",{"type":7,"value":8,"toc":750},"minimark",[9,13,16,21,41,45,156,198,206,316,320,323,341,347,351,354,367,380,451,455,466,477,480,547,551,558,608,618,621,625,682,686,696,702,708,712,741,746],[10,11,12],"p",{},"Profiling asyncio code with cProfile produces numbers that look precise and mean little. Every time a coroutine awaits something that suspends, cProfile sees the function return; when the event loop resumes it, cProfile sees a new call. A handler that awaits a database query three times shows up as four calls with fragmented timings, and the time spent suspended is attributed unpredictably. Summed across thousands of requests, the report points at the wrong functions.",[10,14,15],{},"yappi — Yet Another Python Profiler — understands coroutines. It tracks a coroutine across suspensions as a single call, can exclude suspended time from the coroutine's own cost, and lets you choose between CPU time (what the code actually computed) and wall time (how long it took end to end). It also profiles every thread, not just the one that started it, and can break results down per asyncio task. For an async web service or worker, that is the difference between a profile you can act on and one that sends you chasing awaits.",[17,18,20],"h2",{"id":19},"prerequisites","Prerequisites",[22,23,24,32],"ul",{},[25,26,27,31],"li",{},[28,29,30],"code",{},"yappi >= 1.6",", Python 3.9 or later.",[25,33,34,35,40],{},"Background from ",[36,37,39],"a",{"href":38},"\u002Fsystematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002F","CPU profiling with cProfile and py-spy",".",[17,42,44],{"id":43},"solution","Solution",[46,47,52],"pre",{"className":48,"code":49,"language":50,"meta":51,"style":51},"language-python shiki shiki-themes github-light github-dark","# profile_worker.py\nimport asyncio\nimport yappi\n\nfrom app.worker import main\n\nyappi.set_clock_type(\"cpu\")        # what blocks the event loop?\nyappi.start()\nasyncio.run(main(iterations=2_000))\nyappi.stop()\n\nstats = yappi.get_func_stats(filter_callback=lambda f: \"app\u002F\" in f.module)\nstats.sort(\"ttot\", \"desc\").print_all(\n    columns={0: (\"name\", 60), 1: (\"ncall\", 8), 2: (\"ttot\", 8), 3: (\"tsub\", 8), 4: (\"tavg\", 8)}\n)\n\nstats.save(\"worker.pstat\", type=\"pstat\")     # snakeviz worker.pstat\n","python","",[28,53,54,62,68,74,81,87,92,98,104,110,116,121,127,133,139,145,150],{"__ignoreMap":51},[55,56,59],"span",{"class":57,"line":58},"line",1,[55,60,61],{},"# profile_worker.py\n",[55,63,65],{"class":57,"line":64},2,[55,66,67],{},"import asyncio\n",[55,69,71],{"class":57,"line":70},3,[55,72,73],{},"import yappi\n",[55,75,77],{"class":57,"line":76},4,[55,78,80],{"emptyLinePlaceholder":79},true,"\n",[55,82,84],{"class":57,"line":83},5,[55,85,86],{},"from app.worker import main\n",[55,88,90],{"class":57,"line":89},6,[55,91,80],{"emptyLinePlaceholder":79},[55,93,95],{"class":57,"line":94},7,[55,96,97],{},"yappi.set_clock_type(\"cpu\")        # what blocks the event loop?\n",[55,99,101],{"class":57,"line":100},8,[55,102,103],{},"yappi.start()\n",[55,105,107],{"class":57,"line":106},9,[55,108,109],{},"asyncio.run(main(iterations=2_000))\n",[55,111,113],{"class":57,"line":112},10,[55,114,115],{},"yappi.stop()\n",[55,117,119],{"class":57,"line":118},11,[55,120,80],{"emptyLinePlaceholder":79},[55,122,124],{"class":57,"line":123},12,[55,125,126],{},"stats = yappi.get_func_stats(filter_callback=lambda f: \"app\u002F\" in f.module)\n",[55,128,130],{"class":57,"line":129},13,[55,131,132],{},"stats.sort(\"ttot\", \"desc\").print_all(\n",[55,134,136],{"class":57,"line":135},14,[55,137,138],{},"    columns={0: (\"name\", 60), 1: (\"ncall\", 8), 2: (\"ttot\", 8), 3: (\"tsub\", 8), 4: (\"tavg\", 8)}\n",[55,140,142],{"class":57,"line":141},15,[55,143,144],{},")\n",[55,146,148],{"class":57,"line":147},16,[55,149,80],{"emptyLinePlaceholder":79},[55,151,153],{"class":57,"line":152},17,[55,154,155],{},"stats.save(\"worker.pstat\", type=\"pstat\")     # snakeviz worker.pstat\n",[46,157,159],{"className":48,"code":158,"language":50,"meta":51,"style":51},"# The same workload, wall clock, broken down per task.\nyappi.set_clock_type(\"wall\")\nyappi.start()\nasyncio.run(main(iterations=200))\nyappi.stop()\n\nfor task in yappi.get_task_stats():\n    print(f\"{task.name:\u003C30} {task.ttot:8.3f}s\")\n",[28,160,161,166,171,175,180,184,188,193],{"__ignoreMap":51},[55,162,163],{"class":57,"line":58},[55,164,165],{},"# The same workload, wall clock, broken down per task.\n",[55,167,168],{"class":57,"line":64},[55,169,170],{},"yappi.set_clock_type(\"wall\")\n",[55,172,173],{"class":57,"line":70},[55,174,103],{},[55,176,177],{"class":57,"line":76},[55,178,179],{},"asyncio.run(main(iterations=200))\n",[55,181,182],{"class":57,"line":83},[55,183,115],{},[55,185,186],{"class":57,"line":89},[55,187,80],{"emptyLinePlaceholder":79},[55,189,190],{"class":57,"line":94},[55,191,192],{},"for task in yappi.get_task_stats():\n",[55,194,195],{"class":57,"line":100},[55,196,197],{},"    print(f\"{task.name:\u003C30} {task.ttot:8.3f}s\")\n",[46,199,204],{"className":200,"code":202,"language":203,"meta":51},[201],"language-text","Clock type: CPU\nname                                              ncall    ttot     tsub     tavg\napp\u002Fworker.py:78 Worker.handle_message            2000     4.812    0.041    0.0024\napp\u002Fcodec.py:22 decode_payload                    2000     3.906    3.702    0.0020\napp\u002Frules.py:51 evaluate                          2000     0.771    0.644    0.0004\napp\u002Fdb.py:40 Repo.save                            2000     0.089    0.021    0.0000\n","text",[28,205,202],{"__ignoreMap":51},[207,208,211,312],"figure",{"className":209},[210],"diagram",[212,213,220,221,220,225,220,229,220,237,220,246,220,252,220,262,220,268,220,274,220,278,220,281,220,284,220,288,220,293,220,296,220,301,220,304,220,309],"svg",{"viewBox":214,"role":215,"ariaLabelledBy":216,"xmlns":219},"0 0 800 246","img",[217,218],"yp-t","yp-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[222,223,224],"title",{"id":217},"How cProfile and yappi see one coroutine",[226,227,228],"desc",{"id":218},"A timeline shows a handler coroutine that runs, awaits a database call, resumes, awaits again and finishes. cProfile records three separate calls with fragments of time. yappi records one call; with the CPU clock it counts only the running segments, and with the wall clock it counts the full elapsed time including the waits.",[230,231],"rect",{"x":232,"y":232,"width":233,"height":234,"rx":235,"fill":236},"0","800","246","14","#fffdf8",[203,238,245],{"x":239,"y":240,"textAnchor":241,"fontSize":242,"fontWeight":243,"fill":244},"400","28","middle","15.5","700","#3d405b","One coroutine, two very different reports",[203,247,251],{"x":248,"y":249,"fontSize":250,"fontWeight":243,"fill":244},"30","76","11","timeline",[230,253],{"x":254,"y":255,"width":256,"height":257,"rx":258,"fill":259,"stroke":260,"strokeWidth":261},"130","62","90","22","4","#e6f0ea","#81b29a","1.6",[230,263],{"x":264,"y":255,"width":265,"height":257,"rx":258,"fill":266,"stroke":267},"220","160","#f4f1de","rgba(61,64,91,0.35)",[203,269,273],{"x":270,"y":271,"textAnchor":241,"fontSize":272,"fill":244},"300","78","10","await db",[230,275],{"x":276,"y":255,"width":277,"height":257,"rx":258,"fill":259,"stroke":260,"strokeWidth":261},"380","70",[230,279],{"x":280,"y":255,"width":265,"height":257,"rx":258,"fill":266,"stroke":267},"450",[203,282,273],{"x":283,"y":271,"textAnchor":241,"fontSize":272,"fill":244},"530",[230,285],{"x":286,"y":255,"width":287,"height":257,"rx":258,"fill":259,"stroke":260,"strokeWidth":261},"610","120",[203,289,292],{"x":248,"y":290,"fontSize":250,"fontWeight":243,"fill":291},"126","#8f3d22","cProfile",[203,294,295],{"x":254,"y":290,"fontSize":250,"fill":244},"3 calls, fragmented times, suspension attributed unpredictably",[203,297,300],{"x":248,"y":298,"fontSize":250,"fontWeight":243,"fill":299},"166","#2a5f49","yappi cpu",[203,302,303],{"x":254,"y":298,"fontSize":250,"fill":244},"1 call · green segments only → what blocks the loop",[203,305,308],{"x":248,"y":306,"fontSize":250,"fontWeight":243,"fill":307},"206","#8a5a00","yappi wall",[203,310,311],{"x":254,"y":306,"fontSize":250,"fill":244},"1 call · whole span incl. waits → where latency goes",[313,314,315],"figcaption",{},"Choosing the clock is choosing the question: CPU for \"what hogs the loop\", wall for \"where does a request spend its time\".",[17,317,319],{"id":318},"why-this-works","Why this works",[10,321,322],{},"yappi hooks the interpreter's profiling callbacks like cProfile does, but it keeps per-coroutine state. When a coroutine suspends, yappi notes that it is paused rather than finished; when it resumes, the same call continues. With the CPU clock, the time between suspension and resumption is not counted, because the thread was doing other work — running other coroutines — during it. With the wall clock, it is counted, because from the request's point of view that time elapsed.",[10,324,325,326,329,330,333,334,337,338,340],{},"In the example output, ",[28,327,328],{},"ttot"," is total time including callees and ",[28,331,332],{},"tsub"," is time in the function itself. ",[28,335,336],{},"decode_payload"," has almost all of its time in ",[28,339,332],{},", meaning the cost is in its own body — pure Python parsing on every message — and at about two milliseconds of CPU per call, it holds the event loop for that long each time. Every other coroutine waits. That is the actionable finding: move decoding to a faster library or into a thread pool, and the whole worker's latency improves.",[10,342,343,346],{},[28,344,345],{},"get_task_stats"," aggregates by asyncio task, which helps when a service runs several long-lived tasks — a consumer, a heartbeat, a flusher — and you need to know which one is consuming the loop. With the wall clock, a task's total is roughly its lifetime; with CPU, it is the work it actually did.",[17,348,350],{"id":349},"cpu-clock-for-blocking-wall-clock-for-latency","CPU clock for blocking, wall clock for latency",[10,352,353],{},"The two clocks answer different questions, and running both is usually worthwhile.",[10,355,356,357,359,360,363,364,40],{},"The CPU profile finds code that blocks the event loop. In asyncio, any synchronous work in a coroutine prevents every other coroutine from running until it finishes. A function with high CPU ",[28,358,332],{}," and many calls — JSON parsing, regex matching, Pydantic validation of large payloads — is a direct cause of tail latency under load, even if each call looks fast in isolation. These are the functions to optimise, offload with ",[28,361,362],{},"asyncio.to_thread",", or split with occasional ",[28,365,366],{},"await asyncio.sleep(0)",[10,368,369,370,373,374,376,377,379],{},"The wall profile shows where elapsed time goes, including waits. It tends to be dominated by I\u002FO — database round trips, HTTP calls — and that is information rather than a bug: the fix is often concurrency (",[28,371,372],{},"gather"," independent calls instead of awaiting them sequentially), batching, or caching, not faster code. A coroutine with high wall ",[28,375,328],{}," but low CPU ",[28,378,328],{}," is waiting, not working.",[207,381,383,448],{"className":382},[210],[212,384,220,389,220,392,220,395,220,398,220,401,220,407,220,413,220,418,220,423,220,427,220,430,220,433,220,436,220,440,220,442,220,445],{"viewBox":385,"role":215,"ariaLabelledBy":386,"xmlns":219},"0 0 800 226",[387,388],"ypc-t","ypc-d",[222,390,391],{"id":387},"Interpreting CPU and wall results together",[226,393,394],{"id":388},"A two-by-two grid classifies functions. High CPU and high wall means compute-bound code that blocks the loop. Low CPU and high wall means waiting on I\u002FO, fixed by concurrency or batching. High CPU and low wall is rare and usually means measurement across threads. Low on both means not worth attention.",[230,396],{"x":232,"y":232,"width":233,"height":397,"rx":235,"fill":236},"226",[203,399,400],{"x":239,"y":240,"textAnchor":241,"fontSize":242,"fontWeight":243,"fill":244},"Read the two profiles side by side",[230,402],{"x":287,"y":403,"width":270,"height":249,"rx":272,"fill":404,"stroke":405,"strokeWidth":406},"46","#fbe9e3","#e07a5f","2",[203,408,412],{"x":409,"y":410,"textAnchor":241,"fontSize":411,"fontWeight":243,"fill":244},"270","74","12","high CPU · high wall",[203,414,417],{"x":409,"y":415,"textAnchor":241,"fontSize":416,"fill":291},"98","10.5","blocks the loop — optimise or offload",[230,419],{"x":420,"y":403,"width":270,"height":249,"rx":272,"fill":421,"stroke":422,"strokeWidth":406},"440","#f7f0da","#f2cc8f",[203,424,426],{"x":425,"y":410,"textAnchor":241,"fontSize":411,"fontWeight":243,"fill":244},"590","low CPU · high wall",[203,428,429],{"x":425,"y":415,"textAnchor":241,"fontSize":416,"fill":307},"waiting — gather, batch, cache",[230,431],{"x":287,"y":432,"width":270,"height":249,"rx":272,"fill":266,"stroke":267},"132",[203,434,435],{"x":409,"y":265,"textAnchor":241,"fontSize":411,"fontWeight":243,"fill":244},"high CPU · low wall",[203,437,439],{"x":409,"y":438,"textAnchor":241,"fontSize":416,"fill":244},"184","rare — check thread attribution",[230,441],{"x":420,"y":432,"width":270,"height":249,"rx":272,"fill":259,"stroke":260,"strokeWidth":406},[203,443,444],{"x":425,"y":265,"textAnchor":241,"fontSize":411,"fontWeight":243,"fill":244},"low · low",[203,446,447],{"x":425,"y":438,"textAnchor":241,"fontSize":416,"fill":299},"leave alone",[313,449,450],{},"Only the top-left quadrant is a problem profiling-driven optimisation fixes; the top-right is an architecture question.",[17,452,454],{"id":453},"a-worked-case-offloading-the-decoder","A worked case: offloading the decoder",[10,456,457,458,460,461,465],{},"Acting on the example report shows how the two clocks work together. The CPU profile put ",[28,459,336],{}," at about 80% of the worker's CPU time, almost all of it in the function's own body. The wall profile for the same workload showed the average message taking 14 ms end to end, of which only 2 ms was CPU; the rest was waiting on the database and on other coroutines — including, crucially, waiting for ",[462,463,464],"em",{},"other messages'"," decoding to release the loop.",[10,467,468,469,472,473,476],{},"The fix had two parts. The payload format was JSON, and swapping the standard library parser for ",[28,470,471],{},"orjson"," cut decoding CPU by roughly five times. The remaining cost was still synchronous, so large payloads were moved off the loop with ",[28,474,475],{},"await asyncio.to_thread(decode_payload, raw)"," above a size threshold, keeping small messages inline where the thread hand-off would cost more than it saved.",[10,478,479],{},"Re-profiling confirmed both effects. The CPU profile's top entry was now the rules evaluation, at a much lower absolute level. The wall profile showed average message latency down to about 6 ms and, more importantly, the 99th percentile down far more, because a single large message no longer stalled every other coroutine behind it. That tail improvement is the characteristic signature of removing event-loop blocking, and it is exactly what a function-level CPU report for async code should lead to.",[207,481,483,544],{"className":482},[210],[212,484,220,489,220,492,220,495,220,497,220,500,220,505,220,510,220,515,220,518,220,523,220,526,220,530,220,536,220,539],{"viewBox":485,"role":215,"ariaLabelledBy":486,"xmlns":219},"0 0 800 206",[487,488],"ypw-t","ypw-d",[222,490,491],{"id":487},"Latency before and after offloading decoding",[226,493,494],{"id":488},"Bars compare average and 99th percentile message latency. Before the fix, average latency was 14 milliseconds and p99 was 120 milliseconds. After switching to orjson and offloading large payloads to a thread, average fell to 6 milliseconds and p99 to 18 milliseconds, the larger relative gain coming from removing event-loop blocking.",[230,496],{"x":232,"y":232,"width":233,"height":306,"rx":235,"fill":236},[203,498,499],{"x":239,"y":240,"textAnchor":241,"fontSize":242,"fontWeight":243,"fill":244},"Unblocking the loop fixes the tail most",[203,501,504],{"x":502,"y":271,"fontSize":503,"fontWeight":243,"fill":244},"40","11.5","average",[230,506],{"x":507,"y":255,"width":277,"height":508,"rx":258,"fill":404,"stroke":405,"strokeWidth":509},"150","20","1.4",[203,511,514],{"x":512,"y":513,"fontSize":416,"fill":291},"230","77","14 ms before",[230,516],{"x":507,"y":517,"width":248,"height":508,"rx":258,"fill":259,"stroke":260,"strokeWidth":509},"86",[203,519,522],{"x":520,"y":521,"fontSize":416,"fill":299},"190","101","6 ms after",[203,524,525],{"x":502,"y":507,"fontSize":503,"fontWeight":243,"fill":244},"p99",[230,527],{"x":507,"y":528,"width":529,"height":508,"rx":258,"fill":404,"stroke":405,"strokeWidth":509},"134","600",[203,531,535],{"x":532,"y":533,"textAnchor":534,"fontSize":416,"fill":291},"740","149","end","120 ms before",[230,537],{"x":507,"y":538,"width":256,"height":508,"rx":258,"fill":259,"stroke":260,"strokeWidth":509},"158",[203,540,543],{"x":541,"y":542,"fontSize":416,"fill":299},"250","173","18 ms after",[313,545,546],{},"The average improved about twofold; the p99 about sixfold, because large messages stopped stalling everything behind them.",[17,548,550],{"id":549},"profiling-an-async-test-or-a-single-request","Profiling an async test or a single request",[10,552,553,554,557],{},"A whole-worker profile is useful for a first look at a busy service, but the fastest feedback loop is often a profile of one scenario in a test. yappi works inside pytest as long as it is started before the event loop runs the code of interest. With ",[28,555,556],{},"pytest-asyncio",", a fixture that starts yappi, yields, stops it and prints filtered stats gives a per-test profile:",[46,559,561],{"className":48,"code":560,"language":50,"meta":51,"style":51},"@pytest.fixture\ndef yappi_cpu():\n    yappi.clear_stats()\n    yappi.set_clock_type(\"cpu\")\n    yappi.start()\n    yield\n    yappi.stop()\n    yappi.get_func_stats(filter_callback=lambda f: \"app\u002F\" in f.module) \\\n         .sort(\"tsub\", \"desc\").print_all()\n",[28,562,563,568,573,578,583,588,593,598,603],{"__ignoreMap":51},[55,564,565],{"class":57,"line":58},[55,566,567],{},"@pytest.fixture\n",[55,569,570],{"class":57,"line":64},[55,571,572],{},"def yappi_cpu():\n",[55,574,575],{"class":57,"line":70},[55,576,577],{},"    yappi.clear_stats()\n",[55,579,580],{"class":57,"line":76},[55,581,582],{},"    yappi.set_clock_type(\"cpu\")\n",[55,584,585],{"class":57,"line":83},[55,586,587],{},"    yappi.start()\n",[55,589,590],{"class":57,"line":89},[55,591,592],{},"    yield\n",[55,594,595],{"class":57,"line":94},[55,596,597],{},"    yappi.stop()\n",[55,599,600],{"class":57,"line":100},[55,601,602],{},"    yappi.get_func_stats(filter_callback=lambda f: \"app\u002F\" in f.module) \\\n",[55,604,605],{"class":57,"line":106},[55,606,607],{},"         .sort(\"tsub\", \"desc\").print_all()\n",[10,609,610,611,614,615,40],{},"Request one test with it — ",[28,612,613],{},"pytest -s -k test_large_order_checkout"," with the fixture added to that test — and the top of the table is the synchronous work that request does on the loop. That is a convenient way to check a suspected hot path in isolation, to compare two implementations under identical conditions, or to confirm that an offload to a thread really did remove the work from the loop: after the change, the function should disappear from the CPU table of the loop thread and reappear under the worker thread in ",[28,616,617],{},"get_thread_stats()",[10,619,620],{},"Keep such fixtures strictly opt-in, never autouse, and never enabled in CI. Profiling overhead makes timing-sensitive tests flaky, and a fixture that silently profiles every test slows the suite for everyone. Adding it explicitly to the one or two tests under investigation, and removing it once the question is answered, keeps the profile focused and the rest of the suite untouched.",[17,622,624],{"id":623},"edge-cases-and-failure-modes","Edge cases and failure modes",[22,626,627,634,652,658,672],{},[25,628,629,633],{},[630,631,632],"strong",{},"Overhead."," yappi is a deterministic profiler and slows code substantially. Profile representative workloads, not the full production load, and compare relative numbers.",[25,635,636,639,640,643,644,647,648,651],{},[630,637,638],{},"Forgetting to clear."," Stats accumulate across ",[28,641,642],{},"start","\u002F",[28,645,646],{},"stop"," pairs. Call ",[28,649,650],{},"yappi.clear_stats()"," between separate measurements.",[25,653,654,657],{},[630,655,656],{},"uvloop."," yappi works with uvloop, but time inside the loop's C implementation is not attributed to Python functions.",[25,659,660,663,664,667,668,671],{},[630,661,662],{},"Threads started before yappi."," yappi profiles threads created after ",[28,665,666],{},"start()"," by default; for existing threads, pass ",[28,669,670],{},"profile_threads=True"," and start early.",[25,673,674,677,678,681],{},[630,675,676],{},"Greenlets."," gevent-based code needs ",[28,679,680],{},"yappi.set_context_backend(\"greenlet\")"," to attribute time per greenlet correctly.",[17,683,685],{"id":684},"frequently-asked-questions","Frequently Asked Questions",[10,687,688,691,692,695],{},[630,689,690],{},"Why is cProfile misleading for asyncio code?","\ncProfile records a coroutine's time as ending each time it suspends at an ",[28,693,694],{},"await"," and starting again when resumed, and it counts each resume as a call. Coroutines that wait on I\u002FO show inflated call counts and misleading time. yappi is coroutine-aware and aggregates the whole coroutine.",[10,697,698,701],{},[630,699,700],{},"Should I use wall clock or CPU clock with yappi?","\nUse CPU clock to find code that burns the processor and blocks the event loop. Use wall clock to find where requests spend elapsed time, including waiting on I\u002FO — but remember that waiting is often not something profiling can fix.",[10,703,704,707],{},[630,705,706],{},"Can yappi profile a running server?","\nYes, start and stop it from inside the process, for example from an admin endpoint or a signal handler, and write stats to a file. It adds overhead, so enable it briefly rather than permanently.",[17,709,711],{"id":710},"related","Related",[22,713,714,720,727,734],{},[25,715,716,719],{},[36,717,718],{"href":38},"CPU Profiling with cProfile and py-spy"," — profiler fundamentals.",[25,721,722,726],{},[36,723,725],{"href":724},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-async-code-and-event-loops\u002Ffinding-blocking-calls-with-asyncio-debug-mode\u002F","Finding Blocking Calls with asyncio Debug Mode"," — slow-callback warnings.",[25,728,729,733],{},[36,730,732],{"href":731},"\u002Fsystematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002Fprofiling-a-running-process-with-py-spy\u002F","Profiling a Running Process with py-spy"," — sampling without code changes.",[25,735,736,740],{},[36,737,739],{"href":738},"\u002Fsystematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002Fbenchmarking-with-pytest-benchmark\u002F","Benchmarking with pytest-benchmark"," — confirming the fix.",[10,742,743,744],{},"← Back to ",[36,745,718],{"href":38},[747,748,749],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":51,"searchDepth":64,"depth":64,"links":751},[752,753,754,755,756,757,758,759,760],{"id":19,"depth":64,"text":20},{"id":43,"depth":64,"text":44},{"id":318,"depth":64,"text":319},{"id":349,"depth":64,"text":350},{"id":453,"depth":64,"text":454},{"id":549,"depth":64,"text":550},{"id":623,"depth":64,"text":624},{"id":684,"depth":64,"text":685},{"id":710,"depth":64,"text":711},"Profile asyncio applications correctly with yappi: wall versus CPU clocks, per-coroutine time that excludes suspension, per-task and per-thread stats, and exporting to pstats and flame graphs.","md",{"slug":764,"type":765,"breadcrumb":766,"datePublished":767,"dateModified":767,"faq":768,"howto":775},"profiling-async-code-with-yappi","article","yappi for asyncio","2026-09-18",[769,771,773],{"q":690,"a":770},"cProfile records a coroutine's time as ending each time it suspends at an await and starting again when resumed, and it counts each resume as a call. Coroutines that wait on I\u002FO show inflated call counts and misleading time. yappi is coroutine-aware and aggregates the whole coroutine.",{"q":700,"a":772},"Use CPU clock to find code that burns the processor and blocks the event loop. Use wall clock to find where requests spend elapsed time, including waiting on I\u002FO — but remember that waiting is often not something profiling can fix.",{"q":706,"a":774},"Yes, start and stop it from inside the process, for example from an admin endpoint or a signal handler, and write stats to a file. It adds overhead, so enable it briefly rather than permanently.",{"name":776,"description":777,"steps":778},"How to profile asyncio code with yappi","Choose a clock, run the workload under yappi, and read per-function and per-task statistics that account for coroutine suspension.",[779,782,785,788],{"name":780,"text":781},"Pick the clock","Call yappi.set_clock_type('cpu') for event-loop blocking, or 'wall' for elapsed time.",{"name":783,"text":784},"Profile the workload","Wrap asyncio.run(main()) between yappi.start() and yappi.stop().",{"name":786,"text":787},"Read function stats","Sort get_func_stats() by total time and filter to your package.",{"name":789,"text":790},"Export for visualisation","Save as pstats and open with snakeviz or convert to a flame graph.","\u002Fsystematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002Fprofiling-async-code-with-yappi",{"title":5,"description":761},"systematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002Fprofiling-async-code-with-yappi\u002Findex","NnrewDhXMxz5ybNmZ3P2WgB5zYAtAyLi3jJihMxJv_E",1789718769133]