Skip to content

Serialize AssetId by identity. #3970

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 11, 2025
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
4 changes: 2 additions & 2 deletions build_runner_core/lib/src/asset_graph/graph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class AssetGraph implements GeneratedAssetHider {

/// Deserializes this graph.
factory AssetGraph.deserialize(List<int> serializedGraph) =>
_AssetGraphDeserializer(serializedGraph).deserialize();
deserializeAssetGraph(serializedGraph);

static Future<AssetGraph> build(
BuildPhases buildPhases,
Expand Down Expand Up @@ -106,7 +106,7 @@ class AssetGraph implements GeneratedAssetHider {
return graph;
}

List<int> serialize() => _AssetGraphSerializer(this).serialize();
List<int> serialize() => serializeAssetGraph(this);

@visibleForTesting
Map<String, Map<PostProcessBuildStepId, Set<AssetId>>>
Expand Down
91 changes: 91 additions & 0 deletions build_runner_core/lib/src/asset_graph/identity_serializer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:built_value/serializer.dart';

/// Wraps another `built_value` serializer to add serializing by identity.
///
/// Integer IDs are created as new values are encountered, and a mapping to
/// objects and serialized object values is maintained.
///
/// The serialized data does not contain the object values, only IDs. This might
/// not matter if this `IdentitySerializer` will stay in memory and be used to
/// deserialize. Or, the serialized object values must be serialized separately:
/// they can be obtained from [serializedObjects], and set via
/// [deserializeWithObjects].
///
/// TODO(davidmorgan): consider upstreaming to `built_value`.
class IdentitySerializer<T> implements PrimitiveSerializer<T> {
final Serializer<T> delegate;
final PrimitiveSerializer<T>? _primitiveDelegate;
final StructuredSerializer<T>? _structuredDelegate;

final Map<T, int> _ids = Map.identity();
final List<T> _objects = [];
List<Object?> _serializedObjects = [];

/// A serializer wrapping [delegate] to deduplicate by identity.
IdentitySerializer(this.delegate)
: _primitiveDelegate = delegate is PrimitiveSerializer<T> ? delegate : null,
_structuredDelegate =
delegate is StructuredSerializer<T> ? delegate : null;

/// Sets the stored object values to [objects].
///
/// Serialized values are indices into this list.
void deserializeWithObjects(Iterable<T> objects) {
_ids.clear();
_objects.clear();
_serializedObjects.clear();
for (final object in objects) {
_objects.add(object);
_ids[object] = _objects.length - 1;
}
}

/// The list of unique objects encountered since the most recent [reset].
List<Object?> get serializedObjects => _serializedObjects;

/// Clears the ID to object and serialized object mappings.
///
/// Call this after serializing or deserializing to avoid retaining objects in
/// memory; or, don't call it to continue using the same IDs and objects.
void reset() {
_ids.clear();
_objects.clear();
_serializedObjects = [];
}

@override
Iterable<Type> get types => delegate.types;

@override
String get wireName => delegate.wireName;

@override
T deserialize(
Serializers serializers,
Object serialized, {
FullType specifiedType = FullType.unspecified,
}) => _objects[serialized as int];

@override
Object serialize(
Serializers serializers,
T object, {
FullType specifiedType = FullType.unspecified,
}) {
// If it has already been seen, return the ID.
return _ids.putIfAbsent(object, () {
// Otherwise, serialize it, store the value and serialized value, and
// return the index of the last of `_objects` as the ID.
final serialized =
_primitiveDelegate == null
? _structuredDelegate!.serialize(serializers, object)
: _primitiveDelegate.serialize(serializers, object);
_serializedObjects.add(serialized);
return (_objects..add(object)).length - 1;
});
}
}
207 changes: 77 additions & 130 deletions build_runner_core/lib/src/asset_graph/serialization.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,148 +8,95 @@ part of 'graph.dart';
///
/// This should be incremented any time the serialize/deserialize formats
/// change.
const _version = 27;
const _version = 28;

/// Deserializes an [AssetGraph] from a [Map].
class _AssetGraphDeserializer {
// Iteration order does not matter
final _idToAssetId = HashMap<int, AssetId>();
final Map _serializedGraph;

_AssetGraphDeserializer._(this._serializedGraph);

factory _AssetGraphDeserializer(List<int> bytes) {
dynamic decoded;
try {
decoded = jsonDecode(utf8.decode(bytes));
} on FormatException {
throw AssetGraphCorruptedException();
}
if (decoded is! Map) throw AssetGraphCorruptedException();
if (decoded['version'] != _version) {
throw AssetGraphCorruptedException();
}
return _AssetGraphDeserializer._(decoded);
AssetGraph deserializeAssetGraph(List<int> bytes) {
dynamic serializedGraph;
try {
serializedGraph = jsonDecode(utf8.decode(bytes));
} on FormatException {
throw AssetGraphCorruptedException();
}
if (serializedGraph is! Map) throw AssetGraphCorruptedException();
if (serializedGraph['version'] != _version) {
throw AssetGraphCorruptedException();
}

/// Perform the deserialization, should only be called once.
AssetGraph deserialize() {
var packageLanguageVersions = {
for (var entry
in (_serializedGraph['packageLanguageVersions']
as Map<String, dynamic>)
.entries)
entry.key:
entry.value != null
? LanguageVersion.parse(entry.value as String)
: null,
};
var graph = AssetGraph._(
_deserializeDigest(_serializedGraph['buildActionsDigest'] as String)!,
_serializedGraph['dart_version'] as String,
packageLanguageVersions.build(),
BuiltList<String>.from(_serializedGraph['enabledExperiments'] as List),
);

var packageNames = _serializedGraph['packages'] as List;

// Read in the id => AssetId map from the graph first.
var assetPaths = _serializedGraph['assetPaths'] as List;
for (var i = 0; i < assetPaths.length; i += 2) {
var packageName = packageNames[assetPaths[i + 1] as int] as String;
_idToAssetId[i ~/ 2] = AssetId(packageName, assetPaths[i] as String);
}

// Read in all the nodes and their outputs.
//
// Note that this does not read in the inputs of generated nodes.
for (var serializedItem in _serializedGraph['nodes'] as Iterable) {
graph._add(_deserializeAssetNode(serializedItem as List));
}
identityAssetIdSerializer.deserializeWithObjects(
(serializedGraph['ids'] as List).map(
(id) => assetIdSerializer.deserialize(serializers, id as Object),
),
);

var packageLanguageVersions = {
for (var entry
in (serializedGraph['packageLanguageVersions'] as Map<String, dynamic>)
.entries)
entry.key:
entry.value != null
? LanguageVersion.parse(entry.value as String)
: null,
};
var graph = AssetGraph._(
_deserializeDigest(serializedGraph['buildActionsDigest'] as String)!,
serializedGraph['dart_version'] as String,
packageLanguageVersions.build(),
BuiltList<String>.from(serializedGraph['enabledExperiments'] as List),
);

for (var serializedItem in serializedGraph['nodes'] as Iterable) {
graph._add(_deserializeAssetNode(serializedItem as List));
}

final postProcessOutputs =
serializers.deserialize(
_serializedGraph['postProcessOutputs'],
specifiedType: postProcessBuildStepOutputsFullType,
)
as Map<String, Map<PostProcessBuildStepId, Set<AssetId>>>;
final postProcessOutputs =
serializers.deserialize(
serializedGraph['postProcessOutputs'],
specifiedType: postProcessBuildStepOutputsFullType,
)
as Map<String, Map<PostProcessBuildStepId, Set<AssetId>>>;

for (final postProcessOutputsForPackage in postProcessOutputs.values) {
for (final entry in postProcessOutputsForPackage.entries) {
graph.updatePostProcessBuildStep(entry.key, outputs: entry.value);
}
for (final postProcessOutputsForPackage in postProcessOutputs.values) {
for (final entry in postProcessOutputsForPackage.entries) {
graph.updatePostProcessBuildStep(entry.key, outputs: entry.value);
}

return graph;
}

AssetNode _deserializeAssetNode(List serializedNode) =>
serializers.deserializeWith(AssetNode.serializer, serializedNode)
as AssetNode;
identityAssetIdSerializer.reset();
return graph;
}

/// Serializes an [AssetGraph] into a [Map].
class _AssetGraphSerializer {
// Iteration order does not matter
final _assetIdToId = HashMap<AssetId, int>();

final AssetGraph _graph;

_AssetGraphSerializer(this._graph);

/// Perform the serialization, should only be called once.
List<int> serialize() {
var pathId = 0;
// [path0, packageId0, path1, packageId1, ...]
var assetPaths = <dynamic>[];
var packages = _graph._nodesByPackage.keys.toList(growable: false);
for (var node in _graph.allNodes) {
_assetIdToId[node.id] = pathId;
pathId++;
assetPaths
..add(node.id.path)
..add(packages.indexOf(node.id.package));
}
AssetNode _deserializeAssetNode(List serializedNode) =>
serializers.deserializeWith(AssetNode.serializer, serializedNode)
as AssetNode;

var result = <String, dynamic>{
'version': _version,
'dart_version': _graph.dartVersion,
'nodes': _graph.allNodes
.map((node) => serializers.serializeWith(AssetNode.serializer, node))
.toList(growable: false),
'buildActionsDigest': _serializeDigest(_graph.buildPhasesDigest),
'packages': packages,
'assetPaths': assetPaths,
'packageLanguageVersions':
_graph.packageLanguageVersions
.map((pkg, version) => MapEntry(pkg, version?.toString()))
.toMap(),
'enabledExperiments': _graph.enabledExperiments.toList(),
'postProcessOutputs': serializers.serialize(
_graph._postProcessBuildStepOutputs,
specifiedType: postProcessBuildStepOutputsFullType,
),
};
return utf8.encode(json.encode(result));
}

int findAssetIndex(
AssetId id, {
required AssetId from,
required String field,
}) {
final index = _assetIdToId[id];
if (index == null) {
log.severe(
'The $field field in $from references a non-existent asset '
'$id and will corrupt the asset graph. '
'If you encounter this error please copy '
'the details from this message and add them to '
'https://github.com/dart-lang/build/issues/1804.',
);
}
return index!;
}
/// Serializes an [AssetGraph] into a [Map].
List<int> serializeAssetGraph(AssetGraph graph) {
// Serialize nodes first so all `AssetId` instances are seen by
// `identityAssetIdSeralizer`.
final nodes = graph.allNodes
.map((node) => serializers.serializeWith(AssetNode.serializer, node))
.toList(growable: false);

var result = <String, dynamic>{
'version': _version,
'ids': identityAssetIdSerializer.serializedObjects,
'dart_version': graph.dartVersion,
'nodes': nodes,
'buildActionsDigest': _serializeDigest(graph.buildPhasesDigest),
'packageLanguageVersions':
graph.packageLanguageVersions
.map((pkg, version) => MapEntry(pkg, version?.toString()))
.toMap(),
'enabledExperiments': graph.enabledExperiments.toList(),
'postProcessOutputs': serializers.serialize(
graph._postProcessBuildStepOutputs,
specifiedType: postProcessBuildStepOutputsFullType,
),
};

identityAssetIdSerializer.reset();
return utf8.encode(json.encode(result));
}

Digest? _deserializeDigest(String? serializedDigest) =>
Expand Down
8 changes: 7 additions & 1 deletion build_runner_core/lib/src/asset_graph/serializers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart';
import 'package:crypto/crypto.dart';

import 'identity_serializer.dart';
import 'node.dart';
import 'post_process_build_step_id.dart';

Expand All @@ -24,10 +25,15 @@ final postProcessBuildStepOutputsFullType = FullType(Map, [
postProcessBuildStepOutputsInnerFullType,
]);

final assetIdSerializer = AssetIdSerializer();
final identityAssetIdSerializer = IdentitySerializer<AssetId>(
assetIdSerializer,
);

@SerializersFor([AssetNode])
final Serializers serializers =
(_$serializers.toBuilder()
..add(AssetIdSerializer())
..add(identityAssetIdSerializer)
..add(DigestSerializer())
..add(MapSerializer())
..add(SetSerializer())
Expand Down
Loading