Files
esengine/docs/en/guide/worker-system.md
YHH dfd0dfc7f9 feat(worker): 添加微信小游戏 Worker 支持和 Worker Generator CLI (#297)
* feat(worker): 添加微信小游戏 Worker 支持和 Worker Generator CLI

- 新增 @esengine/worker-generator 包,用于从 WorkerEntitySystem 生成 Worker 文件
- WorkerEntitySystem 添加 workerScriptPath 配置项,支持预编译 Worker 脚本
- CLI 工具支持 --wechat 模式,自动转换 ES6+ 为 ES5 语法
- 修复微信小游戏 Worker 消息格式差异(res 直接是数据,无需 .data)
- 更新中英文文档,添加微信小游戏支持章节

* docs: 更新 changelog,添加 v2.3.1 说明并标注 v2.3.0 为废弃

* fix: 修复 CI 检查问题

- 移除 cli.ts 中未使用的 toKebabCase 函数
- 修复 generator.ts 中正则表达式的 ReDoS 风险(使用 [ \t] 替代 \s*)
- 更新 changelog 版本号(2.3.1 -> 2.3.2)

* docs: 移除未发布版本的 changelog 条目

* fix(worker-generator): 使用 TypeScript 编译器替代手写正则进行 ES5 转换

- 修复 CodeQL 检测的 ReDoS 安全问题
- 使用 ts.transpileModule 进行安全可靠的代码转换
- 移除所有可能导致回溯的正则表达式
2025-12-08 17:02:11 +08:00

571 lines
16 KiB
Markdown

# Worker System
The Worker System (WorkerEntitySystem) is a multi-threaded processing system based on Web Workers in the ECS framework. It's designed for compute-intensive tasks, fully utilizing multi-core CPU performance for true parallel computing.
## Core Features
- **True Parallel Computing**: Execute compute-intensive tasks in background threads using Web Workers
- **Automatic Load Balancing**: Automatically distribute workload based on CPU core count
- **SharedArrayBuffer Optimization**: Zero-copy data sharing for improved large-scale computation performance
- **Graceful Degradation**: Automatic fallback to main thread processing when Workers are not supported
- **Type Safety**: Full TypeScript support and type checking
## Basic Usage
### Simple Physics System Example
```typescript
interface PhysicsData {
id: number;
x: number;
y: number;
vx: number;
vy: number;
mass: number;
radius: number;
}
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics), {
enableWorker: true, // Enable Worker parallel processing
workerCount: 8, // Worker count, auto-limited to hardware capacity
entitiesPerWorker: 100, // Entities per Worker
useSharedArrayBuffer: true, // Enable SharedArrayBuffer optimization
entityDataSize: 7, // Data size per entity
maxEntities: 10000, // Maximum entity count
systemConfig: { // Configuration passed to Worker
gravity: 100,
friction: 0.95
}
});
}
// Data extraction: Convert Entity to serializable data
protected extractEntityData(entity: Entity): PhysicsData {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
const physics = entity.getComponent(Physics);
return {
id: entity.id,
x: position.x,
y: position.y,
vx: velocity.x,
vy: velocity.y,
mass: physics.mass,
radius: physics.radius
};
}
// Worker processing function: Pure function executed in Worker
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
return entities.map(entity => {
// Apply gravity
entity.vy += config.gravity * deltaTime;
// Update position
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
// Apply friction
entity.vx *= config.friction;
entity.vy *= config.friction;
return entity;
});
}
// Apply results: Apply Worker processing results back to Entity
protected applyResult(entity: Entity, result: PhysicsData): void {
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
position.x = result.x;
position.y = result.y;
velocity.x = result.vx;
velocity.y = result.vy;
}
// SharedArrayBuffer optimization support
protected getDefaultEntityDataSize(): number {
return 7; // id, x, y, vx, vy, mass, radius
}
protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void {
if (!this.sharedFloatArray) return;
this.sharedFloatArray[offset + 0] = entityData.id;
this.sharedFloatArray[offset + 1] = entityData.x;
this.sharedFloatArray[offset + 2] = entityData.y;
this.sharedFloatArray[offset + 3] = entityData.vx;
this.sharedFloatArray[offset + 4] = entityData.vy;
this.sharedFloatArray[offset + 5] = entityData.mass;
this.sharedFloatArray[offset + 6] = entityData.radius;
}
protected readEntityFromBuffer(offset: number): PhysicsData | null {
if (!this.sharedFloatArray) return null;
return {
id: this.sharedFloatArray[offset + 0],
x: this.sharedFloatArray[offset + 1],
y: this.sharedFloatArray[offset + 2],
vx: this.sharedFloatArray[offset + 3],
vy: this.sharedFloatArray[offset + 4],
mass: this.sharedFloatArray[offset + 5],
radius: this.sharedFloatArray[offset + 6]
};
}
}
```
## Configuration Options
The Worker system supports rich configuration options:
```typescript
interface WorkerSystemConfig {
/** Enable Worker parallel processing */
enableWorker?: boolean;
/** Worker count, defaults to CPU core count, auto-limited to system maximum */
workerCount?: number;
/** Entities per Worker for load distribution control */
entitiesPerWorker?: number;
/** System configuration data passed to Worker */
systemConfig?: any;
/** Enable SharedArrayBuffer optimization */
useSharedArrayBuffer?: boolean;
/** Float32 count per entity in SharedArrayBuffer */
entityDataSize?: number;
/** Maximum entity count (for SharedArrayBuffer pre-allocation) */
maxEntities?: number;
/** Pre-compiled Worker script path (for platforms like WeChat Mini Game that don't support dynamic scripts) */
workerScriptPath?: string;
}
```
### Configuration Recommendations
```typescript
constructor() {
super(matcher, {
// Decide based on task complexity
enableWorker: this.shouldUseWorker(),
// Worker count: System auto-limits to hardware capacity
workerCount: 8, // Request 8 Workers, actual count limited by CPU cores
// Entities per Worker (optional)
entitiesPerWorker: 200, // Precise load distribution control
// Enable SharedArrayBuffer for many simple calculations
useSharedArrayBuffer: this.entityCount > 1000,
// Set according to actual data structure
entityDataSize: 8, // Ensure it matches data structure
// Estimated maximum entity count
maxEntities: 10000,
// Global configuration passed to Worker
systemConfig: {
gravity: 9.8,
friction: 0.95,
worldBounds: { width: 1920, height: 1080 }
}
});
}
private shouldUseWorker(): boolean {
// Decide based on entity count and complexity
return this.expectedEntityCount > 100;
}
// Get system info
getSystemInfo() {
const info = this.getWorkerInfo();
console.log(`Worker count: ${info.workerCount}/${info.maxSystemWorkerCount}`);
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
console.log(`Current mode: ${info.currentMode}`);
}
```
## Processing Modes
The Worker system supports two processing modes:
### 1. Traditional Worker Mode
Data is serialized and passed between main thread and Workers:
```typescript
// Suitable for: Complex computation logic, moderate entity count
constructor() {
super(matcher, {
enableWorker: true,
useSharedArrayBuffer: false, // Use traditional mode
workerCount: 2
});
}
protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] {
// Complex algorithm logic
return entities.map(entity => {
// AI decisions, pathfinding, etc.
return this.complexAILogic(entity, deltaTime);
});
}
```
### 2. SharedArrayBuffer Mode
Zero-copy data sharing, suitable for many simple calculations:
```typescript
// Suitable for: Many entities with simple calculations
constructor() {
super(matcher, {
enableWorker: true,
useSharedArrayBuffer: true, // Enable shared memory
entityDataSize: 6,
maxEntities: 10000
});
}
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) {
const entitySize = 6;
for (let i = startIndex; i < endIndex; i++) {
const offset = i * entitySize;
// Read data
let x = sharedFloatArray[offset];
let y = sharedFloatArray[offset + 1];
let vx = sharedFloatArray[offset + 2];
let vy = sharedFloatArray[offset + 3];
// Physics calculations
vy += config.gravity * deltaTime;
x += vx * deltaTime;
y += vy * deltaTime;
// Write back data
sharedFloatArray[offset] = x;
sharedFloatArray[offset + 1] = y;
sharedFloatArray[offset + 2] = vx;
sharedFloatArray[offset + 3] = vy;
}
};
}
```
## Use Cases
The Worker system is particularly suitable for:
### 1. Physics Simulation
- **Gravity systems**: Gravity calculations for many entities
- **Collision detection**: Complex collision algorithms
- **Fluid simulation**: Particle fluid systems
- **Cloth simulation**: Vertex physics calculations
### 2. AI Computation
- **Pathfinding**: A*, Dijkstra algorithms
- **Behavior trees**: Complex AI decision logic
- **Swarm intelligence**: Boid, fish school algorithms
- **Neural networks**: Simple AI inference
### 3. Data Processing
- **Bulk entity updates**: State machines, lifecycle management
- **Statistical calculations**: Game data analysis
- **Image processing**: Texture generation, effect calculations
- **Audio processing**: Sound synthesis, spectrum analysis
## Best Practices
### 1. Worker Function Requirements
```typescript
// Recommended: Worker processing function is a pure function
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
// Only use parameters and standard JavaScript APIs
return entities.map(entity => {
// Pure computation logic, no external state dependencies
entity.y += entity.velocity * deltaTime;
return entity;
});
}
// Avoid: Using external references in Worker function
protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] {
// this and external variables are not available in Worker
return entities.map(entity => {
entity.y += this.someProperty; // Error
return entity;
});
}
```
### 2. Data Design
```typescript
// Recommended: Reasonable data design
interface SimplePhysicsData {
x: number;
y: number;
vx: number;
vy: number;
// Keep data structure simple for easy serialization
}
// Avoid: Complex nested objects
interface ComplexData {
transform: {
position: { x: number; y: number };
rotation: { angle: number };
};
// Complex nested structures increase serialization overhead
}
```
### 3. Worker Count Control
```typescript
// Recommended: Flexible Worker configuration
constructor() {
super(matcher, {
// Specify needed Worker count, system auto-limits to hardware capacity
workerCount: 8, // Request 8 Workers
entitiesPerWorker: 100, // 100 entities per Worker
enableWorker: this.shouldUseWorker(), // Conditional enable
});
}
private shouldUseWorker(): boolean {
// Decide based on entity count and complexity
return this.expectedEntityCount > 100;
}
// Get actual Worker info
checkWorkerConfiguration() {
const info = this.getWorkerInfo();
console.log(`Requested Workers: 8`);
console.log(`Actual Workers: ${info.workerCount}`);
console.log(`System maximum: ${info.maxSystemWorkerCount}`);
console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`);
}
```
### 4. Performance Monitoring
```typescript
// Recommended: Performance monitoring
public getPerformanceMetrics(): WorkerPerformanceMetrics {
return {
...this.getWorkerInfo(),
entityCount: this.entities.length,
averageProcessTime: this.getAverageProcessTime(),
workerUtilization: this.getWorkerUtilization()
};
}
```
## Performance Optimization Tips
### 1. Compute Intensity Assessment
Only use Workers for compute-intensive tasks to avoid thread overhead for simple calculations.
### 2. Data Transfer Optimization
- Use SharedArrayBuffer to reduce serialization overhead
- Keep data structures simple and flat
- Avoid frequent large data transfers
### 3. Degradation Strategy
Always provide main thread fallback to ensure normal operation in environments without Worker support.
### 4. Memory Management
Clean up Worker pools and shared buffers promptly to avoid memory leaks.
### 5. Load Balancing
Use `entitiesPerWorker` parameter to precisely control load distribution, avoiding idle Workers while others are overloaded.
## WeChat Mini Game Support
WeChat Mini Game has special Worker limitations and doesn't support dynamic Worker script creation. ESEngine provides the `@esengine/worker-generator` CLI tool to solve this problem.
### WeChat Mini Game Worker Limitations
| Feature | Browser | WeChat Mini Game |
|---------|---------|------------------|
| Dynamic scripts (Blob URL) | Supported | Not supported |
| Worker count | Multiple | Maximum 1 |
| Script source | Any | Must be in code package |
| SharedArrayBuffer | Requires COOP/COEP | Limited support |
### Using Worker Generator CLI
#### 1. Install the Tool
```bash
pnpm add -D @esengine/worker-generator
```
#### 2. Configure workerScriptPath
Configure `workerScriptPath` in your WorkerEntitySystem subclass:
```typescript
@ECSSystem('Physics')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsData> {
constructor() {
super(Matcher.all(Position, Velocity, Physics), {
enableWorker: true,
workerScriptPath: 'workers/physics-worker.js', // Specify Worker file path
systemConfig: {
gravity: 100,
friction: 0.95
}
});
}
protected workerProcess(
entities: PhysicsData[],
deltaTime: number,
config: any
): PhysicsData[] {
// Physics calculation logic
return entities.map(entity => {
entity.vy += config.gravity * deltaTime;
entity.x += entity.vx * deltaTime;
entity.y += entity.vy * deltaTime;
return entity;
});
}
// ... other methods
}
```
#### 3. Generate Worker Files
Run the CLI tool to automatically extract `workerProcess` functions and generate WeChat Mini Game compatible Worker files:
```bash
# Basic usage
npx esengine-worker-gen --src ./src --wechat
# Full options
npx esengine-worker-gen \
--src ./src \ # Source directory
--wechat \ # Generate WeChat Mini Game compatible code
--mapping \ # Generate worker-mapping.json
--verbose # Verbose output
```
The CLI tool will:
1. Scan source directory for all `WorkerEntitySystem` subclasses
2. Read each class's `workerScriptPath` configuration
3. Extract `workerProcess` method body
4. Convert to ES5 syntax (WeChat Mini Game compatible)
5. Generate to configured path
#### 4. Configure game.json
Configure workers directory in WeChat Mini Game's `game.json`:
```json
{
"deviceOrientation": "portrait",
"workers": "workers"
}
```
#### 5. Project Structure
```
your-game/
├── game.js
├── game.json # Configure "workers": "workers"
├── src/
│ └── systems/
│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js'
└── workers/
├── physics-worker.js # Auto-generated
└── worker-mapping.json # Auto-generated
```
### Temporarily Disabling Workers
If you need to temporarily disable Workers (e.g., for debugging), there are two ways:
#### Method 1: Configuration Disable
```typescript
constructor() {
super(matcher, {
enableWorker: false, // Disable Worker, use main thread processing
// ...
});
}
```
#### Method 2: Platform Adapter Disable
Return Worker not supported in custom platform adapter:
```typescript
class MyPlatformAdapter implements IPlatformAdapter {
isWorkerSupported(): boolean {
return false; // Return false to disable Worker
}
// ...
}
```
### Important Notes
1. **Re-run CLI tool after each `workerProcess` modification** to generate new Worker files
2. **Worker functions must be pure functions**, cannot depend on `this` or external variables:
```typescript
// Correct: Only use parameters
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += config.gravity * deltaTime;
return e;
});
}
// Wrong: Using this
protected workerProcess(entities, deltaTime, config) {
return entities.map(e => {
e.y += this.gravity * deltaTime; // Cannot access this in Worker
return e;
});
}
```
3. **Pass configuration data via `systemConfig`**, not class properties
4. **Developer tool warnings can be ignored**:
- `getNetworkType:fail not support` - WeChat DevTools internal behavior
- `SharedArrayBuffer will require cross-origin isolation` - Development environment warning, won't appear on real devices
## Online Demo
See the complete Worker system demo: [Worker System Demo](https://esengine.github.io/ecs-framework/demos/worker-system/)
The demo showcases:
- Multi-threaded physics computation
- Real-time performance comparison
- SharedArrayBuffer optimization
- Parallel processing of many entities
The Worker system provides powerful parallel computing capabilities for the ECS framework, allowing you to fully utilize modern multi-core processor performance, offering efficient solutions for complex game logic and compute-intensive tasks.