Add slow start insights track Change-Id: I797f65ca6092edccc98a441d5879b648c19438bd
diff --git a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts index 38279a1..22bc3f3 100644 --- a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts +++ b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
@@ -18,6 +18,7 @@ import {createQuerySliceTrack} from '../../components/tracks/query_slice_track'; import {TrackNode} from '../../public/workspace'; import {optimizationsTrack} from './optimizations'; +import {slowInsightsTrack} from './slow_insights'; export default class implements PerfettoPlugin { static readonly id = 'dev.perfetto.AndroidStartup'; @@ -64,6 +65,13 @@ if (!!optimizations) { trackNode.addChildLast(optimizations); } + // TODO(haiping): get the deeplink reason ID and package name params. + const reasonId = 0; + const pkgName = ''; + const slowInsights = await slowInsightsTrack(ctx, reasonId, pkgName); + if (!!slowInsights) { + trackNode.addChildLast(slowInsights); + } } private async loadStartupTrack(
diff --git a/ui/src/plugins/dev.perfetto.AndroidStartup/slow_insights.ts b/ui/src/plugins/dev.perfetto.AndroidStartup/slow_insights.ts new file mode 100644 index 0000000..568141f --- /dev/null +++ b/ui/src/plugins/dev.perfetto.AndroidStartup/slow_insights.ts
@@ -0,0 +1,831 @@ +// Copyright (C) 2025 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 {Trace} from '../../public/trace'; +import {STR, LONG, NUM, QueryResult} from '../../trace_processor/query_result'; +import {createQuerySliceTrack} from '../../components/tracks/query_slice_track'; +import {TrackNode} from '../../public/workspace'; + +// The container that keeps track of startups. +interface Startup { + // The startup id. + id: number; + // The package name. + package: string; + // Time start + ts: bigint; + // Time end + ts_end: bigint; +} + +// The slices for slow start reasons. +interface Slice { + // Time start + ts: bigint; + // Duration + dur: bigint; + // Sice name + name : string; +} + +// The log tag +const tag = 'SlowStartInsights'; + +const enum ReasonId { + REASON_ID_UNSPECIFIED = 0, + NO_BASELINE_OR_CLOUD_PROFILES = 1, + RUN_FROM_APK = 2, + UNLOCK_RUNNING = 3, + APP_IN_DEBUGGABLE_MODE = 4, + GC_ACTIVITY = 5, + DEX2OAT_RUNNING = 6, + INSTALLD_RUNNING = 7, + MAIN_THREAD_TIME_SPENT_IN_RUNNABLE = 8, + MAIN_THREAD_TIME_SPENT_IN_INTERRUPTIBLE_SLEEP = 9, + MAIN_THREAD_TIME_SPENT_IN_BLOCKING_IO = 10, + MAIN_THREAD_TIME_SPENT_IN_OPEN_DEX_FILES_FROM_OAT = 11, + TIME_SPENT_IN_BIND_APPLICATION = 12, + TIME_SPENT_IN_VIEW_INFLATION = 13, + TIME_SPENT_IN_RESOURCES_MANAGER_GET_RESOURCES = 14, + TIME_SPENT_VERIFYING_CLASSES = 15, + POTENTIAL_CPU_CONTENTION_WITH_ANOTHER_PROCESS = 16, + JIT_ACTIVITY = 17, + MAIN_THREAD_LOCK_CONTENTION = 18, + MAIN_THREAD_MONITOR_CONTENTION = 19, + JIT_COMPILED_METHODS = 20, + BROADCAST_DISPATCHED_COUNT = 21, + BROADCAST_RECEIVED_COUNT = 22, + STARTUP_RUNNING_CONCURRENT = 23, + MAIN_THREAD_BINDER_TRANSCATIONS_BLOCKED = 24, +} + +/** + * Returns a track node that contains slow start insights + * for the packages that started up in a trace. + * @param trace The loaded trace. + * @returns a track node with the slow start insights. + * `undefined` if there are no app startups detected. + */ +export async function slowInsightsTrack( + trace: Trace, + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + const startups: Array<Startup> = []; + + // Find app startups + let result = await trace.engine.query( + `SELECT startup_id AS id, package, ts, ts_end FROM android_startups;`, + tag, + ); + + const it = result.iter({id: NUM, package: STR, ts: LONG, ts_end: LONG}); + for (; it.valid(); it.next()) { + startups.push({ + id: it.id, + package: it.package, + ts: it.ts, + ts_end: it.ts_end, + }); + } + + if (startups.length === 0) { + // Nothing interesting to report. + return undefined; + } + + const uri = '/android_startups_slow_insights'; + const title = 'Slow Startup Insights'; + const track = await createQuerySliceTrack({ + trace: trace, + uri: uri, + data: { + sqlSource: ` + SELECT + ts, + ts_end - ts as dur, + package as name + FROM android_startups`, + columns: ['ts', 'dur', 'name'], + }, + }); + trace.tracks.registerTrack({ + uri, + title, + track, + }); + const trackNode = new TrackNode({title, uri}); + addChildTrack( + await getProcessDebuggableTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getBaselineProfileTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getRunFromApkTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getUnlockRunningTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getGcActivityTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getMainThreadBinderTransactionsTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getBroadcastDispatchedTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getBroadcastReceivedTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getOpenDexFilesFromOatTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getVerifyClassTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getLockContentionTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getMonitorContentionTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getBindApplicationTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getViewInflationTrack(trace, startups, reasonId, pkgName), trackNode); + addChildTrack( + await getResourcesManagerTrack(trace, startups, reasonId, pkgName), trackNode); + return trackNode; +} + +function addChildTrack( + childNode: TrackNode | undefined, + trackNode: TrackNode, +): void { + if (!!childNode) { + trackNode.addChildLast(childNode); + } +} + +function addResultToSlices( + result: QueryResult, + slices: Slice[], +): void { + const it = result.iter({ts: LONG, dur: LONG, name: STR}); + for (; it.valid(); it.next()) { + slices.push({ + ts: it.ts, + dur: it.dur, + name: it.name, + }); + } +} + +async function getUnlockRunningTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName:string, +): Promise<TrackNode | undefined> { + const slices: Array<Slice> = []; + + // The log tag + const tag = 'UnlockRunning'; + let sliceId = 0; + let selected = false; + const curReasonId = ReasonId.UNLOCK_RUNNING; + for (const startup of startups) { + let result = await trace.engine.query( + ` + SELECT slice.ts AS ts, slice.dur AS dur, slice.name AS name + FROM slice, android_startups launches + JOIN thread_track ON slice.track_id = thread_track.id + JOIN thread USING(utid) + JOIN process USING(upid) + WHERE launches.startup_id = ${startup.id} + AND slice.name = "KeyguardUpdateMonitor#onAuthenticationSucceeded" + AND process.name = "com.android.systemui" + AND slice.ts >= launches.ts + AND (slice.ts + slice.dur) <= launches.ts_end + LIMIT 3`, + tag, + ); + + if (!result) { + continue; + } + if (!selected && pkgName === startup.package && reasonId === curReasonId) { + selected = true; + sliceId = slices.length; + } + addResultToSlices(result, slices); + } + + const uri = `/android_startups/unlock_running`; + const title = `Unlock Running`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, selected, sliceId); +} + +async function getProcessDebuggableTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + const slices: Array<Slice> = []; + + // The log tag + const tag = 'ProcessDebuggable'; + let sliceId = 0; + let selected = false; + const curReasonId = ReasonId.APP_IN_DEBUGGABLE_MODE; + for (const startup of startups) { + const result = await trace.engine.query( + ` + SELECT ts, ts_end - ts AS dur, package AS name + FROM android_startups launch + JOIN android_process_metadata p ON p.process_name = launch.package + WHERE launch.startup_id = ${startup.id} + AND p.debuggable + LIMIT 1`, + tag, + ); + + if (!result) { + continue; + } + if (!selected && pkgName === startup.package && reasonId === curReasonId) { + selected = true; + sliceId = slices.length; + } + addResultToSlices(result, slices); + } + + const uri = `/android_startups/process_duggable`; + const title = `Debuggable Process`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, selected, sliceId); +} + +async function getBaselineProfileTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + const slices: Array<Slice> = []; + + // The log tag + const tag = 'MissingBaseLineProfile'; + let sliceId = 0; + let selected = false; + const curReasonId = ReasonId.NO_BASELINE_OR_CLOUD_PROFILES; + for (const startup of startups) { + let result = await trace.engine.query( + ` + SELECT slice_ts AS ts, slice_dur AS dur, slice_name AS name + FROM ANDROID_SLICES_FOR_STARTUP_AND_SLICE_NAME(${startup.id}, + "location=* status=* filter=* reason=*") + WHERE + -- when location is the package odex file and the reason is "install" or "install-dm", + -- if the compilation filter is not "speed-profile", baseline/cloud profile is missing. + SUBSTR(STR_SPLIT(slice_name, " status=", 0), LENGTH("location=") + 1) + GLOB ("*" || $pkg_name || "*odex") + AND (STR_SPLIT(slice_name, " reason=", 1) = "install" + OR STR_SPLIT(slice_name, " reason=", 1) = "install-dm") + ORDER BY slice_dur DESC + LIMIT 1`, + tag, + ); + + if (!result) { + continue; + } + if (!selected && pkgName === startup.package && reasonId === curReasonId) { + selected = true; + sliceId = slices.length; + } + addResultToSlices(result, slices); + } + + const uri = `/android_startups/missing_baseline_profile`; + const title = `Missing Baseline Profile`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, selected, sliceId); +} + +async function getRunFromApkTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + const slices: Array<Slice> = []; + + // The log tag + const tag = 'RunFromApk'; + let sliceId = 0; + let selected = false; + const curReasonId = ReasonId.RUN_FROM_APK; + for (const startup of startups) { + let result = await trace.engine.query( + ` + SELECT slice_ts AS ts, slice_dur AS dur, slice_name AS name + FROM android_thread_slices_for_all_startups l + WHERE + l.startup_id = ${startup.id} AND is_main_thread AND + slice_name GLOB "location=* status=* filter=* reason=*" AND + STR_SPLIT(STR_SPLIT(slice_name, " filter=", 1), " reason=", 0) + GLOB ("*" || "run-from-apk" || "*") + ORDER BY slice_dur DESC + LIMIT 3`, + tag, + ); + + if (!result) { + continue; + } + if (!selected && pkgName === startup.package && reasonId === curReasonId) { + selected = true; + sliceId = slices.length; + } + addResultToSlices(result, slices); + } + + const uri = `/android_startups/run_from_apk`; + const title = `Run From APK`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, selected, sliceId); +} + +async function getGcActivityTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + const slices: Array<Slice> = []; + + // The log tag + const tag = 'GcActivity'; + let sliceId = 0; + let selected = false; + const curReasonId = ReasonId.GC_ACTIVITY; + for (const startup of startups) { + let result = await trace.engine.query( + ` + SELECT slice_ts AS ts, slice_dur as dur, slice_name AS name + FROM android_thread_slices_for_all_startups slice + WHERE + slice.startup_id = ${startup.id} AND + ( + slice_name GLOB "*semispace GC" OR + slice_name GLOB "*mark sweep GC" OR + slice_name GLOB "*concurrent copying GC" + ) + ORDER BY slice_dur DESC + LIMIT 3`, + tag, + ); + + if (!result) { + continue; + } + if (!selected && pkgName === startup.package && reasonId === curReasonId) { + selected = true; + sliceId = slices.length; + } + addResultToSlices(result, slices); + } + + const uri = `/android_startups/gc_activity`; + const title = `GC Activity`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, selected, sliceId); +} + +async function getMainThreadBinderTransactionsTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + const slices: Array<Slice> = []; + + // The log tag + const tag = 'MainThreadBinderTransactions'; + let sliceId = 0; + let selected = false; + const curReasonId = ReasonId.MAIN_THREAD_BINDER_TRANSCATIONS_BLOCKED; + for (const startup of startups) { + let threshold = 2e7; + let result = await trace.engine.query( + ` + SELECT request.slice_ts as ts, request.slice_dur as dur, + request.slice_name as name + FROM ( + SELECT slice_id as id, slice_dur, is_main_thread, + slice_ts, s.utid, slice_name + FROM android_thread_slices_for_all_startups s + JOIN process ON ( + EXTRACT_ARG(s.arg_set_id, "destination process") = process.pid + ) + WHERE s.startup_id = ${startup.id} AND slice_name GLOB "binder transaction" + AND slice_dur > ${threshold} + ) request + JOIN following_flow(request.id) arrow + JOIN slice reply ON reply.id = arrow.slice_in + JOIN thread USING (utid) + WHERE reply.dur > ${threshold} AND request.is_main_thread + LIMIT 1`, + tag, + ); + + if (!result) { + continue; + } + if (!selected && pkgName === startup.package && reasonId === curReasonId) { + selected = true; + sliceId = slices.length; + } + addResultToSlices(result, slices); + } + + const uri = `/android_startups/main_thread_binder_transactions_blocked`; + const title = `Main Thread Binder Transactions Blocked`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, selected, sliceId); +} + +async function getBroadcastDispatchedTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + // The log tag + const tag = 'Broadcast dispatched'; + const sliceGlob = `'Broadcast dispatched*'`; + const slices: Array<Slice> = []; + const curReasonId = ReasonId.BROADCAST_DISPATCHED_COUNT; + const thresholdCnt = 15; + + const sliceId = + await getBroadcastSlices(trace, tag, sliceGlob, startups, slices, + reasonId, pkgName, curReasonId, thresholdCnt); + + const uri = `/android_startups/broadcast_dispatched`; + const title = `Broadcast Dispatched`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, sliceId >= 0, sliceId); +} + +async function getBroadcastReceivedTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + // The log tag + const tag = 'Broadcast received'; + const sliceGlob = `'broadcastReceiveReg*'`; + const slices: Array<Slice> = []; + const curReasonId = ReasonId.BROADCAST_RECEIVED_COUNT; + const thresholdCnt = 50; + + const sliceId = + await getBroadcastSlices(trace, tag, sliceGlob, startups, slices, + reasonId, pkgName, curReasonId, thresholdCnt); + + const uri = `/android_startups/broadcast_received`; + const title = `Number of Broadcast Received`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, sliceId >= 0, sliceId); +} + +async function getBroadcastSlices( + trace: Trace, + tag: string, + sliceGlob: string, + startups: Startup[], + slices: Slice[], + reasonId: number, + pkgName: string, + curReasonId: number, + thresholdCnt: number, +): Promise<number> { + let sliceId = -1; + let selected = false; + for (const startup of startups) { + let result = await trace.engine.query( + ` + SELECT COUNT(*) as count + FROM slice s + JOIN thread_track t ON s.track_id = t.id + JOIN thread USING(utid) + JOIN ( + SELECT ts, ts_end + FROM android_startups + WHERE startup_id = ${startup.id} + ) launch + WHERE + s.name GLOB ${sliceGlob} AND + s.ts BETWEEN launch.ts AND launch.ts_end`, + tag, + ); + + if (!result || result.iter({count:NUM}).count < thresholdCnt){ + continue; + } + if (!selected && pkgName === startup.package && reasonId === curReasonId) { + selected = true; + sliceId = slices.length; + } + slices.push({ + ts: startup.ts, + dur: startup.ts_end - startup.ts, + name: startup.package + }); + } + return sliceId; +} + +async function getOpenDexFilesFromOatTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + // The log tag + const tag = 'OpenDexFilesFromOat'; + const sliceGlob = `'OpenDexFilesFromOat*'`; + const thresholdPct = 20; + const curReasonId = ReasonId.MAIN_THREAD_TIME_SPENT_IN_OPEN_DEX_FILES_FROM_OAT; + const slices: Array<Slice> = []; + + const sliceId = + await getSlicesByName(trace, tag, sliceGlob, 0, thresholdPct, startups, + slices, reasonId, pkgName, curReasonId, true); + + const uri = `/android_startups/open_dex_files_from_oat`; + const title = `Open Dex Files From Oat`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, sliceId >= 0, sliceId); +} + +async function getVerifyClassTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + // The log tag + const tag = 'VerifyClass'; + const sliceGlob = `'VerifyClass*'`; + const thresholdPct = 15; + const curReasonId = ReasonId.TIME_SPENT_VERIFYING_CLASSES; + const slices: Array<Slice> = []; + + const sliceId = + await getSlicesByName(trace, tag, sliceGlob, 0, thresholdPct, startups, + slices, reasonId, pkgName, curReasonId, false); + + const uri = `/android_startups/verify_class`; + const title = `Verify Class`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, sliceId >= 0, sliceId); +} + +async function getLockContentionTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + // The log tag + const tag = 'LockContention'; + let sliceGlob = `'Lock contention on*'`; + let thresholdPct = 20; + const curReasonId = ReasonId.MAIN_THREAD_LOCK_CONTENTION; + const slices: Array<Slice> = []; + + const sliceId = + await getSlicesByName(trace, tag, sliceGlob, 0, thresholdPct, startups, + slices, reasonId, pkgName, curReasonId, true); + + const uri = `/android_startups/lock_contention`; + const title = `Lock Contention`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, sliceId >= 0, sliceId); +} + +async function getMonitorContentionTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + // The log tag + const tag = 'MonitorContention'; + const sliceGlob = `'Lock contention on a monitor*'`; + const thresholdPct = 15; + const curReasonId = ReasonId.MAIN_THREAD_MONITOR_CONTENTION; + const slices: Array<Slice> = []; + + const sliceId = + await getSlicesByName(trace, tag, sliceGlob, 0, thresholdPct, startups, + slices, reasonId, pkgName, curReasonId, true); + + const uri = `/android_startups/monitor_contention`; + const title = `Monitor Contention`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, sliceId >= 0, sliceId); +} + +async function getBindApplicationTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + // The log tag + const tag = 'BindApplication'; + const sliceGlob = `'bindApplication'`; + const thresholdNs = 1250000000; + const curReasonId = ReasonId.TIME_SPENT_IN_BIND_APPLICATION; + const slices: Array<Slice> = []; + + const sliceId = + await getSlicesByName(trace, tag, sliceGlob, thresholdNs, 0, startups, + slices, reasonId, pkgName, curReasonId, false); + + const uri = `/android_startups/bind_application`; + const title = `Bind Application`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, sliceId >= 0, sliceId); +} + +async function getViewInflationTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + // The log tag + const tag = 'ViewInflation'; + const sliceGlob = `'inflate'`; + const thresholdNs = 450000000; + const curReasonId = ReasonId.TIME_SPENT_IN_VIEW_INFLATION; + const slices: Array<Slice> = []; + + const sliceId = + await getSlicesByName(trace, tag, sliceGlob, thresholdNs, 0, startups, + slices, reasonId, pkgName, curReasonId, false); + + const uri = `/android_startups/view_inflation`; + const title = `View Inflation`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, sliceId >= 0, sliceId); +} + +async function getResourcesManagerTrack( + trace: Trace, + startups: Startup[], + reasonId: number, + pkgName: string, +): Promise<TrackNode | undefined> { + // The log tag + const tag = 'getResources'; + const sliceGlob = `'ResourcesManager#getResources'`; + const thresholdNs = 130000000; + const curReasonId = ReasonId.TIME_SPENT_IN_RESOURCES_MANAGER_GET_RESOURCES; + const slices: Array<Slice> = []; + + const sliceId = + await getSlicesByName(trace, tag, sliceGlob, thresholdNs, 0, startups, + slices, reasonId, pkgName, curReasonId, false); + + const uri = `/android_startups/get_resources`; + const title = `Get Resources`; + return await + getSlowReasonTrackAndSelect(trace, slices, uri, title, sliceId >= 0, sliceId); +} + +async function getSlicesByName( + trace: Trace, + tag: string, + sliceGlob: string, + thresholdNs: number, + thresholdPct: number, + startups: Startup[], + slices: Slice[], + reasonId: number, + pkgName: string, + curReasonId: number, + mainThreadOnly: boolean, +): Promise<number> { + let sliceId = -1; + let selected = false; + const numSlices = 3; + for (const startup of startups) { + let result = await trace.engine.query( + ` + SELECT slice_ts AS ts, slice_dur AS dur, slice_name AS name, is_main_thread as isMainThread + FROM android_thread_slices_for_all_startups l + WHERE l.startup_id = ${startup.id} + AND slice_name GLOB ${sliceGlob} + ORDER BY slice_dur DESC + `, + tag, + ); + + let it = result.iter({ts: LONG, dur: LONG, name: STR, isMainThread: NUM}); + let sum = 0; + for (; it.valid(); it.next()) { + if (mainThreadOnly && it.isMainThread == 0) { + continue; + } + sum += Number(it.dur); + } + if (thresholdPct <= 0 || sum < (startup.ts_end - startup.ts) / BigInt(100) * BigInt(thresholdPct) + &&(thresholdNs <= 0 || sum < thresholdNs)) { + continue; + } + if (!selected && pkgName === startup.package && reasonId == curReasonId) { + selected = true; + sliceId = slices.length; + } + let count = 0; + it = result.iter({ts: LONG, dur: LONG, name: STR, isMainThread: NUM}); + for (; it.valid() && count < numSlices; it.next()) { + if (mainThreadOnly && it.isMainThread == 0) { + continue; + } + slices.push({ + ts: it.ts, + dur: it.dur, + name: it.name, + }); + count++; + } + } + return sliceId; +} + +async function getSlowReasonTrack( + trace: Trace, + slices: Slice[], + uri: string, + title: string, +): Promise<TrackNode | undefined> { + if (slices.length !== 0) { + const sqlSource = slices + .map((slice) => { + return `SELECT + ${slice.ts} AS ts, + ${slice.dur} AS dur, + '${slice.name}' AS name + `; + }) + .join('UNION ALL '); + + const track = await createQuerySliceTrack({ + trace: trace, + uri: uri, + data: { + sqlSource: sqlSource, + columns: ['ts', 'dur', 'name'], + }, + }); + trace.tracks.registerTrack({ + uri, + title, + track, + }); + return new TrackNode({title, uri}); + } + + return undefined; +} + +async function getSlowReasonTrackAndSelect( + trace: Trace, + slices: Slice[], + uri: string, + title: string, + selected: boolean, + sliceId: number, +): Promise<TrackNode | undefined> { + const trackNode = await getSlowReasonTrack(trace, slices, uri, title); + if (!trackNode) { + return undefined; + } + if (selected) { + trace.selection.selectTrackEvent(uri, sliceId, {scrollToSelection: true,}); + } + return trackNode; +} + +