Dart - 检测未完成的 future

标签 dart memory-leaks future

我有一个调用第三方库的 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查找未被垃圾回收的 ReceivePortRawReceivePort 实例,并查看它们是否仍然存在。


同时注意 runZonedGuarded

由于 runZonedGuarded 引入了一个新的错误区域(因为它在新区域中引入了一个 Uncaught Error 处理程序),因此在该区域内部创建的 future 错误将不会在区域外完成。

这意味着代码:

await runZonedGuarded(() async {

如果 body 抛出将不起作用。 future 的错误由区域而不是 await 处理,因此 await 只会看到一个永远不会完成的 future 。

关于Dart - 检测未完成的 future ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72917939/

相关文章:

flutter - flutter 中的 Future.delayed 与 Timer 有什么区别

css - 如何让 CSS 变量在 Dart 中工作

flutter - 如何在注册前检查模型是否已存在获取?

c# - MemoryFailPoint 始终抛出 InsufficientMemoryException,即使内存可用也是如此

c - C 程序从 bash 脚本运行时出现内存泄漏

Future 的 Scala 命名约定

exception - 使用一个 catch 表达式捕获 Dart 中的多种特定异常类型

flutter - 特定状态下的内部方法不被识别,BloC模式

ios - skmaps 通过更新 SKAnnotation 来解决巨大的内存泄漏

Rust 与 Futures 和 Tokio 并发执行