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

ЁЯПЖ рдзрдбрд╛ 12 тАФ Load tests, budgets рдЖрдгрд┐ рд╕рдВрдкреВрд░реНрдг рдЪрд┐рддреНрд░: рдкрд╛рддреНрд░рддрд╛ рд╡реЗрд│рд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 12 ┬╖ рдорд╛рдЧреЗ: lesson-11-web-performance ┬╖ рд╢реЗрд╡рдЯрдЪрд╛ рдзрдбрд╛ ЁЯОУ


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

рдзрдбреЗ 01тАУ11, рдЖрдгрд┐ рддреЗ рд╡реЗрдЧрд╡рд╛рди рдареЗрд╡рдгреЗ: load test traffic рдЦрд╛рд▓реА latency рдХреБрдареЗ рд╡рд│рддреЗ (knee) рддреЗ рд╢реЛрдзрддреЛ, рдЖрдгрд┐ CI рдордзреАрд▓ performance budget рдПрдЦрд╛рджреНрдпрд╛ рдмрджрд▓рд╛рдиреЗ рдЧреЛрд╖реНрдЯреА рдмрд┐рдШрдбрд▓реНрдпрд╛ рддрд░ build fail рдХрд░рддреЛ тАФ рдкреНрд░рддрд┐ page queries, bytes, LCP, p95. рдзрдбрд╛ рдХреЛрд░реНрд╕рдЪреНрдпрд╛ рд╕рдВрдкреВрд░реНрдг рдирдХрд╛рд╢рд╛рдиреЗ рд╕рдВрдкрддреЛ. perf/demo.py рдордзреАрд▓ budgets() рдЖрдгрд┐ perf/sim.py рдордзреАрд▓ load_test рдЖрдгрд┐ check_budget.

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

рдЦрд▒реНрдпрд╛ рдХреНрд░реАрдбрд╛ рджрд┐рдирд╛рдЖрдзреА рд╢рд╛рд│рд╛ рдПрдХ рд░рдВрдЧреАрдд рддрд╛рд▓реАрдо рдШреЗрддреЗ. ЁЯПЯя╕П рддреЗ рдкрд╛рдгреНрдпрд╛рдЪреНрдпрд╛ рдЯреЗрдмрд▓рд╛рд╡рд░реВрди рдЕрдзрд┐рдХрд╛рдзрд┐рдХ рдзрд╛рд╡рдкрдЯреВ рдкрд╛рдард╡рддрд╛рдд, рддреЗ рдХреЗрд╡реНрд╣рд╛ рд╣рд│реВ рд╣реЛрддреЗ рд╣реЗ рдкрд╛рд╣рдгреНрдпрд╛рд╕рд╛рдареА.

рдореНрд╣рдгреВрди рдЖрддрд╛ рддреНрдпрд╛рдВрдирд╛ рдорд╛рд╣реАрдд рдЖрд╣реЗ: 90 рдЪреНрдпрд╛ рдмрд▒реНрдпрд╛рдЪ рдЦрд╛рд▓реА рд░рд╛рд╣рд╛.

рдордЧ рджреАрдкрд┐рдХрд╛ рднрд┐рдВрддреАрд╡рд░ рдкрд╛рддреНрд░рддрд╛ рд╡реЗрд│рд╛ рд▓рд┐рд╣рд┐рддреЗ ЁЯУЭ: "рдирд┐рдХрд╛рд▓рд╛рдЪреЗ page office рдЪреНрдпрд╛ рдЬрд╛рд╕реНрддреАрдд рдЬрд╛рд╕реНрдд 5 рдлреЗрд▒реНрдпрд╛ рд╡рд╛рдкрд░реВ рд╢рдХрддреЗ. рддреЗ 2.5 рд╕реЗрдХрдВрджрд╛рдВрдкреЗрдХреНрд╖рд╛ рдХрдореА рд╡реЗрд│рд╛рдд рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ. рддреНрдпрд╛рдЪреЗ рд╡рдЬрди рдЬрд╛рд╕реНрддреАрдд рдЬрд╛рд╕реНрдд 500 KB рдЕрд╕реВ рд╢рдХрддреЗ." рдХреЛрдгреАрд╣реА рдирд┐рдХрд╛рд▓рд╛рдЪрд╛ program рдмрджрд▓рд▓рд╛ рдХреА рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА рдПрдХ рдорджрддрдиреАрд╕ рдкрд╛рддреНрд░рддрд╛ рд╡реЗрд│рд╛ рддрдкрд╛рд╕рддреЗ. рдмрджрд▓рд╛рдиреЗ рддреНрдпрд╛рддрд▓реА рдПрдЦрд╛рджреА рдореЛрдбрд▓реА, рддрд░ рдорджрддрдиреАрд╕ рдХреНрд░реАрдбрд╛ рджрд┐рдирд╛рдЪреНрдпрд╛ рджрд░рдореНрдпрд╛рди рдирд╡реНрд╣реЗ, рддрд░ рдЖрдзреАрдЪ "рдирд╛рд╣реА тАФ рдкреБрдиреНрд╣рд╛ рдкреНрд░рдпрддреНрди рдХрд░рд╛" рдЕрд╕реЗ рд╕рд╛рдВрдЧрддреЗ. тЭМтЮбя╕ПтЬЕ

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

flowchart LR
    lt["ЁЯПЯя╕П load test, open model<br/>10 ms a request"] --> r50["50/s тЖТ p95 60 ms"]
    r50 --> r70["70/s тЖТ p95 109 ms"]
    r70 --> r90["90/s тЖТ p95 280 ms"]
    r90 --> r110["110/s тЖТ p95 3122 ms<br/>above capacity"]
    b["ЁЯУЭ budget: queries тЙд 5 ┬╖ LCP тЙд 2500 ms<br/>weight тЙд 500 KB ┬╖ p95 тЙд 100 ms"] --> before["before: 51 ┬╖ 5020 ┬╖ 2210 ┬╖ 109 тЖТ FAIL"]
    b --> after["after: 1 ┬╖ 1055 ┬╖ 452.5 ┬╖ 23 тЖТ PASS"]

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг performance рд╢рд╛рдВрддрдкрдгреЗ рдЦрд╛рд▓рд╛рд╡рддреЗ. рдХреЛрдгреАрд╣реА рдореБрджреНрджрд╛рдо N+1 рдЬреЛрдбрдд рдирд╛рд╣реА; рддреЛ рдкреНрд░рддреНрдпреЗрдХ functional test рдкрд╛рд░ рдХреЗрд▓реЗрд▓реНрдпрд╛ рдЫреЛрдЯреНрдпрд╛ рдмрджрд▓рд╛рддреВрди рдпреЗрддреЛ. CI рдордзреАрд▓ budget рддреЛ рд▓рд┐рд╣рд┐рд▓рд╛ рдЧреЗрд▓рд╛ рддреНрдпрд╛рдЪ рджрд┐рд╡рд╢реА рдкрдХрдбрддреЛ, рдЬреЗрд╡реНрд╣рд╛ рдЙрдкрд╛рдп рдПрдХрд╛ рдУрд│реАрдЪрд╛ рдЕрд╕рддреЛ. рдЖрдгрд┐ рдореЛрдареНрдпрд╛ рджрд┐рд╡рд╕рд╛рдЖрдзреАрдЪрд╛ load test knee рдХреБрдареЗ рдЖрд╣реЗ рддреЗ рд╕рд╛рдВрдЧрддреЛ, рдЬреЗрд╡реНрд╣рд╛ рддреЛ рд╣рд▓рд╡рд╛рдпрд▓рд╛ рддреБрдордЪреНрдпрд╛рдХрдбреЗ рдЕрдЬреВрди рд╡реЗрд│ рдЕрд╕рддреЛ.

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

perf/sim.py рдордзреАрд▓ load_test(rates, service_ms, n, seed) рдкреНрд░рддреНрдпреЗрдХ rate рд╡рд░ рдзрдбрд╛ 09 рдЪреЗ simulate_queue (рдПрдХ server, random arrivals, open model) рдЪрд╛рд▓рд╡рддреЗ рдЖрдгрд┐ p50, p95 рдЖрдгрд┐ p99 рдкрд░рдд рджреЗрддреЗ. check_budget(measured, budget) рдкреНрд░рддреНрдпреЗрдХ рдореЛрдЬрдорд╛рдкрд╛рдЪреА рддреНрдпрд╛рдЪреНрдпрд╛ рдорд░реНрдпрд╛рджреЗрд╢реА рддреБрд▓рдирд╛ рдХрд░рддреЗ рдЖрдгрд┐ rows рдЖрдгрд┐ рдПрдХреВрдг pass/fail рдкрд░рдд рджреЗрддреЗ. perf/demo.py рдордзреАрд▓ budgets() "before" values рдЖрдзреАрдЪреНрдпрд╛ рдзрдбреНрдпрд╛рдВрдордзреВрди live рдореЛрдЬрддреЗ тАФ рдзрдбрд╛ 07 рдЪреНрдпрд╛ N+1 рдордзреАрд▓ query count, рдзрдбрд╛ 11 рдордзрд▓реЗ LCP рдЖрдгрд┐ bytes тАФ рдЖрдгрд┐ "after" values рддреНрдпрд╛рдВрдЪреНрдпрд╛ рдЙрдкрд╛рдпрд╛рдВрдордзреВрди. "After" p95 рдЕрд╕реЗ рдЧреГрд╣реАрдд рдзрд░рддреЛ рдХреА profile рдХрд░реВрди рдХреЗрд▓реЗрд▓реНрдпрд╛ рдЙрдкрд╛рдпрд╛рдиреЗ service time 10 ms рд╡рд░реВрди 5 ms рд╡рд░ рдЖрдгрд▓рд╛: рд╣реЗ model рдЪреЗ рдПрдХ input рдЖрд╣реЗ.

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

python3 perf/demo.py budgets
python3 - <<'EOF'
import sys; sys.path.insert(0, "perf"); from sim import load_test, check_budget
for r, p50, p95, p99 in load_test((60, 80, 95), service_ms=5.0):
    print(f"service 5 ms, {r} req/s тЖТ p95 {p95} ms")
rows, ok = check_budget({"SQL queries per page": 7, "page weight KB": 480}, {"SQL queries per page": 5, "page weight KB": 500})
for name, value, limit, good in rows: print(f"{name}: {value} / {limit} {'ok' if good else 'OVER'}")
print("PASS" if ok else "FAIL")
sys.exit(0 if ok else 1)
EOF
echo "exit code: $?"
python3 perf/demo.py
python3 perf/test_perf.py

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

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

тФАтФА a load test (simulated, open model): one server, 10 ms per request, capacity about 100 requests/s
    50 req/s тЖТ p50    14 ms ┬╖ p95    60 ms ┬╖ p99    98 ms
    70 req/s тЖТ p50    25 ms ┬╖ p95   109 ms ┬╖ p99   160 ms
    90 req/s тЖТ p50    64 ms ┬╖ p95   280 ms ┬╖ p99   411 ms
   110 req/s тЖТ p50  1716 ms ┬╖ p95  3122 ms ┬╖ p99  3187 ms
   the knee: past about 80% busy, p95 climbs fast ┬╖ above capacity the queue only grows
тФАтФА budget check, before the fixes: SQL queries per page 51 / 5 тЭМ ┬╖ LCP ms (simulated) 5020 / 2500 тЭМ ┬╖ page weight KB 2210 / 500 тЭМ ┬╖ p95 ms at 70 req/s 109 / 100 тЭМ тЖТ FAIL (the CI job exits 1)
тФАтФА budget check, after the fixes : SQL queries per page 1 / 5 тЬЕ ┬╖ LCP ms (simulated) 1055 / 2500 тЬЕ ┬╖ page weight KB 452.5 / 500 тЬЕ ┬╖ p95 ms at 70 req/s 23 / 100 тЬЕ тЖТ PASS

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

service 5 ms, 60 req/s тЖТ p95 22 ms
service 5 ms, 80 req/s тЖТ p95 25 ms
service 5 ms, 95 req/s тЖТ p95 28 ms
SQL queries per page: 7 / 5 OVER
page weight KB: 480 / 500 ok
FAIL
exit code: 1

рдкреВрд░реНрдг demo тЬЕ done тАФ every runner timed, the relay is faster рдиреЗ рд╕рдВрдкрддреЛ, рдЖрдгрд┐ tests 12/12 passed рдиреЗ.

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

50 рд╡рд░реВрди 90 requests/s (1.8├Ч load) рд╡рд░, p95 60 рд╡рд░реВрди 280 ms рд╡рд░ рдЧреЗрд▓рд╛ (4.7├Ч); 110/s рд╡рд░, capacity рдЪреНрдпрд╛ рдкрд▓реАрдХрдбреЗ, рддреЛ 3 рд╕реЗрдХрдВрдж рдУрд▓рд╛рдВрдбреВрди рдЧреЗрд▓рд╛ рдЖрдгрд┐ рддрд░реАрд╣реА рд╡рд╛рдврддрдЪ рд╣реЛрддрд╛. Service time рдЕрд░реНрдзрд╛ рдХреЗрд▓реНрдпрд╛рдиреЗ knee рд╣рд▓рд▓рд╛: 95 requests/s рд╡рд░ p95 рдлрдХреНрдд 28 ms рд╣реЛрддрд╛. Budget рдиреЗ "before" build рд▓рд╛ рдЪрд╛рд░рд╣реА rows рд╡рд░ fail рдХреЗрд▓реЗ рдЖрдгрд┐ "after" build pass рдХреЗрд▓реЗ. рдЖрдгрд┐ рдорд░реНрдпрд╛рджреЗрдкрд▓реАрдХрдбреЗ рдЧреЗрд▓реЗрд▓реА рдПрдХрдЪ row тАФ 5 рдЪреНрдпрд╛ budget рд╡рд┐рд░реБрджреНрдз 7 queries тАФ script рд▓рд╛ exit 1 рдХрд░рд╛рдпрд▓рд╛ рдкреБрд░реЗрд╢реА рд╣реЛрддреА, рдЖрдгрд┐ CI job рд▓рд╛рд▓ рд╣реЛрдгреНрдпрд╛рд╕рд╛рдареА рдПрд╡рдвреЗрдЪ рд▓рд╛рдЧрддреЗ.

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

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

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ open-model executor рдЖрдгрд┐ run fail рдХрд░рдгрд╛рд▒реНрдпрд╛ thresholds рд╕рд╣ k6 load test (threshold fail рдЭрд╛рд▓реНрдпрд╛рд╕ k6 non-zero exit рдХрд░рддреЛ):

import http from 'k6/http';
export const options = {
  scenarios: { steady: { executor: 'constant-arrival-rate', rate: 70, timeUnit: '1s',
                         duration: '5m', preAllocatedVUs: 50, maxVUs: 200 } },
  thresholds: { http_req_duration: ['p(95)<100'], http_req_failed: ['rate<0.01'] },
};
export default function () { http.get('https://staging.results.school.example/results'); }
k6 run results-load.js

рд╕рд╛рдзреНрдпрд╛ test рд╕рд╛рд░рдЦрд╛ query-count budget (Django):

class ResultsPageBudget(TestCase):
    def test_results_page_queries(self):
        make_runners(50)
        with self.assertNumQueries(2):        # fails if an N+1 sneaks back in
            self.client.get("/results/")

рдкреНрд░рддреНрдпреЗрдХ pull request рд╡рд░ budgets рдЪрд╛рд▓рд╡рдгрд╛рд░рд╛ GitHub Actions job, рдЖрдгрд┐ page рд╕рд╛рдареА Lighthouse CI:

name: performance-budgets
on: [pull_request]
jobs:
  budgets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install -r requirements.txt
      - run: python manage.py test budgets          # query counts, response sizes
      - run: npx @lhci/cli autorun                  # LCP, bytes, CLS against lighthouserc.json assertions

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдкреБрдврдЪреНрдпрд╛ feature рдЖрдзреА budget рд▓рд┐рд╣реВрди рдареЗрд╡рд╛, count рд╡рд░ рдЖрдзрд╛рд░рд┐рдд рднрд╛рдЧ рдЖрдЬрдЪ CI рдордзреНрдпреЗ рдШрд╛рд▓рд╛, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рдореЛрдареНрдпрд╛ рдХрд╛рд░реНрдпрдХреНрд░рдорд╛рдЖрдзреА рддреБрдордЪреНрдпрд╛ рд╕рд░реНрд╡рд╛рдд busy рддрд╛рд╕рд╛рдЪреНрдпрд╛ 1.5тАУ2├Ч рд╡рд░ load test рдЪрд╛рд▓рд╡рд╛. рдЬрд┐рдереЗ CI рдордзреНрдпреЗ рд╡реЗрд│ рддрдкрд╛рд╕рд╛рд╡реА рд▓рд╛рдЧрддреЗ, рддрд┐рдереЗ рддреНрдпрд╛рдЪ machine рд╡рд░рдЪреНрдпрд╛ baseline рд╢реА рддреБрд▓рдирд╛ рдХрд░рд╛, рдХрд┐рдВрд╡рд╛ рддреНрдпрд╛рдРрд╡рдЬреА CPU instructions рдореЛрдЬрд╛ (Cachegrind-рдЖрдзрд╛рд░рд┐рдд tools рд╣реЗ рдХрд░рддрд╛рдд).

ЁЯОУ рдХреНрд░реАрдбрд╛ рджрд┐рди рд╕рдВрдкрд▓рд╛

рд╕реНрдЯреЙрдкрд╡реЙрдЪ mean рдирд╡реНрд╣реЗ рддрд░ percentiles рд╕рд╛рдВрдЧрддрд╛рдд тЖТ benchmark warm up рд╣реЛрддреЛ, рдкреБрдирд░рд╛рд╡реГрддреНрддреА рдХрд░рддреЛ рдЖрдгрд┐ рддреНрдпрд╛рдЪрд╛ рдкрд╕рд╛рд░рд╛ рджрд╛рдЦрд╡рддреЛ тЖТ рдкреНрд░рд╢рд┐рдХреНрд╖рд┐рдХреЗрдЪрд╛ clipboard hot spot рд╢реЛрдзрддреЛ тЖТ рдпреЛрдЧреНрдп algorithm рд╡рдХреНрд░ рдмрджрд▓рддреЛ тЖТ generators рдПрдХрд╛ рд╡реЗрд│реА рдПрдХрдЪ рдЕрдбрдерд│рд╛ рдзрд░рддрд╛рдд тЖТ scoreboard рд▓рдХреНрд╖рд╛рдд рдареЗрд╡рддреЛ, рдЖрдгрд┐ рддреНрдпрд╛рдЪрд╛ hit ratio рдХрд┐рддреА рддреЗ рд╕рд╛рдВрдЧрддреЛ тЖТ рдкреНрд░рддреНрдпреЗрдХ рдмреЕрдЯрди-рдмрджрд▓рд╛рд▓рд╛ рдПрдХ рдлреЗрд░реА рд▓рд╛рдЧрддреЗ, рдореНрд╣рдгреВрди batch рдХрд░рд╛ тЖТ рд╡рд╛рдЯ рдкрд╛рд╣рдгреЗ overlap рд╣реЛрддреЗ, Python рдХрд╛рдорд╛рд▓рд╛ рдмреЕрдЯрди рд▓рд╛рдЧрддреЗ тЖТ 100% busy рдЬрд╡рд│ рдкрд╛рдгреНрдпрд╛рдЪреНрдпрд╛ рдЯреЗрдмрд▓рд╛рд╡рд░рдЪреА рдкреНрд░рддреАрдХреНрд╖рд╛ рд╕реНрдлреЛрдЯрдХ рд╡рд╛рдврддреЗ, рдЖрдгрд┐ рд╕рд░реНрд╡рд╛рдд рд╣рд│реВ рдЯрдкреНрдкрд╛ рд░рд┐рд▓реЗрд▓рд╛ рдорд░реНрдпрд╛рджрд╛ рдШрд╛рд▓рддреЛ тЖТ index 10,000 рд╡рд╛рдЪрдгреНрдпрд╛рдРрд╡рдЬреА 20 рдкрддреНрд░рдХреЗ рд╢реЛрдзрддреЛ тЖТ page рдЪреЗ рдореВрд▓реНрдпрдорд╛рдкрди рдкрд╛рд▓рдХрд╛рдВрдЪреНрдпрд╛ phone рд╡рд░ рд╣реЛрддреЗ тЖТ рдЖрдгрд┐ CI рдордзреАрд▓ рдкрд╛рддреНрд░рддрд╛ рд╡реЗрд│рд╛ рд╣реЗ рд╕рдЧрд│реЗ рд╡реЗрдЧрд╡рд╛рди рдареЗрд╡рддрд╛рдд. рддреБрдореНрд╣реА рдлрдХреНрдд performance рдЪреНрдпрд╛ рдпреБрдХреНрддреНрдпрд╛ рд╢рд┐рдХрд▓реНрдпрд╛ рдирд╛рд╣реАрдд тАФ рддреБрдореНрд╣реА рдХреЛрдгрддрд╛рд╣реА рд╣рд│реВ program рдШреЗрдКрди, рддреЛ рдореЛрдЬреВрди, рдорд╣рддреНрддреНрд╡рд╛рдЪреА рдПрдХрдЪ рдЧреЛрд╖реНрдЯ рд╢реЛрдзреВрди, рддреА рджреБрд░реБрд╕реНрдд рдХрд░реВрди, рддреА рджреБрд░реБрд╕реНрддрдЪ рд░рд╛рд╣рд┐рд▓реА рд╣реЗ рд╕рд┐рджреНрдз рдХрд░реВ рд╢рдХрддрд╛. ЁЯПОя╕ПЁЯПЖЁЯОУ

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

рдЗрддрд░ рд╢рд╛рд│рд╛. Scaling school рдПрдХ machine рдкреБрд░реЗрд╢реА рдирд╕реЗрд▓ рддреЗрд╡реНрд╣рд╛ machines рдЬреЛрдбрддреЗ; Observability school production рдордзрд▓реЗ percentiles рдЖрдгрд┐ traces рдЧреЛрд│рд╛ рдХрд░рддреЗ; Database school queries рдордзреНрдпреЗ рдЕрдзрд┐рдХ рдЦреЛрд▓рд╡рд░ рдЬрд╛рддреЗ; DSA school algorithms рдордзреНрдпреЗ; School portal рдордзреНрдпреЗ рдмрд╛рдХреА рд╕рдЧрд│реЗ рдЖрд╣реЗ.

git checkout main
python3 perf/demo.py     # one last run, for fun

ЁЯПЖ Lesson 12 тАФ Load tests, budgets & the whole picture: qualifying times

ЁЯУН You are here: Lesson 12 of 12 ┬╖ Previous: lesson-11-web-performance ┬╖ The last lesson ЁЯОУ


ЁЯУж What's in this branch

Lessons 01тАУ11, plus keeping it fast: a load test finds where latency bends under traffic (the knee), and a performance budget in CI fails the build when a change makes things worse тАФ queries per page, bytes, LCP, p95. The lesson ends with the whole map of the course. budgets() in perf/demo.py and load_test and check_budget in perf/sim.py.

ЁЯзТ Explain like I'm 5

Before the real sports day, the school holds a rehearsal. ЁЯПЯя╕П They send more and more runners through the water table to see when it gets slow.

So now they know: stay well below 90.

Then Dipika writes qualifying times on the wall ЁЯУЭ: "The results page may use at most 5 trips to the office. It must appear in under 2.5 seconds. It may weigh at most 500 KB." Every time someone changes the results program, a helper checks the qualifying times. If the change breaks one, the helper says "no тАФ try again" before sports day, not during it. тЭМтЮбя╕ПтЬЕ

ЁЯЧ║я╕П Diagram

flowchart LR
    lt["ЁЯПЯя╕П load test, open model<br/>10 ms a request"] --> r50["50/s тЖТ p95 60 ms"]
    r50 --> r70["70/s тЖТ p95 109 ms"]
    r70 --> r90["90/s тЖТ p95 280 ms"]
    r90 --> r110["110/s тЖТ p95 3122 ms<br/>above capacity"]
    b["ЁЯУЭ budget: queries тЙд 5 ┬╖ LCP тЙд 2500 ms<br/>weight тЙд 500 KB ┬╖ p95 тЙд 100 ms"] --> before["before: 51 ┬╖ 5020 ┬╖ 2210 ┬╖ 109 тЖТ FAIL"]
    b --> after["after: 1 ┬╖ 1055 ┬╖ 452.5 ┬╖ 23 тЖТ PASS"]

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

тЭУ What

ЁЯдФ Why

Because performance decays quietly. Nobody adds an N+1 on purpose; it arrives in a small change that passed every functional test. A budget in CI catches it on the day it is written, when the fix is one line. And a load test before a big day tells you where the knee is while you still have time to move it.

ЁЯФз How (in this repo)

load_test(rates, service_ms, n, seed) in perf/sim.py runs lesson 09's simulate_queue (one server, random arrivals, open model) at each rate and returns p50, p95 and p99. check_budget(measured, budget) compares each measurement with its limit and returns the rows and an overall pass/fail. budgets() in perf/demo.py measures the "before" values live from earlier lessons тАФ the query count from lesson 07's N+1, LCP and bytes from lesson 11 тАФ and the "after" values from their fixes. The "after" p95 assumes the profiled fix cut the service time from 10 ms to 5 ms: an input of the model.

ЁЯзк Try it

python3 perf/demo.py budgets
python3 - <<'EOF'
import sys; sys.path.insert(0, "perf"); from sim import load_test, check_budget
for r, p50, p95, p99 in load_test((60, 80, 95), service_ms=5.0):
    print(f"service 5 ms, {r} req/s тЖТ p95 {p95} ms")
rows, ok = check_budget({"SQL queries per page": 7, "page weight KB": 480}, {"SQL queries per page": 5, "page weight KB": 500})
for name, value, limit, good in rows: print(f"{name}: {value} / {limit} {'ok' if good else 'OVER'}")
print("PASS" if ok else "FAIL")
sys.exit(0 if ok else 1)
EOF
echo "exit code: $?"
python3 perf/demo.py
python3 perf/test_perf.py

тЬЕ Verify тАФ what you should see

budgets prints:

тФАтФА a load test (simulated, open model): one server, 10 ms per request, capacity about 100 requests/s
    50 req/s тЖТ p50    14 ms ┬╖ p95    60 ms ┬╖ p99    98 ms
    70 req/s тЖТ p50    25 ms ┬╖ p95   109 ms ┬╖ p99   160 ms
    90 req/s тЖТ p50    64 ms ┬╖ p95   280 ms ┬╖ p99   411 ms
   110 req/s тЖТ p50  1716 ms ┬╖ p95  3122 ms ┬╖ p99  3187 ms
   the knee: past about 80% busy, p95 climbs fast ┬╖ above capacity the queue only grows
тФАтФА budget check, before the fixes: SQL queries per page 51 / 5 тЭМ ┬╖ LCP ms (simulated) 5020 / 2500 тЭМ ┬╖ page weight KB 2210 / 500 тЭМ ┬╖ p95 ms at 70 req/s 109 / 100 тЭМ тЖТ FAIL (the CI job exits 1)
тФАтФА budget check, after the fixes : SQL queries per page 1 / 5 тЬЕ ┬╖ LCP ms (simulated) 1055 / 2500 тЬЕ ┬╖ page weight KB 452.5 / 500 тЬЕ ┬╖ p95 ms at 70 req/s 23 / 100 тЬЕ тЖТ PASS

Your snippet prints:

service 5 ms, 60 req/s тЖТ p95 22 ms
service 5 ms, 80 req/s тЖТ p95 25 ms
service 5 ms, 95 req/s тЖТ p95 28 ms
SQL queries per page: 7 / 5 OVER
page weight KB: 480 / 500 ok
FAIL
exit code: 1

The full demo ends with тЬЕ done тАФ every runner timed, the relay is faster, and the tests with 12/12 passed.

ЁЯПБ What you just proved

From 50 to 90 requests/s (1.8├Ч the load), p95 went from 60 to 280 ms (4.7├Ч); at 110/s, above capacity, it passed 3 seconds and was still climbing. Halving the service time moved the knee: at 95 requests/s, p95 was only 28 ms. The budget failed the "before" build on all four rows and passed the "after" one. And a single row over its limit тАФ 7 queries against a budget of 5 тАФ was enough to make the script exit 1, which is all a CI job needs to go red.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ a k6 load test with an open-model executor and thresholds that fail the run (k6 exits non-zero when a threshold fails):

import http from 'k6/http';
export const options = {
  scenarios: { steady: { executor: 'constant-arrival-rate', rate: 70, timeUnit: '1s',
                         duration: '5m', preAllocatedVUs: 50, maxVUs: 200 } },
  thresholds: { http_req_duration: ['p(95)<100'], http_req_failed: ['rate<0.01'] },
};
export default function () { http.get('https://staging.results.school.example/results'); }
k6 run results-load.js

A query-count budget as a normal test (Django):

class ResultsPageBudget(TestCase):
    def test_results_page_queries(self):
        make_runners(50)
        with self.assertNumQueries(2):        # fails if an N+1 sneaks back in
            self.client.get("/results/")

A GitHub Actions job that runs the budgets on every pull request, and Lighthouse CI for the page:

name: performance-budgets
on: [pull_request]
jobs:
  budgets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install -r requirements.txt
      - run: python manage.py test budgets          # query counts, response sizes
      - run: npx @lhci/cli autorun                  # LCP, bytes, CLS against lighthouserc.json assertions

ЁЯПн Why this matters in production: write the budget down before the next feature, put the count-based parts in CI today, and run a load test at 1.5тАУ2├Ч your busiest hour before every big event. Where timing must be checked in CI, compare against a baseline on the same machine, or count CPU instructions instead (Cachegrind-based tools do this).

ЁЯОУ Sports day is over

The stopwatches report percentiles, not the mean тЖТ a benchmark warms up, repeats and shows its spread тЖТ the coach's clipboard finds the hot spot тЖТ the right algorithm changes the curve тЖТ generators hold one hurdle at a time тЖТ the scoreboard remembers, and its hit ratio says how much тЖТ every hand-off costs a trip, so batch тЖТ waiting overlaps, Python work needs the baton тЖТ the water table's wait explodes near 100% busy, and the slowest leg caps the relay тЖТ the index finds 20 sheets instead of reading 10,000 тЖТ the page is judged on a parent's phone тЖТ and qualifying times in CI keep it all fast. You didn't just learn performance tricks тАФ you can take any slow program, measure it, find the one thing that matters, fix it, and prove it stayed fixed. ЁЯПОя╕ПЁЯПЖЁЯОУ

тПня╕П Next

The other schools. The Scaling school adds machines when one is not enough; the Observability school collects the percentiles and traces in production; the Database school goes deeper on queries; the DSA school on algorithms; the School portal has the rest.

git checkout main
python3 perf/demo.py     # one last run, for fun
тЖР Previousweb performanceFinished! Take the quiz тЖТcheck what stuck

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