| // Copyright 2016 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. | |
| /** | |
| * @unrestricted | |
| */ | |
| export default class ServerTiming { | |
| /** | |
| * @param {string} metric | |
| * @param {?number} value | |
| * @param {?string} description | |
| */ | |
| constructor(metric, value, description) { | |
| this.metric = metric; | |
| this.value = value; | |
| this.description = description; | |
| } | |
| /** | |
| * @param {!Array<!SDK.NetworkRequest.NameValue>} headers | |
| * @return {?Array<!ServerTiming>} | |
| */ | |
| static parseHeaders(headers) { | |
| const rawServerTimingHeaders = headers.filter(item => item.name.toLowerCase() === 'server-timing'); | |
| if (!rawServerTimingHeaders.length) { | |
| return null; | |
| } | |
| const serverTimings = rawServerTimingHeaders.reduce((memo, header) => { | |
| const timing = this.createFromHeaderValue(header.value); | |
| memo.pushAll(timing.map(function(entry) { | |
| return new ServerTiming( | |
| entry.name, entry.hasOwnProperty('dur') ? entry.dur : null, entry.hasOwnProperty('desc') ? entry.desc : ''); | |
| })); | |
| return memo; | |
| }, []); | |
| serverTimings.sort((a, b) => a.metric.toLowerCase().compareTo(b.metric.toLowerCase())); | |
| return serverTimings; | |
| } | |
| /** | |
| * @param {string} valueString | |
| * @return {?Array<!Object>} | |
| */ | |
| static createFromHeaderValue(valueString) { | |
| function trimLeadingWhiteSpace() { | |
| valueString = valueString.replace(/^\s*/, ''); | |
| } | |
| function consumeDelimiter(char) { | |
| console.assert(char.length === 1); | |
| trimLeadingWhiteSpace(); | |
| if (valueString.charAt(0) !== char) { | |
| return false; | |
| } | |
| valueString = valueString.substring(1); | |
| return true; | |
| } | |
| function consumeToken() { | |
| // https://tools.ietf.org/html/rfc7230#appendix-B | |
| const result = /^(?:\s*)([\w!#$%&'*+\-.^`|~]+)(?:\s*)(.*)/.exec(valueString); | |
| if (!result) { | |
| return null; | |
| } | |
| valueString = result[2]; | |
| return result[1]; | |
| } | |
| function consumeTokenOrQuotedString() { | |
| trimLeadingWhiteSpace(); | |
| if (valueString.charAt(0) === '"') { | |
| return consumeQuotedString(); | |
| } | |
| return consumeToken(); | |
| } | |
| function consumeQuotedString() { | |
| console.assert(valueString.charAt(0) === '"'); | |
| valueString = valueString.substring(1); // remove leading DQUOTE | |
| let value = ''; | |
| while (valueString.length) { | |
| // split into two parts: | |
| // -everything before the first " or \ | |
| // -everything else | |
| const result = /^([^"\\]*)(.*)/.exec(valueString); | |
| value += result[1]; | |
| if (result[2].charAt(0) === '"') { | |
| // we have found our closing " | |
| valueString = result[2].substring(1); // strip off everything after the closing " | |
| return value; // we are done here | |
| } | |
| console.assert(result[2].charAt(0) === '\\'); | |
| // special rules for \ found in quoted-string (https://tools.ietf.org/html/rfc7230#section-3.2.6) | |
| value += result[2].charAt(1); // grab the character AFTER the \ (if there was one) | |
| valueString = result[2].substring(2); // strip off \ and next character | |
| } | |
| return null; // not a valid quoted-string | |
| } | |
| function consumeExtraneous() { | |
| const result = /([,;].*)/.exec(valueString); | |
| if (result) { | |
| valueString = result[1]; | |
| } | |
| } | |
| const result = []; | |
| let name; | |
| while ((name = consumeToken()) !== null) { | |
| const entry = {name}; | |
| if (valueString.charAt(0) === '=') { | |
| this.showWarning(ls`Deprecated syntax found. Please use: <name>;dur=<duration>;desc=<description>`); | |
| } | |
| while (consumeDelimiter(';')) { | |
| let paramName; | |
| if ((paramName = consumeToken()) === null) { | |
| continue; | |
| } | |
| paramName = paramName.toLowerCase(); | |
| const parseParameter = this.getParserForParameter(paramName); | |
| let paramValue = null; | |
| if (consumeDelimiter('=')) { | |
| // always parse the value, even if we don't recognize the parameter name | |
| paramValue = consumeTokenOrQuotedString(); | |
| consumeExtraneous(); | |
| } | |
| if (parseParameter) { | |
| // paramName is valid | |
| if (entry.hasOwnProperty(paramName)) { | |
| this.showWarning(ls`Duplicate parameter \"${paramName}\" ignored.`); | |
| continue; | |
| } | |
| if (paramValue === null) { | |
| this.showWarning(ls`No value found for parameter \"${paramName}\".`); | |
| } | |
| parseParameter.call(this, entry, paramValue); | |
| } else { | |
| // paramName is not valid | |
| this.showWarning(ls`Unrecognized parameter \"${paramName}\".`); | |
| } | |
| } | |
| result.push(entry); | |
| if (!consumeDelimiter(',')) { | |
| break; | |
| } | |
| } | |
| if (valueString.length) { | |
| this.showWarning(ls`Extraneous trailing characters.`); | |
| } | |
| return result; | |
| } | |
| /** | |
| * @param {string} paramName | |
| * @return {?function(!Object, string)} | |
| */ | |
| static getParserForParameter(paramName) { | |
| switch (paramName) { | |
| case 'dur': | |
| return function(entry, paramValue) { | |
| entry.dur = 0; | |
| if (paramValue !== null) { | |
| const duration = parseFloat(paramValue); | |
| if (isNaN(duration)) { | |
| this.showWarning(ls`Unable to parse \"${paramName}\" value \"${paramValue}\".`); | |
| return; | |
| } | |
| entry.dur = duration; | |
| } | |
| }; | |
| case 'desc': | |
| return function(entry, paramValue) { | |
| entry.desc = paramValue || ''; | |
| }; | |
| default: | |
| return null; | |
| } | |
| } | |
| /** | |
| * @param {string} msg | |
| */ | |
| static showWarning(msg) { | |
| Common.console.warn(Common.UIString(`ServerTiming: ${msg}`)); | |
| } | |
| } | |
| /* Legacy exported object */ | |
| self.SDK = self.SDK || {}; | |
| /* Legacy exported object */ | |
| SDK = SDK || {}; | |
| /** @constructor */ | |
| SDK.ServerTiming = ServerTiming; |