Skip to content

Commit e780076

Browse files
Merge pull request #115 from Workiva/AF-1540_consumer-specified-timing
AF-1540 AF-2674 consumer-specified timing for "first useful state"
2 parents bd03bf4 + 4d40242 commit e780076

File tree

11 files changed

+345
-47
lines changed

11 files changed

+345
-47
lines changed

docs.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
title: w_module
22
base: github:Workiva/w_module/
3-
src: README.md
3+
src: README.md
4+
topics:
5+
- title: Tracing with w_module
6+
src: docs/tracing.md

documentation/tracing.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Tracing with w_module
2+
3+
This library supports [OpenTracing][opentracingio] using [opentracing_dart][opentracingdart]. Your application will need to provide a `Tracer` and initialize it with `initGlobalTracer` to opt in to this feature.
4+
5+
6+
To get the traces provided by this library, your module must provide a definition for the `name` getter. This can simply be the name of the class. For example:
7+
8+
```dart
9+
class SomeModule extends Module {
10+
@override
11+
final String name = 'SomeModule';
12+
13+
// ... the of the module's implementation
14+
}
15+
```
16+
17+
Spans will be in the form of:
18+
19+
```
20+
$name.$operationName
21+
```
22+
23+
## Types Of Provided Traces
24+
25+
### Tracing Lifecycle Methods
26+
27+
We automically trace each of its lifecycle methods:
28+
29+
- Load
30+
- Unload
31+
- Suspend
32+
- Resume
33+
34+
In addition, any spans created by child modules (loaded with `loadChildModule`) will have a `followsFrom` reference to the parent's span of the respective method to complete the story of the trace.
35+
36+
If you wish to create other `childOf` or `followsFrom` spans on your module's lifecycle spans, you can simply request the `activeSpan`:
37+
38+
```dart
39+
@override
40+
Future<Null> onLoad() {
41+
// ... some loading logic
42+
43+
final span = globalTracer().startSpan(
44+
operationName,
45+
childOf: activeSpan.context, // see this line
46+
);
47+
48+
// ... more loading logic
49+
span.finish()
50+
}
51+
```
52+
53+
Note that `activeSpan` will be null at times when the module is not in the middle of a lifecycle transition.
54+
55+
### Additional Load Time Granularity
56+
57+
Sometimes, lifecycle methods such as `load` will complete before the module is semantically "loaded". For example, you may begin asynchronously fetching data for your module and then return from `onLoad` to keep from blocking the main thread.
58+
59+
In cases like these, use `specifyStartupTiming`:
60+
61+
```dart
62+
Future<Null> onLoad() {
63+
// ... kick off async loadData logic
64+
65+
listenToStream(_events.didLoadData.take(1),
66+
(_) => specifyStartupTiming(StartupTimingType.firstUseful));
67+
}
68+
```
69+
70+
This will create a span starting from the same time as `load()` and ending at the moment the method was called. This library will handle the `operationName` and the `followsFrom` reference to the module's `load` span, but tags and references can be passed into this method just like with any other span in optional parameters.
71+
72+
[opentracingio]: https://opentracing.io/
73+
[opentracingdart]: https://github.com/Workiva/opentracing_dart/

example/panel/modules/data_load_async_module.dart

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'dart:async';
1818

1919
import 'package:meta/meta.dart' show protected;
2020
import 'package:react/react.dart' as react;
21+
import 'package:w_common/disposable.dart';
2122
import 'package:w_flux/w_flux.dart';
2223
import 'package:w_module/w_module.dart';
2324

@@ -27,12 +28,16 @@ class DataLoadAsyncModule extends Module {
2728

2829
DataLoadAsyncActions _actions;
2930
DataLoadAsyncComponents _components;
30-
DataLoadAsyncStore _stores;
31+
DataLoadAsyncEvents _events;
32+
DataLoadAsyncStore _store;
3133

3234
DataLoadAsyncModule() {
3335
_actions = new DataLoadAsyncActions();
34-
_stores = new DataLoadAsyncStore(_actions);
35-
_components = new DataLoadAsyncComponents(_actions, _stores);
36+
_events = new DataLoadAsyncEvents();
37+
_store = new DataLoadAsyncStore(_actions, _events);
38+
_components = new DataLoadAsyncComponents(_actions, _store);
39+
40+
<Disposable>[_events, _store].forEach(manageDisposable);
3641
}
3742

3843
@override
@@ -42,6 +47,8 @@ class DataLoadAsyncModule extends Module {
4247
@protected
4348
Future<Null> onLoad() {
4449
// trigger non-blocking async load of data
50+
listenToStream(_events.didLoadData.take(1),
51+
(_) => specifyStartupTiming(StartupTimingType.firstUseful));
4552
_actions.loadData();
4653
return new Future.value();
4754
}
@@ -62,15 +69,23 @@ class DataLoadAsyncActions {
6269
final Action loadData = new Action();
6370
}
6471

72+
DispatchKey _dispatchKey = new DispatchKey('DataLoadAsync');
73+
74+
class DataLoadAsyncEvents extends EventsCollection {
75+
final Event didLoadData = new Event(_dispatchKey);
76+
DataLoadAsyncEvents() : super(_dispatchKey) {
77+
manageEvent(new Event(_dispatchKey));
78+
}
79+
}
80+
6581
class DataLoadAsyncStore extends Store {
6682
DataLoadAsyncActions _actions;
67-
List<String> _data;
68-
bool _isLoading;
83+
DataLoadAsyncEvents _events;
84+
List<String> _data = [];
85+
bool _isLoading = false;
6986

70-
DataLoadAsyncStore(this._actions) {
71-
_isLoading = false;
72-
_data = [];
73-
triggerOnAction(_actions.loadData, _loadData);
87+
DataLoadAsyncStore(this._actions, this._events) {
88+
manageActionSubscription(_actions.loadData.listen(_loadData));
7489
}
7590

7691
/// Public data
@@ -89,6 +104,8 @@ class DataLoadAsyncStore extends Store {
89104
// trigger on return of final data
90105
_data = ['Aaron', 'Dustin', 'Evan', 'Jay', 'Max', 'Trent'];
91106
_isLoading = false;
107+
_events.didLoadData(null, _dispatchKey);
108+
trigger();
92109
}
93110
}
94111

example/panel/modules/sample_tracer.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,7 @@ class SampleTracer implements AbstractTracer {
130130
references: references,
131131
startTime: startTime,
132132
tags: tags,
133-
)
134-
..whenFinished.then((span) {
133+
)..whenFinished.then((span) {
135134
print(span.toString());
136135
});
137136
}

lib/src/lifecycle_module.dart

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import 'package:opentracing/opentracing.dart';
2222
import 'package:w_common/disposable.dart';
2323

2424
import 'package:w_module/src/simple_module.dart';
25+
import 'package:w_module/src/timing_specifiers.dart';
2526

2627
/// Possible states a [LifecycleModule] may occupy.
2728
enum LifecycleState {
@@ -54,7 +55,7 @@ enum LifecycleState {
5455
/// unified lifecycle API.
5556
abstract class LifecycleModule extends SimpleModule with Disposable {
5657
static int _nextId = 0;
57-
58+
// Used by tracing to tell apart multiple instances of the same module
5859
int _instanceId = _nextId++;
5960

6061
List<LifecycleModule> _childModules = [];
@@ -64,7 +65,10 @@ abstract class LifecycleModule extends SimpleModule with Disposable {
6465
LifecycleState _state = LifecycleState.instantiated;
6566
Completer<Null> _transition;
6667
Span _activeSpan;
67-
SpanContext _loadSpanContext;
68+
69+
// Used by tracing to create a span if the consumer specifies when the module
70+
// reaches its first useful state
71+
DateTime _startLoadTime;
6872

6973
// Lifecycle event StreamControllers
7074
StreamController<LifecycleModule> _willLoadChildModuleController =
@@ -116,9 +120,7 @@ abstract class LifecycleModule extends SimpleModule with Disposable {
116120
_didUnloadController,
117121
].forEach(manageStreamController);
118122

119-
<
120-
String,
121-
Stream>{
123+
<String, Stream>{
122124
'willLoad': willLoad,
123125
'didLoad': didLoad,
124126
'willLoadChildModule': willLoadChildModule,
@@ -153,6 +155,10 @@ abstract class LifecycleModule extends SimpleModule with Disposable {
153155
@protected
154156
Span get activeSpan => _activeSpan;
155157

158+
/// Set internally by this module for the load span so it can be used as a
159+
/// `Reference` to other spans after the span is finished.
160+
SpanContext _loadContext;
161+
156162
/// Set internally by the parent module if this module is called by [loadChildModule]
157163
SpanContext _parentContext;
158164

@@ -177,16 +183,74 @@ abstract class LifecycleModule extends SimpleModule with Disposable {
177183
references.add(new Reference.followsFrom(_parentContext));
178184
}
179185

180-
return tracer.startSpan('$name.$operationName', references: references)
181-
..addTags({
182-
'module.instanceId': _instanceId,
183-
});
186+
return tracer.startSpan(
187+
'$name.$operationName',
188+
references: references,
189+
tags: _defaultTags,
190+
);
191+
}
192+
193+
/// Creates a span with `globalTracer` from the start of [load] until now.
194+
///
195+
/// This span is intended to represent the time it takes for the module to
196+
/// finish asynchronously loading any necessary data and entering a state which
197+
/// is ready for user interaction.
198+
///
199+
/// Any [tags] or [references] specified will be added to this span.
200+
@protected
201+
void specifyFirstUsefulState({
202+
Map<String, dynamic> tags: const {},
203+
List<Reference> references: const [],
204+
}) =>
205+
specifyStartupTiming(
206+
StartupTimingType.firstUseful,
207+
tags: tags,
208+
references: references,
209+
);
210+
211+
/// Creates a span with `globalTracer` from the start of [load] until now.
212+
///
213+
/// The [specifier] indicates the purpose of this span.
214+
///
215+
/// Any [tags] or [references] specified will be added to this span.
216+
@protected
217+
void specifyStartupTiming(
218+
StartupTimingType specifier, {
219+
Map<String, dynamic> tags: const {},
220+
List<Reference> references: const [],
221+
}) {
222+
// Load didn't start
223+
if (_loadContext == null || _startLoadTime == null) {
224+
throw new StateError(
225+
'Calling `specifyStartupTiming` before calling `load()`');
226+
}
227+
228+
final tracer = globalTracer();
229+
if (tracer == null) {
230+
return null;
231+
}
232+
233+
tracer
234+
.startSpan(
235+
'$name.${specifier.operationName}',
236+
references: [tracer.followsFrom(_loadContext)]..addAll(references),
237+
startTime: _startLoadTime,
238+
tags: _defaultTags..addAll(tags),
239+
)
240+
.finish();
241+
242+
_startLoadTime = null;
184243
}
185244

186245
/// Name of the module for identification in exceptions and debug messages.
187246
// ignore: unnecessary_getters_setters
188247
String get name => _defaultName;
189248

249+
Map<String, dynamic> get _defaultTags => {
250+
'span.kind': 'client',
251+
'module.instance_id': _instanceId,
252+
};
253+
190254
/// Deprecated: the module name should be defined by overriding the getter in
191255
/// a subclass and it should not be mutable.
192256
@deprecated
@@ -377,7 +441,8 @@ abstract class LifecycleModule extends SimpleModule with Disposable {
377441
}
378442

379443
_activeSpan = _startTransitionSpan('load');
380-
_loadSpanContext = _activeSpan?.context;
444+
_loadContext = _activeSpan?.context;
445+
_startLoadTime = _activeSpan?.startTime;
381446

382447
_state = LifecycleState.loading;
383448

@@ -455,7 +520,7 @@ abstract class LifecycleModule extends SimpleModule with Disposable {
455520

456521
try {
457522
_childModules.add(childModule);
458-
childModule._parentContext = _loadSpanContext;
523+
childModule._parentContext = _loadContext;
459524

460525
await childModule.load();
461526
await onDidLoadChildModule(childModule);

lib/src/timing_specifiers.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/// The type of 'startup timing metric' to be used by `specifyStartupTiming`
2+
class StartupTimingType {
3+
/// The `operationName` to be used for spans created using this [StartupTimingType].
4+
final String operationName;
5+
6+
const StartupTimingType._(this.operationName);
7+
8+
/// Specifies that the module finished loading necessary data and is ready for user interaction.
9+
static const StartupTimingType firstUseful =
10+
const StartupTimingType._('entered_first_useful_state');
11+
}

lib/w_module.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ export 'package:w_module/src/events_collection.dart';
2626
export 'package:w_module/src/lifecycle_module.dart' hide LifecycleState;
2727
export 'package:w_module/src/module.dart';
2828
export 'package:w_module/src/simple_module.dart';
29+
export 'package:w_module/src/timing_specifiers.dart';

pubspec.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ homepage: https://github.com/Workiva/w_module
1313
dependencies:
1414
logging: ^0.11.0
1515
meta: ^1.0.0
16-
opentracing: ^0.2.1
16+
opentracing: ^0.2.3
1717
platform_detect: ^1.1.0
1818
w_common: ^1.9.0
1919

2020
dev_dependencies:
2121
browser: ^0.10.0+2
2222
coverage: ^0.7.3
2323
dart_dev: ^1.8.0
24-
dart_style: ^0.2.16
25-
dartdoc: ^0.9.0
24+
dart_style: ^1.0.7
25+
dartdoc: ">=0.13.0 <0.16.0"
2626
mockito: ^1.0.1
2727
react: ^3.0.0
2828
test: ^0.12.0

smithy.yaml

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)