Search Unity

Question APK/AAB built using Unity Command line have worse performance than building using Unity Editor

Discussion in 'Editor & General Support' started by cdr9042, Nov 4, 2022.

  1. cdr9042

    cdr9042

    Joined:
    Apr 22, 2018
    Posts:
    173
    I'm setting up CI so my build machine (Macbook running Monterey) automatically builds APK and AAB when I push to Gitlab.

    I install the build to an Android phone, the app runs normally but when it has to loads some Sprite Atlas, it takes an abnormally long time to load: 17 seconds to finish File.Read.

    unity build 1.jpg

    Then I install and play an APK that was built normally through Unity Editor (no command line) to compare, the issue does not happen, File.Read only takes 0.04ms

    unity build 2.jpg

    What's happening? Why do the builds from Command line running File.Read takes so long compared to building normally?

    This is my command:
    Code (CSharp):
    1. #!/usr/bin/env bash
    2.  
    3. set -e
    4. set -x
    5.  
    6. echo "Building for $BUILD_TARGET"
    7.  
    8. export BUILD_PATH=$UNITY_DIR/Builds/$BUILD_TARGET/
    9. mkdir -p $BUILD_PATH
    10.  
    11. ${UNITY_EXECUTABLE:-xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' unity-editor} \
    12.   -projectPath $UNITY_DIR \
    13.   -quit \
    14.   -batchmode \
    15.   -nographics \
    16.   -buildTarget $BUILD_TARGET \
    17.   -customBuildTarget $BUILD_TARGET \
    18.   -customBuildName $BUILD_NAME \
    19.   -customBuildPath $BUILD_PATH \
    20.   -executeMethod BuildCommand.PerformBuild \
    21.   -logFile /dev/stdout
    22.  
    23. echo "Build done"
    24.  
    My build command script:
    Code (CSharp):
    1.  
    2. using UnityEditor;
    3. using System.Linq;
    4. using System;
    5. using System.IO;
    6. static class BuildCommand
    7. {
    8.     private const string KEYSTORE_PASS = "KEYSTORE_PASS";
    9.     private const string KEY_ALIAS_PASS = "KEY_ALIAS_PASS";
    10.     private const string KEY_ALIAS_NAME = "KEY_ALIAS_NAME";
    11.     private const string KEYSTORE = "keystore.keystore";
    12.     private const string BUILD_OPTIONS_ENV_VAR = "BuildOptions";
    13.     private const string ANDROID_BUNDLE_VERSION_CODE = "VERSION_BUILD_VAR";
    14.     private const string ANDROID_APP_BUNDLE = "BUILD_APP_BUNDLE";
    15.     private const string SCRIPTING_BACKEND_ENV_VAR = "SCRIPTING_BACKEND";
    16.     private const string VERSION_NUMBER_VAR = "VERSION_NUMBER_VAR";
    17.     private const string VERSION_iOS = "VERSION_BUILD_VAR";
    18.     static string GetArgument(string name)
    19.     {
    20.         string[] args = Environment.GetCommandLineArgs();
    21.         for (int i = 0; i < args.Length; i++)
    22.         {
    23.             if (args[i].Contains(name))
    24.             {
    25.                 return args[i + 1];
    26.             }
    27.         }
    28.         return null;
    29.     }
    30.     static string[] GetEnabledScenes()
    31.     {
    32.         return (
    33.             from scene in EditorBuildSettings.scenes
    34.             where scene.enabled
    35.             where !string.IsNullOrEmpty(scene.path)
    36.             select scene.path
    37.         ).ToArray();
    38.     }
    39.     static BuildTarget GetBuildTarget()
    40.     {
    41.         string buildTargetName = GetArgument("customBuildTarget");
    42.         Console.WriteLine(":: Received customBuildTarget " + buildTargetName);
    43.         if (buildTargetName.ToLower() == "android")
    44.         {
    45. #if !UNITY_5_6_OR_NEWER
    46.             // https://issuetracker.unity3d.com/issues/buildoptions-dot-acceptexternalmodificationstoplayer-causes-unityexception-unknown-project-type-0
    47.             // Fixed in Unity 5.6.0
    48.             // side effect to fix android build system:
    49.             EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Internal;
    50. #endif
    51.         }
    52.         if (buildTargetName.TryConvertToEnum(out BuildTarget target))
    53.             return target;
    54.         Console.WriteLine($":: {nameof(buildTargetName)} \"{buildTargetName}\" not defined on enum {nameof(BuildTarget)}, using {nameof(BuildTarget.NoTarget)} enum to build");
    55.         return BuildTarget.NoTarget;
    56.     }
    57.     static string GetBuildPath()
    58.     {
    59.         string buildPath = GetArgument("customBuildPath");
    60.         Console.WriteLine(":: Received customBuildPath " + buildPath);
    61.         if (buildPath == "")
    62.         {
    63.             throw new Exception("customBuildPath argument is missing");
    64.         }
    65.         return buildPath;
    66.     }
    67.     static string GetBuildName()
    68.     {
    69.         string buildName = GetArgument("customBuildName");
    70.         Console.WriteLine(":: Received customBuildName " + buildName);
    71.         if (buildName == "")
    72.         {
    73.             throw new Exception("customBuildName argument is missing");
    74.         }
    75.         return buildName;
    76.     }
    77.     static string GetFixedBuildPath(BuildTarget buildTarget, string buildPath, string buildName)
    78.     {
    79.         if (buildTarget.ToString().ToLower().Contains("windows"))
    80.         {
    81.             buildName += ".exe";
    82.         }
    83.         else if (buildTarget == BuildTarget.Android)
    84.         {
    85. #if UNITY_2018_3_OR_NEWER
    86.             buildName += EditorUserBuildSettings.buildAppBundle ? ".aab" : ".apk";
    87. #else
    88.             buildName += ".apk";
    89. #endif
    90.         }
    91.         return buildPath + buildName;
    92.     }
    93.     static BuildOptions GetBuildOptions()
    94.     {
    95.         if (TryGetEnv(BUILD_OPTIONS_ENV_VAR, out string envVar))
    96.         {
    97.             string[] allOptionVars = envVar.Split(',');
    98.             BuildOptions allOptions = BuildOptions.None;
    99.             BuildOptions option;
    100.             string optionVar;
    101.             int length = allOptionVars.Length;
    102.             Console.WriteLine($":: Detecting {BUILD_OPTIONS_ENV_VAR} env var with {length} elements ({envVar})");
    103.             for (int i = 0; i < length; i++)
    104.             {
    105.                 optionVar = allOptionVars[i];
    106.                 if (optionVar.TryConvertToEnum(out option))
    107.                 {
    108.                     allOptions |= option;
    109.                 }
    110.                 else
    111.                 {
    112.                     Console.WriteLine($":: Cannot convert {optionVar} to {nameof(BuildOptions)} enum, skipping it.");
    113.                 }
    114.             }
    115.             return allOptions;
    116.         }
    117.         return BuildOptions.None;
    118.     }
    119.     // https://stackoverflow.com/questions/1082532/how-to-tryparse-for-enum-value
    120.     static bool TryConvertToEnum<TEnum>(this string strEnumValue, out TEnum value)
    121.     {
    122.         if (!Enum.IsDefined(typeof(TEnum), strEnumValue))
    123.         {
    124.             value = default;
    125.             return false;
    126.         }
    127.         value = (TEnum)Enum.Parse(typeof(TEnum), strEnumValue);
    128.         return true;
    129.     }
    130.     static bool TryGetEnv(string key, out string value)
    131.     {
    132.         value = Environment.GetEnvironmentVariable(key);
    133.         return !string.IsNullOrEmpty(value);
    134.     }
    135.     static void SetScriptingBackendFromEnv(BuildTarget platform)
    136.     {
    137.         var targetGroup = BuildPipeline.GetBuildTargetGroup(platform);
    138.         if (TryGetEnv(SCRIPTING_BACKEND_ENV_VAR, out string scriptingBackend))
    139.         {
    140.             if (scriptingBackend.TryConvertToEnum(out ScriptingImplementation backend))
    141.             {
    142.                 Console.WriteLine($":: Setting ScriptingBackend to {backend}");
    143.                 PlayerSettings.SetScriptingBackend(targetGroup, backend);
    144.             }
    145.             else
    146.             {
    147.                 string possibleValues = string.Join(", ", Enum.GetValues(typeof(ScriptingImplementation)).Cast<ScriptingImplementation>());
    148.                 throw new Exception($"Could not find '{scriptingBackend}' in ScriptingImplementation enum. Possible values are: {possibleValues}");
    149.             }
    150.         }
    151.         else
    152.         {
    153.             var defaultBackend = PlayerSettings.GetDefaultScriptingBackend(targetGroup);
    154.             Console.WriteLine($":: Using project's configured ScriptingBackend (should be {defaultBackend} for targetGroup {targetGroup}");
    155.         }
    156.     }
    157.     static void PerformBuild()
    158.     {
    159.         var buildTarget = GetBuildTarget();
    160.         Console.WriteLine(":: Performing build");
    161.         if (TryGetEnv(VERSION_NUMBER_VAR, out var bundleVersionNumber))
    162.         {
    163.             if (buildTarget == BuildTarget.iOS)
    164.             {
    165.                 bundleVersionNumber = GetIosVersion();
    166.             }
    167.             Console.WriteLine($":: Setting bundleVersionNumber to '{bundleVersionNumber}' (Length: {bundleVersionNumber.Length})");
    168.             PlayerSettings.bundleVersion = bundleVersionNumber;
    169.         }
    170.         if (buildTarget == BuildTarget.Android)
    171.         {
    172.             HandleAndroidAppBundle();
    173.             HandleAndroidBundleVersionCode();
    174.             HandleAndroidKeystore();
    175.         }
    176.         var buildPath = GetBuildPath();
    177.         var buildName = GetBuildName();
    178.         var buildOptions = GetBuildOptions();
    179.         var fixedBuildPath = GetFixedBuildPath(buildTarget, buildPath, buildName);
    180.         SetScriptingBackendFromEnv(buildTarget);
    181.         var buildReport = BuildPipeline.BuildPlayer(GetEnabledScenes(), fixedBuildPath, buildTarget, buildOptions);
    182.         if (buildReport.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded)
    183.             throw new Exception($"Build ended with {buildReport.summary.result} status");
    184.         Console.WriteLine(":: Done with build");
    185.         EditorApplication.Exit(0);
    186.     }
    187.     private static void HandleAndroidAppBundle()
    188.     {
    189.         if (TryGetEnv(ANDROID_APP_BUNDLE, out string value))
    190.         {
    191. #if UNITY_2018_3_OR_NEWER
    192.             if (bool.TryParse(value, out bool buildAppBundle))
    193.             {
    194.                 EditorUserBuildSettings.buildAppBundle = buildAppBundle;
    195.                 Console.WriteLine($":: {ANDROID_APP_BUNDLE} env var detected, set buildAppBundle to {value}.");
    196.             }
    197.             else
    198.             {
    199.                 Console.WriteLine($":: {ANDROID_APP_BUNDLE} env var detected but the value \"{value}\" is not a boolean.");
    200.             }
    201. #else
    202.             Console.WriteLine($":: {ANDROID_APP_BUNDLE} env var detected but does not work with lower Unity version than 2018.3");
    203. #endif
    204.         }
    205.     }
    206.     private static void HandleAndroidBundleVersionCode()
    207.     {
    208.         if (TryGetEnv(ANDROID_BUNDLE_VERSION_CODE, out string value))
    209.         {
    210.             if (int.TryParse(value, out int version))
    211.             {
    212.                 PlayerSettings.Android.bundleVersionCode = version;
    213.                 Console.WriteLine($":: {ANDROID_BUNDLE_VERSION_CODE} env var detected, set the bundle version code to {value}.");
    214.             }
    215.             else
    216.                 Console.WriteLine($":: {ANDROID_BUNDLE_VERSION_CODE} env var detected but the version value \"{value}\" is not an integer.");
    217.         }
    218.     }
    219.     private static string GetIosVersion()
    220.     {
    221.         if (TryGetEnv(VERSION_iOS, out string value))
    222.         {
    223.             if (int.TryParse(value, out int version))
    224.             {
    225.                 Console.WriteLine($":: {VERSION_iOS} env var detected, set the version to {value}.");
    226.                 return version.ToString();
    227.             }
    228.             else
    229.                 Console.WriteLine($":: {VERSION_iOS} env var detected but the version value \"{value}\" is not an integer.");
    230.         }
    231.         throw new ArgumentNullException(nameof(value), $":: Error finding {VERSION_iOS} env var");
    232.     }
    233.     private static void HandleAndroidKeystore()
    234.     {
    235. #if UNITY_2019_1_OR_NEWER
    236.         PlayerSettings.Android.useCustomKeystore = false;
    237. #endif
    238.         if (!File.Exists(KEYSTORE))
    239.         {
    240.             Console.WriteLine($":: {KEYSTORE} not found, skipping setup, using Unity's default keystore");
    241.             return;
    242.         }
    243.         PlayerSettings.Android.keystoreName = KEYSTORE;
    244.         string keystorePass;
    245.         string keystoreAliasPass;
    246.         if (TryGetEnv(KEY_ALIAS_NAME, out string keyaliasName))
    247.         {
    248.             PlayerSettings.Android.keyaliasName = keyaliasName;
    249.             Console.WriteLine($":: using ${KEY_ALIAS_NAME} env var on PlayerSettings");
    250.         }
    251.         else
    252.         {
    253.             Console.WriteLine($":: ${KEY_ALIAS_NAME} env var not set, using Project's PlayerSettings");
    254.         }
    255.         if (!TryGetEnv(KEYSTORE_PASS, out keystorePass))
    256.         {
    257.             Console.WriteLine($":: ${KEYSTORE_PASS} env var not set, skipping setup, using Unity's default keystore");
    258.             return;
    259.         }
    260.         if (!TryGetEnv(KEY_ALIAS_PASS, out keystoreAliasPass))
    261.         {
    262.             Console.WriteLine($":: ${KEY_ALIAS_PASS} env var not set, skipping setup, using Unity's default keystore");
    263.             return;
    264.         }
    265. #if UNITY_2019_1_OR_NEWER
    266.         PlayerSettings.Android.useCustomKeystore = true;
    267. #endif
    268.         PlayerSettings.Android.keystorePass = keystorePass;
    269.         PlayerSettings.Android.keyaliasPass = keystoreAliasPass;
    270.     }
    271. }
    272.  
    273.  
     
    Last edited: Nov 4, 2022
  2. antonk-unity

    antonk-unity

    Unity Technologies

    Joined:
    Oct 4, 2016
    Posts:
    50
    Hey, editor build logs and logcat from the device will be more helpful.

    As pure speculation, you could try building without "-nographics". It might be due to properties for null gfx device it wasn't able to save textures in the correct format and they're being converted on load.
     
  3. cdr9042

    cdr9042

    Joined:
    Apr 22, 2018
    Posts:
    173
    I've found out it's due to building with Command line used Compression method ZIP by default (unlike normal Unity Editor which use LZ4 by default), which lead to File.Read taking so long.
    A fix is too add the build option Compress with LZ4 to the build command script:

    Code (CSharp):
    1. BuildOptions allOptions = BuildOptions.None;
    2. allOptions |= BuildOptions.CompressWithLz4;
    new GetBuildOptions() function:

    Code (CSharp):
    1. static BuildOptions GetBuildOptions()
    2.     {
    3.         if (TryGetEnv(BUILD_OPTIONS_ENV_VAR, out string envVar))
    4.         {
    5.             string[] allOptionVars = envVar.Split(',');
    6.             BuildOptions allOptions = BuildOptions.None;
    7.             allOptions |= BuildOptions.CompressWithLz4;
    8.             BuildOptions option;
    9.             string optionVar;
    10.             int length = allOptionVars.Length;
    11.  
    12.             Console.WriteLine($":: Detecting {BUILD_OPTIONS_ENV_VAR} env var with {length} elements ({envVar})");
    13.  
    14.             for (int i = 0; i < length; i++)
    15.             {
    16.                 optionVar = allOptionVars[i];
    17.  
    18.                 if (optionVar.TryConvertToEnum(out option))
    19.                 {
    20.                     allOptions |= option;
    21.                 }
    22.                 else
    23.                 {
    24.                     Console.WriteLine($":: Cannot convert {optionVar} to {nameof(BuildOptions)} enum, skipping it.");
    25.                 }
    26.             }
    27.  
    28.             return allOptions;
    29.         }
    30.  
    31.         return BuildOptions.None;
    32.     }
     
    antonk-unity likes this.