Skip to content

[WIP] Experiment with alternate FastDeploy strategies#11692

Closed
simonrozsival wants to merge 12 commits into
dotnet:mainfrom
simonrozsival:android-fastdev-drop-lz4
Closed

[WIP] Experiment with alternate FastDeploy strategies#11692
simonrozsival wants to merge 12 commits into
dotnet:mainfrom
simonrozsival:android-fastdev-drop-lz4

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Summary

Draft/WIP experiment for alternate Fast Deployment implementations.

This PR adds two opt-in strategies next to the existing FastDeploy task:

  • FastDeploy — current implementation using bundled xamarin.sync/xamarin.find/xamarin.cp helper tools.
  • FastDeploy2 — standalone task using adb push -z any --sync to stage files, then run-as cp -p into files/.__override__.
  • FastDeploy3 — standalone task using fake $ANDROID_PRODUCT_OUT + adb sync -z any data, then run-as cp -p into files/.__override__.

The strategy is selected with:

$(_AndroidFastDevStrategy)=FastDeploy|FastDeploy2|FastDeploy3

Default remains FastDeploy.

Why

I wanted to explore whether we can simplify Fast Deployment by dropping the custom native helper tools and relying on built-in adb compression/sync support.

Current FastDeploy is fast for tiny one-file updates, but full deploys are dominated by xamarin.sync copy time. The adb-based approaches are much simpler and significantly faster for full transfers in the preliminary measurements below.

Implementation notes

FastDeploy2: adb push --sync

Flow:

  1. Stage FastDev files under $(IntermediateOutputPath)/fastdeploy2.

  2. Upload to /tmp/fastdev2/<package>/<user> using:

    adb push -z any --sync <staging>/. /tmp/fastdev2/<package>/<user>
  3. Diff remote staging vs app override using find + stat.

  4. Copy changed files only with run-as cp -p into files/.__override__.

  5. Remove stale override files.

FastDeploy3: adb sync data

Flow:

  1. Stage FastDev files under a fake product output tree:

    $ANDROID_PRODUCT_OUT/data/local/tmp/fastdeploy3/<package>/<user>/...
    
  2. Ask adb for changed files with:

    ANDROID_PRODUCT_OUT=<fake-product-out> adb sync -l data
  3. Upload via:

    ANDROID_PRODUCT_OUT=<fake-product-out> adb sync -z any data
  4. Copy only files listed by adb sync -l plus missing override files into files/.__override__.

This is why FastDeploy3 stages under /data/local/tmp/... instead of /tmp/...: adb sync maps fixed product-output roots like $ANDROID_PRODUCT_OUT/data/... to device /data/....

Preliminary benchmark data

Device: Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V)

Project: samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj

Payload: ~189-195 FastDev files depending on environment/config files included.

Deploy task totals

Case FastDeploy old FastDeploy2 (push --sync) FastDeploy3 (sync data)
First deploy 21.74s 5.85s 5.51s
Touch 1 DLL 2.29s 2.76s 2.65s
Touch all DLLs 18.83s 5.02s 5.20s

Takeaway: adb-based strategies are ~3.6-4x faster for full transfers. The current helper-tool path still wins tiny one-file updates in the initial measurements.

Legacy FastDeploy phase timings

Case Total deploy Tool setup mkdir find compare xamarin.sync copy env copy Changed / skipped
First deploy 21.74s 1.07s 0.07s 0.07s 0.04s 18.72s 0.08s 194 / 0
Touch 1 DLL 2.29s 0.12s 0.07s 0.05s 0.02s 0.17s ~0s 2 / 192
Touch all DLLs 18.83s 0.07s 0.07s 0.08s 0.04s 16.59s ~0s 189 / 5

FastDeploy2/FastDeploy3 phase timings before the latest FastDeploy3 sync -l optimization

Strategy Case Total deploy Upload Stat+diff Copy to app storage Local stage Cleanup Other
FastDeploy2 first deploy 5.85s 2.64s 0.17s 0.96s 0.28s 0.03s 1.73s
FastDeploy2 touch 1 DLL 2.76s 0.04s 0.17s 0.14s 0.44s 0.04s 1.88s
FastDeploy2 touch all DLLs 5.02s 1.84s 0.18s 0.91s 0.24s 0.07s 1.74s
FastDeploy3 first deploy 5.51s 2.61s 0.17s 0.88s 0.24s 0.04s 1.53s
FastDeploy3 touch 1 DLL 2.65s 0.06s 0.16s 0.13s 0.40s 0.04s 1.84s
FastDeploy3 touch all DLLs 5.20s 1.77s 0.17s 0.96s 0.29s 0.05s 1.91s

Raw adb upload comparison

Case adb push --sync to /tmp adb sync data to /data/local/tmp
First upload 10.24s 8.09s
Touch 1 DLL 0.05s 0.06s
Touch all DLLs 8.36s 9.26s
List changed files only n/a ~0.05s

adb push --sync reports only counts (N pushed, M skipped). adb sync -l data can list exact changed file names, which is the reason FastDeploy3 is interesting.

Hardlink experiment

I also tried to avoid the app-private copy using hardlinks:

  • cp -Rl /tmp/fastdev2/... files/.__override__ failed with Cross-device link.
  • /data/local/tmp/... -> files/.__override__ also failed with Cross-device link.
  • even app-private -> app-private hardlink under run-as failed with Permission denied.

So hardlinks do not appear viable for this optimization path on the tested device.

Caveats / TODO

  • This is intentionally draft/experimental.
  • The latest FastDeploy3 path uses adb sync -l data as the changed-file source, but I still need stable follow-up benchmark data after a device disconnect interrupted the clean run.
  • Need broader device/API coverage.
  • Need review of whether $ANDROID_PRODUCT_OUT/adb sync data is acceptable for product usage or should remain experimental.
  • Need cleanup of the diagnostic counters and naming if one approach is selected.

Validation so far

Focused task builds passed in Debug and Release after the latest parity changes:

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

Earlier in this branch, the Release SDK was also rebuilt successfully with:

make prepare CONFIGURATION=Release && make all CONFIGURATION=Release

simonrozsival and others added 2 commits June 17, 2026 23:50
Introduce FastDeploy2 and FastDeploy3 as opt-in fast deployment strategies for comparing adb push --sync and adb sync based file transfer paths. Add diagnostic timing counters to the legacy FastDeploy path for preliminary benchmarking.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Capture non-transfer phase timings for FastDeploy2 and FastDeploy3 so the constant overhead can be broken down separately from staging, upload, stat, and copy work.

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

Copy link
Copy Markdown
Member Author

Additional overhead instrumentation results:

Device: Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V)
Project: samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj

Strategy Case Deploy Orchestration File phases Residual
FastDeploy2 first deploy 6.11s 1.88s 4.21s 0.02s
FastDeploy2 touch 1 DLL 1.10s 0.17s 0.92s 0.01s
FastDeploy2 touch all DLLs 3.63s 0.22s 3.40s 0.01s
FastDeploy3 first deploy 5.68s 1.73s 4.02s ~0s
FastDeploy3 touch 1 DLL 1.07s 0.24s 0.90s ~0s
FastDeploy3 touch all DLLs 3.73s 0.22s 3.57s ~0s

The previous ~1.7s “Other” bucket is mostly explained by first-install orchestration:

  • first deploy: package install is ~1.4-1.5s; package/debuggable check ~0.1-0.2s; process termination ~0.1-0.25s
  • incremental deploys: orchestration drops to ~0.17-0.24s, mostly package check + app termination

So there is no longer a large unexplained constant inside the task after instrumentation; the large apparent “other” in prior tables was because package install/termination/checks were not separately timed.

Break orchestration timing down into package checks, package install, process lookup/kill, and execute/setup substeps so the fixed costs can be measured independently from file transfer phases.

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

Copy link
Copy Markdown
Member Author

Fine-grained orchestration breakdown after adding more timers:

Device: Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V)
Project: samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj

FastDeploy2

Case Deploy package check run-as pwd install terminate get pid local stage upload stat/list app cp changed
first 6.03s 144ms 71ms 1.27s 163ms 162ms 385ms 2.83s 177ms 873ms 195
touch 1 DLL avg (3 runs) 1.18s 65ms 64ms 0ms 131ms 130ms 433ms 88ms 191ms 71ms 2
touch all DLLs 7.55s 91ms 89ms 0ms 105ms 105ms 4.18s 1.89s 195ms 882ms 189

FastDeploy3

Case Deploy package check run-as pwd install terminate get pid local stage sync list upload override list app cp changed
first 10.32s 114ms 61ms 1.35s 190ms 190ms 2.72s 246ms 4.45s 78ms 905ms 195
touch 1 DLL avg (3 runs) 1.11s 77ms 75ms 0ms 125ms 125ms 399ms 56ms 72ms 77ms 69ms 2
touch all DLLs 4.46s 90ms 89ms 0ms 141ms 141ms 1.13s 51ms 1.84s 78ms 905ms 189

Observations:

  • Incremental orchestration is small and stable: package check is mostly one run-as <pkg> pwd (~60-90ms), and terminate is mostly GetProcessId (~100-190ms). No process was running in these runs, so kill time was 0ms.
  • First deploy orchestration is mostly package install (~1.3s).
  • The large variable component for one-file deploys is local staging (copying the staged tree on the host), not run-as/package orchestration.
  • Full deploy cost scales mostly with upload and app-private cp; diff/list work remains small.

@simonrozsival

Copy link
Copy Markdown
Member Author

Interleaved benchmark across all three strategies, with first-deploy runs cleaning app data and temp staging dirs before each strategy.

Cleanup before each first-deploy sequence:

adb uninstall com.xamarin.android.helloworld
adb shell rm -rf /tmp/fastdev2/com.xamarin.android.helloworld/0 \
                 /data/local/tmp/fastdeploy3/com.xamarin.android.helloworld/0 \
                 /data/local/tmp/.xatools
rm obj/Debug/net11.0-android/uploadflags.txt

Device: Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V)
Project: samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj
Two interleaved passes with rotated order:

  1. FastDeploy -> FastDeploy2 -> FastDeploy3
  2. FastDeploy2 -> FastDeploy3 -> FastDeploy

Aggregate timings

Strategy Case Deploy task Main transfer Changed bytes Estimated B/s File phases
FastDeploy first 24.79s 21.56s 147.7 MB 6.85 MB/s 21.87s
FastDeploy touch 1 file 0.75s 0.14s 5 KB latency-bound 0.44s
FastDeploy touch all DLLs 19.26s 18.60s 147.7 MB 7.95 MB/s 18.87s
FastDeploy2 first 6.04s 3.73s 144.5 MB 38.8 MB/s 4.44s
FastDeploy2 touch 1 file 1.16s 0.15s 5 KB latency-bound 0.92s
FastDeploy2 touch all DLLs 4.17s 3.03s 144.5 MB 48.0 MB/s 3.93s
FastDeploy3 first 6.13s 3.54s 144.5 MB 40.9 MB/s 4.54s
FastDeploy3 touch 1 file 1.94s 0.22s 5 KB latency-bound 1.77s
FastDeploy3 touch all DLLs 3.80s 2.89s 144.5 MB 50.0 MB/s 3.63s

Simple warm-case fit using touch-one + touch-all main-transfer times:

Strategy Constant latency Throughput
FastDeploy ~144ms ~8.0 MB/s
FastDeploy2 ~147ms ~50.1 MB/s
FastDeploy3 ~218ms ~54.0 MB/s

Main transfer means:

  • FastDeploy: xamarin.sync file transfer/write phase
  • FastDeploy2/3: adb upload + app-private cp -p

Risk notes for switching away from legacy FastDeploy

FastDeploy2/3 look much better for full transfers, but key risks remain:

  • adb push --sync / adb sync timestamp behavior must be reliable across device filesystems and adb versions.
  • FastDeploy3 depends on fake $ANDROID_PRODUCT_OUT and adb sync data, which is not the intended generic upload UX.
  • FastDeploy3 stages under /data/local/tmp, not /tmp; this may behave differently on devices with constrained /data or different SELinux/storage behavior.
  • We need broader device/API coverage, especially secondary users, system apps/rooted flows, and older adb/platform-tools versions.
  • Tiny one-file updates are latency-bound and legacy FastDeploy can still be competitive or faster.
  • FastDeploy2/3 currently duplicate a lot of FastDeploy behavior for experimentation; if chosen, this needs cleanup/consolidation and tests.

Introduce FastDeploy4 with direct original-file adb push support and selectable SingleFile/Bulk push modes so local staging costs can be evaluated independently.

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

Copy link
Copy Markdown
Member Author

FastDeploy4 experiment: direct original-file adb push without local staging.

Strategy shape:

  • FastDeploy4 avoids copying all FastDev files into a local staging directory.
  • SingleFile mode runs one adb push -z any --sync local-file remote-file per FastDev file. This gives an exact changed-file list from adb output.
  • Bulk mode groups source files by target directory and pushes them directly in batches. This avoids local staging too, but does not identify exact changed files, so it still uses remote staging vs override stat/diff before app-private copy.

Usage:

-p:_AndroidFastDevStrategy=FastDeploy4
-p:_AndroidFastDeploy4PushMode=SingleFile|Bulk

Benchmark on Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V), HelloWorld sample:

Strategy Case Deploy task Local stage Upload Stat/list App cp Changed / pushed / skipped
FastDeploy2 first 6.07s 494ms 2.69s 150ms 902ms 195 / 195 / 0
FastDeploy2 touch 1 DLL 1.31s 637ms 60ms 193ms 81ms 2 / 2 / 193
FastDeploy2 touch all DLLs 9.60s 5.40s 2.73s 173ms 847ms 189 / 189 / 6
FastDeploy4 SingleFile first 13.80s 24ms 10.76s 80ms 957ms 195 / 195 / 0
FastDeploy4 SingleFile touch 1 DLL 7.98s 24ms 7.35s 84ms 90ms 2 / 2 / 193
FastDeploy4 SingleFile touch all DLLs 12.34s 35ms 10.97s 63ms 843ms 189 / 189 / 6
FastDeploy4 Bulk first 7.25s 22ms 4.40s 163ms 846ms 195 / 195 / 0
FastDeploy4 Bulk touch 1 DLL 1.52s 23ms 692ms 185ms 66ms 2 / 2 / 193
FastDeploy4 Bulk touch all DLLs 5.82s 25ms 4.21s 175ms 925ms 189 / 189 / 6

Takeaways:

  • Direct push removes local staging almost entirely (~20-50ms).
  • SingleFile mode is too slow if it probes every file: one adb process per file dominates, even though it gives exact changed-file names.
  • Bulk mode is much better than SingleFile and can beat FastDeploy2 when local staging spikes, but its upload phase is slower than FastDeploy2's staged directory push in this run.
  • Bulk mode still needs diffing because adb only returns pushed/skipped counts for a multi-file push, not exact names.
  • This suggests a possible future hybrid: avoid local staging, but only use single-file pushes when we already know a small changed set from MSBuild/incremental state. Probing every file one-by-one is not viable.

Have experimental FastDeploy strategies retrieve the app private path and process id in a single run-as shell invocation so process termination can avoid a separate adb ps call when possible.

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

Copy link
Copy Markdown
Member Author

Tried combining the package/private-directory probe and process lookup into a single run-as shell invocation.

The command shape is effectively:

run-as <pkg> sh -c 'pwd; pidof <pkg> 2>/dev/null || true'

This returns the app data dir on line 1 and the process id on line 2 when the app is running. The experimental FastDeploy tasks now cache that pid and avoid the separate adb shell ps -A | grep ... path in TerminateApp() for normal apps.

Validation on the Samsung device:

  • app running before deploy: pidof returned 12544
  • deploy used combined probe: run-as pwd+pidof ~103ms
  • terminate no longer spent time in GetProcessId: terminate.get-pid.ms = 0
  • kill path used cached pid and stopped the app: terminate.kill.ms = 153ms
  • pidof after deploy was empty

So this removes one adb process-list call from the incremental path while preserving the actual kill behavior when the app is running.

Allow FastDeploy2-derived strategies to replace app-private copies with symlinks to staged files so the app storage transfer cost can be measured separately from upload and staging.

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

Copy link
Copy Markdown
Member Author

Additional direct-push and symlink experiments.

FastDeploy4: direct original-file push

FastDeploy4 avoids local staging and pushes original source files directly to remote staging.

Mode Case Deploy Local stage Upload Stat/list App copy Notes
FastDeploy2 baseline first 6.07s 494ms 2.69s 150ms 902ms staged directory push
FastDeploy2 baseline touch 1 DLL 1.31s 637ms 60ms 193ms 81ms staged directory push
FastDeploy2 baseline touch all DLLs 9.60s 5.40s 2.73s 173ms 847ms staged directory push
FastDeploy4 SingleFile first 13.80s 24ms 10.76s 80ms 957ms one adb push per file
FastDeploy4 SingleFile touch 1 DLL 7.98s 24ms 7.35s 84ms 90ms one adb push per file
FastDeploy4 SingleFile touch all DLLs 12.34s 35ms 10.97s 63ms 843ms one adb push per file
FastDeploy4 Bulk first 7.25s 22ms 4.40s 163ms 846ms grouped adb push
FastDeploy4 Bulk touch 1 DLL 1.52s 23ms 692ms 185ms 66ms grouped adb push
FastDeploy4 Bulk touch all DLLs 5.82s 25ms 4.21s 175ms 925ms grouped adb push

Takeaway: avoiding local staging works, but one adb invocation per file is too expensive. Bulk direct push is viable, but still slower than staged directory push in some cases.

Symlink app transfer mode

I also tested replacing app-private cp -p with symlinks from files/.__override__ to the remote staging directory.

Manual probe:

  • Full cp -p of staged payload into app storage via remote glob: ~483ms
  • Full ln -s of staged payload into app storage via remote glob: ~188ms
  • App launched successfully with a fully symlinked files/.__override__ tree.

Integrated FastDeploy2 experiment with Copy vs Symlink transfer mode:

Mode Case Deploy Local stage Upload Override stat rm mkdir copy/link Changed / pushed
Copy first 5.66s 240ms 2.67s 78ms 0ms 71ms 842ms 195 / 195
Copy touch 1 DLL 0.95s 286ms 116ms 109ms 0ms 63ms 69ms 2 / 2
Copy touch all DLLs 3.46s 255ms 1.86s 109ms 0ms 65ms 853ms 189 / 189
Symlink first 6.21s 345ms 2.87s 154ms 1080ms 63ms 112ms 195 / 195
Symlink touch 1 DLL 1.02s 516ms 83ms 168ms 0ms 0ms 0ms 0 links / 2 pushed
Symlink touch all DLLs 2.71s 757ms 1.92s 183ms 1ms 0ms 0ms 0 links / 189 pushed

Notes:

  • Symlinks are viable on the tested device; the app starts and runs from symlinked assemblies.
  • Hardlinks are not viable (Cross-device link / Permission denied).
  • Symlink mode avoids app-private copy on incremental updates because the symlink points at the staged file and the staged file is overwritten by adb.
  • First symlink deploy is currently slower because converting from regular copied files to symlinks removes many files before creating links. A clean symlink-first flow could likely be better than this mixed-mode measurement.
  • This approach has risk: it makes runtime assembly loading depend on /tmp (or whichever staging directory) remaining readable and stable for the app process.

@simonrozsival

Copy link
Copy Markdown
Member Author

Per-file adb invocation timing probe for the FastDeploy payload.

Payload: 195 files, 144.5 MB total, pushed one file at a time with:

adb push -z any --sync local-file remote-file

First upload, one adb invocation per file

  • total: 13.22s
  • mean: 67.8ms/file
  • median: 51.9ms/file
  • p90: 90.1ms/file
  • min: 33.6ms
  • max: 919ms
  • pushed/skipped: 195/0

Slowest files were the genuinely large files (Mono.Android.pdb, Mono.Android.dll, System.Private.CoreLib.dll), but there are also small-file outliers around 150ms.

Unchanged upload, one adb invocation per file

  • total: 8.54s
  • mean: 43.8ms/file
  • median: 39.8ms/file
  • p90: 59.4ms/file
  • min: 26.7ms
  • max: 211ms
  • pushed/skipped: 0/195

This confirms the hypothesis: even a skipped per-file adb push --sync has ~40ms median process/protocol overhead. With ~195 files, that alone is ~8s, which explains why FastDeploy4 SingleFile is slow.

Implication: one-by-one push is only attractive if we already know a very small changed set. It is not viable as a way to discover changed files by probing all FastDev files.

Convert the experimental FastDeploy2 strategy to bulk-push original FastDev inputs directly to /tmp/fastdev2 without host-side staging, and use symlinks in files/.__override__ by default.

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

Copy link
Copy Markdown
Member Author

Updated FastDeploy2 to the current preferred experiment shape:

original FastDev item files
  -> adb push --sync -z any directly to /tmp/fastdev2/<pkg>/<user>/...
  -> files/.__override__ symlinks to /tmp/fastdev2/<pkg>/<user>/...

This removes host-side staging for normal files and avoids app-private cp after symlinks are established. The only remaining local file generation is the synthetic environment blob when environment files are present.

Usage:

-p:_AndroidFastDevStrategy=FastDeploy2

FastDeploy2 now defaults to symlink app transfer; Copy remains available with:

-p:_AndroidFastDeployAppFileTransferMode=Copy

Latest Samsung A16 numbers:

Case Deploy Local stage Upload Override stat rm mkdir link Changed Pushed/skipped
first 8.06s 34ms 4.95s 148ms 1.08s 80ms 126ms 195 195 / 0
touch 1 DLL 1.19s 25ms 782ms 153ms 0ms 0ms 0ms 0 links 2 / 193
touch all DLLs 4.76s 44ms 4.28s 188ms 0ms 0ms 0ms 0 links 189 / 6

The app launched successfully from the symlinked override tree after the run (fatal_count=0).

Notes:

  • This confirms host-side staging is essentially gone.
  • Incremental updates no longer need app-private copy/link when the symlink set already exists; adb overwrites the staged target files.
  • First deploy is slower than expected because direct bulk upload from many original paths is slower than staged directory upload, and first symlink setup currently removes/recreates all links.
  • The shape still looks promising for incremental deploys, but we should keep the risk notes around symlinked runtime assets.

@simonrozsival

Copy link
Copy Markdown
Member Author

Compression algorithm experiment for the current FastDeploy2 shape (direct bulk push to /tmp/fastdev2 + symlinked .__override__).

Device: Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V)
Payload: ~144.5 MB full FastDev payload

Algorithm First deploy upload First throughput Touch 1 file upload Touch all DLLs upload Touch all throughput
any 4.75s 30.4 MB/s 946ms 4.47s 32.3 MB/s
none 8.64s 16.7 MB/s 952ms 7.72s 18.7 MB/s
lz4 5.30s 27.2 MB/s 857ms 5.16s 28.0 MB/s
zstd 4.52s 31.9 MB/s 975ms 4.29s 33.7 MB/s
brotli 4.93s 29.3 MB/s 864ms 4.47s 32.3 MB/s

Observations:

  • none is clearly worse for the full payload.
  • zstd was best for full-payload throughput in this run.
  • any and brotli were close behind.
  • lz4 did not win here, despite low CPU overhead, likely because transfer size dominates.
  • One-file updates are latency-bound; algorithm choice has little practical impact (~850-975ms upload phase in this integrated path).

For now, any remains a reasonable default because it lets adb choose, but zstd is worth further repeated testing if we want to optimize for the full-payload case on modern devices.

@simonrozsival

Copy link
Copy Markdown
Member Author

More realistic app-development scenario: touch only app-specific DLLs, leave BCL/SDK/framework DLLs unchanged.

Touched files:

  • HelloWorld.DotNet.dll
  • HelloLibrary.DotNet.dll
  • _Microsoft.Android.Resource.Designer.dll

Total touched DLL bytes: ~15 KB

Each strategy was first given a clean baseline deploy, then only those app DLLs were touched before the measured deploy.

Strategy/config Deploy task Wall time Changed Skipped Upload / sync App copy/link Local prep
FastDeploy old 717ms 9.45s 3 191 206ms included n/a
FastDeploy2 Copy 1.34s 9.80s 3 192 768ms 80ms 40ms
FastDeploy2 Symlink 1.37s 10.29s 0 links 192 897ms 0ms 44ms
FastDeploy3 1.05s 9.91s 3 192 66ms 79ms 421ms
FastDeploy4 SingleFile 7.63s 16.79s 3 192 7.15s 63ms 36ms
FastDeploy4 Bulk 1.38s 9.92s 3 192 657ms 86ms 64ms

Observations:

  • Old FastDeploy still wins app-code-only incremental deploys.
  • FastDeploy3 is the closest adb-based strategy for this scenario because adb sync -l data provides the changed-file set and upload is tiny.
  • FastDeploy2 Symlink does not need app-private link/copy work once symlinks exist, but it still pays bulk adb push overhead over the whole directory.
  • FastDeploy4 SingleFile remains non-viable when it probes every file one by one.
  • Wall time is dominated by MSBuild project work for this small-change scenario; deploy-task differences are sub-second except SingleFile.

Avoid probing normal FastDev source files before adb push and keep the generated environment blob timestamp-stable by only rewriting it when content changes.

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

Copy link
Copy Markdown
Member Author

Small cleanup to the direct-push preparation path:

  • removed File.Exists() probing for normal FastDev inputs; adb push now owns missing-source failure reporting
  • kept environment-file IO because that file is synthetic
  • changed environment generation to only rewrite the file when content changes, preserving mtime when unchanged so adb does not resync it unnecessarily

Quick verification:

baseline: env mtime unchanged, pushed 1, skipped 194
touch-one: env mtime unchanged, pushed 2, skipped 193

So unchanged environment content no longer dirties the environment blob timestamp.

Add a native maui.link helper for the experimental symlink transfer path and wire FastDeploy2 to install and invoke it for app override symlink mirroring.

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

Copy link
Copy Markdown
Member Author

Renamed the symlink helper experiment to maui.link instead of xamarin.link and wired FastDeploy2 symlink mode to install/run it.

Validation on Samsung A16:

deploy.symlink.tool.install.ms = 528
deploy.symlink.tool.ms = 82
deploy.symlink.tool.result = linked [195] unchanged [0] removed [0] errors [0]

The tool was installed under app-private tools:

files/.__tools__/maui.link
files/.__tools__/maui.link.version

It mirrors the staged tree into files/.__override__ as symlinks and removes stale entries in one run-as invocation, with the managed symlink path still available as fallback.

@simonrozsival

Copy link
Copy Markdown
Member Author

Reran current FastDeploy2 default after maui.link integration.

Current shape:

original files -> adb push --sync -z any -> /tmp/fastdev2/<pkg>/<user>/...
files/.__override__ -> symlinks maintained by files/.__tools__/maui.link

Device: Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V)
Project: HelloWorld sample

Case Deploy Local prep Upload maui.link install/check maui.link run adb pushed/skipped maui.link result
first 7.00s 20ms 4.75s 515ms 84ms 195 / 0 linked 195, unchanged 0, removed 0
touch 1 DLL 1.34s 30ms 919ms 67ms 81ms 2 / 193 linked 0, unchanged 195, removed 0
touch all DLLs 5.54s 34ms 3.58s 94ms 74ms 189 / 6 linked 0, unchanged 195, removed 0
touch app DLLs only 1.36s 41ms 882ms 93ms 81ms 3 / 192 linked 0, unchanged 195, removed 0

The app launched successfully from the symlinked override tree after the run (fatal_count=0).

Interpretation:

  • maui.link removes app-private copy cost, and steady-state link maintenance is ~75-85ms.
  • First deploy still pays helper install (~515ms) and full adb upload.
  • Incremental deploys are still dominated by adb bulk-push latency/scan over the full source set, not symlink maintenance.

@simonrozsival

Copy link
Copy Markdown
Member Author

Fresh current-strategy comparison table after the latest FastDeploy2 direct-push + maui.link changes.

Device: Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V)
Project: HelloWorld sample

Strategy/config Case Deploy task Wall Upload/sync App copy/link Changed Pushed/skipped
FastDeploy old first 21.89s 30.38s 18.73s included 194 n/a / 0
FastDeploy old one DLL 0.66s 9.33s 0.15s included 2 n/a / 192
FastDeploy old app DLLs 0.63s 8.94s 0.21s included 3 n/a / 191
FastDeploy old all DLLs 18.29s 28.04s 17.68s included 189 n/a / 5
FastDeploy2 Symlink first 6.88s 15.50s 4.54s maui.link 195 links 195 / 0
FastDeploy2 Symlink one DLL 1.28s 9.80s 0.89s 0 links 0 links 2 / 193
FastDeploy2 Symlink app DLLs 1.26s 9.81s 0.84s 0 links 0 links 3 / 192
FastDeploy2 Symlink all DLLs 4.32s 12.96s 3.93s 0 links 0 links 189 / 6
FastDeploy2 Copy first 7.20s 17.23s 4.50s 0.82s 195 195 / 0
FastDeploy2 Copy one DLL 1.38s 9.74s 0.83s 0.06s 2 2 / 193
FastDeploy2 Copy app DLLs 1.47s 10.02s 0.85s 0.09s 3 3 / 192
FastDeploy2 Copy all DLLs 5.56s 14.41s 4.17s 0.88s 189 189 / 6
FastDeploy3 first 5.84s 14.60s 2.67s 0.78s 195 195 / 0
FastDeploy3 one DLL 0.91s 11.33s 0.07s 0.08s 2 2 / 193
FastDeploy3 app DLLs 1.02s 11.95s 0.09s 0.07s 3 3 / 192
FastDeploy3 all DLLs 4.01s 15.78s 2.14s 0.90s 189 189 / 6
FastDeploy4 SingleFile first 16.70s 29.17s 13.87s 0.95s 195 195 / 0
FastDeploy4 SingleFile one DLL 11.57s 22.00s 11.00s 0.08s 2 2 / 193
FastDeploy4 SingleFile app DLLs 23.87s 36.98s 23.38s 0.07s 3 3 / 192
FastDeploy4 SingleFile all DLLs 15.30s 59.68s 14.05s 0.83s 189 189 / 6
FastDeploy4 Bulk first 7.56s 17.81s 4.66s 0.91s 195 195 / 0
FastDeploy4 Bulk one DLL 1.54s 12.68s 0.79s 0.09s 2 2 / 193
FastDeploy4 Bulk app DLLs 1.51s 11.21s 0.81s 0.08s 3 3 / 192
FastDeploy4 Bulk all DLLs 6.43s 16.43s 4.91s 0.89s 189 189 / 6

Notes:

  • Old FastDeploy still wins small app-code changes in pure deploy-task time.
  • FastDeploy3 is the best adb-based strategy for small changes in this run, because adb sync -l data gives it a tiny upload set.
  • Current FastDeploy2 Symlink removes host staging and app-private copy, but bulk adb push --sync over all source files still costs ~0.8-0.9s for tiny changes.
  • FastDeploy4 SingleFile is conclusively bad as a probe-all-files strategy because one adb invocation per file dominates.
  • FastDeploy4 Bulk avoids host staging but does not beat the staged-directory/bulk approaches consistently.

Remove FastDeploy3 and FastDeploy4 after narrowing the experiment to legacy FastDeploy plus the direct-push symlink FastDeploy2 path.

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

Copy link
Copy Markdown
Member Author

Cleaned up the experiment set:

  • Removed FastDeploy3 (adb sync data) because the fake $ANDROID_PRODUCT_OUT/adb sync approach feels too risky/complex.
  • Removed FastDeploy4 because the direct-push variants were useful for data collection but not the path we want to keep.
  • Kept the current focused FastDeploy2 experiment:
original FastDev inputs
  -> bulk adb push --sync -z any directly to /tmp/fastdev2/<pkg>/<user>/...
  -> app .__override__ symlink mirror maintained by files/.__tools__/maui.link

The target strategy set is back to:

$(_AndroidFastDevStrategy)=FastDeploy|FastDeploy2

FastDeploy2 smoke test after cleanup:

deploy.app.file.transfer.mode = Symlink
deploy.symlink.tool.result = linked [195] unchanged [0] removed [0] errors [0]
deploy.fastdeploy2.adb.pushed.files = 2
deploy.fastdeploy2.adb.skipped.files = 193
deploy.fastdeploy2.bulk.batches = 9

Add an experimental FastDeploy5 strategy which compares a local manifest, pushes only changed files without adb --sync, removes stale staged files, and mirrors the remote staging tree through maui.link.

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

Copy link
Copy Markdown
Member Author

Added FastDeploy5: manifest-driven changed-file transfer.

Shape:

local manifest: target path -> source path + size + mtime
changed set -> adb push -z any only those files, no --sync
remote stale set -> rm from /tmp/fastdeploy5
files/.__override__ -> maui.link symlink mirror

Usage:

-p:_AndroidFastDevStrategy=FastDeploy5

Samsung A16 / HelloWorld comparison:

Strategy Case Deploy task Wall Upload/transfer Changed Pushed/skipped
FastDeploy old first 22.11s 31.61s 19.09s 194 n/a / 0
FastDeploy old one DLL 0.68s 10.10s 0.14s 2 n/a / 192
FastDeploy old app DLLs 0.69s 10.10s 0.20s 3 n/a / 191
FastDeploy old all DLLs 17.72s 27.27s 17.16s 189 n/a / 5
FastDeploy2 first 7.12s 16.70s 4.89s 195 195 / 0
FastDeploy2 one DLL 1.19s 10.40s 0.79s symlink unchanged 2 / 193
FastDeploy2 app DLLs 1.20s 10.79s 0.81s symlink unchanged 3 / 192
FastDeploy2 all DLLs 4.77s 15.27s 4.36s symlink unchanged 189 / 6
FastDeploy5 first 5.41s 14.51s 3.09s 195 195 / 0
FastDeploy5 one DLL 0.56s 9.70s 0.06s 2 manifest changes 2 / 0
FastDeploy5 app DLLs 0.59s 9.99s 0.04s 3 manifest changes 3 / 0
FastDeploy5 all DLLs 3.02s 12.40s 2.42s 189 manifest changes 189 / 0

FastDeploy5 is the first adb-based strategy that beats old FastDeploy on the small-change deploy-task cases in this run. It does that by avoiding both bad forms of change discovery:

  • no full-directory adb push --sync scan over all files
  • no one-adb-call-per-file probing

It only transfers the manifest-detected changed set.

Add a FastDeploy5 mode which uses manifest data to run rm and ln -sf together in batched run-as shell commands, avoiding the maui.link helper for symlink maintenance.

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

Copy link
Copy Markdown
Member Author

FastDeploy5 no-helper experiment: added ShellSymlink mode, which uses the manifest's new/removed set and batched run-as sh -c commands (rm, mkdir, ln -sf) instead of installing/running maui.link.

Mode Case Deploy Upload Symlink/tool/copy
Symlink (maui.link) first 5.46s 3.12s install 548ms + tool 68ms
Symlink (maui.link) one DLL 573ms 46ms tool 80ms
Symlink (maui.link) app DLLs 665ms 72ms tool 84ms
Symlink (maui.link) all DLLs 2.95s 2.39s tool 79ms
ShellSymlink first 5.01s 3.10s shell update 171ms
ShellSymlink one DLL 434ms 62ms shell update 2ms
ShellSymlink app DLLs 463ms 58ms shell update 2ms
ShellSymlink all DLLs 2.87s 2.52s shell update 2ms
Copy one DLL 923ms 63ms copy 165ms

Conclusion: maui.link does not currently justify the extra native helper complexity. The manifest-driven shell path is faster in every measured case, especially incremental cases where no new symlinks are needed.

@simonrozsival

Copy link
Copy Markdown
Member Author

Opened the cleaned-up follow-up PR with the manifest-driven FastDeploy2 implementation: #11698

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.

1 participant