Skip to content

Parallel Processing Performance

github-actions[bot] edited this page Mar 21, 2026 · 1 revision

Parallel-Processing-Performance

wikigenは、複数リポジトリのWiki生成において高いスループットを実現するため、2層構造の並列処理機構を採用している。本ページでは、-pフラグによるリポジトリ(プロジェクト)レベルの並列化と、-ppフラグによるページレベルの並列化の実装詳細、セマフォを用いた並行数制御、sync.WaitGroupによるゴルーチン同期、およびsync/atomicを活用したスレッドセーフな進捗追跡について解説する。CLI全般の仕様についてはCLI Usage & Commandsを、進捗表示の詳細についてはProgress Tracking & Output Modesを参照のこと。

並列処理の全体構造

wikigenの並列処理は「プロジェクトレベル」と「ページレベル」の2階層で構成される。プロジェクトレベルでは複数のWikiプロジェクトを同時に処理し、ページレベルでは1つのプロジェクト内の複数ページを同時に生成する。これらの並列度はそれぞれ-pフラグと-ppフラグで独立して制御できる。

flowchart TD
    A[mainループ] --> B[プロジェクトセマフォ\nsem = make chan struct p]
    B --> C[goroutine: generateWiki]
    C --> D[ページセマフォ\npageSem = make chan struct pp]
    D --> E[goroutine: claudeCall]
    E --> F[ページファイル書き込み]
    F --> G[pageSem 解放]
    G --> H{全ページ完了?}
    H -- No --> D
    H -- Yes --> I[pageWg.Wait]
    I --> J[sem 解放]
    J --> K{全プロジェクト完了?}
    K -- No --> B
    K -- Yes --> L[wg.Wait\n結果集計]
Loading

フラグ定義とデフォルト値

-p および -pp フラグ

Sources: main.go:949-950

flag.IntVar(&parallel, "p", envOrDefaultInt("WIKI_PARALLEL", 1), "parallel projects/repos")
flag.IntVar(&pageParallel, "pp", envOrDefaultInt("WIKI_PAGE_PARALLEL", 3), "parallel pages per project")
フラグ 環境変数 デフォルト値 説明
-p WIKI_PARALLEL 1 同時処理するプロジェクト数
-pp WIKI_PAGE_PARALLEL 3 プロジェクトあたりの同時ページ生成数

CLIフラグは対応する環境変数より優先される。環境変数のみ設定されている場合はその値が使用される。両方未設定の場合はデフォルト値が適用される。設定の優先順位の詳細についてはConfiguration & Environmentを参照のこと。

起動時には有効な並列度設定が標準エラー出力に表示される。

Sources: main.go:1054-1061

fmt.Fprintf(os.Stderr, "🚀 Processing %d wikis (parallel: %d, pages: %d, model: %s%s)\n\n",
    len(tasks), parallel, pageParallel, ...)

プロジェクトレベルの並列処理

セマフォとWaitGroupによる制御

Sources: main.go:1063-1095

プロジェクトのタスクリストをイテレートする際、バッファ付きチャネルをセマフォとして使用し、同時実行数を-pの値に制限する。

sem := make(chan struct{}, parallel)
var wg sync.WaitGroup
var mu sync.Mutex
var failed []string
var results []*WikiResult

for _, t := range tasks {
    wg.Add(1)
    sem <- struct{}{}          // セマフォ獲得(空きがなければブロック)
    go func(t task) {
        defer wg.Done()
        defer func() { <-sem }()  // セマフォ解放

        result, err := generateWiki(...)
        mu.Lock()
        // results / failed スライスへの書き込み
        mu.Unlock()
    }(t)
}
wg.Wait()

パターンの解説:

  • sem <- struct{}{}: チャネルへの送信でセマフォを獲得する。チャネルが満杯の場合はブロックし、空きができるまで待機する。
  • defer func() { <-sem }(): ゴルーチン終了時にチャネルから受信してセマフォを解放する。deferを使用することでパニック時にも確実に解放される。
  • mu.Lock() / mu.Unlock(): 共有スライス(resultsfailed)への並行書き込みをsync.Mutexで保護する。

タスク構造体

Sources: main.go:1000-1003

type task struct {
    name  string
    repos []string
}

各タスクはプロジェクト名とリポジトリのURLまたはローカルパスのリストを保持する。タスクの構築についてはInput Formats & Repository Configurationを参照のこと。

sequenceDiagram
    participant M as main
    participant S as sem (channel)
    participant G1 as goroutine 1
    participant G2 as goroutine 2
    participant G3 as goroutine 3

    M->>S: sem <- struct{}{}
    M->>G1: go generateWiki(task1)
    M->>S: sem <- struct{}{} (空きあり)
    M->>G2: go generateWiki(task2)
    M->>S: sem <- struct{}{} (ブロック, p=2の場合)
    G1-->>S: <-sem (解放)
    S-->>M: ブロック解除
    M->>G3: go generateWiki(task3)
    G2-->>S: <-sem (解放)
    G3-->>S: <-sem (解放)
    M->>M: wg.Wait()
Loading

ページレベルの並列処理

generateWiki関数のシグネチャ

Sources: main.go:495

func generateWiki(claudePath, model string, projectName string, repos []string,
    token, language, outputDir, cloneDir string,
    pageParallel int, dryRun bool, localDir string,
    progress *Progress) (*WikiResult, error)

pageParallelパラメータにより、各プロジェクトのページ生成ゴルーチン数を制御する。

ページ生成ループ

Sources: main.go:594-646

var pageDone int32
pageSem := make(chan struct{}, pageParallel)
var pageWg sync.WaitGroup

for i := range allPages {
    pageWg.Add(1)
    pageSem <- struct{}{}
    go func(idx int) {
        defer pageWg.Done()
        defer func() { <-pageSem }()

        page := &allPages[idx]
        // 進捗更新(atomic操作)
        pagePct := int(atomic.LoadInt32(&pageDone)) * 100 / len(allPages)
        progress.set(projectName, fmt.Sprintf("📝 %d/%d (%d%%) %s",
            atomic.LoadInt32(&pageDone)+1, len(allPages), pagePct, page.Title))

        // リトライつきのClaude呼び出し(最大3回)
        for attempt := 1; attempt <= maxRetries; attempt++ {
            _, err := claudeCall(claudePath, model, repoDirs, "", pagePrompt(...), wikiDir)
            // ファイル書き込み確認
        }

        atomic.AddInt32(&pageDone, 1)
    }(i)
}
pageWg.Wait()

ポイント:

  • プロジェクトレベルと同一のセマフォパターンを採用
  • atomic.AddInt32 / atomic.LoadInt32 により、sync.Mutexを使わずにカウンタを安全に操作
  • ページ生成失敗時は最大3回まで自動リトライ(詳細はError Handling & Retry Mechanismを参照)
flowchart TD
    A[generateWiki呼び出し] --> B[Phase 1: 構造決定\nClaude呼び出し]
    B --> C[allPages 構築]
    C --> D[pageSem 初期化\nmake chan struct pp]
    D --> E[pageWg 初期化]
    E --> F{ページあり?}
    F -- Yes --> G[pageWg.Add 1]
    G --> H[pageSem <- struct{}{}]
    H --> I[go func idx]
    I --> J[claudeCall\nページ生成]
    J --> K{成功?}
    K -- No --> L{retry < 3?}
    L -- Yes --> J
    L -- No --> M[エラー記録]
    K -- Yes --> N[atomic.AddInt32\npageDone++]
    M --> N
    N --> O[pageSem 解放]
    O --> P[pageWg.Done]
    F -- No --> Q[pageWg.Wait]
    P --> Q
    Q --> R[WikiResult 返却]
Loading

スレッドセーフな進捗追跡

Progress構造体

Sources: main.go:21-58

type Progress struct {
    mu         sync.Mutex
    totalItems int
    doneItems  int32
    current    map[string]string
}
フィールド 役割
mu sync.Mutex currentマップへの排他アクセス
totalItems int 全プロジェクト数(不変)
doneItems int32 完了カウンタ(atomic操作)
current map[string]string 現在処理中のプロジェクトと状態

Mutexとatomicの使い分け

Sources: main.go:33-56

func (p *Progress) done(name string) {
    atomic.AddInt32(&p.doneItems, 1)  // ロック不要でカウンタ更新
    p.mu.Lock()
    defer p.mu.Unlock()
    delete(p.current, name)            // マップ操作はロックが必要
    p.print()
}

func (p *Progress) print() {
    done := int(atomic.LoadInt32(&p.doneItems))  // ロック不要で読み取り
    pct := done * 100 / p.totalItems
    // 進捗を標準エラーに出力
}

doneItemsint32型のatomic操作で更新・読み取りを行い、sync.Mutexのオーバーヘッドを回避している。一方、currentマップはスライスやマップの並行操作がGoのランタイムによりサポートされないため、sync.Mutexで保護する。

classDiagram
    class Progress {
        +sync.Mutex mu
        +int totalItems
        +int32 doneItems
        +map current
        +set(name, status)
        +done(name)
        -print()
    }
    class sync.Mutex {
        +Lock()
        +Unlock()
    }
    class sync.WaitGroup {
        +Add(delta)
        +Done()
        +Wait()
    }
    Progress --> sync.Mutex : ミューテックス保護
    Progress ..> sync.WaitGroup : 連携使用
Loading

並列処理のアーキテクチャ全体図

flowchart TD
    subgraph メインループ
        A[tasks スライス] --> B[sem = make chan struct p]
        B --> C[wg sync.WaitGroup]
    end

    subgraph プロジェクトgoroutine
        D[generateWiki] --> E[Phase1: 構造決定]
        E --> F[pageSem = make chan struct pp]
        F --> G[pageWg sync.WaitGroup]
    end

    subgraph ページgoroutine
        H[claudeCall] --> I[ファイル検証]
        I --> J[atomic.AddInt32\npageDone++]
    end

    subgraph 共有状態
        K[Progress struct\nmu + doneItems]
        L[results slice\nmu保護]
        M[failed slice\nmu保護]
    end

    C --> D
    G --> H
    J --> K
    D --> L
    D --> M
Loading

リトライモードの並列処理

--retryフラグで起動するretryFailedPages関数は、pageParallelパラメータを受け取るが、失敗ページの再生成は現在プロジェクトごとに逐次処理される。

Sources: main.go:778-912

func retryFailedPages(claudePath, model, language, outputDir, cloneDir string, pageParallel int) {
    // 出力ディレクトリをスキャンして失敗ページを検出
    // "Content generation failed" マーカーまたは200バイト未満のファイルを対象
    for projectDir, pages := range projectFiles {
        for _, page := range pages {
            os.Remove(filepath.Join(projectDir, page.filename+".md"))
            _, err := claudeCall(...)
            // 結果確認
        }
    }
}

失敗ページの検出条件:

  • ファイル内容に "Content generation failed" が含まれる
  • ファイルサイズが200バイト未満

パフォーマンスチューニングの指針

推奨設定

flowchart TD
    A{ユースケース} --> B[単一リポジトリ]
    A --> C[少数のプロジェクト\n2〜5件]
    A --> D[大量プロジェクト\n10件以上]
    B --> E[-p 1 -pp 5\nページ並列を増加]
    C --> F[-p 3 -pp 3\nバランス型]
    D --> G[-p 5 -pp 2\nプロジェクト並列を重視]
Loading
シナリオ 推奨 -p 推奨 -pp 理由
単一プロジェクト 1 5〜10 プロジェクト並列は不要。ページ生成を高速化
少数プロジェクト(2〜5件) 2〜3 3 バランス型。APIレート制限に注意
大量プロジェクト(10件以上) 4〜5 2 プロジェクト並列を優先。ページ並列は抑制
メモリ制約あり 1 2 最小構成

注意事項

  1. Claude APIのレート制限: -p-ppの積(最大同時Claude呼び出し数)がAPIのレート制限に影響する。p=3, pp=5の場合、最大15の同時リクエストが発生する。
  2. ローカルリソース: 各Claude呼び出しはサブプロセスを起動するため、高並列設定ではシステムのプロセス数上限に注意が必要。
  3. git cloneの競合: リポジトリのクローン処理はWiki生成前に行われるため、並列cloneによるディスクI/O競合が発生する場合がある。
  4. 原子性の保証: ページカウンタはsync/atomicで保護されているが、resultsおよびfailedスライスへの書き込みはsync.Mutexで保護されている点に留意すること。

Sources: main.go:3-17syncおよびsync/atomicのimport)

import (
    ...
    "sync"
    "sync/atomic"
    "time"
)

関連ページ

Clone this wiki locally