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 { BaseElement, element } from '../../base-ui/BaseElement';
17import { SpFlagHtml } from './SpFlag.html';
18const NUM = '000000';
19//vsync二级下拉选框对应的value和content
20const VSYNC_CONTENT = [
21  { value: 'H:VsyncGenerator', content: 'VsyncGeneratior' },
22  { value: 'H:rs_SendVsync', content: 'Vsync-rs' },
23  { value: 'H:rs_SendVsync', content: 'Vsync-app' }
24];
25//cat二级下拉选框对应的value和content
26const CAT_CONTENT = [
27  { value: 'business', content: 'Business first' },
28  { value: 'thread', content: 'Thread first' }
29];
30//hang二级下拉选框对应的value和content
31const HANG_CONTENT = [
32  { value: `33${NUM}`, content: 'Instant' },
33  { value: `100${NUM}`, content: 'Circumstantial' },
34  { value: `250${NUM}`, content: 'Micro' },
35  { value: `500${NUM}`, content: 'Severe' }
36];
37
38//整合默认值
39const CONFIG_STATE: unknown = {
40  'VSync': ['vsyncValue', 'VsyncGeneratior'],
41  'Start&Finish Trace Category': ['catValue', 'Business first'],
42  'Hangs Detection': ['hangValue', 'Instant'],
43};
44
45@element('sp-flags')
46export class SpFlags extends BaseElement {
47  private bodyEl: HTMLElement | undefined | null;
48  private xiaoLubanEl: Element | null | undefined;
49
50  initElements(): void {
51    let parentElement = this.parentNode as HTMLElement;
52    parentElement.style.overflow = 'hidden';
53    this.bodyEl = this.shadowRoot?.querySelector('.body');
54    this.initConfigList();
55  }
56
57  initHtml(): string {
58    return SpFlagHtml;
59  }
60
61  private createConfigDiv(): HTMLDivElement {
62    let configDiv = document.createElement('div');
63    configDiv.className = 'flag-widget';
64    return configDiv;
65  }
66  //控制按钮设置为'Disabled'时,我们需要给一个默认值
67  private createCustomDiv(config: FlagConfigItem, configDiv: HTMLDivElement): void {
68    let configHadDiv = document.createElement('div');
69    configHadDiv.className = 'flag-head-div';
70    let titleLabel = document.createElement('label');
71    titleLabel.textContent = config.title;
72    titleLabel.className = 'flag-title-label';
73    let configSelect = document.createElement('select');
74    configSelect.className = 'flag-select';
75    configSelect.setAttribute('title', config.title);
76    config.switchOptions.forEach((optionItem) => {
77      let configOption = document.createElement('option');
78      configOption.value = optionItem.option;
79      configOption.textContent = optionItem.option;
80      if (optionItem.selected) {
81        configOption.selected = true;
82      }
83      configSelect.appendChild(configOption);
84    });
85    // 页面刷新时,选项并未改变,小鲁班也应当展示
86    this.xiaoLubanEl = document.querySelector('sp-application')?.shadowRoot?.querySelector('#sp-bubbles')
87      ?.shadowRoot?.querySelector('#xiao-luban-help');
88    if (configSelect.title === 'AI' && configSelect.selectedOptions[0].value === 'Enabled') {
89      this.xiaoLubanEl?.setAttribute('enabled', '');
90    }
91    configSelect.addEventListener('change', () => {
92      this.flagSelectListener(configSelect);
93      if (configSelect.title === 'AI') {
94        let userIdInput: HTMLInputElement | null | undefined = this.shadowRoot?.querySelector('#user_id_input');
95        if (configSelect.selectedOptions[0].value === 'Enabled') {
96          if (userIdInput?.value === '') {
97            userIdInput.style.border = '1px solid red';
98          }
99          this.xiaoLubanEl?.setAttribute('enabled', '');
100        } else {
101          userIdInput!.style.border = '1px solid #ccc';
102          this.xiaoLubanEl?.removeAttribute('enabled');
103        }
104      }
105    });
106    let userIdInput: HTMLInputElement | null | undefined = this.shadowRoot?.querySelector('#user_id_input');
107    if (configSelect.title === 'AI' && configSelect.selectedOptions[0].value === 'Enabled' && userIdInput?.value === '') {
108      userIdInput.style.border = '1px solid red';
109    }
110    let description = document.createElement('div');
111    description.className = 'flag-des-div';
112    description.textContent = config.describeContent;
113    configHadDiv.appendChild(titleLabel);
114    configHadDiv.appendChild(configSelect);
115    configDiv.appendChild(configHadDiv);
116    configDiv.appendChild(description);
117  }
118  //监听flag-select的状态选择
119  private flagSelectListener(configSelect: HTMLSelectElement): void {
120    // @ts-ignore
121    let title = configSelect.getAttribute('title');
122    //@ts-ignore
123    let listSelect = this.shadowRoot?.querySelector(`#${CONFIG_STATE[title]?.[0]}`);
124    // @ts-ignore
125    FlagsConfig.updateFlagsConfig(title!, configSelect.selectedOptions[0].value);
126    //@ts-ignore
127    if (listSelect) {
128      // @ts-ignore
129      if (configSelect.selectedOptions[0].value === 'Enabled') {
130        listSelect?.removeAttribute('disabled');
131      } else {
132        listSelect?.childNodes.forEach((child: ChildNode) => {
133          let selectEl = child as HTMLOptionElement;
134          //@ts-ignore
135          if (child.textContent === CONFIG_STATE[title]?.[1]) {
136            selectEl.selected = true;
137            //@ts-ignore
138            FlagsConfig.updateFlagsConfig(CONFIG_STATE[title]?.[0], selectEl.value);
139          } else {
140            selectEl.selected = false;
141          }
142        });
143        listSelect?.setAttribute('disabled', 'disabled');
144      }
145    }
146  }
147
148  //初始化Flag对应的内容
149  private initConfigList(): void {
150    let allConfig = FlagsConfig.getAllFlagConfig();
151    allConfig.forEach((config) => {
152      let configDiv = this.createConfigDiv();
153      this.createCustomDiv(config, configDiv);
154      if (config.title === 'AnimationAnalysis') {
155        let configFooterDiv = document.createElement('div');
156        configFooterDiv.className = 'config_footer';
157        let deviceWidthLabelEl = document.createElement('label');
158        deviceWidthLabelEl.className = 'device_label';
159        deviceWidthLabelEl.textContent = 'PhysicalWidth :';
160        let deviceWidthEl = document.createElement('input');
161        deviceWidthEl.value = <string>config.addInfo!.physicalWidth;
162        deviceWidthEl.addEventListener('keyup', () => {
163          deviceWidthEl.value = deviceWidthEl.value.replace(/\D/g, '');
164        });
165        deviceWidthEl.addEventListener('blur', () => {
166          if (deviceWidthEl.value !== '') {
167            FlagsConfig.updateFlagsConfig('physicalWidth', Number(deviceWidthEl.value));
168          }
169        });
170        deviceWidthEl.className = 'device_input';
171        let deviceHeightLabelEl = document.createElement('label');
172        deviceHeightLabelEl.textContent = 'PhysicalHeight :';
173        deviceHeightLabelEl.className = 'device_label';
174        let deviceHeightEl = document.createElement('input');
175        deviceHeightEl.className = 'device_input';
176        deviceHeightEl.value = <string>config.addInfo!.physicalHeight;
177        deviceHeightEl.addEventListener('keyup', () => {
178          deviceHeightEl.value = deviceHeightEl.value.replace(/\D/g, '');
179        });
180        deviceHeightEl.addEventListener('blur', () => {
181          if (deviceWidthEl.value !== '') {
182            FlagsConfig.updateFlagsConfig('physicalHeight', Number(deviceHeightEl.value));
183          }
184        });
185        configFooterDiv.appendChild(deviceWidthLabelEl);
186        configFooterDiv.appendChild(deviceWidthEl);
187        configFooterDiv.appendChild(deviceHeightLabelEl);
188        configFooterDiv.appendChild(deviceHeightEl);
189        configDiv.appendChild(configFooterDiv);
190      }
191      //@ts-ignore
192      let configKey = CONFIG_STATE[config.title]?.[0];
193      if (config.title === 'VSync') {//初始化Vsync
194        let configFooterDiv = this.createPersonOption(VSYNC_CONTENT, configKey, config);
195        configDiv.appendChild(configFooterDiv);
196      }
197
198      if (config.title === 'Start&Finish Trace Category') {//初始化Start&Finish Trace Category
199        let configFooterDiv = this.createPersonOption(CAT_CONTENT, configKey, config);
200        configDiv.appendChild(configFooterDiv);
201      }
202
203      if (config.title === 'Hangs Detection') {//初始化Hangs Detection
204        let configFooterDiv = this.createPersonOption(HANG_CONTENT, configKey, config);
205        configDiv.appendChild(configFooterDiv);
206      }
207      if (config.title === 'AI') {
208        let configFooterDiv = document.createElement('div');
209        configFooterDiv.className = 'config_footer';
210        let userIdLabelEl = document.createElement('label');
211        userIdLabelEl.className = 'device_label';
212        userIdLabelEl.textContent = 'User Id: ';
213        let userIdInputEl = document.createElement('input');
214        userIdInputEl.value = <string>config.addInfo!.userId;
215        userIdInputEl.addEventListener('blur', () => {
216          if (userIdInputEl.value !== '') {
217            userIdInputEl.style.border = '1px solid #ccc';
218            FlagsConfig.updateFlagsConfig('userId', userIdInputEl.value);
219          } else {
220            userIdInputEl.style.border = '1px solid red';
221          }
222        });
223        userIdInputEl.className = 'device_input';
224        userIdInputEl.id = 'user_id_input';
225        configFooterDiv.appendChild(userIdLabelEl);
226        configFooterDiv.appendChild(userIdInputEl);
227        configDiv.appendChild(configFooterDiv);
228      }
229      this.bodyEl!.appendChild(configDiv);
230    });
231  }
232
233  private createPersonOption(list: unknown, key: string, config: unknown): HTMLDivElement {
234    let configFooterDiv = document.createElement('div');
235    configFooterDiv.className = 'config_footer';
236    let lableEl = document.createElement('lable');
237    lableEl.className = 'list_lable';
238    let typeEl = document.createElement('select');
239    typeEl.setAttribute('id', key);
240    typeEl.className = 'flag-select';
241    //根据给出的list遍历添加option下来选框
242    //@ts-ignore
243    for (const settings of list) {
244      let option = document.createElement('option');
245      option.value = settings.value;
246      option.textContent = settings.content;
247      //初始化二级按钮状态
248      //@ts-ignore
249      if (option.value === config.addInfo?.[key]) {
250        option.selected = true;
251        FlagsConfig.updateFlagsConfig(key, option.value);
252      }
253      typeEl.appendChild(option);
254    }
255    typeEl.addEventListener('change', function () {
256      let selectValue = this.selectedOptions[0].value;
257      FlagsConfig.updateFlagsConfig(key, selectValue);
258    });
259    let flagsItem = window.localStorage.getItem(FlagsConfig.FLAGS_CONFIG_KEY);
260    let flagsItemJson = JSON.parse(flagsItem!);
261    //@ts-ignore
262    let vsync = flagsItemJson[config.title];
263    if (vsync === 'Enabled') {
264      typeEl.removeAttribute('disabled');
265    } else {
266      typeEl.setAttribute('disabled', 'disabled');
267      //@ts-ignore
268      FlagsConfig.updateFlagsConfig(key, config.addInfo?.[key]);
269    }
270    configFooterDiv.appendChild(lableEl);
271    configFooterDiv.appendChild(typeEl);
272    return configFooterDiv;
273  }
274}
275
276export type Params = {
277  [key: string]: unknown;
278};
279
280export class FlagsConfig {
281  static FLAGS_CONFIG_KEY = 'FlagsConfig';
282  static DEFAULT_CONFIG: Array<FlagConfigItem> = [
283    {
284      title: 'TaskPool',
285      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
286      describeContent: 'Analyze TaskPool templates',
287    },
288    {
289      title: 'AnimationAnalysis',
290      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
291      describeContent: 'Analyze Animation effect templates',
292      addInfo: { physicalWidth: 0, physicalHeight: 0 },
293    },
294    {
295      title: 'AppStartup',
296      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
297      describeContent: 'App Startup templates',
298    },
299    {
300      title: 'SchedulingAnalysis',
301      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
302      describeContent: 'Scheduling analysis templates',
303    },
304    {
305      title: 'BinderRunnable',
306      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
307      describeContent: 'support Cpu State Binder-Runnable',
308    },
309    {
310      title: 'FfrtConvert',
311      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
312      describeContent: 'Ffrt Convert templates',
313    },
314    {
315      title: 'HMKernel',
316      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
317      describeContent: '',
318    },
319    {
320      title: 'VSync',
321      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
322      describeContent: 'VSync Signal drawing',
323      addInfo: { vsyncValue: VSYNC_CONTENT[0].value },
324    },
325    {
326      title: 'Hangs Detection',
327      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
328      describeContent: 'hangs type:Instant(33ms~100ms),Circumstantial(100ms~250ms),Micro(250ms~500ms),Severe(>=500ms)',
329      addInfo: { hangValue: HANG_CONTENT[0].value },
330    },
331    {
332      title: 'LTPO',
333      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
334      describeContent: 'Lost Frame and HitchTime templates',
335    },
336    {
337      title: 'Start&Finish Trace Category',
338      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
339      describeContent: 'Asynchronous trace aggregation',
340      addInfo: { catValue: CAT_CONTENT[0].value },
341    },
342    {
343      title: 'UserPluginsRow',
344      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
345      describeContent: 'User Upload Plugin To Draw',
346    },
347    {
348      title: 'CPU by Irq',
349      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
350      describeContent: 'The real CPU after being split by irq and softirq',
351    },
352    {
353      title: 'RawTraceCutStartTs',
354      switchOptions: [{ option: 'Enabled', selected: true }, { option: 'Disabled' }],
355      describeContent: 'Raw Trace Cut By StartTs, StartTs = Max(Cpu1 StartTs, Cpu2 StartTs, ..., CpuN StartTs)',
356    },
357    {
358      title: 'AI',
359      switchOptions: [{ option: 'Enabled' }, { option: 'Disabled', selected: true }],
360      describeContent: 'Start AI',
361      addInfo: { userId: '' },
362    }
363  ];
364
365  static getAllFlagConfig(): Array<FlagConfigItem> {
366    let flagsConfigStr = window.localStorage.getItem(FlagsConfig.FLAGS_CONFIG_KEY);
367    if (flagsConfigStr === null) {
368      let flagConfigObj: Params = {};
369      FlagsConfig.DEFAULT_CONFIG.forEach((config) => {
370        let selectedOption = config.switchOptions.filter((option) => {
371          return option.selected;
372        });
373        let value = config.switchOptions[0].option;
374        if (selectedOption[0] !== undefined) {
375          value = selectedOption[0].option;
376        }
377        flagConfigObj[config.title] = value;
378        if (config.addInfo) {
379          for (const [key, value] of Object.entries(config.addInfo)) {
380            flagConfigObj[key] = value;
381          }
382        }
383      });
384      window.localStorage.setItem(FlagsConfig.FLAGS_CONFIG_KEY, JSON.stringify(flagConfigObj));
385      return FlagsConfig.DEFAULT_CONFIG;
386    } else {
387      let flagsConfig = JSON.parse(flagsConfigStr);
388      FlagsConfig.DEFAULT_CONFIG.forEach((config) => {
389        let cfg = flagsConfig[config.title];
390        if (cfg) {
391          config.switchOptions.forEach((option) => {
392            if (option.option === cfg) {
393              option.selected = true;
394            } else {
395              option.selected = false;
396            }
397          });
398        }
399        if (config.addInfo) {
400          for (const [key, value] of Object.entries(config.addInfo)) {
401            let cfg = flagsConfig[key];
402            if (cfg) {
403              config.addInfo[key] = cfg;
404            }
405          }
406        }
407      });
408    }
409    return FlagsConfig.DEFAULT_CONFIG;
410  }
411
412  static getSpTraceStreamParseConfig(): string {
413    let parseConfig = {};
414    FlagsConfig.getAllFlagConfig().forEach((configItem) => {
415      let selectedOption = configItem.switchOptions.filter((option) => {
416        return option.selected;
417      });
418      // @ts-ignore
419      parseConfig[configItem.title] = selectedOption[0].option === 'Enabled' ? 1 : 0;
420    });
421    return JSON.stringify({ config: parseConfig });
422  }
423
424  static getFlagsConfig(flagName: string): Params | undefined {
425    let flagConfigObj: Params = {};
426    let configItem = FlagsConfig.getAllFlagConfig().find((config) => {
427      return config.title === flagName;
428    });
429    if (configItem) {
430      let selectedOption = configItem.switchOptions.filter((option) => {
431        return option.selected;
432      });
433      let value = configItem.switchOptions[0].option;
434      if (selectedOption[0] !== undefined) {
435        value = selectedOption[0].option;
436      }
437      flagConfigObj[configItem.title] = value;
438      if (configItem.addInfo) {
439        for (const [key, value] of Object.entries(configItem.addInfo)) {
440          flagConfigObj[key] = value;
441        }
442      }
443      return flagConfigObj;
444    } else {
445      return configItem;
446    }
447  }
448
449  static getFlagsConfigEnableStatus(flagName: string): boolean {
450    let config = FlagsConfig.getFlagsConfig(flagName);
451    let enable: boolean = false;
452    if (config && config[flagName]) {
453      enable = config[flagName] === 'Enabled';
454    }
455    return enable;
456  }
457  //获取Cat的二级下拉选框所选的内容
458  static getSecondarySelectValue(value: string): string {
459    let list = window.localStorage.getItem(FlagsConfig.FLAGS_CONFIG_KEY);
460    let listJson = JSON.parse(list!);
461    let catSelectValue = listJson[value];
462    return catSelectValue;
463  }
464
465  static updateFlagsConfig(key: string, value: unknown): void {
466    let flagsConfigStr = window.localStorage.getItem(FlagsConfig.FLAGS_CONFIG_KEY);
467    let flagConfigObj: Params = {};
468    if (flagsConfigStr !== null) {
469      flagConfigObj = JSON.parse(flagsConfigStr);
470    }
471    flagConfigObj[key] = value;
472    window.localStorage.setItem(FlagsConfig.FLAGS_CONFIG_KEY, JSON.stringify(flagConfigObj));
473  }
474}
475
476export interface FlagConfigItem {
477  title: string;
478  switchOptions: OptionItem[];
479  describeContent: string;
480  addInfo?: Params;
481}
482
483export interface OptionItem {
484  option: string;
485  selected?: boolean;
486}
487