Dart: プロジェクト最適化とデプロイ — DataPipeline
最終更新:2026-08-26
動くだけのコードでは十分ではない。速く、安定して、安全に動くコードこそがエンジニアの成果物である。 — Charlie
1. 学べること
- パフォーマンス解析と最適化:Isolate ロードバランシング / Stream バックプレッシャー / メモリ最適化
- AOT コンパイル(
dart compile exe)と出力のスリム化 - CI/CD の設定:GitHub Actions +
dart test+dart analyze - pub.dev への公開ワークフローとドキュメント(README / CHANGELOG / API ドキュメント)
- Alice の最終受け入れ:100 万件のエンドツーエンド負荷テストレポート
2. 開発者のリアルな物語
(1) 課題:動くが遅い
DataPipeline は 1 万件の注文をわずか 0.3 秒で処理した。しかし Alice が 100 万件のデータセットで負荷テストを行ったところ、45 秒もかかった。Bob は問題を特定した:Isolate のシャーディングが不均一(4 コアのうち 1 コアしか動いていなかった)、Stream にバックプレッシャーがなくメモリが 2GB まで急増、そしてデバッグモードでコンパイルされた 80MB の成果物。
(2) 最適化の解決策
動的 Isolate シャーディングで負荷を 4 コアに分散し、Stream にバックプレッシャーを追加してメモリを 200MB 未満に抑え、AOT コンパイルで出力を 15MB に削減し、CI/CD でテストとデプロイを自動化した。
flowchart LR A[コード完成] --> B[パフォーマンス最適化] B --> C[Isolate チューニング] B --> D[Stream バックプレッシャー] B --> E[メモリ解析] C --> F[AOT コンパイル] F --> G[CI/CD] G --> H[GitHub Actions] H --> I[pub.dev 公開] I --> J["v1.0.0"]
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(3) 効果
- 100 万件の注文処理が 45 秒から 8 秒に短縮
- メモリが 2GB から 180MB に削減
- ビルド出力が 80MB から 15MB に縮小
- CI/CD の自動化により、すべてのコミットで品質を保証
3. パフォーマンス解析と最適化
(1) Isolate ロードバランシング
▶ サンプル
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
: 動的シャーディング vs 静的シャーディング
import 'dart:isolate';
// 悪い例:静的チャンク分割 - 作業量が不均一
List<List<T>> staticChunk<T>(List<T> data, int chunks) {
final size = (data.length / chunks).ceil();
return [
for (var i = 0; i < chunks; i++)
data.sublist(i * size, ((i + 1) * size).clamp(0, data.length))
];
}
// 良い例:SendPort による動的なワークスティーリング
class DynamicIsolatePool {
final int _poolSize;
var _pendingChunks = <List<Map<String, dynamic>>>[];
final _results = <AnalysisResult>[];
var _activeWorkers = 0;
late Completer<List<AnalysisResult>> _completer;
DynamicIsolatePool({int poolSize = 4}) : _poolSize = poolSize;
Future<List<AnalysisResult>> process(
List<Map<String, dynamic>> allOrders, {
int chunkSize = 50000,
}) async {
_pendingChunks = [
for (var i = 0; i < allOrders.length; i += chunkSize)
allOrders.sublist(i, (i + chunkSize).clamp(0, allOrders.length))
];
_completer = Completer<List<AnalysisResult>>();
_activeWorkers = 0;
final workers = <_Worker>[];
for (var i = 0; i < _poolSize && _pendingChunks.isNotEmpty; i++) {
final chunk = _pendingChunks.removeAt(0);
workers.add(await _spawnWorker(chunk));
_activeWorkers++;
}
return _completer.future;
}
Future<_Worker> _spawnWorker(List<Map<String, dynamic>> initialChunk) async {
final receivePort = ReceivePort();
final isolate = await Isolate.spawn(_workerEntry, receivePort.sendPort);
final sendPort = await receivePort.first as SendPort;
final responsePort = ReceivePort();
sendPort.send(_WorkMessage(data: initialChunk, responsePort: responsePort.sendPort));
responsePort.listen((result) {
_results.add(result as AnalysisResult);
_activeWorkers--;
if (_pendingChunks.isNotEmpty) {
final nextChunk = _pendingChunks.removeAt(0);
sendPort.send(_WorkMessage(data: nextChunk, responsePort: responsePort.sendPort));
_activeWorkers++;
} else if (_activeWorkers == 0) {
isolate.kill(priority: Isolate.immediate);
receivePort.close();
responsePort.close();
_completer.complete(_results);
}
});
return _Worker(isolate: isolate, sendPort: sendPort);
}
static void _workerEntry(SendPort mainSendPort) {
final receivePort = ReceivePort();
mainSendPort.send(receivePort.sendPort);
receivePort.listen((message) {
if (message is _WorkMessage) {
final orders = message.data.map((m) => Order(
id: m['id'] as String,
amount: (m['amount'] as num).toDouble(),
status: m['status'] as String,
category: m['category'] as String,
)).toList();
final result = const OrderAnalyzer().analyze(orders);
message.responsePort.send(result);
}
});
}
}
class _Worker {
final Isolate isolate;
final SendPort sendPort;
const _Worker({required this.isolate, required this.sendPort});
}
class _WorkMessage {
final List<Map<String, dynamic>> data;
final SendPort responsePort;
const _WorkMessage({required this.data, required this.responsePort});
}
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 最適化項目 | Before | After | 改善効果 |
|---|---|---|---|
| シャーディング戦略 | 静的な均等分割 | 動的なワークスティーリング | 30% 高速化 |
| チャンクサイズ | ワーカーごとに 1 チャンク | 1 チャンク 5 万件 | メモリ半減 |
| ワーカー数 | 固定 4 | CPU コア数に適応 | マルチコアで高速化 |
(2) Stream バックプレッシャー
▶ サンプル
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
: バックプレッシャー制御
import 'dart:async';
class BackpressurePipeline {
final DataSource _source;
final OrderParser _parser;
final int _maxConcurrent;
BackpressurePipeline({
required DataSource source,
required OrderParser parser,
int maxConcurrent = 1000,
}) : _source = source,
_parser = parser,
_maxConcurrent = maxConcurrent;
Future<AnalysisResult> execute() async {
final orders = <Order>[];
var pending = 0;
final completer = Completer<void>();
final subscription = _source.readLines().listen(
(line) {
try {
orders.add(_parser.parse(line));
} on FormatException {
return;
}
pending++;
if (pending >= _maxConcurrent) {
subscription.pause();
}
},
onDone: () {
completer.complete();
},
onError: (e) {
completer.completeError(e);
},
);
await completer.future;
return const OrderAnalyzer().analyze(orders);
}
// バッファ制御付きの一時停止可能なストリーム
Stream<Order> streamWithBackpressure() async* {
var buffer = <Order>[];
await for (final line in _source.readLines()) {
try {
buffer.add(_parser.parse(line));
} on FormatException {
continue;
}
if (buffer.length >= _maxConcurrent) {
for (final order in buffer) {
yield order;
}
buffer = [];
}
}
for (final order in buffer) {
yield order;
}
}
}
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| バックプレッシャー戦略 | 説明 | ユースケース |
|---|---|---|
| pause/resume | ソースストリームを一時停止する | コンシューマーの処理が遅い場合 |
| バッファ制限 | 固定サイズのバッファ | メモリ使用量を制御したい場合 |
| drop latest | 制限を超えたデータを破棄する | リアルタイム監視で損失が許容される場合 |
(3) メモリ最適化
▶ サンプル
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
: メモリ解析テクニック
import 'dart:math';
class MemoryOptimizer {
// 可能な場合はリストの代わりにイテラブルを使う
static Iterable<Order> lazyParse(Iterable<String> lines, OrderParser parser) sync* {
for (final line in lines) {
try {
yield parser.parse(line);
} on FormatException {
continue;
}
}
}
// メモリを制限するチャンク処理
static Future<AnalysisResult> processInChunks(
Stream<String> lines,
OrderParser parser, {
int chunkSize = 100000,
}) async {
var chunk = <Order>[];
var totalResult = _EmptyAnalysisResult();
await for (final line in lines) {
try {
chunk.add(parser.parse(line));
} on FormatException {
continue;
}
if (chunk.length >= chunkSize) {
final partial = const OrderAnalyzer().analyze(chunk);
totalResult = totalResult.merge(partial);
chunk = [];
}
}
if (chunk.isNotEmpty) {
final partial = const OrderAnalyzer().analyze(chunk);
totalResult = totalResult.merge(partial);
}
return totalResult.toAnalysisResult();
}
}
class _EmptyAnalysisResult {
int totalOrders = 0;
int completedOrders = 0;
double revenue = 0;
double tax = 0;
final Map<String, double> revenueByCategory = {};
_EmptyAnalysisResult merge(AnalysisResult other) {
totalOrders += other.totalOrders;
completedOrders += other.completedOrders;
revenue += other.revenue;
tax += other.tax;
for (final entry in other.revenueByCategory.entries) {
revenueByCategory.update(entry.key, (v) => v + entry.value, ifAbsent: () => entry.value);
}
return this;
}
AnalysisResult toAnalysisResult() => AnalysisResult(
totalOrders: totalOrders,
completedOrders: completedOrders,
revenue: revenue,
tax: tax,
total: revenue + tax,
revenueByCategory: Map.unmodifiable(revenueByCategory),
);
}
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 最適化テクニック | 効果 | ユースケース |
|---|---|---|
| 遅延イテラブル | 全データを事前ロードしない | ソースからコンシューマーへ 1 件ずつデータを渡す |
| チャンク処理 | メモリ使用量を一定に保つ | 数百万件のレコードを処理する |
| 結果のマージ | 小さな中間結果にまとめる | チャンク後に統計を集約する |
4. AOT コンパイルと出力スリム化
▶ サンプル
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
: コンパイルコマンドと最適化
# デバッグビルド(大きい、デバッグ情報を含む)
dart compile exe bin/data_pipeline.dart -o build/data_pipeline_debug
# リリースビルド(最適化され、より小さい)
dart compile exe bin/data_pipeline.dart -o build/data_pipeline \
--define=MODE=release
# バイナリサイズを確認
ls -lh build/
# デバッグシンボルを除去(さらにサイズ削減)
# Linux/macOS の場合:
strip build/data_pipeline
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| コンパイルモード | コマンド | 出力サイズ | 起動速度 |
|---|---|---|---|
JIT (dart run) |
dart run bin/main.dart |
0(ソースを実行) | 遅い |
| AOT exe | dart compile exe |
~15MB | 速い |
| AOT aot-snapshot | dart compile aot-snapshot |
~8MB | 最速(dartaotruntime が必要) |
▶ サンプル
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
: コンパイル時設定
// ツリーシェイキングのための条件付きインポート
const _isRelease = bool.fromEnvironment('MODE.release');
void log(String message) {
if (!_isRelease) {
print('[DEBUG] $message');
}
}
// このクラス全体はリリースモードでツリーシェイクされる
class DebugLogger {
void log(String message) {
if (!_isRelease) {
print('[DEBUG] ${DateTime.now()}: $message');
}
}
}
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
5. CI/CD — GitHub Actions
▶ サンプル
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
: 完全な CI 設定
# .github/workflows/ci.yml
name: DataPipeline CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
analyze-and-test:
runs-on: ubuntu-latest
strategy:
matrix:
sdk: [stable, beta]
steps:
- uses: actions/checkout@v4
- uses: dart-lang/setup-dart@v1
with:
sdk: ${{ matrix.sdk }}
- name: Install dependencies
run: dart pub get
- name: Verify formatting
run: dart format --output=none --set-exit-if-changed .
- name: Analyze code
run: dart analyze --fatal-infos
- name: Run tests
run: dart test --coverage=coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: coverage/lcov.info
build-and-release:
needs: analyze-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: dart-lang/setup-dart@v1
- name: Install dependencies
run: dart pub get
- name: Compile AOT binary
run: |
dart compile exe bin/data_pipeline.dart -o build/data_pipeline
- name: Run integration test
run: |
./build/data_pipeline analyze --source csv --input test/fixtures/sample.csv
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: data_pipeline-linux
path: build/data_pipeline
publish:
needs: build-and-release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v4
- uses: dart-lang/setup-dart@v1
- name: Install dependencies
run: dart pub get
- name: Publish to pub.dev
run: dart pub publish --force
env:
PUB_CREDENTIALS: ${{ secrets.PUB_CREDENTIALS }}
| CI ステージ | 実行されるチェック | 失敗時の動作 |
|---|---|---|
| フォーマット | dart format |
PR を自動整形 |
| 静的解析 | dart analyze |
マージをブロック |
| ユニットテスト | dart test |
マージをブロック |
| 統合テスト | AOT バイナリのエンドツーエンド | 公開をブロック |
| 公開 | dart pub publish |
タグでのみトリガー |
6. pub.dev への公開
▶ サンプル
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
: pubspec.yaml 公開設定
name: data_pipeline
description: EC データ分析 CLI ツール - Stream + Isolate で百万件規模の注文を処理
version: 1.0.0
homepage: https://github.com/bob/datapipeline
repository: https://github.com/bob/datapipeline
issue_tracker: https://github.com/bob/datapipeline/issues
environment:
sdk: ^3.0.0
dependencies:
args: ^2.4.2
http: ^1.1.0
csv: ^6.0.0
dev_dependencies:
test: ^1.24.0
build_runner: ^2.4.0
json_serializable: ^6.7.0
executables:
data_pipeline: data_pipeline
▶ サンプル
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
: CHANGELOG.md
# CHANGELOG
---
## 7. リリースノート
### (1) 1.0.0 - 2025-01-15
#### 機能
- CSV と JSON データソースのサポート
- バックプレッシャー付きの Stream ベースのパイプライン
- 並列処理のための Isolate プール
- analyze/export/validate サブコマンド付き CLI
- パターンマッチング + Sealed クラスアーキテクチャ
### (2) パフォーマンス
- 100 万件の注文を約 8 秒で処理(4 コア)
- 100 万件のレコードでメモリ使用量 200MB 未満
### (3) テスト
- 95% のコードカバレッジ
- ユニット + 統合 + E2E テスト
▶ サンプル
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
: 公開前チェックリスト
# 公開前チェックリスト
# 1. 公開のドライラン
dart pub publish --dry-run
# 2. pub.dev のスコアを確認
dart pub publish --dry-run 2>&1 | grep -E "score|warning|error"
# 3. API ドキュメントを生成
dart doc .
# 4. パッケージ内容を確認
dart pub pack # 検査用の .tar.gz を作成
# 5. 最終公開
dart pub publish
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 公開ステップ | コマンド | 目的 |
|---|---|---|
| ドライラン | dart pub publish --dry-run |
パッケージ内容の確認 |
| ドキュメント生成 | dart doc . |
API ドキュメント |
| バージョン確認 | pubspec.yaml version |
セマンティックバージョニング |
| 公開 | dart pub publish |
pub.dev へのプッシュ |
7. API ドキュメント生成
▶ サンプル
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
: ドキュメントコメント規約
/// EC の注文データを解析し、統計結果を生成します。
///
/// データ解析の主要なエントリポイントとして [OrderAnalyzer] を使用します。
/// 対象リージョンに基づいて [taxRate] を設定してください。
///
/// 例:
/// ```dart
/// final analyzer = OrderAnalyzer(taxRate: 0.08);
/// final result = analyzer.analyze(orders);
/// print('Revenue: \$${result.revenue} USD');
/// ```
///
/// 関連項目:
/// - 出力構造については [AnalysisResult]
/// - 入力モデルについては [Order]
class OrderAnalyzer {
/// 指定された [taxRate] でアナライザーを作成します。
///
/// デフォルトは 0.08(米国の税率 8%)。
/// リージョン固有の税率を使用:US=0.08、EU=0.20、UK=0.15。
const OrderAnalyzer({this.taxRate = 0.08});
/// 完了した注文に適用される税率。
final double taxRate;
/// [orders] のリストを解析し、集計された統計を返します。
///
/// 収益の計算には status が 'completed' の注文のみが含まれます。
/// 他のステータスの注文は [AnalysisResult.totalOrders] にカウントされますが、
/// 収益からは除外されます。
AnalysisResult analyze(List<Order> orders) {
// ... 実装
}
}
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
8. 完全なサンプル:パフォーマンスベンチマーク
// ============================================
// DataPipeline パフォーマンスベンチマーク
// Alice のエンドツーエンド負荷テスト
// ============================================
import 'dart:async';
import 'dart:io';
class Benchmark {
final String name;
final Stopwatch _stopwatch;
Benchmark(this.name) : _stopwatch = Stopwatch();
Future<T> run<T>(Future<T> Function() action) async {
_stopwatch.reset();
_stopwatch.start();
final result = await action();
_stopwatch.stop();
print('$name: ${_stopwatch.elapsedMilliseconds}ms');
return result;
}
}
// テストデータを生成
List<String> generateOrders(int count) {
final categories = ['Electronics', 'Clothing', 'Books', 'Home', 'Sports'];
final statuses = ['completed', 'completed', 'completed', 'pending', 'cancelled'];
final regions = ['US', 'EU', 'UK', 'JP'];
return List.generate(count, (i) {
final category = categories[i % categories.length];
final status = statuses[i % statuses.length];
final region = regions[i % regions.length];
final amount = (100 + (i * 37) % 5000).toDouble();
return 'ORD-${(i + 1).toString().padLeft(7, '0')},$amount,$status,$category,$region';
});
}
void main() async {
print('=== DataPipeline Performance Benchmark ===\n');
final sizes = [10000, 100000, 1000000];
for (final size in sizes) {
final label = size >= 1000000 ? '${size ~/ 1000000}M' : '${size ~/ 1000}K';
print('--- $label orders ---');
// 生成して一時ファイルに保存
final data = generateOrders(size);
final tempFile = File('${Directory.systemTemp.path}/bench_${label}.csv');
await tempFile.writeAsString(data.join('\n'));
// ベンチマーク 1:Stream 処理
final streamResult = await Benchmark('Stream ($label)').run(() async {
final source = CsvSource(path: tempFile.path);
final parser = const CsvOrderParser();
final pipeline = Pipeline(source: source, parser: parser);
return pipeline.execute();
});
// ベンチマーク 2:メモリ効率の良いチャンク処理
final chunkedResult = await Benchmark('Chunked ($label)').run(() async {
final source = CsvSource(path: tempFile.path);
return MemoryOptimizer.processInChunks(
source.readLines(),
const CsvOrderParser(),
chunkSize: 50000,
);
});
// 結果が一致するか検証
final streamRev = streamResult.revenue.toStringAsFixed(2);
final chunkedRev = chunkedResult.revenue.toStringAsFixed(2);
print('Revenue match: ${streamRev == chunkedRev ? "PASS" : "FAIL"}');
print('Revenue: \$${streamRev} USD\n');
await tempFile.delete();
}
// ベンチマーク 3:AOT バイナリサイズの確認
print('--- Build Artifacts ---');
final exe = File('build/data_pipeline');
if (await exe.exists()) {
final sizeKB = await exe.length() ~/ 1024;
print('AOT binary: ${sizeKB}KB');
} else {
print('AOT binary: not built (run: dart compile exe bin/data_pipeline.dart)');
}
print('\n=== Benchmark Complete ===');
print('Alice acceptance criteria: 1M orders < 10s, memory < 300MB, binary < 20MB');
}
// ---- 必要なスタブ(前のレッスンからインポート) ----
sealed class DataSource {
const DataSource();
String get displayName;
Stream<String> readLines();
}
class CsvSource extends DataSource {
final String path;
const CsvSource({required this.path});
@override
String get displayName => 'CSV: $path';
@override
Stream<String> readLines() =>
File(path).openRead().transform(utf8.decoder).transform(const LineSplitter());
}
abstract class OrderParser {
const OrderParser();
Order parse(String raw);
}
class CsvOrderParser extends OrderParser {
const CsvOrderParser();
@override
Order parse(String raw) {
final parts = raw.split(',');
if (parts.length < 4) throw FormatException('Invalid row');
return Order(
id: parts[0].trim(),
amount: double.parse(parts[1].trim()),
status: parts[2].trim(),
category: parts[3].trim(),
region: parts.length > 4 ? parts[4].trim() : 'US',
);
}
}
class Order {
final String id;
final double amount;
final String status;
final String category;
final String region;
const Order({required this.id, required this.amount, required this.status, required this.category, this.region = 'US'});
double get tax => amount * 0.08;
}
class AnalysisResult {
final int totalOrders;
final int completedOrders;
final double revenue;
final double tax;
final double total;
final Map<String, double> revenueByCategory;
const AnalysisResult({required this.totalOrders, required this.completedOrders, required this.revenue, required this.tax, required this.total, required this.revenueByCategory});
}
class OrderAnalyzer {
const OrderAnalyzer();
AnalysisResult analyze(List<Order> orders) {
final completed = orders.where((o) => o.status == 'completed').toList();
final revenue = completed.fold<double>(0, (s, o) => s + o.amount);
final tax = completed.fold<double>(0, (s, o) => s + o.tax);
final byCategory = <String, double>{};
for (final o in completed) {
byCategory.update(o.category, (v) => v + o.amount, ifAbsent: () => o.amount);
}
return AnalysisResult(totalOrders: orders.length, completedOrders: completed.length, revenue: revenue, tax: tax, total: revenue + tax, revenueByCategory: byCategory);
}
}
class Pipeline {
final DataSource source;
final OrderParser parser;
const Pipeline({required this.source, required this.parser});
Future<AnalysisResult> execute() async {
final orders = <Order>[];
await for (final line in source.readLines()) {
try { orders.add(parser.parse(line)); } on FormatException { continue; }
}
return const OrderAnalyzer().analyze(orders);
}
}
class MemoryOptimizer {
static Future<AnalysisResult> processInChunks(Stream<String> lines, OrderParser parser, {int chunkSize = 50000}) async {
var chunk = <Order>[];
int totalOrders = 0, completedOrders = 0;
double revenue = 0, tax = 0;
final byCategory = <String, double>{};
await for (final line in lines) {
try { chunk.add(parser.parse(line)); } on FormatException { continue; }
if (chunk.length >= chunkSize) {
final r = const OrderAnalyzer().analyze(chunk);
totalOrders += r.totalOrders;
completedOrders += r.completedOrders;
revenue += r.revenue;
tax += r.tax;
for (final e in r.revenueByCategory.entries) {
byCategory.update(e.key, (v) => v + e.value, ifAbsent: () => e.value);
}
chunk = [];
}
}
if (chunk.isNotEmpty) {
final r = const OrderAnalyzer().analyze(chunk);
totalOrders += r.totalOrders;
completedOrders += r.completedOrders;
revenue += r.revenue;
tax += r.tax;
for (final e in r.revenueByCategory.entries) {
byCategory.update(e.key, (v) => v + e.value, ifAbsent: () => e.value);
}
}
return AnalysisResult(totalOrders: totalOrders, completedOrders: completedOrders, revenue: revenue, tax: tax, total: revenue + tax, revenueByCategory: byCategory);
}
}
> **Output:** ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
9. Alice の受け入れレポート
| 受け入れ基準 | 目標 | 実測 | 状態 |
|---|---|---|---|
| 100 万件の注文処理時間 | < 10 秒 | 約 8 秒 | PASS |
| ピークメモリ使用量 | < 300MB | 約 180MB | PASS |
| AOT 出力サイズ | < 20MB | 約 15MB | PASS |
| テストカバレッジ | > 90% | 95% | PASS |
dart analyze |
警告 0 | 0 | PASS |
dart format |
100% | 100% | PASS |
| ドキュメントカバレッジ | 公開 API の 100% | 100% | PASS |
| pub.dev スコア | > 120 | 140 | PASS |
Alice: 「DataPipeline v1.0 を受け入れます!100 万件の注文を 8 秒で処理し、メモリは 200MB 未満、AOT ビルドの出力はコンパクト、CI/CD の自動化も完成しました。リリース準備完了です!」
❓ よくある質問
Q:AOT ビルドの出力がそれほど大きいのはなぜですか? A: AOT 出力には軽量化された Dart VM + コンパイル済みネイティブコードが含まれます。
stripでデバッグシンボルを除去し、tree shakingで未使用コードを削除してください。一般的な Dart CLI の AOT ビルドは 10〜20MB です。
Q:Isolate の生成オーバーヘッドは大きいですか? A:
Isolate.spawnは約 50〜100ms、Isolate.runは約 30ms かかります。長時間動作するタスクでは、Isolate プールを使ってワーカーを再利用し、頻繁な生成・破棄を避けてください。
Q:Stream バックプレッシャーと Isolate はどのように連携しますか? A: メイン Isolate が Stream を読み取り、固定サイズにバッファリングし、満杯になると一時停止します。ワーカー Isolate がバッファ内のデータを消費し、完了したらメイン Isolate に再開を通知します。これはプロデューサー・コンシューマーパターンに似ています。
Q:pub.dev のスコアを向上させるにはどうすればよいですか? A: 以下を確認してください:1) pubspec.yaml が完全である(homepage/repository/description); 2) README.md が詳細である; 3)
dart analyzeの警告がゼロ; 4) テストカバレッジが 80% 超; 5) すべての公開 API にドキュメントコメントがある。
Q:GitHub Actions で依存関係をキャッシュするにはどうすればよいですか? A:
actions/cacheを使って~/.pub-cacheをキャッシュし、キーはpubspec.lockのハッシュに基づかせます。初回実行は約 30 秒、キャッシュヒット時は約 3 秒です。
Q:CI で AOT コンパイル済みバイナリをテストするにはどうすればよいですか? A:
dart compile exeの後、./build/data_pipeline analyze --input test.csvを直接実行し、終了コードと出力を確認します。これはdart runよりも本番環境に近いテストです。
📖 まとめ
- パフォーマンス最適化の三本柱:動的 Isolate シャーディング、Stream バックプレッシャー制御、メモリ制限のためのチャンク処理
- AOT コンパイルは Dart CLI にネイティブレベルの起動速度をもたらし、
strip+tree shakingが出力をスリム化する - CI/CD の自動化:フォーマット → 解析 → テスト → コンパイル → 統合テスト → 公開
- pub.dev への公開には完全な pubspec.yaml、README、CHANGELOG、API ドキュメントが必要
- Alice が受け入れ:100 万件を 8 秒、メモリ 180MB、ビルド出力 15MB
📝 練習問題
- 基礎(難易度 ⭐):DataPipeline に
dart compile exe用の Makefile/スクリプトを追加し、リリースビルドのワンクリックコンパイルとファイルサイズの出力を実現してください。 - 中級(難易度 ⭐⭐):Stream バックプレッシャーの pause/resume メカニズムを実装してください。1 万件の注文データ処理で検証してください:pause 中はメモリが増えず、処理が正しく再開されること。
- 挑戦(難易度 ⭐⭐⭐):完全な GitHub Actions CI パイプラインを設定してください:フォーマットチェック + 静的解析 + ユニットテスト + AOT コンパイル + 統合テスト。GitHub にプッシュしてパイプラインが通ることを検証してください。