1/* 2 * Copyright (C) 2025 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 {UserNotifier} from 'common/user_notifier'; 18import {ProxyTracingWarnings} from 'messaging/user_warnings'; 19import {AdbDeviceConnection} from 'trace_collection/adb/adb_device_connection'; 20import {AdbFileIdentifier, TraceTarget} from 'trace_collection/trace_target'; 21import {TracingSession} from './tracing_session'; 22 23// Perfetto files 24export const PERFETTO_TRACE_FILE = 25 '/data/misc/perfetto-traces/winscope-proxy-trace.perfetto-trace'; 26export const PERFETTO_DUMP_FILE = 27 '/data/misc/perfetto-traces/winscope-proxy-dump.perfetto-trace'; 28export const PERFETTO_TRACE_CONFIG_FILE = 29 '/data/misc/perfetto-configs/winscope-proxy-trace.conf'; 30export const PERFETTO_DUMP_CONFIG_FILE = 31 '/data/misc/perfetto-configs/winscope-proxy-dump.conf'; 32export const PERFETTO_UNIQUE_SESSION_NAME = 'winscope proxy perfetto tracing'; 33 34// Perfetto query helpers 35export const PERFETTO_TRACING_SESSIONS_START = `TRACING SESSIONS: 36 37ID UID STATE BUF (#) KB DUR (s) #DS STARTED NAME 38=== === ===== ========== ======= === ======= ====\n`; 39export const PERFETTO_TRACING_SESSIONS_END = 40 '\nNOTE: Some tracing sessions are not reported in the list above.'; 41 42export class PerfettoSessionModerator { 43 private queryResult: string | undefined; 44 private prevSessionActive = false; 45 private concurrentSessions: number | undefined; 46 private configFilepath: string; 47 48 constructor(private device: AdbDeviceConnection, private isDump: boolean) { 49 this.configFilepath = isDump 50 ? PERFETTO_DUMP_CONFIG_FILE 51 : PERFETTO_TRACE_CONFIG_FILE; 52 } 53 54 async clearPreviousConfigFiles() { 55 console.debug('Clearing perfetto config file for previous tracing session'); 56 await this.device.runShellCommand(`su root rm -f ${this.configFilepath}`); 57 console.debug('Cleared perfetto config file for previous tracing session'); 58 } 59 60 async isTooManySessions() { 61 if (this.concurrentSessions === undefined) { 62 this.concurrentSessions = await this.getConcurrentSessions(); 63 } 64 const tooManyPerfettoSessions = this.concurrentSessions >= 5; 65 if (tooManyPerfettoSessions) { 66 const warning = 67 'Limit of 5 Perfetto sessions reached on device. Will attempt to collect legacy traces.'; 68 UserNotifier.add(new ProxyTracingWarnings([warning])).notify(); 69 } 70 return tooManyPerfettoSessions; 71 } 72 73 async tryStopCurrentPerfettoSession() { 74 if (this.concurrentSessions === undefined) { 75 this.concurrentSessions = await this.getConcurrentSessions(); 76 } 77 if (!this.prevSessionActive) { 78 return; 79 } 80 console.debug('Stopping already-running winscope perfetto session.'); 81 await this.device?.runShellCommand( 82 'perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop', 83 ); 84 this.prevSessionActive = false; 85 console.debug('Stopped already-running winscope perfetto session.'); 86 } 87 88 async isDataSourceAvailable(ds: string): Promise<boolean> { 89 const queryResult = await this.getQueryResult(); 90 return queryResult.includes(ds); 91 } 92 93 createTracingSession(setupCommands: string[]): TracingSession { 94 if (this.isDump) { 95 return new TracingSession(this.makePerfettoDumpTarget(setupCommands)); 96 } else { 97 return new TracingSession(this.makePerfettoTraceTarget(setupCommands)); 98 } 99 } 100 101 createSetupCommand(ds: string, config?: string): string { 102 const spacer = '\n '; 103 return `cat << EOF >> ${this.configFilepath} 104data_sources: { 105 config { 106 name: "${ds}"${config ? spacer + config : ''} 107 } 108} 109EOF`; 110 } 111 112 private makePerfettoDumpTarget(setupCommands: string[]) { 113 return new TraceTarget( 114 'PerfettoDump', 115 setupCommands, 116 `cat << EOF >> ${PERFETTO_DUMP_CONFIG_FILE} 117buffers: { 118 size_kb: 500000 119 fill_policy: RING_BUFFER 120} 121duration_ms: 1 122EOF 123rm -f ${PERFETTO_DUMP_FILE} 124perfetto --out ${PERFETTO_DUMP_FILE} --txt --config ${PERFETTO_DUMP_CONFIG_FILE} 125echo 'Dumped perfetto'`, 126 '', 127 [new AdbFileIdentifier(PERFETTO_DUMP_FILE, [], 'dump.perfetto-trace')], 128 ); 129 } 130 131 private makePerfettoTraceTarget(setupCommands: string[]) { 132 return new TraceTarget( 133 'PerfettoTrace', 134 setupCommands, 135 `cat << EOF >> ${PERFETTO_TRACE_CONFIG_FILE} 136buffers: { 137 size_kb: 500000 138 fill_policy: RING_BUFFER 139} 140duration_ms: 0 141file_write_period_ms: 999999999 142write_into_file: true 143unique_session_name: "${PERFETTO_UNIQUE_SESSION_NAME}" 144EOF 145rm -f ${PERFETTO_TRACE_FILE} 146perfetto --out ${PERFETTO_TRACE_FILE} --txt --config ${PERFETTO_TRACE_CONFIG_FILE} --detach=WINSCOPE-PROXY-TRACING-SESSION 147echo 'Perfetto trace started.'`, 148 `perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop 149echo 'Perfetto trace stopped.'`, 150 [new AdbFileIdentifier(PERFETTO_TRACE_FILE, [], 'trace.perfetto-trace')], 151 ); 152 } 153 154 private async getConcurrentSessions(): Promise<number> { 155 const queryRes = await this.getQueryResult(); 156 const startIndex = queryRes.indexOf(PERFETTO_TRACING_SESSIONS_START); 157 let numberOfConcurrentSessions = 0; 158 if (startIndex !== -1) { 159 let concurrentSessions = queryRes.slice(startIndex); 160 console.debug(`Concurrent sessions:\n${concurrentSessions}`); 161 concurrentSessions = concurrentSessions.slice( 162 PERFETTO_TRACING_SESSIONS_START.length, 163 ); 164 165 const endIndex = concurrentSessions.indexOf( 166 PERFETTO_TRACING_SESSIONS_END, 167 ); 168 if (endIndex !== -1) { 169 concurrentSessions = concurrentSessions.slice(0, endIndex); 170 } 171 172 numberOfConcurrentSessions = 173 concurrentSessions.length > 0 174 ? concurrentSessions.trim().split('\n').length 175 : 0; 176 177 if (concurrentSessions.includes(PERFETTO_UNIQUE_SESSION_NAME)) { 178 this.prevSessionActive = true; 179 numberOfConcurrentSessions -= 1; 180 } 181 } 182 return numberOfConcurrentSessions; 183 } 184 185 private async getQueryResult() { 186 if (this.queryResult === undefined) { 187 this.queryResult = await this.device.runShellCommand('perfetto --query'); 188 } 189 return this.queryResult; 190 } 191} 192