Danh sách bài viết

Bài 69: CLI --shard=x/y — Chia Test Cross Machine

Flag --shard=x/y cho phép chia test suite thành y phần bằng nhau và chạy mỗi phần trên một CI machine độc lập — giảm wall-clock time tuyến tính theo số shard mà không cần thay đổi code test.

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 --shard (cross-machine) với workers (trong 1 machine).
  • Hiểu Playwright phân phối test theo round-robin và tính deterministic của shard.
  • Viết GitHub Actions matrix job để chạy 4 shard song song.
  • Biết cách kết hợp --shard với --workers--project.
  • Ước tính performance gain và khi nào shard thực sự có lợi.
  • Tránh 5 pitfall phổ biến khi dùng shard.

Phạm vi: Bài này tập trung vào CLI flag --shard=x/y. Blob reporter và merge-reports để aggregate kết quả từ nhiều shard thuộc bài 70–71.

2

Shard vs Workers — Hai Chiều Parallel

Playwright hỗ trợ parallel theo hai chiều độc lập:

Cơ chế Đơn vị Phạm vi Config / Flag
workers Node.js child process Trong 1 machine workers: N hoặc --workers=N
shard CI machine / runner Cross machine --shard=x/y

workers chia test trong một machine theo số process; shard chia test list ngay từ đầu — mỗi shard chỉ "thấy" subset của toàn suite và không biết gì về các shard còn lại.

Hai cơ chế độc lập và có thể kết hợp. Một machine chạy shard số x với N worker: shard quyết định subset test nào, worker quyết định bao nhiêu test trong subset đó chạy song song.

3

Cú Pháp --shard=x/y

Flag --shard=x/y nhận hai số nguyên dương: x là index của shard hiện tại (1-based), y là tổng số shard.

# Machine 1 chạy shard đầu tiên trong 4
npx playwright test --shard=1/4

# Machine 2
npx playwright test --shard=2/4

# Machine 3
npx playwright test --shard=3/4

# Machine 4
npx playwright test --shard=4/4

Mỗi machine chạy lệnh riêng với index khác nhau. Tổng hợp lại thì toàn bộ suite được phủ đúng một lần: không test nào bị bỏ, không test nào chạy hai lần.

Quy tắc hợp lệ:

  • x phải là số nguyên từ 1 đến y (1-based, không có shard 0).
  • y phải là số nguyên dương.
  • x <= y — shard index không vượt tổng số shard.

Playwright báo lỗi ngay khi parse flag nếu vi phạm một trong ba quy tắc trên.

4

Cơ Chế Phân Phối Round-Robin

Trước khi spawn bất kỳ worker nào, Playwright tạo danh sách tất cả test (sau khi áp filter --grep, --project, v.v.), rồi chia list đó cho các shard theo round-robin theo đơn vị file.

Giả sử 8 file test, chia cho 4 shard:

File index:  0    1    2    3    4    5    6    7
Shard:       1    2    3    4    1    2    3    4

→ Shard 1 chạy: file 0, file 4
→ Shard 2 chạy: file 1, file 5
→ Shard 3 chạy: file 2, file 6
→ Shard 4 chạy: file 3, file 7

Một số điểm quan trọng về cơ chế này:

  • Granularity mặc định là file — không phải test riêng lẻ. Một file không bị tách ngang qua hai shard.
  • Deterministic — với cùng suite và cùng --shard=x/y, Playwright luôn assign đúng tập file đó. Không random mỗi lần chạy.
  • Phụ thuộc thứ tự file — Playwright sắp xếp file theo alphabet trước khi round-robin. Thêm hoặc xóa file có thể thay đổi assignment của từng shard.

Khi số file không chia hết cho số shard:

5 file, 4 shard:
  Shard 1: file 0, file 4
  Shard 2: file 1          ← chỉ 1 file
  Shard 3: file 2          ← chỉ 1 file
  Shard 4: file 3          ← chỉ 1 file

Shard 1 có 2 file, các shard còn lại chỉ có 1 — shard 1 sẽ chậm hơn. Đây là một trong các lý do nên cân bằng số test per file.

5

GitHub Actions Matrix Pattern

Pattern phổ biến nhất: dùng strategy.matrix để sinh N job song song, mỗi job chạy 1 shard.

# .github/workflows/playwright.yml
name: Playwright Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false      # Shard khác tiếp tục dù 1 shard fail
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps

      - name: Run tests (shard ${{ matrix.shard }}/4)
        run: npx playwright test --shard=${{ matrix.shard }}/4

GitHub Actions tạo 4 job song song, mỗi job nhận giá trị shard khác nhau từ matrix. Tất cả 4 job chạy đồng thời — tổng thời gian xấp xỉ thời gian của shard chậm nhất.

Lưu ý fail-fast: false: theo mặc định GitHub Actions hủy các job còn lại khi 1 job fail. Với shard, thường muốn toàn bộ suite chạy xong để có report đầy đủ trước khi kết luận.

Upload artifact sau mỗi shard để merge-reports sau (chi tiết bài 70):

      - name: Upload blob report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: blob-report-${{ matrix.shard }}
          path: blob-report/
          retention-days: 1

if: always() đảm bảo artifact được upload kể cả khi shard có test fail.

6

Kết Hợp --shard và --workers

--shard--workers hoạt động ở hai cấp độ khác nhau nên có thể kết hợp tự do:

# 4 machines × 4 workers mỗi machine = 16 test song song tổng
npx playwright test --shard=1/4 --workers=4
npx playwright test --shard=2/4 --workers=4
npx playwright test --shard=3/4 --workers=4
npx playwright test --shard=4/4 --workers=4

Hoặc trong config kết hợp với matrix:

      - name: Run tests
        run: npx playwright test --shard=${{ matrix.shard }}/4 --workers=4

Kết hợp này có lợi khi mỗi CI runner có nhiều vCPU. GitHub Actions ubuntu-latest có 2 vCPU — dùng --workers=2 là hợp lý. Runner lớn hơn (4 vCPU) thì --workers=4.

Tính toán tổng parallel:

N shard × M workers = N×M test chạy song song tối đa

Ví dụ:
  4 shard × 2 workers = 8 test song song
  4 shard × 4 workers = 16 test song song
  8 shard × 2 workers = 16 test song song (cost cao hơn)

Chi phí: mỗi shard là 1 runner bill riêng. 4 shard × 4 workers tốn 4 runner nhưng nhanh gấp 16x so với serial. 8 shard × 2 workers tốn 8 runner nhưng cũng nhanh gấp 16x — chi phí cao hơn, performance tương đương.

7

Kết Hợp --shard và --project

--project filter trước, --shard chia sau. Khi kết hợp, Playwright chỉ tính test list của project đó rồi mới round-robin.

# Shard chỉ chromium tests qua 4 machine
npx playwright test --project=chromium --shard=1/4
npx playwright test --project=chromium --shard=2/4
npx playwright test --project=chromium --shard=3/4
npx playwright test --project=chromium --shard=4/4

Use case phổ biến: chạy browser matrix và shard matrix song song — mỗi combination (browser, shard) là 1 runner.

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        project: [chromium, firefox, webkit]
        shard: [1, 2, 3, 4]
    steps:
      - name: Run tests
        run: npx playwright test --project=${{ matrix.project }} --shard=${{ matrix.shard }}/4

Matrix trên tạo 3 × 4 = 12 job song song. Wall-time gần bằng thời gian của 1 job đơn. Nhưng chi phí runner nhân 12x — chỉ hợp lý khi feedback time quan trọng hơn cost.

Lưu ý: khi --project filter ra ít file, một số shard có thể empty (không có test nào). Playwright vẫn exit 0 với shard rỗng, chỉ in thông báo không tìm thấy test.

8

Performance Impact

Giảm wall-clock time theo lý thuyết:

Số shard Wall-time (lý thuyết) Ghi chú
1 (không shard) 100% Baseline
2 ~52–55% Overhead spawn runner, install deps
4 ~28–32% ~70% time reduction
8 ~18–22% Diminishing return bắt đầu rõ
16 ~14–18% Phần lớn là overhead, không còn lợi nhiều

Overhead của mỗi shard bao gồm:

  • Provision runner: 5–30 giây (tùy CI provider).
  • npm ci hoặc restore node_modules cache: 10–60 giây.
  • npx playwright install: 30–120 giây nếu không cache browser binary.
  • Global setup chạy lại trên mỗi shard: thời gian phụ thuộc logic setup.

Tổng overhead có thể là 1–3 phút mỗi shard. Nếu suite chạy 5 phút trên 1 machine, tách 4 shard mỗi shard chạy ~1.25 phút nhưng overhead 2 phút → thực tế không nhanh hơn. Shard có lợi khi thời gian test đủ lớn để overhead tỷ lệ nhỏ.

Ngưỡng thực tế:

  • Suite < 5 phút: shard thường không có lợi về wall-time.
  • Suite 5–15 phút: 2–4 shard bắt đầu có hiệu quả rõ.
  • Suite > 15 phút: shard là lựa chọn ưu tiên để giảm feedback time.
9

Khi Nào Nên và Không Nên Dùng Shard

Nên dùng shard khi:

  • Suite có hơn 100 test và tổng thời gian chạy vượt 5 phút.
  • CI cost với multiple runner chấp nhận được.
  • Cần fast feedback trên PR — 10 phút thay vì 40 phút.
  • Suite đã tận dụng hết workers trong 1 machine nhưng vẫn chậm.

Không nên dùng shard khi:

  • Suite nhỏ (< 50 test hoặc < 3 phút) — overhead runner vượt benefit.
  • CI runner đắt và budget bị kiểm soát chặt — shard nhân cost theo số shard.
  • Test có sequential dependency bắt buộc giữa các file — shard phá vỡ thứ tự.
  • Global setup tốn nhiều thời gian và không thể cache — overhead nhân lên.

Optimal shard count cho hầu hết case:

Suite 5–10 phút   → 2 shard
Suite 10–20 phút  → 4 shard
Suite 20–40 phút  → 4–8 shard
Suite > 40 phút   → 8 shard + tăng workers mỗi shard

Tăng shard từ 4 lên 8 chỉ cắt thêm ~50% thời gian còn lại (không phải 50% so với baseline). Thường 4 shard là điểm cân bằng tốt nhất giữa cost và speed.

10

State Cross-Shard

Mỗi shard là một lần chạy npx playwright test hoàn toàn độc lập trên machine riêng. Các shard không thể giao tiếp trực tiếp với nhau. Điều này có hệ quả cụ thể:

  • Global setup chạy lại trên mỗi shard — globalSetup trong config không được chia sẻ.
  • Worker-scope fixture không share cross-shard — mỗi shard có worker pool riêng.
  • In-memory state trong fixtures không thể truyền qua shard khác.

Pattern để share state cross-shard:

Vì shard = process độc lập, cách duy nhất để share state là qua storage ngoài: database, file system, object storage (S3), hoặc Redis.

# .github/workflows/playwright.yml — pre-seed trước khi shard chạy
jobs:
  seed:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - name: Seed test database
        run: node scripts/seed-db.js  # Chạy 1 lần, tất cả shard đọc vào

  test:
    needs: seed          # Chờ seed job xong
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - run: npx playwright test --shard=${{ matrix.shard }}/4

Pattern: job seed chạy trước và chuẩn bị DB hoặc fixture data vào external storage. Các shard đọc data đó — không tự generate. Mỗi shard nên dùng data riêng (partitioned) để tránh conflict.

11

Reporter và Merge

Mỗi shard tạo report riêng biệt. HTML reporter mặc định sinh playwright-report/ độc lập trên mỗi machine — không tự gộp lại.

Để có một report tổng hợp từ tất cả shard, cần dùng blob reporter và merge-reports:

  1. Mỗi shard dùng reporter: 'blob' — sinh file nhị phân nén trong blob-report/.
  2. Upload artifact sau mỗi shard.
  3. Job cuối download tất cả artifact rồi chạy npx playwright merge-reports.
// playwright.config.ts — dùng blob reporter khi có shard
export default defineConfig({
  reporter: process.env.CI ? 'blob' : 'html',
});

Blob reporter và merge-reports là chủ đề của bài 70. Bài này chỉ cần biết: mỗi shard cần upload report riêng, không có report tổng hợp tự động.

Nếu chỉ cần biết pass/fail tổng thể (không cần xem chi tiết từng test), bỏ qua merge và xem exit code của từng shard job trực tiếp trên CI dashboard.

12

Pitfalls

Pitfall 1: --shard=0/4 — index bắt đầu từ 1, không phải 0

# Sai: --shard=0/4 → Playwright báo lỗi và exit
npx playwright test --shard=0/4

# Đúng: index bắt đầu từ 1
npx playwright test --shard=1/4

Pitfall 2: --shard=5/4 — x phải ≤ y

Playwright báo lỗi khi x > y. Trong matrix CI nếu dùng array động phải đảm bảo index không vượt tổng. Nếu tổng shard thay đổi, cần update cả array lẫn mẫu số.

Pitfall 3: Chỉ chạy flag trên 1 machine — toàn suite vẫn chạy 1 lần

# Chỉ chạy shard 1 mà không có shard 2,3,4
# → Chỉ chạy 25% suite, còn 75% bị bỏ qua
npx playwright test --shard=1/4

Shard không tự nhân bản. Mỗi shard phải được chạy tường minh. Nếu quên một shard trong CI matrix, toàn bộ test của shard đó không chạy và không fail — chúng chỉ vắng mặt trong report.

Pitfall 4: Global setup sinh file vào cùng đường dẫn cố định

// globalSetup ghi vào path cố định — OK vì mỗi shard là machine riêng
// Nhưng nếu dùng mounted volume share → conflict
async function globalSetup() {
  // Tránh ghi vào shared path khi dùng mounted storage
  await fs.writeFile('./test-data/auth.json', ...);
}

Nếu shard chạy trên cùng machine (hiếm nhưng có thể xảy ra với self-hosted runner), global setup từ hai shard ghi cùng file cùng lúc → race condition. Dùng path có shard index để tránh: auth-shard-${SHARD_INDEX}.json.

Pitfall 5: Không upload report → không thể merge

Khi shard fail, step tiếp theo (upload artifact) bị skip theo mặc định. Phải dùng if: always() trên upload step để đảm bảo artifact được upload kể cả khi shard có test fail:

      - name: Upload blob report
        if: always()    # Không bỏ qua khi test fail
        uses: actions/upload-artifact@v4
        with:
          name: blob-report-${{ matrix.shard }}
          path: blob-report/
13

Quiz

Câu 1. Suite có 12 file test. Bạn chạy --shard=2/4. File nào (theo index 0–11) được assign cho shard này?

Đáp án

Round-robin theo file index: shard 1 → index 0, 4, 8; shard 2 → index 1, 5, 9; shard 3 → index 2, 6, 10; shard 4 → index 3, 7, 11. Vậy shard 2 chạy file index 1, 5, 9 — tức file thứ 2, 6, 10 (1-based).

Câu 2. CI config dùng matrix.shard: [1, 2, 3, 4] nhưng lệnh chạy là npx playwright test --shard=${{ matrix.shard }}/3 (mẫu số là 3, không phải 4). Điều gì xảy ra với job shard 4?

Đáp án

Playwright nhận --shard=4/3 → vi phạm điều kiện x ≤ y → báo lỗi và exit với non-zero code → job fail. Toàn bộ shard 4 fail mà không chạy test nào. Cần đồng bộ mẫu số trong lệnh với độ dài array trong matrix.

Câu 3. Suite có 200 test trải đều trên 20 file, mỗi file 10 test. Dùng --shard=1/4 --workers=2. Shard 1 sẽ chạy bao nhiêu file, bao nhiêu test, và tối đa bao nhiêu test song song?

Đáp án

20 file chia 4 shard round-robin: mỗi shard nhận 5 file. Shard 1 nhận file index 0, 4, 8, 12, 16 → 5 file × 10 test = 50 test. Với workers=2fullyParallel: true (mặc định), tối đa 2 test chạy song song cùng lúc. Nếu fullyParallel: false, tối đa 2 file song song, mỗi file serial — vẫn là 2 test chạy cùng lúc về số lượng nhưng không trộn lẫn file.

Câu 4. Team quyết định tăng shard từ 4 lên 8 để giảm thêm thời gian CI. Suite hiện tại chạy 8 phút với 4 shard. Ước tính wall-time mới với 8 shard, giả sử overhead runner là 2 phút.

Đáp án

Với 4 shard: wall-time = 8 phút (đã tính overhead 2 phút). Test thuần = 8 − 2 = 6 phút. Tăng lên 8 shard: test thuần mỗi shard = 6 / 8 ≈ 0.75 phút. Cộng overhead 2 phút → wall-time ≈ 2.75 phút. Giảm từ 8 phút xuống ~2.75 phút — tiết kiệm ~65%. Nhưng chi phí runner tăng 2x (8 runner thay vì 4). Nếu từ không shard (baseline ~24 phút) thì 4 shard đã giảm ~67%, tăng thêm lên 8 shard chỉ giảm thêm khoảng 65% của 8 phút còn lại — diminishing return rõ.

Câu 5. Workflow có 4 shard jobs. Shard 2 fail vì 3 test fail. Bạn muốn merge report từ cả 4 shard để xem đầy đủ. Nhưng chỉ có artifact từ shard 1, 3, 4 — artifact shard 2 bị thiếu. Nguyên nhân có thể là gì và cách fix?

Đáp án

Nguyên nhân: upload artifact step không có if: always() — khi shard 2 fail, step tiếp theo bị skip. Fix: thêm if: always() vào upload artifact step để nó chạy bất kể kết quả test. Sau khi fix, kể cả shard fail vẫn upload blob report để merge-reports có đủ dữ liệu từ tất cả shard.

14

Bài Tiếp Theo

Bài 70: Blob Reporter & Sharding — cấu hình reporter: 'blob' để mỗi shard xuất binary report, upload artifact, rồi dùng merge-reports để tạo một HTML report duy nhất từ toàn bộ shard.