Skip to content

[FastDev] Add faster FastDeploy2#11698

Closed
simonrozsival wants to merge 19 commits into
dotnet:mainfrom
simonrozsival:android-fastdeploy2-clean
Closed

[FastDev] Add faster FastDeploy2#11698
simonrozsival wants to merge 19 commits into
dotnet:mainfrom
simonrozsival:android-fastdeploy2-clean

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

This is the cleaned-up follow-up to the experimentation PR:

This PR keeps legacy FastDeploy in place, adds a new FastDeploy2 strategy, and makes FastDeploy2 the default for app install fast deployment.

$(_AndroidFastDevStrategy)=FastDeploy|FastDeploy2

FastDeploy remains selectable as a fallback.

Review follow-up

Jon's review feedback is addressed in the latest cleanup commit:

  • FastDeploy2 no longer accepts legacy FastDeploy task inputs it does not use.
  • FastDeploy2 is now a single concrete task with no FastDeploy2Base inheritance split.
  • Development-only FastDeploy2 diagnostic/telemetry JSON was removed, so production builds only keep the functional manifest state.
  • FastDeploy2 manifest JSON serialization uses a System.Text.Json source-generated context.
  • Repeated remote path, shell quoting, and directory grouping helpers are consolidated.
  • Remote staging now uses Android's writable /data/local/tmp/fastdeploy2 instead of /tmp/fastdeploy2, because /tmp is read-only on CI emulators.
  • The local manifest now records device/package/user/ABI identity, and device-side staging/override state is validated with manifest hash markers read in a single adb shell call.
  • FastDeploy2 diagnostic logging now records exact adb commands, exit codes, stdout, and stderr for device-state sanity checks.

A fresh dotnet new maui Android sample build binlog is available here:

CI on-device test fixes

The first full CI run surfaced regressions in existing fast-deployment device tests now that FastDeploy2 is the default. Three deterministic root causes were found (each reproduced and verified on an API 36 emulator) and fixed:

  • Missing sync log markers. IncrementalFastDeployment and SkipFastDevAlreadyInstalledFile assert per-file NotifySync CopyFile / NotifySync SkipCopyFile markers. FastDeploy2 only logged Prepared X => Y, so these tests failed even though deployment succeeded. FastDeploy2 now emits the markers from the manifest's changed-file set, and the abi-skip marker uses the same device-relative identifier for consistency.
  • Symlink mode followed subdirectory symlinks into staging. The full rewrite used ln -sf "$s"/* ., which also symlinked staging subdirectories. Processing a child directory then cd'd through that symlink into the shell-owned /data/local/tmp staging area, and rm -rf ./* failed with "Permission denied" under run-as (XA0133 / XA0129). This broke any app with culture/satellite subdirectories (LocalizedAssemblies_ShouldBeFastDeployed, DotNetNewAndroidTest). The full rewrite now clears and symlinks only the regular files in each directory and leaves subdirectories to their own iterations, so it no longer follows or deletes child directories.
  • Copy-fallback stat format split by the device shell. find ... -exec stat -c "%n|%s|%Y" {} + reached the device shell unquoted, so | was treated as a pipe (exit 127 → XA0129) and the copy fallback also failed. The command is now built as a single string with the format single-quoted (matching the RunAsShell quoting pattern).
  • Device-state probe printf '\n' arrived as a literal \n. The readiness check built its probe with printf '\noverride=' / printf '\n'; the backslash escape does not survive the adb + device-shell quoting layers, so the device emitted remote=<hash>\noverride=<hash>\n on a single line. ParseDeviceManifestState (splitting on real newlines) put the whole tail into RemoteHash and left OverrideHash empty, so remoteReady was always false and every incremental install reset the staging directory and redeployed all files (changed files: 329 in CI), reporting unchanged assemblies as CopyFile. The probe is now built with echo + command substitution (real newlines, no backslash escapes); verified on an API 36 emulator that remoteReady becomes true on an unchanged reinstall.

FastDeploy2 approach

FastDeploy2 uses the final direction from the experiment PR:

  1. Build a local manifest of the deployment target and FastDev inputs:

    device id, package, Android user, ABI
    target path -> local source path, size, mtime
    
  2. Compare with the last successful deploy manifest, then verify that the device-side staging tree and app override tree both carry the expected manifest hash.

  3. Push only changed files to device temp storage, without adb push --sync:

    adb push -z any changed-file-1 changed-file-2 /data/local/tmp/fastdeploy2/<package>/<user>/<abi>/
  4. Remove stale staged files for removed inputs.

  5. Maintain files/.__override__ as symlinks to staged files with batched run-as sh -c commands. The generated shell scripts use short d/s variables plus cd so long source/target directories are not repeated for every file.

  6. After a successful deploy, write the manifest and matching device-side manifest hash markers:

    /data/local/tmp/fastdeploy2/<package>/<user>/.fastdeploy2-manifest-hash
    files/.__override__/.fastdeploy2-manifest-hash
    

Why this shape is viable now

The existing FastDeploy v2.0 design was added when Android 11 broke the older external-storage based fast deployment model. At that time, adb compression and multi-file push/sync behavior were still new: Android SDK Platform-Tools 30.0.0 (April 2020) introduced client-side compression for adb {push,pull,sync} when used with Android 11 devices, and Platform-Tools 30.0.5 (November 2020) later fixed adb push --sync with multiple inputs and improved pushing many files over high-latency links.

Today it is more reasonable to depend on a modern Android SDK Platform-Tools package for developer inner-loop scenarios. adb is supplied by the Android SDK Platform-Tools package, and developers using .NET for Android / MAUI can update it independently of end-user device support. With Platform-Tools 36.0.0, adb push -z any was also tested against API 24 and API 29 emulators that did not advertise sendrecv_v2* compression features; both accepted the command and transferred the file, apparently negotiating down to uncompressed transfer.

This PR also no longer relies on adb push --sync: the local manifest tells us exactly which files changed, so FastDeploy2 pushes only those files and avoids an adb-side scan of the full directory.

What else we tried but abandoned

From the experiment PR:

  • Existing FastDeploy is excellent for very small changes but very slow for full payloads.
  • adb push --sync over the whole directory is much faster for full payloads, but still scans/probes too much for tiny changes.
  • One adb push per file is too slow because every adb invocation has ~40ms median overhead even when skipped.
  • A local manifest gives us the changed set without probing all files through adb.
  • Symlinks avoid app-private copy after the initial setup.
  • A shell-based symlink update was faster than a native helper (maui.link), so this PR does not include a native helper.

How legacy FastDeploy could be improved instead

If we wanted to keep the native FastDeploy tool model, the most promising improvement would be to make xamarin.sync batch-aware. Instead of invoking run-as ... xamarin.sync once per changed file, the host could open a single run-as <package> xamarin.sync-batch stdin stream and write all changed files one after another using a small binary protocol:

magic/version
file-count
repeat file-count times:
  target-path-length
  target-path
  uncompressed-size
  mtime
  compressed-blocks
removed-file-count
repeat removed-file-count times:
  target-path-length
  target-path

The device helper would create directories, write each file to a temporary path, set mtime/permissions, rename into place, and remove stale files. This would keep the existing app-private real-file model and avoid symlink assumptions, while removing most per-file adb/run-as process overhead.

That approach is intentionally not part of this PR because it keeps the custom native tool/protocol complexity. FastDeploy2 tries the simpler path first: use adb's built-in transfer/compression support, keep the changed-file decision local, and leave legacy FastDeploy available as a fallback while we validate the symlink approach across more devices.

Preliminary benchmark from the experiment PR

Device: physical Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V) connected by USB 3 cable
Project: samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj

Strategy Case Deploy task Wall Upload/transfer Changed
FastDeploy (existing) first 22.11s 31.61s 19.09s 194
FastDeploy (existing) one DLL 0.68s 10.10s 0.14s 2
FastDeploy (existing) app DLLs 0.69s 10.10s 0.20s 3
FastDeploy (existing) all DLLs 17.72s 27.27s 17.16s 189
FastDeploy2 first 5.41s 14.51s 3.09s 195
FastDeploy2 one DLL 0.56s 9.70s 0.06s 2
FastDeploy2 app DLLs 0.59s 9.99s 0.04s 3
FastDeploy2 all DLLs 3.02s 12.40s 2.42s 189

Validation

Focused task builds pass in this branch:

dotnet build-server shutdown
 dotnet build build-tools/xa-prep-tasks/xa-prep-tasks.csproj --nologo -v:minimal -m:1
 dotnet build src/Xamarin.Android.Build.Debugging.Tasks/Xamarin.Android.Build.Debugging.Tasks.csproj --no-restore --nologo -v:minimal -m:1
 dotnet build build-tools/xa-prep-tasks/xa-prep-tasks.csproj -c Release --nologo -v:minimal -m:1
 dotnet build src/Xamarin.Android.Build.Debugging.Tasks/Xamarin.Android.Build.Debugging.Tasks.csproj -c Release --no-restore --nologo -v:minimal -m:1

Device/emulator validation passed:

Device Scenario Result
Physical Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V) over USB 3 cable clean first install with default FastDeploy2 symlink mode passed; app launched successfully
Physical Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V) over USB 3 cable one-DLL incremental install with default FastDeploy2 symlink mode passed; app launched successfully
Physical Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V) over USB 3 cable one-DLL incremental install with copy fallback mode passed; app launched successfully
API 36 emulator clean first install with shortened d/s shell-variable symlink scripts passed; symlink tree created successfully
API 36 emulator Symlink-mode install followed by Copy-mode install passed; symlinks were replaced by regular files and the symlink marker was removed

Observed validation details:

first default: deploy=15.84s, mode=Symlink, changed=195, pushed=195, symlink update=161ms
one DLL default: deploy=0.89s, mode=Symlink, changed=2, pushed=2, symlink update=4ms
one DLL Copy fallback: deploy=2.80s, mode=Copy, changed=2, pushed=2, copy=977ms

Additional adb compatibility probes with Platform-Tools 36.0.0:

Target Device adb features adb push -z any
API 24 emulator cmd only, no sendrecv_v2* passed
API 29 emulator cmd only, no sendrecv_v2* passed
API 36 emulator sendrecv_v2, brotli, lz4, zstd passed

Risks

  • Legacy FastDeploy is still present and selectable.
  • Symlinks from app-private files/.__override__ to /data/local/tmp/fastdeploy2/... worked on the tested physical Samsung device over USB.
  • The new approach has only been tested on a single device from a single vendor so far; it needs broader Android/OEM/API/device testing.
  • If symlink creation fails, this code can fallback to copy mode and clears symlink-managed override state before copying.
  • If the local manifest is missing, does not match the current device/package/user/ABI, or does not match the device-side manifest hash marker, FastDeploy2 does a full push.
  • Developers with very old Android SDK Platform-Tools may need to update adb for adb push -z any; older devices can still fall back to uncompressed transfer when driven by a modern adb.

Future work

  • Delete the old FastDeploy implementation once we are confident the new approach is better across a broader device matrix.
  • Delete the old native FastDeploy helper tools once FastDeploy no longer needs them.
  • If symlink validation across devices is not good enough, consider a batched xamarin.sync protocol as the app-private real-file fallback design.

Add a new FastDeploy2 strategy that uses a local manifest to push only changed files to temporary device storage and mirrors the app override directory with shell-created symlinks. Keep legacy FastDeploy selectable while making FastDeploy2 the default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove existing override paths before copying changed files so Copy mode can safely recover from a symlink-based override tree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as ready for review June 18, 2026 10:30
Copilot AI review requested due to automatic review settings June 18, 2026 10:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new manifest-driven fast deployment implementation (FastDeploy2) alongside the legacy FastDeploy, and makes FastDeploy2 the default strategy for app install fast deployment.

Changes:

  • Register and invoke the new FastDeploy2 MSBuild task, with _AndroidFastDevStrategy defaulting to FastDeploy2.
  • Add FastDeploy2Base (core orchestration + adb/run-as helpers) plus a manifest-based FastDeploy2 implementation that pushes only changed files and updates overrides via symlink (with copy fallback).
  • Add MSBuild-time validation for supported strategy/transfer-mode values, and a new adb compression algorithm property default (any).
Show a summary per file
File Description
src/Xamarin.Android.Build.Debugging.Tasks/Xamarin.Android.Common.Debugging.targets Registers FastDeploy2, selects strategy, sets defaults for strategy/transfer mode/compression, and wires task invocation into _Upload.
src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Adds FastDeploy2Base task implementation: device/package orchestration, adb invocation, file staging/stat/diff/copy paths, diagnostics logging, and error handling.
src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Implements manifest-driven incremental upload, remote “ready” marker handling, stale cleanup, and shell-symlink override updates with copy fallback.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 8

Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Outdated
@simonrozsival simonrozsival changed the title [WIP] Add manifest-driven FastDeploy2 [FastDev] Add manifest-driven FastDeploy2 Jun 18, 2026
simonrozsival and others added 4 commits June 18, 2026 13:01
Fix manifest reset handling, device-scoped manifest state, adb batching edge cases, symlink/copy recovery, and diagnostics logging concurrency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use shell variables and cd to avoid repeating long staging and override paths in generated run-as symlink scripts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop leftover experimental staging and symlink helper methods that are no longer referenced by the manifest-driven direct-push implementation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Skip missing FastDev inputs before manifest creation and clear symlink-managed override state before Copy mode so stale symlinks cannot survive mode switches or fallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival changed the title [FastDev] Add manifest-driven FastDeploy2 [FastDev] Add faster FastDeploy2 Jun 18, 2026
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Android PR Reviewer completed successfully!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Needs Changes

Thorough review of the new FastDeploy2 implementation. The architecture is well thought out — manifest-based incremental diffing, a clean strategy/mode split in the targets with condition-first <Error> validation, careful shell-argument quoting (ShellQuote single-quote escaping) and package-name sanitization (IsSafePackageNameForShell) to prevent shell injection, ADB cancellation handling with process kill + output draining, and reuse of the existing XA012x/XA013x error codes. Nice work.

A few things to address before merge:

Issues by severity

  • ⚠️ 2 warnings
    • Order-dependent shell globbing (FastDeploy2.Manifest.cs:132): the full-refresh rm -f ./* / ln -sf "$s"/* . globs also match subdirectories such as the staged {abi}/environment. Because directories is iterated in nondeterministic HashSet order, this can either delete real staged files through a directory symlink or silently fall back to copy. Please handle nested directories explicitly.
    • Dead/duplicated base implementation (FastDeploy2.cs:549): the virtual DeployFastDevFilesWithAdbPush and its exclusive helpers are fully superseded by the override and never run — including a duplicated CreatePushArgsPrefix that still uses --sync. Recommend deleting it (or making the method abstract).
  • 💡 2 suggestions
    • MarkRemoteReady ignores the adb exit code (FastDeploy2.Manifest.cs:447).
    • ManifestEntry.LocalPath is persisted but never read (FastDeploy2.Manifest.cs:496).

See the inline comments for mechanisms and concrete fixes.

CI: license/cla passed; the Azure DevOps builds (Linux/Mac/Windows) are still in progress at review time, so green CI is not yet confirmed — please ensure the internal pipeline passes before merge.

The symlink-glob behavior is the main correctness concern; the rest are maintainability cleanups. I couldn't reproduce on-device, so if I've misread the staging layout for the glob case, a quick confirmation of how {abi}/environment is handled during a full refresh would resolve it.

Generated by Android PR Reviewer for issue #11698 · 1.4K AIC · ⌖ 48.9 AIC · ⊞ 37.8K
Comment /review to run again

Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Outdated

@jonathanpeppers jonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I want to see a .binlog of a deploy to understand what it does -- I might have time to manually test some later.

Overall, I like what it does -- introduce a private property by default you can opt out of if something goes wrong.

Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Outdated
Comment on lines +478 to +489
static string GetSafeFileName (string value)
{
if (string.IsNullOrEmpty (value)) {
return "_";
}

var builder = new StringBuilder (value.Length);
foreach (char c in value) {
builder.Append (char.IsLetterOrDigit (c) || c == '.' || c == '-' || c == '_' ? c : '_');
}
return builder.ToString ();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an existing helper somewhere that does this? I can think of maybe things related to Java names, or Android resources.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked and couldn't find an existing helper for turning an arbitrary string into a filesystem-safe path segment. The Java-name and Android-resource helpers (identifier mangling / resource-designer naming) solve a different problem with different rules — they're about valid C#/Java identifiers, not reversible, collision-free directory names. Here we just need a unique cache directory per device/package/user/abi, which is what Uri.EscapeDataString gives (reversible, no path separators). Happy to switch to a shared helper if you have one in mind — leaving this thread open for your call.

Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Outdated
@simonrozsival

Copy link
Copy Markdown
Member Author

@jonathanpeppers I captured a binlog from building a fresh dotnet new maui sample app with this branch's local SDK:

https://gist.github.com/simonrozsival/c06cacff00c99ee3504c256205e24492

Command used for the successful binlog:

./dotnet-local.sh build JonMauiBinlog/JonMauiBinlog.csproj \
  -f net11.0-android \
  -p:TargetFrameworks=net11.0-android \
  -p:SupportedOSPlatformVersion=24.0 \
  -bl:JonMauiBinlog-net11.0-android.binlog

The TFM override keeps the template Android-only because I only installed maui-android locally; the supported OS override matches the local Android SDK minimum of 24.

simonrozsival and others added 2 commits June 18, 2026 16:22
Move FastDeploy2 diagnostic JSON helpers out of the main task, use System.Text.Json source generation for FastDeploy2 JSON, remove unused FastDeploy2 task inputs, and consolidate repeated path/grouping helpers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Flatten FastDeploy2 into a single concrete task, remove the development diagnostics property bag and JSON payload, and keep only the functional manifest serialization state.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers

Copy link
Copy Markdown
Member

@jonathanpeppers I captured a binlog from building a fresh dotnet new maui sample app with this branch's local SDK:

https://gist.github.com/simonrozsival/c06cacff00c99ee3504c256205e24492

Command used for the successful binlog:

./dotnet-local.sh build JonMauiBinlog/JonMauiBinlog.csproj \
  -f net11.0-android \
  -p:TargetFrameworks=net11.0-android \
  -p:SupportedOSPlatformVersion=24.0 \
  -bl:JonMauiBinlog-net11.0-android.binlog

The TFM override keeps the template Android-only because I only installed maui-android locally; the supported OS override matches the local Android SDK minimum of 24.

I think your command needs -t:Install to see the FastDeploy2 task run.

@simonrozsival

Copy link
Copy Markdown
Member Author

@jonathanpeppers you're right — the earlier binlog was build-only. I updated the same gist with a new -t:Install binlog that includes the FastDeploy2 task:

https://gist.github.com/simonrozsival/c06cacff00c99ee3504c256205e24492

New file:

JonMauiBinlog-net11.0-android-install-fastdeploy2.binlog

Command used:

./dotnet-local.sh build JonMauiBinlog/JonMauiBinlog.csproj \
  -t:Install \
  -f net11.0-android \
  --no-restore \
  -m:1 \
  -nr:false \
  -p:TargetFrameworks=net11.0-android \
  -p:SupportedOSPlatformVersion=24.0 \
  -p:AdbTarget=-s%20R58Y30HZ65V \
  -p:_FastDeploymentDiagnosticLogging=False \
  -bl:JonMauiBinlog-net11.0-android-install-fastdeploy2.binlog \
  -clp:PerformanceSummary \
  -v:minimal

The console log shows FastDeploy2 ran and the build succeeded:

11089 ms  FastDeploy2  1 calls
Build succeeded.

Avoid logging empty run-as command output and buffer optional missing-file messages behind FastDeploy2 diagnostics so normal install binlogs stay readable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers

Copy link
Copy Markdown
Member

There are many log messages that just say rm returned:

image

I think we should probably skip these messages in general unless _FastDeploymentDiagnosticLogging=true. And maybe it should not log if blank?

But then I probably need to see a log where _FastDeploymentDiagnosticLogging is true.

Stage FastDeploy2 files under /data/local/tmp instead of /tmp so Android emulators with read-only /tmp can install. Also remove existing override contents recursively before full symlink refreshes so resource/culture directories do not fail rm.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

@jonathanpeppers to clarify the physical-device validation: that was a Samsung Galaxy A16 (SM-A165F, serial R58Y30HZ65V) connected over a USB 3 cable, not an emulator and not Wi-Fi adb.

The emulator validation rows in the PR description are separate API 24/29/36 emulator checks.

Add target identity to FastDeploy2 manifests and store the manifest hash on both the remote staging tree and the app override tree. Read both device-side hashes in one adb shell command before deciding whether incremental deploy state can be trusted, and expand adb diagnostics to include exact commands, exit codes, stdout, and stderr.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as draft June 22, 2026 11:23
simonrozsival and others added 5 commits June 28, 2026 00:37
FastDeploy2 (the new default fast-deployment strategy) broke several
existing on-device tests. Three deterministic, device-verified root causes:

- Missing sync log markers: tests assert `NotifySync CopyFile`/
  `NotifySync SkipCopyFile` per file. FastDeploy2 only logged
  `Prepared X => Y`, so IncrementalFastDeployment and
  SkipFastDevAlreadyInstalledFile failed even though deployment
  succeeded. Emit the markers from the manifest's changed-file set.

- Symlink mode followed subdirectory symlinks into staging: the full
  rewrite used `ln -sf "$s"/* .`, which also symlinked staging
  subdirectories. Processing a child directory then `cd`'d through that
  symlink into the shell-owned /data/local/tmp staging area and
  `rm -rf ./*` failed with "Permission denied" under run-as (XA0133 /
  XA0129). This broke any app with culture/satellite subdirectories
  (LocalizedAssemblies_ShouldBeFastDeployed, DotNetNewAndroidTest). Only
  symlink regular files, and process parent directories before children
  so a parent's `rm -rf ./*` cannot delete a freshly created child.

- Copy fallback's stat format was split by the device shell: the
  `find ... -exec stat -c "%n|%s|%Y" {} +` argv reached the device shell
  unquoted, so `|` was treated as a pipe (exit 127, XA0129). Build the
  command as a single string with the format single-quoted, matching the
  RunAsShell quoting pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Self-review follow-up. The previous fix replaced `ln -sf "$s"/* .` with a
files-only loop but still ran `rm -rf ./*`, which deletes child directories.
The old glob accidentally masked an incremental-deploy bug by symlinking whole
staging subdirectories: on an incremental deploy where a parent directory gains
all-new files while a child directory keeps unchanged files, the parent's
`rm -rf ./*` wiped the child's still-valid symlinks.

Clear only the regular files in each directory (`for e in ./*; do
[ -d "$e" ] || rm -f "$e"; done`) so subdirectories and their contents survive,
and drop the now-unnecessary parents-first ordering. Verified on an emulator:
the parent relinks its files, the child subdirectory and its symlink are
preserved, and the staging area is untouched.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Self-review follow-up. The abi-skip `NotifySync SkipCopyFile` marker logged
the local build path (file.ItemSpec) while the per-file sync markers log the
device-relative target path, making the NotifySync marker family inconsistent
within the task. Use GetAdbPushTargetPath for the abi-skip marker too so all
markers report the same device-relative identifier.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The device-state readiness check built its probe with
`printf '\noverride='` / `printf '\n'`. The `\n` escape does not survive
the adb + device-shell quoting layers and arrives as a literal two-character
"\n", so the device emits a single line:

  remote=<hash>\noverride=<hash>\n

ParseDeviceManifestState splits on real newlines, so the whole tail lands in
RemoteHash and OverrideHash stays empty. RemoteHash never equals the previous
manifest hash, `remoteReady` is always false, and every incremental install
resets the staging directory and redeploys *all* files (observed as
`changed files: 329` in CI), so unchanged assemblies are reported as
NotifySync CopyFile instead of SkipCopyFile.

Build the probe with `echo` + command substitution instead, which emits real
newlines and contains no backslash escapes to be mangled. Verified on an API 36
emulator: the device now returns two parseable lines and `remoteReady` becomes
true on an unchanged reinstall.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ManifestEntry.LocalPath was written into manifest.json and folded into the
device-side manifest hash, but it is host-specific (an absolute build path) and
nothing reads it for change detection (GetChangedFiles compares Size and
LastWriteTimeUtcTicks; the dictionary key is RelativePath). Persisting and
hashing it bloats the manifest and couples the device readiness check to the
build machine's paths for no benefit. Remove the field, its JSON property, and
its contribution to the canonical manifest text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as ready for review June 29, 2026 06:48
simonrozsival

This comment was marked as outdated.

simonrozsival and others added 2 commits June 29, 2026 08:57
- Remove unused TryParsePushSummary/AdbPushSummaryRegex dead code.
- Make DeployFastDevFilesWithAdbPush failure explicit at the call site.
- Document new FastDeploy2 strategy/transfer-mode/compression knobs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

simonrozsival commented Jun 29, 2026

Copy link
Copy Markdown
Member Author

/review output (fork PR, I had to run it locally):

LGTM — Re-reviewed after the latest commits. All three suggestions from the prior pass are addressed: dead TryParsePushSummary/AdbPushSummaryRegex removed (and the unused RegularExpressions using), DeployFastDevFilesWithAdbPush's result is now explicitly checked, and the new FastDeploy2/transfer-mode/compression knobs are documented in a release note. Task project builds clean (0 warnings). Confirm the queued CI run goes green before merge.

@simonrozsival

Copy link
Copy Markdown
Member Author

Superseded by #11795, which is based on a branch in dotnet/android (origin) instead of a fork. Continuing review there.

jonathanpeppers pushed a commit that referenced this pull request Jul 13, 2026
## Summary

This is the cleaned-up follow-up to the experimentation PR:

- Experiment PR: #11692

This PR keeps legacy `FastDeploy` in place, adds a new `FastDeploy2` strategy, and makes `FastDeploy2` the default for app install fast deployment.

```xml
$(_AndroidFastDevStrategy)=FastDeploy|FastDeploy2
```

`FastDeploy` remains selectable as a fallback.

The final incremental FastDeploy2 hot path uses three serial `adb` processes:

1. one compound live state probe plus app force-stop;
2. one batched push for the changed files; and
3. one combined staging/override manifest finalization.

Switching between FastDeploy2 and legacy FastDeploy is supported without
uninstalling the app, including after `dotnet clean`. If the marker-based fast
path discovers that the remote staging filesystem is incomplete, it retries
once with a fresh staging and override deployment.

## Review follow-up

Jonathan's review feedback is addressed:

- FastDeploy2 no longer accepts legacy FastDeploy task inputs it does not use.
- FastDeploy2 is now a single concrete task with no `FastDeploy2Base` inheritance split.
- Development-only FastDeploy2 diagnostic/telemetry JSON was removed, so production builds only keep the functional manifest state.
- FastDeploy2 manifest JSON serialization uses a `System.Text.Json` source-generated context.
- Repeated remote path, shell quoting, and directory grouping helpers are consolidated.
- Remote staging now uses Android's writable `/data/local/tmp/fastdeploy2` instead of `/tmp/fastdeploy2`, because `/tmp` is read-only on CI emulators.
- The local manifest now records device/package/user/ABI identity, and device-side staging/override state is validated with manifest hash markers read in a single adb shell call.
- FastDeploy2 diagnostic logging now records exact adb commands, exit codes, stdout, and stderr for device-state sanity checks.

### Latest review round (commits `b54be792e`, `da0bfe45c`)

Jonathan's final review identified two remaining gaps: roughly 500 ms of
FastDeploy2 overhead on a one-assembly incremental deployment, and failure when
switching a symlink-managed FastDeploy2 app back to legacy FastDeploy.

- **Warm state and app termination are one live probe.** On an eligible
  incremental deployment, one tagged `adb shell` command reads
  `log.redirect-stdio`, `ro.boot.disable_runas`, the staging/override markers,
  the app-private path and process id, validates `run-as`, and force-stops the
  app. If that probe is incomplete, FastDeploy2 falls back to the individual
  checks so the existing `XA0128` and `XA0131`–`XA0137` diagnostics remain
  actionable.
- **The returned marker state is reused.** FastDeploy2 no longer repeats the
  device-marker probe later in the task.
- **Redundant commands were removed.** An existing-directory DLL update skips
  `mkdir`, and staging/override markers are finalized together.
- **Strategy switching is configuration-aware.** The effective strategy and
  transfer mode are part of the deployment configuration cache. Legacy
  FastDeploy removes the FastDeploy2 override tree before `xamarin.sync` runs;
  a missing cache is treated as changed, so this also works after
  `dotnet clean`.
- **Unexpected remote state has a bounded recovery path.** The marker match
  remains the fast-path filesystem contract. If `adb push` proves an expected
  staging path is missing or has the wrong type, FastDeploy2 clears staging and
  override state and retries once as a full deployment. Success rewrites both
  remote markers and the local manifest; unrelated adb failures still surface
  normally.
- Added focused parser/directory/error-classification host tests and API 36
  device regressions for `FastDeploy2 → Clean → FastDeploy → FastDeploy2` and
  partial staging-directory deletion.


### Earlier review round (commit `ccc45ff6a`)

A second round of review comments was addressed:

- **Leaked `ManualResetEvent` handles.** `RunAdbCommand` created stdout/stderr completion events but never disposed them, leaking OS handles across the many adb invocations in a build. They are now `using var`.
- **Magic-number batching constants are now overridable.** `StaleFileRemovalBatchSize`, `CopyBatchSize`, `MaxShellCommandLength`, and `MaxAdbCommandLength` became `FastDeploy2` task properties (same defaults), wired through `Xamarin.Android.Common.Debugging.targets` as internal `_AndroidFastDeploy*` properties — e.g. `-p:_AndroidFastDeployCopyBatchSize=100` to exercise batching while testing.
- **FastDeploy2 no longer depends on `Mono.AndroidTools`/`Xamarin.AndroidTools`.** All device operations were reimplemented directly on top of `adb` in a new `FastDeploy2.Adb.cs`: `getprop` (properties), `pm uninstall`/`adb install -r -d` (with output-based retry + `ADB####` error classification), `pidof`, and `am force-stop`. `ProcessArgumentBuilder` was replaced by a local quoting helper. The assemblies themselves are intentionally **not** deleted in this PR (legacy `FastDeploy` and `AndroidHelper.ParseTarget` still use them) — that is tracked as separate future work.
- **Documentation.** Added [`Documentation/guides/FastDeploy2.md`](https://github.com/dotnet/android/blob/ccc45ff6a58f4b49eef2a2439fb94341378816d2/Documentation/guides/FastDeploy2.md), a design doc describing each deployment stage and the exact `adb` commands it runs, linked from release note `11795.md`.

**Device validation of the rewritten install path.** The `Mono.AndroidTools`-free
install/uninstall/`getprop`/`pidof`/`force-stop` code path was validated on a physical
Samsung Galaxy A16 (`SM-A165F`, `R58Y30HZ65V`) over USB with the `samples/HelloWorld`
Debug app (every adb command returned exit code 0, 0 build warnings):

| Scenario | Result | Exercised |
|---|---|---|
| Debug fresh install | app launched, no crash | `getprop`, `run-as pidof`, `adb install -d`, `am force-stop`, manifest-hash probe, `push -z any`, override symlink setup |
| Incremental (one-DLL source change) | only the changed files pushed (rest `SkipCopyFile`), app launched | manifest diff + selective `push -z`, override symlinks |
| ReInstall (`-p:_ReInstall=true`) | app launched | `pm uninstall -k`, then `adb install -r -d` |

Confirmed on-device that the override symlinks point into
`/data/local/tmp/fastdeploy2/<package>/<user>/<abi>/`, the staging and override
`.fastdeploy2-manifest-hash` markers match, and that a modern `adb install` (without `-r`)
replaces an already-installed package without hitting the `INSTALL_FAILED_ALREADY_EXISTS`
retry. The signature-mismatch / `RequiresUninstall` retry branch was not force-triggered on a
single device, but its building blocks (`pm uninstall` + `adb install -r`) are both exercised
by the ReInstall scenario.

A fresh `dotnet new maui` Android sample build binlog is available here:

- https://gist.github.com/simonrozsival/c06cacff00c99ee3504c256205e24492

## CI on-device test fixes

The first full CI run surfaced regressions in existing fast-deployment device tests now that `FastDeploy2` is the default. Three deterministic root causes were found (each reproduced and verified on an API 36 emulator) and fixed:

- **Missing sync log markers.** `IncrementalFastDeployment` and `SkipFastDevAlreadyInstalledFile` assert per-file `NotifySync CopyFile` / `NotifySync SkipCopyFile` markers. FastDeploy2 only logged `Prepared X => Y`, so these tests failed even though deployment succeeded. FastDeploy2 now emits the markers from the manifest's changed-file set, and the abi-skip marker uses the same device-relative identifier for consistency.
- **Symlink mode followed subdirectory symlinks into staging.** The full rewrite used `ln -sf "$s"/* .`, which also symlinked staging subdirectories. Processing a child directory then `cd`'d through that symlink into the shell-owned `/data/local/tmp` staging area, and `rm -rf ./*` failed with "Permission denied" under `run-as` (`XA0133` / `XA0129`). This broke any app with culture/satellite subdirectories (`LocalizedAssemblies_ShouldBeFastDeployed`, `DotNetNewAndroidTest`). The full rewrite now clears and symlinks only the regular files in each directory and leaves subdirectories to their own iterations, so it no longer follows or deletes child directories.
- **Copy-fallback `stat` format split by the device shell.** `find ... -exec stat -c "%n|%s|%Y" {} +` reached the device shell unquoted, so `|` was treated as a pipe (exit 127 → `XA0129`) and the copy fallback also failed. The command is now built as a single string with the format single-quoted (matching the `RunAsShell` quoting pattern).
- **Device-state probe `printf '\n'` arrived as a literal `\n`.** The readiness check built its probe with `printf '\noverride='` / `printf '\n'`; the backslash escape does not survive the adb + device-shell quoting layers, so the device emitted `remote=<hash>\noverride=<hash>\n` on a single line. `ParseDeviceManifestState` (splitting on real newlines) put the whole tail into `RemoteHash` and left `OverrideHash` empty, so `remoteReady` was **always false** and every incremental install reset the staging directory and redeployed **all** files (`changed files: 329` in CI), reporting unchanged assemblies as `CopyFile`. The probe is now built with `echo` + command substitution (real newlines, no backslash escapes); verified on an API 36 emulator that `remoteReady` becomes true on an unchanged reinstall.

## FastDeploy2 approach

FastDeploy2 uses the final direction from the experiment PR:

1. Build a local manifest of the deployment target and FastDev inputs:

   ```text
   device id, package, Android user, ABI
   target path -> local source path, size, mtime
   ```

2. Compare with the last successful deploy manifest. On a warm deployment, one
   live probe validates compatibility properties, `run-as`, package/process
   state, and the device-side staging/override manifest markers, then stops the
   app.

3. Push only changed files to device temp storage, without `adb push --sync`:

   ```bash
   adb push -z any changed-file-1 changed-file-2 /data/local/tmp/fastdeploy2/<package>/<user>/<abi>/
   ```

4. Remove stale staged files for removed inputs.

5. Maintain `files/.__override__` as symlinks to staged files with batched `run-as sh -c` commands. The generated shell scripts use short `d`/`s` variables plus `cd` so long source/target directories are not repeated for every file.

6. After a successful deploy, write the matching device-side manifest hash
   markers in one command, then write the local manifest:

   ```text
   /data/local/tmp/fastdeploy2/<package>/<user>/.fastdeploy2-manifest-hash
   files/.__override__/.fastdeploy2-manifest-hash
   ```

7. If an incremental push reports that an expected staging path is missing or
   has the wrong type, clear the per-package/user staging and override trees and
   retry once as a full deployment.

## Why this shape is viable now

The existing FastDeploy v2.0 design was added when Android 11 broke the older external-storage based fast deployment model. At that time, adb compression and multi-file push/sync behavior were still new: Android SDK Platform-Tools 30.0.0 (April 2020) introduced client-side compression for `adb {push,pull,sync}` when used with Android 11 devices, and Platform-Tools 30.0.5 (November 2020) later fixed `adb push --sync` with multiple inputs and improved pushing many files over high-latency links.

Today it is more reasonable to depend on a modern Android SDK Platform-Tools package for developer inner-loop scenarios. `adb` is supplied by the Android SDK Platform-Tools package, and developers using .NET for Android / MAUI can update it independently of end-user device support. With Platform-Tools 36.0.0, `adb push -z any` was also tested against API 24 and API 29 emulators that did not advertise `sendrecv_v2*` compression features; both accepted the command and transferred the file, apparently negotiating down to uncompressed transfer.

This PR also no longer relies on `adb push --sync`: the local manifest tells us exactly which files changed, so FastDeploy2 pushes only those files and avoids an adb-side scan of the full directory.

## What else we tried but abandoned

From the experiment PR:

- Existing `FastDeploy` is excellent for very small changes but very slow for full payloads.
- `adb push --sync` over the whole directory is much faster for full payloads, but still scans/probes too much for tiny changes.
- One `adb push` per file is too slow because every adb invocation has ~40ms median overhead even when skipped.
- A local manifest gives us the changed set without probing all files through adb.
- Symlinks avoid app-private copy after the initial setup.
- A shell-based symlink update was faster than a native helper (`maui.link`), so this PR does not include a native helper.

## How legacy FastDeploy could be improved instead

If we wanted to keep the native FastDeploy tool model, the most promising improvement would be to make `xamarin.sync` batch-aware. Instead of invoking `run-as ... xamarin.sync` once per changed file, the host could open a single `run-as <package> xamarin.sync-batch` stdin stream and write all changed files one after another using a small binary protocol:

```text
magic/version
file-count
repeat file-count times:
  target-path-length
  target-path
  uncompressed-size
  mtime
  compressed-blocks
removed-file-count
repeat removed-file-count times:
  target-path-length
  target-path
```

The device helper would create directories, write each file to a temporary path, set mtime/permissions, rename into place, and remove stale files. This would keep the existing app-private real-file model and avoid symlink assumptions, while removing most per-file adb/run-as process overhead.

That approach is intentionally not part of this PR because it keeps the custom native tool/protocol complexity. FastDeploy2 tries the simpler path first: use adb's built-in transfer/compression support, keep the changed-file decision local, and leave legacy `FastDeploy` available as a fallback while we validate the symlink approach across more devices.

## Final one-assembly head-to-head benchmark

The final review scenario was measured on the same local API 36 arm64 emulator
with two identical `dotnet new android` apps using separate package, `obj`, and
device state. Each timed sample:

- started with the corresponding app running;
- changed one fixed-width source value that changed only the app assembly; and
- measured the complete `dotnet build -t:Install -c Debug --no-restore` wall
  clock plus target/task durations from a compact binlog.

Each implementation state used 5 warmups plus **30 measured samples per
strategy**, interleaved in alternating `ABBA` / `BAAB` blocks. All 120 measured
samples were accepted with no retries or rejections. Positive paired values mean
FastDeploy2 was slower.

| Paired FastDeploy2 − FastDeploy median | Before final review fixes | After |
|---|---:|---:|
| Deploy task | **+204.2 ms** | **−33.3 ms** |
| `_Upload` target | **+201.6 ms** | **−7.3 ms** |
| Full `dotnet build` wall time | +8.3 ms | +225.1 ms |

The paired deploy-task comparison improved by **237.5 ms at the median** and
reversed from FastDeploy2 being slower to slightly faster than legacy
FastDeploy. `_Upload` similarly reached parity. Whole-build wall time remained
very noisy (the paired interquartile range crossed zero in both matrices), so
these samples do not demonstrate an end-to-end CLI wall-clock improvement
outside the deployment target itself.

The measured one-assembly FastDeploy2 hot path dropped from nine serial `adb`
processes to three: compound probe/stop, batched push, and combined marker
finalization.

## Historical benchmark from the experiment PR

Device: physical Samsung Galaxy A16 (`SM-A165F`, `R58Y30HZ65V`) connected by USB 3 cable  
Project: `samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj`

| Strategy | Case | Deploy task | Wall | Upload/transfer | Changed |
|---|---:|---:|---:|---:|---:|
| FastDeploy (existing) | first | 22.11s | 31.61s | 19.09s | 194 |
| FastDeploy (existing) | one DLL | 0.68s | 10.10s | 0.14s | 2 |
| FastDeploy (existing) | app DLLs | 0.69s | 10.10s | 0.20s | 3 |
| FastDeploy (existing) | all DLLs | 17.72s | 27.27s | 17.16s | 189 |
| FastDeploy2 | first | 5.41s | 14.51s | 3.09s | 195 |
| FastDeploy2 | one DLL | 0.56s | 9.70s | 0.06s | 2 |
| FastDeploy2 | app DLLs | 0.59s | 9.99s | 0.04s | 3 |
| FastDeploy2 | all DLLs | 3.02s | 12.40s | 2.42s | 189 |

## Validation

Focused task builds pass in this branch:

```bash
./dotnet-local.sh build \
  src/Xamarin.Android.Build.Debugging.Tasks/Xamarin.Android.Build.Debugging.Tasks.csproj \
  -c Release --no-restore --nologo -v:minimal -m:1

./dotnet-local.sh test \
  bin/TestRelease/net10.0/Xamarin.Android.Build.Tests.dll \
  --filter 'Name~FastDeploy2'
```

The focused FastDeploy2 host suite passes 8/8 tests. Existing
`SkipFastDevAlreadyInstalledFile` and APK `IncrementalFastDeployment` device
tests also pass with the locally built SDK.

Device/emulator validation passed:

| Device | Scenario | Result |
|---|---|---:|
| Physical Samsung Galaxy A16 (`SM-A165F`, `R58Y30HZ65V`) over USB 3 cable | clean first install with default FastDeploy2 symlink mode | passed; app launched successfully |
| Physical Samsung Galaxy A16 (`SM-A165F`, `R58Y30HZ65V`) over USB 3 cable | one-DLL incremental install with default FastDeploy2 symlink mode | passed; app launched successfully |
| Physical Samsung Galaxy A16 (`SM-A165F`, `R58Y30HZ65V`) over USB 3 cable | one-DLL incremental install with copy fallback mode | passed; app launched successfully |
| API 36 emulator | clean first install with shortened `d`/`s` shell-variable symlink scripts | passed; symlink tree created successfully |
| API 36 emulator | Symlink-mode install followed by Copy-mode install | passed; symlinks were replaced by regular files and the symlink marker was removed |
| API 36 emulator | FastDeploy2 → Clean → FastDeploy → FastDeploy2 | passed; symlinks/regular files transitioned correctly and unrelated app data was preserved |
| API 36 emulator | delete one staging ABI directory while retaining the root marker | passed; incremental push detected the invalid state, performed one fresh deployment, and restored symlinks/files/markers |

Observed validation details:

```text
first default: deploy=15.84s, mode=Symlink, changed=195, pushed=195, symlink update=161ms
one DLL default: deploy=0.89s, mode=Symlink, changed=2, pushed=2, symlink update=4ms
one DLL Copy fallback: deploy=2.80s, mode=Copy, changed=2, pushed=2, copy=977ms
```

Additional adb compatibility probes with Platform-Tools 36.0.0:

| Target | Device adb features | `adb push -z any` |
|---|---|---:|
| API 24 emulator | `cmd` only, no `sendrecv_v2*` | passed |
| API 29 emulator | `cmd` only, no `sendrecv_v2*` | passed |
| API 36 emulator | `sendrecv_v2`, `brotli`, `lz4`, `zstd` | passed |

## Risks

- Legacy `FastDeploy` is still present and selectable.
- Symlinks from app-private `files/.__override__` to `/data/local/tmp/fastdeploy2/...` worked on the tested physical Samsung device over USB.
- Physical-device symlink coverage is currently limited to one Samsung model;
  broader Android/OEM/API/device testing is still needed.
- If symlink creation fails, this code can fallback to copy mode and clears symlink-managed override state before copying.
- If the local manifest is missing, does not match the current device/package/user/ABI, or does not match the device-side manifest hash marker, FastDeploy2 does a full push.
- The fast path intentionally trusts matching markers instead of probing every
  expected directory. A push that proves the staging layout is incomplete
  triggers one bounded fresh deployment.
- Staging is isolated by package name and Android user:
  `/data/local/tmp/fastdeploy2/<package>/<user>`. Different apps do not conflict;
  two projects with the same `ApplicationId` on the same device/user
  intentionally share one installed app and staging tree.
- Developers with very old Android SDK Platform-Tools may need to update adb for `adb push -z any`; older devices can still fall back to uncompressed transfer when driven by a modern adb.

## Future work

- Consider separate content and topology/generation markers so a content-only
  DLL update can avoid the final marker `adb` process while retaining safe
  partial-deployment recovery.
- If two adb processes are still too expensive, revisit a small batched
  transfer/commit helper or `xamarin.sync` protocol that uploads all changes and
  commits marker state atomically in one session.
- Measure Visual Studio separately and consider `RegisterTaskObject` caching for
  IDE-only state; it does not help separate CLI invocations and must not be
  required for correctness.
- Validate the warm probe, symlink behavior, strategy transitions, and fresh
  recovery across more API levels and OEM devices.
- Delete the old `FastDeploy` implementation once we are confident the new approach is better across a broader device matrix.
- Delete the old native FastDeploy helper tools (`xamarin.sync`, `xamarin.find`,
  `xamarin.stat`, `xamarin.cp` under `tools/fastdev/`) once `FastDeploy` no longer
  needs them. These are the only consumers of the `termux-elf-cleaner` external
  submodule (used to strip unsupported ELF entries so the Android linker does not
  warn when loading these device-side binaries), so removing them also lets us drop
  the `elfcleaner` build step and the `termux-elf-cleaner` submodule entirely.

---
Replaces #11698 (moved off fork branch to origin).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants