blob: a4c037a13cef75536274c2c8f4754c44c6d0e4fa [file] [log] [blame]
Avi Drissman4e1b7bc32022-09-15 14:03:501// Copyright 2013 The Chromium Authors
[email protected]03bf84a2013-03-23 22:11:482// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//
6// This file contains helper methods to draw the stats timeline graphs.
7// Each graph represents a series of stats report for a PeerConnection,
8// e.g. 1234-0-ssrc-abcd123-bytesSent is the graph for the series of bytesSent
9// for ssrc-abcd123 of PeerConnection 0 in process 1234.
10// The graphs are drawn as CANVAS, grouped per report type per PeerConnection.
11// Each group has an expand/collapse button and is collapsed initially.
12//
13
dpapad303c92812023-10-31 02:08:3514import {$} from 'chrome://resources/js/util.js';
rbpotter5796b002021-03-10 18:49:2115
16import {TimelineDataSeries} from './data_series.js';
17import {peerConnectionDataStore} from './dump_creator.js';
Philipp Hanckef91ac402022-10-17 11:05:0618import {generateStatsLabel} from './stats_helper.js';
rbpotter5796b002021-03-10 18:49:2119import {TimelineGraphView} from './timeline_graph_view.js';
[email protected]03bf84a2013-03-23 22:11:4820
Philipp Hancke0635d3142021-03-16 20:02:0921const STATS_GRAPH_CONTAINER_HEADING_CLASS = 'stats-graph-container-heading';
[email protected]2746df92013-04-26 23:42:3222
Philipp Hanckeba4adef2023-12-20 14:43:1323function isReportBlocklisted(report) {
Philipp Hanckeb7e04abd2022-10-17 09:35:5224 // Codec stats reflect what has been negotiated. They don't contain
25 // information that is useful in graphs.
Dan Beam26d73742020-01-16 05:59:2126 if (report.type === 'codec') {
Henrik Boströmb6d732af2019-04-23 11:32:2927 return true;
28 }
29 // Unused data channels can stay in "connecting" indefinitely and their
30 // counters stay zero.
Dan Beam26d73742020-01-16 05:59:2131 if (report.type === 'data-channel' &&
32 readReportStat(report, 'state') === 'connecting') {
Henrik Boströmb6d732af2019-04-23 11:32:2933 return true;
34 }
35 // The same is true for transports and "new".
Dan Beam26d73742020-01-16 05:59:2136 if (report.type === 'transport' &&
37 readReportStat(report, 'dtlsState') === 'new') {
Henrik Boströmb6d732af2019-04-23 11:32:2938 return true;
39 }
40 // Local and remote candidates don't change over time and there are several of
41 // them.
Dan Beam26d73742020-01-16 05:59:2142 if (report.type === 'local-candidate' || report.type === 'remote-candidate') {
Henrik Boströmb6d732af2019-04-23 11:32:2943 return true;
44 }
45 return false;
46}
47
48function readReportStat(report, stat) {
Philipp Hancke0635d3142021-03-16 20:02:0949 const values = report.stats.values;
Henrik Boströmb6d732af2019-04-23 11:32:2950 for (let i = 0; i < values.length; i += 2) {
Dan Beam26d73742020-01-16 05:59:2151 if (values[i] === stat) {
Henrik Boströmb6d732af2019-04-23 11:32:2952 return values[i + 1];
53 }
54 }
55 return undefined;
56}
57
Philipp Hanckeba4adef2023-12-20 14:43:1358function isStatBlocklisted(report, statName) {
Henrik Boströmb6d732af2019-04-23 11:32:2959 // The priority does not change over time on its own; plotting uninteresting.
Dan Beam26d73742020-01-16 05:59:2160 if (report.type === 'candidate-pair' && statName === 'priority') {
Henrik Boströmb6d732af2019-04-23 11:32:2961 return true;
62 }
Philipp Hancke7132fb6e2023-09-25 09:56:3363 // The mid/rid and ssrcs associated with a sender/receiver do not change
64 // over time; plotting uninteresting.
Philipp Hancke9889636c2024-04-18 12:24:0765 if (['inbound-rtp', 'outbound-rtp',
66 'remote-inbound-rtp', 'remote-outbound-rtp'].includes(report.type) &&
Philipp Hancke7132fb6e2023-09-25 09:56:3367 ['mid', 'rid', 'ssrc', 'rtxSsrc', 'fecSsrc'].includes(statName)) {
Philipp Hanckeccc4b8632022-07-01 12:45:2768 return true;
69 }
Philipp Hancke06f9f8b2024-05-23 13:33:2370 // Last packet sent/received timestamps on candidate-pair and inbound-rtp
71 // do not plot nicely.
72 if (['candidate-pair', 'inbound-rtp'].includes(report.type) &&
73 ['lastPacketSentTimestamp',
74 'lastPacketReceivedTimestamp'].includes(statName)) {
75 return true;
76 }
Henrik Boströmb6d732af2019-04-23 11:32:2977 return false;
78}
79
Philipp Hancke0635d3142021-03-16 20:02:0980const graphViews = {};
rbpotter5796b002021-03-10 18:49:2181// Export on |window| since tests access this directly from C++.
82window.graphViews = graphViews;
Philipp Hancke0635d3142021-03-16 20:02:0983const graphElementsByPeerConnectionId = new Map();
[email protected]03bf84a2013-03-23 22:11:4884
Philipp Hanckeba4adef2023-12-20 14:43:1385// Returns number parsed from |value|, or NaN.
[email protected]78d221d2013-06-05 07:31:3386function getNumberFromValue(name, value) {
Henrik Boströmb6d732af2019-04-23 11:32:2987 if (isNaN(value)) {
88 return NaN;
89 }
[email protected]78d221d2013-06-05 07:31:3390 return parseFloat(value);
91}
92
[email protected]2bc265232013-05-08 20:24:1393// Adds the stats report |report| to the timeline graph for the given
94// |peerConnectionElement|.
rbpotter5796b002021-03-10 18:49:2195export function drawSingleReport(
Philipp Hanckeba4adef2023-12-20 14:43:1396 peerConnectionElement, report) {
Philipp Hancke0635d3142021-03-16 20:02:0997 const reportType = report.type;
98 const reportId = report.id;
99 const stats = report.stats;
Dan Beambdd7d822019-01-05 00:59:42100 if (!stats || !stats.values) {
[email protected]03bf84a2013-03-23 22:11:48101 return;
Dan Beambdd7d822019-01-05 00:59:42102 }
Henrik Boströmb6d732af2019-04-23 11:32:29103
104 const childrenBefore = peerConnectionElement.hasChildNodes() ?
105 Array.from(peerConnectionElement.childNodes) :
106 [];
[email protected]2746df92013-04-26 23:42:32107
Philipp Hancke0635d3142021-03-16 20:02:09108 for (let i = 0; i < stats.values.length - 1; i = i + 2) {
109 const rawLabel = stats.values[i];
Philipp Hancke0635d3142021-03-16 20:02:09110 const rawDataSeriesId = reportId + '-' + rawLabel;
111 const rawValue = getNumberFromValue(rawLabel, stats.values[i + 1]);
[email protected]78d221d2013-06-05 07:31:33112 if (isNaN(rawValue)) {
113 // We do not draw non-numerical values, but still want to record it in the
114 // data series.
Nasko Oskov9303a0662018-10-15 18:03:44115 addDataSeriesPoints(
Philipp Hanckeec3c9542022-07-15 09:19:21116 peerConnectionElement, reportType, rawDataSeriesId, rawLabel,
117 [stats.timestamp], [stats.values[i + 1]]);
[email protected]78d221d2013-06-05 07:31:33118 continue;
119 }
Philipp Hancke0635d3142021-03-16 20:02:09120 let finalDataSeriesId = rawDataSeriesId;
121 let finalLabel = rawLabel;
122 let finalValue = rawValue;
[email protected]03bf84a2013-03-23 22:11:48123
124 // Updates the final dataSeries to draw.
Nasko Oskov9303a0662018-10-15 18:03:44125 addDataSeriesPoints(
Philipp Hanckeec3c9542022-07-15 09:19:21126 peerConnectionElement, reportType, finalDataSeriesId, finalLabel,
127 [stats.timestamp], [finalValue]);
[email protected]03bf84a2013-03-23 22:11:48128
Philipp Hanckeba4adef2023-12-20 14:43:13129 if (isReportBlocklisted(report) || isStatBlocklisted(report, rawLabel)) {
130 // We do not want to draw certain reports but still want to
Philipp Hancke3168f202019-06-13 12:54:41131 // record them in the data series.
132 continue;
133 }
134
[email protected]03bf84a2013-03-23 22:11:48135 // Updates the graph.
Philipp Hanckeba4adef2023-12-20 14:43:13136 const graphType = finalLabel;
Philipp Hancke0635d3142021-03-16 20:02:09137 const graphViewId =
[email protected]3e7fd922013-05-15 00:03:03138 peerConnectionElement.id + '-' + reportId + '-' + graphType;
[email protected]03bf84a2013-03-23 22:11:48139
140 if (!graphViews[graphViewId]) {
Nasko Oskov9303a0662018-10-15 18:03:44141 graphViews[graphViewId] =
142 createStatsGraphView(peerConnectionElement, report, graphType);
Philipp Hanckef212a842022-08-19 08:03:03143 const searchParameters = new URLSearchParams(window.location.search);
144 if (searchParameters.has('statsInterval')) {
145 const statsInterval = Math.max(
146 parseInt(searchParameters.get('statsInterval'), 10),
147 100);
148 if (isFinite(statsInterval)) {
149 graphViews[graphViewId].setScale(statsInterval);
150 }
151 }
Philipp Hancke0635d3142021-03-16 20:02:09152 const date = new Date(stats.timestamp);
[email protected]03bf84a2013-03-23 22:11:48153 graphViews[graphViewId].setDateRange(date, date);
154 }
Henrik Boström23dec442023-07-25 12:13:35155 // Ensures the stats graph title is up-to-date.
156 ensureStatsGraphContainer(peerConnectionElement, report);
[email protected]03bf84a2013-03-23 22:11:48157 // Adds the new dataSeries to the graphView. We have to do it here to cover
158 // both the simple and compound graph cases.
Philipp Hancke0635d3142021-03-16 20:02:09159 const dataSeries =
[email protected]4d41b802013-05-15 18:38:23160 peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
161 finalDataSeriesId);
Dan Beambdd7d822019-01-05 00:59:42162 if (!graphViews[graphViewId].hasDataSeries(dataSeries)) {
[email protected]4d41b802013-05-15 18:38:23163 graphViews[graphViewId].addDataSeries(dataSeries);
Dan Beambdd7d822019-01-05 00:59:42164 }
[email protected]f339c2f2013-05-08 22:18:46165 graphViews[graphViewId].updateEndDate();
[email protected]03bf84a2013-03-23 22:11:48166 }
[email protected]3fdde8a2023-07-07 12:45:13167 // Add a synthetic data series for the timestamp.
168 addDataSeriesPoints(
169 peerConnectionElement, reportType, reportId + '-timestamp',
170 reportId + '-timestamp', [stats.timestamp], [stats.timestamp]);
Henrik Boströmb6d732af2019-04-23 11:32:29171
172 const childrenAfter = peerConnectionElement.hasChildNodes() ?
173 Array.from(peerConnectionElement.childNodes) :
174 [];
175 for (let i = 0; i < childrenAfter.length; ++i) {
176 if (!childrenBefore.includes(childrenAfter[i])) {
177 let graphElements =
178 graphElementsByPeerConnectionId.get(peerConnectionElement.id);
179 if (!graphElements) {
180 graphElements = [];
181 graphElementsByPeerConnectionId.set(
182 peerConnectionElement.id, graphElements);
183 }
184 graphElements.push(childrenAfter[i]);
185 }
186 }
187}
188
rbpotter5796b002021-03-10 18:49:21189export function removeStatsReportGraphs(peerConnectionElement) {
Henrik Boströmb6d732af2019-04-23 11:32:29190 const graphElements =
191 graphElementsByPeerConnectionId.get(peerConnectionElement.id);
192 if (graphElements) {
193 for (let i = 0; i < graphElements.length; ++i) {
194 peerConnectionElement.removeChild(graphElements[i]);
195 }
196 graphElementsByPeerConnectionId.delete(peerConnectionElement.id);
197 }
198 Object.keys(graphViews).forEach(key => {
199 if (key.startsWith(peerConnectionElement.id)) {
200 delete graphViews[key];
201 }
202 });
[email protected]03bf84a2013-03-23 22:11:48203}
204
205// Makes sure the TimelineDataSeries with id |dataSeriesId| is created,
[email protected]ca001942014-02-18 18:10:54206// and adds the new data points to it. |times| is the list of timestamps for
207// each data point, and |values| is the list of the data point values.
208function addDataSeriesPoints(
Philipp Hanckeec3c9542022-07-15 09:19:21209 peerConnectionElement, reportType, dataSeriesId, label, times, values) {
Philipp Hancke0635d3142021-03-16 20:02:09210 let dataSeries =
Nasko Oskov9303a0662018-10-15 18:03:44211 peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
212 dataSeriesId);
[email protected]4d41b802013-05-15 18:38:23213 if (!dataSeries) {
Philipp Hanckeec3c9542022-07-15 09:19:21214 dataSeries = new TimelineDataSeries(reportType);
[email protected]4d41b802013-05-15 18:38:23215 peerConnectionDataStore[peerConnectionElement.id].setDataSeries(
216 dataSeriesId, dataSeries);
[email protected]03bf84a2013-03-23 22:11:48217 }
Philipp Hancke0635d3142021-03-16 20:02:09218 for (let i = 0; i < times.length; ++i) {
[email protected]ca001942014-02-18 18:10:54219 dataSeries.addPoint(times[i], values[i]);
Dan Beambdd7d822019-01-05 00:59:42220 }
[email protected]ca001942014-02-18 18:10:54221}
222
[email protected]35d9ad22023-02-13 08:38:37223// Ensures a div container to the stats graph for a peerConnectionElement is
224// created as a child of the |peerConnectionElement|.
225function ensureStatsGraphTopContainer(peerConnectionElement) {
226 const containerId = peerConnectionElement.id + '-graph-container';
227 let container = document.getElementById(containerId);
228 if (!container) {
229 container = document.createElement('div');
230 container.id = containerId;
231 container.className = 'stats-graph-container';
232 const label = document.createElement('label');
233 label.innerText = 'Filter statistics graphs by type including ';
234 container.appendChild(label);
235 const input = document.createElement('input');
236 input.placeholder = 'separate multiple values by `,`';
237 input.size = 25;
238 input.oninput = (e) => filterStats(e, container);
239 container.appendChild(input);
240
241 peerConnectionElement.appendChild(container);
242 }
243 return container;
244}
245
246// Ensures a div container to the stats graph for a single set of data is
247// created as a child of the |peerConnectionElement|'s graph container.
248function ensureStatsGraphContainer(peerConnectionElement, report) {
249 const topContainer = ensureStatsGraphTopContainer(peerConnectionElement);
Philipp Hancke0635d3142021-03-16 20:02:09250 const containerId = peerConnectionElement.id + '-' + report.type + '-' +
Nasko Oskov9303a0662018-10-15 18:03:44251 report.id + '-graph-container';
Rebekah Potter8148f852022-11-29 18:04:37252 // Disable getElementById restriction here, since |containerId| is not always
253 // a valid selector.
254 // eslint-disable-next-line no-restricted-properties
255 let container = document.getElementById(containerId);
[email protected]03bf84a2013-03-23 22:11:48256 if (!container) {
[email protected]2746df92013-04-26 23:42:32257 container = document.createElement('details');
[email protected]03bf84a2013-03-23 22:11:48258 container.id = containerId;
[email protected]2746df92013-04-26 23:42:32259 container.className = 'stats-graph-container';
[email protected]35d9ad22023-02-13 08:38:37260 container.attributes['data-statsType'] = report.type;
[email protected]03bf84a2013-03-23 22:11:48261
262 peerConnectionElement.appendChild(container);
Jun Kokatsu7eaf4712020-06-01 23:32:07263 container.appendChild($('summary-span-template').content.cloneNode(true));
[email protected]2746df92013-04-26 23:42:32264 container.firstChild.firstChild.className =
265 STATS_GRAPH_CONTAINER_HEADING_CLASS;
Philipp Hancke07263c652023-06-19 10:02:09266 topContainer.appendChild(container);
[email protected]03bf84a2013-03-23 22:11:48267 }
Philipp Hancke07263c652023-06-19 10:02:09268 // Update the label all the time to account for new information.
Philipp Hanckeba4adef2023-12-20 14:43:13269 container.firstChild.firstChild.textContent = 'Stats graphs for ' +
Philipp Hancke07263c652023-06-19 10:02:09270 generateStatsLabel(report);
[email protected]03bf84a2013-03-23 22:11:48271 return container;
272}
273
274// Creates the container elements holding a timeline graph
275// and the TimelineGraphView object.
Nasko Oskov9303a0662018-10-15 18:03:44276function createStatsGraphView(peerConnectionElement, report, statsName) {
Philipp Hancke0635d3142021-03-16 20:02:09277 const topContainer =
[email protected]35d9ad22023-02-13 08:38:37278 ensureStatsGraphContainer(peerConnectionElement, report);
[email protected]03bf84a2013-03-23 22:11:48279
Philipp Hancke0635d3142021-03-16 20:02:09280 const graphViewId =
[email protected]4d41b802013-05-15 18:38:23281 peerConnectionElement.id + '-' + report.id + '-' + statsName;
Philipp Hancke0635d3142021-03-16 20:02:09282 const divId = graphViewId + '-div';
283 const canvasId = graphViewId + '-canvas';
284 const container = document.createElement('div');
[email protected]03bf84a2013-03-23 22:11:48285 container.className = 'stats-graph-sub-container';
286
287 topContainer.appendChild(container);
Jun Kokatsu7eaf4712020-06-01 23:32:07288 const canvasDiv = $('container-template').content.cloneNode(true);
289 canvasDiv.querySelectorAll('div')[0].textContent = statsName;
290 canvasDiv.querySelectorAll('div')[1].id = divId;
291 canvasDiv.querySelector('canvas').id = canvasId;
292 container.appendChild(canvasDiv);
[email protected]03bf84a2013-03-23 22:11:48293 return new TimelineGraphView(divId, canvasId);
294}
[email protected]35d9ad22023-02-13 08:38:37295
296/**
297 * Apply a filter to the stats graphs
298 * @param event InputEvent from the filter input field.
299 * @param container stats table container element.
300 * @private
301 */
302function filterStats(event, container) {
303 const filter = event.target.value;
304 const filters = filter.split(',');
305 container.childNodes.forEach(node => {
306 if (node.nodeName !== 'DETAILS') {
307 return;
308 }
309 const statsType = node.attributes['data-statsType'];
310 if (!filter || filters.includes(statsType) ||
311 filters.find(f => statsType.includes(f))) {
312 node.style.display = 'block';
313 } else {
314 node.style.display = 'none';
315 }
316 });
317}