1// Copyright (C) 2018 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {Message, Method, rpc, RPCImplCallback} from 'protobufjs'; 16 17import { 18 base64Encode, 19} from '../base/string_utils'; 20import {Actions} from '../common/actions'; 21import {TRACE_SUFFIX} from '../common/constants'; 22import { 23 AndroidLogConfig, 24 AndroidLogId, 25 AndroidPowerConfig, 26 BufferConfig, 27 ChromeConfig, 28 ConsumerPort, 29 DataSourceConfig, 30 FtraceConfig, 31 HeapprofdConfig, 32 JavaContinuousDumpConfig, 33 JavaHprofConfig, 34 NativeContinuousDumpConfig, 35 ProcessStatsConfig, 36 SysStatsConfig, 37 TraceConfig, 38} from '../common/protos'; 39import {MeminfoCounters, VmstatCounters} from '../common/protos'; 40import { 41 AdbRecordingTarget, 42 isAdbTarget, 43 isAndroidP, 44 isChromeTarget, 45 isCrOSTarget, 46 RecordConfig, 47 RecordingTarget 48} from '../common/state'; 49 50import {AdbOverWebUsb} from './adb'; 51import {AdbConsumerPort} from './adb_shell_controller'; 52import {AdbSocketConsumerPort} from './adb_socket_controller'; 53import {ChromeExtensionConsumerPort} from './chrome_proxy_record_controller'; 54import { 55 ConsumerPortResponse, 56 GetTraceStatsResponse, 57 isDisableTracingResponse, 58 isEnableTracingResponse, 59 isFreeBuffersResponse, 60 isGetTraceStatsResponse, 61 isReadBuffersResponse, 62} from './consumer_port_types'; 63import {Controller} from './controller'; 64import {App, globals} from './globals'; 65import {Consumer, RpcConsumerPort} from './record_controller_interfaces'; 66 67type RPCImplMethod = (Method|rpc.ServiceMethod<Message<{}>, Message<{}>>); 68 69export function genConfigProto( 70 uiCfg: RecordConfig, target: RecordingTarget): Uint8Array { 71 return TraceConfig.encode(genConfig(uiCfg, target)).finish(); 72} 73 74export function genConfig( 75 uiCfg: RecordConfig, target: RecordingTarget): TraceConfig { 76 const protoCfg = new TraceConfig(); 77 protoCfg.durationMs = uiCfg.durationMs; 78 79 // Auxiliary buffer for slow-rate events. 80 // Set to 1/8th of the main buffer size, with reasonable limits. 81 let slowBufSizeKb = uiCfg.bufferSizeMb * (1024 / 8); 82 slowBufSizeKb = Math.min(slowBufSizeKb, 2 * 1024); 83 slowBufSizeKb = Math.max(slowBufSizeKb, 256); 84 85 // Main buffer for ftrace and other high-freq events. 86 const fastBufSizeKb = uiCfg.bufferSizeMb * 1024 - slowBufSizeKb; 87 88 protoCfg.buffers.push(new BufferConfig()); 89 protoCfg.buffers.push(new BufferConfig()); 90 protoCfg.buffers[1].sizeKb = slowBufSizeKb; 91 protoCfg.buffers[0].sizeKb = fastBufSizeKb; 92 93 if (uiCfg.mode === 'STOP_WHEN_FULL') { 94 protoCfg.buffers[0].fillPolicy = BufferConfig.FillPolicy.DISCARD; 95 protoCfg.buffers[1].fillPolicy = BufferConfig.FillPolicy.DISCARD; 96 } else { 97 protoCfg.buffers[0].fillPolicy = BufferConfig.FillPolicy.RING_BUFFER; 98 protoCfg.buffers[1].fillPolicy = BufferConfig.FillPolicy.RING_BUFFER; 99 protoCfg.flushPeriodMs = 30000; 100 if (uiCfg.mode === 'LONG_TRACE') { 101 protoCfg.writeIntoFile = true; 102 protoCfg.fileWritePeriodMs = uiCfg.fileWritePeriodMs; 103 protoCfg.maxFileSizeBytes = uiCfg.maxFileSizeMb * 1e6; 104 } 105 106 // Clear incremental state every 5 seconds when tracing into a ring buffer. 107 const incStateConfig = new TraceConfig.IncrementalStateConfig(); 108 incStateConfig.clearPeriodMs = 5000; 109 protoCfg.incrementalStateConfig = incStateConfig; 110 } 111 112 const ftraceEvents = new Set<string>(uiCfg.ftrace ? uiCfg.ftraceEvents : []); 113 const atraceCats = new Set<string>(uiCfg.atrace ? uiCfg.atraceCats : []); 114 const atraceApps = new Set<string>(); 115 const chromeCategories = new Set<string>(); 116 uiCfg.chromeCategoriesSelected.forEach(it => chromeCategories.add(it)); 117 118 let procThreadAssociationPolling = false; 119 let procThreadAssociationFtrace = false; 120 let trackInitialOomScore = false; 121 122 if (uiCfg.cpuSched) { 123 procThreadAssociationPolling = true; 124 procThreadAssociationFtrace = true; 125 ftraceEvents.add('sched/sched_switch'); 126 ftraceEvents.add('power/suspend_resume'); 127 ftraceEvents.add('sched/sched_wakeup'); 128 ftraceEvents.add('sched/sched_wakeup_new'); 129 ftraceEvents.add('sched/sched_waking'); 130 ftraceEvents.add('power/suspend_resume'); 131 } 132 133 if (uiCfg.cpuFreq) { 134 ftraceEvents.add('power/cpu_frequency'); 135 ftraceEvents.add('power/cpu_idle'); 136 ftraceEvents.add('power/suspend_resume'); 137 } 138 139 if (uiCfg.gpuFreq) { 140 ftraceEvents.add('power/gpu_frequency'); 141 } 142 143 if (uiCfg.gpuMemTotal) { 144 ftraceEvents.add('gpu_mem/gpu_mem_total'); 145 146 if (!isChromeTarget(target) || isCrOSTarget(target)) { 147 const ds = new TraceConfig.DataSource(); 148 ds.config = new DataSourceConfig(); 149 ds.config.name = 'android.gpu.memory'; 150 protoCfg.dataSources.push(ds); 151 } 152 } 153 154 if (uiCfg.cpuSyscall) { 155 ftraceEvents.add('raw_syscalls/sys_enter'); 156 ftraceEvents.add('raw_syscalls/sys_exit'); 157 } 158 159 if (procThreadAssociationFtrace) { 160 ftraceEvents.add('sched/sched_process_exit'); 161 ftraceEvents.add('sched/sched_process_free'); 162 ftraceEvents.add('task/task_newtask'); 163 ftraceEvents.add('task/task_rename'); 164 } 165 166 if (uiCfg.batteryDrain) { 167 const ds = new TraceConfig.DataSource(); 168 ds.config = new DataSourceConfig(); 169 ds.config.name = 'android.power'; 170 ds.config.androidPowerConfig = new AndroidPowerConfig(); 171 ds.config.androidPowerConfig.batteryPollMs = uiCfg.batteryDrainPollMs; 172 ds.config.androidPowerConfig.batteryCounters = [ 173 AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CAPACITY_PERCENT, 174 AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CHARGE, 175 AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CURRENT, 176 ]; 177 ds.config.androidPowerConfig.collectPowerRails = true; 178 if (!isChromeTarget(target) || isCrOSTarget(target)) { 179 protoCfg.dataSources.push(ds); 180 } 181 } 182 183 if (uiCfg.boardSensors) { 184 ftraceEvents.add('regulator/regulator_set_voltage'); 185 ftraceEvents.add('regulator/regulator_set_voltage_complete'); 186 ftraceEvents.add('power/clock_enable'); 187 ftraceEvents.add('power/clock_disable'); 188 ftraceEvents.add('power/clock_set_rate'); 189 ftraceEvents.add('power/suspend_resume'); 190 } 191 192 let sysStatsCfg: SysStatsConfig|undefined = undefined; 193 194 if (uiCfg.cpuCoarse) { 195 if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig(); 196 sysStatsCfg.statPeriodMs = uiCfg.cpuCoarsePollMs; 197 sysStatsCfg.statCounters = [ 198 SysStatsConfig.StatCounters.STAT_CPU_TIMES, 199 SysStatsConfig.StatCounters.STAT_FORK_COUNT, 200 ]; 201 } 202 203 if (uiCfg.memHiFreq) { 204 procThreadAssociationPolling = true; 205 procThreadAssociationFtrace = true; 206 ftraceEvents.add('mm_event/mm_event_record'); 207 ftraceEvents.add('kmem/rss_stat'); 208 ftraceEvents.add('ion/ion_stat'); 209 ftraceEvents.add('dmabuf_heap/dma_heap_stat'); 210 ftraceEvents.add('kmem/ion_heap_grow'); 211 ftraceEvents.add('kmem/ion_heap_shrink'); 212 } 213 214 if (uiCfg.meminfo) { 215 if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig(); 216 sysStatsCfg.meminfoPeriodMs = uiCfg.meminfoPeriodMs; 217 sysStatsCfg.meminfoCounters = uiCfg.meminfoCounters.map(name => { 218 // tslint:disable-next-line no-any 219 return MeminfoCounters[name as any as number] as any as number; 220 }); 221 } 222 223 if (uiCfg.vmstat) { 224 if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig(); 225 sysStatsCfg.vmstatPeriodMs = uiCfg.vmstatPeriodMs; 226 sysStatsCfg.vmstatCounters = uiCfg.vmstatCounters.map(name => { 227 // tslint:disable-next-line no-any 228 return VmstatCounters[name as any as number] as any as number; 229 }); 230 } 231 232 if (uiCfg.memLmk) { 233 // For in-kernel LMK (roughly older devices until Go and Pixel 3). 234 ftraceEvents.add('lowmemorykiller/lowmemory_kill'); 235 236 // For userspace LMKd (newer devices). 237 // 'lmkd' is not really required because the code in lmkd.c emits events 238 // with ATRACE_TAG_ALWAYS. We need something just to ensure that the final 239 // config will enable atrace userspace events. 240 atraceApps.add('lmkd'); 241 242 ftraceEvents.add('oom/oom_score_adj_update'); 243 procThreadAssociationPolling = true; 244 trackInitialOomScore = true; 245 } 246 247 let heapprofd: HeapprofdConfig|undefined = undefined; 248 if (uiCfg.heapProfiling) { 249 // TODO(hjd): Check or inform user if buffer size are too small. 250 const cfg = new HeapprofdConfig(); 251 cfg.samplingIntervalBytes = uiCfg.hpSamplingIntervalBytes; 252 if (uiCfg.hpSharedMemoryBuffer >= 8192 && 253 uiCfg.hpSharedMemoryBuffer % 4096 === 0) { 254 cfg.shmemSizeBytes = uiCfg.hpSharedMemoryBuffer; 255 } 256 for (const value of uiCfg.hpProcesses.split('\n')) { 257 if (value === '') { 258 // Ignore empty lines 259 } else if (isNaN(+value)) { 260 cfg.processCmdline.push(value); 261 } else { 262 cfg.pid.push(+value); 263 } 264 } 265 if (uiCfg.hpContinuousDumpsInterval > 0) { 266 const cdc = cfg.continuousDumpConfig = new NativeContinuousDumpConfig(); 267 cdc.dumpIntervalMs = uiCfg.hpContinuousDumpsInterval; 268 if (uiCfg.hpContinuousDumpsPhase > 0) { 269 cdc.dumpPhaseMs = uiCfg.hpContinuousDumpsPhase; 270 } 271 } 272 cfg.blockClient = uiCfg.hpBlockClient; 273 if (uiCfg.hpAllHeaps) { 274 cfg.allHeaps = true; 275 } 276 heapprofd = cfg; 277 } 278 279 let javaHprof: JavaHprofConfig|undefined = undefined; 280 if (uiCfg.javaHeapDump) { 281 const cfg = new JavaHprofConfig(); 282 for (const value of uiCfg.jpProcesses.split('\n')) { 283 if (value === '') { 284 // Ignore empty lines 285 } else if (isNaN(+value)) { 286 cfg.processCmdline.push(value); 287 } else { 288 cfg.pid.push(+value); 289 } 290 } 291 if (uiCfg.jpContinuousDumpsInterval > 0) { 292 const cdc = cfg.continuousDumpConfig = new JavaContinuousDumpConfig(); 293 cdc.dumpIntervalMs = uiCfg.jpContinuousDumpsInterval; 294 if (uiCfg.hpContinuousDumpsPhase > 0) { 295 cdc.dumpPhaseMs = uiCfg.jpContinuousDumpsPhase; 296 } 297 } 298 javaHprof = cfg; 299 } 300 301 if (uiCfg.procStats || procThreadAssociationPolling || trackInitialOomScore) { 302 const ds = new TraceConfig.DataSource(); 303 ds.config = new DataSourceConfig(); 304 ds.config.targetBuffer = 1; // Aux 305 ds.config.name = 'linux.process_stats'; 306 ds.config.processStatsConfig = new ProcessStatsConfig(); 307 if (uiCfg.procStats) { 308 ds.config.processStatsConfig.procStatsPollMs = uiCfg.procStatsPeriodMs; 309 } 310 if (procThreadAssociationPolling || trackInitialOomScore) { 311 ds.config.processStatsConfig.scanAllProcessesOnStart = true; 312 } 313 if (!isChromeTarget(target) || isCrOSTarget(target)) { 314 protoCfg.dataSources.push(ds); 315 } 316 } 317 318 if (uiCfg.androidLogs) { 319 const ds = new TraceConfig.DataSource(); 320 ds.config = new DataSourceConfig(); 321 ds.config.name = 'android.log'; 322 ds.config.androidLogConfig = new AndroidLogConfig(); 323 ds.config.androidLogConfig.logIds = uiCfg.androidLogBuffers.map(name => { 324 // tslint:disable-next-line no-any 325 return AndroidLogId[name as any as number] as any as number; 326 }); 327 328 if (!isChromeTarget(target) || isCrOSTarget(target)) { 329 protoCfg.dataSources.push(ds); 330 } 331 } 332 333 if (uiCfg.androidFrameTimeline) { 334 const ds = new TraceConfig.DataSource(); 335 ds.config = new DataSourceConfig(); 336 ds.config.name = 'android.surfaceflinger.frametimeline'; 337 if (!isChromeTarget(target) || isCrOSTarget(target)) { 338 protoCfg.dataSources.push(ds); 339 } 340 } 341 342 if (uiCfg.chromeLogs) { 343 chromeCategories.add('log'); 344 } 345 346 if (uiCfg.taskScheduling) { 347 chromeCategories.add('toplevel'); 348 chromeCategories.add('sequence_manager'); 349 chromeCategories.add('disabled-by-default-toplevel.flow'); 350 } 351 352 if (uiCfg.ipcFlows) { 353 chromeCategories.add('toplevel'); 354 chromeCategories.add('disabled-by-default-ipc.flow'); 355 chromeCategories.add('mojom'); 356 } 357 358 if (uiCfg.jsExecution) { 359 chromeCategories.add('toplevel'); 360 chromeCategories.add('v8'); 361 } 362 363 if (uiCfg.webContentRendering) { 364 chromeCategories.add('toplevel'); 365 chromeCategories.add('blink'); 366 chromeCategories.add('cc'); 367 chromeCategories.add('gpu'); 368 } 369 370 if (uiCfg.uiRendering) { 371 chromeCategories.add('toplevel'); 372 chromeCategories.add('cc'); 373 chromeCategories.add('gpu'); 374 chromeCategories.add('viz'); 375 chromeCategories.add('ui'); 376 chromeCategories.add('views'); 377 } 378 379 if (uiCfg.inputEvents) { 380 chromeCategories.add('toplevel'); 381 chromeCategories.add('benchmark'); 382 chromeCategories.add('evdev'); 383 chromeCategories.add('input'); 384 chromeCategories.add('disabled-by-default-toplevel.flow'); 385 } 386 387 if (uiCfg.navigationAndLoading) { 388 chromeCategories.add('loading'); 389 chromeCategories.add('net'); 390 chromeCategories.add('netlog'); 391 chromeCategories.add('navigation'); 392 chromeCategories.add('browser'); 393 } 394 395 if (chromeCategories.size !== 0) { 396 let chromeRecordMode = ''; 397 if (uiCfg.mode === 'STOP_WHEN_FULL') { 398 chromeRecordMode = 'record-until-full'; 399 } else { 400 chromeRecordMode = 'record-continuously'; 401 } 402 const configStruct = { 403 record_mode: chromeRecordMode, 404 included_categories: [...chromeCategories.values()], 405 memory_dump_config: {}, 406 }; 407 if (chromeCategories.has('disabled-by-default-memory-infra')) { 408 configStruct.memory_dump_config = { 409 allowed_dump_modes: ['background', 'light', 'detailed'], 410 triggers: [{ 411 min_time_between_dumps_ms: 10000, 412 mode: 'detailed', 413 type: 'periodic_interval', 414 }], 415 }; 416 } 417 const traceConfigJson = JSON.stringify(configStruct); 418 419 const traceDs = new TraceConfig.DataSource(); 420 traceDs.config = new DataSourceConfig(); 421 traceDs.config.name = 'org.chromium.trace_event'; 422 traceDs.config.chromeConfig = new ChromeConfig(); 423 traceDs.config.chromeConfig.traceConfig = traceConfigJson; 424 protoCfg.dataSources.push(traceDs); 425 426 427 const metadataDs = new TraceConfig.DataSource(); 428 metadataDs.config = new DataSourceConfig(); 429 metadataDs.config.name = 'org.chromium.trace_metadata'; 430 metadataDs.config.chromeConfig = new ChromeConfig(); 431 metadataDs.config.chromeConfig.traceConfig = traceConfigJson; 432 protoCfg.dataSources.push(metadataDs); 433 } 434 435 if (uiCfg.screenRecord) { 436 atraceCats.add('gfx'); 437 } 438 439 // Keep these last. The stages above can enrich them. 440 441 if (sysStatsCfg !== undefined && 442 (!isChromeTarget(target) || isCrOSTarget(target))) { 443 const ds = new TraceConfig.DataSource(); 444 ds.config = new DataSourceConfig(); 445 ds.config.name = 'linux.sys_stats'; 446 ds.config.sysStatsConfig = sysStatsCfg; 447 protoCfg.dataSources.push(ds); 448 } 449 450 if (heapprofd !== undefined && 451 (!isChromeTarget(target) || isCrOSTarget(target))) { 452 const ds = new TraceConfig.DataSource(); 453 ds.config = new DataSourceConfig(); 454 ds.config.targetBuffer = 0; 455 ds.config.name = 'android.heapprofd'; 456 ds.config.heapprofdConfig = heapprofd; 457 protoCfg.dataSources.push(ds); 458 } 459 460 if (javaHprof !== undefined && 461 (!isChromeTarget(target) || isCrOSTarget(target))) { 462 const ds = new TraceConfig.DataSource(); 463 ds.config = new DataSourceConfig(); 464 ds.config.targetBuffer = 0; 465 ds.config.name = 'android.java_hprof'; 466 ds.config.javaHprofConfig = javaHprof; 467 protoCfg.dataSources.push(ds); 468 } 469 470 if (uiCfg.ftrace || uiCfg.atraceApps.length > 0 || ftraceEvents.size > 0 || 471 atraceCats.size > 0 || atraceApps.size > 0) { 472 const ds = new TraceConfig.DataSource(); 473 ds.config = new DataSourceConfig(); 474 ds.config.name = 'linux.ftrace'; 475 ds.config.ftraceConfig = new FtraceConfig(); 476 // Override the advanced ftrace parameters only if the user has ticked the 477 // "Advanced ftrace config" tab. 478 if (uiCfg.ftrace) { 479 ds.config.ftraceConfig.bufferSizeKb = uiCfg.ftraceBufferSizeKb; 480 ds.config.ftraceConfig.drainPeriodMs = uiCfg.ftraceDrainPeriodMs; 481 for (const line of uiCfg.ftraceExtraEvents.split('\n')) { 482 if (line.trim().length > 0) ftraceEvents.add(line.trim()); 483 } 484 } 485 for (const line of uiCfg.atraceApps.split('\n')) { 486 if (line.trim().length > 0) atraceApps.add(line.trim()); 487 } 488 489 if (atraceCats.size > 0 || atraceApps.size > 0) { 490 ftraceEvents.add('ftrace/print'); 491 } 492 493 let ftraceEventsArray: string[] = []; 494 if (isAndroidP(target)) { 495 for (const ftraceEvent of ftraceEvents) { 496 // On P, we don't support groups so strip all group names from ftrace 497 // events. 498 const groupAndName = ftraceEvent.split('/'); 499 if (groupAndName.length !== 2) { 500 ftraceEventsArray.push(ftraceEvent); 501 continue; 502 } 503 // Filter out any wildcard event groups which was not supported 504 // before Q. 505 if (groupAndName[1] === '*') { 506 continue; 507 } 508 ftraceEventsArray.push(groupAndName[1]); 509 } 510 } else { 511 ftraceEventsArray = Array.from(ftraceEvents); 512 } 513 514 ds.config.ftraceConfig.ftraceEvents = ftraceEventsArray; 515 ds.config.ftraceConfig.atraceCategories = Array.from(atraceCats); 516 ds.config.ftraceConfig.atraceApps = Array.from(atraceApps); 517 if (!isChromeTarget(target) || isCrOSTarget(target)) { 518 protoCfg.dataSources.push(ds); 519 } 520 } 521 522 return protoCfg; 523} 524 525export function toPbtxt(configBuffer: Uint8Array): string { 526 const msg = TraceConfig.decode(configBuffer); 527 const json = msg.toJSON(); 528 function snakeCase(s: string): string { 529 return s.replace(/[A-Z]/g, c => '_' + c.toLowerCase()); 530 } 531 // With the ahead of time compiled protos we can't seem to tell which 532 // fields are enums. 533 function isEnum(value: string): boolean { 534 return value.startsWith('MEMINFO_') || value.startsWith('VMSTAT_') || 535 value.startsWith('STAT_') || value.startsWith('LID_') || 536 value.startsWith('BATTERY_COUNTER_') || value === 'DISCARD' || 537 value === 'RING_BUFFER'; 538 } 539 // Since javascript doesn't have 64 bit numbers when converting protos to 540 // json the proto library encodes them as strings. This is lossy since 541 // we can't tell which strings that look like numbers are actually strings 542 // and which are actually numbers. Ideally we would reflect on the proto 543 // definition somehow but for now we just hard code keys which have this 544 // problem in the config. 545 function is64BitNumber(key: string): boolean { 546 return [ 547 'maxFileSizeBytes', 548 'samplingIntervalBytes', 549 'shmemSizeBytes', 550 'pid' 551 ].includes(key); 552 } 553 function* message(msg: {}, indent: number): IterableIterator<string> { 554 for (const [key, value] of Object.entries(msg)) { 555 const isRepeated = Array.isArray(value); 556 const isNested = typeof value === 'object' && !isRepeated; 557 for (const entry of (isRepeated ? value as Array<{}> : [value])) { 558 yield ' '.repeat(indent) + `${snakeCase(key)}${isNested ? '' : ':'} `; 559 if (typeof entry === 'string') { 560 if (isEnum(entry) || is64BitNumber(key)) { 561 yield entry; 562 } else { 563 yield `"${entry.replace(new RegExp('"', 'g'), '\\"')}"`; 564 } 565 } else if (typeof entry === 'number') { 566 yield entry.toString(); 567 } else if (typeof entry === 'boolean') { 568 yield entry.toString(); 569 } else if (typeof entry === 'object' && entry !== null) { 570 yield '{\n'; 571 yield* message(entry, indent + 4); 572 yield ' '.repeat(indent) + '}'; 573 } else { 574 throw new Error(`Record proto entry "${entry}" with unexpected type ${ 575 typeof entry}`); 576 } 577 yield '\n'; 578 } 579 } 580 } 581 return [...message(json, 0)].join(''); 582} 583 584export class RecordController extends Controller<'main'> implements Consumer { 585 private app: App; 586 private config: RecordConfig|null = null; 587 private extensionPort: MessagePort; 588 private recordingInProgress = false; 589 private consumerPort: ConsumerPort; 590 private traceBuffer: Uint8Array[] = []; 591 private bufferUpdateInterval: ReturnType<typeof setTimeout>|undefined; 592 private adb = new AdbOverWebUsb(); 593 private recordedTraceSuffix = TRACE_SUFFIX; 594 595 // We have a different controller for each targetOS. The correct one will be 596 // created when needed, and stored here. When the key is a string, it is the 597 // serial of the target (used for android devices). When the key is a single 598 // char, it is the 'targetOS' 599 private controllerPromises = new Map<string, Promise<RpcConsumerPort>>(); 600 601 constructor(args: {app: App, extensionPort: MessagePort}) { 602 super('main'); 603 this.app = args.app; 604 this.consumerPort = ConsumerPort.create(this.rpcImpl.bind(this)); 605 this.extensionPort = args.extensionPort; 606 } 607 608 run() { 609 // TODO(eseckler): Use ConsumerPort's QueryServiceState instead 610 // of posting a custom extension message to retrieve the category list. 611 if (this.app.state.updateChromeCategories === true) { 612 if (this.app.state.extensionInstalled) { 613 this.extensionPort.postMessage({method: 'GetCategories'}); 614 } 615 globals.dispatch(Actions.setUpdateChromeCategories({update: false})); 616 } 617 if (this.app.state.recordConfig === this.config && 618 this.app.state.recordingInProgress === this.recordingInProgress) { 619 return; 620 } 621 this.config = this.app.state.recordConfig; 622 623 const configProto = 624 genConfigProto(this.config, this.app.state.recordingTarget); 625 const configProtoText = toPbtxt(configProto); 626 const configProtoBase64 = base64Encode(configProto); 627 const commandline = ` 628 echo '${configProtoBase64}' | 629 base64 --decode | 630 adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace" && 631 adb pull /data/misc/perfetto-traces/trace /tmp/trace 632 `; 633 const traceConfig = genConfig(this.config, this.app.state.recordingTarget); 634 // TODO(hjd): This should not be TrackData after we unify the stores. 635 this.app.publish('TrackData', { 636 id: 'config', 637 data: { 638 commandline, 639 pbBase64: configProtoBase64, 640 pbtxt: configProtoText, 641 traceConfig 642 } 643 }); 644 645 // If the recordingInProgress boolean state is different, it means that we 646 // have to start or stop recording a trace. 647 if (this.app.state.recordingInProgress === this.recordingInProgress) return; 648 this.recordingInProgress = this.app.state.recordingInProgress; 649 650 if (this.recordingInProgress) { 651 this.startRecordTrace(traceConfig); 652 } else { 653 this.stopRecordTrace(); 654 } 655 } 656 657 startRecordTrace(traceConfig: TraceConfig) { 658 this.scheduleBufferUpdateRequests(); 659 this.traceBuffer = []; 660 this.consumerPort.enableTracing({traceConfig}); 661 } 662 663 stopRecordTrace() { 664 if (this.bufferUpdateInterval) clearInterval(this.bufferUpdateInterval); 665 this.consumerPort.disableTracing({}); 666 } 667 668 scheduleBufferUpdateRequests() { 669 if (this.bufferUpdateInterval) clearInterval(this.bufferUpdateInterval); 670 this.bufferUpdateInterval = setInterval(() => { 671 this.consumerPort.getTraceStats({}); 672 }, 200); 673 } 674 675 readBuffers() { 676 this.consumerPort.readBuffers({}); 677 } 678 679 onConsumerPortResponse(data: ConsumerPortResponse) { 680 if (data === undefined) return; 681 if (isReadBuffersResponse(data)) { 682 if (!data.slices || data.slices.length === 0) return; 683 // TODO(nicomazz): handle this as intended by consumer_port.proto. 684 console.assert(data.slices.length === 1); 685 if (data.slices[0].data) this.traceBuffer.push(data.slices[0].data); 686 if (data.slices[0].lastSliceForPacket) this.onTraceComplete(); 687 } else if (isEnableTracingResponse(data)) { 688 this.readBuffers(); 689 } else if (isGetTraceStatsResponse(data)) { 690 const percentage = this.getBufferUsagePercentage(data); 691 if (percentage) { 692 globals.publish('BufferUsage', {percentage}); 693 } 694 } else if (isFreeBuffersResponse(data)) { 695 // No action required. 696 } else if (isDisableTracingResponse(data)) { 697 // No action required. 698 } else { 699 console.error('Unrecognized consumer port response:', data); 700 } 701 } 702 703 onTraceComplete() { 704 this.consumerPort.freeBuffers({}); 705 globals.dispatch(Actions.setRecordingStatus({status: undefined})); 706 if (globals.state.recordingCancelled) { 707 globals.dispatch( 708 Actions.setLastRecordingError({error: 'Recording cancelled.'})); 709 this.traceBuffer = []; 710 return; 711 } 712 const trace = this.generateTrace(); 713 globals.dispatch(Actions.openTraceFromBuffer({ 714 title: 'Recorded trace', 715 buffer: trace.buffer, 716 fileName: `recorded_trace${this.recordedTraceSuffix}`, 717 })); 718 this.traceBuffer = []; 719 } 720 721 // TODO(nicomazz): stream each chunk into the trace processor, instead of 722 // creating a big long trace. 723 generateTrace() { 724 let traceLen = 0; 725 for (const chunk of this.traceBuffer) traceLen += chunk.length; 726 const completeTrace = new Uint8Array(traceLen); 727 let written = 0; 728 for (const chunk of this.traceBuffer) { 729 completeTrace.set(chunk, written); 730 written += chunk.length; 731 } 732 return completeTrace; 733 } 734 735 getBufferUsagePercentage(data: GetTraceStatsResponse): number { 736 if (!data.traceStats || !data.traceStats.bufferStats) return 0.0; 737 let maximumUsage = 0; 738 for (const buffer of data.traceStats.bufferStats) { 739 const used = buffer.bytesWritten as number; 740 const total = buffer.bufferSize as number; 741 maximumUsage = Math.max(maximumUsage, used / total); 742 } 743 return maximumUsage; 744 } 745 746 onError(message: string) { 747 console.error('Error in record controller: ', message); 748 globals.dispatch( 749 Actions.setLastRecordingError({error: message.substr(0, 150)})); 750 globals.dispatch(Actions.stopRecording({})); 751 } 752 753 onStatus(message: string) { 754 globals.dispatch(Actions.setRecordingStatus({status: message})); 755 } 756 757 // Depending on the recording target, different implementation of the 758 // consumer_port will be used. 759 // - Chrome target: This forwards the messages that have to be sent 760 // to the extension to the frontend. This is necessary because this 761 // controller is running in a separate worker, that can't directly send 762 // messages to the extension. 763 // - Android device target: WebUSB is used to communicate using the adb 764 // protocol. Actually, there is no full consumer_port implementation, but 765 // only the support to start tracing and fetch the file. 766 async getTargetController(target: RecordingTarget): Promise<RpcConsumerPort> { 767 const identifier = this.getTargetIdentifier(target); 768 769 // The reason why caching the target 'record controller' Promise is that 770 // multiple rcp calls can happen while we are trying to understand if an 771 // android device has a socket connection available or not. 772 const precedentPromise = this.controllerPromises.get(identifier); 773 if (precedentPromise) return precedentPromise; 774 775 const controllerPromise = 776 new Promise<RpcConsumerPort>(async (resolve, _) => { 777 let controller: RpcConsumerPort|undefined = undefined; 778 if (isChromeTarget(target)) { 779 controller = 780 new ChromeExtensionConsumerPort(this.extensionPort, this); 781 } else if (isAdbTarget(target)) { 782 this.onStatus(`Please allow USB debugging on device. 783 If you press cancel, reload the page.`); 784 const socketAccess = await this.hasSocketAccess(target); 785 786 controller = socketAccess ? 787 new AdbSocketConsumerPort(this.adb, this) : 788 new AdbConsumerPort(this.adb, this); 789 } else { 790 throw Error(`No device connected`); 791 } 792 793 if (!controller) throw Error(`Unknown target: ${target}`); 794 resolve(controller); 795 }); 796 797 this.controllerPromises.set(identifier, controllerPromise); 798 return controllerPromise; 799 } 800 801 private getTargetIdentifier(target: RecordingTarget): string { 802 return isAdbTarget(target) ? target.serial : target.os; 803 } 804 805 private async hasSocketAccess(target: AdbRecordingTarget) { 806 const devices = await navigator.usb.getDevices(); 807 const device = devices.find(d => d.serialNumber === target.serial); 808 console.assert(device); 809 if (!device) return Promise.resolve(false); 810 return AdbSocketConsumerPort.hasSocketAccess(device, this.adb); 811 } 812 813 private async rpcImpl( 814 method: RPCImplMethod, requestData: Uint8Array, 815 _callback: RPCImplCallback) { 816 try { 817 const state = this.app.state; 818 // TODO(hjd): This is a bit weird. We implicity send each RPC message to 819 // whichever target is currently selected (creating that target if needed) 820 // it would be nicer if the setup/teardown was more explicit. 821 const target = await this.getTargetController(state.recordingTarget); 822 this.recordedTraceSuffix = target.getRecordedTraceSuffix(); 823 target.handleCommand(method.name, requestData); 824 } catch (e) { 825 console.error(`error invoking ${method}: ${e.message}`); 826 } 827 } 828} 829