• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2022 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 {ArrayUtils} from 'common/array_utils';
18import {assertDefined} from 'common/assert_utils';
19import {FunctionUtils} from 'common/function_utils';
20import {
21  TracePositionUpdate,
22  WinscopeEvent,
23  WinscopeEventType,
24} from 'messaging/winscope_event';
25import {
26  EmitEvent,
27  WinscopeEventEmitter,
28} from 'messaging/winscope_event_emitter';
29import {AbsoluteEntryIndex, Trace, TraceEntry} from 'trace/trace';
30import {TraceEntryFinder} from 'trace/trace_entry_finder';
31import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
32import {UiData, UiDataMessage} from './ui_data';
33
34export class Presenter implements WinscopeEventEmitter {
35  private readonly trace: Trace<PropertyTreeNode>;
36  private readonly notifyUiDataCallback: (data: UiData) => void;
37  private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
38  private entry?: TraceEntry<PropertyTreeNode>;
39  private uiData = UiData.EMPTY;
40  private originalIndicesOfFilteredOutputMessages: number[] = [];
41
42  private isInitialized = false;
43  private allUiDataMessages: UiDataMessage[] = [];
44  private allTags: string[] = [];
45  private allSourceFiles: string[] = [];
46  private allLogLevels: string[] = [];
47
48  private tagsFilter: string[] = [];
49  private filesFilter: string[] = [];
50  private levelsFilter: string[] = [];
51  private searchString = '';
52
53  constructor(
54    trace: Trace<PropertyTreeNode>,
55    notifyUiDataCallback: (data: UiData) => void,
56  ) {
57    this.trace = trace;
58    this.notifyUiDataCallback = notifyUiDataCallback;
59    this.notifyUiDataCallback(this.uiData);
60  }
61
62  setEmitEvent(callback: EmitEvent) {
63    this.emitAppEvent = callback;
64  }
65
66  async onAppEvent(event: WinscopeEvent) {
67    await event.visit(
68      WinscopeEventType.TRACE_POSITION_UPDATE,
69      async (event) => {
70        await this.initializeIfNeeded();
71        this.entry = TraceEntryFinder.findCorrespondingEntry(
72          this.trace,
73          event.position,
74        );
75        this.computeUiDataCurrentMessageIndex();
76        this.notifyUiDataCallback(this.uiData);
77      },
78    );
79  }
80
81  onLogLevelsFilterChanged(levels: string[]) {
82    this.levelsFilter = levels;
83    this.computeUiData();
84    this.computeUiDataCurrentMessageIndex();
85    this.notifyUiDataCallback(this.uiData);
86  }
87
88  onTagsFilterChanged(tags: string[]) {
89    this.tagsFilter = tags;
90    this.computeUiData();
91    this.computeUiDataCurrentMessageIndex();
92    this.notifyUiDataCallback(this.uiData);
93  }
94
95  onSourceFilesFilterChanged(files: string[]) {
96    this.filesFilter = files;
97    this.computeUiData();
98    this.computeUiDataCurrentMessageIndex();
99    this.notifyUiDataCallback(this.uiData);
100  }
101
102  onSearchStringFilterChanged(searchString: string) {
103    this.searchString = searchString;
104    this.computeUiData();
105    this.computeUiDataCurrentMessageIndex();
106    this.notifyUiDataCallback(this.uiData);
107  }
108
109  onMessageClicked(index: number) {
110    if (this.uiData.selectedMessageIndex === index) {
111      this.uiData.selectedMessageIndex = undefined;
112    } else {
113      this.uiData.selectedMessageIndex = index;
114    }
115    this.notifyUiDataCallback(this.uiData);
116  }
117
118  async onLogTimestampClicked(traceIndex: AbsoluteEntryIndex) {
119    await this.emitAppEvent(
120      TracePositionUpdate.fromTraceEntry(this.trace.getEntry(traceIndex), true),
121    );
122  }
123
124  private async initializeIfNeeded() {
125    if (this.isInitialized) {
126      return;
127    }
128    this.allUiDataMessages = await this.makeAllUiDataMessages();
129
130    this.allLogLevels = this.getUniqueMessageValues(
131      this.allUiDataMessages,
132      (message: UiDataMessage) => message.level,
133    );
134    this.allTags = this.getUniqueMessageValues(
135      this.allUiDataMessages,
136      (message: UiDataMessage) => message.tag,
137    );
138    this.allSourceFiles = this.getUniqueMessageValues(
139      this.allUiDataMessages,
140      (message: UiDataMessage) => message.at,
141    );
142
143    this.computeUiData();
144
145    this.isInitialized = true;
146  }
147
148  private async makeAllUiDataMessages(): Promise<UiDataMessage[]> {
149    const messages: PropertyTreeNode[] = [];
150
151    for (
152      let traceIndex = 0;
153      traceIndex < this.trace.lengthEntries;
154      ++traceIndex
155    ) {
156      const entry = assertDefined(this.trace.getEntry(traceIndex));
157      const message = await entry.getValue();
158      messages.push(message);
159    }
160
161    return messages.map((messageNode, index) => {
162      return {
163        traceIndex: index,
164        text: assertDefined(
165          messageNode.getChildByName('text'),
166        ).formattedValue(),
167        time: assertDefined(messageNode.getChildByName('timestamp')),
168        tag: assertDefined(messageNode.getChildByName('tag')).formattedValue(),
169        level: assertDefined(
170          messageNode.getChildByName('level'),
171        ).formattedValue(),
172        at: assertDefined(messageNode.getChildByName('at')).formattedValue(),
173      };
174    });
175  }
176
177  private computeUiData() {
178    let filteredMessages = this.allUiDataMessages;
179
180    if (this.levelsFilter.length > 0) {
181      filteredMessages = filteredMessages.filter((value) =>
182        this.levelsFilter.includes(value.level),
183      );
184    }
185
186    if (this.tagsFilter.length > 0) {
187      filteredMessages = filteredMessages.filter((value) =>
188        this.tagsFilter.includes(value.tag),
189      );
190    }
191
192    if (this.filesFilter.length > 0) {
193      filteredMessages = filteredMessages.filter((value) =>
194        this.filesFilter.includes(value.at),
195      );
196    }
197
198    filteredMessages = filteredMessages.filter((value) =>
199      value.text.includes(this.searchString),
200    );
201
202    this.originalIndicesOfFilteredOutputMessages = filteredMessages.map(
203      (message) => message.traceIndex,
204    );
205
206    this.uiData = new UiData(
207      this.allLogLevels,
208      this.allTags,
209      this.allSourceFiles,
210      filteredMessages,
211      undefined,
212      undefined,
213    );
214  }
215
216  private computeUiDataCurrentMessageIndex() {
217    if (!this.entry) {
218      this.uiData.currentMessageIndex = undefined;
219      return;
220    }
221
222    if (this.originalIndicesOfFilteredOutputMessages.length === 0) {
223      this.uiData.currentMessageIndex = undefined;
224      return;
225    }
226
227    this.uiData.currentMessageIndex =
228      ArrayUtils.binarySearchFirstGreaterOrEqual(
229        this.originalIndicesOfFilteredOutputMessages,
230        this.entry.getIndex(),
231      ) ?? this.originalIndicesOfFilteredOutputMessages.length - 1;
232  }
233
234  private getUniqueMessageValues(
235    allMessages: UiDataMessage[],
236    getValue: (message: UiDataMessage) => string,
237  ): string[] {
238    const uniqueValues = new Set<string>();
239    allMessages.forEach((message) => {
240      uniqueValues.add(getValue(message));
241    });
242    const result = [...uniqueValues];
243    result.sort();
244    return result;
245  }
246}
247