Skip to content

Commit

Permalink
feat!: revise tracing API (#276)
Browse files Browse the repository at this point in the history
- Support installation of `TracingDelegate` seperately from
  initializing CBL Dart, through `TracingDelegate.install`.

- Allow worker `TracingDelegate` to initialize itself in the
  worker isolate.
  • Loading branch information
blaugold committed Jan 27, 2022
1 parent 1bf6143 commit 22c2b5b
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 71 deletions.
16 changes: 4 additions & 12 deletions packages/cbl/lib/src/couchbase_lite.dart
Expand Up @@ -4,7 +4,6 @@ import 'database/database.dart';
import 'log.dart';
import 'support/ffi.dart';
import 'support/isolate.dart';
import 'tracing.dart';

export 'support/ffi.dart' show LibrariesConfiguration, LibraryConfiguration;
export 'support/listener_token.dart' show ListenerToken;
Expand All @@ -18,14 +17,8 @@ class CouchbaseLite {
CouchbaseLite._();

/// Initializes the `cbl` package, for the main isolate.
static void init({
required LibrariesConfiguration libraries,
TracingDelegate? tracingDelegate,
}) {
initMainIsolate(IsolateContext(
libraries: libraries,
tracingDelegate: tracingDelegate,
));
static void init({required LibrariesConfiguration libraries}) {
initPrimaryIsolate(IsolateContext(libraries: libraries));

_setupLogging();
}
Expand All @@ -34,8 +27,7 @@ class CouchbaseLite {
/// [Isolate].
///
/// This object can be safely passed from one [Isolate] to another.
static Object get context =>
IsolateContext.instance.createSecondaryIsolateContext();
static Object get context => IsolateContext.instance;

/// Initializes the `cbl` package, for a secondary isolate.
///
Expand All @@ -45,7 +37,7 @@ class CouchbaseLite {
throw ArgumentError.value(context, 'context', 'is invalid');
}

initIsolate(context);
initSecondaryIsolate(context);
}
}

Expand Down
6 changes: 3 additions & 3 deletions packages/cbl/lib/src/service/cbl_worker.dart
Expand Up @@ -64,7 +64,7 @@ class CblWorker {
_worker = IsolateWorker(
debugName: 'CblWorker($debugName)',
delegate: _ServiceWorkerDelegate(
context: IsolateContext.instance.createSecondaryIsolateContext(),
context: IsolateContext.instance.createForWorkerIsolate(),
serializationType: serializationTarget,
channel: receivePort.sendPort,
),
Expand Down Expand Up @@ -125,8 +125,8 @@ class _ServiceWorkerDelegate extends IsolateWorkerDelegate {
late final CblService _service;

@override
FutureOr<void> initialize() {
initIsolate(
FutureOr<void> initialize() async {
await initWorkerIsolate(
context,
onTraceData: (data) => _service.channel.call(TraceDataRequest(data)),
);
Expand Down
52 changes: 36 additions & 16 deletions packages/cbl/lib/src/support/isolate.dart
Expand Up @@ -21,8 +21,8 @@ class IsolateContext {
IsolateContext({
required this.libraries,
this.initContext,
TracingDelegate? tracingDelegate,
}) : tracingDelegate = tracingDelegate ?? const NoopTracingDelegate();
this.tracingDelegate,
});

static IsolateContext? _instance;

Expand All @@ -43,32 +43,52 @@ class IsolateContext {

final LibrariesConfiguration libraries;
final InitContext? initContext;
final TracingDelegate tracingDelegate;
final TracingDelegate? tracingDelegate;

IsolateContext createSecondaryIsolateContext() => IsolateContext(
IsolateContext createForWorkerIsolate() => IsolateContext(
libraries: libraries,
tracingDelegate:
effectiveTracingDelegate.createSecondaryIsolateDelegate(),
tracingDelegate: effectiveTracingDelegate.createWorkerDelegate(),
);
}

/// Initializes this isolate for use of Couchbase Lite.
void initIsolate(IsolateContext context, {TraceDataHandler? onTraceData}) {
/// Initializes this isolate for use of Couchbase Lite, and initializes the
/// native libraries.
void initPrimaryIsolate(IsolateContext context) {
_initIsolate(context);
cblBindings.base.initializeNativeLibraries(context.initContext?.toCbl());
}

/// Initializes this isolate for use of Couchbase Lite, after another primary
/// isolate has been initialized.
void initSecondaryIsolate(IsolateContext context) {
_initIsolate(context);
}

/// Initializes this isolate for use as a Couchbase Lite worker isolate.
Future<void> initWorkerIsolate(
IsolateContext context, {
required TraceDataHandler onTraceData,
}) async {
_initIsolate(context, onTraceData: onTraceData);

final tracingDelegate = context.tracingDelegate;
if (tracingDelegate != null) {
TracingDelegate.install(tracingDelegate);
await tracingDelegate.initializeWorkerDelegate();
}
}

void _initIsolate(IsolateContext context, {TraceDataHandler? onTraceData}) {
IsolateContext.instance = context;

CBLBindings.init(
context.libraries.toCblFfi(),
onTracedCall: tracingDelegateTracedNativeCallHandler,
);

MDelegate.instance = CblMDelegate();
effectiveTracingDelegate = context.tracingDelegate;

_onTraceData = onTraceData;
}

set _onTraceData(TraceDataHandler? value) => onTraceData = value;

/// Initializes this isolate for use of Couchbase Lite, and initializes the
/// native libraries.
void initMainIsolate(IsolateContext context) {
initIsolate(context);
cblBindings.base.initializeNativeLibraries(context.initContext?.toCbl());
}
80 changes: 57 additions & 23 deletions packages/cbl/lib/src/tracing.dart
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:meta/meta.dart';

import 'database.dart';
Expand All @@ -22,20 +24,24 @@ late final TraceDataHandler? _onTraceData = onTraceData;
/// See [TracedOperation] and its subclasses for all operations can can be
/// traced.
///
/// # Primary and secondary isolates
/// # User and worker isolates
///
/// User isolates are isolates in which CBL Dart is used but that are not
/// created by CBL Dart.
/// For every user isolate, a [TracingDelegate] can be installed through
/// [install].
///
/// Every isolate in which CBL Dart is used has one [TracingDelegate], which
/// cannot be changed. When initializing CBL Dart, a [TracingDelegate] can
/// be provided, which will become the delegate for the current (primary)
/// isolate. Each time CBL Dart creates background (secondary) isolates,
/// [createSecondaryIsolateDelegate] is called on the primary isolate delegate
/// and the returned delegate is used as the delegate for the new isolate.
/// Each time CBL Dart creates a worker isolate, [createWorkerDelegate] is
/// called on the user isolate delegate and the returned delegate is used as
/// the delegate for the new isolate.
/// Worker isolate delegates get the chance to initialize themselves in
/// [initializeWorkerDelegate].
///
/// ## Tracing context
///
/// When a primary isolate sends a message to a secondary isolate, the
/// primary isolate's [TracingDelegate] can provide a tracing context, which
/// is sent to the secondary isolate along with the message. When a secondary
/// When a user isolate sends a message to a worker isolate, the
/// user isolate's [TracingDelegate] can provide a tracing context, which
/// is sent to the worker isolate along with the message. When a worker
/// isolate receives a message, it's delegate can restore the tracing context.
/// Typically, the tracing context is stored in a zone value and
/// [captureTracingContext] and [restoreTracingContext] are used to transfer
Expand All @@ -44,8 +50,8 @@ late final TraceDataHandler? _onTraceData = onTraceData;
///
/// ## Trace data
///
/// A delegate in a secondary isolate can send arbitrary data through
/// [sendTraceData] to the delegate in the primary isolate, which will receive
/// A delegate in a worker isolate can send arbitrary data through
/// [sendTraceData] to the delegate in the user isolate, which will receive
/// the data through a call to [onTraceData]. The data has to be JSON
/// serializable.
///
Expand All @@ -54,21 +60,49 @@ abstract class TracingDelegate {
/// Const constructor for subclasses.
const TracingDelegate();

/// Creates a new [TracingDelegate], to be used for a secondary isolate,
/// Whether a [TracingDelegate] has been installed for this isolate.
///
/// Delegates can be installed through [install].
static bool get hasBeenInstalled => _hasBeenInstalled;
static bool _hasBeenInstalled = false;

/// Installs a [TracingDelegate] for the current isolate.
///
/// A [TracingDelegate] can only be installed once per isolate and cannot
/// be changed. Whether a [TracingDelegate] has been installed for this
/// isolate can be checked with [hasBeenInstalled].
static void install(TracingDelegate delegate) {
if (_hasBeenInstalled) {
throw StateError('A TracingDelegate has already been installed.');
}
_hasBeenInstalled = true;
effectiveTracingDelegate = delegate;
}

/// Creates a new [TracingDelegate] to be used for a worker isolate,
/// which is about to be created by the current isolate.
///
/// The returned object must be able to be passed from the current isolate to
/// to the secondary isolate.
/// the worker isolate.
///
/// The default implementation returns `this`.
// ignore: avoid_returning_this
TracingDelegate createSecondaryIsolateDelegate() => this;
TracingDelegate createWorkerDelegate() => this;

/// Called when this delegate is about to be used in a worker isolate.
///
/// This allows a worker delegate to initialize itself and its environment.
///
/// [sendTraceData] cannot be called from this method.
///
/// Delegates for a user isolate must already be initialized.
FutureOr<void> initializeWorkerDelegate() {}

/// Allows this delegate to send arbitrary data to the delegate in its
/// primary isolate.
/// Allows this delegate to send arbitrary data to the delegate it was
/// created by.
///
/// The [data] must be JSON serializable and this delegate must be in a
/// secondary isolate.
/// worker isolate.
@protected
@mustCallSuper
void sendTraceData(Object? data) {
Expand All @@ -78,21 +112,21 @@ abstract class TracingDelegate {
_onTraceData!(data);
}

/// Callback for receiving trace data from delegates in secondary isolates.
/// Callback for receiving trace data from delegates in worker isolates.
///
/// When a delegate in a secondary isolate calls [sendTraceData], the data
/// is sent to the primary isolate, which calls this callback.
/// When a delegate in a worker isolate calls [sendTraceData], the data
/// is sent to the user isolate, which calls this callback.
@visibleForOverriding
void onTraceData(Object? data) {}

/// Returns the current tracing context and is called just before a message
/// is sent from a primary to a secondary isolate.
/// is sent from an user to a worker isolate.
///
/// The returned value must be JSON serializable.
Object? captureTracingContext() => null;

/// Restores the tracing context and is called just after a message from a
/// secondary is received a a secondary isolate.
/// user isolate is received by a worker isolate.
///
/// The provided [context] is the value that was returned by
/// [captureTracingContext], when the message was sent.
Expand Down
4 changes: 1 addition & 3 deletions packages/cbl_dart/lib/cbl_dart.dart
Expand Up @@ -33,7 +33,6 @@ class CouchbaseLiteDart {
required Edition edition,
String? filesDir,
String? nativeLibrariesDir,
TracingDelegate? tracingDelegate,
}) async {
final context = filesDir == null ? null : await _initContext(filesDir);

Expand All @@ -42,10 +41,9 @@ class CouchbaseLiteDart {
mergedNativeLibrariesDir: nativeLibrariesDir,
);

initMainIsolate(IsolateContext(
initPrimaryIsolate(IsolateContext(
initContext: context,
libraries: libraries,
tracingDelegate: tracingDelegate,
));

_setupLogging();
Expand Down
2 changes: 1 addition & 1 deletion packages/cbl_e2e_tests/lib/src/service/channel_test.dart
Expand Up @@ -284,7 +284,7 @@ class TestIsolateConfig {
}

void testIsolateMain(TestIsolateConfig config) {
initIsolate(config.context);
initSecondaryIsolate(config.context);

final remote = Channel(
transport: IsolateChannel.connectSend(config.sendPort!),
Expand Down
42 changes: 32 additions & 10 deletions packages/cbl_e2e_tests/lib/src/tracing_test.dart
Expand Up @@ -12,14 +12,14 @@ void main() {

group('tracing', () {
final originalTracingDelegate = effectiveTracingDelegate;
late TestTracingDelegate delegate;
late TestDelegate delegate;

tearDownAll(() {
effectiveTracingDelegate = originalTracingDelegate;
});

setUp(() {
effectiveTracingDelegate = delegate = TestTracingDelegate();
effectiveTracingDelegate = delegate = TestDelegate();
});

test('trace sync operation', () {
Expand All @@ -40,13 +40,28 @@ void main() {
});

test('send and receive trace data', () async {
delegate.secondaryIsolateDelegate.traceData = 'data';
delegate.workerDelegate.traceData = 'data';
await openAsyncTestDatabase(usePublicApi: true);

expect(delegate.traceData, ['data', 'data']);
});

test('capture and restore tracing context', () async {
test('worker delegate is initialized', () async {
delegate.workerDelegate.initializeTraceData = 'init';

await openAsyncTestDatabase(usePublicApi: true);

expect(delegate.traceData, ['init', 'init']);
});

test('worker delegate can send trace data', () async {
delegate.workerDelegate.traceData = 'data';
await openAsyncTestDatabase(usePublicApi: true);

expect(delegate.traceData, ['data', 'data']);
});

test('user delegate can provide tracing context', () async {
delegate.tracingContext = 'context';
await openAsyncTestDatabase(usePublicApi: true);

Expand All @@ -55,13 +70,11 @@ void main() {
});
}

class TestTracingDelegate extends TracingDelegate {
TestSecondaryIsolateDelegate secondaryIsolateDelegate =
TestSecondaryIsolateDelegate();
class TestDelegate extends TracingDelegate {
TestWorkerDelegate workerDelegate = TestWorkerDelegate();

@override
TestSecondaryIsolateDelegate createSecondaryIsolateDelegate() =>
secondaryIsolateDelegate;
TestWorkerDelegate createWorkerDelegate() => workerDelegate;

final List<Object?> traceData = [];

Expand Down Expand Up @@ -94,9 +107,18 @@ class TestTracingDelegate extends TracingDelegate {
}
}

class TestSecondaryIsolateDelegate extends TracingDelegate {
class TestWorkerDelegate extends TracingDelegate {
Object? initializeTraceData;

Object? traceData;

@override
FutureOr<void> initializeWorkerDelegate() {
if (initializeTraceData != null) {
traceData = initializeTraceData;
}
}

@override
void restoreTracingContext(Object? context, void Function() restore) {
if (context != null) {
Expand Down

0 comments on commit 22c2b5b

Please sign in to comment.