1/* 2 * Copyright (C) 2023 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 {Store} from 'common/store'; 19import {CustomQueryType} from 'trace/custom_query'; 20import {Trace} from 'trace/trace'; 21import {Traces} from 'trace/traces'; 22import {TraceType} from 'trace/trace_type'; 23import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node'; 24import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 25import { 26 AbstractLogViewerPresenter, 27 NotifyLogViewCallbackType, 28} from 'viewers/common/abstract_log_viewer_presenter'; 29import {LogSelectFilter} from 'viewers/common/log_filters'; 30import {LogPresenter} from 'viewers/common/log_presenter'; 31import {PropertiesPresenter} from 'viewers/common/properties_presenter'; 32import {TextFilter} from 'viewers/common/text_filter'; 33import {ColumnSpec, LogField, LogHeader} from 'viewers/common/ui_data_log'; 34import {UpdateTransitionChangesNames} from './operations/update_transition_changes_names'; 35import {TransitionsEntry, TransitionStatus, UiData} from './ui_data'; 36 37export class Presenter extends AbstractLogViewerPresenter< 38 UiData, 39 PropertyTreeNode 40> { 41 private static readonly COLUMNS = { 42 id: {name: 'Id', cssClass: 'transition-id right-align'}, 43 type: {name: 'Type', cssClass: 'transition-type'}, 44 sendTime: {name: 'Send Time', cssClass: 'send-time time'}, 45 dispatchTime: {name: 'Dispatch Time', cssClass: 'dispatch-time time'}, 46 duration: {name: 'Duration', cssClass: 'duration right-align'}, 47 handler: {name: 'Handler', cssClass: 'handler'}, 48 participants: {name: 'Participants', cssClass: 'participants'}, 49 flags: {name: 'Flags', cssClass: 'flags'}, 50 status: {name: 'Status', cssClass: 'status right-align'}, 51 }; 52 private transitionTrace: Trace<PropertyTreeNode>; 53 private surfaceFlingerTrace: Trace<HierarchyTreeNode> | undefined; 54 private windowManagerTrace: Trace<HierarchyTreeNode> | undefined; 55 private layerIdToName = new Map<number, string>(); 56 private windowTokenToTitle = new Map<string, string>(); 57 private updateTransitionChangesNames = new UpdateTransitionChangesNames( 58 this.layerIdToName, 59 this.windowTokenToTitle, 60 ); 61 private uniqueFieldValues = new Map<ColumnSpec, Set<string>>(); 62 63 protected override keepCalculated = false; 64 protected override logPresenter = new LogPresenter<TransitionsEntry>(false); 65 protected override propertiesPresenter = new PropertiesPresenter( 66 {}, 67 new TextFilter(), 68 [], 69 ); 70 71 constructor( 72 trace: Trace<PropertyTreeNode>, 73 traces: Traces, 74 readonly storage: Store, 75 notifyViewCallback: NotifyLogViewCallbackType<UiData>, 76 ) { 77 super(trace, notifyViewCallback, UiData.createEmpty()); 78 this.transitionTrace = trace; 79 this.surfaceFlingerTrace = traces.getTrace(TraceType.SURFACE_FLINGER); 80 this.windowManagerTrace = traces.getTrace(TraceType.WINDOW_MANAGER); 81 } 82 83 protected override async initializeTraceSpecificData() { 84 if (this.surfaceFlingerTrace) { 85 const layersIdAndName = await this.surfaceFlingerTrace.customQuery( 86 CustomQueryType.SF_LAYERS_ID_AND_NAME, 87 ); 88 layersIdAndName.forEach((value) => { 89 this.layerIdToName.set(value.id, value.name); 90 }); 91 } 92 93 if (this.windowManagerTrace) { 94 const windowsTokenAndTitle = await this.windowManagerTrace.customQuery( 95 CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE, 96 ); 97 windowsTokenAndTitle.forEach((value) => { 98 this.windowTokenToTitle.set(value.token, value.title); 99 }); 100 } 101 } 102 103 protected override makeHeaders(): LogHeader[] { 104 return [ 105 new LogHeader(Presenter.COLUMNS.id), 106 new LogHeader( 107 Presenter.COLUMNS.type, 108 new LogSelectFilter([], false, '175'), 109 ), 110 new LogHeader(Presenter.COLUMNS.sendTime), 111 new LogHeader(Presenter.COLUMNS.dispatchTime), 112 new LogHeader(Presenter.COLUMNS.duration), 113 new LogHeader( 114 Presenter.COLUMNS.handler, 115 new LogSelectFilter([], false, '250'), 116 ), 117 new LogHeader( 118 Presenter.COLUMNS.participants, 119 new LogSelectFilter([], true, '250', '100%'), 120 ), 121 new LogHeader( 122 Presenter.COLUMNS.flags, 123 new LogSelectFilter([], true, '250', '100%'), 124 ), 125 new LogHeader(Presenter.COLUMNS.status, new LogSelectFilter([])), 126 ]; 127 } 128 129 protected override async makeUiDataEntries( 130 headers: LogHeader[], 131 ): Promise<TransitionsEntry[]> { 132 // TODO(b/339191691): Ideally we should refactor the parsers to 133 // keep a map of time -> rowId, instead of relying on table order 134 headers.forEach((header) => { 135 if (!header.filter) return; 136 this.uniqueFieldValues.set(header.spec, new Set()); 137 }); 138 const transitions = await this.makeTransitions(headers); 139 this.sortTransitions(transitions); 140 return transitions; 141 } 142 143 protected override updateFiltersInHeaders(headers: LogHeader[]) { 144 headers.forEach((header) => { 145 if (!(header.filter instanceof LogSelectFilter)) return; 146 header.filter.options = this.getUniqueUiDataEntryValues(header.spec); 147 }); 148 } 149 150 private getUniqueUiDataEntryValues(spec: ColumnSpec): string[] { 151 const result = [...assertDefined(this.uniqueFieldValues.get(spec))]; 152 153 result.sort((a, b) => { 154 const aIsNumber = !isNaN(Number(a)); 155 const bIsNumber = !isNaN(Number(b)); 156 157 if (aIsNumber && bIsNumber) { 158 return Number(a) - Number(b); 159 } else if (aIsNumber) { 160 return 1; // place number after strings in the result 161 } else if (bIsNumber) { 162 return -1; // place number after strings in the result 163 } 164 165 // a and b are both strings 166 if (a < b) { 167 return -1; 168 } else if (a > b) { 169 return 1; 170 } else { 171 return 0; 172 } 173 }); 174 175 return result; 176 } 177 178 private sortTransitions(transitions: TransitionsEntry[]) { 179 const getId = (a: TransitionsEntry) => 180 assertDefined( 181 a.fields.find((field) => field.spec === Presenter.COLUMNS.id), 182 ).value; 183 transitions.sort((a: TransitionsEntry, b: TransitionsEntry) => { 184 return getId(a) <= getId(b) ? -1 : 1; 185 }); 186 } 187 188 private async makeTransitions( 189 headers: LogHeader[], 190 ): Promise<TransitionsEntry[]> { 191 const transitions: TransitionsEntry[] = []; 192 for ( 193 let traceIndex = 0; 194 traceIndex < this.transitionTrace.lengthEntries; 195 ++traceIndex 196 ) { 197 const entry = assertDefined(this.trace.getEntry(traceIndex)); 198 let transitionNode: PropertyTreeNode; 199 try { 200 transitionNode = await entry.getValue(); 201 } catch (e) { 202 continue; 203 } 204 const wmDataNode = assertDefined(transitionNode.getChildByName('wmData')); 205 const shellDataNode = assertDefined( 206 transitionNode.getChildByName('shellData'), 207 ); 208 this.updateTransitionChangesNames.apply(transitionNode); 209 210 const transitionType = this.extractAndFormatTransitionType(wmDataNode); 211 const handler = this.extractAndFormatHandler(shellDataNode); 212 const participants = this.extractAndFormatParticipants(wmDataNode); 213 const flags = this.extractAndFormatFlags(wmDataNode); 214 const [status, statusIcon, statusIconColor] = 215 this.extractAndFormatStatus(transitionNode); 216 217 const fields: LogField[] = [ 218 { 219 spec: Presenter.COLUMNS.id, 220 value: assertDefined(transitionNode.getChildByName('id')).getValue(), 221 }, 222 {spec: Presenter.COLUMNS.type, value: transitionType}, 223 { 224 spec: Presenter.COLUMNS.sendTime, 225 value: 226 wmDataNode.getChildByName('sendTimeNs')?.getValue() ?? 227 Presenter.VALUE_NA, 228 }, 229 { 230 spec: Presenter.COLUMNS.dispatchTime, 231 value: 232 shellDataNode.getChildByName('dispatchTimeNs')?.getValue() ?? 233 Presenter.VALUE_NA, 234 propagateEntryTimestamp: true, 235 }, 236 { 237 spec: Presenter.COLUMNS.duration, 238 value: 239 transitionNode.getChildByName('duration')?.formattedValue() ?? 240 Presenter.VALUE_NA, 241 }, 242 {spec: Presenter.COLUMNS.handler, value: handler}, 243 {spec: Presenter.COLUMNS.participants, value: participants}, 244 {spec: Presenter.COLUMNS.flags, value: flags}, 245 { 246 spec: Presenter.COLUMNS.status, 247 value: status, 248 icon: statusIcon, 249 iconColor: statusIconColor, 250 }, 251 ]; 252 transitions.push(new TransitionsEntry(entry, fields, transitionNode)); 253 } 254 255 return transitions; 256 } 257 258 private extractAndFormatTransitionType(wmDataNode: PropertyTreeNode): string { 259 const transitionType = 260 wmDataNode.getChildByName('type')?.formattedValue() ?? 'NONE'; 261 assertDefined(this.uniqueFieldValues.get(Presenter.COLUMNS.type)).add( 262 transitionType, 263 ); 264 return transitionType; 265 } 266 267 private extractAndFormatHandler(shellDataNode: PropertyTreeNode): string { 268 const handler = 269 shellDataNode.getChildByName('handler')?.formattedValue() ?? 270 Presenter.VALUE_NA; 271 assertDefined(this.uniqueFieldValues.get(Presenter.COLUMNS.handler)).add( 272 handler, 273 ); 274 return handler; 275 } 276 277 private extractAndFormatFlags(wmDataNode: PropertyTreeNode): string { 278 const flags = 279 wmDataNode.getChildByName('flags')?.formattedValue() ?? 280 Presenter.VALUE_NA; 281 282 const uniqueFlags = assertDefined( 283 this.uniqueFieldValues.get(Presenter.COLUMNS.flags), 284 ); 285 flags 286 .split('|') 287 .map((flag) => flag.trim()) 288 .forEach((flag) => uniqueFlags.add(flag)); 289 290 return flags; 291 } 292 293 private extractAndFormatParticipants(wmDataNode: PropertyTreeNode): string { 294 const targets = wmDataNode.getChildByName('targets')?.getAllChildren(); 295 if (!targets) return Presenter.VALUE_NA; 296 297 const layers = new Set<string>(); 298 const windows = new Set<string>(); 299 targets.forEach((target) => { 300 const layerId = target.getChildByName('layerId'); 301 if (layerId) layers.add(layerId.formattedValue()); 302 const windowId = target.getChildByName('windowId'); 303 if (windowId) windows.add(windowId.formattedValue()); 304 }); 305 const uniqueParticipants = assertDefined( 306 this.uniqueFieldValues.get(Presenter.COLUMNS.participants), 307 ); 308 layers.forEach((layer) => uniqueParticipants.add(layer)); 309 windows.forEach((window) => uniqueParticipants.add(window)); 310 311 return `Layers: ${ 312 layers.size > 0 ? [...layers].join(', ') : Presenter.VALUE_NA 313 }\nWindows: ${ 314 windows.size > 0 ? [...windows].join(', ') : Presenter.VALUE_NA 315 }`; 316 } 317 318 private extractAndFormatStatus( 319 transitionNode: PropertyTreeNode, 320 ): [string, string | undefined, string | undefined] { 321 let status = Presenter.VALUE_NA; 322 let statusIcon: string | undefined; 323 let statusIconColor: string | undefined; 324 if (assertDefined(transitionNode.getChildByName('merged')).getValue()) { 325 status = TransitionStatus.MERGED; 326 statusIcon = 'merge'; 327 statusIconColor = 'gray'; 328 } else if ( 329 assertDefined(transitionNode.getChildByName('aborted')).getValue() 330 ) { 331 status = TransitionStatus.ABORTED; 332 statusIcon = 'close'; 333 statusIconColor = 'red'; 334 } else if ( 335 assertDefined(transitionNode.getChildByName('played')).getValue() 336 ) { 337 status = TransitionStatus.PLAYED; 338 statusIcon = 'check'; 339 statusIconColor = 'green'; 340 } 341 assertDefined(this.uniqueFieldValues.get(Presenter.COLUMNS.status)).add( 342 status, 343 ); 344 return [status, statusIcon, statusIconColor]; 345 } 346} 347