blob: e18639e1a69a251d63977865fedc7f05177dc6fa [file] [log] [blame]
# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import bisect
import json
import logging
import time
# The time range for fetching the sample data
SAMPLING_DELTA_PERIOD = 10
FETCHING_DATA_SLEEP_TIME = 0.01
STREAMING_CHART_OUTPUT_TYPE = "streaming_chart"
LINE_CHART_OUTPUT_TYPE = "linechart"
# The list here can help to store the data name
# that we are not going to pass to the visualization UI
FILTER_DATA_NAME_LIST = ["Sample_msecs"]
class DataSampler:
"""The Class can help to extract the power data to json, define the format,
and return the completed data structure
Attributes:
_data_sample: A DataSample which can save the power's data,
and return to http_server
_logger: Sample generator log
_pm: The constructor of the power measurement,
which can help to get the information of the dut power data
_current_data_format: Store current data format which can help
to compare if data format is changed
"""
def __init__(self, pm):
"""Init DataSampler class which will keep updating the data structure that store
the dut power data
"""
self._data_sample = None
self._logger = logging.getLogger(type(self).__name__)
self._pm = pm
self._current_data_format = []
def get_data_sample(self):
"""Convert data into JSON format and return the json data to visualization UI"""
if self._data_sample is None:
return '""'
output_type = STREAMING_CHART_OUTPUT_TYPE
if self._pm.GetPMStatus():
output_type = LINE_CHART_OUTPUT_TYPE
return self._data_sample.to_json(
output_type, time.time() - SAMPLING_DELTA_PERIOD
)
def compare_data_sample_format(self, latest_sample_format):
"""Compare the data format
If the data we received from the power measurement changes, we need
to change the data format to avoid error and update the data we are
going to pass to the visualization UI. Therefore, we need this function
to figure out if the data format is changed.
Args:
latest_sample_format: The latest data format which can be compared to
the current data format
"""
return sorted(self._current_data_format) == sorted(latest_sample_format)
def get_data_sample_format(self, latest_samples):
"""This function define the data sample format
When we first get the data from power measurement, we need to define the
data name which save each kind of data.
For example: the data got from power measurement is
[('ppdut5', 3.1), ('ppservo5', 4.1), ('ppchg5', 0.0)]
Then save the ['ppdut5', 'ppservo5', 'ppcgh5'] as the data sample format
Args:
latest_samples: The data structure which contains the data from power
measurement. latest_samples[0] is the data name, for ex, 'ppchg5',
'ppdut5', we use these names to define the data sample format
"""
data_sample_format = []
for data_name in latest_samples:
# Filter out the data that we do not want to show
# data_name[0] is the label of the data, we filter out
# the label that we do not want to show in the UI
if not data_name[0] in FILTER_DATA_NAME_LIST:
data_sample_format.append(data_name[0])
return data_sample_format
def define_data_sample_format(self, data_sample_format):
"""Pass the data sample format to the DataSample Constructor
Args:
data_sample_format: The data sample format we got
which will be used to save the dut power data
"""
self._current_data_format = data_sample_format
data_sample_format = ["time"] + data_sample_format
self._data_sample = DataSample(data_sample_format)
def sample_generator(self):
"""This function provide a while loop keeps fetching the updated power data
If the data we get from power is empty, it means that the thread of
power measurement have not opened yet wait for this thread opens.
If the data sample format is empty, we need to define the format first
When we get the data from power measurement,
we feed the data into DataSample Class and convert the data into json format
Notes:
The example data we get from the measure_power
[('ppdut5', 3.1), ('ppservo5', 4.2),
('ppchg5', 0.0), ('Sample_msecs', 302.1)]
"""
self._logger.info("Begin the sample generator to fetch the power data")
while not self._pm.GetPMStatus():
# Get the power data from power management
latest_samples = self._pm.GetSampleData()
if latest_samples:
data_sample_format = self.get_data_sample_format(latest_samples)
# If the data sample format is empty, we need to find
# how many data we currently have, and define the data sample format,
# Or if the latest data sample format is different with the one
# we currently use, we need to redefine the data sample format
if not self._data_sample or not self.compare_data_sample_format(
data_sample_format
):
self.define_data_sample_format(data_sample_format)
# After fetching the data, we can clean the temporary
# data structure in power measurement
self._pm.CleanSampleData()
# After define the format, read the data from power measurement
# and insert into the data structure
temp_latest_samples = []
for data in latest_samples:
# data looks like data: ('ppdut5', 3.1)
# data[0] is the label of the data: 'ppdut5'
# data[1] is the actual value of the data: 3.1
if not data[0] in FILTER_DATA_NAME_LIST:
temp_latest_samples.append(data[1])
self._data_sample.add_samples([time.time()] + temp_latest_samples)
time.sleep(FETCHING_DATA_SLEEP_TIME)
class DataSample:
"""This class help to define data format,insert data and return data in json
We use JSON format to transmit the data, JSON format is a text format
for storing and transporting data, it is commonly used for transmitting
the data to WEB.
JSON defines object to properties and each property has its value
Attributes:
_samples: The data we save
_meta: The data name that we will show on the visualization UI
_logger: DataSample Logger
The data received at html:
matrix: Array(5)
0: (3) [13, 14, 15]
1: (3) [856.2, 854.7, 853.9]
2: (3) [3.5, 2.4, 2.4]
3: (3) [0, 0, 0
4: (3) [184.5, 181.3, 182.2]
length: 5
meta:
rowMeta: Array(5)
0: {name: 'time'}
1: {name: 'ppservo5'}
2: {name: 'ppdut5'}
3: {name: 'ppchg5'}
4: {name: 'Sample_msecs'}
length: 5
type: "streaming_chart"
"""
def __init__(self, sample_names):
"""Init DataSample class by defining the metadata format
There are several trackers in the power measurement,
each tracker contains the data which can help to analysis power's condition
As the real-time visualization, keep fetching the data and
append the data into json format.
In our metadata format we have rowData to save all type of data,
matrix saves the corresponding data value of each type of data,
Args:
sample_names are all the metadata names we get from power measurement
"""
self._logger = logging.getLogger(type(self).__name__)
self._samples = []
# rowMeta contains all type of data we have
self._meta = {"rowMeta": []}
for name in sample_names:
self._samples.append([])
row_meta = {"name": name}
self._meta["rowMeta"].append(row_meta)
def add_samples(self, samples):
"""Insert the sample data
Args:
samples: The data which is going to be insert into the _samples
"""
try:
for sample_pos, sample_element in enumerate(samples):
self._samples[sample_pos].append(sample_element)
except Exception:
if len(self._samples) != len(samples):
self._logger.error(
"The number of new samples"
"does not match with the existing samples"
)
else:
self._logger.error("Error happen while inserting the data")
self._logger.error(
"The power measurement is still working, "
"press ctrl-c to show the summary table "
"and close the visualization server"
)
def to_json(self, out_type, start_time=0):
"""Convert the data into json format
Args:
out_type: The output type will always be "streaming_chart" right now,
this variable can help the UI present our data in different modes
start_time: The start time we are going to fetch the data
Returns:
A json format. For example:
As the example below, the first line in Matrix matched to time in meta,
second line in Matrix matches to ppchg5 in meta...
{"type": "streaming_chart",
"meta": {"rowMeta": [{"name": "time"}, {"name": "ppchg5"},
{"name": "ppdut5"}, {"name": "ppservo5"},
{"name": "Sample_msecs"}]},
"matrix": [[13.1, 14.2, 15.6],
[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0],
[725.1, 727.3, 726.2],
[175.8, 180.4, 182.5]]}
"""
end = len(self._samples[-1])
start = bisect.bisect(self._samples[0], start_time)
transmit = []
for samples in self._samples:
transmit.append(samples[start:end])
data = {"type": out_type, "meta": self._meta, "matrix": transmit}
return json.dumps(data)