blob: 9c702ff39091e94662967ae41f37d021febae7f5 [file] [log] [blame]
// Copyright (c) 2022, 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 'dart:math';
import 'package:clock/clock.dart';
import 'package:meta/meta.dart';
import '../shared/shared_model.dart';
import '_leak_filter.dart';
import '_object_record.dart';
import '_object_records.dart';
import '_primitives/_finalizer.dart';
import '_primitives/_gc_counter.dart';
import '_primitives/_retaining_path/_connection.dart';
import '_primitives/_retaining_path/_retaining_path.dart';
import '_primitives/model.dart';
/// Keeps collection of object records until
/// disposal and garbage gollection.
///
/// If disposal and garbage collection happened abnormally,
/// marks the object as leaked.
class ObjectTracker implements LeakProvider {
/// The optional parameters are injected for testing purposes.
ObjectTracker({
required this.disposalTime,
required this.numberOfGcCycles,
required this.maxRequestsForRetainingPath,
required this.switches,
FinalizerBuilder? finalizerBuilder,
GcCounter? gcCounter,
}) {
finalizerBuilder ??= buildStandardFinalizer;
_finalizer = finalizerBuilder(_onObjectGarbageCollected);
_gcCounter = gcCounter ?? GcCounter();
}
/// Time to allow the disposal invoker to release the reference to the object.
final Duration disposalTime;
late FinalizerWrapper _finalizer;
late GcCounter _gcCounter;
final _objects = ObjectRecords();
late final LeakFilter _leakFilter = LeakFilter(switches);
bool disposed = false;
final int numberOfGcCycles;
final int? maxRequestsForRetainingPath;
final Switches switches;
void startTracking(
Object object, {
required Map<String, dynamic>? context,
required String trackedClass,
required PhaseSettings phase,
}) {
throwIfDisposed();
if (phase.isLeakTrackingPaused || switches.isObjectTrackingDisabled) return;
final record =
_objects.notGCed.putIfAbsent(object, context, phase, trackedClass);
if (phase.leakDiagnosticConfig.collectStackTraceOnStart) {
record.setContext(ContextKeys.startCallstack, StackTrace.current);
}
_finalizer.attach(object, record);
_objects.assertRecordIntegrity(record);
}
void _onObjectGarbageCollected(Object record) {
if (disposed) return;
if (record is! ObjectRecord) throw 'record should be $ObjectRecord.';
_objects.assertRecordIntegrity(record);
record.setGCed(_gcCounter.gcCount, clock.now());
if (record.isGCedLateLeak(disposalTime, numberOfGcCycles)) {
_objects.gcedLateLeaks.add(record);
} else if (record.isNotDisposedLeak) {
_objects.gcedNotDisposedLeaks.add(record);
}
_objects.notGCed.remove(record);
_objects.notGCedDisposedOk.remove(record);
_objects.notGCedDisposedLate.remove(record);
_objects.notGCedDisposedLateCollected.remove(record);
_objects.assertRecordIntegrity(record);
}
void dispatchDisposal(
Object object, {
required Map<String, dynamic>? context,
}) {
throwIfDisposed();
if (switches.isObjectTrackingDisabled) return;
final record = _objects.notGCed.record(object);
// If object is not registered, this may mean that it was created when leak tracking was off.
if (record == null || record.phase.isLeakTrackingPaused) return;
record.mergeContext(context);
if (record.phase.leakDiagnosticConfig.collectStackTraceOnDisposal) {
record.setContext(ContextKeys.disposalCallstack, StackTrace.current);
}
_objects.assertRecordIntegrity(record);
record.setDisposed(_gcCounter.gcCount, clock.now());
_objects.notGCedDisposedOk.add(record);
_objects.assertRecordIntegrity(record);
}
void addContext(Object object, {required Map<String, dynamic>? context}) {
throwIfDisposed();
if (switches.isObjectTrackingDisabled) return;
final record = _objects.notGCed.record(object);
// If object is not registered, this may mean that it was created when leak tracking was off.
if (record == null || record.phase.isLeakTrackingPaused) return;
record.mergeContext(context);
}
@override
Future<LeakSummary> leaksSummary() async {
throwIfDisposed();
await _checkForNewNotGCedLeaks(summary: true);
return LeakSummary({
LeakType.notDisposed: _objects.gcedNotDisposedLeaks.length,
LeakType.notGCed: _objects.notGCedDisposedLate.length,
LeakType.gcedLate: _objects.gcedLateLeaks.length,
});
}
Future<void> _checkForNewNotGCedLeaks({bool summary = false}) async {
_objects.assertIntegrity();
final List<ObjectRecord>? objectsToGetPath = summary ? null : [];
final now = clock.now();
for (ObjectRecord record
in _objects.notGCedDisposedOk.toList(growable: false)) {
if (record.isNotGCedLeak(
_gcCounter.gcCount,
now,
disposalTime,
numberOfGcCycles,
)) {
_objects.notGCedDisposedOk.remove(record);
_objects.notGCedDisposedLate.add(record);
if (record.phase.leakDiagnosticConfig.collectRetainingPathForNotGCed) {
objectsToGetPath?.add(record);
}
}
}
await processIfNeeded(
items: objectsToGetPath,
limit: maxRequestsForRetainingPath,
processor: _addRetainingPath,
);
_objects.assertIntegrity();
}
/// Runs [processor] for first items from [items], at most [limit] items will be processed.
///
/// Noop if [items] is null or empty.
/// Processes all items if [limit] is null.
@visibleForTesting
static Future<void> processIfNeeded<T>({
required List<T>? items,
required int? limit,
required Future<void> Function(List<T>) processor,
}) async {
if (items == null || items.isEmpty) return;
if (limit != null) {
items = items.sublist(
0,
min(limit, items.length),
);
}
await processor(items);
}
Future<void> _addRetainingPath(List<ObjectRecord> objectsToGetPath) async {
final connection = await connect();
final pathSetters = objectsToGetPath.map((record) async {
final path = await retainingPath(connection, record.ref.target);
if (path != null) {
record.setContext(ContextKeys.retainingPath, path);
}
});
await Future.wait(
pathSetters,
eagerError: true,
);
}
@override
Future<void> checkNonGCed() async {
throwIfDisposed();
await _checkForNewNotGCedLeaks();
}
@override
Future<Leaks> collectLeaks() async {
throwIfDisposed();
await _checkForNewNotGCedLeaks();
final result = Leaks({
LeakType.notDisposed: _objects.gcedNotDisposedLeaks
.where(
(record) => _leakFilter.shouldReport(LeakType.notDisposed, record),
)
.map((record) => record.toLeakReport())
.toList(),
LeakType.notGCed: _objects.notGCedDisposedLate
.where(
(record) => _leakFilter.shouldReport(
LeakType.notGCed,
record,
),
)
.map((record) => record.toLeakReport())
.toList(),
LeakType.gcedLate: _objects.gcedLateLeaks
.where((record) => _leakFilter.shouldReport(LeakType.notGCed, record))
.map((record) => record.toLeakReport())
.toList(),
});
_objects.notGCedDisposedLateCollected.addAll(_objects.notGCedDisposedLate);
_objects.notGCedDisposedLate.clear();
_objects.gcedNotDisposedLeaks.clear();
_objects.gcedLateLeaks.clear();
return result;
}
void throwIfDisposed() {
if (disposed) throw StateError('The disposed instance should not be used.');
}
void dispose() {
throwIfDisposed();
disposed = true;
}
int get notGCed => _objects.notGCed.length;
}