chromium/chrome/browser/resources/chromeos/network_ui/network_metrics_ui.ts

// Copyright 2020 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/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_radio_button/cr_radio_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_shared_style.css.js';

import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getTemplate} from './network_metrics_ui.html.js';
import {NetworkUiBrowserProxy, NetworkUiBrowserProxyImpl} from './network_ui_browser_proxy.js';
import {uPlot} from './third_party/uPlot.iife.min.js';

/**
 * @fileoverview
 * Polymer element for UI controlling the WiFi performance
 * metrics and their values.
 */

const NetworkMetricsUiElementBase = I18nMixin(PolymerElement);

class NetworkMetricsUiElement extends NetworkMetricsUiElementBase {
  static get is() {
    return 'network-metrics-ui' as const;
  }

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

  static get properties() {
    return {
      /**
       * Circular buffer of WiFi.SignalStrengthRssi
       * values from shill used for rendering graph.
       */
      rssiValues_: {
        type: Array,
        value: () => [],
      },

      minRssi_: {
        type: Number,
        value: -100,
      },

      maxRssi_: {
        type: Number,
        value: -25,
      },

      /**
       * Circular buffer of data extraction times used for rendering graph.
       */
      timeValues_: {
        type: Array,
        value: () => [],
      },

      running_: {
        type: Boolean,
        value: false,
      },

      graphRendered_: {
        type: Boolean,
        value: false,
      },

      /**
       * Milliseconds delay between extraction of data.
       */
      delay_: {
        type: Number,
        value: 500,
      },

      /**
       * Max data points to track in circular buffer.
       */
      dataCap_: {
        type: Number,
        value: 100,
      },
    };
  }

  private rssiValues_: number[];
  private minRssi_: number;
  private maxRssi_: number;
  private timeValues_: number[];
  private running_: boolean;
  private graphRendered_: boolean;
  private delay_: number;
  private dataCap_: number;

  private browserProxy_: NetworkUiBrowserProxy =
      NetworkUiBrowserProxyImpl.getInstance();

  private start_() {
    this.running_ = true;
  }

  private stop_() {
    this.running_ = false;
  }

  private decreaseDelay_() {
    const minDelay = 1000 / 8;  // 8Hz
    if (this.delay_ > minDelay) {
      this.delay_ /= 2;
    }
  }

  private increaseDelay_() {
    this.delay_ *= 2;
  }

  /**
   * Requests first WiFi's properties and updates metric arrays
   * when response contains the network information.
   */
  private async updateMetrics_() {
    const response = await this.browserProxy_.getFirstWifiNetworkProperties();
    if (response.length <= 0) {
      return;
    }
    const properties = response[0];
    this.updateRssi_(properties['WiFi.SignalStrengthRssi']);
    this.updateTime_();
  }

  /**
   * Updates Rssi array with extracted signal value.
   */
  private updateRssi_(data: number) {
    if (this.rssiValues_.length >= this.dataCap_) {
      this.rssiValues_.shift();
    }
    this.rssiValues_.push(data);
  }

  /**
   * Updates time array with current time value.
   */
  private updateTime_() {
    const currDate = new Date();
    if (this.timeValues_.length > this.dataCap_) {
      this.timeValues_.shift();
    }
    this.timeValues_.push(currDate.getTime() / 1000);
  }

  /**
   * Updates metrics and creates nested array series required
   * as input for the uPlot graph.
   */
  private getMetrics_(): uPlot.AlignedData {
    this.updateMetrics_();
    return [
      this.timeValues_,
      this.rssiValues_,
    ];
  }

  /**
   * Renders uPlot graph and initiates asynchronous loop
   * to keep updating with new values.
   */
  private renderGraph_() {
    if (!this.graphRendered_) {
      const graph = this.makeChart_(this.getMetrics_());
      this.loop_(graph);
      this.graphRendered_ = true;
    }
  }

  private wait_(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  /**
   * Repeatedly updates the uPlot graph with new data while the
   * running state is active. Time between updates is determined by
   * the delay property.
   */
  private async loop_(graph: uPlot) {
    while (true) {
      if (this.running_) {
        const updatedData = this.getMetrics_();
        graph.setData(updatedData);
      }
      await this.wait_(this.delay_);
    }
  }

  /**
   * Handles all uPlot functionality.
   */
  private makeChart_(data: uPlot.AlignedData): uPlot {
    const opts: uPlot.Options = {
      title: 'Rssi vs Time',
      width: window.innerWidth * .9,
      height: window.innerHeight * .667,
      scales: {
        x: {time: true},
        y: {
          auto: false,
          range: [this.minRssi_, this.maxRssi_],
        },
      },
      series: [
        {},
        {
          label: 'Rssi',
          stroke: 'red',
        },
      ],
    };
    return new uPlot(
        opts, data, this.shadowRoot!.getElementById('metrics-graph')!);
  }
}

declare global {
  interface HTMLElementTagNameMap {
    [NetworkMetricsUiElement.is]: NetworkMetricsUiElement;
  }
}

customElements.define(NetworkMetricsUiElement.is, NetworkMetricsUiElement);