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