chrome-devtools-frontend
Version:
Chrome DevTools UI
248 lines (217 loc) • 10.4 kB
text/typescript
// Copyright (c) 2021 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 * as i18n from '../i18n/i18n.js';
import * as SDK from '../sdk/sdk.js';
import * as UI from '../ui/ui.js';
export const UIStrings = {
/**
*@description Text in Indexed DBViews of the Application panel
*/
version: 'Version',
/**
*@description Table heading for Service Workers update information. Update is a noun.
*/
updateActivity: 'Update Activity',
/**
*@description Title for the timeline tab.
*/
timeline: 'Timeline',
/**
*@description Text in Service Workers Update Life Cycle
*@example {2} PH1
*/
startTimeS: 'Start time: {PH1}',
/**
*@description Text for end time of an event
*@example {2} PH1
*/
endTimeS: 'End time: {PH1}',
};
const str_ = i18n.i18n.registerUIStrings('resources/ServiceWorkerUpdateCycleHelper.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class ServiceWorkerUpdateCycleHelper {
static calculateServiceWorkerUpdateRanges(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration):
Array<ServiceWorkerUpdateRange> {
function addRange(ranges: Array<ServiceWorkerUpdateRange>, range: ServiceWorkerUpdateRange): void {
if (range.start < Number.MAX_VALUE && range.start <= range.end) {
ranges.push(range);
}
}
/**
* Add ranges representing Install, Wait or Activate of a sw version represented by id.
*/
function addNormalizedRanges(
ranges: Array<ServiceWorkerUpdateRange>, id: string, startInstallTime: number, endInstallTime: number,
startActivateTime: number, endActivateTime: number,
status: Protocol.ServiceWorker.ServiceWorkerVersionStatus): void {
addRange(ranges, {id, phase: ServiceWorkerUpdateNames.Install, start: startInstallTime, end: endInstallTime});
if (status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Activating ||
status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Activated ||
status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Redundant) {
addRange(ranges, {
id,
phase: ServiceWorkerUpdateNames.Wait,
start: endInstallTime,
end: startActivateTime,
});
addRange(
ranges, {id, phase: ServiceWorkerUpdateNames.Activate, start: startActivateTime, end: endActivateTime});
}
}
function rangesForVersion(version: SDK.ServiceWorkerManager.ServiceWorkerVersion): Array<ServiceWorkerUpdateRange> {
let state: SDK.ServiceWorkerManager.ServiceWorkerVersionState|null = version.currentState;
let endActivateTime: number = 0;
let beginActivateTime: number = 0;
let endInstallTime: number = 0;
let beginInstallTime: number = 0;
const currentStatus = state.status;
if (currentStatus === Protocol.ServiceWorker.ServiceWorkerVersionStatus.New) {
return [];
}
while (state) {
// find the earliest timestamp of different stage on record.
if (state.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Activated) {
endActivateTime = state.last_updated_timestamp;
} else if (state.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Activating) {
if (endActivateTime === 0) {
endActivateTime = state.last_updated_timestamp;
}
beginActivateTime = state.last_updated_timestamp;
} else if (state.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Installed) {
endInstallTime = state.last_updated_timestamp;
} else if (state.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Installing) {
if (endInstallTime === 0) {
endInstallTime = state.last_updated_timestamp;
}
beginInstallTime = state.last_updated_timestamp;
}
state = state.previousState;
}
/** @type {Array <ServiceWorkerUpdateRange>} */
const ranges: Array<ServiceWorkerUpdateRange> = [];
addNormalizedRanges(
ranges, version.id, beginInstallTime, endInstallTime, beginActivateTime, endActivateTime, currentStatus);
return ranges;
}
const versions = registration.versionsByMode();
const modes = [
SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.Active,
SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.Waiting,
SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.Installing,
SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.Redundant,
];
for (const mode of modes) {
const version = versions.get(mode);
if (version) {
const ranges = rangesForVersion(version);
return ranges;
}
}
return [];
}
static createTimingTable(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration): Element {
const tableElement = document.createElement('table');
tableElement.classList.add('service-worker-update-timing-table');
UI.Utils.appendStyle(tableElement, 'resources/serviceWorkerUpdateCycleView.css');
const timeRanges = this.calculateServiceWorkerUpdateRanges(registration);
this.updateTimingTable(tableElement, timeRanges);
return tableElement;
}
private static createTimingTableHead(tableElement: Element): void {
const serverHeader = tableElement.createChild('tr', 'service-worker-update-timing-table-header');
UI.UIUtils.createTextChild(serverHeader.createChild('td'), i18nString(UIStrings.version));
UI.UIUtils.createTextChild(serverHeader.createChild('td'), i18nString(UIStrings.updateActivity));
UI.UIUtils.createTextChild(serverHeader.createChild('td'), i18nString(UIStrings.timeline));
}
private static removeRows(tableElement: Element): void {
const rows = tableElement.getElementsByTagName('tr');
while (rows[0]) {
if (rows[0].parentNode) {
rows[0].parentNode.removeChild(rows[0]);
}
}
}
private static updateTimingTable(tableElement: Element, timeRanges: Array<ServiceWorkerUpdateRange>): void {
this.removeRows(tableElement);
this.createTimingTableHead(tableElement);
/** @type {!Array<ServiceWorkerUpdateRange>} */
const timeRangeArray = timeRanges;
if (timeRangeArray.length === 0) {
return;
}
const startTimes = timeRangeArray.map(r => r.start);
const endTimes = timeRangeArray.map(r => r.end);
const startTime = startTimes.reduce((a, b) => Math.min(a, b));
const endTime = endTimes.reduce((a, b) => Math.max(a, b));
const scale = 100 / (endTime - startTime);
for (const range of timeRangeArray) {
const phaseName = range.phase;
const left = (scale * (range.start - startTime));
const right = (scale * (endTime - range.end));
const tr = tableElement.createChild('tr');
const timingBarVersionElement = tr.createChild('td');
UI.UIUtils.createTextChild(timingBarVersionElement, '#' + range.id);
timingBarVersionElement.classList.add('service-worker-update-timing-bar-clickable');
timingBarVersionElement.setAttribute('tabindex', '0');
timingBarVersionElement.setAttribute('role', 'switch');
UI.ARIAUtils.setChecked(timingBarVersionElement, false);
const timingBarTitleElement = tr.createChild('td');
UI.UIUtils.createTextChild(timingBarTitleElement, phaseName);
this.constructUpdateDetails(tableElement, tr, range);
const barContainer = tr.createChild('td').createChild('div', 'service-worker-update-timing-row');
const bar = <HTMLElement>(
barContainer.createChild('span', 'service-worker-update-timing-bar ' + phaseName.toLowerCase()));
bar.style.left = left + '%';
bar.style.right = right + '%';
bar.textContent = '\u200B'; // Important for 0-time items to have 0 width.
}
}
/**
* Detailed information about an update phase. Currently starting and ending time.
*/
private static constructUpdateDetails(tableElement: Element, tr: Element, range: ServiceWorkerUpdateRange): void {
const detailsElement = tableElement.createChild('tr', 'service-worker-update-timing-bar-details');
detailsElement.classList.add('service-worker-update-timing-bar-details-collapsed');
self.onInvokeElement(tr, event => this.onToggleUpdateDetails(detailsElement, event));
const detailsView = new UI.TreeOutline.TreeOutlineInShadow();
detailsElement.appendChild(detailsView.element);
const startTimeItem = document.createElementWithClass('div', 'service-worker-update-details-treeitem');
const startTime = (new Date(range.start)).toISOString();
startTimeItem.textContent = i18nString(UIStrings.startTimeS, {PH1: startTime});
const startTimeTreeElement = new UI.TreeOutline.TreeElement(startTimeItem);
detailsView.appendChild(startTimeTreeElement);
const endTimeItem = document.createElementWithClass('div', 'service-worker-update-details-treeitem');
const endTime = (new Date(range.end)).toISOString();
endTimeItem.textContent = i18nString(UIStrings.endTimeS, {PH1: endTime});
const endTimeTreeElement = new UI.TreeOutline.TreeElement(endTimeItem);
detailsView.appendChild(endTimeTreeElement);
}
private static onToggleUpdateDetails(detailsRow: Element, event: Event): void {
if (!event.target) {
return;
}
const target: Element = <Element>(event.target);
if (target.classList.contains('service-worker-update-timing-bar-clickable')) {
detailsRow.classList.toggle('service-worker-update-timing-bar-details-collapsed');
detailsRow.classList.toggle('service-worker-update-timing-bar-details-expanded');
const expanded = target.getAttribute('aria-checked') === 'true';
UI.ARIAUtils.setChecked(target, !expanded);
}
}
static refresh(tableElement: Element, registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration): void {
const timeRanges = this.calculateServiceWorkerUpdateRanges(registration);
this.updateTimingTable(tableElement, timeRanges);
}
}
export const enum ServiceWorkerUpdateNames {
Install = 'Install',
Wait = 'Wait',
Activate = 'Activate',
}
export interface ServiceWorkerUpdateRange {
id: string;
phase: ServiceWorkerUpdateNames;
start: number;
end: number;
}