Skip to content

Commit 147f162

Browse files
committed
use MicrotaskEmitter to (drastically) reduce the number of MenuRegistry-change events
1 parent 0ddb3ae commit 147f162

File tree

3 files changed

+70
-60
lines changed

3 files changed

+70
-60
lines changed

src/vs/base/common/event.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,7 @@ export class Emitter<T> {
792792
if (!this._listeners) {
793793
return false;
794794
}
795-
return (!this._listeners.isEmpty());
795+
return !this._listeners.isEmpty();
796796
}
797797
}
798798

@@ -994,6 +994,11 @@ export class MicrotaskEmitter<T> extends Emitter<T> {
994994
this._mergeFn = options?.merge;
995995
}
996996
override fire(event: T): void {
997+
998+
if (!this.hasListeners()) {
999+
return;
1000+
}
1001+
9971002
this._queuedEvents.push(event);
9981003
if (this._queuedEvents.length === 1) {
9991004
queueMicrotask(() => {

src/vs/platform/actions/common/actions.ts

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55

66
import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions';
77
import { CSSIcon } from 'vs/base/common/codicons';
8-
import { Emitter, Event } from 'vs/base/common/event';
9-
import { Iterable } from 'vs/base/common/iterator';
8+
import { Event, MicrotaskEmitter } from 'vs/base/common/event';
109
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
1110
import { LinkedList } from 'vs/base/common/linkedList';
1211
import { ICommandAction, ICommandActionTitle, Icon, ILocalizedString } from 'vs/platform/action/common/action';
@@ -234,12 +233,46 @@ export interface IMenuRegistryChangeEvent {
234233
has(id: MenuId): boolean;
235234
}
236235

236+
class MenuRegistryChangeEvent {
237+
238+
private static _all = new Map<MenuId, MenuRegistryChangeEvent>();
239+
240+
static for(id: MenuId): MenuRegistryChangeEvent {
241+
let value = this._all.get(id);
242+
if (!value) {
243+
value = new MenuRegistryChangeEvent(id);
244+
this._all.set(id, value);
245+
}
246+
return value;
247+
}
248+
249+
static merge(events: IMenuRegistryChangeEvent[]): IMenuRegistryChangeEvent {
250+
const ids = new Set<MenuId>();
251+
for (const item of events) {
252+
if (item instanceof MenuRegistryChangeEvent) {
253+
ids.add(item.id);
254+
}
255+
}
256+
return ids;
257+
}
258+
259+
readonly has: (id: MenuId) => boolean;
260+
261+
private constructor(private readonly id: MenuId) {
262+
this.has = candidate => candidate === id;
263+
}
264+
}
265+
237266
export interface IMenuRegistry {
238267
readonly onDidChangeMenu: Event<IMenuRegistryChangeEvent>;
239-
addCommands(newCommands: Iterable<ICommandAction>): IDisposable;
240268
addCommand(userCommand: ICommandAction): IDisposable;
241269
getCommand(id: string): ICommandAction | undefined;
242270
getCommands(): ICommandsMap;
271+
272+
/**
273+
* @deprecated Use `appendMenuItem` or most likely use `registerAction2` instead. There should be no strong
274+
* reason to use this directly.
275+
*/
243276
appendMenuItems(items: Iterable<{ id: MenuId; item: IMenuItem | ISubmenuItem }>): IDisposable;
244277
appendMenuItem(menu: MenuId, item: IMenuItem | ISubmenuItem): IDisposable;
245278
getMenuItems(loc: MenuId): Array<IMenuItem | ISubmenuItem>;
@@ -249,30 +282,19 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry {
249282

250283
private readonly _commands = new Map<string, ICommandAction>();
251284
private readonly _menuItems = new Map<MenuId, LinkedList<IMenuItem | ISubmenuItem>>();
252-
private readonly _onDidChangeMenu = new Emitter<IMenuRegistryChangeEvent>();
285+
private readonly _onDidChangeMenu = new MicrotaskEmitter<IMenuRegistryChangeEvent>({
286+
merge: MenuRegistryChangeEvent.merge
287+
});
253288

254289
readonly onDidChangeMenu: Event<IMenuRegistryChangeEvent> = this._onDidChangeMenu.event;
255290

256291
addCommand(command: ICommandAction): IDisposable {
257-
return this.addCommands(Iterable.single(command));
258-
}
259-
260-
private readonly _commandPaletteChangeEvent: IMenuRegistryChangeEvent = {
261-
has: id => id === MenuId.CommandPalette
262-
};
292+
this._commands.set(command.id, command);
293+
this._onDidChangeMenu.fire(MenuRegistryChangeEvent.for(MenuId.CommandPalette));
263294

264-
addCommands(commands: Iterable<ICommandAction>): IDisposable {
265-
for (const command of commands) {
266-
this._commands.set(command.id, command);
267-
}
268-
this._onDidChangeMenu.fire(this._commandPaletteChangeEvent);
269295
return toDisposable(() => {
270-
let didChange = false;
271-
for (const command of commands) {
272-
didChange = this._commands.delete(command.id) || didChange;
273-
}
274-
if (didChange) {
275-
this._onDidChangeMenu.fire(this._commandPaletteChangeEvent);
296+
if (this._commands.delete(command.id)) {
297+
this._onDidChangeMenu.fire(MenuRegistryChangeEvent.for(MenuId.CommandPalette));
276298
}
277299
});
278300
}
@@ -288,35 +310,22 @@ export const MenuRegistry: IMenuRegistry = new class implements IMenuRegistry {
288310
}
289311

290312
appendMenuItem(id: MenuId, item: IMenuItem | ISubmenuItem): IDisposable {
291-
return this.appendMenuItems(Iterable.single({ id, item }));
313+
let list = this._menuItems.get(id);
314+
if (!list) {
315+
list = new LinkedList();
316+
this._menuItems.set(id, list);
317+
}
318+
const rm = list.push(item);
319+
this._onDidChangeMenu.fire(MenuRegistryChangeEvent.for(id));
320+
return toDisposable(rm);
292321
}
293322

294323
appendMenuItems(items: Iterable<{ id: MenuId; item: IMenuItem | ISubmenuItem }>): IDisposable {
295-
296-
const changedIds = new Set<MenuId>();
297-
const toRemove = new LinkedList<Function>();
298-
324+
const result = new DisposableStore();
299325
for (const { id, item } of items) {
300-
let list = this._menuItems.get(id);
301-
if (!list) {
302-
list = new LinkedList();
303-
this._menuItems.set(id, list);
304-
}
305-
toRemove.push(list.push(item));
306-
changedIds.add(id);
326+
result.add(this.appendMenuItem(id, item));
307327
}
308-
309-
this._onDidChangeMenu.fire(changedIds);
310-
311-
return toDisposable(() => {
312-
if (toRemove.size > 0) {
313-
for (const fn of toRemove) {
314-
fn();
315-
}
316-
this._onDidChangeMenu.fire(changedIds);
317-
toRemove.clear();
318-
}
319-
});
328+
return result;
320329
}
321330

322331
getMenuItems(id: MenuId): Array<IMenuItem | ISubmenuItem> {
@@ -568,7 +577,9 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable {
568577

569578
// menu
570579
if (Array.isArray(menu)) {
571-
disposables.add(MenuRegistry.appendMenuItems(menu.map(item => ({ id: item.id, item: { command, ...item } }))));
580+
for (const item of menu) {
581+
disposables.add(MenuRegistry.appendMenuItem(item.id, { command, ...item }));
582+
}
572583

573584
} else if (menu) {
574585
disposables.add(MenuRegistry.appendMenuItem(menu.id, { command, ...menu }));

src/vs/workbench/services/actions/common/menusExtensionPoint.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { Iterable } from 'vs/base/common/iterator';
1717
import { index } from 'vs/base/common/arrays';
1818
import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
1919
import { ApiProposalName } from 'vs/workbench/services/extensions/common/extensionsApiProposals';
20-
import { ILocalizedString, ICommandAction } from 'vs/platform/action/common/action';
20+
import { ILocalizedString } from 'vs/platform/action/common/action';
2121

2222
interface IAPIMenu {
2323
readonly key: string;
@@ -631,7 +631,7 @@ export const commandsExtensionPoint = ExtensionsRegistry.registerExtensionPoint<
631631

632632
commandsExtensionPoint.setHandler(extensions => {
633633

634-
function handleCommand(userFriendlyCommand: schema.IUserFriendlyCommand, extension: IExtensionPointUser<any>, bucket: ICommandAction[]) {
634+
function handleCommand(userFriendlyCommand: schema.IUserFriendlyCommand, extension: IExtensionPointUser<any>) {
635635

636636
if (!schema.isValidCommand(userFriendlyCommand, extension.collector)) {
637637
return;
@@ -655,7 +655,7 @@ commandsExtensionPoint.setHandler(extensions => {
655655
if (MenuRegistry.getCommand(command)) {
656656
extension.collector.info(localize('dup', "Command `{0}` appears multiple times in the `commands` section.", userFriendlyCommand.command));
657657
}
658-
bucket.push({
658+
_commandRegistrations.add(MenuRegistry.addCommand({
659659
id: command,
660660
title,
661661
source: extension.description.displayName ?? extension.description.name,
@@ -664,24 +664,22 @@ commandsExtensionPoint.setHandler(extensions => {
664664
category,
665665
precondition: ContextKeyExpr.deserialize(enablement),
666666
icon: absoluteIcon
667-
});
667+
}));
668668
}
669669

670670
// remove all previous command registrations
671671
_commandRegistrations.clear();
672672

673-
const newCommands: ICommandAction[] = [];
674673
for (const extension of extensions) {
675674
const { value } = extension;
676675
if (Array.isArray(value)) {
677676
for (const command of value) {
678-
handleCommand(command, extension, newCommands);
677+
handleCommand(command, extension);
679678
}
680679
} else {
681-
handleCommand(value, extension, newCommands);
680+
handleCommand(value, extension);
682681
}
683682
}
684-
_commandRegistrations.add(MenuRegistry.addCommands(newCommands));
685683
});
686684

687685
interface IRegisteredSubmenu {
@@ -762,8 +760,6 @@ menusExtensionPoint.setHandler(extensions => {
762760
_menuRegistrations.clear();
763761
_submenuMenuItems.clear();
764762

765-
const items: { id: MenuId; item: IMenuItem | ISubmenuItem }[] = [];
766-
767763
for (const extension of extensions) {
768764
const { value, collector } = extension;
769765

@@ -855,10 +851,8 @@ menusExtensionPoint.setHandler(extensions => {
855851
}
856852

857853
item.when = ContextKeyExpr.deserialize(menuItem.when);
858-
items.push({ id: menu.id, item });
854+
_menuRegistrations.add(MenuRegistry.appendMenuItem(menu.id, item));
859855
}
860856
}
861857
}
862-
863-
_menuRegistrations.add(MenuRegistry.appendMenuItems(items));
864858
});

0 commit comments

Comments
 (0)