diff --git a/.github/skills/maui-android-innerloop/SKILL.md b/.github/skills/maui-android-innerloop/SKILL.md new file mode 100644 index 00000000000..e6d02f9d257 --- /dev/null +++ b/.github/skills/maui-android-innerloop/SKILL.md @@ -0,0 +1,123 @@ +--- +name: maui-android-innerloop +description: Guide for running MAUI Android Inner Loop deploy measurements in the dotnet/performance repo. Use this when asked to measure, benchmark, or compare MAUI Android first deploy and incremental deploy times across runtime configurations (Mono+Interpreter, CoreCLR+JIT, etc.). +--- + +# MAUI Android Inner Loop Measurement + +Measures first deploy and incremental deploy times for a .NET MAUI Android app using MSBuild binary logs. Located in `src/scenarios/mauiandroidinnerloop/` within the dotnet/performance repo. + +## Prerequisites + +Before running measurements, verify: + +1. **Android device** connected via USB: `adb devices` should list a device. +2. **.NET SDK** installed at `tools/dotnet/arm64/`. Bootstrap with: + ```bash + cd src/scenarios && . ./init.sh -channel main + ``` +3. **maui-android workload** installed (NOT full `maui` — iOS packages fail without the iOS workload): + ```bash + export DOTNET_ROOT="$(pwd)/tools/dotnet/arm64" + export PATH="$DOTNET_ROOT:$PATH" + dotnet workload install maui-android \ + --from-rollback-file src/scenarios/mauiandroidinnerloop/rollback_maui.json \ + --skip-sign-check + ``` +4. **Startup tool** (binlog parser) built: + ```bash + PERFLAB_TARGET_FRAMEWORKS=net11.0 dotnet publish \ + src/tools/ScenarioMeasurement/Startup/Startup.csproj \ + -c Release -o artifacts/startup --ignore-failed-sources /p:NuGetAudit=false + ``` + +## Environment Setup (every new shell) + +```bash +cd src/scenarios && . ./init.sh -dotnetdir /tools/dotnet/arm64 +cd mauiandroidinnerloop +``` + +## Create the App Template + +```bash +python3 pre.py publish -f net11.0-android --has-workload +``` + +### CRITICAL: Fix csproj after pre.py + +`dotnet new maui` targets all platforms. Since only maui-android workload is installed, you MUST edit `app/MauiAndroidInnerLoop.csproj` and remove the iOS/MacCatalyst/Windows TargetFrameworks conditions, leaving only: + +```xml +net11.0-android +``` + +Remove these two lines that follow the android TargetFrameworks line: +```xml +$(TargetFrameworks);net11.0-ios;net11.0-maccatalyst +$(TargetFrameworks);net11.0-windows10.0.19041.0 +``` + +## MSBuild Args per Configuration + +| Configuration | MSBuild Properties | +|-------------------|---------------------------------------------------------------------------------------| +| Mono+Interpreter | `/p:UseMonoRuntime=true` | +| CoreCLR+JIT | `/p:UseMonoRuntime=false /p:PublishReadyToRun=false /p:PublishReadyToRunComposite=false` | + +## Running a Measurement + +```bash +# Clean from any prior run +rm -rf app/bin app/obj traces + +# Mono+Interpreter +python3 test.py androidinnerloop \ + --csproj-path app/MauiAndroidInnerLoop.csproj \ + --edit-src src/MainPage.xaml.cs \ + --edit-dest app/MainPage.xaml.cs \ + -f net11.0-android -c Debug \ + --msbuild-args "/p:UseMonoRuntime=true" + +# CoreCLR+JIT +python3 test.py androidinnerloop \ + --csproj-path app/MauiAndroidInnerLoop.csproj \ + --edit-src src/MainPage.xaml.cs \ + --edit-dest app/MainPage.xaml.cs \ + -f net11.0-android -c Debug \ + --msbuild-args "/p:UseMonoRuntime=false;/p:PublishReadyToRun=false;/p:PublishReadyToRunComposite=false" +``` + +## What test.py androidinnerloop Does + +1. **First deploy:** `dotnet build -t:Install -c Debug -f net11.0-android /p:UseSharedCompilation=true -bl:traces/first-deploy.binlog` +2. **File edit:** Copies modified `MainPage.xaml.cs` to simulate an incremental code edit +3. **Incremental deploy:** `dotnet build -t:Install ... -bl:traces/incremental-deploy.binlog` +4. **Parse binlogs:** Extracts per-task timings using the Startup tool + +## Clean Between Configurations + +```bash +python3 post.py # Uninstalls APK, shuts down build servers, removes app/traces/etc. +python3 pre.py publish -f net11.0-android --has-workload +# Fix csproj again! (remove iOS/MacCatalyst/Windows targets) +``` + +## Persisting Binlogs + +Binlogs are in `traces/` and get cleaned by `post.py`. To keep them: + +```bash +mkdir -p binlogs +cp traces/first-deploy.binlog binlogs/-first-deploy.binlog +cp traces/incremental-deploy.binlog binlogs/-incremental-deploy.binlog +``` + +## Key Facts & Gotchas + +- **UseSharedCompilation=true** is set by `runner.py` for this scenario, overriding the repo default of `false`. This mirrors a real dev workflow where the Roslyn compiler server stays warm. `post.py` runs `dotnet build-server shutdown` to clean up between runs. +- **FastDev** (Fast Deployment) is ON by default in Debug (`EmbedAssembliesIntoApk=false`). Do NOT set `EmbedAssembliesIntoApk=true` — it disables FastDev and makes deploys 10x slower. +- **Startup tool** targets `net8.0` by default and needs .NET 8 runtime. Build with `PERFLAB_TARGET_FRAMEWORKS=net11.0` to retarget if only .NET 11 is available. +- **Dead NuGet feeds** (`darc-pub-dotnet-android-*`) break Startup tool builds. Use `--ignore-failed-sources /p:NuGetAudit=false`. +- **macOS Spotlight** can race with builds causing random errors (XARDF7024, MAUIR0001, CS2012). Fix: `sudo mdutil -i off `. +- **The repo sets `UseSharedCompilation=false`** in `src/Directory.Build.props` and `src/scenarios/init.sh`. The runner.py override via `/p:UseSharedCompilation=true` on the command line takes precedence. diff --git a/eng/performance/maui_scenarios_android_innerloop.proj b/eng/performance/maui_scenarios_android_innerloop.proj new file mode 100644 index 00000000000..064ecfe501d --- /dev/null +++ b/eng/performance/maui_scenarios_android_innerloop.proj @@ -0,0 +1,72 @@ + + + + + + true + + + + <_MSBuildArgs Condition="'$(RuntimeFlavor)' == 'mono'">/p:UseMonoRuntime=true + <_MSBuildArgs Condition="'$(RuntimeFlavor)' == 'coreclr'">/p:UseMonoRuntime=false + + + <_MSBuildArgs Condition="'$(RuntimeFlavor)' == 'mono' and '$(CodegenType)' == 'AOT'">$(_MSBuildArgs);/p:RunAOTCompilation=true;/p:AndroidEnableProfiledAot=false + + <_MSBuildArgs Condition="'$(RuntimeFlavor)' == 'coreclr' and '$(CodegenType)' == 'JIT'">$(_MSBuildArgs);/p:PublishReadyToRun=false;/p:PublishReadyToRunComposite=false + + <_MSBuildArgs Condition="'$(RuntimeFlavor)' == 'coreclr' and '$(CodegenType)' == 'R2R'">$(_MSBuildArgs);/p:PublishReadyToRun=true;/p:PublishReadyToRunComposite=false + + <_MSBuildArgs Condition="'$(RuntimeFlavor)' == 'coreclr' and '$(CodegenType)' == 'R2RComposite'">$(_MSBuildArgs);/p:PublishReadyToRun=true;/p:PublishReadyToRunComposite=true + + <_MSBuildArgs Condition="'$(RuntimeFlavor)' == 'coreclr' and '$(CodegenType)' == 'NativeAOT'">$(_MSBuildArgs);/p:PublishAot=true + + + <_MSBuildArgs Condition="'$(BuildConfig)' == 'Debug'">$(_MSBuildArgs);/p:EmbedAssembliesIntoApk=true + + $(RuntimeFlavor)_$(CodegenType) + + + + + + + 01:00 + + + + + + mauiandroidinnerloop + $(ScenariosDir)%(ScenarioDirectoryName) + + + + + + + $(Python) pre.py + %(PreparePayloadWorkItem.PayloadDirectory) + + + + + + + set DOTNET_ROOT=%HELIX_CORRELATION_PAYLOAD%\dotnet&& set PATH=%HELIX_CORRELATION_PAYLOAD%\dotnet;%PATH%&& set NUGET_PACKAGES=%HELIX_WORKITEM_ROOT%\.packages + $(Python) test.py androidinnerloop --csproj-path app\MauiAndroidInnerLoop.csproj --edit-src src\MainPage.xaml.cs --edit-dest app\MainPage.xaml.cs -f $(PERFLAB_Framework)-android -c $(BuildConfig) --msbuild-args "$(_MSBuildArgs)" --scenario-name "%(Identity)" $(ScenarioArgs) + $(Python) post.py + + + + + + diff --git a/eng/pipelines/sdk-perf-jobs.yml b/eng/pipelines/sdk-perf-jobs.yml index 4c3a62d1a72..0a65b6b6485 100644 --- a/eng/pipelines/sdk-perf-jobs.yml +++ b/eng/pipelines/sdk-perf-jobs.yml @@ -527,6 +527,82 @@ jobs: ${{ each parameter in parameters.jobParameters }}: ${{ parameter.key }}: ${{ parameter.value }} + # Maui Android inner loop benchmarks (Mono Default) - Release + - template: /eng/pipelines/templates/build-machine-matrix.yml + parameters: + jobTemplate: /eng/pipelines/templates/run-scenarios-job.yml + buildMachines: + - win-x64-android-arm64-pixel + isPublic: false + jobParameters: + runKind: maui_scenarios_android_innerloop + projectFileName: maui_scenarios_android_innerloop.proj + channels: + - main + runtimeFlavor: mono + codeGenType: Default + buildConfig: Release + additionalJobIdentifier: Mono_InnerLoop + ${{ each parameter in parameters.jobParameters }}: + ${{ parameter.key }}: ${{ parameter.value }} + + # Maui Android inner loop benchmarks (CoreCLR Default) - Release + - template: /eng/pipelines/templates/build-machine-matrix.yml + parameters: + jobTemplate: /eng/pipelines/templates/run-scenarios-job.yml + buildMachines: + - win-x64-android-arm64-pixel + isPublic: false + jobParameters: + runKind: maui_scenarios_android_innerloop + projectFileName: maui_scenarios_android_innerloop.proj + channels: + - main + runtimeFlavor: coreclr + codeGenType: Default + buildConfig: Release + additionalJobIdentifier: CoreCLR_InnerLoop + ${{ each parameter in parameters.jobParameters }}: + ${{ parameter.key }}: ${{ parameter.value }} + + # Maui Android inner loop benchmarks (Mono Default) - Debug + - template: /eng/pipelines/templates/build-machine-matrix.yml + parameters: + jobTemplate: /eng/pipelines/templates/run-scenarios-job.yml + buildMachines: + - win-x64-android-arm64-pixel + isPublic: false + jobParameters: + runKind: maui_scenarios_android_innerloop + projectFileName: maui_scenarios_android_innerloop.proj + channels: + - main + runtimeFlavor: mono + codeGenType: Default + buildConfig: Debug + additionalJobIdentifier: Mono_Debug_InnerLoop + ${{ each parameter in parameters.jobParameters }}: + ${{ parameter.key }}: ${{ parameter.value }} + + # Maui Android inner loop benchmarks (CoreCLR Default) - Debug + - template: /eng/pipelines/templates/build-machine-matrix.yml + parameters: + jobTemplate: /eng/pipelines/templates/run-scenarios-job.yml + buildMachines: + - win-x64-android-arm64-pixel + isPublic: false + jobParameters: + runKind: maui_scenarios_android_innerloop + projectFileName: maui_scenarios_android_innerloop.proj + channels: + - main + runtimeFlavor: coreclr + codeGenType: Default + buildConfig: Debug + additionalJobIdentifier: CoreCLR_Debug_InnerLoop + ${{ each parameter in parameters.jobParameters }}: + ${{ parameter.key }}: ${{ parameter.value }} + # Maui iOS scenario benchmarks (Mono - Default) - Debug - template: /eng/pipelines/templates/build-machine-matrix.yml parameters: diff --git a/src/scenarios/mauiandroidinnerloop/post.py b/src/scenarios/mauiandroidinnerloop/post.py new file mode 100644 index 00000000000..edfd2ce4c7d --- /dev/null +++ b/src/scenarios/mauiandroidinnerloop/post.py @@ -0,0 +1,22 @@ +''' +post cleanup script +''' + +import subprocess +from performance.logger import setup_loggers, getLogger +from shared.postcommands import clean_directories +from test import EXENAME + +setup_loggers(True) +logger = getLogger(__name__) + +# Uninstall the app from the connected device so re-runs start from a clean state +package_name = f'com.companyname.{EXENAME.lower()}' +logger.info(f"Uninstalling {package_name} from device") +subprocess.run(['adb', 'uninstall', package_name], check=False) + +# Shut down the build server to release file locks before cleanup +logger.info("Shutting down build server") +subprocess.run(['dotnet', 'build-server', 'shutdown'], check=False) + +clean_directories() diff --git a/src/scenarios/mauiandroidinnerloop/pre.py b/src/scenarios/mauiandroidinnerloop/pre.py new file mode 100644 index 00000000000..8ad6dea3c87 --- /dev/null +++ b/src/scenarios/mauiandroidinnerloop/pre.py @@ -0,0 +1,77 @@ +''' +pre-command: Set up a MAUI Android app for deploy measurement. +Creates the template, restores packages, and prepares the modified file for incremental deploy. +''' +import os +import re +import sys +from performance.logger import setup_loggers, getLogger +from shared import const +from shared.mauisharedpython import install_latest_maui, MauiNuGetConfigContext +from shared.precommands import PreCommands +from test import EXENAME + +setup_loggers(True) +logger = getLogger(__name__) +logger.info("Starting pre-command for MAUI Android deploy measurement") + +precommands = PreCommands() + +with MauiNuGetConfigContext(precommands.framework): + # Cache NuGet packages locally so they ship to Helix in the workitem payload. + # Without this, packages go to the global cache which isn't available on Helix. + packages_dir = os.path.join(sys.path[0], '.packages') + os.makedirs(packages_dir, exist_ok=True) + os.environ['NUGET_PACKAGES'] = packages_dir + logger.info(f"Set NUGET_PACKAGES to {packages_dir}") + + install_latest_maui(precommands) + precommands.print_dotnet_info() + + precommands.new(template='maui', + output_dir=const.APPDIR, + bin_dir=const.BINDIR, + exename=EXENAME, + working_directory=sys.path[0], + no_restore=False) + + # Fix the .csproj to target only Android (remove iOS, MacCatalyst, Windows TFMs). + # The MAUI template targets all platforms, but the Helix machine only has the Android SDK. + csproj_path = os.path.join(const.APPDIR, f'{EXENAME}.csproj') + with open(csproj_path, 'r') as f: + csproj_content = f.read() + + android_tfm = f'{precommands.framework}-android' + csproj_content = re.sub( + r'.*?', + f'{android_tfm}', + csproj_content + ) + + with open(csproj_path, 'w') as f: + f.write(csproj_content) + + logger.info(f"Updated {csproj_path}: TargetFrameworks set to {android_tfm}") + + # Copy the modified MainPage.xaml.cs into src/ for the incremental deploy simulation. + # test.py will copy this over app/MainPage.xaml.cs between deploys. + src_dir = os.path.join(sys.path[0], const.SRCDIR) + os.makedirs(src_dir, exist_ok=True) + + original_file = os.path.join(const.APPDIR, 'MainPage.xaml.cs') + modified_file = os.path.join(src_dir, 'MainPage.xaml.cs') + + with open(original_file, 'r') as f: + content = f.read() + + # Modify a string literal to trigger assembly recompilation + modified_content = content.replace('Hello, World!', 'Hello, World! ') + + if modified_content == content: + # Fallback: append a partial class extension to guarantee a code change + modified_content = content + '\npartial class MainPage { static string _ts = "modified"; }\n' + + with open(modified_file, 'w') as f: + f.write(modified_content) + + logger.info(f"Modified MainPage.xaml.cs written to {modified_file}") diff --git a/src/scenarios/mauiandroidinnerloop/test.py b/src/scenarios/mauiandroidinnerloop/test.py new file mode 100644 index 00000000000..35916e4feca --- /dev/null +++ b/src/scenarios/mauiandroidinnerloop/test.py @@ -0,0 +1,14 @@ +''' +MAUI Android Deploy Time Measurement +Orchestrates first deploy → file edit → incremental deploy → parse binlogs. +''' +import os +from shared.runner import TestTraits, Runner + +EXENAME = 'MauiAndroidInnerLoop' + +if __name__ == "__main__": + traits = TestTraits(exename=EXENAME, + guiapp='false', + ) + Runner(traits).run() diff --git a/src/scenarios/shared/const.py b/src/scenarios/shared/const.py index 074fa7fdf89..3492887ff13 100644 --- a/src/scenarios/shared/const.py +++ b/src/scenarios/shared/const.py @@ -19,6 +19,7 @@ ANDROIDINSTRUMENTATION = "androidinstrumentation" DEVICEPOWERCONSUMPTION = "devicepowerconsumption" BUILDTIME = "buildtime" +ANDROIDINNERLOOP = "androidinnerloop" SCENARIO_NAMES = {STARTUP: 'Startup', SDK: 'SDK', @@ -27,7 +28,8 @@ INNERLOOP: 'Innerloop', INNERLOOPMSBUILD: 'InnerLoopMsBuild', DOTNETWATCH: 'DotnetWatch', - BUILDTIME: 'BuildTime'} + BUILDTIME: 'BuildTime', + ANDROIDINNERLOOP: 'AndroidInnerLoop'} BINDIR = 'bin' PUBDIR = 'pub' diff --git a/src/scenarios/shared/runner.py b/src/scenarios/shared/runner.py index 165f0e882ba..b630eba0082 100644 --- a/src/scenarios/shared/runner.py +++ b/src/scenarios/shared/runner.py @@ -174,6 +174,16 @@ def parseargs(self): buildtimeparser.add_argument('--binlog-path', help='Location of binlog', dest='binlogpath') self.add_common_arguments(buildtimeparser) + androidinnerloopparser = subparsers.add_parser(const.ANDROIDINNERLOOP, + description='measure first and incremental deploy time via binlogs') + androidinnerloopparser.add_argument('--csproj-path', help='Path to .csproj file to build', dest='csprojpath') + androidinnerloopparser.add_argument('--edit-src', help='Path to modified source file (copied before incremental deploy)', dest='editsrc') + androidinnerloopparser.add_argument('--edit-dest', help='Destination path for the modified file', dest='editdest') + androidinnerloopparser.add_argument('--framework', '-f', help='Target framework (e.g., net10.0-android)', dest='framework') + androidinnerloopparser.add_argument('--configuration', '-c', help='Build configuration', dest='configuration', default='Debug') + androidinnerloopparser.add_argument('--msbuild-args', help='Additional MSBuild arguments', dest='msbuildargs', default='') + self.add_common_arguments(androidinnerloopparser) + args = parser.parse_args() if not args.testtype: @@ -196,7 +206,15 @@ def parseargs(self): if self.testtype == const.BUILDTIME: self.binlogpath = args.binlogpath - + + if self.testtype == const.ANDROIDINNERLOOP: + self.csprojpath = args.csprojpath + self.editsrc = args.editsrc + self.editdest = args.editdest + self.framework = args.framework + self.configuration = args.configuration + self.msbuildargs = args.msbuildargs + if self.testtype == const.DEVICESTARTUP: self.packagepath = args.packagepath self.packagename = args.packagename @@ -974,4 +992,53 @@ def run(self): if not (self.binlogpath and os.path.exists(os.path.join(const.TRACEDIR, self.binlogpath))): raise Exception("For build time measurements a valid binlog path must be provided.") self.traits.add_traits(overwrite=True, apptorun="app", startupmetric=const.BUILDTIME, tracename=self.binlogpath, scenarioname=self.scenarioname) - startup.parsetraces(self.traits) \ No newline at end of file + startup.parsetraces(self.traits) + + elif self.testtype == const.ANDROIDINNERLOOP: + import subprocess + import shutil + + if not self.csprojpath: + raise Exception("For Android inner loop measurements, --csproj-path must be provided.") + + scenarioprefix = self.scenarioname or "MAUI Android Deploy" + + os.makedirs(const.TRACEDIR, exist_ok=True) + first_binlog = os.path.join(const.TRACEDIR, 'first-deploy.binlog') + incremental_binlog = os.path.join(const.TRACEDIR, 'incremental-deploy.binlog') + + # Build the base MSBuild command + base_cmd = ['dotnet', 'build', self.csprojpath, '-t:Install'] + if self.configuration: + base_cmd.extend(['-c', self.configuration]) + if self.framework: + base_cmd.extend(['-f', self.framework]) + if self.msbuildargs: + for arg in self.msbuildargs.split(';'): + if arg.strip(): + base_cmd.append(arg.strip()) + + # Step 1: First full deploy + first_cmd = base_cmd + [f'-bl:{first_binlog}'] + getLogger().info("First deploy: %s" % ' '.join(first_cmd)) + subprocess.run(first_cmd, check=True) + + # Step 2: Edit one source file to simulate a developer change + if self.editsrc and self.editdest: + getLogger().info("Editing file: %s -> %s" % (self.editsrc, self.editdest)) + shutil.copy(self.editsrc, self.editdest) + else: + getLogger().warning("No edit-src/edit-dest specified; incremental deploy will be a no-change rebuild") + + # Step 3: Incremental deploy + incremental_cmd = base_cmd + [f'-bl:{incremental_binlog}'] + getLogger().info("Incremental deploy: %s" % ' '.join(incremental_cmd)) + subprocess.run(incremental_cmd, check=True) + + # Step 4: Parse both binlogs using AndroidInnerLoopParser + startup = StartupWrapper() + self.traits.add_traits(overwrite=True, apptorun="app", startupmetric=const.ANDROIDINNERLOOP, tracename='first-deploy.binlog', scenarioname=scenarioprefix + " - First Deploy") + startup.parsetraces(self.traits) + + self.traits.add_traits(overwrite=True, apptorun="app", startupmetric=const.ANDROIDINNERLOOP, tracename='incremental-deploy.binlog', scenarioname=scenarioprefix + " - Incremental Deploy") + startup.parsetraces(self.traits) diff --git a/src/tools/ScenarioMeasurement/Startup/Startup.cs b/src/tools/ScenarioMeasurement/Startup/Startup.cs index 3c8a180561b..1e6f6a73bd7 100644 --- a/src/tools/ScenarioMeasurement/Startup/Startup.cs +++ b/src/tools/ScenarioMeasurement/Startup/Startup.cs @@ -27,6 +27,7 @@ enum MetricType WinUIBlazor, TimeToMain2, BuildTime, + AndroidInnerLoop, } public class InnerLoopMarkerEventSource : EventSource @@ -291,6 +292,7 @@ static void checkArg(string arg, string name) MetricType.WinUIBlazor => new WinUIBlazorParser(), MetricType.TimeToMain2 => new TimeToMain2Parser(AddTestProcessEnvironmentVariable), MetricType.BuildTime => new BuildTimeParser(), + MetricType.AndroidInnerLoop => new AndroidInnerLoopParser(), _ => throw new ArgumentOutOfRangeException(), }; diff --git a/src/tools/ScenarioMeasurement/Util/Parsers/AndroidInnerLoopParser.cs b/src/tools/ScenarioMeasurement/Util/Parsers/AndroidInnerLoopParser.cs new file mode 100644 index 00000000000..f5d55d00021 --- /dev/null +++ b/src/tools/ScenarioMeasurement/Util/Parsers/AndroidInnerLoopParser.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Logging.StructuredLogger; +using StructuredLogViewer; +using Microsoft.Diagnostics.Tracing; +using Reporting; + +namespace ScenarioMeasurement; + +/// +/// Parses Android inner loop (build+deploy) target and task durations from a binary log file. +/// +public class AndroidInnerLoopParser : IParser +{ + public void EnableKernelProvider(ITraceSession kernel) { throw new NotImplementedException(); } + public void EnableUserProviders(ITraceSession user) { throw new NotImplementedException(); } + + public IEnumerable Parse(string binlogFile, string processName, IList pids, string commandLine) + { + var publishTimes = new List(); + + // Build tasks (compilation) + var cscTimes = new List(); + var xamlCTimes = new List(); + var generateJavaStubsTimes = new List(); + var linkAssembliesNoShrinkTimes = new List(); + var d8Times = new List(); + var javacTimes = new List(); + var generateTypeMappingsTimes = new List(); + var processAssembliesTimes = new List(); + var generateJavaCallableWrappersTimes = new List(); + var filterAssembliesTimes = new List(); + var waitForAppDetectionTimes = new List(); + var generateMainAndroidManifestTimes = new List(); + var resolveSdksTimes = new List(); + var generateNativeApplicationConfigSourcesTimes = new List(); + + // Deploy tasks + var fastDeployTimes = new List(); + var androidSignPackageTimes = new List(); + var androidApkSignerTimes = new List(); + var aapt2LinkTimes = new List(); + + // Build targets + var coreCompileTargetTimes = new List(); + var xamlCTargetTimes = new List(); + var generateJavaStubsTargetTimes = new List(); + var linkAssembliesNoShrinkTargetTimes = new List(); + var compileToDalvikTargetTimes = new List(); + var compileJavaTargetTimes = new List(); + + // Deploy targets + var signTargetTimes = new List(); + var uploadTargetTimes = new List(); + var deployApkTargetTimes = new List(); + var buildApkFastDevTargetTimes = new List(); + + if (File.Exists(binlogFile)) + { + var build = BinaryLog.ReadBuild(binlogFile); + BuildAnalyzer.AnalyzeBuild(build); + + foreach (var task in build.FindChildrenRecursive()) + { + var name = task.Name; + var s = task.Duration.TotalMilliseconds / 1000.0; + + if (name.Equals("Csc", StringComparison.OrdinalIgnoreCase)) + cscTimes.Add(s); + else if (name.Equals("XamlCTask", StringComparison.OrdinalIgnoreCase)) + xamlCTimes.Add(s); + else if (name.Equals("GenerateJavaStubs", StringComparison.OrdinalIgnoreCase)) + generateJavaStubsTimes.Add(s); + else if (name.Equals("LinkAssembliesNoShrink", StringComparison.OrdinalIgnoreCase)) + linkAssembliesNoShrinkTimes.Add(s); + else if (name.Equals("D8", StringComparison.OrdinalIgnoreCase)) + d8Times.Add(s); + else if (name.Equals("Javac", StringComparison.OrdinalIgnoreCase)) + javacTimes.Add(s); + else if (name.Equals("GenerateTypeMappings", StringComparison.OrdinalIgnoreCase)) + generateTypeMappingsTimes.Add(s); + else if (name.Equals("ProcessAssemblies", StringComparison.OrdinalIgnoreCase)) + processAssembliesTimes.Add(s); + else if (name.Equals("GenerateJavaCallableWrappers", StringComparison.OrdinalIgnoreCase)) + generateJavaCallableWrappersTimes.Add(s); + else if (name.Equals("FilterAssemblies", StringComparison.OrdinalIgnoreCase)) + filterAssembliesTimes.Add(s); + else if (name.Equals("WaitForAppDetection", StringComparison.OrdinalIgnoreCase)) + waitForAppDetectionTimes.Add(s); + else if (name.Equals("GenerateMainAndroidManifest", StringComparison.OrdinalIgnoreCase)) + generateMainAndroidManifestTimes.Add(s); + else if (name.Equals("ResolveSdks", StringComparison.OrdinalIgnoreCase)) + resolveSdksTimes.Add(s); + else if (name.Equals("GenerateNativeApplicationConfigSources", StringComparison.OrdinalIgnoreCase)) + generateNativeApplicationConfigSourcesTimes.Add(s); + else if (name.Equals("FastDeploy", StringComparison.OrdinalIgnoreCase)) + fastDeployTimes.Add(s); + else if (name.Equals("AndroidSignPackage", StringComparison.OrdinalIgnoreCase)) + androidSignPackageTimes.Add(s); + else if (name.Equals("AndroidApkSigner", StringComparison.OrdinalIgnoreCase)) + androidApkSignerTimes.Add(s); + else if (name.Equals("Aapt2Link", StringComparison.OrdinalIgnoreCase)) + aapt2LinkTimes.Add(s); + } + + foreach (var target in build.FindChildrenRecursive()) + { + var name = target.Name; + var s = target.Duration.TotalMilliseconds / 1000.0; + + if (name.Equals("CoreCompile", StringComparison.Ordinal)) + coreCompileTargetTimes.Add(s); + else if (name.Equals("XamlC", StringComparison.Ordinal)) + xamlCTargetTimes.Add(s); + else if (name.Equals("_GenerateJavaStubs", StringComparison.Ordinal)) + generateJavaStubsTargetTimes.Add(s); + else if (name.Equals("_LinkAssembliesNoShrink", StringComparison.Ordinal)) + linkAssembliesNoShrinkTargetTimes.Add(s); + else if (name.Equals("_CompileToDalvik", StringComparison.Ordinal)) + compileToDalvikTargetTimes.Add(s); + else if (name.Equals("_CompileJava", StringComparison.Ordinal)) + compileJavaTargetTimes.Add(s); + else if (name.Equals("_Sign", StringComparison.Ordinal)) + signTargetTimes.Add(s); + else if (name.Equals("_Upload", StringComparison.Ordinal)) + uploadTargetTimes.Add(s); + else if (name.Equals("_DeployApk", StringComparison.Ordinal)) + deployApkTargetTimes.Add(s); + else if (name.Equals("_BuildApkFastDev", StringComparison.Ordinal)) + buildApkFastDevTargetTimes.Add(s); + } + + publishTimes.Add(build.Duration.TotalMilliseconds / 1000.0); + } + + // Overall duration + if (publishTimes.Count > 0) + yield return new Counter { Name = "Publish Time", MetricName = "s", DefaultCounter = true, TopCounter = true, Results = publishTimes.ToArray() }; + + // Build task counters + if (cscTimes.Count > 0) + yield return new Counter { Name = "Csc Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = cscTimes.ToArray() }; + if (xamlCTimes.Count > 0) + yield return new Counter { Name = "XamlC Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = xamlCTimes.ToArray() }; + if (generateJavaStubsTimes.Count > 0) + yield return new Counter { Name = "GenerateJavaStubs Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = generateJavaStubsTimes.ToArray() }; + if (linkAssembliesNoShrinkTimes.Count > 0) + yield return new Counter { Name = "LinkAssembliesNoShrink Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = linkAssembliesNoShrinkTimes.ToArray() }; + if (d8Times.Count > 0) + yield return new Counter { Name = "D8 Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = d8Times.ToArray() }; + if (javacTimes.Count > 0) + yield return new Counter { Name = "Javac Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = javacTimes.ToArray() }; + if (generateTypeMappingsTimes.Count > 0) + yield return new Counter { Name = "GenerateTypeMappings Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = generateTypeMappingsTimes.ToArray() }; + if (processAssembliesTimes.Count > 0) + yield return new Counter { Name = "ProcessAssemblies Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = processAssembliesTimes.ToArray() }; + if (generateJavaCallableWrappersTimes.Count > 0) + yield return new Counter { Name = "GenerateJavaCallableWrappers Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = generateJavaCallableWrappersTimes.ToArray() }; + if (filterAssembliesTimes.Count > 0) + yield return new Counter { Name = "FilterAssemblies Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = filterAssembliesTimes.ToArray() }; + if (waitForAppDetectionTimes.Count > 0) + yield return new Counter { Name = "WaitForAppDetection Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = waitForAppDetectionTimes.ToArray() }; + if (generateMainAndroidManifestTimes.Count > 0) + yield return new Counter { Name = "GenerateMainAndroidManifest Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = generateMainAndroidManifestTimes.ToArray() }; + if (resolveSdksTimes.Count > 0) + yield return new Counter { Name = "ResolveSdks Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = resolveSdksTimes.ToArray() }; + if (generateNativeApplicationConfigSourcesTimes.Count > 0) + yield return new Counter { Name = "GenerateNativeApplicationConfigSources Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = generateNativeApplicationConfigSourcesTimes.ToArray() }; + + // Build target counters + if (coreCompileTargetTimes.Count > 0) + yield return new Counter { Name = "CoreCompile Target Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = coreCompileTargetTimes.ToArray() }; + if (xamlCTargetTimes.Count > 0) + yield return new Counter { Name = "XamlC Target Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = xamlCTargetTimes.ToArray() }; + if (generateJavaStubsTargetTimes.Count > 0) + yield return new Counter { Name = "_GenerateJavaStubs Target Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = generateJavaStubsTargetTimes.ToArray() }; + if (linkAssembliesNoShrinkTargetTimes.Count > 0) + yield return new Counter { Name = "_LinkAssembliesNoShrink Target Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = linkAssembliesNoShrinkTargetTimes.ToArray() }; + if (compileToDalvikTargetTimes.Count > 0) + yield return new Counter { Name = "_CompileToDalvik Target Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = compileToDalvikTargetTimes.ToArray() }; + if (compileJavaTargetTimes.Count > 0) + yield return new Counter { Name = "_CompileJava Target Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = compileJavaTargetTimes.ToArray() }; + + // Deploy target counters + if (signTargetTimes.Count > 0) + yield return new Counter { Name = "_Sign Target Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = signTargetTimes.ToArray() }; + if (uploadTargetTimes.Count > 0) + yield return new Counter { Name = "_Upload Target Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = uploadTargetTimes.ToArray() }; + if (deployApkTargetTimes.Count > 0) + yield return new Counter { Name = "_DeployApk Target Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = deployApkTargetTimes.ToArray() }; + if (buildApkFastDevTargetTimes.Count > 0) + yield return new Counter { Name = "_BuildApkFastDev Target Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = buildApkFastDevTargetTimes.ToArray() }; + + // Task-level counters (granular) + if (fastDeployTimes.Count > 0) + yield return new Counter { Name = "FastDeploy Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = fastDeployTimes.ToArray() }; + if (androidSignPackageTimes.Count > 0) + yield return new Counter { Name = "AndroidSignPackage Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = androidSignPackageTimes.ToArray() }; + if (androidApkSignerTimes.Count > 0) + yield return new Counter { Name = "AndroidApkSigner Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = androidApkSignerTimes.ToArray() }; + if (aapt2LinkTimes.Count > 0) + yield return new Counter { Name = "Aapt2Link Task Time", MetricName = "s", DefaultCounter = false, TopCounter = true, Results = aapt2LinkTimes.ToArray() }; + } +}