diff --git a/Analyzer/Properties/Resources.Designer.cs b/Analyzer/Properties/Resources.Designer.cs
index 57f5822..d9a101a 100644
--- a/Analyzer/Properties/Resources.Designer.cs
+++ b/Analyzer/Properties/Resources.Designer.cs
@@ -359,5 +359,11 @@ internal static string PackedAssets {
return ResourceManager.GetString("PackedAssets", resourceCulture);
}
}
+
+ internal static string ContentSummary {
+ get {
+ return ResourceManager.GetString("ContentSummary", resourceCulture);
+ }
+ }
}
}
diff --git a/Analyzer/Properties/Resources.resx b/Analyzer/Properties/Resources.resx
index 24d30da..0a616aa 100644
--- a/Analyzer/Properties/Resources.resx
+++ b/Analyzer/Properties/Resources.resx
@@ -268,4 +268,7 @@
..\Resources\PackedAssets.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8
+
+ ..\Resources\ContentSummary.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8
+
diff --git a/Analyzer/Resources/BuildReport.sql b/Analyzer/Resources/BuildReport.sql
index d25bc72..79d216e 100644
--- a/Analyzer/Resources/BuildReport.sql
+++ b/Analyzer/Resources/BuildReport.sql
@@ -15,6 +15,14 @@ CREATE TABLE IF NOT EXISTS build_reports(
asset_bundle_options INTEGER,
output_path TEXT,
crc INTEGER,
+ -- Fields added in Unity 6.6; left NULL when analyzing reports from older versions.
+ build_name TEXT,
+ build_content_options INTEGER,
+ build_session_guid TEXT,
+ build_manifest_hash TEXT,
+ build_profile_path TEXT,
+ build_profile_guid TEXT,
+ data_path TEXT,
PRIMARY KEY (id)
);
diff --git a/Analyzer/Resources/ContentSummary.sql b/Analyzer/Resources/ContentSummary.sql
new file mode 100644
index 0000000..9c684f0
--- /dev/null
+++ b/Analyzer/Resources/ContentSummary.sql
@@ -0,0 +1,72 @@
+CREATE TABLE IF NOT EXISTS build_report_content_summary(
+ id INTEGER,
+ serialized_file_size INTEGER,
+ reused_serialized_file_size INTEGER,
+ resource_data_size INTEGER,
+ header_size INTEGER,
+ serialized_file_count INTEGER,
+ reused_serialized_file_count INTEGER,
+ resource_file_count INTEGER,
+ object_count INTEGER,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE IF NOT EXISTS build_report_content_type_stats(
+ content_summary_id INTEGER NOT NULL,
+ type INTEGER NOT NULL,
+ size INTEGER,
+ object_count INTEGER,
+ resource_count INTEGER,
+ PRIMARY KEY (content_summary_id, type),
+ FOREIGN KEY (content_summary_id) REFERENCES build_report_content_summary(id)
+);
+
+CREATE TABLE IF NOT EXISTS build_report_content_asset_stats(
+ content_summary_id INTEGER NOT NULL,
+ source_asset_guid TEXT,
+ source_asset_path TEXT,
+ size INTEGER,
+ object_count INTEGER,
+ resource_count INTEGER,
+ FOREIGN KEY (content_summary_id) REFERENCES build_report_content_summary(id)
+);
+
+-- Cross-build statistics with the owning BuildReport resolved. build_report_id is the id of the
+-- BuildReport object (type 1125) in the same serialized file as the ContentSummary; it is not
+-- stored on the table, so this view computes it the same way build_report_packed_assets_view does.
+CREATE VIEW build_report_content_summary_view AS
+SELECT
+ cs.id AS content_summary_id,
+ br_obj.id AS build_report_id,
+ sf.name AS build_report_filename,
+ cs.serialized_file_size,
+ cs.reused_serialized_file_size,
+ cs.resource_data_size,
+ cs.header_size,
+ cs.serialized_file_count,
+ cs.reused_serialized_file_count,
+ cs.resource_file_count,
+ cs.object_count
+FROM build_report_content_summary cs
+INNER JOIN objects o ON cs.id = o.id
+INNER JOIN serialized_files sf ON o.serialized_file = sf.id
+LEFT JOIN objects br_obj ON o.serialized_file = br_obj.serialized_file AND br_obj.type = 1125;
+
+-- Per-type statistics with the type name resolved (from TypeIdRegistry or TypeTree analysis) and
+-- the owning BuildReport, so a single build's type breakdown can be selected by build_report_id.
+CREATE VIEW build_report_content_type_stats_view AS
+SELECT
+ cs.id AS content_summary_id,
+ br_obj.id AS build_report_id,
+ sf.name AS build_report_filename,
+ cts.type,
+ COALESCE(t.name, CAST(cts.type AS TEXT)) AS type_name,
+ cts.size,
+ cts.object_count,
+ cts.resource_count
+FROM build_report_content_type_stats cts
+INNER JOIN build_report_content_summary cs ON cts.content_summary_id = cs.id
+INNER JOIN objects o ON cs.id = o.id
+INNER JOIN serialized_files sf ON o.serialized_file = sf.id
+LEFT JOIN objects br_obj ON o.serialized_file = br_obj.serialized_file AND br_obj.type = 1125
+LEFT JOIN types t ON cts.type = t.id;
diff --git a/Analyzer/SQLite/Handlers/BuildReportHandler.cs b/Analyzer/SQLite/Handlers/BuildReportHandler.cs
index e4a4d11..c50b10f 100644
--- a/Analyzer/SQLite/Handlers/BuildReportHandler.cs
+++ b/Analyzer/SQLite/Handlers/BuildReportHandler.cs
@@ -24,11 +24,13 @@ public void Init(SqliteConnection db)
m_InsertCommand.CommandText = @"INSERT INTO build_reports(
id, build_type, build_result, platform_name, subtarget, start_time, end_time, total_time_seconds,
total_size, build_guid, total_errors, total_warnings, options, asset_bundle_options,
- output_path, crc
+ output_path, crc, build_name, build_content_options, build_session_guid, build_manifest_hash,
+ build_profile_path, build_profile_guid, data_path
) VALUES(
@id, @build_type, @build_result, @platform_name, @subtarget, @start_time, @end_time, @total_time_seconds,
@total_size, @build_guid, @total_errors, @total_warnings, @options, @asset_bundle_options,
- @output_path, @crc
+ @output_path, @crc, @build_name, @build_content_options, @build_session_guid, @build_manifest_hash,
+ @build_profile_path, @build_profile_guid, @data_path
)";
m_InsertCommand.Parameters.Add("@id", SqliteType.Integer);
@@ -47,6 +49,13 @@ public void Init(SqliteConnection db)
m_InsertCommand.Parameters.Add("@asset_bundle_options", SqliteType.Integer);
m_InsertCommand.Parameters.Add("@output_path", SqliteType.Text);
m_InsertCommand.Parameters.Add("@crc", SqliteType.Integer);
+ m_InsertCommand.Parameters.Add("@build_name", SqliteType.Text);
+ m_InsertCommand.Parameters.Add("@build_content_options", SqliteType.Integer);
+ m_InsertCommand.Parameters.Add("@build_session_guid", SqliteType.Text);
+ m_InsertCommand.Parameters.Add("@build_manifest_hash", SqliteType.Text);
+ m_InsertCommand.Parameters.Add("@build_profile_path", SqliteType.Text);
+ m_InsertCommand.Parameters.Add("@build_profile_guid", SqliteType.Text);
+ m_InsertCommand.Parameters.Add("@data_path", SqliteType.Text);
m_InsertFileCommand = db.CreateCommand();
m_InsertFileCommand.CommandText = @"INSERT INTO build_report_files(
@@ -110,6 +119,13 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s
m_InsertCommand.Parameters["@asset_bundle_options"].Value = buildReport.AssetBundleOptions;
m_InsertCommand.Parameters["@output_path"].Value = buildReport.OutputPath;
m_InsertCommand.Parameters["@crc"].Value = buildReport.Crc;
+ m_InsertCommand.Parameters["@build_name"].Value = (object)buildReport.BuildName ?? DBNull.Value;
+ m_InsertCommand.Parameters["@build_content_options"].Value = (object)buildReport.BuildContentOptions ?? DBNull.Value;
+ m_InsertCommand.Parameters["@build_session_guid"].Value = (object)buildReport.BuildSessionGuid ?? DBNull.Value;
+ m_InsertCommand.Parameters["@build_manifest_hash"].Value = (object)buildReport.BuildManifestHash ?? DBNull.Value;
+ m_InsertCommand.Parameters["@build_profile_path"].Value = (object)buildReport.BuildProfilePath ?? DBNull.Value;
+ m_InsertCommand.Parameters["@build_profile_guid"].Value = (object)buildReport.BuildProfileGuid ?? DBNull.Value;
+ m_InsertCommand.Parameters["@data_path"].Value = (object)buildReport.DataPath ?? DBNull.Value;
m_InsertCommand.ExecuteNonQuery();
diff --git a/Analyzer/SQLite/Handlers/ContentSummaryHandler.cs b/Analyzer/SQLite/Handlers/ContentSummaryHandler.cs
new file mode 100644
index 0000000..2229e2f
--- /dev/null
+++ b/Analyzer/SQLite/Handlers/ContentSummaryHandler.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Data.Sqlite;
+using UnityDataTools.Analyzer.SerializedObjects;
+using UnityDataTools.FileSystem;
+using UnityDataTools.FileSystem.TypeTreeReaders;
+
+namespace UnityDataTools.Analyzer.SQLite.Handlers;
+
+// Writes the ContentSummary object (Unity 6.6+). There is at most one per serialized file, so this
+// is simpler than PackedAssetsHandler, but it follows the same on-demand schema pattern: the
+// build_report_content_* tables and views are created lazily on the first ContentSummary object.
+public class ContentSummaryHandler : ISQLiteHandler
+{
+ private SqliteConnection m_Database;
+ private bool m_SchemaCreated;
+ private SqliteCommand m_InsertSummaryCommand;
+ private SqliteCommand m_InsertTypeStatCommand;
+ private SqliteCommand m_InsertAssetStatCommand;
+ private SqliteCommand m_InsertTypeCommand;
+
+ // Type ids already added to the types table, to skip redundant INSERTs across all reports.
+ private HashSet m_InsertedTypes = new();
+
+ public void Init(SqliteConnection db)
+ {
+ m_Database = db;
+
+ m_InsertSummaryCommand = db.CreateCommand();
+ m_InsertSummaryCommand.CommandText = @"INSERT INTO build_report_content_summary(
+ id, serialized_file_size, reused_serialized_file_size, resource_data_size, header_size,
+ serialized_file_count, reused_serialized_file_count, resource_file_count, object_count
+ ) VALUES(
+ @id, @serialized_file_size, @reused_serialized_file_size, @resource_data_size, @header_size,
+ @serialized_file_count, @reused_serialized_file_count, @resource_file_count, @object_count
+ )";
+ m_InsertSummaryCommand.Parameters.Add("@id", SqliteType.Integer);
+ m_InsertSummaryCommand.Parameters.Add("@serialized_file_size", SqliteType.Integer);
+ m_InsertSummaryCommand.Parameters.Add("@reused_serialized_file_size", SqliteType.Integer);
+ m_InsertSummaryCommand.Parameters.Add("@resource_data_size", SqliteType.Integer);
+ m_InsertSummaryCommand.Parameters.Add("@header_size", SqliteType.Integer);
+ m_InsertSummaryCommand.Parameters.Add("@serialized_file_count", SqliteType.Integer);
+ m_InsertSummaryCommand.Parameters.Add("@reused_serialized_file_count", SqliteType.Integer);
+ m_InsertSummaryCommand.Parameters.Add("@resource_file_count", SqliteType.Integer);
+ m_InsertSummaryCommand.Parameters.Add("@object_count", SqliteType.Integer);
+
+ m_InsertTypeStatCommand = db.CreateCommand();
+ m_InsertTypeStatCommand.CommandText = @"INSERT INTO build_report_content_type_stats(
+ content_summary_id, type, size, object_count, resource_count
+ ) VALUES(
+ @content_summary_id, @type, @size, @object_count, @resource_count
+ )";
+ m_InsertTypeStatCommand.Parameters.Add("@content_summary_id", SqliteType.Integer);
+ m_InsertTypeStatCommand.Parameters.Add("@type", SqliteType.Integer);
+ m_InsertTypeStatCommand.Parameters.Add("@size", SqliteType.Integer);
+ m_InsertTypeStatCommand.Parameters.Add("@object_count", SqliteType.Integer);
+ m_InsertTypeStatCommand.Parameters.Add("@resource_count", SqliteType.Integer);
+
+ m_InsertAssetStatCommand = db.CreateCommand();
+ m_InsertAssetStatCommand.CommandText = @"INSERT INTO build_report_content_asset_stats(
+ content_summary_id, source_asset_guid, source_asset_path, size, object_count, resource_count
+ ) VALUES(
+ @content_summary_id, @source_asset_guid, @source_asset_path, @size, @object_count, @resource_count
+ )";
+ m_InsertAssetStatCommand.Parameters.Add("@content_summary_id", SqliteType.Integer);
+ m_InsertAssetStatCommand.Parameters.Add("@source_asset_guid", SqliteType.Text);
+ m_InsertAssetStatCommand.Parameters.Add("@source_asset_path", SqliteType.Text);
+ m_InsertAssetStatCommand.Parameters.Add("@size", SqliteType.Integer);
+ m_InsertAssetStatCommand.Parameters.Add("@object_count", SqliteType.Integer);
+ m_InsertAssetStatCommand.Parameters.Add("@resource_count", SqliteType.Integer);
+
+ // Populate the shared types table (INSERT OR IGNORE) so the type-stats view can show type
+ // names even when the build output and its TypeTrees are not analyzed alongside the report.
+ m_InsertTypeCommand = db.CreateCommand();
+ m_InsertTypeCommand.CommandText = "INSERT OR IGNORE INTO types(id, name) VALUES(@id, @name)";
+ m_InsertTypeCommand.Parameters.Add("@id", SqliteType.Integer);
+ m_InsertTypeCommand.Parameters.Add("@name", SqliteType.Text);
+ }
+
+ private void EnsureSchema(SqliteTransaction transaction)
+ {
+ if (m_SchemaCreated)
+ return;
+
+ using var command = m_Database.CreateCommand();
+ command.Transaction = transaction;
+ command.CommandText = Properties.Resources.ContentSummary ?? throw new InvalidOperationException("ContentSummary resource not found");
+ command.ExecuteNonQuery();
+
+ m_SchemaCreated = true;
+ }
+
+ public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize)
+ {
+ EnsureSchema(ctx.Transaction);
+
+ var contentSummary = ContentSummary.Read(reader);
+
+ m_InsertSummaryCommand.Transaction = ctx.Transaction;
+ m_InsertSummaryCommand.Parameters["@id"].Value = objectId;
+ m_InsertSummaryCommand.Parameters["@serialized_file_size"].Value = (long)contentSummary.SerializedFileSize;
+ m_InsertSummaryCommand.Parameters["@reused_serialized_file_size"].Value = (long)contentSummary.ReusedSerializedFileSize;
+ m_InsertSummaryCommand.Parameters["@resource_data_size"].Value = (long)contentSummary.ResourceDataSize;
+ m_InsertSummaryCommand.Parameters["@header_size"].Value = (long)contentSummary.HeaderSize;
+ m_InsertSummaryCommand.Parameters["@serialized_file_count"].Value = contentSummary.SerializedFileCount;
+ m_InsertSummaryCommand.Parameters["@reused_serialized_file_count"].Value = contentSummary.ReusedSerializedFileCount;
+ m_InsertSummaryCommand.Parameters["@resource_file_count"].Value = contentSummary.ResourceFileCount;
+ m_InsertSummaryCommand.Parameters["@object_count"].Value = contentSummary.ObjectCount;
+ m_InsertSummaryCommand.ExecuteNonQuery();
+
+ foreach (var typeStat in contentSummary.TypeStats)
+ {
+ if (m_InsertedTypes.Add(typeStat.Type) &&
+ TypeIdRegistry.TryGetTypeName(typeStat.Type, out var typeName))
+ {
+ m_InsertTypeCommand.Transaction = ctx.Transaction;
+ m_InsertTypeCommand.Parameters["@id"].Value = typeStat.Type;
+ m_InsertTypeCommand.Parameters["@name"].Value = typeName;
+ m_InsertTypeCommand.ExecuteNonQuery();
+ }
+
+ m_InsertTypeStatCommand.Transaction = ctx.Transaction;
+ m_InsertTypeStatCommand.Parameters["@content_summary_id"].Value = objectId;
+ m_InsertTypeStatCommand.Parameters["@type"].Value = typeStat.Type;
+ m_InsertTypeStatCommand.Parameters["@size"].Value = (long)typeStat.Size;
+ m_InsertTypeStatCommand.Parameters["@object_count"].Value = typeStat.ObjectCount;
+ m_InsertTypeStatCommand.Parameters["@resource_count"].Value = typeStat.ResourceCount;
+ m_InsertTypeStatCommand.ExecuteNonQuery();
+ }
+
+ foreach (var assetStat in contentSummary.AssetStats)
+ {
+ m_InsertAssetStatCommand.Transaction = ctx.Transaction;
+ m_InsertAssetStatCommand.Parameters["@content_summary_id"].Value = objectId;
+ m_InsertAssetStatCommand.Parameters["@source_asset_guid"].Value = assetStat.SourceAssetGUID;
+ m_InsertAssetStatCommand.Parameters["@source_asset_path"].Value = assetStat.SourceAssetPath;
+ m_InsertAssetStatCommand.Parameters["@size"].Value = (long)assetStat.Size;
+ m_InsertAssetStatCommand.Parameters["@object_count"].Value = assetStat.ObjectCount;
+ m_InsertAssetStatCommand.Parameters["@resource_count"].Value = assetStat.ResourceCount;
+ m_InsertAssetStatCommand.ExecuteNonQuery();
+ }
+
+ streamDataSize = 0;
+ name = string.Empty;
+ }
+
+ public void Finalize(SqliteConnection db)
+ {
+ }
+
+ void IDisposable.Dispose()
+ {
+ m_InsertSummaryCommand?.Dispose();
+ m_InsertTypeStatCommand?.Dispose();
+ m_InsertAssetStatCommand?.Dispose();
+ m_InsertTypeCommand?.Dispose();
+ }
+}
diff --git a/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs b/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs
index 142de79..759701c 100644
--- a/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs
+++ b/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs
@@ -79,6 +79,7 @@ public class SerializedFileSQLiteWriter : IDisposable
{ "MonoScript", new MonoScriptHandler() },
{ "BuildReport", new BuildReportHandler() },
{ "PackedAssets", new PackedAssetsHandler() },
+ { "ContentSummary", new ContentSummaryHandler() },
};
// serialized files
diff --git a/Analyzer/SerializedObjects/BuildReport.cs b/Analyzer/SerializedObjects/BuildReport.cs
index 7113bf9..1b2b865 100644
--- a/Analyzer/SerializedObjects/BuildReport.cs
+++ b/Analyzer/SerializedObjects/BuildReport.cs
@@ -24,6 +24,14 @@ public class BuildReport
public int TotalWarnings { get; init; }
public int BuildType { get; init; }
public string BuildResult { get; init; }
+ // Fields added in Unity 6.6; null when analyzing reports from older versions.
+ public string BuildName { get; init; }
+ public int? BuildContentOptions { get; init; }
+ public string BuildSessionGuid { get; init; }
+ public string BuildManifestHash { get; init; }
+ public string BuildProfilePath { get; init; }
+ public string BuildProfileGuid { get; init; }
+ public string DataPath { get; init; }
public List Files { get; init; }
public FileListArchiveHelper fileListArchiveHelper { get; init; }
@@ -69,6 +77,13 @@ public static BuildReport Read(RandomAccessReader reader)
return new BuildReport()
{
+ BuildName = summary.HasChild("buildName") ? summary["buildName"].GetValue() : null,
+ BuildContentOptions = summary.HasChild("buildContentOptions") ? summary["buildContentOptions"].GetValue() : null,
+ BuildSessionGuid = ReadOptionalGuid(summary, "buildSessionGUID"),
+ BuildManifestHash = ReadOptionalHash128(summary, "buildManifestHash"),
+ BuildProfilePath = summary.HasChild("buildProfilePath") ? summary["buildProfilePath"].GetValue() : null,
+ BuildProfileGuid = ReadOptionalGuid(summary, "buildProfileGuid"),
+ DataPath = summary.HasChild("dataPath") ? summary["dataPath"].GetValue() : null,
Name = reader["m_Name"].GetValue(),
BuildGuid = guidString,
PlatformName = summary["platformName"].GetValue(),
@@ -114,6 +129,45 @@ static void TrimCommonPathPrefix(List files)
}
}
+ // Reads a Unity GUID (4 uints) if present, returning null when the field is absent (older
+ // Unity) or a default all-zero GUID (e.g. no build profile was active).
+ static string ReadOptionalGuid(RandomAccessReader summary, string fieldName)
+ {
+ if (!summary.HasChild(fieldName))
+ return null;
+
+ var guidData = summary[fieldName];
+ var guid0 = guidData["data[0]"].GetValue();
+ var guid1 = guidData["data[1]"].GetValue();
+ var guid2 = guidData["data[2]"].GetValue();
+ var guid3 = guidData["data[3]"].GetValue();
+
+ if ((guid0 | guid1 | guid2 | guid3) == 0)
+ return null;
+
+ return GuidHelper.FormatUnityGuid(guid0, guid1, guid2, guid3);
+ }
+
+ // Reads a Unity Hash128 (16 bytes) if present, returning null when the field is absent (older
+ // Unity) or an all-zero hash (e.g. non-ContentDirectory builds have no manifest hash).
+ static string ReadOptionalHash128(RandomAccessReader summary, string fieldName)
+ {
+ if (!summary.HasChild(fieldName))
+ return null;
+
+ var hashData = summary[fieldName];
+ var bytes = new byte[16];
+ var allZero = true;
+ for (var i = 0; i < 16; i++)
+ {
+ bytes[i] = hashData[$"bytes[{i}]"].GetValue();
+ if (bytes[i] != 0)
+ allZero = false;
+ }
+
+ return allZero ? null : GuidHelper.FormatUnityHash128(bytes);
+ }
+
public static string GetBuildTypeString(int buildType)
{
return buildType switch
diff --git a/Analyzer/SerializedObjects/ContentSummary.cs b/Analyzer/SerializedObjects/ContentSummary.cs
new file mode 100644
index 0000000..f4226fc
--- /dev/null
+++ b/Analyzer/SerializedObjects/ContentSummary.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+using UnityDataTools.BinaryFormat;
+using UnityDataTools.FileSystem.TypeTreeReaders;
+
+namespace UnityDataTools.Analyzer.SerializedObjects;
+
+// A high level summary of the content included in a build, introduced in Unity 6.6. It carries
+// cross-build totals plus per-type and per-source-asset breakdowns. See
+// https://docs.unity3d.com/6000.6/Documentation/ScriptReference/Build.Reporting.ContentSummary.html
+public class ContentSummary
+{
+ public ulong SerializedFileSize { get; init; }
+ public ulong ReusedSerializedFileSize { get; init; }
+ public ulong ResourceDataSize { get; init; }
+ public ulong HeaderSize { get; init; }
+ public int SerializedFileCount { get; init; }
+ public int ReusedSerializedFileCount { get; init; }
+ public int ResourceFileCount { get; init; }
+ public int ObjectCount { get; init; }
+ public List TypeStats { get; init; }
+ public List AssetStats { get; init; }
+
+ private ContentSummary() { }
+
+ public static ContentSummary Read(RandomAccessReader reader)
+ {
+ var typeStats = new List(reader["m_typeStatsList"].GetArraySize());
+ foreach (var element in reader["m_typeStatsList"])
+ {
+ typeStats.Add(new TypeStat
+ {
+ Type = element["classID"].GetValue(),
+ Size = element["size"].GetValue(),
+ ObjectCount = element["objectCount"].GetValue(),
+ ResourceCount = element["resourceCount"].GetValue()
+ });
+ }
+
+ var assetStats = new List(reader["m_assetStatsList"].GetArraySize());
+ foreach (var element in reader["m_assetStatsList"])
+ {
+ var guidData = element["sourceAssetGUID"];
+ var guidString = GuidHelper.FormatUnityGuid(
+ guidData["data[0]"].GetValue(),
+ guidData["data[1]"].GetValue(),
+ guidData["data[2]"].GetValue(),
+ guidData["data[3]"].GetValue());
+
+ assetStats.Add(new AssetStat
+ {
+ SourceAssetGUID = guidString,
+ SourceAssetPath = element["sourceAssetPath"].GetValue(),
+ Size = element["size"].GetValue(),
+ ObjectCount = element["objectCount"].GetValue(),
+ ResourceCount = element["resourceCount"].GetValue()
+ });
+ }
+
+ return new ContentSummary()
+ {
+ SerializedFileSize = reader["m_serializedFileSize"].GetValue(),
+ ReusedSerializedFileSize = reader["m_reusedSerializedFileSize"].GetValue(),
+ ResourceDataSize = reader["m_resourceDataSize"].GetValue(),
+ HeaderSize = reader["m_headerSize"].GetValue(),
+ SerializedFileCount = reader["m_serializedFileCount"].GetValue(),
+ ReusedSerializedFileCount = reader["m_reusedSerializedFileCount"].GetValue(),
+ ResourceFileCount = reader["m_resourceFileCount"].GetValue(),
+ ObjectCount = reader["m_objectCount"].GetValue(),
+ TypeStats = typeStats,
+ AssetStats = assetStats
+ };
+ }
+}
+
+public class TypeStat
+{
+ public int Type { get; init; }
+ public ulong Size { get; init; }
+ public int ObjectCount { get; init; }
+ public int ResourceCount { get; init; }
+}
+
+public class AssetStat
+{
+ public string SourceAssetGUID { get; init; }
+ public string SourceAssetPath { get; init; }
+ public ulong Size { get; init; }
+ public int ObjectCount { get; init; }
+ public int ResourceCount { get; init; }
+}
diff --git a/Documentation/buildreport.md b/Documentation/buildreport.md
index 5c772cf..df23c7d 100644
--- a/Documentation/buildreport.md
+++ b/Documentation/buildreport.md
@@ -1,91 +1,107 @@
# BuildReport Support
-Unity generates a [BuildReport](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport.html) file for Player builds and when building AssetBundles via [BuildPipeline.BuildAssetBundles](https://docs.unity3d.com/ScriptReference/BuildPipeline.BuildAssetBundles.html). Build reports are **not** generated by the [Addressables](addressables-build-reports.md) package or Scriptable Build Pipeline.
+Unity generates a [BuildReport](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport.html) file for its common build types:
-Build reports are written to `Library/LastBuild.buildreport` as Unity SerializedFiles (the same binary format used for build output). UnityDataTool can read this format and extract detailed build information using the same mechanisms as other Unity object types.
+* Player builds
+* AssetBundle builds via [BuildPipeline.BuildAssetBundles](https://docs.unity3d.com/ScriptReference/BuildPipeline.BuildAssetBundles.html)
+* [Content directory](https://docs.unity3d.com/6000.6/Documentation/ScriptReference/BuildPipeline.BuildContentDirectory.html) builds (Unity 6.6+), including content directories built through the Addressables package
-## UnityDataTool Support
+Build reports are **not** generated for AssetBundle builds done with the [Addressables](addressables-build-reports.md) package or the Scriptable Build Pipeline. Those produce a `buildlayout.json` instead — see [Addressables Build Reports](addressables-build-reports.md).
-Since build reports are SerializedFiles, you can use [`dump`](command-dump.md) to convert them to text format.
+A build report is a Unity SerializedFile (the same binary format used for build output), so UnityDataTool reads it with the same mechanisms as any other Unity object. This page focuses on **what UnityDataTool extracts** and on the **differences between Unity versions**, which the versioned [Unity Manual](https://docs.unity3d.com/6000.6/Documentation/Manual/build-history-file-reference.html#buildreport-file) intentionally does not cover.
-The [`analyze`](command-analyze.md) command extracts build report data into dedicated database tables with custom handlers for:
+## What a build report contains
-* **BuildReport** - The primary object containing build inputs and results
-* **PackedAssets** - Describes the contents of each SerializedFile, .resS, or .resource file, including type, size, and source asset for each object or resource blob, enabling object-level analysis
+The information in a build report depends on the build type and the Unity version. UnityDataTool imports whatever data is present, so the same commands work across all of them; the tables that get populated differ.
-**Note:** PackedAssets information is not currently written for scenes in the build.
+| Data | Player | AssetBundle | Content Directory | UnityDataTool tables |
+|------|:------:|:-----------:|:-----------------:|----------------------|
+| Build summary | ✓ | ✓ | ✓ | `build_reports` |
+| File list | ✓ | ✓ | ✓ | `build_report_files`, `build_report_archive_contents` |
+| PackedAssets (per-object breakdown) | ✓ | ✓ | ✗ | `build_report_packed_*` |
+| ContentSummary (aggregate statistics) | ✓ | ✓ | ✓ | `build_report_content_*` |
-## Examples
+All build types always include the **build summary** and **file list**, so `build_reports` and `build_report_files` are populated for every report.
-These are some example queries that can be run after running analyze on a build report file.
+### Unity version differences
-1. Show all successful builds recorded in the database.
+**ContentSummary** is new in Unity 6.6. It provides aggregate statistics (total sizes, counts, and per-type and per-source-asset breakdowns) that are compact compared to the per-object PackedAssets data. Reports from earlier Unity versions contain no ContentSummary object, and the `build_report_content_*` tables are not created for them.
-```
-SELECT * from build_reports WHERE build_result = "Succeeded"
-```
+* It is **not** written for Player builds that reuse content from a previous build (for example [scripts-only builds](https://docs.unity3d.com/6000.6/Documentation/Manual/build-scripts-only.html)).
+* For incremental AssetBundle builds it reflects only the AssetBundles that were rebuilt, not those reused from a previous build.
-2. Show information about the data in the build that originates from "Assets/Sprites/Snow.jpg".
+> **Tip:** For pre-6.6 reports, the [BuildReportInspector](https://github.com/Unity-Technologies/BuildReportInspector) package has a C# helper that derives ContentSummary-like statistics from PackedAssets.
-```
-SELECT *
-FROM build_report_packed_asset_contents_view
-WHERE build_time_asset_path like "Assets/Sprites/Snow.jpg"
-```
+**PackedAssets** behavior changed at Unity 6.6:
-3. Show the AssetBundles that contain content from "Assets/Sprites/Snow.jpg".
+* Before 6.6, PackedAssets data was not written for scene files.
+* From 6.6, PackedAssets covers scene content in Player and AssetBundle builds.
+* Content directory builds do **not** include PackedAssets. The ContentSummary provides the aggregate view, and [`ContentLayout.json`](contentlayout-database.md) provides the source-asset-to-file mapping, so the larger per-object data was omitted to keep content directory reports small.
-```
-SELECT DISTINCT archive
-FROM build_report_packed_asset_contents_view
-WHERE build_time_asset_path like "Assets/Sprites/Snow.jpg"
-```
+**Build report location** also changed at 6.6 — see [Analyzing multiple build reports](#analyzing-multiple-build-reports).
-4. Show all source assets included in the build (excluding C# scripts, e.g. MonoScript objects)
+The Unity Manual's [Build report file reference](https://docs.unity3d.com/6000.6/Documentation/Manual/build-history-file-reference.html#buildreport-file) documents the full set of build report members and their availability for the current Unity version.
-```
-SELECT build_time_asset_path from build_report_source_assets WHERE build_time_asset_path NOT LIKE "%.cs"
-```
+## What UnityDataTool extracts
-## Cross-Referencing with Build Output
+The [`analyze`](command-analyze.md) command extracts build report data into dedicated database tables using custom handlers for these objects:
-For comprehensive analysis, run `analyze` on both the build output **and** the matching build report file. Use a clean build to ensure PackedAssets information is fully populated.
+* **BuildReport** — the primary object, containing the build inputs and results (`build_reports`, `build_report_files`, `build_report_archive_contents`).
+* **PackedAssets** — the contents of each SerializedFile, `.resS`, or `.resource` file: the type, size, and source asset for each object or resource blob, enabling object-level analysis (`build_report_packed_*`).
+* **ContentSummary** — aggregate content statistics with per-type and per-source-asset breakdowns (`build_report_content_*`, Unity 6.6+).
-`analyze` accepts multiple path arguments, each of which can be a file or a directory, so you can pass the build output directory together with the build report path (or the directory containing it) in a single command:
+These tables are created on demand, so a database analyzed without a build report does not contain them, and a report missing an object (for example a content directory report has no PackedAssets) does not create that object's tables. See the [Database Schema](#database-schema) for the full list and the exact creation rules.
+
+Because a build report is a SerializedFile, you can also use [`dump`](command-dump.md) to convert its full contents to text — useful for inspecting data that `analyze` does not import (see [Information not exported](#information-not-exported)).
+
+## Analyzing build reports
+
+### A single build report
+
+Pass the build report file (or a directory containing it) to `analyze`:
+
+```bash
+UnityDataTool analyze /path/to/Library/LastBuild.buildreport
+```
+
+### Cross-referencing with build output
+
+For the most complete analysis, run `analyze` on the build output **and** the matching build report together. `analyze` accepts multiple path arguments, each a file or a directory:
```bash
UnityDataTool analyze /path/to/build/output /path/to/Library/LastBuild.buildreport
```
-PackedAssets data provides source asset information for each object that isn't available when analyzing only the build output. Objects are listed in the same order as they appear in the output SerializedFile, .resS, or .resource file.
+PackedAssets adds the source asset for each object, which is not available from the build output alone. Objects are listed in the same order they appear in the output SerializedFile, `.resS`, or `.resource` file. For Player and AssetBundle builds, use a clean build so the PackedAssets data is fully populated.
-### Database Relationships
+For **content directory** builds it is more important to pair the build output with its [`ContentLayout.json`](contentlayout-database.md) than with the build report, because the layout carries the source-asset mapping. Adding the build report still contributes useful summary data, but is more optional. The `--build-history` option locates and adds both files automatically:
-- Match `build_report_packed_assets` rows to analyzed SerializedFiles using `object_view.serialized_file` and `build_report_packed_assets.path`
-- Match `build_report_packed_asset_info` entries to objects in the build output using `object_id` (local file ID)
+```bash
+UnityDataTool analyze /path/to/ContentDirectory --build-history /path/to/Library/BuildHistory
+```
+
+See the [`analyze` command reference](command-analyze.md) for details.
+
+#### Database relationships
-**Note:** build_report_packed_assets` also record .resS and .resource files. These rows will not match with the serialized_files table.
+- Match `build_report_packed_assets` rows to analyzed SerializedFiles using `object_view.serialized_file` and `build_report_packed_assets.path`.
+- Match `build_report_packed_asset_info` entries to objects in the build output using `object_id` (local file ID).
-**Note:** The source object's local file ID is not recorded in PackedAssetInfo. While you can identify the source asset (e.g., which Prefab), you cannot directly pinpoint the specific object within that asset. When needed, objects can often be distinguished by name or other properties.
+> **Note:** `build_report_packed_assets` also records `.resS` and `.resource` files. Those rows do not match the `serialized_files` table.
-## Working with Multiple Build Reports
+> **Note:** The source object's local file ID is not recorded in PackedAssetInfo. You can identify the source asset (for example, which Prefab), but not the specific object within that asset. When needed, objects can often be distinguished by name or other properties.
-Multiple build reports can be imported into the same database if their filenames differ. Pass each report (and any build output directories) as separate path arguments to a single `analyze` command. This enables:
-- Comprehensive build history tracking
-- Cross-build comparisons
-- Identifying duplicated data between Player and AssetBundle builds
+### Analyzing multiple build reports
-### Prior to Unity 6.6
+Multiple build reports can be imported into the same database as long as their filenames differ (UnityDataTool keys SerializedFiles by filename). Pass each report, and any build output directories, as separate path arguments to a single `analyze` command. This enables build history tracking, cross-build comparisons, and finding data duplicated between Player and AssetBundle builds.
-Each build overwrites `Library/LastBuild.buildreport`. To compare builds, manually collect the report after each build, rename the copies so the filenames are unique (the analyzer keys serialized files by filename), then pass them to `analyze`:
+**Prior to Unity 6.6**, each build overwrites `Library/LastBuild.buildreport`. To compare builds, collect the report after each build and rename the copies so their filenames are unique, then pass them together:
```bash
UnityDataTool analyze build1.buildreport build2.buildreport
```
-### Unity 6.6 and later
-
-Player and content directory builds record a structured [build history](https://docs.unity3d.com/6000.6/Documentation/ScriptReference/Build.BuildHistory.html) (default location `Library/BuildHistory`). Unity assigns each build its own directory and gives every build report a unique GUID-based filename, so there is no need to copy or rename reports to compare them. Run `analyze` on the entire build history folder, or on specific build report directories:
+**From Unity 6.6**, Player and content directory builds record a structured [build history](https://docs.unity3d.com/6000.6/Documentation/ScriptReference/Build.BuildHistory.html) (default location `Library/BuildHistory`). Unity gives each build its own directory and a unique GUID-based build report filename, so no copying or renaming is needed. Run `analyze` on the whole history folder or on specific build directories:
```bash
# Analyze every build in the history
@@ -97,25 +113,154 @@ UnityDataTool analyze Library/BuildHistory/20260504-153912Z-2dd7642e Library/Bui
AssetBundle builds are not tracked in the build history; they still write only to `Library/LastBuild.buildreport`.
-See the schema sections below for guidance on writing queries that handle multiple build reports correctly.
+For more detail on the build history layout, see the Unity Manual's [Build history file reference](https://docs.unity3d.com/6000.6/Documentation/Manual/build-history-file-reference.html).
+
+## Example queries
+
+Run these after analyzing a build report file.
+
+Show all successful builds recorded in the database:
+
+```sql
+SELECT * FROM build_reports WHERE build_result = 'Succeeded'
+```
+
+Show data in the build that originates from a specific source asset:
+
+```sql
+SELECT *
+FROM build_report_packed_asset_contents_view
+WHERE build_time_asset_path LIKE 'Assets/Sprites/Snow.jpg'
+```
+
+Show the AssetBundles that contain content from a specific source asset:
+
+```sql
+SELECT DISTINCT archive
+FROM build_report_packed_asset_contents_view
+WHERE build_time_asset_path LIKE 'Assets/Sprites/Snow.jpg'
+```
+
+Show all source assets included in the build, excluding C# scripts (MonoScript objects):
+
+```sql
+SELECT build_time_asset_path FROM build_report_source_assets WHERE build_time_asset_path NOT LIKE '%.cs'
+```
+
+Show the content size breakdown by type (Unity 6.6+, requires a ContentSummary):
+
+```sql
+SELECT type_name, size, object_count FROM build_report_content_type_stats_view ORDER BY size DESC
+```
+
+## Database Schema
+
+Build report data is stored in the following tables and views:
+
+| Name | Type | Description |
+|------|------|-------------|
+| `build_reports` | table | Build summary (type, result, platform, duration, etc.) |
+| `build_report_files` | table | Files included in the build (path, role, size). See [BuildReport.GetFiles](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport.GetFiles.html) |
+| `build_report_archive_contents` | table | Files inside each Unity Archive (AssetBundle or ContentArchive) |
+| `build_report_packed_assets` | table | SerializedFile, .resS, or .resource file info. See [PackedAssets](https://docs.unity3d.com/ScriptReference/Build.Reporting.PackedAssets.html) |
+| `build_report_packed_asset_info` | table | Each object inside a SerializedFile (or data in .resS/.resource files). See [PackedAssetInfo](https://docs.unity3d.com/ScriptReference/Build.Reporting.PackedAssetInfo.html) |
+| `build_report_source_assets` | table | Source asset GUID and path for each PackedAssetInfo reference |
+| `build_report_content_summary` | table | Cross-build content totals (sizes and counts). See [ContentSummary](https://docs.unity3d.com/6000.6/Documentation/ScriptReference/Build.Reporting.ContentSummary.html) |
+| `build_report_content_type_stats` | table | Per Unity object type: size, object count, resource count |
+| `build_report_content_asset_stats` | table | Per source asset: GUID, path, size, object count, resource count |
+| `build_report_files_view` | view | All files from all build reports |
+| `build_report_packed_assets_view` | view | All PackedAssets with their BuildReport, Archive, and SerializedFile |
+| `build_report_packed_asset_contents_view` | view | All objects and resources tracked in build reports |
+| `build_report_content_summary_view` | view | Cross-build content totals with their BuildReport |
+| `build_report_content_type_stats_view` | view | Per-type stats with the type name and their BuildReport |
+
+The `build_reports` table holds the primary build information; the other tables hold detailed content data. Views simplify queries by joining tables automatically, which matters most when working with multiple build reports.
+
+These tables and views are created on demand, so a database analyzed without any build report does not contain them:
+
+* The `build_reports`, `build_report_files`, and `build_report_archive_contents` group (and `build_report_files_view`) is created with the first **BuildReport** object.
+* The `build_report_packed_*` tables and views are created with the first **PackedAssets** object (a report with none, such as a content directory build, will not have them).
+* The `build_report_content_*` tables and views are created with the first **ContentSummary** object (Unity 6.6+ only).
+
+The new-in-6.6 `build_reports` columns (`build_name`, `build_content_options`, `build_session_guid`, `build_manifest_hash`, `build_profile_path`, `build_profile_guid`, `data_path`) are left NULL when analyzing reports from older Unity versions.
+
+### Schema overview
+
+Views automatically identify which build report each row belongs to, simplifying multi-report queries. To write custom queries, it helps to understand the relationships:
+
+**Primary relationships**
+
+- `build_reports`: One row per analyzed BuildReport file, corresponding to the BuildReport object in the `objects` table via the `id` column.
+- `build_report_packed_assets`: Records the `id` of each PackedAssets object. Find the associated BuildReport via the shared `objects.serialized_file` value (PackedAssets are processed independently of BuildReport objects).
+- `build_report_content_summary`: Records the `id` of the ContentSummary object. Like PackedAssets, a ContentSummary does not record which build it belongs to, so `build_report_content_summary_view` and `build_report_content_type_stats_view` resolve `build_report_id` via the BuildReport object (type 1125) in the same serialized file.
+
+**Auxiliary tables**
+
+- `build_report_files` and `build_report_archive_contents`: store the BuildReport object `id` for each row (as `build_report_id`).
+- `build_report_packed_asset_info`: stores the PackedAssets object `id` for each row (as `packed_assets_id`).
+- `build_report_source_assets`: normalized table of distinct source asset GUIDs and paths, linked via `build_report_packed_asset_info.source_asset_id`.
+- `build_report_content_type_stats` and `build_report_content_asset_stats`: store the ContentSummary object `id` for each row (as `content_summary_id`).
+
+> **Note:** BuildReport and PackedAssets objects are also linked in the `refs` table (BuildReport references PackedAssets in its appendices array), but this relationship is not used in the built-in views because `refs` population is optional.
+
+**Example: `build_report_packed_assets_view`**
+
+This view demonstrates the key relationships:
+- Finds the BuildReport object (`br_obj`) by type (1125) and shared `serialized_file` with the PackedAssets (`pa`).
+- Retrieves the serialized file name from the `serialized_files` table (`sf.name`).
+- For archive-based builds (AssetBundle, ContentDirectory), retrieves the archive name from `build_report_archive_contents` by matching BuildReport ID and PackedAssets path (`archive` is NULL for Player builds).
+
+```sql
+CREATE VIEW build_report_packed_assets_view AS
+SELECT
+ pa.id,
+ o.object_id,
+ brac.archive,
+ pa.path,
+ pa.file_header_size,
+ br_obj.id as build_report_id,
+ sf.name as build_report_filename
+FROM build_report_packed_assets pa
+INNER JOIN objects o ON pa.id = o.id
+INNER JOIN serialized_files sf ON o.serialized_file = sf.id
+LEFT JOIN objects br_obj ON o.serialized_file = br_obj.serialized_file AND br_obj.type = 1125
+LEFT JOIN build_report_archive_contents brac ON br_obj.id = brac.build_report_id AND pa.path = brac.archive_content;
+```
+
+### Column naming
+
+For consistency and clarity, some database columns use different names than the BuildReport API:
+
+| Database Column | BuildReport API | Notes |
+|-----------------|-----------------|-------|
+| `build_report_packed_assets.path` | `PackedAssets.ShortPath` | Filename of the SerializedFile, .resS, or .resource file ("short" was redundant since only one path is recorded) |
+| `build_report_packed_assets.file_header_size` | `PackedAssets.Overhead` | Size of the file header (zero for .resS and .resource files) |
+| `build_report_packed_asset_info.object_id` | `PackedAssetInfo.fileID` | Local file ID of the object (renamed for consistency with `objects.object_id`) |
+| `build_report_packed_asset_info.type` | `PackedAssetInfo.classID` | Unity object type as a numeric [Class ID](https://docs.unity3d.com/Manual/ClassIDReference.html) (renamed for consistency with `objects.type`). `build_report_packed_asset_contents_view` exposes this as the type name. |
## Alternatives
-UnityDataTool provides low-level access to build reports. Consider these alternatives for easier or more convenient workflows:
+UnityDataTool provides low-level access to build reports. Consider these alternatives for easier or more convenient workflows.
+
+### Build Analysis window
-### BuildReportInspector Package
+Unity 6.6 and later includes a built-in [Build Analysis window](https://docs.unity3d.com/6000.7/Documentation/Manual/build-analysis-window-reference.html) that presents BuildReport information (and the rest of the build history) directly in the Editor. For many cases this is a more convenient UI than the BuildReportInspector package.
-View build reports in the Unity Editor using the [BuildReportInspector](https://github.com/Unity-Technologies/BuildReportInspector) package.
+### BuildReportInspector package
+
+The [BuildReportInspector](https://github.com/Unity-Technologies/BuildReportInspector) package renders a build report in the Editor's Inspector. It predates the built-in Build Analysis window, which now covers many of the same needs in Unity 6.6+. For the package's current status and behavior on Unity 6.6, see [New with Unity 6.6](https://github.com/Unity-Technologies/BuildReportInspector/blob/master/com.unity.build-report-inspector/Documentation~/com.unity.build-report-inspector.md#new-with-unity-66).
### BuildReport API
-Access build report data programmatically within Unity using the BuildReport API:
+Access build report data programmatically within Unity using the BuildReport API. How you obtain a report depends on the Unity version.
+
+**Unity 6.6 and later (build history):** Player and content directory builds are tracked in the [build history](https://docs.unity3d.com/6000.6/Documentation/ScriptReference/Build.BuildHistory.html). Load any tracked report directly with [BuildHistory.LoadBuildReport](https://docs.unity3d.com/6000.6/Documentation/ScriptReference/Build.BuildHistory.LoadBuildReport.html), which is the simplest way to reach recent builds and to enumerate history. AssetBundle builds are not tracked in the build history, so use the pre-6.6 approaches below for them.
+
+**Prior to Unity 6.6 (and for AssetBundle reports):** the build history API is not available, so load reports one of these ways.
-**1. Most recent build:**
-Use [BuildPipeline.GetLatestReport()](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport.GetLatestReport.html)
+Most recent build: use [BuildPipeline.GetLatestReport()](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport.GetLatestReport.html).
-**2. Build report in Assets folder:**
-Load via AssetDatabase API:
+Build report in the Assets folder: load via the AssetDatabase API:
```csharp
using UnityEditor;
@@ -136,9 +281,7 @@ public class BuildReportInProjectUtility
}
```
-**3. Build report outside Assets folder:**
-For files in Library or elsewhere, use `InternalEditorUtility`:
-
+Build report outside the Assets folder: for files in Library or elsewhere, use `InternalEditorUtility`:
```csharp
using System;
@@ -172,108 +315,33 @@ public class BuildReportUtility
}
```
-### Text Format Access
+### Text format access
Build reports can be output in Unity's pseudo-YAML format using a diagnostic flag:

-Text files are significantly larger than binary. You can also convert to text by moving the binary file into your Unity project (assets default to text format).
-
-UnityDataTool's `dump` command produces a non-YAML text representation of build report contents.
+Text files are significantly larger than binary. You can also convert to text by moving the binary file into your Unity project (assets default to text format). UnityDataTool's `dump` command produces a non-YAML text representation of build report contents.
-**When to use text formats:**
-- Quick extraction of specific information via text processing tools (regex, YAML parsers, etc.)
+* **Use text formats** for quick extraction of specific information via text-processing tools (regex, YAML parsers, etc.).
+* **Use structured access** (`analyze`, or Unity's BuildReport API) for working with the full structured data.
-**When to use structured access:**
-- Working with full structured data: use UnityDataTool's `analyze` command or Unity's BuildReport API
+### Addressables build reports
-### Addressables Build Reports
-
-The Addressables package generates `buildlayout.json` files instead of BuildReport files. While the format and schema differ, they contain similar information. See [Addressables Build Reports](addressables-build-reports.md) for details on importing these files with UnityDataTool.
-
-## Database Schema
-
-Build report data is stored in the following tables and views:
-
-| Name | Type | Description |
-|------|------|-------------|
-| `build_reports` | table | Build summary (type, result, platform, duration, etc.) |
-| `build_report_files` | table | Files included in the build (path, role, size). See [BuildReport.GetFiles](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport.GetFiles.html) |
-| `build_report_archive_contents` | table | Files inside each Unity Archive (AssetBundle or ContentArchive) |
-| `build_report_packed_assets` | table | SerializedFile, .resS, or .resource file info. See [PackedAssets](https://docs.unity3d.com/ScriptReference/Build.Reporting.PackedAssets.html) |
-| `build_report_packed_asset_info` | table | Each object inside a SerializedFile (or data in .resS/.resource files). See [PackedAssetInfo](https://docs.unity3d.com/ScriptReference/Build.Reporting.PackedAssetInfo.html) |
-| `build_report_source_assets` | table | Source asset GUID and path for each PackedAssetInfo reference |
-| `build_report_files_view` | view | All files from all build reports |
-| `build_report_packed_assets_view` | view | All PackedAssets with their BuildReport, Archive, and SerializedFile |
-| `build_report_packed_asset_contents_view` | view | All objects and resources tracked in build reports |
-
-The `build_reports` table contains primary build information. Additional tables store detailed content data. Views simplify queries by automatically joining tables, especially when working with multiple build reports.
-
-These tables and views are created on demand, so a database analyzed without any build report does not contain them. The `build_reports`, `build_report_files`, `build_report_archive_contents` group and `build_report_files_view` are created when the first BuildReport object is analyzed; the `build_report_packed_*` tables and views are created when the first PackedAssets object is analyzed (a build report with no PackedAssets objects, such as some scene-only builds, will not have them).
-
-### Schema Overview
-
-Views automatically identify which build report each row belongs to, simplifying multi-report queries. To create custom queries, understand the table relationships:
-
-**Primary relationships:**
-- `build_reports`: One row per analyzed BuildReport file, corresponding to the BuildReport object in the `objects` table via the `id` column
-- `build_report_packed_assets`: Records the `id` of each PackedAssets object. Find the associated BuildReport via the shared `objects.serialized_file` value (PackedAssets are processed independently of BuildReport objects)
-
-**Auxiliary tables:**
-- `build_report_files` and `build_report_archive_contents`: Stores the BuildReport object `id` for each row (as `build_report_id`).
-- `build_report_packed_asset_info`: Stores the PackedAssets object `id` for each row (as `packed_assets_id`).
-- `build_report_source_assets`: Normalized table of distinct source asset GUIDs and paths, linked via `build_report_packed_asset_info.source_asset_id`
-
-**Note:** BuildReport and PackedAssets objects are also linked in the `refs` table (BuildReport references PackedAssets in its appendices array), but this relationship is not used in built-in views since `refs` table population is optional.
-
-**Example: build_report_packed_assets_view**
-
-This view demonstrates key relationships:
-- Finds the BuildReport object (`br_obj`) by type (1125) and shared `serialized_file` with the PackedAssets (`pa`)
-- Retrieves the serialized file name from `serialized_files` table (`sf.name`)
-- For archive-based builds (AssetBundle, ContentDirectory), retrieves the archive name from `build_report_archive_contents` by matching BuildReport ID and PackedAssets path (`archive` is NULL for Player builds)
-
-```
-CREATE VIEW build_report_packed_assets_view AS
-SELECT
- pa.id,
- o.object_id,
- brac.archive,
- pa.path,
- pa.file_header_size,
- br_obj.id as build_report_id,
- sf.name as build_report_filename
-FROM build_report_packed_assets pa
-INNER JOIN objects o ON pa.id = o.id
-INNER JOIN serialized_files sf ON o.serialized_file = sf.id
-LEFT JOIN objects br_obj ON o.serialized_file = br_obj.serialized_file AND br_obj.type = 1125
-LEFT JOIN build_report_archive_contents brac ON br_obj.id = brac.build_report_id AND pa.path = brac.archive_content;
-```
-
-### Column Naming
-
-For consistency and clarity, database columns use slightly different names than the BuildReport API:
-
-| Database Column | BuildReport API | Notes |
-|-----------------|-----------------|-------|
-| `build_report_packed_assets.path` | `PackedAssets.ShortPath` | Filename of the SerializedFile, .resS, or .resource file ("short" was redundant since only one path is recorded) |
-| `build_report_packed_assets.file_header_size` | `PackedAssets.Overhead` | Size of the file header (zero for .resS and .resource files) |
-| `build_report_packed_asset_info.object_id` | `PackedAssetInfo.fileID` | Local file ID of the object (renamed for consistency with `objects.object_id`) |
-| `build_report_packed_asset_info.type` | `PackedAssetInfo.classID` | Unity object type as a numeric [Class ID](https://docs.unity3d.com/Manual/ClassIDReference.html) (renamed for consistency with `objects.type`). `build_report_packed_asset_contents_view` exposes this as the type name. |
+The Addressables package generates `buildlayout.json` files instead of BuildReport files. The format and schema differ but the information is similar. See [Addressables Build Reports](addressables-build-reports.md).
## Limitations
-**Duplicate filenames:** Multiple build reports cannot be imported into the same database if they share the same filename. This is a general UnityDataTool limitation that assumes unique SerializedFile names. The BuildReport files in the build history (introduced in Unity 6.6) intentionally have unique file names (incorporating the build session GUID), so analyze does not hit this issue when analyzing multiple entries from the BuildHistory folder. When analyzing multiple AssetBundle build reports, or build reports from older versions of Unity, be sure to assign a unique filename to each. See also issue #36.
+**Duplicate filenames:** Multiple build reports cannot be imported into the same database if they share the same filename. This is a general UnityDataTool limitation that assumes unique SerializedFile names. Build history reports (Unity 6.6+) have unique filenames (they incorporate the build session GUID), so analyzing multiple entries from a `BuildHistory` folder does not hit this issue. When analyzing multiple AssetBundle build reports, or reports from older Unity versions, assign a unique filename to each. See also issue #36.
-### Information Not Exported
+### Information not exported
-Currently, only the most useful BuildReport data is extracted to SQL. Additional data may be added as needed:
+Currently only the most useful BuildReport data is extracted to SQL. Additional data may be added as needed:
-* [Code stripping](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport-strippingInfo.html) appendix (available only for IL2CPP Player builds)
-* [ScenesUsingAssets](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport-scenesUsingAssets.html) (available only in detailed build reports for Player builds)
-* `BuildReport.m_BuildSteps` array. Supporting this is low priority - SQL is not an ideal representation for this hierarchical data. Starting in Unity 6.6 the build steps are also reported in the BuildLog.jsonl file and Trace Event Profile (TEP) files, which are easy to parse.
-* `BuildAssetBundleInfoSet` appendix (undocumented object listing files in each AssetBundle; `build_report_archive_contents` currently derives this from the File list without relying on that data)
+* [Code stripping](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport-strippingInfo.html) appendix (IL2CPP Player builds only)
+* [ScenesUsingAssets](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport-scenesUsingAssets.html) (detailed Player build reports only)
+* `BuildReport.m_BuildSteps` array. Low priority — SQL is not an ideal representation for this hierarchical data. From Unity 6.6 the build steps are also reported in `BuildLog.jsonl` and Trace Event Profile (TEP) files, which are easy to parse.
+* `BuildAssetBundleInfoSet` appendix (undocumented object listing files in each AssetBundle; `build_report_archive_contents` derives this from the file list without relying on that data)
* Analytics-only appendices (unlikely to be valuable for analysis)
-Tip: All this additional BuildReport data can be viewed using the `dump` command.
\ No newline at end of file
+> **Tip:** All of this additional BuildReport data can be viewed with the `dump` command.
diff --git a/UnityBinaryFormat/GuidHelper.cs b/UnityBinaryFormat/GuidHelper.cs
index 8bacf69..9f12b14 100644
--- a/UnityBinaryFormat/GuidHelper.cs
+++ b/UnityBinaryFormat/GuidHelper.cs
@@ -23,6 +23,22 @@ public static string FormatUnityGuid(uint data0, uint data1, uint data2, uint da
return new string(result);
}
+ ///
+ /// Formats a Unity Hash128 (16 bytes) as a 32-character lowercase hex string, matching
+ /// Unity's Hash128.ToString(). Unlike a GUID, the bytes are emitted in order.
+ ///
+ public static string FormatUnityHash128(byte[] bytes)
+ {
+ char[] result = new char[32];
+ const string hexChars = "0123456789abcdef";
+ for (int i = 0; i < 16; i++)
+ {
+ result[i * 2] = hexChars[bytes[i] >> 4];
+ result[i * 2 + 1] = hexChars[bytes[i] & 0xF];
+ }
+ return new string(result);
+ }
+
///
/// Formats a uint32 as 8 hex digits matching Unity's GUIDToString logic.
/// Unity's implementation extracts nibbles from most significant to least significant
diff --git a/UnityDataTool.Tests/BuildReportTests.cs b/UnityDataTool.Tests/BuildReportTests.cs
index 210955a..4adc2d0 100644
--- a/UnityDataTool.Tests/BuildReportTests.cs
+++ b/UnityDataTool.Tests/BuildReportTests.cs
@@ -14,11 +14,18 @@ public class BuildReportTests
private string m_TestOutputFolder;
private string m_TestDataFolder;
+ // Unity 6.6 reference reports, which include the new Summary fields and a ContentSummary object.
+ private string m_ContentDirectoryReport;
+ private string m_AssetBundleReport;
+
[OneTimeSetUp]
public void OneTimeSetup()
{
m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "test_folder");
m_TestDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "BuildReports");
+ var leadingEdge = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "LeadingEdgeBuilds");
+ m_ContentDirectoryReport = Path.Combine(leadingEdge, "BuildReport-ContentDirectory", "f64157fb08bb9f645971d39c1203bd03.buildreport");
+ m_AssetBundleReport = Path.Combine(leadingEdge, "BuildReport-AssetBundles", "LastBuild.buildreport");
Directory.CreateDirectory(m_TestOutputFolder);
Directory.SetCurrentDirectory(m_TestOutputFolder);
}
@@ -514,4 +521,162 @@ public async Task Analyze_BuildReports_BothReports_ContainsBuildReportFilesData(
Assert.AreEqual(0, playerPackedAssetsWithNonNullBundle,
"Expected all PackedAssets from Player.buildreport have NULL archive");
}
+
+ // The Unity 6.6 Summary adds several fields (issue #107 Part 1). Verify they land in the new
+ // build_reports columns for a ContentDirectory report, which populates all of them.
+ [Test]
+ public async Task Analyze_BuildReport_ContentDirectory_ContainsNewSummaryColumns()
+ {
+ var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
+
+ Assert.AreEqual(0, await Program.Main(new[] { "analyze", m_ContentDirectoryReport }));
+ using var db = SQLTestHelper.OpenDatabase(databasePath);
+
+ SQLTestHelper.AssertQueryString(db, "SELECT build_type FROM build_reports", "ContentDirectory",
+ "Unexpected build_type");
+ SQLTestHelper.AssertQueryString(db, "SELECT build_name FROM build_reports", "ContentDirectory",
+ "Unexpected build_name");
+ SQLTestHelper.AssertQueryInt(db, "SELECT build_content_options FROM build_reports", 32,
+ "Unexpected build_content_options");
+ // The session GUID matches the report's GUID-based filename.
+ SQLTestHelper.AssertQueryString(db, "SELECT build_session_guid FROM build_reports",
+ "f64157fb08bb9f645971d39c1203bd03", "Unexpected build_session_guid");
+ SQLTestHelper.AssertQueryString(db, "SELECT build_manifest_hash FROM build_reports",
+ "baff06b928d147276f2245dd3b19216a", "Unexpected build_manifest_hash");
+ // No build profile was active: the path is present-but-empty, and the all-zero GUID -> NULL.
+ SQLTestHelper.AssertQueryString(db, "SELECT build_profile_path FROM build_reports",
+ "", "Expected empty build_profile_path");
+ SQLTestHelper.AssertQueryString(db, "SELECT COALESCE(build_profile_guid, 'NULL') FROM build_reports",
+ "NULL", "Expected NULL build_profile_guid");
+
+ var dataPath = SQLTestHelper.QueryString(db, "SELECT data_path FROM build_reports");
+ Assert.That(dataPath, Does.Contain("ContentDirectory"), "Unexpected data_path");
+ }
+
+ // An AssetBundle build has no build name; that field is present-but-empty in the report.
+ [Test]
+ public async Task Analyze_BuildReport_AssetBundle_HasEmptyBuildName()
+ {
+ var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
+
+ Assert.AreEqual(0, await Program.Main(new[] { "analyze", m_AssetBundleReport }));
+ using var db = SQLTestHelper.OpenDatabase(databasePath);
+
+ SQLTestHelper.AssertQueryString(db, "SELECT build_name FROM build_reports", "",
+ "Expected empty build_name for AssetBundle build");
+ SQLTestHelper.AssertQueryInt(db, "SELECT build_content_options FROM build_reports", 0,
+ "Expected zero build_content_options for AssetBundle build");
+ }
+
+ // Older Unity reports lack all the new Summary fields; the columns must be NULL and analyze
+ // must still succeed.
+ [Test]
+ public async Task Analyze_BuildReport_OldReport_NewSummaryColumnsAreNull()
+ {
+ var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
+
+ Assert.AreEqual(0, await Program.Main(new[] { "analyze", Path.Combine(m_TestDataFolder, "Player.buildreport") }));
+ using var db = SQLTestHelper.OpenDatabase(databasePath);
+
+ SQLTestHelper.AssertQueryInt(db,
+ @"SELECT COUNT(*) FROM build_reports
+ WHERE build_name IS NULL AND build_content_options IS NULL
+ AND build_session_guid IS NULL AND build_manifest_hash IS NULL
+ AND build_profile_path IS NULL AND build_profile_guid IS NULL AND data_path IS NULL",
+ 1, "Expected all new Summary columns to be NULL for an old report");
+ }
+
+ // issue #107 Part 2: a ContentSummary object (Unity 6.6+) populates the on-demand
+ // build_report_content_* tables and views.
+ [Test]
+ public async Task Analyze_BuildReport_ContentDirectory_ContainsContentSummary()
+ {
+ var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
+
+ Assert.AreEqual(0, await Program.Main(new[] { "analyze", m_ContentDirectoryReport }));
+ using var db = SQLTestHelper.OpenDatabase(databasePath);
+
+ SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM build_report_content_summary", 1,
+ "Expected exactly one ContentSummary row");
+ SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM build_report_content_type_stats", 14,
+ "Unexpected number of type stats rows");
+ SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM build_report_content_asset_stats", 15,
+ "Unexpected number of asset stats rows");
+
+ // Spot-check the cross-build stats.
+ SQLTestHelper.AssertQueryInt(db, "SELECT serialized_file_size FROM build_report_content_summary", 158832,
+ "Unexpected serialized_file_size");
+ SQLTestHelper.AssertQueryInt(db, "SELECT object_count FROM build_report_content_summary", 28,
+ "Unexpected object_count");
+
+ // Spot-check a specific type stat (Cubemap, class id 89).
+ SQLTestHelper.AssertQueryInt(db,
+ "SELECT size FROM build_report_content_type_stats WHERE type = 89", 524532,
+ "Unexpected size for type 89");
+ SQLTestHelper.AssertQueryInt(db,
+ "SELECT resource_count FROM build_report_content_type_stats WHERE type = 89", 1,
+ "Unexpected resource_count for type 89");
+
+ // The type-stats view resolves the class id to a name and the owning build report.
+ SQLTestHelper.AssertQueryString(db,
+ "SELECT type_name FROM build_report_content_type_stats_view WHERE type = 89", "Cubemap",
+ "Expected type_name 'Cubemap' for type 89");
+
+ var buildReportId = SQLTestHelper.QueryInt(db, "SELECT id FROM objects WHERE type = 1125");
+ SQLTestHelper.AssertQueryInt(db,
+ $"SELECT COUNT(*) FROM build_report_content_type_stats_view WHERE build_report_id != {buildReportId}", 0,
+ "All type stats should resolve to the single BuildReport object");
+ SQLTestHelper.AssertQueryInt(db,
+ $"SELECT build_report_id FROM build_report_content_summary_view", buildReportId,
+ "ContentSummary should resolve to the BuildReport object in the same file");
+
+ // A specific source asset appears in the asset stats.
+ SQLTestHelper.AssertQueryInt(db,
+ "SELECT COUNT(*) FROM build_report_content_asset_stats WHERE source_asset_path = 'Assets/Audio/a.mp3'",
+ 1, "Expected the a.mp3 source asset in asset stats");
+ }
+
+ // The ContentSummary tables are created on demand, like PackedAssets: an older report without a
+ // ContentSummary object must not create them.
+ [Test]
+ public async Task Analyze_BuildReport_OldReport_NoContentSummaryTables()
+ {
+ var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
+
+ Assert.AreEqual(0, await Program.Main(new[] { "analyze", Path.Combine(m_TestDataFolder, "Player.buildreport") }));
+ using var db = SQLTestHelper.OpenDatabase(databasePath);
+
+ SQLTestHelper.AssertQueryInt(db,
+ "SELECT COUNT(*) FROM sqlite_master WHERE name LIKE 'build_report_content%'", 0,
+ "Expected no ContentSummary tables/views for a report without a ContentSummary object");
+ }
+
+ // When multiple 6.6 reports are analyzed together, each ContentSummary's rows must resolve back
+ // to the BuildReport object in its own serialized file.
+ [Test]
+ public async Task Analyze_BuildReport_MultipleReports_ContentSummaryJoinsCorrectly()
+ {
+ var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
+
+ Assert.AreEqual(0, await Program.Main(new[] { "analyze", m_ContentDirectoryReport, m_AssetBundleReport }));
+ using var db = SQLTestHelper.OpenDatabase(databasePath);
+
+ SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM build_report_content_summary", 2,
+ "Expected one ContentSummary per report");
+
+ // Each report's type stats resolve to a build report in the matching serialized file.
+ SQLTestHelper.AssertQueryInt(db,
+ @"SELECT COUNT(*) FROM build_report_content_type_stats_view
+ WHERE build_report_filename = 'f64157fb08bb9f645971d39c1203bd03.buildreport'
+ AND type_name = 'Cubemap'",
+ 1, "Expected the ContentDirectory report's Cubemap type stat");
+
+ // Every type/summary row resolves to a non-NULL build report.
+ SQLTestHelper.AssertQueryInt(db,
+ "SELECT COUNT(*) FROM build_report_content_summary_view WHERE build_report_id IS NULL", 0,
+ "Every ContentSummary should resolve to a BuildReport");
+ SQLTestHelper.AssertQueryInt(db,
+ "SELECT COUNT(*) FROM build_report_content_type_stats_view WHERE build_report_id IS NULL", 0,
+ "Every type stat should resolve to a BuildReport");
+ }
}