我有一个调用第三方库的 Dart 控制台应用程序。
当我的控制台应用程序调用第三方库时,对方法的调用返回,但是我的 CLI 应用程序随后“挂起”10 秒左右,然后最终关闭。
我怀疑图书馆有某种类型的资源,它已经创建但尚未关闭/完成。
我最好的猜测是这是一个未完成的 future 。
所以我正在寻找方法来检测尚未释放的资源。
我的第一个停靠点是寻找一种技术来检测尚未完成的 future ,但其他资源类型的解决方案会很有用。
我目前正在使用 runZoneGuarded,传入 ZoneSpecification 以 Hook 调用。
编辑:通过一些实验,我发现我可以检测计时器并取消它们。在一个简单的实验中,我发现未取消的计时器会导致应用程序挂起。如果我取消计时器(在我的 checkLeaks 方法期间),应用程序将关闭,但是,这在我的真实应用程序中还不够,所以我仍在寻找检测其他资源的方法。
这是我的实验代码:
#! /usr/bin/env dcli
import 'dart:async';
import 'package:dcli/dcli.dart';
import 'package:onepub/src/pub/global_packages.dart';
import 'package:onepub/src/pub/system_cache.dart';
import 'package:onepub/src/version/version.g.dart';
import 'package:pub_semver/pub_semver.dart';
void main(List<String> arguments) async {
print(orange('OnePub version: $packageVersion '));
print('');
print(globals);
// await globals.repairActivatedPackages();
await runZonedGuarded(() async {
Timer(Duration(seconds: 20), () => print('timer done'));
unawaited(Future.delayed(Duration(seconds: 20)));
var completer = Completer();
unawaited(
Future.delayed(Duration(seconds: 20), () => completer.complete()));
// await globals.activateHosted(
// 'dcli_unit_tester',
// VersionConstraint.any,
// null, // all executables
// overwriteBinStubs: true,
// url: null, // hostedUrl,
// );
print('end activate');
}, (error, stackTrace) {
print('Uncaught error: $error');
}, zoneSpecification: buildZoneSpec());
print('end');
checkLeaks();
// await entrypoint(arguments, CommandSet.ONEPUB, 'onepub');
}
late final SystemCache cache = SystemCache(isOffline: false);
GlobalPackages? _globals;
GlobalPackages get globals => _globals ??= GlobalPackages(cache);
List<void Function()> actions = [];
List<Source<Timer>> timers = [];
int testCounter = 0;
int timerCount = 0;
int periodicCallbacksCount = 0;
int microtasksCount = 0;
ZoneSpecification buildZoneSpec() {
return ZoneSpecification(
createTimer: (source, parent, zone, duration, f) {
timerCount += 1;
final result = parent.createTimer(zone, duration, f);
timers.add(Source(result));
return result;
},
createPeriodicTimer: (source, parent, zone, period, f) {
periodicCallbacksCount += 1;
final result = parent.createPeriodicTimer(zone, period, f);
timers.add(Source(result));
return result;
},
scheduleMicrotask: (source, parent, zone, f) {
microtasksCount += 1;
actions.add(f);
final result = parent.scheduleMicrotask(zone, f);
return result;
},
);
}
void checkLeaks() {
print(actions.length);
print(timers.length);
print('testCounter $testCounter');
print('timerCount $timerCount');
print('periodicCallbacksCount $periodicCallbacksCount');
print('microtasksCount $microtasksCount');
for (var timer in timers) {
if (timer.source.isActive) {
print('Active Timer: ${timer.st}');
timer.source.cancel();
}
}
}
class Source<T> {
Source(this.source) {
st = StackTrace.current;
}
T source;
late StackTrace st;
}
我是真实世界的测试,我可以看到我确实有由 HTTP 连接引起的挂起计时器。正如我最初猜测的那样,这似乎确实指出了 HTTP 连接未正确关闭的其他一些问题。
Active Timer: #0 new Source (file:///home/bsutton/git/onepub/onepub/bin/onepub.dart:105:21)
#1 buildZoneSpec.<anonymous closure> (file:///home/bsutton/git/onepub/onepub/bin/onepub.dart:68:18)
#2 _CustomZone.createTimer (dart:async/zone.dart:1388:19)
#3 new Timer (dart:async/timer.dart:54:10)
#4 _HttpClientConnection.startTimer (dart:_http/http_impl.dart:2320:18)
#5 _ConnectionTarget.returnConnection (dart:_http/http_impl.dart:2381:16)
#6 _HttpClient._returnConnection (dart:_http/http_impl.dart:2800:41)
#7 _HttpClientConnection.send.<anonymous closure>.<anonymous closure>.<anonymous closure> (dart:_http/http_impl.dart:2171:25)
#8 _rootRunUnary (dart:async/zone.dart:1434:47)
最佳答案
一般来说,不可能找到不会发生的事情。
无法在程序中找到所有的 future。
有了区域,您可能能够截取区域中“注册”的所有回调,但您无法知道必须调用其中的哪些回调。 Future 可以同时具有值处理程序和错误处理程序,并且最多会调用其中一个。因此,仅仅因为未调用 future 的回调,并不意味着 future 没有完成。
不过, future 很可能不会让这个孤立的人活着。 如果没有重要的东西卡在上面,一个未完成的 future 将被垃圾收集。
保持隔离事件的最有可能的罪魁祸首是计时器和接收端口。 (定时器、I/O 和套接字的 VM 内部实现都使用接收端口,所以它实际上只是端口。)
同样,无法以编程方式找到所有打开的端口。 为此,您需要一个带有内存检查工具的调试器。
我建议使用 developer tools查找未被垃圾回收的 ReceivePort
或 RawReceivePort
实例,并查看它们是否仍然存在。
同时注意 runZonedGuarded
。
由于 runZonedGuarded
引入了一个新的错误区域(因为它在新区域中引入了一个 Uncaught Error 处理程序),因此在该区域内部创建的 future 错误将不会在区域外完成。
这意味着代码:
await runZonedGuarded(() async {
如果 body 抛出将不起作用。 future 的错误由区域而不是 await
处理,因此 await
只会看到一个永远不会完成的 future 。
关于Dart - 检测未完成的 future ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72917939/