feat: include snapshots in patch debug dump (#3672)

This commit is contained in:
Brandon DeRosier
2026-03-31 17:13:25 -07:00
committed by GitHub
parent f6ff284f73
commit 54d63a05a8
2 changed files with 135 additions and 0 deletions
@@ -179,6 +179,30 @@ class Apple {
Future<void> dumpDebugInfo() async {
if (dumpDebugInfoDir == null) return;
// Copy snapshots into the debug dump for offline diagnosis.
// 1 release snapshot + 3 patch compilation stages.
final snapshotsDir = Directory(p.join(dumpDebugInfoDir.path, 'snapshots'))
..createSync(recursive: true);
void maybeCopySnapshot(File file, {String? destName}) {
if (file.existsSync()) {
file.copySync(
p.join(snapshotsDir.path, destName ?? p.basename(file.path)),
);
}
}
maybeCopySnapshot(releaseArtifact, destName: 'App');
maybeCopySnapshot(aotOutputFile);
// Intermediate snapshots created by aot_tools during linking.
final patchDir = aotOutputFile.parent.path;
final patchBaseName = p.basenameWithoutExtension(aotOutputFile.path);
maybeCopySnapshot(
File(p.join(patchDir, '$patchBaseName.ct.aot')),
);
maybeCopySnapshot(
File(p.join(patchDir, '$patchBaseName.optimized.aot')),
);
final debugInfoZip = await dumpDebugInfoDir.zipToTempFile();
debugInfoZip.copySync(p.join('build', Patcher.debugInfoFile.path));
logger.detail('Link debug info saved to ${Patcher.debugInfoFile.path}');
@@ -579,6 +579,117 @@ Otherwise, to repair macos, run "flutter create . --platforms macos"''',
).called(1);
});
test('copies existing snapshots into debug dump', () async {
// Create a release artifact in a temp directory.
final releaseDir = Directory.systemTemp.createTempSync();
final releaseArtifactFile = File(p.join(releaseDir.path, 'App'))
..writeAsStringSync('release');
// Create intermediate patch snapshots alongside out.aot.
File(p.join(buildDirectory.path, 'out.ct.aot'))
..writeAsStringSync('ct');
File(p.join(buildDirectory.path, 'out.optimized.aot'))
..writeAsStringSync('optimized');
// Capture the dumpDebugInfoPath so we can inspect it.
String? capturedDebugInfoPath;
when(
() => aotTools.link(
base: any(named: 'base'),
patch: any(named: 'patch'),
analyzeSnapshot: any(named: 'analyzeSnapshot'),
genSnapshot: any(named: 'genSnapshot'),
kernel: any(named: 'kernel'),
outputPath: any(named: 'outputPath'),
workingDirectory: any(named: 'workingDirectory'),
additionalArgs: any(named: 'additionalArgs'),
dumpDebugInfoPath: any(named: 'dumpDebugInfoPath'),
),
).thenAnswer((invocation) async {
capturedDebugInfoPath =
invocation.namedArguments[#dumpDebugInfoPath] as String?;
return linkPercentage;
});
await runWithOverrides(
() => apple.runLinker(
aotOutputFile: aotOutputFile,
kernelFile: File('missing'),
releaseArtifact: releaseArtifactFile,
vmCodeFile: File('missing'),
splitDebugInfoArgs: [],
),
);
expect(capturedDebugInfoPath, isNotNull);
final snapshotsDir = Directory(
p.join(capturedDebugInfoPath!, 'snapshots'),
);
expect(snapshotsDir.existsSync(), isTrue);
final snapshotFiles = snapshotsDir
.listSync()
.whereType<File>()
.map((f) => p.basename(f.path))
.toSet();
expect(
snapshotFiles,
containsAll(<String>[
'App',
'out.aot',
'out.ct.aot',
'out.optimized.aot',
]),
);
});
test('skips missing intermediate snapshots', () async {
// Release artifact exists but intermediate snapshots do not.
final releaseDir = Directory.systemTemp.createTempSync();
final releaseArtifactFile = File(p.join(releaseDir.path, 'App'))
..writeAsStringSync('release');
String? capturedDebugInfoPath;
when(
() => aotTools.link(
base: any(named: 'base'),
patch: any(named: 'patch'),
analyzeSnapshot: any(named: 'analyzeSnapshot'),
genSnapshot: any(named: 'genSnapshot'),
kernel: any(named: 'kernel'),
outputPath: any(named: 'outputPath'),
workingDirectory: any(named: 'workingDirectory'),
additionalArgs: any(named: 'additionalArgs'),
dumpDebugInfoPath: any(named: 'dumpDebugInfoPath'),
),
).thenAnswer((invocation) async {
capturedDebugInfoPath =
invocation.namedArguments[#dumpDebugInfoPath] as String?;
return linkPercentage;
});
await runWithOverrides(
() => apple.runLinker(
aotOutputFile: aotOutputFile,
kernelFile: File('missing'),
releaseArtifact: releaseArtifactFile,
vmCodeFile: File('missing'),
splitDebugInfoArgs: [],
),
);
final snapshotsDir = Directory(
p.join(capturedDebugInfoPath!, 'snapshots'),
);
final snapshotFiles = snapshotsDir
.listSync()
.whereType<File>()
.map((f) => p.basename(f.path))
.toSet();
// Only release + patch should be present (no ct/optimized).
expect(snapshotFiles, equals(<String>{'App', 'out.aot'}));
});
group('when running in codemagic', () {
late Directory codemagicExportDir;