chromium/content/browser/resources/quota/quota_internals.ts

// Copyright 2022 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/cr_elements/cr_tab_box/cr_tab_box.js';

import type {Time} from 'chrome://resources/mojo/mojo/public/mojom/base/time.mojom-webui.js';

import type {BucketTableEntry} from './quota_internals.mojom-webui.js';
import type {RetrieveBucketsTableResult} from './quota_internals_browser_proxy.js';
import {QuotaInternalsBrowserProxy, StorageType} from './quota_internals_browser_proxy.js';

// Object for constructing the bucket row in the usage table.
interface StorageTypeBucketTableEntry {
  'bucketId': string;
  'name': string;
  'usage': string;
  'useCount': string;
  'lastAccessed': string;
  'lastModified': string;
}

// Bucket entries organized by StorageType for constructing the usage table.
interface StorageTypeEntries {
  // key = storageType
  [key: string]: StorageTypeBucketTableEntry[];
}

// StorageTypeEntries organized by StorageKey for constructing the usage table.
interface StorageKeyData {
  'bucketCount': number;
  'storageKeyEntries': StorageTypeEntries;
}

// Map of StorageKey entries for constructing bucket usage table.
interface BucketTableEntriesByStorageKey {
  // key = storageKey
  [key: string]: StorageKeyData;
}

// Converts a mojo time to a JS time.
function convertMojoTimeToJS(mojoTime: Time): Date {
  // The JS Date() is based off of the number of milliseconds since
  // the UNIX epoch (1970-01-01 00::00:00 UTC), while |internalValue|
  // of the base::Time (represented in mojom.Time) represents the
  // number of microseconds since the Windows FILETIME epoch
  // (1601-01-01 00:00:00 UTC). This computes the final JS time by
  // computing the epoch delta and the conversion from microseconds to
  // milliseconds.
  const windowsEpoch = Date.UTC(1601, 0, 1, 0, 0, 0, 0);
  const unixEpoch = Date.UTC(1970, 0, 1, 0, 0, 0, 0);
  // |epochDeltaInMs| equals to
  // base::Time::kTimeTToMicrosecondsOffset.
  const epochDeltaInMs = unixEpoch - windowsEpoch;
  const timeInMs = Number(mojoTime.internalValue) / 1000;

  return new Date(timeInMs - epochDeltaInMs);
}

function getProxy(): QuotaInternalsBrowserProxy {
  return QuotaInternalsBrowserProxy.getInstance();
}

async function renderDiskAvailabilityAndTempPoolSize() {
  const result = await getProxy().getDiskAvailabilityAndTempPoolSize();

  const rowTemplate: HTMLTemplateElement =
      document.body.querySelector<HTMLTemplateElement>('#listener-row')!;
  const tableBody: HTMLTableElement =
      document.body.querySelector<HTMLTableElement>('#listeners-tbody')!;
  const listenerRowTemplate: HTMLTemplateElement =
      rowTemplate.cloneNode(true) as HTMLTemplateElement;
  const listenerRow = listenerRowTemplate.content;

  const availableSpaceBytes =
      (Number(result.availableSpace) / (1024 ** 3)).toFixed(2);
  const totalSpaceBytes = (Number(result.totalSpace) / (1024 ** 3)).toFixed(2);
  const tempPoolSizeBytes =
      (Number(result.tempPoolSize) / (1024 ** 3)).toFixed(2);

  listenerRow.querySelector('.total-space')!.textContent =
      `${totalSpaceBytes} GB`;
  listenerRow.querySelector('.available-space')!.textContent =
      `${availableSpaceBytes} GB`;
  listenerRow.querySelector('.temp-pool-size')!.textContent =
      `${tempPoolSizeBytes} GB`;

  tableBody.append(listenerRow);
}

async function renderGlobalUsage() {
  const typeVals: number[] =
      Object.keys(StorageType).map((v) => Number(v)).filter((v) => !isNaN(v));

  for (const typeVal of typeVals) {
    const result = await getProxy().getGlobalUsage(typeVal);
    const formattedResultString: string = `${Number(result.usage)} B (${
        result.unlimitedUsage} B for unlimited origins)`;
    document.body
        .querySelector(`.${
            StorageType[typeVal].toLowerCase()}-global-and-unlimited-usage`)!
        .textContent = formattedResultString;
  }
}

async function renderEvictionStats() {
  const result = await getProxy().getStatistics();

  const rowTemplate: HTMLTemplateElement =
      document.body.querySelector<HTMLTemplateElement>('#eviction-row')!;
  const tableBody: HTMLTableElement =
      document.body.querySelector<HTMLTableElement>('#eviction-tbody')!;
  const evictionRowTemplate: HTMLTemplateElement =
      rowTemplate.cloneNode(true) as HTMLTemplateElement;
  const evictionRow = evictionRowTemplate.content;

  evictionRow.querySelector('.errors-on-getting-usage-and-quota')!.textContent =
      result.evictionStatistics['errors-on-getting-usage-and-quota'];
  evictionRow.querySelector('.evicted-buckets')!.textContent =
      result.evictionStatistics['evicted-buckets'];
  evictionRow.querySelector('.eviction-rounds')!.textContent =
      result.evictionStatistics['eviction-rounds'];
  evictionRow.querySelector('.skipped-eviction-rounds')!.textContent =
      result.evictionStatistics['skipped-eviction-rounds'];

  tableBody.appendChild(evictionRow);
}

async function renderUsageAndQuotaStats() {
  const bucketTable: RetrieveBucketsTableResult =
      await getProxy().retrieveBucketsTable();
  const bucketTableEntries: BucketTableEntry[] = bucketTable.entries;
  const bucketTableEntriesByStorageKey: BucketTableEntriesByStorageKey = {};

  /* Re-structure bucketTableEntries data to be accessible by a storage key.
   * bucketTableEntriesByStorageKey = {
   *   <storage_key_string>: {
   *     bucketCount: <number>,
   *     storageKeyEntries: {
   *       <storage_type>: [{
   *         bucketId: <bigint>,
   *         name: <string>,
   *         usage: <bigint>,
   *         useCount: <bigint>,
   *         lastAccessed: <Time>,
   *         lastModified: <Time>
   *          }]
   *        }
   *      }
   *    }
   */

  for (let i = 0; i < bucketTableEntries.length; i++) {
    const entry = bucketTableEntries[i];
    const bucketTableEntryObj: StorageTypeBucketTableEntry = {
      bucketId: entry.bucketId.toString(),
      name: entry.name,
      usage: entry.usage.toString(),
      useCount: entry.useCount.toString(),
      lastAccessed: convertMojoTimeToJS(entry.lastAccessed)
                        .toLocaleString('en-US', {timeZoneName: 'short'}),
      lastModified: convertMojoTimeToJS(entry.lastModified)
                        .toLocaleString('en-US', {timeZoneName: 'short'}),
    };

    if (!(entry.storageKey in bucketTableEntriesByStorageKey)) {
      bucketTableEntriesByStorageKey[entry.storageKey] = {
        'bucketCount': 0,
        'storageKeyEntries': {},
      };
    }
    if (!(entry.type in bucketTableEntriesByStorageKey[entry.storageKey]
                                                      ['storageKeyEntries'])) {
      bucketTableEntriesByStorageKey[entry
                                         .storageKey]['storageKeyEntries'][entry
                                                                               .type] =
          [bucketTableEntryObj];
      bucketTableEntriesByStorageKey[entry.storageKey]['bucketCount'] += 1;
    } else {
      bucketTableEntriesByStorageKey[entry
                                         .storageKey]['storageKeyEntries'][entry
                                                                               .type]
          .push(bucketTableEntryObj);
      bucketTableEntriesByStorageKey[entry.storageKey]['bucketCount'] += 1;
    }
  }

  const storageKeys: string[] = Object.keys(bucketTableEntriesByStorageKey);

  /* Populate the rows of the Usage and Quota table by iterating over:
   * each storage key in bucketTableEntriesByStorageKey,
   * each storage key's storage type(s),
   * and each storage type's bucket(s). */

  // Iterate over each storageKey in bucketTableEntriesByStorageKey.
  for (let i = 0; i < storageKeys.length; i++) {
    const storageKey: string = storageKeys[i];
    const storageKeyRowSpan =
        bucketTableEntriesByStorageKey[storageKey]['bucketCount'];
    const bucketsByStorageType: StorageTypeEntries =
        bucketTableEntriesByStorageKey[storageKey]['storageKeyEntries'];
    const storageTypes: StorageType[] =
        Object.keys(bucketsByStorageType).map(typeStr => Number(typeStr));

    // Iterate over each storageType for a given storage key.
    for (let j = 0; j < storageTypes.length; j++) {
      const storageType: StorageType = storageTypes[j];
      const bucketsForStorageType: StorageTypeBucketTableEntry[] =
          bucketsByStorageType[storageType];
      const storageTypeRowSpan: number =
          bucketsByStorageType[storageType].length;

      // Iterate over each bucket for a given storageKey and storageType.
      for (let k = 0; k < bucketsForStorageType.length; k++) {
        const isFirstStorageKeyRow: boolean = (j === 0 && k === 0);
        const isFirstStorageType: boolean = (k === 0);

        // Initialize a Usage and Quota table row template.
        const rowTemplate: HTMLTemplateElement =
            document.body.querySelector<HTMLTemplateElement>(
                '#usage-and-quota-row')!;
        const tableBody: HTMLTableElement =
            document.body.querySelector<HTMLTableElement>(
                '#usage-and-quota-tbody')!;
        const usageAndQuotaRowTemplate: HTMLTemplateElement =
            rowTemplate.cloneNode(true) as HTMLTemplateElement;
        const usageAndQuotaRow = usageAndQuotaRowTemplate.content;

        usageAndQuotaRow.querySelector('.storage-key')!.textContent =
            storageKey;
        usageAndQuotaRow.querySelector('.storage-key')!.setAttribute(
            'rowspan', `${storageKeyRowSpan}`);
        usageAndQuotaRow.querySelector('.storage-type')!.textContent =
            StorageType[storageType];
        usageAndQuotaRow.querySelector('.storage-type')!.setAttribute(
            'rowspan', `${storageTypeRowSpan}`);
        usageAndQuotaRow.querySelector('.bucket')!.textContent =
            bucketsForStorageType[k].name;
        usageAndQuotaRow.querySelector('.usage')!.textContent =
            bucketsForStorageType[k].usage;
        usageAndQuotaRow.querySelector('.use-count')!.textContent =
            bucketsForStorageType[k].useCount;
        usageAndQuotaRow.querySelector('.last-accessed')!.textContent =
            bucketsForStorageType[k].lastAccessed;
        usageAndQuotaRow.querySelector('.last-modified')!.textContent =
            bucketsForStorageType[k].lastModified;

        /* If the current row is not the first of its kind for a given storage
         * key, remove the storage key cell when appending the row to the table
         * body. This creates a nested row to the right of the storage key cell.
         */
        if (!isFirstStorageKeyRow) {
          usageAndQuotaRow.querySelector('.storage-key')!.remove();
        }

        /* If the current storage type (temporary, syncable) is not
         * the first of its kind for a given storage key and storage type,
         * remove the Storage Type cells from the row before
         * appending the row to the table body.
         * This creates a nested row to the right of the Storage Type cell. */
        if (!isFirstStorageType) {
          usageAndQuotaRow.querySelector('.storage-type')!.remove();
        }
        tableBody.appendChild(usageAndQuotaRow);
      }
    }
  }
}

async function renderSimulateStoragePressureButton() {
  getProxy().isSimulateStoragePressureAvailable().then(result => {
    if (!result.available) {
      document.body
          .querySelector('#simulate-storage-pressure-activation-message')
          ?.removeAttribute('hidden');
      document.body.querySelector('#trigger-notification')!.setAttribute(
          'disabled', '');
    }
  });

  document.body.querySelector('#trigger-notification')!.addEventListener(
      'click', () => getProxy().simulateStoragePressure());
}

document.addEventListener('DOMContentLoaded', () => {
  renderDiskAvailabilityAndTempPoolSize();
  renderEvictionStats();
  renderGlobalUsage();
  renderUsageAndQuotaStats();
  renderSimulateStoragePressureButton();
});