// 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 = <String, String>{};

List<List<Object>> 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 = <String, _MethodStats>{};
  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<String>('!', '#.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 = <List<Object>>[];
  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<bool>('!', '#.__trackProfile', DART_RUNTIME_LIBRARY());

trackCall(obj) {
  if (JS<bool>('!', '!#', _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<int>('!', '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;
  }
}
