blob: 8bcf24cd1429ea08fd1714f14b2919a5d08855a2 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:js/js_util.dart';
import '../ui/fake_flutter/dart_ui/dart_ui.dart';
import '../ui/flutter_html_shim.dart';
import '../ui/plotly.dart';
import '../ui/theme.dart';
import 'memory_chart.dart';
class MemoryPlotly {
MemoryPlotly(this._domName, this._memoryChart);
static const String fontFamily = 'sans-serif';
final String _domName;
final MemoryChart _memoryChart;
// We're going to dynamically add the Event timeline chart to our memory
// profiler chart.
EventTimeline eventTimeline;
AxisLayout getXAxisLayout([int startTime = -1, int endTime = -1]) {
return AxisLayout(
type: 'date',
tickformat: '%-I:%M:%S %p',
hoverformat: '%H:%M:%S.%L %p',
titlefont: Font(
family: fontFamily,
color: colorToCss(defaultForeground),
),
tickfont: Font(
family: fontFamily,
color: colorToCss(defaultForeground),
),
showgrid: true,
gridcolor: colorToCss(defaultForeground.withAlpha(50)),
gridwidth: 1,
range: startTime == -1 ? [] : [startTime, endTime],
rangeslider: startTime == -1
? RangeSlider()
: RangeSlider(
autorange: true,
),
);
}
Layout getMemoryLayout(String chartTitle, [bool addEventTimeline = false]) {
Layout layout;
AxisLayout getYAxis(List<num> range) {
return AxisLayout(
domain: range,
titlefont: Font(
family: fontFamily,
color: colorToCss(defaultForeground),
),
fixedrange: true,
tickfont: Font(
family: fontFamily,
color: colorToCss(defaultForeground),
),
showgrid: false,
zeroline: false,
);
}
Legend getLegend([bool events = false]) {
return events
? Legend(
font: Font(
family: fontFamily,
color: colorToCss(defaultForeground),
),
orientation: 'v',
x: 1.03,
xanchor: 'left',
y: 1.1,
)
: Legend(
font: Font(
family: fontFamily,
color: colorToCss(defaultForeground),
),
);
}
final Margin margins = Margin(l: 80, r: 5, b: 5, t: 5, pad: 5);
if (addEventTimeline) {
layout = Layout(
plot_bgcolor: colorToCss(chartBackground),
paper_bgcolor: colorToCss(chartBackground),
title: chartTitle,
xaxis: getXAxisLayout(),
yaxis: getYAxis([0, 0.90]),
yaxis2: AxisLayout(
domain: [.90, 1],
anchor: 'y',
side: 'right',
showgrid: false,
zeroline: false,
showline: false,
ticks: '',
showticklabels: false,
range: [.50, 1.50],
type: 'linear',
title: Title(
text: 'Events',
font: Font(
family: fontFamily,
color: colorToCss(defaultForeground),
size: 10,
),
),
),
legend: getLegend(true),
margin: margins,
shapes: [
// Background of event timeline subplot
Shape(
fillcolor: '#ccc',
line: Line(
width: 0,
),
opacity: .5,
type: 'rect',
xref: 'paper',
x0: 0,
x1: 1,
yref: 'y2',
y0: 0,
y1: 2,
layer: 'below',
),
],
);
} else {
layout = Layout(
plot_bgcolor: colorToCss(chartBackground),
paper_bgcolor: colorToCss(chartBackground),
title: chartTitle,
legend: getLegend(),
xaxis: getXAxisLayout(),
yaxis: getYAxis([]),
margin: margins);
}
return layout;
}
static const int MEMORY_GC_TRACE = 0;
static const int MEMORY_EXTERNAL_TRACE = 1;
static const int MEMORY_USED_TRACE = 2;
static const int MEMORY_CAPACITY_TRACE = 3;
static const int MEMORY_RSS_TRACE = 4;
// Orange 50 - light is Orange 600, dark is Orange 900
static String rssColor =
colorToCss(const ThemedColor(Color(0xFFFB8C00), Color(0xFFE65100)));
// Blue Gray 50 - list is Blue 700, dark is Blue 100
static String capacityColor =
colorToCss(const ThemedColor(Color(0xFF455A64), Color(0xFFCFD8DC)));
// Light Blue 50 - light is Blue 800, dark is Blue 900
static String externalColor =
colorToCss(const ThemedColor(Color(0xFF0277BD), Color(0xFF01579B)));
// Light Blue 50 - light is Blue 300, dark is Blue 400
static String usedColor =
colorToCss(const ThemedColor(Color(0xFF4FC3F7), Color(0xFF29B6F6)));
// Light Blue 50 - light is Blue 500, dark is Blue 600
static String gcColor =
colorToCss(const ThemedColor(Color(0xFF03A9F4), Color(0xFF039BE5)));
Data _createTrace(
String name, {
String color,
String group,
String dash,
String symbol,
int size,
}) {
int widthValue = 0;
String modeName = '';
Line line;
if (color != null) {
if (dash != null) {
widthValue = 2;
modeName = 'lines';
}
line = Line(
color: color,
dash: dash,
width: widthValue,
);
}
Marker marker;
if (symbol != null) {
marker = Marker(
symbol: symbol,
color: color,
size: size,
);
modeName = 'markers';
}
if (marker == null) {
return Data(
// Null is needed so the trace legend entry appears w/o data.
x: [Null],
y: [Null],
text: [],
line: line,
type: 'scatter',
mode: modeName,
stackgroup: group != null ? 'one' : '',
name: name,
hoverinfo: 'y+name',
);
} else {
return Data(
// Null is needed so the trace legend entry appears w/o data.
x: [Null],
y: [Null],
text: [],
marker: marker,
type: 'scatter',
mode: modeName,
stackgroup: group != null ? 'one' : '',
name: name,
hoverinfo: 'y+name',
);
}
}
List<Data> createMemoryTraces() {
final Data gcTrace = _createTrace(
'GC',
color: gcColor,
symbol: 'circle',
size: 10,
);
gcTrace.hoverinfo = 'x+name';
final Data externalTrace = _createTrace(
'External',
color: externalColor,
group: 'one',
);
final Data usedTrace = _createTrace(
'Used',
color: usedColor,
group: 'one',
);
final Data capacityTrace = _createTrace(
'Capacity',
color: capacityColor,
dash: 'dot',
);
final Data rssTrace = _createTrace(
'RSS',
color: rssColor,
dash: 'dash',
);
rssTrace.visible = 'legendonly';
return [gcTrace, externalTrace, usedTrace, capacityTrace, rssTrace];
}
// Resetting to live view, it's an autoscale back to full view.
void _doubleClick(DataEvent data) => _memoryChart.resume();
void plotMemory([bool createEventTimeline = false]) {
final List<Data> memoryTraces = createMemoryTraces();
if (createEventTimeline) {
eventTimeline = EventTimeline(_domName, _memoryChart.element);
eventTimeline.addEventTimelineTo(memoryTraces);
}
Plotly.newPlot(
_domName,
memoryTraces,
getMemoryLayout('', createEventTimeline),
Configuration(
responsive: true,
displaylogo: false,
displayModeBar: false,
),
);
doubleClick(_domName, _doubleClick);
}
bool get hasEventTimeline => eventTimeline != null;
void createEventTimeline() {
final List<Data> memoryTraces = createMemoryTraces();
eventTimeline = EventTimeline(_domName, _memoryChart.element);
final List<Data> eventTraces = eventTimeline.getEventTimelineTraces();
eventTimeline.computeTraceIndexes(memoryTraces);
Plotly.relayout(_domName, getMemoryLayout('', true));
Plotly.addTraces(_domName, eventTraces, [
eventTimeline.resetTraceIndex,
eventTimeline.snapshotTraceIndex,
]);
}
void plotMarkersDataList(List<int> timestamps, List<num> gces) {
extendTraces1(
_domName,
timestamps, // x coordinates for RSS trace.
gces, // y coordinates for RSS trace.
[
MEMORY_GC_TRACE,
],
);
}
void plotMemoryDataList(
List<int> timestamps,
List<num> rsses,
List<num> capacities,
List<num> uses,
List<num> externals,
) {
// TODO(terry): Eliminate this JS call (result of reified List?).
extendTraces4(
_domName,
timestamps, // x coordinates for RSS trace.
timestamps, // x coordinates for capacity trace.
timestamps, // x coordinates for external trace.
timestamps, // x coordinates for used trace.
rsses, // y coordinates for RSS trace.
capacities, // y coordinates for capacity trace.
externals, // y coordinates for external trace.
uses, // y coordinates for used trace.
[
MEMORY_RSS_TRACE,
MEMORY_CAPACITY_TRACE,
MEMORY_EXTERNAL_TRACE,
MEMORY_USED_TRACE,
],
);
if (liveUpdate) {
// Display 2 minutes of collected data in the chart, all data is accessible.
final int startTime = DateTime.fromMillisecondsSinceEpoch(timestamps[0])
.subtract(const Duration(minutes: 2))
.millisecondsSinceEpoch;
rangeSliderToLast(startTime, timestamps[0]);
}
}
void rangeSliderToLast(int startTime, int endTime) {
Plotly.update(
_domName,
[Data()],
Layout(
xaxis: getXAxisLayout(startTime, endTime),
),
);
}
bool liveUpdate = true;
void setLiveUpdate({bool live}) {
liveUpdate = live;
}
void plotSnapshot() {
if (!hasEventTimeline) createEventTimeline();
final List data = getProperty(_memoryChart.element, 'data');
final Data capacityTrace = data[MEMORY_CAPACITY_TRACE];
final int timestamp = capacityTrace.x[capacityTrace.x.length - 1];
eventTimeline.plotSnapshot(timestamp);
}
void plotReset() {
if (!hasEventTimeline) createEventTimeline();
final List data = getProperty(_memoryChart.element, 'data');
final Data capacityTrace = data[MEMORY_CAPACITY_TRACE];
final int timestamp = capacityTrace.x[capacityTrace.x.length - 1];
eventTimeline.plotReset(timestamp);
}
}
/// Create an Event Timeline subplot, notice it is associated with y2. This
/// requires that the layout these traces exist in the 'yaxis2' area. For an
/// example, look at MemoryPloty's getMemoryLayout method it creates a yaxis2
/// positioned above yaxis.
class EventTimeline {
EventTimeline(this._domName, this._chart);
// Light theme is Blue and dark theme is Dark Blue 600. See
// https://standards.google/guidelines/google-material/color/dark-theme.html#style
static ThemedColor snapshotColor =
const ThemedColor(Color(0xFF0000FF), Color(0xFF185AE1));
static ThemedColor resetColor =
const ThemedColor(Color(0xFF0000FF), Color(0xFF185AE1));
// Light theme is Browser's lightblue color, Dark theme is Dark Blue 300.
static ThemedColor eventBgColor =
const ThemedColor(Color(0xFFABD2DF), Color(0xFF79B6F6));
final String _snapshotColorCss = colorToCss(snapshotColor);
final String _resetColorCss = colorToCss(resetColor);
final String _eventBgColorCss = colorToCss(eventBgColor);
final String _domName;
final dynamic _chart;
// Trace index within the traces passed to addEventTimelineTo
int resetTraceIndex;
int snapshotTraceIndex;
List<Data> addEventTimelineTo(List<Data> traces) {
final List<Data> eventTraces = getEventTimelineTraces();
resetTraceIndex = traces.length;
traces.add(eventTraces[RESET_TRACE_INDEX]); // Reset trace.
snapshotTraceIndex = traces.length;
traces.add(eventTraces[SNAPSHOT_TRACE_INDEX]); // Snapshot trace.
return traces;
}
void computeTraceIndexes(List<Data> traces) {
resetTraceIndex = traces.length;
snapshotTraceIndex = resetTraceIndex + 1;
}
// Indexes for traces returned from getEventTimelineTraces
static const int RESET_TRACE_INDEX = 0;
static const int SNAPSHOT_TRACE_INDEX = 1;
List<Data> getEventTimelineTraces() {
// Create traces for the event timeline subplot.
final Data resetTrace = Data(
// Null is needed so the trace legend entry appears w/o data.
x: [Null],
y: [Null],
name: 'Reset',
type: 'scatter',
mode: 'markers',
yaxis: 'y2',
marker: Marker(
color: _resetColorCss,
line: Line(
color: _eventBgColorCss,
width: 2,
),
size: 5,
symbol: 'hexagon2-open-dot',
),
hoverinfo: 'name+x',
showlegend: true,
);
final Data snapshotTrace = Data(
// Null is needed so the trace legend entry appears w/o data.
x: [Null],
y: [Null],
name: 'Snapshot',
type: 'scatter',
mode: 'markers',
yaxis: 'y2',
marker: Marker(
color: _snapshotColorCss,
line: Line(
color: _eventBgColorCss,
width: 2,
),
size: 10,
symbol: 'hexagon2-open',
),
hoverinfo: 'name+x',
showlegend: true,
);
return [resetTrace, snapshotTrace];
}
static const String _EVENT_MEMORY = 'mem';
static const String _SNAPSHOT_EVENT = 's';
static const String _RESET_EVENT = 'r';
String lastEventType = '';
int lastEventTime = -1;
void displayDuration(int time, String eventType) {
if (eventType == _SNAPSHOT_EVENT) {
lastEventType = eventType;
lastEventTime = time;
return;
}
final Layout layout = getProperty(_chart, 'layout');
final List<Shape> shapes = layout.shapes;
final int nextShape = shapes.length;
final jsShape = createEventShape(
'$_EVENT_MEMORY: $lastEventType > $eventType',
nextShape,
lastEventTime,
time);
Plotly.relayout(_domName, jsShape);
lastEventTime = time;
lastEventType = eventType;
}
void plotSnapshot(int timestamp) {
extendTraces1(_domName, [timestamp], [1], [snapshotTraceIndex]);
displayDuration(timestamp, _SNAPSHOT_EVENT);
}
void plotReset(int timestamp) {
extendTraces1(_domName, [timestamp], [1], [resetTraceIndex]);
displayDuration(timestamp, _RESET_EVENT);
}
}