Skip to content

Add MAUI iOS Inner Loop measurements for CI#5187

Draft
davidnguyen-tech wants to merge 69 commits intodotnet:mainfrom
davidnguyen-tech:nguyendav/maui-ios-inner-loop
Draft

Add MAUI iOS Inner Loop measurements for CI#5187
davidnguyen-tech wants to merge 69 commits intodotnet:mainfrom
davidnguyen-tech:nguyendav/maui-ios-inner-loop

Conversation

@davidnguyen-tech
Copy link
Copy Markdown
Member

Summary

Adds MAUI iOS Inner Loop performance measurements to CI, supporting both iOS simulators and physical devices.

What's included:

  • iOSInnerLoopParser.cs — Binlog parser extracting iOS build task/target timings
  • Startup.cs/Reporter.cs — Wiring and null-safety fix
  • ioshelper.py — iOS simulator and physical device management
  • runner.py — IOSINNERLOOP execution branch (build → deploy → measure → incremental)
  • Scenario scripts — pre.py, test.py, post.py, setup_helix.py
  • maui_scenarios_ios_innerloop.proj — Helix workitem definition (simulator + physical device)
  • Pipeline YAML — Job entries in sdk-perf-jobs.yml and routing in run_performance_job.py

Measurements:

  • First deploy: full build + install + launch timing via binlog parsing
  • Incremental deploy: source edit → rebuild + reinstall + relaunch timing

Targets:

  • iOS Simulator (iossimulator-arm64) on macOS Helix machines
  • Physical iPhone (ios-arm64) on Mac.iPhone.17.Perf queue

Based on:

  • Existing Android Inner Loop CI scenario (working reference)
  • Local iOS implementation from feature/measure-maui-ios-deploy branch

davidnguyen-tech and others added 30 commits April 3, 2026 15:54
- Create iOSInnerLoopParser.cs: binlog parser for iOS inner loop build
  timings, extracting iOS-specific tasks (AOTCompile, Codesign, MTouch,
  etc.) and targets (_AOTCompile, _CodesignAppBundle, _CreateAppBundle,
  etc.) plus shared tasks (Csc, XamlC, LinkAssembliesNoShrink)

- Wire into Startup.cs: add iOSInnerLoop to MetricType enum and map it
  to iOSInnerLoopParser in the parser switch expression

- Fix Reporter.cs: guard against null/empty PERFLAB_BUILDTIMESTAMP to
  prevent ArgumentNullException on DateTime.Parse(null) when the env
  var is unset (falls back to DateTime.Now)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- const.py: Add IOSINNERLOOP constant and SCENARIO_NAMES mapping
- ioshelper.py: New module with iOSHelper class for simulator and physical
  device management (boot, install, launch, terminate, uninstall, find bundle)
- runner.py: Add iosinnerloop subparser, attribute assignment, and full
  execution branch (first build+deploy+launch, incremental loop with source
  toggling, binlog parsing, report aggregation, and Helix upload)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pre.py: Install maui-ios workload, create MAUI template (no-restore for
Helix), strip non-iOS TFMs with flexible regex, inject MSBuild properties
(AllowMissingPrunePackageData, UseSharedCompilation), copy merged
NuGet.config for Helix-side restore, create modified source files for
incremental edit loop, check Xcode compatibility.

test.py: Thin entrypoint that builds TestTraits and invokes Runner.

post.py: Uninstall app from simulator, shut down dotnet build server,
clean directories.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create the Helix machine setup script for MAUI iOS inner loop measurements.
This script runs on the macOS Helix machine before test.py and handles:

1. DOTNET_ROOT/PATH configuration from the correlation payload SDK
2. Xcode selection — auto-detects highest versioned Xcode_*.app, matching
   the pattern used by maui_scenarios_ios.proj PreparePayloadWorkItem
3. iOS simulator runtime validation via xcrun simctl
4. Simulator device boot with graceful already-booted handling
5. maui-ios workload install using rollback file from pre.py, with
   --ignore-failed-sources for dead NuGet feeds
6. NuGet package restore with --ignore-failed-sources /p:NuGetAudit=false
7. Spotlight indexing disabled via mdutil to prevent file-lock errors

Follows the same structure as the Android inner loop setup_helix.py:
context dict pattern, step-by-step functions, structured logging to
HELIX_WORKITEM_UPLOAD_ROOT for post-mortem debugging.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Define the Helix .proj file for iOS inner loop measurements, modeled after
the Android inner loop .proj and existing maui_scenarios_ios.proj patterns.

Key design decisions:
- Build on Helix machine (not build agent) because deploy requires a
  connected device/simulator. PreparePayloadWorkItem only creates the
  template and modified source files via pre.py.
- Workload packs stripped from correlation payload (RemoveDotnetFromCorrelation
  Staging) and reinstalled on Helix machine by setup_helix.py.
- Environment variables set via shell 'export' in PreCommands (not in Python)
  because os.environ changes don't persist across process boundaries.
- No XHarness — iOS inner loop uses xcrun simctl directly.
- Simulator-only for now; physical device support (ios-arm64, code signing)
  is structured as a future TODO pending runner.py device support.
- 01:30 timeout to accommodate iOS build + workload install + NuGet restore.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- sdk-perf-jobs.yml: Add Mono Debug job entry for maui_scenarios_ios_innerloop
  on osx-x64-ios-arm64 (Mac.iPhone.17.Perf queue)
- run-performance-job.yml: Add maui_scenarios_ios_innerloop to the in() check
  so --runtime-flavor is forwarded to run_performance_job.py
- run_performance_job.py: Add maui_scenarios_ios_innerloop to
  get_run_configurations() (CodegenType, RuntimeType, BuildConfig) and to the
  binlog copy block for PreparePayloadWorkItems artifacts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ioshelper.py: Add detect_connected_device() with auto-detection via
  xcrun devicectl (JSON + fallback text parsing), uninstall_app_physical,
  terminate_app_physical, close_physical_device, and cleanup() dispatch
- runner.py: Add --device-type arg (simulator/device) to iosinnerloop
  subparser, auto-infer from RuntimeIdentifier, auto-detect device UDID,
  branch setup/install/startup/cleanup for physical vs simulator
- setup_helix.py: Detect device type from IOS_RID env var, skip simulator
  boot for physical device, add detect_physical_device() for Helix
- post.py: Handle physical device uninstall via devicectl with UDID
  auto-detection fallback
- maui_scenarios_ios_innerloop.proj: Add physical device HelixWorkItem
  (conditioned on iOSRid=ios-arm64), pass IOS_RID to Pre/PostCommands,
  add --device-type arg to both simulator and device workitems

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

Fix 1 (Major): Replace non-existent 'devicectl terminate --bundle-id' with
'--terminate-existing' flag on launch command. Make terminate_app_physical()
a no-op with documentation explaining why.

Fix 2 (Medium): Write devicectl JSON output to temp file instead of
/dev/stdout, which mixes human-readable table and JSON. Applied in both
ioshelper.py and setup_helix.py with proper temp file cleanup.

Fix 3 (Medium): Add standard UUID pattern (8-4-4-4-12) to UDID regex in
_detect_device_fallback() for CoreDevice UUID format compatibility.

Fix 4 (Medium): Normalize MAUI template to always use Pages/ subdirectory
in pre.py. If template puts MainPage files at root, move them to Pages/.
Add explanatory comment in .proj documenting the coupling.

Fix 5 (Minor): Use tuple-of-ints version sort for Xcode selection instead
of string comparison (fixes 16.10 < 16.2 ordering bug).

Fix 6 (Minor): Make simulator boot failure fatal with sys.exit(1). Add
dynamic fallback to latest available iPhone simulator before failing.

Fix 7 (Nit): Add missing trailing newline to runner.py.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace deprecated tempfile.mktemp() with tempfile.mkstemp() in both
  ioshelper.py and setup_helix.py to avoid TOCTOU race condition.
- Fix unreachable fallback in detect_connected_device(): when devicectl
  exits non-zero (e.g., older Xcode without --json-output), call
  _detect_device_fallback() instead of returning None immediately.
- Guard against missing JSON report in runner.py IOSINNERLOOP branch:
  Startup.cs only writes reports when PERFLAB_INLAB=1, so local runs
  would crash with FileNotFoundError. Now degrades gracefully with
  empty counters and a warning.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Temporarily disable all other scenario jobs to speed up CI
iteration while validating the new MAUI iOS Inner Loop scenario.
This change should be reverted before merging.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Capture dotnet build output instead of crashing on CalledProcessError
- Create traces/ directory before first build
- Fix setup_helix.py to write output.log (matches .proj expectation)
- Improve error handling for build failures

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dotnet build stdout/stderr wasn't appearing in Helix console logs,
making it impossible to diagnose build failures. Explicitly capture and
print build output through Python logging.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Build 2943141 hit the 90-minute timeout. iOS first build with AOT
compilation can take 30+ minutes, plus 3 incremental iterations.
Increasing to 2.5 hours to allow full completion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Helix machines have Xcode 26.2 but the iOS SDK requires 26.3.
The minor version difference shouldn't affect build correctness,
so bypass the check with ValidateXcodeVersion=false.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mac.iPhone.17.Perf queue uses Intel x64 machines which need
iossimulator-x64, not iossimulator-arm64. Add architecture
detection in setup_helix.py and update default RID in .proj.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The traces upload directory already exists from the first build,
causing copytree() to fail on subsequent iterations. Clear it
before each parsetraces() call.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The -v:n flag was added to debug build errors but produces
excessive file copy logs. Default verbosity shows errors/warnings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add /p:MtouchLink=None to disable managed linker for Debug inner
  loop builds, avoiding MT0180 errors on machines without Xcode 26.3
- Add minimum Xcode version check in setup_helix.py for fast failure
  with clear diagnostics when machine has Xcode < 26.0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove temporary ${{ if false }}: wrappers that disabled all jobs
except iOS inner loop during iterative CI debugging.

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

- Separate install from simulator/device setup in ioshelper.py
- Capture install time for first and incremental deploys in runner.py
- Add "Install Time" counter to both perf reports
- Add CoreCLR Debug job entry in pipeline YAML
- Add device (ios-arm64) job entries for both Mono and CoreCLR
- Wire iOSRid env var through to MSBuild for device builds
- [TEMP] Disable non-iOS-inner-loop jobs for CI validation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the dynamically-resolved manifest references SDK packs not yet
propagated to NuGet feeds, fall back to installing without the
rollback file. This avoids CI being blocked by transient feed
propagation delays.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the rollback file references SDK packs not yet propagated to
NuGet feeds, retry without the rollback file. Matches the fallback
pattern already added to pre.py.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The simulator HelixWorkItem was unconditionally included, even when
iOSRid=ios-arm64. This caused the simulator to receive device RID
in _MSBuildArgs, producing ARM64 binaries that can't install on a
simulator. Add Condition to exclude it from device jobs.

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

Consolidate 12 duplicated simulator/device methods in ioshelper.py into
a unified API (setup_device, install_app, measure_cold_startup, cleanup)
that dispatches internally based on is_physical_device. Removes all
if-is_physical dispatch branches from runner.py.

Extract merge_build_deploy_and_startup and _make_counter to module-level
helpers. Inline the incremental iteration loop (was a nested function
with 10 parameters). Simplify post.py to reuse ioshelper instead of
duplicating device detection. Extract inject_csproj_properties in pre.py.

Net reduction: -232 lines across 4 files.

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

Re-apply run_env_vars (including iOSRid) right before perf_send_to_helix()
so the MSBuild .proj ItemGroup conditions can correctly exclude the
simulator work item from device jobs.

Add '|| exit $?' to setup_helix.py PreCommands so that when setup_helix
exits non-zero (e.g., Xcode too old), the Helix shell stops instead of
continuing to run test.py which would fail with a less clear error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Env var inheritance through msbuild.sh/tools.sh is unreliable for
iOSRid. Add ios_rid field to PerfSendToHelixArgs and pass it as
/p:iOSRid=<value> on the MSBuild command line so it reaches .proj
evaluation deterministically. Also set it via set_environment_variables
as belt-and-suspenders.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mono_InnerLoop → Mono_InnerLoop_Simulator
CoreCLR_InnerLoop → CoreCLR_InnerLoop_Simulator

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace simctl (simulator) and devicectl (device) install/launch commands
with mlaunch to match the real Visual Studio F5 developer experience:

- Simulator: --launchsim combines install + launch (install_app returns 0)
- Device: --installdev for install, --launchdev for launch
- Device cleanup: --uninstalldevbundleid replaces devicectl uninstall
- Simulator cleanup: unchanged (simctl terminate + uninstall)
- Added _resolve_mlaunch() to find mlaunch from iOS SDK packs

Device detection (devicectl) and simulator management (simctl boot/
terminate/uninstall) remain unchanged. The install_app/measure_cold_startup
API is preserved so runner.py requires no changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of making install_app() a no-op for simulator, use
mlaunch --installsim to get a separate install measurement.
measure_cold_startup() still uses --launchsim for launch timing.

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

After installing the maui-ios workload, read _RecommendedXcodeVersion
from the SDK's Versions.props and switch to the matching Xcode_*.app
if the currently active Xcode doesn't match. This handles the case
where Helix agents have a newer Xcode than the SDK requires.

Falls back gracefully to the already-selected Xcode if no matching
installation is found.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
davidnguyen-tech and others added 27 commits April 8, 2026 13:48
Use --from-rollback-file for workload install to get latest nightly
builds. Make Xcode handling diagnostic-only (log version, no switching)
since the SDK validates Xcode compatibility at build time.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dotnet CLI can hang for hours when NuGet feeds are slow or broken
(internal download retries). On build 2946062, CoreCLR Simulator's
workload install hung for 2h28m on DNCENGMAC044 due to download
failures for microsoft.netcore.app.runtime.aot.osx-x64.cross.iossimulator-x64.
The fallback retry started 2 min before the Helix work item timeout and
was killed.

Add _run_workload_cmd() wrapper with a 20-minute timeout per attempt.
Total worst case is 40 min for both attempts, leaving 1:50 for actual
test execution within the 2:30 Helix work item timeout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pin microsoft.net.sdk.ios to 26.2.11591-net11-p4 (preview.3 band)
to bypass the Xcode 26.4 requirement of the latest nightly packs.
This allows CI iteration on the Helix machines which have Xcode 26.2.

Cross-band note: the Helix SDK is preview.4 but the 26.2.x manifests
only exist on the preview.3 band. Using --from-rollback-file to
attempt cross-band installation.

TODO: Remove this pin when Helix machines have Xcode 26.4.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Helix machines in Mac.iPhone.17.Perf may default to Xcode 15.0 while the
iOS SDK packs (26.2.x) require Xcode >= 26.0. The old select_xcode() only
logged the version diagnostically, so the mismatch wasn't caught until
_ValidateXcodeVersion failed 20+ minutes later during the build.

Now select_xcode():
- Finds /Applications/Xcode_*.app dirs, sorted by version number
- Activates the highest one via sudo xcode-select -s
- Falls back to the system default if no Xcode_*.app found
- Parses and validates the version is >= 26.0
- Fails fast with a clear error if the minimum isn't met

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

Refinements from code review:
- Check default Xcode first; only search Xcode_*.app if default < 26
- Log xcode-select -p before AND after selection for diagnostics
- Add comment explaining why >= 26 is hardcoded (packs not yet installed
  at this point so we can't read _RecommendedXcodeVersion)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…est — validate post-switch version

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the iOS workload manifest declares net10.0 cross-targeting packs that
don't exist on any NuGet feed, the standard --from-rollback-file install
fails. This adds a fallback that:

1. Tries --from-rollback-file first (works when all packs are published)
2. If that fails, downloads the manifest nupkg from the NuGet flat container
3. Patches WorkloadManifest.json to remove net10.0 entries from
   workloads.ios.packs, workloads.ios.extends, and top-level packs
4. Places the patched manifest on disk under sdk-manifests/
5. Installs with --skip-manifest-update

Also fixes feed variable scoping: when the fallback feed is used during
package resolution, the feed variable now reflects the actual feed used
(needed for downloading the manifest nupkg in the patching fallback).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Handle workloads.ios.packs as list or dict (manifest format varies)
- Add safety check for top-level packs being unexpected type
- Add timeout=60 to urlopen for service index fetch
- Replace urlretrieve with urlopen+write for nupkg download (timeout=120)
- Narrow except clause from Exception to subprocess.CalledProcessError
- Log warning when manifest patching removes zero entries

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The install_workload() fallback path (--skip-manifest-update) already
works correctly with pre.py's manifest-patching approach:

- pre.py patches the iOS manifest to remove net10.0 entries and places
  it in the SDK tree at $DOTNET_ROOT/sdk-manifests/{band}/...
- The SDK tree ships to Helix as the correlation payload
- The --skip-manifest-update retry tells the CLI to use on-disk
  manifests, picking up the patched version

No functional changes — updated docstring and inline comments to
document why the fallback works and its dependency on pre.py.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix startup measurement: use SpringBoard watchdog events via
  log collect instead of timing the devicectl command wall-clock
- Fix device UDID detection: read hardwareProperties.udid instead
  of CoreDevice identifier from devicectl JSON
- Use devicectl directly for physical device install/launch
  (mlaunch tunnel failures on local machines)
- Skip post-build signing on local runs (MSBuild handles it)
- Prefix binlog filenames with RUNTIME_FLAVOR to prevent overwrites
- Log SDK versions (dotnet --info + versions.json from linked DLLs)
  matching the pattern used by other MAUI scenarios

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ugs, reduce parser boilerplate

- Refactor install_latest_maui() to accept workloads and workload_id params,
  eliminating ~80 lines of duplicated workload resolution logic in pre.py
- Fix feed resolution at import-time bug (now resolved at call time)
- Fix simulator startup timeout silently returning elapsed time instead of raising
- Fix "Time to Main" metric mislabel — device measures total cold startup
  (time-to-main + time-to-first-draw), renamed to "Cold Startup Time"
- Fix hardcoded iossimulator-arm64 RID — now uses IOS_RID env var for
  version extraction on Intel Helix machines
- Fix setup_helix.py device detection to prefer hardware UDID over CoreDevice
  identifier, matching ioshelper.py behavior required by mlaunch
- Reduce iOSInnerLoopParser.cs from 224 to 105 lines using dictionary-driven
  task/target collection instead of 30+ separate List<double> variables
- Add duplication documentation comment on setup_helix.py detect_physical_device

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sign_app_for_device() now warns and skips re-signing when the Helix-specific
'sign' tool is not found, instead of raising FileNotFoundError. For local
builds, MSBuild/Xcode already signs the app with the developer's identity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Simplified orchestration script (~226 lines) that delegates to the
repo's existing init.sh, pre.py, test.py, and post.py instead of
reimplementing their logic. Supports --configs, --device-type,
--iterations, --skip-setup, and --dry-run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
select_xcode.py uses a 3-tier strategy to find the correct Xcode:
1. rollback_maui.json (created by pre.py during workload install)
2. SDK pack _RecommendedXcodeVersion (from installed workload)
3. Highest available >= minimum version (fallback)

run-local.sh now calls this helper instead of naively picking
the highest Xcode version, which could mismatch the SDK.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy .binlog files from traces/ to results/<runtime>/ before
cleanup, so they persist alongside the JSON reports.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserve SDK/runtime version metadata alongside binlogs and
JSON reports in results/<runtime>/.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
setup_device() already uninstalls stale apps on simulator but
skipped physical devices. A leftover install could affect
first-deploy timing. Uses mlaunch --uninstalldevbundleid
(best-effort, matching cleanup() pattern).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a previous run is interrupted, leftover obj/ directories cause
XAML source generator duplicate errors on the next build. Clean app/
and traces/ before running pre.py when --skip-setup is not set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
measure_cold_startup() returns -1 when watchdog event parsing fails on
physical devices (fewer than 4 events found). Without validation, this
invalid sentinel value was silently appended to results and merged into
JSON perf reports, polluting performance data.

Raise RuntimeError immediately at both call sites (first deploy and
incremental deploy iterations) so failures surface as crashes rather
than corrupt data.
Add tasks (ILLink, CompileNativeCode, GetFileHash) and targets
(_RunILLink, _SelectR2RAssemblies, _LinkR2RFramework,
_CompileNativeExecutable, _GenerateDSym) to capture the full
build time breakdown. Binlog analysis showed _SelectR2RAssemblies
alone accounts for 55% of the CoreCLR vs Mono build gap.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- pre.py: Strip whitespace from _RecommendedXcodeVersion to avoid false
  Xcode version mismatch warnings
- pre.py: Use XML element check (<name>) instead of substring match for
  property injection to avoid silent failures from comment matches
- setup_helix.py: Fix iPhone simulator regex to match non-numeric models
  (SE, XR, XS) in fallback selection path
- Reporter.cs: Add CultureInfo.InvariantCulture to DateTime.Parse to
  handle non-US locales correctly

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add _measure_app_size() that walks the .app directory to compute
total file size. Emits 'App Bundle Size' (bytes) counter in both
first-deploy and incremental reports, enabling CoreCLR vs Mono
binary size comparison.

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

Bug 1: Sort watchdog events by timestamp before indexing — log show
ndjson output is not guaranteed chronological, which could produce
incorrect or negative time deltas. Also validate the expected event
sequence (monitor → stop → monitor → stop) and return -1 on mismatch.

Bug 2: find_app_bundle always searched iossimulator-* first, returning
the simulator build even for physical device runs. Add is_physical
parameter to restrict search to ios-arm64 when targeting a physical
device. Update the caller in runner.py to pass the existing is_physical
flag through.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ix work item budget

NuGet restore can hang on dead feeds. Without a timeout, it would consume
the entire 2:30 Helix work item timeout. 10 minutes is generous for restore.
Copilot AI review requested due to automatic review settings April 14, 2026 13:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new MAUI iOS “inner loop” (build → deploy → cold-startup → incremental repeat) performance scenario and wires it into Helix/CI for both simulator and physical devices.

Changes:

  • Added a new binlog parser (iOSInnerLoopParser) to extract iOS build task/target timings and integrated it into ScenarioMeasurement.
  • Implemented iOS simulator/device orchestration (workload install, Xcode selection, deploy/startup measurement, reporting merge) via new scenario scripts and a shared iOSHelper.
  • Updated Helix/pipeline plumbing to pass iOS RID, route the new run kind, and define the new Helix work items.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/tools/ScenarioMeasurement/Util/Parsers/iOSInnerLoopParser.cs New binlog parser emitting task/target/build duration counters for iOS inner loop.
src/tools/ScenarioMeasurement/Startup/Startup.cs Adds new MetricType and wires it to the new parser.
src/tools/Reporting/Reporting/Reporter.cs Makes build timestamp parsing null-tolerant for lab reporting.
src/scenarios/shared/runner.py Adds iosinnerloop runner flow (build/binlog parse + install/startup timings + report merge + uploads).
src/scenarios/shared/mauisharedpython.py Extends MAUI workload install helper to support selecting workload manifest IDs + workload ID.
src/scenarios/shared/ioshelper.py New helper for simulator/device management, install, launch, and (device) watchdog-based startup measurement.
src/scenarios/shared/const.py Adds IOSINNERLOOP scenario constant and display name mapping.
src/scenarios/mauiiosinnerloop/test.py New scenario entrypoint for the iOS inner loop test.
src/scenarios/mauiiosinnerloop/setup_helix.py Helix machine setup (DOTNET_ROOT, Xcode selection, simulator boot/device detect, workload install, restore).
src/scenarios/mauiiosinnerloop/select_xcode.py Standalone helper for selecting an Xcode matching SDK/workload requirements.
src/scenarios/mauiiosinnerloop/run-local.sh Local convenience runner for end-to-end measurements and report collection.
src/scenarios/mauiiosinnerloop/pre.py Creates/normalizes MAUI template for iOS-only build + prepares incremental edit sources; workload install with fallback manifest patching.
src/scenarios/mauiiosinnerloop/post.py Cleanup script for simulator/device uninstall and workspace cleanup.
src/scenarios/mauiiosinnerloop/results/mono/first-debug-e2e-perf-lab-report.json Added example/generated report output (should likely not be committed).
src/scenarios/mauiiosinnerloop/results/mono/incremental-debug-e2e-perf-lab-report.json Added example/generated report output (should likely not be committed).
src/scenarios/mauiiosinnerloop/results/coreclr/first-debug-e2e-perf-lab-report.json Added example/generated report output (should likely not be committed).
src/scenarios/mauiiosinnerloop/results/coreclr/incremental-debug-e2e-perf-lab-report.json Added example/generated report output (should likely not be committed).
scripts/send_to_helix.py Adds ios_rid support and passes it as an MSBuild property for reliable .proj evaluation.
scripts/run_performance_job.py Propagates run_env_vars into environment for MSBuild evaluation; includes ios inner loop in binlog copying; forwards iOSRid into SendToHelix args.
eng/pipelines/templates/run-performance-job.yml Enables --runtime-flavor for the new maui_scenarios_ios_innerloop run kind.
eng/pipelines/sdk-perf-jobs.yml Adds iOS inner loop jobs, but also temporarily disables many unrelated private jobs via if false.
eng/performance/maui_scenarios_ios_innerloop.proj New Helix work item definition for simulator and device inner loop scenario runs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2 to +27
"tests": [
{
"counters": [
{
"name": "Install Time",
"topCounter": false,
"defaultCounter": false,
"higherIsBetter": false,
"metricName": "ms",
"results": [
5950.941801071167
]
},
{
"name": "Cold Startup Time",
"topCounter": true,
"defaultCounter": false,
"higherIsBetter": false,
"metricName": "ms",
"results": [
3416
]
}
]
}
]
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

This results/...perf-lab-report.json file appears to be generated output from running the scenario locally. It will likely become stale and add noise to diffs. Consider removing generated results from the repo and keeping only the code/scripts that produce them.

Suggested change
"tests": [
{
"counters": [
{
"name": "Install Time",
"topCounter": false,
"defaultCounter": false,
"higherIsBetter": false,
"metricName": "ms",
"results": [
5950.941801071167
]
},
{
"name": "Cold Startup Time",
"topCounter": true,
"defaultCounter": false,
"higherIsBetter": false,
"metricName": "ms",
"results": [
3416
]
}
]
}
]
"tests": []

Copilot uses AI. Check for mistakes.
WinUIBlazor,
TimeToMain2,
BuildTime,
iOSInnerLoop,
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

MetricType enum values elsewhere use PascalCase, but iOSInnerLoop starts with a lowercase i. For consistency (and clearer CLI/help output if DragonFruit uses the enum names), consider renaming this to IOSInnerLoop and updating the corresponding parser type name accordingly.

Suggested change
iOSInnerLoop,
IOSInnerLoop,

Copilot uses AI. Check for mistakes.
/// <summary>
/// Parses iOS inner loop (build+deploy) target and task durations from a binary log file.
/// </summary>
public class iOSInnerLoopParser : IParser
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The parser type is named iOSInnerLoopParser (lowercase leading i), which is inconsistent with the PascalCase naming used by other parsers in this directory (e.g., BuildTimeParser, InnerLoopParser). Consider renaming to IOSInnerLoopParser and updating references so it matches the established type naming conventions.

Suggested change
public class iOSInnerLoopParser : IParser
public class IOSInnerLoopParser : IParser

Copilot uses AI. Check for mistakes.
Comment on lines 329 to +334
# Scenario benchmarks
- template: /eng/pipelines/templates/build-machine-matrix.yml
parameters:
jobTemplate: /eng/pipelines/templates/run-scenarios-job.yml
buildMachines:
- win-x64-viper
- ubuntu-x64-viper
- win-arm64-ampere
- ubuntu-arm64-ampere
isPublic: false
jobParameters:
runKind: scenarios
projectFileName: scenarios.proj
channels:
- main
- 9.0
- 8.0
${{ each parameter in parameters.jobParameters }}:
${{ parameter.key }}: ${{ parameter.value }}
- ${{ if false }}: # [TEMP] Disabled for iOS inner loop CI validation
- template: /eng/pipelines/templates/build-machine-matrix.yml
parameters:
jobTemplate: /eng/pipelines/templates/run-scenarios-job.yml
buildMachines:
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

A large set of existing private jobs is currently wrapped in ${{ if false }} with a [TEMP] note. This effectively disables most of the private perf pipeline (scenarios, affinitized scenarios, Android/iOS release/debug, NativeAOT, etc.) whenever this template is used. If the intent is only to gate iOS inner loop validation, please avoid hard-disabling unrelated jobs here; instead gate the new iOS inner loop jobs behind a parameter/condition, or keep the existing jobs enabled and add the new ones alongside them.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +6
<!-- No XHarness needed — iOS inner loop uses xcrun simctl directly for
simulator deploy and does not use the XHarness test infrastructure. -->
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The comment says the inner loop scenario uses xcrun simctl directly for simulator deploy, but the implementation uses mlaunch --installsim/--launchsim for deploy/launch (with simctl mainly for boot/terminate/uninstall). Please update the comment to match the actual tooling so future maintainers aren’t misled about how deploy is performed.

Suggested change
<!-- No XHarness needed — iOS inner loop uses xcrun simctl directly for
simulator deploy and does not use the XHarness test infrastructure. -->
<!-- No XHarness needed — iOS inner loop uses mlaunch
(--installsim/--launchsim) for simulator deploy/launch, with xcrun
simctl used for simulator lifecycle tasks such as boot, terminate,
and uninstall. -->

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +10
{
"tests": [
{
"categories": [
"Startup"
],
"name": "MAUI iOS Inner Loop - Local - First Build and Deploy",
"additionalData": {},
"counters": [
{
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

This results/...perf-lab-report.json file appears to be generated output from running the scenario locally. It will likely become stale and add noise to diffs. Consider removing generated results from the repo and keeping only the code/scripts that produce them.

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +27
"tests": [
{
"counters": [
{
"name": "Install Time",
"topCounter": false,
"defaultCounter": false,
"higherIsBetter": false,
"metricName": "ms",
"results": [
8044.32487487793
]
},
{
"name": "Cold Startup Time",
"topCounter": true,
"defaultCounter": false,
"higherIsBetter": false,
"metricName": "ms",
"results": [
2668
]
}
]
}
]
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

This results/...perf-lab-report.json file appears to be generated output from running the scenario locally. It will likely become stale and add noise to diffs. Consider removing generated results from the repo and keeping only the code/scripts that produce them.

Suggested change
"tests": [
{
"counters": [
{
"name": "Install Time",
"topCounter": false,
"defaultCounter": false,
"higherIsBetter": false,
"metricName": "ms",
"results": [
8044.32487487793
]
},
{
"name": "Cold Startup Time",
"topCounter": true,
"defaultCounter": false,
"higherIsBetter": false,
"metricName": "ms",
"results": [
2668
]
}
]
}
]
"tests": []

Copilot uses AI. Check for mistakes.
private static Build ParseBuildInfo(IEnvironment environment)
{
var buildTimestampStr = environment.GetEnvironmentVariable("PERFLAB_BUILDTIMESTAMP");
var buildTimestamp = !string.IsNullOrEmpty(buildTimestampStr) ? DateTime.Parse(buildTimestampStr, CultureInfo.InvariantCulture) : DateTime.Now;
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

PERFLAB_BUILDTIMESTAMP is now optional, but the fallback uses DateTime.Now and still uses DateTime.Parse(...) which will throw for invalid formats. For more stable reporting (and to avoid local-time ambiguity), consider using DateTimeOffset.TryParse/TryParseExact with DateTimeStyles.RoundtripKind and falling back to DateTimeOffset.UtcNow (or DateTime.UtcNow) when the env var is missing/invalid.

Suggested change
var buildTimestamp = !string.IsNullOrEmpty(buildTimestampStr) ? DateTime.Parse(buildTimestampStr, CultureInfo.InvariantCulture) : DateTime.Now;
var buildTimestamp = DateTimeOffset.TryParse(
buildTimestampStr,
CultureInfo.InvariantCulture,
DateTimeStyles.RoundtripKind,
out var parsedBuildTimestamp)
? parsedBuildTimestamp.UtcDateTime
: DateTimeOffset.UtcNow.UtcDateTime;

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +24
Install and launch use mlaunch (the same tool Visual Studio uses for F5)
to match the real developer inner-loop experience:
- Simulator: mlaunch --installsim / --launchsim
- Device: mlaunch --installdev / --launchdev
Device detection still uses devicectl; simulator management uses simctl.
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The class docstring says physical-device install/launch uses mlaunch --installdev/--launchdev, but install_app() uses xcrun devicectl device install app for physical devices. Please align the docstring with the current behavior (or switch the implementation to match the stated approach) to avoid confusion when debugging deployment failures.

Suggested change
Install and launch use mlaunch (the same tool Visual Studio uses for F5)
to match the real developer inner-loop experience:
- Simulator: mlaunch --installsim / --launchsim
- Device: mlaunch --installdev / --launchdev
Device detection still uses devicectl; simulator management uses simctl.
The helper uses different platform tools depending on the target:
- Simulator: mlaunch for app install/launch; simctl for simulator management
- Device: devicectl for device detection and app install
This mixed-tool approach reflects the current implementation and is useful
to keep in mind when debugging deployment failures.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +10
{
"tests": [
{
"categories": [
"Startup"
],
"name": "MAUI iOS Inner Loop - Local - First Build and Deploy",
"additionalData": {},
"counters": [
{
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

These checked-in results/...perf-lab-report.json files look like generated output from a local run. They are likely to become stale/noisy and can bloat the repo. Consider removing them from source control and (if needed) documenting how to generate them locally or adding a small sanitized example under a dedicated docs/testdata folder.

Copilot uses AI. Check for mistakes.
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.

2 participants