DEV Community

Cover image for What I Learned Reading Unity's BuildReport After Building 16 APKs
HanoStudio
HanoStudio

Posted on

What I Learned Reading Unity's BuildReport After Building 16 APKs

Build Analyzer series, article 2.

In the previous article, I built 16 Android APKs while changing one variable at a time. After that, I had the APK numbers. What I did not have yet was a good explanation for where those bytes came from.

When I finished the build-size experiment, I almost published the wrong number.

Unity handed me three different "build sizes," and for a moment I was not sure which one represented the file users actually download.

That confusion is why I wrote a small BuildReport parser. I already had the final APK deltas, but I wanted evidence from inside Unity's build output too.

It helped, but not in the clean way I expected.

The first surprise: there was no single build size

My first instinct was to look for "the build size" in Unity's BuildReport.

That turned out to be the wrong mental model.

In the final measured Android build, I had three different size numbers:

Metric Value
Final APK file size 17.10 MiB
BuildReport.summary.totalSize 143.54 MiB
Sum of BuildReport.packedAssets entries 5.60 MiB

At first glance that looks suspicious. How can the same build be 17 MiB, 143 MiB, and 5.6 MiB?

The answer is that they are not measuring the same thing.

  • APK bytes: the compressed artifact users download
  • BuildReport.summary.totalSize: Unity's reported build output size
  • Packed asset sum: serialized asset data inside packed build files

Once I separated those three, the rest of the experiment became much easier to reason about.

The smallest parser I needed

I did not start by building a polished UI. I only wanted a repeatable text report after every build.

Unity's IPostprocessBuildWithReport was enough for that.

using UnityEditor.Build;
using UnityEditor.Build.Reporting;

internal sealed class BuildAssetSizeReporter : IPostprocessBuildWithReport
{
    public int callbackOrder => 1000;

    public void OnPostprocessBuild(BuildReport report)
    {
        var rankedAssets = CollectAssetSizes(report);
        var packedAssetBytes = rankedAssets.Aggregate(
            0UL,
            (total, asset) => total + asset.Value);
        var artifactBytes = GetArtifactBytes(report.summary.outputPath);

        LogSummary(report, rankedAssets, packedAssetBytes, artifactBytes);
        WriteReports(report, rankedAssets, packedAssetBytes, artifactBytes);
    }
}
Enter fullscreen mode Exit fullscreen mode

The full source is here:

https://github.com/hellohanostudio/BuildAnalyzer/blob/main/Assets/BuildAnalyzer/Editor/BuildAssetSizeReporter.cs

The important part is not the hook itself. It is what I decided to record:

  1. The final APK file size from disk
  2. BuildReport.summary.totalSize
  3. A ranked list of packed asset entries

I wanted the report to make it hard for me to mix those up later.

Aggregating packed assets

BuildReport.packedAssets contains the packed files generated during the build. Each packed file contains PackedAssetInfo entries.

The fields I used were:

  • sourceAssetPath
  • sourceAssetGUID
  • packedSize

One thing I had to handle: the same source asset can show up more than once. Unity is reporting packed entries, not a neat "one source file, one row" table.

So I grouped by source path before sorting.

private static KeyValuePair<string, ulong>[] CollectAssetSizes(BuildReport report)
{
    var sizesByPath = new Dictionary<string, ulong>(StringComparer.Ordinal);

    foreach (var packedFile in report.packedAssets)
    {
        foreach (var asset in packedFile.contents)
        {
            var path = string.IsNullOrEmpty(asset.sourceAssetPath)
                ? $"<generated:{asset.sourceAssetGUID}>"
                : asset.sourceAssetPath;

            sizesByPath.TryGetValue(path, out var currentSize);
            sizesByPath[path] = currentSize + asset.packedSize;
        }
    }

    return sizesByPath
        .OrderByDescending(pair => pair.Value)
        .ThenBy(pair => pair.Key, StringComparer.Ordinal)
        .ToArray();
}
Enter fullscreen mode Exit fullscreen mode

This was enough to check the asset-shaped parts of the experiment:

  • Did the texture really shrink after ASTC?
  • Did the audio data shrink after Vorbis?
  • Did the Korean font subset remove real packed bytes?

For those questions, the parser was useful right away.

I still measured the APK directly

I did not use BuildReport.summary.totalSize as the APK size.

For the artifact number, I read the file on disk:

private static ulong? GetArtifactBytes(string outputPath)
{
    return File.Exists(outputPath)
        ? (ulong)new FileInfo(outputPath).Length
        : null;
}
Enter fullscreen mode Exit fullscreen mode

That produced a plain summary like this:

artifact_bytes=17928357
artifact_mib=17.0978
build_report_total_bytes=150514033
build_report_total_mib=143.5414
packed_asset_bytes=5873204
packed_asset_mib=5.6011
Enter fullscreen mode Exit fullscreen mode

This is not fancy, but that is the point. I wanted boring measurement code because boring code is harder to argue with.

The CSV had a small surprise

The parser also writes a ranked CSV. This is the top of Results/raw/step-07-dotween-runtime-assets.csv from the final experiment step:

rank,packed_bytes,packed_mib,source_asset_path
1,2796376,2.666832,"Built-in Texture2D: Splash Screen Unity Logo"
2,1871556,1.784855,"Assets/Experiment/Generated/ExperimentTexture.png"
3,656420,0.626011,"Resources/unity_builtin_extra"
4,477252,0.455143,"Assets/Experiment/Generated/ExperimentAudio.wav"
5,69510,0.066290,"Assets/Experiment/Generated/NotoSansKR-Regular.ttf"
Enter fullscreen mode Exit fullscreen mode

The biggest packed texture was not even my generated test texture. It was Unity's splash-screen logo.

That was a useful reminder. A ranked build report is not a list of "my files only". It includes built-in assets Unity packs into the player too.

The raw reports are here:

https://github.com/hellohanostudio/BuildAnalyzer/tree/main/Results/raw

The place where the parser failed

The parser explained textures, audio, and fonts pretty well.

Then I added DOTween.

DOTween Free increased the APK by 0.46 MiB. But the packed-asset CSV attributed only 192 bytes to the DOTween DLL path.

That was the moment the shape of the problem changed for me.

The parser was not "wrong". It was just looking through one narrow window. DOTween affected generated code and metadata, so most of the growth appeared in files such as libil2cpp.so and global-metadata.dat, not in BuildReport.packedAssets.

That was probably the most useful result of the whole parser exercise: it showed me exactly where the parser stopped being enough.

What I would use this for

After this experiment, I would use this kind of parser for asset questions:

  • Which texture is taking packed space?
  • Did an importer setting change actually reduce build data?
  • How much did a font contribute?
  • Did one source asset appear through multiple packed entries?

I would not use it alone for code-size questions:

  • How much did a plugin increase IL2CPP output?
  • Which assemblies affected native binary size?
  • Did managed stripping change generated code size?
  • Which files inside the APK changed?

For that, I need to inspect the final artifact too.

The rule I kept

The main thing I kept from this week is simple:

Do not say "build size" as if it is one number.

For every build, I now want these three values side by side:

  1. Final artifact bytes from the file on disk
  2. BuildReport.summary.totalSize
  3. Sum of BuildReport.packedAssets[].contents[].packedSize

Then I use the packed-asset ranking as evidence, not as the final truth.

That small separation prevented the biggest mistake I could have made in the experiment: confusing what Unity packed as assets with what users actually download.

Sources

Top comments (0)