• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {assertDefined} from 'common/assert_utils';
18import {FunctionUtils} from 'common/function_utils';
19import {PersistentStoreProxy} from 'common/store/persistent_store_proxy';
20import {Store} from 'common/store/store';
21import {TimestampConverter} from 'common/time/timestamp_converter';
22import {
23  InitializeTraceSearchRequest,
24  TracePositionUpdate,
25  TraceRemoveRequest,
26  TraceSearchRequest,
27  WinscopeEvent,
28  WinscopeEventType,
29} from 'messaging/winscope_event';
30import {EmitEvent} from 'messaging/winscope_event_emitter';
31import {Trace} from 'trace/trace';
32import {Traces} from 'trace/traces';
33import {TraceType} from 'trace/trace_type';
34import {QueryResult} from 'trace_processor/query_result';
35import {
36  AddQueryClickDetail,
37  ClearQueryClickDetail,
38  DeleteSavedQueryClickDetail,
39  SaveQueryClickDetail,
40  SearchQueryClickDetail,
41  ViewerEvents,
42} from 'viewers/common/viewer_events';
43import {SearchResultPresenter} from './search_result_presenter';
44import {CurrentSearch, ListedSearch, SearchResult, UiData} from './ui_data';
45
46interface ActiveSearch {
47  search: CurrentSearch;
48  trace?: Trace<QueryResult>;
49  resultPresenter?: SearchResultPresenter;
50}
51
52export class Presenter {
53  private emitWinscopeEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
54  private uiData = UiData.createEmpty();
55  private activeSearchUid = 0;
56  private activeSearches: ActiveSearch[] = [];
57  private savedSearches = PersistentStoreProxy.new<{searches: ListedSearch[]}>(
58    'savedSearches',
59    {searches: []},
60    this.storage,
61  );
62  private viewerElement: HTMLElement | undefined;
63  private runningSearch: CurrentSearch | undefined;
64
65  constructor(
66    private traces: Traces,
67    private storage: Store,
68    private readonly notifyViewCallback: (uiData: UiData) => void,
69    private readonly timestampConverter: TimestampConverter,
70  ) {
71    this.uiData.savedSearches = Array.from(this.savedSearches.searches);
72    this.addSearch();
73  }
74
75  setEmitEvent(callback: EmitEvent) {
76    this.emitWinscopeEvent = callback;
77  }
78
79  addEventListeners(htmlElement: HTMLElement) {
80    this.viewerElement = htmlElement;
81    htmlElement.addEventListener(
82      ViewerEvents.GlobalSearchSectionClick,
83      async (event) => {
84        this.onGlobalSearchSectionClick();
85      },
86    );
87    htmlElement.addEventListener(
88      ViewerEvents.SearchQueryClick,
89      async (event) => {
90        const detail: SearchQueryClickDetail = (event as CustomEvent).detail;
91        this.onSearchQueryClick(detail.query, detail.uid);
92      },
93    );
94    htmlElement.addEventListener(ViewerEvents.SaveQueryClick, async (event) => {
95      const detail: SaveQueryClickDetail = (event as CustomEvent).detail;
96      this.onSaveQueryClick(detail.query, detail.name);
97    });
98    htmlElement.addEventListener(
99      ViewerEvents.DeleteSavedQueryClick,
100      async (event) => {
101        const detail: DeleteSavedQueryClickDetail = (event as CustomEvent)
102          .detail;
103        this.onDeleteSavedQueryClick(detail.search);
104      },
105    );
106    htmlElement.addEventListener(ViewerEvents.AddQueryClick, async (event) => {
107      const detail: AddQueryClickDetail | undefined = (event as CustomEvent)
108        .detail;
109      this.addSearch(detail?.query);
110    });
111    htmlElement.addEventListener(
112      ViewerEvents.ClearQueryClick,
113      async (event) => {
114        const detail: ClearQueryClickDetail = (event as CustomEvent).detail;
115        this.onClearQueryClick(detail.uid);
116      },
117    );
118  }
119
120  async onAppEvent(event: WinscopeEvent) {
121    await event.visit(
122      WinscopeEventType.TRACE_SEARCH_INITIALIZED,
123      async (event) => {
124        this.uiData.searchViews = event.views;
125        this.uiData.initialized = true;
126        this.copyUiDataAndNotifyView();
127      },
128    );
129    await event.visit(WinscopeEventType.TRACE_ADD_REQUEST, async (event) => {
130      if (event.trace.type === TraceType.SEARCH) {
131        this.showQueryResult(event.trace as Trace<QueryResult>);
132      }
133    });
134    await event.visit(WinscopeEventType.TRACE_SEARCH_FAILED, async (event) => {
135      this.onTraceSearchFailed();
136    });
137    for (const activeSearch of this.activeSearches.values()) {
138      await activeSearch.resultPresenter?.onAppEvent(event);
139    }
140  }
141
142  async onGlobalSearchSectionClick() {
143    if (!this.uiData.initialized) {
144      this.emitWinscopeEvent(new InitializeTraceSearchRequest());
145    }
146  }
147
148  async onSearchQueryClick(query: string, uid: number) {
149    const activeSearch = assertDefined(
150      this.activeSearches.find((a) => a.search.uid === uid),
151    );
152    this.resetActiveSearch(activeSearch, query);
153    this.runningSearch = activeSearch.search;
154    this.emitWinscopeEvent(new TraceSearchRequest(query));
155  }
156
157  addSearch(query?: string) {
158    this.activeSearchUid++;
159    this.activeSearches.push({
160      search: new CurrentSearch(this.activeSearchUid, query),
161    });
162    this.updateCurrentSearches();
163  }
164
165  async onClearQueryClick(uid: number) {
166    const activeSearchIndex = this.activeSearches.findIndex(
167      (a) => a.search.uid === uid,
168    );
169    if (activeSearchIndex === -1) {
170      return;
171    }
172    const activeSearch = this.activeSearches.splice(activeSearchIndex, 1)[0];
173    this.resetActiveSearch(activeSearch);
174    this.updateCurrentSearches();
175  }
176
177  onSaveQueryClick(query: string, name: string) {
178    this.uiData.savedSearches.unshift(new ListedSearch(query, name));
179    this.savedSearches.searches = this.uiData.savedSearches;
180    this.copyUiDataAndNotifyView();
181  }
182
183  onDeleteSavedQueryClick(savedSearch: ListedSearch) {
184    this.uiData.savedSearches = this.uiData.savedSearches.filter(
185      (s) => s !== savedSearch,
186    );
187    this.savedSearches.searches = this.uiData.savedSearches;
188    this.copyUiDataAndNotifyView();
189  }
190
191  private onTraceSearchFailed() {
192    this.runningSearch = undefined;
193    this.uiData.lastTraceFailed = true;
194    this.copyUiDataAndNotifyView();
195    this.uiData.lastTraceFailed = false;
196  }
197
198  private async showQueryResult(newTrace: Trace<QueryResult>) {
199    const [traceQuery] = newTrace.getDescriptors();
200    if (this.uiData.recentSearches.length >= 10) {
201      this.uiData.recentSearches.pop();
202    }
203    this.uiData.recentSearches.unshift(new ListedSearch(traceQuery));
204
205    const activeSearch = assertDefined(
206      this.activeSearches.find((a) => a.search.uid === this.runningSearch?.uid),
207    );
208    this.resetActiveSearch(activeSearch, traceQuery);
209    this.initializeResultPresenter(activeSearch, newTrace);
210    this.runningSearch = undefined;
211    this.copyUiDataAndNotifyView();
212  }
213
214  private updateCurrentSearches() {
215    this.uiData.currentSearches = this.activeSearches.map((a) => a.search);
216    this.copyUiDataAndNotifyView();
217  }
218
219  private resetActiveSearch(activeSearch: ActiveSearch, newQuery?: string) {
220    activeSearch.search.query = newQuery;
221    activeSearch.search.result = undefined;
222    if (activeSearch.resultPresenter) {
223      activeSearch.resultPresenter.onDestroy();
224      activeSearch.resultPresenter = undefined;
225    }
226    if (activeSearch.trace) {
227      this.emitWinscopeEvent(new TraceRemoveRequest(activeSearch.trace));
228      activeSearch.trace = undefined;
229    }
230  }
231
232  private async initializeResultPresenter(
233    activeSearch: ActiveSearch,
234    newTrace: Trace<QueryResult>,
235  ) {
236    activeSearch.trace = newTrace;
237    const firstEntry =
238      newTrace.lengthEntries > 0 ? newTrace.getEntry(0) : undefined;
239
240    const presenter = new SearchResultPresenter(
241      newTrace,
242      (result: SearchResult) => {
243        if (activeSearch.search.result) {
244          activeSearch.search.result.scrollToIndex = result.scrollToIndex;
245          activeSearch.search.result.selectedIndex = result.selectedIndex;
246        } else {
247          activeSearch.search.result = result;
248        }
249        this.updateCurrentSearches();
250      },
251      (valueNs: bigint) =>
252        this.timestampConverter.makeTimestampFromBootTimeNs(valueNs),
253      firstEntry ? await firstEntry.getValue() : undefined,
254    );
255    presenter.addEventListeners(assertDefined(this.viewerElement));
256    presenter.setEmitEvent(async (event) => this.emitWinscopeEvent(event));
257    activeSearch.resultPresenter = presenter;
258
259    if (firstEntry) {
260      await this.emitWinscopeEvent(
261        TracePositionUpdate.fromTraceEntry(firstEntry),
262      );
263    }
264  }
265
266  private copyUiDataAndNotifyView() {
267    // Create a shallow copy of the data, otherwise the Angular OnPush change detection strategy
268    // won't detect the new input
269    const copy = Object.assign({}, this.uiData);
270    this.notifyViewCallback(copy);
271  }
272}
273