ЁЯПл The SchoolтА║ЁЯПОя╕П PerformanceтА║ЁЯУЛ рдзрдбрд╛ 03 тАФ Profiling: рдкреНрд░рд╢рд┐рдХреНрд╖рд┐рдХреЗрдЪрд╛ clipboard
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯУЛ рдзрдбрд╛ 03 тАФ Profiling: рдкреНрд░рд╢рд┐рдХреНрд╖рд┐рдХреЗрдЪрд╛ clipboard

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 03 ┬╖ рдорд╛рдЧреЗ: lesson-02-benchmarking ┬╖ рдкреБрдвреЗ: lesson-04-complexity


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ02, рдЕрдзрд┐рдХ profiling: рдХрд╛рд╣реАрд╣реА рдмрджрд▓рдгреНрдпрд╛рдЖрдзреА, рд╡реЗрд│ рдХреБрдареЗ рдЬрд╛рддреЛ рддреЗ рд╢реЛрдзрд╛. Profiler рдХреЛрдгрддреА functions рдЪрд╛рд▓рддрд╛рдд, рдХрд┐рддреА рд╡реЗрд│рд╛, рдЖрдгрд┐ рдХрд┐рддреА рдХрд╛рд│ рддреЗ рдиреЛрдВрджрд╡рддреЛ. рд╣рд╛ рдзрдбрд╛ Python рдЪреНрдпрд╛ built-in cProfile рдордзреВрди call counts рд╡рд╛рдЪрддреЛ тАФ counts рдкреНрд░рддреНрдпреЗрдХ machine рд╡рд░ рд╕рд╛рд░рдЦреЗ рдЕрд╕рддрд╛рдд, рд╡реЗрд│рд╛ рдирд╛рд╣реАрдд тАФ рдЖрдгрд┐ рддреНрдпрд╛рдВрдЪрд╛ рд╡рд╛рдкрд░ рдХрд░реВрди рдПрдХ hot spot рд╢реЛрдзреВрди рддреЛ рджреБрд░реБрд╕реНрдд рдХрд░рддреЛ. perf/demo.py рдордзрд▓реЗ profile() рдЖрдгрд┐ perf/sim.py рдордзрд▓реЗ results_slow, results_fast рдЖрдгрд┐ profile_calls.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдХреНрд░реАрдбрд╛ рджрд┐рдирд╛рдЪреЗ results рдЫрд╛рдкрд╛рдпрд▓рд╛ рдЦреВрдк рд╡реЗрд│ рд▓рд╛рдЧрддреЛ. рдкреНрд░рддреНрдпреЗрдХрд╛рдЪрд╛ рдХрд╛рд╣реАрддрд░реА рдЕрдВрджрд╛рдЬ рдЖрд╣реЗ. "Printer рдЬреБрдирд╛ рдЖрд╣реЗ!" "рдмреЗрд░рдЬрд╛ рдХрдареАрдг рдЖрд╣реЗрдд!"

рдкреНрд░рд╢рд┐рдХреНрд╖рд┐рдХрд╛ рджреАрдкрд┐рдХрд╛ рдЕрдВрджрд╛рдЬ рдХрд░рдд рдирд╛рд╣реА. рддреА рдПрдХ clipboard ЁЯУЛ рдШреЗрддреЗ рдЖрдгрд┐ results рдЪреНрдпрд╛ рдЯреЗрдмрд▓рд╛рд╢реЗрдЬрд╛рд░реА рдЙрднреА рд░рд╛рд╣рддреЗ. рдХреЛрдгреАрд╣реА рдПрдЦрд╛рджреЗ рдХрд╛рдо рдХрд░рддрд╛рдЪ, рддреА рддреНрдпрд╛ рдХрд╛рдорд╛рд╕рдореЛрд░ рдПрдХ рдЦреВрдг рдХрд░рддреЗ.

рд╢реЗрд╡рдЯреА clipboard рд╕рд╛рдВрдЧрддреЛ:

рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ "рдзрд╛рд╡рдкрдЯреВ рд╢реЛрдзрдгреЗ" рдореНрд╣рдгрдЬреЗ 300 рдирд╛рд╡рд╛рдВрдЪреА рдпрд╛рджреА рдПрдХреЗрдХ рдХрд░рдд рдЪрд╛рд│рдгреЗ. рдореНрд╣рдгрдЬреЗ рдЬрд╡рд│рдЬрд╡рд│ 300,000 рдирд╛рд╡рд╛рдВрдЪреНрдпрд╛ рддрдкрд╛рд╕рдгреНрдпрд╛. Printer рдареАрдХ рд╣реЛрддрд╛. рд╢реЛрдзрдгреЗ (lookup) рд╣реАрдЪ рд╕рдорд╕реНрдпрд╛ рд╣реЛрддреА.

рдореНрд╣рдгреВрди рджреАрдкрд┐рдХрд╛ рдПрдХрджрд╛рдЪ bib рдХреНрд░рдорд╛рдВрдХрд╛рдиреБрд╕рд╛рд░ рд▓рд╛рд╡рд▓реЗрд▓реА card box ЁЯЧВя╕П рдмрдирд╡рддреЗ. рдЖрддрд╛ рдкреНрд░рддреНрдпреЗрдХ lookup рдореНрд╣рдгрдЬреЗ рдПрдХ рдЭрдЯрдкрдЯ рдЙрдЪрд▓рдгреЗ. Results рддреЗрдЪ, рдЖрдгрд┐ рд╣рд│реВ рдХрд╛рдо рдирд╛рд╣реАрд╕реЗ рдЭрд╛рд▓реЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    prog["ЁЯПГ results program<br/>300 runners ┬╖ 2000 laps"] --> prof["ЁЯУЛ cProfile<br/>counts every call"]
    prof --> hot["find_runner 2000├Ч<br/>297,098 comparisons"]
    prof --> cold["best_of 4├Ч ┬╖ results_slow 1├Ч"]
    hot --> fix["ЁЯЧВя╕П build a dict by bib once"]
    fix --> after["find_runner 0 comparisons<br/>same results: True"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/performance/lesson-diagrams.html#l03

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рд▓реЛрдХрд╛рдВрдЪреЗ рдЕрдВрджрд╛рдЬ рдЪреБрдХрддрд╛рдд. рд╣рд│реВ рднрд╛рдЧ рдХреНрд╡рдЪрд┐рддрдЪ рддрд┐рдереЗ рдЕрд╕рддреЛ рдЬрд┐рдереЗ рддреЛ рд╣рд│реВ "рджрд┐рд╕рддреЛ", рдЖрдгрд┐ 2% рд╡реЗрд│ рдШреЗрдгрд╛рд░рд╛ code рдЬрд▓рдж рдХреЗрд▓реНрдпрд╛рдиреЗ рдЬрд╛рд╕реНрддреАрдд рдЬрд╛рд╕реНрдд 2% рдлрд╛рдпрджрд╛ рд╣реЛрдК рд╢рдХрддреЛ. рдЖрдзреА profiling рдХреЗрд▓реНрдпрд╛рдиреЗ "рд╣реЗ рдЬрд▓рдж рдХрд░рд╛" рдЪреЗ рд░реВрдкрд╛рдВрддрд░ "рд╣реЗ function рдЬрд▓рдж рдХрд░рд╛" рдордзреНрдпреЗ рд╣реЛрддреЗ, рдЖрдгрд┐ рдордЧ рдзрдбрд╛ 02 рдЪрд╛ benchmark рддреЗ рдХрд╛рдо рдЭрд╛рд▓реЗ рдХрд╛ рддреЗ рд╕рд╛рдВрдЧрддреЛ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

perf/sim.py рдордзрд▓реЗ sports_day() 300 рдзрд╛рд╡рдкрдЯреВ (shuffled рдХреНрд░рдорд╛рдиреЗ) рдЖрдгрд┐ 2000 laps рдмрдирд╡рддреЗ. results_slow рдкреНрд░рддреНрдпреЗрдХ lap рдЪрд╛ рдзрд╛рд╡рдкрдЯреВ find_runner рдиреЗ рд╢реЛрдзрддреЗ, рдЬреЗ рдпрд╛рджреА рдЪрд╛рд│рддреЗ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ comparison COUNT["compares"] рдордзреНрдпреЗ рдореЛрдЬрддреЗ. results_fast рдПрдХрджрд╛рдЪ bib рдиреБрд╕рд╛рд░ dict рдмрдирд╡рддреЗ. profile_calls(fn, ...) function рдЦрд▒реНрдпрд╛ cProfile рдЦрд╛рд▓реА рдЪрд╛рд▓рд╡рддреЗ рдЖрдгрд┐ рдпрд╛ module рдЪреА functions рддреНрдпрд╛рдВрдЪреНрдпрд╛ ncalls рд╕рд╣ рдкрд░рдд рджреЗрддреЗ (рддреЗ comprehension helpers рд╡рдЧрд│рддреЗ, рдЬреЗ рдирд╡реНрдпрд╛ Python versions рд╕реНрд╡рддрдВрддреНрд░ functions рдореНрд╣рдгреВрди рджрд╛рдЦрд╡рдд рдирд╛рд╣реАрдд).

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 perf/demo.py profile
python3 - <<'EOF'
import sys; sys.path.insert(0, "perf"); import sim
runners, laps = sim.sports_day()
for n_laps in (500, 1000, 2000):
    sim.COUNT["compares"] = 0
    _, rows = sim.profile_calls(sim.results_slow, runners, laps[:n_laps])
    print(f"{n_laps:>4} laps тЖТ find_runner {dict(rows)['find_runner']:>4}├Ч ┬╖ {sim.COUNT['compares']:>7,} comparisons")
EOF
python3 -c "import cProfile, sys; sys.path.insert(0, 'perf'); import sim; r, l = sim.sports_day(); cProfile.run('sim.results_slow(r, l)', sort='ncalls')" | head -14

рд╢реЗрд╡рдЯрдЪрд╛ command cProfile рдЪреЗ рд╕рдВрдкреВрд░реНрдг table рдЫрд╛рдкрддреЛ. рддреНрдпрд╛рдЪрд╛ ncalls column find_runner рд╕рд╛рдареА 2000 рд╕рд╛рдВрдЧрддреЛ; рддреНрдпрд╛рдЪреЗ рд╡реЗрд│реЗрдЪреЗ columns (tottime, cumtime, рдЖрдгрд┐ рдкрд╣рд┐рд▓реНрдпрд╛ рдУрд│реАрддрд▓реА рдПрдХреВрдг рд╡реЗрд│) рдЦрд▒реНрдпрд╛ рдШрдбреНрдпрд╛рд│рд╛рдЪреНрдпрд╛ рд╡реЗрд│рд╛ рдЖрд╣реЗрдд тАФ рддреБрдордЪреЗ рдЖрдХрдбреЗ рд╡реЗрдЧрд│реЗ рдЕрд╕рддреАрд▓.

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

profile рд╣реЗ рдЫрд╛рдкрддреЗ:

тФАтФА Dipika profiles the results program with cProfile: 300 runners, 2000 laps (we read CALL COUNTS, not times)
   find_runner 2000├Ч ┬╖ best_of 4├Ч ┬╖ results_slow 1├Ч
   find_runner walks the runner list every time: 297,098 comparisons for 2000 lookups тАФ the hot spot
тФАтФА fix: build a dict by bib once тЖТ   best_of 4├Ч ┬╖ results_fast 1├Ч
   find_runner comparisons now 0 ┬╖ same results: True ┬╖ best 100 m per house (ms) {'blue': 11000, 'green': 11006, 'red': 11006, 'yellow': 11005}

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

 500 laps тЖТ find_runner  500├Ч ┬╖  74,124 comparisons
1000 laps тЖТ find_runner 1000├Ч ┬╖ 149,300 comparisons
2000 laps тЖТ find_runner 2000├Ч ┬╖ 297,098 comparisons

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдПрдХрд╣реА рдЕрдВрджрд╛рдЬ рди рдХрд░рддрд╛ profile рдиреЗ рджреЛрд╖реА рд╢реЛрдзрд▓рд╛: find_runner, 2000 calls, рд╕реБрдорд╛рд░реЗ рдкреНрд░рддреНрдпреЗрдХреА 149 comparisons (рд╕рд░рд╛рд╕рд░реА 300 рдЪреЗ рдирд┐рдореНрдореЗ). рджреБрдкреНрдкрдЯ laps, рджреБрдкреНрдкрдЯ рдХрд╛рдо тАФ рдореНрд╣рдгрдЬреЗ 20,000 laps рдЕрд╕рд▓реЗрд▓реНрдпрд╛ results рджрд┐рд╡рд╢реА рд╕реБрдорд╛рд░реЗ 3 million comparisons рд╣реЛрддреАрд▓. рдЙрдкрд╛рдпрд╛рдиреЗ рдПрдХ data structure рдмрджрд▓рд▓реЗ рдЖрдгрд┐ 0 рдпрд╛рджреА-рдЪрд╛рд│рдгреНрдпрд╛рдВрд╕рд╣ рддреЗрдЪ results рджрд┐рд▓реЗ. рд╣реЗрдЪ рдпрд╛ рд╕рдВрдкреВрд░реНрдг рдХреЛрд░реНрд╕рдЪреЗ рдЪрдХреНрд░ рдЖрд╣реЗ: рдореЛрдЬрд╛ тЖТ profile рдХрд░рд╛ тЖТ рдПрдХ рдЧреЛрд╖реНрдЯ рдмрджрд▓рд╛ тЖТ рдкреБрдиреНрд╣рд╛ рдореЛрдЬрд╛.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ machine рд╡рд░ тАФ рдПрдХрд╛ script рдЪреЗ profile рдХрд░рд╛ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ function рдордзреНрдпреЗ рдЧреЗрд▓реЗрд▓реНрдпрд╛ рд╡реЗрд│реЗрдиреБрд╕рд╛рд░ sort рдХрд░реВрди result рдЙрдШрдбрд╛:

python3 -m cProfile -o results.prof make_results.py
python3 -c "import pstats; pstats.Stats('results.prof').sort_stats('cumtime').print_stats(15)"

py-spy рдЖрдзреАрдЪ рдЪрд╛рд▓реВ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ process рдЪреЗ samples рдШреЗрддреЛ, code рди рдмрджрд▓рддрд╛ рдЖрдгрд┐ рдХрдореА overhead рд╕рд╣ (рддреНрдпрд╛рд▓рд╛ sudo рд▓рд╛рдЧреВ рд╢рдХрддреЛ):

py-spy top --pid 12345                          # a live 'top' of Python functions
py-spy record -o flame.svg --pid 12345 --duration 30   # a flame graph
py-spy dump --pid 12345                         # every thread's stack, right now

Linux perf native code рд╕реБрджреНрдзрд╛ рдкрд╛рд╣рддреЛ; Python 3.12 рдкрд╛рд╕реВрди, CPython -X perf рд╡рд╛рдкрд░реВрди perf рдордзреНрдпреЗ Python function рдЪреА рдирд╛рд╡реЗ рджрд╛рдЦрд╡реВ рд╢рдХрддреЛ:

perf record -g -- python3 -X perf make_results.py
perf report

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ: рдЦрд▒реНрдпрд╛ service рдЪреЗ profile рдХрд░рдгреНрдпрд╛рдЪрд╛ рдорд╛рд░реНрдЧ рдареЗрд╡рд╛ тАФ pod рд╡рд░ py-spy, continuous profiler, рдХрд┐рдВрд╡рд╛ auth рдорд╛рдЧреЗ profiling endpoint. рдЦрд▒реНрдпрд╛ traffic рдЖрдгрд┐ рдЦрд▒реНрдпрд╛ data рдЦрд╛рд▓рдЪрд╛ hot spot рд╣рд╛рдЪ рджреБрд░реБрд╕реНрдд рдХрд░рдгреНрдпрд╛рд╕рд╛рд░рдЦрд╛ рдЕрд╕рддреЛ.

тПня╕П рдкреБрдвреЗ

find_runner рдиреЗ рдЕрд╕реЗ рдХрд╛рдо рдХреЗрд▓реЗ рдЬреЗ рдпрд╛рджреАрдЪреНрдпрд╛ рдЖрдХрд╛рд░рд╛рд╕реЛрдмрдд рд╡рд╛рдврдд рд╣реЛрддреЗ тАФ рдкреНрд░рддреНрдпреЗрдХ lap рд╕рд╛рдареА. рд╣рд╛ complexity рдЪрд╛ рдкреНрд░рд╢реНрди рдЖрд╣реЗ. рдкреБрдвреЗ: O(n┬▓) рд╡рд┐рд░реБрджреНрдз O(n log n), рдореЛрдЬреВрди.

git checkout lesson-04-complexity

ЁЯУЛ Lesson 03 тАФ Profiling: the coach's clipboard

ЁЯУН You are here: Lesson 03 of 12 ┬╖ Previous: lesson-02-benchmarking ┬╖ Next: lesson-04-complexity


ЁЯУж What's in this branch

Lessons 01тАУ02, plus profiling: before you change anything, find where the time goes. A profiler records which functions run, how often, and for how long. This lesson reads call counts from Python's built-in cProfile тАФ counts are the same on every machine, times are not тАФ and uses them to find one hot spot and fix it. profile() in perf/demo.py and results_slow, results_fast and profile_calls in perf/sim.py.

ЁЯзТ Explain like I'm 5

The sports-day results take ages to print. Everyone has a guess. "The printer is old!" "The sums are hard!"

Dipika the coach does not guess. She takes a clipboard ЁЯУЛ and stands next to the results desk. Every time someone does a job, she makes a tick next to that job.

At the end, the clipboard says:

And each "look up a runner" means walking down a list of 300 names, one by one. That is nearly 300,000 name checks. The printer was fine. The lookup was the problem.

So Dipika makes a card box sorted by bib number ЁЯЧВя╕П once. Now each lookup is one quick grab. Same results, and the slow job is gone.

ЁЯЧ║я╕П Diagram

flowchart LR
    prog["ЁЯПГ results program<br/>300 runners ┬╖ 2000 laps"] --> prof["ЁЯУЛ cProfile<br/>counts every call"]
    prof --> hot["find_runner 2000├Ч<br/>297,098 comparisons"]
    prof --> cold["best_of 4├Ч ┬╖ results_slow 1├Ч"]
    hot --> fix["ЁЯЧВя╕П build a dict by bib once"]
    fix --> after["find_runner 0 comparisons<br/>same results: True"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/performance/lesson-diagrams.html#l03

тЭУ What

ЁЯдФ Why

Because people guess wrong. The slow part is rarely where it "looks" slow, and speeding up code that takes 2% of the time can win at most 2%. Profiling first turns "make it faster" into "make this function faster", and then lesson 02's benchmark tells you if it worked.

ЁЯФз How (in this repo)

sports_day() in perf/sim.py makes 300 runners (in shuffled order) and 2000 laps. results_slow finds each lap's runner with find_runner, which walks the list and counts every comparison in COUNT["compares"]. results_fast builds a dict by bib once. profile_calls(fn, ...) runs the function under real cProfile and returns this module's functions with their ncalls (it skips comprehension helpers, which newer Python versions no longer show as separate functions).

ЁЯзк Try it

python3 perf/demo.py profile
python3 - <<'EOF'
import sys; sys.path.insert(0, "perf"); import sim
runners, laps = sim.sports_day()
for n_laps in (500, 1000, 2000):
    sim.COUNT["compares"] = 0
    _, rows = sim.profile_calls(sim.results_slow, runners, laps[:n_laps])
    print(f"{n_laps:>4} laps тЖТ find_runner {dict(rows)['find_runner']:>4}├Ч ┬╖ {sim.COUNT['compares']:>7,} comparisons")
EOF
python3 -c "import cProfile, sys; sys.path.insert(0, 'perf'); import sim; r, l = sim.sports_day(); cProfile.run('sim.results_slow(r, l)', sort='ncalls')" | head -14

The last command prints cProfile's full table. Its ncalls column says 2000 for find_runner; its time columns (tottime, cumtime, and the total in the first line) are real clock times тАФ your numbers will differ.

тЬЕ Verify тАФ what you should see

profile prints:

тФАтФА Dipika profiles the results program with cProfile: 300 runners, 2000 laps (we read CALL COUNTS, not times)
   find_runner 2000├Ч ┬╖ best_of 4├Ч ┬╖ results_slow 1├Ч
   find_runner walks the runner list every time: 297,098 comparisons for 2000 lookups тАФ the hot spot
тФАтФА fix: build a dict by bib once тЖТ   best_of 4├Ч ┬╖ results_fast 1├Ч
   find_runner comparisons now 0 ┬╖ same results: True ┬╖ best 100 m per house (ms) {'blue': 11000, 'green': 11006, 'red': 11006, 'yellow': 11005}

Your snippet prints:

 500 laps тЖТ find_runner  500├Ч ┬╖  74,124 comparisons
1000 laps тЖТ find_runner 1000├Ч ┬╖ 149,300 comparisons
2000 laps тЖТ find_runner 2000├Ч ┬╖ 297,098 comparisons

ЁЯПБ What you just proved

The profile named the culprit without a single guess: find_runner, 2000 calls, about 149 comparisons each (half of 300 on average). Twice the laps, twice the work тАФ so a results day with 20,000 laps would do about 3 million comparisons. The fix changed one data structure and gave the same results with 0 list walks. That is the loop of this whole course: measure тЖТ profile тЖТ change one thing тЖТ measure again.

тЪая╕П Common mistakes

ЁЯПн In production

On a real machine тАФ profile a script and open the result sorted by the time spent in each function:

python3 -m cProfile -o results.prof make_results.py
python3 -c "import pstats; pstats.Stats('results.prof').sort_stats('cumtime').print_stats(15)"

py-spy samples a process that is already running, with no code change and low overhead (it may need sudo):

py-spy top --pid 12345                          # a live 'top' of Python functions
py-spy record -o flame.svg --pid 12345 --duration 30   # a flame graph
py-spy dump --pid 12345                         # every thread's stack, right now

Linux perf sees native code too; since Python 3.12, CPython can show Python function names in perf with -X perf:

perf record -g -- python3 -X perf make_results.py
perf report

ЁЯПн Why this matters in production: keep a way to profile the real service тАФ py-spy on a pod, a continuous profiler, or a profiling endpoint behind auth. The hot spot under real traffic and real data is the one worth fixing.

тПня╕П Next

find_runner did work that grew with the size of the list тАФ for every lap. That is a question of complexity. Next: O(n┬▓) vs O(n log n), counted.

git checkout lesson-04-complexity
тЖР PreviousbenchmarkingNext тЖТcomplexity

This page is the lesson's README from the lesson-03-profiling branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.