Skip to content

Commit 36e2b7c

Browse files
authored
ui: fix race condition on extra macro registration (#3014)
There was a subtle race between us figuring out whether a user is an internal user (and fetching associated data) and registering the macros for that user. Break the race by waiting to register extra macros until after the internal user status is known. Also defer registration of the settings macros until trace load time: there's no reason for this to happen at activate time: most of these macros will be completely unrunnable at that time anyway.
1 parent 3ede3bf commit 36e2b7c

File tree

4 files changed

+57
-26
lines changed

4 files changed

+57
-26
lines changed

ui/src/core/app_impl.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
import {AsyncLimiter} from '../base/async_limiter';
16+
import {defer} from '../base/deferred';
1617
import {assertExists, assertTrue} from '../base/logging';
1718
import {createProxy, getOrCreate} from '../base/utils';
1819
import {ServiceWorkerController} from '../frontend/service_worker_controller';
@@ -98,6 +99,9 @@ export class AppContext {
9899
// via is_internal_user.js
99100
extraMacros: Record<string, CommandInvocation[]>[] = [];
100101

102+
// Promise which is resolved when extra loading is completed.
103+
extrasLoadingDeferred = defer<undefined>();
104+
101105
// The currently open trace.
102106
currentTrace?: TraceContext;
103107

@@ -435,4 +439,12 @@ export class AppImpl implements App {
435439
set isInternalUser(value: boolean) {
436440
this.appCtx.isInternalUser = value;
437441
}
442+
443+
notifyOnExtrasLoadingCompleted() {
444+
this.appCtx.extrasLoadingDeferred.resolve();
445+
}
446+
447+
get extraLoadingPromise(): Promise<undefined> {
448+
return this.appCtx.extrasLoadingDeferred;
449+
}
438450
}

ui/src/core_plugins/dev.perfetto.CoreCommands/index.ts

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import {DurationPrecision, TimestampFormat} from '../../public/timeline';
4242
import {getTimeSpanOfSelectionOrVisibleWindow} from '../../public/utils';
4343
import {Workspace} from '../../public/workspace';
4444
import {showModal} from '../../widgets/modal';
45+
import {assertExists} from '../../base/logging';
46+
import {Setting} from '../../public/settings';
4547

4648
const QUICKSAVE_LOCALSTORAGE_KEY = 'quicksave';
4749

@@ -129,9 +131,14 @@ function getOrPromptForTimestamp(tsRaw: unknown): time | undefined {
129131
return promptForTimestamp('Enter a timestamp');
130132
}
131133

134+
const macroSchema = z.record(z.array(commandInvocationSchema));
135+
type MacroConfig = z.infer<typeof macroSchema>;
136+
132137
export default class CoreCommands implements PerfettoPlugin {
133138
static readonly id = 'dev.perfetto.CoreCommands';
134139

140+
static macrosSetting: Setting<MacroConfig> | undefined = undefined;
141+
135142
static onActivate(ctx: AppImpl) {
136143
if (ctx.sidebar.enabled) {
137144
ctx.commands.registerCommand({
@@ -144,13 +151,10 @@ export default class CoreCommands implements PerfettoPlugin {
144151
});
145152
}
146153

147-
// Use shared commandInvocationSchema where 'id' is the command's unique identifier
148-
const macroSchema = z.record(z.array(commandInvocationSchema));
149-
type MacroConfig = z.infer<typeof macroSchema>;
150154
const macroSettingsEditor = new JsonSettingsEditor<MacroConfig>({
151155
schema: macroSchema,
152156
});
153-
const setting = ctx.settings.register({
157+
CoreCommands.macrosSetting = ctx.settings.register({
154158
id: 'perfetto.CoreCommands#UserDefinedMacros',
155159
name: 'Macros',
156160
description:
@@ -161,27 +165,6 @@ export default class CoreCommands implements PerfettoPlugin {
161165
render: (setting) => macroSettingsEditor.render(setting),
162166
});
163167

164-
// Register both user-defined macros from settings and extra macros from google3
165-
const allMacros = {
166-
...setting.get(),
167-
...ctx.extraMacros.reduce((acc, macro) => ({...acc, ...macro}), {}),
168-
} as MacroConfig;
169-
for (const [macroName, commands] of Object.entries(allMacros)) {
170-
ctx.commands.registerCommand({
171-
id: `dev.perfetto.UserMacro.${macroName}`,
172-
name: macroName,
173-
callback: async () => {
174-
// Macros could run multiple commands, some of which might prompt the
175-
// user in an optional way. But macros should be self-contained
176-
// so we disable prompts during their execution.
177-
using _ = ctx.omnibox.disablePrompts();
178-
for (const command of commands) {
179-
await ctx.commands.runCommand(command.id, ...command.args);
180-
}
181-
},
182-
});
183-
}
184-
185168
const input = document.createElement('input');
186169
input.classList.add('trace_file');
187170
input.setAttribute('type', 'file');
@@ -232,6 +215,23 @@ export default class CoreCommands implements PerfettoPlugin {
232215
}
233216

234217
async onTraceLoad(ctx: TraceImpl): Promise<void> {
218+
const app = AppImpl.instance;
219+
220+
// Rgister macros from settings first.
221+
registerMacros(ctx, assertExists(CoreCommands.macrosSetting).get());
222+
223+
// Register the macros from extras at onTraceReady (the latest time
224+
// possible).
225+
ctx.onTraceReady.addListener(async (_) => {
226+
// Await the promise: we've tried to be async as long as possible but
227+
// now we need the extras to be loaded.
228+
await app.extraLoadingPromise;
229+
registerMacros(
230+
ctx,
231+
app.extraMacros.reduce((acc, macro) => ({...acc, ...macro}), {}),
232+
);
233+
});
234+
235235
ctx.commands.registerCommand({
236236
id: 'dev.perfetto.RunQueryAllProcesses',
237237
name: 'Run query: All processes',
@@ -866,3 +866,21 @@ async function openWithLegacyUi(file: File) {
866866
}
867867
return await openInOldUIWithSizeCheck(file);
868868
}
869+
870+
function registerMacros(trace: TraceImpl, config: MacroConfig) {
871+
for (const [macroName, commands] of Object.entries(config)) {
872+
trace.commands.registerCommand({
873+
id: `dev.perfetto.UserMacro.${macroName}`,
874+
name: macroName,
875+
callback: async () => {
876+
// Macros could run multiple commands, some of which might prompt the
877+
// user in an optional way. But macros should be self-contained
878+
// so we disable prompts during their execution.
879+
using _ = trace.omnibox.disablePrompts();
880+
for (const command of commands) {
881+
await trace.commands.runCommand(command.id, ...command.args);
882+
}
883+
},
884+
});
885+
}
886+
}

ui/src/frontend/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ function main() {
280280
const app = AppImpl.instance;
281281
tryLoadIsInternalUserScript(app).then(() => {
282282
app.analytics.initialize(app.isInternalUser);
283+
app.notifyOnExtrasLoadingCompleted();
283284
});
284285

285286
// Route errors to both the UI bugreport dialog and Analytics (if enabled).

ui/src/frontend/is_internal_user_script_loader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {CommandInvocation} from '../core/command_manager';
2121
// proceeding as if the user is not internal.
2222
const SCRIPT_LOAD_TIMEOUT_MS = 5000;
2323
const SCRIPT_URL =
24-
'https://storage.cloud.google.com/perfetto-ui-internal/is-internal-user/is_internal_user.js';
24+
'https://storage.cloud.google.com/perfetto-ui-internal/internal-data-v1/amalgamated.js';
2525

2626
// This interface describes the required interface that the script expect to
2727
// find on window.globals.

0 commit comments

Comments
 (0)