blob: 01349e4721ff7e90f090bf71d7d14346dd085a2b [file] [log] [blame]
// ignore_for_file: public_member_api_docs, sort_constructors_first
// Copyright (c) 2024, 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 'constant.dart';
import 'definition.dart';
import 'helper.dart';
import 'identifier.dart';
import 'location.dart' show Location;
import 'metadata.dart';
import 'reference.dart';
/// [Recordings] combines recordings of calls and instances with metadata.
///
/// This class acts as the top-level container for recorded usage information.
/// The metadata provides context for the recording, such as version and
/// commentary. The [callsForDefinition] and [instancesForDefinition] store the
/// core data, associating each [Definition] with its corresponding [Reference]
/// details.
///
/// The class uses a normalized JSON format, allowing the reuse of locations and
/// constants across multiple recordings to optimize storage.
class Recordings {
/// [Metadata] such as the recording protocol version.
final Metadata metadata;
/// The collected [CallReference]s for each [Definition].
final Map<Definition, List<CallReference>> callsForDefinition;
late final Map<Identifier, List<CallReference>> calls = callsForDefinition
.map((definition, calls) => MapEntry(definition.identifier, calls));
/// The collected [InstanceReference]s for each [Definition].
final Map<Definition, List<InstanceReference>> instancesForDefinition;
late final Map<Identifier, List<InstanceReference>> instances =
instancesForDefinition.map(
(definition, instances) => MapEntry(definition.identifier, instances),
);
static const _metadataKey = 'metadata';
static const _constantsKey = 'constants';
static const _locationsKey = 'locations';
static const _recordingsKey = 'recordings';
static const _callsKey = 'calls';
static const _instancesKey = 'instances';
static const _definitionKey = 'definition';
Recordings({
required this.metadata,
required this.callsForDefinition,
required this.instancesForDefinition,
});
/// Decodes a JSON representation into a [Recordings] object.
///
/// The format is specifically designed to reduce redundancy and improve
/// efficiency. Identifiers and constants are stored in separate tables,
/// allowing them to be referenced by index in the `recordings` map.
factory Recordings.fromJson(Map<String, Object?> json) {
final constants = <Constant>[];
for (final constantJsonObj in json[_constantsKey] as List? ?? []) {
final constantJson = constantJsonObj as Map<String, Object?>;
constants.add(Constant.fromJson(constantJson, constants));
}
final locations = <Location>[];
for (final locationJsonObj in json[_locationsKey] as List? ?? []) {
final locationJson = locationJsonObj as Map<String, Object?>;
locations.add(Location.fromJson(locationJson));
}
final recordings =
(json[_recordingsKey] as List?)?.whereType<Map<String, Object?>>() ??
[];
final recordedCalls = recordings.where(
(recording) => recording[_callsKey] != null,
);
final recordedInstances = recordings.where(
(recording) => recording[_instancesKey] != null,
);
return Recordings(
metadata: Metadata.fromJson(json[_metadataKey] as Map<String, Object?>),
callsForDefinition: {
for (final recording in recordedCalls)
Definition.fromJson(
recording[_definitionKey] as Map<String, Object?>,
):
(recording[_callsKey] as List)
.map(
(json) => CallReference.fromJson(
json as Map<String, Object?>,
constants,
locations,
),
)
.toList(),
},
instancesForDefinition: {
for (final recording in recordedInstances)
Definition.fromJson(
recording[_definitionKey] as Map<String, Object?>,
):
(recording[_instancesKey] as List)
.map(
(json) => InstanceReference.fromJson(
json as Map<String, Object?>,
constants,
locations,
),
)
.toList(),
},
);
}
/// Encodes this object into a JSON representation.
///
/// This method normalizes identifiers and constants for storage efficiency.
Map<String, Object?> toJson() {
final constants =
{
...callsForDefinition.values
.expand((element) => element)
.whereType<CallWithArguments>()
.expand(
(call) => [
...call.positionalArguments,
...call.namedArguments.values,
],
)
.nonNulls,
...instancesForDefinition.values
.expand((element) => element)
.expand(
(instance) => {
...instance.instanceConstant.fields.values,
instance.instanceConstant,
},
),
}.flatten().asMapToIndices;
final locations =
{
...callsForDefinition.values
.expand((calls) => calls)
.map((call) => call.location),
...instancesForDefinition.values
.expand((instances) => instances)
.map((instance) => instance.location),
}.asMapToIndices;
return {
_metadataKey: metadata.json,
if (constants.isNotEmpty)
_constantsKey:
constants.keys
.map((constant) => constant.toJson(constants))
.toList(),
if (locations.isNotEmpty)
_locationsKey:
locations.keys.map((location) => location.toJson()).toList(),
if (callsForDefinition.isNotEmpty || instancesForDefinition.isNotEmpty)
_recordingsKey: [
if (callsForDefinition.isNotEmpty)
...callsForDefinition.entries.map(
(entry) => {
_definitionKey: entry.key.toJson(),
_callsKey:
entry.value
.map((call) => call.toJson(constants, locations))
.toList(),
},
),
if (instancesForDefinition.isNotEmpty)
...instancesForDefinition.entries.map(
(entry) => {
_definitionKey: entry.key.toJson(),
_instancesKey:
entry.value
.map(
(instance) => instance.toJson(constants, locations),
)
.toList(),
},
),
],
};
}
@override
bool operator ==(covariant Recordings other) {
if (identical(this, other)) return true;
return other.metadata == metadata &&
deepEquals(other.callsForDefinition, callsForDefinition) &&
deepEquals(other.instancesForDefinition, instancesForDefinition);
}
@override
int get hashCode => Object.hash(
metadata.hashCode,
deepHash(callsForDefinition),
deepHash(instancesForDefinition),
);
}
extension on Iterable<Constant> {
Set<Constant> flatten() {
final constants = <Constant>{};
for (final constant in this) {
depthFirstSearch(constant, constants);
}
return constants;
}
void depthFirstSearch(Constant constant, Set<Constant> collected) {
final children = switch (constant) {
ListConstant<Constant>() => constant.value,
MapConstant<Constant>() => constant.value.values,
InstanceConstant() => constant.fields.values,
_ => <Constant>[],
};
for (final child in children) {
if (!collected.contains(child)) {
depthFirstSearch(child, collected);
}
}
collected.add(constant);
}
}
extension _PrivateIterableExtension<T> on Iterable<T> {
/// Transform list to map, faster than using list.indexOf
Map<T, int> get asMapToIndices {
var i = 0;
return {for (final element in this) element: i++};
}
}