// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/js/action_link.js';
import 'chrome://resources/cr_elements/action_link.css.js';
import './strings.m.js';
import {assertNotReached} from 'chrome://resources/js/assert.js';
import {getFaviconForPageURL} from 'chrome://resources/js/icon.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import type {TimeDelta} from 'chrome://resources/mojo/mojo/public/mojom/base/time.mojom-webui.js';
import type {DomRepeatEvent} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {boolToString, durationToString, getOrCreateDetailsProvider} from './discards.js';
import type {DetailsProviderRemote, TabDiscardsInfo} from './discards.mojom-webui.js';
import {LifecycleUnitVisibility} from './discards.mojom-webui.js';
import {getTemplate} from './discards_tab.html.js';
import {LifecycleUnitDiscardReason, LifecycleUnitLoadingState, LifecycleUnitState} from './lifecycle_unit_state.mojom-webui.js';
import {SortedTableMixin} from './sorted_table_mixin.js';
interface DictType {
[key: string]: (boolean|number|string);
}
/**
* Compares two TabDiscardsInfos based on the data in the provided sort-key.
* @param sortKey The key of the sort. See the "data-sort-key"
* attribute of the table headers for valid sort-keys.
* @param a The first value being compared.
* @param b The second value being compared.
* @return A negative number if a < b, 0 if a === b, and a positive
* number if a > b.
*/
export function compareTabDiscardsInfos(
sortKey: string, a: DictType, b: DictType): number {
let val1 = a[sortKey];
let val2 = b[sortKey];
// Compares strings.
if (sortKey === 'title' || sortKey === 'tabUrl') {
val1 = (val1 as string).toLowerCase();
val2 = (val2 as string).toLowerCase();
if (val1 === val2) {
return 0;
}
return val1 > val2 ? 1 : -1;
}
// Compares boolean fields.
if (['isAutoDiscardable'].includes(sortKey)) {
if (val1 === val2) {
return 0;
}
return val1 ? 1 : -1;
}
// Compare lifecycle state. This is actually a compound key.
if (sortKey === 'state') {
// If the keys are discarding state, then break ties using the discard
// reason.
if (val1 === val2 && val1 === LifecycleUnitState.DISCARDED) {
val1 = a['discardReason'];
val2 = b['discardReason'];
}
return (val1 as LifecycleUnitState) - (val2 as LifecycleUnitState);
}
// Compares numeric fields.
// NOTE: visibility, loadingState and state are represented as a numeric
// value.
if ([
'visibility',
'loadingState',
'discardCount',
'utilityRank',
'lastActiveSeconds',
'siteEngagementScore',
].includes(sortKey)) {
return (val1 as number) - (val2 as number);
}
assertNotReached('Unsupported sort key: ' + sortKey);
}
const DiscardsTabElementBase = SortedTableMixin(PolymerElement);
class DiscardsTabElement extends DiscardsTabElementBase {
static get is() {
return 'discards-tab';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
tabInfos_: Array,
isPerformanceInterventionDemoModeEnabled_: {
readOnly: true,
type: Boolean,
value() {
return loadTimeData.getBoolean(
'isPerformanceInterventionDemoModeEnabled');
},
},
};
}
private tabInfos_: TabDiscardsInfo[];
private isPerformanceInterventionDemoModeEnabled_: boolean;
/** The current update timer if any. */
private updateTimer_: number = 0;
private discardsDetailsProvider_: DetailsProviderRemote|null = null;
override connectedCallback() {
this.setSortKey('utilityRank');
this.discardsDetailsProvider_ = getOrCreateDetailsProvider();
this.updateTable_();
}
/**
* Returns a sort function to compare tab infos based on the provided sort
* key and a boolean reverse flag.
* @param sortKey The sort key for the returned function.
* @param sortReverse True if sorting is reversed.
* @return A comparison function that compares two tab infos, returns
* negative number if a < b, 0 if a === b, and a positive
* number if a > b.
* @private
*/
private computeSortFunction_(sortKey: string, sortReverse: boolean):
(a: DictType, b: DictType) => number {
// Polymer 2.0 may invoke multi-property observers before all properties
// are defined.
if (!sortKey) {
return (_a: DictType, _b: DictType) => 0;
}
return function(a: DictType, b: DictType) {
const comp = compareTabDiscardsInfos(sortKey, a, b);
return sortReverse ? -comp : comp;
};
}
/**
* Returns a string representation of a visibility enum value for display in
* a table.
* @param visibility A visibility value.
* @return A string representation of the visibility.
*/
private visibilityToString_(visibility: LifecycleUnitVisibility): string {
switch (visibility) {
case LifecycleUnitVisibility.HIDDEN:
return 'hidden';
case LifecycleUnitVisibility.OCCLUDED:
return 'occluded';
case LifecycleUnitVisibility.VISIBLE:
return 'visible';
}
}
/**
* Returns a string representation of a loading state enum value for display
* in a table.
* @param loadingState A loading state value.
* @return A string representation of the loading state.
*/
private loadingStateToString_(loadingState: LifecycleUnitLoadingState):
string {
switch (loadingState) {
case LifecycleUnitLoadingState.UNLOADED:
return 'unloaded';
case LifecycleUnitLoadingState.LOADING:
return 'loading';
case LifecycleUnitLoadingState.LOADED:
return 'loaded';
}
}
/**
* Returns a string representation of a discard reason.
* @param reason The discard reason.
* @return A string representation of the discarding reason.
*/
private discardReasonToString_(reason: LifecycleUnitDiscardReason): string {
switch (reason) {
case LifecycleUnitDiscardReason.EXTERNAL:
return 'external';
case LifecycleUnitDiscardReason.URGENT:
return 'urgent';
case LifecycleUnitDiscardReason.PROACTIVE:
return 'proactive';
case LifecycleUnitDiscardReason.SUGGESTED:
return 'suggested';
}
}
/**
* Returns a string representation of a lifecycle state.
* @param state The lifecycle state.
* @param reason The discard reason. This
* is only used if the state is discard related.
* @param visibility A visibility value.
* @param hasFocus Whether or not the tab has input focus.
* @param stateChangeTime Delta between Unix Epoch and the time at
* which the lifecycle state has changed.
* @return A string representation of the lifecycle state,
* augmented with the discard reason if appropriate.
*/
private lifecycleStateToString_(
state: LifecycleUnitState, reason: LifecycleUnitDiscardReason,
visibility: LifecycleUnitVisibility, hasFocus: boolean,
stateChangeTime: TimeDelta): string {
function pageLifecycleStateFromVisibilityAndFocus(): string {
switch (visibility) {
case LifecycleUnitVisibility.HIDDEN:
case LifecycleUnitVisibility.OCCLUDED:
// An occluded page is also considered hidden.
return 'hidden';
case LifecycleUnitVisibility.VISIBLE:
return hasFocus ? 'active' : 'passive';
}
}
switch (state) {
case LifecycleUnitState.ACTIVE:
return pageLifecycleStateFromVisibilityAndFocus();
case LifecycleUnitState.THROTTLED:
return pageLifecycleStateFromVisibilityAndFocus() + ' (throttled)';
case LifecycleUnitState.FROZEN:
return 'frozen';
case LifecycleUnitState.DISCARDED:
return 'discarded (' + this.discardReasonToString_(reason) + ')' +
((reason === LifecycleUnitDiscardReason.URGENT) ? ' at ' +
// Must convert since Date constructor takes
// milliseconds.
(new Date(Number(stateChangeTime.microseconds) / 1000)
.toLocaleString()) :
'');
}
}
/** Dispatches a request to update tabInfos_. */
private updateTableImpl_() {
this.discardsDetailsProvider_!.getTabDiscardsInfo().then(response => {
this.tabInfos_ = response.infos;
});
}
/**
* A wrapper to updateTableImpl_ that is called due to user action and not
* due to the automatic timer. Cancels the existing timer and reschedules
* it after rendering instantaneously.
*/
private updateTable_() {
if (this.updateTimer_) {
clearInterval(this.updateTimer_);
}
this.updateTableImpl_();
this.updateTimer_ = setInterval(this.updateTableImpl_.bind(this), 1000);
}
/**
* Formats an items site engagement score for display.
* @param item The item in question.
* @return The formatted site engagemetn score.
*/
private getSiteEngagementScore_(item: TabDiscardsInfo): string {
return item.siteEngagementScore.toFixed(1);
}
/**
* Retrieves favicon style tag value for an item.
* @param item The item in question.
* @return A style to retrieve and display the item's favicon.
*/
private getFavIconStyle_(item: TabDiscardsInfo): string {
return 'background-image:' + getFaviconForPageURL(item.tabUrl, false);
}
/**
* Formats an items lifecycle state for display.
* @param item The item in question.
* @return A human readable lifecycle state.
*/
private getLifeCycleState_(item: TabDiscardsInfo): string {
if (item.loadingState !== LifecycleUnitLoadingState.UNLOADED ||
item.discardCount > 0) {
return this.lifecycleStateToString_(
item.state, item.discardReason, item.visibility, item.hasFocus,
item.stateChangeTime);
} else {
return '';
}
}
/**
* Returns a string representation of a boolean value for display in a
* table.
* @param value A boolean value.
* @return A string representing the bool.
*/
private boolToString_(value: boolean): string {
return boolToString(value);
}
/**
* Converts a |secondsAgo| duration to a user friendly string.
* @param secondsAgo The duration to render.
* @return An English string representing the duration.
*/
private durationToString_(secondsAgo: number): string {
return durationToString(secondsAgo);
}
/**
* Tests whether an item has reasons why it cannot be discarded.
* @param item The item in question.
* @return true iff there are reasons why the item cannot be discarded.
*/
private hasCannotDiscardReasons_(item: TabDiscardsInfo): boolean {
return item.cannotDiscardReasons.length !== 0;
}
/**
* Tests whether an item can be loaded.
* @param item The item in question.
* @return true iff the item can be loaded.
*/
private canLoad_(item: TabDiscardsInfo): boolean {
return item.loadingState === LifecycleUnitLoadingState.UNLOADED;
}
/**
* Tests whether an item can be discarded.
* @param item The item in question.
* @return true iff the item can be discarded.
*/
private canDiscard_(item: TabDiscardsInfo): boolean {
if (item.visibility === LifecycleUnitVisibility.HIDDEN ||
item.visibility === LifecycleUnitVisibility.OCCLUDED) {
// Only tabs that aren't visible can be discarded for now.
switch (item.state) {
case LifecycleUnitState.DISCARDED:
return false;
}
return true;
}
return false;
}
/**
* Event handler that toggles the auto discardable flag on an item.
* @param e The event.
*/
private toggleAutoDiscardable_(e: DomRepeatEvent<TabDiscardsInfo>) {
const item = e.model.item;
this.discardsDetailsProvider_!
.setAutoDiscardable(item.id, !item.isAutoDiscardable)
.then(this.updateTable_.bind(this));
}
/** Event handler that loads a tab. */
private loadTab_(e: DomRepeatEvent<TabDiscardsInfo>) {
this.discardsDetailsProvider_!.loadById(e.model.item.id);
}
/** Event handler that discards a given tab urgently. */
private urgentDiscardTab_(e: DomRepeatEvent<TabDiscardsInfo>) {
this.discardsDetailsProvider_!
.discardById(e.model.item.id, LifecycleUnitDiscardReason.URGENT)
.then(this.updateTable_.bind(this));
}
/** Event handler that discards a given tab proactively. */
private proactiveDiscardTab_(e: DomRepeatEvent<TabDiscardsInfo>) {
this.discardsDetailsProvider_!
.discardById(e.model.item.id, LifecycleUnitDiscardReason.PROACTIVE)
.then(this.updateTable_.bind(this));
}
/** Implementation function to discard the next discardable tab. */
private discardImpl_() {
this.discardsDetailsProvider_!.discard().then(() => {
this.updateTable_();
});
}
/** Event handler that discards the next discardable tab urgently. */
private discardUrgentNow_(_e: Event) {
this.discardImpl_();
}
private toggleBatterySaverMode_(_e: Event) {
this.discardsDetailsProvider_!.toggleBatterySaverMode();
}
private refreshPerformanceTabCpuMeasurements_(_e: Event) {
this.discardsDetailsProvider_!.refreshPerformanceTabCpuMeasurements();
}
}
customElements.define(DiscardsTabElement.is, DiscardsTabElement);