# Game Performance & QPS Analysis (Dice Project)

> This document describes the current dice game backend architecture, key endpoints, approximate QPS capacity, and concrete optimization suggestions.  
> It is written in plain ASCII to avoid any encoding or garbled-text issues.

---

## 1. Runtime Environment Overview

| Item            | Description                                                                 |
|-----------------|-----------------------------------------------------------------------------|
| Framework       | Webman (on top of Workerman, long-running PHP workers)                     |
| Business module | `app/dice` (players, play records, rewards, lottery pool, dashboard, etc.) |
| HTTP endpoint   | Typically `http://0.0.0.0:6688`                                             |
| Worker count    | Recommended: `cpu_count() * 4` (for example, 8 cores -> 32 workers)        |
| Database        | MySQL + ThinkORM, with connection pool (for example, max=20 / min=2)       |
| Cache           | Redis + think-cache, with connection pool (for example, max=20 / min=2)    |

Recommended production configuration:

- Use Redis as default cache: in `.env`, set `CACHE_MODE=redis`.
- Align `DB_POOL_MAX` / `REDIS_POOL_MAX` with worker count and MySQL `max_connections`.

---

## 2. Key Endpoints and Load Characteristics

From the codebase, main game-related endpoints can be roughly grouped into:

- High-load: multiple DB writes plus Redis and non-trivial business logic.
- Medium-load: some DB writes or heavier DB reads.
- Light: mostly cache reads or simple DB queries.

### 2.1 Main endpoints (current project)

Below is a simplified view; exact routes live in the dice module and API controllers:

| Endpoint                            | Purpose                  | Load level | Notes / risks                                            |
|-------------------------------------|--------------------------|------------|----------------------------------------------------------|
| `POST /api/game/playStart`         | Start a play / roll      | High       | Multiple DB + Redis ops, main QPS bottleneck            |
| `POST /api/game/buyLotteryTickets` | Buy tickets / chances    | Med-High   | Multiple table writes, wallet and ticket records        |
| `GET  /api/game/config`            | Game config for frontend | Medium     | Depends on `DiceConfig` and cache                       |
| `GET  /api/game/lotteryPool`       | Lottery / reward config  | Medium     | Depends on `DiceLotteryPoolConfig` / `DiceRewardConfig` |
| `POST /api/v1/getGameUrl`          | Get game URL / entry     | Medium     | JWT + Redis auth; light DB usage                        |
| `POST /api/v1/getPlayerInfo`       | Get player info          | Medium     | Needs index on `dice_player.username`                   |
| `POST /api/v1/getPlayerGameRecord` | Player history records   | Med-High   | Risk of N+1 if not using batch/with                     |
| `POST /api/v1/setPlayerWallet`     | Adjust wallet balance    | Medium     | Wallet + records, must be concurrency-safe              |

For QPS capacity analysis, focus on:

- `playStart` (core hot path).
- Ticket, wallet, record-related endpoints.

---

## 3. `playStart` Call Flow and Resource Usage

The exact implementation is in the dice game logic layer and related services. A typical call flow looks like the following.

### 3.1 DB access (typical)

1. Load player:
   - `DicePlayer::find($playerId)`  
   - Should be done once per request and reused.

2. Load reward EV / minimum real EV:
   - `DiceRewardConfig::getCachedMinRealEv()`  
   - First call hits DB and writes Redis; later calls hit Redis only.

3. Get or create lottery pool:
   - `LotteryService::getOrCreate()`  
  - Reads from Redis; if missing, reads from DB (for example, `DiceLotteryPoolConfig::where('name','default/killScore')->find()`).

4. Load concrete pool config for this play:
   - `DiceLotteryPoolConfig::find($configId)`

5. Persist this play and its side effects (within one or several transactions):
   - `DicePlayRecord::create()` (play record)
   - `DicePlayer::save()` (balance and state)
   - `DicePlayerTicketRecord::create()` (ticket usage)
   - `DiceLotteryPoolConfig::where('id')->update(['ev' => Db::raw(...)]` (EV statistics)
   - `DicePlayerWalletRecord::create()` (wallet history)

6. Refresh player cache:
   - Reuse or reload player and call `UserCache::setUser($player)`.

Rough DB count (assuming good caching and no redundant `find` calls):

- About 3-6 DB queries/updates per `playStart`.
- About 3-6 Redis operations per `playStart`.

If the code calls `DicePlayer::find($playerId)` multiple times in one request, DB usage and latency will increase accordingly.

### 3.2 Redis access

Typical Redis usages in the current design:

- Reward / pool configs: `DiceRewardConfig::getCachedInstance()`, `getCachedByTier()`, `LotteryService::getOrCreate()`.
- Player cache: `UserCache::getUser()`, `UserCache::setUser()`.
- Optional counters: EV / statistics counters can be done via Redis `INCRBY`, etc.

Goal: hot paths should hit Redis most of the time, and DB should primarily be for persistence and cold reads.

---

## 4. Main Bottlenecks and Optimizations

### 4.1 DB connections and slow SQL

1. Connection pool sizing:
   - With 32 workers and `DB_POOL_MAX=20`, connection exhaustion and waiting are likely at higher QPS.
   - Recommendation: set `DB_POOL_MAX` to something like 32-64 (depending on CPU and MySQL `max_connections`), and check that MySQL allows enough connections.

2. Avoid redundant queries:
   - In `playStart`, ensure player is loaded once and reused (do not call `DicePlayer::find` multiple times per request).

3. EV update strategy:
   - Repeated `UPDATE dice_lottery_pool_config SET ev = ev - ?` on a hot row causes lock contention.
   - Better:
     - Accumulate EV deltas in Redis (per pool or per shard).
     - Periodic cron job to aggregate Redis deltas back into MySQL in batches.

4. Index coverage:
   - Ensure all high-frequency queries (`username`, `player_id`, `create_time`, status fields) use proper indexes (see section 6).

### 4.2 Cache and serialization overhead

1. Use Redis cache driver:
   - In `.env`, set `CACHE_MODE=redis`.
   - In `config/cache.php`, the default store should be Redis in production.

2. Player cache encryption:
   - If `UserCache` encrypts/decrypts data (for example, AES), CPU cost can be high at large QPS.
   - Possible mitigations:
     - Keep cached value small (only necessary fields).
     - Limit how often `setUser` is called (only on actual state changes).
     - Avoid double-encryption or repeated heavy serialization in hot paths.

### 4.3 N+1 queries

Endpoints like "get player game record" or wallet histories can easily trigger N+1 patterns:

- Use `whereIn('player_id', $playerIds)` instead of one query per player.
- Use eager loading (`with(['dicePlayer'])`) for related player info.
- Keep `limit` reasonable for load (default `20`, maximum `2000`; records are always scoped to a rolling 7-day window).

### 4.4 Redis vs DB coordination

- Ensure worker count, DB pool and Redis pool are balanced:
  - If CPU is not saturated but DB or Redis is fully blocked, it is a typical sign of misconfiguration.
- Monitor Redis:
  - Use slowlog for heavy commands.
  - Watch for large keys or hot keys; consider hash or sharding if needed.

---

## 5. Approximate QPS Capacity (Single Node)

These are ballpark figures for a single node with something like 8 cores and 32 workers, with MySQL and Redis in the same LAN and the optimizations above applied. Real numbers must come from your own load tests.

### 5.1 Per-endpoint estimates

- `playStart`:
  - Typical latency: about 50-150 ms.
  - Expected QPS: about 200-300 per node.

- `buyLotteryTickets`:
  - Typical latency: about 20-60 ms.
  - Expected QPS: about 500-1000+ per node.

- `getGameUrl` / login-style endpoints:
  - Typical latency: about 30-80 ms.
  - Expected QPS: about 400-800 per node.

### 5.2 Mixed traffic scenario

Example traffic mix:

- 70% `playStart`
- 30% lighter endpoints (`getGameUrl`, `getPlayerInfo`, and similar)

Under such a mix, with reasonable indexing and caching:

- A single 8-core node can typically sustain about 250-400 QPS overall.
- To go significantly beyond that, you will normally use horizontal scaling (multiple app nodes) plus DB/Redis scaling (read replicas, sharding, and so on).

---

## 6. Index and Table Design Recommendations

### 6.1 Must-have indexes

| Table                       | Index                             | Purpose                                  |
|-----------------------------|-----------------------------------|------------------------------------------|
| `dice_player`               | unique(`username`)                | Fast lookup by username (login, API)     |
| `dice_play_record`          | index(`player_id`, `create_time`) | Player record listing plus time sorting  |
| `dice_player_wallet_record` | index(`player_id`, `create_time`) | Wallet history listing                   |
| `dice_player_ticket_record` | index(`player_id`, `create_time`) | Ticket history listing                   |

Example SQL:

```sql
ALTER TABLE dice_player ADD UNIQUE INDEX uk_username (username);
ALTER TABLE dice_play_record ADD INDEX idx_player_create (player_id, create_time);
ALTER TABLE dice_player_wallet_record ADD INDEX idx_player_create (player_id, create_time);
ALTER TABLE dice_player_ticket_record ADD INDEX idx_player_create (player_id, create_time);
```

### 6.2 Large-table strategy

For very large history tables (play records, wallet logs, ticket logs), plan ahead:

- Partitioning or sharding (by time or by player ID).
- Archival strategy (move very old data to cold storage or delete).

---

## 7. Load Testing and Metrics

### 7.1 Load testing `playStart`

1. Prepare:
   - Seed enough test players and balances.
   - Obtain valid JWT tokens.

2. `ab` example:

```bash
ab -n 2000 -c 32 \
   -p post_body.json \
   -T application/json \
   -H "token: YOUR_JWT" \
   http://127.0.0.1:6688/api/game/playStart
```

3. `wrk` example:

```bash
wrk -t4 -c32 -d60s -s play_start.lua http://127.0.0.1:6688/api/game/playStart
```

4. Focus on:
   - QPS (Requests per second)
   - P95 / P99 latency
   - Error rates (HTTP 5xx, timeouts, business failures)
   - MySQL / Redis CPU and connection usage

### 7.2 Typical troubleshooting

- Low QPS but low CPU:
  - Likely DB or Redis connection pool exhaustion or slow queries.
- High P99 spikes:
  - Check for large transactions, schema changes, backup jobs, GC, or disk I/O hiccups.
- Redis issues:
  - Look for big keys, blocking commands (for example, `KEYS`), or high single-key contention.

---

## 8. Configuration and Code Checklist

Use this list before load tests or production deployments.

### 8.1 Configuration

- `.env`:
  - [ ] `CACHE_MODE=redis`
  - [ ] `DB_POOL_MAX` / `DB_POOL_MIN` tuned for CPU and MySQL `max_connections`
  - [ ] `REDIS_POOL_MAX` large enough for concurrent traffic
- MySQL:
  - [ ] `max_connections` greater than or equal to the sum of all app DB pools
  - [ ] Slow query log enabled with a reasonable threshold

### 8.2 Code

- [ ] `playStart` loads player once and reuses it everywhere.  
- [ ] Reward and pool configuration is always accessed via cache; after changes, cache is refreshed.  
- [ ] History and wallet endpoints avoid N+1; use batch queries and eager loading.  
- [ ] EV and other hot counters use Redis aggregation instead of per-request `UPDATE` on hot rows.

---

## 9. Summary

- For the current dice project, `playStart` and related lottery, wallet, and ticket flows define the backend QPS ceiling.  
- With proper connection pool tuning, Redis caching, indexing, and fewer redundant DB operations, a single 8-core node can typically handle a few hundred QPS of mixed traffic.  
- Real capacity must be validated via `ab`, `wrk`, or `k6` load tests against your environment; this document serves as a practical guide for identifying and fixing performance bottlenecks.
