chromium/content/browser/resources/webxr_internals/session_info_table.ts

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {CustomElement} from 'chrome://resources/js/custom_element.js';

import {getTemplate} from './session_info_table.html.js';
import * as TimeUtil from './time_util.js';
import type {SessionRejectedRecord, SessionRequestedRecord, SessionStartedRecord, SessionStoppedRecord} from './webxr_internals.mojom-webui.js';
import * as XRRuntimeUtil from './xr_runtime_util.js';
import * as XRSessionUtil from './xr_session_util.js';

const COLUMN_NAMES = [
  'Trace ID',
  'Session State',
  'Attributes',
];

export class SessionInfoTableElement extends CustomElement {
  private traceIdToTableCell: {[key: string]: HTMLTableCellElement} = {};

  static override get template() {
    return getTemplate();
  }

  constructor() {
    super();

    const table =
        this.getRequiredElement<HTMLTableElement>('#session-info-table');

    const headerRow = table.insertRow();
    COLUMN_NAMES.forEach((columnName) => {
      const headerCell = document.createElement('th');
      headerCell.textContent = columnName;
      headerRow.appendChild(headerCell);
    });
  }

  addSessionRequestedRow(sessionRequestedRecord: SessionRequestedRecord) {
    const {options, requestedTime} = sessionRequestedRecord;
    const {traceId, mode, requiredFeatures, optionalFeatures, depthOptions} =
        options;

    const attributes = [
      `Mode: ${XRSessionUtil.sessionModeToString(mode)}`,
      `Required Features: ${
          requiredFeatures.map(XRSessionUtil.sessionFeatureToString)
              .join(', ')}`,
      `Optional Features: ${
          optionalFeatures.map(XRSessionUtil.sessionFeatureToString)
              .join(', ')}`,
      `Requested Time: ${TimeUtil.formatMojoTime(requestedTime)}`,
    ];

    const depthUsagePreferences = depthOptions?.usagePreferences || [];
    if (depthUsagePreferences.length) {
      attributes.push(`Depth Usage Preferences: ${
          depthUsagePreferences.map(XRSessionUtil.depthUsageToString)
              .join(', ')}`);
    }

    const depthDataFormatPreferences =
        depthOptions?.dataFormatPreferences || [];
    if (depthDataFormatPreferences.length) {
      attributes.push(`Depth Data Format Preferences: ${
          depthDataFormatPreferences.map(XRSessionUtil.depthFormatToString)
              .join(', ')}`);
    }

    this.addSessionRow(traceId.toString(), 'Requested', attributes);
  }

  addSessionRejectedRow(sessionRejectedRecord: SessionRejectedRecord) {
    const {
      traceId,
      failureReason,
      failureReasonDescription,
      rejectedFeatures,
      rejectedTime,
    } = sessionRejectedRecord;

    const rejectedFeaturesDescription = rejectedFeatures.length ?
        ` rejectedFeatures=${
            rejectedFeatures.map(XRSessionUtil.sessionFeatureToString)
                .join(', ')}` :
        '';
    const attributes = [
      `Failure Reason: ${
          XRSessionUtil.requestSessionErrorToString(failureReason)}`,
      `Failure Reason Description: ${failureReasonDescription} ${
          rejectedFeaturesDescription}`,
      `Rejected Time: ${TimeUtil.formatMojoTime(rejectedTime)}`,
    ];

    this.addSessionRow(traceId.toString(), 'Rejected', attributes);
  }

  addSessionStartedRow(sessionStartedRecord: SessionStartedRecord) {
    const {traceId, deviceId, startedTime} = sessionStartedRecord;
    const attributes = [
      `Device Id: ${XRRuntimeUtil.deviceIdToString(deviceId)}`,
      `Started Time: ${TimeUtil.formatMojoTime(startedTime)}`,
    ];

    this.addSessionRow(traceId.toString(), 'Started', attributes);
  }

  addSessionStoppedRow(sessionStoppedRecord: SessionStoppedRecord) {
    const {traceId, stoppedTime} = sessionStoppedRecord;
    const attributes = [
      `Stopped Time: ${TimeUtil.formatMojoTime(stoppedTime)}`,
    ];

    this.addSessionRow(traceId.toString(), 'Stopped', attributes);
  }

  private createTableCell(textContent: string = ''): HTMLTableCellElement {
    const cell = document.createElement('td');
    cell.textContent = textContent;
    return cell;
  }

  private createAttributesList(attributes: string[]): HTMLUListElement {
    const ul = document.createElement('ul');
    ul.style.padding = '0';

    attributes.forEach((attribute) => {
      const li = document.createElement('li');
      li.textContent = attribute;
      ul.appendChild(li);
    });

    return ul;
  }

  private updateTraceIdCell(traceId: string, newRow: HTMLTableRowElement) {
    let traceIdCell = this.traceIdToTableCell[traceId];

    if (traceIdCell === undefined) {
      traceIdCell = this.createTableCell(traceId);
      newRow.appendChild(traceIdCell);
      traceIdCell.rowSpan = 1;
      this.traceIdToTableCell[traceId] = traceIdCell;
    } else {
      traceIdCell.rowSpan++;
    }
  }

  private addSessionRow(
      traceId: string, sessionType: string, attributes: string[]) {
    const table =
        this.getRequiredElement<HTMLTableElement>('#session-info-table');
    const newRow = table.insertRow();

    this.updateTraceIdCell(traceId, newRow);

    const sessionTypeCell = this.createTableCell(sessionType);
    newRow.appendChild(sessionTypeCell);

    const attributesCell = this.createTableCell();
    const ul = this.createAttributesList(attributes);
    attributesCell.appendChild(ul);
    newRow.appendChild(attributesCell);
  }
}

// Declare the custom element
declare global {
  interface HTMLElementTagNameMap {
    'session-info-table': SessionInfoTableElement;
  }
}

customElements.define('session-info-table', SessionInfoTableElement);