Skip to content

[#85] Track dangling references in analyze#96

Merged
SkowronskiAndrew merged 4 commits into
mainfrom
issue85-danglingrefs
Jul 14, 2026
Merged

[#85] Track dangling references in analyze#96
SkowronskiAndrew merged 4 commits into
mainfrom
issue85-danglingrefs

Conversation

@SkowronskiAndrew

Copy link
Copy Markdown
Collaborator

Summary

Fixes #85.

When analyze walks a SerializedFile and finds a reference (PPtr) to an object whose serialized file was not part of the analyzed input, it still assigns that target an object id but never writes an objects row for it. Until now the only trace of these targets was an unexplained gap in the otherwise-dense object id sequence, and the information the reference actually carried — the target's serialized file name and local file id (LFID) — was discarded.

This adds a dangling_refs table and dangling_refs_view that record those unresolved targets, so every object id now resolves to exactly one of objects or dangling_refs, and the referenced file/LFID are preserved. Common cases: analyzing a single AssetBundle or a partial bundle group (cross-bundle references dangle), a Player build referencing unity default resources (shipped without TypeTrees, never analyzed), and ContentDirectory builds analyzed without ContentLayout.json.

Two pre-existing bugs in how null PPtrs were handled surfaced once these gaps became visible, and are fixed here too (see Changes).

Changes

dangling_refs table + view

  • dangling_refs — one row per referenced object that got an id but no objects row. Columns mirror objects (id, object_id = target LFID, serialized_file) so the two tables partition the id space.
  • dangling_refs_view — resolves each dangling target back to the object(s) that reference it, one row per reference (source_id, source_serialized_file, source_object_id, property_path, property_type, target_id, target_serialized_file, target_object_id).
  • Referenced-but-not-analyzed target files are recorded in serialized_files (with archive NULL and no objects of their own) so their name is available.
  • Detection runs after all files are processed, via a new ISQLiteFileParser.FinalizeDatabase() hook called from AnalyzerTool before the database is finalized: the writer reads back the written object/file ids and records any assigned id that never became an object. It honors --skip-references (in that mode neither refs nor dangling_refs are populated).
  • Schema user_version bumped 4 → 5.

Null-PPtr fixes (bugs the feature exposed)

  • Null m_GameObject: the writer resolved a component's m_GameObject even when it was a null PPtr (m_FileID 0, m_PathID 0 — expected for any non-component object), allocating a phantom (file, 0) id. Now only resolved when non-null; otherwise the game_object column is left empty, as the no-m_GameObject branch already did.
  • Null m_Container entries: editor-only assets (ShaderSubGraph, .preset) can appear in an AssetBundle's m_Container with a null PPtr because they have no runtime object. AssetBundleHandler resolved these to a phantom id and wrote an assetbundle_assets row pointing at it. These entries are now skipped; assetbundle_asset_view already excluded them via its INNER JOIN, so only rows that pointed at a non-existent object are dropped.

Docs

  • Documentation/analyzer.md — new dangling_refs / dangling_refs_view sections and a cross-link from the existing "dangling" note under preload_dependencies.

Testing

  • dotnet test — full suite green (UnityFileSystem.Tests 425, Analyzer.Tests 62, UnityDataTool.Tests 241).
  • New coverage in AnalyzeDanglingRefsTests (partial vs. full LeadingEdge AssetBundle analyze) and UnityDataToolPlayerDataTests (Player build → unity default resources): assert the id-space accounting invariant (every refs.referenced_object resolves to objects or dangling_refs, and the two never overlap) and that no phantom object_id = 0 rows are recorded.
  • Existing serialized_files count assertions now count analyzed files only (equal to the previous golden values, so no ExpectedData regeneration was needed).
  • Manual: ran analyze on a large Addressables customer build. Before the null-PPtr fixes it produced two false object_id = 0 dangling refs into already-analyzed CABs; after, dangling_refs contains only the expected unity default resources targets, with the accounting invariants intact.

Add a dangling_refs table and dangling_refs_view recording references
whose target object was assigned an id but never written to the objects
table because its serialized file was not part of the analyzed input
(partial bundle sets, unity default resources, ContentDirectory builds
without ContentLayout.json). Every object id now resolves to exactly one
of objects or dangling_refs.

Detection runs after all files are processed via a new
ISQLiteFileParser.FinalizeDatabase() hook: the writer reads back the
written object/file ids and records any assigned id with no objects row,
adding a serialized_files row (archive NULL) for each un-analyzed target
file so its name is available. Honors --skip-references. Schema
user_version bumped 4 -> 5.

Existing serialized_files count assertions now count analyzed files only
(equal to the previous golden values, so no ExpectedData regeneration).
The writer resolved a component's m_GameObject PPtr to an object id even
when it was null (m_FileID 0 and m_PathID 0), allocating a phantom
(file, 0) object that no row is ever written for. With dangling_refs
tracking this surfaced as confusing object_id 0 entries pointing at
already-analyzed files.

Only resolve m_GameObject when the PPtr is non-null; otherwise leave the
game_object column empty, as the no-m_GameObject branch already does. A
complete AssetBundle build now has zero dangling_refs, and every dangling
target is reachable through refs, so dangling_refs_view covers them all.
Tests assert dangling_refs has no object_id 0 rows.
Editor-only assets (ShaderSubGraph, .preset) can appear in an
AssetBundle's m_Container with a null PPtr (m_FileID 0 and m_PathID 0)
because they have no runtime object. The non-scene branch resolved that
to a phantom (file, 0) object id and wrote an assetbundle_assets row
pointing at it, which then surfaced as a false dangling reference with
object_id 0 into an already-analyzed file.

Skip container entries whose PPtr is null. assetbundle_asset_view already
dropped these rows via its INNER JOIN, so this only removes rows that
pointed at a non-existent object. Verified on a large Addressables build:
the false object_id 0 dangling refs are gone, leaving only the expected
unity default resources targets.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class tracking for unresolved (dangling) Unity object references during analyze by persisting them into the SQLite schema, so object-id gaps become explainable and reference metadata (target serialized file + LFID) is preserved.

Changes:

  • Introduces dangling_refs table + dangling_refs_view, and bumps analyzer DB user_version to 5.
  • Adds a post-parse ISQLiteFileParser.FinalizeDatabase() hook and implements dangling-ref detection/writeback in SerializedFileSQLiteWriter.
  • Fixes two null-PPtr cases (null m_GameObject and null AssetBundle.m_Container entries) and adds targeted test coverage + docs updates.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs Adjusts serialized_files counting to “analyzed only” and adds a player-build dangling-refs regression test.
UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs Updates serialized_files count assertion to exclude referenced-but-not-analyzed target files.
UnityDataTool.Tests/ExpectedDataGenerator.cs Updates expected serialized_files count generation to match new semantics.
UnityDataTool.Tests/AnalyzeDanglingRefsTests.cs New test fixture validating dangling refs for partial vs full AssetBundle analyzes and id-space invariants.
Documentation/analyzer.md Documents dangling_refs / dangling_refs_view and clarifies prior “dangling id” behavior.
Analyzer/Util/IdProvider.cs Exposes id-provider mappings to support finalization-time dangling ref enumeration.
Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs Implements null-PPtr m_GameObject fix and adds FinalizeDatabase() to write dangling refs + target serialized_files rows.
Analyzer/SQLite/Parsers/SerializedFileParser.cs Wires parser-level FinalizeDatabase() into the serialized-file pipeline.
Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs Implements no-op FinalizeDatabase() for build-layout parser.
Analyzer/SQLite/Handlers/ISQLiteHandler.cs Extends ISQLiteFileParser with FinalizeDatabase() hook.
Analyzer/SQLite/Handlers/AssetBundleHandler.cs Skips editor-only m_Container entries with null PPtr to avoid phantom ids.
Analyzer/SQLite/Commands/SerializedFile/AddDanglingRef.cs Adds insert command for the new dangling_refs table.
Analyzer/Resources/Init.sql Adds dangling_refs table, dangling_refs_view, and bumps PRAGMA user_version to 5.
Analyzer/AnalyzerTool.cs Calls FinalizeDatabase() for all parsers before running DB finalize script.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Analyzer/Util/IdProvider.cs
@SkowronskiAndrew SkowronskiAndrew marked this pull request as ready for review July 14, 2026 15:13
Comment thread Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs Outdated
Co-authored-by: Andrew Skowronski <86242170+SkowronskiAndrew@users.noreply.github.com>
@SkowronskiAndrew SkowronskiAndrew merged commit d930e40 into main Jul 14, 2026
5 checks passed
@SkowronskiAndrew SkowronskiAndrew deleted the issue85-danglingrefs branch July 14, 2026 21:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Analyze: track dangling references

2 participants