// Copyright (c) 2017, 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. /// This file supports profiling dynamic calls. part of "dart:_debugger"; class _MethodStats { final String typeName; final String frame; double count = 0.0; _MethodStats(this.typeName, this.frame); } class _CallMethodRecord { var jsError; var type; _CallMethodRecord(this.jsError, this.type); } /// Size for the random sample of dynamic calls. int _callRecordSampleSize = 5000; /// If the number of dynamic calls exceeds [_callRecordSampleSize] this list /// will represent a random sample of the dynamic calls made. var _callMethodRecords = <_CallMethodRecord>[]; /// If the number of dynamic calls exceeds [_callRecordSampleSize] this value /// will be greater than [_callMethodRecords.length]. int _totalCallRecords = 0; /// Minimum number of samples to consider a profile entry relevant. /// This could be set a lot higher. We set this value so users are not /// confused into thinking that a dynamic call that occurred once but was /// randomly included in the sample is relevant. num _minCount = 2; /// Cache mapping from raw stack frames to source mapped stack frames to /// speedup lookup of source map frames when running the profiler. /// The number of source map entries looked up makes caching more important /// in this case than for typical source map use cases. var _frameMappingCache = {}; List> getDynamicStats() { // Process the accumulated method stats. This may be quite slow as processing // stack traces is expensive. If there are performance blockers, we should // switch to a sampling approach that caps the number of _callMethodRecords // and uses random sampling to decide whether to add each additional record // to the sample. Main change required is that we need to still show the total // raw number of dynamic calls so that the magnitude of the dynamic call // performance hit is clear to users. var callMethodStats = {}; if (_callMethodRecords.length > 0) { // Ratio between total record count and sampled records count. var recordRatio = _totalCallRecords / _callMethodRecords.length; for (var record in _callMethodRecords) { String stackStr = JS('!', '#.stack', record.jsError); var frames = stackStr.split('\n'); // Skip first two lines as the first couple frames are from the dart // runtime. var src = frames .skip(2) .map( (f) => _frameMappingCache.putIfAbsent( f, () => stackTraceMapper!('\n$f'), ), ) .firstWhere((f) => !f.startsWith('dart:'), orElse: () => ''); var actualTypeName = dart.typeName(record.type); callMethodStats .putIfAbsent( "$actualTypeName <$src>", () => _MethodStats(actualTypeName, src), ) .count += recordRatio; } // filter out all calls that did not occur at least _minCount times in the // random sample if we are dealing with a random sample instead of a // complete profile. if (_totalCallRecords != _callMethodRecords.length) { for (var k in callMethodStats.keys.toList()) { var stats = callMethodStats[k]!; var threshold = _minCount * recordRatio; if (stats.count + 0.001 < threshold) { callMethodStats.remove(k); } } } } _callMethodRecords.clear(); _totalCallRecords = 0; var keys = callMethodStats.keys.toList(); keys.sort( (a, b) => callMethodStats[b]!.count.compareTo(callMethodStats[a]!.count), ); var ret = >[]; for (var key in keys) { var stats = callMethodStats[key]!; ret.add([stats.typeName, stats.frame, stats.count.round()]); } return ret; } clearDynamicStats() { _callMethodRecords.clear(); } // We need to set this property while the sdk is only partially initialized // so we cannot use a regular Dart field. bool get _trackProfile => JS('!', '#.__trackProfile', DART_RUNTIME_LIBRARY()); trackCall(obj) { if (JS('!', '!#', _trackProfile)) return; int index = -1; _totalCallRecords++; if (_callMethodRecords.length == _callRecordSampleSize) { // Ensure that each sample has an equal // _callRecordSampleSize / _totalCallRecords chance of inclusion // by choosing to include the new record in the sample the with the // appropriate probability randomly evicting one of the existing records. // Unfortunately we can't use the excellent Random.nextInt method defined // by Dart from within this library. index = JS('!', 'Math.floor(Math.random() * #)', _totalCallRecords); if (index >= _callMethodRecords.length) return; // don't sample } var record = _CallMethodRecord( JS('', 'new Error()'), dart.getReifiedType(obj), ); if (index == -1) { _callMethodRecords.add(record); } else { _callMethodRecords[index] = record; } }