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 {Timestamp} from 'common/time/time'; 19import {ParserTimestampConverter} from 'common/time/timestamp_converter'; 20import {AbstractTracesParser} from 'parsers/traces/abstract_traces_parser'; 21import {EntryPropertiesTreeFactory} from 'parsers/transitions/entry_properties_tree_factory'; 22import {CoarseVersion} from 'trace/coarse_version'; 23import {Trace} from 'trace/trace'; 24import {Traces} from 'trace/traces'; 25import {TraceType} from 'trace/trace_type'; 26import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 27 28export class TracesParserTransitions extends AbstractTracesParser<PropertyTreeNode> { 29 private readonly wmTransitionTrace: Trace<PropertyTreeNode> | undefined; 30 private readonly shellTransitionTrace: Trace<PropertyTreeNode> | undefined; 31 private readonly descriptors: string[]; 32 private decodedEntries: PropertyTreeNode[] | undefined; 33 34 constructor(traces: Traces, timestampConverter: ParserTimestampConverter) { 35 super(timestampConverter); 36 const wmTransitionTrace = traces.getTrace(TraceType.WM_TRANSITION); 37 const shellTransitionTrace = traces.getTrace(TraceType.SHELL_TRANSITION); 38 if (wmTransitionTrace && shellTransitionTrace) { 39 this.wmTransitionTrace = wmTransitionTrace; 40 this.shellTransitionTrace = shellTransitionTrace; 41 this.descriptors = this.wmTransitionTrace 42 .getDescriptors() 43 .concat(this.shellTransitionTrace.getDescriptors()); 44 } else { 45 this.descriptors = []; 46 } 47 } 48 49 override getCoarseVersion(): CoarseVersion { 50 return CoarseVersion.LEGACY; 51 } 52 53 override async parse() { 54 if (this.wmTransitionTrace === undefined) { 55 throw new Error('Missing WM Transition trace'); 56 } 57 58 if (this.shellTransitionTrace === undefined) { 59 throw new Error('Missing Shell Transition trace'); 60 } 61 62 const wmTransitionEntries: PropertyTreeNode[] = await Promise.all( 63 this.wmTransitionTrace.mapEntry((entry) => entry.getValue()), 64 ); 65 66 const shellTransitionEntries: PropertyTreeNode[] = await Promise.all( 67 this.shellTransitionTrace.mapEntry((entry) => entry.getValue()), 68 ); 69 70 const allEntries = wmTransitionEntries.concat(shellTransitionEntries); 71 72 this.decodedEntries = this.compressEntries(allEntries); 73 74 await this.createTimestamps(); 75 } 76 77 override async createTimestamps() { 78 this.timestamps = []; 79 const zeroTs = this.timestampConverter.makeZeroTimestamp(); 80 for (let index = 0; index < this.getLengthEntries(); index++) { 81 const entry = await this.getEntry(index); 82 const ts = this.getTimestampFromTransitionProperties(entry); 83 this.timestamps.push(ts ?? zeroTs); 84 } 85 } 86 87 override getLengthEntries(): number { 88 return assertDefined(this.decodedEntries).length; 89 } 90 91 override getEntry(index: number): Promise<PropertyTreeNode> { 92 const entry = assertDefined(this.decodedEntries)[index]; 93 return Promise.resolve(entry); 94 } 95 96 override getDescriptors(): string[] { 97 return this.descriptors; 98 } 99 100 override getTraceType(): TraceType { 101 return TraceType.TRANSITION; 102 } 103 104 override getRealToMonotonicTimeOffsetNs(): bigint | undefined { 105 return undefined; 106 } 107 108 override getRealToBootTimeOffsetNs(): bigint | undefined { 109 return undefined; 110 } 111 112 private compressEntries( 113 allTransitions: PropertyTreeNode[], 114 ): PropertyTreeNode[] { 115 const idToTransition = new Map<number, PropertyTreeNode>(); 116 117 for (const transition of allTransitions) { 118 const id = assertDefined(transition.getChildByName('id')).getValue(); 119 120 const accumulatedTransition = idToTransition.get(id); 121 if (!accumulatedTransition) { 122 idToTransition.set(id, transition); 123 } else { 124 const mergedTransition = this.mergePartialTransitions( 125 accumulatedTransition, 126 transition, 127 ); 128 idToTransition.set(id, mergedTransition); 129 } 130 } 131 132 const compressedTransitions = Array.from(idToTransition.values()); 133 compressedTransitions.forEach((transition) => { 134 EntryPropertiesTreeFactory.TRANSITION_OPERATIONS.forEach((operation) => 135 operation.apply(transition), 136 ); 137 }); 138 return compressedTransitions.sort((a, b) => this.compareByTimestamp(a, b)); 139 } 140 141 private compareByTimestamp(a: PropertyTreeNode, b: PropertyTreeNode): number { 142 const aNs = this.getTimestampFromTransitionProperties(a) ?? 0n; 143 const bNs = this.getTimestampFromTransitionProperties(b) ?? 0n; 144 if (aNs !== bNs) { 145 return aNs < bNs ? -1 : 1; 146 } 147 // fallback to id 148 return assertDefined(a.getChildByName('id')).getValue() < 149 assertDefined(b.getChildByName('id')).getValue() 150 ? -1 151 : 1; 152 } 153 154 private getTimestampFromTransitionProperties( 155 transition: PropertyTreeNode, 156 ): Timestamp | undefined { 157 // Entry timestamps are defined as shell dispatch time - if this is 158 // null and send time is not null we fall back on send time 159 return ( 160 assertDefined(transition.getChildByName('shellData')) 161 .getChildByName('dispatchTimeNs') 162 ?.getValue() ?? 163 assertDefined(transition.getChildByName('wmData')) 164 .getChildByName('sendTimeNs') 165 ?.getValue() 166 ); 167 } 168 169 private mergePartialTransitions( 170 transition1: PropertyTreeNode, 171 transition2: PropertyTreeNode, 172 ): PropertyTreeNode { 173 const mergedTransition = this.mergeProperties( 174 transition1, 175 transition2, 176 false, 177 ); 178 179 const wmData1 = assertDefined(transition1.getChildByName('wmData')); 180 const wmData2 = assertDefined(transition2.getChildByName('wmData')); 181 const mergedWmData = this.mergeProperties(wmData1, wmData2); 182 mergedTransition.addOrReplaceChild(mergedWmData); 183 184 const shellData1 = assertDefined(transition1.getChildByName('shellData')); 185 const shellData2 = assertDefined(transition2.getChildByName('shellData')); 186 const mergedShellData = this.mergeProperties(shellData1, shellData2); 187 mergedTransition.addOrReplaceChild(mergedShellData); 188 189 return mergedTransition; 190 } 191 192 private mergeProperties( 193 node1: PropertyTreeNode, 194 node2: PropertyTreeNode, 195 visitNestedChildren = true, 196 ): PropertyTreeNode { 197 const mergedNode = new PropertyTreeNode( 198 node1.id, 199 node1.name, 200 node1.source, 201 undefined, 202 ); 203 204 node1.getAllChildren().forEach((property1) => { 205 if (!visitNestedChildren && property1.getAllChildren().length > 0) { 206 return; 207 } 208 209 const property2 = node2.getChildByName(property1.name); 210 if ( 211 !property2 || 212 property2.getValue()?.toString() < property1.getValue()?.toString() 213 ) { 214 mergedNode.addOrReplaceChild(property1); 215 return; 216 } 217 218 if (visitNestedChildren && property1.getAllChildren().length > 0) { 219 const mergedProperty = this.mergeProperties(property1, property2); 220 mergedNode.addOrReplaceChild(mergedProperty); 221 return; 222 } 223 224 mergedNode.addOrReplaceChild(property2); 225 }); 226 227 node2.getAllChildren().forEach((property2) => { 228 const existingProperty = mergedNode.getChildByName(property2.name); 229 if (!existingProperty) { 230 mergedNode.addOrReplaceChild(property2); 231 return; 232 } 233 }); 234 235 return mergedNode; 236 } 237} 238