blob: 6628ba19f40829041c54a9bf180d37cd3381a78f [file] [log] [blame]
// Copyright (c) 2024, 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:developer';
import 'dart:math';
/// Manually instrumented profiling.
///
/// Using a general-purpose sampling profiler is difficult with the formatter
/// because so much of its code is recursive. Manual instrumentation can provide
/// more concise, meaningful data.
class Profile {
/// Whether profiling is enabled.
///
/// Enabling profiling has a noticeable effect on overall performance. When
/// measuring, make sure to have profiling enabled when gathering baseline
/// measurements so that the comparison to the optimized version takes into
/// account the profiling overhead.
///
/// When this is `false`, hopefully the compiler is able to completely
/// tree-shake calls to these methods. This should always be `false` in the
/// committed version of this file.
static const enabled = false;
/// Tracks counts of labelled occurrences.
static final Map<String, int> _counts = {};
/// The total accumulated time from completed calls to [begin()]/[end()] for
/// each label, in microseconds.
static final Map<String, int> _accumulatedTimes = {};
/// Label and start time (in microseconds) of each call to [begin()] that has
/// not had a corresponding call to [end()] yet.
static final List<(String, int)> _running = [];
/// Notes that [label] has occurred one more time.
static void count(String label) {
if (!enabled) return;
_counts.update(label, (count) => count + 1, ifAbsent: () => 1);
}
/// Begins measuring the elapsed time between this call and a subsequent
/// matching call to [end()].
///
/// Works like a stack: A call to [end()] always completes the most recent
/// [begin()].
///
/// Multiple calls to [begin()]/[end()] with the same label accumulate their
/// total time. Time is always inclusive. In other words, the time spent in
/// a subsequent call to [begin()]/[end()] before this call's [end()] is not
/// subtracted from this one.
///
/// Also, no attempt is made to account for re-entrancy, so the accumulated
/// time may be greater than the wall clock time if there are recursive calls
/// that use the same label.
static void begin(String label) {
if (!enabled) return;
// Indent to show nesting of profiled regions.
label = '${' ' * _running.length}$label';
_running.add((label, Timeline.now));
// Add the label eagerly to the map so that labels are printed in the
// order they were began.
_accumulatedTimes.putIfAbsent(label, () => 0);
}
static void end(String _) {
if (!enabled) return;
var (label, start) = _running.removeLast();
var elapsed = Timeline.now - start;
_accumulatedTimes.update(label, (accumulated) => accumulated + elapsed);
}
/// Discards all recorded profiling data.
static void reset() {
if (!enabled) return;
// Shouldn't be in the middle of profiling.
assert(_running.isEmpty);
_counts.clear();
_running.clear();
_accumulatedTimes.clear();
}
/// Prints a report of all recorded profiling information.
static void report() {
if (!enabled) return;
// Should have finished everything by now.
assert(_running.isEmpty);
_showTable('Counts', _counts);
_showTable('Times (ms)', _accumulatedTimes, milliseconds: true);
}
static void _showTable(String header, Map<String, int> data,
{bool milliseconds = false}) {
if (data.isEmpty) return;
print('');
print(header);
var labels = data.keys.toList();
var longestLabel =
labels.fold(0, (length, label) => max(length, label.length));
for (var label in labels) {
String value;
if (milliseconds) {
value = (data[label]! / 1000).toStringAsFixed(3).padLeft(10);
} else {
value = data[label]!.toString().padLeft(8);
}
print('${label.padRight(longestLabel)} = $value');
}
}
}