| // Copyright (C) 2026 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import {deferChunkedTask} from '../../../base/chunked_task'; |
| import {uuidv4} from '../../../base/uuid'; |
| import protos from '../../../protos'; |
| import type {App} from '../../../public/app'; |
| import {NUM, NUM_NULL, STR} from '../../../trace_processor/query_result'; |
| import {WasmEngineProxy} from '../../../trace_processor/wasm_engine_proxy'; |
| import type {AdbDevice} from '../../dev.perfetto.RecordTraceV2/adb/adb_device'; |
| import {createAdbTracingSession} from '../../dev.perfetto.RecordTraceV2/adb/adb_tracing_session'; |
| import type {TracingSession} from '../../dev.perfetto.RecordTraceV2/interfaces/tracing_session'; |
| import type {TracedWebsocketTarget} from '../../dev.perfetto.RecordTraceV2/traced_over_websocket/traced_websocket_target'; |
| import type {ConnectionResult} from '../views/connection'; |
| import {download} from '../../../base/download_utils'; |
| import {ProfileSession, type ProfileState} from './profile_session'; |
| |
| export interface ProfileView { |
| readonly pid: number; |
| readonly processName: string; |
| readonly state: ProfileState; |
| readonly bufferUsagePct?: number; |
| /** Wall-clock timestamp (ms) at which profiling started. */ |
| readonly startMs: number; |
| } |
| |
| const SNAPSHOT_INTERVAL_MS = 3_000; // How over to take a snapshot of the runnign trace and extract data. |
| const INITIAL_SNAPSHOT_INTERVAL_MS = 1_000; // Use a shorter interval for the first snapshot to get data on screen faster. |
| const POLLING_INTERVAL_MS = 1_000; // Recording config polling interval for process stats and system stats. |
| |
| let engineCounter = 0; |
| |
| export interface TsValue { |
| ts: number; |
| value: number; |
| } |
| |
| export interface LmkEvent { |
| ts: number; |
| pid: number; |
| processName: string; |
| oomScoreAdj: number; |
| } |
| |
| export interface ProcessInfo { |
| upid: number; |
| pid: number; |
| processName: string; |
| startTs: number | null; |
| debuggable: boolean; |
| } |
| |
| export interface SnapshotData { |
| // counterName → sorted array of {ts, value} |
| systemCounters: Map<string, TsValue[]>; |
| |
| // upid → counterName → sorted array of {ts, value} |
| processCountersByUpid: Map<number, Map<string, TsValue[]>>; |
| |
| // upid → metadata |
| processInfo: Map<number, ProcessInfo>; |
| |
| // LMK events sorted by ts. |
| lmkEvents: LmkEvent[]; |
| |
| // Whether the device is a userdebug build. |
| isUserDebug: boolean; |
| |
| // Trace start timestamp from trace_bounds, used as the x=0 origin for all charts. |
| ts0: number; |
| |
| // X-axis bounds in seconds relative to ts0, derived from the earliest/latest |
| // counter sample. Use these as xAxisMin/xAxisMax on all line charts. |
| xMin: number; |
| xMax: number; |
| } |
| |
| export type OnSnapshotCallback = (data: SnapshotData) => void; |
| |
| /** |
| * Connects to a device or Linux host, starts a memory tracing session, and |
| * polls snapshots on a timer. Each snapshot clones the running trace, loads |
| * it into a reusable TraceProcessor engine, extracts structured data, and |
| * notifies registered callbacks. |
| */ |
| export class LiveSession { |
| private readonly app: App; |
| private session?: TracingSession; |
| private engine?: WasmEngineProxy; |
| private readonly sessionName: string; |
| private readonly device?: AdbDevice; |
| private readonly linuxTarget?: TracedWebsocketTarget; |
| private timer?: ReturnType<typeof setTimeout>; |
| private snapshotInFlight = false; |
| private isDisposed = false; |
| private readonly onSnapshotCallbacks: OnSnapshotCallback[] = []; |
| |
| readonly deviceName: string; |
| snapshotCount = 0; |
| status = 'Starting tracing session...'; |
| isPaused = false; |
| data?: SnapshotData; |
| lastTraceBuffer?: ArrayBuffer; |
| |
| // Timing breakdown for the last snapshot (ms). |
| lastSnapshotMs = 0; |
| lastSnapshotSizeKb = 0; |
| lastBufferUsagePct?: number; |
| lastCloneMs = 0; |
| lastParseMs = 0; |
| lastQueryMs = 0; |
| lastExtractMs = 0; |
| earliestEventTs = 0; |
| |
| // True when the last snapshot took longer than the configured interval. |
| snapshotOverrun = false; |
| |
| // Active process profile (if any). Undefined when no profile is running. |
| private profileImpl?: {session: ProfileSession; startMs: number}; |
| |
| get profile(): ProfileView | undefined { |
| const p = this.profileImpl; |
| if (p === undefined) return undefined; |
| return { |
| pid: p.session.pid, |
| processName: p.session.processName, |
| state: p.session.state, |
| bufferUsagePct: p.session.bufferUsagePct, |
| startMs: p.startMs, |
| }; |
| } |
| |
| constructor(app: App, conn: ConnectionResult) { |
| this.app = app; |
| this.device = conn.device; |
| this.linuxTarget = conn.linuxTarget; |
| this.deviceName = conn.deviceName; |
| this.sessionName = `livemem-${uuidv4().substring(0, 8)}`; |
| this.startAndPoll(); |
| } |
| |
| /** Register a callback invoked after each successful snapshot. */ |
| onSnapshot(cb: OnSnapshotCallback): void { |
| this.onSnapshotCallbacks.push(cb); |
| } |
| |
| /** Pauses snapshot polling. */ |
| pause(): void { |
| if (this.isPaused) return; |
| this.isPaused = true; |
| clearTimeout(this.timer); |
| } |
| |
| /** Resumes snapshot polling. */ |
| resume(): void { |
| if (!this.isPaused) return; |
| this.isPaused = false; |
| if (this.session && !this.snapshotInFlight) { |
| this.scheduleSnapshot(); |
| } |
| } |
| |
| /** Toggles between paused and running. */ |
| togglePause(): void { |
| if (this.isPaused) { |
| this.resume(); |
| } else { |
| this.pause(); |
| } |
| } |
| |
| /** Starts a heap profiling session for a single process. */ |
| async startProfile(pid: number, processName: string): Promise<void> { |
| if (this.profileImpl) { |
| await this.profileImpl.session.cancel(); |
| } |
| const session = await ProfileSession.start( |
| this.linuxTarget ?? this.device!, |
| pid, |
| processName, |
| this.data?.xMax ?? 0, |
| ); |
| this.profileImpl = {session, startMs: Date.now()}; |
| } |
| |
| /** Stops the active profile and opens the trace in the main UI. */ |
| async stopAndOpenProfile(): Promise<void> { |
| const profile = this.profileImpl?.session; |
| if (!profile) return; |
| const processName = profile.processName; |
| const pid = profile.pid; |
| await profile.stop(); |
| const traceData = profile.getTraceData(); |
| this.profileImpl = undefined; |
| if (traceData) { |
| const fileName = `heap-${processName}-${pid}.perfetto-trace`; |
| this.app.openTraceFromBuffer({ |
| buffer: traceData, |
| title: fileName, |
| fileName, |
| }); |
| } |
| } |
| |
| /** Stops the active profile and downloads the trace file. */ |
| async stopAndDownloadProfile(): Promise<void> { |
| const profile = this.profileImpl?.session; |
| if (!profile) return; |
| const processName = profile.processName; |
| const pid = profile.pid; |
| await profile.stop(); |
| const traceData = profile.getTraceData(); |
| this.profileImpl = undefined; |
| if (traceData) { |
| const fileName = `heap-${processName}-${pid}.perfetto-trace`; |
| await download({ |
| content: new Uint8Array(traceData), |
| fileName, |
| }); |
| } |
| } |
| |
| /** Cancels the active profile and discards data. */ |
| async cancelProfile(): Promise<void> { |
| if (!this.profileImpl) return; |
| await this.profileImpl.session.cancel(); |
| this.profileImpl = undefined; |
| } |
| |
| /** Stops the tracing session, polling, and disposes of the engine. */ |
| async dispose(): Promise<void> { |
| // Mark disposed first so any in-flight poll() bails out before touching |
| // the engine again, and the finally block doesn't reschedule. |
| this.isDisposed = true; |
| clearTimeout(this.timer); |
| if (this.session) { |
| await this.session.cancel(); |
| this.session = undefined; |
| } |
| if (this.engine) { |
| this.engine[Symbol.dispose](); |
| this.engine = undefined; |
| } |
| } |
| |
| private async startAndPoll(): Promise<void> { |
| const config = createMonitoringConfig(this.sessionName); |
| const result = this.linuxTarget |
| ? await this.linuxTarget.startTracing(config) |
| : await createAdbTracingSession(this.device!, config); |
| if (!result.ok) { |
| this.status = `Failed to start: ${result.error}`; |
| return; |
| } |
| this.session = result.value; |
| this.status = 'Session started. Waiting for first snapshot...'; |
| this.scheduleSnapshot(INITIAL_SNAPSHOT_INTERVAL_MS); // Initial delay |
| } |
| |
| private scheduleSnapshot(delayMs = 0) { |
| this.timer = setTimeout(() => this.poll(), Math.max(0, delayMs)); |
| } |
| |
| private async poll(): Promise<void> { |
| if (!this.session || this.snapshotInFlight) return; |
| this.snapshotInFlight = true; |
| |
| try { |
| const t0 = performance.now(); |
| |
| const [cloneResult, bufferUsagePct] = await Promise.all([ |
| this.session.snapshot(), |
| this.session.getBufferUsagePct(), |
| ]); |
| if (!cloneResult.ok) { |
| this.status = `Snapshot failed: ${cloneResult.error}`; |
| return; |
| } |
| this.lastTraceBuffer = cloneResult.value.buffer as ArrayBuffer; |
| const tClone = performance.now(); |
| |
| // dispose() may have been called while we were awaiting the snapshot. |
| // Bail out before touching the engine — getOrCreateEngine() would |
| // otherwise resurrect a fresh one that nothing will ever dispose. |
| if (this.isDisposed) return; |
| const engine = this.getOrCreateEngine(); |
| await engine.resetTraceProcessor({ |
| tokenizeOnly: false, |
| cropTrackEvents: false, |
| ingestFtraceInRawTable: false, |
| analyzeTraceProtoContent: false, |
| ftraceDropUntilAllCpusValid: false, |
| forceFullSort: false, |
| }); |
| await engine.parse(cloneResult.value); |
| await engine.notifyEof(); |
| const tParse = performance.now(); |
| |
| const result = await extractSnapshotData(engine); |
| this.data = result.data; |
| |
| for (const cb of this.onSnapshotCallbacks) { |
| cb(this.data); |
| } |
| |
| this.lastCloneMs = Math.round(tClone - t0); |
| this.lastParseMs = Math.round(tParse - tClone); |
| this.lastQueryMs = result.queryMs; |
| this.lastExtractMs = result.extractMs; |
| this.lastSnapshotMs = Math.round(performance.now() - t0); |
| this.lastSnapshotSizeKb = this.lastTraceBuffer.byteLength / 1024; |
| this.lastBufferUsagePct = bufferUsagePct; |
| this.snapshotCount++; |
| this.status = `Snapshot #${this.snapshotCount} OK (${this.lastSnapshotMs}ms)`; |
| } catch (e) { |
| this.status = `Snapshot error: ${e}`; |
| } finally { |
| this.snapshotInFlight = false; |
| if (!this.isPaused && !this.isDisposed) { |
| const nextDelay = SNAPSHOT_INTERVAL_MS - this.lastSnapshotMs; |
| this.snapshotOverrun = nextDelay < 0; |
| this.scheduleSnapshot(nextDelay); |
| } |
| } |
| } |
| |
| private getOrCreateEngine(): WasmEngineProxy { |
| if (this.engine === undefined) { |
| this.engine = new WasmEngineProxy(`livemem-${++engineCounter}`); |
| } |
| return this.engine; |
| } |
| } |
| |
| function createMonitoringConfig( |
| uniqueSessionName: string, |
| ): protos.ITraceConfig { |
| return { |
| uniqueSessionName, |
| buffers: [ |
| { |
| name: 'initial_stats', |
| sizeKb: 4 * 1024, |
| fillPolicy: protos.TraceConfig.BufferConfig.FillPolicy.DISCARD, |
| }, |
| { |
| name: 'polled_stats', |
| sizeKb: 4 * 1024, |
| fillPolicy: protos.TraceConfig.BufferConfig.FillPolicy.RING_BUFFER, |
| }, |
| { |
| name: 'ftrace', |
| sizeKb: 4 * 1024, |
| fillPolicy: protos.TraceConfig.BufferConfig.FillPolicy.RING_BUFFER, |
| }, |
| ], |
| dataSources: [ |
| { |
| config: { |
| name: 'linux.process_stats', |
| targetBufferName: 'initial_stats', |
| processStatsConfig: { |
| scanAllProcessesOnStart: true, |
| recordProcessAge: true, |
| }, |
| }, |
| }, |
| { |
| config: { |
| name: 'android.packages_list', |
| targetBufferName: 'initial_stats', |
| }, |
| }, |
| { |
| config: { |
| name: 'linux.process_stats', |
| targetBufferName: 'polled_stats', |
| processStatsConfig: { |
| scanAllProcessesOnStart: false, |
| procStatsPollMs: POLLING_INTERVAL_MS, |
| recordProcessDmabufRss: true, |
| }, |
| }, |
| }, |
| { |
| config: { |
| name: 'linux.sys_stats', |
| targetBufferName: 'polled_stats', |
| sysStatsConfig: { |
| meminfoPeriodMs: POLLING_INTERVAL_MS, |
| psiPeriodMs: POLLING_INTERVAL_MS, |
| meminfoCounters: [ |
| protos.MeminfoCounters.MEMINFO_MEM_TOTAL, |
| protos.MeminfoCounters.MEMINFO_MEM_FREE, |
| protos.MeminfoCounters.MEMINFO_MEM_AVAILABLE, |
| protos.MeminfoCounters.MEMINFO_BUFFERS, |
| protos.MeminfoCounters.MEMINFO_CACHED, |
| protos.MeminfoCounters.MEMINFO_SWAP_TOTAL, |
| protos.MeminfoCounters.MEMINFO_SWAP_FREE, |
| protos.MeminfoCounters.MEMINFO_SWAP_CACHED, |
| protos.MeminfoCounters.MEMINFO_SHMEM, |
| protos.MeminfoCounters.MEMINFO_ACTIVE_ANON, |
| protos.MeminfoCounters.MEMINFO_INACTIVE_ANON, |
| protos.MeminfoCounters.MEMINFO_ACTIVE_FILE, |
| protos.MeminfoCounters.MEMINFO_INACTIVE_FILE, |
| protos.MeminfoCounters.MEMINFO_ANON_PAGES, |
| protos.MeminfoCounters.MEMINFO_SLAB, |
| protos.MeminfoCounters.MEMINFO_KERNEL_STACK, |
| protos.MeminfoCounters.MEMINFO_PAGE_TABLES, |
| protos.MeminfoCounters.MEMINFO_ZRAM, |
| protos.MeminfoCounters.MEMINFO_MAPPED, |
| protos.MeminfoCounters.MEMINFO_DIRTY, |
| protos.MeminfoCounters.MEMINFO_WRITEBACK, |
| ], |
| vmstatPeriodMs: POLLING_INTERVAL_MS, |
| vmstatCounters: [ |
| protos.VmstatCounters.VMSTAT_PSWPIN, |
| protos.VmstatCounters.VMSTAT_PSWPOUT, |
| protos.VmstatCounters.VMSTAT_PGFAULT, |
| protos.VmstatCounters.VMSTAT_PGMAJFAULT, |
| protos.VmstatCounters.VMSTAT_WORKINGSET_REFAULT_FILE, |
| protos.VmstatCounters.VMSTAT_PGSTEAL_FILE, |
| protos.VmstatCounters.VMSTAT_PGSCAN_FILE, |
| ], |
| }, |
| }, |
| }, |
| { |
| config: { |
| name: 'linux.ftrace', |
| targetBufferName: 'ftrace', |
| ftraceConfig: { |
| ftraceEvents: [ |
| 'dmabuf_heap/dma_heap_stat', |
| 'sched/sched_process_exit', |
| 'sched/sched_process_free', |
| 'task/task_newtask', |
| 'task/task_rename', |
| 'lowmemorykiller/lowmemory_kill', |
| // 'oom/oom_score_adj_update', |
| ], |
| atraceApps: ['lmkd'], |
| disableGenericEvents: true, |
| }, |
| }, |
| }, |
| ], |
| }; |
| } |
| |
| interface ExtractResult { |
| data: SnapshotData; |
| queryMs: number; |
| extractMs: number; |
| } |
| |
| async function extractSnapshotData( |
| engine: WasmEngineProxy, |
| ): Promise<ExtractResult> { |
| const tQueryStart = performance.now(); |
| const [ |
| ts0Result, |
| sysTrackResult, |
| procTrackResult, |
| samplesResult, |
| rateSamplesResult, |
| metaResult, |
| lmkResult, |
| buildResult, |
| ] = await Promise.all([ |
| engine.query(` |
| SELECT |
| (SELECT start_ts FROM trace_bounds) as ts0, |
| MIN(c.ts) as min_ts, |
| MAX(c.ts) as max_ts |
| FROM counter c |
| JOIN counter_track t ON c.track_id = t.id |
| WHERE t.name = 'MemTotal' |
| `), |
| engine.query(` |
| SELECT id, name |
| FROM counter_track |
| WHERE id NOT IN (SELECT id FROM process_counter_track) |
| `), |
| engine.query(` |
| SELECT t.id, p.upid, p.name AS process_name, p.pid, |
| t.name AS counter_name |
| FROM process_counter_track t |
| JOIN process p ON t.upid = p.upid |
| WHERE p.name IS NOT NULL |
| `), |
| // Level counters (meminfo, RSS, oom_score_adj, etc). Pass through as-is. |
| // Excludes psi/vmstat which are cumulative monotonic counters — those |
| // are diffed into per-second rates by the next query. |
| engine.query(` |
| SELECT c.track_id AS trackId, c.ts, c.value |
| FROM counter c |
| JOIN track t ON c.track_id = t.id |
| WHERE t.type NOT IN ('psi', 'vmstat') |
| ORDER BY c.ts |
| `), |
| // Cumulative monotonic kernel counters (psi, vmstat). Diff each track in |
| // SQL via LAG so the UI never sees the raw level. Both end up per-second: |
| // - psi: ns_stall_delta / s_dt → divide by 1e6 to get ms/s. |
| // - vmstat: count_delta / s_dt → events/s. |
| // ts is in ns, so dt_s = (ts - prev_ts) / 1e9; the algebra simplifies to |
| // the expressions below. First-of-track rows (prev_ts IS NULL) are |
| // dropped. |
| engine.query(` |
| WITH diffed AS ( |
| SELECT |
| c.track_id AS trackId, |
| c.ts, |
| t.type AS type, |
| c.value - LAG(c.value) OVER w AS dvalue, |
| c.ts - LAG(c.ts) OVER w AS dts |
| FROM counter c |
| JOIN track t ON c.track_id = t.id |
| WHERE t.type IN ('psi', 'vmstat') |
| WINDOW w AS (PARTITION BY c.track_id ORDER BY c.ts) |
| ) |
| SELECT trackId, ts, |
| MAX(0, CASE |
| WHEN type = 'psi' THEN dvalue * 1e3 / dts |
| ELSE dvalue * 1e9 / dts |
| END) AS value |
| FROM diffed |
| WHERE dts > 0 |
| ORDER BY ts |
| `), |
| engine.query(` |
| SELECT p.upid, p.name, p.pid, p.start_ts, |
| COALESCE(pkg.profileable_from_shell, 0) AS debuggable |
| FROM process p |
| LEFT JOIN package_list pkg ON p.uid = pkg.uid |
| WHERE p.name IS NOT NULL |
| `), |
| engine.query(` |
| SELECT ts, |
| CAST(str_split(s.name, ',', 1) AS INTEGER) AS pid, |
| CAST(str_split(s.name, ',', 3) AS INTEGER) AS oom_score_adj, |
| COALESCE(p.name, '') AS process_name |
| FROM slice s |
| LEFT JOIN process_track pt ON s.track_id = pt.id |
| LEFT JOIN process p ON CAST(str_split(s.name, ',', 1) AS INTEGER) = p.pid |
| AND s.ts >= COALESCE(p.start_ts, 0) |
| AND s.ts <= COALESCE(p.end_ts, (SELECT MAX(ts) FROM counter)) |
| WHERE s.name GLOB 'lmk,*' |
| UNION ALL |
| SELECT c.ts, |
| CAST(c.value AS INTEGER) AS pid, |
| 0 AS oom_score_adj, |
| COALESCE(p.name, '') AS process_name |
| FROM counter c |
| JOIN counter_track ct ON c.track_id = ct.id |
| LEFT JOIN process p ON CAST(c.value AS INTEGER) = p.pid |
| AND c.ts >= COALESCE(p.start_ts, 0) |
| AND c.ts <= COALESCE(p.end_ts, (SELECT MAX(ts) FROM counter)) |
| WHERE ct.name = 'kill_one_process' AND c.value > 0 |
| ORDER BY ts |
| `), |
| engine.query(` |
| SELECT str_value AS fingerprint |
| FROM metadata |
| WHERE name = 'android_build_fingerprint' |
| LIMIT 1 |
| `), |
| ]); |
| const tExtractStart = performance.now(); |
| const task = await deferChunkedTask({priority: 'background'}); |
| const YIELD_CHECK_INTERVAL = 64; // Num loops before yield check |
| |
| const boundsRow = ts0Result.firstRow({ts0: NUM, min_ts: NUM, max_ts: NUM}); |
| const ts0 = boundsRow?.ts0 ?? 0; |
| const xMin = boundsRow !== undefined ? (boundsRow.min_ts - ts0) / 1e9 : 0; |
| const xMax = boundsRow !== undefined ? (boundsRow.max_ts - ts0) / 1e9 : 0; |
| |
| // Build track id → counter name map for system counters. |
| const sysTrackMap = new Map<number, string>(); |
| const sysTrackIter = sysTrackResult.iter({id: NUM, name: STR}); |
| for (; sysTrackIter.valid(); sysTrackIter.next()) { |
| sysTrackMap.set(sysTrackIter.id, sysTrackIter.name); |
| } |
| |
| // Build track id → {upid, processName, pid, counterName} for process counters. |
| const procTrackMap = new Map< |
| number, |
| {upid: number; processName: string; pid: number; counterName: string} |
| >(); |
| const procTrackIter = procTrackResult.iter({ |
| id: NUM, |
| upid: NUM, |
| process_name: STR, |
| pid: NUM, |
| counter_name: STR, |
| }); |
| for (; procTrackIter.valid(); procTrackIter.next()) { |
| procTrackMap.set(procTrackIter.id, { |
| upid: procTrackIter.upid, |
| processName: procTrackIter.process_name, |
| pid: procTrackIter.pid, |
| counterName: procTrackIter.counter_name, |
| }); |
| } |
| |
| // Single pass over all counter samples, dispatching by track type. |
| const systemCounters = new Map<string, TsValue[]>(); |
| const processCountersByUpid = new Map<number, Map<string, TsValue[]>>(); |
| const samplesIter = samplesResult.iter({trackId: NUM, ts: NUM, value: NUM}); |
| for (let i = 0; samplesIter.valid(); samplesIter.next(), ++i) { |
| if (i % YIELD_CHECK_INTERVAL === 0 && task.shouldYield()) { |
| await task.yield(); |
| } |
| const {trackId, ts, value} = samplesIter; |
| |
| const sysName = sysTrackMap.get(trackId); |
| if (sysName !== undefined) { |
| let arr = systemCounters.get(sysName); |
| if (arr === undefined) { |
| arr = []; |
| systemCounters.set(sysName, arr); |
| } |
| arr.push({ts, value}); |
| continue; |
| } |
| |
| const proc = procTrackMap.get(trackId); |
| if (proc !== undefined) { |
| let counters = processCountersByUpid.get(proc.upid); |
| if (counters === undefined) { |
| counters = new Map(); |
| processCountersByUpid.set(proc.upid, counters); |
| } |
| let arr = counters.get(proc.counterName); |
| if (arr === undefined) { |
| arr = []; |
| counters.set(proc.counterName, arr); |
| } |
| arr.push({ts, value}); |
| } |
| } |
| |
| // Rate-converted psi/vmstat samples land in systemCounters under the same |
| // counter names as the levels would have — the UI just sees per-second |
| // rates (ms/s for psi, events/s for vmstat). |
| const rateIter = rateSamplesResult.iter({trackId: NUM, ts: NUM, value: NUM}); |
| for (let i = 0; rateIter.valid(); rateIter.next(), ++i) { |
| if (i % YIELD_CHECK_INTERVAL === 0 && task.shouldYield()) { |
| await task.yield(); |
| } |
| const {trackId, ts, value} = rateIter; |
| const sysName = sysTrackMap.get(trackId); |
| if (sysName === undefined) continue; |
| let arr = systemCounters.get(sysName); |
| if (arr === undefined) { |
| arr = []; |
| systemCounters.set(sysName, arr); |
| } |
| arr.push({ts, value}); |
| } |
| |
| // Build process metadata, keyed by upid. |
| const processInfo = new Map<number, ProcessInfo>(); |
| const metaIter = metaResult.iter({ |
| upid: NUM, |
| name: STR, |
| pid: NUM, |
| start_ts: NUM_NULL, |
| debuggable: NUM, |
| }); |
| for (let i = 0; metaIter.valid(); metaIter.next(), ++i) { |
| if (i % YIELD_CHECK_INTERVAL === 0 && task.shouldYield()) { |
| await task.yield(); |
| } |
| processInfo.set(metaIter.upid, { |
| upid: metaIter.upid, |
| pid: metaIter.pid, |
| processName: metaIter.name, |
| startTs: metaIter.start_ts, |
| debuggable: metaIter.debuggable !== 0, |
| }); |
| } |
| |
| // Build LMK events. |
| const lmkEvents: LmkEvent[] = []; |
| const lmkIter = lmkResult.iter({ |
| ts: NUM, |
| pid: NUM, |
| oom_score_adj: NUM, |
| process_name: STR, |
| }); |
| for (let i = 0; lmkIter.valid(); lmkIter.next(), ++i) { |
| if (i % YIELD_CHECK_INTERVAL === 0 && task.shouldYield()) { |
| await task.yield(); |
| } |
| lmkEvents.push({ |
| ts: lmkIter.ts, |
| pid: lmkIter.pid, |
| processName: lmkIter.process_name, |
| oomScoreAdj: lmkIter.oom_score_adj, |
| }); |
| } |
| |
| // Check if device is userdebug. |
| const buildRow = buildResult.maybeFirstRow({fingerprint: STR}); |
| const isUserDebug = buildRow?.fingerprint?.includes('userdebug') ?? false; |
| |
| return { |
| data: { |
| systemCounters, |
| processCountersByUpid, |
| processInfo, |
| lmkEvents, |
| isUserDebug, |
| ts0, |
| xMin, |
| xMax, |
| }, |
| queryMs: Math.round(tExtractStart - tQueryStart), |
| extractMs: Math.round(performance.now() - tExtractStart), |
| }; |
| } |