# Performance Audit

Audit date: 2026-07-21

## Load Tests

Required commands:

```bash
ab -n 200 -c 10 http://localhost/strong-tower/
ab -n 500 -c 20 http://localhost/strong-tower/
```

Actual results:

| URL | Command | Complete | Failed | Requests/sec | Mean response | Longest | Transfer rate |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
| `/strong-tower/` | `ab -n 200 -c 10` | 200 | 0 | 395.58/sec | 25.279 ms | 224 ms | 9223.44 KB/sec |
| `/strong-tower/` | `ab -n 500 -c 20` | 500 | 0 | 899.83/sec | 22.227 ms | 141 ms | 20980.71 KB/sec |
| `/strong-tower/services.php` | `ab -n 200 -c 10` | 200 | 0 | 837.97/sec | 11.934 ms | 48 ms | 20252.85 KB/sec |
| `/strong-tower/blog.php` | `ab -n 200 -c 10` | 200 | 0 | 510.35/sec | 19.594 ms | 103 ms | 16705.61 KB/sec |
| `/strong-tower/blogs/stroke-in-seniors-what-you-need-to-know` | `ab -n 200 -c 10` | 200 | 0 | 439.79/sec | 22.738 ms | 146 ms | 9254.93 KB/sec |
| `/strong-tower/testimonials.php` | `ab -n 200 -c 10` | 200 | 0 | 580.42/sec | 17.229 ms | 78 ms | 13146.82 KB/sec |

No PHP or MySQL errors were emitted during these load-test command runs.

## Route Verification

Local route smoke test results:

- `/`: 200
- `/index.php`: 200
- `/about.php`: 200
- `/services.php`: 200
- `/blog.php`: 200
- `/blogs/stroke-in-seniors-what-you-need-to-know`: 200
- `/testimonials.php`: 200
- `/contact.php`: 200
- `/jobs.php`: 200
- `/request-care.php`: 200
- `/customer-service.php`: 200
- `/admin/`: 302 to login
- `/admin/login.php`: 200
- `/not-real-page`: 404

## Query Audit

Expected query counts after cache warm-up:

- Homepage: 1 DB query for `site_settings`/content on cold cache; 0-1 public content queries after cache warm-up depending on expired content keys.
- Services: 1 `services` query on cold cache; 0 after cache warm-up.
- Blog listing: 2 cold-cache queries, one count and one paginated listing; 0 after cache warm-up.
- Blog detail: 1 cold-cache slug lookup; 0 after cache warm-up.
- Admin dashboard: 7 bounded listing queries, uncached by design.

Public `SELECT *` queries were replaced on services, blog listing, blog detail, testimonials, and admin login. Admin dashboard listings are bounded with `LIMIT 100`.

N+1 findings: none found on public pages. Blog listing and testimonials render from a single bounded result set each; blog detail uses one slug lookup.

## New Indexes

- `services(active, sort_order, title)`
- `testimonials(approved, created_at)`
- `referrals(status, created_at)`
- `referrals(referrer_id, created_at)`
- `referral_visits(created_at)`
- `blog_posts(created_at)`
- `care_requests(created_at)`
- `contact_messages(created_at)`
- `customer_service_tickets(created_at)`
- `job_applications(created_at)`
- `form_rate_limits(last_attempt)`
- `form_submission_locks(form_name, submission_hash)` unique
- `form_submission_locks(created_at)`

Existing useful indexes retained or restored locally:

- `blog_posts(status, created_at)`
- `blog_posts(category, created_at)`
- `care_requests(status, created_at)`
- `contact_messages(status, created_at)`
- `customer_service_tickets(status, created_at)`
- `job_applications(status, created_at)`

## Cache

- Directory: `cache/`
- Protection: `cache/.htaccess` denies public reads and directory indexes.
- Items: site settings, page content, published services, approved testimonials, blog categories, paginated published blog listings, blog counts, individual published blog posts.
- TTL: 300 seconds for settings/page content, 600 seconds for services/testimonials/blog posts, 3600 seconds for blog categories.
- Atomic writes: PHP cache payload is written to a `.tmp` file with `LOCK_EX`, then renamed into place.
- Invalidation: admin settings/content/services/blog/testimonial moderation writes call `simple_cache_clear()`.
- Failure behavior: cache read/write failures fall back to database queries or existing safe fallback content.
- Local verification: after correcting XAMPP writable directory permissions, `/admin/health-check.php` reported database/cache/uploads `ok`, and visiting `services.php` created cache payload files in `cache/`.

## EXPLAIN Evidence

- Published blog listing used `idx_blog_posts_status_created` (`type=ref`, `key=idx_blog_posts_status_created`).
- Blog detail used the unique `slug` index (`type=const`, `key=slug`).
- Admin care request ordering used `idx_care_created` (`type=index`, `key=idx_care_created`).
- Services and testimonials listed the new indexes as possible keys, but MySQL chose table scans/filesort because the local tables had only 12 and 2 rows respectively. This is expected for tiny tables; the indexes are present for production growth.

## Images

- Original raster total: 6612 KiB.
- Optimized WebP total: 716 KiB.
- WebP files created: 24.
- WebP derivatives resized: 3 oversized images.
- Approximate savings for WebP-capable browsers: 5896 KiB, about 89%.
- Main homepage hero is eager with high fetch priority.
- Below-fold/supporting images are lazy loaded through `responsive_image()`.
