Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
aa8a25e
Add 2-pass release build for Dynamic Dispatch table
bdero Mar 30, 2026
e34661d
Add DD analysis gen_snapshot command to test expectations
bdero Mar 31, 2026
2e3c4f9
Fix macOS universal binary test command order for concurrent DD build
bdero Mar 31, 2026
c6ae274
Skip DD table computation when analyze_snapshot is absent
bdero Apr 1, 2026
3823fa5
Fix macOS universal binary test command order for DD table async
bdero Apr 1, 2026
67cb3e3
fix: pass DD function identity file in base build pipeline
bdero Apr 1, 2026
cf427f6
feat: make DD table max bytes configurable via environment variable
bdero Apr 2, 2026
b6e9f29
chore: bump dart_sdk_revision to cascade-limiter
bdero Apr 3, 2026
8d841d7
chore: checkpoint current DD table base build changes
bdero Apr 7, 2026
90bc6d6
fix: probe gen_snapshot for DD flag support before running DD pipeline
bdero Apr 8, 2026
cef1af2
feat: DD 2-pass release build for cascade limiter
bdero Apr 20, 2026
711ab2e
fix: address flutter_tools test failures
bdero May 2, 2026
9134ff1
review: address build.dart feedback (dead field, fallback, helper ext…
bdero May 6, 2026
401a2a8
revert: drop universal/ analyze_snapshot move; probe parent dir instead
bdero May 7, 2026
0c597a0
chore: bump dart_sdk_revision to 59680e070c3 (cascade-limiter HEAD)
bdero May 7, 2026
e2352a5
chore: bump dart_sdk_revision to f9f552a6e77 (DD VERIFY all-slots fix)
bdero May 7, 2026
1966daf
chore: bump dart_sdk_revision to 109ff541113 (DD sentinel-fill)
bdero May 7, 2026
cb04314
chore: bump dart_sdk_revision to afa77ceb273 (DD sentinel-fill amend)
bdero May 7, 2026
58e5497
fix: harden DD analysis pass — version-match analyze_snapshot, fail l…
bdero May 7, 2026
2a59d65
chore: bump dart_sdk_revision to 49005fce564 (May 7 review fixes)
bdero May 8, 2026
c2fa987
chore: bump dart_sdk_revision to 93ded8b64ac (review #5/#7/#9/#10/#11)
bdero May 8, 2026
cec42c6
chore: bump dart_sdk_revision to c1f23751d53 (post-rebase)
bdero May 13, 2026
4abfc77
feat(flutter_tools): pass --print_dd_resolution_to and copy into supp…
bdero May 13, 2026
0fcc97b
chore: dart format build.dart (CI format check)
bdero May 13, 2026
762e67f
chore: bump dart_sdk_revision to f3656bfc678 (post-merge of #785)
bdero May 14, 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": "f3656bfc67891cb77578149fe3e869b15c78be13",
"dart_sdk_git": "git@github.com:shorebirdtech/dart-sdk.git",
"updater_git": "https://github.com/shorebirdtech/updater.git",
"updater_rev": "90c349287d170991d1b527b7dac7be4fac508768",
Expand Down
225 changes: 225 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,139 @@ 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');
final String ddResolutionPath = linkPath('App.dd_resolution.tsv');

// 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;
}
final int mainPathIndex = baseGenSnapshotArgs.indexOf(mainPath);
baseGenSnapshotArgs.insertAll(mainPathIndex, <String>[
'--dd_slot_mapping=$ddSlotMappingPath',
// Per-slot resolution outcome dump (TSV: slot, outcome, rewritten,
// name). Diagnostic for analyzing which slots got dropped during
// the resolver's run; ends up alongside the other supplement files
// and gets carried into the patch debug bundle.
'--print_dd_resolution_to=$ddResolutionPath',
]);
_logger.printTrace('DD 2-pass build: added --dd_slot_mapping and --print_dd_resolution_to');

_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
16 changes: 13 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,13 @@ 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');
// Per-slot DD resolution outcome diagnostic (TSV).
maybeCopy('App.dd_resolution.tsv');
}
}
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