Merge "ui: Don't show reload toast"
diff --git a/src/base/paged_memory_unittest.cc b/src/base/paged_memory_unittest.cc
index 8a94941..fd2790d 100644
--- a/src/base/paged_memory_unittest.cc
+++ b/src/base/paged_memory_unittest.cc
@@ -164,7 +164,7 @@
#endif // ADDRESS_SANITIZER
TEST(PagedMemoryTest, GuardRegions) {
- const size_t kSize = 4096;
+ const size_t kSize = GetSysPageSize();
PagedMemory mem = PagedMemory::Allocate(kSize);
ASSERT_TRUE(mem.IsValid());
volatile char* raw = reinterpret_cast<char*>(mem.Get());
diff --git a/src/ipc/buffered_frame_deserializer.cc b/src/ipc/buffered_frame_deserializer.cc
index 94d5a82..d893dfb 100644
--- a/src/ipc/buffered_frame_deserializer.cc
+++ b/src/ipc/buffered_frame_deserializer.cc
@@ -39,7 +39,7 @@
BufferedFrameDeserializer::BufferedFrameDeserializer(size_t max_capacity)
: capacity_(max_capacity) {
PERFETTO_CHECK(max_capacity % base::GetSysPageSize() == 0);
- PERFETTO_CHECK(max_capacity > base::GetSysPageSize());
+ PERFETTO_CHECK(max_capacity >= base::GetSysPageSize());
}
BufferedFrameDeserializer::~BufferedFrameDeserializer() = default;
diff --git a/src/trace_processor/export_json.cc b/src/trace_processor/export_json.cc
index 5dfac9d..560d477 100644
--- a/src/trace_processor/export_json.cc
+++ b/src/trace_processor/export_json.cc
@@ -984,6 +984,7 @@
SliceId slice_id,
std::string name,
std::string cat,
+ Json::Value args,
bool flow_begin) {
const auto& slices = storage_->slice_table();
const auto& thread_tracks = storage_->thread_track_table();
@@ -1012,6 +1013,7 @@
if (!flow_begin) {
event["bp"] = "e";
}
+ event["args"] = std::move(args);
return std::move(event);
}
@@ -1025,10 +1027,14 @@
std::string cat;
std::string name;
+ auto args = args_builder_.GetArgs(arg_set_id);
if (arg_set_id != kInvalidArgSetId) {
- auto args = args_builder_.GetArgs(arg_set_id);
cat = args["cat"].asString();
name = args["name"].asString();
+ // Don't export these args since they are only used for this export and
+ // weren't part of the original event.
+ args.removeMember("name");
+ args.removeMember("cat");
} else {
auto opt_slice_out_idx = slice_table.id().IndexOf(slice_out);
PERFETTO_DCHECK(opt_slice_out_idx.has_value());
@@ -1038,10 +1044,10 @@
name = GetNonNullString(storage_, name_id);
}
- auto out_event =
- CreateFlowEventV1(i, slice_out, name, cat, /* flow_begin = */ true);
- auto in_event =
- CreateFlowEventV1(i, slice_in, name, cat, /* flow_begin = */ false);
+ auto out_event = CreateFlowEventV1(i, slice_out, name, cat, args,
+ /* flow_begin = */ true);
+ auto in_event = CreateFlowEventV1(i, slice_in, name, cat, std::move(args),
+ /* flow_begin = */ false);
if (out_event && in_event) {
writer_.WriteCommonEvent(out_event.value());
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index d4f55d0..5ca9bbe 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -152,6 +152,17 @@
PERFETTO_ELOG("Error initializing: %s", error);
sqlite3_free(error);
}
+ // This is a table intended to be used for metric debugging/developing. Data
+ // in the table is shown specially in the UI, and users can insert rows into
+ // this table to draw more things.
+ sqlite3_exec(db,
+ "CREATE TABLE debug_slices (id BIG INT, name STRING, ts BIG INT,"
+ "dur BIG INT, depth BIG INT)",
+ 0, 0, &error);
+ if (error) {
+ PERFETTO_ELOG("Error initializing: %s", error);
+ sqlite3_free(error);
+ }
// Initialize the bounds table with some data so even before parsing any data,
// we still have a valid table.
diff --git a/tools/busy_threads/busy_threads.cc b/tools/busy_threads/busy_threads.cc
index 72b13dc..10f57f6 100644
--- a/tools/busy_threads/busy_threads.cc
+++ b/tools/busy_threads/busy_threads.cc
@@ -23,6 +23,7 @@
#include "perfetto/base/logging.h"
#include "perfetto/base/time.h"
+#include "perfetto/ext/base/scoped_file.h"
#define PERFETTO_HAVE_PTHREADS \
(PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
@@ -51,12 +52,13 @@
void PrintUsage(const char* bin_name) {
#if PERFETTO_HAVE_PTHREADS
PERFETTO_ELOG(
- "Usage: %s --threads=N --period_us=N --duty_cycle=[1-100] "
+ "Usage: %s [--background] --threads=N --period_us=N --duty_cycle=[1-100] "
"[--thread_names=N]",
bin_name);
#else
- PERFETTO_ELOG("Usage: %s --threads=N --period_us=N --duty_cycle=[1-100]",
- bin_name);
+ PERFETTO_ELOG(
+ "Usage: %s [--background] --threads=N --period_us=N --duty_cycle=[1-100]",
+ bin_name);
#endif
}
@@ -92,15 +94,17 @@
}
int BusyThreadsMain(int argc, char** argv) {
+ bool background = false;
int64_t num_threads = -1;
int64_t period_us = -1;
int64_t duty_cycle = -1;
uint32_t thread_name_count = 0;
static struct option long_options[] = {
+ {"background", no_argument, nullptr, 'd'},
{"threads", required_argument, nullptr, 't'},
{"period_us", required_argument, nullptr, 'p'},
- {"duty_cycle", required_argument, nullptr, 'd'},
+ {"duty_cycle", required_argument, nullptr, 'c'},
#if PERFETTO_HAVE_PTHREADS
{"thread_names", required_argument, nullptr, 'r'},
#endif
@@ -110,13 +114,16 @@
int c;
while ((c = getopt_long(argc, argv, "", long_options, &option_index)) != -1) {
switch (c) {
+ case 'd':
+ background = true;
+ break;
case 't':
num_threads = atol(optarg);
break;
case 'p':
period_us = atol(optarg);
break;
- case 'd':
+ case 'c':
duty_cycle = atol(optarg);
break;
#if PERFETTO_HAVE_PTHREADS
@@ -134,6 +141,30 @@
return 1;
}
+ if (background) {
+ pid_t pid;
+ switch (pid = fork()) {
+ case -1:
+ PERFETTO_FATAL("fork");
+ case 0: {
+ PERFETTO_CHECK(setsid() != -1);
+ base::ignore_result(chdir("/"));
+ base::ScopedFile null = base::OpenFile("/dev/null", O_RDONLY);
+ PERFETTO_CHECK(null);
+ PERFETTO_CHECK(dup2(*null, STDIN_FILENO) != -1);
+ PERFETTO_CHECK(dup2(*null, STDOUT_FILENO) != -1);
+ PERFETTO_CHECK(dup2(*null, STDERR_FILENO) != -1);
+ // Do not accidentally close stdin/stdout/stderr.
+ if (*null <= 2)
+ null.release();
+ break;
+ }
+ default:
+ printf("%d\n", pid);
+ exit(0);
+ }
+ }
+
int64_t busy_us =
static_cast<int64_t>(static_cast<double>(period_us) *
(static_cast<double>(duty_cycle) / 100.0));
diff --git a/ui/index.html b/ui/index.html
index 70c765a..b214653 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -4,7 +4,8 @@
<title>Perfetto UI</title>
<!-- See b/149573396 for CSP rationale. -->
<!-- TODO(b/121211019): remove script-src-elem rule once fixed. -->
- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src-elem 'self' https://*.google.com https://*.googleusercontent.com https://www.googletagmanager.com https://www.google-analytics.com 'sha256-eYlPNiizBKy/rhHAaz06RXrXVsKmBN6tTFYwmJTvcwc='; object-src 'none'; connect-src 'self' http://127.0.0.1:9001 https://www.google-analytics.com https://*.googleapis.com; img-src 'self' https://www.google-analytics.com; navigate-to https://*.perfetto.dev;"> <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src-elem 'self' https://*.google.com https://*.googleusercontent.com https://www.googletagmanager.com https://www.google-analytics.com 'sha256-eYlPNiizBKy/rhHAaz06RXrXVsKmBN6tTFYwmJTvcwc='; object-src 'none'; connect-src 'self' http://127.0.0.1:9001 https://www.google-analytics.com https://*.googleapis.com blob: data:; img-src 'self' https://www.google-analytics.com; navigate-to https://*.perfetto.dev;">
+ <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
<link href="perfetto.css" rel="stylesheet">
<link rel="icon" type="image/png" href="assets/favicon.png">
<!-- Global site tag (gtag.js) - Google Analytics -->
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index c926284..f6f5fa5 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -650,3 +650,7 @@
cursor: pointer;
}
}
+
+.disallow-selection {
+ user-select: none;
+}
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 2625b95..9441418 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -17,6 +17,7 @@
import {assertExists} from '../base/logging';
import {randomColor} from '../common/colorizer';
import {ConvertTrace, ConvertTraceToPprof} from '../controller/trace_converter';
+import {DEBUG_SLICE_TRACK_KIND} from '../tracks/debug_slices/common';
import {DEFAULT_VIEWING_OPTION} from './flamegraph_util';
import {
@@ -197,6 +198,34 @@
};
},
+ addDebugTrack(state: StateDraft, args: {engineId: string, name: string}):
+ void {
+ if (state.debugTrackId !== undefined) return;
+ const trackId = `${state.nextId++}`;
+ state.debugTrackId = trackId;
+ this.addTrack(state, {
+ id: trackId,
+ engineId: args.engineId,
+ kind: DEBUG_SLICE_TRACK_KIND,
+ name: args.name,
+ trackGroup: SCROLLING_TRACK_GROUP,
+ config: {
+ maxDepth: 1,
+ }
+ });
+ this.toggleTrackPinned(state, {trackId});
+ },
+
+ removeDebugTrack(state: StateDraft, _: {}): void {
+ const {debugTrackId} = state;
+ if (debugTrackId === undefined) return;
+ delete state.tracks[debugTrackId];
+ state.scrollingTracks =
+ state.scrollingTracks.filter(id => id !== debugTrackId);
+ state.pinnedTracks = state.pinnedTracks.filter(id => id !== debugTrackId);
+ state.debugTrackId = undefined;
+ },
+
updateAggregateSorting(
state: StateDraft, args: {id: string, column: string}) {
let prefs = state.aggregatePreferences[args.id];
@@ -298,6 +327,14 @@
trackGroup.collapsed = !trackGroup.collapsed;
},
+ requestTrackReload(state: StateDraft, _: {}) {
+ if (state.lastTrackReloadRequest) {
+ state.lastTrackReloadRequest++;
+ } else {
+ state.lastTrackReloadRequest = 1;
+ }
+ },
+
setEngineReady(
state: StateDraft,
args: {engineId: string; ready: boolean, mode: EngineMode}): void {
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index b053df4..f2dc00f 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -274,6 +274,8 @@
visibleTracks: string[];
scrollingTracks: string[];
pinnedTracks: string[];
+ debugTrackId?: string;
+ lastTrackReloadRequest?: number;
queries: ObjectById<QueryConfig>;
permalink: PermalinkConfig;
notes: ObjectById<Note|AreaNote>;
diff --git a/ui/src/controller/permalink_controller.ts b/ui/src/controller/permalink_controller.ts
index 039498c..4f955af 100644
--- a/ui/src/controller/permalink_controller.ts
+++ b/ui/src/controller/permalink_controller.ts
@@ -123,6 +123,12 @@
private static async loadState(id: string): Promise<State|RecordConfig> {
const url = `https://storage.googleapis.com/${BUCKET_NAME}/${id}`;
const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(
+ `Could not fetch permalink.\n` +
+ `Are you sure the id (${id}) is correct?\n` +
+ `URL: ${url}`);
+ }
const text = await response.text();
const stateHash = await toSha256(text);
const state = JSON.parse(text);
diff --git a/ui/src/controller/track_controller.ts b/ui/src/controller/track_controller.ts
index 381ebc8..ff27d17 100644
--- a/ui/src/controller/track_controller.ts
+++ b/ui/src/controller/track_controller.ts
@@ -41,6 +41,7 @@
private requestingData = false;
private queuedRequest = false;
private isSetup = false;
+ private lastReloadHandled = 0;
// We choose 100000 as the table size to cache as this is roughly the point
// where SQLite sorts start to become expensive.
@@ -60,6 +61,10 @@
// to be performed before the first onBoundsChange invcation.
async onSetup() {}
+ // Can be overriden by the track implementation to allow some one-off work
+ // when requested reload (e.g. recalculating height).
+ async onReload() {}
+
// Must be overridden by the track implementation. Is invoked when the track
// frontend runs out of cached data. The derived track controller is expected
// to publish new track data in response to this call.
@@ -113,8 +118,19 @@
return result;
}
+ private shouldReload(): boolean {
+ const {lastTrackReloadRequest} = globals.state;
+ return !!lastTrackReloadRequest &&
+ this.lastReloadHandled < lastTrackReloadRequest;
+ }
+
+ private markReloadHandled() {
+ this.lastReloadHandled = globals.state.lastTrackReloadRequest || 0;
+ }
+
shouldRequestData(traceTime: TraceTime): boolean {
if (this.data === undefined) return true;
+ if (this.shouldReload()) return true;
// If at the limit only request more data if the view has moved.
const atLimit = this.data.length === LIMIT;
@@ -227,11 +243,13 @@
this.queuedRequest = true;
} else {
this.requestingData = true;
- let setupPromise = Promise.resolve();
+ let promise = Promise.resolve();
if (!this.isSetup) {
- setupPromise = this.onSetup();
+ promise = this.onSetup();
+ } else if (this.shouldReload()) {
+ promise = this.onReload().then(() => this.markReloadHandled());
}
- setupPromise
+ promise
.then(() => {
this.isSetup = true;
return this.onBoundsChange(
diff --git a/ui/src/frontend/android_bug_tool.ts b/ui/src/frontend/android_bug_tool.ts
new file mode 100644
index 0000000..ff63f58
--- /dev/null
+++ b/ui/src/frontend/android_bug_tool.ts
@@ -0,0 +1,67 @@
+import {defer} from '../base/deferred';
+
+enum WebContentScriptMessageType {
+ UNKNOWN,
+ CONVERT_OBJECT_URL,
+ CONVERT_OBJECT_URL_RESPONSE,
+}
+
+const ANDROID_BUG_TOOL_EXTENSION_ID = 'mbbaofdfoekifkfpgehgffcpagbbjkmj';
+
+interface Attachment {
+ name: string;
+ objectUrl: string;
+ restrictionSeverity: number;
+}
+
+interface ConvertObjectUrlResponse {
+ action: WebContentScriptMessageType.CONVERT_OBJECT_URL_RESPONSE;
+ attachments: Attachment[];
+ issueAccessLevel: string;
+ issueId: string;
+ issueTitle: string;
+}
+
+export interface TraceFromBuganizer {
+ issueId: string;
+ issueTitle: string;
+ file: File;
+}
+
+export function loadAndroidBugToolInfo(): Promise<TraceFromBuganizer> {
+ const deferred = defer<TraceFromBuganizer>();
+
+ // Request to convert the blob object url "blob:chrome-extension://xxx"
+ // the chrome extension has to a web downloadable url "blob:http://xxx".
+ chrome.runtime.sendMessage(
+ ANDROID_BUG_TOOL_EXTENSION_ID,
+ {action: WebContentScriptMessageType.CONVERT_OBJECT_URL},
+ async (response: ConvertObjectUrlResponse) => {
+ switch (response.action) {
+ case WebContentScriptMessageType.CONVERT_OBJECT_URL_RESPONSE:
+ if (response.attachments?.length > 0) {
+ const filesBlobPromises =
+ response.attachments.map(async attachment => {
+ const fileQueryResponse = await fetch(attachment.objectUrl);
+ const blob = await fileQueryResponse.blob();
+ // Note: The blob's media type is always set to "image/png".
+ // Clone blob to clear media type.
+ return new File([blob], attachment.name);
+ });
+ const files = await Promise.all(filesBlobPromises);
+ deferred.resolve({
+ issueId: response.issueId,
+ issueTitle: response.issueTitle,
+ file: files[0],
+ });
+ } else {
+ throw new Error('Got no attachements from extension');
+ }
+ break;
+ default:
+ throw new Error(`Received unhandled response code (${
+ response.action}) from extension.`);
+ }
+ });
+ return deferred;
+}
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index 1c44b98..211d6e5 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -85,25 +85,20 @@
shareTraceSection.push(
m(`input[type=checkbox]`, {
checked,
- oninput: m.withAttr(
- 'checked',
- value => {
- checked = value;
- if (value && engine.source.type === 'FILE') {
- saveTrace(engine.source.file).then((url) => {
- const errMessage = createErrorMessage(errLog, checked, url);
- renderModal(
- errTitle,
- errMessage,
- userDescription,
- shareTraceSection);
- return;
- });
- }
- const errMessage = createErrorMessage(errLog, checked);
+ oninput: (ev: InputEvent) => {
+ checked = (ev.target as HTMLInputElement).checked;
+ if (checked && engine.source.type === 'FILE') {
+ saveTrace(engine.source.file).then(url => {
+ const errMessage = createErrorMessage(errLog, checked, url);
renderModal(
errTitle, errMessage, userDescription, shareTraceSection);
- })
+ return;
+ });
+ }
+ const errMessage = createErrorMessage(errLog, checked);
+ renderModal(
+ errTitle, errMessage, userDescription, shareTraceSection);
+ },
}),
m('span', `Check this box to share the current trace for debugging
purposes.`),
@@ -134,11 +129,9 @@
m('textarea.modal-textarea', {
rows: 3,
maxlength: 1000,
- oninput: m.withAttr(
- 'value',
- v => {
- userDescription = v;
- })
+ oninput: (ev: InputEvent) => {
+ userDescription = (ev.target as HTMLTextAreaElement).value;
+ },
}),
shareTraceSection),
buttons: [
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index e3809a5..519b74b 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -170,7 +170,6 @@
// Called when beginning a canvas redraw.
clearVisibleTracks() {
- this.prevVisibleTracks = new Set(this.visibleTracks);
this.visibleTracks.clear();
}
@@ -181,6 +180,7 @@
value => this.visibleTracks.has(value))) {
globals.dispatch(
Actions.setVisibleTracks({tracks: Array.from(this.visibleTracks)}));
+ this.prevVisibleTracks = new Set(this.visibleTracks);
}
}
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 7ff1840..6d51c73 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -31,6 +31,7 @@
import {CurrentSearchResults, SearchSummary} from '../common/search_data';
import {AnalyzePage} from './analyze_page';
+import {loadAndroidBugToolInfo} from './android_bug_tool';
import {maybeShowErrorDialog} from './error_dialog';
import {
CounterDetails,
@@ -49,6 +50,7 @@
import {RecordPage, updateAvailableAdbDevices} from './record_page';
import {Router} from './router';
import {CheckHttpRpcConnection} from './rpc_http_dialog';
+import {taskTracker} from './task_tracker';
import {TraceInfoPage} from './trace_info_page';
import {ViewerPage} from './viewer_page';
@@ -318,14 +320,32 @@
// /?s=xxxx for permalinks.
const stateHash = Router.param('s');
const urlHash = Router.param('url');
- if (stateHash) {
+ const androidBugTool = Router.param('openFromAndroidBugTool');
+ if (typeof stateHash === 'string' && stateHash) {
globals.dispatch(Actions.loadPermalink({
hash: stateHash,
}));
- } else if (urlHash) {
+ } else if (typeof urlHash === 'string' && urlHash) {
globals.dispatch(Actions.openTraceFromUrl({
url: urlHash,
}));
+ } else if (androidBugTool) {
+ // TODO(hjd): Unify updateStatus and TaskTracker
+ globals.dispatch(Actions.updateStatus({
+ msg: 'Loading trace from ABT extension',
+ timestamp: Date.now() / 1000
+ }));
+ const loadInfo = loadAndroidBugToolInfo();
+ taskTracker.trackPromise(loadInfo, 'Loading trace from ABT extension');
+ loadInfo
+ .then(info => {
+ globals.dispatch(Actions.openTraceFromFile({
+ file: info.file,
+ }));
+ })
+ .catch(e => {
+ console.error(e);
+ });
}
// Prevent pinch zoom.
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 0aae5a9..4e1365d 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -273,25 +273,23 @@
e.stopImmediatePropagation();
},
value: note.text,
- onchange: m.withAttr(
- 'value',
- newText => {
- globals.dispatch(Actions.changeNoteText({
- id: attrs.id,
- newText,
- }));
- }),
+ onchange: (e: InputEvent) => {
+ const newText = (e.target as HTMLInputElement).value;
+ globals.dispatch(Actions.changeNoteText({
+ id: attrs.id,
+ newText,
+ }));
+ },
}),
m('span.color-change', `Change color: `, m('input[type=color]', {
value: note.color,
- onchange: m.withAttr(
- 'value',
- newColor => {
- globals.dispatch(Actions.changeNoteColor({
- id: attrs.id,
- newColor,
- }));
- }),
+ onchange: (e: Event) => {
+ const newColor = (e.target as HTMLInputElement).value;
+ globals.dispatch(Actions.changeNoteColor({
+ id: attrs.id,
+ newColor,
+ }));
+ },
})),
m('button',
{
diff --git a/ui/src/frontend/pages.ts b/ui/src/frontend/pages.ts
index 3091c5e..c595420 100644
--- a/ui/src/frontend/pages.ts
+++ b/ui/src/frontend/pages.ts
@@ -32,7 +32,7 @@
{
onclick: () => globals.dispatch(Actions.clearPermalink({})),
},
- m('i.material-icons', 'close')),
+ m('i.material-icons.disallow-selection', 'close')),
]);
}
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 4de44aa..23bcec7 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -218,7 +218,9 @@
view({attrs}: m.CVnode<Attrs>) {
this.attrs = attrs;
const renderPanel = (panel: m.Vnode) => perfDebug() ?
- m('.panel', panel, m('.debug-panel-border')) :
+ m('.panel',
+ {key: panel.key},
+ [panel, m('.debug-panel-border', {key: 'debug-panel-border'})]) :
m('.panel', {key: panel.key}, panel);
return [
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index 62f728b..b6683f3 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -121,15 +121,14 @@
const recButton = (mode: RecordMode, title: string, img: string) => {
const checkboxArgs = {
checked: cfg.mode === mode,
- onchange: m.withAttr(
- 'checked',
- (checked: boolean) => {
- if (!checked) return;
- const traceCfg = produce(globals.state.recordConfig, draft => {
- draft.mode = mode;
- });
- globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
- })
+ onchange: (e: InputEvent) => {
+ const checked = (e.target as HTMLInputElement).checked;
+ if (!checked) return;
+ const traceCfg = produce(globals.state.recordConfig, draft => {
+ draft.mode = mode;
+ });
+ globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
+ },
};
return m(
`label${cfg.mode === mode ? '.selected' : ''}`,
@@ -801,7 +800,9 @@
m('select',
{
selectedIndex,
- onchange: m.withAttr('value', onTargetChange),
+ onchange: (e: Event) => {
+ onTargetChange((e.target as HTMLSelectElement).value);
+ },
onupdate: (select) => {
// Work around mithril bug
// (https://github.com/MithrilJS/mithril.js/issues/2107): We may
@@ -1311,7 +1312,8 @@
};
const pages: m.Children = [];
- let routePage = Router.param('p');
+ const routePageParam = Router.param('p');
+ let routePage = typeof routePageParam === 'string' ? routePageParam : '';
if (!Object.keys(SECTIONS).includes(routePage)) {
routePage = 'buffers';
}
diff --git a/ui/src/frontend/record_widgets.ts b/ui/src/frontend/record_widgets.ts
index 4d55409..9558919 100644
--- a/ui/src/frontend/record_widgets.ts
+++ b/ui/src/frontend/record_widgets.ts
@@ -74,8 +74,12 @@
onclick: () => onToggle(!enabled),
}),
m('label',
- m(`input[type=checkbox]`,
- {checked: enabled, oninput: m.withAttr('checked', onToggle)}),
+ m(`input[type=checkbox]`, {
+ checked: enabled,
+ oninput: (e: InputEvent) => {
+ onToggle((e.target as HTMLInputElement).checked);
+ },
+ }),
m('span', attrs.title)),
m('div', m('div', attrs.descr), m('.probe-config', children)));
}
@@ -140,13 +144,17 @@
type: 'text',
pattern: '(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}', // hh:mm:ss
value: new Date(val).toISOString().substr(11, 8),
- oninput: m.withAttr('value', v => this.onTimeValueChange(attrs, v))
+ oninput: (e: InputEvent) => {
+ this.onTimeValueChange(attrs, (e.target as HTMLInputElement).value);
+ },
};
} else {
spinnerCfg = {
type: 'number',
value: val,
- oninput: m.withAttr('value', v => this.onValueChange(attrs, v))
+ oninput: (e: InputEvent) => {
+ this.onTimeValueChange(attrs, (e.target as HTMLInputElement).value);
+ },
};
}
return m(
@@ -156,7 +164,11 @@
attrs.icon !== undefined ? m('i.material-icons', attrs.icon) : [],
m(`input[id="${id}"][type=range][min=0][max=${maxIdx}][value=${idx}]
${disabled ? '[disabled]' : ''}`,
- {oninput: m.withAttr('value', v => this.onSliderChange(attrs, v))}),
+ {
+ oninput: (e: InputEvent) => {
+ this.onSliderChange(attrs, +(e.target as HTMLInputElement).value);
+ },
+ }),
m(`input.spinner[min=${min !== undefined ? min : 1}][for=${id}]`,
spinnerCfg),
m('.unit', attrs.unit));
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index a6addae..31bdbe1 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -112,6 +112,16 @@
};
}
+function showDebugTrack(): (_: Event) => void {
+ return (e: Event) => {
+ e.preventDefault();
+ globals.dispatch(Actions.addDebugTrack({
+ engineId: Object.keys(globals.state.engines)[0],
+ name: 'Debug Slices',
+ }));
+ };
+}
+
const EXAMPLE_ANDROID_TRACE_URL =
'https://storage.googleapis.com/perfetto-misc/example_android_trace_15s';
@@ -181,6 +191,11 @@
summary: 'Compute summary statistics',
items: [
{
+ t: 'Show Debug Track',
+ a: showDebugTrack(),
+ i: 'view_day',
+ },
+ {
t: 'All Processes',
a: createCannedQuery(ALL_PROCESSES_QUERY),
i: 'search',
diff --git a/ui/src/frontend/task_tracker.ts b/ui/src/frontend/task_tracker.ts
new file mode 100644
index 0000000..ea795f7
--- /dev/null
+++ b/ui/src/frontend/task_tracker.ts
@@ -0,0 +1,50 @@
+interface PromiseInfo {
+ startTimeMs: number;
+ message: string;
+}
+
+export class TaskTracker {
+ private promisesSeen: number;
+ private promisesRejected: number;
+ private promisesFulfilled: number;
+ private promiseInfo: Map<Promise<unknown>, PromiseInfo>;
+
+ constructor() {
+ this.promisesSeen = 0;
+ this.promisesRejected = 0;
+ this.promisesFulfilled = 0;
+ this.promiseInfo = new Map();
+ }
+
+ trackPromise(promise: Promise<unknown>, message: string): void {
+ this.promiseInfo.set(promise, {
+ startTimeMs: (new Date()).getMilliseconds(),
+ message,
+ });
+ this.promisesSeen += 1;
+ promise.then(() => {
+ this.promisesFulfilled += 1;
+ }).catch(() => {
+ this.promisesRejected += 1;
+ }).finally(() => {
+ this.promiseInfo.delete(promise);
+ });
+ }
+
+ hasPendingTasks(): boolean {
+ return this.promisesSeen > (this.promisesFulfilled + this.promisesRejected);
+ }
+
+ progressMessage(): string|undefined {
+ const {value} = this.promiseInfo.values().next();
+ if (value === undefined) {
+ return value;
+ } else {
+ const nowMs = (new Date()).getMilliseconds();
+ const runtimeSeconds = Math.round((nowMs - value.startTimeMs) / 1000);
+ return `${value.message} (${runtimeSeconds}s)`;
+ }
+ }
+}
+
+export const taskTracker = new TaskTracker();
diff --git a/ui/src/frontend/task_tracker_unittest.ts b/ui/src/frontend/task_tracker_unittest.ts
new file mode 100644
index 0000000..f869843
--- /dev/null
+++ b/ui/src/frontend/task_tracker_unittest.ts
@@ -0,0 +1,34 @@
+// Copyright (C) 2020 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 {defer} from '../base/deferred';
+import {TaskTracker} from './task_tracker';
+
+test('it starts with no pending tasks', () => {
+ const tracker = new TaskTracker();
+ expect(tracker.hasPendingTasks()).toEqual(false);
+ expect(tracker.progressMessage()).toEqual(undefined);
+});
+
+test('it knows if a task is pending', () => {
+ const tracker = new TaskTracker();
+ const deferred = defer();
+
+ tracker.trackPromise(deferred, "Some task");
+
+ expect(tracker.hasPendingTasks()).toEqual(true);
+ expect(tracker.progressMessage()).toEqual("Some task (0s)");
+ deferred.resolve();
+});
+
diff --git a/ui/src/frontend/topbar.ts b/ui/src/frontend/topbar.ts
index 02d29c9..7bac826 100644
--- a/ui/src/frontend/topbar.ts
+++ b/ui/src/frontend/topbar.ts
@@ -19,6 +19,7 @@
import {globals} from './globals';
import {executeSearch} from './search_handler';
+import {taskTracker} from './task_tracker';
const SEARCH = Symbol('search');
const COMMAND = Symbol('command');
@@ -110,17 +111,16 @@
`.omnibox${commandMode ? '.command-mode' : ''}`,
m('input', {
placeholder: PLACEHOLDER[mode],
- oninput: m.withAttr(
- 'value',
- v => {
- globals.frontendLocalState.setOmnibox(
- v, commandMode ? 'COMMAND' : 'SEARCH');
- if (mode === SEARCH) {
- globals.frontendLocalState.setSearchIndex(-1);
- displayStepThrough = v.length >= 4;
- globals.rafScheduler.scheduleFullRedraw();
- }
- }),
+ oninput: (e: InputEvent) => {
+ const value = (e.target as HTMLInputElement).value;
+ globals.frontendLocalState.setOmnibox(
+ value, commandMode ? 'COMMAND' : 'SEARCH');
+ if (mode === SEARCH) {
+ globals.frontendLocalState.setSearchIndex(-1);
+ displayStepThrough = value.length >= 4;
+ globals.rafScheduler.scheduleFullRedraw();
+ }
+ },
value: globals.frontendLocalState.omnibox,
}),
displayStepThrough ?
@@ -179,7 +179,7 @@
if (this.progressBar === undefined) return;
const engine: EngineConfig = globals.state.engines['0'];
if ((engine !== undefined && !engine.ready) ||
- globals.numQueuedQueries > 0) {
+ globals.numQueuedQueries > 0 || taskTracker.hasPendingTasks()) {
this.progressBar.classList.add('progress-anim');
} else {
this.progressBar.classList.remove('progress-anim');
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index c5af2f2..4b53508 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -47,11 +47,18 @@
* The abstract class that needs to be implemented by all tracks.
*/
export abstract class Track<Config = {}, Data extends TrackData = TrackData> {
- constructor(protected trackState: TrackState) {}
+ private trackId: string;
+ constructor(trackState: TrackState) {
+ this.trackId = trackState.id;
+ }
protected abstract renderCanvas(ctx: CanvasRenderingContext2D): void;
+ protected get trackState(): TrackState {
+ return globals.state.tracks[this.trackId];
+ }
+
get config(): Config {
- return this.trackState.config as Config;
+ return globals.state.tracks[this.trackId].config as Config;
}
data(): Data|undefined {
diff --git a/ui/src/tracks/all_controller.ts b/ui/src/tracks/all_controller.ts
index 3d86dc5..116c1b9 100644
--- a/ui/src/tracks/all_controller.ts
+++ b/ui/src/tracks/all_controller.ts
@@ -25,3 +25,4 @@
import './process_summary/controller';
import './thread_state/controller';
import './async_slices/controller';
+import './debug_slices/controller';
diff --git a/ui/src/tracks/all_frontend.ts b/ui/src/tracks/all_frontend.ts
index bfe9f26..6311137 100644
--- a/ui/src/tracks/all_frontend.ts
+++ b/ui/src/tracks/all_frontend.ts
@@ -25,3 +25,4 @@
import './process_summary/frontend';
import './thread_state/frontend';
import './async_slices/frontend';
+import './debug_slices/frontend';
diff --git a/ui/src/tracks/debug_slices/common.ts b/ui/src/tracks/debug_slices/common.ts
new file mode 100644
index 0000000..9625d54
--- /dev/null
+++ b/ui/src/tracks/debug_slices/common.ts
@@ -0,0 +1,22 @@
+// Copyright (C) 2020 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 {Data as ChromeSlicesData} from '../chrome_slices/common';
+
+export const DEBUG_SLICE_TRACK_KIND = 'DebugSliceTrack';
+
+export interface Config {
+ maxDepth: number;
+}
+
+export {Data} from '../chrome_slices/common';
diff --git a/ui/src/tracks/debug_slices/controller.ts b/ui/src/tracks/debug_slices/controller.ts
new file mode 100644
index 0000000..7fab628
--- /dev/null
+++ b/ui/src/tracks/debug_slices/controller.ts
@@ -0,0 +1,97 @@
+// Copyright (C) 2020 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 {assertTrue} from '../../base/logging';
+import {Actions} from '../../common/actions';
+import {fromNs, toNs} from '../../common/time';
+import {globals} from '../../controller/globals';
+import {
+ TrackController,
+ trackControllerRegistry,
+} from '../../controller/track_controller';
+
+import {Config, Data, DEBUG_SLICE_TRACK_KIND} from './common';
+
+class DebugSliceTrackController extends TrackController<Config, Data> {
+ static readonly kind = DEBUG_SLICE_TRACK_KIND;
+
+ async onReload() {
+ const rawResult = await this.query(`select max(depth) from debug_slices`);
+ const maxDepth =
+ (rawResult.numRecords === 0) ? 1 : rawResult.columns[0].longValues![0];
+ globals.dispatch(
+ Actions.updateTrackConfig({id: this.trackId, config: {maxDepth}}));
+ }
+
+ async onBoundsChange(start: number, end: number, resolution: number):
+ Promise<Data> {
+ const rawResult = await this.query(
+ `select id, name, ts, dur, depth from debug_slices where
+ (ts + dur) >= ${toNs(start)} and ts <= ${toNs(end)}`);
+
+ assertTrue(rawResult.columns.length === 5);
+ const [idCol, nameCol, tsCol, durCol, depthCol] = rawResult.columns;
+ const idValues = idCol.longValues! || idCol.doubleValues!;
+ const tsValues = tsCol.longValues! || tsCol.doubleValues!;
+ const durValues = durCol.longValues! || durCol.doubleValues!;
+
+ const numRows = rawResult.numRecords;
+ const slices: Data = {
+ start,
+ end,
+ resolution,
+ length: numRows,
+ strings: [],
+ sliceIds: new Float64Array(numRows),
+ starts: new Float64Array(numRows),
+ ends: new Float64Array(numRows),
+ depths: new Uint16Array(numRows),
+ titles: new Uint16Array(numRows),
+ isInstant: new Uint16Array(numRows),
+ };
+
+ const stringIndexes = new Map<string, number>();
+ function internString(str: string) {
+ let idx = stringIndexes.get(str);
+ if (idx !== undefined) return idx;
+ idx = slices.strings.length;
+ slices.strings.push(str);
+ stringIndexes.set(str, idx);
+ return idx;
+ }
+
+ for (let i = 0; i < rawResult.numRecords; i++) {
+ let sliceStart: number, sliceEnd: number;
+ if (tsCol.isNulls![i] || durCol.isNulls![i]) {
+ sliceStart = sliceEnd = -1;
+ } else {
+ sliceStart = tsValues[i];
+ const sliceDur = durValues[i];
+ sliceEnd = sliceStart + sliceDur;
+ }
+ slices.sliceIds[i] = idCol.isNulls![i] ? -1 : idValues[i];
+ slices.starts[i] = fromNs(sliceStart);
+ slices.ends[i] = fromNs(sliceEnd);
+ slices.depths[i] = depthCol.isNulls![i] ? 0 : depthCol.longValues![i];
+ const sliceName =
+ nameCol.isNulls![i] ? '[null]' : nameCol.stringValues![i];
+ slices.titles[i] = internString(sliceName);
+ slices.isInstant[i] = 0;
+ }
+
+ return slices;
+ }
+}
+
+trackControllerRegistry.register(DebugSliceTrackController);
diff --git a/ui/src/tracks/debug_slices/frontend.ts b/ui/src/tracks/debug_slices/frontend.ts
new file mode 100644
index 0000000..bdda433
--- /dev/null
+++ b/ui/src/tracks/debug_slices/frontend.ts
@@ -0,0 +1,55 @@
+// Copyright (C) 2020 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 * as m from 'mithril';
+
+import {Actions} from '../../common/actions';
+import {TrackState} from '../../common/state';
+import {globals} from '../../frontend/globals';
+import {Track} from '../../frontend/track';
+import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
+import {trackRegistry} from '../../frontend/track_registry';
+import {ChromeSliceTrack} from '../chrome_slices/frontend';
+
+import {DEBUG_SLICE_TRACK_KIND} from './common';
+
+export class DebugSliceTrack extends ChromeSliceTrack {
+ static readonly kind = DEBUG_SLICE_TRACK_KIND;
+ static create(trackState: TrackState): Track {
+ return new DebugSliceTrack(trackState);
+ }
+
+ getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
+ const buttons: Array<m.Vnode<TrackButtonAttrs>> = [];
+ buttons.push(m(TrackButton, {
+ action: () => {
+ globals.dispatch(Actions.requestTrackReload({}));
+ },
+ i: 'refresh',
+ tooltip: 'Refresh tracks',
+ showButton: true,
+ }));
+ buttons.push(m(TrackButton, {
+ action: () => {
+ globals.dispatch(Actions.removeDebugTrack({}));
+ },
+ i: 'close',
+ tooltip: 'Close',
+ showButton: true,
+ }));
+ return buttons;
+ }
+}
+
+trackRegistry.register(DebugSliceTrack);