Skip to content

Commit 42bf782

Browse files
authored
Add ResultReference<T> to record_replay library. (flutter#18)
This introduces a level of indirection that will allow recording objects to differentiate between the invocation result return value, the recorded result value, and the serialized result value. This is used to provide "special handling" of certain invocation results (such as byte arrays) to make recordings more human-readable & editable. This design also yields more technical correctness over the previous design. This is because we used to delay recording an invocation whose result was a Future until that future completed. Now, we record the invocation immediately and late-record future results (with support for awaiting those futures when serializing a recoridng). Another step in flutter#11
1 parent e0aca53 commit 42bf782

File tree

9 files changed

+726
-123
lines changed

9 files changed

+726
-123
lines changed

lib/src/backends/record_replay/common.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,13 @@ String getSymbolName(Symbol symbol) {
2121
int offset = str.indexOf('"') + 1;
2222
return str.substring(offset, str.indexOf('"', offset));
2323
}
24+
25+
/// This class is a work-around for the "is" operator not accepting a variable
26+
/// value as its right operand (https://github.com/dart-lang/sdk/issues/27680).
27+
class TypeMatcher<T> {
28+
/// Creates a type matcher for the given type parameter.
29+
const TypeMatcher();
30+
31+
/// Returns `true` if the given object is of type `T`.
32+
bool matches(dynamic object) => object is T;
33+
}

lib/src/backends/record_replay/encoding.dart

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,51 +16,43 @@ import 'recording_file_system_entity.dart';
1616
import 'recording_io_sink.dart';
1717
import 'recording_link.dart';
1818
import 'recording_random_access_file.dart';
19+
import 'result_reference.dart';
1920

2021
/// Encodes an object into a JSON-ready representation.
2122
typedef dynamic _Encoder(dynamic object);
2223

23-
/// This class is a work-around for the "is" operator not accepting a variable
24-
/// value as its right operand (https://github.com/dart-lang/sdk/issues/27680).
25-
class _TypeMatcher<T> {
26-
/// Creates a type matcher for the given type parameter.
27-
const _TypeMatcher();
28-
29-
/// Returns `true` if the given object is of type `T`.
30-
bool check(dynamic object) => object is T;
31-
}
32-
3324
/// Known encoders. Types not covered here will be encoded using
3425
/// [_encodeDefault].
3526
///
3627
/// When encoding an object, we will walk this map in iteration order looking
3728
/// for a matching encoder. Thus, when there are two encoders that match an
3829
// object, the first one will win.
39-
const Map<_TypeMatcher<dynamic>, _Encoder> _kEncoders =
40-
const <_TypeMatcher<dynamic>, _Encoder>{
41-
const _TypeMatcher<num>(): _encodeRaw,
42-
const _TypeMatcher<bool>(): _encodeRaw,
43-
const _TypeMatcher<String>(): _encodeRaw,
44-
const _TypeMatcher<Null>(): _encodeRaw,
45-
const _TypeMatcher<List<dynamic>>(): _encodeRaw,
46-
const _TypeMatcher<Map<dynamic, dynamic>>(): _encodeMap,
47-
const _TypeMatcher<Iterable<dynamic>>(): _encodeIterable,
48-
const _TypeMatcher<Symbol>(): getSymbolName,
49-
const _TypeMatcher<DateTime>(): _encodeDateTime,
50-
const _TypeMatcher<Uri>(): _encodeUri,
51-
const _TypeMatcher<p.Context>(): _encodePathContext,
52-
const _TypeMatcher<EventImpl<dynamic>>(): _encodeEvent,
53-
const _TypeMatcher<FileSystem>(): _encodeFileSystem,
54-
const _TypeMatcher<RecordingDirectory>(): _encodeFileSystemEntity,
55-
const _TypeMatcher<RecordingFile>(): _encodeFileSystemEntity,
56-
const _TypeMatcher<RecordingLink>(): _encodeFileSystemEntity,
57-
const _TypeMatcher<RecordingIOSink>(): _encodeIOSink,
58-
const _TypeMatcher<RecordingRandomAccessFile>(): _encodeRandomAccessFile,
59-
const _TypeMatcher<Encoding>(): _encodeEncoding,
60-
const _TypeMatcher<FileMode>(): _encodeFileMode,
61-
const _TypeMatcher<FileStat>(): _encodeFileStat,
62-
const _TypeMatcher<FileSystemEntityType>(): _encodeFileSystemEntityType,
63-
const _TypeMatcher<FileSystemEvent>(): _encodeFileSystemEvent,
30+
const Map<TypeMatcher<dynamic>, _Encoder> _kEncoders =
31+
const <TypeMatcher<dynamic>, _Encoder>{
32+
const TypeMatcher<num>(): _encodeRaw,
33+
const TypeMatcher<bool>(): _encodeRaw,
34+
const TypeMatcher<String>(): _encodeRaw,
35+
const TypeMatcher<Null>(): _encodeRaw,
36+
const TypeMatcher<List<dynamic>>(): _encodeRaw,
37+
const TypeMatcher<Map<dynamic, dynamic>>(): _encodeMap,
38+
const TypeMatcher<Iterable<dynamic>>(): _encodeIterable,
39+
const TypeMatcher<Symbol>(): getSymbolName,
40+
const TypeMatcher<DateTime>(): _encodeDateTime,
41+
const TypeMatcher<Uri>(): _encodeUri,
42+
const TypeMatcher<p.Context>(): _encodePathContext,
43+
const TypeMatcher<ResultReference<dynamic>>(): _encodeResultReference,
44+
const TypeMatcher<LiveInvocationEvent<dynamic>>(): _encodeEvent,
45+
const TypeMatcher<FileSystem>(): _encodeFileSystem,
46+
const TypeMatcher<RecordingDirectory>(): _encodeFileSystemEntity,
47+
const TypeMatcher<RecordingFile>(): _encodeFileSystemEntity,
48+
const TypeMatcher<RecordingLink>(): _encodeFileSystemEntity,
49+
const TypeMatcher<RecordingIOSink>(): _encodeIOSink,
50+
const TypeMatcher<RecordingRandomAccessFile>(): _encodeRandomAccessFile,
51+
const TypeMatcher<Encoding>(): _encodeEncoding,
52+
const TypeMatcher<FileMode>(): _encodeFileMode,
53+
const TypeMatcher<FileStat>(): _encodeFileStat,
54+
const TypeMatcher<FileSystemEntityType>(): _encodeFileSystemEntityType,
55+
const TypeMatcher<FileSystemEvent>(): _encodeFileSystemEvent,
6456
};
6557

6658
/// Encodes [object] into a JSON-ready representation.
@@ -72,8 +64,8 @@ const Map<_TypeMatcher<dynamic>, _Encoder> _kEncoders =
7264
/// - [JsonEncoder.withIndent]
7365
dynamic encode(dynamic object) {
7466
_Encoder encoder = _encodeDefault;
75-
for (_TypeMatcher<dynamic> matcher in _kEncoders.keys) {
76-
if (matcher.check(object)) {
67+
for (TypeMatcher<dynamic> matcher in _kEncoders.keys) {
68+
if (matcher.matches(object)) {
7769
encoder = _kEncoders[matcher];
7870
break;
7971
}
@@ -114,7 +106,11 @@ Map<String, String> _encodePathContext(p.Context context) => <String, String>{
114106
'cwd': context.current,
115107
};
116108

117-
Map<String, dynamic> _encodeEvent(EventImpl<dynamic> event) => event.encode();
109+
dynamic _encodeResultReference(ResultReference<dynamic> reference) =>
110+
reference.serializedValue;
111+
112+
Map<String, dynamic> _encodeEvent(LiveInvocationEvent<dynamic> event) =>
113+
event.serialize();
118114

119115
String _encodeFileSystem(FileSystem fs) => kFileSystemEncodedValue;
120116

lib/src/backends/record_replay/events.dart

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:async';
6+
57
import 'recording.dart';
8+
import 'result_reference.dart';
69

710
/// Base class for recordable file system invocation events.
811
///
@@ -53,53 +56,83 @@ abstract class MethodEvent<T> extends InvocationEvent<T> {
5356
Map<Symbol, dynamic> get namedArguments;
5457
}
5558

56-
/// Non-exported implementation of [InvocationEvent].
57-
abstract class EventImpl<T> implements InvocationEvent<T> {
58-
/// Creates a new `EventImpl`.
59-
EventImpl(this.object, this.result, this.timestamp);
59+
/// An [InvocationEvent] that's in the process of being recorded.
60+
abstract class LiveInvocationEvent<T> implements InvocationEvent<T> {
61+
/// Creates a new `LiveInvocationEvent`.
62+
LiveInvocationEvent(this.object, this._result, this.timestamp);
63+
64+
final dynamic _result;
6065

6166
@override
6267
final Object object;
6368

6469
@override
65-
final T result;
70+
T get result {
71+
dynamic result = _result;
72+
while (result is ResultReference) {
73+
ResultReference<dynamic> reference = result;
74+
result = reference.recordedValue;
75+
}
76+
return result;
77+
}
6678

6779
@override
6880
final int timestamp;
6981

70-
/// Encodes this event into a JSON-ready format.
71-
Map<String, dynamic> encode() => <String, dynamic>{
82+
/// A [Future] that completes once [result] is ready for serialization.
83+
///
84+
/// If [result] is a [Future], this future completes when [result] completes.
85+
/// If [result] is a [Stream], this future completes when the stream sends a
86+
/// "done" event. If [result] is neither a future nor a stream, this future
87+
/// completes immediately.
88+
///
89+
/// It is legal for [serialize] to be called before this future completes,
90+
/// but doing so will cause incomplete results to be serialized. Results that
91+
/// are unfinished futures will be serialized as `null`, and results that are
92+
/// unfinished streams will be serialized as the data that has been received
93+
/// thus far.
94+
Future<Null> get done async {
95+
dynamic result = _result;
96+
while (result is ResultReference) {
97+
ResultReference<dynamic> reference = result;
98+
await reference.complete;
99+
result = reference.recordedValue;
100+
}
101+
}
102+
103+
/// Returns this event as a JSON-serializable object.
104+
Map<String, dynamic> serialize() => <String, dynamic>{
72105
'object': object,
73-
'result': result,
106+
'result': _result,
74107
'timestamp': timestamp,
75108
};
76109

77110
@override
78-
String toString() => encode().toString();
111+
String toString() => serialize().toString();
79112
}
80113

81-
/// Non-exported implementation of [PropertyGetEvent].
82-
class PropertyGetEventImpl<T> extends EventImpl<T>
114+
/// A [PropertyGetEvent] that's in the process of being recorded.
115+
class LivePropertyGetEvent<T> extends LiveInvocationEvent<T>
83116
implements PropertyGetEvent<T> {
84-
/// Create a new `PropertyGetEventImpl`.
85-
PropertyGetEventImpl(Object object, this.property, T result, int timestamp)
117+
/// Creates a new `LivePropertyGetEvent`.
118+
LivePropertyGetEvent(Object object, this.property, T result, int timestamp)
86119
: super(object, result, timestamp);
87120

88121
@override
89122
final Symbol property;
90123

91124
@override
92-
Map<String, dynamic> encode() => <String, dynamic>{
125+
Map<String, dynamic> serialize() => <String, dynamic>{
93126
'type': 'get',
94127
'property': property,
95-
}..addAll(super.encode());
128+
}..addAll(super.serialize());
96129
}
97130

98-
/// Non-exported implementation of [PropertySetEvent].
99-
class PropertySetEventImpl<T> extends EventImpl<Null>
131+
/// A [PropertySetEvent] that's in the process of being recorded.
132+
class LivePropertySetEvent<T> extends LiveInvocationEvent<Null>
100133
implements PropertySetEvent<T> {
101-
/// Create a new `PropertySetEventImpl`.
102-
PropertySetEventImpl(Object object, this.property, this.value, int timestamp)
134+
/// Creates a new `LivePropertySetEvent`.
135+
LivePropertySetEvent(Object object, this.property, this.value, int timestamp)
103136
: super(object, null, timestamp);
104137

105138
@override
@@ -109,17 +142,18 @@ class PropertySetEventImpl<T> extends EventImpl<Null>
109142
final T value;
110143

111144
@override
112-
Map<String, dynamic> encode() => <String, dynamic>{
145+
Map<String, dynamic> serialize() => <String, dynamic>{
113146
'type': 'set',
114147
'property': property,
115148
'value': value,
116-
}..addAll(super.encode());
149+
}..addAll(super.serialize());
117150
}
118151

119-
/// Non-exported implementation of [MethodEvent].
120-
class MethodEventImpl<T> extends EventImpl<T> implements MethodEvent<T> {
121-
/// Create a new `MethodEventImpl`.
122-
MethodEventImpl(
152+
/// A [MethodEvent] that's in the process of being recorded.
153+
class LiveMethodEvent<T> extends LiveInvocationEvent<T>
154+
implements MethodEvent<T> {
155+
/// Creates a new `LiveMethodEvent`.
156+
LiveMethodEvent(
123157
Object object,
124158
this.method,
125159
List<dynamic> positionalArguments,
@@ -143,10 +177,10 @@ class MethodEventImpl<T> extends EventImpl<T> implements MethodEvent<T> {
143177
final Map<Symbol, dynamic> namedArguments;
144178

145179
@override
146-
Map<String, dynamic> encode() => <String, dynamic>{
180+
Map<String, dynamic> serialize() => <String, dynamic>{
147181
'type': 'invoke',
148182
'method': method,
149183
'positionalArguments': positionalArguments,
150184
'namedArguments': namedArguments,
151-
}..addAll(super.encode());
185+
}..addAll(super.serialize());
152186
}

lib/src/backends/record_replay/mutable_recording.dart

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,45 @@ import 'recording.dart';
1414

1515
/// A mutable live recording.
1616
class MutableRecording implements LiveRecording {
17-
final List<InvocationEvent<dynamic>> _events = <InvocationEvent<dynamic>>[];
18-
1917
/// Creates a new `MutableRecording` that will serialize its data to the
2018
/// specified [destination].
2119
MutableRecording(this.destination);
2220

21+
final List<LiveInvocationEvent<dynamic>> _events =
22+
<LiveInvocationEvent<dynamic>>[];
23+
24+
bool _flushing = false;
25+
2326
@override
2427
final Directory destination;
2528

2629
@override
27-
List<InvocationEvent<dynamic>> get events =>
28-
new List<InvocationEvent<dynamic>>.unmodifiable(_events);
30+
List<LiveInvocationEvent<dynamic>> get events =>
31+
new List<LiveInvocationEvent<dynamic>>.unmodifiable(_events);
2932

30-
// TODO(tvolkert): Add ability to wait for all Future and Stream results
3133
@override
32-
Future<Null> flush() async {
33-
Directory dir = destination;
34-
String json = new JsonEncoder.withIndent(' ', encode).convert(_events);
35-
String filename = dir.fileSystem.path.join(dir.path, kManifestName);
36-
await dir.fileSystem.file(filename).writeAsString(json, flush: true);
34+
Future<Null> flush({Duration awaitPendingResults}) async {
35+
if (_flushing) {
36+
throw new StateError('Recording is already flushing');
37+
}
38+
_flushing = true;
39+
try {
40+
if (awaitPendingResults != null) {
41+
Iterable<Future<Null>> futures =
42+
_events.map((LiveInvocationEvent<dynamic> event) => event.done);
43+
await Future
44+
.wait<String>(futures)
45+
.timeout(awaitPendingResults, onTimeout: () {});
46+
}
47+
Directory dir = destination;
48+
String json = new JsonEncoder.withIndent(' ', encode).convert(_events);
49+
String filename = dir.fileSystem.path.join(dir.path, kManifestName);
50+
await dir.fileSystem.file(filename).writeAsString(json, flush: true);
51+
} finally {
52+
_flushing = false;
53+
}
3754
}
3855

3956
/// Adds the specified [event] to this recording.
40-
void add(InvocationEvent<dynamic> event) => _events.add(event);
57+
void add(LiveInvocationEvent<dynamic> event) => _events.add(event);
4158
}

lib/src/backends/record_replay/recording.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,20 @@ abstract class LiveRecording extends Recording {
3535
/// Writes this recording to disk.
3636
///
3737
/// Live recordings will *not* call `flush` on themselves, so it is up to
38-
/// callers to call this method when they wish to write the recording to disk.
38+
/// callers to call this method when they wish to write the recording to
39+
/// disk.
40+
///
41+
/// If [awaitPendingResults] is specified, this will wait the specified
42+
/// duration for any results that are `Future`s or `Stream`s to complete
43+
/// before serializing the recording to disk. Futures that don't complete
44+
/// within the specified duration will have their results recorded as `null`,
45+
/// and streams that don't send a "done" event within the specified duration
46+
/// will have their results recorded as the list of events the stream has
47+
/// fired thus far.
48+
///
49+
/// Throws a [StateError] if a flush is already in progress.
3950
///
4051
/// Returns a future that completes once the recording has been fully written
4152
/// to disk.
42-
Future<Null> flush();
53+
Future<Null> flush({Duration awaitPendingResults});
4354
}

0 commit comments

Comments
 (0)