Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Analyzer/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Analyzer/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,7 @@
<data name="PackedAssets" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\PackedAssets.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
<data name="ContentSummary" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\ContentSummary.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
</root>
8 changes: 8 additions & 0 deletions Analyzer/Resources/BuildReport.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);

Expand Down
72 changes: 72 additions & 0 deletions Analyzer/Resources/ContentSummary.sql
Original file line number Diff line number Diff line change
@@ -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;
20 changes: 18 additions & 2 deletions Analyzer/SQLite/Handlers/BuildReportHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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(
Expand Down Expand Up @@ -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();

Expand Down
158 changes: 158 additions & 0 deletions Analyzer/SQLite/Handlers/ContentSummaryHandler.cs
Original file line number Diff line number Diff line change
@@ -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<int> 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();
}
}
1 change: 1 addition & 0 deletions Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public class SerializedFileSQLiteWriter : IDisposable
{ "MonoScript", new MonoScriptHandler() },
{ "BuildReport", new BuildReportHandler() },
{ "PackedAssets", new PackedAssetsHandler() },
{ "ContentSummary", new ContentSummaryHandler() },
};

// serialized files
Expand Down
54 changes: 54 additions & 0 deletions Analyzer/SerializedObjects/BuildReport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BuildFile> Files { get; init; }
public FileListArchiveHelper fileListArchiveHelper { get; init; }

Expand Down Expand Up @@ -69,6 +77,13 @@ public static BuildReport Read(RandomAccessReader reader)

return new BuildReport()
{
BuildName = summary.HasChild("buildName") ? summary["buildName"].GetValue<string>() : null,
BuildContentOptions = summary.HasChild("buildContentOptions") ? summary["buildContentOptions"].GetValue<int>() : null,
BuildSessionGuid = ReadOptionalGuid(summary, "buildSessionGUID"),
BuildManifestHash = ReadOptionalHash128(summary, "buildManifestHash"),
BuildProfilePath = summary.HasChild("buildProfilePath") ? summary["buildProfilePath"].GetValue<string>() : null,
BuildProfileGuid = ReadOptionalGuid(summary, "buildProfileGuid"),
DataPath = summary.HasChild("dataPath") ? summary["dataPath"].GetValue<string>() : null,
Name = reader["m_Name"].GetValue<string>(),
BuildGuid = guidString,
PlatformName = summary["platformName"].GetValue<string>(),
Expand Down Expand Up @@ -114,6 +129,45 @@ static void TrimCommonPathPrefix(List<BuildFile> 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<uint>();
var guid1 = guidData["data[1]"].GetValue<uint>();
var guid2 = guidData["data[2]"].GetValue<uint>();
var guid3 = guidData["data[3]"].GetValue<uint>();

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<byte>();
if (bytes[i] != 0)
allZero = false;
}

return allZero ? null : GuidHelper.FormatUnityHash128(bytes);
}

public static string GetBuildTypeString(int buildType)
{
return buildType switch
Expand Down
Loading
Loading