1/*
2 * Copyright (C) 2022 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import {
17  BaseStruct,
18  ns2x,
19  Render,
20  RequestMessage,
21  isFrameContainPoint,
22  drawLoadingFrame,
23  Rect,
24} from './ProcedureWorkerCommon';
25import { TraceRow } from '../../component/trace/base/TraceRow';
26
27export class EnergyPowerRender extends Render {
28  renderMainThread(
29    powerReq: { useCache: boolean; context: CanvasRenderingContext2D; type: string; appName: string },
30    row: TraceRow<EnergyPowerStruct>
31  ): void {
32    let list = row.dataList;
33    let filter = row.dataListCache;
34    power(
35      list,
36      filter,
37      TraceRow.range!.startNS,
38      TraceRow.range!.endNS,
39      TraceRow.range!.totalNS,
40      row.frame,
41      powerReq.useCache || !TraceRow.range!.refresh,
42      powerReq.appName
43    );
44    drawLoadingFrame(powerReq.context, row.dataListCache, row);
45    powerReq.context.beginPath();
46    let find = false;
47    for (let i = 0; i < list.length; i++) {
48      let re = list[i];
49      EnergyPowerStruct.draw(powerReq, i, re, row);
50      if (row.isHover && re.frame && isFrameContainPoint(re.frame, row.hoverX, row.hoverY)) {
51        EnergyPowerStruct.hoverEnergyPowerStruct = re;
52        find = true;
53      }
54    }
55    if (!find && row.isHover) {
56      EnergyPowerStruct.hoverEnergyPowerStruct = undefined;
57    }
58    TraceRow.range!.refresh = true;
59    if (EnergyPowerStruct.maxPower !== 0) {
60      let s = EnergyPowerStruct.maxPower + 'mAs';
61      let textMetrics = powerReq.context.measureText(s);
62      powerReq.context.globalAlpha = 1.0;
63      powerReq.context.fillStyle = '#f0f0f0';
64      powerReq.context.fillRect(0, 5, textMetrics.width + 8, 18);
65      powerReq.context.globalAlpha = 1;
66      powerReq.context.fillStyle = '#333';
67      powerReq.context.textBaseline = 'middle';
68      powerReq.context.fillText(s, 4, 5 + 9);
69    }
70    powerReq.context.closePath();
71    let spApplication = document.getElementsByTagName('sp-application')[0];
72    let isDark = spApplication.hasAttribute('dark');
73    drawLegend(powerReq, isDark);
74  }
75}
76
77export function drawLegend(
78  req: { useCache: boolean; context: CanvasRenderingContext2D; type: string; appName: string },
79  isDark?: boolean
80): void {
81  let textList = ['CPU', 'LOCATION', 'GPU', 'DISPLAY', 'CAMERA', 'BLUETOOTH', 'FLASHLIGHT', 'AUDIO', 'WIFISCAN'];
82  for (let index = 0; index < textList.length; index++) {
83    let text = req.context.measureText(textList[index]);
84    req.context.fillStyle = EnergyPowerStruct.getHistogramColor(textList[index]);
85    req.context.globalAlpha = 1;
86    let canvasEndX = req.context.canvas.clientWidth - EnergyPowerStruct.OFFSET_WIDTH;
87    let textColor = isDark ? '#FFFFFF' : '#333';
88    if (index === 0) {
89      req!.context.fillRect(canvasEndX - EnergyPowerStruct.powerItemNumber * 80, 12, 8, 8);
90      req.context.globalAlpha = 1;
91      req.context.fillStyle = textColor;
92      req.context.textBaseline = 'middle';
93      req.context.fillText(textList[index], canvasEndX - EnergyPowerStruct.powerItemNumber * 80 + 10, 18);
94      EnergyPowerStruct.currentTextWidth = canvasEndX - EnergyPowerStruct.powerItemNumber * 80 + 40 + text.width;
95    } else {
96      req!.context.fillRect(EnergyPowerStruct.currentTextWidth, 12, 8, 8);
97      req.context.globalAlpha = 1;
98      req.context.fillStyle = textColor;
99      req.context.textBaseline = 'middle';
100      req!.context.fillText(textList[index], EnergyPowerStruct.currentTextWidth + 12, 18);
101      EnergyPowerStruct.currentTextWidth = EnergyPowerStruct.currentTextWidth + 40 + text.width;
102    }
103  }
104  req.context.fillStyle = '#333';
105}
106
107export function power(
108  list: Array<EnergyPowerStruct>,
109  res: Array<EnergyPowerStruct>,
110  startNS: number,
111  endNS: number,
112  totalNS: number,
113  frame: Rect,
114  use: boolean,
115  appName: string
116): void {
117  EnergyPowerStruct.maxPower = 0;
118  list.length = 0;
119  let firstData = [];
120  if (use && res.length > 0) {
121    for (let index = 0; index < res.length; index++) {
122      let item = res[index];
123      //@ts-ignore
124      let obj = item[appName];
125      if (obj !== undefined && obj.ts + 1000000000 > (startNS || 0) && (obj.ts || 0) < (endNS || 0)) {
126        firstData.push(obj);
127      }
128    }
129    let array = firstData.sort((a, b) => a.ts - b.ts);
130    setFirstDataArray(array, list);
131    computeMaxPower(array, list, startNS, endNS, totalNS, frame);
132  }
133}
134
135function setFirstDataArray(array: EnergyPowerStruct[], list: Array<EnergyPowerStruct>): void {
136  array.forEach((item) => {
137    if (
138      list.length > 0 &&
139      item.ts + 500000000 >= list[list.length - 1].ts &&
140      item.ts - 500000000 <= list[list.length - 1].ts
141    ) {
142      list[list.length - 1].cpu = item.cpu === 0 ? list[list.length - 1].cpu : item.cpu;
143      list[list.length - 1].location = item.location === 0 ? list[list.length - 1].location : item.location;
144      list[list.length - 1].gpu = item.gpu === 0 ? list[list.length - 1].gpu : item.gpu;
145      list[list.length - 1].display = item.display === 0 ? list[list.length - 1].display : item.display;
146      list[list.length - 1].camera = item.camera === 0 ? list[list.length - 1].camera : item.camera;
147      list[list.length - 1].bluetooth = item.bluetooth === 0 ? list[list.length - 1].bluetooth : item.bluetooth;
148      list[list.length - 1].flashlight = item.flashlight === 0 ? list[list.length - 1].flashlight : item.flashlight;
149      list[list.length - 1].audio = item.audio === 0 ? list[list.length - 1].audio : item.audio;
150      list[list.length - 1].wifiscan = item.wifiscan === 0 ? list[list.length - 1].wifiscan : item.wifiscan;
151    } else {
152      list.push(item);
153    }
154  });
155}
156
157function computeMaxPower(
158  array: Array<EnergyPowerStruct>,
159  list: Array<EnergyPowerStruct>,
160  startNS: number,
161  endNS: number,
162  totalNS: number,
163  frame: Rect
164): void {
165  array.forEach((item) => {
166    if (list.indexOf(item) >= 0) {
167      EnergyPowerStruct.setPowerFrame(item, 5, startNS || 0, endNS || 0, totalNS || 0, frame);
168      let max =
169        (item.cpu || 0) +
170        (item.location || 0) +
171        (item.gpu || 0) +
172        (item.display || 0) +
173        (item.camera || 0) +
174        (item.bluetooth || 0) +
175        (item.flashlight || 0) +
176        (item.audio || 0) +
177        (item.wifiscan || 0);
178      if (max > EnergyPowerStruct.maxPower) {
179        EnergyPowerStruct.maxPower = max;
180      }
181    }
182  });
183}
184
185export class EnergyPowerStruct extends BaseStruct {
186  static maxPower: number = 0;
187  static maxPowerName: string = '0';
188  static powerItemNumber: number = 9;
189  static currentTextWidth: number = 0;
190  static rowHeight: number = 200;
191  static appName: string | undefined;
192  static hoverEnergyPowerStruct: EnergyPowerStruct | undefined;
193  static selectEnergyPowerStruct: EnergyPowerStruct | undefined;
194  static OFFSET_WIDTH: number = 266;
195  name: string | undefined;
196  appKey: string | undefined;
197  eventValue: string | undefined;
198  eventName: string | undefined;
199  id: number | undefined;
200  ts: number = 0;
201  cpu: number = 0;
202  location: number = 0;
203  gpu: number = 0;
204  display: number = 0;
205  camera: number = 0;
206  bluetooth: number = 0;
207  flashlight: number = 0;
208  audio: number = 0;
209  wifiscan: number = 0;
210
211  static draw(
212    req: { useCache: boolean; context: CanvasRenderingContext2D; type: string; appName: string },
213    index: number,
214    data: EnergyPowerStruct,
215    row: TraceRow<EnergyPowerStruct>
216  ): void {
217    if (data.frame) {
218      req!.context.globalAlpha = 1.0;
219      req!.context.lineWidth = 1;
220      this.currentTextWidth = 0;
221      let cpuHeight = this.drawHistogram(req, data, -1, data.cpu!, 'CPU', row.frame);
222      let locationHeight = this.drawHistogram(req, data, cpuHeight, data.location!, 'LOCATION', row.frame);
223      let gpuHeight = this.drawHistogram(req, data, cpuHeight - locationHeight, data.gpu!, 'GPU', row.frame);
224      let dHight = cpuHeight - locationHeight - gpuHeight;
225      let displayHeight = this.drawHistogram(req, data, dHight, data.display!, 'DISPLAY', row.frame);
226      let cHight = cpuHeight - locationHeight - gpuHeight - displayHeight;
227      let cameraHeight = this.drawHistogram(req, data, cHight, data.camera!, 'CAMERA', row.frame);
228      let bHeight = cpuHeight - locationHeight - gpuHeight - displayHeight - cameraHeight;
229      let bluetoothHeight = this.drawHistogram(req, data, bHeight, data.bluetooth!, 'BLUETOOTH', row.frame);
230      let fHeight = cpuHeight - locationHeight - gpuHeight - displayHeight - cameraHeight - bluetoothHeight;
231      let flashlightHeight = this.drawHistogram(req, data, fHeight, data.flashlight!, 'FLASHLIGHT', row.frame);
232      let aHeight =
233        cpuHeight - locationHeight - gpuHeight - displayHeight - cameraHeight - bluetoothHeight - flashlightHeight;
234      let audioHeight = this.drawHistogram(req, data, aHeight, data.audio!, 'AUDIO', row.frame);
235      let wHeight =
236        cpuHeight -
237        locationHeight -
238        gpuHeight -
239        displayHeight -
240        cameraHeight -
241        bluetoothHeight -
242        flashlightHeight -
243        audioHeight;
244      let wifiHeight = this.drawHistogram(req, data, wHeight, data.wifiscan!, 'WIFISCAN', row.frame);
245      let maxPointY = this.drawPolyline(req, index, data, row.frame, wifiHeight);
246      let startNS = TraceRow.range!.startNS;
247      let endNS = TraceRow.range!.endNS;
248      let totalNS = TraceRow.range!.totalNS;
249      if (data.ts === EnergyPowerStruct.hoverEnergyPowerStruct?.ts) {
250        let endPointX = ns2x((data.ts || 0) + 500000000, startNS, endNS, totalNS, row.frame);
251        let startPointX = ns2x((data.ts || 0) - 500000000, startNS, endNS, totalNS, row.frame);
252        let frameWidth = endPointX - startPointX <= 1 ? 1 : endPointX - startPointX;
253        req.context.globalAlpha = 1;
254        req!.context.lineWidth = 2;
255        req.context.fillStyle = '#333';
256        req!.context.strokeRect(startPointX, maxPointY, frameWidth, req.context.canvas.width - maxPointY);
257      }
258    }
259    req!.context.globalAlpha = 1.0;
260    req!.context.lineWidth = 1;
261  }
262
263  static drawHistogram(
264    req: { useCache: boolean; context: CanvasRenderingContext2D; type: string; appName: string },
265    data: EnergyPowerStruct,
266    height: number,
267    itemValue: number,
268    textItem: string,
269    rowFrame: Rect
270  ): number {
271    let endPointX = ns2x(
272      (data.ts || 0) + 500000000,
273      TraceRow.range!.startNS,
274      TraceRow.range!.endNS,
275      TraceRow.range!.totalNS,
276      rowFrame
277    );
278    let startPointX = ns2x(
279      (data.ts || 0) - 500000000,
280      TraceRow.range!.startNS,
281      TraceRow.range!.endNS,
282      TraceRow.range!.totalNS,
283      rowFrame
284    );
285    let frameWidth = endPointX - startPointX <= 1 ? 1 : endPointX - startPointX;
286    let histogramColor = this.getHistogramColor(textItem);
287    req!.context.fillStyle = histogramColor;
288    req!.context.strokeStyle = histogramColor;
289    let dataHeight: number = Math.floor(((itemValue || 0) * (this.rowHeight - 40)) / EnergyPowerStruct.maxPower);
290    if (itemValue !== 0 && dataHeight < 15) {
291      dataHeight = 15;
292    }
293    let drawStartY = 0;
294
295    if (height === -1) {
296      drawStartY = data.frame!.y + this.rowHeight - dataHeight + 4;
297      req!.context.fillRect(startPointX, drawStartY, frameWidth, dataHeight);
298      return drawStartY;
299    } else {
300      drawStartY = height - dataHeight;
301      req!.context.fillRect(startPointX, drawStartY, frameWidth, dataHeight);
302      if (textItem === 'WIFISCAN') {
303        return drawStartY;
304      }
305      return dataHeight;
306    }
307  }
308
309  static drawPolyline(
310    req: { useCache: boolean; context: CanvasRenderingContext2D; type: string; appName: string },
311    index: number,
312    data: EnergyPowerStruct,
313    rowFrame: Rect,
314    totalHeight: number
315  ): number {
316    let pointX = ns2x(data.ts || 0, TraceRow.range!.startNS, TraceRow.range!.endNS, TraceRow.range!.totalNS, rowFrame);
317    let maxHeight =
318      (data.cpu || 0) +
319      (data.location || 0) +
320      (data.gpu || 0) +
321      (data.display || 0) +
322      (data.camera || 0) +
323      (data.bluetooth || 0) +
324      (data.flashlight || 0) +
325      (data.audio || 0) +
326      (data.wifiscan || 0);
327    let drawHeight: number = Math.floor(((maxHeight || 0) * (this.rowHeight - 40)) / EnergyPowerStruct.maxPower);
328    let drawY = data.frame!.y + this.rowHeight - drawHeight + 5;
329    req!.context.fillStyle = '#ED6F21';
330    req!.context.strokeStyle = '#ED6F21';
331
332    if (index === 0) {
333      req.context.beginPath();
334      req.context.arc(pointX, totalHeight, 4, 0, 2 * Math.PI);
335      req.context.fill();
336      req.context.moveTo(pointX, totalHeight);
337    } else {
338      req.context.lineTo(pointX, totalHeight);
339      req.context.stroke();
340      req.context.beginPath();
341      req.context.arc(pointX, totalHeight, 4, 0, 2 * Math.PI);
342      req.context.fill();
343    }
344    return totalHeight;
345  }
346
347  static setPowerFrame(
348    powerNode: unknown,
349    padding: number,
350    startNS: number,
351    endNS: number,
352    totalNS: number,
353    frame: unknown
354  ): void {
355    let startPointX: number;
356    let endPointX: number;
357    //@ts-ignore
358    if ((powerNode.ts || 0) < startNS) {
359      startPointX = 0;
360    } else {
361      //@ts-ignore
362      startPointX = ns2x((powerNode.ts || 0) - 500000000, startNS, endNS, totalNS, frame);
363    }
364    //@ts-ignore
365    if (powerNode.ts + 500000000 > endNS) {
366      //@ts-ignore
367      endPointX = frame.width;
368    } else {
369      //@ts-ignore
370      endPointX = ns2x(powerNode.ts + 500000000, startNS, endNS, totalNS, frame);
371    }
372    let frameWidth = endPointX - startPointX <= 1 ? 1 : endPointX - startPointX;
373    //@ts-ignore
374    if (!powerNode.frame) {
375      //@ts-ignore
376      powerNode.frame = {};
377    }
378    //@ts-ignore
379    powerNode.frame.x = Math.floor(startPointX);
380    //@ts-ignore
381    powerNode.frame.y = frame.y + padding;
382    //@ts-ignore
383    powerNode.frame.width = Math.ceil(frameWidth);
384    //@ts-ignore
385    powerNode.frame.height = Math.floor(frame.height - padding * 2);
386  }
387
388  static getHistogramColor(textItem: string): string {
389    switch (textItem) {
390      case 'CPU':
391        return '#92D6CC';
392      case 'LOCATION':
393        return '#61CFBE';
394      case 'GPU':
395        return '#86C5E3';
396      case 'DISPLAY':
397        return '#46B1E3';
398      case 'CAMERA':
399        return '#C386F0';
400      case 'BLUETOOTH':
401        return '#8981F7';
402      case 'AUDIO':
403        return '#AC49F5';
404      case 'WIFISCAN':
405        return '#92C4BD';
406      default:
407        return '#564AF7';
408    }
409  }
410}
411