Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9f01092
Add 2-pass release build for Dynamic Dispatch table
bdero Mar 30, 2026
13e4a9c
Add DD analysis gen_snapshot command to test expectations
bdero Mar 31, 2026
7e0b85b
Fix macOS universal binary test command order for concurrent DD build
bdero Mar 31, 2026
2b4214b
Skip DD table computation when analyze_snapshot is absent
bdero Apr 1, 2026
7936bc6
Fix macOS universal binary test command order for DD table async
bdero Apr 1, 2026
e57009f
fix: pass DD function identity file in base build pipeline
bdero Apr 1, 2026
3287119
feat: make DD table max bytes configurable via environment variable
bdero Apr 2, 2026
360bff6
chore: bump dart_sdk_revision to cascade-limiter
bdero Apr 3, 2026
e0dc2ad
chore: checkpoint current DD table base build changes
bdero Apr 7, 2026
856b8f0
fix: probe gen_snapshot for DD flag support before running DD pipeline
bdero Apr 8, 2026
90adeb6
feat: DD 2-pass release build for cascade limiter
bdero Apr 20, 2026
30a65d4
fix: address flutter_tools test failures
bdero May 2, 2026
e1d2149
review: address build.dart feedback (dead field, fallback, helper ext…
bdero May 6, 2026
42a20ce
revert: drop universal/ analyze_snapshot move; probe parent dir instead
bdero May 7, 2026
6f4f270
chore: bump dart_sdk_revision to 59680e070c3 (cascade-limiter HEAD)
bdero May 7, 2026
ff330bc
fix: update updater cbindgen toml inputs after upstream API split
bdero May 7, 2026
0bf71a9
chore: bump dart_sdk_revision to f9f552a6e77 (DD VERIFY all-slots fix)
bdero May 7, 2026
db712d6
chore: bump dart_sdk_revision to 109ff541113 (DD sentinel-fill)
bdero May 7, 2026
95c23d4
chore: bump dart_sdk_revision to afa77ceb273 (DD sentinel-fill amend)
bdero May 7, 2026
edc510b
fix: harden DD analysis pass — version-match analyze_snapshot, fail l…
bdero May 7, 2026
fc55f58
chore: bump dart_sdk_revision to 49005fce564 (May 7 review fixes)
bdero May 8, 2026
b235f3a
chore: bump dart_sdk_revision to 93ded8b64ac (review #5/#7/#9/#10/#11)
bdero May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DEPS
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ vars = {
'skia_git': 'https://skia.googlesource.com',
'llvm_git': 'https://llvm.googlesource.com',
'skia_revision': 'a183ded9ad67d998a5b0fe4cd86d3ef5402ffb45',
"dart_sdk_revision": "3f8b97e369a83033089608c86c996a3f67897f8c",
"dart_sdk_revision": "93ded8b64ac9b0a35a6a1d6f2d0b88c2c10a76c1",
"dart_sdk_git": "git@github.com:shorebirdtech/dart-sdk.git",
"updater_git": "https://github.com/shorebirdtech/updater.git",
"updater_rev": "34509fca3c65388ebc84cfaea8f38733ce41f41a",
Expand Down
3 changes: 2 additions & 1 deletion engine/src/flutter/shell/common/shorebird/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ if (shorebird_updater_supported) {
"$shorebird_updater_dir/Cargo.lock",
"$shorebird_updater_dir/library/Cargo.toml",
"$shorebird_updater_dir/library/build.rs",
"$shorebird_updater_dir/library/cbindgen.toml",
"$shorebird_updater_dir/library/cbindgen_dart.toml",
"$shorebird_updater_dir/library/cbindgen_engine.toml",
"$shorebird_updater_dir/library/.cargo/config.toml",
]
inputs += shorebird_updater_rs_sources
Expand Down
212 changes: 212 additions & 0 deletions packages/flutter_tools/lib/src/base/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:io' show Platform;

import 'package:process/process.dart';

import '../artifacts.dart';
Expand Down Expand Up @@ -29,7 +31,9 @@ class GenSnapshot {
required Artifacts artifacts,
required ProcessManager processManager,
required Logger logger,
required FileSystem fileSystem,
}) : _artifacts = artifacts,
_fileSystem = fileSystem,
_processUtils = ProcessUtils(logger: logger, processManager: processManager);

final Artifacts _artifacts;
Expand All @@ -54,6 +58,75 @@ class GenSnapshot {
' See dartbug.com/30524 for more information.',
};

/// Returns the path to an analyze_snapshot binary that matches gen_snapshot's
/// SDK version, or null if no matching binary can be found.
///
/// gen_snapshot and analyze_snapshot share a snapshot-format hash baked into
/// each binary; if the hashes disagree, analyze_snapshot rejects gen_snapshot's
/// output with "Wrong full snapshot version" at parse time. Pre-existing binary
/// layouts (e.g. an older `universal/analyze_snapshot_<arch>` left behind from
/// a previous BUILD.gn revision) can shadow the freshly built one. Resolve by
/// probing every candidate location and returning only the one whose
/// `--sdk_version` matches gen_snapshot's `--version` output.
String? getAnalyzeSnapshotPath(SnapshotType snapshotType, DarwinArch? darwinArch) {
final Artifact genSnapshotArtifact;
final String analyzeName;
if (snapshotType.platform == TargetPlatform.ios ||
snapshotType.platform == TargetPlatform.darwin) {
if (darwinArch == DarwinArch.arm64) {
genSnapshotArtifact = Artifact.genSnapshotArm64;
analyzeName = 'analyze_snapshot_arm64';
} else {
genSnapshotArtifact = Artifact.genSnapshotX64;
analyzeName = 'analyze_snapshot_x64';
}
} else {
genSnapshotArtifact = Artifact.genSnapshot;
analyzeName = 'analyze_snapshot';
}
final String genSnapshotPath = getSnapshotterPath(snapshotType, genSnapshotArtifact);
final String? genVersion = _probeSdkVersion(genSnapshotPath, '--version');
final String dir = _fileSystem.path.dirname(genSnapshotPath);
// Cached SDK layout puts analyze_snapshot alongside gen_snapshot. Local
// engine layout resolves gen_snapshot via .../universal/gen_snapshot_arm64
// while analyze_snapshot lives one level up at the build-dir root. Probe
// both. If we successfully read gen_snapshot's version, accept only a
// candidate whose --sdk_version matches; otherwise fall back to the first
// existing candidate (better than failing closed when the version probe
// itself misbehaves).
for (final candidate in <String>[
_fileSystem.path.join(dir, analyzeName),
_fileSystem.path.join(dir, '..', analyzeName),
]) {
if (!_fileSystem.file(candidate).existsSync()) {
continue;
}
if (genVersion == null) {
return candidate;
}
final String? candidateVersion = _probeSdkVersion(candidate, '--sdk_version');
if (candidateVersion == genVersion) {
return candidate;
}
}
return null;
}

/// Runs [binary] with [versionFlag] and returns the trimmed stderr output,
/// or null if the binary couldn't be invoked or printed nothing.
/// gen_snapshot and analyze_snapshot both write their version line to stderr.
String? _probeSdkVersion(String binary, String versionFlag) {
try {
final RunResult result = _processUtils.runSync(<String>[binary, versionFlag]);
final String stderr = result.stderr.trim();
return stderr.isEmpty ? null : stderr;
} on Exception {
return null;
}
}

final FileSystem _fileSystem;

Future<int> run({
required SnapshotType snapshotType,
DarwinArch? darwinArch,
Expand Down Expand Up @@ -95,15 +168,18 @@ class AOTSnapshotter {
}) : _logger = logger,
_fileSystem = fileSystem,
_xcode = xcode,
_processUtils = ProcessUtils(logger: logger, processManager: processManager),
_genSnapshot = GenSnapshot(
artifacts: artifacts,
processManager: processManager,
logger: logger,
fileSystem: fileSystem,
);

final Logger _logger;
final FileSystem _fileSystem;
final Xcode _xcode;
final ProcessUtils _processUtils;
final GenSnapshot _genSnapshot;

/// Builds an architecture-specific ahead-of-time compiled snapshot of the specified script.
Expand Down Expand Up @@ -229,6 +305,22 @@ class AOTSnapshotter {
genSnapshotArgs.add(mainPath);

final snapshotType = SnapshotType(platform, buildMode);

final int ddMaxBytes = _readDdMaxBytes();
if (ddMaxBytes > 0 && usesLinker) {
final int pass1Exit = await _runDdAnalysisPass(
snapshotType: snapshotType,
darwinArch: darwinArch,
outputDir: outputDir,
baseGenSnapshotArgs: genSnapshotArgs,
mainPath: mainPath,
ddMaxBytes: ddMaxBytes,
);
if (pass1Exit != 0) {
return pass1Exit;
}
}

final int genSnapshotExitCode = await _genSnapshot.run(
snapshotType: snapshotType,
additionalArgs: genSnapshotArgs,
Expand Down Expand Up @@ -257,6 +349,126 @@ class AOTSnapshotter {
}
}

/// Reads the SHOREBIRD_DD_MAX_BYTES env var (preferring `dart-define` over
/// the process environment). Returns 0 if unset, malformed, or non-positive,
/// which signals "no DD pass."
int _readDdMaxBytes() {
const fromDefine = String.fromEnvironment('SHOREBIRD_DD_MAX_BYTES');
final String? raw = fromDefine.isNotEmpty ? fromDefine : Platform.environment['SHOREBIRD_DD_MAX_BYTES'];
return int.tryParse(raw ?? '') ?? 0;
}

/// Runs the DD analysis pass: gen_snapshot → ELF + DD identity, then
/// analyze_snapshot to compute the DD table, caller links, and slot mapping.
/// On success, mutates [baseGenSnapshotArgs] to add `--dd_slot_mapping=...`
/// before [mainPath] so the subsequent gen_snapshot run picks it up.
/// Returns the gen_snapshot pass-1 exit code (0 on success).
Future<int> _runDdAnalysisPass({
required SnapshotType snapshotType,
required DarwinArch? darwinArch,
required Directory outputDir,
required List<String> baseGenSnapshotArgs,
required String mainPath,
required int ddMaxBytes,
}) async {
_logger.printTrace('DD 2-pass build: dd_max_bytes=$ddMaxBytes');

String linkPath(String name) => _fileSystem.path.join(outputDir.parent.path, name);
final String elfForAnalysis = linkPath('App_dd_analysis.so');
final String ddIdentityPath = linkPath('App.dd_identity.link');
final String ddTablePath = linkPath('App.dd.link');
final String ddCallerLinksPath = linkPath('App.dd_callers.link');
final String ddSlotMappingPath = linkPath('App.dd_slots.link');

// Pass 1: build ELF for analysis + DD identity. Strip the existing snapshot
// kind/output args from the base set; mainPath must remain at the end.
final elfArgs = <String>[
...baseGenSnapshotArgs.where((String a) =>
a != mainPath &&
!a.startsWith('--snapshot_kind=') &&
!a.startsWith('--assembly=') &&
!a.startsWith('--elf=')),
'--snapshot_kind=app-aot-elf',
'--elf=$elfForAnalysis',
'--print_dd_function_identity_to=$ddIdentityPath',
mainPath,
];
final int pass1Exit = await _genSnapshot.run(
snapshotType: snapshotType,
additionalArgs: elfArgs,
darwinArch: darwinArch,
);
if (pass1Exit != 0) {
_logger.printError('DD pass 1 (ELF for analysis) failed with exit code $pass1Exit');
return pass1Exit;
}

final String? analyzeSnapshotPath = _genSnapshot.getAnalyzeSnapshotPath(snapshotType, darwinArch);
if (analyzeSnapshotPath == null) {
_logger.printError(
'DD pass: could not find an analyze_snapshot binary whose --sdk_version '
'matches gen_snapshot. The release will ship without DD activation; '
'patches against it will fall back to on-the-fly DD computation and '
'produce a structurally divergent snapshot (devastating link percentage). '
'Aborting the build instead.',
);
_fileSystem.file(elfForAnalysis).deleteSync();
return 1;
}

final int ddTableExit = await _processUtils.stream(<String>[
analyzeSnapshotPath,
'--compute_dd_table=$ddTablePath',
'--dd_caller_links=$ddCallerLinksPath',
'--dd_max_bytes=$ddMaxBytes',
elfForAnalysis,
]);
if (ddTableExit != 0) {
_logger.printError(
'DD pass: analyze_snapshot --compute_dd_table failed with exit code '
'$ddTableExit. App.dd.link will not be produced and the release would '
'ship without DD activation.',
);
_fileSystem.file(elfForAnalysis).deleteSync();
return ddTableExit;
}

final int ddSlotMappingExit = await _processUtils.stream(<String>[
analyzeSnapshotPath,
'--compute_dd_slot_mapping=$ddSlotMappingPath',
'--dd_table_data=$ddTablePath',
'--dd_caller_links=$ddCallerLinksPath',
'--dd_function_identity=$ddIdentityPath',
elfForAnalysis,
]);
if (ddSlotMappingExit != 0) {
_logger.printError(
'DD pass: analyze_snapshot --compute_dd_slot_mapping failed with exit '
'code $ddSlotMappingExit. The DD slot mapping will not be available and '
'gen_snapshot pass 2 would emit a no-DD snapshot.',
);
_fileSystem.file(elfForAnalysis).deleteSync();
return ddSlotMappingExit;
}

if (!_fileSystem.file(ddSlotMappingPath).existsSync()) {
_logger.printError(
'DD pass: analyze_snapshot --compute_dd_slot_mapping reported success '
'but $ddSlotMappingPath was not produced.',
);
_fileSystem.file(elfForAnalysis).deleteSync();
return 1;
}
baseGenSnapshotArgs.insert(
baseGenSnapshotArgs.indexOf(mainPath),
'--dd_slot_mapping=$ddSlotMappingPath',
);
_logger.printTrace('DD 2-pass build: added --dd_slot_mapping');

_fileSystem.file(elfForAnalysis).deleteSync();
return 0;
}

/// Builds an iOS or macOS framework at [outputPath]/App.framework from the assembly
/// source at [assemblyPath].
Future<int> _buildFramework({
Expand Down
14 changes: 11 additions & 3 deletions packages/flutter_tools/lib/src/build_system/targets/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -521,11 +521,13 @@ abstract final class LinkSupplement {
}

void maybeCopy(String name) {
final File file = environment.fileSystem.file(
environment.fileSystem.path.join(inputBuildDir, name),
);
final path = environment.fileSystem.path.join(inputBuildDir, name);
final File file = environment.fileSystem.file(path);
if (file.existsSync()) {
file.copySync(environment.fileSystem.path.join(shorebirdDir.path, name));
environment.logger.printTrace('LinkSupplement: copied $name');
} else {
environment.logger.printTrace('LinkSupplement: missing $name at $path');
}
}

Expand All @@ -537,5 +539,11 @@ abstract final class LinkSupplement {
maybeCopy('App.dispatch_table.json');
maybeCopy('App.ft.link');
maybeCopy('App.field_table.json');
// DD table files for cascade limiter (produced by 2-pass release build
// when SHOREBIRD_DD_MAX_BYTES is set).
maybeCopy('App.dd.link');
maybeCopy('App.dd_callers.link');
maybeCopy('App.dd_identity.link');
maybeCopy('App.dd_slots.link');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ void main() {
artifacts: artifacts,
logger: logger,
processManager: processManager,
fileSystem: MemoryFileSystem.test(),
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,14 @@ flavors:
.createSync(recursive: true);

final build = environment.buildDir.path;
// Both archs go through the same code path now (the DD-pass is gated on
// SHOREBIRD_DD_MAX_BYTES which isn't set in this test), so neither
// architecture has an extra async hop. With concurrent Future.wait, the
// archs reach gen_snapshot in iteration order: arm64 first
// (`kDarwinArchs = 'arm64 x86_64'`), x86_64 second. They then
// interleave at each subsequent await point in the same order.
processManager.addCommands(<FakeCommand>[
// arm64 gen_snapshot runs first (iteration order).
FakeCommand(
command: <String>[
'Artifact.genSnapshotArm64.TargetPlatform.darwin.release',
Expand All @@ -900,6 +907,7 @@ flavors:
environment.buildDir.childFile('app.dill').path,
],
),
// x86_64 gen_snapshot runs next.
FakeCommand(
command: <String>[
'Artifact.genSnapshotX64.TargetPlatform.darwin.release',
Expand All @@ -910,6 +918,7 @@ flavors:
environment.buildDir.childFile('app.dill').path,
],
),
// From here on the two builds interleave: arm64 then x86_64 at each step.
FakeCommand(
command: <String>[
'xcrun',
Expand Down
Loading