Danh sách bài viết

Bài 63: workers: N | '50%' — Cấu Hình Số Worker Parallel

Bài mở nhóm A.7 — đào sâu option workers trong playwright.config.ts: ba dạng giá trị, cơ chế CPU detection, RAM per-worker, pattern CI vs local, tương tác với fullyParallel, và các pitfall cấu hình hay gặp trên production.

28/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

Sau bài này, bạn sẽ:

  • Phân biệt ba dạng giá trị hợp lệ của workers: number, 'N%', undefined.
  • Hiểu Playwright tính số worker từ os.cpus().length và cách làm tròn khi dùng percentage.
  • Ước tính RAM per-worker để không OOM trên CI runner nhỏ.
  • Viết config phân biệt CI vs local bằng process.env.CI.
  • Biết CLI override --workers=N thắng config trong mọi trường hợp.
  • Hiểu sự tương tác giữa workersfullyParallel — hai option độc lập nhưng phối hợp.
  • Tránh 5 pitfall cấu hình workers phổ biến.

Lưu ý phạm vi: Bài này tập trung vào option workers trong playwright.config.ts. CLI flag --workers đã được đào sâu ở bài 68 của series Cơ Bản. fullyParallel có bài riêng (bài 64). Sharding (bài 70–72) thuộc các bài sau trong nhóm A.7.

2

Nhóm A.7 — Parallel & Sharding

A.7 đào sâu cách Playwright scale parallel: workers, fullyParallel, sharding, blob report.

Bức tranh tổng thể của nhóm:

  • workers (bài này) — số process Node chạy song song trong một máy.
  • fullyParallel (bài 64) — cách test được phân phối vào các worker: per-file hay per-test.
  • Sharding (bài 70–72) — chia suite ra nhiều CI machine, scale ngang.
  • Blob report (bài 73) — merge kết quả từ nhiều shard về một report duy nhất.

workers là nền tảng: không set đúng workers thì các tính năng parallel khác không có tác dụng hoặc gây OOM.

3

Ba Dạng Giá Trị của workers

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Dạng 1: Number — hardcode số worker tuyệt đối
  workers: 4,

  // Dạng 2: Percentage string — % CPU cores
  // workers: '50%',

  // Dạng 3: undefined — Playwright tự pick (khoảng CPU/2)
  // workers: undefined,
});
Dạng Ví dụ Ý nghĩa
number workers: 4 Cố định 4 worker, không phụ thuộc phần cứng máy chạy.
string (percentage) workers: '50%' 50% số CPU core. Thích nghi với nhiều loại machine.
undefined / không set không khai báo Playwright tự chọn — khoảng Math.ceil(cpus / 2).

Khi nào dùng dạng nào:

  • number — CI runner có spec cố định và bạn muốn kiểm soát chính xác. Vd runner 4 vCPU → workers: 2 để dành CPU cho browser process.
  • string — config chạy được trên nhiều máy khác nhau (dev machine, staging runner, production CI). '75%' là sweet spot phổ biến.
  • undefined — chỉ dùng khi không cần kiểm soát, hoặc ở vế else của pattern CI/local.

Ví dụ thực tế kết hợp:

export default defineConfig({
  // CI: cố định 2 worker (runner nhỏ)
  // Local: 75% CPU (thích nghi máy dev)
  workers: process.env.CI ? 2 : '75%',
});
4

Worker Process Model

Mỗi worker là một Node.js child process độc lập — không phải worker thread. Điều này có hệ quả cụ thể:

  • Mỗi worker có PID riêng, heap riêng, module cache riêng.
  • Mỗi worker khởi tạo một browser instance riêng thông qua worker-scope fixture browser.
  • State không chia sẻ giữa worker qua process boundary — biến module-level ở worker A không ảnh hưởng worker B.
  • Worker chạy test tuần tự trong nội bộ: test xếp hàng, worker nhận từng test, xong mới nhận cái kế.
  • Parallel xảy ra giữa các worker — nhiều worker xử lý các test song song với nhau.
Playwright main process (orchestrator)
├── Worker 0 (PID 1234) — Node process
│     └── Browser instance 0
│           ├── test A (context riêng)
│           ├── test B (context riêng)   ← serial trong worker
│           └── test C (context riêng)
│
├── Worker 1 (PID 1235) — Node process
│     └── Browser instance 1
│           ├── test D (context riêng)   ← parallel với worker 0
│           └── test E (context riêng)
│
└── Worker 2 (PID 1236) — Node process
      └── Browser instance 2
            └── test F (context riêng)

Main process là orchestrator: duy trì queue test, phân phối test cho worker rảnh (work-stealing), nhận kết quả về, ghi report.

Worker recycle: worker bị tắt và khởi tạo lại khi worker-scope fixture cần reset (vd thay đổi storageState giữa các test trong cùng worker), hoặc khi test fail và cần retry sạch.

5

CPU Detection — os.cpus()

Khi dùng percentage hoặc undefined, Playwright gọi os.cpus().length để lấy số logical CPU (bao gồm hyperthreading).

// Playwright internal logic (simplified)
import * as os from 'os';

function resolveWorkers(workers: number | string | undefined): number {
  const cpus = os.cpus().length;
  if (workers === undefined) {
    return Math.ceil(cpus / 2);
  }
  if (typeof workers === 'string') {
    // '50%' → parseInt('50') → 50
    const pct = parseInt(workers, 10) / 100;
    return Math.max(1, Math.floor(cpus * pct));
  }
  return workers;
}

Ví dụ tính toán:

Máy (logical CPU) workers config Workers thực tế
8 core '50%' 4
8 core '75%' 6
8 core '100%' 8
4 core '50%' 2
2 core '50%' 1
1 core '50%' 1 (tối thiểu)
8 core undefined 4 (ceil(8/2))

Verify số worker thực tế:

# Dòng đầu output khi chạy test cho biết worker count
npx playwright test --reporter=list 2>&1 | head -1
# Running 120 tests using 4 workers

# Check CPU count của máy
node -e "console.log(require('os').cpus().length)"

Lưu ý với container/CI: trong Docker hoặc một số CI (GitHub Actions, GitLab CI), os.cpus().length trả về số vCPU được cấp phát cho container — thường là 2 hoặc 4, không phải số core vật lý của host.

6

RAM Per-Worker

Mỗi worker tiêu thụ RAM cho hai thành phần chính:

  • Node.js process: ~80–150 MB (tải module, heap Playwright).
  • Browser instance (Chromium headless): ~150–350 MB, phụ thuộc số page, DOM complexity, nội dung media.

Tổng mỗi worker: ~300–500 MB trong điều kiện bình thường.

Workers RAM ước tính RAM khả dụng tối thiểu khuyến nghị
1 ~400 MB 2 GB
2 ~800 MB 4 GB
4 ~1.6 GB 6 GB
8 ~3.2 GB 10 GB
16 ~6.4 GB 18 GB

"RAM khả dụng" = tổng RAM máy trừ OS (~1.5 GB) và các process nền. Trên máy dev còn phải trừ IDE, browser đang mở.

Tính nhanh cho CI runner:

GitHub Actions ubuntu-latest: 2 vCPU, 7 GB RAM
  RAM khả dụng: 7 - 1.5 (OS) - 1.5 (CI agent + Docker) ≈ 4 GB
  4 GB / 500 MB per worker = 8 → nhưng chỉ có 2 vCPU
  → workers: 2 là hợp lý nhất

Test tốn nhiều bộ nhớ (video recording bật, nhiều tab, page lớn) có thể đẩy mức tiêu thụ lên 600–800 MB per worker. Tính với hệ số 1.5x khi thiết kế workers cho suite có video/trace enabled.

7

Pattern CI vs Local

Pattern phổ biến nhất — phân biệt môi trường qua process.env.CI:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // CI: 2 worker — runner nhỏ, tránh OOM
  // Local: 75% CPU — tận dụng máy dev, để lại 25% cho IDE
  workers: process.env.CI ? 2 : '75%',

  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  forbidOnly: !!process.env.CI,
});

Use cases theo từng giá trị:

  • Local dev auto (undefined hoặc '50%') — cân bằng test speed với IDE responsiveness. Không bị IDE lag khi chạy test.
  • CI nhỏ (workers: 2) — GitHub Actions free tier (2 vCPU), GitLab Shared Runner. Tránh OOM, tránh swap làm chậm toàn bộ run.
  • CI lớn (workers: 4 hoặc '50%') — runner 8 vCPU trả phí, self-hosted. Tận dụng nhiều core hơn để rút ngắn thời gian.
  • Performance investigation (workers: 1) — chạy serial, log tuần tự, dễ debug ordering bug và timing issue.
  • Throughput max ('100%') — chạy trên máy dedicated chỉ để test, không chạy thứ gì khác đồng thời.

Dùng env var tùy chỉnh cho các tier CI khác nhau:

// playwright.config.ts — hỗ trợ nhiều tier
const ciWorkers = process.env.PLAYWRIGHT_WORKERS
  ? parseInt(process.env.PLAYWRIGHT_WORKERS, 10)
  : process.env.CI
    ? 2
    : undefined;

export default defineConfig({
  workers: ciWorkers,
});
# .github/workflows/test.yml
- name: Run tests (large runner)
  run: npx playwright test
  env:
    CI: 'true'
    PLAYWRIGHT_WORKERS: '6'   # Override cho runner 8 vCPU
8

CLI Override

CLI flag --workers luôn thắng giá trị trong config. Thứ tự ưu tiên:

  1. CLI flag--workers=4 hoặc --workers=50%
  2. Configworkers trong playwright.config.ts
  3. Default — Playwright tự pick (khoảng CPU/2)
# Config có workers: 2, nhưng CLI thắng
npx playwright test --workers=6

# Percentage cũng hợp lệ trên CLI
npx playwright test --workers=50%

Điều này có nghĩa config chỉ là giá trị mặc định — dev có thể override nhanh mà không cần sửa file. Hữu ích khi cần chạy nhanh một lần với workers cao hơn bình thường.

Kiểm tra workers đang dùng:

npx playwright test --reporter=list 2>&1 | head -1
# Running 48 tests using 6 workers
9

Tương Tác Với fullyParallel

workers quy định số process. fullyParallel quy định cách test được phân phối vào các process đó. Hai option độc lập nhưng phối hợp với nhau.

workers fullyParallel Behavior thực tế
4 false (default cũ) 4 process; mỗi file chạy serial trong 1 worker. Parallel ở cấp độ file.
4 true 4 process; từng test được phân phối vào worker rảnh bất kể file. Parallel ở cấp độ test.
1 true Serial — chỉ 1 process, fullyParallel không có tác dụng.

Ví dụ minh họa:

Suite: 3 file × 4 test = 12 test tổng

fullyParallel: false, workers: 4
  Worker 0: chạy hết 4 test của file A (serial)
  Worker 1: chạy hết 4 test của file B (serial)
  Worker 2: chạy hết 4 test của file C (serial)
  Worker 3: idle (không có file thứ 4)
  → 3 test chạy song song, mỗi test series trong file

fullyParallel: true, workers: 4
  Worker 0: test A1, A3, B2, C4 (queue picking)
  Worker 1: test A2, B1, C2
  Worker 2: test A4, B3, C1
  Worker 3: test B4, C3
  → 4 test chạy song song bất kể file nguồn

npx create-playwright@latest sinh ra config mặc định với fullyParallel: true. Project cũ (trước v1.31) thường thiếu option này — thêm vào để tận dụng workers đã cấu hình.

Deep dive về fullyParallel, test.describe.configure({ mode: 'serial' }), và tương tác với test.describe.configure({ mode: 'parallel' }) thuộc bài 64.

10

Isolation Giữa Worker

Do worker là child process riêng biệt, state không leak qua process boundary. Tuy nhiên cần hiểu rõ những gì được isolated và không:

Được isolated (không share):

  • Biến module-level và global state trong test file.
  • Browser instance — mỗi worker có browser riêng.
  • Node.js module cache — require() load riêng trong mỗi worker.

KHÔNG được isolated (share qua filesystem/network):

  • File trên disk — hai worker ghi cùng file → race condition.
  • Database — hai worker insert cùng row → conflict.
  • Hard-coded port — hai worker bind port 3000 → chỉ 1 thành công.
  • Environment variables — tất cả worker kế thừa từ process cha.

Giải pháp cho shared resource:

// test/fixtures.ts
import { test as base } from '@playwright/test';

export const test = base.extend<{ uniquePort: number }>({
  uniquePort: async ({}, use, workerInfo) => {
    // workerInfo.workerIndex: 0, 1, 2, 3...
    const port = 3000 + workerInfo.workerIndex;
    await use(port);
  },
});

// Dùng trong test
test('server test', async ({ page, uniquePort }) => {
  await page.goto(`http://localhost:${uniquePort}`);
});

State chia sẻ trong cùng 1 worker: Các test chạy tuần tự trong cùng worker có thể chia sẻ state qua worker-scope fixture (scope 'worker'). Đây là cách hợp lệ, vd chia sẻ DB connection pool hoặc auth token đã đăng nhập một lần.

11

Bottleneck CPU & Sweet Spot

Tăng worker không đồng nghĩa với tăng tốc tuyến tính. Có ba vùng performance:

  • Underutilize — workers < số core. CPU còn nhiều idle. Tăng workers sẽ giảm wall-time.
  • Sweet spot — workers ≈ 50–75% số core. Throughput cao, context switch thấp, còn CPU cho I/O hệ thống.
  • Overcommit — workers > số core. OS phải schedule nhiều hơn số core xử lý được. Context switching tăng, wall-time tăng hoặc không giảm thêm.

Ngoài CPU, kiểm tra xem test có bị bottleneck bởi thứ khác không:

  • Network I/O — test gọi external API, tốc độ phụ thuộc băng thông và latency. Tăng workers không giúp nếu bottleneck là response time của server.
  • Database — nhiều worker insert/query cùng DB → connection pool exhausted hoặc lock contention.
  • Disk I/O — screenshot, video recording nhiều worker cùng ghi → IOPS bottleneck trên HDD hoặc container storage.

Cách xác định bottleneck:

# Đo wall-time với từng giá trị workers
time npx playwright test --workers=1  # baseline
time npx playwright test --workers=2
time npx playwright test --workers=4
time npx playwright test --workers=8

# Nếu wall-time ngừng giảm (hoặc tăng) ở workers=4
# → sweet spot = 4, không cần tăng thêm

Chú ý cột real (wall-time) thay vì user (tổng CPU time). Khi parallel hiệu quả, user > real vì nhiều core làm việc song song.

12

Pitfalls

Pitfall 1: workers mặc định cao trên CI nhỏ gây OOM

Khi không set workersprocess.env.CI cũng không được set, Playwright tự pick khoảng CPU/2. GitHub Actions runner có 2 vCPU → 1 worker. Nhưng nếu set biến CI=true mà không khai báo workers trong config, Playwright vẫn chỉ dùng default (không phải tự giảm về 1). Default về 1 worker trên CI chỉ xảy ra nếu config khai báo rõ workers: process.env.CI ? 1 : undefined.

// Sai: không khai báo workers, kỳ vọng CI tự về 1 — không đúng
export default defineConfig({
  fullyParallel: true,
  // workers: không có
});

// Đúng: khai báo rõ ràng
export default defineConfig({
  workers: process.env.CI ? 2 : '75%',
});

Pitfall 2: '50%' trên máy 1 CPU

Math.floor(1 * 0.5) = 0 — nhưng Playwright có Math.max(1, ...) nên kết quả là 1 worker. Không crash, nhưng nếu kỳ vọng có 2+ worker thì sẽ thất vọng. Trên container 1 vCPU, dùng workers: 1 tường minh thay vì percentage.

Pitfall 3: Set workers khi runtime trong test — không có tác dụng

// Sai: cố gắng đổi workers trong test — không hoạt động
test('some test', async ({ page }) => {
  // process.env.WORKERS = '4'; // Không ảnh hưởng workers đang chạy
});

workers là config-time option — được đọc một lần khi Playwright khởi động, trước khi spawn bất kỳ worker nào. Thay đổi sau khi chạy không có tác dụng.

Pitfall 4: Local workers cao → IDE lag

Máy dev 8 core với workers: '100%' = 8 worker → 8 browser instance song song → CPU 100%, IDE và browser dev tool lag. Sweet spot cho local dev là '50%' đến '75%'.

Pitfall 5: workers lớn hơn số test

workers: 8, nhưng suite chỉ có 3 test
→ 3 worker được dùng, 5 worker spawn rồi idle ngay
→ Spawn overhead ~200ms × 5 = 1 giây waste
→ Dùng workers: 3 hoặc để auto-detect

Playwright tự giới hạn workers thực tế không vượt số test trong queue — nhưng spawn overhead vẫn xảy ra trước khi biết queue size. Trên suite nhỏ (< 20 test), workers cao không có lợi.

13

Quiz

Câu 1. Config khai báo workers: '75%' trên máy 8 logical CPU. CI runner có 4 vCPU. Số worker thực tế trên local và CI là bao nhiêu?

Đáp án

Local (8 CPU): Math.floor(8 × 0.75) = 6 worker. CI (4 vCPU): Math.floor(4 × 0.75) = 3 worker. Percentage tự thích nghi theo CPU của machine đang chạy.

Câu 2. Bạn có config workers: process.env.CI ? 2 : '75%'. Chạy CI=true npx playwright test --workers=6. Số worker thực tế là bao nhiêu?

Đáp án

6 worker. CLI flag --workers=6 có ưu tiên cao nhất, thắng hoàn toàn giá trị trong config. Biến CI=true chỉ ảnh hưởng config; config đã bị CLI override.

Câu 3. Suite có 4 file, mỗi file có 10 test. Config workers: 4, fullyParallel: false. Bao nhiêu test có thể chạy song song cùng một lúc?

Đáp án

Tối đa 4 test — mỗi worker chạy 1 test từ file của mình tại một thời điểm. Vì fullyParallel: false, mỗi file được assign cho 1 worker và test trong file đó chạy serial. 4 worker × 1 file mỗi worker → 4 file chạy song song, nhưng mỗi file xử lý tuần tự từng test. Tại bất kỳ thời điểm nào, có đúng 4 test đang chạy (1 per worker).

Câu 4. Máy có 16 core, 32 GB RAM. Sau khi set workers: '100%', wall-time không giảm so với workers: 8. Nguyên nhân có thể là gì?

Đáp án

Một số khả năng: (1) Bottleneck không phải CPU mà là network I/O hoặc external API — thêm worker không giảm latency. (2) Database connection pool bị exhausted — 16 worker tranh nhau ít connection hơn, có worker phải chờ. (3) Test có sequential dependency và không bật fullyParallel — tất cả test cùng file vẫn serial. (4) Context switch overhead của 16 process trên 16 core gần bằng 8 worker trên 16 core do hyperthreading (2 threads/core, 16 logical / 2 = 8 physical).

Câu 5. CI runner GitHub Actions (2 vCPU, 7 GB RAM) chạy suite 200 test với workers: 8. Run bị OOM. Tính toán giá trị workers phù hợp và giải thích.

Đáp án

RAM khả dụng: 7 GB − 1.5 GB (OS) − 1 GB (CI agent) ≈ 4.5 GB. Mỗi worker chiếm ~400–500 MB. Max workers an toàn: 4500 / 500 = 9 theo RAM, nhưng chỉ có 2 vCPU nên không có lợi khi > 2 worker. Kết luận: workers: 2. Nếu test có video/trace: workers: 1 để an toàn hơn.

14

Bài Tiếp Theo

Bài 64: fullyParallel: true/false — File-level vs Test-level Parallelism — đào sâu cách Playwright phân phối test vào worker queue, test.describe.configure({ mode }), và khi nào nên giữ serial trong file.